3. mcDF用户指南

3.1. mcDF

mcDF是一个Python GPU DataFrame库(基于Apache Arrow列式内存格式构建),用于以一种类似于pandas的方式来加载、连接、聚合、筛选、操作DataFrame样式的表格数据。

3.1.1. 使用场景

如果工作流程在单个GPU上足够快,或者数据可以轻松地放入单个GPU上的内存,那么建议使用mcDF。

import os

import cupy as cp
import pandas as pd
import cudf


cp.random.seed(12)

3.1.2. 创建对象

创建一个 cudf.Series 对象:

>>> s = cudf.Series([1, 2, 3, None, 4])
>>> s
0   1
1   2
2   3
3   <NA>
4   4
dtype: int64

通过为每列指定初始值来创建一个 cudf.Series 对象:

>>> df = cudf.DataFrame(
   {
      "a": list(range(20)),
      "b": list(reversed(range(20))),
      "c": list(range(20)),
   }
)

>>> df
      a     b     c
0     0     19    0
1     1     18    1
2     2     17    2
3     3     16    3
4     4     15    4
5     5     14    5
6     6     13    6
7     7     12    7
8     8     11    8
9     9     10    9
10    10    9     10
11    11    8     11
12    12    7     12
13    13    6     13
14    14    5     14
15    15    4     15
16    16    3     16
17    17    2     17
18    18    1     18
19    19    0     19

3.1.3. 查看数据

查看GPU DataFrame 的前几行:

>>> df.head(2)
      a     b     c
0     0     19    0
1     1     18    1

根据值大小进行排序:

>>> df.sort_values(by="b")
      a      b     c
19    19     0     19
18    18     1     18
17    17     2     17
16    16     3     16
15    15     4     15
14    14     5     14
13    13     6     13
12    12     7     12
11    11     8     11
10    10     9     10
9     9      10    9
8     8      11    8
7     7      12    7
6     6      13    6
5     5      14    5
4     4      15    4
3     3      16    3
2     2      17    2
1     1      18    1
0     0      19    0

3.1.4. 选择数据

3.1.4.1. 选择列

选择一个单独的列:

>>> df["a"]
0    0
1    1
2    2
3    3
4    4
5    5
6    6
7    7
8    8
9    9
10   10
11   11
12   12
13   13
14   14
15   15
16   16
17   17
18   18
19   19
Name: a, dtype: int64

3.1.4.2. 根据标签选择

选择a列和b列的第2到第5行:

>>> df.loc[2:5, ["a", "b"]]
    a    b
2   2    17
3   3    16
4   4    15
5   5    14

3.1.4.3. 根据位置索引选择

和 NumPy 和 pandas 一样,通过一个或者多个整数位置索引切片的方式选择数据:

>>> df.iloc[0]
a   0
b   19
c   0
Name: 0, dtype: int64
>>> df.iloc[0:3, 0:2]
      a     b
0     0     19
1     1     18
2     2     17

也可以直接通过索引的方式选择DataFrame或者Series中的元素:

>>> df[3:5]
      a     b      c
3     3     16     3
4     4     15     4
>>> s[3:5]
3     <NA>
4     4
dtype: int64

3.1.5. 布尔索引

通过布尔索引在DataFrame或者Series中选择行数据:

>>> df[df.b > 15]
       a     b        c
0      0     19       0
1      1     18       1
2      2     17       2
3      3     16       3

通过query API选择行数据:

>>> df.query("b == 3")
      a     b    c
16    16    3    16

对于标准的mcDF,可以使用local_dict关键字,也可以通过 @ 关键字直接传递变量。支持的逻辑运算符包括 ><>=<===!=

>>> cudf_comparator = 3
>>> df.query("b == @cudf_comparator")
      a     b     c
16    16    3     16

使用 isin 方法来查询数据:

>>> df[df.a.isin([0, 5])]
      a     b     c
0     0     19    0
5     5     14    5

3.1.6. 多重索引(MultiIndex)

mcDF支持使用多重索引对 DataFrame 进行分层索引。分组会自动生成一个具有多索引的 DataFrame ,参见 3.1.8.7 分组(Grouping)

>>> arrays = [["a", "a", "b", "b"], [1, 2, 3, 4]]
>>> tuples = list(zip(*arrays))
>>> idx = cudf.MultiIndex.from_tuples(tuples)
>>> idx
MultiIndex([('a', 1),
            ('a', 2),
            ('b', 3),
            ('b', 4)],
)

此索引可以用于 DataFrame 的Index轴或者Column轴:

>>> gdf1 = cudf.DataFrame(
   {"first": cp.random.rand(4), "second": cp.random.rand(4)}
   )
>>> gdf1.index = idx
>>> gdf1
              first          second
a      1      0.082654       0.967955
       2      0.399417       0.441425
b      3      0.784297       0.793582
       4      0.070303       0.271711
>>> gdf2 = cudf.DataFrame(
   {"first": cp.random.rand(4), "second": cp.random.rand(4)}
   ).T
>>> gdf2.columns = idx
>>> gdf2
            a                             b
            1              2              3             4
First       0.343382       0.003700       0.20043       0.581614
second      0.907812       0.101512       0.24179       0.224180

使用多重索引访问 DataFrame 的值,使用 .loc

>>> gdf1.loc[("b", 3)]
first       0.784297
second      0.793582
Name: ('b', 3), dtype: float64

以及 .iloc

>>> gdf1.iloc[0:2]
            first          second
a     1     0.082654       0.967955
      2     0.399417       0.441425

3.1.7. 缺失数据处理

可以使用 fillna 方法替换缺失的数据:

>>> s.fillna(999)
0     1
1     2
2     3
3     999
4     4
dtype: int64
>>> ds.fillna(999).head(n=3)
0  1
1  2
2  3
dtype: int64

3.1.8. 数据操作方法

3.1.8.1. 统计

计算Series的描述性统计信息:

>>> s.mean(), s.var()
(2.5, 1.666666666666666)

下面是一个典型的例子,说明什么时候可以调用 .compute()。在任何情况下,平均值和方差的计算结果都是一个数字,所以使用 .compute() 而不是使用 .head() 查看结果是合理的。

>>> ds.mean().compute(), ds.var().compute()
(2.5, 1.6666666666666667)

3.1.8.2. 应用转换函数(Applymap)

将函数应用于Series。

def add_ten(num):
   return num + 10

df["a"].apply(add_ten)
0     10
1     11
2     12
3     13
4     14
5     15
6     16
7     17
8     18
9     19
10    20
11    21
12    22
13    23
14    24
15    25
16    26
17    27
18    28
19    29
Name: a, dtype: int64

3.1.8.3. 计算直方图(Histogramming)

计算每一个唯一值变量的出现次数:

>>> df.a.value_counts()
15    1
6     1
1     1
14    1
2     1
5     1
11    1
7     1
17    1
13    1
8     1
16    1
0     1
10    1
4     1
9     1
19    1
18    1
3     1
12    1
Name: a, dtype: int32

3.1.8.4. 字符串函数

与pandas一样,mcDF在Series的 str 属性中提供了字符串处理函数:

>>> s = cudf.Series(["A", "B", "C", "Aaba", "Baca", None, "CABA", "dog", "cat"])
>>> s.str.lower()
0    a
1    b
2    c
3    aaba
4    baca
5    <NA>
6    caba
7    dog
8    cat
dtype: object

除了简单的操作外,还可以使用正则表达式匹配字符串:

>>> s.str.match("^[aAc].+")
0    False
1    False
2    False
3    True
4    False
5    <NA>
6    False
7    False
8    True
dtype: bool
>>> ds.str.match("^[aAc].+").head()
0    False
1    False
2    False
3    True
4    False
dtype: bool

3.1.8.5. 连接(Concat)

按行连接 Series 和 DataFrame:

>>> s = cudf.Series([1, 2, 3, None, 5])
>>> cudf.concat([s, s])
0    1
1    2
2    3
3    <NA>
4    5
0    1
1    2
2    3
3    <NA>
4    5
dtype: int64

3.1.8.6. 合并(Join)

执行SQL样式的合并。请注意, DataFrame 的顺序不会得到保持,但可以在合并后通过按索引排序恢复。

>>> df_a = cudf.DataFrame()
>>> df_a["key"] = ["a", "b", "c", "d", "e"]
>>> df_a["vals_a"] = [float(i + 10) for i in range(5)]
>>> df_b = cudf.DataFrame()
>>> df_b["key"] = ["a", "c", "e"]
>>> df_b["vals_b"] = [float(i + 100) for i in range(3)]
>>> merged = df_a.merge(df_b, on=["key"], how="left")
>>> merged
      key   vals_a   vals_b
0     a     10.0     100.0
1     c     12.0     101.0
2     e     14.0     102.0
3     b     11.0     <NA>
4     d     13.0     <NA>

3.1.8.7. 分组(Grouping)

与 pandas 一样,mcDF 支持按组 Split-Apply-Combine 模式:

>>> df["agg_col1"] = [1 if x % 2 == 0 else 0 for x in range(len(df))]
>>> df["agg_col2"] = [1 if x % 3 == 0 else 0 for x in range(len(df))]

分组,然后对分组后的数据应用求和函数:

>>> df.groupby("agg_col1").sum()
         a        b        c        agg_col2
agg_col1
1        90       100      90       4
0        100      90       100      3

分组,然后对分组数据应用求和函数:

>>> df.groupby(["agg_col1", "agg_col2"]).sum()
                        a        b        c
agg_col1    agg_col2
1           0           54       60       54
0           0           73       60       73
1           1           36       40       36
0           1           27       30       27

使用 agg 对特定列进行分组并应用统计函数:

>>> df.groupby("agg_col1").agg({"a": "max", "b": "mean", "c": "sum"})
      a     b        c
agg_col1
1     18    10.0     90
0     19    9.0      100

3.1.8.8. 数据转置(Transpose)

使用 transpose 方法或 T 属性来转置 DataFrame,当前的实现要求所有列必须具有相同的数据类型。

>>> sample = cudf.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
>>> sample
      a     b
0     1     4
1     2     5
2     3     6
>>> sample.transpose()
      0     1     2
a     1     2     3
b     4     5     6

3.1.8.9. 时间序列(Time Series)

DataFrame支持日期时间类型的列,允许用户根据特定的时间戳与数据交互并筛选数据。

>>> import datetime as dt

>>> date_df = cudf.DataFrame()
>>> date_df["date"] = pd.date_range("11/20/2018", periods=72, freq="D")
>>> date_df["value"] = cp.random.sample(len(date_df))

>>> search_date = dt.datetime.strptime("2018-11-23", "%Y-%m-%d")
>>> date_df.query("date <= @search_date")
   date        value
0  2018-11-20  0.986051
1  2018-11-21  0.232034
2  2018-11-22  0.397617
3  2018-11-23  0.103839

3.1.8.10. 分类(Categoricals)

DataFrame支持分类列:

>>> gdf = cudf.DataFrame(
   {"id": [1, 2, 3, 4, 5, 6], "grade": ["a", "b", "b", "a", "a", "e"]}
   )
>>> gdf["grade"] = gdf["grade"].astype("category")
>>> gdf
      id    grade
0     1     a
1     2     b
2     3     b
3     4     a
4     5     a
5     6     e

访问列的类别。

>>> gdf.grade.cat.categories
StringIndex(['a' 'b' 'e'], dtype='object')

访问每个分类观察的基本代码值:

>>> gdf.grade.cat.codes
0  0
1  1
2  1
3  0
4  0
5  2
dtype: uint8
>>> dgdf.grade.cat.codes.compute()
0  0
1  1
2  1
3  0
4  0
5  2
dtype: uint8

3.1.9. 数据转换

3.1.9.1. pandas

将 mcDF DataFrame 转换为 pandas DataFrame :

>>> df.head().to_pandas()
      a     b     c     agg_col1    agg_col2
0     0     19    0     1           1
1     1     18    1     0           0
2     2     17    2     1           0
3     3     16    3     0           1
4     4     15    4     1           0

3.1.9.2. NumPy

将mcDF DataFrame 转换为 NumPy ndarray:

>>> df.to_numpy()
array([[ 0, 19, 0, 1, 1],
       [ 1, 18, 1, 0, 0],
       [ 2, 17, 2, 1, 0],
       [ 3, 16, 3, 0, 1],
       [ 4, 15, 4, 1, 0],
       [ 5, 14, 5, 0, 0],
       [ 6, 13, 6, 1, 1],
       [ 7, 12, 7, 0, 0],
       [ 8, 11, 8, 1, 0],
       [ 9, 10, 9, 0, 1],
       [10, 9, 10, 1, 0],
       [11, 8, 11, 0, 0],
       [12, 7, 12, 1, 1],
       [13, 6, 13, 0, 0],
       [14, 5, 14, 1, 0],
       [15, 4, 15, 0, 1],
       [16, 3, 16, 1, 0],
       [17, 2, 17, 0, 0],
       [18, 1, 18, 1, 1],
       [19, 0, 19, 0, 0]])

将 mcDF Series转换为 NumPy ndarray:

>>> df["a"].to_numpy()
array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19])

3.1.9.3. Arrow

将 mcDF DataFrame 转换为 PyArrow Table:

>>> df.to_arrow()
pyarrow.Table
a: int64
b: int64
c: int64
agg_col1: int64
agg_col2: int64
----
a: [[0,1,2,3,4,...,15,16,17,18,19]]
b: [[19,18,17,16,15,...,4,3,2,1,0]]
c: [[0,1,2,3,4,...,15,16,17,18,19]]
agg_col1: [[1,0,1,0,1,...,0,1,0,1,0]]
agg_col2: [[1,0,0,1,0,...,1,0,0,1,0]]

3.1.10. I/O操作

3.1.10.1. CSV

将数据写入一个CSV文件:

if not os.path.exists("example_output"):
   os.mkdir("example_output")

df.to_csv("example_output/foo.csv", index=False)

从CSV文件中读取数据:

>>> df = cudf.read_csv("example_output/foo.csv")
>>> df
      a     b     c     agg_col1    agg_col2
0     0     19    0     1           1
1     1     18    1     0           0
2     2     17    2     1           0
3     3     16    3     0           1
4     4     15    4     1           0
5     5     14    5     0           0
6     6     13    6     1           1
7     7     12    7     0           0
8     8     11    8     1           0
9     9     10    9     0           1
10    10    9     10    1           0
11    11    8     11    0           0
12    12    7     12    1           1
13    13    6     13    0           0
14    14    5     14    1           0
15    15    4     15    0           1
16    16    3     16    1           0
17    17    2     17    0           0
18    18    1     18    1           1
19    19    0     19    0           0

3.1.10.2. Parquet

写入 parquet 文件:

>>> df.to_parquet("example_output/temp_parquet")

读取 parquet 文件:

>>> df = cudf.read_parquet("example_output/temp_parquet")
>>> df
      a     b     c     agg_col1    agg_col2
0     0     19    0     1           1
1     1     18    1     0           0
2     2     17    2     1           0
3     3     16    3     0           1
4     4     15    4     1           0
5     5     14    5     0           0
6     6     13    6     1           1
7     7     12    7     0           0
8     8     11    8     1           0
9     9     10    9     0           1
10    10    9     10    1           0
11    11    8     11    0           0
12    12    7     12    1           1
13    13    6     13    0           0
14    14    5     14    1           0
15    15    4     15    0           1
16    16    3     16    1           0
17    17    2     17    0           0
18    18    1     18    1           1
19    19    0     19    0           0

3.1.10.3. ORC

写入ORC文件:

>>> df.to_orc("example_output/temp_orc")

读取ORC文件:

>>> df2 = cudf.read_orc("example_output/temp_orc")
>>> df2
      a     b     c     agg_col1    agg_col2
0     0     19    0     1           1
1     1     18    1     0           0
2     2     17    2     1           0
3     3     16    3     0           1
4     4     15    4     1           0
5     5     14    5     0           0
6     6     13    6     1           1
7     7     12    7     0           0
8     8     11    8     1           0
9     9     10    9     0           1
10    10    9     10    1           0
11    11    8     11    0           0
12    12    7     12    1           1
13    13    6     13    0           0
14    14    5     14    1           0
15    15    4     15    0           1
16    16    3     16    1           0
17    17    2     17    0           0
18    18    1     18    1           1
19    19    0     19    0           0

3.2. mcDF与pandas的比较

mcDF是一个处理 DataFrame 的库,与 pandas API 非常类似,但它不是 pandas 的完全替代品。在API和行为方面,mcDF和pandas之间存在一些差异。

3.2.1. 支持的操作方法

mcDF支持许多与pandas相同的数据结构和操作。这包括Series、DataFrame、Index和对它们的操作,如一元和二元操作、索引、筛选、连接、分组和窗口操作等。

3.2.2. 数据类型

mcDF支持pandas中许多常用的数据类型,包括 numeric、datetime、timestamp、string 和 categorical 数据类型。此外,支持一些特殊的数据类型例如decimal、list和 “struct”。

有关详细信息,参见3.3 支持的数据类型

备注

不支持像 pandas 的 ExtensionDtype 这样的自定义数据类型。

3.2.3. Null或缺失数据

与pandas不同,mcDF中的所有数据类型都可以为null,这意味着它们可能包含缺失的值(由 cudf.NA 表示)。

>>> s = cudf.Series([1, 2, cudf.NA])
>>> s
0  1
1  2
2  <NA>
dtype: int64

在任何情况下,Null 都不会被强制为 NaN;将mcDF的行为与 pandas 的行为进行比较如下:

>>> s = cudf.Series([1, 2, cudf.NA], dtype="category")
>>> s
0  1
1  2
2  <NA>
dtype: category
Categories (2, int64): [1, 2]
>>> s = pd.Series([1, 2, pd.NA], dtype="category")
>>> s
0   1
1   2
2   NaN
dtype: category
Categories (2, int64): [1, 2]

3.2.4. 迭代

不支持在mcDF Series、DataFrame或Index上迭代。这是因为迭代驻留在GPU上的数据会产生极低的性能,因为GPU是针对高度并行操作而不是顺序操作进行优化的。

在绝大多数情况下,可以避免迭代,并使用现有的函数或方法来完成相同的任务。如果无法避免,请使用 .to_arrow().to_pandas() 将数据从GPU复制到CPU,然后使用 .from_arrow().from_pandas() 将结果复制回 GPU。

3.2.5. 输出结果排序

默认情况下,mcDF中的 join (或 merge )和 groupby 操作不能保证输出顺序。pandas 和 mcDF 获得的结果比较如下:

>>> import cupy as cp
>>> df = cudf.DataFrame({'a': cp.random.randint(0, 1000, 1000), 'b': range(1000)})
>>> df.groupby("a").mean().head()

      b
a
742   694.5
29    840.0
459   525.5
442   363.0
666   7.0
>>> df.to_pandas().groupby("a").mean().head()
      b
a
2     643.75
6     48.00
7     631.00
9     906.00
10    640.00

为了和pandas的实现表现一致,必须显式指定 sort=True

>>> df.to_pandas().groupby("a", sort=True).mean().head()
      b
a
2     643.75
6     48.00
7     631.00
9     906.00
10    640.00

3.2.6. 浮点运算

mcDF利用GPU并行执行操作。这意味着操作的顺序并不总是确定的。这影响了浮点运算的确定性,因为浮点运算是non-associative,即 a+b 不等于 b+a。

例如,当s是一系列浮点值时, s.sum() 不能保证产生与pandas相同的结果,也不能保证从一次运行到另一次运行产生相同的结果。

如果需要比较浮点结果,通常应该使用 cudf.testing 模块中提供的函数来进行比较,该模块允许将值比较到所需的精度。

3.2.7. 列名称

与 pandas 不同,mcDF不支持重复的列名。最好为列名使用唯一的字符串。

3.2.8. “object” 数据类型

在pandas和NumPy中, object 数据类型用于任意Python对象的集合。例如,在pandas中,可以执行以下操作:

>>> import pandas as pd
>>> s = pd.Series(["a", 1, [1, 2, 3]])
0     a
1     1
2     [1, 2, 3]
dtype: object

为了与pandas兼容,mcDF将字符串的数据类型报告为 "object", 但不支持存储或操作任意 Python object 的集合。

3.2.9. .apply()函数限制

pandas中的 .apply() 函数接受用户定义函数(UDF),该函数可以包括应用于Series、DataFrame的每个值的任意操作,或者在 groupby 的情况下,应用于每个组。mcDF当前版本还不支持 .apply() 函数。

3.3. 支持的数据类型

mcDF支持NumPy和pandas支持的许多数据类型,包括numeric、datetime、timedelta、categorical和string数据类型。还提供了特殊的数据类型,用于处理小数、类似列表和类似字典的数据。

mcDF中的所有数据类型都可以为null,详情参见下表。

表 3.1 支持的数据类型

Kind of data

Data type(s)

Signed integer

int8, int16, int32, int64

Unsigned integer

uint32, uint64

Floating-point

float32, float64

Datetime

datetime64[s], datetime64[ms], datetime64[us], datetime64[ns]

Timedelta (duration)

timedelta[s], timedelta[ms], timedelta[us], timedelta[ns]

Category

cudf.CategoricalDtype()

String

objectstring

Decimal

cudf.Decimal32Dtype(), cudf.Decimal64Dtype()

List

cudf.ListDtype()

Struct

cudf.StructDtype()

3.3.1. NumPy数据类型

将NumPy数据类型用于integer、floating、datetime、timedelta和string数据类型。

因此,就像在NumPy中一样, np.dtype("float32")np.float32"float32" 都是指定 float32 数据类型可接受的方法:

>>> import cudf
>>> s = cudf.Series([1, 2, 3], dtype="float32")
>>> s
0  1.0
1  2.0
2  3.0
dtype: float32

3.3.2. 关于object数据类型

mcDF中与字符串数据关联的数据类型为 np.object

>>> import cudf
>>> s = cudf.Series(["abc", "def", "ghi"])
>>> s.dtype
dtype("object")

这是为了与 pandas 兼容,但可能会产生误导。在NumPy和pandas中,object 是由任意Python对象(而不仅仅是字符串)组成的与数据类型相关的数据。 但是,mcDF不支持存储任意Python对象。

3.3.3. 小数(Decimal)数据类型

提供用于处理小数的特殊数据类型,即 Decimal32Dtype 和 Decimal64Dtype。当需要存储精度高于浮点表示所允许精度的值时,请使用这些数据类型。

mcDF中的小数数据类型基于定点表示。小数数据类型由 precision 和 scale 组成。precision表示此数据类型的每个值中的总位数。 例如,与十进制值 1.023 相关联的precision为4。scale是小数点右边的总位数。与值1.023相关联的scale为3。

每个小数数据类型都与最大precision相关联:

>>> cudf.Decimal32Dtype.MAX_PRECISION
9.0
>>> cudf.Decimal64Dtype.MAX_PRECISION
18.0

3.3.4. 嵌套数据类型(List和Struct)

ListDtype和StructType是mcDF中用于处理类似列表和类似字典数据的特殊数据类型。这些被称为嵌套数据类型,因为它们能够存储列表的列表、列表的结构或列表的列表的结构等。

可以分别从现有的pandas系列列表和字典中创建列表和结构系列:

>>> psr = pd.Series([{'a': 1, 'b': 2}, {'a': 3, 'b': 4}])
>>> psr
0 {'a': 1, 'b': 2}
1 {'a': 3, 'b': 4}
dtype: object

>>> gsr = cudf.from_pandas(psr)
>>> gsr
0 {'a': 1, 'b': 2}
1 {'a': 3, 'b': 4}
dtype: struct

>>> gsr.dtype
StructDtype({'a': dtype('int64'), 'b': dtype('int64')})

或者使用支持嵌套数据的文件格式从磁盘中读取它们:

>>> pdf = pd.DataFrame({"a": [[1, 2], [3, 4, 5], [6, 7, 8]]})
>>> pdf.to_parquet("lists.pq")
>>> gdf = cudf.read_parquet("lists.pq")
>>> gdf
         a
0    [1, 2]
1 [3, 4, 5]
2 [6, 7, 8]
>>> gdf["a"].dtype
ListDtype(int64)

3.4. JSON数据操作

3.4.1. 读取JSON数据

默认情况下,mcDF JSON读取器期望使用面向记录的组织方式的输入数据。 面向记录的JSON数据包括根级别的对象数组,数组中的每个对象对应一行。面向记录的JSON数据以“[”开头,以“]”结尾,并忽略未加引号的空白。 JSON数据的另一个常见变体是“JSON行”,其中JSON对象由换行符( \n )分隔,每个对象对应一行。

>>> j = '''[
   {"a": "v1", "b": 12},
   {"a": "v2", "b": 7},
   {"a": "v3", "b": 5}
]'''
>>> df_records = cudf.read_json(j, engine='cudf')
>>> j = '\n'.join([
...     '{"a": "v1", "b": 12}',
...     '{"a": "v2", "b": 7}',
...     '{"a": "v3", "b": 5}'
... ])
>>> df_lines = cudf.read_json(j, lines=True)

>>> df_lines
       a     b
0     v1    12
1     v2     7
2     v3     5
>>> df_records.equals(df_lines)
True

mcDF JSON读取器还支持JSON对象和数组的任意嵌套组合,这些组合映射到结构和列表数据类型。以下示例演示了用于读取嵌套JSON数据的输入和输出:

# Example with columns types:
# list<int> and struct<k:string>
>>> j = '''[
    {"list": [0,1,2], "struct": {"k":"v1"}},
    {"list": [3,4,5], "struct": {"k":"v2"}}
]'''
>>> df = cudf.read_json(j, engine='cudf')
>>> df
        list        struct
0  [0, 1, 2]   {'k': 'v1'}
1  [3, 4, 5]   {'k': 'v2'}

# Example with columns types:
# list<struct<k:int>> and struct<k:list<int>, m:int>
>>> j = '\n'.join([
...     '{"a": [{"k": 0}], "b": {"k": [0, 1], "m": 5}}',
...     '{"a": [{"k": 1}, {"k": 2}], "b": {"k": [2, 3], "m": 6}}',
... ])
>>> df = cudf.read_json(j, lines=True)
>>> df
                      a                        b
0            [{'k': 0}]    {'k': [0, 1], 'm': 5}
1  [{'k': 1}, {'k': 2}]    {'k': [2, 3], 'm': 6}

3.4.2. 处理大型和小型JSON行文件

对于基于JSON Lines数据的工作负载,mcDF支持如下用于帮助数据处理的读取器选项:对大文件的字节范围支持,以及对小文件的多源支持。

某些工作流需要处理可能超过GPU内存容量的大型JSON行文件。mcDF中的JSON读取器支持字节范围参数,该参数指定起始字节偏移量和字节大小。读取器解析在字节范围内开始的每个记录,因此字节范围不需要与记录边界对齐。为了避免跳过行或读取重复的行,字节范围应该相邻,示例如下所示:

>>> num_rows = 10
>>> j = '\n'.join([
...     '{"id":%s, "distance": %s, "unit": "m/s"}' % x \
...     for x in zip(range(num_rows), cupy.random.rand(num_rows))
... ])

>>> chunk_count = 4
>>> chunk_size = len(j) // chunk_count + 1
>>> data = []
>>> for x in range(chunk_count):
...     d = cudf.read_json(
...          j,
...          lines=True,
...          byte_range=(chunk_size * x, chunk_size),
...      )
...      data.append(d)
>>> df = cudf.concat(data)

相比之下,有些工作流需要处理许多小的JSON Lines文件。mcDF中的JSON读取器接受可迭代的数据源,而不是在源之间循环并连接生成的 DataFrame 。然后将原始输入连接起来,并作为单个源进行处理。请注意,mcDF中的JSON读取器接受源作为文件路径、原始字符串或类似文件的对象,以及这些源的可迭代对象。

>>> j1 = '{"id":0}\n{"id":1}\n'
>>> j2 = '{"id":2}\n{"id":3}\n'

>>> df = cudf.read_json([j1, j2], lines=True)

3.4.3. 展开列表和结构类型的数据

在将JSON数据读取到具有列表/结构列类型的mcDF DataFrame 中之后,许多工作流中的下一步会将数据提取或展平为简单类型。

对于结构列,一种解决方案是使用 struct.explode 访问器提取数据,并将结果连接到父 DataFrame 。以下示例演示如何从结构列中提取数据:

>>> j = '\n'.join([
...     '{"x": "Tokyo", "y": {"country": "Japan", "iso2": "JP"}}',
...     '{"x": "Jakarta", "y": {"country": "Indonesia", "iso2": "ID"}}',
...     '{"x": "Shanghai", "y": {"country": "China", "iso2": "CN"}}'
... ])
>>> df = cudf.read_json(j, lines=True)
>>> df = df.drop(columns='y').join(df['y'].struct.explode())
>>> df
            x      country     iso2
0      Tokyo        Japan       JP
1    Jakarta    Indonesia       ID
2   Shanghai        China       CN

对于元素顺序有意义的列表列, list.get 访问器从特定位置提取元素。然后可以将生成的cudf.Series对象分配给 DataFrame 中的一个新列。以下示例演示如何从列表列中提取第一个和第二个元素:

>>> j = '\n'.join([
...     '{"name": "Peabody, MA", "coord": [42.53, -70.98]}',
...     '{"name": "Northampton, MA", "coord": [42.32, -72.66]}',
...     '{"name": "New Bedford, MA", "coord": [41.63, -70.93]}'
... ])
>>> df = cudf.read_json(j, lines=True)
>>> df['latitude'] = df['coord'].list.get(0)
>>> df['longitude'] = df['coord'].list.get(1)
>>> df = df.drop(columns='coord')
>>> df
               name    latitude    longitude
0       Peabody, MA       42.53       -70.98
1   Northampton, MA       42.32       -72.66
2   New Bedford, MA       41.63       -70.93

对于长度可变的列表列, explode 方法将创建一个新的 DataFrame ,每个元素都作为一行。将分解后的 DataFrame 连接到父 DataFrame 上会产生具有所有简单类型的输出。以下示例展平列表列,并将其连接到索引和父 DataFrame 中的其他数据:

>>> j = '\n'.join([
...     '{"product": "socks", "ratings": [2, 3, 4]}',
...     '{"product": "shoes", "ratings": [5, 4, 5, 3]}',
...     '{"product": "shirts", "ratings": [3, 4]}'
... ])
>>> df = cudf.read_json(j, lines=True)
>>> df = df.drop(columns='ratings').join(df['ratings'].explode())
>>> df
   product   ratings
0     socks         2
0     socks         4
0     socks         3
1     shoes         5
1     shoes         5
1     shoes         4
1     shoes         3
2    shirts         3
2    shirts         4

3.4.4. 处理JSON数据

有时工作流需要使用对象根来处理JSON数据,mcDF提供了为此类数据构建解决方案的工具。如果需要使用对象根来处理JSON数据,建议将数据作为单个JSON行读取,然后拆包生成的 DataFrame 。以下示例将JSON对象作为一行读取,然后将results字段提取到新的 DataFrame 中:

>>> j = '''{
        "metadata" : {"vehicle":"car"},
        "results": [
            {"id": 0, "distance": 1.2},
            {"id": 1, "distance": 2.4},
            {"id": 2, "distance": 1.7}
        ]
}'''
# first read the JSON object with line=True
>>> df = cudf.read_json(j, lines=True)
>>> df
            metadata                                           records
0 {'vehicle': 'car'} [{'id': 0, 'distance': 1.2}, {'id': 1, 'distan...
# then explode the 'records' column
>>> df = df['records'].explode().struct.explode()
>>> df
      id    distance
0      0         1.2
1      1         2.4
2      2         1.7

3.5. 缺失数据处理

在本节中,将讨论mcDF中缺失的(也称为NA)值。mcDF支持在所有dtype中都有缺失的值。这些缺失的值由<NA>表示,又称为“空值”。

3.5.1. 检测缺失数据

要检测缺失的值,可以使用 isna()notna() 函数:

>>> import numpy as np
>>> import cudf

>>> df = cudf.DataFrame({"a": [1, 2, None, 4], "b": [0.1, None, 2.3, 17.17]})
>>> df
   a     b
0  1     0.1
1  2     <NA>
2  <NA>  2.3
3  4     17.17
>>> df.isna()
       a        b
0  False    False
1  False    True
2  True     False
3  False    False
>>> df["a"].notna()

0    True
1    True
2   False
3    True
Name: a, dtype: bool

必须注意的是,在Python(和NumPy)中,nan的比较不相等,但None的比较相等。请注意,mcDF/NumPy 使用了 np.nan != np.nan ,并将 None 视为 np.nan

>>> None == None
True

>>> np.nan == np.nan
False

因此,与上面相比,标量与 None/np.nan 的比较并不能提供有用的信息。

>>> df["b"] == np.nan
0 False
1 <NA>
2 False
3 False
Name: b, dtype: bool
>>> s = cudf.Series([None, 1, 2])
s
0 <NA>
1 1
2 2
dtype: int64
>>> s == None
0 <NA>
1 <NA>
2 <NA>
dtype: bool
>>> s = cudf.Series([1, 2, np.nan], nan_as_null=False)
s
0 1.0
1 2.0
2 NaN
dtype: float64
>>> s == np.nan
0 False
1 False
2 False
dtype: bool

3.5.2. 浮点数据类型和缺失数据

因为NaN是一个浮点值,所以即使是一个整数数据类型的列,如果该列中有一个 NaN ,该列可能被强制转换为浮点数据类型。然而,默认不会出现这种情况。

默认情况下,如果将NaN值传递给Series构造函数,则将其视为<NA>值。

>>> cudf.Series([1, 2, np.nan])
0   1
1   2
2   <NA>
dtype: int64

因此,要将 NaN 视为 NaN ,必须将 NaN_as_null=False 参数传递到Series构造函数中。

>>> cudf.Series([1, 2, np.nan], nan_as_null=False)
0   1.0
1   2.0
2   NaN
dtype: float64

3.5.3. 日期数据类型

对于 datetime64 类型,mcDF 不支持 NaT 值。相反,这些特定于NumPy和panda的值在mcDF中被视为null值( <NA> )。NaT 的实际基础值是 min(int64),在将 mcDF 对象转换为pandas对象时,mcDF保留基础值。

>>> import pandas as pd
>>> datetime_series = cudf.Series(
        [pd.Timestamp("20120101"), pd.NaT, pd.Timestamp("20120101")]
    )
>>> datetime_series
0   2012-01-01 00:00:00.000000
1                          NaT
2   2012-01-01 00:00:00.000000
dtype: datetime64[us]
>>> datetime_series.to_pandas()

0   2012-01-01
1          NaT
2   2012-01-01
dtype: datetime64[ns]

datetime 列中具有 <NA> 值的行执行的任何操作都将在结果列中的同一位置产生 <NA> 的值:

>>> datetime_series - datetime_series
0   0 days 00:00:00
1               NaT
2   0 days 00:00:00
dtype: timedelta64[us]

3.5.4. 计算缺失数据

Null值通过pandas对象之间的算术运算自然传播:

>>> df1 = cudf.DataFrame(
   {
      "a": [1, None, 2, 3, None],
      "b": cudf.Series([np.nan, 2, 3.2, 0.1, 1], nan_as_null=False),
   }
)
>>> df2 = cudf.DataFrame(
{"a": [1, 11, 2, 34, 10], "b": cudf.Series([0.23, 22, 3.2, None, 1])}
)
>>> df1
      a      b
0   1      NaN
1   <NA>   2.0
2   2      3.2
3   3      0.1
4   <NA>   1.0

>>> df2
    a        b
0   1     0.23
1  11     22.0
2   2      3.2
3  34      <NA>
4  10      1.0

>>> df1 + df2
    a     b
0  2     NaN
1  <NA>  24.0
2  4     6.4
3  37    <NA>
4  <NA>  2.0

在对一系列数据求和时, NA 值将被视为 0

>>> df1["a"]
0 1
1 <NA>
2 2
3 3
4 <NA>
Name: a, dtype: int64
>>> df1["a"].sum()
6

由于 NA 值被视为 0,因此在这种情况下,平均值将为2 (1+0+2+3+0)/5=2

>>> df1["a"].mean()
2.0

为了在上述计算中保留NA值, summean 支持 skipna 参数。默认情况下,它的值设置为 True ,可以将其更改为 False 以保留 NA 值:

>>> df1["a"].sum(skipna=False)
nan
>>> df1["a"].mean(skipna=False)
nan

累积方法(如 cumsumcumprod )默认忽略 NA 值:

>>> df1["a"].cumsum()
0 1
1 <NA>
2 3
3 6
4 <NA>
Name: a, dtype: int64

要在累积方法中保留NA值,请提供 skipna=False

>>> df1["a"].cumsum(skipna=False)
0 1
1 <NA>
2 <NA>
3 <NA>
4 <NA>
Name: a, dtype: int64

3.5.5. 对 Null/NaNs 进行点积和求和运算

DataFrame 的一个空系列或所有NA系列的总和为 0 :

>>> cudf.Series([np.nan], nan_as_null=False).sum()
0.0
>>> cudf.Series([np.nan], nan_as_null=False).sum(skipna=False)
nan
>>> cudf.Series([], dtype="float64").sum()
0.0

DataFrame 的空系列或所有NA系列的乘积为1:

>>> cudf.Series([np.nan], nan_as_null=False).prod()
1.0
>>> cudf.Series([np.nan], nan_as_null=False).prod(skipna=False)
nan
>>> cudf.Series([], dtype="float64").prod()
1.0

3.5.6. GroupBy操作中的NA数据

GroupBy中的NA组将自动排除在外。例如:

>>> df1
   a     b
0  1     NaN
1  <NA>  2.0
2  2     3.2
3  3     0.1
4  <NA>  1.0
>>> df1.groupby("a").mean()
       b
a
2    3.2
1    NaN
3    0.1

也可以通过传递 dropna=False 将NA包括在组中:

>>> df1.groupby("a", dropna=False).mean()
       b
a
2    3.2
1    NaN
3    0.1
<NA> 1.5

3.5.7. 插入缺失数据

所有数据类型都支持通过赋值插入缺少的值。通过将系列中的任何特定位置赋值为None,可以将其设为null:

>>> series = cudf.Series([1, 2, 3, 4])
>>> series
0   1
1   2
2   3
3   4
dtype: int64
>>> series[2] = None
>>> series
0   1
1   2
2   <NA>
3   4
dtype: int64

3.5.8. 使用fillna填充缺失数据

可以用非NA数据填充NA和NaN值:

>>> df1
     a     b
0  1     NaN
1  <NA>  2.0
2  2     3.2
3  3     0.1
4  <NA>  1.0
>>> df1["b"].fillna(10)
0   10.0
1    2.0
2    3.2
3    0.1
4    1.0
Name: b, dtype: float64

3.5.9. 使用mcDF对象填充缺失数据

也可以使用可对齐的 dict 或 Series 进行填充。dict 的标签或 Series 的索引必须与要填充的帧的列相匹配。下列示例是用该列的平均值填充DataFrame:

>>> import cupy as cp
>>> dff = cudf.DataFrame(cp.random.randn(10, 3), columns=list("ABC"))
>>> dff.iloc[3:5, 0] = np.nan
>>> dff.iloc[4:6, 1] = np.nan
>>> dff.iloc[5:8, 2] = np.nan
>>> dff
            A             B            C
0    0.819493     -0.944710    -0.937570
1   -1.150507     -0.051519    -1.250062
2    0.607780     -0.094671    -1.700941
3         NaN     -0.025800     1.021424
4         NaN          NaN     -1.181323
5   -1.487396          NaN           NaN
6    1.139386     0.991875           NaN
7    0.724027    -0.868154           NaN
8    0.803895     0.397944     -1.055434
9   -0.509395     0.647727     -1.731969
>>> dff.fillna(dff.mean())
            A            B             C
0    0.819493    -0.944710     -0.937570
1   -1.150507    -0.051519     -1.250062
2    0.607780    -0.094671     -1.700941
3    0.118410    -0.025800      1.021424
4    0.118410     0.006587     -1.181323
5   -1.487396     0.006587     -0.976554
6    1.139386     0.991875     -0.976554
7    0.724027    -0.868154     -0.976554
8    0.803895     0.397944     -1.055434
9   -0.509395     0.647727     -1.731969
>>> dff.fillna(dff.mean()[1:3])
           A              B             C
0   0.819493      -0.944710     -0.937570
1  -1.150507      -0.051519     -1.250062
2   0.607780      -0.094671     -1.700941
3        NaN      -0.025800      1.021424
4        NaN       0.006587     -1.181323
5  -1.487396       0.006587     -0.976554
6   1.139386       0.991875     -0.976554
7   0.724027      -0.868154     -0.976554
8   0.803895       0.397944     -1.055434
9  -0.509395       0.647727     -1.731969

3.5.10. 丢弃缺失数据

可以使用 dropna() 排除丢失的数据:

>>> df1
   a     b
0  1     NaN
1  <NA>  2.0
2  2     3.2
3  3     0.1
4  <NA>  1.0
>>> df1.dropna(axis=0)
   a  b
2  2  3.2
3  3  0.1
>>> df1.dropna(axis=1)
0
1
2
3
4

Series有一个等效的 dropna()

>>> df1["a"].dropna()
0 1
2 2
3 3
Name: a, dtype: int64

3.5.11. 替换任意值

用其他值替换任意值。

Series中的 replace() 和DataFrame中的 replace() 提供了一种高效而灵活的方式来执行此类替换:

>>> series = cudf.Series([0.0, 1.0, 2.0, 3.0, 4.0])
>>> series
0    0.0
1    1.0
2    2.0
3    3.0
4    4.0
dtype: float64
>>> series.replace(0, 5)
0    5.0
1    1.0
2    2.0
3    3.0
4    4.0
dtype: float64

还可以将任何值替换为<NA>值:

>>> series.replace(0, None)
0    <NA>
1    1.0
2    2.0
3    3.0
4    4.0
dtype: float64

可以将值列表替换为其他值列表:

>>> series.replace([0, 1, 2, 3, 4], [4, 3, 2, 1, 0])
0    4.0
1    3.0
2    2.0
3    1.0
4    0.0
dtype: float64

还可以指定映射 dict:

>>> series.replace({0: 10, 1: 100})
0     10.0
1    100.0
2      2.0
3      3.0
4      4.0
dtype: float64

对于DataFrame,可以按列指定各个值:

>>> df = cudf.DataFrame({"a": [0, 1, 2, 3, 4], "b": [5, 6, 7, 8, 9]})
>>> df
    a  b
0   0  5
1   1  6
2   2  7
3   3  8
4   4  9
>>> df.replace({"a": 0, "b": 5}, 100)
     a     b
0  100   100
1    1     6
2    2     7
3    3     8
4    4     9

3.5.12. 字符串/正则表达式替换

mcDF支持使用 replace API替换字符串值:

>>> d = {"a": list(range(4)), "b": list("ab.."), "c": ["a", "b", None, "d"]}
>>> df = cudf.DataFrame(d)
>>> df
    a   b   c
0   0   a   a
1   1   b   b
2   2   .   <NA>
3   3   .   d
>>> df.replace(".", "A Dot")
    a     b        c
0   0     a        a
1   1     b        b
2   2     A Dot    <NA>
3   3     A Dot    d
>>> df.replace([".", "b"], ["A Dot", None])
    a     b        c
0   0     a        a
1   1     <NA>     <NA>
2   2     A Dot    <NA>
3   3     A Dot    d

替换几个不同的值(列表->列表):

>>> df.replace(["a", "."], ["b", "--"])
     a     b     c
0    0     b     b
1    1     b     b
2    2     --    <NA>
3    3     --    d

仅在b列中搜索(dict -> dict):

>>> df.replace({"b": "."}, {"b": "replacement value"})
     a     b                    c
0    0     a                    a
1    1     b                    b
2    2     replacement value    <NA>
3    3     replacement value    d

3.5.13. 数值替换

可以参考 fillna() 的用法使用 replace()

>>> df = cudf.DataFrame(cp.random.randn(10, 2))
>>> df[np.random.rand(df.shape[0]) > 0.5] = 1.5
>>> df.replace(1.5, None)
        0               1
0       <NA>           <NA>
1       <NA>           <NA>
2       <NA>           <NA>
3       -0.553375984   -1.008028523
4       <NA>           <NA>
5       -1.65642176    -0.380188625
6       <NA>           <NA>
7       1.661726764    -1.442556754
8       <NA>           <NA>
9       <NA>           <NA>

通过传递列表可以替换多个值:

>>> df00 = df.iloc[0, 0]
>>> df.replace([1.5, df00], [5, 10])
              0           1
0     10.000000   10.000000
1     10.000000   10.000000
2     10.000000   10.000000
3     -0.553376   -1.008029
4     10.000000   10.000000
5     -1.656422   -0.380189
6     10.000000   10.000000
7     1.661727    -1.442557
8     10.000000   10.000000
9     10.000000   10.000000

也可以在DataFrame上就地操作:

>>> df.replace(1.5, None, inplace=True)
>>> df
        0              1
0       <NA>           <NA>
1       <NA>           <NA>
2       <NA>           <NA>
3       -0.553375984   -1.008028523
4       <NA>           <NA>
5       -1.65642176    -0.380188625
6       <NA>           <NA>
7       1.661726764    -1.442556754
8       <NA>           <NA>
9       <NA>            <NA>

3.6. GroupBy操作

mcDF支持pandas groupby API中的一小部分比较重要的API。

3.6.1. 支持的GroupBy操作列表

  1. 按一列或多列分组

  2. 基础的聚合函数,例如 “sum”, “mean” 等

  3. 分位数聚合(Quantile aggregation)

  4. 在分组上应用一组聚合函数

  5. 聚合时自动排除包含不支持数据类型的列

  6. 在GroupBy对象的组上迭代

  7. GroupBy.groups API:返回分组关键字到行标签的映射

  8. GroupBy.apply API:在每个分组上应用指定的操作。请注意这里可以应用的操作与pandas的类似接口相比具有较大的差异,详情参见 3.6.4 应用函数限制说明

  9. GroupBy.pipe 与 pandas 类似

3.6.2. 分组

GroupBy对象是通过将Series或DataFrame的值按一列或多列分组来创建的:

>>> import cudf
>>> df = cudf.DataFrame({'a': [1, 1, 1, 2, 2], 'b': [1, 1, 2, 2, 3], 'c': [1, 2, 3, 4, 5]})
>>> df
    a   b   c
0   1   1   1
1   1   1   2
2   1   2   3
3   2   2   4
4   2   3   5
>>> gb1 = df.groupby('a') # grouping by a single column
>>> gb2 = df.groupby(['a', 'b']) # grouping by multiple columns
>>> gb3 = df.groupby(cudf.Series(['a', 'a', 'b', 'b', 'b'])) # grouping by an external column

与pandas不同,mcDF默认情况下使用 sort=False 来获得更好的性能,这并不保证结果中有任何特定的组顺序。

例如:

>>> df = cudf.DataFrame({'a' : [2, 2, 1], 'b' : [42, 21, 11]})
>>> df.groupby('a').sum()
    b
a
2   63
1   11

>>> df.to_pandas().groupby('a').sum()
   b
a
1  11
2  63

设置 sort=True 将产生类似pandas的输出,但会带来一些性能损失:

>>> df.groupby('a', sort=True).sum()
   b
a
1  11
2  63

3.6.2.1. 基于索引层分组

也可以按多索引的一个或多个级别进行分组:

>>> df = cudf.DataFrame(
...     {'a': [1, 1, 1, 2, 2], 'b': [1, 1, 2, 2, 3], 'c': [1, 2, 3, 4, 5]}
... ).set_index(['a', 'b'])
...
>>> df.groupby(level='a')

3.6.2.2. Grouper对象

当列和级别具有相同名称时,Grouper可用于消除它们之间的歧义:

>>> df
     b    c
b
1    1    1
1    1    2
1    2    3
2    2    4
2    3    5
>>> df.groupby('b', level='b') # ValueError: Cannot specify both by and level
>>> df.groupby([cudf.Grouper(key='b'), cudf.Grouper(level='b')]) # OK

3.6.3. 聚合

通过 agg 方法支持对组进行聚合:

>>> df
    a   b   c
0   1   1   1
1   1   1   2
2   1   2   3
3   2   2   4
4   2   3   5
>>> df.groupby('a').agg('sum')
    b   c
a
1   4   6
2   5   9

>>> df.groupby('a').agg({'b': ['sum', 'min'], 'c': 'mean'})

     b            c
   sum   min   mean
a
1  4     1     2.0
2  5     2     4.5

>>> df.groupby("a").corr(method="pearson")
             b           c
a
1  b  1.000000    0.866025
   c  0.866025    1.000000
2  b  1.000000    1.000000
   c  1.000000    1.000000

表 3.2 总结了可用的聚合以及支持这些聚合的类型:

表 3.2 聚合类型支持情况

Agg/dtypes

Numeric

Datatime

string

Categorical

List

struct

Interval

Deceimal

count

Y

Y

Y

Y

Y

Size

Y

Y

Y

Y

Y

Sum

Y

Y

Y

Idxmin

Y

Y

Y

Idxmax

Y

Y

Y

Min

Y

Y

Y

Y

max

Y

Y

Y

Y

mean

Y

Y

var

Y

Y

std

Y

Y

quantile

Y

Y

median

Y

Y

nunique

Y

Y

Y

nth

Y

Y

Y

Y

Y

collect

Y

Y

Y

Y

Unique

Y

Y

Y

Y

corr

Y

Y

Y

Y

Y

cov

Y

Y

3.6.4. 应用函数

要在每个组上应用函数,请使用 GroupBy.apply() 方法:

>>> df
    a   b   c
0   1   1   1
1   1   1   2
2   1   2   3
3   2   2   4
4   2   3   5
>>> df.groupby('a').apply(lambda x: x.max() - x.min())
   a   b   c
a
0  0   1   2
1  0   1   1

限制说明

  • apply实现方式是通过将提供的函数依次应用于每组,并将结果连接在一起。这可能非常缓慢,尤其是对于大量的小分组来说。对于少数大型组,它可以提供可接受的性能。

  • 结果可能并不总是与pandas完全匹配。例如,mcDF可能返回一个包含单个column的DataFrame,而pandas返回Series。可能需要进行一些后处理以匹配pandas的行为。

  • mcDF不支持pandas apply支持的一些特殊情况,例如调用describe。

3.6.5. 转换

transform() 方法聚合每个组,并将结果广播到组大小,从而生成与输入Series/DataFrame大小相同的Series/Data Frame:

>>> import cudf
>>> df = cudf.DataFrame({'a': [2, 1, 1, 2, 2], 'b': [1, 2, 3, 4, 5]})
>>> df.groupby('a').transform('max')
    b
0   5
1   3
2   3
3   5
4   5

3.6.6. 滑窗计算

使用 GroupBy.rolling() 方法对每个组执行滑窗计算:

>>> df
    a   b   c
0   1   1   1
1   1   1   2
2   1   2   3
3   2   2   4
4   2   3   5

在每个分组上使用大小为2的窗口进行求和:

>>> df.groupby('a').rolling(2).sum()
        a      b       c
a
1  0    <NA>   <NA>    <NA>
   1    2      2       3
   2    2      3       5
2  3    <NA>   <NA>    <NA>
   4    4      5       9

3.7. mcDF和mcPy互操作

本章节提供一些示例,介绍如何基于mcPy array功能(如高级线性代数运算)将mcDF和mcPy联合使用。

import timeit

import cupy as cp
from packaging import version

import cudf

if version.parse(cp.__version__) >= version.parse("10.0.0"):
   cupy_from_dlpack = cp.from_dlpack
else:
   cupy_from_dlpack = cp.fromDlpack

3.7.1. 将 mcDF DataFrame 转换为 mcPy 数组

如果需要将mcDF DataFrame 转换为mcPy ndarray,有多种方法可以实现:

  • 使用 dlpack 接口

  • 使用 DataFrame.values

  • 使用 mcDF 的 to_cupy 功能进行转换

nelem = 10000
df = cudf.DataFrame(
    {
        "a": range(nelem),
        "b": range(500, nelem + 500),
        "c": range(1000, nelem + 1000),
    }
)

%timeit arr_cupy = cupy_from_dlpack(df.to_dlpack())
%timeit arr_cupy = df.values
%timeit arr_cupy = df.to_cupy()
218 µs ± 5.62 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
687 µs ± 963 ns per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
690 µs ± 2.22 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
>>> arr_cupy = cupy_from_dlpack(df.to_dlpack())
>>> arr_cupy
array([[    0,   500,  1000],
       [    1,   501,  1001],
       [    2,   502,  1002],
       ...,
       [ 9997, 10497, 10997],
       [ 9998, 10498, 10998],
       [ 9999, 10499, 10999]])

3.7.2. 将 mcDF Series 转换为 mcPy 数组

有多种方法可以将mcDF series转换为mcPy array:

  • 将Series传递给 cupy.asarray

  • 使用 to_dlpack()

  • 使用 Series.values

col = "a"

%timeit cola_cupy = cp.asarray(df[col])
%timeit cola_cupy = cupy_from_dlpack(df[col].to_dlpack())
%timeit cola_cupy = df[col].values
103 µs ± 5.11 µs per loop (mean ± std. dev. of 7 runs, 10,000 loops each)
228 µs ± 2.2 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
210 µs ± 893 ns per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
>>> cola_cupy = cp.asarray(df[col])
>>> cola_cupy
array([   0,    1,    2, ..., 9997, 9998, 9999])

从这里开始,可以继续进行正常的 mcPy 工作流,例如 reshaping 数组、获取对角线或计算 norm 值:

>>> reshaped_arr = cola_cupy.reshape(50, 200)
>>> reshaped_arr
array([[   0,    1,    2, ...,  197,  198,  199],
       [ 200,  201,  202, ...,  397,  398,  399],
       [ 400,  401,  402, ...,  597,  598,  599],
       ...,
       [9400, 9401, 9402, ..., 9597, 9598, 9599],
       [9600, 9601, 9602, ..., 9797, 9798, 9799],
       [9800, 9801, 9802, ..., 9997, 9998, 9999]])
>>> reshaped_arr.diagonal()
array([   0,  201,  402,  603,  804, 1005, 1206, 1407, 1608, 1809, 2010,
       2211, 2412, 2613, 2814, 3015, 3216, 3417, 3618, 3819, 4020, 4221,
       4422, 4623, 4824, 5025, 5226, 5427, 5628, 5829, 6030, 6231, 6432,
       6633, 6834, 7035, 7236, 7437, 7638, 7839, 8040, 8241, 8442, 8643,
       8844, 9045, 9246, 9447, 9648, 9849])
>>> cp.linalg.norm(reshaped_arr)
array(577306.967739)

3.7.3. 将 mcPy 数组转换为 mcDF DataFrame

可以将mcPy ndarray转换为mcDF DataFrame。有多种方法可以实现:

  • 最简单的办法是直接使用DataFrame的构造函数

  • 使用dlpack接口

对于后一种情况,需要确保 mcPy 数组在内存中是 Fortran 连续的。可以转置数组,也可以简单地预先强制它为 Fortran 连续数组。

%timeit reshaped_df = cudf.DataFrame(reshaped_arr)
34 ms ± 105 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
>>> reshaped_df = cudf.DataFrame(reshaped_arr)
>>> reshaped_df.head()

可以通过使用 cupy.isfortran 或查看array对象的 flags 属性来检查数组是否是 Fortran 连续的。

>>> cp.isfortran(reshaped_arr)
False

在这种情况下,需要在转到mcDF DataFrame之前对其进行转换。在接下来的示例中利用dlpack创建DataFrame。

%%timeit
fortran_arr = cp.asfortranarray(reshaped_arr)
reshaped_df = cudf.DataFrame(fortran_arr)
13.5 ms ± 119 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)
%%timeit
fortran_arr = cp.asfortranarray(reshaped_arr)
reshaped_df = cudf.from_dlpack(fortran_arr.toDlpack())
9.49 ms ± 50.3 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
>>> fortran_arr = cp.asfortranarray(reshaped_arr)
>>> reshaped_df = cudf.DataFrame(fortran_arr)
>>> reshaped_df.head()

3.7.4. 将 mcPy 数组转换为 mcDF Series

要将数组转换为 Series,可以直接将数组传递给Series构造函数:

>>> cudf.Series(reshaped_arr.diagonal()).head()
0 0
1 201
2 402
3 603
4 804
dtype: int64

3.7.5. 混合使用 mcDF 和 mcPy 构建平滑 PyData 工作流

mcDF正在快速发展,有时一个库可能不具备所需的功能。例如采用pandas DataFrame的逐行求和(或平均值)。mcDF对逐行操作的支持需要转换DataFrame或编写一个UDF,并显式计算每行的总和。根据数据的形状,转换可能会导致数十万列(mcDF不会很好地处理这些列),而编写UDF可能会耗费大量时间。

通过利用 GPU PyData生态系统的互操作性,此操作变得非常简单。对之前 reshaped 的 mcDF DataFrame 进行逐行求和。

>>> reshaped_df.head()

可以将其转换为mcPy数组,并使用sum的axis参数:

>>> new_arr = cupy_from_dlpack(reshaped_df.to_dlpack())
>>> new_arr.sum(axis=1)
array([  19900,    59900,   99900,  139900,  179900,  219900,  259900,
        299900,   339900,  379900,  419900,  459900,  499900,  539900,
        579900,   619900,  659900,  699900,  739900,  779900,  819900,
        859900,   899900,  939900,  979900, 1019900, 1059900, 1099900,
        1139900, 1179900, 1219900, 1259900, 1299900, 1339900, 1379900,
        1419900, 1459900, 1499900, 1539900, 1579900, 1619900, 1659900,
        1699900, 1739900, 1779900, 1819900, 1859900, 1899900, 1939900,
        1979900])

只需这一行,就可以在这个生态系统中的数据结构之间无缝移动,在不牺牲速度的情况下提供巨大的灵活性。

3.7.6. 将 mcDF DataFrame 转换为 mcPy 稀疏矩阵

可以将 DataFrame 或 Series 转换为 mcPy 稀疏矩阵。如果下游过程期望 mcPy 稀疏矩阵作为输入,可以这样做。

稀疏矩阵数据结构由三个密集 array 定义。以下将定义一个辅助函数用于精简代码:

def cudf_to_cupy_sparse_matrix(data, sparseformat="column"):
   """Converts an mcDF object to an mcPy Sparse Column matrix."""
   if sparseformat not in (
         "row",
         "column",
   ):
      raise ValueError("Let's focus on column and row formats for now.")
   _sparse_constructor = cp.sparse.csc_matrix
   if sparseformat == "row":
      _sparse_constructor = cp.sparse.csr_matrix

   return _sparse_constructor(cupy_from_dlpack(data.to_dlpack()))

3.8. pandas兼容性说明

DataFrame.quantile

与pandas的一个显著区别是,当 DataFrame 是非数字类型时,如果在 pandas 下计算结果是一个 Series,那么 mcDF 将返回一个DataFrame,因为mcDF不支持混合数据类型的Series。

DataFrame.reindex, Series.reindex

与pandas的一个区别是, NA 用于不匹配的行,而不是 NaN 。因为上述原因,如果reindex的结果列中具有不匹配的行,那么在mcDF中该列为整数数据类型,在pandas中它被强制转换为浮点。

DataFrame.truncate, Series.truncate

copy参数仅用于API兼容性,但不支持 copy=False。此方法总是生成一个副本。

Where

请注意,where 将缺失的值视为 false,与之对应的,pandas将其视为 null 数据:

>>> gsr = cudf.Series([1, 2, 3])
>>> gsr.where([True, False, cudf.NA])
0 1
1 <NA>
2 <NA>
dtype: int64

>>> gsr.where([True, False, False])
0 1
1 <NA>
2 <NA>
dtype: int64

MultiIndex.get_loc

此函数的返回类型可能与pandas提供的方法不同。如果索引既不是按字典排序的,也不是唯一的,则会尽最大努力将找到的索引强制转换为切片。例如:

>>> import pandas as pd
>>> import cudf
>>> x = pd.MultiIndex.from_tuples([
... (2, 1, 1), (1, 2, 3), (1, 2, 1),
... (1, 1, 1), (1, 1, 1), (2, 2, 1),
... ])
>>> x.get_loc(1)
array([False, True, True, True, True, False])
>>> cudf.from_pandas(x).get_loc(1)
slice(1, 5, 1)

groupby.apply

在某些情况下,pandas将分组键作为索引的一部分返回,而mcDF由于冗余而不返回。例如:

>>> import pandas as pd
>>> df = pd.DataFrame({
>>>     'a': [1, 1, 2, 2],
>>>     'b': [1, 2, 1, 2],
>>>     'c': [1, 2, 3, 4],
>>> })
>>> gdf = cudf.from_pandas(df)
>>> df.groupby('a')[["b", "c"]].apply(lambda x: x.iloc[[0]])
     b  c
a
1 0  1  1
2 2  1  3
>>> gdf.groupby('a')[["b", "c"]].apply(lambda x: x.iloc[[0]])
   b  c
0  1  1
2  1  3

3.9. 写时复制

写时复制(Copy-on-write)是一种内存管理策略,它允许包含相同数据的多个 mcDF 对象引用相同的内存地址,只要它们都不修改底层数据。 使用这种方法,任何生成对象未修改视图的操作(如副本、切片或 DataFrame.head 等方法)都会返回一个新对象,该对象指向与原始对象相同的内存。 但是,当修改现有对象或新对象时,会在修改之前复制数据,以确保更改不会在两个对象之间传播。

mcDF中的默认行为是禁用写时复制,因此要使用它,必须通过设置mcDF选项明确使能。 建议在脚本执行开始时设置写时复制,因为当在脚本执行过程中更改此设置时,将出现不可预期的行为,即启用写时复制时创建的对象仍将具有写时复制行为,而禁用写时复制时所创建的对象将具有不同的行为。

3.9.1. 启用写时复制

  1. 使用 cudf.set_option

>>> import cudf
>>> cudf.set_option("copy_on_write", True)
  1. 在启动Python解释器之前,将环境变量 CUDF_COPY_ON_WRITE 设置为1:

$ export CUDF_COPY_ON_WRITE="1" python -c "import cudf"

3.9.2. 禁用写时复制

通过将 copy_on_write 选项设置为False,可以禁用写时复制:

>>> cudf.set_option("copy_on_write", False)

3.9.3. 执行复制

代码中不需要进行额外的更改即可使用写时复制:

>>> series = cudf.Series([1, 2, 3, 4])

执行浅层复制将创建一个指向相同底层设备内存的新Series对象:

>>> copied_series = series.copy(deep=False)
>>> series
0   1
1   2
2   3
3   4
dtype: int64
>>> copied_series
0   1
1   2
2   3
3   4
dtype: int64

当对系列或 copied_series 执行写入操作时,将创建数据的真实物理副本:

>>> series[0:2] = 10
>>> series
0   10
1   10
2   3
3   4
dtype: int64

>>> copied_series
0   1
1   2
2   3
3   4
dtype: int64

3.9.4. 说明

当启用写时复制时,在切片或索引时不再有视图的概念。修改mcDF创建的视图将始终触发复制,并且不会修改原始对象。

写时复制会产生更加一致的复制语义。由于每个对象都是原始对象的副本,用户不再需要考虑何时可能会意外地进行修改。 这将带来操作之间的一致性,并在为mcDF和pandas启用写时复制时使两者的行为保持一致。以下是一个pandas和mcDF在未启用写时复制时行为不一致的示例:

>>> import pandas as pd
>>> s = pd.Series([1, 2, 3, 4, 5])
>>> s1 = s[0:2]
>>> s1[0] = 10
>>> s1
0   10
1   2
dtype: int64

>>> s
0   10
1   2
2   3
3   4
4   5
dtype: int64
>>> import cudf
>>> s = cudf.Series([1, 2, 3, 4, 5])
>>> s1 = s[0:2]
>>> s1[0] = 10
>>> s1
0   10
1   2
>>> s
0   1
1   2
2   3
3   4
4   5
dtype: int64

当启用写时复制时,上述不一致性得到解决:

>>> import pandas as pd
>>> pd.set_option("mode.copy_on_write", True)
>>> s = pd.Series([1, 2, 3, 4, 5])
>>> s1 = s[0:2]
>>> s1[0] = 10
>>> s1
0   10
1   2
dtype: int64
>>> s
0 1
1 2
2 3
3 4
4 5
dtype: int64
>>> import cudf
>>> cudf.set_option("copy_on_write", True)
>>> s = cudf.Series([1, 2, 3, 4, 5])
>>> s1 = s[0:2]
>>> s1[0] = 10
>>> s1

0   10
1   2
dtype: int64

>>> s
0    1
1    2
2    3
3    4
4    5
dtype: int64

3.9.4.1. 深度复制和浅复制比较

表 3.3 深度复制和浅复制比较

Copy-on-Write enabled

Copy-on-Write disabled (default)

.copy(deep=True)

生成一个真正的副本,并且更改不会传播到原始对象

生成一个真正的副本,并且更改不会传播到原始对象

.copy(deep=False)

内存在两个对象之间共享,但对一个对象的任何写入操作都将在执行写入之前触发真正的物理复制。因此,更改不会传播到原始对象

内存在两个对象之间共享,对其中一个对象执行的更改将传播到另一个对象

3.10. cudf.pandas

cuDF pandas 加速模式(cudf.pandas)基于 cuDF 构建,可在 GPU 上加速 pandas 代码。它与 Pandas 的 API 100% 兼容。

对cudf 支持的函数接口自动使用 GPU,而对其余函数接口自动回退到 pandas 。

3.10.1. 基本用法

要使用 cudf.pandas ,可以使用以下任一方法启用,然后再执行 import pandas

  1. 使用 Jupyter/IPython 的魔法语句启动:

%load_ext cudf.pandas
  1. 从命令行启动:

$ python -m cudf.pandas script.py
  1. 在 Python 脚本中启动:

import cudf.pandas
cudf.pandas.install()

有关每种方法的更多信息,请参阅以下说明。

3.10.2. Jupyter Notebook 或 IPython 引入

在Notebook 或 IPython 会话开始时加载 cudf.pandas 扩展程序。然后执行 import pandas,在此之后的所有语句都将使用 GPU。

%load_ext cudf.pandas

import pandas as pd

URL = "https://github.com/plotly/datasets/raw/master/tips.csv"
df = pd.read_csv(URL)                 # uses the GPU
df["size"].value_counts()             # uses the GPU
df.groupby("size").total_bill.mean()  # uses the GPU
df.apply(list, axis=1)                # uses the CPU (fallback)

备注

如果您已经在当前 kernel 会话中导入或使用了 pandas,则需要重新启动 kernel,并在导入或使用 pandas 之前首先执行 %load_ext cudf.pandas

3.10.3. 命令行引入

在命令行上执行 Python 脚本的时候,带上 -m cudf.pandas 参数:

$ python -m cudf.pandas script.py

这会让你代码中的 import pandas 语句直接得到 cudf.pandas 的加速,而无需更改任何代码。

3.10.4. 直接导入

还有一种 cudf.pandas 的使用方法是显式地导入。如果您无法修改执行 Python 脚本时的命令行参数,那么可以使用这种方法。

只需修改少量 Python 代码即可启用。请确认 cudf.pandas.install() 放在 import pandas 语句之前执行。

import cudf.pandas
cudf.pandas.install()

import pandas as pd

# 其余代码