DuckDB + Polars:在A股数据存储和分析中的应用

DuckDB + Polars:在A股数据存储和分析中的应用

本文从基础到高级应用全面介绍了DuckDB在A股数据存储和分析中的应用。内容涵盖DuckDB的基础概念、安装配置,A股数据获取与预处理,存储管理,SQL查询基础,数据分析与挖掘,以及性能优化与高级特性等多个方面。
文中内容仅限技术学习与代码实践参考,市场存在不确定性,技术分析需谨慎验证,不构成任何投资建议。适合量化新手建立系统认知,为策略开发打下基础。

一、DuckDB 基础

(一)DuckDB 简介与安装配置

1. DuckDB 简介

在数据分析领域,高效的数据库管理系统是处理和分析大规模数据的关键工具。DuckDB 应运而生,它是一款开源的嵌入式关系型数据库管理系统,凭借其高性能的查询处理能力,在数据分析领域迅速崭露头角。

DuckDB 的核心优势在于能够高效地执行复杂的 SQL 查询,这得益于其先进的查询优化器和执行引擎。它不仅支持多种数据类型,包括对 JSON、Parquet 等格式数据的处理,还能够轻松应对结构化数据的快速分析和处理需求,这使得 DuckDB 在金融、互联网、科研等众多领域具有广泛的应用前景。

特别是在金融数据分析方面,DuckDB 能够快速处理海量的交易记录、市场行情数据等,为投资决策提供有力支持。例如,在 A 股市场分析中,它可以快速计算股票的收益率、波动率等关键指标,帮助分析师做出更准确的市场预测。

2. 配置 DuckDB 的运行环境

为了能够顺利地在你的应用程序中使用 DuckDB,进行基本的配置是必不可少的。首先,你需要安装 DuckDB 的 Python 库,这可以通过 Python 的包管理工具 pip 来轻松完成。在终端中执行以下命令:

pip install duckdb

安装完成后,你可以通过以下简单代码来验证是否安装成功:

import duckdb
conn = duckdb.connect()
print(conn.execute("SELECT 1+1").fetchone())  # 输出应为(2,)

(二)DuckDB 的核心概念

1. 表、数据库、模式等基本概念

在关系型数据库的世界里,数据的组织结构有着明确的层次和规范,DuckDB 也不例外。

表(Table)

表是 DuckDB 中最基本的数据存储单位,它由行和列组成,就像一个二维表格。每一列都有一个特定的数据类型,如整数、浮点数、字符串等,用于定义该列可以存储的数据种类。例如,在 A 股市场数据中,我们可以创建一个表来存储股票的基本信息,包括股票代码、股票名称、所属行业等字段,这些字段的数据类型分别为字符串、字符串和字符串。

数据库(Database)

数据库是一个容器,用于存储和管理多个表。在 DuckDB 中,你可以创建一个数据库文件,将相关的表存储在其中。例如,你可以创建一个名为stock_analysis.duckdb的数据库,用于存储所有与 A 股市场分析相关的表,如股票日线数据表、财务数据表等,这样可以方便地对这些数据进行统一管理和查询。

模式(Schema)

模式在 DuckDB 中用于对数据库中的对象进行逻辑分组和组织。随着数据量和数据种类的增加,模式可以帮助你将相关的表、视图等数据库对象归类到一起,便于管理和访问。例如,在一个较大的 A 股市场分析项目中,你可以创建不同的模式来分别存储股票基础信息、市场交易数据、宏观经济数据等,使整个数据库的结构更加清晰合理。

2. 数据类型与基本操作

DuckDB 支持多种数据类型,以满足不同类型数据的存储需求。

  • 整数类型:如 TINYINT、SMALLINT、INTEGER、BIGINT,用于存储整数数据。例如,股票的交易数量可以用整数类型来表示。
  • 浮点数类型:如 REAL、DOUBLE,用于存储小数或浮点数。像股票的收盘价、开盘价等通常需要用浮点数类型来精确表示。
  • 字符串类型:如 VARCHAR,用于存储文本数据。股票名称、所属行业等文本信息适合用字符串类型存储。
  • 日期和时间类型:如 DATE、TIME、TIMESTAMP,用于存储日期和时间信息。交易日期、时间戳等数据可以使用这些类型来准确记录。
  • 布尔类型:BOOLEAN,用于存储真值(TRUE 或 FALSE)。例如,判断某只股票是否满足特定条件,可以用布尔类型来表示结果。

在 DuckDB 中,你可以使用 SQL 语句来创建表、插入数据、查询数据等基本操作,从而实现对数据的管理和分析。例如,创建一个存储 A 股股票基本信息的表:

CREATE TABLE stock_basic (
    ts_code VARCHAR,  -- 股票代码
    name VARCHAR,     -- 股票名称
    industry VARCHAR  -- 所属行业
);

插入数据到表中:

INSERT INTO stock_basic (ts_code, name, industry) VALUES
('600000.SH', '浦发银行', '银行'),
('600016.SH', '民生银行', '银行'),
('600019.SH', '宝钢股份', '钢铁');

查询表中的数据:

SELECT * FROM stock_basic;

通过以上操作,你可以开始在 DuckDB 中构建自己的数据存储结构,并进行简单的数据操作,为后续深入的数据分析奠定基础。

二、数据获取与预处理

(一)使用 Tushare 获取 A 股数据

1. 安装 Tushare 库

在 Python 环境中,通过 pip 安装 Tushare 库 (https://tushare.pro/)是极其方便的。只需在终端中执行以下命令,即可完成安装:

pip install tushare

安装完成后,你可以通过以下代码来验证是否安装成功:

import tushare as ts
print(ts.__version__)  # 输出应为已安装的 Tushare 版本号

3. 获取 A 股股票数据

获取股票基本信息

获取股票基本信息是进行 A 股市场分析的基础。以下代码示例展示了如何使用 Tushare 获取 A 股股票的基本信息,并进行了简单的错误处理,以确保数据获取的可靠性:

import tushare as ts

# 设置你的 Tushare 接口码
ts.set_token('your_token')
pro = ts.pro_api()

try:
    # 获取 A 股股票基本信息
    df = pro.stock_basic(
        exchange="", list_status="L", 
        fields="ts_code,symbol,name,area,industry,list_date"
    )
    print("获取股票基本信息成功!")
    print(df.head())
except Exception as e:
    print("获取股票基本信息失败:", e)

在上述代码中,我们使用了 try-except 语句来捕获可能出现的异常情况,如网络问题、接口调用错误等,并输出相应的错误信息,方便进行问题排查。

获取股票日线数据

股票日线数据是分析股票价格走势的重要依据。以下代码示例展示了如何获取特定股票在指定日期范围内的日线数据:

# 获取特定股票的日线数据
try:
    df = pro.daily(ts_code="600000.SH", start_date="20200101", end_date="20250101")
    print("获取日线数据成功!")
    print(df.head())
except Exception as e:
    print("获取日线数据失败:", e)

同样,我们添加了错误处理机制,以确保在数据获取过程中出现问题时能够及时发现并处理。

(二)使用 Polars 进行数据处理

1. Polars 数据框的基本操作

在获取到原始数据后,通常需要对其进行一系列的处理,如数据清洗、转换、合并等,以使其更适合后续的分析和建模。Polars 是一个高性能的数据处理库,特别适用于大规模数据分析,能够高效地完成这些任务。

安装 Polars

安装 Polars 同样简单,只需在终端中执行以下命令:

pip install polars

安装完成后,可以通过以下代码验证安装是否成功:

import polars as pl
print(pl.__version__)  # 输出应为已安装的 Polars 版本号
创建 DataFrame

DataFrame 是 Polars 中最基本的数据结构,用于存储表格型数据。以下示例展示了如何从字典创建 DataFrame:

import polars as pl

# 从字典创建 DataFrame
df = pl.DataFrame(
    {
        "ts_code": ["000001.SZ", "000002.SZ"],
        "name": ["平安银行", "万科A"],
        "industry": ["银行", "全国地产"],
        "list_date": ["19910403", "19910129"],
    }
)
print(df)

通过这种方式,你可以轻松地将各种数据源转换为 Polars 的 DataFrame 格式,为后续的数据处理做好准备。

数据选择与过滤

在数据分析中,经常需要从数据集中选择特定的列或筛选出符合条件的行。Polars 提供了简单而强大的方法来实现这些操作。

# 选择特定列
selected_df = df.select(["ts_code", "name"])
print("选择特定列后的数据:")
print(selected_df)

# 条件过滤
filtered_df = df.filter(pl.col("industry") == "银行")
print("条件过滤后的数据:")
print(filtered_df)

通过 select 方法,你可以指定需要保留的列名,从而提取出感兴趣的部分数据。而 filter 方法则允许你根据指定的条件筛选出行数据,例如选择某一特定行业的股票。

2. 数据清洗与转换

处理缺失值

在实际数据中,缺失值是一个常见的问题,需要根据具体情况采取适当的处理方法。Polars 提供了方便的函数来处理缺失值。

# 将指定列缺失值替换为指定值
df = df.with_columns(pl.col("industry").fill_null("未知行业"))
print("处理缺失值后的数据:")
print(df)

上述代码将 “industry” 列中的缺失值替换为 “未知行业”,确保数据的完整性,以便后续分析的准确性。

数据类型转换

为了更好地进行数据分析,有时需要将数据转换为合适的数据类型。例如,将日期格式的字符串转换为日期类型,以便进行时间相关的操作。

# 将某一列转换为特定数据类型
df = df.with_columns(pl.col("list_date").str.to_date("%Y%m%d"))
print("数据类型转换后的数据:")
print(df)

通过 str.to_date 方法,我们将 “list_date” 列从字符串类型转换为日期类型,这将使我们能够更方便地进行日期相关的计算和分析,如计算股票上市时间等。

3. 数据合并与分组操作

数据合并

在数据分析过程中,常常需要将来自不同数据源的数据进行合并,以获得更全面的信息。Polars 的 join 方法提供了强大的数据合并功能。

# 合并两个 DataFrame
df1 = pl.DataFrame({"ts_code": ["600000.SH"], "pe": [5.2]})
df2 = pl.DataFrame({"ts_code": ["600000.SH"], "pb": [0.8]})
merged_df = df1.join(df2, on="ts_code")
print("合并后的数据:")
print(merged_df)

在上述示例中,我们将两个 DataFrame 按照 “ts_code” 列进行合并,得到了包含市盈率(pe)和市净率(pb)信息的数据集,为后续的财务分析提供了更丰富的数据支持。

分组与聚合

分组与聚合是数据分析中常用的手段,用于对数据进行分类汇总,以发现不同类别之间的规律和特征。

# 按行业分组并计算平均市盈率
df = pro.bak_basic(start_date="20250101")
df = pl.from_dataframe(df)

grouped_df = df.group_by("industry").agg(pl.col("pe").mean().alias("avg_pe"))
print("分组与聚合后的数据:")
print(grouped_df)

通过 group_byagg 方法,我们按照 “industry” 列对数据进行分组,并计算了每个行业股票的平均市盈率,这有助于我们了解不同行业的整体估值水平,为投资决策提供参考依据。

以上内容为你详细介绍了如何使用 Tushare 获取 A 股数据,以及如何利用 Polars 进行数据处理的基本操作。这些步骤和方法是 A 股市场数据分析的基础,掌握它们将为你后续深入的数据分析和挖掘打下坚实的基础。

三、数据存储与管理

(一)Parquet 文件格式与分区存储

1. Parquet 文件格式的特点与优势

在大数据处理领域,选择合适的文件格式对于提高数据存储和查询效率至关重要。Parquet 是一种被广泛采用的列式存储格式,它具有以下显著特点和优势:

  • 列式存储:Parquet 按列存储数据,这种存储方式在查询特定列的数据时,能够显著减少数据读取量,从而提高查询性能。例如,在分析股票数据时,如果只关心收盘价和交易日期,列式存储可以快速定位并读取这些列的数据,而无需扫描整个行记录。

  • 压缩友好:Parquet 支持多种压缩编码,如 Snappy、GZIP 等。通过压缩,可以有效减少数据的存储空间,同时在读取数据时,压缩和解压缩的操作通常在内存中进行,不会对查询性能产生明显的负面影响。

  • 数据类型丰富:Parquet 支持多种数据类型,包括整数、浮点数、字符串、布尔值等,能够满足复杂数据结构的存储需求。例如,在存储股票基本信息时,可以同时保存股票代码(字符串类型)、市盈率(浮点数类型)、是否上市(布尔类型)等不同类型的字段。

  • 可分割性:Parquet 文件具有良好的可分割性,这使得它非常适合在分布式环境中进行并行处理。例如,在使用 DuckDB 进行大规模数据查询时,可以将 Parquet 文件分割成多个部分,每个部分由不同的处理器核心同时处理,从而加速查询执行。

2. 将处理后的 A 股数据存储为 Parquet 格式

在完成数据获取和预处理后,将数据存储为 Parquet 格式是一个明智的选择,以便于后续的高效查询和分析。以下示例展示了如何使用 Polars 将数据保存为 Parquet 格式:

import polars as pl

# 获取股票行情数据
df = pro.daily(ts_code="600000.SH, 600016.SH", start_date="20250101")
df = pl.from_pandas(df)

# 将数据写入 Parquet 文件
df.write_parquet("./data/stock_data.parquet")

# 获取股票历史列表
df1 = pro.bak_basic(ts_code="600000.SH", start_date="20250101")
df1 = pl.from_pandas(df1)
df2 = pro.bak_basic(ts_code="600016.SH", start_date="20250101")
df2 = pl.from_pandas(df2)
df3 = df1.vstack(df2)

# 将数据写入 Parquet 文件
df3.write_parquet("./data/stock_basic.parquet")

通过上述代码,数据被保存到指定路径的 Parquet 文件中。在存储过程中,可以指定压缩算法等参数,以优化存储性能。例如,添加 compression="snappy" 参数可以使用 Snappy 压缩算法对数据进行压缩存储。

3. 按照日期、股票代码等进行分区存储的策略与实现

对于大规模的 A 股数据,按照一定的规则进行分区存储可以进一步提高数据管理的效率。分区存储将数据按照一个或多个维度划分为多个子集,每个子集存储在一个单独的目录中。常见的分区策略包括按日期分区、按股票代码分区等。

以下示例展示了如何按照日期和股票代码进行分区存储:

# 创建分区路径
df.write_parquet("partitioned_stock_data", partition_by=["ts_code", "trade_date"])

执行上述代码后,数据将根据 “ts_code” 和 “trade_date” 列的值被划分到不同的分区目录中。例如,股票代码为 “600000.SH” 且交易日期为 “20230101” 的数据将被存储在 “partitioned_stock_data/ts_code=600000.SH/trade_date=20230101” 路径下。

分区存储的优势在于能够快速定位和查询特定分区内的数据,减少不必要的数据扫描。例如,在查询某只股票在某一时间段内的数据时,可以直接访问相应的分区目录,提高查询效率。

(二)DuckDB 与外部数据源的集成

1. DuckDB 读取 Parquet 文件的方法

DuckDB 提供了便捷的方法来读取 Parquet 文件,从而能够轻松地将外部数据引入到其查询环境中。以下示例展示了如何使用 DuckDB 读取 Parquet 文件:

import duckdb

# 读取 Parquet 文件
df = duckdb.read_parquet("./data/stock_data.parquet").fetchdf()

print(df.head())

在上述代码中,我们首先建立与 DuckDB 的连接,然后使用 read_parquet 函数指定 Parquet 文件的路径,将其数据读取到 DuckDB 的查询环境中,并通过 fetchdf 方法将结果以 Pandas DataFrame 的形式返回,便于后续操作。

2. 将 Parquet 分区存储的数据导入 DuckDB

对于分区存储的 Parquet 数据,DuckDB 同样能够方便地进行读取。以下示例展示了如何读取分区存储的数据:

# 读取分区存储的数据
df = duckdb.read_parquet(
    "./partitioned_stock_data/ts_code=*/trade_date=*/*.parquet"
).fetchdf()

print(df.head())

在查询语句中,我们使用通配符 * 来匹配分区目录下的所有文件,从而将整个分区的数据读取出来。这种方式能够充分利用分区存储的优势,快速获取所需的数据子集。

3. 通过 DataFrame 创建 DuckDB 表

在将数据读取到 DuckDB 后,通常需要将其存储为 DuckDB 的表,以便于进行更复杂的查询和分析操作。以下示例展示了如何通过 DataFrame 创建 DuckDB 表:


# 连接数据文件
conn = duckdb.connect("./data/mydb.duckdb")

# 行情数据创建表
conn.execute("DROP TABLE IF EXISTS stock_data")
conn.execute("CREATE TABLE stock_data AS SELECT * FROM df")

# 股票历史列表创建表
conn.execute("DROP TABLE IF EXISTS stock_basic")
conn.execute("CREATE TABLE stock_basic AS SELECT * FROM df")

# 关闭连接
conn.close()

在上述代码中,我们首先获取数据并将其转换为 Polars DataFrame,然后保存为 Parquet 文件。接着,连接到 DuckDB 数据库,并使用 CREATE TABLE ... AS SELECT 语句根据 DataFrame 创建表。这种方式能够自动推断数据类型,并将数据高效地存储到 DuckDB 表中,为后续的 SQL 查询和分析做好准备。

通过以上内容,你已经学习了如何将 A 股数据存储为 Parquet 格式,以及如何利用 DuckDB 与外部数据源进行集成,包括读取 Parquet 文件和创建 DuckDB 表等操作。这些技能将帮助你在实际的数据分析项目中更好地管理和利用数据,提高工作效率和分析质量。

四、DuckDB SQL 查询基础

(一)SQL 基础语法在 DuckDB 中的应用

1. SELECT、FROM、WHERE 等基本语句的使用

在数据分析过程中,熟练掌握 SQL 查询语句是必不可少的技能。DuckDB 支持标准的 SQL 语法,使得用户能够灵活地进行数据查询和操作。

首先,SELECT 语句用于指定要查询的列。你可以选择全部列或特定的列,这取决于分析需求。FROM 语句则用于指定数据来源的表。当需要从多个表中获取数据时,可以结合 JOIN 语句来实现表之间的连接。

WHERE 语句是一个强大的工具,用于对查询结果进行条件筛选。通过 WHERE,你可以根据特定的条件(如数值范围、字符串匹配等)来过滤数据,从而获取感兴趣的数据子集。

以下是一些基本的 SQL 语句示例:

import duckdb

# 连接到 DuckDB
conn = duckdb.connect("./data/mydb.duckdb")

# 查询所有数据
result = conn.execute("SELECT * FROM stock_data").fetchdf()
print(result.head())

# 查询特定列
result = conn.execute("SELECT ts_code, trade_date, close FROM stock_data").fetchdf()
print(result.head())

# 使用 WHERE 子句筛选数据
result = conn.execute("SELECT * FROM stock_data WHERE ts_code = '600000.SH'").fetchdf()
print(result.head())

# 使用 LIMIT 限制返回的行数
result = conn.execute("SELECT * FROM stock_data LIMIT 10").fetchdf()
print(result)

在实际应用中,根据分析需求灵活组合这些基本语句,可以实现各种复杂的数据查询操作。

2. 数据过滤、排序与分组查询

数据过滤

数据过滤是数据分析中至关重要的一步,它允许你从大量数据中提取出符合特定条件的记录。除了基本的等值条件过滤外,还可以使用各种比较运算符(如 >、<、>=、<= 等)和逻辑运算符(如 AND、OR、NOT 等)来构建复杂的过滤条件。

# 筛选收盘价大于 10 的股票
result = conn.execute("SELECT * FROM stock_data WHERE close > 10").fetchdf()
print(result.head())

# 筛选特定日期范围内的数据
result = conn.execute(
    "SELECT * FROM stock_data WHERE trade_date BETWEEN '20230101' AND '20230131'"
).fetchdf()
print(result.head())
排序

对查询结果进行排序可以帮助你更好地理解和分析数据趋势。使用 ORDER BY 子句,你可以按照一个或多个列的值进行升序或降序排列。

# 按收盘价降序排列
result = conn.execute("SELECT * FROM stock_data ORDER BY close DESC").fetchdf()
print(result.head())

# 按交易日期升序排列
result = conn.execute("SELECT * FROM stock_data ORDER BY trade_date ASC").fetchdf()
print(result.head())
分组查询

分组查询是数据分析中用于汇总和比较不同组别数据的重要手段。通过 GROUP BY 子句,你可以将数据按照一个或多个列的值进行分组,并结合聚合函数对每个组进行统计计算。

# 按股票代码分组,计算每只股票的平均收盘价
result = conn.execute("SELECT ts_code, AVG(close) AS avg_close FROM stock_data GROUP BY ts_code").fetchdf()
print(result.head())

# 按行业分组,计算每个行业的股票数量
result = conn.execute("SELECT industry, COUNT(*) AS stock_count FROM stock_basic GROUP BY industry").fetchdf()
print(result.head())

3. 聚合函数的应用

聚合函数在数据分析中扮演着核心角色,它们能够对一组值进行计算并返回单一结果,常用于统计分析和数据汇总。

# 计算所有股票的平均收盘价
result = conn.execute("SELECT AVG(close) AS overall_avg_close FROM stock_data").fetchdf()
print(result)

# 计算每只股票的最高收盘价
result = conn.execute("SELECT ts_code, MAX(close) AS max_close FROM stock_data GROUP BY ts_code").fetchdf()
print(result.head())

# 计算每个行业的股票总数和平均市盈率
result = conn.execute("SELECT industry, COUNT(*) AS stock_count, AVG(pe) AS avg_pe FROM stock_basic GROUP BY industry").fetchdf()
print(result.head())

通过合理运用聚合函数,可以快速获取数据的统计特征,为后续的决策和分析提供有力支持。

(二)针对 A 股数据的查询实践

1. 查询特定股票的历史价格走势

分析特定股票的历史价格走势是投资决策的重要依据。通过查询和计算,可以获得股票的价格变化趋势、收益率等关键指标。

# 查询浦发银行的历史价格数据
result = conn.execute("SELECT trade_date, close FROM stock_data WHERE ts_code = '600000.SH' ORDER BY trade_date").fetchdf()
print(result.head())

# 计算浦发银行的收益率
result = conn.execute("""
    SELECT
        trade_date,
        close,
        close / LAG(close, 1) OVER (ORDER BY trade_date) - 1 AS return
    FROM stock_data
    WHERE ts_code = '600000.SH'
    ORDER BY trade_date
""").fetchdf()
print(result.head())

2. 筛选符合特定财务指标的股票

根据财务指标筛选股票是价值投资的重要方法。通过设定市盈率、市净率等指标的范围,可以快速找出具有投资潜力的股票。

# 筛选市盈率小于 20 且市净率小于 2 的股票
result = conn.execute("""
    SELECT ts_code, name, pe, pb
    FROM stock_basic
    WHERE pe < 20 AND pb < 2
""").fetchdf()
print(result.head())

3. 计算股票的收益率、波动率等指标

收益率和波动率是评估股票风险和收益的重要指标。通过计算这些指标,可以对股票的投资价值进行更全面的评估。

# 计算每只股票的收益率
result = conn.execute("""
    SELECT
        ts_code,
        trade_date,
        close,
        close / LAG(close, 1) OVER (PARTITION BY ts_code ORDER BY trade_date) - 1 AS return
    FROM stock_data
""").fetchdf()
print(result.head())

# 计算每只股票的收益率波动率
result = conn.execute("""
    SELECT
        ts_code,
        STDDEV(return) AS volatility
    FROM (
        SELECT
            ts_code,
            close / LAG(close, 1) OVER (PARTITION BY ts_code ORDER BY trade_date) - 1 AS return
        FROM stock_data
    ) sub
    GROUP BY ts_code
""").fetchdf()
print(result.head())

以上内容详细介绍了如何在 DuckDB 中运用 SQL 查询基础语法,结合 A 股数据进行各种查询操作和分析实践。通过这些方法,你可以深入挖掘数据价值,为投资决策提供有力支持。

五、数据分析与挖掘

1. 描述性统计分析

在进行深入的数据分析之前,对数据进行描述性统计分析是必不可少的一步。这有助于我们了解数据的基本特征,如数据的集中趋势、离散程度、分布形态等,从而为后续的分析和建模打下坚实的基础。

# 计算股票收盘价的描述性统计
result = conn.execute("""
    SELECT
        AVG(close) AS avg_close,
        MEDIAN(close) AS median_close,
        STDDEV(close) AS std_close,
        MIN(close) AS min_close,
        MAX(close) AS max_close
    FROM stock_data
""").fetchdf()

print(result)

通过上述查询,我们可以获取股票收盘价的平均值、中位数、标准差、最小值和最大值等关键统计指标。这些指标能够帮助我们快速了解股票价格的整体水平和波动情况。例如,标准差较大的股票通常具有较高的波动性,可能伴随着更高的风险和收益潜力。

2. 相关性分析

相关性分析是研究不同变量之间线性关系强弱的重要手段。在 A 股市场分析中,通过计算不同股票价格之间的相关性,可以了解它们的联动关系,这对于构建投资组合、分散风险具有重要意义。

# 计算浦发银行和民生银行收盘价的相关性
result = conn.execute("""
    SELECT
        CORR(s1.close, s2.close) AS correlation
    FROM
        stock_data AS s1
    JOIN
        stock_data AS s2
    ON
        s1.trade_date = s2.trade_date
    WHERE
        s1.ts_code = '600000.SH' AND s2.ts_code = '600016.SH'
""").fetchdf()

print(result)

上述查询计算了浦发银行和民生银行在相同交易日期下的收盘价相关性。相关系数的取值范围在 -1 到 1 之间,接近 1 表示正相关性较强,即两只股票的价格走势相似;接近 -1 则表示负相关性较强,价格走势相反;接近 0 则表示相关性较弱,价格走势无明显关联。

3. 时间序列分析

时间序列分析是研究数据随时间变化规律的一种方法,在金融数据分析中应用广泛。通过对股票价格进行时间序列分析,可以观察其趋势、季节性变化以及周期性波动等特征,从而为预测未来价格走势提供依据。

# 计算浦发银行的滚动平均收盘价
result = conn.execute("""
    SELECT
        trade_date,
        close,
        AVG(close) OVER (
            ORDER BY trade_date
            ROWS BETWEEN 30 PRECEDING AND CURRENT ROW
        ) AS rolling_avg_close
    FROM stock_data
    WHERE ts_code = '600000.SH'
    ORDER BY trade_date
""").fetchdf()
print(result.head())

在上述查询中,我们计算了浦发银行收盘价的 30 日滚动平均值。滚动平均可以平滑数据的短期波动,突出长期趋势,帮助我们更清晰地观察股票价格的整体走势。通过分析滚动平均线的变化,可以判断股票价格是处于上升趋势、下降趋势还是盘整阶段。

以上内容展示了如何使用 DuckDB 进行 A 股数据的描述性统计分析、相关性分析和时间序列分析。这些分析方法能够帮助你从不同角度深入了解数据,为投资决策提供有力支持。

六、性能优化与高级特性

1. 索引的创建与使用

虽然 DuckDB 中索引功能相对传统数据库较为有限,但在某些场景下合理使用索引仍可显著提升查询效率。对于频繁用于查询过滤的列,如 A 股市场数据中的股票代码(ts_code)列,创建索引是优化查询性能的有效手段。

# 创建索引
conn.execute("CREATE INDEX idx_ts_code ON stock_data(ts_code)")

通过上述语句,DuckDB 将在 stock_data 表的 ts_code 列上创建一个索引,命名为 idx_ts_code。创建索引后,当执行涉及 ts_code 列的查询时,DuckDB 可以利用该索引快速定位符合条件的行,从而减少数据扫描的范围,提高查询速度。

需要注意的是,索引的创建和维护会占用一定的存储空间,并且在数据插入、更新或删除时会增加一定的开销。因此,在决定是否创建索引时,需要综合考虑查询性能提升和数据写入性能的影响,权衡利弊。

2. 查询优化

优化 SQL 语句的结构和逻辑是提升查询性能的关键步骤。一个良好设计的查询语句不仅能够提高查询效率,还能减少系统资源的消耗。以下是一些具体的优化建议:

  • 避免使用 SELECT *:尽量只选择需要的列,避免不必要的数据扫描。这可以减少数据读取量和内存使用,从而提高查询速度。

    # 优化前
    result = conn.execute("SELECT * FROM stock_data WHERE ts_code = '600000.SH'").fetchdf()
    
    # 优化后
    result = conn.execute("SELECT trade_date, close FROM stock_data WHERE ts_code = '600000.SH'").fetchdf()
    
  • 减少子查询的使用:子查询在某些情况下可能会导致性能下降,尤其是在处理大规模数据时。如果可能,尝试使用 JOIN 或其他替代方法来优化查询逻辑。

  • 合理使用聚合函数和分组:在使用聚合函数时,尽量减少不必要的分组和计算。确保只对需要的列进行聚合操作,并避免在 GROUP BY 子句中包含过多的列。

通过以上优化措施,可以显著提高 SQL 查询的性能,特别是在处理大规模数据集时,效果更为明显。

3. 并行计算与分布式处理

DuckDB 支持并行查询处理,能够充分利用多核 CPU 的计算能力。对于大规模的 A 股市场数据,开启并行处理可以大幅提高查询性能。

# 启用并行处理
conn.execute("SET threads=4")  # 设置使用 4 个线程

上述代码通过设置 PRAGMA threads 参数来指定 DuckDB 使用的线程数量。根据服务器的 CPU 核心数合理设置该参数,可以充分利用硬件资源,加速查询执行。

此外,对于超出单机处理能力的超大规模数据集,可以考虑将 DuckDB 与分布式计算框架(如 Apache Spark)结合使用。通过分布式处理,可以将数据分割成多个部分,分布在集群的各个节点上进行并行处理,进一步提升处理效率。不过,这种架构的搭建和维护相对复杂,需要根据实际需求和资源情况进行权衡。

4. 高级查询优化技巧

除了基本的查询优化方法外,还有一些更高级的技巧可以帮助进一步提升查询性能,特别是在处理复杂查询和大规模数据时。

子查询优化

子查询在某些情况下可能会导致性能下降,尤其是在处理大规模数据时。如果可能,尝试使用 JOIN 或其他替代方法来优化查询逻辑。

# 原始查询使用子查询
result = conn.execute("""
    SELECT *
    FROM stock_data
    WHERE ts_code IN (SELECT ts_code FROM stock_basic WHERE industry = '银行')
""").fetchdf()

# 优化后使用 JOIN
result = conn.execute("""
    SELECT stock_data.*
    FROM stock_data
    JOIN stock_basic ON stock_data.ts_code = stock_basic.ts_code
    WHERE stock_basic.industry = '银行'
""").fetchdf()

通过将子查询替换为 JOIN,可以利用数据库的查询优化器更高效地执行查询计划,减少不必要的数据扫描和计算。

窗口函数优化

窗口函数在数据分析中非常强大,但使用不当可能会导致性能问题。合理使用窗口函数并结合分区和排序优化,可以显著提高查询效率。

# 计算每只股票的滚动平均收盘价
result = conn.execute("""
    SELECT
        trade_date,
        close,
        AVG(close) OVER (
            PARTITION BY ts_code
            ORDER BY trade_date
            ROWS BETWEEN 30 PRECEDING AND CURRENT ROW
        ) AS rolling_avg_close
    FROM stock_data
""").fetchdf()

在上述查询中,通过使用 PARTITION BY 对每只股票分别计算滚动平均值,避免了对整个数据集的重复扫描,提高了查询性能。

查询缓存

对于频繁执行且数据变化不频繁的查询,可以考虑使用查询缓存机制。DuckDB 支持将查询结果缓存到内存中,以便后续相同的查询可以直接从缓存中获取结果,而无需重新执行查询。

# 启用查询缓存
conn.execute("SET memory_limit='1024MB';") # 设置缓存大小为 1024 MB

# 执行查询并缓存结果
result = conn.execute("""
    SELECT * FROM stock_data WHERE ts_code = '600000.SH'
""").fetchdf()

通过合理设置缓存大小和启用缓存机制,可以减少重复查询的执行时间,提高整体系统性能。

以上内容详细介绍了 DuckDB 在性能优化与高级特性方面的多种方法和技巧。通过合理运用这些优化措施,可以显著提升数据处理和查询的效率,特别是在处理大规模 A 股市场数据时,能够为数据分析和挖掘提供更强大的支持。

附录

配置参考 (Configuration Reference)

配置选项具有不同的默认范围:GLOBAL 和 LOCAL。以下是按范围列出的所有可用配置选项的列表。

全局配置选项(Global Configuration Options)

名称描述类型默认值
Calendar当前日历VARCHAR系统(本地)日历
TimeZone当前时区VARCHAR系统(本地)时区
access_mode数据库的访问模式(AUTOMATICREAD_ONLYREAD_WRITEVARCHARautomatic
allocator_background_threads是否启用分配器后台线程BOOLEANfalse
allocator_bulk_deallocation_flush_threshold如果发生大于此值的大规模释放,则刷新未完成的分配VARCHAR512.0 MiB
allocator_flush_threshold完成任务后,达到此峰值分配阈值时刷新分配器VARCHAR128.0 MiB
allow_community_extensions是否允许加载社区构建的扩展BOOLEANtrue
allow_extensions_metadata_mismatch是否允许加载元数据不兼容的扩展BOOLEANfalse
allow_persistent_secrets是否允许创建持久化密钥,这些密钥会在重启时存储和加载BOOLEANtrue
allow_unredacted_secrets是否允许打印未脱敏的密钥BOOLEANfalse
allow_unsigned_extensions是否允许加载具有无效或缺失签名的扩展BOOLEANfalse
allowed_directories始终允许查询的目录/前缀列表,即使在 enable_external_accessfalseVARCHAR[][]
allowed_paths始终允许查询的文件列表,即使在 enable_external_accessfalseVARCHAR[][]
arrow_large_buffer_size是否使用大缓冲区导出 Arrow 格式的字符串、二进制、UUID 和位数据BOOLEANfalse
arrow_lossless_conversion当 DuckDB 类型在 Arrow 中没有明确的原生或规范扩展匹配时,是否以 duckdb.type_name 扩展名导出类型BOOLEANfalse
arrow_output_list_view是否在导出到 Arrow 格式时使用 ListView 作为 LIST 列的物理布局BOOLEANfalse
autoinstall_extension_repository覆盖扩展安装的自定义端点(自动加载时)VARCHAR
autoinstall_known_extensions是否允许在查询依赖扩展时自动安装已知扩展BOOLEANtrue
autoload_known_extensions是否允许在查询依赖扩展时自动加载已知扩展BOOLEANtrue
binary_as_string在 Parquet 文件中,是否将二进制数据解释为字符串BOOLEAN
ca_cert_file自签名证书的自定义证书文件路径VARCHAR
catalog_error_max_schemas系统在目录中扫描“您是否想……”风格错误的最大模式数量UBIGINT100
checkpoint_threshold, wal_autocheckpoint自动触发检查点的 WAL 大小阈值(例如 1GB)VARCHAR16.0 MiB
custom_extension_repository覆盖远程扩展安装的自定义端点VARCHAR
custom_user_agentDuckDB 调用者的元数据VARCHAR
default_block_size新 DuckDB 数据库文件的默认块大小(仅针对尚未存在的文件)UBIGINT262144
default_collation未指定时使用的排序规则设置VARCHAR
default_null_order, null_order未指定时使用的 NULL 排序方式(NULLS_FIRSTNULLS_LASTVARCHARNULLS_LAST
default_order未指定时使用的排序类型(ASCDESCVARCHARASC
default_secret_storage切换密钥的默认存储方式VARCHARlocal_file
disable_parquet_prefetching禁用 Parquet 的预取机制BOOLEANfalse
disabled_compression_methods禁用特定的压缩方法(逗号分隔)VARCHAR
disabled_filesystems禁用特定文件系统以防止访问(例如 LocalFileSystem)VARCHAR
disabled_log_types设置禁用的日志记录器列表VARCHAR
duckdb_apiDuckDB API 表面VARCHARcli
enable_external_access是否允许数据库访问外部状态(例如通过加载/安装模块、COPY TO/FROM、CSV 读取器、pandas 替代扫描等)BOOLEANtrue
enable_fsst_vectors是否允许在 FSST 压缩段的扫描中使用压缩向量以利用延迟解压缩BOOLEANfalse
enable_geoparquet_conversion如果加载了空间扩展,则尝试在 GeoParquet 文件中解码/编码几何数据BOOLEANtrue
enable_http_metadata_cache是否使用全局 HTTP 元数据缓存 HTTP 元数据BOOLEANfalse
enable_logging是否启用日志记录器BOOLEAN0
enable_macro_dependencies是否允许创建的宏对引用的对象(例如表)创建依赖关系BOOLEANfalse
enable_object_cache[占位符] 已废弃,无实际作用BOOLEANNULL
enable_server_cert_verification是否启用服务器端证书验证BOOLEANfalse
enable_view_dependencies是否允许创建的视图对引用的对象(例如表)创建依赖关系BOOLEANfalse
enabled_log_types设置启用的日志记录器列表VARCHAR
extension_directory设置扩展存储的目录VARCHAR
external_threads用于处理 DuckDB 任务的外部线程数量UBIGINT1
force_download是否强制提前下载文件BOOLEANfalse
http_keep_alive保持连接。将其设置为 false 可以帮助解决连接失败问题BOOLEANtrue
http_proxy_passwordHTTP 代理密码VARCHAR
http_proxy_usernameHTTP 代理用户名VARCHAR
http_proxyHTTP 代理主机VARCHAR
http_retriesHTTP I/O 错误时的重试次数UBIGINT3
http_retry_backoff指数递增重试等待时间的退避因子FLOAT4
http_retry_wait_ms重试间隔时间(毫秒)UBIGINT100
http_timeoutHTTP 超时时间(读/写/连接/重试,单位:秒)UBIGINT30
immediate_transaction_mode是否在调用 BEGIN TRANSACTION 时立即启动事务,而不是按需延迟启动BOOLEANfalse
index_scan_max_count索引扫描的最大计数设置了一个阈值。如果匹配的行数少于 MAX(index_scan_max_count, index_scan_percentage * total_row_count),则执行索引扫描而不是表扫描UBIGINT2048
index_scan_percentage索引扫描的百分比设置了一个阈值。如果匹配的行数少于 MAX(index_scan_max_count, index_scan_percentage * total_row_count),则执行索引扫描而不是表扫描DOUBLE0.001
lock_configuration是否允许修改配置BOOLEANfalse
logging_level日志中记录的日志级别VARCHARINFO
logging_mode启用日志记录器VARCHARLEVEL_ONLY
logging_storage设置日志存储位置(内存/标准输出/文件)VARCHARmemory
max_memory, memory_limit系统的最大内存限制(例如 1GB)VARCHAR80% 的可用内存
max_temp_directory_size设置 temp_directory 中允许存储的最大数据量(例如 1GB)VARCHAR90% 的可用磁盘空间
max_vacuum_tasks在检查点期间计划的最大清理任务数量UBIGINT100
old_implicit_casting是否允许隐式转换为/从 VARCHAR 类型BOOLEANfalse
parquet_metadata_cache是否缓存 Parquet 元数据,这对于多次读取相同文件很有用BOOLEANfalse
password使用的密码。仅用于与旧版本兼容,实际上会被忽略VARCHARNULL
prefetch_all_parquet_files是否为所有类型的 Parquet 文件启用预取机制BOOLEANfalse
preserve_insertion_order是否保留插入顺序。如果设置为 false,系统可以重新排序未包含 ORDER BY 子句的结果BOOLEANtrue
produce_arrow_string_view是否以 Utf8View 格式而不是 Utf8 格式生成字符串BOOLEANfalse
s3_access_key_idS3 访问密钥 IDVARCHAR
s3_endpointS3 终端节点VARCHAR
s3_regionS3 区域VARCHARus-east-1
s3_secret_access_keyS3 访问密钥VARCHAR
s3_session_tokenS3 会话令牌VARCHAR
s3_uploader_max_filesizeS3 上传器的最大文件大小(50GB 到 5TB 之间)VARCHAR800GB
s3_uploader_max_parts_per_file每个文件的最大分片数量(1 到 10000 之间)UBIGINT10000
s3_uploader_thread_limitS3 上传器的全局线程限制UBIGINT50
s3_url_compatibility_mode禁用 S3 URL 上的通配符和查询参数BOOLEANfalse
s3_url_styleS3 URL 风格VARCHARvhost
s3_use_ssl是否在 S3 中使用 SSLBOOLEANtrue
secret_directory持久化密钥存储的目录VARCHAR~/.duckdb/stored_secrets
storage_compatibility_version在检查点时序列化,以兼容指定版本的 DuckDBVARCHARv0.10.2
temp_directory设置临时文件的存储目录VARCHAR<database_name>.tmp 或在内存模式下为 .tmp
threads, worker_threads系统使用的总线程数BIGINTCPU 核心数
username, user使用的用户名。仅用于与旧版本兼容,实际上会被忽略VARCHARNULL
zstd_min_string_length启用 ZSTD 压缩的平均字符串长度,默认为 4096UBIGINT4096

本地配置选项(Local Configuration Options)

名称描述类型默认值
custom_profiling_settings接收一个 JSON,用于启用自定义指标VARCHAR{"ROWS_RETURNED": "true", "LATENCY": "true", "RESULT_SET_SIZE": "true", "OPERATOR_TIMING": "true", "OPERATOR_ROWS_SCANNED": "true", "CUMULATIVE_ROWS_SCANNED": "true", "OPERATOR_CARDINALITY": "true", "OPERATOR_TYPE": "true", "OPERATOR_NAME": "true", "CUMULATIVE_CARDINALITY": "true", "EXTRA_INFO": "true", "CPU_TIME": "true", "BLOCKE...
dynamic_or_filter_threshold从哈希连接动态生成的 OR 过滤器的最大数量UBIGINT50
enable_http_logging是否启用 HTTP 日志记录BOOLEANfalse
enable_profiling启用分析,并设置输出格式(JSONQUERY_TREEQUERY_TREE_OPTIMIZERVARCHARNULL
enable_progress_bar_printenable_progress_bartrue 时,是否控制进度条的打印BOOLEANtrue
enable_progress_bar是否启用进度条,在长查询时将进度打印到终端BOOLEANtrue
errors_as_json是否以结构化的 JSON 格式输出错误消息,而不是原始字符串BOOLEANfalse
explain_outputEXPLAIN 语句的输出格式(ALLOPTIMIZED_ONLYPHYSICAL_ONLYVARCHARphysical_only
file_search_path输入文件的搜索目录列表,以逗号分隔VARCHAR
home_directory设置系统使用的主目录VARCHAR
http_logging_outputHTTP 日志输出保存的文件路径,为空时打印到终端VARCHAR
ieee_floating_point_ops是否使用 IEEE754 兼容的浮点运算(返回 NAN 而不是错误/NULL)BOOLEANtrue
integer_division是否将 / 运算符默认为整数除法,而不是浮点除法BOOLEANfalse
late_materialization_max_rowsLIMIT/SAMPLE 中触发延迟物化的最大行数UBIGINT50
log_query_path指定查询日志保存的路径(默认为 NULL,不记录查询)VARCHARNULL
max_expression_depth解析器中表达式的最大深度限制。警告:增加此设置并使用非常深的表达式可能会导致堆栈溢出错误UBIGINT1000
merge_join_threshold在选择合并连接时,任一表所需的行数UBIGINT1000
nested_loop_join_threshold在选择嵌套循环连接时,任一表所需的行数UBIGINT5
order_by_non_integer_literal是否允许按非整数字面量排序——按此类字面量排序没有任何效果BOOLEANfalse
ordered_aggregate_threshold在排序之前累积的行数,用于调优UBIGINT262144
partitioned_write_flush_threshold在使用 PARTITION_BY 写入时,达到此行数阈值后刷新线程状态UBIGINT524288
partitioned_write_max_open_files在使用 PARTITION_BY 写入时,系统在刷新到磁盘之前可以保持打开的最大文件数量UBIGINT100
perfect_ht_threshold使用完美哈希表的字节阈值UBIGINT12
pivot_filter_threshold切换使用过滤聚合与专用透视运算符的 LIST 的阈值UBIGINT20
pivot_limitPIVOT 语句中允许的最大透视列数UBIGINT100000
prefer_range_joins是否强制使用带有混合谓词的范围连接BOOLEANfalse
preserve_identifier_case是否保留标识符的大小写,而不是始终将所有未加引号的标识符转换为小写BOOLEANtrue
profile_output, profiling_output分析输出保存的文件路径,为空时打印到终端VARCHAR
profiling_mode分析模式(STANDARDDETAILEDVARCHARNULL
progress_bar_time设置查询需要运行多长时间(以毫秒为单位)才会开始打印进度条BIGINT2000
scalar_subquery_error_on_multiple_rows当标量子查询返回多行时,是否返回随机一行而不是报错BOOLEANtrue
schema设置默认搜索模式。等同于将 search_path 设置为单一值VARCHARmain
search_path设置默认的目录搜索路径,以逗号分隔的值列表VARCHAR
streaming_buffer_size在从流式结果中获取数据时,缓冲的最大内存大小(例如 1GB)VARCHAR976.5 KiB

风险提示与免责声明
本文内容基于公开信息研究整理,不构成任何形式的投资建议。历史表现不应作为未来收益保证,市场存在不可预见的波动风险。投资者需结合自身财务状况及风险承受能力独立决策,并自行承担交易结果。作者及发布方不对任何依据本文操作导致的损失承担法律责任。市场有风险,投资须谨慎。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

船长@Quant

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值