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小時內與您取得聯系。