0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【日本株分析プラットフォーム】データの検証ツールが、1回目の復元の性質に頼っていた話(2026-10-15)

0
Last updated at Posted at 2026-10-15

このプロジェクトについて

EDINET(有価証券報告書等)の開示データをもとに、日本株の財務指標・投資判断スコアを日次で算出・公開しているサービスです。

公開サイトはこちら → https://stocks.happy-life-design.net
前回の記事(2026-10-14公開)はこちら → https://qiita.com/rightcross-ae86/items/e5e7bf43d30244452856

この記事について

株価データの一部を、別の株価サイト(外部サイト)の値で置き換える作業(復元)をしています。復元は数回に分けて進めていて(会社をグループに分けて)、1回目は28社、2回目は63社でした。置き換えのたびに、書き込んだ値が計画どおりになっているかを確かめる検証をします。この記事は、2026-10-09に行った2回目の復元で、その検証が164件も「不一致」になった話です。1回目の復元では通っていました。データは正しく、間違っていたのは比べ方のほうでした。

この記事は、単独で読めます。2026-10-14公開の株式分割の記事の続きではなく、別の話題です。

復元の背景と、1回目の復元の確かめ方は、次の記事に書いています(この記事は、それらの続編でもありません)。この記事では、復元の中身には深入りせず、検証ツールの話に絞ります。

この記事では、次のように呼びます。

  • 「行」: 1社の1日分の株価(始値・高値・安値・終値・出来高)
  • 「復元」: 外部サイトの値で、データベースの株価を置き換えること
  • 「1回目」「2回目」: 復元を数回に分けて進めている、その順番です。会社をグループに分け、1回目が28社、2回目が63社です
  • 「計画」: 復元で、どの行をどの値に置き換えるかを、書き込む前に一覧にしたもの
  • 「期待値」: 計画に書いてある、置き換えたあとの値
  • 「係数」: 外部サイトの値を、株式分割の調整後の尺度に合わせて換算するための、会社ごとの数
  • 「適用後の検証」: 書き込んだ直後に、データベースから読み直して、期待値どおりになっているかを確かめること
  • 「本番」: 実際に稼働していて、公開サイトが読んでいるデータベースのこと
  • 「小数4桁の列」: データベースの株価の列は、小数4桁までを保存する型(numeric(18,4))です。5桁目以降は四捨五入して保存します
  • 「循環小数」: 0.333…のように、同じ数字が際限なく続く小数

先に要点

  • 復元の「適用後の検証」は、書き込んだ値と期待値を、完全一致で比べていました
  • 1回目の復元(28社)では通りました。2回目の復元(63社・計画1,501行)では、164件(会社と日付の組)が不一致になりました
  • 原因はデータではなく、比べ方でした。係数に3が関わる会社では、期待値が循環小数になります。データベースは小数4桁で保存するので、4桁より先まで続く期待値とは、完全一致になりません
  • 切り分けは、不一致の実例を数件表示して、データが違うのか、比べ方が違うのかを分けることから始めました。元のツールは触らず、診断用のコピーを作りました
  • 直し方は、期待値をデータベースと同じ4桁(四捨五入)にそろえて比べる、数行の変更です。ゆるめたのではなく、データベースが保存できる精度にそろえました
  • 見つかったのは、本番に反映する前の、適用して検証し最後に取り消す形の試験です。この不一致で、本番のデータが壊れることはありませんでした

検証の仕組みと、起きたこと

復元のツールは、計画の各行をデータベースに書き込み、適用後の検証を行います。検証では、計画の全ての行について、次の3つを確かめます。

  • 始値・高値・安値・終値・出来高が、期待値と等しい
  • 出所を示す列が、復元に使った外部サイトの値であることを示している
  • 調整後終値の列が、空(NULL)になっている

ツールには、本番に反映する前の試験として、適用して検証し、最後に取り消す動かし方があります。本番のデータは変わりません。

1回目の復元 2回目の復元
会社数 28社 63社
計画の行数 750行 1,501行
係数の種類 割り切れる値だけ(1/2・5・10) 3が関わる値を含む(1/3や3)
適用後の検証 通った 164件が不一致

切り分け: データが違うのか、比べ方が違うのか

不一致の数を見て、すぐ復元の計画やデータを疑うと、正しいデータを書き直しかねません。まず、不一致の実例を数件表示して、どちら側が違うのかを分けました。

手順は次のとおりです。

  1. 元のツールには触らず、診断用のコピーを作る
  2. コピーに1行足して、不一致と判定した行のうち、最初の12件だけ、期待値・実際の値・出所の列・調整後終値の列を表示する(「技術的な補足」の2つ目の抜粋)
  3. 期待値と実際の値を並べて、違いの出方を見る

並べてみると、期待値は循環小数で、データベースの値は、それを小数4桁に四捨五入したものと同じでした。データは正しく、完全一致という比べ方が、データベースの保存の仕方に合っていませんでした。

原因: 割り切れない係数と、小数4桁の列

計算が割り切れる係数なら、期待値は有限の小数になり、4桁に収まります。割り切れない係数だと、期待値が循環小数になり、データベースに保存された4桁の値とは、完全一致になりません。

次の小さなコードで再現しました。このコードは、この記事のために書いたもので、実際の復元ツールのコードではありません。実際の換算の式は会社ごとに違いますが、ここでは「ある数で割る」形に単純化しています。値も説明用で、実際の株価ではありません。

from decimal import Decimal, ROUND_HALF_UP

before = Decimal("2062")                 # 説明用の値(実際の株価ではない)
for k in (2, 5, 10, 3):                  # 換算を「k で割る」形に単純化
    exp = before / k                     # 期待値(計画に書く値)
    db = exp.quantize(Decimal("0.0001"), rounding=ROUND_HALF_UP)   # 小数4桁で保存される値
    print(k, exp, db, exp == db)

実行すると、次のように出ます(実行結果のままです)。

2 1031 1031.0000 True
5 412.4 412.4000 True
10 206.2 206.2000 True
3 687.3333333333333333333333333 687.3333 False
割る数 期待値 小数4桁で保存される値 完全一致
2 1031 1031.0000 一致
5 412.4 412.4000 一致
10 206.2 206.2000 一致
3 687.3333333333333333333333333 687.3333 不一致

1回目の復元の係数は、2・5・10のように割り切れるものだけでした。そのため、この比べ方の弱点は、1回目の復元では見えませんでした。

直し方: データベースと同じ4桁にそろえて比べる

期待値を、データベースと同じ小数4桁(四捨五入)にしてから比べるようにしました。元のツールは直さず、修正版を別のファイルとして作りました。修正版の中身は、次の「技術的な補足」に実物を載せています。

  • 価格の4つの値(始値・高値・安値・終値)だけを丸める。出来高は整数なので、そのまま
  • 丸めたあとも、4桁までは完全一致で比べる。比べ方をゆるめて、少しの違いを許す形にはしていない

修正版で、2回目の復元の検証は通りました。計画の1,501行は期待値どおりで、追加と削除はありませんでした。その後の復元でも、修正版を使っています。

その後: 別の検査でも、同じ型の端数差が出た

2026-10-14に、復元の対象を別の150社に広げる準備をしました。計画を作ったあとの自己検査(外部サイトの値を換算した値と、データベースの値を比べる検査)で、不一致が225行出ました。

この225行は、全て計画に入っていない行(すでに一致と判定した行)でした。違いは、係数が1/3などの会社の、保存されている値と、厳密に計算した値との端数で、最大0.000067、小数4桁目より下でした。計画に入れる行では、不一致は0でした。

同じ型の端数差として説明でき、計画に入れる行には影響しませんでした。

1回目の復元で気づけなかった理由と、次から守ること

検証ツールは、1回目の復元の性質(係数が割り切れること)に、黙って頼って書かれていました。割り切れない係数の会社が入って、初めて通らなくなりました。

次から守ることを、3つ決めました。

  • 検証ツールを新しい回の復元で使う前に、対象の性質(今回は係数の種類)が、前の回と同じ範囲かを確かめる
  • 不一致が出たら、直す前に、実例を数件表示して、データの誤りか、比べ方の誤りかを分ける
  • 元のツールは触らず、診断用と修正版を別のファイルとして作る

限界

  • 4桁より下の違い: 修正版の比較は、小数4桁までの一致しか確かめません。データベースが保存できない精度なので、4桁より下は見えなくて構いませんが、見えていないことは確かです
  • 元のツールは直していません: 元のツールをそのまま使うと、割り切れない係数の会社で、また同じ不一致が出ます。以後の復元では、修正版を使っています

技術的な補足(実際に使ったコード)

ここに載せるのは、実際のソースコードからの抜粋で、元のファイルの連続した行です。実物と変えたのは、共通のインデントを除いたことだけです。取得元の名前が入る行は、抜粋の範囲に含めていません。データベースの接続情報も含まれません。

1. 元の検証(完全一致で比べる)

r.afterは、計画に書いた置き換えのあとの値(始値・高値・安値・終値・出来高を|でつないだ文字列)です。expが期待値、gotがデータベースから読み直した値です。このあと、別の行でgotとexpを比べ、1つでも違えば不一致として数えます(その行には外部サイトの名前が入るため、抜粋の範囲から外しました)。

exp = [Decimal(x) for x in r.after.split('|')]
got = [row[3], row[4], row[5], row[6], row[8]]

2. 診断用のコピー(不一致の実例を表示する)

元の検証に、2行足したコピーです。不一致と判定されたとき、数える(bad += 1)とともに、badが12以下の間だけ、会社・日付・期待値・実際の値・出所の列・調整後終値の列を表示します。表示するのは、最初の12件までです。

bad += 1
if bad <= 12: print('  不一致例', r.ticker, d, r.action, '期待', r.after, '/ 実際', [str(x) for x in got], row[11], row[7])

3. 修正版(データベースと同じ4桁で比べる)

価格の4つの値([:4])だけを、小数4桁・四捨五入(ROUND_HALF_UP)にそろえます。出来高([4])は、そのままです。ROUND_HALF_UPは、Python標準のdecimalから読み込んでいます。

# 2026/10/09: DBの列はnumeric(18,4)。係数が1/3や3の銘柄は期待値が循環小数になるため、DBと同じ4桁(四捨五入)で比べる
exp = [Decimal(x).quantize(Decimal('0.0001'), rounding=ROUND_HALF_UP) for x in r.after.split('|')[:4]] + [Decimal(r.after.split('|')[4])]
got = [row[3], row[4], row[5], row[6], row[8]]

まとめ

  • 復元の「適用後の検証」が、2回目の復元で164件不一致になりました。原因はデータではなく、期待値が循環小数になるのに、データベースが小数4桁で保存していることでした
  • 切り分けは、不一致の実例を数件表示して、データの誤りか、比べ方の誤りかを分けることから始めました。元のツールは触らず、診断用のコピーと修正版を別に作りました
  • 直し方は、期待値をデータベースと同じ4桁(四捨五入)にそろえて比べる、数行の変更です。比べ方をゆるめたのではなく、保存できる精度にそろえました
  • 検証ツールは、新しい回の復元で初めて使うとき、前の回の性質に頼っていないかを確かめてから使います

気になる点・ご質問があればコメントでお知らせください。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?