使用 Danfo.js 构建数据驱动应用(全)
原文:
zh.annas-archive.org/md5/074CFA285BE35C0386726A8DBACE1A4F
译者:飞龙
前言
大多数数据分析师使用 Python 和 pandas 进行数据处理和操作,这得益于这些库提供的便利性和性能。然而,JavaScript 开发人员一直希望浏览器中也能实现机器学习(ML)。本书重点介绍了 Danfo.js 如何将数据处理、分析和 ML 工具带给 JavaScript 开发人员,以及如何充分利用这个库来开发数据驱动的应用程序。
本书从 JavaScript 概念和现代 JavaScript 的介绍开始。然后,您将使用 Danfo.js 和 Dnotebook 进行数据分析和转换,这是 JavaScript 的交互式计算环境。之后,本书涵盖了如何加载不同类型的数据集,并通过执行操作(如处理缺失值、合并数据集和字符串操作)来分析它们。您还将专注于数据绘图、可视化、数据聚合和组合操作,通过将 Danfo.js 与 Plotly 结合使用。随后,您将使用 Danfo.js 创建一个无代码数据分析和处理系统。然后,您将介绍基本的 ML 概念,以及如何使用 Tensorflow.js 和 Danfo.js 构建推荐系统。最后,您将使用 Danfo.js 构建由 Twitter 驱动的分析仪表板。
通过本书,您将能够在服务器端 Node.js 或浏览器中构建和嵌入数据分析、可视化和 ML 功能的 JavaScript 应用程序。
这本书是为谁准备的
本书适用于数据科学初学者、数据分析师和希望使用各种数据集探索数据分析和科学计算的 JavaScript 开发人员。如果您是数据分析师、数据科学家或 JavaScript 开发人员,并希望在 ML 工作流程中实现 Danfo.js,您也会发现本书很有用。对 JavaScript 编程语言、数据科学和 ML 的基本理解将有助于您理解本书涵盖的关键概念;然而,本书的第一章和附录中提供了 JavaScript 的入门部分。
本书涵盖的内容
第一章,现代 JavaScript 概述,讨论了 ECMA 6 语法和import
语句、类方法、extend
方法和构造函数的使用。它还深入解释了Promise
方法的使用,async
和await
函数的使用,以及fetch
方法。它还介绍了如何建立支持现代 JavaScript 语法的环境,以及适当的版本控制,以及如何编写单元测试。
第二章,Dnotebook-用于 JavaScript 的交互式计算环境,深入探讨了 Dnotebook。对于来自 Python 生态系统的读者来说,这类似于 Jupyter Notebook。我们讨论了如何使用 Dnotebook,如何创建和删除单元格,如何在其中编写 Markdown,以及如何保存和共享您的笔记本。
第三章,使用 Danfo.js 入门,介绍了 Danfo.js 以及如何创建数据框架和系列。它还介绍了一些数据分析和处理的基本方法。
第四章,数据分析、整理和转换,探讨了 Danfo.js 在实际数据集中的实际应用。在这里,您将学习如何加载不同类型的数据集,并通过执行操作(如处理缺失值、计算描述性统计、执行数学运算、合并数据集和字符串操作)来分析它们。
第五章,使用 Plotly.js 进行数据可视化,介绍了数据绘图和可视化。在这里,您将学习数据可视化和绘图的基础知识,以及如何使用 Plotly.js 进行基本绘图。
第六章**,使用 Danfo.js 进行数据可视化,介绍了使用 Danfo.js 进行数据绘图和可视化。在这里,您将学习如何在 DataFrame 或 series 上直接使用 Danfo.js 创建图表。您还将学习如何自定义 Danfo.js 图表。
第七章**,数据聚合和分组操作,介绍了分组操作以及如何使用 Danfo.js 执行这些操作,包括如何按一个或多个列进行分组,如何使用提供的分组-聚合函数,以及如何使用.apply
创建自定义聚合函数。我们还展示了分组操作的内部工作原理。
第八章**,创建无代码数据分析/处理系统,展示了 Danfo.js 可以让我们做什么。在本章中,我们将创建一个无代码数据处理和分析环境,用户可以在其中上传他们的数据,然后进行艺术化的分析和处理。
第九章**,机器学习基础,以简单的术语介绍了机器学习。它还向您展示了如何在浏览器中借助一些 ML JavaScript 工具进行机器学习。
第十章**,TensorFlow.js 简介,介绍了 TensorFlow.js。它还展示了如何执行基本的数学运算以及如何创建、训练、保存和重新加载 ML 模型。本章还展示了如何有效地集成 Danfo.js 和 Tensorflow.js 来训练模型。
第十一章**,使用 Danfo.js 和 TensorFlow.js 构建推荐系统,向您展示了如何使用 TensorFlow.js 和 Danfo.js 构建电影推荐系统。它向您展示了如何在 Node.js 中训练模型以及如何将其与客户端集成。它还展示了 Danfo.js 如何使数据预处理变得简单。
第十二章**,构建 Twitter 分析仪表盘,您将在此构建一个使用 Danfo.js 作为前端和后端的 Twitter 分析仪表盘;目标是展示在数据分析应用中使用同一库的简易性,相比于例如在后端使用 Python 和在前端使用 JavaScript。
第十三章**,附录:JavaScript 基本概念,介绍了 JavaScript 编程语言。在这里,我们向初学者介绍了变量定义、函数创建以及在 JavaScript 中执行计算的不同方式。
充分利用本书
在本书中,您需要对 JavaScript 有基本的了解,并且了解 Next.js、React.js、TensorFlow.js 和 tailwindcss 等框架将是一个优势。
如果您使用的是本书的数字版本,我们建议您自己输入代码或从书的 GitHub 存储库中访问代码(链接在下一节中提供)。这样做将帮助您避免与复制和粘贴代码相关的任何潜在错误。
下载示例代码文件
您可以从 GitHub 上下载本书的示例代码文件,链接为github.com/PacktPublishing/Building-Data-Driven-Applications-with-Danfo.js
。如果代码有更新,将在 GitHub 存储库中进行更新。
我们还提供了来自我们丰富书籍和视频目录的其他代码包,可在github.com/PacktPublishing/
上找到。快去看看吧!
下载彩色图片
我们还提供了一个 PDF 文件,其中包含本书中使用的屏幕截图和图表的彩色图片。您可以在这里下载:static.packt-cdn.com/downloads/9781801070850_ColorImages.pdf
。
使用的约定
本书中使用了许多文本约定。
文本中的代码
:表示文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄。例如:"在financial_df
DataFrame 的情况下,当我们使用read_csv
函数下载数据集时,索引是自动生成的。"
代码块设置如下:
const df = new DataFrame({...})
df.plot("my_div_id").<chart type>
当我们希望引起您对代码块的特定部分的注意时,相关行或项目以粗体显示:
…
var config = {
displayModeBar: true,
modeBarButtonsToAdd: [
…
任何命令行输入或输出都以以下方式编写:
npm install @tensorflow/tfjs
粗体:表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词以粗体显示。例如:"在 Microsoft Edge 中,打开浏览器窗口右上角的 Edge 菜单,然后选择F12 开发人员工具。"
提示或重要说明
像这样。
第一部分:基础知识
本节介绍了 JavaScript 和 Node.js 框架。这些概念是为了充分理解和使用 Danfo.js 而需要的。它还介绍了如何使用 Babel 和 Node.js 设置现代 JavaScript 环境,还教读者一些代码测试的基础知识。
本节包括以下章节:
- 第一章**,现代 JavaScript 概述
第二章:现代 JavaScript 概述
在这一章中,我们将讨论一些核心的 JavaScript 概念。如果你是 JavaScript 的新手,需要介绍的话,请查看第十三章,附录:基本 JavaScript 概念。
理解一些现代 JavaScript 概念并不是使用 Danfo.js 的先决条件,但是如果您是 JavaScript 的新手或者来自 Python 背景,我们建议您阅读本章,原因是在使用 Danfo.js 构建应用程序时,我们将使用这里介绍的大部分概念。另外,值得一提的是,这里介绍的许多概念通常会帮助您编写更好的 JavaScript。
本章将向您介绍一些现代 JavaScript 概念,到最后,您将学习并理解以下概念:
-
理解
let
和var
之间的区别 -
解构
-
展开语法
-
作用域和闭包概述
-
理解数组和对象方法
-
理解 this 属性
-
箭头函数
-
Promises 和 async/await
-
面向对象编程和 JavaScript 类
-
使用转译器设置现代 JavaScript 环境
-
使用 Mocha 和 Chai 进行单元测试
技术要求
主要要求是已安装 Node.js 和 NPM。您可以按照官方安装指南在nodejs.org/en/download/
上安装适用于您操作系统的 Node。本章的代码可以在 GitHub 仓库中找到:github.com/PacktPublishing/Building-Data-Driven-Applications-with-Danfo.js/tree/main/Chapter01
。
我们将从理解let
和var
之间的区别开始,以及为什么您应该更频繁地使用let
。
理解let
和var
之间的区别
在 ECMA 6 之前,创建变量的常见方式是使用var
。然而,使用var
有时会引入在运行时出现的错误,以及在运行时未显现但可能影响代码运行方式的错误。
如前段提到的,var
的一些属性会引入错误,如下所示:
-
var
允许变量的重新声明。 -
var
不是块级作用域;因此,它要么附加到全局作用域,要么附加到函数作用域。
让我们详细讨论上面列出的两个属性。
var 允许变量的重新声明
var
允许用户在代码中重新声明变量,因此覆盖了之前同名的变量。如果没有被捕获,这个特性可能不会显示错误,但肯定会影响代码的行为:
var population_count = 490;
var new_count = 10;
//along the line; you mistakenly re-declare the variable
var population_count = "490"
//do some arithmetic operation with the variable
var total_count = population_count + new_count
//output: "49010"
在前面的代码片段中,不会出现任何错误,但是代码的主要目标会因为var
没有警告我们已经声明了这样一个变量而被改变。
假设我们用let
替换var
,如下所示:
let population_count = 490;
// ...some other code goes here
let population_count = "490"
//output: Error: Identifier population count as already being declared
从前面的错误输出中可以看出,let
与var
不同,不允许在同一命名空间中声明变量两次。
接下来,让我们看看使用var
声明的变量的作用域属性。
var 不是块级作用域
使用var
声明的变量具有以下属性:
-
它们在定义的作用域内是立即可用的。
-
它们在被声明的作用域内是可用的。
在下面的代码中,我们将检查使用var
声明的estimate
变量在变量声明作用域内的所有作用域中都是可访问的:
var estimate = 6000;
function calculate_estimate() {
console.log(estimate);
}
calculate_estimate() // output 6000
if(true){
console.log(estimate);
}
现在,对于像if
、while
循环和for
循环这样的块级作用域,块级作用域内的代码应该在作用域可用时运行。同样,变量应该只在作用域可用时存在,一旦作用域再次不可用,变量就不应该被访问。
使用var
声明变量会使前面的语句不可能。在下面的代码中,我们使用var
声明一个变量,并调查其在所有可能的作用域中的可用性:
if(true){
var estimate = 6000;
}
console.log(estimate)
这将输出估计值为6000
。该变量不应存在于if
块之外。使用let
有助于解决这个问题:
if(true){
let estimate = 6000;
}
console.log(estimate)
//output: ReferenceError: estimate is not defined
这表明使用let
声明变量有助于减少代码中的意外错误。在下一节中,我们将讨论另一个重要概念,称为解构。
解构
20
,John
,Doe
和2019
分配到指定的变量中:
let data2 = [20, "John", "Doe", "2019"];
let [ age1, firstName1, lastName1, year1] = data2
解构使得可以将数组的元素分配给一个变量,而不是像在下面的代码中所示的旧的常规方法访问数组元素:
//Old method of accessing an array
let data = [20, "John", "Doe", "2019"];
let firstName = data[1];
let age = data[0];
let lastName = data[2];
let year = data[3];
解构也适用于对象,就像在下面的代码中所示的那样:
let data3 = {
age: 20,
firstName: "john",
lastName: "Doe",
year: 2019
}
let { age2, firstName2, lastName2, year2 } = data3
在对象解构中,请注意我们使用{}
而不是[]
,就像用于数组的一样。这是因为左侧的类型必须与右侧的类型相同。
重要提示
如果我们在解构对象时使用[]
,我们会收到一个错误,显示{}
。对于数组解构,你可能不会得到任何错误,但变量将是未定义的。
在接下来的部分,我们将看一下展开语法。
展开语法
展开语法是可迭代元素的另一种解构形式,例如字符串和数组。展开语法可以在涉及数组和对象的许多情况下使用。在本节中,我们将快速查看展开语法的一些用例。
将可展开的可迭代对象展开或解包到数组中。
可迭代对象可以展开/解包成数组。在下面的示例中,我们将展示如何使用展开运算符来解包字符串变量:
let name = "stephen"
let name_array = [...name];
该代码将name
字符串展开为name_array
,因此,name_array
将具有以下值:['s','t','e','p','h','e','n'
]。
在将字符串元素展开为数组的同时,我们可以添加其他值,就像在下面的代码中所示的那样:
let name = "stephen"
let name_array = [...name, 1,2,3]
console.log(name_array)
// output ['s', 't', 'e','p', 'h', 'e','n',1,2,3]
请记住,任何可迭代对象都可以展开成数组。这表明我们也可以展开一个数组到另一个数组中,就像在下面的代码中演示的那样:
let series = [1,2,3,4,5,6,7,8]
let new_array = [...series, 100, 200]
console.log(new_array)
// output [1, 2, 3, 4, 5,6, 7, 8, 100, 200]
接下来,我们将把展开运算符应用到对象上。
从现有对象创建新对象
从现有对象创建新对象遵循与展开运算符相同的模式:
Let data = {
age: 20,
firstName: "john",
lastName: "Doe",
year: 2019
}
let new_data = {...data}
这将创建一个具有与前一个对象相同属性的新对象。在将前一个对象展开为新对象时,可以同时添加新属性:
let data = {
age: 20,
firstName: "john",
lastName: "Doe",
year: 2019
}
let new_data = { ...data, degree: "Bsc", level: "expert" }
console.log(new_data)
//output
// {
// age: 20,
// Degree: "Bsc",
// FirstName: "John",
// lastName: "Doe",
// Level: "expert",
// Year: 2019
// }
函数参数
对于需要许多参数的函数,展开语法可以帮助一次性传递许多参数到函数中,从而减少逐个填充函数参数的压力。
在下面的代码中,我们将看到如何将参数数组传递给函数:
function data_func(age, firstName, lastName, year) {
console.log(`Age: ${age}, FirstName: ${firstName}, LastName: ${lastName}, Year: ${year}`);
}
let data = [30, "John", "Neumann", '1948']
data_func(...data)
//output Age: 30, FirstName: John, LastName: Neumann, Year: 1984
Age: 30, FirstName: John, LastName: Neumann, Year: 1984
在前面的代码中,首先,我们创建了一个名为data_func
的函数,并定义了要传递的一组参数。然后我们创建了一个包含要传递给data_func
的参数列表的数组。
通过使用展开语法,我们能够传递数据数组并将数组中的每个值分配为参数值–data_func(...data)
。每当一个函数需要许多参数时,这将变得非常方便。
在下一节中,我们将看一下作用域和闭包,以及如何使用它们更好地理解您的 JavaScript 代码。
作用域和闭包概述
在理解 let 和 var 之间的区别部分,我们讨论了作用域,并谈到了var
在全局作用域和函数作用域中都可用。在本节中,我们将更深入地了解作用域和闭包。
作用域
为了理解作用域,让我们从下面的代码开始:
let food = "sandwich"
function data() {
}
food
变量和data
函数都分配给了全局作用域;因此,它们被称为全局变量和全局函数。这些全局变量和函数始终对 JavaScript 文件中的每个其他作用域和程序都是可访问的。
本地范围可以进一步分为以下几类:
-
函数范围
-
块范围
函数范围仅在函数内部可用。也就是说,在函数范围内创建的所有变量和函数在函数外部是不可访问的,并且只有在函数范围可用时才存在,例如:
function func_scope(){
// function scope exist here
}
块范围仅存在于特定上下文中。例如,它可以存在于花括号{ }
内,以及if
语句、for
循环和while
循环中。下面的代码片段中还提供了另外两个例子:
if(true){
// if block scope
}
在前面的if
语句中,您可以看到块范围仅存在于花括号内部,并且在if
语句内声明的所有变量都是局部变量。另一个例子是for
循环,如下面的代码片段所示:
for(let i=0; i< 5; i++){
//for loop's block scope
}
块范围还存在于for...
循环的花括号内。在这里,您可以访问i
计数器,并且无法在块外部访问内部声明的任何变量。
接下来,让我们了解闭包的概念。
闭包
闭包利用了函数内部作用域的概念。请记住,我们同意在函数范围内声明的变量在函数范围外部是不可访问的。闭包使我们能够利用这些私有属性(或变量)。
假设我们想创建一个程序,该程序将始终将值2
和1
添加到表示人口估计的estimate
变量中。可以使用以下代码的一种方法:
let estimate = 6000;
function add_1() {
return estimate + 1
}
function add_2() {
return estimate + 2;
}
console.log(add_1()) // 60001
console.log(add_2()) // 60002
前面的代码没有问题,但是随着代码库变得非常庞大,我们可能会迷失estimate
值,也许在某个时候需要一个函数来更新该值,并且我们可能还希望通过将全局estimate
变量设置为局部变量来清理全局范围。
因此,我们可以创建一个函数范围来为我们执行此操作,并最终清理全局范围。以下是下面代码片段中的一个示例:
function calc_estimate(value) {
let estimate = value;
function add_2() {
console.log('add two', estimate + 2);
}
function add_1() {
console.log('add one', estimate + 1)
}
add_2();
add_1();
}
calc_estimate(6000) //output: add two 60002 , add one 60001
前面的代码片段与我们定义的第一个代码片段类似,只是有一个小差异,即函数接受estimate
值,然后在calc_estimate
函数内部创建add_2
和add_1
函数。
使用前面的代码更好地展示闭包的一种方法是能够在任何时候更新估计值,而不是在调用函数的实例中。让我们看一个例子:
function calc_estimate(value) {
let estimate = value;
function add_2() {
estimate += 2
console.log('add 2 to estimate', estimate);
}
return add_2;
}
let add_2 = calc_estimate(50);
// we have the choice to add two to the value at any time in our code
add_2() // add 2 to estimate 52
add_2() // add 2 to estimate 54
add_2() // add 2 to estimate 56
在前面的代码片段中,内部函数add_2
将值2
添加到estimate
变量中,从而改变了值。调用calc_estimate
并将其分配给变量add_2
。因此,每当我们调用add_2
时,我们都会将估计值更新为2
。
我们更新calc_estimate
内部的add_2
函数,以接受一个值,该值可用于更新estimate
值:
function calc_estimate(value){
let estimate = value;
function add_2(value2){
estimate +=value2
console.log('add 2 to estimate', estimate);
}
return add_2;
}
let add_2 = calc_estimate(50);
// we have the choice to add two to the value at any time in our code
add_2(2) // add 2 to estimate 52
add_2(4) // add 2 to estimate 56
add_2(1) // add 2 to estimate 5
现在您已经了解了作用域和闭包,我们将在下一节中讨论数组、对象和字符串方法。
进一步阅读
要更详细地了解闭包,请查看Ved Antani的书《精通 JavaScript》。
理解数组和对象方法
数组和对象是 JavaScript 中最重要的两种数据类型。因此,我们专门设置了一个部分来讨论它们的一些方法。我们将从数组方法开始。
数组方法
我们无法讨论如何构建数据驱动的产品而不讨论数组方法。了解不同的数组方法使我们能够访问我们的数据并创建工具来操作/处理我们的数据。
数组可以以两种不同的形式创建:
let data = []
// or
let data = new Array()
[ ]
方法主要用于初始化数组,而new Array()
方法主要用于创建大小为n的空数组,如下面的代码片段所示:
let data = new Array(5)
console.log(data.length) // 5
console.log(data) // [empty × 5]
创建的空数组可以稍后用值填充,如下面的代码所示:
data[0] = "20"
data[1] = "John"
data[2] = "Doe"
data[3] = "1948"
console.log(data) // ["20", "John","Doe","1948", empty]
// try access index 4
console.log(data[4]) // undefined
创建这样一个空数组不仅限于使用new Array()
方法。它也可以使用[ ]
方法创建,如下面的代码片段所示:
let data = []
data.length = 5; // create an empty array of size 5
console.log(data) // [empty × 5]
您可以看到我们在创建后明确设置了长度,因此new Array()
方法更方便。
现在让我们看一些常见的数组方法,这些方法将用于构建一些数据驱动的工具。
Array.splice
删除和更新数组值始终是数据驱动产品中的基本操作之一。JavaScript 有一个delete
关键字,用于删除数组中特定索引处的值。该方法实际上并不删除值,而是用空值或 undefined 值替换它,如下面的代码所示:
let data = [1,2,3,4,5,6];
delete data[4];
console.log(data) // [1,2,3,4 empty, 6]
在data
变量中,如果我们尝试访问索引4
处的值,我们会发现它返回undefined
:
console.log(data[4]) // undefined
但是,每当我们使用splice
删除数组中的一个值时,数组的索引会重新排列,如下面的代码片段所示:
let data = [1,2,3,4,5,6]
data.splice(4,1) // delete index 4
console.log(data) // [1,2,3,4,6]
Array.splice
接受以下参数,start,[deleteCount, value-1,......N-values]
。在前面的代码片段中,由于我们只是删除,所以我们使用了start
和deleteCount
。
data.splice(4,1)
命令删除从索引4
开始的值,只有一个计数,因此它删除了索引5
处的值。
如果我们将data.splice(4,1)
中的值1
替换为2
,结果为data.splice(4,2)
,将从索引4
开始删除data
数组中的两个值(5
和6
),如下面的代码块所示:
let data = [1,2,3,4,5,6]
data.splice(4,0,10,20) // add values between 5 and 6
console.log(data) // [1,2,3,4,5,10,20,6]
data.splice(4,0,10, 20);
指定从索引4
开始,0
指定不删除任何值,同时在5
和6
之间添加新值(10
和20
)。
Array.includes
这种方法用于检查数组是否包含特定值。我们在下面的代码片段中展示了一个例子:
let data = [1,2,3,4,5,6]
data.includes(6) // true
Array.slice
Array.slice
用于通过指定范围获取数组元素;Array.slice(start-index, end-index)
。让我们在下面的代码中看一个使用这种方法的例子:
let data = [1,2,3,4,5,6]
data.slice(2,4)
//output [3,4]
前面的代码从索引2
(具有元素3
)开始提取元素,直到索引5
。请注意,数组没有输出[3,4,5]
,而是[3,4]
。Array.splice
总是排除结束索引值,因此它使用一个闭区间。
Array.map
Array.map
方法遍历数组的所有元素,对每次迭代应用一些操作,然后将结果作为数组返回。下面的代码片段是一个例子:
let data = [1,2,3,4,5,6]
let data2 = data.map((value, index)=>{
return value + index;
});
console.log(data2) // [1,3,5,7,9,11]
data2
变量是通过使用map
方法迭代每个数据元素创建的。在map
方法中,我们将数组的每个元素(值)添加到其索引中。
Array.filter
Array.filter
方法用于过滤数组中的一些元素。让我们看看它的运行方式:
let data = [1,2,3,4,5,6]
let data2 = data.filter((elem, index)=>{
return (index %2 == 0)
})
console.log(data2) // [1,3,5]
在前面的代码片段中,使用2
的模数(%)过滤掉了数据中每个偶数索引的数组元素。
有很多数组方法,但我们只涵盖了这些方法,因为它们在数据处理过程中总是很方便,我们无法覆盖所有方法。
但是,如果在本书的后续章节中使用了任何新方法,我们肯定会提供解释。在下一节中,我们将讨论对象方法。
对象
对象是 JavaScript 中最强大和重要的数据类型,在本节中,我们将介绍一些重要的对象属性和方法,使得与它们一起工作更容易。
访问对象元素
访问对象中的键/值很重要,因此存在一个特殊的for...in
循环来执行这个操作:
for (key in object) {
// run some action with keys
}
for...in
循环返回对象中的所有键,这可以用于访问对象值,如下面的代码所示:
let user_profile = {
name: 'Mary',
sex: 'Female',
age: 25,
img_link: 'https://some-image-link.png',
}
for (key in user_profile) {
console.log(key, user_profile[key]);
}
//output:
// name Mary
// sex Female
// age 25
// img_link https://some-image-link.png
在下一节中,我们将展示如何测试属性的存在。
测试属性是否存在
要检查属性是否存在,可以使用"key"
in
对象语法,如下面的代码片段所示:
let user_profile = {
name: 'Mary',
sex: 'Female',
age: 25,
img_link: 'https://some-image-link.png',
}
console.log("age" in user_profile)
//outputs: true
if ("rank" in user_profile) {
console.log("Your rank is", user_profile.rank)
} else {
console.log("rank is not a key")
}
//outputs: rank is not a key
删除属性
在对象属性之前使用delete
关键字将从对象中删除指定的属性。看看下面的例子:
let user_profile = {
name: 'Mary',
sex: 'Female',
age: 25,
img_link: 'https://some-image-link.png',
}
delete user_profile.age
console.log(user_profile)
//output:
// {
// img_link: "https://some-image-link.png",
// name: "Mary",
// sex: "Female"
// }
您可以看到age
属性已经成功地从user_profile
对象中删除。接下来,让我们看看如何复制和克隆对象。
复制和克隆对象
将旧对象分配给新对象只是创建对旧对象的引用。也就是说,对新对象的任何修改也会影响旧对象。例如,在下面的例子中,我们将user_profile
对象分配给一个新变量new_user_profile
,然后删除age
属性:
let user_profile = {
name: 'Mary',
sex: 'Female',
age: 25,
img_link: 'https://some-image-link.png',
}
let new_user_profile = user_profile
delete new_user_profile.age
console.log("new_user_profile", new_user_profile)
console.log("user_profile", user_profile)
//output:
// "new_user_profile" Object {
// img_link: "https://some-image-link.png",
// name: "Mary",
// sex: "Female"
// }
// "user_profile" Object {
// img_link: "https://some-image-link.png",
// name: "Mary",
// sex: "Female"
// }
您会注意到从user_profile
对象中删除age
属性也会从new_user_profile
中删除。这是因为复制只是对旧对象的引用。
为了将对象复制/克隆为新的独立对象,您可以使用Object.assign
方法,如下面的代码所示:
let new_user_profile = {}
Object.assign(new_user_profile, user_profile)
delete new_user_profile.age
console.log("new_user_profile", new_user_profile)
console.log("user_profile", user_profile)
//output
"new_user_profile" Object {
img_link: "https://some-image-lik.png",
name: "Mary",
sex: "Female"
}
"user_profile" Object {
age: 25,
img_link: "https://some-image-lik.png",
name: "Mary",
sex: "Female"
}
Object.assign
方法也可以用于一次从多个对象中复制属性。我们在下面的代码片段中提供了一个示例:
let user_profile = {
name: 'Mary',
sex: 'Female',
age: 25,
img_link: 'https://some-image-lik.png',
}
let education = { graduated: true, degree: 'BSc' }
let permissions = { isAdmin: true }
Object.assign(user_profile, education, permissions);
console.log(user_profile)
//output:
// {
// name: 'Mary',
// sex: 'Female',
// img_link: 'https://some-image-link.png',
// graduated: true,
// degree: 'BSc',
// isAdmin: true
// }
您可以看到我们能够从两个对象(education
和permissions
)中复制属性到我们的原始对象user_profile
中。通过这种方式,我们可以通过简单列出所有对象来调用Object.assign
方法,将任意数量的对象复制到另一个对象中。
提示
您还可以使用spread运算符执行深拷贝。这实际上更快,更容易编写,如下面的示例所示:
let user_profile = {
name: 'Mary',
sex: 'Female'
}
let education = { graduated: true, degree: 'BSc' }
let permissions = { isAdmin: true }
const allObjects = {...user_profile, ...education, ...permissions}
allObjects. This syntax is easier and quicker than the object.assign method and is largely used today.
在下一节中,我们将讨论与 JavaScript 对象相关的另一个重要概念,称为this属性。
理解 this 属性
this关键字是一个对象属性。当在函数内部使用时,它以函数在调用时绑定的对象的形式出现。
在每个 JavaScript 环境中,我们都有一个全局对象。在 Node.js 中,全局对象被命名为global,在浏览器中,全局对象被命名为window。
所谓的全局对象是指所有变量声明和函数都表示为这个全局对象的属性和方法。例如,在浏览器脚本文件中,我们可以访问全局对象,如下面的代码片段所示:
name = "Dale"
function print() {
console.log("global")
}
// using the browser as our environment
console.log(window.name) // Dale
window.print() // global
在前面的代码块中,name
变量和print
函数是在全局范围声明的,因此它们可以作为window全局对象的属性(window.name
)和方法(window.print()
)来访问。
前面一句话中的陈述可以总结为全局名称和函数默认绑定(或分配)到全局对象 window。
这也意味着我们可以将这个变量绑定到任何具有相同name
变量和相同函数print
的对象上。
为了理解这个概念,首先让我们将window.print()
重写为print.call(window)
。这种新的方法在 JavaScript 中被称为 de-sugaring;它就像看到一个方法的实现形式一样。
.call
方法只是简单地接受我们想要绑定函数调用的对象。
让我们看看print.call()
和这个属性是如何工作的。我们将重写print
函数以访问name
变量,如下面的代码片段所示:
name = "Dale"
object_name = "window"
function print(){
console.log(`${this.name} is accessed from ${this.object_name}`)
}
console.log(print.call(window)) // Dale is accessed from window
现在,让我们创建一个自定义对象,并且给它与window
对象相同的属性,如下面的代码片段所示:
let custom_object = {
name: Dale,
Object_name: "custom_object"
}
print.call(custom_object) // Dale is accessed from custom_object
这个概念可以应用于所有的对象方法,如下面的代码所示:
data = {
name: 'Dale',
obj_name: 'data',
print: function () {
console.log(`${this.name} is accessed from ${this.obj_name}`);
}
}
data.print() // Dale is accessed from data
// don't forget we can also call print like this
data.print.call(data) // Dale is accessed from data
有了这个,我们也可以将data
中的print()
方法绑定到另一个对象,如下面的代码片段所示:
let data2 = {
name: "Dale D"
Object_name: "data2"
}
data.print.call(data2) // Dale D is accessed from data2
这种方法展示了 this 属性如何依赖于函数调用时的运行时。这个概念也影响了 JavaScript 中一些事件操作的工作方式。
进一步阅读
为了更深入地理解这个概念,Emberjs 和 TC39 成员之一 Yehuda Katz 在他的文章理解 JavaScript 函数调用和 "this"中对此进行了更详细的阐述。
箭头函数
箭头函数只是未命名或匿名函数。箭头函数的一般语法如下所示:
( args ) => { // function body }
箭头函数提供了一种创建简洁可调用函数的方法。这意味着箭头函数不可构造,也就是说,它们不能用 new
关键字实例化。
以下是如何以及何时使用箭头函数的不同方式:
- 箭头函数可以赋值给一个变量:
const unnamed = (x) => {
console.log(x)
}
unnamed(10) // 10
- 箭头函数可以用作IIFE(立即调用函数表达式)。IIFE 是一旦被 JavaScript 编译器遇到就立即调用的函数:
((x) => {
console.log(x)
})("unnamed function as IIFE") // output: unnamed function as IIFE
- 箭头函数可以用作回调:
function processed(arg, callback) {
let x = arg * 2;
return callback(x);
}
processed(2, (x) => {
console.log(x + 2)
}); // output: 6
虽然箭头函数在某些情况下很棒,但使用它们也有缺点。例如,箭头函数没有自己的 this
作用域,因此它的作用域始终绑定到一般作用域,从而改变了我们对函数调用的整体理念。
在理解 this 属性部分,我们谈到了函数如何绑定到它们的调用范围,并使用这种能力来支持闭包,但使用箭头函数默认情况下会剥夺我们这个特性:
const Obj = {
name: "just an object",
func: function(){
console.log(this.name);
}
}
Obj.func() // just an object
即使在对象中,如代码片段所示,我们使用了匿名函数(但不是箭头函数),我们仍然可以访问对象的 Obj
属性:
const Obj = {
name: "just an object",
func: () => {
console.log(this.name);
}
}
Obj.func() // undefined
使用的箭头函数使 Obj.func
的输出为 undefined
。让我们看看如果全局作用域中有一个名为 name
的变量时它是如何工作的:
let name = "in the global scope"
const Obj = {
name: "just an object",
func: () => {
console.log(this.name);
}
}
Obj.func() // in the global
正如我们所看到的,Obj.func
调用了全局作用域中的变量。因此,我们必须知道何时何地使用箭头函数。
在下一节中,我们将讨论 Promise 和 async/await 概念。这将使我们能够轻松管理长时间运行的任务,并避免回调地狱(回调中有回调)。
Promise 和 async/await
让我们深入一下异步函数的世界,现在调用但稍后完成的函数。在本节中,我们将看到为什么我们需要Promise和async/await。
让我们从下面的代码片段中显示的一个简单问题开始。我们需要使用一个函数在调用函数后的 1
秒后更新一个数组:
let syncarray = ["1", "2", "3", "4", "5"]
function addB() {
setTimeout(() => {
syncarray.forEach((value, index)=>{
syncarray[index] = value + "+B"
})
console.log("done running")
}, 1000)
}
addB()
console.log(syncarray);
// output
// ["1", "2", "3", "4", "5"]
// "done running"
console.log(syncarray)
在 addB()
函数之前执行,因此我们在更新之前看到了 syncarray
的输出。这是一种异步行为。解决这个问题的一种方法是使用回调:
let syncarray = ["1", "2", "3", "4", "5"]
function addB(callback) {
setTimeout(() => {
syncarray.forEach((value, index)=>{
syncarray[index] = value + "+B"
})
callback() //call the callback function here
}, 1000)
}
addB(()=>{
// here we can do anything with the updated syncarray
console.log(syncarray);
})
// output
// [ '1+B', '2+B', '2+B', '4+B', '5+B' ]
使用前面的回调方法意味着我们总是传递回调以执行对更新后的 syncarray
函数的其他操作。让我们稍微更新一下代码,这次我们还将字符串 "A"
添加到 syncarray
中,然后打印出更新后的数组:
let syncarray = ["1", "2", "3", "4", "5"]
function addB(callback) {
setTimeout(() => {
syncarray.forEach((value, index) => {
syncarray[index] = value + "+B"
})
callback() //call the callback function here
}, 1000)
}
addB(() => {
setTimeout(() => {
syncarray.forEach((value, index) => {
syncarray[index] = value + "+A";
})
console.log(syncarray);
}, 1000)
})
// output
// [ '1+B+A', '2+B+A', '3+B+A', '4+B+A', '5+B+A' ]
前面的代码块显示了传递 callback
的快速方法。根据我们讨论的箭头函数,通过创建一个命名函数可以使代码更有组织性。
使用 Promise 清理回调
使用回调很快变得难以控制,并且很快就会陷入回调地狱。摆脱这种情况的一种方法是使用 Promise。Promise 使我们的回调更有组织性。它提供了一种可链接的机制,用于统一和编排依赖于先前函数的代码,正如你将在下面的代码块中看到的:
let syncarray = ["1", "2", "3", "4", "5"]
function addA(callback) {
return new Promise((resolve, reject) => {
setTimeout(() => {
syncarray.forEach((value, index) => {
syncarray[index] = value + "+A";
})
resolve()
}, 1000);
})
}
addA().then(() => console.log(syncarray));
//output
//[ '1+A', '2+A', '2+A', '4+A', '5+A' ]
在前面的代码片段中,setTimeout
被包裹在 Promise
函数中。使用以下表达式始终实例化 Promise
:
New Promise((resolve, rejection) => {
})
Promise
要么被解决,要么被拒绝。当它被解决时,我们可以做其他事情,当它被拒绝时,我们需要处理错误。
例如,让我们确保以下的 Promise
被拒绝:
let syncarray = ["1", "2", "3", "4", "5"]
function addA(callback) {
return new Promise((resolve, reject) => {
setTimeout(() => {
syncarray.forEach((value, index) => {
syncarray[index] = value + "+A";
})
let error = true;
if (error) {
reject("just testing promise rejection")
}
}, 1000);
})
}
addA().catch(e => console.log(e)) // just testing promise rejection
每当我们有多个 Promise 时,我们可以使用 .then()
方法来处理每一个:
addA.then(doB)
.then(doC)
.then(doD)
.then(doF)
.catch(e= > console.log(e));
使用多个.then()
方法来处理多个 promise 可能会很快变得难以控制。为了防止这种情况,我们可以使用Promise.all()
、Promise.any()
和Promise.race()
等方法。
Promise.all()
方法接受一个要执行的 promise 数组,并且只有当所有 promise 都被实现时才会解析。在下面的代码片段中,我们向我们之前的示例中添加了另一个异步函数,并使用Promise.all()
来处理它们:
let syncarray = ["1", "2", "2", "4", "5"]
function addA() {
return new Promise((resolve, reject) => {
setTimeout(() => {
syncarray.forEach((value, index) => {
syncarray[index] = value + "+A";
})
resolve()
}, 1000);
})
}
function addB() {
return new Promise((resolve, reject) => {
setTimeout(() => {
syncarray.forEach((value, index) => {
syncarray[index] = value + "+B";
})
resolve()
}, 2000);
})
}
Promise.all([addA(), addB()])
.then(() => console.log(syncarray)); // [ '1+A+B', '2+A+B', '2+A+B', '4+A+B', '5+A+B' ]
在前面的部分输出中,您可以看到每个异步函数按添加顺序执行,并且最终结果是这两个函数对syncarray
变量的影响。
另一方面,promise.race
方法将在数组中的任何 promise 被解析或拒绝时立即返回。您可以将其视为一场比赛,其中每个 promise 都试图首先解析或拒绝,一旦发生这种情况,比赛就结束了。要查看深入的解释以及代码示例,您可以访问 MDN 文档:developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/any
。
最后,promise.any
方法将返回第一个实现的 promise,而不管其他被拒绝的promise
函数。如果所有 promise 都被拒绝,那么Promise.any
通过为所有 promise 提供错误来拒绝 promise。要查看深入的解释以及代码示例,您可以访问 MDN 文档:developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/race
。
虽然使用 promise 来处理回调解决了很多问题,但实现或使用它们的更好方法是async/await函数。我们将在下一节介绍这些函数,并向您展示如何使用它们。
async/await
正如前面所说,async/await 提供了一种更优雅的处理 promise 的方式。它赋予我们控制在函数内部如何以及何时调用每个 promise 函数的能力,而不是使用.then()
和Promise.all()
。
以下代码片段显示了如何在代码中使用 async/await:
Async function anyName() {
await anyPromiseFunction()
await anyPromiseFunction()
}
前面的async
函数可以包含尽可能多的 promise 函数,每个函数在执行之前都在等待其他函数执行。此外,注意async
函数被解析为Promise
。也就是说,您只能使用.then()
或在另一个async
/await
函数中调用它来获取前面anyName
函数的返回变量(或解析函数):
Async function someFunction() {
await anyPromiseFunction()
await anotherPromiseFunction()
return "done"
}
// To get the returned value, we can use .then()
anyName().then(value => console.log(value)) // "done"
// we can also call the function inside another Async/await function
Async function resolveAnyName() {
const result = await anyName()
console.log(result)
}
resolveAnyName() // "done"
有了这个知识,我们可以重新编写前一节中的 promise 执行,而不是使用Promise.all([addA(), addB()])
:
let syncarray = ["1", "2", "2", "4", "5"]
function addA(callback) {
return new Promise((resolve, reject) => {
setTimeout(() => {
syncarray.forEach((value, index) => {
syncarray[index] = value + "+A";
})
resolve()
}, 1000);
})
}
function addB(callback) {
return new Promise((resolve, reject) => {
setTimeout(() => {
syncarray.forEach((value, index) => {
syncarray[index] = value + "+B";
})
resolve()
}, 2000);
})
}
Async function runPromises(){
await addA()
await addB()
console.log(syncarray);
}
runPromises()
//output: [ '1+A+B', '2+A+B', '2+A+B', '4+A+B', '5+A+B' ]
您可以从前面的输出中看到,我们与使用Promise.all
语法时得到了相同的输出,但采用了更简洁和清晰的方法。
注意
使用多个 await 而不是promise.all
的一个缺点是效率。尽管很小,但promise.all
是处理多个独立 promise 的首选和推荐方式。
Stack Overflow 上的这个主题(stackoverflow.com/questions/45285129/any-difference-between-await-promise-all-and-multiple-await
)清楚地解释了为什么这是处理多个 promise 的推荐方式。
在下一节中,我们将讨论 JavaScript 中的面向对象编程(OOP)以及如何使用 ES6 类。
面向对象编程和 JavaScript 类
OOP 是大多数高级语言支持的常见编程范式。在 OOP 中,您通常使用对象的概念来编写应用程序,这些对象可以是数据和代码的组合。
数据表示对象的信息,而代码表示可以在对象上执行的属性、属性和行为。
面向对象编程打开了一个全新的可能性世界,因为许多问题可以被模拟或设计为不同对象之间的交互,从而更容易设计复杂的程序,并且更易于维护和扩展它们。
JavaScript,像其他高级语言一样,提供了对面向对象编程概念的支持,尽管不是完全的(developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes
),但实质上,大多数重要的面向对象编程概念,如对象、类和继承都得到了支持,这些概念大多足以解决使用面向对象编程建模的许多问题。在接下来的部分,我们将简要介绍类以及它们与 JavaScript 中的面向对象编程的关系。
类
面向对象编程中的类就像对象的蓝图。也就是说,它们以一种抽象对象的模板定义,使得可以通过遵循该蓝图创建多个副本。这里的副本官方称为实例。因此,实质上,如果我们定义了一个类,那么我们可以轻松地创建该类的多个实例。
在 ECMA 2015 中,使用 ES16 的class
关键字的User
对象:
class User {
constructor(firstName, lastName, email) {
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
}
getFirstName() {
return this.firstName;
}
getLastName() {
return this.lastName;
}
getFullName() {
return `${this.firstName} ${this.lastName}`;
}
getEmail() {
return this.email;
}
setEmail(email) {
this.email = email;
}
}
let Person1 = new User("John", "Benjamin", "[email protected]")
console.log(Person1.getFullName());
console.log(Person1.getEmail());
// outputs
// "John Benjamin"
// "[email protected]"
通过使用class
关键字,您可以以更清晰的方式将数据(名称和电子邮件)与功能(函数/方法)结合在一起,从而有助于易于维护和理解。
在我们继续之前,让我们更详细地分解类模板,以便更好地理解。
第一行以class
关键字开头,通常后面跟着一个类名。按照惯例,类名采用驼峰命名法,例如UserModel
或DatabaseModel
。
类定义中可以添加一个可选的构造函数。constructor
类是一个初始化函数,每次从类创建新实例时都会运行。在这里,通常会添加代码,用特定属性初始化每个实例。例如,在以下代码片段中,我们从User
类创建两个实例,并使用特定属性进行初始化:
let Person2 = new User("John", "Benjamin", "[email protected]")
let Person3 = new User("Hannah", "Joe", "[email protected]")
console.log(Person2.getFullName());
console.log(Person3.getFullName());
//outputs
// "John Benjamin"
// "Hannah Montanna"
类的下一个重要部分是添加函数。函数充当class
方法,并通常为类添加特定行为。函数也对从类创建的每个实例都可用。在我们的User
类中,添加了诸如getFirstName
、getLastName
、getEmail
和setEmail
等方法,以根据它们的实现执行不同的功能。要在类实例上调用函数,通常使用点表示法,就像访问对象的属性时一样。例如,在以下代码中,我们返回Person1
实例的全名:
Person1.getFullName()
有了类之后,我们现在转向面向对象编程中的下一个概念,称为继承。
继承
在面向对象编程中,继承是一个类使用另一个类的属性/方法的能力。这是一种通过使用另一个类(超类/父类)来扩展一个类(子类/子类)特征的简单方法。这样,子类继承了父类的所有特征,并且可以扩展或更改这些特性。让我们使用一个示例来更好地理解这个概念。
在我们的应用程序中,假设我们已经在上一节中定义了User
类,但我们想创建一个名为Teachers
的新用户组。教师也是用户类,他们也将需要基本属性,例如User
类已经具有的名称和电子邮件。因此,我们可以简单地扩展它,而不是创建一个具有这些现有属性和方法的新类,如下面的代码片段所示:
class Teacher extends User {
}
请注意我们使用了extends
关键字。这个关键字简单地使得父类(User
)中的所有属性都可以在子类(Teacher
)中使用。只需基本的设置,Teacher
类就自动可以访问User
类的所有属性和方法。例如,我们可以像创建User
值一样实例化和创建一个新的Teacher
:
let teacher1 = new Teacher("John", "Benjamin", "[email protected]")
console.log(teacher1.getFullName());
//outputs
// "John Benjamin"
在扩展一个类之后,我们基本上想要添加新的特性。我们可以通过简单地在子类模板中添加新的函数或属性来实现这一点,就像下面的代码所示:
class Teacher extends User {
getUserType(){
return "Teacher"
}
}
在上面的代码片段中,我们添加了一个新的方法getUserType
,它返回user
类型的字符串。通过这种方式,我们可以添加更多原本不在parent
类中的特性。
值得一提的是,你可以通过在child
类中创建一个同名的新函数来替换parent
函数。这个过程在Teacher
类中称为getFullName
函数,我们可以这样做:
class User {
constructor(firstName, lastName, email) {
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
}
getFirstName() {
return this.firstName;
}
getLastName() {
return this.lastName;
}
getFullName() {
return `${this.firstName} ${this.lastName}`;
}
getEmail() {
return this.email;
}
setEmail(email) {
this.email = email;
}
}
class Teacher extends User {
getFullName(){
return `Teacher: ${this.firstName} ${this.lastName}`;
}
getUserType(){
return "Teacher"
}
}
let teacher1 = new Teacher("John", "Benjamin", "[email protected]")
console.log(teacher1.getFullName());
//output
// "Teacher: John Benjamin"
这里可能会有一个问题:如果我们想要在初始化Teacher
类时除了firstname
、lastname
和email
之外添加额外的实例,该怎么办?这是可以实现的,我们可以通过使用一个新的关键字super
轻松扩展构造函数。我们在下面的代码中演示了如何做到这一点:
// class User{
// previous User class goes here
// ...
// }
class Teacher extends User {
constructor(firstName, lastName, email, userType, subject) {
super(firstName, lastName, email) //calls parent class constructor
this.userType = userType
this.subject = subject
}
getFullName() {
return `Teacher: ${this.firstName} ${this.lastName}`;
}
getUserType() {
return "Teacher"
}
}
let teacher1 = new Teacher("Johnny", "Benjamin", "[email protected]", "Teacher", "Mathematics")
console.log(teacher1.getFullName());
console.log(teacher1.userType);
console.log(teacher1.subject);
//outputs
// "Teacher: Johnny Benjamin"
// "Teacher"
// "Mathematics"
在上面的代码中,我们进行了两件新的事情。首先,我们向Teacher
类添加了两个新的实例属性(userType
和subject
),然后我们调用了super
函数。super
函数简单地调用父类(User
),执行实例化,然后立即初始化Teacher
类的新属性。
通过这种方式,我们能够在初始化类属性之前先初始化父类属性。
类在面向对象编程中非常有用,JavaScript 中提供的class
关键字使得使用面向对象编程变得容易。值得一提的是,在幕后,JavaScript 将类模板转换为对象,因为它没有对类的一流支持。这是因为 JavaScript 默认是基于原型的面向对象语言。因此,JavaScript 在幕后调用的类接口被称为底层原型模型上的语法糖。你可以在以下链接中阅读更多关于这个问题:es6-features.org/#ClassDefinition
。
现在我们对 JavaScript 中的面向对象编程有了基本的了解,我们可以开始创建易于维护的复杂应用程序。在接下来的部分中,我们将讨论 JavaScript 开发的另一个重要方面,即使用现代 JavaScript 支持设置开发环境。
设置一个支持转译器的现代 JavaScript 环境
JavaScript 的一个独特特性,也是它非常受欢迎的原因之一,就是它的跨平台支持。JavaScript 几乎可以在任何地方运行,从浏览器和桌面到甚至服务器端。虽然这是一个独特的特性,但要让 JavaScript 在这些环境中运行得最佳,需要一些设置和配置,使用第三方工具/库。设置工具的另一个原因是,你可以用不同的风格来编写 JavaScript,因为这些现代/新的风格可能不被旧版浏览器支持。这意味着你在新语法中编写的代码,通常是 ES15 之后的语法,需要被转译成 ES16 之前的格式,才能在大多数浏览器中正确运行。
在本节中,你将学习如何设置和配置一个 JavaScript 项目,以支持跨平台和现代 JavaScript 代码。你将使用两个流行的工具——Babel和webpack来实现这一点。
Babel
Babel 是一个工具,用于将用 ES15 编写的 JavaScript 代码转换为现代或旧版浏览器中向后兼容的 JavaScript 版本。Babel 可以帮助你做到以下几点:
-
转换/转译语法。
-
填充在目标环境中缺失的功能。Babel 会自动添加一些在旧环境中不可用的现代功能。
-
转换源代码。
在下面的代码中,我们展示了一个经过 Babel 转换的代码片段的示例:
// Babel Input: ES2015 arrow function
["Lion", "Tiger", "Shark"].map((animal) => console.log(animal));
// Babel Output: ES5 equivalent
["Lion", "Tiger", "Shark"].map(function(animal) {
console.log(animal)
});
您会注意到在前面的代码片段中,现代箭头函数会自动转译为所有浏览器都支持的function
关键字。这就是 Babel 在幕后对您的源代码所做的事情。
接下来,让我们了解 webpack 的作用。
Webpack
webpack 也是一个转译器,可以执行与 Babel 相同的功能,甚至更多。webpack 可以将几乎任何东西,包括图像、HTML、CSS和JavaScript打包和捆绑成一个优化的脚本,可以轻松在浏览器中使用。
在本节中,我们将利用 Babel 和 webpack 来展示如何设置一个跨平台的 JavaScript 项目。让我们马上开始吧。
使用 Babel 和 webpack 的示例项目
在本节中,我们将使用npm
创建一个简单的 JavaScript 项目。因此,您应该在本地安装 Node.js 以便跟随操作。执行以下步骤来实现这一点:
- 在您喜欢的目录中打开终端,并使用以下命令创建一个文件夹:
cross-env-js, in your directory, and then change the directory as well.
- 创建一个
package.json
文件。虽然您可以手动创建,但使用npm
创建会更容易。在终端中运行以下命令:
package.json file and accept all default options. Ideally, this should output the following:![Figure 1.1 – Output from running the npm init –y command ](https://gitee.com/OpenDocCN/freelearn-js-zh/raw/master/docs/bd-dtdvn-app-danfo/img/B17076_01_01.jpg)Figure 1.1 – Output from running the npm init –y command
- 接下来,安装所有相关的软件包,以帮助我们进行捆绑和转译:
package.json file should look like this:
{
"name": "cross-env-js",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo "Error: no test specified" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/cli": "⁷.12.8",
"@babel/core": "⁷.12.9",
"@babel/preset-env": "⁷.12.7"
"babel-loader": "⁸.2.2",
"webpack": "⁵.9.0",
"webpack-cli": "⁴.2.0"
},
"dependencies": {
"@babel/polyfill": "⁷.12.1"
}
}
- 添加一些代码,我们将对其进行转译和测试。对于这一部分,您可以从终端创建文件和文件夹,也可以使用代码编辑器。我将在这里使用 Visual Studio Code 编辑器。
在您的代码编辑器中,打开cross-env-js
项目文件夹,然后创建以下文件和文件夹:
├── dist
│ └── index.html
├── src
│ ├── index.js
│ ├── utils.js
也就是说,您将创建两个名为dist
和src
的文件夹。dist
将包含一个 HTML 文件(index.html
),用于测试我们的捆绑应用程序,src
将包含我们想要转译的现代 JavaScript 代码。
创建这些文件和文件夹后,整个目录结构应如下所示:
├── dist
│ └── index.html
├── node_modules
├── package-lock.json
├── package.json
└── src
├── index.js
└── utils.js
注意
如果您使用 Git 等版本控制工具,通常会添加一个.gitignore
文件,指定node_modules
可以被忽略。
- 创建一个
dist
文件夹,在该文件夹中创建一个带有以下代码的index.html
文件:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="img/bundle.js"></script>
<title>Cross Environment Support</title>
</head>
<body>
</body>
</html>
HTML 文件对您来说应该很熟悉,但请注意我们添加了一个指向bundle.js
文件的script
标签。这个文件目前还不存在,将由 webpack 在幕后使用 Babel 生成。
- 在
src
文件夹中编写一些现代 JavaScript。从utils.js
开始,我们将创建并导出一些函数,然后导入它们以在index.js
中使用。
从utils.js
开始,添加以下代码:
const subjects = {
John: "English Language",
Mabel: "Mathematics",
Mary: "History",
Joe: "Geography"
}
export const names = ["John", "Mabel", "Mary", "Joe"]
export const getSubject = (name) =>{
return subjects[name]
}
utils.js
脚本使用一些现代的 JS 语法,比如export
和箭头函数,这些在转译后只能兼容旧浏览器。
接下来,在index.js
脚本中,您将导入这些函数并使用它们。将以下代码添加到您的index.js
脚本中:
import { names, getSubject } from "./utils";
names.forEach((name) =>{
console.log(`Teacher Name: ${name}, Teacher Subject: ${getSubject(name)}`)
})
您会注意到我们还在index.js
文件中使用箭头函数和解构导入。在这里,我们从utils.js
脚本中导入了导出的数组(names)和getSubject
函数。我们还使用箭头函数和模板文字(
)的组合来检索和记录每个Teacher
的详细信息。
- 现在我们的现代 JS 文件已经准备好,我们将创建一个配置文件,告诉 webpack 在哪里找到我们的源代码来捆绑,以及使用哪个转译器,就我们的情况而言,是 Babel。
在您的根目录中,创建一个webpack.config.js
文件,并添加以下代码:
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
publicPath: '/dist'
},
module: {
rules: [
{
test: /\.js$/,
exclude: /(node_modules)/,
use: {
loader: 'babel-loader',
}
}
]
}
};
让我们了解一下这个文件中发生了什么:
a) 配置文件的第一部分需要path
模块,这将有助于解决所有与路径相关的函数。
b) 接下来,您会注意到entry
字段。这个字段简单地告诉 webpack 在哪里找到起始/主要脚本。webpack 将使用这个文件作为起点,然后递归地遍历每个导入依赖项,以链接与入口文件相关的所有文件。
c) 接下来是output
字段,它告诉 webpack 在哪里保存捆绑文件。在我们的示例中,我们将捆绑文件保存到dist
文件夹下的bundle.js
文件中(请记住我们在 HTML 文件中引用了bundle.js
)。
d) 最后,在module
字段中,我们指定要使用 Babel 转译每个脚本,并且排除转译node_modules
。有了这个 webpack 配置文件,您就可以准备转译和捆绑您的源代码了。
- 在您的
package.json
文件中,您将添加一个脚本命令,该命令将调用webpack
,如下面的代码块所示:
{
...
"scripts": {
"build": "webpack --mode=production",
"test": "echo \"Error: no test specified\" && exit 1"
},
...
}
- 在您的终端中,运行以下命令:
package.json file, and this, in turn, will ask webpack to bundle your code referencing the config file you created earlier.Following successful compilation, you should have the following output in your terminal:
图 1.2 – webpack 捆绑成功的输出
在完成上述步骤后,您可以导航到dist
文件夹,在那里您将找到一个额外的文件–bundle.js
。这个文件已经被index.html
文件引用,因此每当我们在浏览器中加载index.html
文件时,它将被执行。
要测试这一点,打开默认浏览器中的index.html
文件。可以通过导航到目录并双击index.html
文件来完成。
一旦您在浏览器中打开了index.html
文件,您应该打开开发者控制台,在那里您可以找到您的代码输出,就像下面的截图中一样:
图 1.3 – 浏览器控制台中的 index.js 输出
这表明您已成功将现代 JS 代码转译和捆绑成可以在任何浏览器中执行的格式,无论是旧的还是新的。
进一步阅读
捆绑文件可能会很快变得困难和混乱,特别是在项目变得更大时。如果您需要进一步了解如何捆绑文件,您可以参考以下资源:
-
使用 webpack(
webpack.js.org/guides/getting-started/
)入门 -
使用指南(
babeljs.io/docs/en/usage
) for Babel -
如何在 Node 和 Express 中启用 ES6(及更高版本)语法(
www.freecodecamp.org/news/how-to-enable-es6-and-beyond-syntax-with-node-and-express-68d3e11fe1ab/
)
在下一节中,您将学习如何设置测试并在您的 JavaScript 应用程序中进行单元测试。
使用 Mocha 和 Chai 进行单元测试
为您的应用程序代码编写测试非常重要,但在大多数书籍中很少谈到。这就是为什么我们决定添加这一部分关于使用 Mocha 进行单元测试。虽然您可能不会为本书中构建的每个示例应用程序编写冗长的测试,但我们将向您展示您需要了解的基础知识,并且您甚至可以在自己的项目中使用它们。
测试,或自动化测试,用于在开发过程中检查我们的代码是否按预期运行。也就是说,函数的编写者通常会预先知道函数的行为,因此可以测试结果与预期结果是否一致。
it
和describe
,可用于自动编写和运行测试。Mocha 的美妙之处在于它可以在 node 和浏览器环境中运行。Mocha 还支持与各种断言库的集成,如Chai (www.chaijs.com/
),Expect.js (github.com/LearnBoost/expect.js
),Should.js (github.com/shouldjs/should.js
),甚至是 Node.js 的内置assert (nodejs.org/api/assert.html
)模块。在本书中,我们将使用 Chai 断言库,因为它是 Mocha 中最常用的断言库之一。
设置测试环境
在我们开始编写测试之前,我们将建立一个基本的 Node.js 项目。执行以下步骤来实现这一点:
- 在你当前的工作目录中,创建一个名为
unit-testing
的新文件夹:
$ mkdir unit-testing
$ cd unit-testing
- 使用以下命令使用
npm
初始化一个新的 Node.js 项目:
$ npm init -y
- 安装 Mocha 和 Chai 作为开发依赖项:
$ npm install mocha chai --save-dev
- 安装成功后,打开你的
package.json
文件,并将scripts
中的test
命令更改为以下内容:
{
...
"scripts": {
"test": "mocha"
},
...
}
这意味着我们可以通过在终端中运行npm run test
命令来运行测试。
- 创建两个文件夹,
src
和test
。src
文件夹将包含我们的源代码/脚本,而test
文件夹将包含我们代码的相应测试。创建完文件夹后,你的项目树应该如下所示:
├── package-lock.json
├── package.json
├── src
└── test
- 在
src
文件夹中,创建一个名为utils.js
的脚本,并添加以下函数:
exports.addTwoNumbers = function (num1, num2) {
if (typeof num1 == "string" || typeof num2 == "string"){
throw new Error("Cannot add string type to number")
}
return num1 + num2;
};
exports.mean = function (numArray) {
let n = numArray.length;
let sum = 0;
numArray.forEach((num) => {
sum += num;
});
return sum / n;
};
前面的函数执行一些基本的计算。第一个函数将两个数字相加并返回结果,而第二个函数计算数组中数字的平均值。
注意
我们在这里编写的是 ES16 之前的 JavaScript。这是因为我们不打算为这个示例项目设置任何转译器。在使用现代 JavaScript 的项目中,你通常会在测试之前转译源代码。
- 在你的
test
文件夹中,添加一个名为utils.js
的新文件。建议使用这种命名约定,因为不同的文件应该与其对应的源代码同名。在test
文件夹中的utils.js
文件中,添加以下代码:
const chai = require("chai");
const expect = chai.expect;
const utils = require("../src/utils");
describe("Test addition of two numbers", () => {
it("should return 20 for addition of 15 and 5", () => {
expect(utils.addTwoNumbers(15, 5)).equals(20);
});
it("should return -2 for addition of 10 and -12", () => {
expect(utils.addTwoNumbers(10, -12)).equals(-2);
});
it("should throw an error when string data type is passed", () => {
expect(() => utils.addTwoNumbers("One", -12)).to.throw(
Error,
"Cannot add string type to number"
);
});
});
describe("Test mean computation of an array", () => {
it("should return 25 as mean of array [50, 25, 15, 10]", () => {
expect(utils.mean([50, 25, 15, 10])).equals(25);
});
it("should return 2.2 as mean of array [5, 2, 1, 0, 3]", () => {
expect(utils.mean([5, 2, 1, 0, 3])).equals(2.2);
});
});
在上述代码片段的前三行中,我们导入了chai
和expect
,以及包含我们源代码的utils
脚本。
接下来,我们使用 Mocha 的describe
和it
函数来定义我们的测试用例。请注意,我们有两个describe
函数对应于我们源代码中的两个函数。这意味着每个describe
函数将包含测试我们代码不同方面的单元测试。
第一个describe
函数测试addTwoNumber
函数,并包含三个单元测试,其中一个测试了在传递字符串数据类型时是否抛出了正确的错误。第二个describe
函数通过提供不同的值来测试mean
函数。
- 要运行我们的测试,去你的终端并运行以下命令:
package.json file, and outputs a formatted test case report, as shown in the following screenshot:
图 1.4 - Mocha 测试输出显示所有测试通过
通过遵循上述步骤,我们能够编写并运行一些在第一次运行时通过的测试。这在大多数情况下可能不是这样,因为你的测试通常会在通过之前失败很多次,特别是当你有许多不同边界情况的单元测试时。
例如,我们将添加一个新的测试用例,当传递给平均函数的数组不包含任何元素时,期望出现错误。
在测试脚本中,在第二个describe
函数下,添加以下单元测试:
...
it("should throw error on empty array arg", () => {
expect(() => utils.mean([])).to.throw(Error, "Cannot compute mean of empty array")
});
...
再次运行测试,我们将看到以下错误:
图 1.5 - Mocha 测试输出显示一个测试失败
Mocha 提供的错误消息告诉我们,当传递一个空数组时,我们的函数应该抛出一个错误,但目前并没有这样做。为了修复这个错误,我们将转到我们的源代码并更新mean
函数,如下面的代码块所示:
exports.mean = function (numArray) {
if (numArray.length == 0){
throw new Error("Cannot compute mean of empty array")
}
let n = numArray.length;
let sum = 0;
numArray.forEach((num) => {
sum += num;
});
return sum / n;
};
现在,如果我们再次运行测试,我们应该看到它成功通过:
图 1.6 - Mocha 测试输出显示所有测试都通过了
进一步阅读
Mocha 是多才多艺的,并为您可能遇到的几乎所有测试用例和场景提供支持。要了解更多信息,您可以访问官方文档:mochajs.org/
。
Chai, 另一方面,提供了许多断言语句和函数,您可以使用它们来丰富您的测试。您可以在这里了解更多关于这些断言:www.chaijs.com/api/
。
恭喜您完成了本章!这是一个冗长的章节,但所涵盖的概念很重要,因为它们将帮助您构建更好的数据驱动产品,正如您将在未来章节中看到的。
总结
在本章中,我们介绍并讨论了 ECMA 6 中引入的一些现代 JavaScript 语法。我们首先考虑了let
和var
之间的区别,并讨论了为什么let
是初始化变量的首选方法。在此之后,我们讨论了解构、展开运算符、作用域,以及闭包。然后,我们介绍了一些数组、对象和字符串的重要方法。在此之后,我们讨论了箭头函数,包括它们相对于传统函数的优势,然后我们继续讨论了 JavaScript 的 promises 和 async/await。
然后,我们简要介绍了 JavaScript 中的面向对象编程概念和支持,并通过示例展示了如何编写类。我们还学习了如何使用诸如 Babel 和 webpack 之类的工具建立现代 JavaScript 环境,支持转译和捆绑。最后,我们介绍了使用 Mocha 和 Chai 库进行单元测试。
在下一章中,我们将介绍 Dnotebook,这是一个交互式计算环境,可以在 JavaScript 中进行快速和交互式的实验。
第二部分:使用 Danfo.js 和 Dnotebook 进行数据分析和操作
本节向读者介绍了 Danfo.js 和 Dnotebook(JavaScript 中的交互式计算环境)。它还深入研究了 Danfo.js 的内部,检查了数据框架和系列,数据转换和分析,绘图和可视化,以及数据聚合和分组操作。
本节包括以下章节:
-
第二章,Dnotebook - 用于 JavaScript 的交互式计算环境
-
第三章,使用 Danfo.js 入门
-
第四章,数据分析,整理和转换
-
第五章,使用 Plotly.js 进行数据可视化
-
第六章,使用 Danfo.js 进行数据可视化
-
第七章,数据聚合和分组操作
第三章:Dnotebook - 用于 JavaScript 的交互式计算环境
使我们的代码足够表达人类可读,而不仅仅是供机器消费的想法是由 Donald Knuth 开创的,他还写了一本名为《文学编程》的书(www.amazon.com/Literate-Programming-byKnuth-Knuth/dp/B004WKFC4S
)。诸如 Jupyter Notebook 之类的工具同样重视散文和代码,因此程序员和研究人员可以通过代码和文本(包括图像和工作流程)进行广泛表达。
在本章中,您将学习有关Dnotebook的知识 - 用于 JavaScript 的交互式编码环境。您还将学习如何在本地安装 Dnotebook。此外,您还将学习如何在其中编写代码和 Markdown。此外,您还将学习如何保存和导入已保存的笔记本。
本章将涵盖以下主题:
-
Dnotebook 介绍
-
Dnotebook 的设置和安装
-
Dnotebook 中交互式计算的基本概念
-
编写交互式代码
-
使用 Markdown 单元格
-
保存笔记本
技术要求
要成功跟随本章内容,您需要在计算机上安装Node.js和现代浏览器,如 Chrome、Safari、Firefox 或 Opera。
要安装 Node.js,您可以在这里按照官方指南进行:nodejs.org/en/
。
本章的代码可在 GitHub 上克隆,网址为github.com/PacktPublishing/Building-Data-Driven-Applications-with-Danfo.js/tree/main/Chapter02
Dnotebook 介绍
在过去几年的数据科学领域,诸如 Jupyter Notebook 和 JupyterLab 之类的交互式计算环境实际上已经对代码共享产生了巨大影响,这增强了想法的快速迭代。
近年来,数据科学正朝着浏览器端发展,以支持 Web 开发人员等各种用户。这意味着 Python 生态系统中许多成熟的数据科学工具需要在 JavaScript 中进行移植或提供。基于这一推理,我们本书的作者以及 Danfo.js 的创建者决定创建一个专门针对 JavaScript 生态系统的 Jupyter Notebook 的新版本。
正如我们所称呼的,Dnotebook 可以帮助您在 JavaScript 中进行快速和交互式的实验/原型设计。这意味着您可以以交互式和笔记本式的方式编写代码并立即查看结果,就像下面的屏幕截图所示:
图 2.1 - 使用 Dnotebook 进行交互式编码示例
Dnotebook 可以用于许多领域和不同的事物,例如以下内容:
-
数据科学/分析:它可以帮助您使用高效的 JavaScript 包(如Danfo.js、Plotly.js、Vega、Imagecook等)轻松进行交互式数据探索和分析。
-
机器学习:它可以帮助您使用机器学习库(如Tensorflow.js)轻松构建、训练和原型化机器学习模型。
-
交互式学习 JavaScript:它可以帮助您以交互式和可视化的方式学习或教授 JavaScript。这有助于学习和理解。
-
纯粹的实验/原型设计:任何可以用 JavaScript 编写的实验都可以在 Dnotebook 上运行,因此这可以帮助快速实验想法。
现在您已经了解了 Dnotebook 是什么,让我们学习如何在本地设置和使用它。
Dnotebook 的设置和安装
要在本地安装和运行 Dnotebook,您需要确保已安装 Node.js。安装 Node.js 后,您可以通过在终端中运行以下命令轻松安装 Dnotebook:
npm install –g dnotebook
上述命令会全局安装 Dnotebook。这是推荐的安装方式,因为它确保了 Dnotebook 服务器可以从计算机的任何位置启动。
注意
您还可以在不安装 Dnotebook 的情况下在线使用它;请查看 Dnotebook 游乐场(playnotebook.jsdata.org/demo
)。
安装后,您可以通过在终端/命令提示符中运行以下命令来启动服务器:
> dnotebook
此命令将在默认浏览器中打开一个选项卡,端口为 http://localhost:4400,如下截图所示:
图 2.2 – Dnotebook 主页
打开的页面是 Dnotebook 界面的默认页面,从这里您可以开始编写 JavaScript 和 Markdown。
注意
我们目前使用的是Dnotebook 版本 0.1.1,因此,在将来使用本书时,您可能会注意到一些细微的变化,特别是在用户界面方面。
Dnotebook 中交互式计算的基本概念
为了在 Dnotebook 中编写交互式代码/Markdown,您需要了解一些概念,比如单元格和持久性/状态。我们从解释这些概念开始这一部分。
单元格
Dnotebook 中的单元格是一个可以写入代码或文本以便执行的单元块。以下是一个示例截图,显示了代码和 Markdown 单元格:
图 2.3 – Dnotebook 中的空代码和 Markdown 单元格
每个单元格都有编辑按钮,可以用于不同的目的,如下截图所示:
图 2.4 – 每个单元格中可用的操作按钮
现在,让我们了解一下这些按钮的作用:
-
运行:运行按钮可用于执行单元格以显示输出。
-
添加代码:添加代码按钮有两种变体(向上和向下),由箭头方向指定。它们可以用于在当前单元格上方或下方添加代码单元格。
-
添加 Markdown:添加 Markdown 按钮与添加代码按钮类似,有两种变体,可以在当前单元格下方或上方添加 Markdown 单元格。
-
删除:顾名思义,此按钮可用于删除单元格。
有两种类型的单元格,即代码单元格和 Markdown 单元格。
代码单元格
代码单元格是一个可以编写和执行任何 JavaScript 代码的单元格。新笔记本中的第一个单元格始终是代码单元格,我们可以通过经典的 hello world 示例来测试这一点。
在您打开的 Dnotebook 中,写入以下命令并单击运行按钮:
console.log('Hello World!')
注意
悬停在代码单元格上会显示运行按钮。或者,您可以使用 Windows 中的快捷键Ctrl + Enter或 Mac 中的Command + Enter来运行代码单元格。
hello world 的代码和输出应该与下面的截图类似:
图 2.5 – Dnotebook 中的代码单元格和执行输出
接下来,让我们了解 Markdown 单元格。
Markdown 单元格
Markdown 单元格与代码单元格类似,不同之处在于它们只能执行 Markdown 或文本。这意味着 Markdown 文本可以编译任何使用 Markdown 语法编写的文本。
Dnotebook 中的 Markdown 单元格通常是白色的,可以通过单击打开单元格中的文本按钮来打开。文本按钮通常适用于每个单元格,如下截图所示:
图 2.6 – 在 Dnotebook 中打开一个 Markdown 单元格
单击文本按钮会打开一个 Markdown 单元格,如下截图所示:
图 2.7 – 在 Markdown 单元格中编写 Markdown 文本
在这里,您可以编写任何 Markdown 格式的文本,当执行时,结果将被编译为文本并显示在 Markdown 单元格的位置上,如下所示:
图 2.8 – Markdown 单元格的输出
现在,让我们谈谈交互式编程中的另一个重要概念,称为持久性/状态。
持久性/状态
交互式计算中的持久性或状态是环境变量或数据在创建它的单元格之外继续存在(持续)的能力。这意味着在一个单元格中声明/创建的变量可以在另一个单元格中使用,而不管单元格的位置如何。
每个 Dnotebook 实例都运行一个持久状态,而在没有 let
和 const
声明的单元格中声明的变量可供所有单元格使用。
注意
在 Dnotebook 中工作时,我们鼓励您以两种主要方式声明变量。
选项 1 – 没有声明关键字(首选方法):
food_price = 100
clothing_price = 200
total = food_price + clothing_price
选项 2 – 使用 var
全局关键字(这样做可以,但不建议):
var food_price = 100
var clothing_price = 200
var total = food_price + clothing_price
使用 let
或 const
等关键字会使变量在新单元格中无法访问。
为了更好地理解这一点,让我们声明一些变量,并尝试在之后或之前创建的多个单元格中访问它们:
- 在您的打开笔记本中创建一个新单元格,并添加以下代码:
num1 = 20
num2 = 35
sum = num1 + num2
console.log(sum)
//output 55
运行此代码单元格,您将看到总和打印在单元格下方,如下面的截图所示:
图 2.9 – 简单的代码来相加两个数字
- 接下来,通过单击代码单元格按钮,在第一个单元格后创建一个新单元格,并尝试使用
sum
变量,如下面的代码块所示:
newSum = sum + 30
console.log(newSum)
//outputs 85
通过执行前面的单元格,您将得到 85
的输出。这意味着第一个单元格中的变量 sum 也会持续到第二个单元格以及您将创建的任何其他单元格,如下面的截图所示:
图 2.10 – 两个共享持久状态的代码单元
注意
Markdown 单元格不会保留变量,因为它们不执行 JavaScript 代码。
现在您了解了单元格和持久性是什么,您现在可以在 Dnotebook 中轻松编写交互式代码,在下一节中,我们将向您展示如何做到这一点。
编写交互式代码
在本节中,我们将强调在 Dnotebook 中编写交互式代码时需要了解的一些重要事项。
加载外部包
在编写 JavaScript 时,将外部包导入笔记本非常重要,因此 Dnotebook 具有一个名为 load_package
的内置函数来执行此操作。
load_package
方法可以帮助您通过它们的 CDN 链接轻松地将外部包/库添加到您的笔记本中。例如,要加载 Tensorflow.js
和 Plotly.js
,您可以将它们的 CDN 链接传递给 load_package
函数,如下面的代码所示:
load_package(["https://cdn.jsdelivr.net/npm/@tensorflow/[email protected]/dist/tf.min.js","https://cdn.plot.ly/plotly-latest.min.js"])
这将加载包并将它们添加到笔记本状态中,以便可以从任何单元格中访问它们。在下一节中,我们将使用刚刚导入的 Plotly
库。
将以下代码添加到笔记本中的新单元格中:
trace1 = {
x: [1, 2, 3, 4],
y: [10, 11, 12, 13],
mode: 'markers',
marker: {
size: [40, 60, 80, 100]
}
};
data = [trace1];
layout = {
title: 'Marker Size',
showlegend: false,
height: 600,
width: 600
};
Plotly.newPlot(this_div(), data, layout); //this_div is a built-in function that returns the current output's div name.
执行前面部分的代码单元格将显示一个图表,如下面的截图所示:
图 2.11 – 使用外部包制作图表
因此,通过使用 load_package
,您可以添加任何您选择的外部 JavaScript 包,并在 Dnotebook 中进行交互操作。
加载 CSV 文件
将数据导入笔记本,特别是导入到数据框中,非常重要。因此,我们在这里介绍的另一个内置函数是 load_csv
。
注意
数据框以行和列的形式表示数据。它们类似于电子表格或数据库中的行和列集合。我们将在 第三章 中深入介绍数据框和系列,使用 Danfo.js 入门。
load_csv
函数帮助您异步将 CSV 文件加载到Danfo.js
DataFrame 中。当读取大文件并且想要跟踪进度时,您应该使用这个函数,而不是 Danfo 的内置read_csv
函数。这是因为load_csv
会在导航栏上显示一个旋转器来指示进度。
让我们通过一个例子更好地理解这一点。在一个新的代码单元格中,添加以下代码:
load_csv("https://raw.githubusercontent.com/plotly/datasets/master/finance-charts-apple.csv")
.then((data)=>{
df = data
})
执行单元格后,如果您查看右上角,您会注意到一个旋转器,指示数据加载的进度。
执行单元格后,您可以像处理 Danfo DataFrame 一样与数据集交互。例如,您可以使用另一个内置函数table
来轻松地以表格格式显示数据。
在一个新的单元格中,添加以下代码:
table(df)
执行时,您应该会看到数据表,如下截图所示:
图 2.12–加载和显示 CSV 文件
接下来,我们将简要介绍另一个内置函数,它有助于在笔记本中显示图表。
获取绘图的 div 容器
为了显示图表,大多数绘图库都需要某种容器或 HTML div
。这是使用 Danfo.js 和 Plotly.js 库进行绘图所必需的。为了更容易地访问输出div
,Dnotebook 内置了this_div
函数。
this_div
函数将返回当前代码单元格输出的 HTML ID。例如,在以下代码中,我们将this_div
的值传递给 DataFrame 的plot
方法:
const df = new dfd.DataFrame({col1: [1,2,3,4], col2: [2,4,6,8]})
df.plot(this_div()).line({x: "col1", y: "col2"})
这将当前单元格的div
ID 传递给 DataFrame 的plot
方法,并在执行时显示生成的图表,如下截图所示:
图 2.13–绘制 DataFrame
](https://gitee.com/OpenDocCN/freelearn-js-zh/raw/master/docs/bd-dtdvn-app-danfo/img/B17076_02_13.jpg)
图 2.13–绘制 DataFrame
最后,在下一节中,我们将简要讨论在for
循环中打印值的问题。这不会按预期工作,我们将解释原因。
在使用 for 循环时要注意的事项
当您编写for
循环并尝试在 Dnotebook 代码单元格中打印每个元素时,您只会得到最后一个元素。这个问题与浏览器中控制台的工作方式有关。例如,尝试执行以下代码并观察输出:
for(let i=0; i<10; i++){
console.log(i)
}
//outputs 9
如果您想要在运行for
循环时看到所有输出,特别是在 Dnotebook 中进行调试,您可以使用 Dnotebook 内置的forlog
方法。这个方法已经附加到默认的控制台对象上,并且可以像以下代码块中所示那样使用:
for(let i=0; i<10; i++){
console.forlog(i)
}
执行前面的代码单元格将返回所有值,如下截图所示:
图 2.14–比较 for 和 forlog 方法
您会注意到,当使用console.forlog
方法时,每个输出都会打印在新的一行上,就像在脚本环境中console.log
的默认行为一样。
在本节中,我们介绍了一些在 Dnotebook 环境中编写交互式代码时会有用的重要函数和特性。在下一节中,我们将看一下如何使用 Markdown 单元格。
使用 Markdown 单元格
Dnotebook 支持 Markdown,这使得您可以将代码与文本和多媒体混合使用,从而使得那些可以访问笔记本的人更容易理解。
Markdown 是一种使用纯文本编辑器创建格式化文本的标记语言。它广泛用于博客、文档页面和 README 文件。如果您使用 GitHub 等工具,那么您可能已经使用过 Markdown。
与许多其他工具一样,Dnotebook 支持所有 Markdown 语法、图像导入、添加链接等。
在接下来的几节中,我们将看到在 Dnotebook 中使用 Markdown 时可以利用的一些重要功能。
创建一个 Markdown 单元格
为了在 Dnotebook 环境中编写 Markdown,您需要通过单击文本按钮(向上或向下)添加一个 Markdown 单元格。此操作会向您的笔记本添加一个新的 Markdown 单元格。以下屏幕截图显示了在 Markdown 单元格中编写的示例文本:
图 2.15–在 Markdown 单元格中编写简单文本
在 Markdown 单元格中编写 Markdown 文本后,您可以单击运行按钮来执行它。这将用读取模式中的转译文本替换单元格。双击文本会再次显示 Markdown 单元格以进行编辑。
添加图像
要将图像添加到 Markdown 单元格中,您可以使用以下代码中显示的图像语法:
![alt Text](https://gitee.com/OpenDocCN/freelearn-js-zh/raw/master/docs/bd-dtdvn-app-danfo/img/links to the image)
以下是输出:
图 2.16–添加图像
例如,在前面的屏幕截图中,我们添加了一个指向互联网上可用图像的链接。代码如下所示:
![](https://tinyurl.com/yygzqzrq)
提供的链接是指向狗图像的链接。需要单击运行按钮以查看图像的结果,如下所示:
图 2.17–Markdown 图像结果
在接下来的部分中,您将学习一些基本的 Markdown 语法,您也可以将其添加到您的笔记本中。要查看全面的指南,您可以访问网站www.markdownguide.org/basic-syntax/
。
标题
要创建标题,您只需在单词或短语前面添加井号符号(#)
:
# First Heading
## Second Heading
### Third Heading
如果我们将前面的文本粘贴到 Markdown 中并单击运行按钮,我们将得到以下输出:
图 2.18–添加标题文本
在结果中,您会注意到在文本前面有不同数量的井号会导致不同的大小。
列表
列表对于枚举对象很重要,可以通过在文本前加上星号符号(*****)来添加。我们在以下部分提供了一个示例:
* Food
* Cat
* kitten
* Dog
前面的示例创建了一个无序列表,其中包括食物、猫和狗,小猫作为猫的子列表。
为了创建一个编号列表,只需在文本前面添加数字,如下所示:
-
第一项
-
第二项
-
更多
在 Markdown 输入字段中输入前面的文本应该输出以下内容:
图 2.19–列表
在接下来的部分中,我们将介绍 Dnotebook 的一个重要部分–保存。这对于重用和与其他人共享您的笔记本非常重要。
保存笔记本
Dnotebook 支持保存和导入已保存的笔记本。保存和导入笔记本可以让您/其他人重用您的笔记本。
要保存和导入笔记本,请单击文件菜单,然后根据您想要执行的操作选择下载笔记本或上传笔记本按钮。选项显示在以下屏幕截图中:
图 2.20–保存和导入笔记本
单击下载笔记本会以 JSON 格式保存笔记本,这可以很容易地共享或重新加载。
保存和导入
要测试此功能,请转到playnotebook.jsdata.org/demo
。尝试保存演示笔记本。然后打开一个新笔记本,playnotebook.jsdata.org
,并导入保存的文件。
总结
在本章中,我们介绍了 Dnotebook,这是一个支持文本和多媒体的交互式库。首先,我们介绍了在本地安装 Dnotebook,并指出您可以免费在线运行部署版本。接下来,我们介绍了一些基本概念和在处理代码和 Markdown 时需要注意的事项,最后,我们向您展示了如何保存笔记本以供共享和重用。
在下一章中,我们将开始使用 Danfo.js,并介绍这个令人惊叹的库的一些基本概念。
第三章:开始使用 Danfo.js
与其他编程语言相比,Python数据科学和机器学习生态系统非常成熟,但在数据呈现和客户端方面,JavaScript占据主导地位。从其强大的数据呈现工具到其在浏览器中的易用性,JavaScript 总是被推荐的。
在本章中,我们将向您介绍 Danfo.js 库,为您提供 JavaScript 中高效的数据分析和操作工具。我们将介绍 Danfo.js 的核心数据结构——DataFrames 和 Series。我们将向您展示如何将不同类型的文件(如 JSON、Excel 和 CSV)加载到 Danfo.js 中,最后,您将了解一些在 JavaScript 中使数据分析和预处理更容易的重要函数。
在本章中,我们将涵盖以下主题:
-
为什么你需要 Danfo.js
-
安装 Danfo.js
-
介绍 Series 和 DataFrames
-
Danfo.js 中的重要函数和方法
-
数据加载和处理不同的文件格式
技术要求
为了跟随本章,您应该具备以下条件:
-
现代浏览器,如 Chrome、Safari、Opera 或 Firefox
-
在您的系统上安装了 Node.js、Danfo.js 和 Dnotebook
本章的代码在这里可用:github.com/PacktPublishing/Building-Data-Driven-Applications-with-Danfo.js/tree/main/Chapter03
。
注意
对于大多数代码片段,您可以使用在线提供的 Dnotebook:playnotebook.jsdata.org/
。
为什么你需要 Danfo.js
要成功地将用 Python 编写的机器学习项目带到 Web 上,需要执行许多过程和任务,例如模型部署、使用 Flask、FastAPI 或 Django 等框架创建 API 路由,然后使用 JavaScript 向模型发送 HTTP 请求。您可以清楚地观察到该过程涉及大量 JavaScript。如果我们能够仅使用 JavaScript 执行所有这些过程,那将是非常棒的,不是吗?好消息是我们可以。
在过去的几年里,浏览器的计算能力稳步增加,并且可以支持高强度的计算,因此在处理数据密集型任务时,JavaScript 具有挑战 Python 的优势。
借助 Node.js,JavaScript 可以访问本地计算机上可用的 GPU,使我们能够使用 Node.js 进行全栈机器学习项目,并使用纯 JavaScript 进行前端开发。
使用 JavaScript 的好处之一是可以轻松地在客户端进行推断,因此数据不必离开客户端到服务器。此外,JavaScript 使我们的程序具有跨平台支持,并且借助内容传送网络(CDN),所有用于应用程序开发的 JavaScript 包都可以轻松使用,而无需安装它们。
随着 TensorFlow.js 和 Danfo.js 等工具的引入,我们看到 JavaScript 在数据科学和机器学习生态系统中得到了更多支持。
想象一下在 Web 上拥有强大的数据处理库,比如 Python pandas(pandas.pydata.org/
),并考虑将这样的工具融入 JavaScript 中流行的现代框架,如 Vue 或 React;可能性是无限的。这就是 Danfo.js 带给 Web 的力量。
JavaScript 开发人员已经做出了各种尝试,将 pandas 的数据处理能力带到 Web 上。因此,我们有诸如pandas-js
、dataframe-js
、data-forge
和jsdataframe
等库。
将 Python pandas 移植到 JavaScript 的需求主要是因为需要在浏览器中执行数据预处理和操作任务。
这是一个关于 Stack Overflow 的讨论串 (stackoverflow.com/questions/30610675/python-pandas-equivalent-in-JavaScript/43825646
),其中详细介绍了 JavaScript 中用于数据预处理和操作的不同工具,但大多数这些工具因为以下两个原因而失败,如串中所述:
-
缺乏将多个库合并为单个库的协作(许多库试图执行不同的任务)
-
大多数库都没有 pandas 的主要功能,如:
github.com/pandas-dev/pandas#main-features
除了这里列出的两个原因,我们在尝试大多数现有工具时观察到的另一个问题是缺乏像 Python 的 pandas 那样良好的用户体验。pandas 非常受欢迎,由于大多数使用 JavaScript 创建的工具都模仿 pandas,因此最好拥有与 pandas 相当相似的用户体验。
Danfo.js 的构建是为了弥补现有数据处理库所面临的差距和问题。API 经过精心设计,模仿了 pandas API,并且为从 Python 背景而来的人提供了类似的体验,同时也为 JavaScript 开发人员提供了简单直观的 API。
除了简单且熟悉的 API 外,由于 TensorFlow.js 的支持,Danfo.js 更快。Danfo.js 在 JavaScript 数据科学社区中备受欢迎,在不到一年的时间里在 GitHub 上获得了超过 1500 颗星,同时也得到了 Google 的 TensorFlow.js 团队的认可 (blog.tensorflow.org/2020/08/introducing-danfo-js-pandas-like-library-in-JavaScript.html
)。另外,值得一提的是 Danfo.js 得到了积极贡献者的大力维护和不断发展,他们确保其始终保持最新和稳定。
除了前述原因,我们决定撰写这本关于 Danfo.js 的书籍还有许多其他原因,以便为您提供在 JavaScript 中执行数据操作任务所需的技能和知识。在下一节中,我们将首先学习如何在浏览器中安装和导入 Danfo.js,同时也在 Node.js 环境中进行。
安装 Danfo.js
Danfo.js 在浏览器和 Node.js 环境中都可以轻松获取。要在浏览器中使用 Danfo.js,可以将 script
标签添加到 HTML
文件的头部,如下所示:
<head>
...
<script src="img/bundle.min.js"></script>
...
</head>
在撰写本文时,浏览器环境中 Danfo.js 的最新版本为 0.2.7。这很可能已经改变,但请放心,本书中使用的所有代码和片段将在未来版本中起作用。
注意
要安装或获取 Danfo.js 的最新版本,可以在官方文档的发布页面 danfo.jsdata.org/release-notes
中查看。
在 Node.js 环境中,可以使用 yarn
安装 Danfo.js,如下所示:
//NPM
npm install danfojs-node
// Yarn
yarn add danfojs-node
安装完成后,可以使用以下任一命令导入 Danfo.js:
//Common js style
const dfd = require("danfojs-node")
// ES6 style
import * as dfd from 'danfojs-node'
注意
为了练习,您可以始终使用 playnotebook.jsdata.org/
,如前一章所述。有了这个,您可以在线访问 Dnotebook 进行快速实验。要在 Dnotebook 中使用 Danfo.js 的最新版本,您可以始终使用 load_package
函数。
Danfo.js 的浏览器和 Node.js 版本遵循相同的 API 设计。主要区别在于在浏览器中,Danfo.js 被添加到全局范围下的名称 dfd
。因此,dfd
变量可用于所有 JavaScript 或 HTML 文件。
现在我们安装完成后,我们将继续下一节,讨论 Danfo.js 和 Series 中提供的主要数据结构和方法。
介绍 Series 和 DataFrames
Danfo.js 公开了两种主要数据结构,Series 和 DataFrames,可以轻松进行所有数据操作。DataFrames 和 Series 为不同类型的数据提供了通用表示,因此可以将相同的数据处理过程应用于具有不同格式的数据集。
在本节中,我们将看到创建 Series 和 DataFrames 的不同方法。我们将了解如何处理 DataFrame 和 Series 格式的数据。我们还将研究不同的 DataFrame 和 Series 方法来处理数据。
我们将从处理 Series 数据结构的数据开始本节。
Series
Series 提供了处理一维数据的入口,例如具有相同数据类型的一系列值的单个数组。
在本节中,我们将熟悉以下 Series 方法的 Series 数据结构:
* table() and print() method:let sdata= new dfd.Series([1,3,5,7,9,11])
table( sdata) // Displays the table in Dnotebook
上述代码给出了以下输出表:
图 3.1 – Series 表
在上述代码片段中,我们使用 table()
来打印 Series 表。这是因为我们在本章的大部分代码中都在使用 Dnotebook。要在浏览器或 Node.js 控制台中打印 DataFrames 或 Series,可以始终调用 print()
如下:
sdata.print() // will give us same output as table( sdata)
在随后的代码中,我们将始终使用 table()
函数,这允许我们在浏览器网页上打印 DataFrame。
.index
属性: 默认情况下,Series 表的列名始终为 0,除非另有指定。还要注意,Series 表的索引范围为数据的0
到n – 1
,其中n
是数据的长度。索引也可以通过以下方式获取:
console.log(sdata.index) // [0,1,2,3,4,5]
.dtype
和astype()
: 有时传入 Series 的数据可能不是同质的,并且可能包含混合数据类型的字段,如下所示:
let sdata = new dfd.Series([1,2,3, "two:","three"])
console.log(sdata.dtype)
// string
dtype
输出了 string
,而数据是不同的数据类型,因此我们可以始终像以下代码一样更改 dtype
:
sdata.astype('int32')
上述代码更改了 Series 的数据类型,但实际上并没有将数据中的字符串转换为 int32
。因此,如果对数据执行数值操作,将会引发错误。
.tensor
属性: Series 中的数据可以以两种主要格式获取 – 作为 JavaScriptseries.tensor
是一个有效的 TensorFlow.js 张量,因此可以对其执行所有支持的张量操作。例如,在以下代码中,我们对 Seriestensor
对象调用指数函数:
sdata.tensor.exp(2).arraySync()
// [3,7,20,55,148]
在上述代码中,我们能够打印出 Series 数据的指数,因为我们可以访问底层张量。arraySync
方法返回张量的数组格式。让我们来看一下 set_index()
方法。
set_index()
方法: 在创建 Series 数据结构时可以指定索引。我们在以下代码中演示了这一点:
series = new dfd.Series([1,2,3,4,5], {index: ["one","two", "three", "four", "five"]})
table(series)
这将设置 Series 的索引并替换默认的数值索引,如下所示:
图 3.2 – 具有命名索引的 Series
我们可以始终更改 Series 的默认索引值,如以下代码所示:
sdata = new dfd.Series(["Humans","Life","Meaning","Fact","Truth"])
new_series = sdata.set_index({ "index": ["H", "L", "M","F","T"] })
table(new_series)
sdata.set_index()
重置索引并返回一个新的 Series,如 图 3.2 (左) 所示,而不会改变原始 Series。我们可以将 set_index()
设置为实际更新原始 Series 的索引并且不返回新的 Series,方法是将 inplace
键设置为 true
:
sdata.set_index({ index: ["H", "L", "M","F","T"] , inplace: true })
现在我们来看一下 .apply()
方法。
.apply()
方法: 由于 Series 是单维数组,因此可以轻松应用类似于在 JavaScript 中处理数组的操作。Series 有一个名为.apply
的方法,可以在 Series 数据的每个值上应用任何函数:
sdata = new dfd.Series(["Humans","Life","Meaning","Fact","Truth"])
series_new = sdata.apply((x) => {
return x.toLocaleUpperCase()
})
table(series_new)
这通过在每个值上应用 x.toLocaleUpperCase()
将 Series 中的每个字符串转换为大写。下图显示了在应用 x.toLocaleUpperCase
之前和之后的表输出:
图 3.3 - 左:将索引设置为字符串。右:使用 apply 将所有字符串转换为大写
.apply
方法也非常适用于对 Series 数据的每个值执行数值操作:
sf = new dfd.Series([1, 2, 3, 4, 5, 6, 7, 8])
sf_new = sf.apply(Math.log)
table(sf_new)
我们使用.apply
函数找到了 Series 中每个值的对数。对于所有其他数学运算也是一样的。
上述代码输出以下表格:
图 3.4 - 对 Series 的每个值应用数学运算(Math.log)
Series 数据结构还包含.map
方法,类似于.apply
。该方法将 Series 的每个值映射到另一个值:
sf = new dfd.Series([1,2,3,4])
map = { 1: "ok", 2: "okie", 3: "frit", 4: "gop" }
sf_new = sf.map(map)
table(sf_new)
map 接受对象和函数作为参数。在上述代码中,我们创建了一个名为map
的对象,其中包含 Series 中的值作为键,以及每个键应映射到的值。
上述代码输出以下表格:
图 3.5 - 在 Series 值上使用 map
接下来,我们将看一下.isna()
方法。
.isna()
方法:到目前为止,我们传递到 Series 中的数据似乎都没有问题,不包含缺失值或未定义值,但在处理真实数据时,我们总会有NaN
或未定义的值。我们可以随时检查我们的 Series 中是否存在这样的值:
sf_nan = new dfd.Series([1,2, NaN, 20, undefined, 100])
table(sf_nan)
作为测试,我们创建了一个包含NaN
和一个未定义变量的 Series。我们得到以下输出:
图 3.6 - 包含 NaN 值的 Series 框架的表
让我们检查包含NaN
和未定义值的行;对于这些行中的每一行,我们输出true
,如果没有,则显示为false
,如下面的代码所示:
table(sf_nan.isna())
我们得到以下输出:
图 3.7 - Series 数据的 isna 表
.add()
方法如下。
.add()
方法:Series 数据结构还支持 Series 之间的逐元素操作(如加法、减法、乘法和除法),在此操作之后,索引会自动对齐:
sf1 = new dfd.Series([2,20,23,10,40,5])
sf2 = new dfd.Series([30,20,40,10,2,3])
sf_add = sf1.add(sf2)
table(sf_add)
我们有以下输出:
图 3.8 - 添加两个 Series
](https://gitee.com/OpenDocCN/freelearn-js-zh/raw/master/docs/bd-dtdvn-app-danfo/img/B17076_3_8.jpg)
图 3.8 - 添加两个 Series
上述代码片段将sf2
Series 添加到sf1
,并且每个值都是按元素逐行添加的。
还要注意,可以将单个值传递给sf1.add
,这将向sf1
Series 中的每个元素添加一个常量值:
table(sf1.add(10))
以下是上述代码的输出:
图 3.9 - 向 Series 元素添加常量值
先前显示的操作也适用于所有其他数学运算以及 Series。
在下一小节中,我们将研究数据框的操作,如何创建数据框以及如何使用数据框操作处理数据。
数据框
数据框表示一组 Series,尽管与 Series 不同,它基本上是数据的二维表示(尽管它也可以表示更高的维度),包含多列和多行。
数据框基本上可以使用 JavaScript 对象构建,如下面的代码所示:
data = {
artist: ['Drake','Rihanna','Gambino', 'Bellion', 'Paasenger'],
rating: [5, 4.5, 4.0, 5, 5],
dolar: [ '$1m', '$2m','$0.1m', '$2.5m','$3m']
}
df = new dfd. DataFrame (data)
table(df)
// print out the DataFrame
生成的数据框应如下所示:
图 3.10 - 数据框表
在上述代码中,创建了包含键和值的 JavaScript 对象数据。请注意 JavaScript 对象的以下内容:
-
每个键包含一个列表值,表示每列的数据。
-
每个键代表列和列名。
-
每个键必须具有相同长度的列表值。
以下是处理数据框数据结构的常见方法:
head()
和tail()
方法:有时,在处理大型数据集时,您可能需要查看数据的前几行(可能是前 5 行或前 30 行,只要您喜欢的数量),以及数据的最后几行。
由于我们的 DataFrame 包含五行,让我们打印表的前两行:
table(df.head(2))
head()
方法默认打印 DataFrame 的前五行,但我们可以指定2
,因为我们的数据很小。
以下表显示了前述代码的结果:
图 3.11 - 用于 df.head(2)的表
此外,要查看 DataFrame 中的最后一组行,我们使用tail()
方法。默认情况下,tail
方法会打印出 DataFrame 中的最后五行:
table(df.tail(2))
tail
函数接受一个值,该值指定要从 DataFrame 表末尾打印的行数,如下所示:
图 3.12 - 打印最后两行
data
变量包含数据,表示为数组的数组。数据数组中的每个数组按行排列的值将在 DataFrame 表中表示为[Drake, 5, $1m]
表示 DataFrame 中的一行,而每个值(Drake
,5
,$1m
)表示不同的列。Drake
属于artist
列,$1m
属于dollar
列,如下图所示:
图 3.13 - DataFrame 表
回想一下,我们说过 DataFrame 是 Series 的集合,因此可以像访问 JavaScript 对象元素的方式一样从 DataFrame 中访问这些 Series。下面是如何做到这一点的:
table(df.artist)
请注意,artist
是 DataFrame 中的列名,因此如果我们将 DataFrame 视为对象,则artist
是键,键的值是 Series。前述代码应输出以下表:
图 3.14 - 列系列
artist
列也可以通过df['artist']
访问:
table(df['dollar'])
此方法也输出与使用初始df.dollar
方法相同的结果:
图 3.15 - 访问列值
除了通过类似对象的方法访问列值之外,我们还可以按以下方式更改列数据:
df['dollar'] = ['$20m','$10m', '$40m', '$35m', '$10m']
如果我们要打印df.dollar
,我们将获得以下输出:
图 3.16 - 更改列值
也可以通过创建一个 Series 框架,然后将其分配给 DataFrame 中的列来更改列数据,如下所示:
rating = new dfd.Series([4.9,5.0, 7.0, 3.0,2.0])
df['rating'] = rating
table(df['rating'])
前述代码更改了列评分的值,并输出了以下表:
图 3.17 - 使用 Series 值更改列数据
在处理与时间相关的列时,使用 Series 数据来更新列数据的方法非常方便。我们可能需要提取日期列中的小时或月份,并用提取的信息(小时或月份)替换列。
让我们向 DataFrame 添加一个日期列:
date = [ "06-30-02019", "07-29-2019", "08-28-2019", "09-12-2019","12-03-2019" ]
df.addColumn({column: "date", value: date})
我们使用addColumn
方法向 DataFrame 添加新列。addColumn
方法接受一个必须包含以下键的对象参数:
a) 要添加的“列”的名称
b) 列的新“值”
前述代码应输出以下表:
图 3.18 - 添加新列
创建的新列是一个日期时间列,因此我们可以将此列转换为时间序列数据结构,然后提取列中每个数据点的月份名称:
date_series = df['date']
df['date'] = date_series.dt.month_name()
我们提取日期列并将其分配给date_series
变量。从date_series
(实际上是一个 Series 数据结构)中,我们提取每个值的月份名称。Series 数据结构包含dt
方法,它将 Series 结构转换为时间序列结构,然后使用month_name()
方法提取月份名称。
上述代码输出了以下表格:
图 3.19 – 提取月份名称
我们将要查看的下一个方法是.values
和.tensor
。
.values
和.tensor
:DataFrame 的值,即每个列值,可以使用df.values
作为一个巨大的数组获得:
df.values
//output
[[Drake,4.9,$20m,Jun],[Rihanna,5,$10m,Jul],[Gambino,7,$40m,Aug],[Bellion,3,$35m,Sep],[Passenger,2,$10m,Dec]]
有时,我们可能希望将 DataFrame 作为张量获得用于机器学习操作,因此以下显示了如何将 DataFrame 作为张量值获得:
df.tensor
//output
{"kept":false,"isDisposedInternal":false,"shape":[5,4],"dtype":"string","size":20,"strides":[4],"dataId":{},"id":28,"rankType":"2"}
tensor
属性返回了一个 TensorFlow.js 张量数据结构。由于张量必须始终包含唯一的数据类型,因此我们 DataFrame 中的所有数据点都转换为dtype
字符串。
.transpose()
方法:由于我们声称 DataFrame 是数据的二维表示,因此我们应该能够对 DataFrame 进行转置:
df.transpose()
df.transpose()
将列列表作为索引,将索引作为列,如下表所示:
图 3.20 – 转置 DataFrame
我们可以获取 DataFrame 的索引并查看列是否现在是当前索引:
df2 = df.transpose()
console.log(df2.index)
// [artist,rating,dollar,date]
上述代码是转置 DataFrame 的索引。在前面的示例中,索引始终是数值,但是通过这个示例,可以看到索引也可以是字符串。
.set_index()
方法:使用 DataFrame 本身,在对其进行转置之前,让我们将 DataFrame 的索引从整数更改为字符串索引:
df3 = df.set_index({key:["a","b","c","d","e"]})
使用 DataFrame 中的set_index
方法使用给定的键值设置索引。set_index
还接受inplace
键(inplace
键是一个布尔键,它接收true
或false
作为值,但默认值为false
)来指定 DataFrame 是否应该更新或返回包含更新的新 DataFrame。
以下表格显示了上述代码的结果:
图 3.21 – 设置 DataFrame 的索引
由于索引可以是任何内容,因此我们也可以将 DataFrame 的索引设置为 DataFrame 中的一列:
df4 = df.set_index({key:"artist"})
这一次,set_index
接受一个值而不是一个提议的索引值数组。如果将单个值作为键索引传递,set_index
认为它应该作为 DataFrame 中的列可用。
artist
列中的值用于替换默认索引。还要注意inplace
的默认值为false
,因此会创建并返回一个包含更新索引的新 DataFrame。
以下显示了创建的 DataFrame 的输出:
图 3.22 – 将索引设置为艺术家列
到目前为止,您应该已经了解如何创建 Series 和 DataFrame 以及如何利用它们各自的方法进行数据处理。
在下一节中,我们将研究一些在 Danfo.js 中常用于数据分析和处理的基本函数和方法。
Danfo.js 中的基本函数和方法
在本节中,我们将介绍与 Series 和 DataFrames 相关的一些重要函数和方法。每个数据结构中的方法都很多,我们可以随时查看文档以获取更多方法。本节将只提到一些最常用的方法:
-
loc
和iloc
索引 -
排序:
sort_values
和sort_index
方法 -
过滤器
-
算术运算,如
add
,sub
,div
,mul
和cumsum
loc 和 iloc 索引
使用loc
和iloc
方法更容易地访问 DataFrame 的行和列;这两种方法都允许您指定要访问的行和列。对于那些来自 Python 的 pandas 库的人来说,Danfo.js 中实现的loc
和iloc
格式应该是熟悉的。
使用loc
方法访问具有非数字索引的 DataFrame,如下所示:
df_index = df4.loc({rows:['Bellion','Rihanna','Drake']})
利用上一节中更新的 DataFrame,我们使用loc
方法从 DataFrame 中获取特定的行索引。这个索引值在rows
关键元素中指定:
图 3.23-访问特定的行索引值
此外,loc
和iloc
方法使我们能够对数组进行切片。这对于 Python 用户来说应该是熟悉的。如果你不知道它是什么,不要担心;下面的例子将展示它的含义和如何做到这一点。
为了获得一系列的索引,我们可以指定非数字索引的上限和下限:
df_index = df4.loc({rows:["Rihanna:Bellion"]})
当 DataFrame 的列名和索引是字符串时,使用loc
方法。对于上述代码,我们指定要提取Rihanna
行索引和Bellion
行索引之间的数据值。
这个冒号有助于指定范围边界。以下表格显示了上述代码的输出:
图 3.24-字符串索引
除了对行进行索引,我们还可以提取特定的列:
df_index = df4.loc({rows:["Rihanna:Bellion"], columns:["rating"]})
将columns
键传递到loc
中以提取名为rating
的列。
以下表格显示了上述代码的结果:
图 3.25-列和行索引
我们还可以指定索引列的范围,如下所示:
df_loc= df4.loc({rows:["Rihanna:Bellion"], columns:["rating:date"]})
这应该返回一个类似于以下的表:
图 3.26-列范围
从上表中,我们可以看到范围与行索引相比是完全不同的;列索引排除了上限。
DataFrame 中的loc
方法使用字符来索引行数据和列数据。iloc
也是一样,但是使用整数来索引行和列。
使用如下表格中显示的初始 DataFrame,我们将使用iloc
方法进行索引:
图 3.27-艺术家表
让我们使用iloc
来访问图 3.27中显示的表的索引2
到4
:
t_df = df.iloc({rows:['2:4']})
table(t_df)
iloc
方法接受与loc
方法相同的关键字:rows
和columns
关键字。数字被包装在字符串中,因为我们想执行loc
方法。
以下表格显示了上述代码的结果:
图 3.28-使用封闭上限从 2 到 4 索引表行
切片数组(花式索引)
对于那些来自 Python 的人来说,切片数组将是熟悉的。在 Python 中,可以使用一系列值对数组进行索引,例如,test = [1,2,3,4,5,6,7] test [2:5] => [3,4,5]
。JavaScript 没有这个属性,因此 Danfo.js 通过将范围作为字符串传递,并从字符串中提取上限和下限来实现这一点。
与在前面的例子中对行进行的相同的切片操作也可以对列进行:
column_df = df.iloc({columns:['1:']})
table(column_df)
df.iloc({columns:['1:']})
用于提取从索引 1(表示rating
列)到最后一列的列。对于整数索引,当未指定上限时,它会选择最后一列作为上限。
在本节中,我们讨论了对 DataFrame 进行索引和不同的索引方法。在下一节中,我们将讨论如何按列值和行索引对 DataFrame 进行排序。
排序
Danfo.js 支持两种排序数据的方法 - 按索引或按列值。此外,Series 数据只能按索引排序,因为它是一个只有单列的 DataFrame 对象。DataFrame 有一个名为sort_values
的方法,它使您能够按特定列对数据进行排序,因此通过对特定列进行排序,我们正在对其他所有列进行排序。
让我们创建一个包含数字的 DataFrame:
data = {"A": [-20, 30, 47.3],
"B": [ -4, 5, 6],
"C": [ 2, 3, 30]}
df = new dfd. DataFrame (data)
table(df)
这将输出以下表格:
图 3.29 – 数字 DataFrame
现在,让我们按列B
中的值对 DataFrame 进行排序:
df.sort_values({by: "C", inplace: true, ascending: false})
table(df)
sort_values
方法接受以下关键字参数:
-
by
:用于对 DataFrame 进行排序的列的名称。 -
inplace
:是否应更新原始 DataFrame(true
或false
)。 -
ascending
:列是否应按升序排序。默认值始终为false
。
在上面的代码片段中,我们指定按列C
对 DataFrame 进行排序,并将inplace
设置为true
,因此 DataFrame 被更新,并且列也按降序排序。以下表格显示了输出:
图 3.30 – 按列值排序的 DataFrame
有时我们可能不想更新原始 DataFrame 本身。这就是inplace
关键字的影响所在:
sort_df = df.sort_values({by: "C", inplace:false, ascending: false})
table(sort_df)
在上面的代码中,DataFrame 按列排序。让我们尝试按其索引对相同的 DataFrame 进行排序:
index_df = sort_df.sort_index({ascending:true})
table(index_df)
sort_index
还接受关键字参数:ascending
和inplace
。我们将ascending
关键字设置为true
,这应该会给我们一个按升序排列的 DataFrame。
以下是获得的输出:
图 3.31 – 排序索引 DataFrame
在 Series 中进行排序需要一个非常简单的方法;通过调用sort_values
方法来进行排序,如下所示:
data1 = [20, 30, 1, 2, 4, 57, 89, 0, 4]
series = new dfd.Series(data1)
sort_series = series.sort_values()
table(sort_series)
sort_values
方法还接受关键字参数ascending
和inplace
。默认情况下,ascending
设置为true
,inplace
设置为false
。
以下表格显示了上述代码的结果:
图 3.32 – 按值排序 Series
请注意,sort_values
和sort_index
方法也适用于包含字符串的列和索引。
使用我们的artist
DataFrame,让我们尝试按以下方式对一个列进行排序:
sort_df = df.sort_values({by: "artist", inplace:false, ascending: false})
table(sort_df)
DataFrame 使用artist
列按降序排序。这将导致一个表,其中Rihanna
在第一行,其后是Passenger
,基于第一个字符:
图 3.33 – 按字符串列排序
在本节中,我们看到了如何按列值和行索引对 DataFrame 进行排序。在下一节中,我们将看到如何过滤 DataFrame 值。
过滤
根据列中的某些特定值过滤 DataFrame 的行在数据操作和处理过程中大多数时间都很方便。Danfo.js 有一个名为query
的方法,用于根据列中的值过滤行。
query
方法接受以下关键字参数:
-
column
:列名 -
is
:指定要使用的逻辑运算符(>,<,>=,<=,==
) -
to
:用于过滤 DataFrame 的值 -
inplace
:更新原始 DataFrame 或返回一个新的
以下是query
方法的工作示例。
首先,我们创建一个 DataFrame:
data = {"A": [30, 1, 2, 3],
"B": [34, 4, 5, 6],
"C": [20, 20, 30, 40]}
df = new dfd. DataFrame (data)
以下图显示了表格:
图 3.34 – DataFrame
让我们按列B
中的值对 DataFrame 进行排序:
query_df = df.query({ column: "B", is: ">", to: 5 })
table(query_df)
在这里,我们通过大于 5 的值来过滤列B
。这将导致返回包含列B
中大于 5 的值的行,如下所示:
图 3.35 – 按列 B 中大于 5 的值过滤
让我们通过检查列C
中的值是否等于20
来过滤 DataFrame:
query_df = df.query({ column: "C", is: "==", to: 20})
table(query_df)
通过这样,我们得到了以下输出:
图 3.36 – 在 C 列值等于 20 的情况下进行过滤
在本节中,我们看到了如何通过列值过滤 DataFrame。在下一节中,我们将看到如何执行不同的算术运算。
算术运算
在本小节中,我们将研究不同的算术运算以及如何使用它们来预处理我们的数据。
可以在以下之间进行加法、减法、乘法和除法等算术运算:
-
一个 DataFrame 和一个 Series
-
一个 DataFrame 和一个数组
-
一个 DataFrame 和一个
标量
值
我们首先进行了 DataFrame 和标量
值之间的算术运算:
data = {"Col1": [10, 45, 56, 10],
"Col2": [23, 20, 10, 24]}
ar_df = new dfd. DataFrame (data)
//add a scalar variable
add_df = ar_df.add(20)
table(add_df)
add
方法用于在 DataFrame 的每一行上添加一个标量
变量 20。add
方法接受两个参数:
-
other
:这表示 DataFrame、Series、数组或标量
。 -
axis
:指定操作是按行还是按列应用的。行轴用0
表示,列轴用1
表示。默认为0
。
对于标量
操作,我们得到了以下结果:
图 3.37 – 左:原始 DataFrame。右:进行标量加法后的 DataFrame
让我们创建一个 Series 并将其传递给add
方法:
add_series = new dfd.Series([20,30])
add_df = ar_df.add(add_series, axis=1)
table(add_df)
创建了一个包含两行元素的 Series。这两行元素等于ar_df
DataFrame 中的列数,这意味着add_series
的第一行值属于ar_df
的第一列,就像第二行值对应于ar_df
的第二列一样。
前面段落中的解释意味着20
将用于乘以Col1
,30
将用于乘以Col2
。为了实现这一点,我们指定轴为1
,这告诉 add 操作按列进行操作,正如我们在下表中所看到的:
图 3.38 – DataFrame 和 Series 之间的加法操作
DataFrame 和 Series 之间的加法操作与 DataFrame 和普通 JavaScript 数组之间的操作相同,因为 Series 只是具有一些扩展功能的 JavaScript 数组。
让我们看看两个 DataFrame 之间的操作:
data = {"Col1": [1, 4, 5, 0],
"Col2": [2, 0, 1, 4]}
data2 = {"new_col1": [1, 5, 20, 10],
"new_Col2": [20, 2, 1, 2]}
df = new dfd.DataFrame(data)
df2 = new dfd.DataFrame(data2)
df_new = df.add(df2)
首先,我们创建两组 DataFrame,然后将它们相加;但是我们必须确保两个 DataFrame 具有相同数量的列和行。此外,此操作是逐行进行的。
该操作实际上并不关心两个 DataFrame 是否具有相同的列名,但是结果列名是我们要添加到前一个 DataFrame 的 DataFrame 的列名。
以下表显示了前面代码中创建的 DataFrame 的结果:
图 3.39 – 从左到右:DataFrame df 和 df2 的表格以及 df 和 df2 之间的加法
使用 add 操作进行的示例与sub()
、mul()
、div()
、pow()
和mod()
方法相同。
此外,对于 DataFrame 和 Series,还有一组称为累积的数学运算,其中包括以下方法:
-
累积求和:
cumsum()
-
累积最小值:
cummin()
-
累积最大值:
cummax()
-
累积乘积:
cumprod()
这些操作中的每一个在传递参数方面都是相同的。它们都接受操作应该在哪个轴上执行的参数:
data = [[11, 20, 3], [1, 15, 6], [2, 30, 40], [2, 89, 78]]
cols = ["A", "B", "C"]
df = new dfd. DataFrame (data, { columns: cols })
new_df = df.cumsum({ axis: 0 })
我们通过指定其数据和列来创建一个 DataFrame,然后沿着0
轴执行累积求和。
以下表显示了前面代码中创建的 DataFrame 的结果:
图 3.40 - 左:cumsum()之前的 DataFrame。右:cumsum()之后的 DataFrame
让我们沿着 DataFrame 的轴1
执行cumsum
操作:
new_df = df.cumsum({ axis: 1 })
这给我们以下表格:
图 3.41 - 沿轴 1 的累积和
相同的累积操作也可以应用于 Series。让我们将相同的累积和应用于 Series 数据:
series = new dfd.Series([2,3,4,5,6,7,8,9])
c_series = series.cumsum()
table(c_series)
Series 中的cumsum
不接受任何参数。通过前面的操作,我们得到了以下表格图 3.42,即在应用累积 Series 之前的原始 Series。
以下表格显示了在前面代码中series.cumsum()
操作的结果:
图 3.42 - 累积和 Series
所有其他累积操作,如cumprod
、cummin
和cummax
,都与前面cumsum
示例中所示的方式相同。
在本节中,我们研究了不同的算术操作以及如何利用这些操作来预处理数据。在下一节中,我们将深入研究逻辑操作,例如如何在 DataFrame 和 Series、数组和标量
值之间执行逻辑操作。
逻辑操作
在本节中,我们将看到如何执行逻辑操作,例如比较 DataFrame 和 Series、数组和标量值之间的值。
逻辑操作的调用和使用方式与算术操作的工作方式非常相似。逻辑操作可以在以下之间进行:
-
一个 DataFrame 和一个 Series
-
一个 DataFrame 和一个数组
-
一个 DataFrame 和一个标量值
实施了以下逻辑操作:
-
等于(
==
):.eq()
-
大于(
>
):gt()
-
小于(
<
):lt()
-
不等于(
!=
):ne()
-
小于或等于(
<=
):le()
-
大于或等于(
>=
):ge()
所有这些方法都接受相同的参数,即other
和axis
。使用lt()
方法,让我们看看它们是如何工作的:
data = {"Col1": [10, 45, 56, 10],
"Col2": [23, 20, 10, 24]}
df = new dfd. DataFrame (data)
df_rep = df.lt(20)
table(df_rep)
代码检查 DataFrame 中的每个值是否小于 20。结果可以在下图中看到:
图 3.43 - 左:df 的 DataFrame。右:df_rep 的 DataFrame
同样的操作也可以在 DataFrame 和 Series 之间进行,如下所示:
series = new dfd.Series([45,10])
df_rep = df.lt(series, axis=1)
table(df_rep)
DataFrame 与 Series 的series
操作是按列进行的。以下表格是结果:
图 3.44 - DataFrame 和 Series 之间的逻辑操作
让我们在两个 DataFrame 之间执行逻辑操作,如下所示:
data = {"Col1": [10, 45, 56, 10],
"Col2": [23, 20, 10, 24]}
data2 = {"new_col1": [10, 45, 200, 10],
"new_Col2": [230, 200, 110, 24]}
df = new dfd.DataFrame (data)
df2 = new dfd.DataFrame (data2)
df_rep = df.lt(df2)
table(df_rep)
代码输出了以下结果:
图 3.45 - DataFrame 之间的逻辑操作
在本节中,我们研究了一些重要的 DataFrame 操作,例如如何按列值过滤 DataFrame。我们还看到了如何按列值和行索引对 DataFrame 进行排序。此外,本节还深入研究了使用loc
和iloc
对 DataFrame 进行索引,最后,我们研究了算术和逻辑操作。
在下一节中,我们将深入研究如何处理不同的数据格式,如何将这些文件解析为 Danfo.js,并将它们转换为 DataFrame 或 Series。
数据加载和处理不同的文件格式
在本节中,我们将看看如何处理不同的文件格式。Danfo.js 提供了一种处理三种不同文件格式的方法:CSV、Excel 和 JSON。以这些格式呈现的数据可以轻松地被读取并呈现为 DataFrame。
每种文件格式所需的方法如下:
-
read_csv()
: 读取 CSV 文件 -
read_json()
: 读取 JSON 文件 -
read_excel()
: 读取以.xslx
格式呈现的 Excel 文件
这些方法中的每一个都可以在本地和互联网上读取数据。此外,在Node.js
环境中,这些方法只能访问本地文件。在网络上,这些方法可以读取提供的文件(CSV、JSON 和.xslx
文件),只要这些文件在互联网上可用。
让我们看看如何在 Node.js 环境中读取本地文件:
const dfd = require("Danfo.Js-node")
dfd.read_csv('titanic.csv').then(df => {
df.head().print()
})
首先,我们导入 Node.js 版本的 Danfo.js,然后调用read_csv()
来读取同一目录中可用的titanic.csv
文件。请注意,如果要读取的文件不在同一目录中,您需要指定路径。
df.head().print()
在 Node.js 控制台中打印 DataFrame 表的前五行;print()
函数类似于我们在 Dnotebook 中使用的table()
函数。我们从前面的代码中得到以下表格:
图 3.46 – 从 CSV 文件读取的 DataFrame 表
同样,可以在网络上进行相同的操作,但我们将使用http
链接,这样相同的数据就可以在线使用:
csvUrl =
"https://storage.googleapis.com/tfjs-examples/multivariate-linear-regression/data/boston-housing-train.csv";
dfd.read_csv(csvUrl).then((df) => {
df.print()
});
这与前一个代码块产生相同的结果。对所有其他文件格式方法也是一样的:
const dfd = require("Danfo.Js-node")
dfd.read_excel('SampleData.xlsx', {header_index: 7}).then(df => {
df.head().print()
})
read_excel()
方法接受一个可选的配置参数,确保正确解析 Excel 文件:
-
source
:字符串、URL 或本地文件路径,用于检索 Excel 文件。 -
sheet_name
(可选):要解析的工作表的名称。默认为第一个工作表。 -
header_index
(可选):int,表示标题列的行索引。 -
data_index
(可选):int,指示数据开始的行索引。
在前面的代码块中,我们将header_index
的值指定为7
,因为标题列位于那里,因此我们得到以下结果:
图 3.47 – 从 Excel 文件读取的 DataFrame 表
read_json()
方法在传递参数方面与read_csv()
非常相似;该方法只接受指向 JSON 文件的 URL 或 JSON 文件所在目录路径:
const dfd = require("Danfo.Js-node")
dfd.read_json('book.json').then(df => {
df.head().print()
})
这读取了同一目录中名为book.json
的文件,并输出了以下内容:
图 3.48 – 从 JSON 文件读取的 DataFrame
此外,Danfo.js 包含一个名为reader
的终极表格文件读取方法;该方法可以读取 CSV 和 Excel。它还可以读取其他文件,如frictionlessdata.io/
中 Frictionless 规范中指定的Datapackage
。
reader
方法使用名为Frictionless.js
的包在github.com/frictionlessdata/frictionless-js
中读取本地或远程文件。
reader
方法与read_csv
和read_excel
具有相同的 API 设计,只是它可以读取两种文件类型,如下面的代码所示:
const dfd = require("Danfo.Js-node")
// for reading csv
dfd.read('titanic.csv').then(df => {
df.head().print()
})
从前面的代码中,我们输出了与图 3.48中显示的相同的表格。
将 DataFrame 转换为另一种文件格式
经过一系列数据处理后,我们可能希望将最终的 DataFrame 转换为文件格式以便保存。Danfo.js 实现了一种将 DataFrame 转换为 CSV 格式的方法。
让我们将迄今为止创建的 DataFrame 转换为 CSV 文件。请注意,这只适用于 Node.js 环境:
df.to_csv("/home/link/to/path.csv").then((csv) => {
console.log(csv);
}).catch((err) => {
console.log(err);
})
这(df.to_csv()
)将 DataFrame 保存为 CSV 文件,保存在指定的路径目录中,并赋予给它的名称(path.csv)。
在本节中,我们研究了在 Danfo.js 中可用的不同格式的文件,如 CSV、JSON 和 Excel。我们研究了读取这些文件的不同方法,还研究了一种更通用的读取这些文件格式的方法。
我们还看到了如何将 DataFrame 转换为用户指定的文件格式。
总结
在本章中,我们讨论了为什么需要 Danfo.js,然后深入了解了 Series 和 DataFrames 的实际含义。我们还讨论了 Danfo.js 中一些基本功能,并在 DataFrames 和 Series 中实现了这些功能。
我们还看到了如何使用 DataFrames 和 Series 方法来处理和预处理数据。我们学会了如何根据列值筛选 DataFrame。我们还按行索引和列值对 DataFrame 进行了排序。本章使我们能够执行日常数据操作,如以不同格式读取文件、转换格式,并将预处理后的 DataFrames 保存为理想的文件格式。
在下一章中,我们将深入研究数据分析、数据整理和转换。我们将进一步讨论数据处理和预处理,并了解如何处理缺失的数字,以及如何处理字符串和时间序列数据。
第五章:数据分析、整理和转换
数据分析、整理和转换是任何数据驱动项目的重要方面,作为数据分析师/科学家,您大部分时间将花在进行一种形式的数据处理或另一种形式上。虽然 JavaScript 是一种灵活的语言,具有良好的用于操作数据结构的功能,但编写实用程序函数来执行数据整理操作非常繁琐。因此,我们在 Danfo.js 中构建了强大的数据整理和转换功能,这可以大大减少在此阶段花费的时间。
在本章中,我们将向您展示如何在真实数据集上实际使用 Danfo.js。您将学习如何加载不同类型的数据集,并通过执行操作(如处理缺失值、计算描述性统计、执行数学运算、合并数据集和执行字符串操作)来分析它们。
在本章中,我们将涵盖以下主题:
-
转换数据
-
合并数据集
-
Series 数据访问器
-
计算统计数据
技术要求
要跟随本章,您应该具备以下条件:
-
现代浏览器,如 Chrome、Safari、Opera 或 Firefox
-
Node.js、Danfo.js 和 Dnotebook 已安装在您的系统上
-
稳定的互联网连接用于下载数据集
Danfo.js 的安装说明可以在第三章中找到,开始使用 Danfo.js,而 Dnotebook 的安装步骤可以在第二章中找到,Dnotebook – JavaScript 的交互式计算环境。
注意
如果您不想安装任何软件或库,可以使用 Dnotebook 的在线版本playnotebook.jsdata.org/
。但是,在使用任何功能之前,请不要忘记安装最新版本的 Danfo.js!
转换数据
数据转换是根据定义的步骤/过程将数据从一种格式(主格式)转换为另一种格式(目标格式)的过程。数据转换可以是简单的或复杂的,取决于数据集的结构、格式、最终目标、大小或复杂性,因此重要的是要了解 Danfo.js 中用于进行这些转换的功能。
在本节中,我们将介绍 Danfo.js 中用于进行数据转换的一些功能。在每个子节下,我们将介绍一些函数,包括fillna
、drop_duplicates
、map
、addColumns
、apply
、query
和sample
,以及用于编码数据的函数。
替换缺失值
许多数据集都存在缺失值,为了充分利用这些数据集,我们必须进行某种形式的数据填充/替换。Danfo.js 提供了fillna
方法,当给定 DataFrame 或 Series 时,可以自动用指定的值填充任何缺失字段。
当您将数据集加载到 Danfo.js 数据结构中时,所有缺失值(可能是未定义的、空的、null、none 等)都存储为NaN
,因此fillna
方法可以轻松找到并替换它们。
替换 Series 中的值
Series
对象的fillna
方法的签名如下:
Series.fillna({value, inplace})
value
参数是您想要用于替换缺失值的新值,而inplace
参数用于指定是直接对对象进行更改还是创建副本。让我们看一个例子。
假设我们有以下 Series:
sdata = new dfd.Series([NaN, 1, 2, 33, 4, NaN, 5, 6, 7, 8])
sdata.print() //use print to show series in browser or node environment
table(sdata) //use table to show series in Dnotebook environment
在您的 Dnotebook 环境中,table(sdata)
将显示如下图所示的表格:
图 4.1 – 具有缺失值的 Series
注意
如果您在浏览器或 Node.js 环境中工作,可以在控制台中查看print
函数的输出。
要替换缺失值(NaN
),我们可以这样做:
sdata_new = sdata.fillna({ value: -999})
table(sdata_new)
打印输出给我们以下表格:
图 4.2 – 带有缺失值的 Series
你也可以原地填充缺失值,也就是说,你可以直接改变对象,而不是创建一个新的对象。这可以在以下代码中看到:
sdata.fillna({ value: -999, inplace: true})
table(sdata)
在某些情况下,原地填充可以帮助减少内存使用,特别是在处理大型 DataFrame 或 Series 时。
替换 DataFrame 中的值
fillna
函数也可以用于填充 DataFrame 中的缺失值。DataFrame 对象的 fillna
方法的签名如下:
DataFrame.fillna({columns, value, inplace})
首先,让我们了解参数:
-
columns
:columns
参数是要填充的列名数组。 -
values
:values
参数也是一个数组,必须与columns
的大小相同,保存你想要替换的相应值。 -
inplace
: 这指定我们是否应修改当前的 DataFrame 还是返回一个新的 DataFrame。
现在,让我们看一个例子。
假设我们有以下 DataFrame:
data = {"Name":["Apples", "Mango", "Banana", undefined],
"Count": [NaN, 5, NaN, 10],
"Price": [200, 300, 40, 250]}
df = new dfd.DataFrame(data)
table(df)
代码的输出如下图所示:
图 4.3 – 带有缺失值的 DataFrame
在填充缺失值方面,我们可以采取两种方法。首先,我们可以用单个值填充所有缺失字段,如下面的代码片段所示:
df_filled = df.fillna({ values: [-99]})
table(df_filled)
代码输出如下图所示:
图 4.4 – 用单个值填充 DataFrame 中的所有缺失值
在处理 DataFrame 时,用同一个字段填充所有缺失值的情况并不可取,甚至没有用,因为你必须考虑到不同字段具有不同类型的值,这意味着填充策略会有所不同。
为了处理这种情况,我们可以指定要填充的列及其相应的值的列表,如下所示。
使用前面例子中使用的相同 DataFrame,我们可以指定 Name
和 Count
列,并用 Apples
和 -99
值填充它们,如下所示:
data = {"Name":["Apples", "Mango", "Banana", undefined],
"Count": [NaN, 5, NaN, 10],
"Price": [200, 300, 40, 250]}
df = new dfd.DataFrame(data)
df_filled = df.fillna({columns: ["Name", "Count"], values: ["Apples", -99]})
table(df_filled)
代码的输出如下图所示:
图 4.5 – 用特定值填充 DataFrame 中的缺失值
注意
在数据分析中,常用填充值如-9、-99 和-999 来表示缺失值。
让我们看看如何在下一节中删除重复项。
删除重复项
在处理 Series 或 DataFrame 中的列时,重复字段是常见的情况。例如,看一下以下代码中的 Series:
data = [10, 45, 56, 10, 23, 20, 10, 10]
sf = new dfd.Series(data)
table(sf)
代码的输出如下图所示:
图 4.6 – 带有重复字段的 Series
在上图中,你可以看到值 10
出现了多次。如果有需要,你可以使用 drop_duplicates
函数轻松删除这些重复项。drop_duplicates
函数的签名如下:
Series.drop_duplicates({inplace, keep)
drop_duplicates
函数只有两个参数,第一个参数(inplace
)非常直观。第二个参数 keep
可以用于指定要保留哪个重复项。你可以保留 Series 中的第一个或最后一个重复项。这有助于在删除重复项后保持 Series 中的结构和值的顺序。
让我们看看这个实例。在下面的代码块中,我们正在删除所有重复的值,只保留第一次出现的值:
data1 = [10, 45, 56, 10, 23, 20, 10, 10, 20, 20]
sf = new dfd.Series(data1)
sf_drop = sf.drop_duplicates({keep: "first"})
table(sf_drop)
代码的输出如下图所示:
图 4.7 – 在 keep 设置为 first 的情况下删除重复项后的 Series
从前面的输出中,您可以看到保留了重复项的第一个值,而其他值被丢弃。相反,让我们将keep
参数设置为last
,并观察字段的顺序变化:
sf_drop = sf.drop_duplicates({keep: "last"})
table(sf_drop)
代码的输出如下图所示:
图 4.8 - 在保留最后一个重复项的情况下删除重复项后的 Series
请注意,保留了最后的10
和20
重复字段,并且顺序与我们将keep
设置为first
时不同。下图将帮助您理解在删除重复项时keep
参数的含义:
图 4.9 - 当 keep 设置为 first 或 last 时的输出差异
从上图中,您会注意到将keep
设置为first
或last
的主要区别是生成值的顺序。
在下一小节中,我们将使用map
函数进行数据转换。
使用map
函数进行数据转换
在某些情况下,您可能希望对 Series 或 DataFrame 列中的每个值应用转换。当您有一些自定义函数或映射并希望轻松应用它们到每个字段时,这通常很有用。Danfo.js 提供了一个称为map
的简单接口,可以在这里使用。让我们看一个例子。
假设我们有一个包含物品及其对应的重量(以克为单位)的 DataFrame,如下面的代码所示:
df = new dfd.DataFrame({'item': ['salt', 'sugar', 'rice', 'apple', 'corn', 'bread'],
'grams': [400, 200, 120, 300, 70.5, 250]})
table(df)
代码的输出如下图所示:
图 4.10 - DataFrame 中物品及其对应的重量(以克为单位)
我们想要创建一个名为kilograms
的新列,其值是相应的克数,但转换为千克。在这里,我们可以这样做:
- 创建一个名为
convertToKg
的函数,代码如下:
function convertToKg(gram){
return gram / 1000
}
- 在
grams
列上调用map
函数,如下面的代码块所示:
kilograms = df['grams'].map(convertToKg)
- 使用
addColumn
函数将新的kilograms
列添加到 DataFrame 中,如下面的代码所示:
df.addColumn({ "column": "kilograms", "value": kilograms });
将所有这些放在一起,我们有以下代码:
df = new dfd.DataFrame({'item': ['salt', 'sugar', 'rice', 'apple', 'corn', 'bread'],
'grams': [400, 200, 120, 300, 70.5, 250]})
function convertToKg(gram){
return gram / 1000
}
kilograms = df['grams'].map(convertToKg)
df.addColumn({ "column": "kilograms", "value": kilograms });
table(df)
代码的输出如下图所示:
图 4.11 - DataFrame 中物品及其对应的重量(以克为单位)
当您将一个对象与键值对传递给map
函数时,map
函数也可以执行一对一的映射。这可用于诸如编码、快速映射等情况。让我们看一个例子。
假设我们有以下 Series:
sf = new dfd.Series([1, 2, 3, 4, 4, 4])
在这里,我们想要将每个数字值映射到其对应的名称。我们可以指定一个mapper
对象并将其传递给map
函数,如下面的代码所示:
mapper = { 1: "one", 2: "two", 3: "three", 4: "four" }
sf = sf.map(mapper)
table(sf)
代码的输出如下图所示:
图 4.12 - 数值映射为字符串名称的 Series
map
函数在处理 Series 时非常有用,但有时您可能希望将函数应用于特定轴(行或列)。在这种情况下,您可以使用另一个 Danfo.js 函数,称为apply
函数。
在下一节中,我们将介绍apply
函数。
使用apply
函数进行数据转换
apply
函数可用于将函数或转换映射到 DataFrame 中的特定轴。它比map
函数稍微高级和强大,我们将解释原因。
首先是我们可以在指定的轴上应用张量操作的事实。让我们看一个包含以下值的 DataFrame:
data = [[1, 2, 3], [4, 5, 6], [20, 30, 40], [39, 89, 78]]
cols = ["A", "B", "C"]
df = new dfd.DataFrame(data, { columns: cols })
table(df)
代码的输出如下图所示:
图 4.13 - 具有三列的示例 DataFrame
现在,我们可以在指定的轴上应用任何兼容的张量操作。例如,在以下代码中,我们可以对 DataFrame 中的每个元素应用softmax
函数(en.wikipedia.org/wiki/Softmax_function
):
function sum_vals(x) {
return x.softmax()
}
let df_new = df.apply({axis: 0, callable: sum_vals })
table(df_new)
代码的输出如下图所示:
图 4.14-在逐个元素应用 softmax 函数后的 DataFrame
注意
我们将轴设置为0
以进行逐个元素的张量操作。这是因为一些张量操作无法逐个元素执行。您可以在js.tensorflow.org/api/latest/
上阅读有关支持的操作的更多信息。
让我们看另一个示例,应用一个既可以在列(axis=1)上工作,也可以在行(axis=0)上工作的函数。
我们将使用 TensorFlow 的sum
函数进行操作,如下面的代码所示。首先,让我们在行轴上应用它:
data = [[1, 2, 3], [4, 5, 6], [20, 30, 40], [39, 89, 78]]
cols = ["A", "B", "C"]
df = new dfd.DataFrame(data, { columns: cols })
function sum_vals(x) {
return x.sum()
}
df_new = df.apply({axis: 0, callable: sum_vals })
table(df_new)
打印df_new
DataFrame 的结果如下输出:
图 4.15-在行(0)轴上应用 sum 函数后的 DataFrame
使用与之前相同的 DataFrame,将轴更改为1
以进行列操作,如下面的代码片段所示:
df_new = df.apply({axis: 1, callable: sum_vals })
table(df_new)
打印 DataFrame 的结果如下输出:
图 4.16-在列(1)轴上应用 sum 函数后的 DataFrame
自定义 JavaScript 函数也可以与apply
函数一起使用。这里唯一的注意事项是,您不需要指定轴,因为 JavaScript 函数是逐个元素应用的。
让我们看一个使用 JavaScript 函数的apply
的示例。在以下代码块中,我们将toLowerCase
字符串函数应用于 DataFrame 中的每个元素:
data = [{ short_name: ["NG", "GH", "EGY", "SA"] },
{ long_name: ["Nigeria", "Ghana", "Eqypt", "South Africa"] }]
df = new dfd.DataFrame(data)
function lower(x) {
return '${x}'.toLowerCase()
}
df_new = df.apply({ callable: lower })
table(df_new)
打印 DataFrame 的结果如下输出:
图 4.17-在逐个元素应用 JavaScript 函数后的 DataFrame
apply
函数非常强大,可以用于在 DataFrame 或 Series 上应用自定义转换到列或行。在下一小节中,我们将看一下过滤和查询 DataFrame 和 Series 的不同方法。
过滤和查询
当我们需要获取满足特定布尔条件的数据子集时,过滤和查询非常重要。我们将在以下示例中演示如何在 DataFrame 和 Series 上使用query
方法进行过滤和查询。
假设我们有一个包含以下列的 DataFrame:
data = {"A": [30, 1, 2, 3],
"B": [34, 4, 5, 6],
"C": [20, 20, 30, 40]}
cols = ["A", "B", "C"]
df = new dfd.DataFrame(data, { columns: cols })
table(df)
打印 DataFrame 的结果如下输出:
图 4.18-用于查询的示例值的 DataFrame
现在,让我们过滤 DataFrame,并仅返回B
列的值大于5
的行。这应该返回行0
和3
。我们可以使用query
函数来实现,如下面的代码所示:
df_filtered = df.query({ column: "B", is: ">", to: 5})
table(df_filtered)
这给我们以下输出:
图 4.19-查询后的 DataFrame
query
方法接受所有 JavaScript 布尔运算符(>
,<
,>=
,<=
和==
),并且也适用于字符串列,如下面的示例所示。
首先,让我们创建一个具有字符串列的 DataFrame:
data = {"A": ["Ng", "Yu", "Mo", "Ng"],
"B": [34, 4, 5, 6],
"C": [20, 20, 30, 40]}
df = new dfd.DataFrame(data)
table(df)
输出如下图所示:
图 4.20-应用字符串查询之前的 DataFrame
接下来,我们将使用等于运算符("=="
)运行一个查询:
query_df = df.query({ column: "A", is: "==", to: "Ng"})
table(query_df)
这将产生以下输出:
图 4.21 - 应用字符串查询后的数据框
在大多数情况下,您查询的数据框很大,因此可能希望执行就地查询。这可以通过将inplace
指定为true
来完成,如下面的代码块所示:
data = {"A": [30, 1, 2, 3],
"B": [34, 4, 5, 6],
"C": [20, 20, 30, 40]}
cols = ["A", "B", "C"]
df = new dfd.DataFrame(data, { columns: cols })
df.query({ column: "B", is: "==", to: 5, inplace: true })
table(df)
打印数据框的结果如下:
图 4.22 - 执行就地查询后的数据框
query
方法非常重要,在通过特定属性过滤数据时经常使用它。使用query
函数的另一个好处是它允许inplace
功能,这意味着它对于过滤大型数据集非常有用。
在下一个子节中,我们将看看另一个有用的概念,称为随机抽样。
随机抽样
从数据框或系列中进行随机抽样在需要随机重新排序行时非常有用。这在机器学习(ML)之前的预处理步骤中非常有用。
让我们看一个使用随机抽样的例子。假设我们有一个包含以下值的数据框:
data = [[1, 2, 3], [4, 5, 6], [20, 30, 40], [39, 89, 78]]
cols = ["A", "B", "C"]
df = new dfd.DataFrame(data, { columns: cols })
table(df)
这导致以下输出:
图 4.23 - 随机抽样前的数据框
现在,让我们通过在数据框上调用sample
函数来随机选择两行,如下面的代码所示:
async function load_data() {
let data = {
Name: ["Apples", "Mango", "Banana", "Pear"],
Count: [21, 5, 30, 10],
Price: [200, 300, 40, 250],
};
let df = new dfd.DataFrame(data);
let s_df = await df.sample(2);
s_df.print();
}
load_data()
这导致以下输出:
图 4.24 - 随机抽取两行后的浏览器控制台中的数据框
在上述代码中,您会注意到代码被包装在一个async
函数中。这是因为sample
方法返回一个 promise,因此我们必须等待结果。上述代码可以在浏览器或 node.js 环境中按原样执行,但是它需要一点调整才能在 Dnotebook 中工作。
如果您想在 Dnotebook 中运行确切的代码,可以像这样调整它:
var sample;
async function load_data() {
let data = {
Name: ["Apples", "Mango", "Banana", "Pear"],
Count: [21, 5, 30, 10],
Price: [200, 300, 40, 250],
};
let df = new dfd.DataFrame(data);
let s_df = await df.sample(2);
sample = s_df
}
load_data()
在上述代码中,您可以看到我们明确使用var
定义了sample
。这使得sample
变量对所有单元格都可用。在这里改用let
声明将使变量仅对定义它的单元格可用。
接下来,在一个新的单元格中,您可以使用table
方法打印样本数据框,如下面的代码所示:
table(sample)
这导致以下输出:
图 4.25 - 在 Dnotebook 中随机抽取两行后的数据框
在下一节中,我们将简要介绍 Danfo.js 中提供的一些编码特性。
编码数据框和系列
编码是在应用于 ML 或统计建模之前可以应用于数据框/系列的另一个重要转换。这是在将数据馈送到模型之前始终执行的重要数据预处理步骤。
ML 或统计模型只能使用数值,因此所有字符串/分类列必须适当地转换为数值形式。有许多类型的编码,例如独热编码,标签编码,均值编码等,选择编码可能会有所不同,这取决于您拥有的数据类型。
Danfo.js 目前支持两种流行的编码方案:标签和独热编码器,在接下来的部分中,我们将解释如何使用它们。
标签编码器
标签编码器将列中的类别映射到列中唯一类别的整数值之间的值。让我们看一个在数据框上使用这个的例子。
假设我们有以下数据框:
data = { fruits: ['pear', 'mango', "pawpaw", "mango", "bean"] ,
Count: [20, 30, 89, 12, 30],
Country: ["NG", "NG", "GH", "RU", "RU"]}
df = new dfd.DataFrame(data)
table(df)
打印数据框的结果如下:
图 4.26 - 编码前的数据框
现在,让我们使用LabelEncoder
对fruits
列进行编码,如下面的代码块所示:
encode = new dfd.LabelEncoder()
encode.fit(df['fruits'])
在前面的代码块中,您会注意到我们首先创建了一个LabelEncoder
对象,然后在列(fruits
)上调用了fit
方法。fit
方法简单地学习并存储编码器对象中的映射。稍后可以用它来转换任何列/数组,我们很快就会看到。
调用fit
方法后,我们必须调用transform
方法来应用标签,如下面的代码块所示:
sf_enc = encode.transform(df['fruits'])
table(sf_enc)
打印 DataFrame 的结果如下:
图 4.27 - 标签编码前的 DataFrame
从前面的输出中,您可以看到为每个标签分配了唯一的整数。在调用transform
时,如果列中有一个在fit
(学习)阶段不可用的新类别,则用值-1
表示。让我们看一个例子,使用前面示例中的相同编码器:
new_sf = encode.transform(["mango","man", "car", "bean"])
table(new_sf)
打印 DataFrame 的结果如下:
图 4.28 - 应用 transform 后的新类别数组的 DataFrame
在前面的示例中,您可以看到我们在一个具有新类别(man
和car
)的数组上调用了训练过的编码器。请注意,输出中用-1
代替了这些新类别,正如我们之前解释的那样。
接下来,让我们谈谈独热编码。
独热编码
独热编码主要应用于数据集中的有序类别。在这种编码方案中,使用二进制值0
(热)和1
(冷)来编码唯一类别。
使用与前一节相同的 DataFrame,让我们创建一个独热编码器对象,并将其应用于country
列,如下面的代码块所示:
encode = new dfd.OneHotEncoder()
encode.fit(df['country'])
sf_enc = encode.transform(df['country'])
table(sf_enc)
打印 DataFrame 的结果如下:
图 4.29 - 应用独热编码后的 DataFrame
从前面的输出中,您可以看到对于每个唯一类别,一系列 1 和 0 被用来替换类别,并且现在我们生成了两列额外的列。以下图表直观地展示了每个唯一类别如何映射到独热编码:
图 4.30 - 三个类别的独热编码映射
Danfo.js 中的最后一个可用编码特性是get_dummies
函数。这个函数的工作方式与独热编码函数相同,但主要区别在于您可以在整个 DataFrame 上应用它,并且它将自动编码找到的任何分类列。
让我们看一个使用get_dummies
函数的例子。假设我们有以下 DataFrame:
data = { fruits: ['pear', 'mango', "pawpaw", "mango", "bean"] ,
count: [20, 30, 89, 12, 30],
country: ["NG", "NG", "GH", "RU", "RU"]}
df = new dfd.DataFrame(data)
table(df)
打印 DataFrame 的结果如下:
图 4.31 - 应用get_dummies
函数前的 DataFrame
现在,我们可以通过将 DataFrame 传递给get_dummies
函数一次性编码所有分类列(fruits
和country
),如下面的代码块所示:
df_enc = dfd.get_dummies({data: df})
table(df_enc)
打印 DataFrame 的结果如下:
图 4.32 - 三个类别的独热编码映射
从前面的图中,您可以看到get_dummies
函数自动检测到了fruits
和country
分类变量,并对它们进行了独热编码。列是自动生成的,它们的名称以相应的列和类别开头。
注意
您可以指定要编码的列,以及为每个编码列命名前缀。要了解更多可用选项,请参考官方 Danfo.js 文档:danfo.jsdata.org/
。
在下一节中,我们将看看组合数据集的各种方法。
组合数据集
数据框和序列可以使用 Danfo.js 中的内置函数进行组合。存在诸如danfo.merge
和danfo.concat
之类的方法,根据配置的不同,可以帮助您使用熟悉的类似数据库的连接方式以不同形式组合数据集。
在本节中,我们将简要讨论这些连接类型,从merge
函数开始。
数据框合并
merge
操作类似于数据库中的Join
操作,它在对象中执行列或索引的连接操作。merge
操作的签名如下:
danfo.merge({left, right, on, how})
让我们了解每个参数的含义:
-
left
:要合并到左侧的数据框/序列。 -
right
:要合并到右侧的数据框/序列。 -
on
:要连接的列的名称。这些列必须在左侧和右侧的数据框中都找到。 -
how
:how
参数指定如何执行合并。它类似于数据库风格的连接,可以是left
、right
、outer
或inner
。
Danfo.js 的merge
函数与 pandas 的merge
函数非常相似,因为它执行类似于关系数据库连接的内存中连接操作。以下表格提供了 Danfo 的合并方法和 SQL 连接的比较:
图 4.33 – 比较 Danfo.js 合并方法和 SQL 连接(来源:pandas 文档:https://pandas.pydata.org/pandas-docs/stable/user_guide/merging.html#brief-primer-on-merge-methods-relational-algebra)
在接下来的几节中,我们将提供一些示例,帮助您了解何时以及如何使用merge
函数。
通过单个键进行内部合并
在单个共同键上合并数据框默认会导致内连接。内连接要求两个数据框具有匹配的列值,并返回两者的交集;也就是说,它返回一个只包含具有共同特征的行的数据框。
假设我们有两个数据框,如下所示:
data = [['K0', 'k0', 'A0', 'B0'], ['k0', 'K1', 'A1', 'B1'],
['K1', 'K0', 'A2', 'B2'], ['K2', 'K2', 'A3', 'B3']]
data2 = [['K0', 'k0', 'C0', 'D0'], ['K1', 'K0', 'C1', 'D1'],
['K1', 'K0', 'C2', 'D2'], ['K2', 'K0', 'C3', 'D3']]
colum1 = ['Key1', 'Key2', 'A', 'B']
colum2 = ['Key1', 'Key2', 'A', 'D']
df1 = new dfd.DataFrame(data, { columns: colum1 })
df2 = new dfd.DataFrame(data2, { columns: colum2 })
table(df1)
打印第一个数据框的结果如下:
图 4.34 – 执行合并操作的第一个数据框
现在我们将打印第二个数据框,如下所示:
table(df2)
这将导致以下输出:
图 4.35 – 执行合并操作的第二个数据框
接下来,我们将执行内连接,如下面的代码所示:
merge_df = dfd.merge({left: df1, right: df2, on: ["Key1"]})
table(merge_df)
打印数据框的结果如下:
图 4.36 – 在单个键上对两个数据框进行内部合并
从前面的输出中,您可以看到在Key1
上的内连接导致了一个包含来自数据框 1 和数据框 2 的所有值的数据框。这也被称为两个数据框的笛卡尔积。
我们可以进一步合并多个键。我们将在下一节中讨论这个问题。
在两个键上进行内部合并
在两个键上执行内部合并还将返回两个数据框的交集,但它只会返回在数据框的左侧和右侧都找到的键的行。
使用我们之前创建的相同数据框,我们可以在两个键上执行内部连接,如下所示:
merge_mult_df = dfd.merge({left: df1, right: df2, on: ["Key1", "Key2"]})
table(merge_mult_df)
打印数据框的结果如下:
图 4.37 – 在两个键上进行内部合并
从前面的输出中,您可以看到在两个键上执行内部合并会导致一个只包含在数据框的左侧和右侧中都存在的键的数据框。
通过单个键进行外部合并
外部合并,正如我们在前面的表中所看到的,执行两个数据框的并集。也就是说,它返回在两个数据框中都存在的键的值。在单个键上执行外部连接将返回与在单个键上执行内部连接相同的结果。
使用前面的相同数据框,我们可以指定how
参数来将合并行为从其默认的inner
连接更改为outer
,如下面的代码所示:
merge_df = dfd.merge({ left: df1, right: df2, on: ["Key1"], how: "outer"})
table(merge_df)
打印数据框的结果如下:
图 4.38 - 两个数据框在单个键上的外部合并
从前面的图表中,您可以看到具有键(K0
,K0
,K1
,K2
)的第一个数据框和具有键(K0
,K1
,K1
,K2
)的第二个数据框的并集是(K0
,K0
,K1
,K1
,K2
)。然后使用这些键来对数据框进行并集操作。在这样做之后,我们收到了前面表格中显示的结果。
我们还可以在两个键上执行外部连接。这将与在两个键上执行内部连接不同,正如我们将在以下子节中看到的。
在两个键上的外部合并
仍然使用前面的相同数据框,我们可以添加第二个键,如下面的代码所示:
merge_df = dfd.merge({ left: df1, right: df2, on: ["Key1", "Key2"], how: "outer"})
table(merge_df)
打印数据框的结果如下:
图 4.39 - 两个数据框在两个键上的外部合并
在前面的输出中,我们可以看到返回的行始终具有Key1
和Key2
中的键。如果第一个数据框中存在的键不在第二个数据框中,则值始终填充为NaN
。
注意
在 Danfo.js 中进行合并不支持一次在两个以上的键上进行合并。这可能会在将来发生变化,因此,如果您需要支持这样的功能,请务必查看官方 Danfo.js GitHub 存储库的讨论部分(github.com/opensource9ja/danfojs
)。
在接下来的几节中,我们将简要介绍左连接和右连接,这在执行合并时也很重要。
右连接和左连接
右连接和左连接非常简单;它们只返回具有指定how
参数中存在的键的行。例如,假设我们将how
参数指定为right
,如下面的代码所示:
merge_df = dfd.merge({ left: df1, right: df2, on: ["Key1", "Key2"], how: "right"})
table(merge_df)
打印数据框的结果如下:
图 4.40 - 两个数据框在两个键上的右连接
结果图表仅显示了右侧数据框中存在的键的行。这意味着在执行合并时,右侧数据框被赋予了更高的优先级。
如果我们将how
设置为left
,那么左侧数据框将更偏好,并且我们只会看到键存在于左侧数据框中的行。下面的代码显示了执行left
连接的示例:
merge_df = dfd.merge({ left: df1, right: df2, on: ["Key1", "Key2"], how: "left"})
table(merge_df)
打印数据框的结果如下:
图 4.41 - 两个数据框在两个键上的左连接
从前面的输出中,我们可以确认结果行更偏向于左侧数据框。
Danfo.js 的merge
函数非常强大和有用,在开始使用具有重叠键的多个数据集时会派上用场。在下一节中,我们将介绍另一个有用的函数,称为连接,用于转换数据集。
数据连接
连接数据是另一种重要的数据组合技术,正如其名称所示,这基本上是沿着一个轴连接、堆叠或排列数据。
在 Danfo.js 中用于连接数据的函数被公开为danfo.concat
,该函数的签名如下:
danfo.concat({df_list, axis})
让我们了解每个参数代表什么:
-
df_list
:df_list
参数是要连接的 DataFrames 或 Series 的数组。 -
axis
:axis
参数可以接受行(0)或列(1),并指定对象将对齐的轴。
接下来,我们将介绍一些示例,这些示例将帮助您了解如何使用concat
函数。首先,我们将学习如何沿行轴连接三个 DataFrames。
沿行轴(0)连接 DataFrames
首先,让我们创建三个 DataFrames,如下面的代码所示:
df1 = new dfd.DataFrame(
{
"A": ["A_0", "A_1", "A_2", "A_3"],
"B": ["B_0", "B_1", "B_2", "B_3"],
"C": ["C_0", "C_1", "C_2", "C_3"]
},
index=[0, 1, 2],
)
df2 = new dfd.DataFrame(
{
"A": ["A_4", "A_5", "A_6", "A_7"],
"B": ["B_4", "B_5", "B_6", "B_7"],
"C": ["C_4", "C_5", "C_6", "C_7"],
},
index=[4, 5, 6],
)
df3 = new dfd.DataFrame(
{
"A": ["A_8", "A_9", "A_10", "A_11"],
"B": ["B_8", "B_9", "B_10", "B_11"],
"C": ["C_8", "C_9", "C_10", "C_11"]
},
index=[8, 9, 10],
)
table(df1)
table(df2)
table(df3)
使用 Node.js 和浏览器中的print
函数或 Dnotebook 中的table
函数打印每个 DataFrame,将给我们以下输出。
df1
DataFrame 的输出如下:
图 4.42 - 要连接的第一个 DataFrame(df1)
df2
DataFrame 的输出如下:
图 4.43 - 要连接的第二个 DataFrame(df2)
最后,df3
DataFrame 的输出如下:
图 4.44 - 要连接的第三个 DataFrame(df3)
现在,让我们使用concat
函数组合这些 DataFrames,如下面的代码所示:
df_frames = [df1, df2, df3]
combined_df = dfd.concat({df_list: df_frames, axis: 0})
table(combined_df)
这导致以下输出:
图 4.45 - 沿行轴(0)连接三个 DataFrames 的结果
在这里,您可以看到concat
函数简单地沿列轴组合每个 DataFrame(df1
,df2
,df3
);也就是说,它们在df_list
中第一个 DataFrame 的下方堆叠,以创建一个巨大的组合。
此外,索引看起来不同。这是因为在内部,Danfo.js 为组合生成了新的索引。如果您需要数字索引,那么可以使用reset_index
函数,如下面的代码所示:
combined_df.reset_index(true)
table(combined_df)
这导致以下输出:
图 4.46 - 重置组合 DataFrames 的索引
现在,使用相同的 DataFrames 进行沿列轴的连接会是什么样子?我们将在下一节中尝试这个。
沿列轴(1)连接 DataFrames
使用我们之前创建的相同 DataFrames,只需在concat
代码中将axis
更改为1
,如下面的代码所示:
df_frames = [df1, df2, df3]
combined_df = dfd.concat({df_list: df_frames, axis: 1})
table(combined_df)
打印 DataFrame 的结果如下:
图 4.47 - 沿列轴(0)应用 concat 到三个 DataFrames 的结果
连接也适用于 Series 对象。我们将在下一节中看一个例子。
沿指定轴连接 Series
Series 也是 Danfo.js 数据结构,因此concat
函数也可以在它们上面使用。这与连接 DataFrames 的方式相同,我们很快将进行演示。
使用上一节的 DataFrames,我们将创建一些 Series 对象,如下面的代码所示:
series_list = [df1['A'], df2['B'], df3['D']]
请注意,我们正在使用 DataFrame 子集来从 DataFrames 中抓取不同的列作为 Series。现在,我们可以将这些 Series 组合成一个数组,然后将其传递给concat
函数,如下面的代码所示:
series_list = [df1['A'], df2['B'], df3['D']]
combined_series = dfd.concat({df_list: series_list, axis: 1})
table(combined_series)
打印 DataFrame 的结果如下:
图 4.48 - 沿行轴(0)应用 concat 到三个 Series 的结果
将轴更改为行(0)也可以工作,并返回一个有很多缺失条目的 DataFrame,如下图所示:
图 4.49 - 沿列轴(1)应用 concat 到三个 Series 的结果
现在,您可能想知道为什么在生成的组合中有很多缺失的字段。这是因为当对象沿指定轴组合时,有时对象的位置/长度与第一个 DataFrame 不对齐。使用NaN
进行填充以返回一致的长度。
在本小节中,我们介绍了两个重要的函数(merge
和concat
),它们可以帮助您对 DataFrame 或 Series 执行复杂的组合。在下一节中,我们将讨论另一种不同但同样重要的内容,即字符串/文本操作。
Series 数据访问器
Danfo.js 在各种访问器下提供了特定于数据类型的方法。访问器是 Series 对象内的命名空间,只能应用/调用特定数据类型。目前为字符串和日期时间 Series 提供了两个访问器,在本节中,我们将讨论每个访问器并提供一些示例以便理解。
字符串访问器
DataFrame 中的字符串列或具有dtype
字符串的 Series 可以在str
访问器下访问。在这样的对象上调用str
访问器会暴露出许多用于操作数据的字符串函数。我们将在本节中提供一些示例。
假设我们有一个包含以下字段的 Series:
data = ['lower boy', 'capitals', 'sentence', 'swApCaSe']
sf = new dfd.Series(data)
table(sf)
打印此 Series 的结果如下图所示:
图 4.50 - 沿列轴(1)将三个 Series 应用 concat 的结果
从前面的输出中,我们可以看到 Series(sf
)包含文本,并且是字符串类型。您可以使用dtype
函数来确认这一点,如下面的代码所示:
console.log(sf.dtype)
输出如下:
string
现在我们有了我们的 Series,它是字符串数据类型,我们可以在其上调用str
访问器,并使用各种 JavaScript 字符串方法,如capitalize
、split
、len
、join
、trim
、substring
、slice
、replace
等,如下例所示。
以下代码将capitalize
函数应用于 Series:
mod_sf = sf.str.capitalize()
table(mod_sf)
打印此 Series 的结果如下:
图 4.51 - 将 capitalize 函数应用于字符串 Series 的结果
以下代码将substring
函数应用于 Series:
mod_sf = sf.str.substring(0,3) //returns a substring by start and end index
table(mod_sf)
打印此 Series 的结果如下:
图 4.52 - 将 substring 函数应用于字符串 Series 的结果
以下代码将replace
函数应用于 Series:
mod_sf = sf.str.replace("lower", "002") //replaces a string with specified value
table(mod_sf)
打印此 Series 的结果如下:
图 4.53 - 将 replace 函数应用于字符串 Series 的结果
以下代码将join
函数应用于 Series:
mod_sf = sf.str.join("7777", "+") // joins specified value to Series
table(mod_sf)
打印此 Series 的结果如下图所示:
图 4.54 - 将 join 函数应用于字符串 Series 的结果
以下代码将indexOf
函数应用于 Series:
mod_sf = sf.str.indexOf("r") //Returns the index where the value is found else -1
table(mod_sf)
打印此 Series 的结果如下:
图 4.55 - 将 indexOf 函数应用于字符串 Series 的结果
注意
str
访问器提供了许多公开的字符串方法。您可以在 Danfo.js 文档中的完整列表中查看(danfo.jsdata.org/api-reference/series#string-handling
)。
日期时间访问器
Danfo.js 在系列对象上公开的第二个访问器是日期时间访问器。这可以在dt
命名空间下访问。从日期时间列中处理和提取不同的信息,如日期、月份和年份,是进行数据转换时的常见过程,因为日期时间的原始格式几乎没有用处。
如果您的数据有一个日期时间列,则可以在其上调用dt
访问器,这将公开各种函数,可用于提取信息,如年份、月份、月份名称、星期几、小时、秒和一天中的分钟数。
让我们看一些在具有日期列的系列上使用dt
访问器的示例。首先,我们将创建一个具有一些日期时间字段的系列:
timeColumn = ['12/13/2016 15:00:20', '10/20/2019 18:30:00', '1/1/2020 12:00:00', '1/30/2020 16:20:00', '11/12/2019 22:00:30']
sf = new dfd.Series(timeColumn, {columns: ["times"]})
table(sf)
打印此系列的结果如下:
图 4.56 - 具有日期时间字段的系列
现在我们有了一个具有日期字段的系列,我们可以提取一些信息,例如一天中的小时数、年份、月份或星期几。我们需要做的第一件事是使用dt
访问器将系列转换为 Danfo.js 的日期时间格式,如下面的代码所示:
dateTime = sf['times'].dt
dateTime
变量现在公开了不同的方法来提取日期信息。利用这一点,我们可以做以下任何一项:
- 获取一天中的小时作为新系列:
dateTime = sf.dt
hours = dateTime.hour()
table(hours)
这给我们以下输出:
图 4.57 - 显示提取的一天中的小时的新系列
- 获取年份作为新系列:
dateTime = sf.dt
years = dateTime.year()
table(years)
这给我们以下输出:
图 4.58 - 显示提取的年份的新系列
- 获取月份作为新系列:
dateTime = sf.dt
month_name = dateTime.month_name()
table(month_name)
这给我们以下输出:
图 4.59 - 显示提取的月份名称的新系列
- 获取星期几作为新系列:
dateTime = sf.dt
weekdays = dateTime.weekdays()
table(weekdays)
这给我们以下输出:
图 4.60 - 显示提取的星期几的新系列
dt
访问器公开的一些其他函数如下:
-
Series.dt.month
:这将返回一个整数月份,从 1(一月)到 12(十二月)。 -
Series.dt.day
:这将返回一周中的天数作为整数,从 0(星期一)到 6(星期日)。 -
Series.dt.minutes
:这将返回一天中的分钟数作为整数。 -
Series.dt.seconds
:这将返回一天中的秒数作为整数。
恭喜您完成了本节!数据转换和聚合是数据分析的非常重要的方面。有了这些知识,您可以正确地转换和整理数据集。
在下一节中,我们将向您展示如何使用 Danfo.js 对数据集进行描述性统计。
计算统计数据
Danfo.js 带有一些重要的统计和数学函数。这些函数可以用于生成整个数据框或单个系列的摘要或描述性统计。在数据集中,统计数据很重要,因为它们可以为我们提供更好的数据洞察。
在接下来的几节中,我们将使用流行的泰坦尼克号数据集,您可以从以下 GitHub 存储库下载:github.com/PacktPublishing/Building-Data-Driven-Applications-with-Danfo.js/blob/main/Chapter03/data/titanic.csv
。
首先,让我们使用read_csv
函数将数据集加载到 Dnotebook 中,如下面的代码所示:
var df //using var so df is available to all cells
load_csv("https://raw.githubusercontent.com/plotly/datasets/master/finance-charts-apple.csv")
.then((data)=>{
df = data
})
上述代码从指定的 URL 加载泰坦尼克号数据集,并将其保留在df
变量中。
注意
我们在这里使用load_csv
和var
声明,因为我们在 Dnotebook 中工作。正如我们一直提到的,你不会在浏览器或 Node.js 脚本中使用这种方法。
现在,在一个新的单元格中,我们可以打印加载数据集的头部:
table(df.head())
打印 DataFrame 的结果如下:
图 4.61 - 泰坦尼克号数据集的前五行
我们可以对整个数据调用describe
函数,快速获取所有列的描述统计信息,如下面的代码所示:
table(df.describe())
打印 DataFrame 的结果如下:
图 4.62 - 在泰坦尼克号数据集上使用 describe 函数的输出
在上面的代码中,我们调用了describe
函数,然后用table
打印了输出。describe
函数只适用于数值数据类型,并且默认情况下只会自动选择float32
或int32
类型的列。
注意
默认情况下,在进行计算之前,所有的统计和数学计算都会删除NaN
值。
describe
函数提供了以下内容的描述性统计信息:
-
NaN
值。 -
均值:一列中的平均值。
-
标准差(std):一组值的变化或离散程度的度量。
-
最小值(min):一列中的最小值。
-
中位数:一列中的中间值。
-
最大值(max):一列中的最大值。
-
方差:值从平均值的传播程度的度量。
按轴计算统计信息
describe
函数虽然快速且易于应用,但在需要沿特定轴计算统计信息时并不是很有帮助。在本节中,我们将介绍一些最流行的方法,并向您展示如何根据指定的轴计算统计信息。
可以在指定轴上对 DataFrame 调用诸如均值、众数和中位数等中心趋势。这里,轴0
代表行
,而轴1
代表列
。
在下面的代码中,我们正在计算泰坦尼克号数据集中数值列的均值、众数和中位数:
df_nums = df.select_dtypes(['float32', "int32"]) //select all numeric dtype columns
console.log(df_nums.columns)
[AAPL.Open,AAPL.High,AAPL.Low,AAPL.Close,AAPL.Volume,AAPL.Adjusted,dn,mavg,up]
在上面的代码中,我们选择了所有的数值列,以便可以应用数学运算。接下来,我们将调用mean
函数,默认情况下返回列轴(1
)上的均值:
col_mean = df_nums.mean()
table(col_mean)
打印 DataFrame 的结果如下:
图 4.63 - 在数值列上调用均值函数
前面输出中显示的精度看起来有点太长了。我们可以将结果四舍五入到两位小数,使其更具可呈现性,如下面的代码所示:
col_mean = df_nums.mean().round(2)
table(col_mean)
打印 DataFrame 的结果如下:
图 4.64 - 在数值列上调用均值函数并将值四舍五入到两位小数
将结果四舍五入到两位小数后,输出看起来更清晰。接下来,让我们计算沿着行轴的mean
:
row_mean = df_nums.mean(axis=0)
table(row_mean)
这给我们以下输出:
图 4.65 - 在行轴上调用均值函数
在上面的输出中,您可以看到返回的值具有数值标签。这是因为行轴最初具有数值标签。
使用相同的思路,我们可以计算众数、中位数、标准差、方差、累积和、累积均值、绝对值等统计信息。以下是 Danfo.js 当前可用的统计函数:
-
abs
:返回每个元素的绝对数值的 Series/DataFrame。 -
count
:计算每列或每行的单元格数,不包括NaN
值。 -
cummax
:返回 DataFrame 或 Series 的累积最大值。 -
cummin
:返回 DataFrame 或 Series 的累积最小值。 -
cumprod
:返回 DataFrame 或 Series 的累积乘积。 -
cumsum
:返回 DataFrame 或 Series 的累积和。 -
describe
:生成描述性统计。 -
max
:返回所请求轴的最大值。 -
mean
:返回所请求轴的平均值。 -
median
:返回所请求轴的中位数。 -
min
:返回所请求轴的最小值。 -
mode
:返回沿所选轴的元素的众数。 -
sum
:返回所请求轴的值的总和。 -
std
:返回所请求轴的标准偏差。 -
var
:返回所请求轴的无偏方差。 -
nunique
:计算所请求轴上的不同元素的数量。
注意
支持的函数列表可能会发生变化,并且会添加新的函数。因此,最好通过查看danfo.jsdata.org/api-reference/dataframe#computations-descriptive-stats
来跟踪新的函数。
描述性统计非常重要,在本节中,我们成功介绍了一些重要的函数,这些函数可以帮助您在 Danfo.js 中有效地基于指定轴计算统计数据。
摘要
在本章中,我们成功介绍了 Danfo.js 中可用的数据转换和整理函数。首先,我们向您介绍了用于替换值、填充缺失值和检测缺失值的各种整理函数,以及应用和映射方法,用于将自定义函数应用于数据集。掌握这些函数和技术可以确保您具有构建数据驱动产品所需的基础,以及从数据中获取见解的能力。
接下来,我们向您展示了如何在 Danfo.js 中使用各种合并和连接函数。最后,我们向您展示了如何对数据集进行描述性统计。
在下一章中,我们将进一步介绍如何使用 Danfo.js 的内置绘图功能以及集成第三方绘图库来制作美丽而惊人的图表/图形。
第六章:使用 Plotly.js 进行数据可视化
绘图和可视化是数据分析中非常重要的任务,因此我们将一个完整的章节来专门讨论它们。数据分析师通常会在探索性数据分析(EDA)阶段执行绘图和数据可视化。这可以极大地帮助识别数据中隐藏的有用模式,并建立数据建模的直觉。
在本章中,您将学习如何使用Plotly.js创建丰富和交互式的图表,这些图表可以嵌入到任何 Web 应用程序中。
具体来说,我们将涵盖以下主题:
-
关于 Plotly.js 的简要介绍
-
Plotly.js 的基础知识
-
使用 Plotly.js 创建基本图表
-
使用 Plotly.js 创建统计图表
技术要求
为了跟上本章的内容,您应该具备以下条件:
-
现代浏览器,如 Chrome、Safari、Opera 或 Firefox
-
Node.js和可选地,安装在您系统上的Danfo Notebook(Dnotebook)
-
稳定的互联网连接以下载数据集
有关 Dnotebook 的安装说明,请参见第二章,Dnotebook - 用于 JavaScript 的交互式计算环境。
注意
如果您不想安装任何软件或库,可以在playnotebook.jsdata.org/
上使用 Dnotebook 的在线版本。
Danfo.js带有一个用于轻松制作图表的绘图应用程序编程接口(API),在幕后,它使用 Plotly。这是我们在本章介绍 Plotly.js 的主要原因,因为在这里获得的知识将帮助您轻松定制下一章中使用 Danfo.js 创建的图表。
关于 Plotly.js 的简要介绍
Plotly.js (plotly.com/javascript/
),根据作者的说法,是一个建立在流行的 D3.js (d3js.org/
)和 stack.gl (github.com/stackgl
)库之上的开源、高级、声明性图表库。
它支持超过 40 种图表类型,包括以下类型:
-
基本图表,如散点图、线图、条形图和饼图
-
统计图表,如箱线图、直方图和密度图
-
科学图表,如热图、对数图和等高线图
-
金融图表,如瀑布图、蜡烛图和时间序列图
-
地图,如气泡图、区域图和 Mapbox 地图
-
三维(3D)散点图和曲面图,以及 3D 网格
要使用 Plotly.js,您需要访问浏览器的React
和Vue
。在下一节中,我们将看到如何安装 Plotly.js。
通过script
标签使用 Plotly.js
为了在script
标签中使用 Plotly.js。在下面的代码片段中,我们将在简单的 HTML 文件的头部添加 Plotly.js 的script
标签:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="img/plotly-1.2.0.min.js"></script>
</head>
<body>
</body>
</html>
一旦您按照上面的代码片段添加了 Plotly.js 的script
标签,保存 HTML 文件并在浏览器中打开它。输出将是一个空白页面,但在幕后,Plotly.js 被添加并在页面中可用。我们可以通过按照这里的步骤制作一个简单的图表来测试这一点:
- 在 HTML 主体中创建一个
div
标签,图表将在其中绘制。我们将给这个一个myPlot
,如下所示:
<body>
<div id="myPlot">
</body
- 在您的 HTML 页面中,创建样本
x
和y
数据,然后绘制scatter
图,如下面的代码片段所示:
...
<body>
<div id="myPlot"></div>
<script>
let data = [{
x: [1, 3, 5, 6, 8, 9, 5, 8],
y: [2, 4, 6, 8, 0, 2, 1, 2],
mode: 'markers',
type: 'scatter'
}]
Plotly.newPlot("myPlot", data)
</script>
</body>
...
在浏览器中打开 HTML 文件将给出以下输出:
图 5.1 - 使用 Plotly 制作的简单散点图
在 Dnotebook 中,我们将在本章中经常使用,您可以通过首先在顶部单元格中使用load_package
函数加载并使用 Plotly,如下面的代码片段所示:
load_package(["https://cdn.plot.ly/plotly-1.58.4.min.js"])
然后,在一个新的单元格中,您可以添加以下代码:
let data = [{
x: [1,3,5,6,8,9,5,8],
y: [2,4,6,8,0,2,1,2],
mode: 'markers',
type: 'scatter'
}]
Plotly.newPlot(this_div(), data)
运行上述代码单元将给出以下输出:
图 5.2 - 在 Dnotebook 上使用 Plotly 制作的简单散点图
您可以看到,前面部分的代码与 HTML 版本相同,只有一个细微的区别 - 将this_div
函数传递给Plotly.newPlot
。
this_div
函数只是一个 Dnotebook 的辅助函数,它创建并返回代码单元块下方的div
标签的 ID。这意味着每当您在 Dnotebook 中处理图表时,都可以使用this_div
函数获取div
标签。
注意
接下来,我们将使用this_div
而不是指定div
标签 ID。这是因为我们将主要在 Dnotebook 环境中工作。要在 HTML 或其他this_div
中使用代码,将this_div
指定为要使用的div
标签的 ID。
现在您知道如何安装 Plotly,我们将继续下一节,关于创建基本图表。
Plotly.js 的基础知识
使用 Plotly.js 的一个主要优势是它很容易上手,并且有很多配置可以指定,使您的图表更好。在本节中,我们将介绍一些重要的可用配置选项,并向您展示如何指定这些选项。
在我们继续之前,让我们了解如何将数据传递给 Plotly。
数据格式
要创建x
和y
键,如下面的代码示例所示:
const trace1 = {
x: [20, 30, 40],
y: [2, 4, 6]
}
注意
在 Plotly 中,数据点通常称为trace。这是因为您可以在单个图表中绘制多个数据点。这里提供了一个示例:
var data = [trace1, trace2]
Plotly.newPlot("my_div", data);
x
和y
数组可以包含字符串和数字数据。如果它们包含字符串数据,数据点将按原样绘制,即逐点。这是一个例子:
var trace1 = {
x:['2020-10-04', '2021-11-04', '2023-12-04'],
y: ["cat", "goat", "pig"],
type: 'scatter'
};
Plotly.newPlot(this_div(), trace1);
运行上述代码单元将产生以下输出:
图 5.3 - 使用 Plotly 绘制日期与字符串值的图表
另一方面,如果您的数据是数字的,Plotly 将自动排序,然后选择默认比例。看下面的例子:
var trace1 = {
x: ['2020-10-04', '2021-11-04', '2023-12-04'],
y: [90, 20, 10],
type: 'scatter'
};
var data = [trace1];
Plotly.newPlot(this_div(), data);
运行上述代码单元将产生以下输出:
图 5.4 - 使用 Plotly 绘制日期与数值的图表
在我们进入配置部分之前,让我们看一个在同一图表中绘制多个轨迹的示例。首先,我们设置我们的数据,如下面的代码片段所示:
var trace1 = {
x:['2020-10-04', '2021-11-04', '2023-12-04'],
y: [90, 20, 10],
type: 'scatter'
};
var trace2 = {
x: ['2020-10-04', '2021-11-04', '2023-12-04'],
y: [25, 35, 65],
mode: 'markers',
marker: {
size: [20, 20, 20],
}
};
var data = [trace1, trace2];
Plotly.newPlot(this_div(), data);
运行上述代码单元将产生以下输出:
图 5.5 - 多个轨迹共享相同的 x 轴的图表
注意
在单个图表中绘制多个轨迹时,建议轨迹共享一个公共轴。这样可以使您的图表更易于阅读。
如果您想知道是否可以向数据数组添加更多轨迹,答案是肯定的 - 您可以添加任意数量的轨迹,但必须考虑可解释性,因为添加更多轨迹可能不容易解释。
现在您知道如何将数据传递到图表中,让我们了解一些基本的配置选项,您可以在制作图表时传递给 Plotly。
图表的配置选项
配置可以用于设置图表的交互性和模式栏等属性。配置是一个对象,通常作为Plotly.newPlot
调用的最后一个参数传递,如下面的代码片段所示:
config = { … }
Plotly.newPlot("my_div", data, layout, config)
在接下来的几节中,我们将介绍一些常见的配置选项,这些选项将在第八章中使用,创建一个无代码数据分析/处理系统。如果您想知道有哪些可用的配置选项,可以在这里阅读更多信息:plotly.com/javascript/configuration-options/
。
配置模式栏
模式栏是一个水平工具栏,提供了许多选项,可用于与图表进行交互。默认情况下,只有在悬停在图表上时,模式栏才会变为可见,尽管可以更改这一点,我们将在下一节中看到。
使模式栏始终可见
要使模式栏始终可见,可以将displayModeBar
属性设置为true
,如下面的代码片段所示:
var trace1 = {
x: ['2020-10-04', '2021-11-04', '2023-12-04'],
y: [90, 20, 10],
type: 'scatter'
};
var data = [trace1];
var layout = {
title: 'Configure modebar'
};
var config = {
displayModeBar: true
};
Plotly.newPlot(this_div(), data, layout, config);
运行上述代码单元将产生以下输出:
图 5.6 - 配置模式栏始终显示
如果不需要模式栏,则将displayModeBar
函数设置为false
将确保即使在悬停在其上时,模式栏也会被隐藏。
从模式栏中删除按钮
您可以通过将您不想要的按钮的名称传递给modeBarButtonsToRemove
config
属性来从模式栏中移除按钮,我们将在本节中进行演示。
使用与使模式栏始终可见部分相同的示踪,我们将从模式栏中移除缩放按钮。您可以在下面的截图中看到在移除之前的放大按钮:
图 5.7 - 移除缩放按钮之前
在上面的截图中,我们展示了移除缩放按钮之前的图表。接下来,我们将设置config
选项以移除该按钮,如下面的代码片段所示:
var config = {
displayModeBar: true,
modeBarButtonsToRemove: ['zoomIn2d']
};
Plotly.newPlot(this_div(), data, layout, config);
运行上述代码单元将产生以下输出:
图 5.8 - 移除缩放按钮后的图表
使用前面示例中演示的方法,您可以从您的图表中移除任何按钮。您可以在这里查看可以移除的所有模式栏按钮的名称:plotly.com/javascript/configuration-options/#remove-modebar-buttons
。
向模式栏添加自定义按钮
Plotly 提供了一种方法,可以向模式栏添加具有自定义行为的按钮。当我们想要通过自定义行为扩展我们的图表时,这将非常有用,例如,链接到您的个人网站。
在下面的示例中,我们将添加一个自定义按钮,当用户单击时显示This
is
an
example
of
a
plot
that
answers
a
question
on
click
。
注意
在 Dnotebook 中添加自定义按钮将不起作用,因此我们将在 HTML 文件中进行操作。您可以设置一个带有 Plotly 脚本的 HTML 文件,就像我们在通过脚本标签使用 Plotly.js部分中演示的那样。
在您的 HTML 文件的 body 部分中,添加以下代码:
<div id="mydiv"></div>
<script>
...
var config = {
displayModeBar: true,
modeBarButtonsToAdd: [
{
name: 'about',
icon: Plotly.Icons.question,
click: function (gd) {
alert('This is an example of a plot that answers a question on click')
}
}]
}
Plotly.newPlot("mydiv", data, layout, config);
</script>
保存并在浏览器中打开上述 HTML 文件,然后单击您刚刚创建的按钮。它应该显示一个带有您指定的文本的警报,类似于下面截图中显示的内容:
图 5.9 - 带有自定义按钮的图表
在我们之前展示的代码片段中,注意modeBarButtonsToAdd
配置选项。这个选项是我们定义要添加的按钮以及点击它时发生的事情的地方。创建自定义按钮时可以指定的主要属性列在这里:
-
name
:按钮的名称。 -
icon
:显示在模式栏中的图标/图片。这可以是自定义图标或任何内置的 Plotly 图标(github.com/plotly/plotly.js/blob/master/src/fonts/ploticon.js
)。 -
click
:定义单击按钮时发生的情况。在这里,您可以指定任何 JavaScript 函数,甚至更改图表的行为。
接下来,让我们看看如何制作静态图表。
制作静态图表
默认情况下,Plotly 图表是交互式的。如果要使它们静态,可以在 config
对象中指定以下选项:
var config = {
staticPlot: true
}
当我们只想显示一个没有干扰交互的图表时,静态图表是很有用的。
接下来,我们将向您展示如何创建响应式图表。
制作响应式图表
要使图表响应式,使其能够自动调整大小以适应显示的窗口,可以将 responsive
属性设置为 true
,就像下面的代码片段中所示:
var config = {
responsive: true
}
响应式图表在创建将在不同屏幕尺寸上显示的网页时非常有用。
在下一节中,我们将向您展示如何下载并设置图表的下载选项。
自定义下载图表选项
默认情况下,当显示模式栏时,可以将 Plotly 图表保存为便携式网络图形(PNG)文件。这可以进行自定义,您可以设置下载图像类型,以及其他属性,如文件名、高度、宽度等。
为了实现这一点,您可以在 config
对象中设置 toImageButtonOptions
属性,就像我们在下面的代码片段中演示的那样:
var config = {
toImageButtonOptions: {
format: 'jpeg', // one of png, svg, jpeg, webp
filename: 'my_image', // the name of the file
height: 600,
width: 700,
}
}
最后,在下一节中,我们将演示如何将图表的区域设置更改为其他语言。
更改默认区域设置
在为讲其他语言的人制作图表时,区域设置是很重要的。这可以极大地提高图表的可解释性。
按照下面的步骤,我们将把默认区域设置从英语更改为法语:
- 获取特定的区域设置,并将其添加到您的 HTML 文件中(或者在 Dnotebook 中使用
load_scripts
加载它),就像下面的代码片段中所示的那样:
...
<head>
<script src="img/plotly-1.58.4.min.js"></script>
<script src="img/plotly-locale-fr-latest.js"></script> <!-- load locale -->
</head>
...
在 Dnotebook 中,可以使用 load_package
来完成这个操作,如下所示:
load_package(["https://cdn.plot.ly/plotly-1.58.4.min.js", "https://cdn.plot.ly/plotly-locale-fr-latest.js"])
- 在您的
config
对象中,指定区域设置,就像下面的代码片段中所示的那样:
var config = {
locale: "fr"
}
让我们看一个完整的示例及相应的输出。将以下代码添加到 HTML 文件的主体中:
<div id="mydiv"></div>
<script>
var trace1 = {
x: ['2020-10-04', '2021-11-04', '2023-12-04'],
y: [90, 20, 10],
type: 'scatter'
};
var trace2 = {
x: ['2020-10-04', '2021-11-04', '2023-12-04'],
y: [25, 35, 65],
mode: 'markers',
marker: {
size: [20, 20, 20],
}
};
var data = [trace1, trace2];
var layout = {
title: 'Change Locale',
showlegend: false
};
var config = {
locale: "fr"
};
Plotly.newPlot("mydiv", data, layout, config);
</script>
在浏览器中加载 HTML 页面会显示以下图表,其中 locale
设置为法语:
图 5.10 - 区域设置为法语的图表
现在您知道如何配置您的图表,我们将继续讨论图表配置的另一个重要方面:布局。
Plotly 布局
layout
(plotly.com/javascript/reference/layout/
) 是传递给 Plotly.newPlot
函数的第三个参数。它用于配置绘制图表的区域/布局,以及标题、文本、图例等属性。
有六个布局属性可以设置——标题、图例、边距、大小、字体和颜色。我们将演示如何使用它们,并附有示例。
配置图表标题
title
属性配置图表标题,即显示在图表顶部的文本。要添加标题,只需将文本传递给 layout
对象中的 title
属性,就像下面的代码片段中演示的那样:
var layout = {
title: 'This is an example title,
};
要更加明确,特别是如果需要配置标题文本的位置,可以设置 title
的 text
属性,就像下面的代码片段中所示的那样:
var layout = {
title: {text: 'This is an example title'}
};
使用上述格式,我们可以轻松地配置其他属性,比如使用其他属性来设置标题位置,如下所述:
-
x
:一个介于 0 和 1 之间的数字,用于设置标题文本相对于显示它的容器的x
位置。 -
y
:也是一个介于 0 和 1 之间的数字,用于设置标题文本相对于显示它的容器的y
位置。 -
xanchor
:可以是auto
,left
,center
或right
对齐。它设置标题相对于其x
位置的水平对齐。 -
yanchor
:可以是auto
,top
,middle
或bottom
对齐。它设置标题相对于其y
位置的垂直对齐。
让我们看一个将title
配置为显示在图表右上角的示例,如下所示:
var trace1 = {
x:['2020-10-04', '2021-11-04', '2023-12-04'],
y: [90, 20, 10],
type: 'scatter'
};
var data = [trace1];
var layout = {
title: { text: 'This is an example title',
x: 0.8,
y: 0.9,
xanchor: "right"}
};
Plotly.newPlot(this_div(), data, layout, config);
这将产生以下输出:
图 5.11 - 标题配置为右上角的图表
您还可以设置标题的填充。填充可以接受以下参数:
-
t
:设置标题的顶部填充 -
b
:设置标题的底部填充 -
r
:设置右填充,仅在xanchor
属性设置为right
时有效 -
l
:设置左填充,仅在xanchor
属性设置为left
时有效
例如,要设置标题的right
填充,您可以将xanchor
属性设置为right
,然后配置pad
的r
属性,如下面的代码片段所示:
var layout = {
title: {text: 'This is an example title',
xanchor: "right",
pad: {r: 100}}
};
请注意,前面代码片段中填充参数为100
表示 100px。
在下一节中,我们将看看如何配置图表的图例。
配置 Plotly 图例
图例描述了图表中显示的数据。当您在单个图表中显示多种形式的数据时,图例非常重要。
默认情况下,当图表中有多个数据迹时,Plotly 会显示一个图例。您也可以通过将layout
的showLegend
属性显式设置为true
来显示图例,如下面的代码片段所示:
var layout = {
showLegend: true
};
一旦图例被激活,您可以通过设置以下属性来自定义它的显示方式:
-
bgcolor
:设置图例的背景颜色。默认情况下,它设置为#fff
(白色)。 -
bordercolor
:设置图例边框的颜色。 -
borderwidth
:设置图例边框的宽度(以 px 为单位)。 -
font
:一个具有以下属性的对象:
a) family
:任何支持的 HTML 字体系列。
b) size
:图例文本的大小。
c) color
:图例文本的颜色。
在下面的代码片段中,我们展示了使用这些属性来配置图例的示例:
var trace1 = {
x: ['2020-10-04', '2021-11-04', '2023-12-04'],
y: [90, 20, 10],
type: 'scatter'
};
var trace2 = {
x: ['2020-10-04', '2021-11-04', '2023-12-04'],
y: [25, 35, 65],
mode: 'markers',
marker: {
size: [20, 20, 20],
}
};
var data = [trace1, trace2];
var layout = {
title: {text: 'This is an example title'},
showLegend: true,
legend: {bgcolor: "#fcba03",
bordercolor: "#444",
borderwidth: 1,
font: {family: "Arial", size: 30, color: "#fff"}}
};
Plotly.newPlot(this_div(), data, layout, config);
前面的代码产生以下输出:
图 5.12 - 显示自定义配置的图例
接下来,我们将展示如何配置整体布局的边距。
配置布局边距
margin
属性配置图表在屏幕上的位置。它支持所有的 margin 属性(l
,r
,t
和b
)。在下面的代码片段中,我们使用所有四个属性来演示设置布局边距:
...
var layout = {
title: {text: 'This is an example title'},
margin: {l: 200, r: 200, t: 230, b: 100}
};
Plotly.newPlot(this_div(), data, layout, config);
这将产生以下输出:
图 5.13 - 配置了边距的图表
注意前面截图中图表周围的空间?那就是设置的边距。重要的是要注意,边距也是以 px 为单位的。
接下来,我们将看看如何设置布局大小。
配置布局大小
有时,我们可能希望有一个更大或更小的布局,可以使用width
,height
或者方便的autosize
属性进行配置,如下面的代码片段所示:
...
var layout = {
title: {text: 'This is an example title'},
width: 1000,
height: 500
};
Plotly.newPlot(this_div(), data, layout, config);
这将产生以下输出:
图 5.14 - 配置了大小的图表
当我们希望 Plotly 自动设置布局的大小时,autosize
属性可以设置为true
。
注意
要查看可以在layout
中配置的其他属性,您可以访问 Plotly 的官方 API 参考页面:plotly.com/javascript/reference/layout
。
在接下来的部分,我们将向您展示如何根据您想要传达的信息制作不同类型的图表。
使用 Plotly.js 创建基本图表
Plotly.js 支持许多基本图表,可以快速用于传达信息。Plotly 中可用的一些基本图表示例包括散点图、线图、条形图、饼图、气泡图、点图、树状图、表格等。您可以在这里找到支持的基本图表的完整列表:plotly.com/javascript/basic-charts/
。
在本节中,我们将介绍一些基本图表,如散点图、条形图和气泡图。
首先,我们将从散点图开始。
使用 Plotly.js 创建散点图
散点图通常用于将两个变量相互绘制。该图显示为一组点,因此称为散点图。以下截图显示了散点图的示例:
图 5.15 - 散点图示例,显示票价与年龄间的边距
要使用 Plotly 制作散点图,您只需指定图表类型,就像我们在下面的示例中所示的那样:
var trace1 = {
x: [2, 5, 7, 12, 15, 20],
y: [90, 80, 10, 20, 30, 40],
type: 'scatter'
};
var data = [trace1];
Plotly.newPlot(this_div(), data);
这给出了以下输出:
图 5.16 - 销售与边距的散点图示例
默认情况下,点是使用线连接在一起的。您可以通过设置模式类型来更改此行为。模式类型可以是以下任何一种:
-
标记
-
线
-
文本
-
无
您还可以使用多种模式,通过加号连接它们在一起,例如,标记+文本+线
或标记+线
。
在下面的示例中,我们将标记
和文本
作为我们的模式类型:
var trace1 = {
x: [2, 5, 7, 12, 15, 20],
y: [90, 80, 10, 20, 30, 40],
type: 'scatter',
mode: 'markers+text'
};
var data = [trace1];
Plotly.newPlot(this_div(), data);
这给出了以下输出:
图 5.17 - 散点图,模式类型设置为标记+文本
如前所述,您可以在单个图表中绘制多个散点图,并且可以根据需要配置每个轨迹。在下面的示例中,我们绘制了三个散点图,并配置了不同的模式:
var trace1 = {
x: [1, 2, 3, 4, 5],
y: [1, 6, 3, 6, 1],
mode: 'markers',
type: 'scatter',
name: 'Trace 1',
};
var trace2 = {
x: [1.5, 2.5, 3.5, 4.5, 5.5],
y: [4, 1, 7, 1, 4],
mode: 'lines',
type: 'scatter',
name: 'Trace 2',
};
var trace3 = {
x: [1, 2, 3, 4, 5],
y: [4, 1, 7, 1, 4],
mode: 'markers+text',
type: 'scatter',
name: 'Trace 3',
};
var data = [ trace1, trace2, trace3];
var layout = {
title:'Data Labels Hover',
width: 1000
};
Plotly.newPlot(this_div(), data, layout);
运行上述代码单元会得到以下输出:
图 5.18 - 散点图,带有三个轨迹
现在您已经在本节中学习了基本图表的概念,您可以轻松地根据自定义数据点创建散点图,并使用所需的属性自定义大小。
接下来,我们将简要介绍条形图。
创建条形图
条形图是 Plotly.js 中提供的另一种流行类型的图表。它用于显示使用矩形条之间的高度或长度与它们代表的值成比例的数据点之间的关系。条形图主要用于绘制分类数据。
注意
分类数据或分类变量是具有固定或有限数量可能值的变量。英文字母就是分类数据的一个例子。
要在 Plotly.js 中制作条形图,您需要传递一个具有相应条高度/长度的分类数据点,然后将类型设置为bar
,就像下面的示例中所示的那样:
var data = [
{
x: ['Apple', 'Mango', 'Pear', 'Banana'],
y: [20, 20, 15, 40],
type: 'bar'
}
];
Plotly.newPlot(this_div(), data);
运行上述代码单元会得到以下输出:
图 5.19 - 带有四个变量的简单条形图
您可以在单个布局中绘制多个条形图,方法是创建多个轨迹并将它们作为数组传递。例如,在下面的代码片段中,我们创建了两个轨迹和一个图表:
var trace1 = {
x: ['Apple', 'Mango', 'Pear', 'Banana'],
y: [20, 20, 15, 40],
type: 'bar'
}
var trace2 = {
x: ['Goat', 'Lion', 'Spider', 'Tiger'],
y: [25, 10, 14, 36],
type: 'bar'
}
var data = [trace1, trace2]
Plotly.newPlot(this_div(), data);
运行上述代码单元会得到以下输出:
图 5.20 - 带有两个轨迹的条形图
在同一类别内绘制多个跟踪时,可以指定barmode
属性。barmode
属性可以是stack
或group
模式之一。例如,在下面的代码片段中,我们以stack
模式制作了两个跟踪的条形图:
var trace1 ={
x: ['Apple', 'Mango', 'Pear', 'Banana'],
y: [20, 20, 15, 40],
type: 'bar',
name: "Bar1"
}
var trace2 = {
x: ['Apple', 'Mango', 'Pear', 'Banana'],
y: [25, 10, 14, 36],
type: 'bar',
name: "Bar2"
}
var data = [trace1, trace2]
var layout = {
barmode: 'stack'
}
Plotly.newPlot(this_div(), data, layout);
运行上述代码单元格会产生以下输出:
图 5.21 – 两个跟踪的堆叠模式条形图
在下面的代码片段中,我们将barmode
属性更改为group
(默认模式):
...
var layout = {
barmode: 'group'
}
...
这将产生以下输出:
图 5.22 – 两个跟踪的组模式条形图
您可以在制作条形图时指定许多其他选项,但我们在本节中不会覆盖所有选项。您可以在官方文档中查看所有配置选项,以及创建良好条形图的清晰示例:plotly.com/javascript/bar-charts/
。
在下一节中,我们将简要介绍气泡图。
创建气泡图
气泡图是另一种非常受欢迎的图表类型,可用于覆盖信息。它基本上是散点图的扩展,指定了点的大小。让我们看下面的代码示例:
var trace1 = {
x: [1, 2, 3, 4],
y: [10, 11, 12, 13],
mode: 'markers',
marker: {
size: [40, 60, 80, 100]
}
};
var data = [trace1]
Plotly.newPlot(this_div(), data, layout);
运行上述代码单元格会产生以下输出:
图 5.23 – 一个简单的气泡图
在气泡图的上一个代码片段中,您可以看到主要更改是模式和指定大小的标记。大小与点一一映射,如果要为每个气泡应用大小,必须指定大小。
您还可以通过传递颜色数组来更改单个气泡的颜色,如下面的代码片段所示:
...
marker: {
size: [40, 60, 80, 100],
color: ['rgb(93, 164, 214)', 'rgb(255, 144, 14)', 'rgb(44, 160, 101)', 'rgb(255, 65, 54)'],
}
...
运行上述代码单元格会产生以下输出:
图 5.24 – 一个简单的气泡图,带有不同的颜色
气泡图非常有用,如果您需要了解更多信息或查看一些更高级的示例,可以在 Plotly 的文档中查看示例页面:plotly.com/javascript/bubble-charts/
。
您可以在制作许多其他类型的基本图表,但遗憾的是我们无法覆盖所有。Plotly 文档中的基本图表页面(plotly.com/javascript/basic-charts/
)是学习如何制作这些出色图表的好地方,我们鼓励您去看一看。
在下一节中,我们将向您介绍一些统计图表。
使用 Plotly.js 创建统计图表
统计图表是统计学家或数据科学家主要使用的不同类型的图表,用于传达信息。统计图的一些示例包括直方图、箱线图、小提琴图、密度图等。在下一小节中,我们将简要介绍三种类型的统计图表-直方图、箱线图和小提琴图。
使用 Plotly.js 创建直方图图表
直方图用于表示数值/连续数据的分布或传播。直方图类似于条形图,有时人们可能会混淆两者。区分它们的简单方法是它们可以显示的数据类型。直方图使用连续变量而不是分类变量,并且只需要一个值作为数据。
在下面的代码片段中,我们展示了一个使用生成的随机数的直方图示例:
var x = [];
for (let i = 0; i < 1000; i ++) { //generate random numbers
x[i] = Math.random();
}
var trace = {
x: x,
type: 'histogram',
};
var data = [trace];
Plotly.newPlot(this_div(), data);
在上述代码片段中,您将观察到trace
属性仅指定了x
数据。这符合我们之前提到的内容-直方图只需要一个值。我们还指定绘图类型为histogram
,运行代码单元格会产生以下输出:
图 5.25 – 具有随机 x 值的直方图
指定y
值而不是x
将导致水平直方图,如下例所示:
...
var trace = {
y: y,
type: 'histogram',
};
...
运行上述代码单元格会产生以下输出:
图 5.26 – 具有随机 y 值的直方图
您还可以通过创建多个踪迹并将barmode
属性设置为stack
来创建堆叠、叠加或分组的直方图,如下例所示:
var x1 = [];
var x2 = [];
for (var i = 0; i < 1000; i ++) {
x1[i] = Math.random();
x2[i] = Math.random();
}
var trace1 = {
x: x1,
type: "histogram",
};
var trace2 = {
x: x2,
type: "histogram",
};
var data = [trace1, trace2];
var layout = {barmode: "stack"};
Plotly.newPlot(this_div(), data, layout);
运行上述代码单元格会产生以下输出:
图 5.27 – 堆叠模式下的直方图
通过改变barmode
叠加,我们得到以下输出:
…
var layout = {barmode: "overlay"};
…
运行上述代码单元格会产生以下输出:
图 5.28 – 叠加模式下的直方图
要查看更多关于绘制直方图以及各种配置选项的示例,您可以在这里查看直方图文档页面:plotly.com/javascript/histograms/
。
在下一节中,我们将介绍箱线图。
使用 Plotly.js 创建箱线图
箱线图是描述性统计中非常常见的一种图表类型。它使用四分位数图形地呈现数值数据组。箱线图还有延伸在其上下的线,称为须。须代表上下四分位数之外的变异性。
提示
四分位数将指定数量的数据点分为四部分或四分之一。第一四分位数是最低的 25%数据点,第二四分位数在 25%和 50%之间(达到中位数),第三四分位数在 50%到 75%之间(高于中位数),最后,第四四分位数表示最高的 25%数字。
以下的图表可以帮助你更好地理解箱线图:
图 5.29 – 描述箱线图的图表(来源:重新绘制自 https://aiaspirant.com/box-plot/)
在 Plotly.js 中,我们通过传递数据并将trace
类型设置为box
来制作箱线图。我们在下面的例子中演示了这一点:
var y0 = [];
var y1 = [];
for (var i = 0; i < 50; i ++) {//generate some random numbers
y0[i] = Math.random();
y1[i] = Math.random() + 1;
}
var trace1 = {
y: y0,
type: 'box'
};
var trace2 = {
y: y1,
type: 'box'
};
var data = [trace1, trace2];
Plotly.newPlot(this_div(), data);
运行上述代码单元格会产生以下输出:
图 5.30 – 一个简单的箱线图,有两个踪迹
我们可以将箱线图的布局配置为水平格式,而不是默认的垂直格式。在下一节中,我们将演示如何做到这一点。
制作水平箱线图
通过在踪迹中指定x
值而不是y
值,您可以制作水平图。我们在下面的代码片段中演示了这一点:
var x0 = [];
var x1 = [];
for (var i = 0; i < 50; i ++) {
x0[i] = Math.random();
x1[i] = Math.random() + 1;
}
var trace1 = {
x: x0,
type: 'box'
};
var trace2 = {
x: x1,
type: 'box'
};
var data = [trace1, trace2];
Plotly.newPlot(this_div(), data);
运行上述代码单元格会产生以下输出:
图 5.31 – 一个简单的箱线图,有两个踪迹
您还可以制作分组的箱线图,如下一节所示。
制作分组的箱线图
多个共享相同x轴的踪迹可以被分组成一个单独的箱线图,如下面的代码片段所示:
var x = ['Season 1', 'Season 1', 'Season 1', 'Season 1', 'Season 1', 'Season 1',
'Season 2', 'Season 2', 'Season 2', 'Season 2', 'Season 2', 'Season 2']
var trace1 = {
y: [2, 2, 6, 1, 5, 4, 2, 7, 9, 1, 5, 3],
x: x,
name: 'Blues FC',
marker: {color: '#3D9970'},
type: 'box'
};
var trace2 = {
y: [6, 7, 3, 6, 0, 5, 7, 9, 5, 8, 7, 2],
x: x,
name: 'Reds FC',
marker: {color: '#FF4136'},
type: 'box'
};
var trace3 = {
y: [1, 3, 1, 9, 6, 6, 9, 1, 3, 6, 8, 5],
x: x,
name: 'Greens FC',
marker: {color: '#FF851B'},
type: 'box'
};
var data = [trace1, trace2, trace3];
var layout = {
yaxis: {
title: 'Points in two seasons',
},
boxmode: 'group'
};
Plotly.newPlot(this_div(), data, layout);
运行上述代码单元格会产生以下输出:
图 5.32 – 三个踪迹分组在一起的箱线图
您可以在制作箱线图时设置许多其他选项,但我们将让您在此处阅读更多关于它们的箱线图文档:plotly.com/javascript/box-plots/
。
在下一节中,我们将简要介绍小提琴图。
使用 Plotly.js 创建小提琴图
小提琴图是箱线图的扩展。它也使用四分位数描述数据点,就像箱线图一样,只有一个主要区别——它还显示数据的分布。
以下图表显示了小提琴图和箱线图之间的共同特性:
图 5.33 - 小提琴图和箱线图之间的共同特性(重新绘制自 https://towardsdatascience.com/violin-plots-explained-fb1d115e023d)
小提琴图的曲线区域显示了数据的基础分布,并传达了比箱线图更多的信息。
在 Plotly 中,您可以通过将类型更改为violin
来轻松制作小提琴图。例如,在下面的代码片段中,我们正在重用箱线图部分的代码,只有两个主要更改:
var x = ['Season 1', 'Season 1', 'Season 1', 'Season 1', 'Season 1', 'Season 1',
'Season 2', 'Season 2', 'Season 2', 'Season 2', 'Season 2', 'Season 2']
var trace1 = {
y: [2, 2, 6, 1, 5, 4, 2, 7, 9, 1, 5, 3],
x: x,
name: 'Blues FC',
marker: {color: '#3D9970'},
type: 'violin'
};
var trace2 = {
y: [6, 7, 3, 6, 0, 5, 7, 9, 5, 8, 7, 2],
x: x,
name: 'Reds FC',
marker: {color: '#FF4136'},
type: 'violin'
};
var trace3 = {
y: [1, 3, 1, 9, 6, 6, 9, 1, 3, 6, 8, 5],
x: x,
name: 'Greens FC',
marker: {color: '#FF851B'},
type: 'violin',
};
var data = [trace1, trace2, trace3];
var layout = {
yaxis: {
title: 'Points in two seasons',
},
violinmode: 'group'
};
Plotly.newPlot(this_div(), data, layout);
运行上述代码单元格将得到以下输出:
图 5.34 - 三个迹线组合在一起的小提琴图
就像其他图表类型一样,您也可以配置小提琴图的显示方式。例如,我们可以在小提琴图中显示基础箱线图,如下面的代码片段所示:
var x = ['Season 1', 'Season 1', 'Season 1', 'Season 1', 'Season 1', 'Season 1',
'Season 2', 'Season 2', 'Season 2', 'Season 2', 'Season 2', 'Season 2']
var trace = {
y: [1, 3, 1, 9, 6, 6, 9, 1, 3, 6, 8, 5],
x: x,
name: 'Greens FC',
marker: {color: '#FF851B'},
type: 'violin',
box: {
visible: true
},
};
var data = [trace];
var layout = {
yaxis: {
title: 'Point in two seasons',
},
};
Plotly.newPlot(this_div(), data, layout);
运行上述代码单元格将得到以下输出:
图 5.35 - 显示基础箱线图的小提琴图
要查看其他配置选项以及一些高级设置,您可以在这里查看小提琴图的文档:plotly.com/javascript/violin/
。
通过这样,我们已经结束了对 Plotly.js 的介绍部分。在下一章中,我们将向您展示如何使用 Danfo.js 快速轻松地为这个特定库支持的任何类型的数据制作图表。
摘要
在本章中,我们介绍了使用 Plotly.js 进行绘图和可视化。首先,我们简要介绍了 Plotly.js,包括安装设置。然后,我们转向图表配置和布局定制。最后,我们向您展示了如何创建一些基本和统计图表。
您在本章中所学到的知识将帮助您轻松创建交互式图表,您可以将其嵌入到您的网站或 Web 应用程序中。
在下一章中,我们将介绍使用 Danfo.js 进行数据可视化,您将看到如何利用 Plotly.js 的知识,可以直接从您的 DataFrame 或 Series 轻松创建令人惊叹的图表。
第七章:使用 Danfo.js 进行数据可视化
在前一章中,您学会了如何使用 Plotly.js 创建丰富和交互式的图表,可以嵌入到任何 Web 应用程序中。我们还提到了 Danfo.js 如何在内部使用 Plotly 来直接在 DataFrame 或 Series 上制作图表。在本章中,我们将向您展示如何使用 Danfo.js 绘图 API 轻松创建这些图表。具体来说,在本章中,我们将涵盖以下内容:
-
设置 Danfo.js 进行绘图
-
使用 Danfo.js 创建折线图
-
使用 Danfo.js 创建散点图
-
使用 Danfo.js 创建箱线图和小提琴图
-
使用 Danfo.js 创建直方图
-
使用 Danfo.js 创建条形图
技术要求
为了跟随本章的内容,您应该具备以下条件:
-
现代浏览器,如 Chrome、Safari、Opera 或 Firefox
-
Node.js、Danfo.js,以及可选的 Dnotebook 已安装在您的系统上。
-
稳定的互联网连接以下载数据集
有关 Danfo.js 的安装说明可以在第三章中找到,使用 Danfo.js 入门,而有关 Dnotebook 的安装步骤可以在第二章中找到,Dnotebook-用于 JavaScript 的交互式计算环境。
设置 Danfo.js 进行绘图
默认情况下,Danfo.js 提供了一些基本的图表类型。这些图表可以在任何 DataFrame 或 Series 对象上调用,如果传递了正确的参数,它将显示相应的图表。
在撰写本文时,Danfo.js 附带以下图表:
-
折线图
-
箱线图和小提琴图
-
表格
-
饼图
-
散点图
-
条形图
-
直方图
这些图表通过plot
函数公开。也就是说,如果您有一个 DataFrame 或 Series 对象,在它们上调用plot
函数将公开这些图表。
plot
方法需要一个div
ID,用于显示图表。例如,假设df
是一个 DataFrame,我们可以按照下面的代码片段调用plot
函数:
const df = new DataFrame({...})
df.plot("my_div_id").<chart type>
图表类型可以是line
、bar
、scatter
、hist
、pie
、box
、violin
或table
。
每种图表类型都将接受特定于图表的参数,但它们都共享一个名为config
的公共参数。config
对象用于自定义图表以及布局。将config
参数视为我们在第五章中使用的布局和配置属性的组合。
config
参数是一个具有以下格式的对象:
config = {
layout: {…}, // plotly layout parameters like title, font, e.t.c.
... // other Plotly configuration parameters like showLegend, displayModeBar, e.t.c.
}
在接下来的部分中,我们将展示使用不同图表类型的一些示例,以及如何配置它们。
注意
在接下来的部分中,我们将下载并使用两个真实世界的数据集。这意味着您需要互联网连接来下载数据集。
将 Danfo.js 添加到您的代码中
要使用 Danfo.js 进行绘图,您需要将其添加到您的项目中。如果您正在使用我们的示例中将要使用的 Dnotebook,则可以使用load_package
函数加载 Danfo.js 和 Plotly.js,如下面的代码片段所示:
load_package(["https://cdn.plot.ly/plotly-1.58.4.min.js","https://cdn.jsdelivr.net/npm/[email protected]/lib/bundle.min.js"])
上述代码将在 Dnotebook 中安装 Danfo.js 和 Plotly.js。Danfo.js 使用安装的 Plotly.js 来制作图表。除非显式加载 Plotly,否则图表将无法工作。
注意
较旧版本的 Danfo.js(0.2.3 之前)附带了 Plotly.js。在新版本中已经删除,如此处显示的发布说明中所述:danfo.jsdata.org/release-notes#latest-release-node-v-0-2-5-browser-0-2-4
。
如果您在 HTML 文件中制作图表,请确保在头部添加script
标签,如下面的代码片段所示:
...
<head>
<script src="img/plotly-1.2.0.min.js"></script>
<script src="img/bundle.min.js"></script>
</head>
...
最后,在诸如 React 或 Vue 之类的 UI 库中,确保通过 npm 或 yarn 等包管理器安装 Danfo.js 和 Plotly.js。
下载数据集以绘制
在本节中,我们将下载一个真实的金融数据集,这个数据集将用于我们所有的示例。在 Dnotebook 中,您可以在顶部单元格中下载数据集,并在其他单元格中使用如下:
var financial_df;
dfd.read_csv("https://raw.githubusercontent.com/plotly/datasets/master/finance-charts-apple.csv")
.then(data => {
financial_df = data
})
注意
确保使用var
声明financial_df
。这样可以使financial_df
在 Dnotebook 的每个单元格中都可用。如果在 React 或纯 HTML 中工作,则建议使用let
或const
。
我们可以使用head
函数和table
来显示financial_df
的前五行,如下面的代码片段所示:
table(financial_df.head())
运行上述代码会产生以下输出:
图 6.1 - 表格显示金融数据集的前五行
现在我们有了数据集,我们可以开始制作一些有趣的图表。首先,我们将从一个简单的折线图开始。
使用 Danfo.js 创建折线图
折线图是简单的图表类型,主要用于 Series 数据或单列。它们可以显示数据点的趋势。要在单列上制作折线图 - 比如,在financial_df
中的AAPL.Open
,我们可以这样做:
var layout = {
yaxis: {
title: 'AAPL open points',
}
}
var config = {
displayModeBar: false,
layout
}
financial_df ['AAPL.Open'].plot(this_div()).line(config)
运行上述代码会产生以下输出:
图 6.2 - 金融数据集的前五行
请注意,我们使用 DataFrame 子集(financial_df["column name"]
)来获取单个列 - AAPl.Open
- 作为一个 Series。然后,我们调用.line
图表类型并传入一个config
对象。config
对象接受layout
属性以及 Danfo.js 和 Plotly 使用的其他参数。
如果要绘制特定列,可以将列名数组传递给config
参数,如下面的代码片段所示:
var layout = {
yaxis: {
title: 'AAPL open points',
}
}
var config = {
columns: ["AAPL.Open", "AAPL.Close"],
displayModeBar: true,
layout
}
financial_df.plot(this_div()).line(config)
运行上述代码会产生以下输出:
图 6.3 - 将两列绘制为折线图
默认情况下,图表的x轴是 DataFrame 或 Series 的索引。对于financial_df
DataFrame,当我们使用read_csv
函数下载数据集时,索引是自动生成的。如果要更改索引,可以使用set_index
函数,如下面的代码片段所示:
var new_df = financial_df.set_index({key: "Date"})
table(new_df.head())
输出如下:
图 6.4 - 表格显示前五行,索引设置为日期
如果我们制作与之前相同的图表,我们会发现x轴会自动格式化为日期:
图 6.5 - 两列(AAPL.open,AAPL.close)针对日期索引的图表
您可以通过将它们传递到layout
属性或config
对象的主体中来指定其他 Plotly 配置选项,例如宽度、字体等。例如,要配置字体、文本大小、布局宽度,甚至添加自定义按钮到您的图表,您可以这样做:
var layout = {
... legend: {bgcolor: "#fcba03",
bordercolor: "#444",
borderwidth: 1,
font: {family: "Arial", size: 10, color: "#fff"}
},
...}
var config = {
columns: ["AAPL.Open", "AAPL.Close"],
displayModeBar: true,
modeBarButtonsToAdd: [{
name: 'about',
icon: Plotly.Icons.question,
click: function (gd) {
alert('An example of configuring Danfo.Js Plots')
}
}] ,
layout
}
new_df.plot(this_div()).line(config)
运行上述代码单元格会产生以下输出:
图 6.6 - 具有各种配置以及指定布局属性的折线图
有了上述信息,您就可以开始从数据集中制作漂亮的折线图了。在下一节中,我们将介绍 Danfo.js 中可用的另一种类型的图表 - 散点图。
使用 Danfo.js 创建散点图
我们可以通过将绘图类型指定为scatter
来轻松制作散点图。例如,使用前一节中的代码,使用 Danfo.js 创建折线图,我们只需将绘图类型从line
更改为scatter
,就可以得到所选列的散点图,如下面的代码块所示:
var layout = {
title: "Time series plot of AAPL open and close points",
width: 1000,
yaxis: {
title: 'AAPL open points',
},
xaxis: {
title: 'Date',
}
}
var config = {
columns: ["AAPL.Open", "AAPL.Close"],
layout
}
new_df.plot(this_div()).scatter(config)
运行上述代码单元格会产生以下输出:
图 6.7 - 两列的散点图
如果您需要在 DataFrame 中指定两个特定列之间的散点图,可以在config
对象中指定x
和y
值,如下面的代码所示:
var layout = {
title: "Time series plot of AAPL open and close points",
width: 1000,
yaxis: {
title: 'AAPL open points',
},
xaxis: {
title: 'Date',
}
}
var config = {
x: "AAPL.Low",
y: "AAPL.High",
layout
}
new_df.plot(this_div()).scatter(config)
运行上述代码单元格会产生以下输出:
图 6.8 - 明确指定 x 和 y 列的散点图
要自定义布局或设置config
,您可以将相应的选项传递给config
对象,就像我们在使用 Danfo.js 创建折线图部分中所做的那样。
在下一节中,我们将简要介绍两种类似的图表类型 - 箱线图和小提琴图。
使用 Danfo.js 创建箱线图和小提琴图
箱线图和小提琴图非常相似,通常会使用相同的参数。因此,我们将在本节中同时介绍它们。
在以下示例中,我们将首先制作一个箱线图,然后仅通过更改绘图类型选项将其更改为小提琴图。
为系列创建箱线图和小提琴图
要为系列或 DataFrame 中的单个列创建箱线图,首先,我们要对其进行子集化以获取系列,然后我们将在其上调用绘图类型,如下面的代码片段所示:
var layout = {
title: "Box plot on a Series",
}
var config = {
layout
}
new_df["AAPL.Open"].plot(this_div()).box(config)
运行上述代码单元格会产生以下输出:
图 6.9 - 系列的箱线图
现在,为了将前述图更改为小提琴图,您只需将绘图类型更改为violin
,如下面的代码片段所示:
...
new_df["AAPL.Open"].plot(this_div()).violin(config)
…
运行上述代码单元格会产生以下输出:
图 6.10 - 系列的小提琴图
当我们需要一次为多个列制作箱线图时会发生什么?好吧,在下一节中,我们将向您展示。
多列的箱线图和小提琴图
为了在 DataFrame 中为多个列创建箱线图/小提琴图,您可以将列名数组传递给绘图,就像我们在下面的代码片段中演示的那样:
var layout = {
title: "Box plot on multiple columns",
}
var config = {
columns: ["AAPL.Open", "AAPL.Close", "AAPL.Low", "AAPL.High"],
layout
}
new_df.plot(this_div()).box(config)
运行上述代码单元格会产生以下输出:
图 6.11 - 一次绘制多列的箱线图
通过重用先前的代码,我们可以通过更改绘图类型轻松将箱线图更改为小提琴图,如下所示:
…
new_df.plot(this_div()).violin(config)
...
我们得到以下输出:
图 6.12 - 一次绘制多列的小提琴图
最后,如果我们想要指定x
和y
值会发生什么?我们将在下一节中展示这一点。
具体的 x 和 y 值的箱线图和小提琴图
我们可以使用特定的x
和y
值制作箱线图和小提琴图。x
和y
值是必须在 DataFrame 中存在的列名。
注意
建议箱线图的x
值是分类的,即具有固定数量的类别。这样可以确保可解释性。
在以下示例中,我们将向您展示如何明确指定x
和y
值到一个图中:
var layout = {
title: "Box plot on x and y values",
}
var config = {
x: "direction",
y: "AAPL.Open",
layout
}
new_df.plot(this_div()).box(config)
运行上述代码单元格会产生以下输出:
图 6.13 - 从特定 x 和 y 值绘制箱线图
请注意,x
值是一个名为direction
的分类变量。该列有两个固定的类别 - Increasing
和Decreasing
。
和往常一样,我们可以通过更改类型获得相应的小提琴图:
...
new_df.plot(this_div()).violin(config)
…
运行上述代码单元格会产生以下输出:
图 6.14 - 从特定 x 和 y 值绘制小提琴图
现在,如果我们为x
和y
同时指定了连续值会发生什么?好吧,在以下示例中让我们找出来:
var layout = {
title: "Box plot on two continuous variables",
}
var config = {
x: "AAPL.Low",
y: "AAPL.Open",
layout
}
new_df.plot(this_div()).box(config)
运行上述代码单元格会产生以下输出:
图 6.15 - 两个连续变量的箱线图
从上述输出可以看出,图表变得几乎无法解释,并且无法实现使用箱线图/小提琴图的目标。因此,建议对于分类x
值使用箱线图/小提琴图。
在下一节中,我们将介绍用于制作直方图的hist
绘图类型。
使用 Danfo.js 创建直方图
正如我们之前解释的,直方图是数据分布的表示。绘图命名空间提供的hist
函数可以用于从 DataFrame 或 Series 制作直方图,我们将在下一节中进行演示。
从 Series 创建直方图
要从 Series 创建直方图,可以在 Series 上调用hist
函数,或者如果在 DataFrame 上绘图,可以使用列名对 DataFrame 进行子集化,如下面的示例所示:
var layout = {
title: "Histogram on a Series data",
}
var config = {
layout
}
new_df["AAPL.Open"].plot(this_div()).hist(config)
运行上述代码单元格会得到以下输出:
图 6.16 - Series 数据的直方图
接下来,我们将一次为 DataFrame 中的多个列制作直方图。
从多列创建直方图
如果要为 DataFrame 中的多个列制作直方图,可以将列名作为列名数组传递,如下面的代码片段所示:
var layout = {
title: "Histogram of two columns",
}
var config = {
columns: ["dn", "AAPL.Adjusted"],
layout
}
new_df.plot(this_div()).hist(config)
运行上述代码单元格会得到以下输出:
图 6.17 - 两列的直方图
如果需要指定单个值x
或y
来生成直方图,可以将x
或y
值传递给config
对象。
注意
一次只能指定x
或y
中的一个。这是因为直方图是一种单变量图表。因此,如果指定了x
值,直方图将是垂直的,如果指定了y
,它将是水平的。
在下面的示例中,通过指定y
值制作了水平直方图:
var layout = {
title: "A horizontal histogram",
}
var config = {
y: "dn",
layout
}
new_df.plot(this_div()).hist(config)
运行上述代码单元格会得到以下输出:
图 6.18 - 水平直方图
默认情况下,直方图是垂直的,相当于设置了x
参数。
在下一节中,我们将介绍条形图。
使用 Danfo.js 创建条形图
条形图以矩形条形呈现分类数据,其长度与它们代表的值成比例。
bar
函数也可以在plot
命名空间上调用,并且还可以应用各种配置选项。在接下来的几节中,我们将演示如何从 Series 以及具有多个列的 DataFrame 创建条形图。
从 Series 创建条形图
要从 Series 制作简单的条形图,可以执行以下操作:
var layout = {
title: "A simple bar chart on a series",
}
var config = {
layout
}
new_df["AAPL.Volume"].plot(this_div()).bar(config)
运行上述代码单元格会得到以下输出:
图 6.19 - Series 上的条形图
从上图可以看出,我们有大量的条形。这是因为AAPL.Volume
列是一个连续变量,每个点都创建了一个条形。
为了避免这种无法解释的图表,建议对具有固定数量的数值类别的变量使用条形图。我们可以通过创建一个简单的 Series 来演示这一点,如下面的代码所示:
custom_sf = new dfd.Series([1, 3, 2, 6, 10, 34, 40, 51, 90, 75])
custom_sf.plot(this_div()).bar(config)
运行上述代码单元格会得到以下输出:
图 6.20 - 具有固定值的 Series 上的条形图
在下一节中,我们将向您展示如何从指定的列名列表中制作分组条形图。
从多列创建条形图
要从列名列表创建分组条形图,可以将列名传递给config
对象,如下面的示例所示:
var layout = {
title: "A bar chart on two columns",
}
var config = {
columns: ["price", "cost"],
layout
}
var df = new dfd.DataFrame({'price': [20, 18, 489, 675, 1776],
'cost': [40, 22, 21, 60, 19],
'count': [4, 25, 281, 600, 1900]},
{index: [1990, 1997, 2003, 2009, 2014]})
df.plot(this_div()).bar(config)
运行上述代码单元格会得到以下输出:
图 6.21 - 两列的条形图
请注意,在前面的示例中,我们创建了一个新的 DataFrame。这是因为金融数据集不包含条形图所需的数据类型,正如我们之前所说的。
这就是本章的结束。恭喜您走到了这一步!我们相信您已经学到了很多,并且可以在个人项目中应用在这里获得的知识。
总结
在本章中,我们介绍了使用 Danfo.js 进行绘图和可视化。首先,我们向您展示了如何在新项目中设置 Danfo.js 和 Plotly,然后继续下载数据集,将其加载到 DataFrame 中。接下来,我们向您展示了如何创建基本图表,如折线图、条形图和散点图,然后是统计图表,如直方图以及箱线图和小提琴图。最后,我们向您展示了如何配置使用 Danfo.js 创建的图表。
在本章和第五章《使用 Plotly.js 进行数据可视化》中所获得的知识将在创建数据驱动的应用程序以及自定义仪表板时发挥实际作用。
在下一章中,您将学习有关数据聚合和分组操作,从而了解如何执行数据转换,如合并、连接和串联。
第八章:数据聚合和分组操作
对分组数据进行groupby
操作(聚合或转换)以生成一组新值。然后将结果值组合成单个数据组。
这种方法通常被称为split-apply-combine。这个术语实际上是由 Hadley Wickham 创造的,他是许多流行的R包的作者,用来描述分组操作。图 7.1以图形方式描述了 split-apply-combine 的概念:
图 7.1 – groupby 说明
在本章中,我们将探讨执行分组操作的方法:如何按列键对数据进行分组,并在分组数据上联合或独立地执行数据聚合。
本章还将展示如何通过键访问分组数据。它还提供了如何为您的数据创建自定义聚合函数的见解。
本章将涵盖以下主题:
-
数据分组
-
遍历分组数据
-
使用
.apply
方法 -
分组数据的数据聚合
技术要求
为了跟随本章,您应该具有以下内容:
-
像 Chrome、Safari、Opera 或 Firefox 这样的现代浏览器
-
Node.js、Danfo.js和Dnotebook已安装在您的系统上
本章的代码在此处可用:github.com/PacktPublishing/Building-Data-Driven-Applications-with-Danfo.js/tree/main/Chapter07
。
数据分组
Danfo.js 只提供了通过特定列中的值对数据进行分组的能力。对于当前版本的 Danfo.js,分组的指定列数只能是一列或两列。
在本节中,我们将展示如何按单个列和双列进行分组。
单列分组
首先,让我们通过创建一个 DataFrame 然后按单个列对其进行分组来开始:
let data = { 'A': [ 'foo', 'bar', 'foo', 'bar',
'foo', 'bar', 'foo', 'foo' ],
'C': [ 1, 3, 2, 4, 5, 2, 6, 7 ],
'D': [ 3, 2, 4, 1, 5, 6, 7, 8 ] };
let df = new dfd.DataFrame(data);
let group_df = df.groupby([ "A"]);
上述代码涉及以下步骤:
-
首先,我们使用
object
方法创建一个 DataFrame。 -
然后我们调用
groupby
方法。 -
然后,我们指定 DataFrame 应该按列
A
进行分组。
df.groupby(['A'])
返回一个groupby
数据结构,其中包含对数据进行分组所需的所有必要方法。
我们可以决定对由A
分组的所有列执行数据操作,或者指定任何其他列。
上述代码输出以下表:
图 7.2 – DataFrame
在下面的代码中,我们将看到如何对分组数据执行一些常见的groupby
操作:
group_df.mean().print()
使用上述代码片段中创建的groupby_df
操作,我们调用groupby
的mean
方法。该方法计算每个组的平均值,如下图所示:
图 7.3 – groupby DataFrame
下图以图形方式显示了上述代码的操作以及如何生成上述表的输出:
图 7.4 – groupby 方法的图形描述
根据本章开头讨论的 split-apply-combine 方法,df.groupby(['A'])
将 DataFrame 分成两个键-foo
和bar
。
将 DataFrame 分组为foo
和bar
键后,其他列(C
和D
)中的值分别根据它们的行对齐分配给每个键。为了强调这一点,如果我们选择bar
键,从图 7.2中,我们可以看到列C
在bar
行中有三个数据点(3
、4
、2
)。
因此,如果我们要执行数据聚合操作,比如计算分配给bar
键的数据的平均值,分配给bar
键的列C
的数据点将具有平均值3
,这对应于图 7.3中的表。
注意
与前面段落中描述的相同操作发生在foo
键上,所有其他数据点都被分配
如我之前所说,这次对分组均值方法的调用适用于所有按A
分组的列。让我们选择一个特定应用组操作的列,如下所示:
let col_c = group_df.col(['C'])
col_c.sum().print()
首先,我们调用groupby
中的col
方法。该方法接受一个列名的数组。上述代码的主要目的是获取每个分组键(foo
和bar
)的列C
的总和,这给出以下表格输出:
图 7.5 – 列 C 的分组操作
同样的操作可以应用于分组数据中的任何其他列,如下所示:
let col_d = group_df.col(['D'])
col_d.count().print()
这段代码片段遵循与上述代码相同的方法,只是将count
groupby
操作应用于列D
,这给出了以下输出:
图 7.6 – 列 D 的 groupby“count”操作
DataFrame 也可以按两列进行分组;单列分组所示的操作也适用于双列分组,我们将在下一小节中看到。
双列分组
首先,让我们创建一个 DataFrame 并添加一个额外的列,如下所示:
let data = { 'A': [ 'foo', 'bar', 'foo', 'bar',
'foo', 'bar', 'foo', 'foo' ],
' B': [ 'one', 'one', 'two', 'three',
'two', 'two', 'one', 'three' ],
'C': [ 1, 3, 2, 4, 5, 2, 6, 7 ],
'D': [ 3, 2, 4, 1, 5, 6, 7, 8 ] };
let df = new dfd.DataFrame(data);
let group_df = df.groupby([ "A", "B"]);
我们添加了一个额外的列B
,其中包含分类数据 - one
,two
和three
。DataFrame 按列A
和B
进行分组,如下表所示:
图 7.7 – 包含列 B 的 DataFrame
我们也可以像单列分组一样计算平均值,如下所示:
group_df.mean().print()
这将根据列A
和B
的分组键对列C
和D
应用平均值。例如,在图 7.7中,我们可以看到我们有一个来自列A
和B
的分组键,名为(foo
,one
)。这个键在数据中出现了两次。
上述代码输出以下表格:
图 7.8 – 按 A 和 B 分组的 groupby 均值 DataFrame
在图 7.7中,列C
的值为1
和6
,属于(foo
,one
)键。同样,D
的值为3
和7
,属于相同的键。如果我们计算平均值,我们会发现它对应于图 7.8中的第一列。
我们还可以继续按列A
和B
对分组的一列进行求和。让我们选择列C
:
let col_c = group_df.col(['C']);
col_c.sum().print();
我们从分组数据中获取了列C
,然后计算了每个分组键的总和,如下表所示:
图 7.9 – 每组列 C 的总和
在本节中,我们看到了如何按单列或双列分组数据。我们还研究了执行数据聚合和访问分组 DataFrame 的列数据。在下一节中,我们将看看如何访问按分组键分组的数据。
遍历分组数据
在本节中,我们将看到如何根据分组键访问分组数据,循环遍历这些分组数据,并对其执行数据聚合操作。
通过单列和双列分组数据进行迭代
在本节中,我们将看到 Danfo.js 如何提供迭代通过groupby
操作创建的每个组的方法。这些数据是按groupby
列中包含的键进行分组的。
键存储为一个名为data_tensors
的类属性中的字典或对象。该对象包含分组键作为其键,并将与键关联的 DataFrame 数据存储为对象值。
使用上一个 DataFrame,让我们按列A
进行分组,然后遍历data_tensors
:
let group_df = df.groupby([ "A"]);
console.log(group_df.data_tensors)
我们按列A
对 DataFrame 进行分组,然后打印出data_tensors
,如下截图所示:
图 7.10 – data_tensors 输出
图 7.10包含了关于data_tensors
属性包含的更详细信息,但整个内容可以总结为以下结构:
{
foo: DataFrame,
bar: DataFrame
}
键是列A
的值,键的值是与这些键关联的 DataFrame。
我们可以迭代data_tensors
并打印出DataFrame
表格,以查看它们包含的内容,如下面的代码所示:
let grouped_data = group_df.data_tensors;
for (let key in grouped_data) {
grouped_data[key].print();
}
首先,我们访问data_tensors
,这是一个groupby
类属性,并将其赋值给一个名为grouped_data
的变量。然后我们循环遍历grouped_data
,访问每个键,并将它们对应的 DataFrame 打印为表格,如下面的截图所示:
图 7.11 – groupby 键和它们的 DataFrame
此外,我们可以将与前面代码相同的方法应用到由两列分组的数据上,如下所示:
let group_df = df.groupby([ "A", "B"]);
let grouped_data = group_df.data_tensors;
for (let key in grouped_data) {
let key_data = grouped_data[key];
for (let key2 in key_data) {
grouped_data[key][key2].print();
}
}
以下是我们在前面的代码片段中遵循的步骤:
-
首先,
df
DataFrame 按两列(A
和B
)进行分组。 -
我们将
data_tensors
赋值给一个名为grouped_data
的变量。 -
我们循环遍历
grouped_data
以获取键。 -
我们循环遍历
grouped_data
对象,也循环遍历其内部对象(key_data
)每个键,由于为grouped_data
生成的对象数据格式,如下所示:
{
foo : {
one: DataFrame,
two: DataFrame,
three: DataFrame
},
bar: {
one: DataFrame,
two: DataFrame,
three: DataFrame
}
}
代码片段给我们提供了以下输出:
图 7.12 – DataFrame 的两列分组输出
在本节中,我们学习了如何迭代分组数据。我们看到了data_tensor
的对象格式如何根据数据的分组方式而变化,无论是单列还是双列。
我们看到了如何迭代data_tensor
以获取键和它们关联的数据。在下一小节中,我们将看到如何在不手动循环data_tensor
的情况下获取与每个键关联的数据。
使用get_groups
方法
Danfo.js 提供了一个名为get_groups()
的方法,可以轻松访问每个键值 DataFrame,而无需循环遍历data_tensors
对象。每当我们需要访问属于一组键组合的特定数据时,这将非常方便。
让我们从单列分组开始,如下所示:
let group_df = df.groupby([ "A"]);
group_df.get_groups(["foo"]).print()
我们按列A
进行分组,然后调用get_groups
方法。get_groups
方法接受一个键组合作为数组。
对于单列分组,我们只有一个键组合。因此,我们传入名为foo
的其中一个键,然后打印出相应的分组数据,如下面的截图所示:
图 7.13 – foo 键的 get_groups
同样的方法也可以应用于所有其他键,如下所示:
group_df.get_groups(["bar"]).print()
前面的代码给我们提供了以下输出:
图 7.14 – bar 键的 get_groups
与单列groupby
相同的方法也适用于两列分组:
let group_df = df.groupby([ "A", "B"]);
group_df.get_groups(["foo","one"]).print()
请记住,get_groups
方法接受键的组合作为数组。因此,对于两列分组,我们传入要使用的列A
和B
的键组合。因此,我们获得以下输出:
图 7.15 – 获取 foo 键和一个组合的 DataFrame
可以对任何其他键组合做同样的操作。让我们尝试bar
和two
键,如下所示:
group_df.get_groups(["bar","two"]).print()
我们获得以下输出:
图 7.16 – bar 和 two 键的 DataFrame
在本节中,我们介绍了如何迭代分组数据,数据可以按单列或双列进行分组。我们还看到了内部的data_tensor
数据对象是如何根据数据的分组方式进行格式化的。我们还看到了如何在不循环的情况下访问与每个分组键相关联的数据。
在下一节中,我们还将研究如何使用.apply 方法创建自定义数据聚合函数。
使用.apply 方法
在本节中,我们将使用.apply
方法创建自定义数据聚合函数,可以应用于我们的分组数据。
.apply
方法使得可以将自定义函数应用于分组数据。这是本章前面讨论的分割-应用-合并方法的主要函数。
Danfo.js 中实现的groupby
方法只包含了一小部分用于组数据的数据聚合方法,因此.apply
方法使用户能够从分组数据中构建特殊的数据聚合方法。
使用前面的数据,我们将创建一个新的 DataFrame,不包括前一个 DataFrame 中的列B
,然后创建一个将应用于分组数据的自定义函数:
let group_df = df.groupby([ "A"]);
const add = (x) => {
return x.add(2);
};
group_df.apply(add).print();
在前面的代码中,我们按列A
对 DataFrame 进行分组,然后创建一个名为add
的自定义函数,将值2
添加到分组数据中的所有数据点。
注意
要传递到这个函数的参数add
和类似的函数,如sub
、mul
和div
,可以是 DataFrame、Series、数组或整数值。
前面的代码生成了以下输出:
图 7.17 - 将自定义函数应用于组数据
让我们创建另一个自定义函数,从每个分组数据中减去最小值:
let data = { 'A': [ 'foo', 'bar', 'foo', 'bar',
'foo', 'bar', 'foo', 'foo' ],
'B': [ 'one', 'one', 'two', 'three',
'two', 'two', 'one', 'three' ],
'C': [ 1, 3, 2, 4, 5, 2, 6, 7 ],
'D': [ 3, 2, 4, 1, 5, 6, 7, 8 ] };
let df = new DataFrame(data);
let group_df = df.groupby([ "A", "B"]);
const subMin = (x) => {
return x.sub(x.min());
};
group_df.apply(subMin).print();
首先,我们创建了一个包含列A
、B
、C
和D
的 DataFrame。然后,我们按列A
和C
对 DataFrame 进行分组。
创建一个名为subMin
的自定义函数,用于获取分组数据的最小值,并从分组数据中的每个数据点中减去最小值。
然后,通过.apply 方法将这个自定义函数应用于group_df
分组数据,因此我们得到了以下输出表:
图 7.18 - subMin 自定义应用函数
如果我们看一下前面图中的表,我们可以看到一些组数据只出现一次,比如属于bar
和two
、bar
和one
、bar
和three
以及foo
和three
键的组数据。
前一个键的组数据只有一个项目,因此最小值也是组中包含的单个值;因此,C_apply
和D_apply
列的值为0
。
我们可以调整subMin
自定义函数,只有在键对有多行时才从每个值中减去最小值,如下所示:
const subMin = (x) => {
if (x.values.length > 1) {
return x.sub(x.min());
} else {
return x;
}
};
group_df.apply(subMin).print();
自定义函数给出了以下输出表:
图 7.19 - 自定义 apply 函数
下图显示了前面代码的图形表示:
图 7.20 - groupby 和 subMin apply 方法示例
.apply
方法还使我们能够对数据进行分组后的数据归一化处理。
在机器学习中,我们有所谓的标准化,它涉及将数据重新缩放到范围-1 和 1 之间。标准化的过程涉及从数据中减去数据的平均值,然后除以标准差。
使用前面的 DataFrame,让我们创建一个自定义函数,对数据进行标准化处理:
let data = { 'A': [ 'foo', 'bar', 'foo', 'bar',
'foo', 'bar', 'foo', 'foo' ],
'C': [ 1, 3, 2, 4, 5, 2, 6, 7 ],
'D': [ 3, 2, 4, 1, 5, 6, 7, 8 ] };
let df = new DataFrame(data);
let group_df = df.groupby([ "A"]);
// (x - x.mean()) / x.std()
const norm = (x) => {
return x.sub(x.mean()).div(x.std());
};
group_df.apply(norm).print();
在前面的代码中,我们首先按列A
对数据进行分组。然后,我们创建了一个名为norm
的自定义函数,其中包含正在应用于数据的标准化过程,以产生以下输出:
图 7.21 - 对分组数据进行标准化
我们已经看到了如何使用.apply
方法为groupby
操作创建自定义函数。因此,我们可以根据所需的操作类型创建自定义函数。
在下一节中,我们将研究数据聚合以及如何将不同的数据聚合操作分配给分组数据的不同列。
对分组数据进行数据聚合
数据聚合涉及收集数据并以摘要形式呈现,例如显示其统计数据。聚合本身是为统计目的收集数据并将其呈现为数字的过程。在本节中,我们将看看如何在 Danfo.js 中执行数据聚合
以下是所有可用的聚合方法列表:
-
mean()
: 计算分组数据的平均值 -
std()
: 计算标准差 -
sum()
: 获取组中值的总和 -
count()
: 计算每组的总值数 -
min()
: 获取每组的最小值 -
max()
: 获取每组的最大值
在本章的开头,我们看到了如何在组数据上调用先前列出的一些聚合方法。groupby
类还包含一个名为.agg
的方法,它允许我们同时对不同列应用不同的聚合操作。
对单列分组进行数据聚合
我们将创建一个 DataFrame,并按列对 DataFrame 进行分组,然后在不同列上应用两种不同的聚合方法:
let data = { 'A': [ 'foo', 'bar', 'foo', 'bar',
'foo', 'bar', 'foo', 'foo' ],
'C': [ 1, 3, 2, 4, 5, 2, 6, 7 ],
'D': [ 3, 2, 4, 1, 5, 6, 7, 8 ] };
let df = new DataFrame(data);
let group_df = df.groupby([ "A"]);
group_df.agg({ C:"mean", D: "count" }).print();
我们创建了一个 DataFrame,然后按列A
对 DataFrame 进行分组。然后通过调用.agg
方法对分组数据进行聚合。
.agg
方法接受一个对象,其键是 DataFrame 中列的名称,值是我们要应用于每个列的聚合方法。在前面的代码块中,我们指定了键为C
和D
,值为mean
和count
:
图 7.22 - 对组数据进行聚合方法
我们已经看到了如何在单列分组上进行数据聚合。现在让我们看看如何在由双列分组的 DataFrame 上执行相同的操作。
对双列分组进行数据聚合
对于双列分组,让我们应用相同的聚合方法:
let data = { 'A': [ 'foo', 'bar', 'foo', 'bar',
'foo', 'bar', 'foo', 'foo' ],
'B': [ 'one', 'one', 'two', 'three',
'two', 'two', 'one', 'three' ],
'C': [ 1, 3, 2, 4, 5, 2, 6, 7 ],
'D': [ 3, 2, 4, 1, 5, 6, 7, 8 ] };
let df = new DataFrame(data);
let group_df = df.groupby([ "A", "B"]);
group_df.agg({ C:"mean", D: "count" }).print();
前面的代码给出了以下输出:
图 7.23 - 对两列分组数据进行聚合方法
在本节中,我们已经看到了如何使用.apply
方法为分组数据创建自定义函数,以及如何对分组数据的每一列执行联合数据聚合。这里显示的示例可以扩展到任何特定数据,并且可以根据需要创建自定义函数。
在真实数据上应用 groupby 的简单示例
我们已经看到了如何在虚拟数据上使用groupby
方法。在本节中,我们将看到如何使用groupby
来分析数据。
我们将使用此处提供的流行的titanic
数据集:web.stanford.edu/class/archive/cs/cs109/cs109.1166/stuff/titanic.csv
。我们将看看如何根据性别和阶级估计幸存泰坦尼克号事故的平均人数。
让我们将titanic
数据集读入 DataFrame 并输出其中的一些行:
const dfd = require('danfojs-node')
async function analysis(){
const df = dfd.read_csv("titanic.csv")
df.head().print()
}
analysis()
前面的代码应该输出以下表格:
图 7.24 - DataFrame 表
从数据集中,我们想要估计根据性别(Sex
列)和他们的旅行等级(Pclass
列)生存的人数。
以下代码显示了如何估计先前描述的平均生存率:
const dfd = require('danfojs-node')
async function analysis(){
const df = dfd.read_csv("titanic.csv")
df.head().print()
//groupby Sex column
const sex_df = df.groupby(["Sex"]).col(["Survived"]).mean()
sex_df.head().print()
//groupby Pclass column
const pclass_df = df.groupby(["Pclass"]).col(["Survived"]).mean()
pclass_df.head().print()
}
analysis()
以下表格显示了每个性别的平均生存率:
图 7.25 - 基于性别的平均生存率
以下表格显示了每个等级的平均生存率:
图 7.26 - 基于 Pclass 的平均生存率
在本节中,我们简要介绍了如何使用groupby
操作来分析现实生活中的数据。
总结
在本章中,我们广泛讨论了在 Danfo.js 中实现的groupby
操作。我们讨论了对数据进行分组,并提到目前,Danfo.js 仅支持按单列和双列进行分组;计划在未来的版本中使其更加灵活。我们还展示了如何遍历分组数据并访问组键及其关联的分组数据。我们看了如何在不循环的情况下获得与组键相关的分组数据。
我们还看到了.apply
方法如何为我们提供了创建自定义数据聚合函数的能力,最后,我们演示了如何同时对分组数据的不同列执行不同的聚合函数。
本章使我们具备了对数据进行分组的知识,更重要的是,它向我们介绍了 Danfo.js 的内部工作原理。有了这个,我们可以将groupby
方法重塑成我们想要的味道,并且有能力为 Danfo.js 做出贡献。
在下一章中,我们将继续介绍更多应用基础知识,包括如何使用 Danfo.js 构建数据分析 Web 应用程序,一个无代码环境。我们还将看到如何将 Danfo.js 方法转换为 React 组件。
第三部分:构建数据驱动应用程序
在第三部分中,我们采用实践方法,向您展示如何构建数据驱动的应用程序。首先,我们向您展示如何构建无代码环境进行数据分析和数据处理。然后,我们向您介绍了使用 Danfo.js 和 TensorFlow.js 的机器学习基础。接下来,我们向您介绍推荐系统,并向您展示如何构建一个。最后,我们通过向您展示如何创建 Twitter 分析仪表板来结束本节。
本节包括以下章节:
-
第八章,创建无代码数据分析/处理系统
-
第九章,机器学习基础
-
第十章,介绍 TensorFlow.js
-
第十一章,使用 Danfo.js 和 TensorFlow.js 构建推荐系统
-
第十二章,构建 Twitter 分析仪表板
-
附录:基本的 JavaScript 概念
第九章:创建无代码数据分析/处理系统
创建Danfo.js的主要目的之一是在浏览器中轻松启用数据处理。这使得能够将数据分析和处理数据无缝地集成到 Web 应用程序中。除了能够将数据处理添加到 Web 应用程序中,我们还有工具可以使数据处理和分析看起来更像设计师在使用Photoshop和Figma时所做的事情;他们如何在画布上混合刷子笔触,只需点击一下,或者如何通过拖放和一些按钮点击在画布上叠加画布来操纵图像。
使用 Danfo.js,我们可以轻松地启用这样的环境(使用React.js和Vue.js等工具),在这样的环境中,数据科学家可以像艺术家一样通过几次点击按钮来操纵数据,并在不实际编写任何代码的情况下获得所需的输出。
许多具有这些功能的工具通常存在,但 Danfo.js 的很酷的地方是使用 JavaScript 构建整个应用程序的工具。事实上,在浏览器中执行所有操作而不调用服务器是非常惊人的。
本章的目标是展示如何使用 Danfo.js 和 React.js 构建这样的环境。还要注意,这里使用的工具(除了 Danfo.js 之外)不是构建应用程序的必需工具;这些只是我非常熟悉的工具。
本章将涵盖以下主题:
-
设置项目环境
-
构建和设计应用程序
-
应用程序布局和
DataTable
组件 -
创建不同的
DataFrame
操作组件 -
实现
Chart
组件
技术要求
本章的基本环境和知识要求如下:
-
现代 Web 浏览器,如Chrome
-
适合的代码编辑器,如VScode
-
已安装Node.js
-
对
tailwindcss
和React-chart-js
有一些了解 -
需要了解 React.js 的基础知识。要了解 React.js,请访问官方网站
reactjs.org/docs/hello-world.html
-
本章的代码可在 GitHub 上克隆,网址为
github.com/PacktPublishing/Building-Data-Driven-Applications-with-Danfo.js/tree/main/Chapter08
设置项目环境
项目使用 React.js,并且为了设置 React 应用程序,我们将使用create-react-app
包自动生成前端构建流水线。但首先,请确保您已安装 Node.js 和npx
,这是随npm 5.2+一起提供的包运行工具。
在我们开始设置环境之前,这里是本章中需要的工具,并将在其中安装:
-
React.js:用于构建 UI 的 JavaScript 框架
-
Draggable:使 HTML 元素可以拖放的拖放库
-
图表
组件 -
React-table-v6:用于显示表格的 React 库
以下是前述工具的一些替代方案:
-
Vue.js:用于构建 UI 的 JavaScript 库
-
rechart.js:基于 React.js 构建的可组合图表库
-
Material-table:基于material-UI的 React 数据表
要创建 React 应用程序流水线,我们使用npx
调用create-react-app
,然后指定我们的项目名称如下:
$ npx create-react-app data-art
此命令将在启动命令的父目录中创建一个名为data-art
的目录。data-art
目录中预先填充了 React.js 模板和所有所需的包。
这是data-art
文件夹的结构:
图 8.1 – React.js 目录结构
安装完成后,我们可以使用以下命令始终启动应用程序(假设您不在终端中的data-art
目录中):
$ cd data-art
$ yarn start
以下命令将启动应用程序服务器,并在终端中输出应用程序运行的服务器端口:
图 8.2 – yarn start 输出
如图 8.1所示,应用程序在http://localhost:3000
上提供。如果一切正常,那么一旦服务器启动,它将自动打开 Web 浏览器以显示 React 应用程序。
在应用程序的开发中,我们不会花费更多时间在样式上,但我们将使将来集成样式变得更容易,并且还可以实现快速原型设计;我们将使用tailwindcss
。
为了使 Tailwind 与 React.js 应用程序一起工作,我们需要进行一些额外的配置。
让我们按照tailwindcss
文档中所示通过npm
安装 Tailwind 及其对等依赖项:tailwindcss.com/docs/guides/create-react-app
:
npm install -D tailwindcss@npm:@tailwindcss/postcss7-compat postcss@⁷ autoprefixer@⁹
安装完成后,我们将继续安装craco
模块,它允许我们覆盖postcss
配置如下:
npm install @craco/craco
安装完craco
后,我们可以继续配置如何构建、启动和测试 React 应用程序。这将通过更改package.json
中"start"
、"build"
和"test"
的命令来完成,如下所示:
{
"start": "craco start",
"build": "craco build",
"test": "craco test",
}
通过之前所做的更改,让我们创建一个配置文件,使craco
在构建 React 应用程序时始终注入tailwindcss
和autoprefixer
,如下所示:
// craco.config.js
module.exports = {
style: {
postcss: {
plugins: [
require('tailwindcss'),
require('autoprefixer'),
],
},
},
}
让我们配置tailwindcss
本身。通过这个配置,我们可以告诉tailwindcss
在生产中删除未使用的样式,我们可以添加自定义主题,还可以添加tailwindcss
包中未包含的自定义颜色、字体、宽度和高度,如下所示:
//tailwind.config.js
module.exports = {
purge: ["./src/**/*.{js,jsx,ts,tsx}", "./public/index.html"],
darkMode: false, // or 'media' or 'class'
theme: {
extend: {},
},
variants: {
extend: {},
},
plugins: [],
};
在配置 Tailwind 之后,我们将编辑src
目录中的css
文件index.css
。我们将向文件中添加以下内容:
/* ./src/index.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
我们已经完成了配置;现在我们可以在index.js
中导入index.css
:
//index.js
. . . . . .
import "./index.css
. . . . . .
请注意,在App.js
中,我们仍然有create-react-app
包中提供的默认代码;让我们编辑代码。这是初始代码:
function App() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
}
我们通过编辑 HTML 代码在App
注释中编辑 HTML,并用应用程序的名称替换它:
function App() {
return (
<div className="">
Data-Art
</div>
);
}
通过更新App.js
并保存前面的代码,您应该直接在浏览器中看到所做的更改,如下截图所示:
图 8.3 – React 应用程序
让我们测试我们的tailwindcss
配置,以确保它设置正确。我们将通过向前面的代码添加一些样式来做到这一点,如下所示:
function App() {
return (
<div className="max-w-2xl border mx-auto text-3xl mt-60 text-center">
Data-Art
</div>
);
}
css
样式是在名为className
的div
属性中声明的。首先,我们设置最大宽度和边框,然后在x轴上创建一个边距(左右边距为auto
),声明字体大小为text-3xl
,将顶部边距设置为60
,然后在div
实例中居中文本。
根据样式,我们应该看到以下输出:
图 8.4 – 居中 div 和文本
代码库已经设置好,我们准备实现无代码环境。
在本节中,我们看到了如何为我们的应用程序设置 React 环境。我们还看到了如何为我们的应用程序配置tailwindcss
。在下一节中,我们将学习如何构建和设计应用程序。
构建和设计应用程序
React.js 有一些关于应用程序设计的核心理念,主要是将 UI 分解为组件层次结构,另一个理念是确定状态应该存在的位置。
在本节中,我们将看到如何使用 React.js 设计无代码应用程序的结构,并考虑 React 应用程序设计的理念。根据这个原则,我们将发现在 React 中实现基本 UI 很容易。
首先,让我们了解什么是无代码环境,以及我们希望通过它实现什么。无代码环境用于通过点击几下按钮使数据处理和分析变得更加容易。
我们将创建一个平台,用户可以上传其数据,进行分析,并进行代码操作,例如以下操作:
-
DataFrame 到 DataFrame 操作,例如
concat
-
诸如
cummax
和cumsum
之类的算术操作 -
通过查询按列值筛选出 DataFrame
-
描述 DataFrame
我们希望能够在不编写代码的情况下完成所有这些操作,并且所有操作都将在浏览器中完成。我们还希望通过数据可视化(使用柱状图、折线图和饼图)获得数据洞察。以下图显示了应用程序设计的草图:
图 8.5 - 应用程序结构和设计草图
图 8.5显示了应用程序的结构和设计。应用程序分为三个主要组件,如下所示:
-
“导航栏”组件,包含文件上传、柱状图、折线图和
DataFrame
操作选择字段 -
包含“数据表”组件和“图表”组件的主体
-
“侧边栏”,包含图表和
DataFrame
操作的侧面板
应用程序工作流程如下所述:
-
首先,上传了一个数据文件(csv)。
-
通过上传文件,创建了第一个“数据表”。这是一个包含 DataFrame 显示的组件。
-
要执行任何操作,例如 DataFrame 操作或图表操作,必须选择数据表,以便我们可以识别要执行操作的正确表。
-
对于图表操作,单击柱状图、折线图或饼图。此单击事件将激活图表操作的“侧面板”。
-
如果选择了
DataFrame
操作,则“侧面板”将被激活以进行DataFrame
操作。 -
当您在“侧面板”中填写必要的字段时,将创建新的图表组件和“数据表”组件。
以下图描述了整个工作流程:
图 8.6 - 应用程序工作流程
工作流程显示了每个组件如何相互响应。例如,没有上传文件,主体和“侧面板”将不可见。即使上传了文件,“侧面板”仍然隐藏,只有在要对特定数据表执行 DataFrame 或图表操作时才会出现。
由此可见,我们需要创建一个状态来管理在上传文件时激活主体的状态,还需要创建一个状态来管理在对数据表进行操作时激活“侧面板”的状态。还要注意,“侧面板”包含两种操作,我们必须根据所选操作的类型显示这些操作的字段。
如果选择了图表操作,则“侧面板”需要显示所选绘图图表(柱状图、折线图或饼图)所需的字段,如果选择了DataFrame
操作,则“侧面板”需要显示如下图所示的 DataFrame 操作字段:
图 8.7 - 侧面板操作字段
从图 8.7中,我们可以看到数据表和“图表”组件具有“数据表”组件和“图表”组件。每个组件都有一个状态,这样就可以根据需要创建、更新和删除组件。
如应用程序工作流程所述,“侧面板”操作需要可视化和 DataFrame 操作的数据,这些数据是通过单击我们想要处理的所需“数据表”来获取的。每个“数据表”都存储着自己的 DataFrame 对象(在实施步骤时我们将深入研究这一点)。因此,每当单击数据表时,都会获取其在数据表状态中的索引,并将其传递到侧面板中,同时传递指示要处理哪个数据表的数据表状态。
此外,为了让侧面板知道所需的图表类型(柱状图、折线图或饼图)是什么,或者要执行什么类型的DataFrame
操作,我们创建一个状态来管理当前选定的图表或DataFrame
类型。
总之,所需的状态集描述如下:
-
管理
DataTable
列表的状态 -
管理图表列表的状态
-
管理
SidePlane
可见性的状态 -
管理当前
DataTable
索引的状态 -
管理所选图表类型的状态
-
管理当前选定的
DataFrame
操作的状态
这里创建的状态并不是很优化。可以管理创建的状态的数量;例如,管理Side Plane
可见性的相同状态也可以用于管理所选图表类型。
由于我们将使用超过一个或两个状态,并且其中一些状态会与另一个状态交互,我们可以使用useReducer
(一个 React Hook)来管理状态交互,但我们希望简化这个过程,而不是增加额外的知识负担。
在本节中,我们讨论了应用程序的设计和结构。我们还设计了应用程序的工作流程,并讨论了为应用程序创建的不同状态。在下一节中,我们将讨论应用程序的布局和DataTable
组件。我们将看到如何创建数据表组件以及如何管理状态。我们还将研究如何在浏览器中使用 Danfo.js 上传文件。
应用程序布局和 DataTable 组件
在本节中,我们将看到如何根据前一节讨论的设计和工作流程来布置应用程序。此外,我们将实现DataTable
组件,负责显示DataFrame
表格。我们还将实现DataTables
组件,负责显示不同的DataTable
组件。
我们已经看到了应用程序的草图,也看到了应用程序的基本工作流程。我们将开始实施这些步骤,首先构建应用程序的基本布局,然后实现数据表组件。
使用tailwindcss
,布置应用程序非常容易。让我们创建一个名为App.js
的文件,并输入以下代码:
function App() {
return (
<div className="max-w-full mx-auto border-2 mt-10">
<div className="flex flex-col">
<div className="border-2 mb-10 flex flex-row">
Nav
</div>
<div className="flex flex-row justify-between border-2">
<div className="border-2 w-full">
<div>
Main Body
</div>
</div>
<div className="border-2 w-1/3">
Side Plane
</div>
</div>
</div>
</div>
);
}
前面的代码片段创建了一个带有flex
框的应用程序布局。这个布局显示了应用程序的基本组件,即Nav
、Main Body
和Side Plane
。
注意
本教程中使用的css
实例不会被解释。我们的主要重点是构建应用程序的功能。
如果一切顺利,我们应该得到以下输出:
图 8.8 - 应用程序布局
我们已经布置好了应用程序。让我们继续实现DataTable
组件,以显示所有DataFrame
操作的结果。
实现 DataTable 组件
DataTable
组件负责显示数据表。对于每个 DataFrame 操作,我们生成一个新的数据表,显示操作的结果,如下图所示:
图 8.9 - 数据表
为了显示表格,我们将使用一个名为react-table-v6
的 React 包,由于我们希望DataTable
组件可以在页面上拖动,所以有一个名为react-draggable
的包,它可以更容易地实现这个功能。
注意
DataTable
的代码可以从这里获取:github.com/PacktPublishing/Building-Data-Driven-Applications-with-Danfo.js/blob/main/Chapter08/src/components/DataTable.js
。
我们需要使用yarn
将这些包添加到我们的代码库中:
yarn add react-table-v6 react-draggable
安装完包之后,在src/component/DataTable.js
文件中创建一个DataTable
组件,具体步骤如下:
- 我们导入必要的包:
import React from "react";
import ReactTable from 'react-table-v6'
import Draggable from 'react-draggable';
import 'react-table-v6/react-table.css'
- 我们创建
DataTable
组件:
export default function DataTable({ columns, values, setCompIndex, index }) {
// DataTable component code here
}
DataTable
组件接受以下props
值:
-
columns
:数据表列名。 -
values
:每列的数据表值。 -
setCompIndex
:这是一个状态函数,用于管理当前选择的数据表。 -
index
:这是当前表格的索引。
- 对于
react-table
组件,我们需要重塑列和值,以适应react-table
组件所需的输入。
让我们重塑要传递给react-table
的列值:
const dataColumns = columns.map((val, index) => {
return { Header: val,
accessor: val,
Cell: (props) => (
<div className={val || ''}>
<span>{props.value}</span>
</div>
),
width:
index === 0 && (1280 * 0.8333 - 30) / columns.length < 130
? 130
: undefined,
}
});
使用上述代码,表格的列名(即表格的Header
)被转换为以下形式:
[{
Header: "A",
accessor: "A"
},{
Header: "B",
accessor: "B"
},{
Header: "C",
accessor: "C"
}]
Header
键是要在表格中显示的列名,accessor
是数据中的键。
- 我们需要将数据表值转换为
react-table
所需的格式。以下代码用于转换数据表值:
const data = values.map(val =>{
let rows_data = {}
val.forEach((val2, index) => {
let col = columns[index];
rows_data[col] = val2;
})
return rows_data;
})
如前所示,我们将把数据表值转换为以下数据形式:
[{
A: 2,
B: 3,
C: 5
},{
A: 1,
B: 20,
C: 50
},{
A: 23,
B: 43,
C: 55
}]
最初,values
是一个数组的数组,正在转换为上述数据格式,然后赋给data
变量。
在上述列格式中声明的访问器指向字典中每个键的值。有时,我们可能会有以下格式的嵌套数据:
[{
dummy: {
A: 1.0,
B: 3.0
},
dummy2: {
J: "big",
k: "small"
}
}, . . . . ]
对于这种数据格式,我们可以声明data
列的格式如下:
[{
Header: "A",
accessor: "dummy.A"
},
{
Header: "B",
accessor: "dummy.B"
},
{
Header: "J",
accessor: "dummy2.J"
},
{
Header: "K",
accessor: "dummy2.K"
}]
对于这个项目,我们不会使用这种嵌套数据格式,所以不需要深入研究,但如果你感兴趣,可以查看react-table-v6
文档。
包括Header
在内的列名和表格数据现在已经处于正确的格式,并准备传递给react
表格。DataTable
组件现在已更新,包含以下代码:
function DataTable({ columns, values, setCompIndex, index }) {
. . . . . . . . . . . . . . . . . . . .
const handleSidePlane = ()=>{
setCompIndex(index)
}
return (
<Draggable >
<div className="w-1/2" onClick={()=> handleSidePlane()}>
<ReactTable
data={data}
columns={dataColumns}
getTheadThProps={() => {
return { style: { wordWrap: 'break-word', whiteSpace: 'initial' } }
}}
showPageJump={true}
showPagination={true}
defaultPageSize={10}
showPageSizeOptions={true}
minRows={10}
/>
</div>
</Draggable>
)
}
ReactTable
组件被包装在Draggable
组件中,以使DataTable
组件可拖动。在ReactTable
组件中,我们设置了一些分页字段,例如将默认页设置为10
。
回想一下,当设计应用程序的工作流程时,我们提到了跟踪单击的Data Table
的 ID。handleSide Plane
函数用于调用setCompIndex
。setCompIndex
用于更新存储所选Data Table
的索引的compIndex
状态。
注意
DataTables
的代码在这里:github.com/PacktPublishing/Building-Data-Driven-Applications-with-Danfo.js/blob/main/Chapter08/src/components/DataTables.js
。
每个操作将生成多个数据表,因此我们需要管理这些Data Table
的显示。我们将创建一个组件来管理生成的所有Data Table
的显示;因此,我们将在组件目录中创建一个名为Data Tables
的文件,并包含以下代码:
import React from 'react'
import DataTable from './DataTable'
export default function DataTables({datacomp, setCompIndex,}) {
return (
<div>
{datacomp.map((val,index) => {
return(
<>
<DataTable
key={index}
columns={val.columns}
values={val.values}
setCompIndex={setCompIndex}
index={index} />
</>
)
})}
</div>
)
}
该组件循环遍历datacomp
状态,并将每个属性传递给DataTable
组件。
在下一个子部分中,我们将继续初始化不同的状态,并展示如何上传 CSV 并获取我们的数据。
文件上传和状态管理
从应用程序设计中,我们可以看到任何操作都需要先上传文件。通过上传文件,我们创建了将被DataTable
和chart
组件使用的DataFrame
。
注意
本章中,App.js
中的代码会根据新组件的实现逐渐更新。但是App.js
的最终代码可以在这里找到:github.com/PacktPublishing/Building-Data-Driven-Applications-with-Danfo.js/blob/main/Chapter08/src/App.js
。
我们将更新App.js
中的代码,包括Data
组件状态、文件上传和状态更新:
- 我们导入
React
和一个名为useState
的 React hook:
import React, { useState } from 'react';
- 我们导入
read_csv
方法,该方法将用于读取上传的 CSV 文件:
import { read_csv } from 'danfojs/src/io/reader' // step 2
- 我们创建一个状态来存储每个生成的
DataTable
组件的数据列表:
const [dataComp, setDataComp] = useState([])
- 然后我们创建一个函数来管理文件上传并将上传的文件读入
DataFrame
中:
const changeHandler = function (event) {
const content = event.target.files[0]
const url = URL.createObjectURL(content)
read_csv(url).then(df => {
const columns = df.columns
const values = df.values
setDataComp(prev => {
let new_data = prev.slice()
let key = new_data.length + 1
let dict = {
columns: columns,
values: values,
df: df,
keys: "df" + key
}
new_data.push(dict)
return new_data
})
}).catch((error) => {
console.log(error)
})
}
在上述代码中,我们使用URL.createObjectURL
从上传的文件生成一个 blob URL。这是因为Danfo.js
中的read_csv
代码只接受 CSV 文件的本地路径、CSV 文件的 HTTP URL 和 CSV 文件的 blob URL。
然后将生成的 URL 传递给read_csv
函数。由于read_csv
是一个异步函数,我们需要等待承诺被解决,然后通过then
方法收集承诺的返回值。解决承诺的返回值是一个DataFrame
。
使用read_csv
,CSV 数据被转换为DataFrame
,然后更新DataComponent
状态。使用setDataComp
状态函数,我们创建了一个包含以下键的对象:
a) columns
: 存储 CSV 文件的标题(列名)
b) values
: 存储 CSV 数据点,即DataFrame
的值
c) df
: 存储生成的DataFrame
d) keys
: 为每个数据组件data
生成一个键
需要做出一个决定,是在每个组件的状态数据中实际保存 DataFrame 本身。由于我们已经存储了列名和DataFrame
值,这看起来是多余的。
但我们最终选择存储它的原因是,每次需要执行DataFrame
操作时,总是从列和值中创建DataFrame
将会非常昂贵。
此外,columns
和values
被存储以便在我们想要从react-table
组件生成表格时轻松访问。但仍然感觉冗余,作为个人练习(在本节末尾列出的待办事项清单中),您可以继续清理它。
- 一旦状态更新,我们就在浏览器控制台中打印出
dataComp
状态的输出:
if (dataComp.length) { //step 8
console.log("dataComp column", dataComp[0].columns)
console.log("dataComp values", dataComp[0].values)
console.log("dataComp dataFame", dataComp[0].df)
}
以下截图显示了应用程序的更新 UI:
图 8.10 - 文件上传的更新 UI
一旦上传文件,我们应该在浏览器控制台中看到以下输出:
图 8.11 - dataComp 状态输出
我们已经设置了文件上传和每个DataTable
组件的状态管理。让我们将创建的DataTables
组件集成到应用程序中。
将 DataTable 组件集成到 App.js 中
App.js
将按以下步骤进行更新:
- 我们导入
DataTables
组件并创建一个compIndex
状态,它使我们能够存储我们想要处理的DataTable
组件的索引:
. . . . . . . . . . . .
import DataTables from './components/DataTables';
function App() {
. . . . . . . . . .
const [compIndex, setCompIndex] = useState()
. . . . . . . . . .
}
- 然后将
DataTables
组件添加到App
组件中:
<div>
{(dataComp.length > 0) &&
<DataTables
datacomp={dataComp}
setCompIndex={setCompIndex}
/>
}
</div>
为了使DataTable
组件可见,我们检查dataComp
状态是否为空。在上传文件之前,如果dataComp
状态为空,DataTable
组件将不可见。一旦文件更新,DataTable
组件就变得可见,因为dataComp
状态不再为空。
一旦上传文件,上述代码应该给我们以下输出:
图 8.12 - 文件上传时显示的 DataTable 组件
在本节中,我们讨论了文件上传和DataTable
的创建和管理,并看到了如何管理状态。在下一节中,我们将实现不同的DataFrame
操作组件,并为DataFrame
操作实现Side Plane
。
创建不同的 DataFrame 操作组件
在这一部分,我们将创建不同的DataFrame
操作组件,并为DataFrame
操作组件实现Side Plane
。Danfo.js 包含许多DataFrame
操作。如果我们为每个操作设计一个组件,那将会非常紧张和冗余。
为了防止为每个DataFrame
方法创建一个组件,我们根据它们的(关键字)参数对DataFrame
操作进行分组,也就是根据传递给它们的变量进行分组。例如,有一些DataFrame
方法只接受操作的轴,因此我们可以将这些类型的方法分组在一起。
以下是要创建的DataFrame
操作组件列表和它们下面的DataFrame
方法:
-
DataFrame
方法的参数只是操作的轴,可以是1
或0
。用于在DataFrame
上执行算术操作的方法包括min
、max
、sum
、std
、var
、sum
、cumsum
、cummax
和cummin
。 -
DataFrame
组件,例如DataFrame
和系列之间的逻辑操作,值或DataFrame
之间的逻辑操作。用于执行这些操作的方法包括concat
、lt
、gte
、lte
、gt
和neq
。 -
DataFrame
查询。 -
DataFrame
统计。
我们将从最不复杂的组件开始查看这些组件的实现,顺序为:Describe
、query
、Df2df
和Arithmetic
。
实现 Describe 组件
在这一部分,我们将实现Describe
组件,并集成Side Plane
组件。
在src/Components/
目录中,让我们创建另一个名为Side Planes
的文件夹。这个文件夹将包含所有用于DataFrame
操作的组件。
在Side Planes/
文件夹中,让我们创建一个名为Describe.js
的".js"
文件,并按照以下步骤进行更新:
- 我们创建
Describe
函数组件,接受dataComp
状态和setDataComp
状态函数,以使用生成的DataFrame
更新dataComp
状态:
export default function Describe({ dataComp, setDataComp}) {
}
- 我们创建一个名为
Describe
的按钮:
return (
<div>
<button onClick={()=> describe()} className="bg-blue-700 text-white rounded-sm p-2">Describe</button>
</div>
)
Describe
组件具有按钮接口,因为它不接受任何参数。按钮具有onClick
事件,每当点击按钮时都会触发Describe
函数。
- 然后我们实现
describe()
函数,它会在点击describe
按钮时触发:
const describe = ()=> {
const df = dataComp.df.describe()
let column = df.columns.slice()
column.splice(0,0, "index")
const values = df.values
const indexes = df.index
const new_values = values.map((val, index)=> {
let new_val = val.slice()
new_val.splice(0,0, indexes[index])
return new_val
})
. . . . . . . .
}
我们从dataComp
状态中获取包含DataFrame
的df
键,然后调用describe
方法。
从describe
操作生成的DataFrame
中,我们获取列并将索引添加到列名列表中。索引添加在列表的开头;这是因为需要捕获数据中每一行的索引,这是从describe
方法生成的。
接下来,我们获取DataFrame
值,循环遍历它,并将索引值添加到获取的DataFrame
值中。
- 我们使用新生成的列、值和
DataFrame
更新dataComp
状态:
setDataComp(prev => { // step 7
let new_data = prev.slice()
let dict = {
columns: column,
values: new_values,
df: df
}
new_data.push(dict)
return new_data
})
要查看此组件的操作,我们需要实现DataFrame
操作选择字段,如图 8.5中的App
设计草图所示。这个DataFrame
操作选择字段使我们能够选择在Side Plane
中显示哪个DataFrame
操作组件。
为此,我们需要在Navbar
组件中添加DataFrame
操作的select
字段,以及文件上传的输入字段。此外,我们需要为Side Plane
中显示的每个DataFrame
操作组件实现条件渲染。
为 Describe 组件设置 SidePlane
在Side Planes/
文件夹中,让我们创建一个名为Side Plane.js
的文件,并输入以下代码:
import React from 'react'
import Describe from './Describe'
export default function SidePlanes({dataComp,
dataComps,
setDataComp,
df_index,
dfOpsType}) {
if(dfOpsType === "Arithemtic") {
return <div> Arithmetic </div>
}
else if(dfOpsType === "Describe") {
return <Describe
dataComp={dataComp}
setDataComp={setDataComp}
/>
}
else if(dfOpsType === "Df2df") {
return <div> Df2df </div>
}
else if(dfOpsType === "Query") {
return <div> Query </div>
}
return (
<div>
No Plane
</div>
)
}
在上述代码中,我们创建了一个Side Plane
组件。这个组件根据所选的数据操作类型进行条件渲染。所选的DataFrame
操作由dfOpsType
状态管理。
Side Plane
接受dataComp
状态,它可以是dataComps
状态中存储的任何数据。一些DataFrame
操作将需要所选的dataComp
状态以及整个状态,即dataComps
状态,进行操作。
在Side Plane
组件中,我们将检查dfOpsType
以找出传递的操作类型和在侧面板中渲染的接口。
在将Side Plane
集成到App.js
之前,让我们在Side Planes/
文件夹中创建一个index.js
文件。通过这样做,我们可以定义要导入的组件。由于我们正在使用Side Plane
组件的条件渲染,我们只需要在index.js
实例中导出Side Plane
组件,如下面的代码所示:
import SidePlane from './SidePlane'
export { SidePlane }
上述代码使我们能够在App.js
中导入Side Plane
。
将 SidePlane 集成到 App.js 中
创建了Side Plane
。让我们将其集成到App.js
中,并在App.js
中为DataFrame
操作添加 HTMLselect
字段,如下面的代码所示:
- 导入
SidePlane
组件:
import { SidePlane } from './components/SidePlanes'
- 我们使用以下代码更新
App
组件功能:
function App() {
. . . . . . . .
const [dfOpsType, setDfOpsType] = useState() // step 2
const [showSidePlane, setSidePlane] = useState(false) //step 3
. . . . . . . . .
const dataFrameOps = ["Arithemtic", "Describe", "Df2df", "Query"] // step 4
const handleDfops = (e) => { //step 6
const value = e.target.value
setDfOpsType(value)
setSidePlane("datatable")
}
. . . . . . . . . . . . . .
}
在上述代码中,我们创建了dfOpsType
状态来存储当前选择的DataFrame
操作的类型。
还创建了showSidePlane
来管理SidePlane
的可见性。还创建了一个DataFrame
操作的数组。然后创建一个函数来处理点击DataFrame
操作时更新dfOpsType
和showSidePlane
状态。
- 然后添加
SidePlane
组件:
<div className="border-2 w-1/3">
{showSidePlane
&&
(
showSidePlane === "datatable" ?
<div className="border-2 w-1/3">
<SidePlane
dataComp={dataComp[compIndex]}
dataComps={dataComp}
df_index={compIndex}
setDataComp={setDataComp}
dfOpsType={dfOpsType}
/>
</div> :
<div className="border-2 w-1/3">
Chart Plane
</div>
)
}
</div>
在上述代码中,我们首先检查SidePlane
状态不是false,然后检查SidePlane
的类型来显示SidePlane
。由于我们只实现了DataFrame
操作列表中的Describe
组件,让我们上传一个文件然后执行DataFrame
操作。以下截图显示了在DataTable
上执行Describe
操作的结果:
图 8.13 - 在 DataTable 上执行 Describe 操作
在上述截图中,当上传文件时,左上角的数据表生成,右下角的DataFrame
是Describe
操作的结果。
在本节中,我们看到了如何实现Describe
组件以及如何管理Side Plane
的可见性。在下一节中,我们将为DataFrame
的Query
方法实现Query
组件。
实现 Query 组件
在本节中,我们将为DataFrame
查询方法创建一个组件。该组件将帮助按Data Table
的列值对DataFrame
进行过滤。
注意
Query
组件的代码在这里可用:github.com/PacktPublishing/Building-Data-Driven-Applications-with-Danfo.js/blob/main/Chapter08/src/components/SidePlanes/Query.js
。
让我们在components/Side Planes/
文件夹中创建一个名为Query.js
的文件,并按以下步骤更新它:
- 创建
Query
组件:
import React, { useRef } from 'react'
export default function Query({ dataComp, setDataComp}) {
// step 1
const columnRef = useRef()
const logicRef = useRef()
const valuesRef = useRef()
. . . . . . . . . . . .
}
我们创建了一个useRef
hook 变量,它使我们能够获取以下输入字段的当前值:列字段(输入要查询的列的名称)、逻辑字段(输入要用于查询的逻辑值)和值字段(输入要用于查询所选列的值)。
- 然后使用以下代码更新
Query
组件:
const columns = dataComp.columns
const logics = [">", "<", "<=", ">=", "==", "!="]
在上述代码中,我们获取当前Data Table
的DataFrame
中可用的列名。这个列名将用于填充选择字段,用户可以选择要查询的列。
我们还创建了一系列符号来表示我们要执行的逻辑操作的类型。这些符号也将用于填充选择字段,用户可以选择用于查询的逻辑操作。
- 创建一个
query
函数。每当点击查询按钮时,将触发此函数执行查询操作:
const query = ()=>{
const qColumn = columnRef.current.value
const qLogic = logicRef.current.value
const qValue = valuesRef.current.value
const df = dataComp.df.query({column: qColumn, is: qLogic, to: qValue})
setDataComp(prev => {
let new_data = prev.slice()
let dict = {
columns: df.columns,
values: df.values,
df: df
}
new_data.push(dict)
return new_data
})
}
每当触发query
函数时,我们获取每个输入字段(选择字段)的值。例如,要获取列字段的值,我们使用columnRef.current.value
。同样的方法也适用于获取另一个字段的值。
我们还调用当前dataComp
状态所属的DataFrame
的查询方法。从每个输入字段获得的值传递到查询方法中执行操作。
使用setDataComp
状态函数更新dataComps
状态。通过更新dataComps
状态,创建一个包含query
方法结果的新DataComp
状态。
实现查询组件接口
我们已经看到了Query
组件的后端,现在让我们为其构建一个接口。让我们在Query.js
中更新上述代码,采取以下步骤:
- 对于查询 UI,我们创建一个包含三个不同输入字段的表单。首先,我们创建列字段的输入字段:
<div>
<span className="mr-2">Column</span>
<select ref={columnRef} className="border">
{
columns.map((column, index)=> {
return <option value={column}>{column}</option>
})
}
</select>
</div>
对于列字段,我们循环遍历列数组,为DataFrame
中的列列表创建 HTML 选择字段选项。我们还包括columnRef
来跟踪所选的列名。
- 然后我们创建逻辑输入字段:
<div>
<span className="mr-2">is</span>
<select ref={logicRef} className="border">
{
logics.map((logic, index)=> {
return <option value={logic}>{logic}</option>
})
}
</select>
</div>
我们循环遍历logic
数组,并用逻辑运算符填充 HTML 选择字段。同时,将logicRef
添加到 HTML 选择字段中以获取所选的逻辑运算符。
- 然后我们创建查询值的
input
字段:
<div>
<span className="mr-2">to</span>
<input ref={valuesRef} placeholder="value" className="border"/>
</div>
- 我们创建一个
button
类名来调用query
函数:
<button onClick={()=>query()} className="btn btn-default dq-btn-add">Query</button>
为了在主应用程序中可视化query
组件,让我们更新SidePlane.js
中的SidePlane
组件:
Previous code:
. . . . . . . .
else if(dfOpsType === "Query") {
return <div> Query </div>
}
Updated code:
else if(dfOpsType === "Query") {
return <Query
dataComp={dataComp}
setDataComp={setDataComp}
/>
}
上述代码更新了Side Plane
以包含Query
组件。如果我们对上传的文件执行查询操作,我们应该得到以下结果:
图 8.14 - 在列 C 上运行的查询操作,检查其值是否大于 20
在本节中,我们创建了一个query
组件。在下一节中,我们将研究创建一个涉及DataFrame
-to-DataFrame
操作、series 和 scalar 值的组件。
实现 Df2df 组件
在本节中,我们将实现一个组件,用于在DataFrame
和另一个DataFrame
、Series
和Scalar
值之间执行操作。
注意
Df2df
组件的代码在这里可用:github.com/PacktPublishing/Building-Data-Driven-Applications-with-Danfo.js/blob/main/Chapter08/src/components/SidePlanes/Df2df.js
。
Danfo.js 中有不同的组件,用于在DataFrame
和Series
之间以及DataFrame
和Scalar
值之间执行操作。为了避免为每个方法创建一个组件,我们可以将它们组合在一起形成一个单一的组件。
我们计划将一组DataFrame
方法列在一起的列表如下:
-
小于(
df.lt
) -
大于(
df.gt
) -
不等于(
df.ne
) -
等于(
df.eq
) -
大于或等于(
df.ge
) -
加法(
df.add
) -
减法(
df.sub
) -
乘法(
df.mul
) -
除法(
df.div
) -
幂(
df.pow
)
在上述方法列表中的一个共同属性是它们都接受相同类型的参数,即值(可以是DataFrame
、Series
或scalar
值)和要执行操作的轴。
如果我们看一下DataFrame
concat
方法,它也接受与上述列表中的方法类似的模式的参数。唯一的区别是对于concat
方法,df_list
参数是一个DataFrames
数组。
让我们在Side Planes/
文件夹中创建一个名为Df2df.js
的文件。在这个文件中,我们将按照以下步骤实现Df2df
组件:
- 首先,我们从 Danfo.js 中导入
concat
,然后创建Df2df
组件:
import React, { useRef } from 'react'
import { concat } from 'danfojs/src/core/concat'
export default function Df2df({dataComp, dataComps,df_index, setDataComp}) {
const dfRef = useRef()
const inpRef = useRef()
const axisRef = useRef()
const opsRef = useRef()
const allOps = [
"lt", "ge", "ne",
"eq", "gt", "add",
"sub", "mul", "div",
"pow", "concat"
]
. . . . . . . . . . . . .
}
我们为每个输入字段创建了一个引用变量。对于Df2df
操作,我们有四个输入字段(DataFrame
选择字段,scalar
值输入字段,axis
字段和operation
类型字段)。
operation
类型字段包含Df2df
组件中所有可用操作的列表。这将是一个选择字段,因此用户可以选择要使用的任何操作。
我们还创建了allOps
列表,其中包含Df2df
组件提供的所有操作。
- 我们还需要创建一个函数,以便在单击
submit
按钮时执行Df2df
操作:
const df2df = () => {
// step 4
let dfIndex = dfRef.current.value
let inp = parseInt(inpRef.current.value)
let axis = parseInt(axisRef.current.value)
let ops = opsRef.current.value
. . . . . . . . . . . . . .
}
我们从属于每个输入字段的所有引用变量中获取值。
- 我们使用以下代码更新了
df2df
函数:
if( ops != "concat") {
let value = dfIndex === "None" ? inp : dataComps[dfIndex].df
let df = dataComp.df
let rslt = eval('df.${ops}(value, axis=${axis})') // step 6
setDataComp(prev => {
let new_data = prev.slice()
let key = new_data.length +1
let dict = {
columns: rslt.columns,
values: rslt.values,
df: rslt,
keys: "df" + key
}
new_data.push(dict)
return new_data
})
}
我们检查所选的操作是否不是concat
操作。这是因为concat
操作接受的是一个DataFrames
列表,而不仅仅是一个DataFrame
或Series
。
我们使用eval
函数来防止编写多个if
条件来检查要调用哪个DataFrame
操作。
- 我们实现了
concat
操作的条件。我们还在DataFrame
中调用了concat
方法:
. . . . . . . . .
else { // step 7
let df2 = dataComps[dfIndex].df
let df1 = dataComp.df
let rslt = concat({ df_list: [df1, df2], axis: axis })
let column = rslt.columns.slice()
column.splice(0,0,"index")
let rsltValues = rslt.values.map((val, index) => {
let newVal = val.slice()
newVal.splice(0,0, rslt.index[index])
return newVal
})
. . . . . . . . . . .
}
前面的步骤展示了Df2df
组件的后端实现。
实现 Df2df 组件接口
让我们按照以下步骤更新 UI 的代码:
- 对于 UI,我们需要创建一个包含四个输入字段的表单。首先,我们创建一个输入字段来选择我们想要执行的
DataFrame
操作的类型:
<div>
<span className="mr-2"> Operations</span>
<select ref={opsRef}>
{
allOps.map((val,index) => {
return <option value={val} key={index}>{val}</option>
})
}
</select>
</div>
我们循环遍历allops
数组,创建一个input
字段来选择不同类型的DataFrame
操作。
- 然后,我们创建一个
input
字段来选择要在其上执行所选操作的DataFrame
:
<div>
<span className="mr-2"> DataFrames</span>
<select ref={dfRef}>
<option key={-1}>None</option>
{
dataComps.map((val,index) => {
if( df_index != index) {
return <option value={index} key={index}>{'df${index}'}</option>
}
})
}
</select>
</div>
我们还循环遍历dataComps
状态,以获取其中除了我们正在执行操作的dataComp
状态之外的所有dataComp
状态。
- 然后,我们创建一个
input
字段来输入我们的值;在这种情况下,我们正在执行DataFrame
和Scalar
值之间的操作:
<div>
<span>input a value</span>
<input ref={inpRef} className="border" />
</div>
- 我们创建一个
input
字段来选择操作的轴:
<div>
<span>axis</span>
<select ref={axisRef} className="border">
{
[0,1].map((val, index) => {
return <option value={val} key={index}>{val}</option>
})
}
</select>
</div>
- 然后,我们创建一个按钮,触发
df2df
函数根据输入字段执行 Df2df 操作:
<button onClick={()=>df2df()} className="bg-blue-500 p-2 text-white rounded-sm">generate Dataframe</button>
在前面的步骤中,我们为组件创建了 UI。
让我们更新SidePlane
组件,以包含 Df2df 组件:
import Df2df from './Df2df'
export default function SidePlanes({dataComp,
dataComps,
setDataComp,
df_index,
dfOpsType}) {
. . . . . . . .
else if(dfOpsType === "Df2df") {
return <Df2df
dataComp={dataComp}
dataComps={dataComps}
df_index={df_index}
setDataComp={setDataComp}
/>
}
. . . . . . . . .
}
前面的代码将Df2df
组件添加到SidePlane
组件中,并在Df2df
组件中传递所需的 props。以下截图显示了上传具有相同内容的两个 CSV 文件:
图 8.15 - 上传具有相同内容的 CSV 文件
以下显示了在所选“数据表”上执行Df2df
操作(具体是concat
操作)的输出:
图 8.16 - 在数据表上执行 concat 操作
在本节中,我们创建了Df2df
组件,用于在两个DataFrames
之间以及在DataFrame
和Series
/Scalar
值之间执行操作。
在下一节中,我们将实现最后一个DataFrame
组件,即arithmetic
组件,用于DataFrame
算术运算。
实现算术组件
我们将实现arithmetic
组件,以执行Danfo.js
中提供的一些算术运算。
注意
Arithmetic
组件的代码在这里可用:github.com/PacktPublishing/Building-Data-Driven-Applications-with-Danfo.js/blob/main/Chapter08/src/components/SidePlanes/Arithemtic.js
。
让我们在Side Planes/
文件夹中创建一个名为Arithmetic.js
的文件。以下步骤将用于创建Arithmetic
组件:
- 我们创建一个
Arithmetic
组件:
import React, { useRef } from 'react'
export default function Arithmetic({ dataComp, setDataComp}) {
const seriesOps = ["median", "min", "max", "std", "var", "count", "sum"]
const dfOps = ["cumsum", "cummax", "cumprod", "cummin"]
const all = ["median", "min", "max", "std", "var", "count", "sum",
"cumsum", "cummax", "cumprod", "cummin"]
const axisRef = useRef()
const opsRef = useRef()
. . . . . . . . . . . .
}
我们创建不同的数组来存储不同的操作,比如seriesOps
用于 Series 操作,dfOps
用于 DataFrame 操作。我们还创建一个all
数组,将所有这些操作(Series
和DataFrame
)存储在一起。
- 我们创建一个名为
arithmetic
的函数。这个函数用于执行算术操作:
const arithemtic = () => {
let sOps = opsRef.current.value
let axis = axisRef.current.value
if( seriesOps.includes(sOps)) {
let df_comp = dataComp.df
let df = eval('df_comp.${sOps}(axis=${axis})')
let columns = Array.isArray(df.columns) ? df.columns.slice() : [df.columns]
columns.splice(0,0, "index")
let values = df.values.map((val,index) => {
return [df.index[index], val]
})
. . . . . . . . . . .
}
我们从输入字段opsRef.current.value
和axisRef.current.value
中获取值。我们还检查所选的操作是否属于seriesOps
。如果是,我们执行所选的操作。
- 如果操作不属于
seriesOps
,我们执行DataFrame
操作:
else {
let df_comp2 = dataComp.df
let df = eval('df_comp2.${sOps}({axis:${axis}})')
setDataComp(prev => {
let new_data = prev.slice()
let dict = {
columns: df.columns,
values: df.values,
df: df
}
new_data.push(dict)
return new_data
})
}
上述步骤用于创建Arithmetic
组件。Arithmetic
的 UI 与其他创建的DataFrame
操作组件相同。
让我们将arithmetic
组件添加到SidePlane
组件中:
import Arithmetic from './Arithmetic'
export default function SidePlanes({dataComp,
dataComps,
setDataComp,
df_index,
dfOpsType}) {
. . . . . . . .
if(dfOpsType === "Arithmetic") {
return <Arithmetic
dataComp={dataComp}
setDataComp={setDataComp}
/>
}
. . . . . . . . .
}
上述代码导入了Arithmetic
组件,并检查dfOpsType
组件是否为Arithmetic
。
以下截图显示了在Data Table
上执行算术操作的示例:
图 8.17 – 算术操作
在本节中,我们讨论并实现了不同的DataFrame
操作作为 React 组件。我们能够将一些方法组织到单个组件中,以防止为每个操作创建组件。
在下一节中,我们将为不同的可视化实现一个chart
组件。
实现图表组件
在本节中,我们将创建chart
组件来显示常见和简单的图表,如条形图、折线图和饼图。然后我们将实现图表Side Plane
以启用设置图表组件变量。
注意
已实现的Chart
和ChartPlane
组件的代码在此处可用:github.com/PacktPublishing/Building-Data-Driven-Applications-with-Danfo.js/blob/main/Chapter08/src/components/ChartPlane.js
。
在src/components/
目录中,让我们创建一个名为Chart.js
的文件,Chart
组件将通过以下步骤实现:
- 我们从
react-chartjs-2
中导入我们想要的绘图组件,然后创建Chart
组件:
import { Bar as BarChart } from 'react-chartjs-2';
import { Line as LineChart } from "react-chartjs-2";
import { Pie as PieChart} from "react-chartjs-2";
import Draggable from 'react-draggable';
export default function Chart({labels, dataset,type}) {
let data = {
labels: labels,
datasets: [{
backgroundColor: [
. . . . . . . .
],
borderColor: [
. . . . . . . .
],
borderWidth:1,
data: dataset,
}]
};
在上述代码中,Chart
组件接受以下 props:labels
,dataset
和type
。labels
表示列名,dataset
表示dataComp
的值,type
表示我们想要绘制的图表类型。
在Chart
组件中,我们创建一个名为data
的变量,这是一个按照react-chartjs-2
所需的格式化的对象,用于绘制我们想要的图表。
- 我们在这里创建一组条件渲染,因为我们希望根据传递到
Chart
组件的prop
类型来渲染特定类型的图表:
if(type==="BarChart"){
return(
<Draggable>
<div className="max-w-md">
<BarChart data={data} options={options} width="100" height="100" />
</div>
</Draggable>
)
}
. . . . . . .
我们检查要渲染的图表类型。如果是条形图,我们调用react-chartjs-2
中的BarChart
组件,并传入必要的 props。BarChart
组件包装在Draggable
组件中,使得渲染的chart
组件可以被拖动。上述代码适用于渲染所有其他Chart
组件,如react-chartjs-2
中的LineChart
和PieChart
。
要深入了解react-chartjs-2
,您可以在此处查看文档:github.com/reactchartjs/react-chartjs-2
。
实现 ChartPlane 组件
我们已经创建了chart
组件,现在让我们创建图表Side Plane
。在components/
文件夹中,让我们创建一个名为ChartPlane.js
的文件,具体步骤如下:
- 我们创建一个
ChartPlane
组件:
export default function ChartPlane({setChartComp, dataComp, chartType}) {
const df = dataComp.df
const compCols = dataComp.columns
let x;
let y;
if( compCols[0] === "index") {
x = compCols
y = dataComp.values[0].map((val, index)=> {
if(typeof val != "string") {
return compCols[index]
}
})
} else {
x = df.columns
const dtypes = df.dtypes
y = dtypes.map((val, i)=>{
if(val != "string") {
return x[i]
}
})
}
在上述代码中,我们创建了一个接受以下 props 的ChartPlane
组件:
a) SetChartComp
: 更新chartComp
状态的函数
b) dataComp
:当前的DataTable
组件,用于生成图表
c) chartType
:我们想要生成的图表类型
首先,在组件中,我们获取可能的x轴变量列表,并将它们存储在x
变量中。这些x轴变量可以是带有String
或数字dtypes
的列名。
由于我们正在绘制y轴相对于x轴,我们的y轴(y
变量)必须是整数。因此,我们检查DataFrame
的列是否不是字符串,如果不是,我们将该列添加到y轴变量的列表中。
注意
这是灵活的。有时图表可以翻转,使y轴实际上是标签,而x轴包含数据。
- 我们创建
ChartPlane
组件的 UI。根据我们为其他组件设计 UI 的方式,x
和y
变量用于创建一个输入字段,用户可以用它来选择x轴标签和y轴标签:
<select ref={xRef} className="border">
{
x.map((val, index)=> {
return <option value={val} key={index} >{val}</option>
})
}
</select>
<select ref={yRef} className="border">
{
y.map((val, index) => {
return <option value={val} key={index}>{val}</option>
})
}
</select>
这个 UI 还包含一个按钮,触发名为handleChart
的函数,该函数更新chart
组件:
<button onClick={()=>handleChart()} className="bg-blue-500 p-2 text-white rounded-sm">generate Chart</button>
- 我们创建一个名为
handleChart
的函数,该函数获取x轴和y轴输入字段的值,并使用它们来创建相应的图表:
const handleChart = () => {
const xVal = xRef.current.value
const yVal = yRef.current.value
const labels = xVal === "index" ? df.index : df[xVal].values
const data = yVal === "index" ? df.index : df[yVal].values
setChartComp((prev) => {
const newChart = prev.slice()
const key = newChart.length + 1
const dict = {
labels: labels,
data: data,
key: "chart" + key,
type: chartType
}
newChart.push(dict)
return newChart
})
}
xVal
和yVal
是x轴和y轴输入字段的值。创建labels
和data
变量,以包含从xVal
和yVal
的相应列中获取的值。然后使用标签和数据来更新chartComp
状态。
实现 ChartViz 组件
前面的步骤用于创建图表Side Plane
,但目前我们无法看到更新的chartComp
组件。为了查看图表,让我们创建一个组件来管理所有要显示的图表组件。
注意
要实现的ChartViz
的代码在这里:github.com/PacktPublishing/Building-Data-Driven-Applications-with-Danfo.js/blob/main/Chapter08/src/components/ChartsViz.js
。
让我们在components/
文件夹中创建一个名为ChartViz.js
的文件。将以下代码添加到文件中:
import React from 'react'
import Chart from './Chart'
export default function ChartsViz({chartComp,setChartComp}) {
return (
<div>
{
chartComp.map((chart)=> {
return(
<>
<Chart
labels={chart.labels}
dataset={chart.data}
type={chart.type}
/>
</>
)
})
}
</div>
)
}
在前面的代码中,我们导入我们的chart
组件,然后创建一个包含以下chartComp
和setChartComp
props 的ChartViz
组件。我们循环遍历chartComp
状态,并将每个状态值作为 props 传递给chart
组件。
将 ChartViz 和 ChartPlane 集成到 App.js 中
现在我们已经完成了chart
组件的所有必要部分。让我们更新我们的App.js
组件,根据以下步骤激活chart
组件:
- 我们将
ChartViz
和ChartPlane
导入到App.js
中:
import ChartsViz from './components/ChartsViz'
import ChartPlane from './components/ChartPlane'
- 我们需要创建一些状态来管理我们想要的图表类型和
chart
组件:
const [chartType, setChartType] = useState()
const [chartComp, setChartComp] = useState([])
const charts = ["BarChart", "LineChart", "PieChart"]
在前面的代码中,我们还创建一个数组变量来存储我们想要在Navbar
中显示的图表列表。
- 我们创建一个函数来更新
chartType
组件和Side Plane
组件,每当创建一个图表时:
const handleChart = (e) => { // step 4
const value = e.target.innerHTML
setChartType(value)
setSidePlane("chart")
}
在handleChart
函数中,我们获取目标值,即用户选择的图表类型。使用此值来更新chartType
组件,并且我们通过使用chart
字符串更新showSidePlane
状态,通知Side Plane
显示图表。
- 我们循环
nav
字段中的charts
变量,并将它们显示为按钮:
. . . . . .
{
charts.map((chart, i) => {
return <button disabled={dataComp.length > 0 ? false : true}
className={classes}
onClick={handleChart}
>
{chart}
</button>
})
}
. . . . . .
在前面的代码中,我们循环遍历charts
数组,并为数组中的每个值创建一个按钮。通过检查dataComp
状态是否为空来禁用按钮,也就是说,是否没有上传文件。
- 我们调用
ChartViz
组件并传入必要的 props:
{(chartComp.length > 0) &&
<ChartsViz
chartComp={chartComp}
setChartComp={setChartComp}
/>
}
我们检查chartComp
状态是否为空。如果不是,我们调用ChartViz
组件,然后显示创建的图表。
- 然后添加
ChartPlane
组件:
<div className="border-2 w-1/3">
<ChartPlane
dataComp={dataComp[compIndex]}
setChartComp={setChartComp}
chartType={chartType}
/>
</div>
如果showSide Plane
图表是一个值图表,那么ChartPlane
组件将显示在Side``Plane
中。
以下屏幕截图显示了通过在可用的“数据表”上绘制条形图、折线图和饼图来更新图表:
图 8.18 - 显示的图表组件
在本节中,我们实现了ChartComponent
和ChartPlane
。我们利用了React-chart-js
来简化每个图表组件的开发。
摘要
在本章中,我们看到了如何创建一个无代码环境,您可以在其中上传数据,然后立即开始处理和进行数据分析。我们还看到了如何将 Danfo.js 中的每个DataFrame
方法转换为 React 组件。这使得能够将所有 Danfo.js 方法转换为 React 组件,从而为 Danfo.js 创建一个 React 组件库。
此外,我们还看到了如何设计应用程序的流程以及如何在 React 中管理状态。即使创建的一些状态是多余的,这也是您贡献和更新应用程序以使其更加健壮的机会。如果您可以更新应用程序以使其能够删除、更新和保存正在进行的每个操作,这将使应用程序更加健壮,甚至可以投入生产。
在下一章中,我们将介绍机器学习。本章将以尽可能简单的形式介绍机器学习背后的基本思想。
第十章:机器学习基础
机器学习(ML)领域每天都在不断发展,进行大量研究,并利用机器学习算法构建各种智能应用。这个领域越来越受到关注,越来越多的人对它的工作原理和如何利用它感到着迷。
在本章中,我们将尝试基本了解机器学习的原理和工作方式,以及看到它在现实生活中的各种应用形式。
在本章中,我们将研究以下主题,以了解机器学习的基础知识:
-
机器学习简介
-
机器学习为什么有效
-
机器学习问题/任务
-
JavaScript 中的机器学习
-
机器学习应用
-
深入理解机器学习的资源
技术要求
本章以简单形式介绍了机器学习,因此不需要先前的知识。
机器学习简介
在本节中,我们将使用一个简单的类比来介绍机器学习,这可能作为建立我们解释的共同基础。我们还将看到机器学习为什么以及如何工作。
我们将通过使用信息传输系统作为机器学习的简单类比来开始本节。
机器学习系统的简单类比
我记得有一次我参加了一个关于机器学习和其他一些很酷的话题的Twitter Space 讨论。有人让我给那些感兴趣但没有完全理解要点的人简要介绍一下机器学习。
在这个 Twitter Space 中,大多数人是没有数学、统计学或与机器学习相关的任何知识的软件工程师,我遇到了一些人由于增加了一些技术术语而无法理解该主题的术语的情况。
本节旨在通过避免使用太多技术术语,并通过可以解释机器学习的共同基础来解释机器学习。
使用信息传输系统,比如电话,信息从源头获取,然后编码成数字信号,通过传输通道传输到接收器,接收器将信号解码成源输入,可以是声音、图像等等。
以下图示显示了信息传输的完整概念:
图 9.1 – 信息传输
前面的定义是指一个发送者和接收者位于不同端点的信息传输系统,但对于像扩音器这样的系统,输入声音被编码成数字信号,然后在输出端解码和放大。
以下是一个扩音器的图示:
图 9.2 – 简单信息传输系统
使用前面的段落,我们可以建立对机器学习的概述。在前面的段落中,我们提到了一些特定的关键词,这些关键词被编码和解码。
在信息传输系统中,大量的信息(声音或图像)被编码或压缩成数字信号,然后在输出端被解码回源信息。
前面段落中描述的事情也适用于机器学习系统——大量信息被编码或压缩成表示形式(注意高亮显示的词),然后解码为概念性、智能或决策性输出。
请注意前两个段落中的术语数字信号和表示形式。在信息传输系统中,有一些信息理论负责将任何形式的输入(任何类型的图像、任何类型的声音/语音)转换成数字信号。
但在机器学习中,我们有一些理论和算法。这些算法不仅仅是处理输入信息并给出输出。首先,获取一部分信息样本。这些信息被处理并用来构建一种总结整个信息并将其映射到决策输出的表示形式。
这种表示形式被用来构建最终的机器学习系统,该系统接受一个输入源,将其与表示形式进行比较,并输出一个匹配源输入和表示形式之间比较的解码决策(智能输出)。
下图显示了前两段的概念图示:
图 9.3 – 机器学习的概念图示
从前面的段落中,有一些关于机器学习的关键事项需要注意,如下:
-
首先,我们从大量信息中生成一种表示形式。另外,需要注意的是,从一组信息中生成一种表示形式的过程被称为训练。
-
然后生成的表示形式被用来创建最终的机器学习系统,这个过程被称为推理。
在下一小节中,我们将看到如何生成这种形式的表示,从大量信息中生成一种表示形式的整个想法,然后使用这种表示形式来构建最终的机器学习系统。
为什么机器学习有效
在我们训练机器学习模型的说明中,我们谈到了生成一种表示形式来构建我们的机器学习系统。需要注意的一点是,用于生成表示形式的这些信息或数据是我们未来信息源的数据表示。我们为什么需要未来信息源的数据表示?在这一小节中,我们将探讨这一点,并看看它如何帮助创建机器学习模型。
假设我们被要求对特定社区的产品兴趣进行研究。想象一下,这个社区有大量的人,而我们只能接触到其中的一部分人——比如说社区人口的 50%。
这个想法是,从我们获得的 50%人口的信息中,我们应该能够推广到剩下的 50%人口。我们之所以这样假设,是因为来自同一社区或人口的一组人被假定具有相当多的相同属性和信念。因此,如果我们使用从该人口 50%个体获得的信息来训练我们的模型,我们的模型应该能够区分来自同一人口的任何个体的信息。
在最坏的情况下,人群中可能会有一些离群值——那些与其他人不持相同信念的人,或者我们从 50%的人群中获得的个人信息可能无法捕捉到另外 50%人群的属性。在这种情况下,如果将这些信息传递到模型中,模型将会失败。
前面的段落说明了为什么在机器学习中,默认情况下,数据量越大,机器学习模型就越好。下图显示了一个样本分布(我们获得的 50%个人信息)和人群本身:
图 9.4 – 人口分布
从图 9.3中,当我们说我们在机器学习中进行训练时,我们的意思是机器学习算法正在学习控制和推广我们抽样人口的参数(在这种情况下,这个参数就是表示形式)。我们有两个参数,beta 和 alpha,我们训练的目标是让模型从这些控制人口的参数中获得最佳值。
让我们看一个更具体的例子:我们想要创建一个只将特定产品分配给狗的应用。但是你知道,我们有不同品种的狗,狗也有一些与猫相似的面部特征。
为了创建这个应用的机器学习模型,我们抽样了一些狗的图像,但这个样本并没有捕捉到所有品种的狗。机器学习模型的目标是从给定的数据中捕捉到独特的狗的属性/参数(这些独特的属性/参数是表示形式)。
如果模型很好,它应该能够判断输入图像是否是狗。那么我们如何衡量模型的好坏?以下方法用于实现这一点:
-
客观函数
-
评估指标
在接下来的子章节中,我们将看到这些方法是如何工作的。
客观函数
我们已经看到如何从大量人口中抽样数据,并用它来训练我们的模型,并希望模型能很好地泛化。在训练过程中,我们想要衡量我们的模型与我们的目标有多接近,为此我们创建了一个客观函数。有些人称这个函数为不同的名称,比如损失函数或错误率。这个函数返回的分数越低,我们的模型就越好。
为了分类图像是否是狗,我们有包含狗和猫图像的数据集,例如。这个数据集也包含标签。数据集中的每个图像都有一个标签,告诉我们数据集中的图像是狗图像还是猫图像。
以下是数据集的示例:
图 9.5 - 数据集样本
在训练过程中,数据中的每个图像,如图 9.5所示,都被输入到模型中,并且模型预测标签。模型预测的标签与图 9.4中显示的标签通过客观函数进行比较。我们持续训练模型,直到模型预测数据集中每个图像的真实标签。
模型可能能够根据客观函数正确分类数据集中的所有图像,但这并不意味着模型泛化良好,也就是说,模型可能能够在训练期间正确分类一些狗图像,但当给出数据集中没有的图像时,如图 9.4所示,模型会错误分类图像。这引出了第二种方法。
评估指标
我们已经训练了我们的模型,并且它给出了一个非常低的损失分数,这是好的,但我们需要确定模型是否已经捕捉到了整个人口的属性,还是只是对用于训练的数据集的抽样人口。我在说什么?模型在训练时可能表现良好,但如果我们要在包含狗和猫的其他图像上进行测试,实际上可能是糟糕的。
为了检查模型是否良好并且是否捕捉到了独特于狗和猫每个人口的属性,我们在一组数据集上测试模型,这组数据集也是从用于训练的相同人口中抽样得到的。如果模型能够给出更好的分数,那么模型是好的;如果分数与客观函数的相比较差,那么模型是不好的。这个过程称为评估过程,我们使用不同的指标来衡量模型的性能。这些指标称为评估指标。
以下图表显示了机器学习模型的流程:
图 9.6 - 机器学习流程
在这一部分,我们讨论了基于机器学习的信息传递。我们看到了机器学习模型的工作原理以及机器学习模型的基本工作流程。在下一节中,我们将讨论将机器学习任务分组到不同类别中。
机器学习问题/任务
基于模型学习方式的不同,诸如分类问题的机器学习问题或任务可以被归类到不同的组别中。
在这一部分,我们将研究机器学习问题中最流行的两个类别:
-
监督学习
-
无监督学习
首先,我们将研究监督学习。
监督学习
在这个类别中,模型在监督下学习。通过监督,我们指的是模型知道根据提供的标签自己的表现如何。在训练时,我们提供了一个包含一组标签的数据集,这些标签用于纠正和改进模型。有了这个,我们就可以衡量模型的表现如何。
以下机器学习问题/任务属于这个类别:
-
分类问题:在这种类型的问题中,模型被制作成将输入分类到一组离散的类别,比如分类图像是狗还是猫。
-
回归问题:这涉及模型将输入映射到一组连续值。例如,创建一个模型来预测房屋的价格,给定房屋的一些特征。
以下图表显示了分类的示例:
图 9.7 – 分类问题
以下图表显示了回归问题的示例:
图 9.8 – 回归问题
总之,监督学习算法用于数据集中提供了标签的问题,其中标签用于衡量模型的性能。有时我们有数据,但没有一个可以衡量模型表现的标签。这就引出了无监督学习。
无监督学习
当我们没有标签,但有数据时,我们可以做什么?最好的办法是从数据中获取见解。
还记得本节开头的人口例子吗?假设我们从人口中抽取了一些实体,但对他们的行为没有先验知识。最好的办法是研究一段时间,这样我们就可以了解他们的喜好和厌恶,并找出使他们独特的因素。
通过这种观察,我们可以根据他们的信仰、职业、食物口味等将人口分成不同的类别。
以下机器学习问题属于无监督学习类别:
-
聚类问题:聚类问题涉及在数据集(我们抽样的人口)中揭示一些隐藏的属性,然后根据这些属性将人口中的每个实体分组。
-
关联问题:这涉及在人口中发现关联规则。它涉及知道参与一项活动的人是否也参与另一项活动。
这主要是因为我们希望从数据集中获得隐藏的见解,如下图所示:
图 9.9 – 无监督学习(聚类示例)
在这一部分,我们研究了一些机器学习问题的类别,我们还看到了每个机器学习问题类别都很重要的场景,以及它们用于的任务类型。
在下一节中,我们将讨论如何使机器学习更易于访问。
JavaScript 中的机器学习
网络是最可访问的平台,JavaScript 是网络上使用的语言,因此 JavaScript 中的机器学习给了我们更多的控制和可访问性。在第三章的为什么需要 Danfo.js部分,开始使用 Danfo.js,我们谈到了将机器学习带到网络的重要性。我们还谈到了浏览器的计算能力正在增加,这对 JavaScript 来说是一个好处。
在本节中,我将列出一些用于浏览器中机器学习任务的开源工具:
-
TensorFlow.js (tfjs) (
github.com/tensorflow/tfjs
):用于训练和部署机器学习模型的 WebGL 加速 JavaScript 库。 -
datacook (
github.com/imgcook/datacook
):用于数据集特征工程的 JavaScript 框架。 -
Nlp.js(
github.com/axa-group/nlp.js
):用于 NLP 任务的 JavaScript 框架,如情感分析、自动语言识别、实体提取等。 -
Natural(
github.com/NaturalNode/natural
):另外,NLP,它涵盖了几乎所有 NLP 任务所需的算法。 -
Pipcook(
github.com/alibaba/pipcook
):面向 Web 开发人员的机器学习平台。 -
Jimp(
github.com/oliver-moran/jimp
):一个完全用 JavaScript 编写的图像处理库。 -
Brain.js(
github.com/BrainJS/brain.js
):用于浏览器和 Node.js 的 GPU 加速神经网络。
上述工具是最受欢迎的,并且有最新的更新。通过使用这些工具,您可以将 ML 集成到您的下一个 Web 应用程序中。
在下一节中,我们将探讨 ML 在现实世界中的一些应用。
机器学习的应用
ML 正在改变软件开发,并且也使事物更加自动、自动驾驶和自动操作。在本节中,我们将探讨一些 ML 应用的例子。
以下是机器学习应用的例子:
-
机器翻译:ML 使我们能够构建轻松将一种语言翻译成另一种语言的软件。
-
游戏:借助一些先进的 ML 算法,一些软件正在变得更擅长玩更复杂的游戏,比如围棋,并在他们最擅长的领域击败世界冠军。例如,这是一个关于AlphaGo的视频:
www.youtube.com/watch?v=WXuK6gekU1Y
。 -
视觉:机器正在变得更擅长看和为他们所看到的提供意义。
a) 自动驾驶汽车:ML 正在帮助创建完全自动驾驶汽车。
b) 特斯拉展示自动驾驶汽车:www.youtube.com/watch?v=VG68SKoG7vE
- 推荐引擎:ML 算法正在改进推荐引擎并吸引顾客。
Netflix如何使用 ML 进行个性化推荐:netflixtechblog.com/artwork-personalization-c589f074ad76
- 艺术:ML 被用来生成艺术作品、新故事、新绘画和新图像。
a) 这是一个生成从未存在的人的图像的网站:thispersondoesnotexist.com/
。
b) 生成的艺术画廊:www.artaigallery.com/
。
c) 使用 ML 的建筑设计:span-arch.org/
在本节中,我们看到了 ML 如何被用于不同的目的的一些例子。在下一节中,我们将提供一些材料来更好地理解 ML。
深入了解机器学习的资源
在本节中,我们将提供资源,以更深入地了解 ML,并更好地创建利用 ML 算法的软件。
以下是可以用来理解 ML 的资源:
-
fastai(
www.fast.ai/
):这个社区为 ML 从业者提供课程、框架和书籍。 -
Cs231n(
cs231n.stanford.edu/
):这门课程介绍了深度学习的基础知识,并向您介绍了计算机视觉。 -
Hugging Face:Hugging Face 提供了最好的自然语言处理框架和不同的变压器模型。它还有一个课程(
huggingface.co/course/chapter1
),提供了变压器模型和部署的详细信息。 -
Andrew Ng 课程:YouTube 上的一个 ML 课程,也提供了完整的 ML 细节。
有大量的在线学习机器学习的材料可供学习。只需沿着一条路径走到底,避免跳来跳去。
总结
在本章中,我们通过信息传递的概念来研究机器学习。然后我们探讨了它是如何以及为什么起作用的。我们还谈到了从人口中进行抽样以了解人口的概念。
我们讨论了不同类别的机器学习问题,还讨论了在网络平台上进行机器学习所需的一些工具,并展示了一些机器学习在现实世界中的应用示例。
本章的目的是在个人学习过程中帮助理解机器学习的整体概念。
在下一章中,我们将介绍TensorFlow.js。TensorFlow.js 在将机器学习集成到您的 Web 应用程序中非常有用。
第十一章:TensorFlow.js 简介
在上一章中,你已经了解了机器学习(ML)的基础知识,并学习了一些理论基础,这些基础是构建和使用 ML 模型所必需的。
在本章中,我们将向你介绍 JavaScript 中一个高效且流行的 ML 库 TensorFlow.js。在本章结束时,你将知道如何安装和使用 TensorFlow.js,如何创建张量,如何使用 Core 应用程序编程接口(API)对张量进行操作,以及如何使用 TensorFlow.js 的 Layer API 构建回归模型。
在本章中,我们将涵盖以下主题:
-
什么是 TensorFlow.js?
-
安装和使用 TensorFlow.js
-
张量和张量的基本操作
-
使用 TensorFlow.js 构建简单的回归模型
技术要求
在本章中,你应该具备以下工具或资源:
-
现代浏览器,如 Chrome、Safari、Opera 或 Firefox。
-
在你的系统上安装了 Node.js
-
稳定的互联网连接,用于下载软件包和数据集
-
本章的代码可在 GitHub 上克隆并获取,网址为
github.com/PacktPublishing/Building-Data-Driven-Applications-with-Danfo.js/tree/main/Chapter10
什么是 TensorFlow.js?
TensorFlow.js(tfjs)是一个 JavaScript 库,用于在浏览器或 Node.js 中创建、训练和部署 ML 模型。它是由 Google 的 Nikhil Thorat 和 Daniel Smilkov 创建的,最初被称为 Deeplearn.js,在 2018 年并入 TensorFlow 团队并更名为 TensorFlow.js。
TensorFlow.js 提供了两个主要层,如下所述:
-
CoreAPI:这是直接处理张量的低级 API——TensorFlow.js 的核心数据结构。
-
LayerAPI:这是建立在 CoreAPI 层之上的高级层,用于轻松构建 ML 模型。
在后面的章节中,张量和张量的基本操作和使用 TensorFlow.js 构建简单的回归模型,你将学到更多关于 CoreAPI 和 LayerAPI 层的细节。
使用 TensorFlow.js,你可以做到以下几点:
-
执行硬件加速的数学运算
-
为浏览器或 Node.js 开发 ML 模型
-
使用迁移学习(TL)重新训练现有的 ML 模型
-
重用使用 Python 训练的现有 ML 模型
在本章中,我们将介绍执行硬件加速的数学运算以及使用 TensorFlow.js 开发 ML 模型。如果你想了解最后两种用例——重新训练和重用 ML 模型——那么官方的 TensorFlow.js 文档(www.tensorflow.org/js/guide
)是一个很好的起点。
现在我们已经介绍完了,接下来的章节中,我们将向你展示如何在浏览器和 Node.js 环境中安装和使用 TensorFlow.js。
安装和使用 TensorFlow.js
正如我们之前提到的,TensorFlow.js 可以在浏览器和 Node.js 环境中安装和运行。在接下来的段落中,我们将向你展示如何实现这一点,从浏览器开始。
在浏览器中设置 TensorFlow.js
在浏览器中安装 TensorFlow.js 有两种方式。这里进行了概述:
-
通过脚本标签
-
使用诸如Node Package Manager(npm)或Yarn之类的包管理器
通过脚本标签安装
通过script
标签安装 TensorFlow.js 很容易。只需将script
标签放在你的超文本标记语言(HTML)文件的头文件中,如下面的代码片段所示:
<script src="img/tf.min.js"></script>
要确认 TensorFlow.js 已安装,打开浏览器中的 HTML 文件,并检查网络标签。你应该看到名称为tf.min.js
和状态码为200
,如下截图所示:
图 10.1 - 网络标签显示了 tfjs 成功安装
您可以在 HTML 文件的 body 中添加一个简单的脚本来确认成功安装tfjs
。在 HTML 文件的script
部分中,添加以下代码:
...
<script>
tf.ready().then(()=>{
console.log("Tensorflow.js loaded successfully!");
})
</script>
...
上面的代码片段将在浏览器控制台中记录文本Tensorflow.js loaded
successfully!
,一旦 TensorFlow.js 加载并准备好在页面上使用。要查看输出,请在浏览器中打开 HTML 文件并检查控制台输出。您应该会看到一个输出结果,如下面的屏幕截图所示:
图 10.2 - add 操作的张量输出
接下来,让我们看看如何通过软件包管理器安装tfjs
。
通过软件包管理器安装
您可以通过npm
或yarn
等软件包管理器安装tfjs
。当您需要在客户端项目(如 React 和 Vue 项目)中使用tfjs
时,这是非常有用的。
要使用npm
安装,请在命令行界面(CLI)中运行以下命令:
npm install @tensorflow/tfjs
要使用yarn
安装,也可以在 CLI 中运行以下命令:
yarn add @tensorflow/tfjs
注意
在通过 CLI 成功安装软件包之前,您必须在系统中安装npm
或yarn
之一,最好是全局安装。如果您已经安装了 Node.js,那么您已经有了npm
。要安装yarn
,您可以按照这里的步骤进行操作:classic.yarnpkg.com/en/docs/install/#mac-stable
。
安装成功后,您可以导入并使用tfjs
,如下面的代码片段所示:
import * as tf from '@tensorflow/tfjs';
const x = tf.tensor2d([1, 2, 3, 4], [2, 2]);
const y = tf.tensor2d([1, 3, 5, 7], [2, 2]);
const sum = x.add(y)
sum.print()
运行上面的代码片段将在控制台中产生以下输出:
图 10.3 - 使用软件包管理器安装 tfjs 的输出
通过按照上面的代码块中的步骤,您应该能够在浏览器或客户端框架中安装和使用tfjs
。在下一节中,我们将向您展示如何在 Node.js 环境中安装tfjs
。
在 Node.js 中安装 TensorFlow.js
在 Node.js 中安装tfjs
非常简单,但首先确保您的系统上已安装了 Node.js、npm
或yarn
。
Node.js 中的 TensorFlow.js 有三个选项,安装的选择将取决于您的系统规格。在接下来的子章节中,我们将向您展示这三个选项。
使用本机 C++绑定安装 TensorFlow.js
@tensorflow/tfjs-node
(www.npmjs.com/package/@tensorflow/tfjs-node
)版本的tfjs
直接连接到 TensorFlow 的本机 C++绑定。这使它快速,并且使其与 TensorFlow 的 Python 版本具有接近的性能。这意味着tfjs-node
和tf.keras
在内部使用相同的 C++绑定。
要安装tfjs-node
,只需通过 CLI 运行以下命令:
npm install @tensorflow/tfjs-node
或者,如果使用yarn
,也可以通过 CLI 运行以下命令:
yarn add @tensorflow/tfjs-node
安装支持 GPU 的 TensorFlow.js
@tensorflow/tfjs-node-gpu
版本的tfjs
支持在tfjs-node-gpu
上运行操作,通常比tfjs-node
快,因为操作可以很容易地进行矢量化。
要安装tfjs-node-gpu
,只需通过 CLI 运行以下命令:
npm install @tensorflow/tfjs-node-gpu
或者,如果您使用yarn
,也可以通过 CLI 运行以下命令:
yarn add @tensorflow/tfjs-node-gpu
安装普通的 TensorFlow.js
@tensorflow/tfjs
版本是tfjs
的纯 JavaScript 版本。在性能方面它是最慢的,应该很少使用。
要安装此版本,只需通过 CLI 运行以下命令:
npm install @tensorflow/tfjs
或者,如果您使用yarn
,也可以通过 CLI 运行以下命令:
yarn add @tensorflow/tfjs
如果您按照上述步骤操作,那么您应该至少安装了tfjs
的一个版本。您可以使用以下代码示例测试安装是否成功:
const tf = require('@tensorflow/tfjs-node')
// const tf = require('@tensorflow/tfjs-node-gpu') GPU version
// const tf = require('@tensorflow/tfjs') Pure JS version
const xs = tf.randomNormal([100, 10])
const ys = tf.randomNormal([100, 1])
const sum = xs.add(ys)
const xsSum = xs.sum()
const xsMean = xs.mean()
console.log("Sum of xs and ys")
sum.print()
console.log("Sum of xs")
xsSum.print()
console.log("Mean of xs")
xsMean.print()
注意
当我们想要查看底层数据时,我们在张量上调用print()
函数。如果我们使用默认的console.log
,我们将得到Tensor
对象。
运行前面的代码应该在控制台中输出以下内容:
图 10.4 - 在 Node.js 中测试 tfjs 的输出
现在您已经成功在项目中安装了tfjs
,在下一节中,我们将向您介绍tfjs
的核心数据结构——张量。
张量和张量的基本操作
张量是tfjs
中的基本数据结构。您可以将张量视为向量、矩阵或高维数组的泛化。我们在什么是 TensorFlow.js?部分介绍的CoreAPI公开了不同的函数,用于创建和处理张量。
以下屏幕截图显示了标量、向量和矩阵与张量之间的简单比较:
图 10.5 - 简单的 n 维数组与张量的比较
提示
矩阵是一个m x n
数字的网格,其中m
表示行数,n
表示列数。矩阵可以是一维或多维的,形状相同的矩阵支持彼此的直接数学运算。
另一方面,向量是一个一维矩阵,形状为(1,1);也就是说,它有一行和一列,例如,[2, 3],[3, 1, 4]。
我们之前提到过,张量更像是一个广义的矩阵,它扩展了矩阵的概念。张量可以通过它们的秩来描述。秩类似于形状的概念,但是用一个数字表示,而不是形状。在下面的列表中,我们看到了不同类型的张量秩及其示例:
-
秩为 0 的张量是标量,例如,1、20 或 100。
-
秩为 1 的张量是向量,例如,[1, 20]或[20, 100, 23.6]。
-
秩为 2 的张量是矩阵,例如,[[1, 3, 6], [2.3, 5, 7]]。
请注意,我们可以有秩为 4 或更高的张量,这些被称为更高维度的张量,可能难以可视化。请参见下面的屏幕截图,以更好地理解张量:
图 10.6 - 不同秩的张量比较
除了秩,张量还具有其他属性,如dtype
、data
、axis
和shape
。这些在这里更详细地描述:
-
dtype
属性(数据类型)是张量持有的数据类型,例如,秩为 1 的张量具有以下数据[2.5, 3.8],其 dtype 为float32
。默认情况下,数值张量的 dtype 为float32
,但可以在创建过程中更改。TensorFlow.js 支持float32
、int32
、bool
、complex64
和string
数据类型。 -
data
属性是张量的内容。这通常存储为数组。 -
axis
属性是张量的特定维度,例如,m x n张量具有m或n的轴。轴可用于指定在哪个维度上执行操作。 -
shape
属性是张量的维度。将形状视为张量每个轴上的元素数量。
现在您对张量是什么有了基本的了解,在下一小节中,我们将向您展示如何创建张量并对其进行一些基本操作。
创建张量
张量可以使用tf.tensor()
方法创建,如下面的代码片段所示:
const tf = require('@tensorflow/tfjs-node')
const tvector = tf.tensor([1, 2, 3, 4]);
console.log(tvector)
//output
Tensor {
kept: false,
isDisposedInternal: false,
shape: [ 4 ],
dtype: 'float32',
size: 4,
strides: [],
dataId: {},
id: 0,
rankType: '1'
}
在前面的代码片段中,我们将一个平坦数组(向量)传递给tf.tensor()
方法,以创建一个tfjs
张量。创建后,我们现在可以访问不同的属性和函数,用于操作或转换张量。
其中一个属性是shape
属性,我们可以按照下面的代码片段中所示进行调用:
console.log('shape:', tvector.shape);
//outputs: shape: [ 4 ]
请注意,当您使用console.log
记录张量时,您会得到一个张量对象。如果您需要查看底层张量数组,可以在张量上调用print()
函数,如下面的代码片段所示:
tvector.print();
//outputs
Tensor
[1, 2, 3, 4]
如果您需要访问张量的基础数据,可以调用array()
或arraySync()
方法。两者之间的区别在于,array()
是异步运行的,并返回一个解析为基础数组的 promise,而arraySync()
是同步运行的。您可以在这里看到一个示例:
const tvectorArray = tvector.array()
const tvectorArraySync = tvector.arraySync()
console.log(tvectorArray)
console.log(tvectorArraySync)
//outputs
Promise { <pending> }
[ 1, 2, 3, 4 ]
您还可以通过指定shape
参数来创建张量。例如,在下面的代码片段中,我们从一个平坦数组创建一个 2 x 2(二维(2D))张量:
const ts = tf.tensor([1, 2, 3, 4], [2, 2]);
console.log('shape:', ts.shape);
ts.print();
//outputs
shape: [ 2, 2 ]
Tensor
[[1, 2],
[3, 4]]
或者,我们可以创建一个 1 x 4(一维(1D))张量,如下面的代码片段所示:
const ts = tf.tensor([1, 2, 3, 4], [1, 4]);
console.log('shape:', ts.shape);
ts.print();
//outputs
shape: [ 1, 4 ]
Tensor
[[1, 2, 3, 4],]
但请注意,形状必须匹配元素的数量,例如,您不能从具有四个元素的平坦数组创建一个2 x 5
维的张量。以下代码将引发形状错误:
const ts = tf.tensor([1, 2, 3, 4], [2, 5]);
输出如下所示:
图 10.7 – 形状不匹配引发的错误
Tfjs
明确提供了用于创建 1D、2D、shape
参数的函数。您可以在官方tfjs
API 中阅读更多关于创建张量的信息:js.tensorflow.org/api/latest/#Tensors-Creation
。
默认情况下,张量具有float32
的dtype
属性,因此您创建的每个张量都将具有float32
的dtype
。如果这不是所需的dtype
,您可以在张量创建时指定类型,就像我们在以下代码片段中演示的那样:
const tsInt = tf.tensor([1, 2, 3, 4], [1, 4], 'int32');
console.log('dtype:', tsInt.dtype);
//outputs
dtype: int32
现在您知道如何创建张量,我们将继续对张量进行操作。
对张量进行操作
正如我们之前所说,张量以网格形式存储数据,并允许进行许多操作来操作或转换这些数据。tfjs
提供了许多用于线性代数和机器学习的运算符。
tfjs
中的操作被分成不同的部分。以下是一些常见操作的解释:
-
add()
用于张量的加法,sub()
用于张量的减法,mul()
用于张量的乘法,div()
用于张量的除法。在这里可以看到带有示例的完整列表:js.tensorflow.org/api/3.7.0/#Operations-Arithmetic
。 -
cos()
用于计算张量的余弦,sin()
用于计算张量的正弦,exp()
用于计算张量的指数,log()
用于计算张量的自然对数。在这里可以看到带有示例的完整列表:js.tensorflow.org/api/3.7.0/#Operations-Basic%20math
。 -
矩阵:这些运算符用于矩阵运算,如点积、范数或转置。您可以在这里看到支持的运算符的完整列表:
js.tensorflow.org/api/3.7.0/#Operations-Matrices
。 -
conv1d
,用于计算输入x
的 1D 卷积,以及maxpool3D
,用于计算 3D 最大池化操作。在这里可以看到完整列表:js.tensorflow.org/api/3.7.0/#Operations-Convolution
。 -
min
、max
、sum
、mean
、argMax
和argMin
。您可以在这里看到带有示例的完整列表:js.tensorflow.org/api/3.7.0/#Operations-Reduction
。 -
equal
、greater
、greaterEqual
和less
。您可以在这里看到带有示例的完整列表:js.tensorflow.org/api/3.7.0/#Operations-Logical
。
您可以在官方 API 中看到支持的操作的完整列表:js.tensorflow.org/api/3.7.0/#Operations
。
现在您对可用的张量运算符有了基本的了解,我们将展示一些代码示例。
对张量应用算术运算
我们可以通过直接在第一个张量上调用add()
方法并将第二个张量作为参数传递来添加两个张量,如下面的代码片段所示:
const tf = require('@tensorflow/tfjs-node')
const a = tf.tensor1d([1, 2, 3, 4]);
const b = tf.tensor1d([10, 20, 30, 40]);
a.add(b).print();
//outputs
Tensor
[11, 22, 33, 44]
请注意,您还可以通过在tf
对象上调用运算符来直接添加或应用任何运算符,如下面的代码片段所示:
const tf = require('@tensorflow/tfjs-node')
const a = tf.tensor1d([1, 2, 3, 4]);
const b = tf.tensor1d([10, 20, 30, 40]);
const sum = tf.add(a, b)
sum.print()
//outputs
Tensor
[11, 22, 33, 44]
使用这些知识,您可以执行其他算术运算,如减法、乘法、除法和幂运算,如下面的代码片段所示:
const a = tf.tensor1d([1, 2, 3, 4]);
const b = tf.tensor1d([10, 20, 30, 40]);
const tfsum = tf.add(a, b)
const tfsub = tf.sub(b, a)
const tfdiv = tf.div(b, a)
const tfpow = tf.pow(b, a)
const tfmax = tf.maximum(a, b)
tfsum.print()
tfsub.print()
tfdiv.print()
tfpow.print()
tfmax.print()
//outputs
Tensor
[11, 22, 33, 44]
Tensor
[9, 18, 27, 36]
Tensor
[10, 10, 10, 10]
Tensor
[10, 400, 27000, 2560000]
Tensor
[10, 20, 30, 40]
值得一提的是,传递给运算符的张量的顺序很重要,因为顺序的改变会导致结果不同。例如,如果我们将前面的div
操作的顺序从const tfsub = tf.sub(b, a)
改为const tfsub = tf.sub(a, b)
,那么我们会得到一个负结果,如下面的输出所示:
Tensor
[-9, -18, -27, -36]
请注意,涉及两个张量的所有操作只有在两个张量具有相同形状时才能工作。例如,以下操作将引发无效形状错误:
const a = tf.tensor1d([1, 2, 3, 4]);
const b = tf.tensor1d([10, 20, 30, 40, 50]);
const tfsum = tf.add(a, b)
图 10.8–在具有不同形状的张量上执行操作时出现无效形状错误
在下一小节中,我们将看一些关于张量的基本数学运算的例子。
在张量上应用基本数学运算
根据前一小节的示例格式,在张量上应用算术运算,我们给出了一些在张量上计算数学运算的示例,如下所示:
const tf = require('@tensorflow/tfjs-node')
const x = tf.tensor1d([-1, 2, -3, 4]);
x.abs().print(); // Computes the absolute values of the tensor
x.cos().print(); // Computes the cosine of the tensor
x.exp().print(); // Computes the exponential of the tensor
x.log().print(); // Computes the natural logarithm of the tensor
x.square().print(); // Computes the sqaure of the tensor
输出如下所示:
Tensor
[1, 2, 3, 4]
Tensor
[0.5403023, -0.4161468, -0.9899925, -0.6536436]
Tensor
[0.3678795, 7.3890562, 0.0497871, 54.5981522]
Tensor
[NaN, 0.6931472, NaN, 1.3862944]
Tensor
[1, 4, 9, 16]
正如我们之前提到的,您可以直接从tf
对象调用运算符,例如,x.cos()
变成了tf.cos(x)
。
在张量上应用减少操作
我们还可以对张量应用诸如mean
、min
、max
、argMin
和argMax
之类的减少操作。以下是一些mean
、min
、max
、argMin
和argMax
的例子:
const x = tf.tensor1d([1, 2, 3]);
x.mean().print(); // or tf.mean(x) Returns the mean value of the tensor
x.min().print(); // or tf.min(x) Returns the smallest value in the tensor
x.max().print(); // or tf.max(x) Returns the largest value in the tensor
x.argMax().print(); // or tf.argMax(x) Returns the index of the largest value
x.argMin().print(); // or tf.argMin(x) Returns the index of the smallest value
输出如下所示:
Tensor 2
Tensor 1
Tensor 3
Tensor 2
Tensor 0
掌握了 ML、张量和可以在张量上执行的操作的基本知识,现在您已经准备好构建一个简单的 ML 模型了。在本章的下一节中,我们将总结您在本节中学到的所有内容。
使用 TensorFlow.js 构建一个简单的回归模型
在上一章[第九章](B17076_09_ePub_RK.xhtml#_idTextAnchor166),机器学习基础中,您已经了解了 ML 的基础知识,特别是回归和分类模型的理论方面。在本节中,我们将向您展示如何使用tfjs
LayerAPI创建和训练回归模型。具体来说,在本节结束时,您将拥有一个可以从超市数据中预测销售价格的回归模型。
在本地设置您的环境
在构建回归模型之前,您必须在本地设置您的环境。在本节中,我们将在 Node.js 环境中工作。这意味着我们将使用 TensorFlow.js 和 Danfo.js 的node
版本。
按照这里的步骤设置您的环境:
- 在新的工作目录中,为您的项目创建一个文件夹。我们将创建一个名为
sales_predictor
的文件夹,如下面的代码片段所示:
mkdir sales_predictor
cd sales_predictor
- 接下来,在文件夹目录中打开终端,并通过运行以下命令初始化一个新的
npm
项目:
npm init
- 接下来,按照以下步骤安装
Danfo.js
节点包:
yarn add danfojs-node
or if using npm
npm install danfojs-node
- 还可以从终端创建一个
src
文件夹,并添加train.js
,model.js
和data
_proc.js
文件。您可以通过代码编辑器手动创建这些文件夹/文件,也可以通过在终端中运行以下命令来创建:
data_proc.js, and model.js) in the src folder. These files will contain code for processing data, creating a tfjs model, and model training, respectively.
现在您已经设置好了项目和文件,我们将在下一节中继续进行数据检索和处理步骤。
检索和处理训练数据集
我们将用于模型训练的数据集称为BigMart 销售数据集(www.kaggle.com/devashish0507/big-mart-sales-prediction
)。它作为一个公共数据集在 Kaggle 上可用,这是一个流行的数据科学竞赛平台。
您可以直接从本章的代码库中下载数据集:github.com/PacktPublishing/Building-Data-Driven-Applications-with-Danfo.js-/blob/main/Chapter10/sales_predictor/src/dataset/Train.csv
。成功下载后,在您的项目目录中创建一个名为dataset
的文件夹,并将数据集复制到其中。
为了确认一切都正常,您的项目src
文件夹应该具有以下文件结构:
|-data-proc.js
|-dataset
| └── Train.csv
|-model.js
|-train.js
与所有数据科学问题一样,通常会提供一个通用的问题陈述,以指导您解决的问题。就 BigMart 销售数据集而言,问题陈述如下:
BigMart 已经收集了 2013 年在不同城市的 10 家商店中 1,559 种产品的销售数据。此外,每种产品和商店的某些属性已经被定义。目标是建立一个预测模型,找出每种产品在特定商店的销售情况。
从前面的问题陈述中,您将注意到构建此模型的目的是帮助 BigMart 有效预测每种产品在特定商店的销售情况。现在,这里的销售价格意味着一个连续的值,因此,我们有一个回归问题。
现在您已经可以访问数据并理解了问题陈述,您将使用Danfo.js
加载数据集并进行一些数据处理和清理。
注意
我们在代码库中提供了一个单独的Danfo Notebook(Dnotebook)文件:github.com/PacktPublishing/Building-Data-Driven-Applications-with-Danfo.js-/blob/main/Chapter10/sales_predictor/src/bigmart%20sales%20notebook.json
。在笔记本中,我们对销售数据集进行了一些数据探索和分析,其中大部分将帮助我们进行以下处理步骤。
在您的代码编辑器中打开data_proc.js
文件,按照这里给出的步骤处理 BigMart 销售数据集:
- 首先,我们将导入
danfojs-node
,如下所示:
const dfd = require("danfojs-node")
- 然后,我们创建一个名为
processData
的函数,该函数接受数据集路径,如下所示:
async function processData(trainDataPath) {
//… process code goes here
}
- 接下来,在
processData
函数的主体中,我们使用read_csv
函数加载数据集并打印标题,如下所示:
const salesDf = await dfd.read_csv(trainDataPath)
salesDf.head().print()
- 为了确保数据加载正常工作,您可以将数据集的路径传递给
processData
函数,如下面的代码片段所示:
processData("./dataset/train.csv")
- 在您的终端中,使用以下命令运行
data_proc.js
文件:
node data_proc.js
这将输出以下内容:
图 10.9 - 显示 BigMart 销售数据集的头部值
- 从 Dnotebook 文件的分析中,我们注意到
Item_Weight
和Outlet_Sales
两列存在缺失值。在下面的代码片段中,我们将使用均值和众数分别填充这些缺失值:
...
salesDf.fillna({
columns: ["Item_Weight", "Outlet_Size"],
values: [salesDf['Item_Weight'].mean(), "Medium"],
inplace: true
})
...
- 正如我们注意到的,数据集是混合的分类(字符串)列和数值(
float32
和int32
)列。这意味着我们必须在将它们传递给我们的模型之前,将所有分类列转换为数值形式。在下面的代码片段中,我们使用 Danfo.js 的LabelEncoder
将每个分类列编码为数值列:
...
let encoder = new dfd.LabelEncoder()
let catCols = salesDf.select_dtypes(includes = ['string']).column_names // get all categorical column names
catCols.forEach(col => {
encoder.fit(salesDf[col])
enc_val = encoder.transform(salesDf[col])
salesDf.addColumn({ column: col, value: enc_val })
})
...
- 接下来,我们将从训练数据集中分离出目标。目标,正如我们从问题陈述中注意到的那样,是销售价格。这对应于最后一列
Item_Outlet_Sales
。在下面的代码片段中,我们将使用iloc
函数拆分数据集:
...
let Xtrain, ytrain;
Xtrain = salesDf.iloc({ columns: [`1:${salesDf.columns.length - 1}`] })
ytrain = salesDf['Item_Outlet_Sales']
console.log(`Training Dataset Shape: ${Xtrain.shape}`)
...
- 接下来,我们将标准化我们的数据集。标准化数据集会强制使每一列都在同一比例上,从而提高模型训练。在下面的代码片段中,我们使用 Danfo.js 的
StandardScaler
来标准化数据集:
...
let scaler = new dfd.MinMaxScaler()
scaler.fit(Xtrain)
Xtrain = scaler.transform(Xtrain)
...
- 最后,为了完成
processData
函数,我们将返回原始张量,如下面的代码片段所示:
...
return [Xtrain.tensor, ytrain.tensor]
...
注意
您可以在此处的代码存储库中查看完整的代码:github.com/PacktPublishing/Building-Data-Driven-Applications-with-Danfo.js/blob/main/Chapter10/sales_predictor/src/data-proc.js
。
执行并打印最终的data_proc.js
文件中的张量应该会给您类似于以下截图中显示的张量:
图 10.10 - 处理后的 Final BigMart 数据张量
现在您有一个可以处理原始数据集并返回张量的函数,让我们继续使用tfjs
创建模型。
使用 TensorFlow.js 创建模型
正如我们之前提到的,tfjs
提供了一个 Layers API,可用于定义和创建 ML 模型。Layers API 类似于流行的 Keras API,因此已经熟悉 Keras 的 Python 开发人员可以轻松地将其代码移植到tfjs
。
Layers API 提供了创建模型的两种方式 - 顺序和模型格式。我们将在以下子部分中简要解释并举例说明这两种方式。
创建模型的顺序方式
这是创建模型的最简单和最常见的方式。它只是一个多个模型层的堆叠,其中堆栈中的第一层定义了输入,最后一层定义了输出,而中间层可以有很多。
以下代码片段显示了一个两层顺序模型的示例:
const model = tf.sequential();
// First layer must have an input shape defined.
model.add(tf.layers.dense({units: 32, inputShape: [50]}));
model.add(tf.layers.dense({units: 24}));
model.add(tf.layers.dense({units: 1}));
您会注意到前面的代码片段中,序列中的第一层提供了inputShape
参数。这意味着模型期望输入有50
列。
您还可以通过传递层列表来创建顺序层,如下面的代码片段所示:
const model = tf.sequential({
layers: [tf.layers.dense({units: 32, inputShape: [50]}),
tf.layers.dense({units: 24}),
tf.layers.dense({units: 1})]
});
接下来,让我们看看模型格式。
创建模型的模型方式
使用模型格式创建模型在创建模型时提供了更大的灵活性。与仅接受线性层堆叠的模型不同,使用模型层定义的模型可以是非线性的、循环的,可以像您想要的那样高级或连接。
例如,在以下代码片段中,我们使用模型格式创建了一个两层网络:
const input = tf.input({ shape: [5] });
const denseLayer1 = tf.layers.dense({ units: 16, activation: 'relu' });
const denseLayer2 = tf.layers.dense({ units: 8, activation: 'relu' });
const denseLayer3 = tf.layers.dense({ units: 1 })
const output = denseLayer3.apply(denseLayer2.apply(denseLayer1.apply(input)))
const model = tf.model({ inputs: input, outputs: output });
从前面的示例代码中,您可以看到我们明确调用了apply
函数,并将要连接的层作为参数传递。这样,我们可以构建具有类似图形连接的混合和高度复杂的模型。
您可以在官方tfjs
文档中了解有关 Layers API 的更多信息:js.tensorflow.org/api/latest/#Models
。
现在您知道如何使用 Layer API 创建模型,我们将在下一节中创建一个简单的三层回归模型。
创建一个简单的三层回归模型
回归模型,正如我们在上一章第九章,机器学习基础中所解释的,是具有连续输出的模型。要使用tfjs
创建回归模型,我们定义层的堆栈,并在最后一层将units
的数量设置为1
。例如,打开代码存储库中的model.js
文件。在第 7-11 行,您应该看到以下顺序模型定义:
...
const model = tf.sequential();
model.add(tf.layers.dense({ inputShape: [11], units: 128, kernelInitializer: 'leCunNormal' }));
model.add(tf.layers.dense({units: 64, activation: 'relu' }));
model.add(tf.layers.dense({units: 32, activation: 'relu' }));
model.add(tf.layers.dense({units: 1}))
...
请注意,在第一层中,我们将inputShape
参数设置为11
。这是因为我们的 BigMart 数据集中有11
个训练列。您可以通过打印处理后的张量的形状来确认这一点。在最后一层,我们将units
属性设置为1
,因为我们想要预测一个单一的连续值。
中间的层可以有很多,单位可以取任意数量。因此,在本质上,增加中间层会给我们一个更深的模型,增加单位会给我们一个更宽的模型。选择要使用的层不仅取决于问题,还取决于执行多次实验和训练。
有了这几行代码,您已经成功地在tfjs
中创建了一个三层回归模型。
创建模型后,您通常要做的下一件事是编译模型。那么,编译是什么?编译是为训练和评估准备模型的过程。这意味着在编译阶段,我们必须设置模型的优化器、损失和/或训练指标。
在开始训练之前,tfjs
模型必须先进行编译。那么,在tfjs
中如何编译模型呢?这可以通过在已定义的模型上调用compile
函数,并设置您想要计算的优化器和指标来完成。
在model.js
文件的13-17 行中,我们通过将优化器设置为Adam
,将loss
和metrics
属性设置为meanSquaredError
来编译了我们的回归模型。请查看以下代码片段:
...
model.compile({
optimizer: tf.train.adam(LEARNING_RATE),
loss: tf.losses.meanSquaredError,
metrics: ['mse']
});
...
值得一提的是,有不同类型的优化器可供选择;请在js.tensorflow.org/api/latest/#Training-Optimizers
上查看完整列表。选择使用哪种优化器将取决于您的经验,以及多次实验。
在损失方面,问题将告诉您使用哪种损失函数。在我们的情况下,由于这是一个回归问题,我们可以使用均方误差(MSE)函数。要查看可用损失函数的完整列表,请访问js.tensorflow.org/api/latest/#Training-Losses
。
最后,在模型训练期间计算和显示的指标方面,我们可以指定多个选项,就像损失一样,指定的指标将取决于您要解决的问题。在我们的情况下,我们也可以计算 MSE。要查看支持的指标的完整列表,请访问js.tensorflow.org/api/latest/#Metrics
。
现在您已经定义并编译了模型,我们将继续进行本章的下一个也是最后一个部分,即模型训练。
使用处理过的数据集训练模型
train.js
文件包含了对处理过的数据集进行三层回归模型训练的代码。在接下来的步骤中,我们将带您完成整个模型训练的过程:
- 首先,让我们使用
processData
函数加载和处理数据集,如下所示:
…
const data = await processData("./dataset/train.csv")
const Xtrain = data[0]
const ytrain = data[1]
…
- 接下来,我们使用
getModel
函数加载模型,如下所示:
…
const model = getModel()
…
- 接下来,非常重要的是,我们在模型上调用
fit
函数,传递训练数据、目标和一些参数,如epoch
、batchSize
和validationSplits
参数,以及一个名为onEpochEnd
的回调函数,如下所示:
…
await model.fit(Xtrain, ytrain, {
batchSize: 24,
epochs: 20,
validationSplit: 0.2,
callbacks: {
onEpochEnd: async (epoch, logs) => {
const progressUpdate = `EPOCH (${epoch + 1}): Train MSE: ${Math.sqrt(logs.mse)}, Val MSE: ${Math.sqrt(logs.val_mse)}\n`
console.log(progressUpdate);
}
}
});
...
让我们了解一下我们传递给fit
函数的参数的作用,如下所示:
-
Xtrain
:训练数据。 -
ytrain
:目标数据。 -
epoch
:epoch 大小是迭代训练数据的次数。 -
batchSize
:批量大小是用于计算一个梯度更新的数据点或样本的数量。 -
validationSplit
:验证分割是一个方便的参数,告诉tfjs
保留指定百分比的数据用于验证。当我们不想手动将数据集分割成训练集和测试集时,可以使用这个参数。 -
callbacks
:回调函数,顾名思义,接受在模型训练的不同生命周期中调用的函数列表。回调函数在监控模型训练中非常重要。在这里可以看到完整的回调函数列表:js.tensorflow.org/api/latest/#tf.Sequential.fitDataset
。
- 最后,我们保存模型,以便在进行新预测时使用:
...
await model.save("file://./sales_pred_model")
...
运行train.js
文件将加载和处理数据集,加载模型,并对指定数量的 epochs 运行模型训练。我们指定的回调函数(onEpochEnd
)将在每个 epoch 结束后打印出损失和均方根误差,如下面的截图所示:
图 10.11 – 显示损失和均方根误差的模型训练日志
就是这样!您已经成功地创建、训练和保存了一个可以使用 TensorFlow.js 预测销售价格的回归模型。在本章的下一个和最后一节中,我们将向您展示如何加载您保存的模型并用它进行预测。
使用训练好的模型进行预测
为了进行预测,我们必须加载保存的模型,并在其上调用predict
函数。TensorFlow.js 提供了一个loadLayersModel
函数,用于从文件系统加载保存的模型。在以下步骤中,我们将向您展示如何实现这一点:
-
创建一个名为
predict.js
的新文件。 -
在
predict.js
文件中,添加以下代码:
const dfd = require("danfojs-node")
const tf = dfd.tf
async function loadModel() {
const model = await tf.loadLayersModel('file://./sales_pred_model/model.json');
model.summary()
return model
}
loadModel()
前面的代码从文件路径加载了保存的模型并打印了摘要。摘要的输出应该与下面的截图类似:
图 10.12 – 保存模型的模型摘要
- 现在,创建一个名为
predict
的新函数,该函数使用保存的模型进行预测,如下面的代码片段所示:
...
async function predict() {
//You'll probably have to do some data pre-processing as we did before training
const data = [0.1, 0.21, 0.25, 0.058, 0.0, 0.0720, 0.111, 1, 0, 0.5, 0.33] //sample processed test data
const model = await loadModel()
const value = model.predict(tf.tensor(data, [1, 11])) //cast data to required shape
console.log(value.arraySync());
}
predict()
输出如下:
[ [ 738.65380859375 ] ]
...
在前面的函数中,我们在模型上调用predict
函数,并传递一个具有正确形状(批次,11)的张量,这是我们的模型所期望的。这将返回一个预测的张量,从这个张量中,我们可以得到基础值。从这个值,我们可以得知具有这些特定值的产品大约会售价美元(USD)$739。
注意
在实际应用中,您通常会从另一个逗号分隔值(CSV)文件中加载测试数据集,并应用与训练过程中相同的数据处理步骤。本示例使用内联数据点,只是为了演示如何使用保存的模型进行预测。
这就是本章的结束了!恭喜您走到了这一步。我相信您已经学到了很多。在下一章中,我们将通过构建一个更实用的应用程序——一个推荐系统来深入探讨!
总结
在这一章中,我们向您介绍了 TensorFlow.js 的基础知识。具体来说,您学习了如何在浏览器和 Node.js 环境中安装 TensorFlow.js,学习了张量和tfjs
的核心数据结构,学习了核心和层 API,最后,您学会了如何构建、训练和保存回归模型。
在下一章中,我们将深入探讨一个更实用和动手的项目,这里所学到的知识将帮助您使用 TensorFlow.js 和 Danfo.js 构建出色的产品。
第十二章:使用 Danfo.js 和 TensorFlow.js 构建推荐系统
在前一章中,我们向您介绍了 TensorFlow.js,并向您展示了如何创建一个简单的回归模型来预测销售价格。在本章中,我们将进一步创建一个推荐系统,可以根据用户的偏好向不同用户推荐电影。通过本章的学习,您将了解推荐系统的工作原理,以及如何使用 JavaScript 构建一个推荐系统。
具体来说,我们将涵盖以下主题:
-
推荐系统是什么?
-
创建推荐系统的神经网络方法
-
构建电影推荐系统
技术要求
要在本章中跟进,您将需要以下内容:
-
现代浏览器,如 Chrome、Safari、Opera 或 Firefox
-
Node.js、Danfo.js、TensorFlow.js 和(可选)Dnotebook已安装在您的系统上
-
稳定的互联网连接用于下载数据集
-
本章的代码可在 GitHub 上找到并克隆:
github.com/PacktPublishing/Building-Data-Driven-Applications-with-Danfo.js/tree/main/Chapter11
Danfo.js、TensorFlow.js 和 Dnotebook 的安装说明可以在第三章**,使用 Danfo.js 入门,第十章**,TensorFlow.js 简介和第二章**,Dnotebook - JavaScript 交互计算环境中找到。
什么是推荐系统?
推荐系统是任何可以预测用户对物品的偏好或有用性评分的系统。利用这个偏好评分,它可以向用户推荐物品。
这里的物品可以是数字产品,如电影、音乐、书籍,甚至衣服。每个推荐系统的目标是能够推荐用户会喜欢的物品。
推荐系统非常受欢迎,几乎无处不在;例如:
-
诸如Netflix、Amazon Prime、Hulu和Disney+等电影流媒体平台使用推荐系统向您推荐电影。
-
诸如Facebook、Twitter和Instagram等社交媒体网站使用推荐系统向用户推荐朋友。
-
诸如Amazon和AliExpress等电子商务网站使用推荐系统向用户推荐衣服、书籍和电子产品等产品。
推荐系统主要是使用用户-物品互动的数据构建的。因此,在构建推荐系统时,通常遵循三种主要方法。这些方法是协同过滤、基于内容的过滤和混合方法。我们将在以下子章节中简要解释这些方法。
协同过滤方法
在协同过滤方法中,推荐系统是基于用户的过去行为或历史建模的。也就是说,这种方法利用现有的用户互动,如对物品的评分、喜欢或评论,来建模用户的偏好,从而了解用户喜欢什么。下图显示了协同过滤方法如何帮助构建推荐系统:
图 11.1 - 基于协同过滤的推荐系统构建方法
在上图中,您可以看到两个观看了相同电影,可能评分相同的用户被分为相似用户,因为左边的人看过的电影被推荐给了右边的人。在基于内容的过滤方法中,推荐系统是基于物品特征建模的。也就是说,物品可能预先标记有某些特征,比如类别、价格、流派、大小和收到的评分,利用这些特征,推荐系统可以推荐相似的物品。
下图显示了基于内容的过滤方法构建推荐系统的工作原理:
图 11.2 - 基于内容的过滤方法构建推荐系统
在上图中,您可以观察到相互相似的电影会被推荐给用户。
混合过滤方法
混合方法,顾名思义,是协同和基于内容的过滤方法的结合。也就是说,它结合了两种方法的优点,创建了一个更好的推荐系统。大多数现实世界的推荐系统今天都使用这种方法来减轻各种方法的缺点。
下图显示了将基于内容的过滤方法与协同过滤方法相结合,创建混合推荐系统的一种方式:
图 11.3 - 构建推荐系统的混合方法
在上图中,您可以看到我们有两个输入输入混合系统。这些输入进入协同(CF)和基于内容的系统,然后这些系统的输出被结合起来。这种组合可以定制,甚至可以作为其他高级系统的输入,比如神经网络。总体目标是通过结合多个推荐系统来创建一个强大的混合系统。
值得一提的是,任何用于创建推荐系统的方法都需要某种形式的数据。例如,在协同过滤方法中,您将需要用户-物品交互历史记录,而在基于内容的方法中,您将需要物品元数据。
如果您有足够的数据来训练一个推荐系统,您可以利用众多的机器学习和非机器学习技术来对数据进行建模,然后再进行推荐。您可以使用一些流行的算法,比如K 最近邻(en.wikipedia.org/wiki/K-nearest_neighbors_algorithm
)、聚类算法(en.wikipedia.org/wiki/Cluster_analysis
)、决策树(en.wikipedia.org/wiki/Decision_trees
)、贝叶斯分类器(en.wikipedia.org/wiki/Naive_Bayes_classifier
),甚至人工神经网络(en.wikipedia.org/wiki/Artificial_neural_networks
)。
在本章中,我们将使用神经网络方法来构建推荐系统。我们将在下一节中详细解释这一点。
创建推荐系统的神经网络方法
近年来,神经网络在解决机器学习(ML)领域的许多问题时已成为瑞士军刀。这在 ML 突破领域明显,如图像分类/分割和自然语言处理。随着数据的可用性,神经网络已成功用于构建大规模推荐系统,如 Netflix(https://research.netflix.com/research-area/machine-learning)和 YouTube(https://research.google/pubs/pub45530/)使用的系统。
尽管有不同的方法来使用神经网络构建推荐系统,但它们都依赖于一个主要事实:它们需要一种有效的方法来学习项目或用户之间的相似性。在本章中,我们将利用一种称为嵌入的概念来有效地学习这些相似性,以便轻松地为我们的推荐系统提供动力。
但首先,嵌入是什么,为什么我们在使用它们?在下一小节中,我们将简要回答这些问题。
什么是嵌入?
嵌入是将离散变量映射到连续或实值变量的映射。也就是说,给定一组变量,例如[好,坏],嵌入可以将每个离散项映射到n维的连续向量 - 例如,好可以表示为[0.1, 0.6, 0.1, 0.8],坏可以表示为[0.8, 0.2, 0.6, 0.1],如下图所示:
图 11.4 - 用实值变量表示离散类别
嗯,嵌入有两个主要的区别,技术上来说,是优势:
- 嵌入表示可以是小的或大的,具体取决于指定的维度。这与独热编码等编码方案不同,其中表示的维度随着离散类的数量增加而增加。
例如,下图显示了一个独热编码表示中使用的维度随着唯一国家数量的增加而增加:
图 11.5 - 嵌入和独热编码之间的大小比较
- 嵌入可以与神经网络中的权重一起学习。这是与其他编码方案相比的主要优势,因为具有此属性,学习的嵌入成为离散类的相似性集群,这意味着您可以轻松找到相似的项目或用户。例如,查看以下证明,您可以看到我们有两组学习的单词嵌入:
图 11.6 - 嵌入单词并在嵌入空间中显示相似性(重新绘制自:https://medium.com/@hari4om/word-embedding-d816f643140)
在前面的图中,您可以看到代表男人,女人,国王和皇后的组被传递到嵌入中,结果输出是一个嵌入空间,其中意义相近的单词被分组。这是通过学习的单词嵌入实现的。
那么,我们如何利用嵌入来创建推荐系统呢?嗯,正如我们之前提到的,嵌入可以有效地表示数据,这意味着我们可以使用它们来学习或表示用户-项目的交互。因此,我们可以轻松地使用学习到的嵌入来找到相似的项目进行推荐。我们甚至可以进一步将嵌入与监督机器学习任务相结合。
将学习到的嵌入表示与监督机器学习任务相结合的这种方法,将是我们在下一节中创建电影推荐系统的做法。
构建电影推荐系统
要构建一个电影推荐系统,我们需要某种用户-电影交互数据集。幸运的是,我们可以使用由Grouplens提供的MovieLens 100k
数据集(grouplens.org/datasets/movielens/100k/
)。这个数据包含了 1,000 个用户对 1,700 部电影的 100,000 个电影评分。
以下截图显示了数据集的前几行:
图 11.7 - MovieLens 数据集的前几行
从前面的截图中,您可以看到我们有user_id
,item_id
(电影)以及用户给予项目(电影)的评分。仅凭这种交互和使用嵌入,我们就可以有效地建模用户的行为,并因此了解他们喜欢什么类型的电影。
要了解我们将如何构建和学习嵌入与神经网络的交互,请参考以下架构图:
图 11.8 - 我们推荐系统的高层架构
从前面的图表中,您可以看到我们有两个嵌入层,一个用于用户,另一个用于项目(电影)。这两个嵌入层然后在传递到一个密集层之前被合并。
因此,实质上,我们将嵌入与监督学习任务相结合,其中来自嵌入的输出被传递到一个密集层,以预测用户将给出的项目(电影)的评分。
您可能会想,如果我们正在学习预测用户将给出的产品的评分,那么这如何帮助我们进行推荐呢?嗯,诀窍在于,如果我们能有效地预测用户将给一部电影的评分,那么,使用学习到的相似性嵌入,我们就可以预测用户将给所有电影的评分。然后,有了这个信息,我们就可以向用户推荐预测评分最高的电影。
那么,我们如何在 JavaScript 中构建这个看似复杂的推荐系统呢?嗯,在下一个小节中,我们将向您展示如何轻松地使用 TensorFlow.js,结合 Danfo.js,来实现这一点。
设置项目目录
您需要成功跟进本章的代码和数据集,这些都可以在本章的代码存储库中找到(github.com/PacktPublishing/Building-Data-Driven-Applications-with-Danfo.js/tree/main/Chapter11
)。
您可以下载整个项目到您的计算机上以便轻松跟进。如果您已经下载了项目代码,那么请导航到您的根目录,其中src
文件夹可见。
在src
文件夹中,您有以下文件夹/脚本:
-
book_recommendation_model
:这是保存训练模型的文件夹。 -
data
:这个文件夹包含我们的训练数据。 -
data_proc.js
:这个脚本包含了我们所有的数据处理代码。 -
model.js
:这个脚本定义并编译了推荐模型。 -
recommend.js
:这个脚本包含制作推荐的代码。 -
train.js
:这个脚本包含训练推荐模型的代码。
要快速测试预训练的推荐模型,首先使用yarn
(推荐)或NPM
安装所有必要的包,然后运行以下命令:
yarn recommend
这将为用户 ID 为196
、880
和13
的用户推荐10
、5
和20
部电影。如果成功,你应该看到类似以下的输出:
图 11.9 - 训练推荐系统提供的推荐电影
你也可以通过运行以下命令重新训练模型:
yarn retrain
默认情况下,上述命令将使用批量大小为128
、时代大小为5
来重新训练模型,并在完成时将训练好的模型保存到book_recommender_model
文件夹中。
现在你已经在本地设置了项目,我们将逐步解释每个部分,并解释如何从头开始构建推荐系统。
检索和处理训练数据集
我们使用的数据集是从 Grouplens 网站检索的。默认情况下,电影数据(https://files.grouplens.org/datasets/movielens/ml-100k.zip
)是一个包含制表符分隔文件的 ZIP 文件。为了简单起见,我已经下载并将你在这个项目中需要的两个文件转换成了CSV
格式。你可以从这个项目的代码库的data
文件夹中获取这些文件(github.com/PacktPublishing/Building-Data-Driven-Applications-with-Danfo.js/tree/main/Chapter11/src/data
)。
主要有两个文件:
-
movieinfo.csv
:这个文件包含了关于每部电影的元数据,比如标题、描述和链接。 -
movielens.csv
:这是用户评分数据集。
要使用movielens
数据集,我们必须用 Danfo.js 读取数据集,处理数据,然后将其转换为我们可以传递给神经网络的张量。
在项目的代码中,打开data_proc.js
脚本。这个脚本导出了一个名为processData
的主要函数,代码如下:
...
const nItem = (moviesDF["item_id"]).max()
const nUser = (moviesDF["user_id"]).max()
const moviesIdTrainTensor = (moviesDF["item_id"]).tensor
const userIdTrainTensor = (moviesDF["user_id"]).tensor
const targetData = (moviesDF["rating"]).tensor
return {
trainingData: [moviesIdTrainTensor, userIdTrainTensor],
targetData,
nItem,
nUser
}
...
那么,在上述代码中我们在做什么呢?幸运的是,我们不需要进行太多的数据预处理,因为user_id
、item_id
和ratings
列已经是数字形式。所以,我们只是做了两件事:
-
检索物品和用户列的最大 ID。这个数字,称为词汇量大小,将在创建我们的模型时传递给嵌入层。
-
检索和返回用户、物品和评分列的基础张量。用户和物品张量将作为我们的训练输入,而评分张量将成为我们的监督学习目标。
现在你知道如何处理数据了,让我们开始使用 TensorFlow.js 构建神经网络。
构建推荐模型
我们的推荐模型的完整代码可以在model.js
文件中找到。这个模型使用了混合方法,就像我们在高级架构图中看到的那样(见图 11.8)。
注意
我们正在使用我们在第十章**TensorFlow.js 简介中介绍的 Model API 来创建网络。这是因为我们正在创建一个复杂的架构,我们需要更多的控制输入和输出。
在接下来的步骤中,我们将解释模型并展示创建它的相应代码:
user
和另一个来自物品:
...
const itemInput = tf.layers.input({ name: "itemInput", shape: [1] })
const userInput = tf.layers.input({ name: "userInput", shape: [1] })
...
注意,输入的形状参数设置为1
。这是因为我们的输入张量是具有1
维的向量。
InputDim
:这是嵌入向量的词汇量大小。最大整数索引为+ 1
。
b) OutputDim
:这是用户指定的输出维度。也就是说,它用于配置嵌入向量的大小。
接下来,我们将合并这些嵌入层。
dot
乘积,将输出扁平化,并将输出传递给一个密集层:
...
const mergedOutput = tf.layers.dot({ axes: 0}).apply([itemEmbedding, userEmbedding])
const flatten = tf.layers.flatten().apply(mergedOutput)
const denseOut = tf.layers.dense({ units: 1, activation: "sigmoid", kernelInitializer: "leCunUniform" }).apply(flatten)
...
通过上述输出,我们现在可以使用 Models API 定义我们的模型。
- 最后,我们将定义并编译模型,如下面的代码片段所示:
...
const model = tf.model({ inputs: [itemInput, userInput], outputs: denseOut })
model.compile({
optimizer: tf.train.adam(LEARNING_RATE),
loss: tf.losses.meanSquaredError
});
...
上述代码使用 Models API 来定义输入和输出,然后调用 compile 方法,该方法接受训练优化器(Adam 优化器)和损失函数(均方误差)。您可以在model.js
文件中查看完整的模型代码。
有了模型架构定义,我们就可以开始训练模型。
训练和保存推荐模型
模型的训练代码可以在train.js
文件中找到。此代码有两个主要部分。我们将在这里看到两者。
第一部分,如下面的代码块所示,使用批量大小为128
,时代大小为5
,并且将10
%的数据用于模型验证的验证分割来训练模型,这部分数据是为了模型验证而保留的:
...
await model.fit(trainingData, targetData, {
batchSize: 128,
epochs: 5,
validationSplit: 0.1,
callbacks: {
onEpochEnd: async (epoch, logs) => {
const progressUpdate = `EPOCH (${epoch + 1}): Train MSE: ${Math.sqrt(logs.loss)}, Val MSE: ${Math.sqrt(logs.val_loss)}\n`
console.log(progressUpdate);
}
}
});
...
在上述训练代码中,我们在每个训练时代之后打印了损失。这有助于我们跟踪训练进度。
下面的代码块将训练好的模型保存到提供的文件路径。在我们的情况下,我们将其保存到movie_recommendation_model
文件夹中:
...
await model.save(`file://${path.join(__dirname, "movie_recommendation_model")}`)
...
请注意此文件夹的名称,因为我们将在下一小节中使用它进行推荐。
要训练模型,您可以在src
文件夹中运行以下命令:
yarn train
或者,您也可以直接使用node
运行train.js
:
node train.js
这将开始指定数量的时代模型训练,并且一旦完成,将模型保存到指定的文件夹。训练完成后,您应该有类似以下的输出:
图 11.10 - 推荐模型的训练日志
一旦您有了训练好并保存的模型,您就可以开始进行电影推荐。
使用保存的模型进行电影推荐
recommend.js
文件包含了进行推荐的代码。我们还包括了一个名为getMovieDetails
的实用函数。此函数将电影 ID 映射到电影元数据,以便我们可以显示有用的信息,例如电影的名称。
但是我们如何进行推荐呢?由于我们已经训练了模型来预测用户对一组电影的评分,我们可以简单地将用户 ID 和所有电影传递给模型来进行评分预测。
有了所有电影的评分预测,我们可以简单地按降序对它们进行排序,然后返回前几部电影作为推荐。
要做到这一点,请按照以下步骤进行:
- 首先,我们必须获取所有唯一的电影 ID 进行预测:
...
const moviesDF = await dfd.read_csv(moviesDataPath)
const uniqueMoviesId = moviesDF["item_id"].unique().values
const uniqueMoviesIdTensor = tf.tensor(uniqueMoviesId)
...
- 接下来,我们必须构建一个与电影 ID 张量长度相同的用户张量。该张量将在所有条目中具有相同的用户 ID,因为对于每部电影,我们都在预测同一用户将给出的评分:
...
const userToRecommendForTensor = tf.fill([uniqueMoviesIdTensor.shape[0]], userId)
...
- 接下来,我们必须加载模型并通过传递电影和用户张量作为输入来调用
predict
函数:
...
const model = await loadModel()
const ratings = model.predict([uniqueMoviesIdTensor,
userToRecommendForTensor])
...
这将返回一个张量,其中包含用户将给每部电影的预测评分。
- 接下来,我们必须构建一个包含名为
movie_id
(唯一电影 ID)和ratings
(用户对每部电影的预测评分)的两列的 DataFrame:
...
const recommendationDf = new dfd.DataFrame({
item_id: uniqueMoviesId,
ratings: ratings.arraySync()
})
...
- 将预测评分和相应的电影 ID 存储在 DataFrame 中有助于我们轻松地对评分进行排序,如下面的代码所示:
...
const topRecommendationsDF = recommendationDf
.sort_values({
by: "ratings",
ascending: false
})
.head(top) //return only the top rows
...
- 最后,我们必须将排序后的电影 ID 数组传递给
getMovieDetails
实用函数。此函数将每个电影 ID 映射到相应的元数据,并返回一个包含两列(电影标题和电影发行日期)的 DataFrame,如下面的代码所示:
...
const movieDetailsDF = await getMovieDetails(topRecommendationsDF["movie_id"].values)
...
recommend.js
文件在src
文件夹中包含了完整的推荐代码,包括将电影 ID 映射到其元数据的实用函数。
要测试推荐,您需要调用recommend
函数并传递电影 ID 和您想要的推荐数量,如下面的示例所示:
recommend(196, 10) // Recommend 10 movies for user with id 196
上述代码在控制台中给出了以下输出:
[
'Remains of the Day, The (1993)',
'Star Trek: First Contact (1996)',
'Kolya (1996)',
'Men in Black (1997)',
'Hunt for Red October, The (1990)',
'Sabrina (1995)',
'L.A. Confidential (1997)',
'Jackie Brown (1997)',
'Grease (1978)',
'Dr. Strangelove or: How I Learned to Stop Worrying and Love the Bomb (1963)'
]
就是这样!您已成功使用神经网络嵌入创建了一个推荐系统,可以高效地向不同用户推荐电影。利用本章学到的概念,您可以轻松地创建不同的推荐系统,可以推荐不同的产品,如音乐、书籍和视频。
总结
在本章中,我们成功地构建了一个推荐系统,可以根据用户的偏好向他们推荐电影。首先,我们定义了推荐模型是什么,然后简要讨论了设计推荐系统的三种方法。接着,我们谈到了神经网络嵌入以及为什么决定使用它们来创建我们的推荐模型。最后,我们通过构建一个电影推荐模型,将学到的所有概念整合起来,可以向用户推荐指定数量的电影。
通过本章学到的知识,您可以轻松地创建一个可以嵌入到 JavaScript 应用程序中的推荐系统。
在下一章,您将使用 Danfo.js 和 Twitter API 构建另一个实际应用程序。
第十三章:构建 Twitter 分析仪表板
本章的主要目标是展示如何使用 Danfo.js 在后端和前端构建全栈 Web 分析平台。
为了演示这一点,我们将构建一个小型的单页面 Web 应用程序,在这个应用程序中,您可以搜索 Twitter 用户,获取他们在特定日期被提及的所有推文,并进行一些简单的分析,比如情感分析,从数据中得出一些见解。
在本章中,我们将研究构建 Web 应用程序的以下主题:
-
设置项目环境
-
构建后端
-
构建前端
技术要求
本章需要以下内容:
-
React.js 的知识
-
本章的代码在这里可用:
github.com/PacktPublishing/Building-Data-Driven-Applications-with-Danfo.js/tree/main/Chapter12
设置项目环境
对于这个项目,我们将构建一个既有后端又有前端的单个网页。我们将使用 Next.js 框架来构建应用程序。Next.js 使您能够快速轻松地构建后端和前端。我们还将使用tailwindcss
,就像我们之前为一些项目所做的那样,比如无代码环境项目。
设置我们的项目环境与 Next.js 包含默认的tailwindcss
配置,我们只需要运行以下命令:
$ npx create-next-app -e with-tailwindcss twitterdashboard
npx
命令运行create-next-app
,它在twitterdashboard
目录中创建了 Next.js 样板代码,包括tailwindcss
配置。请注意,twitterdashboard
目录(也称为项目名称)可以根据您的选择命名。如果一切安装成功,您应该会得到以下截图中显示的输出:
图 12.1 – 代码环境设置
现在我们已经完成了安装,如果一切正常工作,您应该在项目中有以下文件:
图 12.2 – 目录结构
最后,为了测试项目是否安装成功并准备就绪,让我们运行以下命令:
$ npm run dev
这个命令应该自动启动应用程序并打开浏览器,显示以下界面:
图 12.3 – Next.js UI
对于这个项目,我们将修改图 12.3中显示的界面以适应我们的口味。
现在代码环境已经设置好了,让我们继续创建我们的应用程序。
构建后端
在本节中,我们将学习如何为我们的应用程序创建以下 API:
-
/api/tweet
:这个 API 负责获取 Twitter 用户并获取他们的数据。 -
/api/nlp
:这个 API 负责对获取的用户数据进行情感分析。
这些 API 将被前端组件所使用,并将用于创建不同的可视化和分析。让我们从创建用于获取 Twitter 用户数据的 API 开始。
构建 Twitter API
在本节中,我们将构建一个 API,使得轻松获取 Twitter 用户被提及的推文。从每条推文中,我们将获取它们的元数据,比如文本、发送者的姓名、喜欢和转发的次数、用于发推的设备以及推文创建的时间。
为了构建用于获取 Twitter 用户数据的 Twitter API,并将其结构化为我们喜欢的形式,以便在前端轻松使用,我们需要安装一个工具,使其更容易与主要的 Twitter 开发者 API 进行交互。在以下命令中,我们将安装twit.js
以便轻松访问和处理 Twitter API:
$ npm i twit
一旦安装了twit
,我们需要对其进行配置。为了使用twit
,我们需要各种 Twitter 开发者密钥,比如以下内容:
consumer_key='....',
consumer_secret='....',
access_token='.....',
access_token_secret='.....'
如果您没有这些密钥,您将需要创建一个 Twitter 开发者账户,然后申请通过developer.twitter.com/
获得 API 访问权限。如果获得使用 Twitter API 的权限,您可以访问developer.twitter.com/en/apps
创建一个应用程序并设置您的凭证密钥。
注意
获取 Twitter API 可能需要几天的时间,这取决于您描述用例的方式。要获得关于设置 Twitter API 和获取必要密钥的逐步指南以及视觉辅助,请按照这里的步骤:realpython.com/twitter-bot-python-tweepy/#creating-twitter-api-authentication-credentials
。
在获得项目所需的 Twitter 开发者密钥之后,我们将在我们的代码中使用它们。为了防止将密钥暴露给公众,让我们创建一个名为.env.local
的文件,并在这个文件中添加我们的 API 密钥,如下面的代码块所示:
CONSUMER_KEY='Put in your CONSUMER_KEY',
CONSUMER_SECRET='Your CONSUMER_SECRET',
ACCESS_TOKEN ='Your ACCESS_TOKEN',
ACCESS_TOKEN_SECRET='Your ACCESS_TOKEN_SECRET'
在 Next.js 中,所有 API 都是在/pages/api
文件夹中创建的。Next.js 使用pages/
文件夹中的文件结构来创建 URL 路由。例如,如果您在pages/
文件夹中有一个名为login.js
的文件,那么login.js
中的内容将在http://localhost:3000/login
中呈现。
前面的段落展示了基于文件名和结构在 Next.js 中为网页创建路由的方法。同样的方法也适用于在 Next.js 中创建 API。
假设我们在pages/api
中创建了一个用于注册的 API,名为signup.js
。这个 API 将自动在http://localhost:3000/api/signup
中可用,如果我们要在应用程序内部使用这个 API,我们可以这样调用它:/api/signup
。
对于我们的/api/tweet
API,让我们在pages/api/
中创建一个名为tweet.js
的文件,并按照以下步骤更新文件:
- 首先,我们导入
twit.js
,然后创建一个函数来清理每个推文:
const Twit = require('twit')
function clean_tweet(tweet) {
tweet = tweet.normalize("NFD") //normalize text
tweet = tweet.replace(/(RT\s(@\w+))/g, '') //remove Rt tag followed by an @ tag
tweet = tweet.replace(/(@[A-Za-z0-9]+)(\S+)/g, '') // remove user name e.g @name
tweet = tweet.replace(/((http|https):(\S+))/g, '') //remove url
tweet = tweet.replace(/[!#?:*%$]/g, '') //remove # tags
tweet = tweet.replace(/[^\s\w+]/g, '') //remove punctuations
tweet = tweet.replace(/[\n]/g, '') //remove newline
tweet = tweet.toLowerCase().trim() //trim text
return tweet
}
clean_tweet
函数接受推文文本,规范化文本,移除标签字符,用户名称,URL 链接和换行符,然后修剪文本。
- 然后我们创建一个名为
twitterApi
的函数,用于创建我们的 API:
export default function twitterAPI(req, res) {
// api code here
}
twitterApi
函数接受两个参数,req
和res
,分别是服务器请求和响应的参数。
- 我们现在将使用必要的代码更新
twitterApi
:
if (req.method === "POST") {
const { username } = req.body
const T = new Twit({
consumer_key: process.env.CONSUMER_KEY,
consumer_secret: process.env.CONSUMER_SECRET,
access_token: process.env.ACCESS_TOKEN,
access_token_secret: process.env.ACCESS_TOKEN_SECRET,
timeout_ms: 60 * 1000, // optional HTTP request timeout to apply to all requests.
strictSSL: true, // optional - requires SSL certificates to be valid.
})
}
首先,我们检查req.method
请求方法是否为POST
方法,然后我们从通过搜索框发送的请求体中获取用户名。
Twit
类被实例化,并且我们的 Twitter API 密钥被传入。由于我们的 Twitter 开发者 API 密钥存储为.env.local
中的环境密钥,我们可以使用process.env
轻松访问每个密钥。
- 我们已经使用我们的 API 密钥配置了
twit.js
。现在让我们搜索提到用户的所有推文:
T.get('search/tweets', { q: `@${username}`, tweet_mode: 'extended' }, function (err, data, response) {
let dfData = {
text: data.statuses.map(tweet => clean_tweet(tweet.full_text)),
length: data.statuses.map(tweet => clean_tweet(tweet.full_text).split(" ").length),
date: data.statuses.map(tweet => tweet.created_at),
source: data.statuses.map(tweet => tweet.source.replace(/<(?:.|\n)*?>/gm, '')),
likes: data.statuses.map(tweet => tweet.favorite_count),
retweet: data.statuses.map(tweet => tweet.retweet_count),
users: data.statuses.map(tweet => tweet.user.screen_name)
}
res.status(200).json(dfData)
})
我们使用T.get
方法中的search/tweets
API 搜索所有推文。然后我们传入包含我们想要搜索的用户的用户名的param
对象。
创建一个dfData
对象来根据我们希望的 API 输出响应的方式对数据进行结构化。dfData
包含以下键,这些键是我们想要从推文中提取的元数据:
-
text
:推文中的文本 -
length
:推文的长度 -
date
:推文发布日期 -
source
:用于创建推文的设备 -
likes
:推文的点赞数 -
retweet
:推文的转发数 -
users
:创建推文的用户
前面列表中的元数据是从前面代码中的T.get()
方法返回的search/tweets
中提取的 JSON 数据中提取的。从这个 JSON 数据中提取的所有元数据都包含在一个名为statuses
的对象数组中,以下是 JSON 数据的结构:
{
statuses:[{
......
},
......
]
}
Twitter API 已经创建并准备好使用。让我们继续创建情感分析 API。
构建文本情感 API
从/api/tweet
API 中,我们将获取结构化的 JSON 数据,然后进行情感分析。
数据的情感分析将通过/api/nlp
路由获取。因此,在本节中,我们将看到如何为我们的 Twitter 数据创建情感分析 API。
让我们在/pages/api/
文件夹中创建一个名为nlp.js
的文件,并按以下步骤更新它:
- 我们将使用
nlp-node.js
包进行情感分析。我们还将使用danfojs-node
进行数据预处理,因此让我们安装这两个包:
$ npm i node-nlp danfojs-node
- 我们从
nlp-node
和danfojs-node
中导入SentimentAnalyzer
和DataFrame
:
const { SentimentAnalyzer } = require('node-nlp')
const { DataFrame } = require("danfojs-node")
- 接下来,我们将创建一个默认的
SentimentApi
导出函数,其中将包含我们的 API 代码:
export default async function SentimentApi(req, res) {
}
- 然后,我们将检查请求方法是否为
POST
请求,然后对从请求体获取的数据进行一些数据预处理:
if (req.method === "POST") {
const sentiment = new SentimentAnalyzer({ language: 'en' })
const { dfData, username } = req.body
//check if searched user is in the data
const df = new DataFrame(dfData)
let removeUserRow = df.query({
column: "users",
is: "!=",
to: username
})
//filter rows with tweet length <=1
let filterByLength = removeUserRow.query({
column: "length",
is: ">",
to: 1
})
. . . . .
}
在上述代码中,我们首先实例化了SentimentAnalyzer
,然后将其语言配置设置为英语(en
)。然后我们从请求体中获取了dfData
和username
。
为了分析和从数据中创建见解,我们只想考虑用户与他人的互动,而不是他们自己;也就是说,我们不想考虑用户回复自己的推文。因此,我们从从dfData
生成的 DataFrame 中过滤出用户的回复。
有时,一条推文可能只包含一个标签或使用@
符号引用用户。但是,在我们之前的清理过程中,我们删除了标签和@
符号,这将导致一些推文最终不包含任何文本。因此,我们将创建一个新的filterBylength
DataFrame,其中将包含非空文本:
- 我们将继续创建一个对象,该对象将包含用户数据的整体情感计数,并在我们调用 API 时发送到前端:
let data = {
positive: 0,
negative: 0,
neutral: 0
}
let sent = filterByLength["text"].values
for (let i in sent) {
const getSent = await sentiment.getSentiment(sent[i])
if (getSent.vote === "negative") {
data.negative += 1
} else if (getSent.vote === "positive") {
data.positive += 1
} else {
data.neutral += 1
}
}
res.status(200).json(data)
在上述代码中,我们创建数据对象来存储整体情感分析。由于情感分析只会在filterByLength
DataFrame 中的文本数据上执行,我们提取文本列值。
- 然后,我们循环遍历提取的文本列值,并将它们传递给
sentiment.getSentiment
。对于传递给sentiment.getSentiment
的每个文本,将返回以下类型的对象:
{
score: 2.593,
numWords: 36,
numHits: 8,
average: 0.07202777777777777,
type: 'senticon',
locale: 'en',
vote: 'positive'
}
对于我们的用例,我们只需要vote
的键值。因此,我们检查文本的vote
值是negative
、positive
还是neutral
,然后递增数据对象中每个键的计数。
因此,每当调用/api/nlp
时,我们应该收到以下响应,例如:
{
positive: 20,
negative: 12,
neutral: 40
}
在本节中,我们看到了如何在 Next.js 中创建 API,更重要的是,我们看到了在后端使用 Danfo.js 是多么方便。在下一节中,我们将实现应用程序的前端部分。
构建前端
对于我们的前端设计,我们将使用 Next.js 默认的 UI,如图 12.3所示。我们将为我们的前端实现以下一组组件:
-
Search
组件:创建一个搜索框来搜索 Twitter 用户。 -
ValueCount
组件:获取唯一值的计数并使用条形图或饼图绘制它。 -
Plot
组件:此组件用于以条形图的形式绘制我们的情感分析。 -
Table
组件:用于以表格形式显示获取的用户数据。
在接下来的几节中,我们将实现上述组件列表。让我们开始实现Search
组件。
创建搜索组件
Search
组件是设置应用程序运行的主要组件。它提供了输入字段,可以在其中输入 Twitter 用户的名称,然后进行搜索。search
组件使我们能够调用创建的两个 API:/api/tweet
和/api/nlp
。
在我们的twitterdashboard
项目目录中,让我们创建一个名为Search.js
的目录,并在该目录中创建一个名为Search.js
的 JavaScript 文件。
在Search.js
中,让我们输入以下代码:
import React from 'react'
export default function Search({ inputRef, handleKeyEvent, handleSubmit }) {
return (
<div className='border-2 flex justify-between p-2 rounded-md md:p-4'>
<input id='searchInput'
type='text'
placeholder='Search twitter user'
className='focus:outline-none'
ref={inputRef}
onKeyPress={handleKeyEvent}
/>
<button className='focus:outline-none'
onClick={() => { handleSubmit() }}>
<img src="img/search.svg" />
</button>
</div>
)
}
在上述代码中,我们创建了一个带有以下一组 props 的Search
函数:
-
inputRef
:此 prop 是从useRef
React Hook 获取的。它将用于跟踪搜索输入字段的当前值。 -
handleKeyEvent
:这是一个事件函数,将被传递给搜索输入字段,以便通过按Enter键进行搜索。 -
handleSubmit
:这是一个函数,每次单击搜索按钮时都会激活。handleSubmit
函数负责调用我们的 API。
让我们继续前往/pages/index.js
,通过导入Search
组件并根据以下步骤创建所需的 props 列表来更新文件:
- 首先,我们将导入 React、React Hooks 和
Search
组件:
import React, { useRef, useState } from 'react'
import Search from '../components/Search'
- 然后,我们将创建一些状态集:
let [data, setData] = useState() // store tweet data from /api/tweet
let [user, setUser] = useState() // store twitter usersname
let [dataNlp, setDataNlp] = useState() // store data from /api/nlp
let inputRef = useRef() // monitor the current value of search input field
- 然后,我们将创建
handleSubmit
函数来调用我们的 API 并更新状态数据:
const handleSubmit = async () => {
const res = await fetch(
'/api/tweet',
{
body: JSON.stringify({
username: inputRef.current.value
}),
headers: {
'Content-Type': 'application/json'
},
method: 'POST'
}
)
const result = await res.json()
. . . . . . .
}
首先,在handleSubmit
中,我们调用/api/tweet
API 来获取用户的数据。在fetch
函数中,我们获取inputRef.current.value
搜索字段的当前值,并将其转换为 JSON 对象,然后传递到请求体中。然后使用变量 result 来从 API 获取 JSON 数据。
- 我们进一步更新
handleSubmit
函数以从/api/nlp
获取数据:
const resSentiment = await fetch(
'/api/nlp',
{
body: JSON.stringify({
username: inputRef.current.value,
dfData: result
}),
headers: {
'Content-Type': 'application/json'
},
method: 'POST'
},
)
const sentData = await resSentiment.json()
上述代码与步骤 3中的代码相同。唯一的区别是我们调用/api/nlp
API,然后将步骤 3中的结果数据和从搜索输入字段获取的用户名传递到请求体中。
- 然后我们在
handleSubmit
中更新以下状态:
setDataNlp(sentData)
setUser(inputRef.current.value)
setData(result)
- 接下来,我们将创建
handleKeyEvent
函数以通过按Enter键进行搜索:
const handleKeyEvent = async (event) => {
if (event.key === 'Enter') {
await handleSubmit()
}
}
在上述代码中,我们检查按键是否为Enter键,如果是,则调用handleSubmit
函数。
- 最后,我们调用我们的
Search
组件:
<Search inputRef={inputRef} handleKeyEvent={handleKeyEvent} handleSubmit={handleSubmit} />
记住,我们说过我们将使用 Next.js 的默认 UI。因此,在index.js
中,让我们将Welcome to Next.js
转换为Welcome to Twitter Dashboard
。
更新index.js
后,您可以在http://localhost:3000/
检查浏览器的更新。您将看到以下更改:
图 12.4 - index.js 更新为搜索组件
Search
组件已经实现并融入到主应用程序中,所有必需的状态数据都可以轻松地由Search
组件更新。让我们继续实现ValueCounts
组件。
创建 ValueCounts 组件
我们将为从/api/tweet
获取的数据创建一个简单的分析。这个分析涉及检查列中唯一值存在的次数。我们将获得source
列和users
列的值计数。
source
列的值计数告诉我们其他 Twitter 用户用于与我们搜索的用户进行交互的设备。users
列的值计数告诉我们与我们搜索的用户互动最多的用户。
注意
这里使用的代码是从第八章的实现图表组件部分中复制的,创建无代码数据分析/处理系统。此部分的代码可以在此处获取:github.com/PacktPublishing/Building-Data-Driven-Applications-with-Danfo.js-/blob/main/Chapter12/components/ValueCounts.js
。这里大部分代码不会在这里详细解释。
让我们转到components/
目录并创建一个名为ValueCounts.js
的文件,然后按以下步骤更新它:
- 首先,我们导入必要的模块:
import React from "react"
import { DataFrame } from 'danfojs/src/core/frame'
import { Pie as PieChart } from "react-chartjs-2";
import { Bar as BarChart } from 'react-chartjs-2';
- 然后,我们创建一个名为
ValueCounts
的函数:
export default function ValueCounts({ data, column, username, type }) {
}
该函数接受以下 props:
a) data
:这是来自/api/tweet
的数据。
b) column
:我们要获取值计数的列的名称。
c) username
:来自搜索字段的输入用户名。
d) type
:我们要绘制的图表类型。
- 接下来,我们更新
ValueCounts
函数:
const df = new DataFrame(data)
const removeUserData = df.query({
column: "users",
is: "!=",
to: username
})
const countsSeries = removeUserData[column].value_counts()
const labels = countsSeries.index
const values = countsSeries.values
在上述代码中,我们首先从数据创建一个 DataFrame,然后过滤掉包含搜索用户的行,因为我们不希望用户与自己交互的推文。然后,我们从传入的列中提取value_counts
值。从创建的countSeries
变量中,我们生成标签和值,这将用于绘制我们的图表。
- 然后,我们创建一个名为
dataChart
的图表数据变量,它将符合chart
组件所接受的格式:
const dataChart = {
labels: labels,
datasets: [{
. . . .
data: values,
}]
};
dataChart
对象包含在步骤 3中创建的标签和值。
- 我们创建一个条件渲染来检查要绘制的图表类型:
if (type === "BarChart") {
return (
<div className="max-w-md">
<BarChart data={dataChart} options={options} width="100" height="100" />
</div>
)
} else {
return (<div className="max-w-md">
<PieChart data={dataChart} options={options} width="100" height="100" />
</div>)
}
ValueCounts
组件已设置。
现在,我们可以使用以下步骤将ValueCounts
组件导入index.js
:
- 我们导入
ValueCounts
:
import dynamic from 'next/dynamic'
const DynamicValueCounts = dynamic(
() => import('../components/ValueCounts'),
{ ssr: false }
)
我们导入ValueCounts
的方式与导入Search
组件的方式不同。这是因为在ValueCounts
中,我们使用了 TensorFlow.js 中的一些核心浏览器特定工具,这是 Danfo.js 所需的。因此,我们需要防止 Next.js 从服务器渲染组件,以防止出现错误。
为了防止 Next.js 从服务器渲染组件,我们使用next/dynamic
,然后将要导入的组件包装在dynamic
函数中,并将ssr
键设置为false
。
注意
要了解有关next/dynamic
的更多信息,请访问nextjs.org/docs/advanced-features/dynamic-import
。
- 我们调用
ValueCounts
组件,现在命名为DynamicValueCounts
:
{typeof data != "undefined" && <DynamicValueCounts data={data} column={"source"} type={"PieChart"} />}
我们检查状态数据是否未定义,并且用户数据是否已从/api/tweet
获取。如果是这样,我们就为source
列呈现ValueCounts
组件。
- 让我们还为
users
列添加ValueCounts
:
{typeof data != "undefined" && <DynamicValueCounts data={data} column={"users"} username={user} type={"BarChart"} />}
我们为用户的ValueCounts
图表指定BarChart
,并为ValueCounts
源指定PieChart
。
以下显示了每当搜索用户时,ValueCounts
显示源和用户交互的显示:
图 12.5 – 源和用户列的 ValueCounts 图表结果
值计数已完成并且工作正常。让我们继续从/api/nlp
获取的情感分析数据创建一个图表。
创建情感分析的图表组件
当用户使用搜索字段搜索用户时,sentiData
状态将更新为包含来自/api/nlp
的情感数据。在本节中,我们将为数据创建一个Plot
组件。
让我们在components/
目录中创建一个Plot.js
文件,并按照以下步骤进行更新:
- 首先,我们导入所需的模块:
import React from "react"
import { Bar as BarChart } from 'react-chartjs-2';
- 然后,我们创建一个名为
Plot
的函数来绘制情感数据的图表:
export default function Plot({ data }) {
const dataChart = {
labels: Object.keys(data),
datasets: [{
. . . . . . . .
data: Object.values(data),
}]
};
return (
<div className="max-w-md">
<BarChart data={dataChart} options={options} width="100" height="100" />
</div>
)
}
该函数接受一个data
属性。然后我们创建一个包含chart
组件格式的dataChart
对象。我们通过获取data
属性中的键来指定图表标签,并通过获取data
属性的值来指定dataChart
中键数据的值。dataChart
对象传递到BarChart
组件中。Plot
组件现在用于情感分析图表。
- 下一步是在
index.js
中导入Plot
组件并调用它:
import Plot from '../components/Plot'
. . . . . .
{typeof dataNlp != "undefined" && <Plot data={dataNlp} />}
通过在index.js
中进行上述更新,每当搜索用户时,我们应该看到情感分析的以下图表:
图 12.6 – 情感分析图表
情感分析已经完成并完全集成到index.js
中。让我们继续创建Table
组件来显示我们的用户数据。
创建 Table 组件
我们将实现一个Table
组件来显示获取的数据。
注意
表格实现与第八章中创建的 DataTable 实现相同,创建无代码数据分析/处理系统。有关代码的更好解释,请查看第八章,创建无代码数据分析/处理系统。
让我们在components/
目录中创建一个Table.js
文件,并按以下步骤更新文件:
- 我们导入了必要的模块:
import React from "react";
import ReactTable from 'react-table-v6'
import { DataFrame } from 'danfojs/src/core/frame'
import 'react-table-v6/react-table.css'
- 我们创建了一个名为
Table
的函数:
export default function DataTable({ dfData, username }) {
}
该函数接受dfData
(来自/api/nlp
的情感数据)和username
(来自搜索字段)作为 props。
- 我们使用以下代码更新函数:
const df = new DataFrame(dfData)
const removeUserData = df.query({
column: "users",
is: "!=",
to: username
})
const columns = removeUserData.columns
const values = removeUserData.values
我们从dfData
创建一个 DataFrame,过滤掉包含用户推文的行,然后提取 DataFrame 的列名和值。
- 然后我们将此列格式化为
ReactTable
接受的格式:
const dataColumns = columns.map((val, index) => {
return {
Header: val,
accessor: val,
Cell: (props) => (
<div className={val || ''}>
<span>{props.value}</span>
</div>
),
. . . . . .
}
});
- 我们还将值格式化为
ReactTable
接受的格式:
const data = values.map(val => {
let rows_data = {}
val.forEach((val2, index) => {
let col = columns[index];
rows_data[col] = val2;
})
return rows_data;
})
再次,步骤 4、5和6中的代码在第八章中有详细解释,创建无代码数据分析/处理系统。
- 然后我们调用
ReactTable
组件并传入dataColumns
和data
:
<ReactTable
data={data}
columns={dataColumns}
getTheadThProps={() => {
return { style: { wordWrap: 'break-word', whiteSpace: 'initial' } }
}}
showPageJump={true}
showPagination={true}
defaultPageSize={10}
showPageSizeOptions={true}
minRows={10}
/>
table
组件已完成;下一步是在 Next.js 中导入组件,然后调用该组件。
请注意,由于我们使用的是 Danfo.js 的 Web 版本,我们需要使用next/dynamic
加载此组件,以防止应用程序崩溃:
const Table = dynamic(
() => import('../components/Table'),
{ ssr: false }
)
. . . . . . .
{typeof data != "undefined" && <Table dfData={data} username={user} />}
在上述代码中,我们动态导入了Table
组件,并在内部实例化了Table
组件,并传入了dfData
和username
的 prop 值。
如果您切换到浏览器并转到项目的localhost
端口,您应该看到完整更新的应用程序,如下面的屏幕截图所示:
图 12.7 - 提取的用户数据
应用程序的最终结果应如下所示:
图 12.8 - Twitter 用户仪表板
在本节中,我们为前端实现构建了不同的组件。我们看到了如何在 Next.js 中使用 Danfo.js,还了解了如何使用next/dynamic
加载组件。
总结
在本章中,我们看到了如何使用 Next.js 构建快速全栈应用程序。我们看到了如何在后端使用 Danfo.js 节点,还使用了 JavaScript 包,如 twit.js 和nlp-node
来获取 Twitter 数据并进行情感分析。
我们还看到了如何轻松地将 Danfo.js 与 Next.js 结合使用,以及如何通过使用next/dynamic
加载组件来防止错误。
本章的目标是让您看到如何轻松使用 Danfo.js 构建全栈(后端和前端)数据驱动应用程序,我相信本章在这方面取得了很好的成就。
我相信我们在本书中涵盖了很多内容,从介绍 Danfo.js 到使用 Danfo.js 构建无代码环境,再到构建推荐系统和 Twitter 分析仪表板。通过在各种 JavaScript 框架中使用 Danfo.js 的各种用例,我们能够构建分析平台和机器学习驱动的 Web 应用程序。
我们已经完成了本书的学习,我相信我们现在已经具备了在下一个 Web 应用程序中包含数据分析和机器学习的技能,并且可以为 Danfo.js 做出贡献。
第十四章:附录:基本的 JavaScript 概念
欢迎来到本书的最后一章。我们将这一章放在书的最后,以免让有经验的 JavaScript 开发人员对基础概念感到无聊。这一章应该由那些在尝试使用Danfo.js
之前想要复习 JavaScript 基础知识的开发人员阅读。
通过本章结束时,您将了解 JavaScript 的基本概念,这些概念对于使用该语言构建应用程序至关重要。您将学习有关数据类型、条件分支和循环结构以及 JavaScript 函数的知识。
具体来说,我们将涵盖以下概念:
-
JavaScript 的快速概述
-
了解 JavaScript 的基本原理
技术要求
我们将在本章的所有代码示例中使用开发者控制台。要在默认浏览器中运行任何代码片段,您需要打开开发者控制台。各种浏览器打开控制台的命令如下所示:
-
Google Chrome:要在 Google Chrome 中打开开发者控制台,请从浏览器窗口右上角打开Chrome菜单,然后选择更多工具 | 开发者工具。您还可以在 macOS 上使用Option + Command + J快捷键,或在 Windows/Linux 上使用Shift + Ctrl + J。
-
Microsoft Edge:在 Microsoft Edge 中,打开浏览器窗口右上角的Edge菜单,然后选择F12 开发者工具,或者您也可以直接按F12打开它。
-
Mozilla Firefox:在 Mozilla Firefox 中,点击浏览器右上角的Firefox菜单,然后选择Web 开发者 | 浏览器控制台。您还可以在 macOS 上使用Shift + ⌘ + J快捷键,或在 Windows/Linux 上使用Shift + Ctrl + J。
-
Apple Safari:在 Safari 中,您首先需要在浏览器设置中启用开发者菜单。要做到这一点,打开 Safari 的偏好设置(Safari 菜单 | 偏好设置),选择高级选项卡,然后启用开发者菜单。一旦该菜单被启用,您可以通过点击开发 | 显示 JavaScript 控制台来找到开发者控制台。您还可以使用Option + ⌘ + C快捷键。
一旦您的开发者控制台打开,根据您的浏览器,您将看到一个类似于这里显示的控制台:
图 13.1 – Chrome 浏览器开发者控制台
打开控制台后,您就可以开始编写和测试 JavaScript 代码了。在下一节中,我们将快速介绍 JavaScript 语言的一些重要概念。
JavaScript 的快速概述
根据Stack Overflow 2020 开发者调查(stackoverflow.blog/2020/05/27/2020-stack-overflow-developer-survey-results/
),JavaScript(也称为JS)是世界上最常见的编程语言,大约有 70%的开发人员在某种任务中使用它。这一统计数据并不令人意外,因为在调查进行之前的许多年里,JavaScript 一直是最受欢迎的语言。为什么会这样有很多原因,我们将在这里列出其中一些:
-
它在最常见和最常用的平台上运行——浏览器。
-
许多有用的框架如 Node.js、React 和 Angular 都是围绕它构建的。
-
它是多才多艺的——即它可以用于前端和后端应用。例如,您可以使用 JavaScript 库如 React、Vue 和 Angular 来构建出色的用户界面(UI),而您可以使用服务器端包如 Node.jS 和 Deno 来构建高效的后端/服务器端应用。
-
它可以用于物联网(IoT)和跨平台移动应用。
JavaScript 最初被创建为仅限浏览器的语言,但很快发展成几乎可以在任何地方使用,从前端应用到具有 Node.js 的后端应用,再到物联网应用,以及最近的数据科学/机器学习(ML)领域。
注意
不要将 JavaScript 与 Java 编程语言混淆(en.wikipedia.org/wiki/Java_(programming_language)
)。尽管名称可能相似,但它们在用途、语法甚至语义上都有很大的不同。
JavaScript 是一种动态和事件驱动的语言,因此是许多关注的焦点,特别是对于来自其他语言的程序员。这导致了创建可以直接转译为 JavaScript 的语言。其中一些语言是TypeScript(由微软开发的具有严格数据类型的语言)、Dart(由谷歌开发的独立语言)、CoffeeScript等等。
理解 JavaScript 的基本原理
在本节中,作为记忆提醒,我们将快速介绍现代 JavaScript 的基本概念。如果您熟悉 JavaScript,那么您可以跳过本节。
正如我们反复提到的,JavaScript 可以用于前端和服务器端脚本编写,因此每个环境都有特定的语法或特性,例如,浏览器端 JavaScript 由于安全原因无法访问 Node.js 等文件系统。因此,在本节中,我们将介绍可以在两个环境/任何环境中工作的概念。
声明变量
const
和let
,尽管在旧脚本中,您会发现用于声明变量的var
关键字。通常不鼓励使用var
来声明变量,应该很少使用。在后面的部分中,我们将讨论为什么在现代 JavaScript 中鼓励使用let
而不是var
的一些原因。
以下语句使用let
关键字声明变量:
let name;
let price = 200;
let message, progress, id;
message = "This is a new message";
progress = 80;
id = "gh12345";
接下来的关键字const
可以用于声明常量变量,即在应用程序运行时其引用不能被更改的变量。
以下代码片段显示了一些const
声明的示例:
const DB_NAME;
const LAST_NAME = "Williams";
const TEXT_COLOR = "#B00";
const TITLE_COLOR = "#F00";
重要提示
在声明常量时使用大写变量名是一种常见且被广泛鼓励的做法,不仅在 JavaScript 中,在许多编程语言中也是如此。
数据类型
JavaScript 支持八种基本数据类型。声明变量的数据类型会在 JavaScript 编译器在运行时自动推断,因为 JavaScript 是一种动态类型语言。支持的八种数据类型是number、string、Boolean、object、BigInt、undefined、null和symbol。
数字
number
类型可以表示整数
、浮点数
、infinity
、-infinity
和number
:
let num1 = 10; //integer
let num2 = 20.234; //floating point number
console.log(num1 + num2); //outputs 30.234
重要提示
infinity
和-infinity
代表数学上的正无穷和负无穷(∞)。这些值大于任何数字。您可以在这里看到一个示例:
console.log(1 / 0); // Infinity
NaN
子类型表示数字的数学运算中的错误,例如,尝试用数字除以字符串,如下所示:
console.log("girl" / 2); // NaN
字符串
JavaScript 中的字符串代表文本,必须用单引号(' '
)、双引号(" "
)或反引号(' '
)引号括起来。您可以在以下代码片段中看到一些字符串的示例:
let name = "John Doe";
let msg = 'I am on my way';
let address = '10 Slow ave, NY';
双引号或单引号的选择基于个人偏好,因为它们都具有相同的功能。另一方面,反引号比基本引号具有更多的功能。它们允许您轻松地将变量和表达式嵌入字符串中,使用${...}
模板文字语法。例如,在以下代码片段中,我们展示了如何轻松地将name
、address
和dog_counts
变量嵌入到一个新变量消息中:
let name = "John Doe";
let address = '10 Slow ave, NY';
let dog_counts = 10;
let msg = '${name} lives in ${address} and has ${dog_counts} dogs';
console.log(msg)
//outputs
John Doe lives in 10 Slow ave, NY and has 10 dogs
提示
字符串数据类型有许多内置函数用于操作它。您可以在Mozilla 开发者网络(MDN)文档页面上找到许多这些函数(developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String
)。
布尔
布尔数据类型是一种逻辑类型。它只有两个值:true
和false
。布尔值主要用于比较操作和存储是/否值。让我们看下面的例子:
let pageClicked = false; // No, the page was not clicked
let pageOpened = true; // Yes, the page was opened
if (5 > 1) { //Evaluates to the Boolean value true
console.log("Yes! It is.")
}
接下来,让我们谈谈对象类型。
对象
JavaScript 中的对象是一种非常特殊的类型。它可能是 JavaScript 中最重要的类型,几乎在语言的各个方面都有所应用。对象数据类型是 JavaScript 中唯一的非原始类型。它可以存储不同类型的数据的键控集合,包括它自己。
提示
编程语言中的原始类型是最基本的类型,通常只存储一种类型的值。另一方面,非原始类型可以存储多种类型的值,并且还可以扩展以执行其他功能。
有两种创建对象的方式,如下所述:
- 第一种最常见的方法是使用花括号和可选的键-值属性列表,如下所示:
let page = {}
- 还有一种不太常见的方法是使用对象构造函数语法,如下所示:
let page = new Object()
对象可以在同一步骤中创建和初始化,如下代码片段所示:
let page = {
title: 'Home page',
Id: 23422,
text: 'This is the first page in my website',
rank: 32.09,
"closed": false,
owner: {
name: 'John Doe',
level: 'admin',
lastLoggedIn: 'Aug 5th',
},
}
请注意,对象页面可以存储多种类型的数据,甚至其他对象(作为所有者)。这表明对象可以嵌套以包含更多对象。
要访问对象值,可以使用方括号,如下所示:
console.log(page["closed"])
//outputs false
或者,您可以使用点page.rank
表示法,如下所示:
console.log(page.rank)
这将产生以下输出:
//outputs 32.09
在第一章中,现代 JavaScript 概述,您了解了使对象非常强大的一些重要属性。
条件分支和循环
条件分支和循环是任何编程语言的重要方面,JavaScript 也提供了它们。为了根据某些条件执行不同的操作,您可以使用if
、if..else
、条件/三元运算符和switch
语法。除了条件分支,您可能希望根据特定次数执行重复操作。这就是循环发挥作用的地方。JavaScript 提供了诸如while
、do...while
、for..of
、for..in
和传统for
循环等循环结构。我们将在下一节简要介绍这些语句。
if 语句
if
语句评估括号中的表达式、条件或代码片段,只有在为true
时才执行该语句。这是一个单向条件,只有在为真时才运行。您可以在以下代码片段中看到这个例子:
let age = 25
if (age >= 18){
console.log("You're an adult!") //will run since age is greater than 18
}
//outputs
You're an adult!
if...else 语句
if...else
语句在初始条件变为false
时提供一个额外的块运行。以下是一个例子:
let age = 15
if (age > 18){
console.log("You're an adult!")
}else{
console.log("You're still a teenager!")
}
//outputs
You're still a teenager!
条件/三元运算符
三元运算符是一种更短、更简洁的写法,用于根据两个条件分配变量。三元运算符的语法如下所示:
let result = condition ? val1 : val2;
如果条件为真,则执行val1
并将其分配给result
,否则将分配val2
。以下代码片段显示了使用此功能的示例:
let age = 20
let isAdult = age > 18 ? true : false; //age is greater than 18, so true is returned
console.log(isAdult)
//outputs
true
switch 语句
switch
语句可以以更简洁和可读的方式替代多个if...else
语句。语法如下所示:
switch(value) {
case 'option1': // if (value === 'option1')
...
[break]
case 'option2': // if (value === 'option2')
...
[break]
case 'option3': // if (value === 'option3')
...
[break]
default:
...
[break]
}
switch
括号中的变量或表达式将与每个case
语句进行比较,如果条件为真,则执行相应的代码。当其他条件都失败时,将执行默认语句。以下代码片段显示了使用switch
语句的示例:
let value = 4
switch (value) {
case 1:
console.log( 'You selected 1' );
break;
case 2:
console.log( 'You selected 2' );
break;
case 4:
console.log( 'You selected 4!' ); //gets executed
break;
default:
console.log( "No such value" );
}
//outputs
"You selected 4!"
在每个 case 之后使用break
语句很重要,以停止执行后续的 case。如果忽略break
语句,那么后续的 case 也将被执行。
while 循环
while
循环将重复执行一段代码,只要条件为真。语法如下所示:
while (condition) {
// loop body
}
让我们来看一个使用while
循环的例子:
let year = 2015;
while (year <= 2020) {
console.log( year );
year++
}
//outputs
2015
2016
2017
2018
2019
2020
上面的代码将不断打印年份,只要它小于或等于2020
。注意year++
部分。这是为了在某个时刻打破while
循环的重要部分。
do...while 循环
do...while
循环与while
循环非常相似,只有一个小差别——在测试条件之前至少执行一次循环体。语法如下所示:
do {
// loop body
} while (condition);
让我们来看一个do...while
循环的例子:
let year = 2015;
do {
console.log( year );
year++;
} while (year <= 2020);
如果需要在进行条件检查之前至少执行一次代码,则do...while
循环很重要。
重要提示
始终记得设置一个在某个时刻打破循环的条件,否则你的循环将永远执行——理论上,不过浏览器和服务器端会在一定时间后停止这样的循环。
for 循环
for
循环是 JavaScript 中最流行的循环结构。语法如下所示:
for (initialization; condition; step) {
// loop body
}
语法有三个重要部分,如下所示:
-
let i = 0
)。 -
条件部分:在每次循环交互之前检查此代码。如果为假,则停止循环。
-
步骤部分:步骤很重要,因为它通常会增加一个将被测试的计数器或变量。
这里给出了使用for
循环的例子:
for (let i=0; i <=5; i++){
console.log(i)
}
//outputs
0
1
2
3
4
5
注意
注意我们在for
循环的步骤部分使用了后增量运算符(i++
)?这是非常标准的,这只是i = i + 1
的简写。
for...of 和 for...in 循环
for...of
和for...in
循环是用于遍历可迭代对象的for
循环的变体。可迭代对象是特殊的数据类型,如对象、数组、映射和字符串,具有可迭代属性——换句话说,可以循环遍历属性或值。
for...of
循环主要用于遍历数组和字符串等对象。语法如下所示:
for (let val of iterable) {
//loop body
}
让我们来看一个使用for...of
循环的例子:
let animals = [ "bear", "lion", "giraffe", "jaguar" ];
// Print out each type of animal
for (let animal of animals) {
console.log(animal);
}
//outputs
"bear"
"lion"
"giraffe"
"jaguar"
for...in
循环用于遍历对象。语法与for...of
循环非常相似,如下面的代码片段所示:
for (let val in iterable) {
//loop body
}
for...in
循环应该只用于枚举对象,而不是用于可迭代对象如数组。
以下是使用for...in
循环的例子:
let user = {
name: "Hannah Montana",
age: 24,
level: 2,
};
for (key in user) {
console.log(user[key]);
}
//outputs
"Hannah Montana"
24
2
进一步阅读
for...in
和for...of
是用于遍历数据结构的循环结构。它们之间的主要区别是它们遍历的数据结构。for...in
遍历对象的所有可枚举属性键,而for...of
遍历可迭代对象的值。
JavaScript 函数
在本节中,我们将学习 JavaScript 函数。每当我们需要向控制台打印文本时,就会使用console.log
内置函数。这显示了函数可以有多重要,因为一旦定义,单个函数可以以任何方式和任何次数调用以执行相同的操作。
JavaScript 中的函数通常如下所示:
function sayHi() {
console.log('Hello World!')
}
sayHi()
//output
"Hello World!"
函数还可以在括号内接受一系列逗号分隔的参数,用于执行计算。例如,具有一些参数的函数通常看起来像这样:
function sayHi(name, message) {
console.log('This is a message from ${name}. The message read thus: ${message}')
}
sayHi("Jenny", "How are you doing?")
//output
"This is a message from Jenny. The message read thus: How are you doing?"
以下是关于函数的一些重要事项:
- 函数内声明的变量只在该函数内部有效,只能在函数内部访问。以下是一个例子:
function sayHi() {
let name = 'Jane'
console.log(name)
}
sayHi()
//output
"Jane"
console.log(name) //throws error: ReferenceError: name is not defined
- 函数可以访问全局变量。全局变量是在函数范围之外声明的变量,并且可以在每个代码块中使用。你可以在这里看到一个例子:
let name = 'Jenny'
function sayHi() {
console.log(name)
}
sayHi()
//output
"Jenny"
- 函数可以返回值。当函数执行某些计算并且需要在其他地方使用结果时,这一点非常重要。到目前为止,我们一直在使用的函数大多返回空/未定义,并且被称为无效函数。让我们看下面的一个函数返回值的例子:
function add(num1, num2) {
let sum = num1 + num2
return sum
}
let result = add(25, 30)
console.log(result)
//output
55
提示
函数中的return
语句一次只能返回一个值。为了返回多个值,可以将结果作为 JavaScript 对象返回。您可以在这里看到一个示例:
function add(num1, num2) {
let sum = num1 + num2
return {sum: sum, funcName: "add"}}
let result = add(25, 30)
console.log(result)
//输出
Object { funcName: "add", sum: 55}
回调函数
回调在 JavaScript 中非常重要。回调是作为参数传递给另一个函数的函数。根据外部函数中的一些条件,调用此参数来执行某些操作。让我们看下面的例子来更好地理解回调:
function showValue(value) {
console.log(value)
}
function err() {
console.log("Wrong value specified!!")
}
在上面的代码示例中,我们创建了两个回调函数,showValue
和error
。您可以在下面的代码片段中看到它们的使用:
function printValues(value, showValue, error) {
if (value > 0){
showValue(value)
}else{
error()
}
}
showValue
回调函数将传递给它的参数打印到控制台,而error
回调函数则打印错误消息。可以像下面的代码片段中所示那样使用回调函数。
printValues(20, showValue, err);
//outputs 20
printValues(0, showValue, err);
//outputs "Wrong value specified!!"
printValues
函数首先测试值是否大于0
,然后调用showValue
或error
回调函数。
回调不仅接受命名函数。它们还可以接受匿名函数;通过这种方式,可以创建嵌套回调。
假设我们有一个doSomething
函数执行特定任务,并且我们希望在任务完成之前执行不同的操作。这意味着我们可以将回调函数传递给另一个回调函数,从而创建嵌套回调,如下面的代码块所示:
doSomething(((((()=>{
//dosomething A
})=>{
//dosomething B
})=>{
//dosomething C
})=>{
//dosomething D
})=>{
//dosomething E
})
上面的代码是我们所谓的回调地狱。这种方法有很多问题,变得难以管理。在第一章中,我们介绍了一种现代和更有效的方法来使用应用程序编程接口(API)来处理回调。这样做更清晰,可以消除与回调地狱相关的许多问题。
重要提示
JavaScript 默认是异步的,因此长时间执行的函数会被排队,可能在你需要它们之前永远不会被执行。回调主要用于继续代码执行并确保返回正确的结果。
总结
在本章中,我们介绍了 JavaScript 编程语言的基本方面。我们从一些基本背景开始,解释了为什么 JavaScript 是当今世界上最流行的语言,以及 JavaScript 的各种用途。接下来,我们讨论了语言的基本概念,讨论了声明变量以及 JavaScript 中可用的八种数据类型。之后,我们讨论了 JavaScript 中的分支和循环结构,并展示了一些使用它们的示例,最后,我们简要讨论了 JavaScript 中的函数和类。
标签:df,代码,DataFrame,Danfo,构建,js,data,我们 From: https://www.cnblogs.com/apachecn/p/18205993