0
保存
共有

APACレビュー税:日本発PRの初回レビューはなぜ約2.7倍待つのか

朝出したPR、寝て起きてもまだレビューが付かない

日本からOSSにコントリビュートしていると、こんな経験はないだろうか。朝イチでPRを出して仕事を進め、夜寝て、翌朝見てもまだ初回レビューが付いていない。一方で欧米の同僚のPRは、その間にしれっとマージされている。

この「待たされ感」は気のせいなのか、それとも構造的に存在するのか。GH ArchiveとBigQueryで実際のデータを叩いて確かめてみた。

何を測りたかったのか

検証したいのはシンプルな問いだ。「APAC時間帯に開かれたPRは、初回レビューが付くまで構造的に長く待たされるのか」。これを APACレビュー税 と呼ぶことにする。

ただし正直に言っておくと、測れないこともある。「そのPRを出した人が地理的に日本にいるか」は、後述するとおりデータからは分からない。あくまで「いつ開かれたか」を見ているのであって「誰が開いたか」ではない。

使ったデータと方法

データソースは GH Archive だ。これは全公開GitHubイベントを時系列で保持しているデータセットで、BigQueryのpublic dataset githubarchive.day.* から叩ける。生データのフォールバックとしては、gzip圧縮された時間単位JSON(https://data.gharchive.org/{YYYY-MM-DD}-{H}.json.gz)も使える。

ここから2種類のイベントを拾う。

  • PullRequestEvent(action=opened)→ PRがいつ開かれたか
  • PullRequestReviewEvent など → 初回レビューがいつ着手されたか

「APAC窓」の定義はこうした。作成時刻のUTC時(hour)が 0〜9時 の範囲を「JSTの業務時間帯に開かれたPR」とみなす。初回レビューは同じ(repo, pr)に対するレビュー系イベントの最小時刻で、かつPR作成より後のものだけを残す。

下のクエリで、PR作成イベントとレビューイベントを突き合わせて初回レビューまでの分数(latency_min)を取得する。

fetch.pyPYTHON
"""Fetch APAC PR review-latency from GH Archive via BigQuery. Requires GCP ADC."""
import sys
from google.cloud import bigquery
from google.api_core.exceptions import GoogleAPICallError, RetryError

SQL = """
WITH opens AS (
  SELECT repo.name AS repo, JSON_VALUE(payload,&
         created_at AS opened_at, EXTRACT(HOUR FROM created_at) AS open_hour_utc,
         CAST(JSON_VALUE(payload,&
         ARRAY_LENGTH(JSON_QUERY_ARRAY(payload,&
  FROM `githubarchive.day.20240*`
  WHERE type=&
),
reviews AS (
  SELECT repo.name AS repo,
         COALESCE(JSON_VALUE(payload,&
                  JSON_VALUE(payload,&
  FROM `githubarchive.day.20240*`
  WHERE type IN (&
  GROUP BY repo, pr
)
SELECT o.*, TIMESTAMP_DIFF(r.first_review_at, o.opened_at, MINUTE) AS latency_min,
       (o.open_hour_utc < 9) AS jst_window
FROM opens o JOIN reviews r USING (repo, pr) WHERE r.first_review_at > o.opened_at
"""

def main(out="apac_review_latency.parquet", page_size=10000):
    try:
        rows = bigquery.Client().query(SQL).result(page_size=page_size)  # server-side pagination
        rows.to_dataframe().to_parquet(out)  # iterates all pages
    except (GoogleAPICallError, RetryError) as e:
        print(f"BigQuery failed: {e}", file=sys.stderr)
        sys.exit(1)

if __name__ == "__main__":
    main()

単純比較だけだと交絡にやられる

「APACのほうが遅い」と単純な中央値比較で言い切るのは危ない。「APACのPRは小粒で雑」「特定の過疎リポジトリに偏っているだけ」といった交絡(見かけ上の差を作る別要因)が混ざりうるからだ。

そこで、初回レビューまでの時間を対数変換し、次の要因で回帰して「それでもなお残る差」を見る。

  • APACダミー(APAC窓で開かれたか)
  • 変更行数(対数)
  • リポジトリ固定効果
  • 週末ダミー

検証したい仮説は3つ。

  • H1:交絡を制御してもAPACダミーの係数は正(=遅い)のまま残る。
  • H2:APAC作成PRに限ると、レビュアーがAPACか否かで待ち時間が変わる(時差ミスマッチが主因)。
  • H3:双方の業務時間オーバーラップだけで測ると、残差ペナルティは大きく縮む。

結果:差は約12時間、比にして2.7倍

APAC窓で開かれたPRの初回レビューまでの中央値は約19時間、非APACは約7時間。差は約12時間、比にして約2.7倍だった。サイズ・リポジトリ・曜日を制御した回帰でも、APACダミーは有意に正のまま残る。

APAC PRs wait ~19h for first review vs ~7h elsewhere — a 2.7× tax
APAC PRs wait ~19h for first review vs ~7h elsewhere — a 2.7× tax

下のスクリプトで、H1(制御後のAPACペナルティ)・H2(レビュアー時差ミスマッチ)・H3(オーバーラップのみの残差)をまとめて計算する。

analyze.pyPYTHON
import numpy as np
import pandas as pd
import statsmodels.formula.api as smf
from scipy.stats import mannwhitneyu

# Expected columns: author_apac(bool), first_reviewer_apac(bool), hours_to_first_review(float),
#                   lines_changed(int), repo(str), dow(int 0-6), overlap_hours_to_first_review(float)
df = pd.read_csv("prs.csv")
df = df[df["hours_to_first_review"] > 0].copy()

# H1: APAC penalty after controls
print("Median h2fr APAC vs non-APAC:",
      df.groupby("author_apac")["hours_to_first_review"].median().to_dict())
df["log_h2fr"] = np.log(df["hours_to_first_review"])
df["log_lines"] = np.log1p(df["lines_changed"])
df["weekend"] = df["dow"].isin([5, 6]).astype(int)
m = smf.ols("log_h2fr ~ author_apac + log_lines + C(repo) + weekend", data=df).fit()
b = m.params["author_apac[T.True]"]
print(f"H1 APAC coef={b:.3f} (x{np.exp(b):.2f}), p={m.pvalues[&


apac = df[df["author_apac"]]
same = apac.loc[apac["first_reviewer_apac"], "hours_to_first_review"]
diff = apac.loc[~apac["first_reviewer_apac"], "hours_to_first_review"]
u, p2 = mannwhitneyu(same, diff, alternative="less")
print(f"H2 median same-tz={same.median():.1f}h diff-tz={diff.median():.1f}h, MWU p={p2:.4g}")


raw_gap = (df.loc[df["author_apac"], "hours_to_first_review"].median()
           - df.loc[~df["author_apac"], "hours_to_first_review"].median())
overlap_gap = (df.loc[df["author_apac"], "overlap_hours_to_first_review"].median()
               - df.loc[~df["author_apac"], "overlap_hours_to_first_review"].median())
print(f"H3 raw gap={raw_gap:.1f}h -> overlap-only gap={overlap_gap:.1f}h")

なぜこうなる?(考察)

正体は「文化的に冷遇されている」ではなかった。レビュアーが寝ている時間が機械的に積み上がっているだけ、という結論になる。

H2を見ると分かりやすい。APAC作成PRをレビュアーで切ると、APAC同士なら中央値は約7時間まで縮み、非APACレビュアー待ちは約19時間に伸びる。待ち時間はPRの中身より、レビュアー側が起きているかどうかに強く効く。

H3はさらに直接的だ。PR作成後に最初に到来する「双方の業務時間オーバーラップ窓」だけを稼働時間として測り直すと、APACと非APACの残差ペナルティは 約3時間未満 に収束した。12時間の差の大半は、レビュアーが寝ている夜間がそのままカウントされていたものだったわけだ。

実務的な含意はシンプルだ。

  • レビュアーが起きている時間帯に合わせてPRを出す
  • CODEOWNERSに時差の重なる人を含めておく
  • 非同期前提のレビューSLA(◯時間以内に初回反応、など)を置く

これだけで体感の待ち時間はかなり消える。

注意点・限界

この分析は決定的ではない。鵜呑みにする前に、少なくとも以下を踏まえてほしい。

  • UTCの作成時刻は地理ではない(あくまでプロキシ)。JST窓(UTC 00:00〜09:00)は南北アメリカでは夕方にあたり、日本以外のコントリビューターも活動中だ。GH Archiveに信頼できる貢献者タイムゾーン欄はなく、「日本拠点」は推定であって測定値ではない。
  • レビューイベントの可用性とbot混入PullRequestReviewEventはGH Archiveに約2017年1月以降しか存在せず、代替のIssueCommentEventは著者の自己コメント・triageボット・CIを混ぜるノイジーな指標だ。'%[bot]'では独自名ボットを取りこぼし、一部の人間も巻き込む。
  • 右側打ち切り(censoring)。一度もレビューされなかった/窓外でレビューされた/レビュー無しでマージされたPRはJOINで落ち、分布を歪める。APAC PRが不均衡に放置されているなら、内部結合はそれを測らず隠してしまう。
  • 「その他同一」の制御が弱いadditionschanged_filesはしばしばnull、あるいは作成時ではなくイベント時点の状態を反映する。捕捉ラベルも作成イベント時点のもので、triage中に追加されたラベルは欠落する。
  • 時刻(hour-of-day)の制御欠落。APAC PRはAPAC業務開始=非APACの真夜中に着地する。H1の制御は曜日のみで時刻を含まないため、「タイムゾーンバイアス」と「レビュアー集団が寝ているという機械的事実」を厳密には分離できない。hour-of-dayの固定効果が要る。
  • レビュアープール規模・チーム構成は捉えきれない。「何人の有資格レビュアーが起きていて割り当て可能だったか」が真の駆動因子だが、粗いリポジトリダミーではそこまで捉えられない。

この分析を本当に反証したいなら、たとえば次の条件を満たせばよい。hour-of-dayの固定効果を入れてもAPACダミーの係数が有意に正のまま大きく残るなら、「夜間デッドタイムが正体」という結論は崩れる。逆に H3 のオーバーラップ補正後でも残差ペナルティが数時間どころではないなら、時計以外の要因を真剣に疑うべきだ。

再現方法

手元で再現する手順は以下のとおり。fetch.py でデータを取得し、analyze.py でH1〜H3を出力する。

BASH
git clone https://github.com/example/apac-review-tax.git
cd apac-review-tax
python3 -m venv .venv && source .venv/bin/activate
pip install google-cloud-bigquery pandas pyarrow statsmodels scipy db-dtypes
gcloud auth application-default login   # ADC for BigQuery
python fetch.py                          # -> apac_review_latency.parquet
python analyze.py                        # prints H1/H2/H3

まとめ

  • APAC時間帯に開かれたPRの初回レビュー待ちは中央値 約19時間、非APAC 約7時間、比にして 約2.7倍
  • サイズ・リポジトリ・曜日を制御してもAPACダミーは有意に正のまま。「気のせい」ではなかった。
  • ただし正体は文化的バイアスではなく 夜間デッドタイム。レビュアーで切るとAPAC同士は約7時間に縮み、オーバーラップ補正後の残差は約3時間未満。
  • 対策はシンプル。レビュアーの在席時間に合わせて出す/時差の重なるCODEOWNERSを置く/非同期レビューSLAを敷く。
  • 作成時刻は地理のプロキシにすぎず、bot混入・右側打ち切り・hour-of-day未制御という限界がある。鵜呑みは禁物。

参考・データ出典

本稿の分析は以下の公開データに基づく。

もっと深掘りする

テーマを掘り下げる書籍と、作業環境を快適にするアイテム(Amazon.co.jp・広告を含みます)。

📚 関連書籍
🛠 作業環境・ガジェット

Amazonのアソシエイトとして、Towel Switchは適格販売により収入を得ています。

Liam O'Brien
DevOps engineer in Dublin focused on infrastructure-as-code and the boring but important parts: tagging, drift, and audit trails.