scientific-agent-skills 项目实战指南:精通 Astropy 表格数据操作(astropy.table)全解析
导读
astropy.table 是 Python 天文/天体物理生态中处理星表、观测记录、谱线数据等表格数据的核心模块。它融合了 NumPy 的高性能数组操作、数据库风格的 join/group 操作、物理单位(units)感知与掩码(masked value)缺失数据处理,并原生支持 FITS、HDF5、VOTable、CSV/ECSV/IPAC、Parquet、ASDF 等多种格式的读写。本仓库的 astropy skill(见 SKILL.md)面向需要 Astropy API 的天文分析工作流,本文基于其配套参考文档 tables.md 展开,结合仓库内 skill 定义与依赖清单,系统讲解表的构建、访问、修改、排序筛选、I/O、join/group、单位列、掩码、索引与性能优化,帮助你直接用可运行的代码完成从"读入星表"到"交叉匹配、分箱统计"的完整数据链路。
一、认识 astropy.table:为什么天文学分析需要它
从源码结构看,astropy skill 将核心能力拆解为 7 份主题参考文档:units、coordinates、cosmology、fits、tables、time 以及 wcs_and_other_modules,分别对应 skills/astropy/references/ 下的同名文件。astropy.table 的价值在于它不是又一个 DataFrame 克隆,而是为天文数据量身定制:
- 单位感知:列可以携带物理单位,结合
QTable在计算中自动保持量纲一致; - 掩码支持:原生处理缺失/坏点数据,统计运算自动跳过掩码;
- 格式互通:一条
Table.read/Table.write即可桥接 FITS、CSV、VOTable、HDF5 等天文与通用格式; - 与生态融合:可无缝往返于 NumPy 数组与 pandas DataFrame。仓库的测试依赖清单 tests/skill-requirements.toml 中为
[skills.astropy]声明了astropy、matplotlib、numpy、pandas、dask,印证了该 skill 面向"pandas + 大规模数组(dask)"的扩展工作流。
二、创建表格:六种入口一次掌握
2.1 从列数组创建(最常用)
from astropy.table import Table, QTable
import astropy.units as u
import numpy as np
# 从列数组创建
a = [1, 4, 5]
b = [2.0, 5.0, 8.2]
c = ['x', 'y', 'z']
t = Table([a, b, c], names=('id', 'flux', 'name'))
# 带单位的列:使用 QTable
flux = [1.2, 2.3, 3.4] * u.Jy
wavelength = [500, 600, 700] * u.nm
t = QTable([flux, wavelength], names=('flux', 'wavelength'))
传列数组时列元素长度必须一致;
names可选,缺省时自动生成col0, col1, ...。凡涉及物理单位(Jy、nm、deg、pc 等)建议直接使用QTable,这正是 SKILL.md 中 Best Practice 第 8 条"Use QTable for unit-aware tables"的依据。
2.2 从行列表创建
# 行 = 元组
rows = [(1, 10.5, 'A'), (2, 11.2, 'B'), (3, 12.3, 'C')]
t = Table(rows=rows, names=('id', 'value', 'name'))
# 行 = 字典(键即列名)
rows = [{'id': 1, 'value': 10.5}, {'id': 2, 'value': 11.2}]
t = Table(rows)
2.3 从 NumPy 数组创建
# 结构化数组(dtype 自带列名与类型)
arr = np.array([(1, 2.0, 'x'), (4, 5.0, 'y')],
dtype=[('a', 'i4'), ('b', 'f8'), ('c', 'U10')])
t = Table(arr)
# 2D 数组 + 显式列名
data = np.random.random((100, 3))
t = Table(data, names=['col1', 'col2', 'col3'])
2.4 从 pandas DataFrame 创建
import pandas as pd
df = pd.DataFrame({'a': [1, 2, 3], 'b': [4, 5, 6]})
t = Table.from_pandas(df)
反向转换使用 t.to_pandas()。由于 skill 测试环境声明了 pandas(见 tests/skill-requirements.toml),Astropy 表与 DataFrame 的互转是本 skill 日常分析中的标准桥接动作。
三、访问表格数据
3.1 列 / 行 / 单元格三级访问
# 列访问:返回 Column 对象
ra_col = t['ra'] # Returns Column object
dec_col = t['dec']
# 行访问:单行返回 Row 对象,切片返回新 Table
first_row = t[0] # Returns Row object
row_slice = t[10:20] # Returns new Table
# 单元格访问(两种等价写法)
value = t['ra'][5] # 'ra' 列的第 6 个值
value = t[5]['ra'] # 同上
# 一次取多列
subset = t['ra', 'dec', 'mag']
3.2 表级属性与遍历
len(t) # 行数
t.colnames # 列名列表
t.dtype # 各列数据类型
t.info # 详细信息(含单位、格式、描述)
t.meta # 元数据字典
# 遍历行
for row in t:
print(row['ra'], row['dec'])
# 遍历列
for colname in t.colnames:
print(t[colname])
四、修改表格:增删列、重命名、加行与改值
4.1 添加列
# 直接赋新列
t['new_col'] = [1, 2, 3, 4, 5]
t['calc'] = t['a'] + t['b'] # 由已有列计算出的新列
# 带单位的列
t['velocity'] = [10, 20, 30] * u.km / u.s
# 添加空列(可指定长度与 dtype)
from astropy.table import Column
t['empty'] = Column(length=len(t), dtype=float)
# 插入到指定位置(index 为插入后的位置)
t.add_column([7, 8, 9], name='inserted', index=2)
4.2 删除列
t.remove_column('old_col') # 删除单列
t.remove_columns(['col1', 'col2']) # 删除多列
del t['col_name'] # 等价的 del 语法
t.keep_columns(['ra', 'dec', 'mag']) # 只保留指定列
4.3 重命名列
t.rename_column('old_name', 'new_name') # 重命名单列
t.rename_columns(['old1', 'old2'], ['new1', 'new2']) # 批量重命名
4.4 添加与修改数据
# 添加单行:按列序传列表,或按字典传键值
t.add_row([1, 2.5, 'new'])
t.add_row({'ra': 10.5, 'dec': 41.2, 'mag': 18.5})
# 注意:逐行 add_row 很慢!
# 更优做法:先收集行,再一次建表(见第八节性能优化)
# 修改列(整体替换/条件赋值)
t['flux'] = t['flux'] * gain
t['mag'][t['mag'] < 0] = np.nan
# 修改单个单元格
t['ra'][5] = 10.5
# 整行替换
t[0] = [new_id, new_ra, new_dec]
五、排序与筛选
5.1 排序
t.sort('mag') # 单列升序
t.sort('mag', reverse=True) # 降序
t.sort(['priority', 'mag']) # 多列依次排序
# 不修改原表:拿到排序索引再切片
indices = t.argsort('mag')
sorted_table = t[indices]
5.2 布尔索引筛选(与 NumPy 习惯一致)
bright = t[t['mag'] < 18] # 亮于 18 等的源
nearby = t[t['distance'] < 100*u.pc] # 距离 100 pc 内(单位比较自动成立)
# 多条件组合(必须加括号)
selected = t[(t['mag'] < 18) & (t['dec'] > 0)]
# 借助 numpy 函数做信噪比筛选
high_snr = t[np.abs(t['flux'] / t['error']) > 5]
布尔数组本质上是 NumPy 索引的扩展,因此 &(与)、|(或)、~(非)等位运算也可直接使用。
六、文件的读写
6.1 支持的格式
tables.md 明确列出:FITS、HDF5、ASCII(CSV、ECSV、IPAC 等)、VOTable、Parquet、ASDF。多样的格式支持正是 SKILL.md 中"table operations(reading catalogs, cross-matching, filtering, joining)"工作流的底层保障。
6.2 读取
# 自动探测格式(按扩展名)
t = Table.read('catalog.fits')
t = Table.read('data.csv')
t = Table.read('table.vot')
# 显式指定格式
t = Table.read('data.txt', format='ascii')
t = Table.read('catalog.hdf5', path='/dataset/table')
# 读取 FITS 指定 HDU
t = Table.read('file.fits', hdu=2)
FITS 是多 HDU(Header Data Unit)文件,
hdu参数用于定位存放表格的扩展;若表格在扩展 2,可直接用hdu=2跳开前导扩展。
6.3 写出
# 由扩展名自动推断格式
t.write('output.fits')
t.write('output.csv')
# 显式格式 / 指定 HDF5 路径 / 序列化元数据
t.write('output.txt', format='ascii.csv')
t.write('output.hdf5', path='/data/table', serialize_meta=True)
# 覆盖已存在文件
t.write('output.fits', overwrite=True)
serialize_meta=True 会把表元数据与列描述等一并写入 HDF5;overwrite=True 在目标已存在时避免抛错(Astropy 默认拒绝覆盖)。
6.4 ASCII 格式的细分选项
# 自定义分隔符的 CSV
t.write('output.csv', format='ascii.csv', delimiter='|')
# 定宽格式
t.write('output.txt', format='ascii.fixed_width')
# IPAC 格式(红外天文数据中心标准)
t.write('output.tbl', format='ascii.ipac')
# LaTeX 表(直接用于论文排版)
t.write('table.tex', format='ascii.latex')
七、表的组合运算:stack、join、group 与去重
7.1 纵向堆叠 vstack 与横向拼接 hstack
from astropy.table import vstack
# 上下拼接(列结构需一致)
t1 = Table([[1, 2], [3, 4]], names=('a', 'b'))
t2 = Table([[5, 6], [7, 8]], names=('a', 'b'))
t_combined = vstack([t1, t2])
from astropy.table import hstack
# 左右拼接(行数需一致)
t1 = Table([[1, 2]], names=['a'])
t2 = Table([[3, 4]], names=['b'])
t_combined = hstack([t1, t2])
7.2 数据库式 join
from astropy.table import join
# 在公共列 'id' 上内连接
t1 = Table([[1, 2, 3], ['a', 'b', 'c']], names=('id', 'data1'))
t2 = Table([[1, 2, 4], ['x', 'y', 'z']], names=('id', 'data2'))
t_joined = join(t1, t2, keys='id')
# left / right / outer 连接类型
t_joined = join(t1, t2, join_type='left')
t_joined = join(t1, t2, join_type='outer')
join_type 的取值语义与 SQL 完全对齐:'inner'(默认,仅保留匹配键)、'left'(保留左侧全部行,右侧缺失为掩码/NaN)、'outer'(保留两侧全部行)。
7.3 分组与聚合
# 按列分组(如按滤光片 filter)
g = t.group_by('filter')
# 对每组聚合:返回以 group key + 聚合结果为列的新表
means = g.groups.aggregate(np.mean)
# 逐组迭代
for group in g.groups:
print(f"Filter: {group['filter'][0]}")
print(f"Mean mag: {np.mean(group['mag'])}")
aggregate 接受任意可作用于数组并返回标量的函数(np.mean、np.median、len 等),非常适合做按测光波段统计亮度等常规任务。
7.4 唯一行
t_unique = t.unique('id') # 按单列去重
t_unique = t.unique(['ra', 'dec']) # 按多列组合去重(如重复天体坐标)
八、单位感知:QTable 与等价换算
使用 QTable(而非 Table)即可让列携带物理单位,并在运算中自动校验与换算量纲:
from astropy.table import QTable
t = QTable()
t['flux'] = [1.2, 2.3, 3.4] * u.Jy
t['wavelength'] = [500, 600, 700] * u.nm
# 单位换算
t['flux'].to(u.mJy)
t['wavelength'].to(u.angstrom)
# 计算自动保持单位;spectral() 等价表实现波长↔频率互换
t['freq'] = t['wavelength'].to(u.Hz, equivalencies=u.spectral())
equivalencies=u.spectral() 是天文的典型用法——波长(nm/angstrom)与频率(Hz)本属不同量纲,借助光谱等价表(equivalency)才可无缝互换。单位换算体系的细节可进一步参考本 skill 的 references/units.md。
九、缺失数据与掩码:MaskedColumn
天文观测中的非探测、坏像素常产生缺失值。掩码列可区分"数值本身"与"数据是否有效":
from astropy.table import MaskedColumn
# 显式指定 mask:第二元素被掩(缺失)
flux = MaskedColumn([1.2, np.nan, 3.4], mask=[False, True, False])
t = Table([flux], names=['flux'])
# 统计运算自动跳过被掩元素
mean_flux = np.ma.mean(t['flux'])
# 将掩码值替换为指定值(如 0)
t['flux'].filled(0) # Replace masked with 0
需要注意:掩码列基于 numpy.ma 实现,因此统计时应使用 np.ma.* 系列函数(np.ma.mean 等)以正确处理掩码。
十、索引:加速大规模星表检索
当表很大、需要按主键反复查询时,为列建索引可把查找从线性扫描提升为索引检索:
# 为列建索引
t.add_index('id')
# 按索引值快速定位行
row = t.loc[12345] # Find row where id=12345
# 范围查询
subset = t.loc[100:200]
# 多个索引并存时显式指定:优先用 'name' 索引查找
# 注意:t.loc["b", 2](首元素当索引名)自 7.2 起弃用,将于 9.0 移除
t.add_index('name')
row = t.loc.with_index('name')['M31']
从 SKILL.md 的版本说明可知,将表索引标识符作为 .loc 首元素传入的写法(t.loc["b", 2])自 Astropy 7.2 起已弃用,应改用 t.loc.with_index("b")[2];新代码请避免旧式写法。
十一、元数据管理
表级与列级元数据都是普通字典,用于保存观测上下文,并可在文件写出时序列化:
# 表级元数据
t.meta['TELESCOPE'] = 'HST'
t.meta['FILTER'] = 'F814W'
t.meta['EXPTIME'] = 300.0
# 列级元数据
t['ra'].meta['unit'] = 'deg'
t['ra'].meta['description'] = 'Right Ascension'
t['ra'].description = 'Right Ascension' # 便捷属性写法
在 SKILL.md 的 FITS 工作流中,header['EXPTIME']、header['FILTER'] 这类从文件头读取观测参数的场景与此处的 meta 写入形成互补:前者读 FITS 头,后者随表写入文件(需 serialize_meta=True)。
十二、性能优化:三个高频实践
12.1 批量构造胜过逐行 add_row
# SLOW:逐行 add_row(每次调用都有额外开销)
t = Table(names=['a', 'b'])
for i in range(1000):
t.add_row([i, i**2])
# FAST:先构造行列表,一次性建表
rows = [(i, i**2) for i in range(1000)]
t = Table(rows=rows, names=['a', 'b'])
tables.md 特别标注了这一点——add_row 仅适合少量追加,批量数据务必"收集后统一建表"。
12.2 FITS 内存映射读取超大文件
# 不全量载入内存,数据在访问时才从磁盘读取
t = Table.read('huge_catalog.fits', memmap=True)
subset = t[10000:10100] # 只触发所需分片的 IO,效率高
memmap=True 对 GB 级巡天星表尤其关键,可避免一次性 OOM;配合 skill 依赖中的 dask(见 tests/skill-requirements.toml),可进一步做大表的分块/并行处理。
12.3 区分视图与拷贝
# 视图:共享底层数据,操作快(修改会影响原表)
t_view = t['ra', 'dec']
# 拷贝:数据相互独立
t_copy = t['ra', 'dec'].copy()
需要强调:列切片默认返回视图(共享数据),若后续要独立修改,务必显式 .copy()。
十三、展示与格式控制
# 控制台打印
print(t)
# 浏览器可视化(jsviewer=True 支持交互式排序/筛选)
t.show_in_browser()
t.show_in_browser(jsviewer=True)
# 分页浏览(交互式逐屏查看,类比 Unix more)
t.more()
# 自定义列格式(旧式 % 与新版 str.format 均支持)
t['flux'].format = '%.3f'
t['ra'].format = '{:.6f}'
十四、格式互转:连接 NumPy / pandas / dict
# 转 NumPy 数组(结构化数组)
arr = np.array(t)
# 转 pandas DataFrame
df = t.to_pandas()
# 转字典(列名 → 列对象)
d = {name: t[name] for name in t.colnames}
与 2.4 节的 Table.from_pandas 组合,即可实现 NumPy ↔ Astropy Table ↔ pandas DataFrame 三者任意互转,满足不同下游库的输入需求。
十五、综合实战:两个经典天文用例
15.1 双星表交叉匹配(cross-match)
以赤经赤纬为单位建 SkyCoord,再按角距离阈值筛选匹配对,是巡天数据合并的标准流程:
from astropy.coordinates import SkyCoord, match_coordinates_sky
# 从表列构造坐标对象
coords1 = SkyCoord(t1['ra'], t1['dec'], unit='deg')
coords2 = SkyCoord(t2['ra'], t2['dec'], unit='deg')
# 为 coords1 的每个源找 coords2 中最近邻
idx, sep, _ = coords1.match_to_catalog_sky(coords2)
# 以 1 角秒为阈值筛掉非真匹配
max_sep = 1 * u.arcsec
matches = sep < max_sep
# 生成互相配对的子表
t1_matched = t1[matches]
t2_matched = t2[idx[matches]]
sep 为带 arcsec 单位的角距离 Quantity,可直接与 max_sep 比较。该用例也在 SKILL.md 的 Common Workflows 中作为标准星表交叉匹配示例出现,完整的坐标系转换细节见 references/coordinates.md。
15.2 按星等分箱(binning)
利用 np.digitize 把连续量离散到分箱索引,再交给 group_by 聚合,即可快速得到星等亮度函数:
# 按星等分箱:0.5 mag 步长,覆盖 10~20 mag
mag_bins = np.arange(10, 20, 0.5)
binned = t.group_by(np.digitize(t['mag'], mag_bins))
counts = binned.groups.aggregate(len)
binned.groups 每个子组对应一个星等 bin,aggregate(len) 即每个 bin 内的源计数——无需手写循环即可完成亮度分布统计。
十六、小结与延伸
astropy.table 提供了一条覆盖创建 → 访问 → 修改 → 排序/筛选 → 文件 I/O → 组合运算(stack/join/group)→ 单位/掩码/索引 → 性能调优的完整表格数据处理链路,且天然嵌入 Astropy 生态(units、coordinates、io.fits)。要在天文 Agent 工作流中熟练运用本主题,建议:
- 凡涉及单位一律用
QTable,让量纲一致性由库来保证; - 大批量读写优先
memmap、批量构造、索引检索,避免逐行与全量载入; - mask 数据用
MaskedColumn+np.ma.*统计,确保缺失值不被误当数值; - 交叉匹配、坐标换算等进阶场景分别查阅 references/coordinates.md、references/fits.md、references/units.md,整体 skill 的安装版本(如
uv pip install "astropy==7.2.0",Python 3.11+)与环境说明见 SKILL.md。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00