Pythonで計算の過程を宣言的に記述する

表計算の手軽さをPythonに実現する

Yasunori Horikoshi
JDSC Tech Blog
Jul 22, 2022

--

私は、普段、小売店舗での商品需要の予測など様々な数学モデルを利用したサービスを提供する部署で働いています。そういったサービスの効果を把握するために、様々な場面でシミュレーション計算が必要となります。

シミュレーションのプログラムは面倒くさい

ビジネスで用いるシミュレーションのプログラムは、

  • 多数の変量の時間発展を管理する
  • それら変量は、お互いの過去の値に複雑に依存している
  • その依存関係は、ビジネス的な仮説・仮定が変わると簡単に変更される

という性質を持つことが多くあります。こういったプログラムを正しく管理するためには

  • 各時点の変量間の依存関係を把握し、正しい順番で計算する
  • 依存関係が変更されたら、それに応じて計算順序を正しく変更する

ということが要求されます。これは実際なかなか面倒くさく、デバッグや変更時の影響把握を困難にします。

表計算は便利。それは何故か

一方で、なかなかに複雑なシミュレーションを、プログラマではないビジネスサイドのメンバーが、Excelを使って組み上げるという場面もしばしばあります。

プログラムを書こうと思ったら面倒なのにExcelだと簡単にできるのは何でか・・と考えると、Pythonなどのプログラミング言語は手続き的に計算を定義するのに対して、Excelなどの表計算ソフトは、計算を宣言的に書けるものなのだなと気づきました。ここで、手続き的・宣言的とは、だいたい以下のような意味で使っています。

手続き的:計算の手順を記述する。変数間の依存関係が変わる場合には、適切に計算順序を変更する必要がある。

宣言的:変数の間の関係を記述する。計算順序は実行環境が自動で調整する。結果、依存関係が変わろうとも、人間は計算順序について考える必要はない。

例として、ある期間における商品の在庫数をシミュレートする計算を考えてみます。

  • 初日の期初在庫が与えられる
  • 期間中の各営業日の入荷量・出荷量が与えられる
  • 各営業日において期末在庫・期初在庫を計算せよ

というような問題です。

表計算では、上の図のように、期初在庫を計算する式を先に入力しようとも、期末在庫を計算する式を先に入力しようとも、得られる結果は同じです。一方で、手続き的な計算では、期末在庫の値を計算してから翌日の期初在庫の値を代入しないと、正しく計算されません。

つまり、表計算では計算対象の依存関係に基づいて自動で計算順序が調整されるのに対し、手続き的な計算では計算順序について人間が管理する必要があります。

Pythonでも宣言的に書く

というようなことを考えた結果、変数間の依存関係を保持して、そこから自動で計算順序を導くようなパッケージを作れば、Excelでできるようなシミュレーションは楽に管理できるはずだと思いました。というわけで、作ってみました。ソースの全体は、GitHubで公開しています link 。実行方法は、レポジトリのreadmeを見てください。

記事が長くなってしまうので、詳細な実装は稿を改めることとして、Pythonで宣言的に計算を定義できると、どういう風にプログラムが書けるのかを、ここでは紹介します。

準備

このプログラムのエントリーポイントは、 /mysheet/__main__.py に定義されています。このファイルの冒頭に、以下のような定数の宣言があり、以下のコード例で参照されます。

冒頭の定数宣言

普通の計算

1つ目のエントリーポイントforward 関数は、以下のように定義されています。これは、通常の手続き的なPythonプログラムと同じような順序で処理が並んでいます。

標準的な順序の計算

23行目に登場する Sheet は、このレポジトリの中で定義されているクラスです。これは、行のインデックスとして名前、列のインデックスとして日付を持つ2次元の配列のように扱うことができます。

このプログラムを見ているだけだと、単純に要素アクセスが便利なコンテナにしか見えません。しかし、次の例は、少し不思議に見えると思います。

ちょっと変な順番の計算

2つ目のエントリーポイント forward2 は、以下のように定義されています。

ほとんど先ほどの forward と同じですが、計算の順番が変わっています。 today の期末在庫は、同じ日の期初在庫・入荷量・出荷量から計算されるのに、それらよりも先に計算式が代入されています。一見すると、このコードでは、期末在庫の計算の際に、正しい期初在庫etcの値が使えないように見えます・・が、forwardforward2 は、全く同じ計算結果を返します。

実は、Sheetクラスのオブジェクトは単に値を保持するコンテナではありません。Sheetクラスのオブジェクトは、各セル間の依存関係を保持し、計算時に依存関係を解決し、適切な順番でセルの値を計算していきます。つまり、表計算ソフトがやっているのと同じことを裏側で実行しています。

これによって、人間は、各セルの間の関係を宣言的に記述することだけを考えればよくなります。

行ごとに計算

プログラムの記述順序から開放されることで、以下のように、項目ごとに別々に計算の定義を記述することも可能です。

上のプログラムでは、

  • 74–81行目で期末在庫の計算
  • 83–87行目で期初在庫の計算
  • 89–93行目で入荷量・出荷量の計算

を定義していることが明白です。このように、特定の計算ロジックが局所化されていると、いずれかの変数の計算方法が変わった際にも、影響範囲が限定されることになり、保守性の高いプログラムになります。

終わりに

JDSCではデータサイエンスやエンジニアリングの力によって日本を変えたい仲間を募集しています!ご興味を持ってくださったらカジュアルにお話しましょう!(by tech blog 編集部)

https://jdsc.ai/recruit/

--

--

Yasunori Horikoshi
JDSC Tech Blog

A data scientist in a BtoB service company. I have been a quant of a sercurities company and a DS in WEB marketing.