Cristiano L. Fontana 发布的文章

通过向一批图像添加效果来学习 GIMP 的脚本语言 Script-Fu。

 title=

前一段时间,我想给方程图片加一个黑板式的外观。我开始是使用 GIMP 来处理的,我对结果很满意。问题是我必须对图像执行几个操作,当我想再次使用此样式,不想对所有图像重复这些步骤。此外,我确信我会很快忘记这些步骤。

 title=

傅立叶变换方程式(Cristiano Fontana,[CC BY-SA 4.0] 4

GIMP 是一个很棒的开源图像编辑器。尽管我已经使用了多年,但从未研究过其批处理功能或 Script-Fu 菜单。这是探索它们的绝好机会。

什么是 Script-Fu?

Script-Fu 是 GIMP 内置的脚本语言。是一种基于 Scheme) 的编程语言。如果你从未使用过 Scheme,请尝试一下,因为它可能非常有用。我认为 Script-Fu 是一个很好的入门方法,因为它对图像处理具有立竿见影的效果,所以你可以很快感觉到自己的工作效率的提高。你也可以使用 Python 编写脚本,但是 Script-Fu 是默认选项。

为了帮助你熟悉 Scheme,GIMP 的文档提供了深入的 教程。Scheme 是一种类似于 Lisp 的语言,因此它的主要特征是使用 前缀 表示法和 许多括号。函数和运算符通过前缀应用到操作数列表中:

(函数名 操作数 操作数 ...)

(+ 2 3)
↳ 返回 5

(list 1 2 3 5)
↳ 返回一个列表,包含 1、 2、 3 和 5

我花了一些时间才找到完整的 GIMP 函数列表文档,但实际上很简单。在 Help 菜单中,有一个 Procedure Browser,其中包含所有可用的函数的丰富详尽文档。

 title=

使用 GIMP 的批处理模式

你可以使用 -b 选项以批处理的方式启动 GIMP。-b 选项的参数可以是你想要运行的脚本,或者用一个 - 来让 GIMP 进入交互模式而不是命令行模式。正常情况下,当你启动 GIMP 的时候,它会启动图形界面,但是你可以使用 -i 选项来禁用它。

开始编写你的第一个脚本

创建一个名为 chalk.scm 的文件,并把它保存在 Preferences 窗口中 Folders 选项下的 Script 中指定的 script 文件夹下。就我而言,是在 $HOME/.config/GIMP/2.10/scripts

chalk.scm 文件中,写入下面的内容:

(define (chalk filename grow-pixels spread-amount percentage)
   (let* ((image (car (gimp-file-load RUN-NONINTERACTIVE filename filename)))
          (drawable (car (gimp-image-get-active-layer image)))
          (new-filename (string-append "modified_" filename)))
     (gimp-image-select-color image CHANNEL-OP-REPLACE drawable '(0 0 0))
     (gimp-selection-grow image grow-pixels)
     (gimp-context-set-foreground '(0 0 0))
     (gimp-edit-bucket-fill drawable BUCKET-FILL-FG LAYER-MODE-NORMAL 100 255 TRUE 0 0)
     (gimp-selection-none image)
     (plug-in-spread RUN-NONINTERACTIVE image drawable spread-amount spread-amount)
     (gimp-drawable-invert drawable TRUE)
     (plug-in-randomize-hurl RUN-NONINTERACTIVE image drawable percentage 1 TRUE 0)
     (gimp-file-save RUN-NONINTERACTIVE image drawable new-filename new-filename)
     (gimp-image-delete image)))

定义脚本变量

在脚本中, (define (chalk filename grow-pixels spread-amound percentage) ...) 函数定义了一个名叫 chalk 的新函数。它的函数参数是 filenamegrow-pixelsspread-amoundpercentage。在 define 中的所有内容都是 chalk 函数的主体。你可能已经注意到,那些名字比较长的变量中间都有一个破折号来分割。这是类 Lisp 语言的惯用风格。

(let* ...) 函数是一个特殊 过程 procedure ,可以让你定义一些只有在这个函数体中才有效的临时变量。临时变量有 imagedrawable 以及 new-filename。它使用 gimp-file-load 来载入图片,这会返回它所包含的图片的一个列表。并通过 car 函数来选取第一项。然后,它选择第一个活动层并将其引用存储在 drawable 变量中。最后,它定义了包含图像新文件名的字符串。

为了帮助你更好地了解该过程,我将对其进行分解。首先,启动带 GUI 的 GIMP,然后你可以通过依次点击 Filters → Script-Fu → Console 来打开 Script-Fu 控制台。 在这种情况下,不能使用 let *,因为变量必须是持久的。使用 define 函数定义 image 变量,并为其提供查找图像的正确路径:

(define image (car (gimp-file-load RUN-NONINTERACTIVE "Fourier.png" "Fourier.png")))

似乎在 GUI 中什么也没有发生,但是图像已加载。 你需要通过以下方式来让图像显示:

(gimp-display-new image)

 title=

现在,获取活动层并将其存储在 drawable 变量中:

(define drawable (car (gimp-image-get-active-layer image)))

最后,定义图像的新文件名:

(define new-filename "modified_Fourier.png")

运行命令后,你将在 Script-Fu 控制台中看到以下内容:

 title=

在对图像执行操作之前,需要定义将在脚本中作为函数参数的变量:

(define grow-pixels 2)
(define spread-amount 4)
(define percentage 3)

处理图片

现在,所有相关变量都已定义,你可以对图像进行操作了。 脚本的操作可以直接在控制台上执行。第一步是在活动层上选择黑色。颜色被写成一个由三个数字组成的列表,即 (list 0 0 0) 或者是 '(0 0 0):

(gimp-image-select-color image CHANNEL-OP-REPLACE drawable '(0 0 0))

 title=

扩大选取两个像素:

(gimp-selection-grow image grow-pixels)

 title=

将前景色设置为黑色,并用它填充选区:

(gimp-context-set-foreground '(0 0 0))
(gimp-edit-bucket-fill drawable BUCKET-FILL-FG LAYER-MODE-NORMAL 100 255 TRUE 0 0)

 title=

删除选区:

(gimp-selection-none image)

 title=

随机移动像素:

(plug-in-spread RUN-NONINTERACTIVE image drawable spread-amount spread-amount)

 title=

反转图像颜色:

(gimp-drawable-invert drawable TRUE)

 title=

随机化像素:

(plug-in-randomize-hurl RUN-NONINTERACTIVE image drawable percentage 1 TRUE 0)

 title=

将图像保存到新文件:

(gimp-file-save RUN-NONINTERACTIVE image drawable new-filename new-filename)

 title=

傅立叶变换方程 (Cristiano Fontana, CC BY-SA 4.0)

以批处理模式运行脚本

现在你知道了脚本的功能,可以在批处理模式下运行它:

gimp -i -b '(chalk "Fourier.png" 2 4 3)' -b '(gimp-quit 0)'

在运行 chalk 函数之后,它将使用 -b 选项调用第二个函数 gimp-quit 来告诉 GIMP 退出。

了解更多

本教程向你展示了如何开始使用 GIMP 的内置脚本功能,并介绍了 GIMP 的 Scheme 实现:Script-Fu。如果你想继续前进,建议你查看官方文档及其入门教程。如果你不熟悉 Scheme 或 Lisp,那么一开始的语法可能有点吓人,但我还是建议你尝试一下。这可能是一个不错的惊喜。


via: https://opensource.com/article/21/1/gimp-scripting

作者:Cristiano L. Fontana 选题:lujun9972 译者:amwps290 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

ZeroMQ 是一个快速灵活的消息库,用于数据收集和不同编程语言间的数据共享。

作为软件工程师,我有多次在要求完成指定任务时感到浑身一冷的经历。其中有一次,我必须在一些新的硬件基础设施和云基础设施之间写一个接口,这些硬件需要 C 语言,而云基础设施主要是用 Python。

实现的方式之一是 用 C 写扩展模块,Python 支持 C 扩展的调用。快速浏览文档后发现,这需要编写大量的 C 代码。这样做的话,在有些情况下效果还不错,但不是我喜欢的方式。另一种方式就是将两个任务放在不同的进程中,并使用 ZeroMQ 消息库 在两者之间交换消息。

在发现 ZeroMQ 之前,遇到这种类型的情况时,我选择了编写扩展的方式。这种方式不算太差,但非常费时费力。如今,为了避免那些问题,我将一个系统细分为独立的进程,通过 通信套接字 发送消息来交换信息。这样,不同的编程语言可以共存,每个进程也变简单了,同时也容易调试。

ZeroMQ 提供了一个更简单的过程:

  1. 编写一小段 C 代码,从硬件读取数据,然后把发现的东西作为消息发送出去。
  2. 使用 Python 编写接口,实现新旧基础设施之间的对接。

Pieter Hintjens 是 ZeroMQ 项目发起者之一,他是个拥有 有趣视角和作品 的非凡人物。

准备

本教程中,需要:

Fedora 系统上的安装方法:

$ dnf install clang zeromq zeromq-devel python3 python3-zmq

Debian 和 Ubuntu 系统上的安装方法:

$ apt-get install clang libzmq5 libzmq3-dev python3 python3-zmq

如果有问题,参考对应项目的安装指南(上面附有链接)。

编写硬件接口库

因为这里针对的是个设想的场景,本教程虚构了包含两个函数的操作库:

  • fancyhw_init() 用来初始化(设想的)硬件
  • fancyhw_read_val() 用于返回从硬件读取的数据

将库的完整代码保存到文件 libfancyhw.h 中:

#ifndef LIBFANCYHW_H
#define LIBFANCYHW_H

#include <stdlib.h>
#include <stdint.h>

// This is the fictitious hardware interfacing library

void fancyhw_init(unsigned int init_param)
{
    srand(init_param);
}

int16_t fancyhw_read_val(void)
{
    return (int16_t)rand();
}

#endif

这个库可以模拟你要在不同语言实现的组件间交换的数据,中间有个随机数发生器。

设计 C 接口

下面从包含管理数据传输的库开始,逐步实现 C 接口。

需要的库

开始先加载必要的库(每个库的作用见代码注释):

// For printf()
#include <stdio.h>
// For EXIT_*
#include <stdlib.h>
// For memcpy()
#include <string.h>
// For sleep()
#include <unistd.h>

#include <zmq.h>

#include "libfancyhw.h"

必要的参数

定义 main 函数和后续过程中必要的参数:

int main(void)
{
    const unsigned int INIT_PARAM = 12345;
    const unsigned int REPETITIONS = 10;
    const unsigned int PACKET_SIZE = 16;
    const char *TOPIC = "fancyhw_data";

    ...

初始化

所有的库都需要初始化。虚构的那个只需要一个参数:

fancyhw_init(INIT_PARAM);

ZeroMQ 库需要实打实的初始化。首先,定义对象 context,它是用来管理全部的套接字的:

void *context = zmq_ctx_new();

if (!context)
{
    printf("ERROR: ZeroMQ error occurred during zmq_ctx_new(): %s\n", zmq_strerror(errno));

    return EXIT_FAILURE;
}

之后定义用来发送数据的套接字。ZeroMQ 支持若干种套接字,各有其用。使用 publish 套接字(也叫 PUB 套接字),可以复制消息并分发到多个接收端。这使得你可以让多个接收端接收同一个消息。没有接收者的消息将被丢弃(即不会入消息队列)。用法如下:

void *data_socket = zmq_socket(context, ZMQ_PUB);

套接字需要绑定到一个具体的地址,这样客户端就知道要连接哪里了。本例中,使用了 TCP 传输层(当然也有 其它选项,但 TCP 是不错的默认选择):

const int rb = zmq_bind(data_socket, "tcp://*:5555");

if (rb != 0)
{
    printf("ERROR: ZeroMQ error occurred during zmq_ctx_new(): %s\n", zmq_strerror(errno));

    return EXIT_FAILURE;
}

下一步, 计算一些后续要用到的值。 注意下面代码中的 TOPIC,因为 PUB 套接字发送的消息需要绑定一个主题。主题用于供接收者过滤消息:

const size_t topic_size = strlen(TOPIC);
const size_t envelope_size = topic_size + 1 + PACKET_SIZE * sizeof(int16_t);

printf("Topic: %s; topic size: %zu; Envelope size: %zu\n", TOPIC, topic_size, envelope_size);

发送消息

启动一个发送消息的循环,循环 REPETITIONS 次:

for (unsigned int i = 0; i < REPETITIONS; i++)
{
    ...

发送消息前,先填充一个长度为 PACKET_SIZE 的缓冲区。本库提供的是 16 个位的有符号整数。因为 C 语言中 int 类型占用空间大小与平台相关,不是确定的值,所以要使用指定宽度的 int 变量:

int16_t buffer[PACKET_SIZE];

for (unsigned int j = 0; j < PACKET_SIZE; j++)
{
    buffer[j] = fancyhw_read_val();
}

printf("Read %u data values\n", PACKET_SIZE);

消息的准备和发送的第一步是创建 ZeroMQ 消息,为消息分配必要的内存空间。空白的消息是用于封装要发送的数据的:

zmq_msg_t envelope;

const int rmi = zmq_msg_init_size(&envelope, envelope_size);
if (rmi != 0)
{
    printf("ERROR: ZeroMQ error occurred during zmq_msg_init_size(): %s\n", zmq_strerror(errno));

    zmq_msg_close(&envelope);

    break;
}

现在内存空间已分配,数据保存在 ZeroMQ 消息 “信封”中。函数 zmq_msg_data() 返回一个指向封装数据缓存区顶端的指针。第一部分是主题,之后是一个空格,最后是二进制数。主题和二进制数据之间的分隔符采用空格字符。需要遍历缓存区的话,使用类型转换和 指针算法。(感谢 C 语言,让事情变得直截了当。)做法如下:

memcpy(zmq_msg_data(&envelope), TOPIC, topic_size);
memcpy((void*)((char*)zmq_msg_data(&envelope) + topic_size), " ", 1);
memcpy((void*)((char*)zmq_msg_data(&envelope) + 1 + topic_size), buffer, PACKET_SIZE * sizeof(int16_t))

通过 data_socket 发送消息:

const size_t rs = zmq_msg_send(&envelope, data_socket, 0);
if (rs != envelope_size)
{
    printf("ERROR: ZeroMQ error occurred during zmq_msg_send(): %s\n", zmq_strerror(errno));

    zmq_msg_close(&envelope);

    break;
}

使用数据之前要先解除封装:

zmq_msg_close(&envelope);

printf("Message sent; i: %u, topic: %s\n", i, TOPIC);

清理

C 语言不提供 垃圾收集) 功能,用完之后记得要自己扫尾。发送消息之后结束程序之前,需要运行扫尾代码,释放分配的内存:

const int rc = zmq_close(data_socket);

if (rc != 0)
{
    printf("ERROR: ZeroMQ error occurred during zmq_close(): %s\n", zmq_strerror(errno));

    return EXIT_FAILURE;
}

const int rd = zmq_ctx_destroy(context);

if (rd != 0)
{
    printf("Error occurred during zmq_ctx_destroy(): %s\n", zmq_strerror(errno));

    return EXIT_FAILURE;
}

return EXIT_SUCCESS;

完整 C 代码

保存下面完整的接口代码到本地名为 hw_interface.c 的文件:

// For printf()
#include <stdio.h>
// For EXIT_*
#include <stdlib.h>
// For memcpy()
#include <string.h>
// For sleep()
#include <unistd.h>

#include <zmq.h>

#include "libfancyhw.h"

int main(void)
{
    const unsigned int INIT_PARAM = 12345;
    const unsigned int REPETITIONS = 10;
    const unsigned int PACKET_SIZE = 16;
    const char *TOPIC = "fancyhw_data";

    fancyhw_init(INIT_PARAM);

    void *context = zmq_ctx_new();

    if (!context)
    {
        printf("ERROR: ZeroMQ error occurred during zmq_ctx_new(): %s\n", zmq_strerror(errno));

        return EXIT_FAILURE;
    }

    void *data_socket = zmq_socket(context, ZMQ_PUB);

    const int rb = zmq_bind(data_socket, "tcp://*:5555");

    if (rb != 0)
    {
        printf("ERROR: ZeroMQ error occurred during zmq_ctx_new(): %s\n", zmq_strerror(errno));

        return EXIT_FAILURE;
    }

    const size_t topic_size = strlen(TOPIC);
    const size_t envelope_size = topic_size + 1 + PACKET_SIZE * sizeof(int16_t);

    printf("Topic: %s; topic size: %zu; Envelope size: %zu\n", TOPIC, topic_size, envelope_size);

    for (unsigned int i = 0; i < REPETITIONS; i++)
    {
        int16_t buffer[PACKET_SIZE];

        for (unsigned int j = 0; j < PACKET_SIZE; j++)
        {
            buffer[j] = fancyhw_read_val();
        }

        printf("Read %u data values\n", PACKET_SIZE);

        zmq_msg_t envelope;
   
        const int rmi = zmq_msg_init_size(&envelope, envelope_size);
        if (rmi != 0)
        {
            printf("ERROR: ZeroMQ error occurred during zmq_msg_init_size(): %s\n", zmq_strerror(errno));
   
            zmq_msg_close(&envelope);
   
            break;
        }
       
        memcpy(zmq_msg_data(&envelope), TOPIC, topic_size);

        memcpy((void*)((char*)zmq_msg_data(&envelope) + topic_size), " ", 1);

        memcpy((void*)((char*)zmq_msg_data(&envelope) + 1 + topic_size), buffer, PACKET_SIZE * sizeof(int16_t));
   
        const size_t rs = zmq_msg_send(&envelope, data_socket, 0);
        if (rs != envelope_size)
        {
            printf("ERROR: ZeroMQ error occurred during zmq_msg_send(): %s\n", zmq_strerror(errno));
   
            zmq_msg_close(&envelope);
   
            break;
        }
   
        zmq_msg_close(&envelope);

        printf("Message sent; i: %u, topic: %s\n", i, TOPIC);

        sleep(1);
    }

    const int rc = zmq_close(data_socket);

    if (rc != 0)
    {
        printf("ERROR: ZeroMQ error occurred during zmq_close(): %s\n", zmq_strerror(errno));

        return EXIT_FAILURE;
    }

    const int rd = zmq_ctx_destroy(context);

    if (rd != 0)
    {
        printf("Error occurred during zmq_ctx_destroy(): %s\n", zmq_strerror(errno));

        return EXIT_FAILURE;
    }

    return EXIT_SUCCESS;
}

用如下命令编译:

$ clang -std=c99 -I. hw_interface.c -lzmq -o hw_interface

如果没有编译错误,你就可以运行这个接口了。贴心的是,ZeroMQ PUB 套接字可以在没有任何应用发送或接受数据的状态下运行,这简化了使用复杂度,因为这样不限制进程启动的次序。

运行该接口:

$ ./hw_interface
Topic: fancyhw_data; topic size: 12; Envelope size: 45
Read 16 data values
Message sent; i: 0, topic: fancyhw_data
Read 16 data values
Message sent; i: 1, topic: fancyhw_data
Read 16 data values
...
...

输出显示数据已经通过 ZeroMQ 完成发送,现在要做的是让一个程序去读数据。

编写 Python 数据处理器

现在已经准备好从 C 程序向 Python 应用传送数据了。

需要两个库帮助实现数据传输。首先是 ZeroMQ 的 Python 封装:

$ python3 -m pip install zmq

另一个就是 struct 库,用于解码二进制数据。这个库是 Python 标准库的一部分,所以不需要使用 pip 命令安装。

Python 程序的第一部分是导入这些库:

import zmq
import struct

重要参数

使用 ZeroMQ 时,只能向常量 TOPIC 定义相同的接收端发送消息:

topic = "fancyhw_data".encode('ascii')

print("Reading messages with topic: {}".format(topic))

初始化

下一步,初始化上下文和套接字。使用 subscribe 套接字(也称为 SUB 套接字),它是 PUB 套接字的天生伴侣。这个套接字发送时也需要匹配主题。

with zmq.Context() as context:
    socket = context.socket(zmq.SUB)

    socket.connect("tcp://127.0.0.1:5555")
    socket.setsockopt(zmq.SUBSCRIBE, topic)

    i = 0

    ...

接收消息

启动一个无限循环,等待接收发送到 SUB 套接字的新消息。这个循环会在你按下 Ctrl+C 组合键或者内部发生错误时终止:

    try:
        while True:

            ... # we will fill this in next

    except KeyboardInterrupt:
        socket.close()
    except Exception as error:
        print("ERROR: {}".format(error))
        socket.close()

这个循环等待 recv() 方法获取的新消息,然后将接收到的内容从第一个空格字符处分割开,从而得到主题:

binary_topic, data_buffer = socket.recv().split(b' ', 1)

解码消息

Python 此时尚不知道主题是个字符串,使用标准 ASCII 编解码器进行解码:

topic = binary_topic.decode(encoding = 'ascii')

print("Message {:d}:".format(i))
print("\ttopic: '{}'".format(topic))

下一步就是使用 struct 库读取二进制数据,它可以将二进制数据段转换为明确的数值。首先,计算数据包中数值的组数。本例中使用的 16 个位的有符号整数对应的是 struct 格式字符 中的 h

packet_size = len(data_buffer) // struct.calcsize("h")

print("\tpacket size: {:d}".format(packet_size))

知道数据包中有多少组数据后,就可以通过构建一个包含数据组数和数据类型的字符串,来定义格式了(比如“16h”):

struct_format = "{:d}h".format(packet_size)

将二进制数据串转换为可直接打印的一系列数字:

data = struct.unpack(struct_format, data_buffer)

print("\tdata: {}".format(data))

完整 Python 代码

下面是 Python 实现的完整的接收端:

#! /usr/bin/env python3

import zmq
import struct

topic = "fancyhw_data".encode('ascii')

print("Reading messages with topic: {}".format(topic))

with zmq.Context() as context:
    socket = context.socket(zmq.SUB)

    socket.connect("tcp://127.0.0.1:5555")
    socket.setsockopt(zmq.SUBSCRIBE, topic)

    i = 0

    try:
        while True:
            binary_topic, data_buffer = socket.recv().split(b' ', 1)

            topic = binary_topic.decode(encoding = 'ascii')

            print("Message {:d}:".format(i))
            print("\ttopic: '{}'".format(topic))

            packet_size = len(data_buffer) // struct.calcsize("h")

            print("\tpacket size: {:d}".format(packet_size))

            struct_format = "{:d}h".format(packet_size)

            data = struct.unpack(struct_format, data_buffer)

            print("\tdata: {}".format(data))

            i += 1

    except KeyboardInterrupt:
        socket.close()
    except Exception as error:
        print("ERROR: {}".format(error))
        socket.close()

将上面的内容保存到名为 online_analysis.py 的文件。Python 代码不需要编译,你可以直接运行它。

运行输出如下:

$ ./online_analysis.py
Reading messages with topic: b'fancyhw_data'
Message 0:
        topic: 'fancyhw_data'
        packet size: 16
        data: (20946, -23616, 9865, 31416, -15911, -10845, -5332, 25662, 10955, -32501, -18717, -24490, -16511, -28861, 24205, 26568)
Message 1:
        topic: 'fancyhw_data'
        packet size: 16
        data: (12505, 31355, 14083, -19654, -9141, 14532, -25591, 31203, 10428, -25564, -732, -7979, 9529, -27982, 29610, 30475)
...
...

小结

本教程介绍了一种新方式,实现从基于 C 的硬件接口收集数据,并分发到基于 Python 的基础设施的功能。借此可以获取数据供后续分析,或者转送到任意数量的接收端去。它采用了一个消息库实现数据在发送者和处理者之间的传送,来取代同样功能规模庞大的软件。

本教程还引出了我称之为“软件粒度”的概念,换言之,就是将软件细分为更小的部分。这种做法的优点之一就是,使得同时采用不同的编程语言实现最简接口作为不同部分之间沟通的组件成为可能。

实践中,这种设计使得软件工程师能以更独立、合作更高效的方式做事。不同的团队可以专注于数据分析的不同方面,可以选择自己中意的实现工具。这种做法的另一个优点是实现了零代价的并行,因为所有的进程都可以并行运行。ZeroMQ 消息库 是个令人赞叹的软件,使用它可以让工作大大简化。


via: https://opensource.com/article/20/3/zeromq-c-python

作者:Cristiano L. Fontana 选题:lujun9972 译者:silentdawn-zz 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

让我们使用 C99 和 C++11 完成常见的数据科学任务。

虽然 PythonR 之类的语言在数据科学中越来越受欢迎,但是 C 和 C++ 对于高效的数据科学来说是一个不错的选择。在本文中,我们将使用 C99C++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() 函数将每一行分成 字元 token 。遍历字元,选择所需的列:

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);
}

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

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 文件、加载 gslgslcblas 库、并将结果保存到 fitting_C99。命令行上的结果输出为:

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

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

 title=

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 生成的结果图像:

 title=

结论

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

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

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


via: https://opensource.com/article/20/2/c-data-science

作者:Cristiano L. Fontana 选题:lujun9972 译者:wxy 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

了解如何使用 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 np
from scipy import stats
import matplotlib.pyplot as plt

你也可以决定只导入一个子模块(如第二行和第三行)。语法有两个(基本上)等效的方式:import module.submodulefrom module import submodule

定义变量

Python 的变量是在第一次赋值时被声明的:

input_file_name = "anscombe.csv"
delimiter = "\t"
skip_header = 3
column_x = 0
column_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 #inch
fig_height = fig_width / 16 * 9 #inch
fig_dpi = 100

fig = 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.500091
Intercept: 3.000091
Correlation coefficient: 0.816421

这是 Matplotlib 产生的图像:

 title=

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.csv
octave: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; %inch
fig_height = fig_width / 16 * 9; %inch
fig_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.500091
Intercept: 3.000091
Correlation coefficient: 0.816421

它显示了用 Octave 生成的结果图像。

 title=

接下来

Python 和 GNU Octave 都可以绘制出相同的信息,尽管它们的实现方式不同。如果你想探索其他语言来完成类似的任务,我强烈建议你看看 Rosetta Code。这是一个了不起的资源,可以看到如何用多种语言解决同样的问题。

你喜欢用什么语言绘制数据?在评论中分享你的想法。


via: https://opensource.com/article/20/2/python-gnu-octave-data-science

作者:Cristiano L. Fontana 选题:lujun9972 译者:heguangzhi 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出