在資料科學中使用 C 和 C++
讓我們使用 C99 和 C++11 完成常見的資料科學任務。
雖然 Python 和 R 之類的語言在資料科學中越來越受歡迎,但是 C 和 C++ 對於高效的資料科學來說是一個不錯的選擇。在本文中,我們將使用 C99 和 C++11 編寫一個程式,該程式使用 Anscombe 的四重奏資料集,下面將對其進行解釋。
我在一篇涉及 Python 和 GNU Octave 的文章中寫了我不斷學習程式語言的動機,值得大家回顧。這裡所有的程式都需要在命令列上執行,而不是在圖形使用者介面(GUI)上執行。完整的示例可在 polyglot_fit 儲存庫中找到。
程式設計任務
你將在本系列中編寫的程式:
- 從 CSV 檔案中讀取資料
- 用直線插值資料(即
f(x)=m ⋅ x + q
) - 將結果繪製到影象檔案
這是許多資料科學家遇到的普遍情況。示例資料是 Anscombe 的四重奏的第一組,如下表所示。這是一組人工構建的資料,當擬合直線時可以提供相同的結果,但是它們的曲線非常不同。資料檔案是一個文字檔案,其中的製表符用作列分隔符,前幾行作為標題。該任務將僅使用第一組(即前兩列)。
C 語言的方式
C 語言是通用程式語言,是當今使用最廣泛的語言之一(依據 TIOBE 指數、RedMonk 程式語言排名、程式語言流行度指數和 GitHub Octoverse 狀態 得來)。這是一種相當古老的語言(大約誕生在 1973 年),並且用它編寫了許多成功的程式(例如 Linux 核心和 Git 僅是其中的兩個例子)。它也是最接近計算機內部執行機制的語言之一,因為它直接用於操作記憶體。它是一種編譯語言;因此,原始碼必須由編譯器轉換為機器程式碼。它的標準庫很小,功能也不多,因此人們開發了其它庫來提供缺少的功能。
我最常在數字運算中使用該語言,主要是因為其效能。我覺得使用起來很繁瑣,因為它需要很多樣板程式碼,但是它在各種環境中都得到了很好的支援。C99 標準是最新版本,增加了一些漂亮的功能,並且得到了編譯器的良好支援。
我將一路介紹 C 和 C++ 程式設計的必要背景,以便初學者和高階使用者都可以繼續學習。
安裝
要使用 C99 進行開發,你需要一個編譯器。我通常使用 Clang,不過 GCC 是另一個有效的開源編譯器。對於線性擬合,我選擇使用 GNU 科學庫。對於繪圖,我找不到任何明智的庫,因此該程式依賴於外部程式:Gnuplot。該示例還使用動態資料結構來儲存資料,該結構在伯克利軟體分發版(BSD)中定義。
在 Fedora 中安裝很容易:
sudo dnf install clang gnuplot gsl gsl-devel
程式碼註釋
在 C99 中,註釋的格式是在行的開頭放置 //
,行的其它部分將被直譯器丟棄。另外,/*
和 */
之間的任何內容也將被丟棄。
// 這是一個註釋,會被直譯器忽略
/* 這也被忽略 */
必要的庫
庫由兩部分組成:
- 標頭檔案,其中包含函式說明
- 包含函式定義的原始檔
標頭檔案包含在原始檔中,而庫檔案的原始檔則連結到可執行檔案。因此,此示例所需的標頭檔案是:
// 輸入/輸出功能
#include <stdio.h>
// 標準庫
#include <stdlib.h>
// 字串操作功能
#include <string.h>
// BSD 佇列
#include <sys/queue.h>
// GSL 科學功能
#include <gsl/gsl_fit.h>
#include <gsl/gsl_statistics_double.h>
主函式
在 C 語言中,程式必須位於稱為主函式 main() 的特殊函式內:
int main(void) {
...
}
這與上一教程中介紹的 Python 不同,後者將執行在原始檔中找到的所有程式碼。
定義變數
在 C 語言中,變數必須在使用前宣告,並且必須與型別關聯。每當你要使用變數時,都必須決定要在其中儲存哪種資料。你也可以指定是否打算將變數用作常量值,這不是必需的,但是編譯器可以從此資訊中受益。 以下來自儲存庫中的 fitting_C99.c 程式:
const char *input_file_name = "anscombe.csv";
const char *delimiter = "\t";
const unsigned int skip_header = 3;
const unsigned int column_x = 0;
const unsigned int column_y = 1;
const char *output_file_name = "fit_C99.csv";
const unsigned int N = 100;
C 語言中的陣列不是動態的,從某種意義上說,陣列的長度必須事先確定(即,在編譯之前):
int data_array[1024];
由於你通常不知道檔案中有多少個資料點,因此請使用單鏈列表。這是一個動態資料結構,可以無限增長。幸運的是,BSD 提供了連結串列。這是一個示例定義:
struct data_point {
double x;
double y;
SLIST_ENTRY(data_point) entries;
};
SLIST_HEAD(data_list, data_point) head = SLIST_HEAD_INITIALIZER(head);
SLIST_INIT(&head);
該示例定義了一個由結構化值組成的 data_point
列表,該結構化值同時包含 x
值和 y
值。語法相當複雜,但是很直觀,詳細描述它就會太冗長了。
列印輸出
要在終端上列印,可以使用 printf() 函式,其功能類似於 Octave 的 printf()
函式(在第一篇文章中介紹):
printf("#### Anscombe's first set with C99 ####\n");
printf()
函式不會在列印字串的末尾自動新增換行符,因此你必須新增換行符。第一個引數是一個字串,可以包含傳遞給函式的其他引數的格式資訊,例如:
printf("Slope: %f\n", slope);
讀取資料
現在來到了困難的部分……有一些用 C 語言解析 CSV 檔案的庫,但是似乎沒有一個庫足夠穩定或流行到可以放入到 Fedora 軟體包儲存庫中。我沒有為本教程新增依賴項,而是決定自己編寫此部分。同樣,討論這些細節太囉嗦了,所以我只會解釋大致的思路。為了簡潔起見,將忽略原始碼中的某些行,但是你可以在儲存庫中找到完整的示例程式碼。
首先,開啟輸入檔案:
FILE* input_file = fopen(input_file_name, "r");
然後逐行讀取檔案,直到出現錯誤或檔案結束:
while (!ferror(input_file) && !feof(input_file)) {
size_t buffer_size = 0;
char *buffer = NULL;
getline(&buffer, &buffer_size, input_file);
...
}
getline() 函式是 POSIX.1-2008 標準新增的一個不錯的函式。它可以讀取檔案中的整行,並負責分配必要的記憶體。然後使用 strtok() 函式將每一行分成字元。遍歷字元,選擇所需的列:
char *token = strtok(buffer, delimiter);
while (token != NULL)
{
double value;
sscanf(token, "%lf", &value);
if (column == column_x) {
x = value;
} else if (column == column_y) {
y = value;
}
column += 1;
token = strtok(NULL, delimiter);
}
最後,當選擇了 x
和 y
值時,將新資料點插入連結串列中:
struct data_point *datum = malloc(sizeof(struct data_point));
datum->x = x;
datum->y = y;
SLIST_INSERT_HEAD(&head, datum, entries);
malloc() 函式為新資料點動態分配(保留)一些永續性記憶體。
擬合資料
GSL 線性擬合函式 gslfitlinear() 期望其輸入為簡單陣列。因此,由於你將不知道要建立的陣列的大小,因此必須手動分配它們的記憶體:
const size_t entries_number = row - skip_header - 1;
double *x = malloc(sizeof(double) * entries_number);
double *y = malloc(sizeof(double) * entries_number);
然後,遍歷連結串列以將相關資料儲存到陣列:
SLIST_FOREACH(datum, &head, entries) {
const double current_x = datum->x;
const double current_y = datum->y;
x[i] = current_x;
y[i] = current_y;
i += 1;
}
現在你已經處理完了連結串列,請清理它。要總是釋放已手動分配的記憶體,以防止記憶體洩漏。記憶體洩漏是糟糕的、糟糕的、糟糕的(重要的話說三遍)。每次記憶體沒有釋放時,花園侏儒都會找不到自己的頭:
while (!SLIST_EMPTY(&head)) {
struct data_point *datum = SLIST_FIRST(&head);
SLIST_REMOVE_HEAD(&head, entries);
free(datum);
}
終於,終於!你可以擬合你的資料了:
gsl_fit_linear(x, 1, y, 1, entries_number,
&intercept, &slope,
&cov00, &cov01, &cov11, &chi_squared);
const double r_value = gsl_stats_correlation(x, 1, y, 1, entries_number);
printf("Slope: %f\n", slope);
printf("Intercept: %f\n", intercept);
printf("Correlation coefficient: %f\n", r_value);
繪圖
你必須使用外部程式進行繪圖。因此,將擬合資料儲存到外部檔案:
const double step_x = ((max_x + 1) - (min_x - 1)) / N;
for (unsigned int i = 0; i < N; i += 1) {
const double current_x = (min_x - 1) + step_x * i;
const double current_y = intercept + slope * current_x;
fprintf(output_file, "%f\t%f\n", current_x, current_y);
}
用於繪製兩個檔案的 Gnuplot 命令是:
plot 'fit_C99.csv' using 1:2 with lines title 'Fit', 'anscombe.csv' using 1:2 with points pointtype 7 title 'Data'
結果
在執行程式之前,你必須編譯它:
clang -std=c99 -I/usr/include/ fitting_C99.c -L/usr/lib/ -L/usr/lib64/ -lgsl -lgslcblas -o fitting_C99
這個命令告訴編譯器使用 C99 標準、讀取 fitting_C99.c
檔案、載入 gsl
和 gslcblas
庫、並將結果儲存到 fitting_C99
。命令列上的結果輸出為:
#### Anscombe's first set with C99 ####
Slope: 0.500091
Intercept: 3.000091
Correlation coefficient: 0.816421
這是用 Gnuplot 生成的結果影象:
C++11 方式
C++ 語言是一種通用程式語言,也是當今使用的最受歡迎的語言之一。它是作為 C 的繼承人建立的(誕生於 1983 年),重點是物件導向程式設計(OOP)。C++ 通常被視為 C 的超集,因此 C 程式應該能夠使用 C++ 編譯器進行編譯。這並非完全正確,因為在某些極端情況下它們的行為有所不同。 根據我的經驗,C++ 與 C 相比需要更少的樣板程式碼,但是如果要進行物件導向開發,語法會更困難。C++11 標準是最新版本,增加了一些漂亮的功能,並且基本上得到了編譯器的支援。
由於 C++ 在很大程度上與 C 相容,因此我將僅強調兩者之間的區別。我在本部分中沒有涵蓋的任何部分,則意味著它與 C 中的相同。
安裝
這個 C++ 示例的依賴項與 C 示例相同。 在 Fedora 上,執行:
sudo dnf install clang gnuplot gsl gsl-devel
必要的庫
庫的工作方式與 C 語言相同,但是 include
指令略有不同:
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <algorithm>
extern "C" {
#include <gsl/gsl_fit.h>
#include <gsl/gsl_statistics_double.h>
}
由於 GSL 庫是用 C 編寫的,因此你必須將這個特殊情況告知編譯器。
定義變數
與 C 語言相比,C++ 支援更多的資料型別(類),例如,與其 C 語言版本相比,string
型別具有更多的功能。相應地更新變數的定義:
const std::string input_file_name("anscombe.csv");
對於字串之類的結構化物件,你可以定義變數而無需使用 =
符號。
列印輸出
你可以使用 printf()
函式,但是 cout
物件更慣用。使用運算子 <<
來指示要使用 cout
列印的字串(或物件):
std::cout << "#### Anscombe's first set with C++11 ####" << std::endl;
...
std::cout << "Slope: " << slope << std::endl;
std::cout << "Intercept: " << intercept << std::endl;
std::cout << "Correlation coefficient: " << r_value << std::endl;
讀取資料
該方案與以前相同。將開啟檔案並逐行讀取檔案,但語法不同:
std::ifstream input_file(input_file_name);
while (input_file.good()) {
std::string line;
getline(input_file, line);
...
}
使用與 C99 示例相同的功能提取行字元。代替使用標準的 C 陣列,而是使用兩個向量。向量是 C++ 標準庫中對 C 陣列的擴充套件,它允許動態管理記憶體而無需顯式呼叫 malloc()
:
std::vector<double> x;
std::vector<double> y;
// Adding an element to x and y:
x.emplace_back(value);
y.emplace_back(value);
擬合資料
要在 C++ 中擬合,你不必遍歷列表,因為向量可以保證具有連續的記憶體。你可以將向量緩衝區的指標直接傳遞給擬合函式:
gsl_fit_linear(x.data(), 1, y.data(), 1, entries_number,
&intercept, &slope,
&cov00, &cov01, &cov11, &chi_squared);
const double r_value = gsl_stats_correlation(x.data(), 1, y.data(), 1, entries_number);
std::cout << "Slope: " << slope << std::endl;
std::cout << "Intercept: " << intercept << std::endl;
std::cout << "Correlation coefficient: " << r_value << std::endl;
繪圖
使用與以前相同的方法進行繪圖。 寫入檔案:
const double step_x = ((max_x + 1) - (min_x - 1)) / N;
for (unsigned int i = 0; i < N; i += 1) {
const double current_x = (min_x - 1) + step_x * i;
const double current_y = intercept + slope * current_x;
output_file << current_x << "\t" << current_y << std::endl;
}
output_file.close();
然後使用 Gnuplot 進行繪圖。
結果
在執行程式之前,必須使用類似的命令對其進行編譯:
clang++ -std=c++11 -I/usr/include/ fitting_Cpp11.cpp -L/usr/lib/ -L/usr/lib64/ -lgsl -lgslcblas -o fitting_Cpp11
命令列上的結果輸出為:
#### Anscombe's first set with C++11 ####
Slope: 0.500091
Intercept: 3.00009
Correlation coefficient: 0.816421
這就是用 Gnuplot 生成的結果影象:
結論
本文提供了用 C99 和 C++11 編寫的資料擬合和繪圖任務的示例。由於 C++ 在很大程度上與 C 相容,因此本文利用了它們的相似性來編寫了第二個示例。在某些方面,C++ 更易於使用,因為它部分減輕了顯式管理記憶體的負擔。但是其語法更加複雜,因為它引入了為 OOP 編寫類的可能性。但是,仍然可以用 C 使用 OOP 方法編寫軟體。由於 OOP 是一種程式設計風格,因此可以在任何語言中使用。在 C 中有一些很好的 OOP 示例,例如 GObject 和 Jansson庫。
對於數字運算,我更喜歡在 C99 中進行,因為它的語法更簡單並且得到了廣泛的支援。直到最近,C++11 還沒有得到廣泛的支援,我傾向於避免使用先前版本中的粗糙不足之處。對於更復雜的軟體,C++ 可能是一個不錯的選擇。
你是否也將 C 或 C++ 用於資料科學?在評論中分享你的經驗。
via: https://opensource.com/article/20/2/c-data-science
作者:Cristiano L. Fontana 選題:lujun9972 譯者:wxy 校對:wxy
訂閱“Linux 中國”官方小程式來檢視
相關文章
- C與C++在函式和資料的比較C++函式
- (資料科學學習手札91)在Python中妥善使用進度條資料科學Python
- (資料科學學習手札161)高效能資料分析利器DuckDB在Python中的使用資料科學Python
- Notebook在復現資料科學研究成果中的絲滑使用資料科學
- 在 Fedora 上搭建 Jupyter 和資料科學環境資料科學
- 在資料科學方面,python和R有何區別?資料科學Python
- Accelerated C++學習筆記--組織程式和資料C++筆記
- (資料科學學習手札125)在Python中操縱json資料的最佳方式資料科學PythonJSON
- C++中的&和&&C++
- C和C++中的staticC++
- (資料科學學習手札96)在geopandas中疊加線上地圖資料科學地圖
- 2021年最新整理, C++ 學習資料[持續更新中]C++
- CPDA資料分析師:為什麼Python在資料科學方面超越R和SQL?Python資料科學SQL
- c++ 中.h 和.cppC++
- 資料科學資料科學
- 足球比賽中的資料科學資料科學
- C++中常資料的使用及初始化C++
- C++ Vector資料插入C++
- C++資料型別C++資料型別
- C++ 中的 volatile 和 atomicC++
- C++中&和*的含義C++
- C++中的NULL和nullptrC++Null
- C++資料結構和pb資料結構的轉換C++資料結構
- (資料科學學習手札160)使用miniforge代替miniconda資料科學
- C++之父談C++ :一天之內你就能學會出色使用C++C++
- C++基礎學習2-資料型別C++資料型別
- C和C++中的名字空間和作用域C++
- 【PAT乙級、C++】1024 科學計數法 (20分)C++
- 在 C++ 中捕獲 Python 異常C++Python
- C# 左移右移在資料型別轉換中的使用C#資料型別
- c++和python先學哪個?C++Python
- [譯]在CUDA C/C++中使用共享儲存器C++
- c/c++資料對齊問題C++
- 聊聊 C++ 和 C# 中的 lambda 玩法C++C#
- C/C++中的實參和形參C++
- C++編譯SQLite資料庫以及如何使用加密資料庫SQLCipherC++編譯SQLite資料庫加密
- 學會在 C++ 中使用變數:從定義到實踐C++變數
- C++基礎資料二C++