在数据科学中使用 C 和 C++

大数据 后端
我在一篇涉及 Python 和 GNU Octave 的文章中写了我不断学习编程语言的动机,值得大家回顾。这里所有的程序都需要在命令行上运行,而不是在图形用户界面(GUI)上运行。完整的示例可在 polyglot_fit 存储库中找到。

让我们使用 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++

C 语言的方式

C 语言是通用编程语言,是当今使用最广泛的语言之一(依据 TIOBE 指数、RedMonk 编程语言排名、编程语言流行度指数和 GitHub Octoverse 状态 得来)。这是一种相当古老的语言(大约诞生在 1973 年),并且用它编写了许多成功的程序(例如 Linux 内核和 Git 仅是其中的两个例子)。它也是最接近计算机内部运行机制的语言之一,因为它直接用于操作内存。它是一种编译语言;因此,源代码必须由编译器转换为机器代码。它的标准库很小,功能也不多,因此人们开发了其它库来提供缺少的功能。

我最常在数字运算中使用该语言,主要是因为其性能。我觉得使用起来很繁琐,因为它需要很多样板代码,但是它在各种环境中都得到了很好的支持。C99 标准是最新版本,增加了一些漂亮的功能,并且得到了编译器的良好支持。

我将一路介绍 C 和 C++ 编程的必要背景,以便初学者和高级用户都可以继续学习。

安装

要使用 C99 进行开发,你需要一个编译器。我通常使用 Clang,不过 GCC 是另一个有效的开源编译器。对于线性拟合,我选择使用 GNU 科学库。对于绘图,我找不到任何明智的库,因此该程序依赖于外部程序:Gnuplot。该示例还使用动态数据结构来存储数据,该结构在伯克利软件分发版(BSD)中定义。

在 Fedora 中安装很容易:

  1. sudo dnf install clang gnuplot gsl gsl-devel 

代码注释

在 C99 中,注释的格式是在行的开头放置 //,行的其它部分将被解释器丢弃。另外,/* 和 */ 之间的任何内容也将被丢弃。

  1. // 这是一个注释,会被解释器忽略 
  2. /* 这也被忽略 */ 

必要的库

库由两部分组成:

  • 头文件,其中包含函数说明
  • 包含函数定义的源文件

头文件包含在源文件中,而库文件的源文件则链接到可执行文件。因此,此示例所需的头文件是:

  1. // 输入/输出功能 
  2. #include <stdio.h> 
  3. // 标准库 
  4. #include <stdlib.h> 
  5. // 字符串操作功能 
  6. #include <string.h> 
  7. // BSD 队列 
  8. #include <sys/queue.h> 
  9. // GSL 科学功能 
  10. #include <gsl/gsl_fit.h> 
  11. #include <gsl/gsl_statistics_double.h> 

主函数

在 C 语言中,程序必须位于称为主函数 main() 的特殊函数内:

  1. int main(void) { 
  2.     ... 

这与上一教程中介绍的 Python 不同,后者将运行在源文件中找到的所有代码。

定义变量

在 C 语言中,变量必须在使用前声明,并且必须与类型关联。每当你要使用变量时,都必须决定要在其中存储哪种数据。你也可以指定是否打算将变量用作常量值,这不是必需的,但是编译器可以从此信息中受益。 以下来自存储库中的 fitting_C99.c 程序:

  1. const char *input_file_name = "anscombe.csv"
  2. const char *delimiter = "\t"
  3. const unsigned int skip_header = 3; 
  4. const unsigned int column_x = 0; 
  5. const unsigned int column_y = 1; 
  6. const char *output_file_name = "fit_C99.csv"
  7. const unsigned int N = 100; 

C 语言中的数组不是动态的,从某种意义上说,数组的长度必须事先确定(即,在编译之前):

  1. int data_array[1024]; 

由于你通常不知道文件中有多少个数据点,因此请使用单链列表。这是一个动态数据结构,可以无限增长。幸运的是,BSD 提供了链表。这是一个示例定义:

  1. struct data_point { 
  2.     double x; 
  3.     double y; 
  4.     SLIST_ENTRY(data_point) entries; 
  5. }; 
  6. SLIST_HEAD(data_list, data_point) head = SLIST_HEAD_INITIALIZER(head); 
  7. SLIST_INIT(&head); 

该示例定义了一个由结构化值组成的 data_point 列表,该结构化值同时包含 x 值和 y 值。语法相当复杂,但是很直观,详细描述它就会太冗长了。

打印输出

要在终端上打印,可以使用 printf() 函数,其功能类似于 Octave 的 printf() 函数(在第一篇文章中介绍):

  1. printf("#### Anscombe's first set with C99 ####\n"); 

printf() 函数不会在打印字符串的末尾自动添加换行符,因此你必须添加换行符。第一个参数是一个字符串,可以包含传递给函数的其他参数的格式信息,例如:

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

读取数据

现在来到了困难的部分……有一些用 C 语言解析 CSV 文件的库,但是似乎没有一个库足够稳定或流行到可以放入到 Fedora 软件包存储库中。我没有为本教程添加依赖项,而是决定自己编写此部分。同样,讨论这些细节太啰嗦了,所以我只会解释大致的思路。为了简洁起见,将忽略源代码中的某些行,但是你可以在存储库中找到完整的示例代码。

首先,打开输入文件:

  1. FILE* input_file = fopen(input_file_name, "r"); 

然后逐行读取文件,直到出现错误或文件结束:

  1. while (!ferror(input_file) && !feof(input_file)) { 
  2.     size_t buffer_size = 0; 
  3.     char *buffer = NULL
  4.     
  5.     getline(&buffer, &buffer_size, input_file); 
  6.     ... 

getline() 函数是 POSIX.1-2008 标准新增的一个不错的函数。它可以读取文件中的整行,并负责分配必要的内存。然后使用 strtok() 函数将每一行分成字元token。遍历字元,选择所需的列:

  1. char *token = strtok(buffer, delimiter); 
  2. while (token != NULL
  3.     double value; 
  4.     sscanf(token, "%lf", &value); 
  5.     if (column == column_x) { 
  6.         x = value; 
  7.     } else if (column == column_y) { 
  8.         y = value; 
  9.     } 
  10.     column += 1; 
  11.     token = strtok(NULL, delimiter); 

最后,当选择了 x 和 y 值时,将新数据点插入链表中:

  1. struct data_point *datum = malloc(sizeof(struct data_point)); 
  2. datum->x = x; 
  3. datum->y = y; 
  4. SLIST_INSERT_HEAD(&head, datum, entries); 

malloc() 函数为新数据点动态分配(保留)一些持久性内存。

拟合数据

GSL 线性拟合函数 gslfitlinear() 期望其输入为简单数组。因此,由于你将不知道要创建的数组的大小,因此必须手动分配它们的内存:

  1. const size_t entries_number = row - skip_header - 1; 
  2. double *x = malloc(sizeof(double) * entries_number); 
  3. double *y = malloc(sizeof(double) * entries_number); 

然后,遍历链表以将相关数据保存到数组:

  1. SLIST_FOREACH(datum, &head, entries) { 
  2.     const double current_x = datum->x; 
  3.     const double current_y = datum->y; 
  4.     x[i] = current_x; 
  5.     y[i] = current_y; 
  6.     i += 1; 

现在你已经处理完了链表,请清理它。要总是释放已手动分配的内存,以防止内存泄漏。内存泄漏是糟糕的、糟糕的、糟糕的(重要的话说三遍)。每次内存没有释放时,花园侏儒都会找不到自己的头:

  1. while (!SLIST_EMPTY(&head)) { 
  2.     struct data_point *datum = SLIST_FIRST(&head); 
  3.     SLIST_REMOVE_HEAD(&head, entries); 
  4.     free(datum); 

终于,终于!你可以拟合你的数据了:

  1. gsl_fit_linear(x, 1, y, 1, entries_number, 
  2.                &intercept, &slope, 
  3.                &cov00, &cov01, &cov11, &chi_squared); 
  4. const double r_value = gsl_stats_correlation(x, 1, y, 1, entries_number); 
  5. printf("Slope: %f\n", slope); 
  6. printf("Intercept: %f\n", intercept); 
  7. printf("Correlation coefficient: %f\n", r_value); 

绘图

你必须使用外部程序进行绘图。因此,将拟合数据保存到外部文件:

  1. const double step_x = ((max_x + 1) - (min_x - 1)) / N; 
  2. for (unsigned int i = 0; i < N; i += 1) { 
  3.     const double current_x = (min_x - 1) + step_x * i; 
  4.     const double current_y = intercept + slope * current_x; 
  5.     fprintf(output_file, "%f\t%f\n", current_x, current_y); 

用于绘制两个文件的 Gnuplot 命令是:

  1. plot 'fit_C99.csv' using 1:2 with lines title 'Fit''anscombe.csv' using 1:2 with points pointtype 7 title 'Data' 

结果

在运行程序之前,你必须编译它:

  1. 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。命令行上的结果输出为:

  1. #### Anscombe's first set with C99 #### 
  2. Slope: 0.500091 
  3. Intercept: 3.000091 
  4. Correlation coefficient: 0.816421 

这是用 Gnuplot 生成的结果图像:

在数据科学中使用 C 和 C++

C++11 方式

C++ 语言是一种通用编程语言,也是当今使用的最受欢迎的语言之一。它是作为 C 的继承人创建的(诞生于 1983 年),重点是面向对象程序设计(OOP)。C++ 通常被视为 C 的超集,因此 C 程序应该能够使用 C++ 编译器进行编译。这并非完全正确,因为在某些极端情况下它们的行为有所不同。 根据我的经验,C++ 与 C 相比需要更少的样板代码,但是如果要进行面向对象开发,语法会更困难。C++11 标准是最新版本,增加了一些漂亮的功能,并且基本上得到了编译器的支持。

由于 C++ 在很大程度上与 C 兼容,因此我将仅强调两者之间的区别。我在本部分中没有涵盖的任何部分,则意味着它与 C 中的相同。

安装

这个 C++ 示例的依赖项与 C 示例相同。 在 Fedora 上,运行:

  1. sudo dnf install clang gnuplot gsl gsl-devel 

必要的库

库的工作方式与 C 语言相同,但是 include 指令略有不同:

  1. #include <cstdlib> 
  2. #include <cstring> 
  3. #include <iostream> 
  4. #include <fstream> 
  5. #include <string> 
  6. #include <vector> 
  7. #include <algorithm> 
  8. extern "C" { 
  9. #include <gsl/gsl_fit.h> 
  10. #include <gsl/gsl_statistics_double.h> 

由于 GSL 库是用 C 编写的,因此你必须将这个特殊情况告知编译器。

定义变量

与 C 语言相比,C++ 支持更多的数据类型(类),例如,与其 C 语言版本相比,string 类型具有更多的功能。相应地更新变量的定义:

  1. const std::string input_file_name("anscombe.csv"); 

对于字符串之类的结构化对象,你可以定义变量而无需使用 = 符号。

打印输出

你可以使用 printf() 函数,但是 cout 对象更惯用。使用运算符 << 来指示要使用 cout 打印的字符串(或对象):

  1. std::cout << "#### Anscombe's first set with C++11 ####" << std::endl; 
  2. ... 
  3. std::cout << "Slope: " << slope << std::endl; 
  4. std::cout << "Intercept: " << intercept << std::endl; 
  5. std::cout << "Correlation coefficient: " << r_value << std::endl; 

读取数据

该方案与以前相同。将打开文件并逐行读取文件,但语法不同:

  1. std::ifstream input_file(input_file_name); 
  2. while (input_file.good()) { 
  3.     std::string line; 
  4.     getline(input_file, line); 
  5.     ... 

使用与 C99 示例相同的功能提取行字元。代替使用标准的 C 数组,而是使用两个向量。向量是 C++ 标准库中对 C 数组的扩展,它允许动态管理内存而无需显式调用 malloc():

  1. std::vector<double> x; 
  2. std::vector<double> y; 
  3. // Adding an element to x and y: 
  4. x.emplace_back(value); 
  5. y.emplace_back(value); 

拟合数据

要在 C++ 中拟合,你不必遍历列表,因为向量可以保证具有连续的内存。你可以将向量缓冲区的指针直接传递给拟合函数:

  1. gsl_fit_linear(x.data(), 1, y.data(), 1, entries_number, 
  2.                &intercept, &slope, 
  3.                &cov00, &cov01, &cov11, &chi_squared); 
  4. const double r_value = gsl_stats_correlation(x.data(), 1, y.data(), 1, entries_number); 
  5. std::cout << "Slope: " << slope << std::endl; 
  6. std::cout << "Intercept: " << intercept << std::endl; 
  7. std::cout << "Correlation coefficient: " << r_value << std::endl; 

绘图

使用与以前相同的方法进行绘图。 写入文件:

  1. const double step_x = ((max_x + 1) - (min_x - 1)) / N; 
  2. for (unsigned int i = 0; i < N; i += 1) { 
  3.     const double current_x = (min_x - 1) + step_x * i; 
  4.     const double current_y = intercept + slope * current_x; 
  5.     output_file << current_x << "\t" << current_y << std::endl; 
  6. output_file.close(); 

然后使用 Gnuplot 进行绘图。

结果

在运行程序之前,必须使用类似的命令对其进行编译:

  1. clang++ -std=c++11 -I/usr/include/ fitting_Cpp11.cpp -L/usr/lib/ -L/usr/lib64/ -lgsl -lgslcblas -o fitting_Cpp11 

命令行上的结果输出为:

  1. #### Anscombe's first set with C++11 #### 
  2. Slope: 0.500091 
  3. Intercept: 3.00009 
  4. Correlation coefficient: 0.816421 

这就是用 Gnuplot 生成的结果图像:

在数据科学中使用 C 和 C++

结论

本文提供了用 C99 和 C++11 编写的数据拟合和绘图任务的示例。由于 C++ 在很大程度上与 C 兼容,因此本文利用了它们的相似性来编写了第二个示例。在某些方面,C++ 更易于使用,因为它部分减轻了显式管理内存的负担。但是其语法更加复杂,因为它引入了为 OOP 编写类的可能性。但是,仍然可以用 C 使用 OOP 方法编写软件。由于 OOP 是一种编程风格,因此可以在任何语言中使用。在 C 中有一些很好的 OOP 示例,例如 GObject 和 Jansson库。

对于数字运算,我更喜欢在 C99 中进行,因为它的语法更简单并且得到了广泛的支持。直到最近,C++11 还没有得到广泛的支持,我倾向于避免使用先前版本中的粗糙不足之处。对于更复杂的软件,C++ 可能是一个不错的选择。

你是否也将 C 或 C++ 用于数据科学?在评论中分享你的经验。

责任编辑:未丽燕 来源: Linux.cn
相关推荐

2011-05-12 18:14:29

算法

2011-05-18 18:05:47

C#C++

2011-05-17 16:20:46

C++

2011-03-30 10:41:11

C++数据库

2011-05-18 17:56:38

C#C++

2012-08-20 10:43:50

IBMdW

2016-08-31 16:39:59

PythonRC++

2009-04-14 14:53:06

C++Lambda函数多线程

2010-01-26 15:51:06

C++变量

2020-08-18 08:09:55

Docker容器工具

2020-07-30 12:40:35

CC++编程语言

2011-05-19 09:53:33

数据库对象

2009-08-19 10:09:21

C#和C++

2011-04-11 09:43:25

C++C

2010-01-27 15:58:35

C++数据结构

2021-02-26 10:41:59

C++程序员代码

2021-01-22 05:53:08

C# IndexRange

2010-01-20 09:54:27

C++数据类型

2011-07-14 17:45:06

CC++

2009-09-04 17:34:11

C#CC++
点赞
收藏

51CTO技术栈公众号