このプロジェクトについて
EDINET(有価証券報告書等)の開示データをもとに、日本株の財務指標・投資判断スコアを日次で算出・公開しているサービスです。
公開サイトはこちら → https://stocks.happy-life-design.net
前回の記事(2026-10-04公開・前半)はこちら → https://qiita.com/rightcross-ae86/items/e9578b77b910732913e2
この記事は2部構成の後半です
| 内容 | 公開 | |
|---|---|---|
| 前半 | 株価データにコピーが混ざっているのを見つけた経緯と、見つかった4つの型 | 2026-10-04(上のリンク) |
| 後半(この記事) | 何を消して何を消さなかったかの判断、削除では直せずに残った課題、削除が狙ったデータだけだったかの確かめ方 | 2026-10-05 |
前半(2026-10-04公開)の要点は次の3つです。
- 株価データの中に、別の日の値をそのまま写したようなデータ(前日のコピー等)が混ざっていた
- 2026-09-30に1,021件、2026-10-03に全約1,600万件を調べ直して4つの型で3,441件、合わせて4,462件(全体の約0.03%)を削除した
- 原因は、現時点でも解明できていない
この記事では、1社の1日分の株価(始値・高値・安値・終値・出来高)を「1件」と数えます。始値・高値・安値・終値の4つの値を、「四本値」と呼びます。
先に結論
- 「四本値と出来高が前の日と同じ」というだけでは、消しませんでした。出来高の大きさか、日付のまとまりを条件に加えています
- 出来高が少ない銘柄で、同じ値が3日前後続いた5か所は、外部サイトの日足(1日ごとの株価)と全て一致した実データでした。これは残しました
- 削除では直せず、残っているものが3つあります(815件・約4,100件・出来高が大半0になる平日)。3つとも、まだ対応していません
- 削除が狙ったデータだけだったかは、全社の件数などを作業の前後で比べて確かめました
全件調査で見つかったものの行き先
全件調査(2026-10-03)で見つかったものが、削除・残す・課題のどこに分かれたかは次のとおりです。
「同じ値が続く」だけでは消さなかった
全件調査では、「四本値と出来高が前の日と全く同じ」データがたくさん見つかりました。ただし、同じ値が続いていても、必ずコピーとは限りません。そこで、2つのことを確かめて、消す条件を絞りました。
偶然の一致は、どのくらい起きるか
同じ銘柄の、10〜30日離れた日どうしで、四本値と出来高が一致する割合を調べました。離れた日どうしなので、一致はほとんど偶然です(遠い日のコピーも含むため、偶然の割合としては大きめに出る、上限の目安です)。
| 出来高 | 偶然に一致する割合(上限の目安) |
|---|---|
| 100〜1,000株 | 約0.01%(1万件に1件程度) |
| 1,000〜1万株 | 約0.003% |
| 1万〜10万株 | 約0.0006% |
| 100万株以上 | ほぼ0 |
出来高が大きいほど、四本値と出来高が偶然に一致することは、ほとんどありません。
出来高が少ない銘柄の「同じ値が3日前後続く」は、実データだった
一方で、出来高が少ない銘柄(200〜3,000株)に、四本値と出来高が同じ日が3日前後続く5か所がありました。外部サイトの日足と照合したところ、5か所とも全て一致しました。実際の市場で、同じ値が続いていたのです。外部サイトの日足を確認できるのは2014年7月以降なので、照合できたのはその範囲です。
そのため、「同じ値が複数回ある」というだけでは削除せず、出来高の大きさか、日付のまとまりを条件に加えました。
| 型 | 「前の日と同じ」に加えた条件 | 削除した件数 |
|---|---|---|
| ① | 2014年7月より前で、出来高1万株以上の銘柄に、同じ四本値が5営業日以内に3回以上ある。この組のうち、実データであり得るのは最大1件だが、どれか見分けられないため、全ての出現を削除した | 66件 |
| ② | 前日と四本値・出来高が完全に同じで、出来高が0より大きく、値動きがある。かつ、特定の7日にまとまっている(偶然の一致は1日あたり約1〜2件と見積もられるのに対し、この7日は1日に120〜1,080件) | 2,954件 |
| ③ | 取引所の休場日なのに、出来高が0より大きい(休場日に実際の取引は存在しない) | 210件 |
| ④ | 2014年7月より前で、前日と同じ株価のうち、次のどれかに当てはまる。出来高10万株以上/その日に前日と同じ株価が8件以上集中している/4〜7件の日で出来高1万株以上 | 211件 |
| 合計 | 3,441件 |
型②の削除に使ったコード
削除は、1つのトランザクション(途中で止まったら、全て元に戻る単位)で行いました。次は、型②の削除スクリプトの抜粋です。接続情報などは除き、省略した部分は(省略)と書いています。
for cid in ids:
rows = byc[cid]
# (省略)
a, b = rows[-2], rows[-1]
assert a[3:8] == b[3:8], (cid, d, a, b) # OHLCV完全一致
assert b[7] > 0 and b[4] > b[5], (cid, d, b) # 出来高>0・高値>安値
# (省略)
cur.execute(f"DELETE FROM stock_prices WHERE company_id IN ({lit}) AND time >= '{lo}' AND time < '{hi}'")
assert cur.rowcount == len(ids), (d, cur.rowcount, len(ids))
# (省略)
if DRY:
conn.rollback(); print('DRY RUN: rolled back')
else:
conn.commit(); print('COMMITTED')
- 削除する日ごとに、対象の各社の直近の行を取り直し、「当日の行と直前の行で、四本値と出来高(コード中の
OHLCV)が完全に一致している」「出来高が0より大きい」「高値が安値より高い(値動きがある)」を確かめます。1社でも外れると、assertで止まり、コミットされません - 削除した件数が、対象の社数と一致しなければ、同じく止まります
- 実行時に
dryを付けると、最後にコミットせず、巻き戻します。本番の削除の前に、この形で一度試しました
消さずに残したデータと、その理由
| 残したデータ | 件数 | 理由 |
|---|---|---|
| 出来高が少ない銘柄(200〜3,000株)で、四本値・出来高が3日前後続いたもの | 5か所 | 外部サイトの日足と全て一致した実データだった |
| 休場日に入っていた、出来高0の株価 | 1日あたり約1,700件 | 取引がなかったことを表す穴埋めのため、残した |
このほかに、削除していない815件(根拠が足りず、消せなかったもの)は、次の節で書きます。
削除では直せず、残っている課題
3つあります。どれも、対応は終わっていません。
課題1: 偶然の一致と見分けられない815件
2014年7月より前で、出来高が1万株未満、かつ特定の日に集中していない、前日と同じ株価が815件あります。上の表の割合と比べると、約9割はコピーと推定されます。ただし、1件ずつ見たときに、偶然の一致と見分ける方法がありません。削除する根拠がないため、削除していません。
課題2: 日付が1営業日ずれた型の約4,100件
2014年7月以降に、前日と同じ株価が約4,100件あります。2019年4月〜2020年3月には、1日に60〜120社ありました。流動性の低い(取引量が少ない)ETF・ETNで、日付が1営業日ずれた区間の終わりに、同じ値の重複が出る型です。約700銘柄に及びます。
ある銘柄(2019年11月5日〜8日)では、外部サイトの日足と照合して、データの11月5日・6日・7日の3日分が、実際は6日・7日・8日の値だったと確認できました。ずれた日付の行には、別の日の値が入っています。重複した行を削除しても、その日の株価が抜けるだけで、正しい値にはなりません。外部サイトの値で入れ直す必要があり、数千件規模になります。どう直すかは、まだ決めていません。
課題3: 平日なのに、大半の株価で出来高が0になる日
平日なのに、その日の株価の大半で出来高が0になっている日が、いくつかあります。出来高が付いている株価の割合は、次のとおりです。
| 日付 | 出来高が付いている株価の割合 |
|---|---|
| 2000-11-14・11-16・11-29・12-29 | 約49% |
| 2001-03-14 | 2.4% |
| 2003-08-15 | 24% |
| 2015-12-25 | 8% |
| 2016-07-11 | 7.7% |
取得元側のデータの欠けとみていますが、まだ調べていません。削除も訂正もしていません。
原因について
現時点でも、原因はまだ解明できていません。取得元のデータにもともと入っていたのか、取り込みの途中で入ったのかも、まだ区別できていません。今回の作業は、見つかったコピーの削除が中心でした。
削除したのは狙ったデータだけだったか
削除の前後で、次の3つを確かめました(2026-10-03)。
- 全社について、「件数・全ての行の内容から計算した照合用の値(ハッシュ値)・価格の合計」を、作業の前後で比べました。削除の対象でなかった社は、全社で一致しました(①の削除は4,587社、②は3,083社、③と④は4,382社)
- 削除した社は、「作業後 = 作業前 − 削除した分」が全社で成り立ちました(②は1,516社中1,516社、③と④は217社中217社)。残った株価は、1件も変わっていません
- 移動平均などの指標を、株価から独立に計算し直して、データベースの値と比べました。最大の差は0.00005で、丸め誤差の範囲です
照合に使ったコード
作業前後の集計は、次のSQLで社ごとに出しました(接続情報は除いています)。
select company_id, count(*), bit_xor(hashtextextended(t::text,0)),
coalesce(sum(open_price),0), coalesce(sum(high_price),0), coalesce(sum(low_price),0), coalesce(sum(close_price),0),
coalesce(sum(adjusted_close),0), coalesce(sum(volume),0), min(trade_date), max(trade_date)
from stock_prices t group by company_id
bit_xor(hashtextextended(t::text,0))が、各行の内容から計算した値(ハッシュ値)を、社ごとに全行分、XOR(排他的論理和)で1つにまとめたものです。同じ値どうしをXORすると互いに打ち消し合うので、作業後の値は、作業前の値に、削除した行の値をXORしたものと一致するはずです。削除した行は、バックアップのCSVから取り出して同じ集計をし、社ごとに比べました。次は、その比較の抜粋です(cbが作業前、caが作業後、rが削除した行の集計です)。
for cid, r in dele.items():
cb, ca = b[cid], a[cid]
cnt_ok = ca[0] == cb[0] - r[1]
h_ok = ca[1] == (cb[1] ^ r[2])
sums_ok = all(Decimal(ca[i]) == Decimal(cb[i]) - Decimal(r[i+1]) for i in range(2, 8)) # open..volume
件数と価格の合計は引き算で、ハッシュ値はXORで比べています。
まとめ
- 「同じ値が続く」だけでは消さず、出来高の大きさか、日付のまとまりを条件に加えた。出来高が少ない銘柄の5か所は、実データだったので残した
- 削除では直せず、残っているのは、815件・約4,100件・出来高が大半0になる平日の3つで、いずれも対応していない
- 削除が狙ったデータだけだったかは、全社の前後比較で確かめた。原因は、現時点でも解明できていない
気になる点・ご質問があればコメントでお知らせください。