同じコードなのに結果が違う。分析環境の差異をどう切り分けるか
同じRプログラムを実行した。
使ったデータも同じで、処理はエラーなく最後まで終わっている。
それなのに、顧客が作成した資料と、確認環境で出力した結果の数値が一致しない。
こういうとき、どこから調べますか。
Rのバージョンでしょうか。インストールされているパッケージでしょうか。OSやロケール、乱数、並列処理も気になりますよね。
環境の違いを探し始めると、確認項目はいくらでも出てきます。
でも、違いをたくさん集めれば、原因へ近づけるとは限りません。
今回の調査でも、最初は二つの環境に入っているパッケージと、そのバージョン差を広く確認していました。
ところが、比較対象が増えるほど、どの違いが結果に関係しているのか分かりにくくなっていきます。
そこで、調査の視点を変えました。
二つの環境にある違いを探すのではなく、この結果を再現するために必要な条件を特定する。
処理を段階に分けて、どこまでは一致しているのかを見る。結果が分かれた処理に関係する条件だけを、一つずつ確認します。
原因を断定できない段階では、確認できた事実と仮説を分け、次に何を調べるべきかを示します。
分析結果の再現性を確かめる仕事は、環境差分の一覧を作ることではありません。
結果を変えた可能性のある条件を整理し、顧客が次の判断へ進める状態をつくることです。
コードが動くことと、結果が再現することは違う
今回確認したのは、複数のRプログラムで構成された分析処理です。
顧客が作成した結果資料と、確認担当者が手元の環境で実行した結果を突き合わせました。
対象となったプログラムは、すべて実行できています。エラーも出ておらず、出力ファイルも作られました。
それでも、一部の分析結果で数値が一致しませんでした。
プログラムが最後まで動けば、ひとまず問題はないように見えますよね。
でも、分析プログラムの品質確認では、「動いたか」だけでは足りません。
同じ入力から期待した結果が得られ、顧客が確認した数値を再現できるか。そこまで見て、初めて結果の確認になります。
とくに統計解析では、コード以外の条件が結果へ影響することがあります。
R本体やパッケージのバージョンだけでなく、関数へ渡した引数、乱数の設定、並列処理の方法、外部から取得した参照データも確認が必要です。
同じコードに見えても、その周囲にある条件まで同じとは限りません。
だからこそ、結果が違ったときに、いきなり一つの原因へ決めつけることはできません。
最初は、環境にある違いをすべて探していた
調査の初期段階では、顧客環境と確認環境に入っているパッケージを比較しました。
パッケージ名とバージョンを並べ、異なるものを原因候補として挙げていきます。
環境差異を調べる方法としては、自然に思えますよね。
結果が違い、環境も違う。それなら、違っているものを探せばよい。
ただ、Rの環境には、今回の分析処理へ直接関係しないパッケージも数多く含まれています。
二つの環境に100個の違いがあったとしても、その処理で使われていなければ、結果差異には関係しません。
差異を列挙するだけでは、再現に必要な条件は見えてきませんでした。
そこで顧客から、調査の視点について指摘を受けます。
大量のパッケージ差分を比較するのではなく、分析結果の再現に必要なパッケージを特定する視点で確認してほしい。
この指摘が、調査の転換点になりました。
環境全体の違いではなく、差異が発生した処理に注目し、その中で実際に何が使われているのかを確認する。
調査対象を広げるのではなく、結果が分かれた場所から狭めていく。
同じ「環境差異の確認」でも、見ている方向が変わったんですよね。
どこから結果が分かれたのか
確認対象となった複数のプログラムについて、入力から最終出力までを段階ごとに追いました。
前処理後の中間データ、次の統計処理、遺伝子セット解析、最後に図表や最終資料へ使われるデータを、それぞれ比較します。
その結果、前処理後の中間データまでは一致していることが分かりました。
差異が発生していたのは、その中間データを使ったGSEA以降の処理です。
ここまで分かると、調査範囲は大きく変わります。
入力データや前処理のロジックが違う可能性、その前段階で使われる多数のパッケージについては、原因候補としての優先度を下げられます。
原因は、最後に一致した出力と、最初に異なった出力の間にある。
そう考えられるからです。
分析処理を一つの大きな箱として見ると、どこを調べればよいか分かりません。
処理を区切り、それぞれの出力を比較することで、差異が生まれた区間を絞れます。
再現性の調査で最初に探すべきなのは、「違う環境」ではありません。
最後に一致する地点と、最初に異なる地点です。
パッケージ名ではなく、結果へ関与したものを見る
差異がGSEA以降で発生していると分かったため、その処理で使われるパッケージを確認しました。
GSEAの実行に直接関わるものだけでなく、遺伝子セットを用意するもの、内部で別の計算処理を呼び出すもの、結果の整形に使われるものも候補になります。
ただし、パッケージ名を挙げるだけでは足りません。
どの関数を呼び、その関数がどのアルゴリズムを使うのか。明示していない引数や既定値があり、外部データや依存パッケージの処理が関係していないか。
そこまで追って、初めて「結果へ影響し得る条件」が見えてきます。
環境に入っているものと、実行時に使われたものは同じではありませんよね。
インストールされていても、ロードされていないパッケージがあります。反対に、コードから直接呼んでいなくても、内部で利用される依存パッケージもあります。
確認する対象は、パッケージ一覧ではありません。
結果が分かれた処理の中で、実際に何が使われたかです。
環境情報は、いつ取得したかまで見る
顧客からは、Rのセッション情報も受領していました。
Rのバージョンや実行環境、ロードされたパッケージなどを確認できる情報です。
ところが、その出力には、今回のGSEAやエンリッチメント解析に関係するパッケージが記録されていませんでした。
では、顧客環境では、そのパッケージが使われていなかったのでしょうか。
そうとは限りません。
分析とは別のタイミングで取得したセッション情報だった可能性があります。
Rのセッション情報が示すのは、取得した時点の状態です。分析を実行した日とは別の日や別のRセッションで取得したり、パッケージをロードする前や分析処理を終了した後に出力したりしていれば、問題となった実行時の状態を十分に説明できません。
環境情報は、「取得した」というだけでは足りないんですよね。
いつ、どの処理の直後に取得し、その時点でどのパッケージが実際にロードされていたのかまで確認する必要があります。
今回の調査では、GSEAを実行した直後に、関連するパッケージのバージョンを直接出力するための確認コードを用意し、顧客へ共有しました。
再現性を考えるなら、コード、入力データ、出力結果と、その実行時のパッケージや設定を結び付けて残す必要があります。
これらが同じ単位で残っていなければ、後から環境を再現しようとしても、どの状態を再現すればよいのか分からなくなります。
バージョンをそろえても、結果はそろわなかった
次に、顧客側の主要なパッケージバージョンへ合わせた検証環境を用意しました。
普段使っている環境へ直接パッケージを入れ替えると、ほかの作業へ影響する可能性があります。
そこで、元の環境とは分けて確認できるようにしました。
検証環境を分離しておけば、変更した条件が分かりやすくなり、元の状態へ戻すこともできます。調査のために、普段の開発環境を壊すことも避けられます。
主要なパッケージのバージョンをそろえ、同じ処理を実行しました。
これで結果が一致すれば、パッケージバージョンが原因だった可能性が高まります。
ところが、結果は一致しませんでした。
調査が振り出しへ戻ったように感じるかもしれません。
でも、そうではありません。
「パッケージバージョンが違うから、結果が違う」という仮説の優先度を下げられました。
仮説が外れることも、切り分けの進展です。
原因調査では、最初に立てた予想を当てることが目的ではありません。
条件を一つ変えて結果を確認し、変化がなければ、その候補の優先度を下げて残った変数へ進む。
この繰り返しで、原因へ近づいていきます。
乱数と並列処理も確認する
GSEAのような分析では、処理方法や設定によって、乱数が結果へ影響する場合があります。
同じコードでも、乱数の系列が異なれば、結果が変わる可能性があります。
並列処理についても確認が必要です。複数の処理を同時に動かす場合、単純に一つのシードを設定しただけでは、各処理で同じ乱数系列を再現できないことがあります。
そこで今回の調査でも、シードが明示され、乱数の設定やRの乱数生成方式が一致しているかを確認しました。並列処理の有無と、並列実行時の乱数条件についても見ています。
ただし、乱数や並列処理が今回の原因だったわけではありません。
原因候補として条件をそろえ、結果への影響を確認した上で、優先度を下げています。
ここでも大切なのは、「乱数が怪しい」と思った段階で、原因だと書かないことです。
影響する可能性を考え、条件をそろえて確認し、結果が変わるかを見る。
その順番を守ります。
パッケージの次に、パッケージが使ったデータを見る
主要なパッケージバージョンをそろえても、結果は一致しませんでした。乱数や並列処理についても確認しています。
そこで次に着目したのが、GSEAへ入力する遺伝子セットです。
分析コードとパッケージのバージョンが同じでも、パッケージが参照したデータベースや遺伝子セットの内容が違えば、入力条件まで同じとは言えません。
ここは見落としやすいところですよね。
「同じパッケージを使っている」と聞くと、同じデータを使っているように感じます。
でも、分析用パッケージの中には、外部のデータベースを参照したり、取得済みのデータを使ったりするものがあります。
パッケージ本体の版が同じでも、参照したデータベースの版や取得時期、キャッシュされている内容が違うかもしれません。顧客が別に保存していた遺伝子セットを使っている可能性もあります。
そこで、実際に分析へ渡した遺伝子セットそのものを受領し、内容を比較する案へ進みました。
ソフトウェアの版をそろえた後は、ソフトウェアが読んだデータの版を見る。
再現性を確認するときには、この視点も欠かせません。
動かすための回避策が、再現性を壊すこともある
調査中には、依存関係のチェックを一時的に回避する方法も検討しました。
必要とされるパッケージが存在するように見せれば、処理自体は先へ進められるかもしれません。
エラーを解消するだけなら、便利そうですよね。
でも、そのパッケージから本来読み込まれる参照データや処理まで失われたら、どうなるでしょうか。
コードは最後まで動いても、元の分析とは別の条件で計算されることになります。
それでは、再現できたことにはなりません。
実行を成功させることと、分析条件を再現することは別です。
エラーを回避するための暫定策が、新しい結果差異を生むこともあります。
だから、回避策を入れたときには、何を飛ばしたのか、本来その依存先から何を取得していたのか、計算結果へ影響するデータや処理まで失っていないかを確認する必要があります。
第三の環境で、原因の所属を切り分ける
顧客環境と確認担当者の環境だけを比べていると、どちら側に固有の問題があるのか判断しにくい場合があります。
そこで、別の担当者の環境でも同じ処理を実行し、現象を再現できるか確認することにしました。
第三の環境で顧客の結果が再現されれば、確認担当者の環境に固有の条件がある可能性が高まります。
反対に、確認担当者と同じ結果になれば、顧客環境に固有の条件が残っているかもしれません。どちらとも異なる結果になれば、コードや入力以外に、複数の変数が影響している可能性も考えられます。
二つの環境だけで比較すると、「どちらが正しいか」という話になりがちです。
第三の環境を入れることで、問題がどこに属しているのかを、別の角度から確認できます。
再現性の調査は、正しい側と間違っている側を決める仕事ではありません。
どの条件で、どの結果が出るのかを整理する仕事です。
見つけた差異を、すぐに誤りと決めつけない
別の出力を確認した際には、顧客資料の図と確認環境の図で、値の正負が反転している箇所も見つかりました。
再実行しても、結果は変わりません。
そこでコードを追うと、図を作る段階で、データの前にマイナス記号を付ける処理がありました。
では、このコードが間違いなのでしょうか。
そう断定することはできませんよね。
表示の向きをそろえるために、意図的に正負を反転している可能性もあります。反対に、過去の修正で残った処理かもしれません。
この時点で確認できたのは、顧客資料と確認結果で正負が反転していること、コード上に正負を反転する処理があること、その処理の意図までは確認できていないことです。
担当者は、それぞれを分けて報告しました。
差異を見つけたとき、すぐに「バグです」と言いたくなることがあります。
でも、調査担当者の役割は、推測で結論を作ることではありません。
顧客や関係者が判断できるよう、確認できた事実と、まだ分からないことを整理することです。
原因が確定していなくても、報告はできる
調査を進めても、その時点で原因を一つに特定できないことはあります。
そんなとき、「まだ分かりません」だけでは、顧客も次に何をすればよいか判断できません。
一方で、確証のない原因を断定することもできません。
そこで報告では、確認できた事実、検証によって優先度を下げた候補、現在残っている仮説、未確認の条件、次に必要な情報や作業を分けます。
今回であれば、前処理後の中間データまでは一致している一方、GSEAの実行後から出力に差異があることは確認できています。
主要なパッケージバージョンを合わせても一致せず、乱数と並列処理の条件についても確認しました。ただし、実際に使用した遺伝子セットの内容が一致しているかは、まだ確認できていません。
そのため、遺伝子セットの版や実体が異なる可能性が残っており、次に分析へ渡した遺伝子セットそのものを比較する。
このように整理すれば、原因が確定していなくても、調査は前へ進みます。
顧客も、次にどの情報を用意すべきか判断できます。
原因を断定しないことと、何も報告できないことは別なんですよね。
再現性の調査は、違いを集める仕事ではない
同じコードで結果が違う。
そんなとき、環境にあるすべての違いを並べても、原因へ近づけるとは限りません。
まず、比較している入力とコード、引数が本当に同じかを確認します。その上で処理を段階に分け、どこまでは一致し、どの地点から結果が分かれたのかを特定します。
次に、その処理で使われるパッケージ、設定、乱数、並列処理、参照データを確認する。
一度にすべてを変えるのではなく、条件を一つずつ合わせ、仮説が外れたら、その候補の優先度を下げて残った変数へ進みます。必要であれば、第三の環境でも再実行します。
最後に、確認できた事実と仮説、未確認事項、次に必要な確認を分けて報告します。
再現性を確認するとは、二つの環境を見た目の上で完全に同じにすることではありません。
その結果を生んだ条件を、第三者が確かめられるところまで明らかにすることです。
ディーシステムでは、コードが正常終了しただけで確認を終えません。
顧客が得た結果と同じ結果になるか。異なるなら、どの処理から分かれ、何を確認すれば次の判断へ進めるのか。
そこまで整理することも、分析プログラムの品質確認に含まれます。
分析結果が一致しないときに必要なのは、環境の違いを大量に集めることではありません。処理を分け、最後に一致する地点を探し、結果を変え得る条件を一つずつ確認する。原因を断定できない場合も、事実と仮説を分け、次に必要な確認を示す。それが、顧客が分析結果を判断するための品質確認です。
分析結果を、次の判断へつなげる
分析プログラムの確認では、コードが動くことだけでなく、同じ条件で同じ結果を再現できるかを見る必要があります。
ディーシステムでは、結果に差異がある場合、環境差を無差別に並べるのではなく、処理経路を分け、差異が生まれた地点と、その処理に関係する条件を一つずつ確認します。
原因が確定していない段階でも、事実、仮説、未確認事項、次の確認を整理し、顧客が次の判断へ進める状態をつくります。
分析プログラムの検証や既存システムの品質確認に関する支援については、サービス情報でも紹介しています。
