使用 Python 和 GNU Octave 繪製資料

2020-02-29 11:48:00

了解如何使用 Python 和 GNU Octave 完成一項常見的資料科學任務。

資料科學是跨越程式語言的知識領域。有些語言以解決這一領域的問題而聞名,而另一些則鮮為人知。這篇文章將幫助你熟悉用一些流行的語言完成資料科學的工作。

選擇 Python 和 GNU Octave 做資料科學工作

我經常嘗試學習一種新的程式語言。為什麼?這既有對舊方式的厭倦,也有對新方式的好奇。當我開始學習程式設計時,我唯一知道的語言是 C 語言。那些年的程式設計生涯既艱難又危險,因為我必須手動分配記憶體、管理指標、並記得釋放記憶體。

後來一個朋友建議我試試 Python,現在我的程式設計生活變得輕鬆多了。雖然程式執行變得慢多了,但我不必通過編寫分析軟體來受苦了。然而,我很快就意識到每種語言都有比其它語言更適合自己的應用場景。後來我學習了一些其它語言,每種語言都給我帶來了一些新的啟發。發現新的程式設計風格讓我可以將一些解決方案移植到其他語言中,這樣一切都變得有趣多了。

為了對一種新的程式語言(及其文件)有所了解,我總是從編寫一些執行我熟悉的任務的範例程式開始。為此,我將解釋如何用 Python 和 GNU Octave 編寫一個程式來完成一個你可以歸類為資料科學的特殊任務。如果你已經熟悉其中一種語言,從它開始,然後通過其他語言尋找相似之處和不同之處。這篇文章並不是對程式語言的詳盡比較,只是一個小小的展示。

所有的程式都應該在命令列上執行,而不是用圖形化使用者介面(GUI)。完整的例子可以在 polyglot_fit 儲存庫中找到。

程式設計任務

你將在本系列中編寫的程式:

  • CSV 檔案中讀取資料
  • 用直線插入資料(例如 f(x)=m ⋅ x + q
  • 將結果生成影象檔案

這是許多資料科學家遇到的常見情況。範例資料是 Anscombe 的四重奏的第一組,如下表所示。這是一組人工構建的資料,當用直線擬合時會給出相同的結果,但是它們的曲線非常不同。資料檔案是一個文字檔案,以製表符作為列分隔符,開頭幾行作為標題。此任務將僅使用第一組(即前兩列)。

Python 方式

Python 是一種通用程式語言,是當今最流行的語言之一(依據 TIOBE 指數RedMonk 程式語言排名程式語言流行指數GitHub Octoverse 狀態和其他來源的調查結果)。它是一種直譯語言;因此,原始碼由執行該指令的程式讀取和評估。它有一個全面的標準庫並且總體上非常好用(我對這最後一句話沒有證據;這只是我的拙見)。

安裝

要使用 Python 開發,你需要直譯器和一些庫。最低要求是:

Fedora 安裝它們是很容易的:

sudo dnf install python3 python3-numpy python3-scipy python3-matplotlib

程式碼註釋

在 Python中,注釋是通過在行首新增一個 # 來實現的,該行的其餘部分將被直譯器丟棄:

# 這是被直譯器忽略的注釋。

fitting_python.py 範例使用注釋在原始碼中插入許可證資訊,第一行是特殊注釋,它允許該指令碼在命令列上執行:

#!/usr/bin/env python3

這一行通知命令列直譯器,該指令碼需要由程式 python3 執行。

需要的庫

在 Python 中,庫和模組可以作為一個物件匯入(如範例中的第一行),其中包含庫的所有函數和成員。可以通過使用 as 方式用自定義標籤重新命名它們:

import numpy as npfrom scipy import statsimport matplotlib.pyplot as plt

你也可以決定只匯入一個子模組(如第二行和第三行)。語法有兩個(基本上)等效的方式:import module.submodulefrom module import submodule

定義變數

Python 的變數是在第一次賦值時被宣告的:

input_file_name = "anscombe.csv"delimiter = "\t"skip_header = 3column_x = 0column_y = 1

變數型別由分配給變數的值推斷。沒有具有常數值的變數,除非它們在模組中宣告並且只能被讀取。習慣上,不應被修改的變數應該用大寫字母命名。

列印輸出

通過命令列執行程式意味著輸出只能列印在終端上。Python 有 print() 函數,預設情況下,該函數列印其引數,並在輸出的末尾新增一個換行符:

print("#### Anscombe's first set with Python ####")

在 Python 中,可以將 print() 函數與字串類格式化能力相結合。字串具有format 方法,可用於向字串本身新增一些格式化文字。例如,可以新增格式化的浮點數,例如:

print("Slope: {:f}".format(slope))

讀取資料

使用 NumPy 和函數 genfromtxt() 讀取 CSV 檔案非常容易,該函數生成 NumPy 陣列

data = np.genfromtxt(input_file_name, delimiter = delimiter, skip_header = skip_header)

在 Python 中,一個函數可以有數量可變的引數,你可以通過指定所需的引數來傳遞一個引數的子集。陣列是非常強大的矩陣狀物件,可以很容易地分割成更小的陣列:

x = data[:, column_x]y = data[:, column_y]

冒號選擇整個範圍,也可以用來選擇子範圍。例如,要選擇陣列的前兩行,可以使用:

first_two_rows = data[0:1, :]

擬合資料

SciPy 提供了方便的資料擬合功能,例如 linregress() 功能。該函數提供了一些與擬合相關的重要值,如斜率、截距和兩個資料集的相關係數:

slope, intercept, r_value, p_value, std_err = stats.linregress(x, y)print("Slope: {:f}".format(slope))print("Intercept: {:f}".format(intercept))print("Correlation coefficient: {:f}".format(r_value))

因為 linregress() 提供了幾條資訊,所以結果可以同時儲存到幾個變數中。

繪圖

Matplotlib 庫僅僅繪製資料點,因此,你應該定義要繪製的點的坐標。已經定義了 xy 陣列,所以你可以直接繪製它們,但是你還需要代表直線的資料點。

fit_x = np.linspace(x.min() - 1, x.max() + 1, 100)

linspace() 函數可以方便地在兩個值之間生成一組等距值。利用強大的 NumPy 陣列可以輕鬆計算縱坐標,該陣列可以像普通數值變數一樣在公式中使用:

fit_y = slope * fit_x + intercept

該公式在陣列中逐元素應用;因此,結果在初始陣列中具有相同數量的條目。

要繪圖,首先,定義一個包含所有圖形的圖形物件

fig_width = 7 #inchfig_height = fig_width / 16 * 9 #inchfig_dpi = 100fig = plt.figure(figsize = (fig_width, fig_height), dpi = fig_dpi)

一個圖形可以畫幾個圖;在 Matplotlib 中,這些圖被稱為。本範例定義一個單軸物件來繪製資料點:

ax = fig.add_subplot(111)ax.plot(fit_x, fit_y, label = "Fit", linestyle = '-')ax.plot(x, y, label = "Data", marker = '.', linestyle = '')ax.legend()ax.set_xlim(min(x) - 1, max(x) + 1)ax.set_ylim(min(y) - 1, max(y) + 1)ax.set_xlabel('x')ax.set_ylabel('y')

將該圖儲存到 PNG 圖形檔案中,有:

fig.savefig('fit_python.png')

如果要顯示(而不是儲存)該繪圖,請呼叫:

plt.show()

此範例參照了繪圖部分中使用的所有物件:它定義了物件 fig 和物件 ax。這在技術上是不必要的,因為 plt 物件可以直接用於繪製資料集。《Matplotlib 教學》展示了這樣一個介面:

plt.plot(fit_x, fit_y)

坦率地說,我不喜歡這種方法,因為它隱藏了各種物件之間發生的重要互動。不幸的是,有時官方的例子有點令人困惑,因為他們傾向於使用不同的方法。在這個簡單的例子中,參照圖形物件是不必要的,但是在更複雜的例子中(例如在圖形化使用者介面中嵌入圖形時),參照圖形物件就變得很重要了。

結果

命令列輸入:

#### Anscombe's first set with Python ####Slope: 0.500091Intercept: 3.000091Correlation coefficient: 0.816421

這是 Matplotlib 產生的影象:

Plot and fit of the dataset obtained with Python

GNU Octave 方式

GNU Octave 語言主要用於數值計算。它提供了一個簡單的操作向量和矩陣的語法,並且有一些強大的繪圖工具。這是一種像 Python 一樣的解釋語言。由於 Octave 的語法幾乎相容 MATLAB,它經常被描述為一個替代 MATLAB 的免費方案。Octave 沒有被列為最流行的程式語言,而 MATLAB 則是,所以 Octave 在某種意義上是相當流行的。MATLAB 早於 NumPy,我覺得它是受到了前者的啟發。當你看這個例子時,你會看到相似之處。

安裝

fitting_octave.m 的例子只需要基本的 Octave 包,在 Fedora 中安裝相當簡單:

sudo dnf install octave

程式碼註釋

在 Octave 中,你可以用百分比符號(%)為程式碼新增註釋,如果不需要與 MATLAB 相容,你也可以使用 #。使用 # 的選項允許你編寫像 Python 範例一樣的特殊注釋行,以便直接在命令列上執行指令碼。

必要的庫

本例中使用的所有內容都包含在基本包中,因此你不需要載入任何新的庫。如果你需要一個庫,語法pkg load module。該命令將模組的功能新增到可用功能列表中。在這方面,Python 具有更大的靈活性。

定義變數

變數的定義與 Python 的語法基本相同:

input_file_name = "anscombe.csv";delimiter = "\t";skip_header = 3;column_x = 1;column_y = 2;

請注意,行尾有一個分號;這不是必需的,但是它會抑制該行結果的輸出。如果沒有分號,直譯器將列印表示式的結果:

octave:1> input_file_name = "anscombe.csv"input_file_name = anscombe.csvoctave:2> sqrt(2)ans =  1.4142

列印輸出結果

強大的函數 printf() 是用來在終端上列印的。與 Python 不同,printf() 函數不會自動在列印字串的末尾新增換行,因此你必須新增它。第一個引數是一個字串,可以包含要傳遞給函數的其他引數的格式資訊,例如:

printf("Slope: %f\n", slope);

在 Python 中,格式是內建在字串本身中的,但是在 Octave 中,它是特定於 printf() 函數。

讀取資料

dlmread() 函數可以讀取類似 CSV 檔案的文字內容:

data = dlmread(input_file_name, delimiter, skip_header, 0);

結果是一個矩陣物件,這是 Octave 中的基本資料型別之一。矩陣可以用類似於 Python 的語法進行切片:

x = data(:, column_x);y = data(:, column_y);

根本的區別是索引從 1 開始,而不是從 0 開始。因此,在該範例中,x 列是第一列。

擬合資料

要用直線擬合資料,可以使用 polyfit() 函數。它用一個多項式擬合輸入資料,所以你只需要使用一階多項式:

p = polyfit(x, y, 1);slope = p(1);intercept = p(2);

結果是具有多項式係數的矩陣;因此,它選擇前兩個索引。要確定相關係數,請使用 corr() 函數:

r_value = corr(x, y);

最後,使用 printf() 函數列印結果:

printf("Slope: %f\n", slope);printf("Intercept: %f\n", intercept);printf("Correlation coefficient: %f\n", r_value);

繪圖

與 Matplotlib 範例一樣,首先需要建立一個表示擬合直線的資料集:

fit_x = linspace(min(x) - 1, max(x) + 1, 100);fit_y = slope * fit_x + intercept;

與 NumPy 的相似性也很明顯,因為它使用了 linspace() 函數,其行為就像 Python 的等效版本一樣。

同樣,與 Matplotlib 一樣,首先建立一個物件,然後建立一個物件來儲存這些圖:

fig_width = 7; %inchfig_height = fig_width / 16 * 9; %inchfig_dpi = 100;fig = figure("units", "inches",             "position", [1, 1, fig_width, fig_height]);ax = axes("parent", fig);set(ax, "fontsize", 14);set(ax, "linewidth", 2);

要設定軸物件的屬性,請使用 set() 函數。然而,該介面相當混亂,因為該函數需要一個逗號分隔的屬性和值對列表。這些對只是代表屬性名的一個字串和代表該屬性值的第二個物件的連續。還有其他設定各種屬性的函數:

xlim(ax, [min(x) - 1, max(x) + 1]);ylim(ax, [min(y) - 1, max(y) + 1]);xlabel(ax, 'x');ylabel(ax, 'y');

繪圖是用 plot() 功能實現的。預設行為是每次呼叫都會重置坐標軸,因此需要使用函數 hold()

hold(ax, "on");plot(ax, fit_x, fit_y,     "marker", "none",     "linestyle", "-",     "linewidth", 2);plot(ax, x, y,     "marker", ".",     "markersize", 20,     "linestyle", "none");hold(ax, "off");

此外,還可以在 plot() 函數中新增屬性和值對。legend 必須單獨建立,標籤應手動宣告:

lg = legend(ax, "Fit", "Data");set(lg, "location", "northwest");

最後,將輸出儲存到 PNG 影象:

image_size = sprintf("-S%f,%f", fig_width * fig_dpi, fig_height * fig_dpi);image_resolution = sprintf("-r%f,%f", fig_dpi);print(fig, 'fit_octave.png',      '-dpng',      image_size,      image_resolution);

令人困惑的是,在這種情況下,選項被作為一個字串傳遞,帶有屬性名和值。因為在 Octave 字串中沒有 Python 的格式化工具,所以必須使用 sprintf() 函數。它的行為就像 printf() 函數,但是它的結果不是列印出來的,而是作為字串返回的。

在這個例子中,就像在 Python 中一樣,圖形物件很明顯被參照以保持它們之間的互動。如果說 Python 在這方面的文件有點混亂,那麼 Octave 的文件就更糟糕了。我發現的大多數例子都不關心參照物件;相反,它們依賴於繪圖命令作用於當前活動圖形。全域性根圖形物件跟蹤現有的圖形和軸。

結果

命令列上的結果輸出是:

#### Anscombe's first set with Octave ####Slope: 0.500091Intercept: 3.000091Correlation coefficient: 0.816421

它顯示了用 Octave 生成的結果影象。

Plot and fit of the dataset obtained with Octave

接下來

Python 和 GNU Octave 都可以繪製出相同的資訊,儘管它們的實現方式不同。如果你想探索其他語言來完成類似的任務,我強烈建議你看看 Rosetta Code。這是一個了不起的資源,可以看到如何用多種語言解決同樣的問題。

你喜歡用什麼語言繪製資料?在評論中分享你的想法。