这篇能做出什么
先说结论:照着走完,你会得到一份 CSV 候选表,每行是一个天体,带坐标、星等、颜色、自行、变光周期,以及"是否已被已知星表收录"的标记。表里通常会有几十到几百个对象,其中绝大多数是已知天体、伪影或重复记录,剩下少数几个值得进一步看一眼——这就是"疑似"两个字的重量。
具体产出:
- 一个可复现的 Python 项目,查询、清洗、匹配、出图四个脚本分开
- 一份从天区查询到候选筛选的流水线
- 一张候选表
candidates.csv - 一套人工复核清单,帮你把假阳性挡在报告之前
需要说明的是:业余或个人环境下做出的候选,几乎不可能直接变成"新发现"。真正的确认要靠专业台站的光谱和多年观测。把它当成一次完整的数据挖掘演练,心态会好很多。
前置条件清单
环境
- Python 3.10 以上(具体版本以 Python 官方文档当前版本为准)
- pip 可用的虚拟环境
- 内存 4 GB 以上,磁盘留 1 GB 以上(TESS 光变曲线文件会占空间)
- 能访问 CDS/VizieR、Gaia Archive、MAST 这几个数据服务(部分网络环境需要配置代理)
Python 包
```bash
python -m venv .venv
source .venv/bin/activate # Windows 用 .venv\Scripts\activate
pip install astropy astroquery pandas numpy matplotlib lightkurve
```
Claude Code
安装方式看官方文档,版本以官方文档当前版本为准。它能读写你项目里的文件、执行 shell 命令,所以务必在一个专用目录里开工,别在 home 目录裸奔。
知识准备
不需要天文专业背景。只要知道:星表就是一张大表格,每行一个天体,列是坐标(ra/dec,单位度)、亮度(星等,数值越小越亮)、颜色(两个波段星等之差)。坐标匹配是整件事的核心动作。
第 1 步:给 Claude Code 立规矩
很多人一上来就让 Claude Code "写个脚本找新天体",结果得到的代码看起来专业、跑起来报错。正确做法是先写一份 CLAUDE.md 放在项目根目录,把约束讲清楚。
```markdown
项目:星表候选天体筛选
目标
从 Gaia 公开星表取一个天区,交叉匹配已知变星表,输出候选列表。
硬性约束
- 只用 astropy / astroquery / pandas / numpy / lightkurve
- 每个脚本必须能独立运行,且写出文件到 data/ 或 output/
- 不要在脚本里硬编码 API key
- 任何查询都要有 TOP 限制或半径限制,禁止全表扫描
- 输出统一用 ECSV 或 CSV,列名保持原样不要重命名
工作方式
- 改代码前先说明改动理由
- 每写完一个脚本,立刻运行一次并贴出结果
- 报错不要猜,先看完整 traceback
```
这份文件的作用是让 Claude Code 的每次输出都落在同一个框架里,而不是每次重新发明一套写法。
第 2 步:搭骨架
让 Claude Code 建目录,或者自己敲:
```
astro-dig/
├── CLAUDE.md
├── data/ # 原始星表
├── output/ # 中间结果与图
├── 01_fetch_gaia.py
├── 02_clean_features.py
├── 03_crossmatch.py
├── 04_lightcurve.py
└── 05_report.py
```
提示词可以这样写:
```
读 CLAUDE.md。创建 01_fetch_gaia.py:
用 astroquery.gaia 查询 Gaia DR3,天区中心 ra=210.0, dec=54.0,半径 0.5 度,
G 星等限制在 12 到 16 之间,取 TOP 5000。
需要的列:source_id, ra, dec, parallax, parallax_error, pmra, pmdec,
phot_g_mean_mag, bp_rp, ruwe, astrometric_excess_noise。
结果写到 data/gaia_field.ecsv。打印行数和前 5 行。
写完直接运行给我看结果。
```
生成的脚本大致长这样:
```python
from astroquery.gaia import Gaia
from astropy.table import Table
QUERY = """
SELECT TOP 5000
source_id, ra, dec, parallax, parallax_error,
pmra, pmdec, phot_g_mean_mag, bp_rp,
ruwe, astrometric_excess_noise
FROM gaiadr3.gaia_source
WHERE 1 = CONTAINS(
POINT('ICRS', ra, dec),
CIRCLE('ICRS', 210.0, 54.0, 0.5))
AND phot_g_mean_mag BETWEEN 12 AND 16
"""
job = Gaia.launch_job_async(QUERY)
tbl = job.get_results()
tbl.write("data/gaia_field.ecsv", overwrite=True)
print(len(tbl))
print(tbl[:5])
```
跑通再往下走。如果这一步就报网络错误,先解决网络,别急着写后面的逻辑。
第 3 步:清洗与特征构造
原始星表里有一堆要看一眼才知道能不能用的列。让 Claude Code 写 02_clean_features.py,做这几件事:
```python
import numpy as np
from astropy.table import Table
t = Table.read("data/gaia_field.ecsv")
视差信噪比:小于 5 的视差基本不可信
t["parallax_snr"] = t["parallax"] / t["parallax_error"]
自行总模
t["pm_total"] = np.hypot(t["pmra"], t["pmdec"])
颜色缺失的直接丢掉
t = t[np.isfinite(t["bp_rp"])]
常用经验阈值:RUWE 明显高于 1.4 往往意味着非单星解
t["ruwe_flag"] = t["ruwe"] > 1.4
t.write("output/gaia_features.ecsv", overwrite=True)
print(f"保留 {len(t)} 行")
```
这里有个要点:ruwe > 1.4 是社区常用的经验线,不是判据。它筛选出的是"值得看一眼"的对象,不是"就是双星"。让 Claude Code 把这类阈值集中写在文件开头,方便你后面调。
第 4 步:交叉匹配已知星表
这一步是整个流程里最有价值的部分。用 VizieR 查 AAVSO 的变星表(B/vsx/vsx),再用 astropy 做球面匹配。
```python
import astropy.units as u
from astropy.coordinates import SkyCoord
from astropy.table import Table
from astroquery.vizier import Vizier
t = Table.read("output/gaia_features.ecsv")
viz = Vizier(row_limit=-1)
center = SkyCoord(ra=210.0 * u.deg, dec=54.0 * u.deg, frame="icrs")
cats = viz.query_region(center, radius=0.5 * u.deg,
catalog=["B/vsx/vsx"])
known = cats[0]
print(f"已知变星 {len(known)} 颗")
src = SkyCoord(ra=t["ra"], dec=t["dec"], frame="icrs")
kn = SkyCoord(ra=known["RAJ2000"], dec=known["DEJ2000"], frame="icrs")
idx, sep, _ = src.match_to_catalog_sky(kn)
t["vsx_match_sep_arcsec"] = sep.arcsec
t["vsx_matched"] = t["vsx_match_sep_arcsec"] < 3.0
cand = t[~t["vsx_matched"]]
cand.write("output/candidates.csv", overwrite=True)
print(f"未匹配已知变星:{len(cand)} 个")
```
匹配半径取 3 角秒是个折中。半径太小会漏掉真实对应体(不同星表位置精度不同),太大会把邻居错认成同一个源。可以先画一张 sep 的直方图,看看分布再定。
第 5 步:加一层光变验证
坐标匹配只能说明"不在已知表里",不能说明"真的有变化"。用 TESS 的公开光变曲线补上这一环。这一步需要先知道目标的 TIC 编号,可以在 MAST 的 TIC 星表里按坐标查。
```python
import lightkurve as lk
tic_id = "TIC 123456789" # 换成候选表里的真实编号
sr = lk.search_lightcurve(tic_id, mission="TESS")
if len(sr) > 0:
lc = sr.download_all().stitch().remove_nans().normalize()
pg = lc.to_periodogram(method="lombscargle", normalization="amplitude")
period = 1.0 / pg.frequency_at_max_power
print(f"主周期约 {period:.5f} 天")
pg.plot().savefig("output/periodogram.png")
```
把每个候选都跑一遍会很慢,建议先挑一二十个出来,人工过一遍。周期图有明显尖峰的才值得继续。
第 6 步:人工复核清单
在把任何东西写进报告之前,逐个回答这些问题:
1. 坐标附近有没有亮得多的邻居?点扩散函数溢出会造成假变光。
2. 该源在 Gaia 里 parallax_snr 是多少?小于 5 就别谈距离。
3. ruwe 是不是高得离谱(比如超过 2)?那可能是解算失败而非物理双星。
4. 在 SIMBAD 里按坐标查一次,看看有没有文献提到过。
5. 用 ESASky 或 Aladin 之类的工具看一眼图像,确认不是星系或伪源。
6. TESS 光变曲线有没有数据缺口模式和数据完全对齐的迹象?那通常是仪器效应。
常见坑与排错
TAP 查询超时:半径缩小、加 TOP、用 launch_job_async 而不是同步查询。Gaia 的表是全量扫描,不加限制会被拒。
单位搞混:VizieR 返回的列名带下划线前缀(如 _RAJ2000)和普通列(如 RAJ2000)含义不同,前者是匹配后的位置。混用会导致几十角秒的偏差。让 Claude Code 打印前几行和前几列的 unit,确认再继续。
历元不一致:Gaia 的位置对应特定参考历元,高自行天体在不同历元的位置会差好几角秒。如果候选自行很大,先按自行把坐标外推到同一历元再匹配。
列为空:bp_rp、parallax 在某些源上是 NaN,不做过滤会污染后续统计。用 np.isfinite 统一处理。
Claude Code 编出看起来对的 API:这是高频问题。函数名、参数名都像真的,但模块里没有。对策是让它在写完立刻运行,报错就把完整 traceback 贴回去,它会自己改。别接受"应该可以运行"这种说法。
下载限速:MAST 批量下载会触发速率限制。加 sleep,或者一次性下载少量目标。
下一步建议
- 扩大时域数据:ZTF、ASAS-SN 的公开数据能覆盖更长的时间基线,对找长周期变光更有用。
- 换筛选思路:当前流程是"亮 + 不在已知表里"。也可以反过来,从高自行、视差异常、色指数异常这些维度找样本。
- 上异常检测:把特征矩阵丢给孤立森林或自编码器,让模型告诉你哪些源"长得不像邻居"。
- 走正规提交渠道:如果确实找到一个稳定周期、多次观测一致的候选,可以按 AAVSO 的变星提交流程报过去,或者写成观测提案。流程细节以相关机构官方页面为准。
- 把流程做成模板:把天区半径、星等范围、匹配阈值都抽成配置文件,下次换天区只改参数,不动代码。
整个流程跑下来,你收获的与其说是一个天体,不如说是一套能反复使用的数据挖掘方法:定问题、拉数据、构造特征、交叉验证、人工复核。这套东西换个数据集照样能用。
