11  Pandas的极限与内存管理

11.1 先修知识与学习目标

本章承接 Pandas 的索引、分组聚合和时间序列知识。读者应会使用 read_hdf()groupby()astype(),并理解整数、浮点数与字符串的基本类型。这里讨论的核心问题不是笼统地问 Pandas 能处理多少行,而是判断一次具体计算的峰值内存、数据复制和算法复杂度。

完成本章后,读者应能:

  • 目标 11.1:解释 NumPy 缓冲区、Python 对象、索引和缺失值掩码如何共同形成 DataFrame 的内存占用;
  • 目标 11.2:根据取值范围与误差容忍度选择整数、浮点、分类和 Arrow 类型;
  • 目标 11.3:使用选择性读取与分块聚合控制峰值内存,并验证分块结果;
  • 目标 11.4:识别降级溢出、浮点精度损失、类别基数过高和链式计算隐式复制等风险。

目标—活动—核心练习—答案证据映射

表 11.1: 第十一章教学闭环映射
正式目标 学习活动或示例 可观察产出 核心练习 完整答案中的评分证据
目标 11.1 小节 11.2小节 11.3 的内存分解 估算分类列成本并输出真实切片的深层内存表 小节 11.8.1 公式代入、MiB 换算及峰值内存解释均有明确得分点
目标 11.2 小节 11.4.1小节 11.4.2小节 11.4.3小节 11.4.4 依据范围、误差、基数和互操作需求提交类型决策 小节 11.8.2小节 11.8.3小节 11.8.5 类型上界、值一致性、Arrow 决策表和浮点误差断言构成答案证据
目标 11.3 小节 11.5 的真实 Parquet 批次扫描 输出分块充分统计量并与直接聚合逐项相等 小节 11.8.4 sum/count 合并、均值计算和 assert_series_equal 完整计分
目标 11.4 小节 11.4.5 的共享内存与临时数组对照 报告溢出、精度、基数和隐式复制的风险判断 小节 11.8.2小节 11.8.5 uint16 边界、财富路径精度差及 shares_memory 断言可自动评分

表 11.1 汇总本节的计算或审计结果,解释时应遵循正文给出的口径与限制。

11.2 内存模型、复杂度与边界

一个 DataFrame 的内存并不等于文件大小。忽略分配器碎片时,可用 式 11.1 近似表示:

\[ M_{\text{frame}} \approx M_{\text{index}} + \sum_{j=1}^{p}\left(M_{\text{buffer},j}+M_{\text{mask},j}+M_{\text{object},j}\right). \tag{11.1}\]

定长数值列主要由连续缓冲区构成;可空类型还需要有效性掩码。传统 object 字符串列的数组保存 Python 对象指针,字符串内容另在堆中分配,因此浅层统计会低估成本。memory_usage(deep=True) 会递归估计对象内容,更适合教学中的相对比较,但它仍不是进程峰值内存的精确测量。

若一列有 \(n\) 个值、\(k\) 个不同类别,类别编码宽度为 \(b_c\) 字节,类别字典平均每项成本为 \(b_d\),则分类存储成本近似为:

\[ M_{\text{category}} \approx n b_c + k b_d. \tag{11.2}\]

\(k \ll n\) 时,式 11.2 通常远小于逐行保存字符串;当几乎每行都不同,类别字典反而可能增加内存。类型转换需要扫描全部 \(n\) 个值,时间复杂度通常为 \(O(n)\),并可能在转换期间同时保留新旧缓冲区,使峰值接近转换前后内存之和。

表 11.2: Pandas 内存技术的成本与边界
技术 典型时间复杂度 额外峰值内存 主要边界
整数或浮点降级 \(O(n)\) 一个新列缓冲区 溢出、舍入和缺失值
category 编码 平均 \(O(n)\) 编码与类别字典 高基数、类别集合变化
Arrow 类型转换 \(O(n)\) 或共享缓冲区 取决于源布局 类型兼容性与后续算子
分块聚合 \(O(n)\) \(O(c+g)\) 聚合必须可合并

表 11.2 汇总本节的计算或审计结果,解释时应遵循正文给出的口径与限制。

其中 \(c\) 是块大小,\(g\) 是分组数。分块并不自动保证正确:求和、计数、最小值和最大值可直接合并;均值必须同时维护分子与分母;中位数和全局排序则不能仅合并各块结果。

11.3 本地真实行情的选择性读取

本章使用上海、江苏、浙江的上汽集团、恒瑞医药、海康威视和宁波银行 2023 年前复权日行情。第一段代码先建立跨平台路径,再只读取所需证券、期间和列;这比先载入 1536 万行全表再筛选更符合内存约束。

import platform  # 识别操作系统以复用同一份教材代码
from pathlib import Path  # 以结构化路径表达本地数据位置
import numpy as np  # 提供数值类型边界与精度检查工具
import pandas as pd  # 执行HDF选择性读取和内存分析
import pyarrow.dataset as ds  # 以记录批次扫描真实Parquet分片

DATA_ROOT = 'C:/qiufei/data' if platform.system() == 'Windows' else '/home/ubuntu/r2_data_mount/data'  # 选择规范数据根目录
PRICE_PATH = Path(DATA_ROOT) / 'stock' / 'stock_price_pre_adjusted.h5'  # 指向前复权日行情文件
MARKET_DATASET = Path(DATA_ROOT) / 'stock' / 'full_market_with_return_2023.parquet'  # 指向可批量扫描的真实Parquet目录
YRD_CODES = ['600104.XSHG', '600276.XSHG', '002415.XSHE', '002142.XSHE']  # 固定长三角公司样本
PRICE_COLUMNS = ['open', 'close', 'volume', 'total_turnover']  # 仅请求分析所需字段
PRICE_FILTERS = [f'order_book_id in {YRD_CODES}', 'date >= Timestamp(\'2023-01-01\')', 'date <= Timestamp(\'2023-12-31\')']  # 在HDF层过滤证券与日期
stock_prices = pd.read_hdf(PRICE_PATH, key='data', where=PRICE_FILTERS, columns=PRICE_COLUMNS).reset_index()  # 只把目标切片载入内存
assert set(stock_prices.columns) == {'order_book_id', 'date', 'open', 'close', 'volume', 'total_turnover'}  # 显式核验真实schema
print(stock_prices.shape)  # 报告实际载入规模
print(stock_prices.dtypes)  # 展示HDF读取后的原始类型
(968, 6)
order_book_id             object
date              datetime64[ns]
open                     float64
close                    float64
volume                   float64
total_turnover           float64
dtype: object

表 11.3 报告真实切片的深层内存,而不是用随机行情代替市场数据。

memory_profile = pd.DataFrame({'dtype': stock_prices.dtypes.astype(str), 'minimum': stock_prices.select_dtypes('number').min(), 'maximum': stock_prices.select_dtypes('number').max(), 'memory_kb': stock_prices.memory_usage(deep=True) / 1024})  # 合并类型、范围和深层内存证据
memory_profile.index.name = 'column'  # 明确表格索引的业务含义
memory_profile.round(3)  # 输出便于阅读的诊断表
表 11.3: 真实行情切片的原始类型、范围与内存占用
dtype minimum maximum memory_kb
column
Index NaN NaN NaN 0.125
close float64 1.248500e+01 4.886100e+01 7.562
date datetime64[ns] NaN NaN 7.562
open float64 1.246600e+01 4.878800e+01 7.562
order_book_id object NaN NaN 64.281
total_turnover float64 1.449006e+08 6.226335e+09 7.562
volume float64 1.029398e+07 1.378805e+08 7.562

11.4 安全的类型优化

11.4.1 整数降级:先证明范围,再转换

把成交量机械地改成 int16 会溢出。正确顺序是先检查最小值、最大值和缺失值,再选能覆盖样本与合理未来范围的类型。这里成交量非负,因此无缺失时可以使用无符号整数。

has_missing_volume = stock_prices['volume'].isna().any()  # 判断普通NumPy整数是否能表达该列
maximum_volume = stock_prices['volume'].max()  # 提取样本最大成交量用于边界证明
assert not has_missing_volume  # 确认转换不会丢失缺失值语义
assert maximum_volume <= np.iinfo(np.uint32).max  # 确认所有观测落在uint32范围内
optimized_prices = stock_prices.copy()  # 保留基准版本以比较结果和内存
optimized_prices['volume'] = optimized_prices['volume'].astype('uint32')  # 在边界验证后压缩成交量
print(optimized_prices['volume'].dtype)  # 输出最终成交量类型
uint32

11.4.2 浮点降级:误差容忍度是业务约束

float32 只有约七位十进制有效数字。价格降级是否安全,不能由数据类型名称决定,而应由计算用途决定。若教学任务允许最大绝对价格误差不超过 \(10^{-3}\) 元,可用 式 11.3 验证:

\[ e_{\max}=\max_i\left|x_i-\operatorname{float32}(x_i)\right|. \tag{11.3}\]

close_float32 = optimized_prices['close'].astype('float32')  # 生成候选低精度价格缓冲区
maximum_price_error = (optimized_prices['close'] - close_float32.astype('float64')).abs().max()  # 在同一精度下计算绝对误差
assert maximum_price_error < 0.001  # 按千分之一元容忍度判断本案例是否可接受
optimized_prices['open'] = optimized_prices['open'].astype('float32')  # 在相同价格量级下压缩开盘价
optimized_prices['close'] = close_float32  # 应用已验证的收盘价类型
print(f'最大价格误差为 {maximum_price_error:.8f} 元')  # 报告精度损失而非宣称无损
最大价格误差为 0.00000189 元

在逐笔成交金额、会计金额核对或长期复利计算中,误差可能累积;此时应保留 float64,或使用定点整数表示最小货币单位。降级是有条件的工程决策,不是越小越好。

11.4.3 分类类型:基数决定收益

股票代码只有四个不同值,是低基数列。转换前后必须同时比较内存与值一致性。

code_memory_before_kb = optimized_prices['order_book_id'].memory_usage(deep=True) / 1024  # 度量对象字符串的深层内存
original_codes = optimized_prices['order_book_id'].copy()  # 保存原值用于无损性验证
optimized_prices['order_book_id'] = optimized_prices['order_book_id'].astype('category')  # 将低基数证券代码编码为分类类型
code_memory_after_kb = optimized_prices['order_book_id'].memory_usage(deep=True) / 1024  # 度量编码和字典的总内存
assert original_codes.equals(optimized_prices['order_book_id'].astype('object'))  # 验证编码前后业务值完全一致
print(f'代码列内存由 {code_memory_before_kb:.2f} KB 降至 {code_memory_after_kb:.2f} KB')  # 报告本机实测结果
代码列内存由 64.41 KB 降至 1.50 KB

11.4.4 Arrow 后端:列式布局不等于无条件零拷贝

Arrow 字符串把偏移量、字节内容和有效性位图组织为列式缓冲区,避免逐值 Python 对象。Pandas 可用 convert_dtypes(dtype_backend='pyarrow') 获得 Arrow 扩展类型,但转换已有 NumPy/object 列通常需要分配新缓冲区。后续与 Arrow 生态互操作能否零拷贝,还取决于类型是否兼容、缓冲区是否连续、是否需要合并 chunks,以及接收方是否接受只读内存。

arrow_prices = stock_prices.convert_dtypes(dtype_backend='pyarrow')  # 将兼容列转换为Pandas Arrow扩展类型
arrow_memory_kb = arrow_prices.memory_usage(deep=True).sum() / 1024  # 度量转换后的DataFrame内存
original_memory_kb = stock_prices.memory_usage(deep=True).sum() / 1024  # 度量同一观测集的原始内存
print(arrow_prices.dtypes)  # 检查每列实际采用的Arrow类型
print(f'原始内存 {original_memory_kb:.2f} KB,Arrow类型内存 {arrow_memory_kb:.2f} KB')  # 报告特定环境的实测值
order_book_id            string[pyarrow]
date              timestamp[ns][pyarrow]
open                     double[pyarrow]
close                    double[pyarrow]
volume                    int64[pyarrow]
total_turnover           double[pyarrow]
dtype: object
原始内存 102.22 KB,Arrow类型内存 52.12 KB

易混淆概念辨析:列式存储、Arrow 后端与零拷贝

列式存储描述同一列值的组织方式;Arrow 是一种内存格式;零拷贝则是一次具体交换是否共享既有缓冲区。三者相关但不等价。read_hdf() 读取 HDF5 后再转换为 Arrow 已经发生了解码与分配,因此不能把整条链路称为零拷贝。

11.4.5 共享视图、Copy-on-Write 与隐式复制

“没有写 .copy()”不等于没有复制。相同 NumPy 类型的 astype(..., copy=False) 可能共享既有缓冲区;类型降级必须新建另一种元素宽度的缓冲区;算术表达式、布尔筛选、排序和许多跨库转换也通常产生结果数组。Pandas 的 Copy-on-Write 主要约束对象修改时的隔离语义,不会消除计算本身所需的临时数组。

因此,内存审计至少回答两个问题:最终对象占多少内存,以及转换期间是否同时存活源缓冲区、目标缓冲区和中间结果。np.shares_memory() 可以为 NumPy 支持的数组提供局部证据,但不能据此推断整条 DataFrame 或 Arrow 链路都零拷贝。小节 11.8.5 将类型不变、类型降级和算术临时结果放在同一审计表中。

11.5 分块计算与峰值内存

若最终结果只是每只股票的总成交额,不必保留全部明细。对可加总指标,分块算法维护 \(g\) 个组的部分和,工作内存由全量 \(O(n)\) 降为约 \(O(c+g)\)

HDF5 已在前文用于选择性读取。为了让批次边界由文件扫描器真正控制,下面对同一年度的本地 Parquet 分片使用 Arrow record batch;这避免 PyTables 对大表迭代筛选时的额外扫描状态。

turnover_parts = []  # 收集每个块的证券级部分和
market_dataset = ds.dataset(MARKET_DATASET, format='parquet')  # 从真实Parquet分片建立只读数据集
market_filter = ds.field('order_book_id').isin(YRD_CODES) & (ds.field('trade_date') < pd.Timestamp('2024-01-01'))  # 把证券和年度筛选下推到Arrow扫描器
market_scanner = market_dataset.scanner(columns=['order_book_id', 'total_turnover'], filter=market_filter, batch_size=100_000)  # 将物理扫描批次限制为至多十万行
for record_batch in market_scanner.to_batches():  # 逐批推进而不拼接日度明细
    price_chunk = record_batch.to_pandas()  # 只把当前小批次转换为Pandas
    chunk_totals = price_chunk.groupby('order_book_id')['total_turnover'].sum()  # 在块内先压缩为证券级统计量
    turnover_parts.append(chunk_totals)  # 保存小型中间结果而非行情明细
chunked_turnover = pd.concat(turnover_parts, axis=1).fillna(0).sum(axis=1).sort_values(ascending=False)  # 合并各块的可加总统计量
direct_parquet_prices = market_dataset.to_table(columns=['order_book_id', 'total_turnover'], filter=market_filter).to_pandas()  # 仅为验证物化同源的小型目标切片
direct_turnover = direct_parquet_prices.groupby('order_book_id')['total_turnover'].sum().sort_values(ascending=False)  # 计算同一Parquet来源的内存内基准结果
pd.testing.assert_series_equal(chunked_turnover, direct_turnover, check_names=False, check_index_type=False)  # 忽略跨格式字符串索引实现并核验数值一致
chunked_turnover.to_frame('total_turnover_yuan')  # 输出各公司全年成交额
total_turnover_yuan
order_book_id
600276.XSHG 3.781830e+11
002415.XSHE 3.286586e+11
002142.XSHE 1.993612e+11
600104.XSHG 6.962311e+10

11.5.1 结果解释

以上输出是本地真实行情在当前 peter 环境中的测量值。分类列收益较大,是因为四个代码被重复使用约千次;数值降级收益较温和,因为日期和成交额仍保留原类型。分块聚合与直接聚合的一致性断言说明:对求和而言,块内先聚合再合并没有改变结果。它并不证明所有分析都可分块,尤其不能直接推广到精确中位数、全局排名或跨块滚动窗口。

11.6 常见误区

  1. 只看文件大小估计内存。 压缩 Parquet/HDF5 解码后可能显著膨胀,操作中的临时数组还会提高峰值。
  2. 把所有整数改成最小类型。 新数据超出历史范围会溢出;含缺失值时普通 NumPy 整数还会丢失缺失语义。
  3. 认为 float32 对金融价格天然安全。 单价展示与组合净值累计的误差要求不同,必须用业务容忍度验证。
  4. 把所有字符串改成 category 高基数字段会维护庞大字典,频繁拼接不同类别集合也有重编码成本。
  5. 把分块读取等同于超内存计算。 若把每个块追加到列表后再次拼成全表,峰值内存并未真正受控。

11.7 本章小结

Pandas 的边界由数据布局、算子和峰值内存共同决定,而不是由固定行数决定。安全优化遵循同一审计链:先选择性读取,再测量深层内存与取值范围,随后应用类型或分块策略,最后验证数值和业务语义不变。Arrow 能改善列式互操作,但零拷贝必须逐条链路判断。

11.8 分层练习与完整答案

11.8.1 练习 11.1:概念检查

某证券代码列有 \(n=10^7\) 行、\(k=5000\) 个类别。类别编码使用 2 字节,类别字典平均每项 80 字节。忽略其他开销,估算分类存储内存,并解释为何不能仅凭该结果保证转换时不会内存溢出。

答案

式 11.2\(M=10^7\times2+5000\times80=20{,}400{,}000\) 字节,约 19.46 MiB。转换阶段通常同时存在原 object 缓冲区、Python 字符串对象、新编码和类别字典,因此峰值可能远大于最终 19.46 MiB;还要计入索引、掩码和分配器开销。

11.8.2 练习 11.2:类型边界诊断

验证本章真实样本的成交量能否使用 uint16,并给出正确选择。

答案

sample_maximum_volume = stock_prices['volume'].max()  # 提取真实样本的最大成交量
uint16_maximum = np.iinfo(np.uint16).max  # 取得uint16可表达的上界
can_use_uint16 = sample_maximum_volume <= uint16_maximum  # 对照数据范围与类型上界
recommended_dtype = 'uint16' if can_use_uint16 else 'uint32'  # 选择能够覆盖观测的较小无符号类型
print(f'样本最大成交量为 {sample_maximum_volume:.0f},uint16上界为 {uint16_maximum}')  # 输出判断证据
print(f'推荐类型为 {recommended_dtype}')  # 输出可复核的类型结论
样本最大成交量为 137880485,uint16上界为 65535
推荐类型为 uint32

输出应显示 uint16 不足,因为 A 股日成交量可以远高于 65,535 股;当前样本应选择 uint32

11.8.3 练习 11.3:真实基础信息优化

读取 stock_basic_data.h5,使用经核验的真实字段 provinceindustry_nameboard_type,比较转为分类类型前后的内存,并确保值不变。

答案

BASIC_PATH = Path(DATA_ROOT) / 'stock' / 'stock_basic_data.h5'  # 指向真实股票基础信息文件
stock_basic = pd.read_hdf(BASIC_PATH, key='stock_basic_info')  # 读取规模较小的完整基础信息表
category_columns = ['province', 'industry_name', 'board_type']  # 使用实测存在的地区、行业与板块字段
assert set(category_columns).issubset(stock_basic.columns)  # 防止错误schema被静默跳过
basic_values_before = stock_basic[category_columns].copy()  # 保存业务值作为转换基准
basic_memory_before = stock_basic.memory_usage(deep=True).sum()  # 计算转换前深层内存
stock_basic[category_columns] = stock_basic[category_columns].astype('category')  # 编码三列低基数字段
basic_memory_after = stock_basic.memory_usage(deep=True).sum()  # 计算转换后深层内存
pd.testing.assert_frame_equal(basic_values_before, stock_basic[category_columns].astype('object'))  # 验证所有文本值保持一致
print(f'总内存由 {basic_memory_before / 1024**2:.2f} MiB 降至 {basic_memory_after / 1024**2:.2f} MiB')  # 报告本地实测结果
总内存由 8.22 MiB 降至 6.93 MiB

结果应解释为该文件与环境下的观测值,而不是可推广到任意数据的固定节省比例。

11.8.4 练习 11.4:综合挑战

用分块方式同时计算四只股票的成交额总和与记录数,再据此计算平均日成交额;验证它等于直接聚合结果。说明为何不能对各块均值再做简单平均。

答案

chunk_statistics = []  # 保存每块的求和与计数充分统计量
exercise_scanner = market_dataset.scanner(columns=['order_book_id', 'total_turnover'], filter=market_filter, batch_size=100_000)  # 为练习创建新的真实行情批次扫描器
for exercise_batch in exercise_scanner.to_batches():  # 逐批读取目标证券观测
    exercise_chunk = exercise_batch.to_pandas()  # 只物化当前批次
    grouped_chunk = exercise_chunk.groupby('order_book_id')['total_turnover'].agg(['sum', 'count'])  # 为均值保留分子和分母
    chunk_statistics.append(grouped_chunk)  # 累积小型分组统计而非原始行
combined_statistics = pd.concat(chunk_statistics).groupby(level=0).sum()  # 跨块分别合并总额与记录数
combined_statistics['mean'] = combined_statistics['sum'] / combined_statistics['count']  # 用全局分子除以全局分母
combined_statistics  # 输出分块均值及其组成
sum count mean
order_book_id
002142.XSHE 1.993612e+11 241 8.272249e+08
002415.XSHE 3.286586e+11 241 1.363729e+09
600104.XSHG 6.962311e+10 241 2.888926e+08
600276.XSHG 3.781830e+11 241 1.569224e+09
direct_mean = direct_parquet_prices.groupby('order_book_id')['total_turnover'].mean().sort_index()  # 计算同一Parquet来源的直接验证基准
chunk_mean = combined_statistics['mean'].sort_index()  # 对齐分块结果的证券顺序
pd.testing.assert_series_equal(chunk_mean, direct_mean, check_names=False, check_index_type=False)  # 忽略跨格式字符串索引实现并验证数值一致
print(chunk_mean)  # 输出最终平均日成交额
order_book_id
002142.XSHE    8.272249e+08
002415.XSHE    1.363729e+09
600104.XSHG    2.888926e+08
600276.XSHG    1.569224e+09
Name: mean, dtype: float64

各块行数可能不同,简单平均各块均值会给小块与大块相同权重;保留 sumcount 才能得到正确的加权结果。

11.8.5 练习 11.5:Arrow 类型、浮点精度与隐式复制审计

针对本章真实行情回答三组问题:

  1. 分别为 order_book_iddatevolumeclose 选择适合“本章 Pandas 分析”或“交给 Arrow 引擎”的类型,并说明选择条件;
  2. 比较 float64float32 日收益复利财富路径的最大绝对差,判断为什么单价展示通过 \(10^{-3}\) 元阈值不等于长期复利或金额核对也安全;
  3. 用共享内存检查区分相同类型 astype(copy=False)float32 降级与乘法表达式,指出哪两步必然需要新数组。

评分点(100 分):四列类型选择与条件 35 分;精度计算和业务判断 30 分;三种内存共享结果 25 分;不夸大零拷贝结论 10 分。

答案

表 11.4: 练习 11.5 的条件化类型选择
字段与用途 建议类型 选择理由与边界
order_book_id,本章四家公司反复分组 Pandas category 低基数重复值最适合字典编码;若类别集合频繁变化,拼接时可能重编码
order_book_id,交给 Arrow/DuckDB 且类别不断扩展 string[pyarrow] 保留字符串语义并减少 Python 对象;从现有 object 转换仍通常复制字节内容
date timestamp[ns][pyarrow] 或 Pandas datetime64[ns] 两者都保留纳秒时间语义;是否采用 Arrow 取决于后续算子与互操作合同
volume 经范围核验的 uint32;需要缺失时用 uint32[pyarrow]UInt32 uint16 上界不足;可空扩展类型保留缺失掩码
close 财富累计与金额核对保留 float64double[pyarrow] float32 是否可用取决于最终指标误差,而不只取决于单个价格舍入误差

表 11.4 汇总本节的计算或审计结果,解释时应遵循正文给出的口径与限制。

表 11.5 汇总本节的计算或审计结果,解释时应遵循正文给出的口径与限制。

anchor_close_float64 = stock_prices.loc[stock_prices['order_book_id'].eq('600276.XSHG')].sort_values('date')['close'].astype('float64')  # 取恒瑞医药真实价格作为共同输入
anchor_returns_float64 = anchor_close_float64.pct_change(fill_method=None).dropna()  # 以双精度计算相邻真实交易日收益
wealth_float64 = (1 + anchor_returns_float64).cumprod()  # 形成双精度财富路径基准
wealth_float32 = (1 + anchor_returns_float64.astype('float32')).cumprod().astype('float64')  # 在单精度收益上执行同一复利递推
maximum_wealth_path_gap = (wealth_float64 - wealth_float32).abs().max()  # 度量舍入在多期乘法中的累计影响
same_dtype_view = anchor_close_float64.astype('float64', copy=False)  # 请求在类型不变时复用源缓冲区
narrowed_float32 = anchor_close_float64.astype('float32')  # 元素宽度变化要求分配新的目标缓冲区
scaled_close = anchor_close_float64 * 100  # 算术表达式生成新的结果数组而非原地改写价格
shares_same_dtype = np.shares_memory(anchor_close_float64.to_numpy(copy=False), same_dtype_view.to_numpy(copy=False))  # 检查类型不变路径是否实际共享内存
shares_narrowed = np.shares_memory(anchor_close_float64.to_numpy(copy=False), narrowed_float32.to_numpy(copy=False))  # 检查降级数组是否错误复用不同宽度缓冲区
shares_scaled = np.shares_memory(anchor_close_float64.to_numpy(copy=False), scaled_close.to_numpy(copy=False))  # 检查乘法结果是否另行分配
assert shares_same_dtype and not shares_narrowed and not shares_scaled  # 固定三种路径的可评分内存合同
pd.DataFrame({'audit': ['float32复利路径最大绝对差', '同类型astype共享内存', 'float32降级共享内存', '乘法结果共享内存'], 'observed': [maximum_wealth_path_gap, shares_same_dtype, shares_narrowed, shares_scaled]})  # 输出精度与复制证据
表 11.5: 练习11.5的浮点精度与共享内存审计
audit observed
0 float32复利路径最大绝对差 0.000001
1 同类型astype共享内存 True
2 float32降级共享内存 False
3 乘法结果共享内存 False

答案中的 maximum_wealth_path_gap 必须如实报告而不能预设为零。即便本样本的误差很小,也只能支持这条财富路径在当前容忍度下可接受;会计对账、逐笔金额或更长持有期需要重新定义阈值。后两项 False 说明类型降级和乘法表达式产生新数组;第一项 True 只是这个同类型 NumPy 缓冲区的局部证据,不能推广成整个 DataFrame 或 Arrow 流程无条件零拷贝。