Python 开发指南:数值分析库(Numpy、Pandas预览)

2023年 7月 12日 25.1k 0

Numpy 预览

完整内容可移步至官方指南:Numpy and Scipy Documentation — Numpy and Scipy documentation,这里仅介绍基本功能。

Numpy 是于 2005 年创建并开源的库,旨在提供比传统 Python 列表快 50 倍的数组对象。为了保证更高的性能,Numpy 有近 35% 的部分是由 C 语言实现的。见:GitHub - numpy/numpy: The fundamental package for scientific computing with Python.

Numpy 常用于科学计算,因此在本章默认数组的元素全都是不可变数值,且仅讨论一维数组和二维数组。

数组与矩阵

使用 Numpy 可以创建出数组 ( array )。它可以被看作是原 Python 列表类型的内存优化版本 ( 通过 dtype 指定元素类型以从内存中分配更加紧凑的空间 ),因此兼容原列表的各种操作。

import numpy as np

arr1d = np.array([1, 2, 3, 4], dtype="int")

#  支持列表推导式
print(*[x*2 for x in arr1d])

arr2d = np.array(
    [[1, 2, 3],
     [4, 5, 6],
     [7, 8, 9]], dtype="int"
)

# 支持 (高维的) 切片
sub = arr2d[0:2, 0:2]
"""
[[1,2],
 [4,5]]
"""
print(*sub)

其中,二维数组可被视为矩阵 ( Matrix ) 或列向量 ( Vector )。可以通过调用数组的 shape 属性确定维度。比如:

print(arr1d.shape)  # (4,) -> 长度为 4 的一维数组
print(sub.shape)  # (2, 2) -> 2×2 的方阵

使用 reshape() 方法可以将一维数组堆叠为二维数组。-1 参数有特殊的语义,表示 "任意长度",实际取决于原数组的形状。比如对一个二维数组做如下操作:

  • reshape(-1) 表示展平成任意长度的一维数组。
  • reshape(-1, 1) 表示变成任意行数,但列数为 1 的列向量,是二维数组。
  • reshape(1, -1) 表示变成行数为 1,但列数任意的行向量,是二维数组。
  • """
        np.arange(n) 相当于 range(n) 的 numpy 版本。
        matrix:
        [[ 0  1  2  3]
         [ 4  5  6  7]
         [ 8  9 10 11]]
    """
    matrix = np.arange(12).reshape(3, 4)
    print(matrix)
    
    # [ 0  1  2  3  4  5  6  7  8  9 10 11]
    vect_1d = matrix.reshape(-1)
    print(vect_1d)
    # [[ 0  1  2  3  4  5  6  7  8  9 10 11]]
    vect_v = matrix.reshape(1,-1)
    print(vect_v)
    
    """
    [[ 0]
     [ 1]
       :
       :
     [11]]
    """
    vect_h = matrix.reshape(-1,1)
    print(vect_h)
    

    对于一维数组向二维数组的堆叠,可以通过 order 参数指定填充顺序。选 "C" 进行横向次序填充 ( 默认的 ) 或者 "F" 纵向次序填充,比如:

     """
        [[ 0  1  2  3]
         [ 4  5  6  7]
         [ 8  9 10 11]]
    """
    m1 = np.arange(12).reshape([3,4],order="C")
    print(m1)
    
    """
        [[ 0  3  6  9]
         [ 1  4  7 10]
         [ 2  5  8 11]]
    """
    m2 = np.arange(12).reshape([3,4],order="F")
    print(m2)
    

    通过向 np.where() 函数传入搜索条件,可以得到满足条件的元素下标。比如:

    v1 = np.array([1, 2, 3, 3, 6, 3])
    #  谓词 'v1 == 3' 中的 'v1' 表示 v1 数组中的每一个元素。
    indexes = np.where(v1 == 3)
    #  表示 [2, 3, 5] 下标出现了值为 3 的元素。 
    print(indexes)
    

    数组拼接

    Numpy 支持对数组进行拼接。对于一维数组的拼接,称之 堆叠 ( stack )。堆叠可以分为水平堆叠 ( hstack() 函数 ) 和纵向堆叠 ( stack() 函数 )。水平堆叠的结果仍然是一维数组,而纵向堆叠的结果将是矩阵。

    v1 = np.array([1, 2, 3])
    v2 = np.array([4, 5, 6])
    
    v3 = np.hstack([v1,v2])  # [1, 2, 3, 4, 5, 6]
    print(v3)
    
    """
        [[1 2 3]
        [4 5 6]]
    """
    v4 = np.stack([v1,v2])
    print(v4)
    

    np.concatenate() 函数支持矩阵拼接,类似地,这里使用 axis 标识了拼接方向。在不指定参数的情况下默认 axis=1

    m1 = np.array([
        [1, 2],
        [3, 4]
    ])
    
    m2 = np.array([
        [5, 6],
        [7, 8]
    ])
    
    """
        列数不变,对行追加 (纵向拼接)
        [[1 2]
         [3 4]
         [5 6]
         [7 8]]
    """
    m3 = np.concatenate([m1, m2], axis=0)
    print(m3)
    """
        行数不变,对列追加 (横向拼接)
        [[1 2 5 6]
        [3 4 7 8]]
    """
    m4 = np.concatenate([m1, m2], axis=1)
    print(m4)
    
    

    线性代数

    由于二维数组可被视作矩阵或者向量,因此可以延伸出线性代数的相关计算。比如,通过 matrix.T 即可实现转置操作:

    matrix = np.arange(12).reshape(3, 4)
    """
        [[ 0  4  8]
         [ 1  5  9]
         [ 2  6 10]
         [ 3  7 11]]
    """
    print(matrix.T)
    

    Numpy 还提供四个函数用于生成单位矩阵,全 0 矩阵,全 1 矩阵,以及随机数矩阵。见:

    n, m = 3, 4
    
    E = np.eye(n)  # 生成 m 阶单位矩阵
    I = np.zeros([m, n])  # 生成 m × n 维全 0 矩阵
    K = np.ones([m, n])  # 生成 m × n 维全 1 矩阵
    H = np.empty([m, n]) # 生成 m × n 随机数矩阵
    

    np.linalg.det() 函数可以展开矩阵行列式并计算求值,而 np.linalg.inv() 函数可求矩阵的逆 ( 底层是求解 Ax = E 的解 x,因此存在精度问题 )。比如:

    A = np.array([
        [1,2],
        [3,4]
    ],dtype=int)
    
    det = int(np.linalg.det(A))
    print(det)  # 1*4 - 2*3
    
    A_inverse = np.linalg.inv(A)
    print(A_inverse)
    

    现在假设有另一个矩阵 B。通过 np.matmul() 函数或者 @ 运算符 ( 运算符重载 ) 可以计算矩阵的乘积 ( 不满足交换律 ):

    B = np.array([
        [1,3],
        [2,4]
    ], dtype=int)
    
    """
    [[1*1 + 2*2, 1*3 + 2*4],
     [3*1 + 4*2, 3*3 + 4*4]]
    """
    print(np.matmul(A, B))
    print(A @ B)
    

    特殊地,假设有两个一维数组 a 和 b,此时的 @ 将视作向量内积 ( 满足交换律 ),结果是一个数值。比如:

    a = np.array([1, 2, 3], dtype=int)
    b = np.array([4, 5, 6], dtype=int)
    c = np.arrat([1, 2], dtype=int)
    print(np.matmul(a,b))
    

    通过 np.multiply() 函数或者 * 运算符可以计算矩阵与标量的积 ( 满足交换律 ):

    """
    [[1*3, 2*3],
     [3*3, 4*3]]
    """
    print(np.multiply(A,3))
    print(A * 3)
    

    以上方法可以使用一个 np.dot() 函数概括。根据不同的输入,它将表示不同的行为:

  • 当两个参数的其中一个为标量时,等价于 A * n
  • 当两个参数均为二维矩阵时,等价于 A @ B
  • 当两个参数分别为一个二维矩阵和一维数组时,将一维数组提升为二维列向量,然后作矩阵乘积 @
  • 当两个参数均为一维矩阵时,视作向量内积,即 a·b。
  • 见:numpy中dot()、outer()、multiply()以及matmul()的区别 - 简书 (jianshu.com)

    # np.array_equal 用于比较两个数组元素是否按位相同。
    assert np.array_equal(np.dot(a, b), a @ b)
    assert np.array_equal(np.dot(A, B), A @ B)
    assert np.array_equal(np.dot(A, c), A @ c)
    assert np.array_equal(np.dot(A, 3), A * 3)
    print("test passed")
    

    为了加以区分,二维矩阵的运算更推荐使用 np.matmul() 或者 @

    Pandas 预览

    如果你很熟悉 SQL 操作,那么 Pandas 上手起来会非常容易。完整内容见官方指南:User Guide — pandas 1.4.3 documentation (pydata.org),这里仅介绍基础功能。

    我们主要使用 Pandas 库处理 表结构 的数据,它们被抽象为了 Dataframe 类型。首先演示基本的 IO 操作:先将 Python 原生的数据结构转换为 Dataframe,然后将它输出到 music.csv 文件,程序演示如下:

    import pandas as pd
    
    #  在 Pandas 中,表数据的单个列被称之 Series.
    artist = ["Billie Holiday", "Jimi Hendrix", "Miles Davis", "SIA"]
    genre = ["Jazz", "Rock", "Jazz", "Pop"]
    listeners = [1_300_000, 2_700_000, 1_500_000, 2_000_000]
    plays = [27_000_000, 70_000_000, 48_000_000, 74_000_000]
    
    dict_ = {"artist": artist,
            "genre": genre,
            "listeners": listeners,
            "plays": plays
            }
    
    # dict 的 kw 对已经隐含了列名信息,因此这里不需要显式指定 columns。
    df = pd.DataFrame(dict_)
    
    #  这里不需要额外生成索引下标序列,因此做以下设置: index=False。
    df.to_csv(path_or_buf="music.csv",index=False)
    

    pd.DataFrame 可以将 Python 原生的 dict 字典结构转化为表。其中 key 表示了列名,而 value 表示了该列的数据。另一种更加自然的想法是:以行的形式组织 DataFrame,这里将每一行抽象成了元组。

    table = [
        ("Billie Holiday", "Jazz", 1_300_000, 27_000_000),
        ("Jimi Hendrix", "Rock", 2_700_000, 70_000_000),
        ("Miles Davis", "Jazz", 1_500_000, 48_000_000),
        ("SIA", "Pop", 2_000_000, 74_000_000)
    ]
    df = pd.DataFrame(data=table, columns=["artist", "genre", "listeners", "plays"])
    df.to_csv(path_or_buf="music.csv", index=False)
    

    生成的表结构如下:

    artist genre listener plays
    Billie Holiday Jazz 1300000 27000000
    Jimi Hendrix Rock 2700000 70000000
    Miles Davis Jazz 1500000 48000000
    SIA Pop 2000000 74000000

    下一步,试着 pd.read_csv(path) 重新将它以 Dataframe 的形式读取进内存。在默认情况下,Pandas 将读取到的第一行视作表头 Header 。

    df = pd.read_csv("music.csv")
    """
                   artist genre  listeners     plays
        0  Billie Holiday  Jazz    1300000  27000000
        1    Jimi Hendrix  Rock    2700000  70000000
        2     Miles Davis  Jazz    1500000  48000000
        3             SIA   Pop    2000000  74000000
    """
    print(df)
    

    数据提取

    下面从最基本的操作开始说起。若要提取出 df 的某一列数据,可以直接使用重载的 [] 运算符 ( 见前一章的运算符重载 ) 进行指定,支持传入单个列名,或者是由多个列名组成的列表。下面演示了提取单个列和多个列的情形:

    df["artist"]  # 提取一个列
    df[["artist","plays"]]  # 提取多个列
    

    特地强调,如果仅提取一个列,得到的将是 Series 类型;如果提取多个列,得到的将是 Dataframe 类型。

    如果要从 df 中提取出第 n 行,则使用 df.loc[n] 实现。事实上,这里的 n 代表索引,在默认情况下是 0 起始的数据下标。比如提取第一行的行号:

    # 提取下标 0 行的 row
    # 0  Billie Holiday  Jazz    1300000  27000000
    row = df.loc[0]
    
    # 从 row 中再提取 artist 列
    # Billie Holiday
    print(row["artist"])
    

    Pandas 提供了查看数据的前 n 行或最后 n 行的函数:

    n = 2
    print(df.head(n))  # 查看前 n 行数据
    print(df.tail(n))  # 查看后 n 行数据
    

    过滤 ( WHERE 谓词 ) 是最基本的表操作。比如,筛选出 plays 数据高于 5000 万的数据:

    top_df = df[df["plays"] > 50_000_000]
    print(top_df)
    

    另一种直观的方式是调用 query() 方法并直接传递谓词语句,字符串内容遵守 Python 语法。比如:

    top_df = df.query("plays >= 50_000_000 and listeners >= 2_500_000")
    print(top_df)
    

    假定要从 genre 列中归纳出音乐人的歌曲类型,则可以使用 unique() 方法进行去重。比如:

    col = df["genre"].unique()
    
    # ['Jazz' 'Rock' 'Pop']
    # col.tolist()
    print(col)
    

    排列 ( SORT BY 谓词 ) 也是常见的表操作之一。比如,这里按照 plays 列的数据以降序 Desc 排列:

    sorted_df = df.sort_values("plays",ascending=False)
    """
                   artist genre  listeners     plays
        3             SIA   Pop    2000000  74000000
        1    Jimi Hendrix  Rock    2700000  70000000
        2     Miles Davis  Jazz    1500000  48000000
        0  Billie Holiday  Jazz    1300000  27000000
    """
    print(sorted_df)
    

    聚合操作

    可以参考这篇优质文章:Pandas教程 | 超好用的Groupby用法详解 - 知乎 (zhihu.com)

    Pandas 就像 SQL 那样提供聚合操作。比如我们期望按照 genre 字段对表进行分组,并统计每个组的行数。它可以被表达为:

    """SQL:
        select genre, count(*)
        from df
        group by genre
    """
    print(df.groupby("genre")["artist"].count())
    

    除了 count() 计数方法之外,其它的聚合方法还包括:mean() 均值,sum() 求和,median() 中位数,min() 最小值,max() 最大值,var() 方差,std() 标准差。一种更通用的聚合方法为 agg(),它允许接收 **kwargs 来对不同列进行不同的聚合操作,key 为列名,value 为聚合函数名。比如:

    report_df = df.groupby("genre").agg({
        "artist": "count",
        "listeners": "mean",
        "plays": "max"
    })
    
    """
               artist  listeners     plays
        genre                             
        Jazz        2  1400000.0  48000000
        Pop         1  2000000.0  74000000
        Rock        1  2700000.0  70000000
    """
    print(report_df)
    

    转换操作

    Pandas 提供的 Dataframe 是 可变 数据,这意味着我们可以对原数据表进行改动。比如,根据 listensersplays 计算每个音乐人的影响力 score,并将其作为新的数据列附着在原数据表上:

    df = pd.read_csv("music.csv")
    """
               artist genre  listeners     plays  score
    0  Billie Holiday  Jazz    1300000  27000000  0.605
    1    Jimi Hendrix  Rock    2700000  70000000  1.535
    2     Miles Davis  Jazz    1500000  48000000  1.035
    3             SIA   Pop    2000000  74000000  1.580
    """
    # 假定 score = 5 * listeners_count + 2 * plays
    df["score"] = (df["listeners"] * 5 + df["plays"] * 2) / 10**8
    print(df)
    

    map() 函数提供了以 lambda 表达式对 单列数据 ( 即 Series 类型 ) 进行变换的途径。比如:

    """
                   artist genre  listeners   plays
        0  Billie Holiday  Jazz    1300000  50mio-
        1    Jimi Hendrix  Rock    2700000  50mio+
        2     Miles Davis  Jazz    1500000  50mio-
        3             SIA   Pop    2000000  50mio+
    """
    df["plays"] = df["plays"].map(lambda x: "50mio+" if x > 50_000_000 else "50mio-")
    print(df)
    

    如果要基于多列数据 ( 此时为 DataFrame 类型 ) 进行变换,则需要引入 apply() 方法。比如:

    # axis=1 表示按数据列进行操作
    df["scores"] = df[["listeners","plays"]].apply(lambda t: (t["listeners"]*5 + 2*t["plays"]) / 10**8, axis=1)
    print(df)
    

    表连接

    可以通过 merge() 方法实现类似 SQL 表的内连接 ( 默认 ),左连接,右连接,全外连接操作,通过 how 参数进行配置。比如:

    info = [
        ("Billie Holiday", "US"),
        ("Jimi Hendrix", "US"),
        ("Justin bieber", "Canada"),
    ]
    artist_info = pd.DataFrame(data=info, columns=["artist", "country"])
    
    m0 = df.merge(artist_info)  # how="inner"
    m1 = df.merge(artist_info, how="outer")
    m2 = df.merge(artist_info, how="left")
    m3 = df.merge(artist_info, how="right")
    

    使用 Pandas 自身提供的 concat() 函数可以连接两个数据表。比如:

    row = [("Justin Bieber", "Pop", 300_000, 1_000_000)]
    append_df = pd.DataFrame(data=row, columns=["artist", "genre", "listeners", "plays"])
    
    # axis=0 表示按行进行尾追加
    ndf = pd.concat([df, append_df], axis=0)
    print(ndf)

    作者:花花子

    来源:稀土掘金

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论