redits: aijs.rocks
虽然python或r编程语言有一个相对容易的学习曲线,但是Web开发人员更喜欢在他们舒适的javascript区域内做事情。目前来看,node.js已经开始向每个领域应用javascript,在这一大趋势下我们需要理解并使用JS进行机器学习。由于可用的软件包数量众多,python变得流行起来,但是JS社区也紧随其后。这篇文章会帮助初学者学习如何构建一个简单的分类器。
扩展:
2019年11个javascript机器学习库
https://blog.bitsrc.io/javascript-for-machine-learning-using-tensorflow-js-6411bcf2d5cd
很棒的机器学习库,可以在你的下一个应用程序中添加一些人工智能!
Big.bitsrc.io
我们可以创建一个使用tensorflow.js在浏览器中训练模型的网页。考虑到房屋的“avgareanumberofrows”,模型可以学习去预测房屋的“价格”。
为此我们要做的是:
加载数据并为培训做好准备。
定义模型的体系结构。
训练模型并在训练时监控其性能。
通过做出一些预测来评估经过训练的模型。
创建一个HTML页面并包含JavaScript。将以下代码复制到名为index.html的HTML文件中。
<!DOCTYPE html> <html> <head> <title>TensorFlow.js Tutorial</title> <!-- Import TensorFlow.js --> <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@1.0.0/dist/tf.min.js"></script> <!-- Import tfjs-vis --> <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-vis@1.0.2/dist/tfjs-vis.umd.min.js"></script> <!-- Import the main script file --> <script src="script.js"></script> </head> <body> </body> </html>
为代码创建javascript文件
在与上面的HTML文件相同的文件夹中,创建一个名为script.js的文件,并将以下代码放入其中。
console.log('Hello TensorFlow');
测试
既然已经创建了HTML和JavaScript文件,那么就测试一下它们。在浏览器中打开index.html文件并打开devtools控制台。
如果一切正常,那么应该在devtools控制台中创建并可用两个全局变量:
现在你应该可以看到一条消息,上面写着“Hello TensorFlow”。如果是这样,你就可以继续下一步了。
需要这样的输出
注意:可以使用Bit来共享可重用的JS代码
Bit(GitHub上的Bit)是跨项目和应用程序共享可重用JavaScript代码的最快和最可扩展的方式。可以试一试,它是免费的:
组件发现与协作·Bit
Bit是开发人员共享组件和协作,共同构建令人惊叹的软件的地方。发现共享的组件…
Bit.dev
例如:Ramda用作共享组件
Ramda by Ramda·Bit
一个用于JavaScript程序员的实用函数库。-256个javascript组件。例如:等号,乘…
Bit.dev
我们将加载“house”数据集,可以在这里找到。它包含了特定房子的许多不同特征。对于本教程,我们只需要有关房间平均面积和每套房子价格的数据。
将以下代码添加到script.js文件中。
async function getData() { Const houseDataReq=await fetch('https://raw.githubusercontent.com/meetnandu05/ml1/master/house.json'); const houseData=await houseDataReq.json(); const cleaned=houseData.map(house=> ({ price: house.Price, rooms: house.AvgAreaNumberofRooms, })) .filter(house=> (house.price !=null && house.rooms !=null)); return cleaned; }
这可以删除没有定义价格或房间数量的任何条目。我们可以将这些数据绘制成散点图,看看它是什么样子的。
将以下代码添加到script.js文件的底部。
async function run() { // Load and plot the original input data that we are going to train on. const data=await getData(); const values=data.map(d=> ({ x: d.rooms, y: d.price, })); tfvis.render.scatterplot( {name: 'No.of rooms v Price'}, {values}, { xLabel: 'No. of rooms', yLabel: 'Price', height: 300 } ); // More code will be added below } document.addEventListener('DOMContentLoaded', run);
刷新页面时,你可以在页面左侧看到一个面板,上面有数据的散点图,如下图。
散点图
通常,在处理数据时,最好找到方法来查看数据,并在必要时对其进行清理。可视化数据可以让我们了解模型是否可以学习数据的任何结构。
从上面的图中可以看出,房间数量与价格之间存在正相关关系,即随着房间数量的增加,房屋价格普遍上涨。
这一步我们将编写代码来构建机器学习模型。模型主要基于此代码进行架构,所以这是一个比较重要的步骤。机器学习模型接受输入,然后产生输出。对于tensorflow.js,我们必须构建神经网络。
将以下函数添加到script.js文件中以定义模型。
function createModel() { // Create a sequential model const model=tf.sequential(); // Add a single hidden layer model.add(tf.layers.dense({inputShape: [1], units: 1, useBias: true})); // Add an output layer model.add(tf.layers.dense({units: 1, useBias: true})); return model; }
这是我们可以在tensorflow.js中定义的最简单的模型之一,我们来试下简单分解每一行。
实例化模型
const model=tf.sequential();
这将实例化一个tf.model对象。这个模型是连续的,因为它的输入直接流向它的输出。其他类型的模型可以有分支,甚至可以有多个输入和输出,但在许多情况下,你的模型是连续的。
添加层
model.add(tf.layers.dense({inputShape: [1], units: 1, useBias: true}));
这为我们的网络添加了一个隐藏层。因为这是网络的第一层,所以我们需要定义我们的输入形状。输入形状是[1],因为我们有1这个数字作为输入(给定房间的房间数)。
单位(链接)设置权重矩阵在层中的大小。在这里将其设置为1,我们可以说每个数据输入特性都有一个权重。
model.add(tf.layers.dense({units: 1}));
上面的代码创建了我们的输出层。我们将单位设置为1,因为我们要输出1这个数字。
创建实例
将以下代码添加到前面定义的运行函数中。
// Create the model const model=createModel(); tfvis.show.modelSummary({name: 'Model Summary'}, model);
这样可以创建实例模型,并且在网页上有显示层的摘要。
为了获得TensorFlow.js的性能优势,使培训机器学习模型实用化,我们需要将数据转换为Tensors。
将以下代码添加到script.js文件中。
function convertToTensor(data) { return tf.tidy(()=> { // Step 1. Shuffle the data tf.util.shuffle(data); // Step 2. Convert data to Tensor const inputs=data.map(d=> d.rooms) const labels=data.map(d=> d.price); const inputTensor=tf.tensor2d(inputs, [inputs.length, 1]); const labelTensor=tf.tensor2d(labels, [labels.length, 1]); //Step 3. Normalize the data to the range 0 - 1 using min-max scaling const inputMax=inputTensor.max(); const inputMin=inputTensor.min(); const labelMax=labelTensor.max(); const labelMin=labelTensor.min(); const normalizedInputs=inputTensor.sub(inputMin).div(inputMax.sub(inputMin)); const normalizedLabels=labelTensor.sub(labelMin).div(labelMax.sub(labelMin)); return { inputs: normalizedInputs, labels: normalizedLabels, // Return the min/max bounds so we can use them later. inputMax, inputMin, labelMax, labelMin, } }); }
接下来,我们可以分析一下将会出现什么情况。
随机播放数据
// Step 1. Shuffle the data tf.util.shuffle(data);
在训练模型的过程中,数据集被分成更小的集合,每个集合称为一个批。然后将这些批次送入模型运行。整理数据很重要,因为模型不应该一次又一次地得到相同的数据。如果模型一次又一次地得到相同的数据,那么模型将无法归纳数据,并为运行期间收到的输入提供指定的输出。洗牌将有助于在每个批次中拥有各种数据。
转换为Tensor
// Step 2. Convert data to Tensor const inputs=data.map(d=> d.rooms) const labels=data.map(d=> d.price); const inputTensor=tf.tensor2d(inputs, [inputs.length, 1]); const labelTensor=tf.tensor2d(labels, [labels.length, 1]);
这里我们制作了两个数组,一个用于输入示例(房间条目数),另一个用于实际输出值(在机器学习中称为标签,在我们的例子中是每个房子的价格)。然后我们将每个数组数据转换为一个二维张量。
规范化数据
//Step 3. Normalize the data to the range 0 - 1 using min-max scaling const inputMax=inputTensor.max(); const inputMin=inputTensor.min(); const labelMax=labelTensor.max(); const labelMin=labelTensor.min(); const normalizedInputs=inputTensor.sub(inputMin).div(inputMax.sub(inputMin)); const normalizedLabels=labelTensor.sub(labelMin).div(labelMax.sub(labelMin));
接下来,我们规范化数据。在这里,我们使用最小-最大比例将数据规范化为数值范围0-1。规范化很重要,因为您将使用tensorflow.js构建的许多机器学习模型的内部设计都是为了使用不太大的数字。规范化数据以包括0到1或-1到1的公共范围。
返回数据和规范化界限
return { inputs: normalizedInputs, labels: normalizedLabels, // Return the min/max bounds so we can use them later. inputMax, inputMin, labelMax, labelMin, }
我们可以在运行期间保留用于标准化的值,这样我们就可以取消标准化输出,使其恢复到原始规模,我们就可以用同样的方式规范化未来的输入数据。
通过创建模型实例、将数据表示为张量,我们可以准备开始运行模型。
将以下函数复制到script.js文件中。
async function trainModel(model, inputs, labels) { // Prepare the model for training. model.compile({ optimizer: tf.train.adam(), loss: tf.losses.meanSquaredError, metrics: ['mse'], }); const batchSize=28; const epochs=50; return await model.fit(inputs, labels, { batchSize, epochs, shuffle: true, callbacks: tfvis.show.fitCallbacks( { name: 'Training Performance' }, ['loss', 'mse'], { height: 200, callbacks: ['onEpochEnd'] } ) }); }
我们把它分解一下。
准备运行
// Prepare the model for training. model.compile({ optimizer: tf.train.adam(), loss: tf.losses.meanSquaredError, metrics: ['mse'], });
我们必须在训练前“编译”模型。要做到这一点,我们必须明确一些非常重要的事情:
优化器:这是一个算法,它可以控制模型的更新,就像上面看到的例子一样。TensorFlow.js中有许多可用的优化器。这里我们选择了Adam优化器,因为它在实践中非常有效,不需要进行额外配置。
损失函数:这是一个函数,它用于检测模型所显示的每个批(数据子集)方面完成的情况如何。在这里,我们可以使用meansquaredrror将模型所做的预测与真实值进行比较。
度量:这是我们要在每个区块结束时用来计算的度量数组。我们可以用它计算整个训练集的准确度,这样我们就可以检查自己的运行结果了。这里我们使用mse,它是meansquaredrror的简写。这是我们用于损失函数的相同函数,也是回归任务中常用的函数。
const batchSize=28; const epochs=50;
接下来,我们选择一个批量大小和一些时间段:
batchSize指的是模型在每次运行迭代时将看到的数据子集的大小。常见的批量大小通常在32-512之间。对于所有问题来说,并没有一个真正理想的批量大小,描述各种批量大小的精确方式这一知识点本教程没有相关讲解,对这些有兴趣可以通过别的渠道进行了解学习。
epochs指的是模型将查看你提供的整个数据集的次数。在这里,我们通过数据集进行50次迭代。
启动列车环路
return model.fit(inputs, labels, { batchSize, epochs, callbacks: tfvis.show.fitCallbacks( { name: 'Training Performance' }, ['loss', 'mse'], { height: 200, callbacks: ['onEpochEnd'] } ) });
model.fit是我们调用的启动循环的函数。它是一个异步函数,因此我们返回它给我们的特定值,以便调用者可以确定运行结束时间。
为了监控运行进度,我们将一些回调传递给model.fit。我们使用tfvis.show.fitcallbacks生成函数,这些函数可以为前面指定的“损失”和“毫秒”度量绘制图表。
把它们放在一起
现在我们必须调用从运行函数定义的函数。
将以下代码添加到运行函数的底部。
// Convert the data to a form we can use for training. const tensorData=convertToTensor(data); const {inputs, labels}=tensorData; // Train the model await trainModel(model, inputs, labels); console.log('Done Training');
刷新页面时,几秒钟后,你应该会看到图形正在更新。
这些是由我们之前创建的回调创建的。它们在每个时代结束时显示丢失(在最近的批处理上)和毫秒(在整个数据集上)。
当训练一个模型时,我们希望看到损失减少。在这种情况下,因为我们的度量是一个误差度量,所以我们希望看到它也下降。
既然我们的模型经过了训练,我们想做一些预测。让我们通过观察它预测的低到高数量房间的统一范围来评估模型。
将以下函数添加到script.js文件中
function testModel(model, inputData, normalizationData) { const {inputMax, inputMin, labelMin, labelMax}=normalizationData; // Generate predictions for a uniform range of numbers between 0 and 1; // We un-normalize the data by doing the inverse of the min-max scaling // that we did earlier. const [xs, preds]=tf.tidy(()=> { const xs=tf.linspace(0, 1, 100); const preds=model.predict(xs.reshape([100, 1])); const unNormXs=xs .mul(inputMax.sub(inputMin)) .add(inputMin); const unNormPreds=preds .mul(labelMax.sub(labelMin)) .add(labelMin); // Un-normalize the data return [unNormXs.dataSync(), unNormPreds.dataSync()]; }); const predictedPoints=Array.from(xs).map((val, i)=> { return {x: val, y: preds[i]} }); const originalPoints=inputData.map(d=> ({ x: d.rooms, y: d.price, })); tfvis.render.scatterplot( {name: 'Model Predictions vs Original Data'}, {values: [originalPoints, predictedPoints], series: ['original', 'predicted']}, { xLabel: 'No. of rooms', yLabel: 'Price', height: 300 } ); }
在上面的函数中需要注意的一些事情。
const xs=tf.linspace(0, 1, 100); const preds=model.predict(xs.reshape([100, 1]));
我们生成100个新的“示例”以提供给模型。model.predict是我们如何将这些示例输入到模型中的。注意,他们需要有一个类似的形状([num_的例子,num_的特点每个_的例子])当我们做培训时。
// Un-normalize the data const unNormXs=xs .mul(inputMax.sub(inputMin)) .add(inputMin); const unNormPreds=preds .mul(labelMax.sub(labelMin)) .add(labelMin);
为了将数据恢复到原始范围(而不是0–1),我们使用规范化时计算的值,但只需反转操作。
return [unNormXs.dataSync(), unNormPreds.dataSync()];
.datasync()是一种方法,我们可以使用它来获取存储在张量中的值的typedarray。这允许我们在常规的javascript中处理这些值。这是通常首选的.data()方法的同步版本。
最后,我们使用tfjs-vis来绘制原始数据和模型中的预测。
将以下代码添加到运行函数中。
testModel(model, data, tensorData);
刷新页面,现在已经完成啦!
现在你已经学会使用tensorflow.js创建一个简单的机器学习模型了。这里是Github存储库供参考。
我开始接触这些是因为机器学习的概念非常吸引我,还有就是我想看看有没有方法可以让它在前端开发中实现,我很高兴发现tensorflow.js库可以帮助我实现我的目标。这只是前端开发中机器学习的开始,TensorFlow.js还可以完成很多工作。谢谢你的阅读!
本文由阿里云云栖社区组织翻译。
文章原标题《JavaScript for Machine Learning using TensorFlow.js》作者:Priyesh Patel
译者:么凹 审校:Viola
Tika是一个内容分析工具,自带全面的parser工具类,能解析基本所有常见格式的文件,得到文件的metadata,content等内容,返回格式化信息。总的来说可以作为一个通用的解析工具。特别对于搜索引擎的数据抓去和处理步骤有重要意义。Tika是Apache的Lucene项目下面的子项目,在lucene的应用中可以使用tika获取大批量文档中的内容来建立索引,非常方便,也很容易使用。Apache Tika toolkit可以自动检测各种文档(如word,ppt,xml,csv,ppt等)的类型并抽取文档的元数据和文本内容。Tika集成了现有的文档解析库,并提供统一的接口,使针对不同类型的文档进行解析变得更简单。Tika针对搜索引擎索引、内容分析、转化等非常有用。
应用程序员可以很容易地在他们的应用程序集成Tika。Tika提供了一个命令行界面和图形用户界面,使它比较人性化。在本章中,我们将讨论构成Tika架构的四个重要模块。下图显示了Tika的四个模块的体系结构:
每当一个文本文件被传递到Tika,它将检测在其中的语言。它接受没有语言的注释文件和通过检测该语言添加在该文件的元数据信息。支持语言识别,Tika 有一类叫做语言标识符在包org.apache.tika.language及语言识别资料库里面包含了语言检测从给定文本的算法。Tika 内部使用N-gram算法语言检测。
Tika可以根据MIME标准检测文档类型。Tika默认MIME类型检测是使用org.apache.tika.mime.mimeTypes。它使用org.apache.tika.detect.Detector 接口大部分内容类型检测。内部Tika使用多种技术,如文件匹配替换,内容类型提示,魔术字节,字符编码,以及其他一些技术。
org.apache.tika.parser 解析器接口是Tika解析文档的主要接口。该接口从提取文档中的文本和元数据,并总结了其对外部用户愿意写解析器插件。采用不同的具体解析器类,具体为各个文档类型,Tika 支持大量的文件格式。这些格式的具体类不同的文件格式提供支持,无论是通过直接实现逻辑分析器或使用外部解析器库。
使用的Tika facade类是从Java调用Tika的最简单和直接的方式,而且也沿用了外观的设计模式。可以在 Tika API的org.apache.tika包Tika 找到外观facade类。通过实现基本用例,Tika作为facade的代理。它抽象了的Tika库的底层复杂性,例如MIME检测机制,解析器接口和语言检测机制,并提供给用户一个简单的接口来使用。
实现word文档转html
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>springboot-demo</artifactId>
<groupId>com.et</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>tika</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.tika</groupId>
<artifactId>tika-parsers</artifactId>
<version>1.17</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
</project>
package com.et.tika.controller;
import com.et.tika.convertor.WordToHtmlConverter;
import com.et.tika.dto.ConvertedDocumentDTO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.util.HashMap;
import java.util.Map;
@RestController
@Slf4j
public class HelloWorldController {
@RequestMapping("/hello")
public Map<String, Object> showHelloWorld(){
Map<String, Object> map=new HashMap<>();
map.put("msg", "HelloWorld");
return map;
}
@Autowired
WordToHtmlConverter converter;
/**
* Transforms the Word document into HTML document and returns the transformed document.
*
* @return The content of the uploaded document as HTML.
*/
@RequestMapping(value="/api/word-to-html", method=RequestMethod.POST)
public ConvertedDocumentDTO convertWordDocumentIntoHtmlDocument(@RequestParam(value="file", required=true) MultipartFile wordDocument) {
log.info("Converting word document into HTML document");
ConvertedDocumentDTO htmlDocument=converter.convertWordDocumentIntoHtml(wordDocument);
log.info("Converted word document into HTML document.");
log.trace("The created HTML markup looks as follows: {}", htmlDocument);
return htmlDocument;
}
}
package com.et.tika.convertor;
import com.et.tika.dto.ConvertedDocumentDTO;
import com.et.tika.exception.DocumentConversionException;
import lombok.extern.slf4j.Slf4j;
import org.apache.tika.exception.TikaException;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.parser.ParseContext;
import org.apache.tika.parser.Parser;
import org.apache.tika.parser.microsoft.ooxml.OOXMLParser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import org.xml.sax.SAXException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.TransformerException;
import javax.xml.transform.sax.SAXTransformerFactory;
import javax.xml.transform.sax.TransformerHandler;
import javax.xml.transform.stream.StreamResult;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
/**
*
*/
@Component
@Slf4j
public class WordToHtmlConverter {
/**
* Converts a .docx document into HTML markup. This code
* is based on <a href="http://stackoverflow.com/a/9053258/313554">this StackOverflow</a> answer.
*
* @param wordDocument The converted .docx document.
* @return
*/
public ConvertedDocumentDTO convertWordDocumentIntoHtml(MultipartFile wordDocument) {
log.info("Converting word document: {} into HTML", wordDocument.getOriginalFilename());
try {
InputStream input=wordDocument.getInputStream();
Parser parser=new OOXMLParser();
StringWriter sw=new StringWriter();
SAXTransformerFactory factory=(SAXTransformerFactory)
SAXTransformerFactory.newInstance();
TransformerHandler handler=factory.newTransformerHandler();
handler.getTransformer().setOutputProperty(OutputKeys.ENCODING, "utf-8");
handler.getTransformer().setOutputProperty(OutputKeys.METHOD, "html");
handler.getTransformer().setOutputProperty(OutputKeys.INDENT, "yes");
handler.setResult(new StreamResult(sw));
Metadata metadata=new Metadata();
metadata.add(Metadata.CONTENT_TYPE, "text/html;charset=utf-8");
parser.parse(input, handler, metadata, new ParseContext());
return new ConvertedDocumentDTO(wordDocument.getOriginalFilename(), sw.toString());
}
catch (IOException | SAXException | TransformerException | TikaException ex) {
log.error("Conversion failed because an exception was thrown", ex);
throw new DocumentConversionException(ex.getMessage(), ex);
}
}
}
package com.et.tika.dto;
import org.apache.commons.lang.builder.ToStringBuilder;
/**
*
*/
public class ConvertedDocumentDTO {
private final String contentAsHtml;
private final String filename;
public ConvertedDocumentDTO(String filename, String contentAsHtml) {
this.contentAsHtml=contentAsHtml;
this.filename=filename;
}
public String getContentAsHtml() {
return contentAsHtml;
}
public String getFilename() {
return filename;
}
@Override
public String toString() {
return new ToStringBuilder(this)
.append("filename", this.filename)
.append("contentAsHtml", this.contentAsHtml)
.toString();
}
}
package com.et.tika.exception;
/**
*
*/
public final class DocumentConversionException extends RuntimeException {
public DocumentConversionException(String message, Exception ex) {
super(message, ex);
}
}
以上只是一些关键代码,所有代码请参见下面代码仓库
启动Spring Boot应用
Pandas 的简介开始,一步一步讲解了 Pandas的发展现状、内存优化等问题。既适合用过 Pandas 的读者,也适合没用过但想要上手的小白。
本文包括以下内容:
Pandas 发展现状;
内存优化;
索引;
方法链;
随机提示。
在阅读本文时,我建议你阅读每个你不了解的函数的文档字符串(docstrings)。简单的 Google 搜索和几秒钟 Pandas 文档的阅读,都会使你的阅读体验更加愉快。
Pandas 的定义和现状
什么是 Pandas?
Pandas 是一个「开源的、有 BSD 开源协议的库,它为 Python 编程语言提供了高性能、易于使用的数据架构以及数据分析工具」。总之,它提供了被称为 DataFrame 和 Series(对那些使用 Panel 的人来说,它们已经被弃用了)的数据抽象,通过管理索引来快速访问数据、执行分析和转换运算,甚至可以绘图(用 matplotlib 后端)。
Pandas 的当前最新版本是 v0.25.0 (https://github.com/pandas-dev/pandas/releases/tag/v0.25.0)
Pandas 正在逐步升级到 1.0 版,而为了达到这一目的,它改变了很多人们习以为常的细节。Pandas 的核心开发者之一 Marc Garcia 发表了一段非常有趣的演讲——「走向 Pandas 1.0」。
演讲链接:https://www.youtube.com/watch?v=hK6o_TDXXN8
用一句话来总结,Pandas v1.0 主要改善了稳定性(如时间序列)并删除了未使用的代码库(如 SparseDataFrame)。
数据
让我们开始吧!选择「1985 到 2016 年间每个国家的自杀率」作为玩具数据集。这个数据集足够简单,但也足以让你上手 Pandas。
数据集链接:https://www.kaggle.com/russellyates88/suicide-rates-overview-1985-to-2016
在深入研究代码之前,如果你想重现结果,要先执行下面的代码准备数据,确保列名和类型是正确的。
import pandas as pdimport numpy as npimport os# to download https://www.kaggle.com/russellyates88/suicide-rates-overview-1985-to-2016data_path='path/to/folder/'df=(pd.read_csv(filepath_or_buffer=os.path.join(data_path, 'master.csv')) .rename(columns={'suicides/100k pop' : 'suicides_per_100k', ' gdp_for_year ($) ' : 'gdp_year', 'gdp_per_capita ($)' : 'gdp_capita', 'country-year' : 'country_year'}) .assign(gdp_year=lambda _df: _df['gdp_year'].str.replace(',','').astype(np.int64)) )
提示:如果你读取了一个大文件,在 read_csv(https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.read_csv.html)中参数设定为 chunksize=N,这会返回一个可以输出 DataFrame 对象的迭代器。
这里有一些关于这个数据集的描述:
>>> df.columnsIndex(['country', 'year', 'sex', 'age', 'suicides_no', 'population', 'suicides_per_100k', 'country_year', 'HDI for year', 'gdp_year', 'gdp_capita', 'generation'], dtype='object')
这里有 101 个国家、年份从 1985 到 2016、两种性别、六个年代以及六个年龄组。有一些获得这些信息的方法:
可以用 unique() 和 nunique() 获取列内唯一的值(或唯一值的数量);
>>> df['generation'].unique()array(['Generation X', 'Silent', 'G.I. Generation', 'Boomers', 'Millenials', 'Generation Z'], dtype=object)>>> df['country'].nunique()101
可以用 describe() 输出每一列不同的统计数据(例如最小值、最大值、平均值、总数等),如果指定 include='all',会针对每一列目标输出唯一元素的数量和出现最多元素的数量;
可以用 head() 和 tail() 来可视化数据框的一小部分。
通过这些方法,你可以迅速了解正在分析的表格文件。
内存优化
在处理数据之前,了解数据并为数据框的每一列选择合适的类型是很重要的一步。
在内部,Pandas 将数据框存储为不同类型的 numpy 数组(比如一个 float64 矩阵,一个 int32 矩阵)。
有两种可以大幅降低内存消耗的方法。
import pandas as pddef mem_usage(df: pd.DataFrame) -> str: """This method styles the memory usage of a DataFrame to be readable as MB. Parameters ---------- df: pd.DataFrame Data frame to measure. Returns ------- str Complete memory usage as a string formatted for MB. """ return f'{df.memory_usage(deep=True).sum() / 1024 ** 2 : 3.2f} MB'def convert_df(df: pd.DataFrame, deep_copy: bool=True) -> pd.DataFrame: """Automatically converts columns that are worth stored as ``categorical`` dtype. Parameters ---------- df: pd.DataFrame Data frame to convert. deep_copy: bool Whether or not to perform a deep copy of the original data frame. Returns ------- pd.DataFrame Optimized copy of the input data frame. """ return df.copy(deep=deep_copy).astype({ col: 'category' for col in df.columns if df[col].nunique() / df[col].shape[0] < 0.5})
Pandas 提出了一种叫做 memory_usage() 的方法,这种方法可以分析数据框的内存消耗。在代码中,指定 deep=True 来确保考虑到了实际的系统使用情况。
memory_usage():https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.memory_usage.html
了解列的类型(https://pandas.pydata.org/pandas-docs/stable/getting_started/basics.html#basics-dtypes)很重要。它可以通过两种简单的方法节省高达 90% 的内存使用:
了解数据框使用的类型;
了解数据框可以使用哪种类型来减少内存的使用(例如,price 这一列值在 0 到 59 之间,只带有一位小数,使用 float64 类型可能会产生不必要的内存开销)
除了降低数值类型的大小(用 int32 而不是 int64)外,Pandas 还提出了分类类型:https://pandas.pydata.org/pandas-docs/stable/user_guide/categorical.html
如果你是用 R 语言的开发人员,你可能觉得它和 factor 类型是一样的。
这种分类类型允许用索引替换重复值,还可以把实际值存在其他位置。教科书中的例子是国家。和多次存储相同的字符串「瑞士」或「波兰」比起来,为什么不简单地用 0 和 1 替换它们,并存储在字典中呢?
categorical_dict={0: 'Switzerland', 1: 'Poland'}
Pandas 做了几乎相同的工作,同时添加了所有的方法,可以实际使用这种类型,并且仍然能够显示国家的名称。
回到 convert_df() 方法,如果这一列中的唯一值小于 50%,它会自动将列类型转换成 category。这个数是任意的,但是因为数据框中类型的转换意味着在 numpy 数组间移动数据,因此我们得到的必须比失去的多。
接下来看看数据中会发生什么。
>>> mem_usage(df)10.28 MB>>> mem_usage(df.set_index(['country', 'year', 'sex', 'age']))5.00 MB>>> mem_usage(convert_df(df))1.40 MB>>> mem_usage(convert_df(df.set_index(['country', 'year', 'sex', 'age'])))1.40 MB
通过使用「智能」转换器,数据框使用的内存几乎减少了 10 倍(准确地说是 7.34 倍)。
索引
Pandas 是强大的,但也需要付出一些代价。当你加载 DataFrame 时,它会创建索引并将数据存储在 numpy 数组中。这是什么意思?一旦加载了数据框,只要正确管理索引,就可以快速地访问数据。
访问数据的方法主要有两种,分别是通过索引和查询访问。根据具体情况,你只能选择其中一种。但在大多数情况中,索引(和多索引)都是最好的选择。我们来看下面的例子:
>>> %%time>>> df.query('country=="Albania" and year==1987 and sex=="male" and age=="25-34 years"')CPU times: user 7.27 ms, sys: 751 μs, total: 8.02 ms#==================>>> %%time>>> mi_df.loc['Albania', 1987, 'male', '25-34 years']CPU times: user 459 μs, sys: 1 μs, total: 460 μs
什么?加速 20 倍?
你要问自己了,创建这个多索引要多长时间?
%%timemi_df=df.set_index(['country', 'year', 'sex', 'age'])CPU times: user 10.8 ms, sys: 2.2 ms, total: 13 ms
通过查询访问数据的时间是 1.5 倍。如果你只想检索一次数据(这种情况很少发生),查询是正确的方法。否则,你一定要坚持用索引,CPU 会为此感激你的。
.set_index(drop=False) 允许不删除用作新索引的列。
.loc[]/.iloc[] 方法可以很好地读取数据框,但无法修改数据框。如果需要手动构建(比如使用循环),那就要考虑其他的数据结构了(比如字典、列表等),在准备好所有数据后,创建 DataFrame。否则,对于 DataFrame 中的每一个新行,Pandas 都会更新索引,这可不是简单的哈希映射。
>>> (pd.DataFrame({'a':range(2), 'b': range(2)}, index=['a', 'a']) .loc['a']) a ba 0 0a 1 1
因此,未排序的索引可以降低性能。为了检查索引是否已经排序并对它排序,主要有两种方法:
%%time>>> mi_df.sort_index()CPU times: user 34.8 ms, sys: 1.63 ms, total: 36.5 ms>>> mi_df.index.is_monotonicTrue
更多详情请参阅:
Pandas 高级索引用户指南:https://pandas.pydata.org/pandas-docs/stable/user_guide/advanced.html;
Pandas 库中的索引代码:https://github.com/pandas-dev/pandas/blob/master/pandas/core/indexing.py。
方法链
使用 DataFrame 的方法链是链接多个返回 DataFrame 方法的行为,因此它们都是来自 DataFrame 类的方法。在现在的 Pandas 版本中,使用方法链是为了不存储中间变量并避免出现如下情况:
import numpy as npimport pandas as pddf=pd.DataFrame({'a_column': [1, -999, -999], 'powerless_column': [2, 3, 4], 'int_column': [1, 1, -1]}) df['a_column']=df['a_column'].replace(-999, np.nan) df['power_column']=df['powerless_column'] ** 2 df['real_column']=df['int_column'].astype(np.float64) df=df.apply(lambda _df: _df.replace(4, np.nan)) df=df.dropna(how='all')
用下面的链替换:
df=(pd.DataFrame({'a_column': [1, -999, -999], 'powerless_column': [2, 3, 4], 'int_column': [1, 1, -1]}) .assign(a_column=lambda _df: _df['a_column'].replace(-999, np.nan)) .assign(power_column=lambda _df: _df['powerless_column'] ** 2) .assign(real_column=lambda _df: _df['int_column'].astype(np.float64)) .apply(lambda _df: _df.replace(4, np.nan)) .dropna(how='all') )
说实话,第二段代码更漂亮也更简洁。
方法链的工具箱是由不同的方法(比如 apply、assign、loc、query、pipe、groupby 以及 agg)组成的,这些方法的输出都是 DataFrame 对象或 Series 对象(或 DataFrameGroupBy)。
了解它们最好的方法就是实际使用。举个简单的例子:
(df .groupby('age') .agg({'generation':'unique'}) .rename(columns={'generation':'unique_generation'})# Recommended from v0.25# .agg(unique_generation=('generation', 'unique')))
获得每个年龄范围中所有唯一年代标签的简单链
在得到的数据框中,「年龄」列是索引。
除了了解到「X 代」覆盖了三个年龄组外,分解这条链。第一步是对年龄组分组。这一方法返回了一个 DataFrameGroupBy 对象,在这个对象中,通过选择组的唯一年代标签聚合了每一组。
在这种情况下,聚合方法是「unique」方法,但它也可以接受任何(匿名)函数。
在 0.25 版本中,Pandas 引入了使用 agg 的新方法:https://dev.pandas.io/whatsnew/v0.25.0.html#groupby-aggregation-with-relabeling。
(df .groupby(['country', 'year']) .agg({'suicides_per_100k': 'sum'}) .rename(columns={'suicides_per_100k':'suicides_sum'})# Recommended from v0.25# .agg(suicides_sum=('suicides_per_100k', 'sum')) .sort_values('suicides_sum', ascending=False) .head(10))
用排序值(sort_values)和 head 得到自杀率排前十的国家和年份
(df .groupby(['country', 'year']) .agg({'suicides_per_100k': 'sum'}) .rename(columns={'suicides_per_100k':'suicides_sum'})# Recommended from v0.25# .agg(suicides_sum=('suicides_per_100k', 'sum')) .nlargest(10, columns='suicides_sum'))
用排序值 nlargest 得到自杀率排前十的国家和年份
在这些例子中,输出都是一样的:有两个指标(国家和年份)的 MultiIndex 的 DataFrame,还有包含排序后的 10 个最大值的新列 suicides_sum。
「国家」和「年份」列是索引。
nlargest(10) 比 sort_values(ascending=False).head(10) 更有效。
另一个有趣的方法是 unstack:https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.unstack.html,这种方法允许转动索引水平。
(mi_df .loc[('Switzerland', 2000)] .unstack('sex') [['suicides_no', 'population']])
「age」是索引,列「suicides_no」和「population」都有第二个水平列「sex」。
下一个方法 pipe 是最通用的方法之一。这种方法允许管道运算(就像在 shell 脚本中)执行比链更多的运算。
管道的一个简单但强大的用法是记录不同的信息。
def log_head(df, head_count=10): print(df.head(head_count)) return dfdef log_columns(df): print(df.columns) return dfdef log_shape(df): print(f'shape={df.shape}') return df
和 pipe 一起使用的不同记录函数。
举个例子,我们想验证和 year 列相比,country_year 是否正确:
(df .assign(valid_cy=lambda _serie: _serie.apply( lambda _row: re.split(r'(?=\d{4})', _row['country_year'])[1]==str(_row['year']), axis=1)) .query('valid_cy==False') .pipe(log_shape))
用来验证「country_year」列中年份的管道。
管道的输出是 DataFrame,但它也可以在标准输出(console/REPL)中打印。
shape=(0, 13)
你也可以在一条链中用不同的 pipe。
(df .pipe(log_shape) .query('sex=="female"') .groupby(['year', 'country']) .agg({'suicides_per_100k':'sum'}) .pipe(log_shape) .rename(columns={'suicides_per_100k':'sum_suicides_per_100k_female'})# Recommended from v0.25# .agg(sum_suicides_per_100k_female=('suicides_per_100k', 'sum')) .nlargest(n=10, columns=['sum_suicides_per_100k_female']))
女性自杀数量最高的国家和年份。
生成的 DataFrame 如下所示:
索引是「年份」和「国家」。
标准输出的打印如下所示:
shape=(27820, 12)shape=(2321, 1)
除了记录到控制台外,pipe 还可以直接在数据框的列上应用函数。
from sklearn.preprocessing import MinMaxScalerdef norm_df(df, columns): return df.assign(**{col: MinMaxScaler().fit_transform(df[[col]].values.astype(float)) for col in columns}) for sex in ['male', 'female']: print(sex) print( df .query(f'sex=="{sex}"') .groupby(['country']) .agg({'suicides_per_100k': 'sum', 'gdp_year': 'mean'}) .rename(columns={'suicides_per_100k':'suicides_per_100k_sum', 'gdp_year': 'gdp_year_mean'}) # Recommended in v0.25 # .agg(suicides_per_100k=('suicides_per_100k_sum', 'sum'), # gdp_year=('gdp_year_mean', 'mean')) .pipe(norm_df, columns=['suicides_per_100k_sum', 'gdp_year_mean']) .corr(method='spearman') ) print('\n')
自杀数量是否和 GDP 的下降相关?是否和性别相关?
上面的代码在控制台中的打印如下所示:
male suicides_per_100k_sum gdp_year_meansuicides_per_100k_sum 1.000000 0.421218gdp_year_mean 0.421218 1.000000
female suicides_per_100k_sum gdp_year_meansuicides_per_100k_sum 1.000000 0.452343gdp_year_mean 0.452343 1.000000
深入研究代码。norm_df() 将一个 DataFrame 和用 MinMaxScaling 扩展列的列表当做输入。使用字典理解,创建一个字典 {column_name: method, …},然后将其解压为 assign() 函数的参数 (colunmn_name=method, …)。
在这种特殊情况下,min-max 缩放不会改变对应的输出:https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.corr.html,它仅用于参数。
在(遥远的?)未来,缓式评估(lazy evaluation)可能出现在方法链中,所以在链上做一些投资可能是一个好想法。
最后(随机)的技巧
下面的提示很有用,但不适用于前面的任何部分:
itertuples() 可以更高效地遍历数据框的行;
>>> %%time>>> for row in df.iterrows(): continueCPU times: user 1.97 s, sys: 17.3 ms, total: 1.99 s>>> for tup in df.itertuples(): continueCPU times: user 55.9 ms, sys: 2.85 ms, total: 58.8 ms
注意:tup 是一个 namedtuple
join() 用了 merge();
在 Jupyter 笔记本中,在代码块的开头写上 %%time,可以有效地测量时间;
UInt8 类:https://pandas.pydata.org/pandas-docs/stable/user_guide/gotchas.html#support-for-integer-na支持带有整数的 NaN 值;
记住,任何密集的 I/O(例如展开大型 CSV 存储)用低级方法都会执行得更好(尽可能多地用 Python 的核心函数)。
还有一些本文没有涉及到的有用的方法和数据结构,这些方法和数据结构都很值得花时间去理解:
数据透视表:https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.pivot.html?source=post_page---------------------------
时间序列/日期功能:https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html?source=post_page---------------------------;
绘图:https://pandas.pydata.org/pandas-docs/stable/user_guide/visualization.html?source=post_page---------------------------。
总结
希望你可以因为这篇简短的文章,更好地理解 Pandas 背后的工作原理,以及 Pandas 库的发展现状。本文还展示了不同的用于优化数据框内存以及快速分析数据的工具。希望对现在的你来说,索引和查找的概念能更加清晰。最后,你还可以试着用方法链写更长的链。
这里还有一些笔记:https://github.com/unit8co/medium-pandas-wan?source=post_page---------------------------
除了文中的所有代码外,还包括简单数据索引数据框(df)和多索引数据框(mi_df)性能的定时指标。
熟能生巧,所以继续修炼技能,并帮助我们建立一个更好的世界吧。
PS:有时候纯用 Numpy 会更快。
原文链接:https://medium.com/unit8-machine-learning-publication/from-pandas-wan-to-pandas-master-4860cf0ce442
*请认真填写需求信息,我们会在24小时内与您取得联系。