はじめに
データ分析の間違いは、たいていエラーになりません。SQLは正常に返り、数字もそれっぽく、そのまま資料に載ります。この記事では、そういう「静かな間違い」の典型を8つ挙げ、それぞれを出す前に見つける方法を、SQLの例つきで整理します。
- 間違いの多くは「数える範囲がずれる」「分母を取り違える」「比べる集団の中身が違う」の3つのどれか
- 見つけるには、結合の前後で行数を比べる・同じ指標を2通りで出す・内訳ごとに結論が保たれるか見る、が効く
- 仮説をきれいに裏付ける結果ほど、一度疑う
対象読者: SQLで集計はできるが、出した数字の検証を体系立ててやったことはない人
ネタ元は、Anthropicが公開しているデータアナリスト向けのClaude用スキル(validate-data と statistical-analysis)です。そこに「共有前に照らし合わせる落とし穴のカタログ」がまとまっていて、Claudeに限らず人間の分析にもそのまま使えると感じたので、自分の言葉と例で書き直しました。
参考文献
先に一次情報を置いておきます。8つの落とし穴は、1本目のスキルで詳しく書かれている7つに、同じスキルの「その他の統計の罠」の一覧からシンプソンのパラドックスを加えたものです。後半の「怪しい結果のサイン」「検算の方法」も1本目が元です。
SQLの例はSQLite 3で実行して結果を確かめています。日付関数などの書き方はDBごとに違うので、お使いの方言に読み替えてください。
8つの落とし穴の全体像
先に一覧を出します。詳しくは順に見ていきます。
| # | 落とし穴 | 起きること | 見つけ方 |
|---|---|---|---|
| 1 | JOINで行が増える | 件数・合計が膨らむ | 結合の前後で行数を比べる |
| 2 | 今いる人だけを見る | やめた人が分析から消える | 「このデータに居ないのは誰か」を問う |
| 3 | 月の途中と丸1か月を比べる | 今月が下がって見える | 同じ日数で比べる |
| 4 | 分母がいつの間にか変わる | 率が上下するが中身は同じ | 定義の変更履歴を確かめる |
| 5 | 平均の平均 | 群の大きさを無視した値になる | 元データから集計し直す |
| 6 | タイムゾーンで日付がずれる | 日別の件数が前後の日に漏れる | 1つのタイムゾーンに揃える |
| 7 | 結果で切ったグループで比べる | 自分で選んだ人が「効いた」ように見える | 施策の前の属性でグループを切る |
| 8 | 全体と内訳で逆になる | 全体の結論が内訳で逆転する | 主要な切り口ごとに確かめる |
1. JOINで行が増える
一番よく起きて、一番気づきにくいものです。注文テーブルに「使ったクーポン」のテーブルを結合すると、クーポンを2枚使った注文は2行になります。その状態で SUM(amount) を取ると、その注文の金額が2回足されます。
注文4件・クーポン3行の小さなデータで試すと、こうなります。
-- 結合後の行数と、そこに含まれる注文の数を比べる
SELECT COUNT(*) AS joined_rows, COUNT(DISTINCT o.order_id) AS orders
FROM orders o
JOIN coupons c ON o.order_id = c.order_id;
-- joined_rows = 3, orders = 2 → 行が注文より多い
-- 誤り:重複した行の金額まで足している
SELECT SUM(o.amount) AS coupon_sales
FROM orders o
JOIN coupons c ON o.order_id = c.order_id;
-- 4000
-- 正しい:注文の行は増やさず、クーポンを使ったかどうかだけを見る
SELECT SUM(o.amount) AS coupon_sales
FROM orders o
WHERE EXISTS (SELECT 1 FROM coupons c WHERE c.order_id = o.order_id);
-- 3000
見つけ方は単純で、結合の前後で行数を比べることです。結合キーの重複は、結合する前にこれで確かめられます。
SELECT order_id, COUNT(*) AS n
FROM coupons
GROUP BY order_id
HAVING COUNT(*) > 1;
なぜ行が増えるのかの仕組みは、次の記事が図つきで丁寧に説明しています。結合キーが両方のテーブルで重複していると、一致した組み合わせを全部返すから、という話です。
2. 今いる人だけを見る(生存者バイアス)
「今のアクティブユーザーは、この機能をよく使っている。だからこの機能は継続に効いている」。よく見る推論ですが、この分析には、機能を使ったのにやめていった人が入っていません。今いる人だけを見ると、途中で抜けた人の情報がまるごと消えます。
手を動かして見つけるというより、集計を始める前に「このテーブルに居ないのは誰か」と一度問うのが対策です。具体的には次の2つを確かめます。
- 抽出条件に「現在の状態」(
status = 'active'など)が入っていないか - 退会・削除されたレコードが物理的に消えるテーブルではないか
後者は意外と多く、その場合は履歴テーブルやスナップショットを探す必要があります。
3. 月の途中と丸1か月を比べる
10月15日の朝に「今月の売上が先月より大きく落ちています」と報告すると、ほぼ確実に間違いです。10月はまだ14日分しかないからです。
-- 両月とも1〜14日に揃えて比べる
SELECT strftime('%Y-%m', order_date) AS month, SUM(amount) AS sales
FROM orders
WHERE CAST(strftime('%d', order_date) AS INTEGER) <= 14
GROUP BY month
ORDER BY month;
同じ日数でも曜日の並びが違えば差は出るので、日次の推移を見る仕事なら、前年同期比や同じ曜日どうしで比べるほうが安全です。週の途中で「今週の登録が少ない」と言うのも同じ間違いです。
4. 分母がいつの間にか変わる
コンバージョン率が先月から急に上がった。中身を見ると、「対象ユーザー」の定義から無料トライアルの人を外していた——というパターンです。分子がほとんど変わらなくても、分母が小さくなれば率は上がります。
このパターンはSQLの書き方では防げません。比べる期間のあいだで指標の定義が変わっていないかを確かめ、変わっていたら資料に書く、というのが対策になります。社内のダッシュボードの定義が途中で変わっていることもあるので、急な変化を見たら最初に疑うポイントです。
5. 平均の平均
100人で平均50ドルのグループAと、10人で平均200ドルのグループBがあるとします。全体の平均を、グループごとの平均からさらに平均して出すと125ドルです。しかし実際の平均は、人数で重み付けした63.64ドルです。
-- 誤り:グループごとの平均をさらに平均する
SELECT AVG(avg_revenue) AS wrong_avg
FROM (SELECT grp, AVG(revenue) AS avg_revenue FROM users GROUP BY grp) AS t;
-- 125.0
-- 正しい:元データから平均を取る
SELECT ROUND(AVG(revenue), 2) AS right_avg FROM users;
-- 63.64
事前集計したテーブルの平均の列を、BIツールで AVG() すると起きます。これについては以前、別の記事で詳しく書きました。
その記事でも書いたとおり、厄介なのはエラーが一切出ないことです。平均や比率は「足し算できない数字」なので、集計済みのテーブルには分子と分母を別の列で持たせておき、最後に割るのが根本的な対策になります。
6. タイムゾーンで日付がずれる
UTCで保存された時刻を、そのまま DATE() で日付にすると、日本時間の0時から9時のあいだの出来事が前日に入ります。深夜帯の多いサービスほど、日別・曜日別の傾向がずれます。さらに、期間の絞り込みは日本時間、日付の切り方はUTC、のように食い違うと、月の端の1日だけ件数が少なくなったり、前の月の日付が混ざったりします。
SELECT event_id,
created_at,
DATE(created_at) AS utc_date,
DATE(created_at, '+9 hours') AS jst_date
FROM events;
-- 2026-10-01 15:30:00 (UTC) は、日本時間では 2026-10-02
この書き方はSQLiteのもので、PostgreSQLやBigQuery、Snowflakeでは関数が違います。大事なのは関数ではなく、「この列はどのタイムゾーンで入っているか」を最初に確かめ、分析の中で1つに揃えて、どれに揃えたかを書いておくことです。複数のデータ源を突き合わせるときほど、ここでずれます。
7. 結果で切ったグループで比べる
「オンボーディングを最後まで終えた人は、継続率が高い」。これは、オンボーディングが継続を生んだとは言えません。もともと意欲の高い人が最後まで終え、その人たちがそのまま続けているだけかもしれないからです。比べるグループを、測りたい結果に近い行動で切ってしまうと、自分で選んだ人たちの差を施策の効果と取り違えます。
対策は、施策を受ける前に決まっている属性(登録時の流入元、登録した月など)でグループを切ることです。
観察データで「施策を受けた人と受けなかった人」を素朴に比べると、施策以外の要因で偏ってしまう問題を、どう補正するかは次の記事が詳しいです。
似た構造の罠として、2つの要因の両方から影響を受ける結果(合流点)で絞り込んだせいで、本来は関係のない2つの要因に見かけの相関が出る「合流バイアス」もあります。以前に小売の例で書きました。
8. 全体と内訳で逆になる
最後はシンプソンのパラドックスです。新規客と既存客のどちらで見ても施策Bのほうが購入率が高いのに、全体で合わせると施策Aのほうが高くなる、ということが起きます。
| 施策A | 施策B | |
|---|---|---|
| 新規客 | 5.0%(10/200) | 6.0%(48/800) |
| 既存客 | 20.0%(160/800) | 22.0%(44/200) |
| 全体 | 17.0%(170/1000) | 9.2%(92/1000) |
原因は配り方の偏りです。施策Aは購入率がもともと高い既存客に多く配られ、施策Bは購入率の低い新規客に多く配られていました。全体の数字は、施策の効果ではなく「誰に配ったか」の差を映しています。
全体の結論が出たら、主要な切り口(新規と既存、地域、チャネルなど)ごとに同じ結論が保たれるかを確かめます。仕組みは以前の記事でも、店舗と広告の例で書いています。
怪しい結果の5つのサイン
ここまでの8つは「間違いの型」でした。逆に、結果の側から「どこかで間違えていそう」と気づくためのサインもあります。元のスキルには次の5つが挙がっています。
- 前期比で50%を超える変化に、思い当たる理由がない
- 件数や合計がきりのいい数字になっている(フィルタや初期値の問題を疑う)
- 率がちょうど0%や100%になっている(データが欠けている可能性)
- 期間や切り口が違うのに値が同じ(クエリが切り口を無視している)
- 仮説をきれいに裏付ける結果
最後のものが一番刺さりました。期待どおりの数字が出たとき、そこで手を止めていませんか? 元のスキルの言い方を借りると、現実はもっと散らかっているはず、という前提に立ちます。
切り口をたくさん試して、たまたま差が出たところだけを報告する、というのもこの仲間です。偶然の相関がどれくらい簡単に出るかは、488品目×47都市の家計データで大量の相関を計算した、次の記事の例が分かりやすいです。
差がゼロのデータでも、有意水準5%で互いに独立な検定を20回すると、「どこかが有意」になる確率は64%まで上がります(元のスキルの言い方では、20個の指標を調べれば約1個は偶然有意になる)。この計算は以前の記事で実際にやってみました。
検算の5つの方法
サインに気づいたら、あるいは重要な数字を出す前には、次のどれかで確かめます。
- 同じ指標を別の2通りで計算し、一致するか見る(例:注文テーブルから出した売上と、明細テーブルから出した売上)
- 個別のレコードを数件選び、元データまで手でたどる
- 既存のダッシュボードや財務の数字と照らし合わせる
- 逆算する(1人あたりの値 × 人数 が合計とおおよそ合うか)
- 1日・1人・1カテゴリに絞ったとき、結果がおかしくないか見る
1の「2通りで出す」は、1番目のJOINの膨らみを見つけるのにも効きます。結合したクエリと結合しないクエリで同じ合計が出るかを比べれば、行が増えていればすぐ分かります。
まとめ
8つを並べてみると、根っこは3つに集約できました。数える範囲がずれる(1・3・6)、分母を取り違える(4・5)、比べる集団の中身が違う(2・7・8)。新しい集計をするたびに、この3つのどれかが起きていないかを考えるだけでも、かなりの間違いは防げそうです。
元のスキルでは、このカタログは「人に見せる前の総点検」の一部として置かれていて、点検の結果を「そのまま共有してよい」「注記を付けて共有」「作り直し」の3段階で判定するところまでが手順になっています。間違いを見つけたら直す、直せない限界は注記に書く、という使い方です。
自分が踏んだことのある落とし穴があれば、どれだったか教えてもらえるとうれしいです。

