シンギュラリティ実験ノート

購入した1000ドルPCで何がどこまでできるのか実験した記録です。

「新プロジェクトX H3ロケット宇宙への激闘」を見たかった理由

私事であるが手術のため2週間ほど入院していた。無事退院することができ、まだまだ本調子ではないのだが、滞っていたブログ更新をぼちぼち再開していきたい。

 

退院後にまずしたかったことは、録画しておいた「新プロジェクトX H3ロケット宇宙への激闘~革命エンジンに挑んだ技術者たち~」(NHK5月3日放送)を見ることだった。

 

その理由わけ)は、技術者の男達がプロジェクト成功の瞬間、抱き合って喜ぶ姿を確認したかったからだ。こちらが衛星を軌道に投入できた後の管制室最上段でのJAXA岡田プロジェクトリーダーがJAXA理事と抱き合い、互いに背中を叩きながら喜ぶシーンである。

説明のため放送の録画から引用する。

 

抱き合って喜ぶJAXA岡田プロジェクトリーダ(左)ーとJAXA理事(右)

 

私はこの映像を2024年2月のH3ロケット打ち上げ成功の日、ニュースで見た記憶があった。その時に「男同士が抱き合って喜ぶプロジェクトがここにあるんだ」と思った。

 

スポーツの世界ではよく見られる光景かもしれない。最近ではWBC2023が記憶に新しい。大谷投手と中村捕手が優勝を決めた瞬間に抱き合って喜ぶ姿を見た人は多いだろう。

https://www.wbc2023.jp/news/article/20230322_1.html

上記記事から説明のため引用する。

抱き合って喜ぶ大谷投手と中村捕手

 

私も、人生の中で一度だけ男同士で抱き合って喜んだ経験がある。それはやはりスポーツの中でのことで、大学時代にやったアメリカンフットボールでの経験だ。地方の一部リーグだったが、優勝決定戦の最終クオーターに逆転のタッチダウンを決め、ほぼ勝利と優勝を決定づけることができた時、チームメイトと抱き合って喜んだ。

 

翻ってビジネスにおけるプロジェクトでプロジェクト完成、あるいは成功の後、プロジェクトメンバーが抱き合って喜ぶことがあるだろうか。

私の経験と感覚からすると、握手したりハイタッチくらいすることはあるが、プロジェクトが成功したからといって男同士が抱き合って喜ぶことは、流石に小っ恥ずかしくてできなかった。むしろ「これくらい当たり前ですよ」という涼しい風をしているのがかっこいいという感覚のように思う。

 

スポーツにおける試合とビジネスにおけるプロジェクトは、似ているようでいて違いがいくつかある。その違いは私が大学時代にスポーツを経験した後、社会人となりシステムエンジニアとしてシステム開発のプロジェクトに参画したり、自分がプロジェクトリーダーとなる経験をするようになってから、感じることになる。

 

スポーツにおける試合とビジネスにおけるプロジェクトの違い

  • 開始と終了
    ・試合/ゲーム:試合時間やゲーム回数によって開始と終了が明確。
    ・プロジェクト:プロジェクト(設計/開発/テスト)の期間と運用開始は明確だが、運用はシステム寿命を迎えるまで粛々と続くため終了が不明確。
  • 勝ち負け/成功と失敗
    ・試合/ゲーム:勝敗は試合終了時に得点により明確になる。
    ・プロジェクト:成功して当たり前で基本的に失敗は許されない。プロジェクトを途中で断念することになればお客様から訴えられる可能もある。

 

上記の違いから、プロジェクトが無事に終了し運用が開始されたとして、ほっと胸を撫で下ろすことはあるとしても、プロジェクトメンバーが抱き合って喜ぶことはないのが普通なのだ。

 

私も年金を受取る年齢となり、社会人としての過去を振り返った時、もちろん記憶に残るプロジェクト、思い出深いプロジェクトはいくつかある。しかしあの大学時代の優勝経験のような輝いた瞬間、感動した瞬間は経験できなかったな、という思いが若干ある。

それは上記のようなスポーツの世界とビジネスの世界の違いあるから仕方がないことなんだろうと、自分を納得させていた。そんな時に偶然見たニュースが、JAXAH3ロケット打ち上げ成功と、岡田プロジェクトリーダーがプロジェクトメンバーと抱き合って喜ぶ姿であった。

 

JAXAH3ロケットのプロジェクトが如何に過酷なプロジェクトであるかは、NKHの「新プロジェクトX」の放送を見ればよく分かる。これだけのプロジェクトになれば、相当に重い責任を感じるであろうし、逃げ出したくなるようなプレッシャーと闘うことになる。

  • 予算規模が桁違いにでかい(R7年時点で総開発費2393億円)
  • プロジェクト期間が長い(H3ロケットの場合は開始から約10年)
  • 国家プロジェクトである(国の威信と将来がかかっている)

 

さらにロケット開発プロジェクトには、設計/製造/試験の後に「ロケット打ち上げ」という大きなイベントがある。「ロケット打ち上げ」には「スポーツにおける試合」との共通点がいくつかある。

 

スポーツにおける試合とプロジェクトにおけるロケット打ち上げの共通点

  • 開始と終了
    ・試合/ゲーム:試合時間やゲーム回数によって開始と終了が明確。
    ・ロケット打ち上げ:打ち上げから衛星の軌道投入まで開始と終了が明確。
  • 勝ち負け/成功と失敗
    ・試合/ゲーム:勝敗は試合終了時に得点により明確になる。
    ・ロケット打ち上げ:成功と失敗が明確。衛星を軌道に投入できれば成功、できなければ失敗。成功するか失敗するかはやってみなければ分からない。

 

これだけの条件がJAXAのプロジェクトには揃っている。そう考えると男同士が抱き合って喜ぶことも当然のように思えてくる。極限までのプレッシャーから開放された瞬間に人間としての本能がそうさせるのだろう。繰り返しとなるがこの感動シーンを確認し、ブログを見てくださる方とも感動を共有したいと思ったのが「新プロジェクトX H3ロケット宇宙への激闘」を見たかった理由である。

JAXAの岡田プロジェクトマネージャーとプロジェクトに関わった全ての方達には、心からお疲れ様でしたと申し上げたい。

 

折角なので、岡田プロジェクトマネージャーがプロジェクトメンバーとハグする感動シーン、JAXAのエンジン設計を主導された黒須氏と、ターボポンプ開発にあたられたIHIの木村氏がハグするシーンも引用して紹介しておきたい。

JAXA岡田プロジェクトマネージャー(左から二人目)とプロジェクトメンバーのハグシーン

JAXA新エンジン責任者黒須氏とIHIエンジニア木村氏がハグするシーン

 

AIは簿記3級の試験問題を解けるか

暇だったのでAIに簿記3級の試験問題を解かせてみた。

Grok3に描いてもらいました

 

仕分けの問題

簿記3級の試験問題は第1問が仕分け、第2問が各種台帳への記帳、第3問が決算処理というのが一般的パターンだったと思う。

まずは小手調べに仕分けをやらせてみた。家にあった日商簿記3級の問題集から1問だけ試してみた。

 

 

下記の各取引について仕訳しなさい。ただし、勘定科目は、設問ごとに最も適当と思われるものを選び、ア~カの記号で解答すること。

新田株式会社に¥600,000を貸し付け、同額の約束手形を受け取り、利息¥6,000を差し引いた残額を普通預金口座から新田株式会社の普通預金口座に振り込んだ。
ア.当座預金     イ.定期預金     ウ.普通預金
エ.手形貸付金     オ.貸付金     カ.受取利息

 

ローカルLLMのqwen3:30b-a3b-q8_0で試した。

(qwen3:30b-a3b-q8_0の回答)

仕訳:
借方: 手形貸付金(エ)¥600,000
貸方: 普通預金(ウ)¥594,000
貸方: 受取利息(カ)¥6,000

解説:

  • 手形貸付金(エ): 約束手形を受け取ったため、貸付金の一種として計上。
  • 普通預金(ウ): 600,000円の貸し付けから利息6,000円を差し引いた残額594,000円を相手方の普通預金口座に振り込むため、当社の銀行口座を減額。
  • 受取利息(カ): 貸し付けに伴う利息収入として計上。

回答: エ、ウ、カ

 

正解である。今どきのローカルLLMなら仕分けの問題程度なら難なくできるようだ。

 

決算処理の問題

問題はいちいち手入力しないといけない。全部手入力するほど暇ではないので、いきなりだが決算処理の問題を試すことにした。

次の(1)決算整理前残高試算表と(2)決済整理事項等にもとづいて、答案用紙の(  )内に文字または数値を記入し、貸借対照表損益計算書を完成しなさい。

なお、会計期間はX7年4月1日からX8年3月31日までの1年間である。

 

(1)決算整理前残高試算表

借方 勘定科目 貸方
98,000 現金  
1,000 現金過不足  
520,000 普通預金  
360,000 売掛金  
120,000 仮払金  
10,000 仮払法人税  
180,000 繰越商品  
2,000,000 備品  
1,000,000 土地  
  買掛金 210,000
  借入金 200,000
  貸倒引当金 200
  備品減価償却累計額 500,000
  資本金 2,000,000
(中略) (中略) (中略)
90,000 支払家賃  
1,200 保険料  
8,710,200   8,710,200

 

(2)決算整理報告等

  1. 現金過不足¥1,000のうち¥800は通信費の記入漏れであった。残高は不明のため適切に処理した。
  2. 得意先から商品の内金¥20,000を現金で受け取っていたが、これを売上として処理していたので適切に修正する。
  3. (中略)
  4. (中略)
  5. (中略)
  6. 備品について、残高差額価値をゼロ、耐用年数を8年とする定額法により減価償却を行う。当期新たに取得した備品についても同様の条件で減価償却費を月割により計算する。
  7. (中略)
  8. 受取手数料のうち¥360,000(月額¥30,000)は、X7年5月1日に、向こう1年間の手数料を受け取ったものである。
  9. (中略)
  10. 法人税等の金額が¥30,000と計算されたので仮払法人税等との差額を未払法人税等として計上する。

 

答案用紙

貸借対照表

x8年3月31日

          (単位:円)
現金   (  ) 買掛金   (  )
普通預金   (  ) 借入金   (  )
売掛金 (  )   (  )   (  )
貸倒引当金 (△ ) (  ) 未払法人税   (  )
商品   (  ) 未払費用   (  )
(  )費用   (  ) 前受収益   (  )
備品 (  )   資本金   (  )
減価償却累計額 (△ ) (  ) 繰越利益剰余金   (  )
土地   (  )      
合計   (  ) 合計   (  )

 

 

損益計算書
x7年4月1日からx8年3月31日まで

          (単位:円)
売上原価   (  ) 売上高   (  )
給料   (  ) 受取手数料   (  )
貸倒引当金繰入   (  )      
減価償却   (  )      
通信費   (  )      
支払家賃   (  )      
保険料   (  )      
雑(  )   (  )      
支払利息   (  )      
法人税   (  )      
当期純(  )   (  )      
合計   (  ) 合計   (  )

 

著作権の絡みがあるので説明に必要と思われる部分のみ引用させていただいた。

決算整理前残高試算表の表と回答用紙の貸借対照表損益計算書は実際にはタブ区切り形式のデータにしてプロンプトから渡した。

しかし貸借対照表の借方に、本来は貸方の「貸倒引当金」や「減価償却累計額」が記載された特殊な形式となっている。人間が見ればその上にある売掛金と貸倒引当金のマイナス値を足せばいいことがわかる。(貸倒引当金に△の記号があり、その下に罫線が引いてある)

この微妙なニュアンスをAIに正確に伝えるのは難しい。仕方ないので以下のようなシンプルなフォーマットにして「借方側の△の表記は貸方の勘定科目を借方としていることを示す」という注釈をつけることにした。

      (単位:円)
現金 (  ) 買掛金 (  )
普通預金 (  ) 借入金 (  )
売掛金   (  ) (  )
貸倒引当金 (△ ) 未払法人税 (  )
商品 (  ) 未払費用 (  )
(  )費用 (  ) 前受収益 (  )
備品   資本金 (  )
減価償却累計額 (△ ) 繰越利益剰余金 (  )
土地 (  )    
合計 (  ) 合計 (  )

 

 

AIの回答

このレベルになるとローカルの30BクラスのLLMでは難しいようだ。そこで以下の大型LLMで試してみた。

  1. ChatGPT 4o-mini
  2. Claude-3.7 Sonnet
  3. Gemini-2.5 Flash
  4. DeepSeek R1
  5. Qwen3-235B
  6. Grok3

結論としては、完璧に正解できたLLMはなかった。しかしすべてのLLMが1〜2箇所のミスのみで成績としては配点35点で30点以上の回答を返してくれた。

  1. ChatGPT 4o-mini
    30点  減価償却費の仕分けミス。これにより当期純利益も不正解。
  2. Claude-3.7 Sonnet
    33点🎉  仕分けミスなし。唯一「前受金」と前受収益」を正しく処理。何故か当期純利益計算ミスで不正解。
  3. Gemini-2.5 Flash
    30点  「前受金」を「前受収益」と合算して処理
  4. DeepSeek R1
    30点  現金過不足の仕分けミスで現金額不正解。「前受金」も処理漏れ。
  5. Qwen3-235B
    30点  「前受金」を「前受収益」と合算して処理
  6. Grok3
    30点  「前受金」を「前受収益」と合算して処理

 

最も正解に近かったClaude-2.5 Sonnetの結果のみ紹介したい。

当期純利益の計算ミス(正解は70,300円)と、貸借対照表の貸方にも当期純利益を入れているミスはあるが、それ以外は完璧である。

 

Claude-2.5 Sonnet の回答

日商簿記3級の合格ラインは100点満点中70点以上である。AIへの問題(データ)の渡し方に工夫が必要になるが、AIが理解し易いように問題(データ)を渡せば、おそらく今の大型LLMクラスなら簿記3級試験に合格すると思われる。

 

所感

AIが簿記3級レベルの能力を持つようになったら、これからの会計システムはどうなるのだろう?

会計システムには詳しくないのだが、おそらく今は人が仕分けをしてシステムに入力しているのだと思う。この仕分け作業もAIエージェントが行って人間はチェックするだけになるんだろうか?

Qwen3-235BやDeepSeek R1ならローカルPCでも動くので、そこでAIエージェントも動かせるなら、経理の人手不足で困っている会社は助かるんじゃないかな〜。無責任にそんなことを妄想した。

 

AIによる剛体シミュレーションプログラムの作成(結果発表)

様々なAIで「回転する七角形の中で跳ねる20個のボール」をシミュレーションするPythonプログラムを作らせてみた。主な結果をここにまとめておきたい。

Blenderというソフトではこの種のシミュレーションを「剛体シミュレーション」と呼ぶらしい。Blenderでは更に3次元でそれができるのだが、今回AIで試したことは2次元での剛体シミュレーションプログラムの作成ということになる。


 

評価したLLMと使用サイト

Gemma3-27Bのみローカルで動く小型LLMで、それ以外はクラウドで動く大型LLMである。私は大型LLMの課金ユーザではないが無料で使えるサイトを選んで試した。

なおクラウドLLMは無料とはいえ使うためにはメールアドレス等によるアカウント登録が必要だ。

  1. ChatGPT 4o-mini
  2. Claude-3.7 Sonnet
  3. Gemini2.5 Flash
  4. DeepSeek R1
  5. Qwen3-235B 私はHuggingChatのサイトを使用
  6. Grok PCでXから使用
  7. Llama4-Marverick Groqのサイトを使用
  8. Gemma3-27B LM-Studioで使用

 

結果発表

今回は別サイト(レンタルサーバーの「ロリポップ」)にページを作成し、各プログラムが動く様子を動画で表示してみた。

下記GIF画像をクリックするとロリポップの結果発表ページに飛ぶ。

 

 

寸評

ChatGPT 4o-mini、Claude 3.7 Sonnet、Gemini2.5 Flash、DeepSeek R1はほぼ同レベルで、一発で動くプログラムを返してくれた。

ボールの数字の表示を見るとGemini2.5 Flashのみボールの回転の様子が表現できていないことが唯一の問題点か。

Qwen3-235Bもよくできているが、先日のブログに書いた通り、初期状態では重力の指定に問題がありボールが落下しなかったこと、重力を大きくしすぎてボール速度が速くなるとボールが外に飛び出してしまう点は減点対象か。

GrokとLlama4は大型LLMとしては残念な結果である。

唯一小型LLMとしてのGemma3-27Bの結果を載せたが、総じてローカルで動くサイズ(30~70B)のLLMではこのレベルのシミュレーションプログラムの作成は難しい。

Qwen3-32Bでも試してみたが動かなかった。

GLM-4-32Bはモデルカードでこのシミュレーションができるようなことを書いてあったので期待して試したが成功しなかった。tkinterを使わずpygameで作らせると一応動くプログラムはできるようだ。

2025-05-04 追記

Redditの投稿によるとQwen3-14Bでもpygameを使わせれば動くプログラムを作れるらしい。

Qwen 3 14B seems incredibly solid at coding.

ーー追記ここまでーー

 

はてなブログでの動画の表示

話はそれるが、ブログでの動画表示方法についてメモしておきたい。

HTML5からvideoタグが使用できるようになり、MP4ファイルを置く場所を確保できれば以下のようにしてブログにも動画を表示できることが分かった。

<p><video controls="controls" width="300" muted="muted"><source src="http://singula-2045.angry.jp/video/heptagon_chatgpt.mp4" type="video/mp4" />お使いのブラウザはビデオタグをサポートしていません。</video></p>

 

 

ただし、MP4ファイルでも表示できる場合とできない場合があった。当初某サイトでwebmファイルをトリミングしてMP4ファイルを作成したところ、PCでは表示できたがiPhoneでは表示できなかった。

結果的にはWindows11のClipchampというアプリでwebmからMP4に変換したところ問題なくiPhoneでも表示できるようになった。おそらくコーデックの細かな違いかと思われるが詳細は不明。

なおwebmファイルのままでもPCでは問題ないがiPhoneでは途中でエラーとなった。

iPhoneでの表示を考慮する場合、videoタグにplaysinline属性を指定したほうがいいのだが、はてな無料ユーザは厳密にこれが指定できない。ただ自分のiPhoneで試したところ、拡大表示されてしまうが表示自体に問題はなさそうである。

 

多角形の中で弾むボールのシミューレーションプログラム(その後)

AIが作成してくれたシミュレーションプログラムでボールを眺めていると、商店街の抽選会で使われるガラガラと回す”アレ”を思い出した。”アレ”の名前は何だろうと重い調べてみたら正式名称は「新井式回転抽選器」という。「ガラガラ」または「ガラポン」は俗称らしい。

この抽選器を高速で回すとボールはどうなるのか知りたくて動画を検索した。

 

ガラガラの動画(3DCG動画)

するとこんな動画を見つけた。

 

www.youtube.com

 

最初は本物の抽選器と思っていたのだが、よく見るとこれも3次元CGによる物理シミュレーションの動画であるとわかった。Blenderというソフトを使っているらしい。実によくできている。

AIにシミュレーションプログラムを作ってもらって喜んでいたのだが、こんなすごい3次元CGモデリング&シミュレーションプログラムがオープンソースソフトウェアで公開されていたとは驚いた。

Blender 2.81 リファレンスマニュアル

 

この動画でボールの動きを見ると、抽選器の中に障害物(ボールを取り出す仕組み)があり、そこにボールがひっかかり、壁に押しつけられる形で時計回りに持ち上げられ、時計の9時から12時の位置あたりから自由落下している様子が見られる。

AIが作成してくれたシミュレーションプログラムも5秒/回転で実行すると同様な動きとなる。

さらに回転数を上げて2秒/回転にするとボールは壁に張り付いた状態となる。

 

私は当初はこれを「遠心力」がシミュレーションされているのだと単純に理解していた。しかしAIにプログラムのモジュール仕様書を書かせてボールの動きを運動方程式で説明してもらったところ、「遠心力」のロジックは実装されていないと言われた。

ではなぜボールは壁に張り付くのだろう? 気になっていたので動画で確認してみようと思ったのだ。

 

回転速度を上げるとボールが壁に張り付く理由(仮説)

動画の中に高速で回転させる様子は無かったが、ガラガラの中に障害物がある場合のボールの動きがヒントになった。

AI作成シミュレーションプログラムの七角形の中に障害物はないが、約125度の角度の壁がある。

七角形の回転速度を上げるとボールが壁に張り付く動きは以下のように説明できるのではないか、と思った。

  1. 動きが遅くなったボールは125度の壁と他のボールに挟まれ、重力により下側の壁に押し付けられる。
  2. そのまま時計回りで9時の位置あたりまで上がる。
  3. 9時の位置で壁の向きが重力方向と同じになり、重力で押さえつける力が働かなくなりボールは自由落下する。
  4. 回転速度が早い場合はボールが飛び出す向きが壁とほぼ水平方向になる。
  5. 壁が更に回転することで飛び出す方向が壁の外側になるため、ボールは自由落下せずに壁に押し付けられた状態となる。

図で書いたほうがわかり易いと思うがとりあえず文章で書くとこんな感じになる

この仮説を検証するため、多角形の形を変化させて実験してみることにした。

 

仮説検証用プログラム

先日のソースに以下の定数を追加した。

  • NUM_ANGLE:多角形の角数(3以上を指定すること)
  • ROTATION_TIME:一回転にかかる秒数

他に定義されている定数は以下の通り。

  • WIDTH:800(画面横幅)
  • HEIGHT:800(画面縦幅)
  • RADIUS_POLYGON:350(多角形の半径)
  • BALL_RADIUS:20(ボールの半径)
  • NUM_BALLS:20(ボールの数 最大20まで)
  • GRAVITY:700.0(重力、単位はビクセル/秒^2)
  • FRICTION:0.01(摩擦係数)
  • ANGULAR_FRICTION:0.01(ボールの回転摩擦係数)
  • COR:0.8(反発係数)

注)重力の値を大きくしすぎたり、回転速度を上げすぎるとボールの移動速度に計算が間に合わずにボールが壁を通り越すことがある。これを少しでも防ぐため現在100FPS(dt=0.01)としているが、マシン性能によっては値の調整が必要かもしれない。

仮説検証用Pythonプログラム
import tkinter as tk
import math
import random
import numpy as np
from dataclasses import dataclass

# Constants
WIDTH = 800
HEIGHT = 800
CENTER_X = WIDTH // 2
CENTER_Y = HEIGHT // 2
RADIUS_POLYGON = 350
BALL_RADIUS = 20
NUM_BALLS = 20
GRAVITY = 700.0
FRICTION = 0.01
ANGULAR_FRICTION = 0.01
COR = 0.7  # Coefficient of restitution
NUM_ANGLE = 7
ROTATION_TIME = 5

# Ball colors from the problem
COLORS = [
    "#f8b862", "#f6ad49", "#f39800", "#f08300", "#ec6d51", "#ee7948", "#ed6d3d",
    "#ec6800", "#ec6800", "#ee7800", "#eb6238", "#ea5506", "#ea5506", "#eb6101",
    "#e49e61", "#e45e32", "#e17b34", "#dd7a56", "#db8449", "#d66a35"
]

@dataclass
class Ball:
    x: float
    y: float
    vx: float = 0.0
    vy: float = 0.0
    radius: float = BALL_RADIUS
    color: str = "#000000"
    number: int = 1
    angular_velocity: float = 0.0  # radians per second
    angle: float = 0.0  # radians

    def update(self, dt):
        # Apply gravity
        self.vy += GRAVITY * dt

        # Apply linear friction
        friction = (1 - FRICTION * dt)
        self.vx *= friction
        self.vy *= friction

        # Update position
        self.x += self.vx * dt
        self.y += self.vy * dt

        # Apply angular friction
        self.angular_velocity *= (1 - ANGULAR_FRICTION * dt)

        # Update angle
        self.angle += self.angular_velocity * dt


class Heptagon:
    def __init__(self, center_x, center_y, radius):
        self.center_x = center_x
        self.center_y = center_y
        self.radius = radius
        self.angle = 0  # rotation angle in radians
        self.angular_velocity = math.radians(360 / ROTATION_TIME)  # degrees per second

    def update(self, dt):
        # Update rotation angle
        self.angle += self.angular_velocity * dt
        if self.angle > 2 * math.pi:
            self.angle -= 2 * math.pi

    def get_vertices(self):
        # Returns list of vertices
        vertices = []
        for i in range(NUM_ANGLE):
            theta = self.angle + 2 * math.pi * i / NUM_ANGLE
            x = self.center_x + self.radius * math.cos(theta)
            y = self.center_y + self.radius * math.sin(theta)
            vertices.append((x, y))
        return vertices

    def get_walls(self):
        # Returns list of walls as ((x1,y1), (x2,y2))
        vertices = self.get_vertices()
        walls = []
        for i in range(NUM_ANGLE):
            a = vertices[i]
            b = vertices[(i + 1) % NUM_ANGLE]
            walls.append((a, b))
        return walls


def collide_ball_wall(ball, wall, heptagon, dt):
    a = np.array(wall[0], dtype=float)
    b = np.array(wall[1], dtype=float)
    p = np.array([ball.x, ball.y], dtype=float)
    ab = b - a

    if np.allclose(ab, 0):
        closest = a.copy()
    else:
        t = np.dot(p - a, ab) / np.dot(ab, ab)
        t = max(0.0, min(1.0, t))
        closest = a + ab * t

    dx = p[0] - closest[0]
    dy = p[1] - closest[1]
    distance = math.hypot(dx, dy)

    if distance <= ball.radius + 1e-6:
        if distance == 0:
            normal = np.array([0.0, 1.0])
        else:
            normal = np.array([dx, dy], dtype=float) / distance

        rel_x = closest[0] - heptagon.center_x
        rel_y = closest[1] - heptagon.center_y
        v_wall_x = -rel_y * heptagon.angular_velocity
        v_wall_y = rel_x * heptagon.angular_velocity
        v_wall = np.array([v_wall_x, v_wall_y], dtype=float)

        v_ball = np.array([ball.vx, ball.vy], dtype=float)
        v_rel = v_ball - v_wall

        j = (1 + COR) * np.dot(v_rel, normal)
        v_ball_new = v_ball - j * normal

        ball.vx, ball.vy = v_ball_new[0], v_ball_new[1]

        overlap = ball.radius - distance
        if overlap > 0:
            ball.x += normal[0] * overlap
            ball.y += normal[1] * overlap

        return True
    return False


def collide_balls(ball1, ball2, dt):
    dx = ball2.x - ball1.x
    dy = ball2.y - ball1.y
    distance = math.hypot(dx, dy)

    if distance < 2 * BALL_RADIUS and distance > 0.0:
        normal = np.array([dx, dy], dtype=float) / distance
        v_rel = np.array([ball2.vx - ball1.vx, ball2.vy - ball1.vy], dtype=float)
        j = (1 + COR) * np.dot(v_rel, normal)

        ball1.vx += j * normal[0] * 0.5
        ball1.vy += j * normal[1] * 0.5
        ball2.vx -= j * normal[0] * 0.5
        ball2.vy -= j * normal[1] * 0.5

        overlap = 2 * BALL_RADIUS - distance
        if overlap > 0:
            move_dir = normal
            ball1.x -= move_dir[0] * overlap / 2
            ball1.y -= move_dir[1] * overlap / 2
            ball2.x += move_dir[0] * overlap / 2
            ball2.y += move_dir[1] * overlap / 2

        return True
    return False


def update_frame(heptagon, balls, canvas, root):
    dt = 0.01  # ~100 FPS

    # Update heptagon rotation
    heptagon.update(dt)

    # Update all balls
    for ball in balls:
        ball.update(dt)

    # Check collisions with walls
    for ball in balls:
        walls = heptagon.get_walls()
        for wall in walls:
            collide_ball_wall(ball, wall, heptagon, dt)

    # Check ball-ball collisions
    for i in range(len(balls)):
        for j in range(i + 1, len(balls)):
            collide_balls(balls[i], balls[j], dt)

    # Redraw
    draw(canvas, heptagon, balls)

    # Schedule next frame
    root.after(16, update_frame, heptagon, balls, canvas, root)


def draw(canvas, heptagon, balls):
    canvas.delete("all")

    # Draw heptagon
    walls = heptagon.get_walls()
    for wall in walls:
        x1, y1 = wall[0]
        x2, y2 = wall[1]
        canvas.create_line(x1, y1, x2, y2, fill="black", width=2)

    # Draw balls
    for ball in balls:
        x = ball.x
        y = ball.y
        r = ball.radius

        # Draw ball
        canvas.create_oval(x - r, y - r, x + r, y + r, fill=ball.color, outline="black")

        # Draw number with spin indication
        number_angle = ball.angle
        text_offset = r / 2
        text_x = x + text_offset * math.cos(number_angle)
        text_y = y + text_offset * math.sin(number_angle)
        canvas.create_text(text_x, text_y, text=str(ball.number), fill="black")


def main():
    root = tk.Tk()
    root.title("Bouncing Balls in Spinning Heptagon")
    canvas = tk.Canvas(root, width=WIDTH, height=HEIGHT, bg="white")
    canvas.pack()

    # Create heptagon
    heptagon = Heptagon(CENTER_X, CENTER_Y, RADIUS_POLYGON)

    # Initialize balls
    balls = []
    for i in range(NUM_BALLS):
        angle = random.uniform(0, 2 * math.pi)
        speed = 1.0
        vx = math.cos(angle) * speed
        vy = math.sin(angle) * speed
        ball = Ball(
            x=CENTER_X,
            y=CENTER_Y,
            vx=vx,
            vy=vy,
            radius=BALL_RADIUS,
            color=COLORS[i],
            number=i + 1,
            angular_velocity=random.uniform(-0.1, 0.1)
        )
        balls.append(ball)

    # Start update loop
    root.after(100, update_frame, heptagon, balls, canvas, root)
    root.mainloop()


if __name__ == "__main__":
    main()

 

おそらく三角形や四角形にすると壁が障害物になり、角にボールが滞留しやすくなる。回転速度を上げると壁にボールが張り付いた状態になる。

逆に角の数を増やすと限りなく円に近づき、ボールは跳ね回ることもなく多角形の下側に滞留し、回転速度を上げても壁にはりつかなくなる。

 

検証結果

ROTATION_TIME(一回転にかかる秒数)を3秒として実行した。

四角形の場合 三角形の場合

20角形の場合

 

四角形、三角形の場合は最初の落下でボールは飛び跳ねるが、しばらくすると角にボールが滞留して壁に張り付いた状態になった。

20角形の場合は最初の落下による動きが落ち着いた後は、下の方に滞留したままの状態になる。

 

まとめ

回転速度を上げるとボールが壁に張り付いた状態に近づく理由が理解できた(気がする)。

Qwen3-235B-A22Bが作成してくれたプログラムは多角形の角数を変化させても破綻することなくシミュレーションしている。よくできているな〜と感心した。

 

余談になるが、Blenderという素晴らしいオープンソースのソフトがあることも初めて知った。

ガラポンをBlenderで作るまでのメーキング動画もアップされていた。動画では短時間で作っているように見えるが、機能が豊富なため操作方法を理解して覚えるだけでも大変そうだ。興味はあるが、安易に手が出せそうにない。

 

Qwen3-235Bのコーディング能力を検証してみた

Qwen3がリリースされたので試してみた。

私が試したのは「回転する七角形の中で跳ねる20個のボール」のシミュレーションだ。このテーマはボールの重力による落下、壁との衝突、ボール同士の衝突や摩擦による回転などを考慮する必要があり、かなり難易度が高い。

これまで様々なLLMで試してきたが、まともに動いたのはDeepSeek-R1、4o-mini、Claude-3.7-Sonnet、Gemini2.5Flashくらいである。つまりこのテーマはトップレベルのモデルでないと難しいのだ。

Qwen3のリリース文によると「Qwen3-235B-A22B は、コーディング、数学、一般能力などのベンチマーク評価において、DeepSeek-R1、o1、o3-mini、Grok-3、Gemini-2.5-Proといったトップモデルと比較しても競争力のある結果を達成」となっている。

Qwen3-32Bモデルもかなり優秀になったようだが32Bクラスではおそらく難しい。Qwen3-235Bモデルで試すことにした。このクラスをローカルで動かそうとすると、512GBメモリのMac Studioくらいのマシンがないと動かせないと思うが、ちょっと試すだけならHuggingfaceのアカウントで、HuggingChatのデモ環境で動かすことができる。

Qwen3のサイトでも試すことができるようだが私はHuggingfaceのサイトを使用した。

 

Qwen3-235B-A22B

試したプロンプトは以下の通り。

Write a Python program that shows 20 balls bouncing inside a spinning heptagon:
- All balls have the same radius.
- All balls have a number on it from 1 to 20.
- All balls drop from the heptagon center when starting.
- Colors are: #f8b862, #f6ad49, #f39800, #f08300, #ec6d51, #ee7948, #ed6d3d, #ec6800, #ec6800, #ee7800, #eb6238, #ea5506, #ea5506, #eb6101, #e49e61, #e45e32, #e17b34, #dd7a56, #db8449, #d66a35
- The balls should be affected by gravity and friction, and they must bounce off the rotating walls realistically. There should also be collisions between balls.
- The material of all the balls determines that their impact bounce height will not exceed the radius of the heptagon, but higher than ball radius.
- All balls rotate with friction, the numbers on the ball can be used to indicate the spin of the ball.
- The heptagon is spinning around its center, and the speed of spinning is 360 degrees per 5 seconds.
- The heptagon size should be large enough to contain all the balls.
- Do not use the pygame library; implement collision detection algorithms and collision response etc. by yourself. The following Python libraries are allowed: tkinter, math, numpy, dataclasses, typing, sys.
- All codes should be put in a single Python file.

 

(日本語訳)

回転する七角形の中で20個のボールが跳ね回る様子を示すPythonプログラムを作成してください。

- すべてのボールの半径は同じです。
- すべてのボールに1から20までの数字が書かれています。
- すべてのボールは、開始時に七角形の中心から落下します。
- 色は次のとおりです。#f8b862、#f6ad49、#f39800、#f08300、#ec6d51、#ee7948、#ed6d3d、#ec6800、#ec6800、#ee7800、#eb6238、#ea5506、#ea5506、#eb6101、#e49e61、#e45e32、#e17b34、#dd7a56、#db8449、#d66a35
- ボールは重力と摩擦の影響を受け、回転する壁からリアルに跳ね返る必要があります。ボール同士の衝突も必要です。
- すべてのボールの材質によって、衝突時の跳ね返りの高さは七角形の半径を超えず、ボールの半径よりも高くなります。
- すべてのボールは摩擦によって回転します。ボールに表示されている数字は、ボールの回転数を示すために使用できます。
- 七角形は中心の周りを回転し、回転速度は5秒あたり360度です。
- 七角形のサイズは、すべてのボールが収まる大きさである必要があります。
- pygameライブラリは使用しないでください。衝突検出アルゴリズムや衝突時の応答などは、ご自身で実装してください。使用できるPythonライブラリは、tkinter、math、numpy、dataclasses、typing、sysです。
- すべてのコードは、1つのPythonファイルにまとめてください。

 

返ってきたPythonのコードを実行した結果がこちら。

七角形は回転するのだが、ボールが落ちてこない。ちょっとがっかりしたが、動きをよく見ていると少しずつ動いているように見えた。ソースを確認したところ「GRAVITY = 0.5」となっている。試しにこの数値(重力)を増やしてみたところボールが落ち始めた。結果的には「GRAVITY = 700.0」くらいが丁度いいようだ。

 

 

GRAVITYをこれ以上大きくすると、いくつかのボールは壁を通り越して落ちてしまう。壁との距離の判定ロジックがまだ甘いようだ。

DeepSeek-R1のコーディング能力に比べると詰めが甘い点は多々あるが、ボールが衝突する様子はリアルである。七角形の回転速度5秒/1回転から2.5秒/1回転にすると、半分くらいのボールは壁に張り付くようになる。さらに回転速度を上げるとほとんどのボールが壁に張り付いた状態になる。これは私の物理的感覚からすると正しい動きのように思う。なぜそうなるのかを力学的に説明しろと言われると困るのだが。。回転速度をいくら上げてもボールが七角形の下の方でウネウネと動くだけではリアルでない気がするのだ。

DeepSeek-R1は671Bのパラメタサイズだが、Qwen3は235Bのパラメタサイズでこのレベルを実現できている点を評価すべきだと思う。

 

動画ではリアルさが伝わらないので、Pythonのソースをここに置くことにした。興味のある方は重力や回転速度、ボールの数や大きさ等を変更して試してもらいたい。なお、上記の通り完璧なソースではないのでその点はご容赦いただきたい。

Pythonソースプログラム
import tkinter as tk
import math
import random
import numpy as np
from dataclasses import dataclass

# Constants
WIDTH = 800
HEIGHT = 800
CENTER_X = WIDTH // 2
CENTER_Y = HEIGHT // 2
RADIUS_HEPTAGON = 350
BALL_RADIUS = 20
GRAVITY = 700.0
FRICTION = 0.01
ANGULAR_FRICTION = 0.01
COR = 0.8  # Coefficient of restitution

# Ball colors from the problem
COLORS = [
    "#f8b862", "#f6ad49", "#f39800", "#f08300", "#ec6d51", "#ee7948", "#ed6d3d",
    "#ec6800", "#ec6800", "#ee7800", "#eb6238", "#ea5506", "#ea5506", "#eb6101",
    "#e49e61", "#e45e32", "#e17b34", "#dd7a56", "#db8449", "#d66a35"
]

@dataclass
class Ball:
    x: float
    y: float
    vx: float = 0.0
    vy: float = 0.0
    radius: float = BALL_RADIUS
    color: str = "#000000"
    number: int = 1
    angular_velocity: float = 0.0  # radians per second
    angle: float = 0.0  # radians

    def update(self, dt):
        # Apply gravity
        self.vy += GRAVITY * dt

        # Apply linear friction
        friction = (1 - FRICTION * dt)
        self.vx *= friction
        self.vy *= friction

        # Update position
        self.x += self.vx * dt
        self.y += self.vy * dt

        # Apply angular friction
        self.angular_velocity *= (1 - ANGULAR_FRICTION * dt)

        # Update angle
        self.angle += self.angular_velocity * dt


class Heptagon:
    def __init__(self, center_x, center_y, radius):
        self.center_x = center_x
        self.center_y = center_y
        self.radius = radius
        self.angle = 0  # rotation angle in radians
        self.angular_velocity = math.radians(360 / 5)  # 72 degrees per second

    def update(self, dt):
        # Update rotation angle
        self.angle += self.angular_velocity * dt
        if self.angle > 2 * math.pi:
            self.angle -= 2 * math.pi

    def get_vertices(self):
        # Returns list of vertices
        vertices = []
        for i in range(7):
            theta = self.angle + 2 * math.pi * i / 7
            x = self.center_x + self.radius * math.cos(theta)
            y = self.center_y + self.radius * math.sin(theta)
            vertices.append((x, y))
        return vertices

    def get_walls(self):
        # Returns list of walls as ((x1,y1), (x2,y2))
        vertices = self.get_vertices()
        walls = []
        for i in range(7):
            a = vertices[i]
            b = vertices[(i + 1) % 7]
            walls.append((a, b))
        return walls


def collide_ball_wall(ball, wall, heptagon, dt):
    a = np.array(wall[0], dtype=float)
    b = np.array(wall[1], dtype=float)
    p = np.array([ball.x, ball.y], dtype=float)
    ab = b - a

    if np.allclose(ab, 0):
        closest = a.copy()
    else:
        t = np.dot(p - a, ab) / np.dot(ab, ab)
        t = max(0.0, min(1.0, t))
        closest = a + ab * t

    dx = p[0] - closest[0]
    dy = p[1] - closest[1]
    distance = math.hypot(dx, dy)

    if distance <= ball.radius + 1e-6:
        if distance == 0:
            normal = np.array([0.0, 1.0])
        else:
            normal = np.array([dx, dy], dtype=float) / distance

        rel_x = closest[0] - heptagon.center_x
        rel_y = closest[1] - heptagon.center_y
        v_wall_x = -rel_y * heptagon.angular_velocity
        v_wall_y = rel_x * heptagon.angular_velocity
        v_wall = np.array([v_wall_x, v_wall_y], dtype=float)

        v_ball = np.array([ball.vx, ball.vy], dtype=float)
        v_rel = v_ball - v_wall

        j = (1 + COR) * np.dot(v_rel, normal)
        v_ball_new = v_ball - j * normal

        ball.vx, ball.vy = v_ball_new[0], v_ball_new[1]

        overlap = ball.radius - distance
        if overlap > 0:
            ball.x += normal[0] * overlap
            ball.y += normal[1] * overlap

        return True
    return False


def collide_balls(ball1, ball2, dt):
    dx = ball2.x - ball1.x
    dy = ball2.y - ball1.y
    distance = math.hypot(dx, dy)

    if distance < 2 * BALL_RADIUS and distance > 0.0:
        normal = np.array([dx, dy], dtype=float) / distance
        v_rel = np.array([ball2.vx - ball1.vx, ball2.vy - ball1.vy], dtype=float)
        j = (1 + COR) * np.dot(v_rel, normal)

        ball1.vx += j * normal[0] * 0.5
        ball1.vy += j * normal[1] * 0.5
        ball2.vx -= j * normal[0] * 0.5
        ball2.vy -= j * normal[1] * 0.5

        overlap = 2 * BALL_RADIUS - distance
        if overlap > 0:
            move_dir = normal
            ball1.x -= move_dir[0] * overlap / 2
            ball1.y -= move_dir[1] * overlap / 2
            ball2.x += move_dir[0] * overlap / 2
            ball2.y += move_dir[1] * overlap / 2

        return True
    return False


def update_frame(heptagon, balls, canvas, root):
    dt = 0.016  # ~60 FPS

    # Update heptagon rotation
    heptagon.update(dt)

    # Update all balls
    for ball in balls:
        ball.update(dt)

    # Check collisions with walls
    for ball in balls:
        walls = heptagon.get_walls()
        for wall in walls:
            collide_ball_wall(ball, wall, heptagon, dt)

    # Check ball-ball collisions
    for i in range(len(balls)):
        for j in range(i + 1, len(balls)):
            collide_balls(balls[i], balls[j], dt)

    # Redraw
    draw(canvas, heptagon, balls)

    # Schedule next frame
    root.after(16, update_frame, heptagon, balls, canvas, root)


def draw(canvas, heptagon, balls):
    canvas.delete("all")

    # Draw heptagon
    walls = heptagon.get_walls()
    for wall in walls:
        x1, y1 = wall[0]
        x2, y2 = wall[1]
        canvas.create_line(x1, y1, x2, y2, fill="black", width=2)

    # Draw balls
    for ball in balls:
        x = ball.x
        y = ball.y
        r = ball.radius

        # Draw ball
        canvas.create_oval(x - r, y - r, x + r, y + r, fill=ball.color, outline="black")

        # Draw number with spin indication
        number_angle = ball.angle
        text_offset = r / 2
        text_x = x + text_offset * math.cos(number_angle)
        text_y = y + text_offset * math.sin(number_angle)
        canvas.create_text(text_x, text_y, text=str(ball.number), fill="black")


def main():
    root = tk.Tk()
    root.title("Bouncing Balls in Spinning Heptagon")
    canvas = tk.Canvas(root, width=WIDTH, height=HEIGHT, bg="white")
    canvas.pack()

    # Create heptagon
    heptagon = Heptagon(CENTER_X, CENTER_Y, RADIUS_HEPTAGON)

    # Initialize balls
    balls = []
    for i in range(20):
        angle = random.uniform(0, 2 * math.pi)
        speed = 1.0
        vx = math.cos(angle) * speed
        vy = math.sin(angle) * speed
        ball = Ball(
            x=CENTER_X,
            y=CENTER_Y,
            vx=vx,
            vy=vy,
            radius=BALL_RADIUS,
            color=COLORS[i],
            number=i + 1,
            angular_velocity=random.uniform(-0.1, 0.1)
        )
        balls.append(ball)

    # Start update loop
    root.after(100, update_frame, heptagon, balls, canvas, root)
    root.mainloop()


if __name__ == "__main__":
    main()

Ubuntu環境でTkをインストールする必要があり、その方法は私の先日の記事を参照いただきたい。

 

まとめ

Qwen3の235Bモデルのコーディング能力は他のトップモデル(DeepSeek-R1、O4など)と同レベルの実力があることを確認した。

 

unslothのブログを見るとDeepSeek-V3の量子化版でも2.71-bit以上の量子化であれば「回転する七角形の中で跳ねる20個のボール」のシミュレーションが正しくでき、2.71-bit未満では性能が落ちて困難になるようだ。

 

DeepSeek-V3を動かすには「VRAMとRAMを合わせて少なくとも160GB以上のメモリを搭載したマシンが必要。Appleの統合メモリを使用している場合を除き、GPUなしで実行することは避けてください」と書かれている。

このレベルのLLMがローカルで動く環境があればな〜と思う今日この頃である。

 

多角形の中で弾むボールのシミューレーションプログラム

redditを眺めていたら面白い投稿を見つけた。「回転する七角形の中で跳ねる20個のボール」をシミュレーションするプログラムを様々なLLMに作らせた結果が報告されていた。

Added GPT-4.1, Gemini-2.5-Pro, DeepSeek-V3-0324 etc...

 

同じ内容がXにもポストされていた。

 

親切なことにプロンプトも掲載されている。面白そうなので私も試してみた。

 

gemma3:12b (ローカルLLM)

私が今愛用するローカルLLMのgemma3:12bでまず試してみた。実行したプロンプトは上記に掲載されていた英語のプロンプトをそのまま使った。説明のため以下に日本語訳を引用する。

回転する七角形の中で20個のボールが跳ね回る様子を示すPythonプログラムを作成してください。

- すべてのボールの半径は同じです。
- すべてのボールに1から20までの数字が書かれています。
- すべてのボールは、開始時に七角形の中心から落下します。
- 色は次のとおりです。#f8b862、#f6ad49、#f39800、#f08300、#ec6d51、#ee7948、#ed6d3d、#ec6800、#ec6800、#ee7800、#eb6238、#ea5506、#ea5506、#eb6101、#e49e61、#e45e32、#e17b34、#dd7a56、#db8449、#d66a35
- ボールは重力と摩擦の影響を受け、回転する壁からリアルに跳ね返る必要があります。ボール同士の衝突も必要です。
- すべてのボールの材質によって、衝突時の跳ね返りの高さは七角形の半径を超えず、ボールの半径よりも高くなります。
- すべてのボールは摩擦によって回転します。ボールに表示されている数字は、ボールの回転数を示すために使用できます。
- 七角形は中心の周りを回転し、回転速度は5秒あたり360度です。
- 七角形のサイズは、すべてのボールが収まる大きさである必要があります。
- pygameライブラリは使用しないでください。衝突検出アルゴリズムや衝突時の応答などは、ご自身で実装してください。使用できるPythonライブラリは、tkinter、math、numpy、dataclasses、typing、sysです。
- すべてのコードは、1つのPythonファイルにまとめてください。

 

それらしいプログラムを返してくれたが、実行すると全く駄目だった。七角形が画面に正しく表示されず、ボールも一瞬表示されるだけで直ぐに画面の外に消えてしまった。

 

 

DeepSeek-R1

動くプログラムがほしいので、今私が使える環境で最も賢いと思われるDeepSeek-R1に頼んでみた。じっくり考えた後、動くプログラムを一発で返してくれた。

プログラムではシミュレーションの条件を変数で指定できるようになっている。

以下の初期条件で実行した。

  • ボールの数: 20個
  • 重力: 700Gal
  • 七角形の回転速度: 1回転5秒
  • ボールの半径: 20
  • 七角形の半径: 350

壁やボールの反発係数も定義されていて変更可能となっている。

 

 

かなりリアルな動きになっていると思う。重力、回転速度、ボールの大きさを変更しても破綻せずにリアルな動きをしてくれる。

Pythonのプログラムの場合、動画でしかお見せできないのが残念だ。Webアプリにして是非実際の動きを見てもらいたいので、DeepSeekにこのPythonのプログラムをそのままJavaScriptに移植してくれと頼んでみた。プログラムはすぐにできたが、残念ながらJavaScript版ではうまく動かなかった。ボールが七角形の外側に出てしまったり、壁から跳ね返る速度も不自然であった。

 

 

何度もDeepSeekに修正すべき点を伝えて直させようとしたのだが直らなかった。世の中「vibe coding」とか言っているが、そんなにうまくいかない。

ソースを眺めて関係しそうなところに手を加えてみたのだが、ソースの処理を理解できていない人がいくら時間をかけても直せそうにない。別のAIに頼むことにした。

 

ChatGPT4o-mini

Pythonのプログラムをアップロードして以下のプロンプトを投げた。

PythonのプログラムをJavaScriptに移植してください。変数名や関数名もできる限り忠実にPythonのプログラムの内容に従ってください。

Freeプランで実行したので、どのAIが対応してくれたのかよく分からない。多分「4o-mini」か。

こちらは一発で動くJavaScriptとHTMLを返してくれた。

 

✅ これを .html として保存してブラウザで開けば即動作するよ!
もしよければ、このまま音付きバージョンや重力変更UI付きとかも作れるので、気になるなら声かけて!✨

 

しかも気さくな言葉で機能追加の提案までしてくれた。お言葉に甘えて重力と回転速度をスライダで変更できるようにしてもらったアプリがこちら。(クリックするとアプリが起動します)

 

このWebアプリをWebAppLabに早速登録した。他のアプリもあるので興味のある方はこちらから是非使ってみてほしい。

Web App Lab - Webアプリコレクション

 

まとめ

この内容のプログラムを自力で直ぐに作れる人は有名理工学部の大学院レベルだろう。何度も書いているが、今のAIはそのレベルにある(と思う)。

自分の頭ではいくら時間をかけても作れそうにないプログラムも、AIが作ってくれるのは確かにありがたい。

しかしこの賢いAIを使いこなすことは難しい。AIにいくら頼んでもできないことがある。そうすると更に賢いAIに頼むしかなくなる。しかしそのできあがったプログラムが本当に正しいのか。もはや自分にはわからないという怖さがある。

今回できあがったプログラムも、回転する壁から跳ね返る角度に若干違和感がなくもない。しかしこれを自分で解決できる自信もない。

「とりあえずこれでいいか」と思ってしまう自分がちょっとこわい😅。

 

参考情報

Webアプリ版ではできないが、Pythonならボールの大きさや数も自由に変更して実行できるので、これはこれでお勧めである。DeepSeek-R1に頼むとすぐに作ってくれる。DeepSeekは無料でも試すことができるので興味のある方はやってみてほしい。

ただし実行するにはtkinterが必要なためUbuntu環境でTkをインストールする必要がある。

pip install tkinter で簡単にできるかと思ったらできなかった。以下の2つのコマンドを実行する必要があったのでメモしておく。

pip install pytk
sudo apt-get install python3-tk

 

日本の地方公務員数を集計してグラフにしてみた

日本の都道府県別の地方公務員数はこちらに政府統計として集計され公開されている。データは都道府県、政令指定都市、市区町村、一部事務組合の4つに別れているので、これらをまとめることで日本の地方公務員の数を都道府県別に把握することができる。

データがexcelで公開されているのでexcelが得意な人はexcelのピポットテーブル機能で簡単に集計できるのかもしれない。私は会社を退職してからexcelを使わなくなったのでピポットテーブルの使い方も忘れてしまった。今回は頑張ってSqliteのDBを使って集計してみた。

人口データの最新データが2023年のため、集計した公務員のデータも2023年のデータである。

 

集計結果

地方公務員には以下の種別がある。

  • 一般行政(一般管理、福祉関係)
  • 教育
  • 警察
  • 消防
  • 公営企業(県立病院、市立病院等の職員)

上記の公務員種別ごとに、所属する組織として都道府県、市区町村、事務組合の場合があり、公務員種別、所属組織別、都道府県別のマトリックスのデータになる。

これら種別を合計した地方公務員数を都道府県ごとに人口1,000人あたりに換算してヒートマップで表したグラフがこちらになる。

 

考察

グラフを見ると、島根県高知県の赤色が目立つ。そうすると、島根県高知県は公務員の人数が多すぎるのかと思ってしまうのだが、ちょっと調べるとそうではなさそうだ。

下記のニュースは島根県のニュース(2024年7月)だが、「島根県内の小中学校ではことし4月の時点で29人の教員が足りていない」と書かれている。

教員不足解消へ 教員免許保持者ら対象に県教委が説明会 松江|NHK 島根県のニュース

同様に鳥取県のニュース(2024年5月)では、「鳥取県内の小学校で教員不足が深刻な状況」と書かれている。

県と鳥大 小学校教員確保のワーキングループ立ち上げで合意|NHK 鳥取県のニュース

 

単位人口あたりの人数が多いから無駄が多いという単純な話ではなさそうだ。教員数は子供の人口と学校の設置数が関係するだろう。学校を運営するには最低限必要な人数という制約もあるだろうから、単純に子供の人口に比例した人数がいればいいという話でもないのだ。

 

グラフをよく見ると例えば東北地方を見ると宮城県のみが青色で比較的少ない公務員数で運営されているのがわかる。九州を見ても福岡県のみが濃い青色になっている。

ここから類推できることは、人口密度の高い都道府県ではより少ない人数で公共サービスが提供できているということだ。宮城県には仙台市政令指定都市としてある。福岡県では北九州市と福岡市の2つの政令指定都市がある。

人口密度の低い地域(グラフでは島根県高知県)では1000人あたりの公務員数が約38人であるのに対し、人口密度の高い地域(グラフでは神奈川県と埼玉県)では同公務員数が約17.5人と半分の人数で足りている。つまり公務員一人あたりの公共サービスの生産性が2倍になっていると理解できる。

 

人口密度と単位人口あたりの公務員数には負の相関があるのかもしれない。AIに頼んで散布図を書かせてみた。

 

 

どうやら負の相関性があるらしい。しかも指数関数的な、おそらくは冪乗則の関係がありそうだ。冪乗則とはX軸とY軸を対数としたときに相関が見られる関係である。両軸を対数にしてみた。

 

 

負の相関性と冪乗則の関係がありそうだ。AIに頼んで冪乗則のモデルでカーブフィットをさせてみた。

 

 

決定係数 (R^2) = 0.69とまあまあきれいにカーブフィットできた。冪乗則が成り立っていると考えてよさそうだ。

組織別に見ると必ずしも全ての組織で冪乗則が成り立っているわけではない。特に警察官の人数と人口密度にはあまり相関性がない。警察官はほぼ地域の人口に応じた配置になっているようだ。東京都のみが人数が多いがそれでも最低ラインの2倍程度だ。

 

 

消防員数と公営企業職員数も人口密度との相関性が低い。これらを除いた公務員数、つまり一般行政と教員数のみでカーブフィットしたグラフが以下である。

 

 

決定係数 (R^2) = 0.71と少しだけフィッティング精度はよくなった。

 

まとめ

地方公務員数を集計し、ヒートマップで日本地図にプロットしてみた。その結果、人口密度と単位人口あたりの公務員数には負の相関性、人口密度と公務員のサービス生産性には相関性があること、更には冪乗則の関係があることに気づいた。

法則オタクの私としてはこれだけで十分に満足なのだが、「だからなんなの?」と聞かれると答えに困る(笑)。

一つだけ言えることは、人口密度(1平方キロメートルあたりの人口)1,000人程度までは人口密度が高い方がサービス生産性は向上するが、それ以上に人口密度が高くなるとサービス生産性の向上は期待できなくなる、ということくらいか。

とすると福岡県くらいの人口密度(1004.6人)が公共サービス提供の点では最もコストパフォーマンスがいいことになる。東京都、大阪府の人口が分散して47都道府県がこのレベルになるのが公共サービスの点では理想的だろう。

しかし実際には日本の人口はこれから減少していく。人口密度の高い地域と低い地域(つまり過疎化した地域)の二極化は更に進むと思われる。なぜならサービス生産性の高い地域の方が、より品質の高い多様なサービスを受けられると期待できるからだ。

過疎化する地域の行政サービスを維持するにはどうすべきか。考えてみたが私にはテーマが重すぎる。下手なことを書くとミスリードしてしまいそうなのでこの辺で終わりとしたい。

 

使用データ

総務省地方公共団体定員管理関係の資料はこちら

私が集計したデータをCSVで以下に置かせてもらう。総務省の「令和6年地方公共団体定員管理調査結果のポイント」資料のR5年職員数の集計値(2,801,596人)と一致していることを確認している。

都道府県別公務員数(2023年)
,一般行政,教育,警察,消防,公営企業,合計,人口,密度(人口/平方km),一般行政/密度,教育/密度,警察/密度,消防/密度,公営企業/密度,合計2
北海道,46837,51682,11751,9161,20813,140244,5041000,60.4,9.3,10.3,2.3,1.8,4.1,27.8
青森県,11637,11939,2684,2592,7944,36796,1177000,122,9.9,10.1,2.3,2.2,6.7,31.3
岩手県,12759,13211,2430,2000,7664,38064,1154000,75.5,11.1,11.4,2.1,1.7,6.6,33.0
宮城県,18442,21657,4360,3087,8429,55975,2239000,307.4,8.2,9.7,1.9,1.4,3.8,25.0
秋田県,10337,9938,2356,2049,2961,27641,909000,78.1,11.4,10.9,2.6,2.3,3.3,30.4
山形県,11061,10962,2322,1550,6686,32581,1018000,109.1,10.9,10.8,2.3,1.5,6.6,32.0
福島県,18256,19143,3915,2486,4881,48681,1751000,127,10.4,10.9,2.2,1.4,2.8,27.8
茨城県,20948,26736,5406,4480,4412,61982,2744000,450,7.6,9.7,2.0,1.6,1.6,22.6
栃木県,14887,17661,3887,2502,1713,40650,1848000,288.3,8.1,9.6,2.1,1.4,0.9,22.0
群馬県,14357,18741,3906,2552,6738,46294,1831000,287.7,7.8,10.2,2.1,1.4,3.7,25.3
埼玉県,41600,53223,12857,8419,10030,126129,7113000,1872.9,5.8,7.5,1.8,1.2,1.4,17.7
千葉県,38899,45343,12259,7989,13284,117774,6081000,1179.2,6.4,7.5,2.0,1.3,2.2,19.4
東京都,96781,77703,47346,19440,22222,263492,13448000,6129.3,7.2,5.8,3.5,1.4,1.7,19.6
神奈川県,48928,61576,17418,9797,18719,156438,8970000,3712.2,5.5,6.9,1.9,1.1,2.1,17.4
新潟県,19605,21222,4748,3291,7872,56738,2107000,167.4,9.3,10.1,2.3,1.6,3.7,26.9
富山県,9215,9713,2281,1304,5640,28153,987000,232.3,9.3,9.8,2.3,1.3,5.7,28.5
石川県,9389,10475,2323,1599,5808,29594,1091000,260.6,8.6,9.6,2.1,1.5,5.3,27.1
福井県,8251,8762,2080,1222,3176,23491,729000,173.9,11.3,12.0,2.9,1.7,4.4,32.2
山梨県,8466,8472,1960,1231,2892,23021,776000,173.7,10.9,10.9,2.5,1.6,3.7,29.7
長野県,20202,20070,3942,2521,7582,54317,1965000,144.8,10.3,10.2,2.0,1.3,3.9,27.6
岐阜県,16141,19585,3951,2751,5772,48200,1868000,175.8,8.6,10.5,2.1,1.5,3.1,25.8
静岡県,23906,30108,7038,4571,12000,77623,3449000,443.4,6.9,8.7,2.0,1.3,3.5,22.5
愛知県,50581,56225,14538,8238,25311,154893,7195000,1390.8,7.0,7.8,2.0,1.1,3.5,21.5
三重県,15143,17043,3426,2553,5586,43751,1669000,289,9.1,10.2,2.1,1.5,3.3,26.2
滋賀県,11505,14696,2633,1651,5170,35655,1367000,340.2,8.4,10.8,1.9,1.2,3.8,26.1
京都府,18363,20495,7123,3244,7015,56240,2465000,534.4,7.4,8.3,2.9,1.3,2.8,22.8
大阪府,50970,67943,23119,9985,12345,164362,8488000,4454.8,6.0,8.0,2.7,1.2,1.5,19.4
兵庫県,33573,46310,12509,5975,20698,119065,5247000,624.5,6.4,8.8,2.4,1.1,3.9,22.7
奈良県,11715,12146,2794,1756,2869,31280,1279000,346.5,9.2,9.5,2.2,1.4,2.2,24.5
和歌山県,9935,9729,2527,1501,4439,28131,884000,187.1,11.2,11.0,2.9,1.7,5.0,31.8
鳥取県,7009,6566,1458,749,3061,18843,532000,151.6,13.2,12.3,2.7,1.4,5.8,35.4
島根県,8408,8973,1799,1202,4171,24553,640000,95.4,13.1,14.0,2.8,1.9,6.5,38.4
岡山県,14564,19082,4033,2480,3375,43534,1814000,254.9,8.0,10.5,2.2,1.4,1.9,24.0
広島県,20053,23517,5784,3589,6995,59938,2682000,316.3,7.5,8.8,2.2,1.3,2.6,22.3
山口県,12241,13032,3546,1994,3503,34316,1279000,209.2,9.6,10.2,2.8,1.6,2.7,26.8
徳島県,8501,8289,1836,1076,3048,22750,688000,165.9,12.4,12.0,2.7,1.6,4.4,33.1
香川県,8591,9550,2145,1190,4702,26178,910000,484.8,9.4,10.5,2.4,1.3,5.2,28.8
愛媛県,12215,12457,2872,1872,5467,34883,1277000,224.9,9.6,9.8,2.2,1.5,4.3,27.3
高知県,9576,8347,1921,1196,4112,25152,660000,92.9,14.5,12.6,2.9,1.8,6.2,38.1
福岡県,31312,41841,12135,4962,5949,96199,5011000,1004.6,6.2,8.3,2.4,1.0,1.2,19.2
佐賀県,8580,9136,2003,1077,1669,22465,786000,322,10.9,11.6,2.5,1.4,2.1,28.6
長崎県,12374,13857,3520,1670,3526,34947,1254000,303.5,9.9,11.1,2.8,1.3,2.8,27.9
熊本県,15605,17813,3518,2382,4833,44151,1685000,227.4,9.3,10.6,2.1,1.4,2.9,26.2
大分県,11071,11778,2441,1638,3139,30067,1079000,170.1,10.3,10.9,2.3,1.5,2.9,27.9
宮崎県,10559,11487,2335,1200,3750,29331,1032000,133.4,10.2,11.1,2.3,1.2,3.6,28.4
鹿児島県,16272,19193,3441,2353,4680,45939,1533000,166.8,10.6,12.5,2.2,1.5,3.1,30.0
沖縄県,12663,18636,3228,1675,4893,41095,1443000,632.2,8.8,12.9,2.2,1.2,3.4,28.5
,942283,1066063,287904,163802,341544,2801596,121195000,,,,,,,