likes
comments
collection
share

Numpy中ndarray到底有什么秘密?

作者站长头像
站长
· 阅读数 2

现在我们已经对 NumPy 有了基本的认识,知道了 NumPy 的强大,并且得知 NumPy 核心就是利用多维数组对数据的高效处理方法

但是你是否思考过什么是数组呢?只有了解到什么是数组,才能理解它可以解决什么问题,进而在遇到该类问题时快速选择使用它。我们还应该注意到多维这个概念,正如楼房有多层一样,数组有多维。分清楼房的多层可以帮助我们不会走错家门,而理清数组的多维同样可以帮助我们更好地组织数组、操作数组、查找数据。

每个事物都存在着属性,我们在使用这个事物时其实是针对属性进行使用的,为了更好地使用数组,使数组能够帮助我们解决具体的问题,我们还应该了解数组的属性。例如我们食用苹果,是针对它的饱腹、营养、味甜的属性,挑选苹果时则依赖色红、个圆等属性。

这些问题或许你比较模糊,不用着急,今天我们就会对这些问题进行讲解。

维度、属性与轴

维度是用来描述事物所在的空间维度。在不同领域,维度有着不同的含义。例如,在物理学中的三维空间有长度、宽度和高度三个维度;在数学中,点是零维,线是一维,平面是二维,立方体就是三维。在计算机领域中,可以用三维来表示一个人的属性,如身高、肤色和体重,每个属性对应一个维度。

从数组上讲,维度就是数的组合叠加,将任意个数字组合在一起我们称为一维数组,将任意个一维数组组合在一起我们称为二维数组,这样将当前维度数组视作一个单位元素去堆叠就生成了高一维的数组。  

而轴就是用来描述多维数组中的数据位置的一组直线,每一个维度对应一个轴。 轴与数组的维度相关联,通过不同的轴,我们可以访问数组中不同维度的数据。

  在一维空间中,我们用一个轴(通常为axis 0)表示直线空间上的离散点。 在二维空间中,需要用两个轴(通常为axis 0和axis 1)来表示平面空间上的离散点,其中axis 0表示行,axis 1表示列。 在三维空间中,需要用三个轴(通常为axis 0、axis 1和axis 2)来表示立方体空间上的离散点。

这里需要注意,axis0通常指向最新的维度,比如三维相比二维增加了高,那么axis0就是高这个维度的轴,其他以此类推。

通过指定多个不同的轴,我们可以在多维数组中定位和处理数据。

Numpy中ndarray到底有什么秘密?

属性是描述事物特征或性质的概念。在不同领域,属性可以表示不同事物的特征。例如,在计算机领域中,我们要识别合适的礼仪队员,对人进行建模。人的肤色、身高和体重就是三个不同的属性。我们可以用一个维度的数据组织形式来表达这三个属性的数据集,也可以用关系型数据库来存储这三个属性和属性值。在数学和计算机科学中,属性类似于数据的特征,可以用于描述数据集合的性质和特点。

举个例子,有一个二维数组(矩阵):

[[1, 2, 3],  
 [4, 5, 6],  
 [7, 8, 9]]

这个数组有两个维度,可以视为平面空间上的数据。在numpy中,我们通常用axis 0和axis 1来表示这两个维度。axis 0表示行,axis 1表示列。属性可以类比为矩阵中的元素,每个元素都代表矩阵中的一个数据点,而这个数据点可以用于描述矩阵的性质和特征。例如,矩阵中的元素可以表示一个人的某个属性值,如肤色、身高或体重。

通过理解维度、轴和属性的概念,我们可以更好地处理多维数组数据、描述事物特征以及进行数学和科学计算。这些概念在数据处理、人工智能、数学建模等领域中都起着重要的作用。

什么是数组?

数组是一系列具有相同数据类型的元素组合,并且还是有次序的。换句话来讲,数组就是一组数据元素,并且这些元素的数据类型都是相同的。我们可以用工厂流水线来比作数组,流水线中的产品就是数据元素,它们有着相同的类型且是连续的。

很多人学习的第一门编程语言就是 C 语言,不是说 C 语言很简单,而是 C 语言更偏向于底层。通过对 C 语言的学习,能够深刻地了解到数据在内存中的存储方式,所以使用 C 语言来诠释数组在内存中如何存储和调用是非常直观的。接下来我们利用它来创建数组,诠释数组的本质和特点。

C 语言创建数组比较简单,如下示例代码,将一个学生的六科成绩作为一个数组进行创建,并将其打印输出。

#include <stdio.h>
#include <stdlib.h>
int main(){
         int grade[6]={100,106,100,83,67,78};
         for(int i=0;i<6;i++){
                  printf("成绩%d:%d",i,grade[i]);
         }
         return 0;
}

结果如下:

成绩0:100
成绩1:106
成绩2:100
成绩3:83
成绩4:67
成绩5:78

C 语言创建数组是通过定义创建的,但仅仅明白如何创建数组是不够的,更重要的是我们还需要知道数组在内存中是如何存放的。我们访问数组元素是通过下标进行访问的,这是很常见的形式,但是下标访问的依据是什么呢?我们通过查看数组在内存中的地址来进行解释,看以下代码:

#include <stdio.h>
#include <stdlib.h>
int main(){
         int grade[6]={100,106,100,83,67,78};
         for(int i=0;i<6;i++){
                  printf("成绩%d:%d\n",i,grade[i]);
         }
         for(int i=0;i<6;i++){
                  printf("地址%d:%x\n",i,&grade[i]);
         }
         return 0;
}

结果如下:

成绩0:100
成绩1:106
成绩2:100
成绩3:83
成绩4:67
成绩5:78
地址0:251df8e8
地址1:251df8ec
地址2:251df8f0
地址3:251df8f4
地址4:251df8f8
地址5:251df8fc

从打印的地址结果来看,数组在内存中是连续存储的,并且每个地址有 4 个字节的存储空间,示例如图所示:

Numpy中ndarray到底有什么秘密?

假设数组的起始地址为 0,根据打印结果,每个数据元素占 4 个字节的空间。数组在内存中是连续存储的,并且存储数据的空间是相同的。

在这个过程中,需要明确两个问题。

  • 第一,为什么该数组的存储空间是 4 个字节?这个很好理解,该数组类型为 int 型,而 int 型在内存中一般占 4 个字节。
  • 第二,数组是怎样通过下标进行访问元素的?在 C 语言中,指针指向的是一个内存地址。在这里,数组名 grade 是一个指针,存储着该数组的起始地址 0(即为基地址),再通过基地址进行访问其他数据的存储地址,由于内存地址是连续的,并且存储空间大小一致,下标访问就是基于地址的有序增加实现的。

Numpy中ndarray到底有什么秘密?

& 表示取址:

grade[1]=&grade[0]+4
grade[2]=&grade[0]+8

这就是 C 语言中数组在内存中的创建与访问过程,但这仅仅是一维数组的创建,那么一旦对数组的维度进行增加,会怎样创建?数组中对于维度的增加其实是通过数组的嵌套实现的,在很多语言中都是支持数组的嵌套的,这也是能够实现维度化的关键点

上述只取一名同学的成绩,创建了一维数组,再另取多名同学的成绩进行分析,能够实现数据的嵌套,改变数据的维度。

#include <stdio.h>
 #include <stdlib.h>
 int main(){
         int grade[3][6]={{100,106,100,83,67,78},
 {100,106,100,83,67,78},
 {100,106,100,83,67,78}};
         for(int i=0;i<3;i++){
                  for(int j=0;j<6;j++){
                          printf("第%d名的成绩%d:%d,成绩存储地址%d%d:%x\n",i+1,j,grade[i][j],i+1,j,&grade[i][j]);
                  }
printf("\n");
         }
          return 0;
 }

结果如下:

第1名的成绩0:100,成绩存储地址10:684ff6d0
第1名的成绩1:106,成绩存储地址11:684ff6d4
第1名的成绩2:100,成绩存储地址12:684ff6d8
第1名的成绩3:83,成绩存储地址13:684ff6dc
第1名的成绩4:67,成绩存储地址14:684ff6e0
第1名的成绩5:78,成绩存储地址15:684ff6e4

第2名的成绩0:100,成绩存储地址20:684ff6e8
第2名的成绩1:106,成绩存储地址21:684ff6ec
第2名的成绩2:100,成绩存储地址22:684ff6f0
第2名的成绩3:83,成绩存储地址23:684ff6f4
第2名的成绩4:67,成绩存储地址24:684ff6f8
第2名的成绩5:78,成绩存储地址25:684ff6fc

第3名的成绩0:100,成绩存储地址30:684ff700
第3名的成绩1:106,成绩存储地址31:684ff704
第3名的成绩2:100,成绩存储地址32:684ff708
第3名的成绩3:83,成绩存储地址33:684ff70c
第3名的成绩4:67,成绩存储地址34:684ff710
第3名的成绩5:78,成绩存储地址35:684ff714

观察二维数组的内存结果,我们可知数组元素在内存中是以线性连续的方式存储的,但是对于数组的访问是通过两层的下标进行访问,如下示意图:

Numpy中ndarray到底有什么秘密?

外层是一个含有三个元素的数组,其中每一个元素又都是有六个元素的数组,最终组成一个二维的数组。三个元素代表三个学生,而每个学生都有自身的属性,例如成绩。这是符合数据的维度化的。通过数组的嵌套就能够产生多维的数组。同理,这里内存空间是连续的,按照抽象的理解,外层的三个数组,每个数组的所占有内存空间是六个 int 型的总空间,即 24 个字节,但实际上内存中二维数组还是线性排列,因此下标查询仍然是根据内存有序增加来进行搜索的。

grade[0][1]=&grade[0]+4
grade[1][1]=&grade[0]+24
grade[2][1]=&grade[0]+48

通过 C 语言创建数组和数组在内存中的存储形式,可以简单了解到有关数组的一些特点,我将数组的这些特点进行总结,如下:

  • 数组在内存中是线性连续的;
  • 数组中元素的数据类型是相同的,它们的存储空间大小是一样的;
  • 多维数组的创建是通过数组的嵌套实现的,但是存储方式仍然是线性连续的。

我们已经学习了数组的底层原理,但这并不是我们的最终目的,使用数组才是最终目的,那么使用数组到底能有什么好处呢?

数组的好处

数据是很多方面的因素影响形成的,所以我们在分析数据时需要从不同的方面,即维度入手,而这些延展出来的维度会产生属性,很多属性才最终组成一个数据项。因此,数据不是单一的,用数组能够将这些数据形成一个整体进行表示。比如学生,属性有学号、年龄、性别、年级、各科成绩等。本来是一个个的数据,通过数组进行创建,就能看成一个整体,共同组成学生这一数据项。因此,数组能够使数据的属性形成一个整体进行分析

其次,数组中的多维数组的存在,使我们能够同时对多个数据项进行整体的分析。对于学校来说,看中的不仅仅是一个学生的成绩,而是全体学生的成绩情况,对多项数据进行整合分析,有助于进行成绩预测与分析。

说了很多的数组知识,这跟 Numpy 有什么关系呢?那是因为在 Numpy 这个库中最最常用、最最基础的就是数组,我们只有先了解了普通的数组,才能更进一步地了解 Numpy 中的数组,了解到它为什么能够运用于数据分析。

Numpy 中的数组——ndarray

在 Numpy 中重要的内核就是 ndarray 多维数组,进行数据分析时,对数据进行运算很大程度上依赖于 ndarray。

ndarray名字中的"n"代表"多维"(Multi-dimensional),"d"代表"维度"(Dimension),"array"则指"数组"。ndarray是Numpy的核心数据结构,用于表示多维数组对象,它由一系列相同数据类型的元素组成,且每个元素占用相同大小的内存块。ndarray是一个快速且灵活的数据容器。

为了方便理解ndarray,下面举一个例子来说明。唐僧西天取经团队的成员编号可以通过一维数组来表示,数组名为QF_boys,数组QF_boys中存储的是数值类型的数据,分别是100,101,102,103,104。

索引成员编号
0100
1101
2102
3103
4104

我们可以通过自带的索引来获取元素,例如QF_boys[0]代表的就是唐僧的工号100.

ndarray的主要特点有:

  • 多维数组形式:ndarray可以是一维、二维、或更高维度的数组,因此能够灵活地处理复杂的数据结构。这使得在科学计算、统计分析、图像处理等领域中,对高维数据的表示和处理变得更加方便和自然。
  • 同一数据类型:数组中的所有元素必须是同一种数据类型,这种强制性约束保证了数据的一致性,也使得数据的处理更为高效。由于数据类型固定,ndarray的元素在内存中存储紧凑,避免了额外的类型检查,提升了运算速度。
  • 从0开始的索引:ndarray的索引从0开始,与许多编程语言一致,例如C和C++,这为数据的访问和处理提供了统一的规则。这种索引方式更符合数学中的矩阵表示,也使得代码更易于编写和阅读。
  • 支持并行化和向量化运算:借助底层的优化实现,ndarray可以进行并行化和向量化运算,极大地提升了数据处理的效率。这种优化能力使得Numpy在大规模数据处理和科学计算中表现突出。
  • 存储高效:由于ndarray中所有元素的数据类型相同,数据存储在内存中是连续的,这样寻址和存取操作都变得高效。此外,ndarray支持使用内存映射文件进行数据的读写,便于处理大规模数据集。

正因为这些特点和优势,使用ndarray可以极大地简化代码,省去了许多繁琐的循环语句,使得数据处理和计算更加简洁和高效。 通过下面这张图片,我们可以直观认识到ndarray的数据分布图。

Numpy中ndarray到底有什么秘密?

ndarray 的常见属性有形状数据类型元素个数数组维度等等,通过这些属性,能够帮助我们更好地了解 ndarray。下面我们就来介绍一下 ndarray 的相关属性吧!

shape

首先我们先来学习一下 ndarray 的形状。通过获取数组的形状,我们可知晓数组的行数和列数,方便调整数组的维度。

shape 为 Ndarray 的形状,通常由一个包含了各个维度中数组的大小的元组表示。比如,一个二行三列的数组,它的的形状便可以由(2,3)这个元组表示。

在生活中,我们最常使用的数组多为一维数组、二维数组以及三维数组这三种,更高维度的数组我们一般很少使用。下面我们就通过图示,为大家介绍一下常用的几种数组的形状都是怎样的。

一维数组示意图:

Numpy中ndarray到底有什么秘密?

通过图示,我们可以看见一维数组的形状是水平方向的一条线。

二维数组示意图:

Numpy中ndarray到底有什么秘密?

通过观察二维数组的示意图可以发现,二维数组的形状类似于一个平面,可以看成是由多个一维数组纵向叠加而成。

下面我们再来看看三维数组的示意图:

Numpy中ndarray到底有什么秘密?

通过观察三维数组的示意图,我们发现三维数组的形状是一个立体图,它比二维数组又多了一个轴向,是多个二维数组在新的轴向上的堆叠。

由此我们可以得出结论:更高维度的数组就是较低维度数组的堆叠

那么,如何查看一个 ndarray 数据对象的形状呢?

代码示例:

import numpy as np  
arr=np.array([[1,2,3],[4,5,6]])  
print(arr.shape)

输出结果:

(2, 3)

可以看见我们利用 . 加 shape 的方式成功获取到了 ndarray 的形状信息。下面我们再来学习一下 ndarray 多维数组的维度属性吧!

ndim

ndim 是 ndarray 数组对象的维度属性,维度我们可以理解为这个数组轴的数量或者理解为代表数组形状的元组的大小。比如一个三行四列的数组,它的 shape 属性为(3,4),该元组的大小为 2,我们就可以说该数组的维度 ndim 为 2。

我们还可以通过另外一种方式确定数组的维度,如果我们想要获取数组中的某个元素,下标需要指定几个,数组的维度便是几。

比如,我们有如下一个数组:

Arr=[[1 2 3]
[4 5 6]]

如果想要获取元素 2 就需要通过 Arr[0][1] 来获取,此时下标指定了两层,所以该数组的维度便为 2。了解了数组的维度,下面我们就来看看怎样才能查看一个 ndarray 数组的维度吧!

代码示例:

import numpy as np  
arr=np.array([[1,2,3],[4,5,6]])
print('数组的维度:',arr.ndim)

输出结果:

数组的维度: 2

可以看见我们利用 . 加 ndim 的方式成功获取到了 ndarray 的维度信息。下面我们再来学习一下 ndarray 多维数组的大小属性吧!

size

size 为 ndarray 数组的大小,也就是数组中的元素个数,由一个 int 型的整数表示。通常 ndarray 数组的大小为各个维度上数组大小的乘积,换句话说就是代表形状的元组的各个元素的乘积,比如一个三维数组,它的形状为(3,4,2),据此我们就可以知道它的大小为 24。

下面我们就来看一下怎么通过代码查看一个数组的大小吧!

代码示例:

import numpy as np  
arr=np.array([[1,2,3],[4,5,6]])
print('数组的大小:',arr.size)

输出结果:

数组的大小: 6

同时,数组的形状 shape 也与数组的其他属性有一定的关系,表示数组形状的元组的大小便是该数组的维度 ndim,比如一个二行三列的数组的维度便为 2;元组内各个元素的乘积便是该数组的大小 size,比如这个二行三列的数组的大小便为 6。也就是说,我们知道一个数组的形状,便能够推导出数组的维度以及大小这两个属性。

可以看见我们利用 . 加 size 的方式成功获取到了 ndarray 的维度信息。下面我们再来学习一下 ndarray 多维数组的数据类型吧!

dtype

dtype 是 ndarray 的数据类型,通过该属性我们能够知道 ndarray 中存储数据的数据类型。

Numpy 的数据类型主要分为以下几种。

  • 整数类型:包括 int8、int16、int32、int64,这些类型分别表示 8 位、16 位、32 位、64 位的带符号整数。
  • 浮点类型:包括 float16、float32、float64,这些类型分别表示 16 位、32 位、64 位的浮点数。
  • 复数类型:包括 complex64、complex128,这些类型分别表示 64 位和 128 位的复数。
  • 布尔类型:即 bool_,它只有两个取值:True 和 False。
  • 字符串类型:即 str_,用于存储字符串。

下面我们就来看看如何通过代码的方式查看一个数组的数据类型吧!

代码示例:

import numpy as np  
arr=np.array([[1,2,3],[4,5,6]])  
print(arr.dtype)

输出结果:

int32

同样我们也可以借助 . 加 dtype 的方式查看 ndarray 的数据类型。

由此我们知道,如果想要通过代码查看一个数组的各种属性,我们只需要通过 ndarray.属性名的方式便可以获取对应的属性值。

了解了 ndarray 数组的属性,那么 C 语言数组与 ndarray 数组有什么异同之处呢?

C 语言数组和 ndarray 数组的异同

ndarray 数组不单单是由数据所构成的,实际上它是一个数组对象。示例图如下:

Numpy中ndarray到底有什么秘密?

ndarray 对象分为两个部分:一个部分是 header、一个是 data。header 部分可以理解为数组属性块,里面有着 ndarray 数组对象所需要的一些属性,比如该数组的步长、数组的形状、数组中数据的类型,等等。data 部分就可以理解为 C 语言中的数组了,存储的是数据。

针对 data 部分,也是符合上面所说的有关数组的特点的。

  • 数组在内存中是线性连续的。
  • 数组所存储的数据的类型是一致的,也就是在内存中元素所占用的空间大小是不变的。
  • 多维数组的创建是通过数组的嵌套实现的,但是存储方式仍然是线性连续的。

对于 header 部分,就是数组所需要的属性部分,通过这些属性我们可以知道有关数组的一些基础的信息。比如查看 dtype 属性获取数据类型信息,利用 shape 属性知晓数组形状。利用这些属性,数组能够通过索引快速访问所需数据。除此之外,header 中还存在其他属性,比如说字节序、读写权限、C-order(行优先存储)等。

接下来介绍一下 ndarray 的数据类型。

ndarray 中的数据类型

我们在学习 Python 时学习了整型、浮点型、字符型等多种数据类型,NumPy 作为 Python 的扩展包,自然支持 Python 的所有数据类型,并且还提供了更加丰富的数据类型,如 float64、int32、bool_,在 numpy 中数据类型基本上都是由数据类型和位数组成的。在 numpy 中除了定义时默认的数据类型,在实际需要时,我们也能够改变数据类型使其成为所需要的数据类型。

改变数据类型使用的是 astype 方法。astype 方法是强制转换数据类型,但是转换之后是不改变原数组元素类型的,而是生成一个新的数组,因此下面的示例中需要通过赋值来重新构建数组。

下面我们有一组实验的输入数据,数据类型为 int32,现在一模型要求输入数据为 float 型,该如何处理呢?

代码示例:

import numpy as np  
arr=np.array([[1,2,3],[4,5,6]])  
print('原数组数据类型',arr.dtype)  
arr=arr.astype('float')  
print('强制转化之后数据类型',arr.dtype)

输出结果:

原数组数据类型 int32
强制转化之后数据类型 float64

前面我们所看见的数组都存储的是同一数据类型的元素,但是我们知道 NumPy 数组和结构化数据是紧密联系的,例如数据库数据、Excel 表格文件等,这些数据都有一个特点,就是一条数据中包含多个属性数据。例如一张成绩表中,每条数据都包含姓名、学号、成绩等数据,分别对应字符串、整型、浮点型等数据类型,而在数据类型中,我们不能从已有的数据类型中找到一个合适的数据类型来对应我们成绩表中的数据,这时候我们就需要利用到数据类型对象来新建一个结构化数据类型

在 C 语言中,我们学习过利用定义结构体,也就是通过 struct 定义结构类型,实现多种数据类型的封装,进而形成我们所需要的数据类型。那么在 NumPy 中是怎样操作的呢?

NumPy 中定义结构化数据需要用到 dtype 对象。数据类型对象(Data Type Object)又称 dtype 对象,主要用来描述数组元素的数据类型、大小以及字节顺序。同时,它也可以用来创建结构化数据。比如常见的 int64、float32 都是 dtype 对象的实例。在创建结构化数据时,我们需要用到对应数据类型的标识码,不同的数据类型字符码是不一样的,如下所示:

字符数据类型
b布尔类型
i带符号整型
u无符号整型
f浮点数类型
c复数类型
m日期类型
O对象类型
S,a字符串类型
UUnicode类型
Vvoid类型

例如现在我们需要创建一个符合成绩单的结构化数据,那么我们就需要建立姓名、年龄、语文、数学、英语这五个数据字段,对应的数据类型依次为字符串、整型、浮点型、浮点型、浮点型。下面我们来看看如何利用 dtype 来定义结构化数据并且应用。

代码示例:

import numpy as np
grade = np.dtype([('name', 'S32'), ('age', 'i'),
                  ('chinese', 'f'),
                  ('math', 'f'),
                  ('english', 'f')])
grades = np.array([("Jack", 15, 86, 90, 85), ("Tom", 16, 87, 91, 75),
                   ("Lucy", 15, 86, 90, 85)], grade)
print(grades)
[(b'Jack', 15, 86., 90., 85.) (b'Tom', 16, 87., 91., 75.) (b'Lucy', 15, 86., 90., 85.)]

可以看到,经过我们的 dtype 定义之后的数据类型对成绩单数据进行了适配。通过定义结构化数据不只是能配合我们的数据类型,并且在之后的存取、计算方面都有许多好处。 在Numpy中,dtype(数据类型)是ndarray的关键之一,它定义了数组中元素的数据类型。dtype的存在使得Numpy能够高效地映射到机器表示,并实现与其他低级语言(如C、Fortran)的无缝集成。这种直接映射和优化保证了Numpy在数据处理和计算方面的强大性能。Numpy支持丰富的数据类型,包括整数、浮点数、复数等,并且允许自定义数据类型,适应各种应用场景。

总结

数组是相同数据类型的有序组合,为储存数据提供了便捷的方法,NumPy 中的核心对象便是多维数组。与 C 语言中的数组不同,Numpy 的 ndarray 数组对象由 data 数据部分和 header 头部信息两部分组成,其中数据部分就可以类比为 C 语言的数组,header 部分则存储了数组的属性等信息。通过调用数组的相关属性,我们能够对数组的形状、维度等信息有所了解。数组的形状通过元组的形式进行表示,高维数组我们可以理解为较低维度数组的堆叠,这些信息会帮助我们更好地理解高维数组的索引。

在使用 Numpy 的强制类型转换方法 astype 时,有一点值得我们注意,利用该方法并不会直接对原数组进行类型转换,而是会形成一个新的满足数据类型要求的数组。

由于数组的最初被定义为是相同数据类型的组合,一旦数据类型不符合就会报错。数组对于数据类型的统一成就了它的效率,但是限制就意味着不便,为了突破这方面的限制,自定义数据类型对象就是我们所必须掌握的。通过自定义数据类型对象我们可以使数据的基本类型多样化,并且贴合日常使用习惯。