- date
- entry
- 002
- topic
- method
- rev
- —
分頁上限做出來的假趨勢
我在抓取腳本裡設了一個保險用的上限。它沒有出錯、沒有報警,只是安靜地讓資料長出一個不存在的趨勢——而且方向跟事實相反。
我想知道某個題目最近有多少研究,於是寫了一段分頁抓取,把 arXiv 上幾個分類、三個月內的論文抓下來。 為了避免程式邏輯出錯時無限翻頁,我加了一個上限:
MAXPAGE = 60 # 60 * 100 = 6000 筆/分類,跑飛時的保險
for page in range(MAXPAGE):
start = page * 100
url = (f"...search_query=cat:{cat}+AND+submittedDate:[{A}+TO+{B}]"
f"&start={start}&max_results=100"
f"&sortBy=submittedDate&sortOrder=descending")
...
這是很常見的防呆。它的用意是「正常情況下不會碰到」,所以我沒有為它寫任何告警。
資料看起來像一個發現
抓完之後,按月統計是這樣:
2026-05: 4,902
2026-06: 7,671
2026-07: 8,648
三個月成長將近一倍。這個數字很誘人,因為它剛好可以支持我原本想講的故事——這個領域正在快速升溫。 我差一點就把它寫進結論。
會讓你出事的資料,通常不是明顯錯誤的那種,是剛好符合你預期的那種。
停下來的原因很單純:arXiv 的投稿量不會在三個月內變成三倍。這個成長幅度不像真的。
問資料來源,不要問資料
要驗證抽樣有沒有偏差,最直接的方法是問一個不依賴自己抓取結果的問題。
arXiv 的 API 回應裡有 <opensearch:totalResults>,可以只要一筆結果、單純問「這個條件總共有幾篇」:
url = (f"...search_query=cat:{cat}"
f"+AND+submittedDate:[{a}0000+TO+{b}2359]"
f"&start=0&max_results=1")
total = int(re.search(r'<opensearch:totalResults[^>]*>(\d+)<', s).group(1))
拿三個星期去問,結果是這樣:
| 分類 | 05-01~05-07 | 06-01~06-07 | 07-24~07-30 |
|---|---|---|---|
| cs.AI | 1,331 | 1,444 | 944 |
| cs.LG | 1,350 | 1,325 | 835 |
| cs.CL | 536 | 731 | 376 |
五月的第一週比七月的最後一週還多。 真實方向跟我的資料完全相反。那個「成長」百分之百是我自己造出來的。
為什麼截斷會落在舊的那一端
關鍵在 sortOrder=descending。結果從最新的開始給,所以抓到第 6,000 筆停下時,
手上握的是最近的 6,000 筆,被丟掉的是時間軸較舊的那一端。
三個大分類在三個月內各有一到兩萬篇,全都撞到上限;其他六個分類量小,完整抓完了。 於是最終資料呈現的樣子是:越靠近現在,覆蓋越完整;越往前,缺得越多。 把它按月畫出來,就長得跟一條漂亮的成長曲線一模一樣。
這個錯誤有兩個性質讓它特別難抓:
- 它不會報錯。迴圈正常結束,沒有例外,沒有重試,日誌乾淨。
- 它產生的是合理的形狀。如果截斷是隨機的,資料會變得雜亂而讓人起疑;但依時間排序的截斷產生的是單調趨勢,而單調趨勢看起來像知識。
修法:讓抓取自己證明自己完整
真正的修法不是把上限調大——調大只是把問題推遠,下次資料量成長時它會再回來,而且一樣不出聲。 修法是把「我抓完了」從假設變成可驗證的斷言:先問總數,抓完之後比對。
for c in CATS:
exp = total(c) # 先問官方總數
got = 0
for start in range(0, exp + 100, 100): # 迴圈邊界改由 exp 決定
...
got += n
verify[c] = {'official_total': exp, 'fetched': got}
print(f"{c} 官方 {exp} / 實抓 {got} {'✓' if got >= exp else '✗'}")
兩個差別。第一,迴圈的終止條件來自資料來源而不是我拍腦袋的常數。 第二,每一類都留下一行「官方說有幾筆、我拿到幾筆」,而且這行會跟資料一起存檔—— 日後任何人(包括我自己)要質疑這份資料完不完整,有東西可以看。
重跑之後九個分類全部相符。這時候的數字才拿得出去。
順帶一提:cat: 不是你以為的那個 cat
同一次還踩到另一個坑。search_query=cat:cs.AI 匹配的是
任何一個分類標籤是 cs.AI 的論文,包含交叉列名,不是「主分類為 cs.AI」。
所以九個分類各自查詢再去重,跟「這九類總共有幾篇不重複的論文」是兩件事,
而且單一分類的 totalResults 也不等於該分類的主分類論文數。
我是在核對數字時發現去重後的量遠小於各類相加,才回頭確認這件事。
如果只記得一件事
你自己設的保險上限,在輸出裡是隱形的。 程式不會告訴你它碰到了上限——除非你叫它說。而排序過的截斷, 產生的不是雜訊,是趨勢。
所以看到單調的成長曲線時,先懷疑自己的取樣,再懷疑世界。 尤其是當那條曲線剛好支持你原本想講的話的時候。
修訂紀錄
- 首次發布