投票行動を正規分布で把握する 【JavaScript】

Python

統計的なデータ分析の試みとして、選挙の投票行動を正規分布でモデリングしてみます。
JavaScript でシミュレータを自作し、できるだけシンプルなモデルで投票行動が把握できるのか調べてみます。ひとつの例として投票行動を取り上げますが、一般のマーケティング分析にも応用できると思います。

以下の環境で動作確認をしています。
環境: Windows パソコン、Microsoft Edge

背景 ~ 投票行動は簡単な正規分布のモデルで把握できるのではないだろうか

最近、選挙が続いています。今後も選挙が予定されています。

ニュースの解説などを見ていると感覚的な用語がたくさん出てきます。たとえば、「無党派層」、「浮動票」、「風が吹く」、「票を固めきる」、などです。
解説者や立候補者には、政治経済分野の大学の学部や大学院を出ている方もたくさんいると思うのですが、定量的な把握や分析をほとんど見かけません。また、専門分野を学んでいるはずなのに、現状の認識や考えている内容が人によってバラバラです。
具体的な数字とどういったモデルに基づく主張かを明確化して、誰もがわかる形で説明してほしいものです。

最近はデータサイエンスといった用語がよく出てきます。選挙結果を見ていると、人々の投票行動は正規分布などで統計的に扱うことができるのではないかと思うことがあります。
ということで最近の選挙結果を例に、どのような投票行動になっているのか定量的な把握を試みることにします。

基本的な考え方

・ 簡単のため、有権者の投票行動を1次元(x軸の軸上)で把握するものとします。最もシンプルな1次元のモデルが妥当そうであれば、2次元などに拡張していけばよいと考えます。
・ 各党の固定支持層は、各党ごとに x軸上に配置されており、正規分布状に存在しているものとします。
・ 選挙ごとに投票先が変動する現象をモデル化するため、無党派層を定義します。
無党派層も同様に、x軸上の点を中心に正規分布状に存在するものとします。
・ 各正規分布は、x軸上に中心位置 x = x0、標準偏差 sigma1、面積 area1 で定義します。
・ 各党の固定支持層と無党派層を重ねたプロファイルを有権者全体の分布とします。プロファイルの面積が有権者数、票数(∝議席数)に対応するものとします。
・ 隣り合う2つの政党(x = x1, x2)について、x1 と x2 の中間を境界とし(直線を引き)、この境界のどちら側にあるかで投票先が決まるものとします(上記のグラフ参照)。つまり、x軸上で複数の正規分布が重なる等の場合は、最も近い党に投票先が決まるものとします。
無党派層の場合は、正規分布の各点で最も近い党に投票先が決まるものとします。
したがって、上記の2つめのグラフのように矩形の境界線を引くと、その矩形内のプロファイルの面積が獲得票数に該当することになります。

・ 上記の数式モデルを設定し、過去の選挙結果をうまく説明するように、正規分布の各パラメータを探します。これにより、各党の位置関係(距離)、支持者の大小・分布、無党派層の割合、投票行動の変化など、顕在化されていないパラメータを特定・推測していきます。

シミュレータの使い方

動くサンプル

シミュレータの動くサンプルを以下に貼っておきます。まずは、下記の mutoha (ピンク)のグラフをドラッグしてみてください。

使い方

・ 上記の画面上で、ピンクの mutoha(無党派層の正規分布のグラフ)をドラッグしてみてください。無党派層が動くにつれ議席数が増減するようすをシミュレーションできます。
上から2つ目のグラフに、対応する各政党の獲得議席数のシミュレーション結果が表示されます。
・ 同様に、各党のグラフをドラッグすると、議席数の変化をシミュレーションできます。
つまり、各党の政策・ポジショニングを、他党や無党派層の中心に近づけると/遠ざけると議席数がどのように変化するか、シミュレーションをすることができます。
・ グラフの元データは、画面の下方のテキスト欄で定義しています。
・ テキスト欄のデータを変更した場合は、[set] ボタンをクリックしてください。グラフに反映されます。
・ テキスト欄の各行は、各政党(支持層等)に対応しています。
各行では、半角スペース区切りで以下を定義しています。
中心座標x0 標準偏差sigma1 面積area1 文字列(政党名)

・ テキスト欄の最後の1行では、無党派層を定義します。必須の行です。
無党派層も同様に正規分布で定義しますが、上記の2つめのグラフの計算の際、無党派層のプロファイルについては、各党に割り振られるようにしています。
無党派層を表示させたくない場合は、末尾行を “1.00 1.00 0.00 -” (面積 area1 = 0.00 とする)などとしてください。
・ 操作の一例として、テキスト欄の末尾で、mutoha の座標位置を x0 = 0.00 → -2.00 に変え、[set] ボタンをクリックしてください。無党派層が動いた場合の議席数を計算できます。
また、標準偏差 sigma1、面積 area1 の値も変更してみてください。
要素(政党)の追加・削除も可能です。
・ 縦軸のスケールを変えたい場合は、y1_scale、y2_scale の値を変更してください。
・ 横軸のスケールを変えたい場合は、x1_max の値を変更してください。グラフは、-x1_max から +x1_max の範囲で描画します。また、この領域が下記の積分計算の範囲となります。
・ 議席数は total1 で設定してください。サンプルでは、初期値として衆議院の議席数としていますが、店舗ごとの来客数、製品ごとの販売数、売上高などを設定し、顧客動向などを分析することも可能だと思います。
・ HTML の画面の下の部分に、各政党の獲得議席数、支持率、固定支持層の割合も、べた書きで表示しています。ここで固定支持層は、第2のグラフで対応する矩形範囲内での面積の差から求めています。(全体のプロファイルの面積から無党派層のプロファイルの面積を引くことで求めています。)

計算処理の説明

・ 上記のシミュレータでは、まず、テキスト欄で定義された内容を取得します。
・ つぎに、1つめのグラフで、各政党支持層と無党派層の正規分布を生成しています。
・ 2つめのグラフで、各正規分布のグラフを集計し、全体のプロファイル(赤ライン)を生成しています。
なお、無党派層のグラフは、1つめのグラフでも、2つめのグラフでも表示しています。2つめのグラフでは、全体のプロファイルの中で、無党派層がどのように占めているのかがわかるようにしています。
・ つぎに、1つめのグラフで、隣り合う政党のピーク位置の中間の座標を計算し、2つめのグラフでライン(矩形)を描いています。
・ この矩形の範囲が、対応する政党への投票に該当します。そこで、2つめのグラフのプロファイルについて積分計算を行うことで、面積(得票数)を求めます。
・ 積分計算で求めた各党の得票数(面積)の比率に応じ、議席総数 total1 を分配することで、各政党の獲得議席数を求めます。
・ JavaScript のプログラムを末尾に貼っておきますので、アレンジしたい方は流用してみてください。

数値モデルの作り方の例

上記の数値モデル、数値データの作り方の手順の概略をまとめておきます。2021年、2024年の衆院選を例にとります。
① インターネットなどで過去の獲得議席数のデータを集めます。
② Excel などを使って、各党の議席数を総議席数で割って、各党の議席の占有率を求めます。
もし無党派層が 0% なのであれば、この占有率が各党の正規分布の面積 area1 に対応します。
③ 無党派層は 50% 前後はあるといわれています。そこでまず、初期値として②の占有率を 1/2 倍して各党の area1 を設定します。つぎに、無党派層を占有率 50% (area1 = 0.500)として、末尾で定義します。
なお、各 area1 の値については、各党の獲得面積の比率から、議席数 total1 を割り振ることになります。したがって、各 area1 の値を一斉に n 倍しても、結果は同じになります。
④ 2021年から2024年で、各党の議席の増減を求めます。
つぎに初期値として、2021年の無党派層を中央 x=0 に配置します。また、2021年から2024年にかけ、無党派層が左側にシフトする(x = 0.00 → -2.00)とします。左にシフトするか、右にシフトするか、シフト量をどれくらいにするかは、最初は単なる初期値ですので、特に意味はありません。
上記のようにおくと、正規分布の形から、議席数の増減が大きい政党が x = 0 の原点近くに位置する(※1)ことになります。議席数の変化が少ない政党は、正規分布となっている無党派層からは遠いところにあることになります。
また、無党派層が左にシフトするとすると、議席が増加する政党は x < 0 の領域に、議席が減少する政党は x > 0 の領域に配置する(※2)ことになります。
そこで、(※1)、(※2)を満たすように、たとえば、x0 = -8, -6, -4, -2, 0, 2, 4, 6, 8 などと適当な間隔をあけ、過去の獲得議席数に基づき、各政党を配置していきます。
標準偏差 sigma1 は、初期値としては隣り合う政党と標準偏差のグラフが重ならない程度に設定し、選挙結果(固定票の流出・流入の度合い)から、値を調整します。
一般に、±3σ 内、左右のレンジで 6σ 内 に正規分布の面積の 99% が入ります(68-95-99 ルール)。したがって、σ を各政党の正規分布の平均とみなし各政党を 6 σ 程度ずつ離して配置すると、各正規分布はほぼ離れて並ぶことになります。
実際には、固定支持の票にも流出入があります。そこで、各政党の距離が 4σ 程度となるようにおくなどσの初期値を設定します。選挙結果の説明がつかない場合は値を調整していきます。大きな政党は固定票の流出入も大きくなる傾向があるため sigma1 も大きめに微調整していくことになると思います。
⑤ パラメータを設定するごとに、[set] ボタンをクリックし、議席数を確認します。
⑥ 2021年、2024年の結果がうまく再現できるように、各座標位置、標準偏差、面積を調整しつつ、値を探していきます。

※ 本来、④~⑥の最適化の部分については、Python などを使ってベストフィットを求めるプログラムを書くことも可能です。しかし今回は、モデルが妥当かどうか、数値モデルを設定したときに抽出される課題は何か、などがわかりませんでした。
そこで、プログラムを作りこむよりは、正規分布が視覚的に JavaScript で把握できるようにし、モデルの妥当性を確認することを優先しました。

わかること

主要プレーヤや浮動層の分布、ポジショニング

現実の現象を再現する分布を特定していくことで、各主要プレーヤーや浮動層の位置関係、母集団の大きさなどを推測することが可能です。
一例として、上記のシミュレータで、無党派層 mutoha を、自民党側から立憲民主党側にスライドさせる(x0 = 0.00 → -2.00 とする)と、2021年の衆議院選挙、2024年の衆議院選挙の結果を有効1桁~2桁程度で再現できることがわかります。
現実の結果とは、数議席程度のずれがあります。
パラメータを微調整していくことで、また、モデルの置き方を変えることで、さらにずれ量を最小化できる可能性があります。
今回は、モデルの妥当性が確認できれば十分ですので、計算モデルと数値例を公開することを優先しています。

浮動層の大局的な動き

上記の衆院選の結果で、左側に左寄りの政党、右側に右寄りの政党となっているとわかりやすいです。
ところが、選挙結果から各党の位置関係を見積もると、実際には思想的には混在した結果になっています。
つまり、無党派層の動きを1つの正規分布で近似すると、第一近似としては、今回の無党派層の動きは、一般的な右寄り、左寄りといった政治信条に基づく指標ではなく、既存の政党が見限られたなど、別のモノサシで動いているといえそうです。
少し見方を変えた別の例を挙げると、高齢化している政党(グラフの右側の政党)から無党派層が離れ、若い政党、新しい人々の取り込みに成功している政党、新しい候補者への入れ替えを進めている政党、が左側に位置しているというとらえ方もできます。

人口動態からすると毎年人々が数%ずつ入れ替わるため、選挙のたび、多かれ少なかれこうした影響も入り込むと考えられます。

一般によくいわれる現象

上記のシミュレータを使って、与党第一党、野党第一党、無党派層の3つの正規分布からなるシンプルなモデルを設定すると、よく観察されている現象の多くが再現できます。
たとえば、与党第一党と野党第一党は票数を集めているので、無党派層の中心は与党第一党と野党第一党の中間近辺にあって、与党寄りにあると考えるのが自然です。
無党派層の中心が与党第一党の左側にあるのか、右側にあるのかは、選挙ごとに動く可能性があります。

そこで、上記の状態で、野党第一党の政策(ポジション)を与党側(与党や無党派層の中心側)に寄せるとします。すると、議席数が増加する様子をシミュレーションできます。
逆に、野党第一党のポジション・政策を、与党側ではなく他の野党側に寄せる(非常に小さな政党と共闘する/連帯する、少数意見に寄せすぎる)と、無党派層の中心からは遠ざかることになって得票数を下げることになります。
これは、2024年の東京都知事選などで見られた現象です。言われてみれば当たり前ですが、シミュレータで現象を可視化することができます。
(理数系の教育を受けていないのか、定量的な把握をせずに、この統計的な現象を理解していないと思われる人々が結構多くの割合でいそうな印象があります。混沌としたまま政策や選挙戦略を作っているのでしょうか?!)

主要プレーヤの政策変更の影響

2024年衆院選では、野党第一党と与党第一党の党首がともに変わり、与野党のトップの政策・方針が似てきたといわれています。
この場合、野党第一党としては、獲得議席の増加が見込めることになります。
つまり、規模の小さい側が、党首や公約を変え、規模の大きい政党や無党派層の中心位置に寄せていくと、浮動票を分け合う格好になるため有利といえます。コバンザメ戦略といったところです。

一方、与党の側からすると、自分以外の政党では野党第一党が母集団としては最大です。そこで、政策を野党第一党に寄せて(自分たちの正規分布を第一野党側に動かして)、相手方の無党派層や固定支持層を取り込みたくなります。
ところが、無党派層の位置(民意の中心)を超えて、自分たちより小さな野党に寄せすぎてしまうと、自分たちの政党に乗っていた無党派層が流出する可能性が高くなります。
これは例えば、2021年の獲得議席をシミュレータで再現しておき、与党の位置を無党派層を超えてさらに野党側にシフトさせることで、票を失っていく状況をシミュレーションできます。
つまり、自分たちの規模より少数となっている野党側(少数意見、小数ニーズ、ニッチ)に政策を寄せすぎると、(取り込む無党派層の票数)<(失う無党派層の票数)になるということです。実際にはこれに加え、政策を変えると、固定支持層がばらける(固定支持層の標準偏差 sigma1 が大きくなる)効果も加わると考えられます。

もし、与野党の第一党同士が表面上は批判しあっていたとしても、互いに近づきたいのであれば、投票行動をする側からみると同じポジションに近づいていることになります。政界再編が生じやすくなる方向性といえます。
いいかえると、政策を明確化しない/同一化戦略を採る/あいまい戦略を採る/政策変更を繰り返す/批判だけをすると、あらゆる支持者を集められると考えるかも知れません。しかしながら他方で、差別化要因を失っていくことになります。自分たちに正規分布で集まっている人々に対し響くポイントを失ってしまい、票がばらける要因を強めてしまう(標準偏差 sigma1 が大きくなる)ことになります。

投票者側に対する票固め(標準偏差 sigma1 を小さくする)という観点では、何をしたいのか/何が異なるのか、旗印を明確にしていったほうが有利といえます。
このように、主要与野党や無党派層の大きさや位置関係によって、同一化戦略が有利か、差別化戦略が有利かなど、採るべき戦略が切り替わることを定量的なモデルから見積もっていくことが可能です。

少数野党の観点で見るとどうなるか

また、少数野党で、スクープを連発して与党を攻撃するような場合を考えます。この場合、攻撃が成功すると、与党を支持していた無党派層が動き、票がばらけることになります。
ところが、現状の投票結果から推測したモデルからすると、少数野党のポジショニングは、無党派層の中心から遠すぎることがわかります。
たとえば、議席数を 500 議席とすると、1議席は 0.2% に対応します。少数野党の議席の変動が2議席前後だったとすると、0.4% 程度となります。標準偏差の 65-95-99 ルールからすると、0.4% は、3σ以上の距離となります。つまり、上記の少数政党は、無党派層の中心から 3σ 程度以上、離れている可能性があります。無党派層が全体の50%程度だとすると、さらに距離が離れていることになります。
したがって、3σ以上離れたところで無党派層が動いたとしても、議席数の大きな変化、大勝利は望みにくいといえます。(政策変更をして無党派層の中心に近づくか、新規支持者を取り込むようにしたほうが、議席数拡大にはつながる。)
すると、少数野党としてはスクープによる攻撃はしたものの、自分からは遠いところで票がばらける/動くことで、無党派層の中心に近いところでポジショニングを取っている複数の他党を利するだけになる。候補者をいくら立てたところで、無党派層の中心からは3σ以上離れているので落選者が増えるだけになる、といった現象も定量的に把握することができます。(通行人が0人のところでラーメン店を10店舗開業しても客数が増えるわけではない。)

また、無党派層の中心寄りに政策変更をする意思のない少数政党は、無党派層の正規分布の右側か左側でポジショニングを取ることになります。すると、シミュレーション結果からすると、こういった少数野党同士で票の奪い合いが起きているととらえることができます。(少数野党は少数野党どうしで勝ち組と負け組にわかれていく。少数野党のライバルは与党というよりは他の少数野党となっている。

正規分布のモデルからみた固定支持層とは何か

上記のモデルでは、標準偏差 sigma1 の値を変えることで、正規分布のすそ野が、隣接する他党に入り込む設定をすることも可能です。
よく「票固めをする」という表現がありますが、上記の正規分布のモデルからすると「票固めをする」とは、自分の党が政策的にカバーしている/リーチできる有権者の範囲に対し、標準偏差 sigma1 を小さくすることで、自党に流れ込む支持者の票数を最大化すること、とらえることができます。
一例として最近のニュースでは、与党は支持層の6割程度しか票固めができなかったという報道があります。
上記のモデルでいえば、無党派層がまず逃げて、残った自分の党を支持する正規分布のうち、4割前後が他党に逃げたことになります。得票は、上記のシミュレータの2番目のグラフの矩形領域内となるため、正規分布のすそ野は、矩形領域の外側に逃げていることになります。
ここから、標準偏差 sigma1 の値についても、他党より広いのか狭いのか等、推測できることになります。各報道で出てくるニュースの背景を定量的に特定していくことが可能といえます。
ざっくりで見積もると、票数が大きな与野党は、標準偏差 sigma1 が大きめ、少数野党は標準偏差が狭め、とすると選挙結果を説明しやすいようです。
このことから、昔から生き残っている少数政党は、結果的に、ニッチ戦略を採っていることが推測できます。
つまり、ニッチ戦略で生き残っている少数政党は、政治的信条か宗教的理由か等は問わず、標準偏差を大政党より小さくすることで、逃げない支持者を維持し続けている、無党派層に依存していない、無党派層・主要なマーケットを意図的に無視している、かのような戦略を採っていると考えられます。
見方を変えると、これまでニッチ戦略を採っていた組織なり企業なりが、主要なマーケットを追いかけようとして従来の方針・ポリシーを二転三転させると、それまで支持してきた強いファンが散逸してしまい(標準偏差 sigma1 が大きくなり)、組織の存続が難しくなる可能性があるため注意が必要となります。少数の組織、新しい組織(政党、企業など)の注意点といったところです。

統計的な観点でみるとどうなるか

今回はまず、何がしかのばらつきのある集団の行動について、集団の行動が一次元の x 軸上に分布しているとみなすとどのように把握できるのか、という観点で現象をとらえています。
ここで x 軸は、各政策など複数のモノサシや属性があるなかで、投票行動として結果的に大きくばらける何がしかの軸となります。主成分分析でいう、第1主成分に近いイメージです。
この x 軸上で、多くの人々が各党や無党派層を選択して集団を形成するものとして、党Aを選択する人々は、平均すると x0 あたりにいて、他党と迷っている相反する両端の2割は x0 ± XX のあたりにいる。党B、党C、…、無党派層についても同様、となります。
ここで、各集団の位置や分布は平均値や集計値として出てくるので、中心極限定理が働いてくるのではないかという気がしてきます。すると、投票行動は正規分布で把握できるのではないだろうかといった冒頭の疑問が沸くので、上記の数式モデルをざっと作り、シミュレーションをしてみました。
こういった分野について詳しい方がいらっしゃったら、何か分析例を教えていただきたいくらいです。面白そうな着眼点で分析ができるのではないかと思います。

上記以外にも、多くの示唆を得ることが可能です。しかしながら、いくらでも考察が続いてしまい収拾がつかなくなるため、これ以降は省略することにします。

まとめ

投票行動を正規分布でモデリングし、実際の投票結果の把握を試みました。
議席数やそれらの増減については正規分布のモデルでおおかた再現することができ、定量的な把握は可能であると考えられます。
加えて、現状を極力模擬したモデルから、定量的に何がいえるのか考察を行いました。
同一化戦略、差別化戦略、ニッチ戦略など、マーケティングや企業戦略でよくいわれる概念について、正規分布の数値モデル上で、具体的な位置関係として把握可能と考えられます。

より詳細には、Python などで最適化計算を行うなどすれば、さらに詳細を調べていくことが可能です。機械学習を入れていくことも可能です。

今回は投票行動のモデリングを行いましたが、一般のマーケティング分析も実質的に同じといえます。
上記の政党を、各店舗、各製品、ウェブサービスなどに置き換え、実際の購買データなどから、固定客、浮動客、ユーザー動向、天候、気温の影響などをモデルに入れていけばよいことになります。

上記の程度の内容は、政治学、国際関係学、国際政治、経済学、社会学など、どこかの大学の研究室や学生、分析機関、報道機関・メディアなどでやっていてもよさそうです。ところが、インターネットで検索しても、論文を検索しても、なぜか近いものが見つかりませんでした。そこでざっとモデリングをして数量的に状況を見積もってみました。

他にも、ネット検索のトレンドから各ワードの相関係数を求めたり、機械学習で株価予測を試みたりしています。関心のある方は、関連リンクなども参照してみてください。

各政党の綱領の文字列などから、各政党間の相関係数のマトリックスを求める。相関係数のマトリックスを scikit-learn などでクラスタリングして2次元平面に落とし込み、各政党間の距離・位置関係を求める。今回の政党支持層と無党派層を2次元平面上に拡張してマッピングし、投票行動を可視化する。このようにすると、人々の潜在的な需要や動きをさらに定量的に評価・分析できるような感じがします。どこかの大学や企業の研究室などで、どなたかやってみてください。。

関連リンク
・ 任意の単語間の相関係数を求めてみる 【Python & Google Trends】
・ 機械学習で株価予測 【Python】

外部リンク
・ 公職選挙法
・ 総務省 選挙関連資料

サンプルスクリプト

正規分布シミュレータ normal_distribution_simulator1.html


<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>normal_distribution v0.1</title>
</head>
<body>
normal distribution simulator v0.1 <br>

    <canvas id="canvas1" width="800" height="200" style="border:1px solid #000000;"></canvas>
    <canvas id="canvas2" width="800" height="200" style="border:1px solid #000000; margin-top: 20px;"></canvas><br>

    y1_scale:<input type="text" id="textbox01" style="width:60px; height:14px;" value="4.0" onchange="set1()">
    y2_scale:<input type="text" id="textbox02" style="width:60px; height:14px;" value="3.0" onchange="set1()">
    x1_max:<input type="text" id="textbox03" style="width:60px; height:14px;" value= "10" onchange="set1()">
    total1:<input type="text" id="textbox04" style="width:60px; height:14px;" value="465" onchange="set1()">

    <div id="result1">-</div><br>
    <button onclick="ini1()">initialize</button>
    <button onclick="set1()">set</button><br> 
    <textarea id="textarea1" rows="14" cols="50" placeholder=""></textarea><br>

    <div id="result2">-</div>
    <div id="result3">-</div>

<script>

const canvas1 = document.getElementById('canvas1');
const canvas2 = document.getElementById('canvas2');
const result1 = document.getElementById('result1');
const result2 = document.getElementById('result2');
const contxt1 = canvas1.getContext('2d');
const contxt2 = canvas2.getContext('2d');

let y1_scale = 1.0; 
let y2_scale = 1.0; 
let x1_max = 10.0; 
let total1 = 465; 
let dragging1 = null;

let distributions1 = null; 
let rects1 = null; 

let totalProfile1 = new Array(canvas1.width).fill(0);
let totalProfile2 = new Array(canvas1.width).fill(0);

window.onload = onload1; 

function onload1() {
    ini1(); 
} 

function ini1() { 
    let str1 = `
-6.70 0.50 0.000 hosyu 
-6.50 0.50 0.001 sansei 
-5.40 0.50 0.007 reiwa 
-5.10 0.50 0.006 kokumin
-3.00 0.75 0.050 rikken
 0.90 0.75 0.320 jimin
 3.00 0.75 0.020 ishin
 3.10 0.50 0.070 komei 
 5.70 0.50 0.015 kyosan 
 7.00 0.50 0.030 syoha 
 8.50 0.50 0.001 syamin 
-2.00 2.00 0.600 mutoha
`.trim(); 

    document.getElementById('textarea1').value = str1; 

    set1(); 
} 

function set1() { 
    y1_scale = parseFloat(document.getElementById('textbox01').value); 
    y2_scale = parseFloat(document.getElementById('textbox02').value); 
    x1_max   = parseFloat(document.getElementById('textbox03').value); 
    total1   = parseInt(document.getElementById('textbox04').value); 
    let str1 = document.getElementById('textarea1').value; 
    distributions1 = parseText1( str1 ); 

    calculateDistributions1();
} 

function unscaleX1(x1) { 
    return parseInt(((x1 + x1_max) / (2.0*x1_max)) * (canvas1.width-1));
} 

function scaleX1(x1) {
    return (x1 / (canvas1.width-1)) * (2.0*x1_max) - x1_max;
} 

function gaussian1(x1, x0, sigma1) {
    return (1.0 / (sigma1 * Math.sqrt(2.0 * Math.PI))) * Math.exp(-0.5 * Math.pow((x1 - x0) / sigma1, 2));
} 

function parseText1(text1) {
    const lines1 = text1.replace('  ', ' ').trim().split('\n');
    let obj1 = lines1.map(line1 => {
        const elements = line1.trim().split(' ');
        const x0 = parseFloat(elements[0]);
        const sigma1 = parseFloat(elements[1]);
        const area1  = parseFloat(elements[2]);
        const label1 = elements[3]; 
        return { x0, sigma1, area1, label1 };
    });
    return obj1; 
};

function generate_rects1() { 
    let a1 = []; 
    let area1 = 0.0; 
    let area2 = 0.0; 
    let x1 = -x1_max; 
    for (let i1 = 0; i1 < distributions1.length-2; i1++) { 
        let x21 = distributions1[i1  ].x0; 
        let x22 = distributions1[i1+1].x0; 
        let x2 = (x21+x22)/2.0; 
        a1.push( { x1, x2, area1, area2 } ); 
        x1 = x2; 
    } 
    let x2 = x1_max; 
    a1.push( {x1, x2, area1, area2 } ); 
    rects1 = a1; 
} 

function color1( n1 ) { 
    color0 = [ "#ff0000", "#ffa500", "#daa520", "#008000", "#0000ff", "#4b0082", "#8a2be2", "#db7093", "#a52a2a", "#696969", "#008b8b", "#ff00ff" ]; 
    n0 = n1 % color0.length; 
    return color0[n0]; 
} 

function profile1(n0, x1) { 
    return distributions1[n0].area1 * gaussian1(scaleX1(x1), distributions1[n0].x0, distributions1[n0].sigma1); 
} 

function calculateTotalProfile1() { 
    let totalProfile01 = new Array(canvas1.width).fill(0); 
    let totalProfile02 = new Array(canvas1.width).fill(0); 
    for (let n0 = 0; n0 < distributions1.length; n0++ ) { 
        for (let x1 = 0; x1 < canvas1.width; x1++) {
            totalProfile01[x1] += profile1( n0, x1 ); 
        } 
    } 
    for (let x1 = 0; x1 < canvas1.width; x1++) {
        totalProfile02[x1] += profile1( distributions1.length-1, x1 ); 
    } 
    totalProfile1 = totalProfile01; 
    totalProfile2 = totalProfile02; 
} 

function calculateDistributions1() { 
    generate_rects1(); 
    calculateTotalProfile1(); 

    contxt1.clearRect(0, 0, canvas1.width, canvas1.height);
    contxt2.clearRect(0, 0, canvas2.width, canvas2.height);

    drawRectangles1(); 

    distributions1.forEach((distribution1, index1)  => {
        contxt1.beginPath();
        contxt1.strokeStyle = color1(index1);
        contxt1.moveTo(0, 0); 
        for (let x1 = 0; x1 < canvas1.width; x1++) {
            const scaledX = scaleX1(x1);
            const y1 = canvas1.height * (1.0 - y1_scale * distribution1.area1 * gaussian1(scaledX, distribution1.x0, distribution1.sigma1));
            contxt1.lineTo(x1, y1);
        }
        contxt1.stroke();
        contxt1.fillStyle = color1(index1);
        contxt1.font = '14px Arial';
        let str1 = distribution1.label1; 
        let textWidth1 = contxt2.measureText(str1).width; 
        let x1 = unscaleX1(distribution1.x0) - textWidth1/2.0; 
        contxt1.fillText(str1, x1, canvas1.height - 10);
    });


    contxt2.beginPath();
    contxt2.moveTo(0, 0); 
    contxt2.strokeStyle = '#ff0000';
    for (let x1 = 0; x1 < canvas1.width; x1++) {
        const y1 = canvas2.height * (1.0 - y2_scale*totalProfile1[x1]);
        contxt2.lineTo(x1, y1);
    }
    contxt2.stroke();

    contxt2.beginPath();
    contxt2.moveTo(0, 0); 
    contxt2.strokeStyle = color1(rects1.length);
    for (let x1 = 0; x1 < canvas1.width; x1++) {
        const y1 = canvas2.height * (1.0 - y2_scale*totalProfile2[x1]);
        contxt2.lineTo(x1, y1);
    }
    contxt2.stroke();

} 

function drawRectangles1() { 
    calculateArea1();
    rects1.forEach((rect1, index1) => {
        contxt2.strokeStyle = color1(index1);
        contxt2.lineWidth = 1; 
        contxt2.strokeRect(unscaleX1(rect1.x1) + 2, 0, unscaleX1(rect1.x2) - unscaleX1(rect1.x1) - 2, canvas2.height);
        let str1 = distributions1[index1].label1; 
        let str2 = (rects1[index1].area1*total1).toFixed(0); 
        let textWidth1 = contxt2.measureText(str1).width; 
        let textWidth2 = contxt2.measureText(str2).width; 
        let x1 = (unscaleX1(rect1.x1)+unscaleX1(rect1.x2))/2.0-textWidth1/2.0; 
        let x2 = (unscaleX1(rect1.x1)+unscaleX1(rect1.x2))/2.0-textWidth2/2.0; 
        contxt2.fillStyle = color1(index1);
        contxt2.font = '14px Arial';
        contxt2.fillText(str1, x1, 20);
        contxt2.fillText(str2, x2, 40);
    });
} 

function calculateArea0( n1, totalProfile0 ) { 
    let area0 = 0.0; 
    for (let x1 = Math.min(unscaleX1(rects1[n1].x1), unscaleX1(rects1[n1].x2)); x1 <= Math.max(unscaleX1(rects1[n1].x1), unscaleX1(rects1[n1].x2)); x1++) {
        area0 += totalProfile0[x1];
    } 
    area0 = area0*((2.0*x1_max)/canvas1.width); 
    return area0; 
} 

function calculateArea1() { 
    let rects_area1 = new Array(rects1.length).fill(0); 
    let rects_area2 = new Array(rects1.length).fill(0); 
    let sum1 = 0.0; 
    let sum2 = 0.0; 
    for (let n0 = 0; n0 < rects1.length; n0++) { 
        let val1 = calculateArea0( n0, totalProfile1 ); 
        sum1 = sum1 + val1; 
        rects_area1[n0] = val1; 

        let val2 = calculateArea0( n0, totalProfile2 ); 
        sum2 = sum2 + val2; 
        rects_area2[n0] = val2; 

    } 
    for (let n0 = 0; n0 < rects1.length; n0++) { 
        rects1[n0].area1 = rects_area1[n0]/sum1; 
        rects1[n0].area2 = rects_area2[n0]/sum1; 

    } 
    let str1 = ""; 
    for (let n0 = 0; n0 < rects1.length; n0++) { 
        str1 = str1 + distributions1[n0].label1 + ": " + (rects1[n0].area1*total1).toFixed(0) + " "; 
        str1 = str1 + rects1[n0].area1.toFixed(3) + " "; 
        str1 = str1 + (1.0 - rects1[n0].area2/rects1[n0].area1).toFixed(3) + " "; 

    } 
    result2.textContent = str1; 
};

canvas1.addEventListener('mousedown', e => {
    const x1 = e.clientX - canvas1.offsetLeft;
    dragging1 = distributions1.find(distribution1 => Math.abs(scaleX1(x1) - distribution1.x0) < distribution1.sigma1);
});

canvas1.addEventListener('mousemove', e => {
    if (dragging1) {
        const x1 = e.clientX - canvas1.offsetLeft;
        dragging1.x0 = scaleX1(x1);
        calculateDistributions1();
    }
}); 

canvas1.addEventListener('mouseup', mouseup1);

function mouseup1() { 
    dragging1 = null; 
    let distribution0 = distributions1.pop(); 
    distributions1.sort((a1, a2) => a1.x0 - a2.x0); 
    distributions1.push(distribution0); 
    calculateDistributions1();
} 

function showProfile1(e, n1) { 
    let x1 = e.offsetX; 

    let y1 = (1.0 - e.offsetY/canvas1.height);
    if (n1 == 0) { 
        y1 = y1/y1_scale; 
    } else { 
        y1 = y1/y2_scale; 
    } 
    let str1 = " x1:" + scaleX1(x1).toFixed(2) + " y1:" + y1.toFixed(2) ; 
    let str2 = "-" 
    if ( x1 >= 0 && x1 < totalProfile1.length ) { 
        str2 = "totalProfile1[x1]:" + totalProfile1[x1].toFixed(2) + " totalProfile2[x1]:" + totalProfile1[x1].toFixed(2); 
    } 
    result1.textContent = str1 + " " + str2; 
} 

canvas1.addEventListener('mousemove', e => {
    showProfile1(e, 0); 
});

canvas2.addEventListener('mousemove', e => {
    showProfile1(e, 1); 
});

</script>
</body>
</html>


タイトルとURLをコピーしました