乐趣区

关于dolphindb:干货丨Orca入门指南

本文将具体介绍 Orca 的装置办法、基本操作,以及 Orca 绝对 pandas 的差别,用户在应用 Orca 编程时须要留神的细节,以便用户能写出高效的 Orca 代码。

  1. 装置

Orca 反对 Linux 和 Windows 零碎,要求 Python 版本为 3.6 及以上,pandas 版本为 0.25.1 及以上。Orca 我的项目曾经集成到 DolphinDB Python API 中。通过 pip 工具装置 DolphinDB Python API,就能够应用 Orca。

pip install dolphindb

Orca 是基于 DolphinDB Python API 开发的,因而,用户须要有一个 DolphinDB 服务器,并通过 connect 函数连贯到这个服务器,而后运行 Orca:

>>> import dolphindb.orca as orca
>>> orca.connect(MY_HOST, MY_PORT, MY_USERNAME, MY_PASSWORD)

如果用户曾经有现成的 pandas 程序,能够将 pandas 的 import 替换为:

# import pandas as pd
import dolphindb.orca as pd

pd.connect(MY_HOST, MY_PORT, MY_USERNAME, MY_PASSWORD)
  1. 疾速入门

通过传入一列值创立一个 Orca Series 对象。Orca 会主动为它增加一个默认索引:

>>> s = orca.Series([1, 3, 5, np.nan, 6, 8])
>>> s

0    1.0
1    3.0
2    5.0
3    NaN
4    6.0
5    8.0
dtype: float64

通过传入一个字典创立与 Orca DataFrame 对象。字典中的每个元素必须是能转化为相似 Series 的对象:

>>> df = orca.DataFrame(...     {"a": [1, 2, 3, 4, 5, 6],
...      "b": [100, 200, 300, 400, 500, 600],
...      "c": ["one", "two", "three", "four", "five", "six"]},
...      index=[10, 20, 30, 40, 50, 60])
>>> df
    a    b      c
10  1  100    one
20  2  200    two
30  3  300  three
40  4  400   four
50  5  500   five
60  6  600    six

也能够间接传入一个 pandas DataFrame 以创立 Orca DataFrame:

>>> dates = pd.date_range('20130101', periods=6)
>>> pdf = pd.DataFrame(np.random.randn(6, 4), index=dates, columns=list('ABCD'))
>>> df = orca.DataFrame(pdf)
>>> df
                   A         B         C         D
2013-01-01  0.758590 -0.180460 -0.066231  0.259408
2013-01-02  1.165941  0.961164 -0.716258  0.143499
2013-01-03  0.441121 -0.232495 -0.275688  0.516371
2013-01-04  0.281048 -0.782518 -0.683993 -1.474788
2013-01-05 -0.959676  0.860089  0.374714 -0.535574
2013-01-06  1.357800  0.729484  0.142948 -0.603437

当初 df 就是一个 Orca DataFrame 了:

>>> type(df)
<class 'orca.core.frame.DataFrame'>

间接打印一个 Orca 对象时,服务端通常会把对应的整个 DolphinDB 数据传送到本地,这样做可能会造成不必要的网络开销。用户能够通过 head 函数查看一个 Orca 对象的顶部数行:

>>> df.head()
                   A         B         C         D
2013-01-01  0.758590 -0.180460 -0.066231  0.259408
2013-01-02  1.165941  0.961164 -0.716258  0.143499
2013-01-03  0.441121 -0.232495 -0.275688  0.516371
2013-01-04  0.281048 -0.782518 -0.683993 -1.474788
2013-01-05 -0.959676  0.860089  0.374714 -0.535574

通过 index, columns 查看数据的索引、列名:

>>> df.index
DatetimeIndex(['2013-01-01', '2013-01-02', '2013-01-03', '2013-01-04',
               '2013-01-05', '2013-01-06'],
              dtype='datetime64[ns]', freq='D')

>>> df.columns
Index(['A', 'B', 'C', 'D'], dtype='object')

通过 to_pandas 把一个 Orca DataFrame 转换成 pandas DataFrame:

>>> pdf1 = df.to_pandas()
>>> type(pdf1)
<class 'pandas.core.frame.DataFrame'>

通过 read_csv 加载一个 CSV 文件,要求 CSV 文件位于 DolphinDB 服务端,所给的门路是它在服务端的门路:

>>> df = orca.read_csv("/home/DolphinDB/Orca/databases/USPrices.csv")
  1. Orca 的架构

Orca 的顶层是 pandas API,底层是 DolphinDB 数据库,通过 DolphinDB Python API 实现 Orca 客户端与 DolphinDB 服务端的通信。Orca 的根本工作原理是,在客户端通过 Python 生成 DolphinDB 脚本,将脚本通过 DolphinDB Python API 发送到 DolphinDB 服务端解析执行。Orca 的 DataFrame 中只存储对应的 DolphinDB 的表的元数据,真正的存储和计算都是在服务端。

Orca 如何贮存数据

Orca 对象在 DolphinDB 中以一个 DolphinDB 表的模式贮存。无论是 Orca DataFrame 还是 Orca Series,它们的底层存储都是 DolphinDB 表,数据列和索引列存储在同一个表中。一个 Orca DataFrame 所示意的 DolphinDB 表蕴含若干数据列,以及若干索引列。而一个 Orca Series 所示意的 DolphinDB 表蕴含一列数据列,以及若干索引列。这使得索引对齐、表内各列计算、分组聚合等操作都能较容易地实现。

Orca 的 DataFrame 中只存储对应的 DolphinDB 的表的元数据,包含表名、数据的列名、索引的列名等。如果尝试拜访一个 DataFrame 的列,返回 Series 时并不会创立一个新的表。返回的 Series 和原有的 DataFrame 应用同一个表,只是 Orca 对象所记录的元数据产生了变动。

  1. Orca 的性能限度

因为 Orca 的架构,Orca 的接口有局部限度:

  • 列的数据类型

DolphinDB 的表的每一个列必须指定一种数据类型。DolphinDB 的 ANY 类型不能作为列的数据类型。因而,Orca 的每一个列不能包含混合的数据类型。此外,列中的数据也不容许是一个 DolphinDB 不反对的 Python 对象,例如 Python 内置的 list, dict,或规范库中的 datetime 等对象。

某些为这些 DolphinDB 不反对的类型而设计的函数,例如 DataFrame.explode,在 Orca 中就没有实际意义。

  • 列名的限度

DolphinDB 的表中的列名必须是非法的 DolphinDB 变量名,即,仅蕴含字母、数字或下划线,且以字母结尾,且不是 DolphinDB 的保留字,比方 if。

DolphinDB 不容许反复的列名。因而 Orca 的列名不能反复。

以大写字母加下划线 ORCA_结尾的列名是 Orca 的列名保留字,Orca 会在外部将某些非凡的列(比方 index)以这种模式命名。用户应该防止应用这类字符串作为 Orca 的列名,否则可能会呈现预期之外的行为。

  • 分区表没有严格程序关系

如果 DataFrame 对应的 DolphinDB 表是一个分区表,数据存储并非间断,所以就没有 RangeIndex 的概念。DolphinDB 分区表的各分区之间没有严格程序关系。因而,如果一个 DataFrame 示意的是一个 DolphinDB 分区表,这些操作无奈实现:

(1)对分区表通过 iloc 拜访相应的行

(2)将一个不同分区类型的 Series 或 DataFrame 赋值给一个 DataFrame

  • 局部函数仅不反对分布式调用

DolphinDB 的某些内置函数目前暂不反对分布式的版本,例如 median, quantile, mad。

  • 空值机制不同

DolphinDB 的数值空值是用每个数据类型的最小值示意。而 pandas 的空值是用浮点数的 nan 示意。Orca 的空值机制和 DolphinDB 保持一致,仅当产生网络传输(下载)时,会将 DolphinDB 蕴含空值的数值列转化成浮点数类型,将其中的空值转化为 nan。

对于字符串类型,pandas 的空值仍然是 nan,这就导致,pandas 在贮存蕴含空值的字符串时,实际上是应用字符串和浮点数混合类型。而混合类型的列在 DolphinDB 中是不容许的。DolphinDB 用空字符串示意字符串类型的空值。用户如果想要上传一个蕴含空值的字符串,应该对字符串列进行预处理,填充空值:

df = pd.DataFrame({"str_col": ["hello", "world", np.nan]})
odf = orca.DataFrame(df)    # Error
odf = orca.DataFrame(df.fillna({"str_col": ""}))    # Correct way to upload a string column with NULL values
  • 轴(axis)的限度

DolphinDB 作为列式存储的数据库,对逐行(row-wise)操作的反对要好于逐列(column-wise)操作。许多操作,例如求和、求平均值等聚合运算,跨行的聚合(求每一列的函数值)的性能要高于跨列的聚合(求每一行的函数值),大多函数都反对跨行计算,但仅有大量函数,例如 sum, mean, max, min, var, std 等,反对跨列计算。在 pandas 中,在函数的参数中指定 axis= 0 或 axis=’index’ 就能实现跨行的计算,而指定 axis= 1 或 axis=’columns’ 能实现跨列的计算。而 Orca 函数经常仅反对 axis= 0 或 axis=’index’。

Orca 的 DataFrame 也不反对 transpose(转置)操作。因为转置后的 DataFrame 中的一列就可能蕴含混合类型的数据。

  • 不承受 Python 可调用对象作为参数

DolphinDB Python API 目前无奈解析 Python 函数,因而,例如 DataFrame.apply, DataFrame.agg 等函数无奈承受一个 Python 可调用对象作为参数。

对于这个限度,Orca 提供了一个备选计划:传入一个 DolphinDB 字符串,它能够是 DolphinDB 的内置函数、自定义函数或条件表达式等。具体内容请参考高阶函数一节。

  1. 最佳实际

  • 缩小 to_pandas 和 from_pandas 的调用

orca 应用 DolphinDB Python API 与服务端通信。理论的数据贮存、查问和计算都产生在服务端,orca 仅仅是一个提供了相似 pandas 接口的客户端。因而,零碎的瓶颈经常在网络通信上。用户在编写高性能的 orca 程序时,须要关注如何优化程序,以缩小网络通信量。

调用 to_pandas 函数将 orca 对象转化为 pandas 对象时,服务端会把整个 DolphinDB 对象传输到客户端。如果没有必要,个别应该缩小这样的转换。此外,以下操作会隐式调用 to_pandas,因而也须要留神:

(1)打印一个示意非分区表的 Orca DataFrame 或 Series

(2)调用 to_numpy 或拜访 values

(3)调用 Series.unique, orca.qcut 等返回 numpy.ndarray 的函数

(4)调用 plot 相干函数画图

(5)将 Orca 对象导出为第三方格局的数据

相似地,from_pandas 会将本地的 pandas 对象上传到 DolphinDB 服务端。当 orca.DataFrame 和 orca.Series 的 data 参数为非 Orca 对象时,也会先在本地创立一个 pandas 对象,而后上传到 DolphinDB 服务端。在编写 Orca 代码时,应该思考缩小来回的网络通信。

  • Orca 并非总是立即求值

Orca 采纳了惰性求值策略,某些操作不会立即在服务端计算,而是转化成一个两头表达式,直到真正须要时才产生计算。须要触发计算时,用户应调用 compute 函数。例如,对同一个 DataFrame 中的列进行四则运算,不会立即触发计算:

>>> df = orca.DataFrame({"a": [1, 2, 3], "b": [10, 10, 30]})
>>> c = df["a"] + df["b"]
>>> c    # not calculated yet
<orca.core.operator.ArithExpression object at 0x0000027FA5527B70>

>>> c.compute()    # trigger the calculation
0    11
1    12
2    33
dtype: int64

又如,条件过滤查问不会立即触发计算:

>>> d = df[df["a"] > 2]
>>> d
<orca.core.frame.DataFrame object with a WHERE clause>

>>> d.compute()    # trigger the calculation
   a   b
2  3  30

分组后应用 cumsum 等函数聚合,或调用 transform,也不会立即返回后果:

>>> c = df.groupby("b").cumsum()
>>> c
<orca.core.operator.DataFrameContextByExpression object at 0x0000017C010692B0>

>>> c.compute()    # trigger the calculation
   a
0  1
1  3
2  3

>>> c = df.groupby("b").transform("count")
>>> c
<orca.core.operator.DataFrameContextByExpression object at 0x0000012C414FE128>

>>> c.compute()    # trigger the calculation
   a
0  2
1  2
2  1
  • 操作同一个 DataFrame 里的列以进步性能

如果操作的是同一个 DataFrame 里的列,Orca 能够将这些操作优化为单个 DolphinDB SQL 表达式。这样的操作会有较高性能。例如:

(1)逐元素计算:df.x + df.y, df * df, df.x.abs()

(2)过滤行的操作:df[df.x > 0]

(3)isin 操作:df[df.x.isin([1, 2, 3])]

(4)工夫类型 / 字符串拜访器:df.date.dt.month

(5)用同样长度的计算结果赋值:df[“ret”] = df[“ret”].abs()

当 DataFrame 是通过过滤的后果时,如果过滤的条件完全相同(在 Python 中是同一个对象,即调用 id 函数取得的值雷同),也能做到这样的优化。

以下脚本能够优化:

df[df.x > 0] = df[df.x > 0] + 1

上述脚本中,等号两边的过滤条件尽管看似雷同,但在 Python 中理论产生了两个不同的对象。在 DolphinDB 引擎中会先执行一个 select 语句,再执行一个 update 语句。如果将这个过滤条件赋值给一个两头变量,Orca 就能够将上述代码优化为单个 DolphinDB 的 update 语句:

df_x_gt_0 = df.x > 0
df[df_x_gt_0] = df[df_x_gt_0] + 1
  • 批改表数据的限度

在 DolphinDB 中,一个表的列的数据类型无奈批改。

此外,一个非内存表(例如 DFS 表)有这些限度:

(1)无奈增加新的列

(2)无奈通过 update 语句批改其中的数据

而一个分区表有这些限度:

(1)不同分区的数据之间没有严格的程序关系

(2)无奈通过 update 语句将一个向量赋值给一个列

因而,当用户尝试对一个 Orca 对象进行批改时,操作可能会失败。Orca 对象的批改有以下规定:

(1)更新的数据类型不兼容,例如将一个字符串赋值给一个整数列时,会抛出异样

(2)为一个示意非内存表的 orca 对象增加列,或批改其中的数据时,会将这个表复制为内存表中,并给出一个正告

(3)主动为一个示意分区表的 orca 对象增加默认索引时,并不会真正增加一个列,此时会给出一个正告

(4)为一个示意分区表的 orca 对象设置或增加一个列时,如果这个列是一个 Python 或 numpy 数组,或一个示意内存表的 orca Series 时,会抛出异样

当尝试给示意非内存表的 orca 对象增加列,或批改其中数据时,数据会复制为内存表,而后再进行批改。当解决海量数据时,可能导致内存不足。因而应该尽量避免对这类 orca 对象的批改操作。

Orca 局部函数不反对 inplace 参数。因为 inplace 波及到批改数据自身。

例如,以下 orca 脚本尝试为 df 增加一个列,会将 DFS 表复制为内存表,在数据量较大时可能会有性能问题:

df = orca.load_table("dfs://orca", "tb")
df["total"] = df["price"] * df["amount"]     # Will copy the DFS table as an in-memory segmented table!
total_group_by_symbol = df.groupby(["date", "symbol"])["total"].sum()

以上脚本能够优化,不设置新的列,以防止大量数据复制。本例采纳的优化办法是将分组字段 date 和 symbol 通过 set_index 设置为索引,并通过指定 groupby 的 level 参数,按索引字段进行分组聚合,指定 groupby 的 lazy 参数为 True,不立即对 total 进行计算。这样做,能防止增加一个新的列:

df = orca.load_table("dfs://orca", "tb")
df.set_index(["date", "symbol"], inplace=True)
total = df["price"] * df["amount"]     # The DFS table is not copied
total_group_by_symbol = total.groupby(level=[0,1], lazy=True).sum()
  • 高阶函数

pandas 的许多接口,例如 DataFrame.apply, GroupBy.filter 等,都容许承受一个 Python 的可调用对象作为参数。Orca 实质上是通过 Python API,将用户的程序解析为 DolphinDB 的脚本进行调用。因而,Orca 目前不反对解析 Python 的可调用对象。如果用户传入一个或多个可调用对象,这些函数会尝试将 Orca 对象转换为 pandas 对象,调用 pandas 的对应接口,而后将后果转换回 Orca 对象。这样做不仅带来额定的网络通信,也会返回一个新的 DataFrame,使得局部计算无奈达到在同一个 DataFrame 上操作时那样的高性能。

作为代替计划,对于这些接口,Orca 能够承受一个字符串,将这个字符串传入 DolphinDB 进行计算。这个字符串能够是一个 DolphinDB 的内置函数(或内置函数的局部利用),一个 DolphinDB 的自定义函数,或者一个 DolphinDB 条件表达式,等等。这个代替计划为 Orca 带来了灵活性,用户能够按本人的须要,编写一段 DolphinDB 的脚本片段,而后,像 pandas 调用用户自定义函数一样,利用 DolphinDB 计算引擎执行这些脚本。

以下是将 pandas 承受可调用对象作为参数的代码改写为 Orca 代码的例子:

(1)求分组加权平均数

pandas:

wavg = lambda df: (df["prc"] * df["vol"]).sum() / df["vol"].sum()
df.groupby("symbol").apply(wavg)

Orca:

df.groupby("symbol")["prc"].apply("wavg{,vol}")

Orca 脚本通过 apply 函数,对 group by 之后的 prc 列调用了一个 DolphinDB 的局部利用 wavg{,vol},转化为 DolphinDB 的脚本,等价于:

select wavg{,vol}(prc) from df group by symbol

将这个局部利用开展,等价于:

select wavg(prc,vol) from df group by symbol

(2)分组后按条件过滤

pandas:

df.groupby("symbol").filter(lambda x: len(x) > 1000)

Orca:

df.groupby("symbol").filter("size(*) > 1000")

上述例子的 Orca 脚本中,filter 函数承受的字符串是一个过滤的条件表达式,转化为 DolphinDB 的脚本,等价于:

select * from df context by symbol having size(*) > 10000

即,filter 的字符串呈现在了 SQL 的 having 语句中。

(3)对整个 Series 利用一个运算函数

pandas:

s.apply(lambda x: x + 1)

Orca:

s.apply("(x->x+1)")

pandas:

s.apply(np.log)

Orca:

s.apply("log")

罕用的计算函数,比方 log, exp, floor, ceil, 三角函数,反三角函数等,Orca 曾经集成。例如,求对数,通过 s.log()即可实现。

(4)过滤时用逗号 (,) 代替 & 符号

DolphinDB 的 where 表达式中,逗号示意执行程序,并且效率更高,只有在前一个条件通过后才会持续验证下一个条件。Orca 对 pandas 的条件过滤进行了扩大,反对在过滤语句中用逗号:

pandas:

df[(df.x > 0) & (df.y < 0)]

Orca:

df[(df.x > 0), (df.y < 0)]

应用传统的 & 符号,会在最初生成 DolphinDB 脚本时将 where 表达式中的 & 符号转换为 DolphinDB 的 and 函数。而应用逗号,会在 where 表达式中的对应地位应用逗号,以达到更高的效率。

(5)如何实现 DolphinDB 的 context by 语句

DolphinDB 反对 context by 语句,反对在分组内解决数据。在 Orca 中,这个性能能够通过 groupby 后调用 transform 实现。而 transform 通常须要用户提供一个 DolphinDB 自定义函数字符串。Orca 对 transform 进行了扩大。对一个两头表达式调用 groupby,并指定扩大参数 lazy=True,而后不给定参数调用 transform,则 Orca 会对调用 groupby 的表达式进行 context by 的计算。例如:

pandas:

df.groupby("date")["prc"].transform(lambda x: x.shift(5))

Orca 的改写:

df.groupby("date")["id"].transform("shift{,5}")

Orca 的扩大用法:

df.shift(5).groupby("date", lazy=True)["id"].transform()

这是 Orca 的一个特地的用法,它充分利用了惰性求值的劣势。在上述代码中,df.shift(5)并没有产生真正的计算,而只是生成了一个两头表达式(通过 type(df.shift(5))会发现它是一个 ArithExpression,而不是 DataFrame)。如果指定了 groupyby 的扩大参数 lazy=True,groupby 函数就不会对表达式计算后的后果进行分组。

在动量交易策略教程中,咱们就充分利用了这个扩大性能,来实现 DolphinDB 的 context by。

  1. 如果 Orca 目前无奈解决我的问题,我该怎么做?

本文解释了诸多 Orca 与 pandas 的差别,以及 Orca 的一些限度。如果你无奈躲避这些限度(比方,Orca 的函数不反对某个参数,或者,apply 一个简单的自定义函数,其中包含了第三方库函数调用,DolphinDB 中没有这些性能),那么,你能够将 Orca 的 DataFrame/Series 通过 to_pandas 函数转化为 pandas 的 DataFrame/Series,通过 pandas 执行计算后,将计算结果转换回 Orca 对象。

比方,Orca 目前不反对 rank 函数的 method=”average” 和 na_option=”keep” 参数,如果你必须应用这些参数,你能够这么做:

>>> df.rank(method='average', na_option='keep')
ValueError: method must be 'min'

>>> pdf = df.to_pandas()
>>> rank = pdf.rank(method='average', na_option='keep')
>>> rank = orca.DataFrame(rank)

这样做能够解决你的问题,但它带来了额定的网络通信,同时,新的 DataFrame 的底层存储的表不再是原先的 DataFrame 所示意的表,因而无奈执行针对同一个 DataFrame 操作的一些优化。

退出移动版