Sympyを使ってセンター試験の問題を解く&類似問題を生成する

この記事は RECRUIT MARKETING PARTNERS Advent Calendar 2017 の投稿記事です。

こんにちは。データエンジニアの大西です。

最近SymPyというPythonのライブラリにハマっています。この記事では、それを使ってセンター試験の問題を解いたり類似問題を生成させる小ネタをご紹介します。

Sympyの紹介

SympyはPython上で数学の代数計算を実施できるライブラリです。方程式を解いたり、微分を計算したりといった数学的な操作がPythonで計算できます。

インストール

Python3系がインストールした環境であれば、$ pip install sympy でOKです。その他の環境の場合は公式のドキュメントを参照ください。

シンボル操作と代数計算

Sympyを使うとxyを数学で計算する時の変数として定義して操作できます。

In [1]: from sympy import *
In [2]: x = Symbol('x')
In [3]: y = Symbol('y')
In [4]: f = (x + y)**2
In [5]: f
Out[5]: (x + y)**2
In [6]: f.expand()  #式の展開
Out[6]: x**2 + 2*x*y + y**2

三角関数や指数関数なども定義されているので、

In [7]: g=sin(2*x)  
In [8]: g.expand(trig=True)
Out[8]: 2*sin(x)*cos(x)

のように三角関数を含んだ式の代数計算もできます。

微分・積分・極限

これらも解析的に解くことができます。

In [9]: limit(sin(x)/x, x, 0)
Out[9]: 1
In [10]: limit(cos(x)/x, x, 0)
Out[10]: oo  # 無限大∞
In [11]: x = Symbol('x')
In [12]: diff(x**2 + x + 1, x)
Out[12]: 2*x + 1
In [13]: integrate(2*x + 2, x)
Out[13]: x**2 + 2*x

正規分布の計算もバッチリです。

In [14]: integrate(exp(-x**2), (x, -oo, oo))
Out[14]: sqrt(pi)

この他にも、確率、行列、微分方程式もカバーしていますし、物理も古典力学や量子力学といった範囲を扱うことが出来ます。

一つ一つは紹介できないのですが、興味がある方は是非公式のドキュメントを参照してください。「マジで、こんなのも実装されてるの!?」とか変な笑みを浮かべながら読んでます、自分は。

問題を解いてみる

さっそくSympyを使って数学の問題を解いてみましょう。今回題材にするのは、平成29年度のセンター試験で実際に出題された以下の問題です。


独立行政法人 大学入試センターHP掲載の平成29年度センター試験問題 数学I・数学Aより引用

Sympyを使えば数式をそのままPython上に定義して、計算をすることができます。

問題で与えられているのは

In [1]: from sympy import *
In [2]: x = Symbol('x')
In [3]: f = x**2 + 4/x**2

ですね。

それでは、1つ目の求める式(x + 2/x)^2gとおきましょう。答えを求めるためには、gf で表してあげればよいです。手で解くときは展開するのが早いのですが、ここは多項式の除算ができるdivを使いましょう。

In [4]: g = (x + 2/x)**2
In [5]: div(g, f)
Out[5]: (1, 4) # g = 1 * f + 4 ということです。

実際に確かめると、

In [6]: q,r = div(g,f)
In [7]: h = q * f + r
In [8]: h
Out[8]: x**2 + 4 + 4/x**2
In [9]: g.expand() == h
Out[9]: True

となって、元の gと除算結果から作ったhが一致しますね!

あとはhfの値を代入してあげれば最初の答えが得られます。代入にはsubsを使います。引数として代入するパラメータと値を辞書形式で与える必要があります。

In [10]: h.subs({f:9})
Out[10]: 13  # ア=1 イ=3

同様にして、2つ目の式x^3 + 8/x^3 も計算していきましょう。

In [11]: p = x**3 + 8/x**3
In [12]: q = x + 2/x
In [13]: div(p,q)
Out[13]: (x**2 - 2 + 4/x**2, 0)  # ウ=2
In [14]: p = sqrt(13) * (9 -2)
In [15]: p
Out[15]: 7*sqrt(13) # エ=7 オ=1 カ=3

はい、ということでア=1、イ=3、ウ=2、エ=7、オ=1、カ=3と答えが求まりました。

どうでしょう。手元で数学の計算をしている感覚で、Pythonを使ってすっきり解くことが出来ますよね!

類似問題を生成してみる

単に解くだけだとSympyを使わなくてもそれなりに解けるので、Sympyの力をもっと見てもらうために類似問題の生成をやってみましょう。

類似問題としては、基本的な式はそのままで係数の値を変えて違う問題にしてみることを目指します。今回の場合、条件として与えらえる x^2 + 2/x^2 = 9 の係数をパラメータとしてみるのでx^2 + a^2 /x^2) = b とする形ですね。
Sympyを使うので、これらの係数を含んだまま計算ができます。

In [1]: from sympy import *
In [2]: x = Symbol('x')
In [3]: a, b = symbols('a, b')
In [4]: given_expr = x**2 + a**2 / x**2  # 与えられる式
In [5]: ans_expr_1 = (x + a /x)**2  #1つ目の答えとして求める式
In [6]: div(ans_expr_1, given_expr)
Out[6]: (1, 2*a)
In [7]: ans_1 = b + 2*a  # 1つ目の答え

実際に値を代入して確かめてみましょう。

In [8]: given_expr.subs({a:2})
Out[8]: x**2 + 4/x**2
In [9]: ans_expr_1.subs({a:2})
Out[9]: (x + 2/x)**2
In [10]: ans_1.subs({a:2, b:9})
Out[10]: 13

元の問題が生成できていますね!

同様に2つ目の式も生成しましょう。ウの値になるところを新しいパラメータc とすると、求める式は

In [11]: c = Symbol('c')
In [12]: p = (x + a/x)*(x**2 + a**2/x**2 -c)
In [13]: p.expand()
Out[13]: a**3/x**3 + a**2/x - a*c/x + a*x - c*x + x**3
In [14]: collect(p.expand(), x)
Out[14]: a**3/x**3 + x**3 + x*(a - c) + (a**2 - a*c)/x

なのですが、これがx^3 + a^3/x^3 になってほしいのです。
なので、残りの項が消えてほしいので、実はc = aであることがわかります。
したがって、2つ目の式も一つ目の式と与えられた式で表すと、

In [15]: p = sqrt(ans_expr_1) * (b -a)
In [16]: ans_expr_2.subs({a:2, b:9, ans_expr_1:13})
Out[16]: 7*sqrt(13)

となって、元の問題を生成できるようになっていることがわかります。

これで、全ての式が代数式で表現できたので、あとはパラメータa, bの値を変えてやれば類似問題が生成できることになります。

問題生成のコードをまとめると以下のようなスクリプトになります。

from sympy import *
from random import randint, choice
def generate_problem():
    x = Symbol('x')
    a,b = symbols('a,b')
    a_var_list = range(1,6)
    b_var_list = range(6,20)
    a_var = choice(a_var_list)
    b_var = choice(b_var_list)
    expr = x + a/x
    given_expr = x**2 + a**2 / x**2
    ans_expr_1 = expr**2
    ans_1 = b + 2*a
    expr_var = sqrt(ans_1).subs({a:a_var, b:b_var})
    ans_expr_2 = expr * (b - a)
    print('生成したa: ' + str(a_var))
    print('生成したb: ' + str(b_var))
    print('与えられた式: ' + str(expand(given_expr).subs({a:a_var})))
    print('与えられた式の値: ' + str(b.subs({b:b_var})))
    print('求める式1: ' + str(ans_expr_1.subs({a:a_var})))
    print('答えアイ: ' + str(ans_1.subs({a:a_var, b:b_var})))
    print('求める式2: ' + str(expand(ans_expr_2).subs({a:a_var})))
    print('答えウ: ' + str(a.subs({a:a_var})))
    print('答えエオカ: ' + str(ans_expr_2.subs({a:a_var, b:b_var, expr:expr_var})))
if __name__ == '__main__':
    generate_problem()

実行するたびにパラメータと答えが変わります。

$ python advent_calendar.py
生成したa: 3
生成したb: 13
与えられた式: x**2 + 9/x**2
与えられた式の値: 13
求める式1: (x + 3/x)**2
答えアイ: 19
求める式2: x**3 + 27/x**3
答えウ: 3
答えエオカ: 10*sqrt(19)

上記のコードをNotebook形式にしたものを別途公開しています。

注意が必要なのは、パラメータa, bの値を全くランダムに生成してはいけないということです。x^2 + a^2/x^2 = bなのでbは正の実数です。そして、aも正の実数でも一般性を失いません。
そして、大事なことですが、どちらもそこまで大きくない整数にしておいた方が手で解けるレベルに収まります。この辺りは、難易度調整も絡んでくるので、使ってくれる人(今は自分ですが:p)のことを考えた設計にしましょう。

まとめ: 数式をそのままPython上に移植できるのが魅力

いかがでしょう。問題を解くときにもSympyの計算機能が有効でしたが、パラメータを変えて類似問題を生成する場合にはシンボルを使ったまま計算を進められるSympyならでは特徴が活かされていましたね。
微分積分やベクトル、確率といった問題も同様に計算・類似問題生成が可能です。

今回はSympyのパワーをお見せするために大学入試問題を取り上げましたが、Sympyの魅力は数式をほぼそのままPython上に移植して、簡単に計算を進めることができる点にあります。
また、今回は割愛しましたがJupyter NotebookでSympyを利用すると、数式をLaTeX形式で表示できるため可読性もとても高いという利点もあります。
機械学習や統計解析を扱う場面でも有効に利用できますので、業務でもどんどん使って行きたいと考えています。