2024-2I プログラミング1 第07回 講義資料

2024年06月04日(火)1・2時限

1 準備と講義概要

matrix
ERROR-LOGO

2 Mathライブラリ

各種数学の計算には mathライブラリ を利用します。

プログラムのなかで「mathライブラリ」を使用するためには、次のようにインポートが必要です。

%reset -f
import math  # 要インポート
print(f'円周率は {math.pi:.5f} です')

特に、次の定数や関数は 利用頻度が高い ため覚えておいてください。

このほかにも、mathモジュールには様々な関数が存在します。それらの一覧や詳細は リファレンス で確認してください。

組み合せ数の総数の求め方

\(n\) 個の異なるものから \(r\) 個を選ぶ「組み合わせ」の総数 \(_{n}\mathrm{C}_{r}\) は、1年次の「基礎数学B」で学習済みですが、次のように計算できます。

\[ {}_{n}\mathrm{C}_{r} = \frac{n!}{r!(n-r)!} \]

なお、上記の式はTeX (テキストセルで有効) では $$ {}_{n}\mathrm{C}_{r} = \frac{n!}{r!(n-r)!} $$ のように表記します。

2.1 演習1 ( 目標時間: 12分)

確率 \(p\) の事象が、\(n\) 回の試行で \(k\) 回発生する確率 \(P\) について、次式 (二項分布) で計算できることが数学的に知られています。二項分布は、4年次の「確率統計」の授業で学びますが、高専2年でも十分に理解できる内容なので興味がある人はYouTube動画などで学んでください 。

\[ P={}_{n} \mathrm{C}_{k} \cdot p^{k}\cdot(1-p)^{n-k}\]

例えば「あたり」の割合が \(15\%\) のクジを \(5\) 回引いたときに、\(1\) 回だけ「あたり」がでる確率 \(P_{1}\) は、\(n=5\)\(k=1\)\(p=0.15\)として上式に代入して、次のように計算できます。

\[ P_{1}={}_{5} \mathrm{C}_{1} \cdot 0.15^{1}\cdot(1-0.15)^{5-1} \fallingdotseq 0.392\]

同様に、\(2\) 回だけ「あたり」がでる確率 \(P_{2}\) は次のように計算できます。

\[ P_{2}={}_{5} \mathrm{C}_{2} \cdot 0.15^{2}\cdot(1-0.15)^{5-2} \fallingdotseq 0.138\]

また、「\(1\) 回 あるいは \(2\) 回」の「あたり」がでる確率は \(P_{1}+P_{2}\) のように計算できます。

以上を踏まえて、以下の問題についてPythonプログラムを使って解いてください。


あるスマホゲームのガチャにおいて「メタスラの剣」というアイテムの提供割合(排出率)は \(0.5\%\) となっている。\(1\) 回のガチャには \(300\) ジェムが必要である。また \(10,000\) 円で \(11,720\) ジェムが購入できる。

注意

計算式に対して直接的に「数値 (数値リテラル)」を組み込まないでください。計算式は「変数」を使って構成し、数値は「変数」に対して与えるようにしてください

%reset -f
import math
p1 = math.comb(100,0) * 0.005**0 * (1-0.005)**(100-0)
print(f'問1 {p1:.2%}')  # .2% → 小数第2位までの百分率の書式指定子
%reset -f
import math
p = 0.005 # 排出率
n = 100   # 挑戦回数
k = 0     # 入手数
p1 = math.comb(n,k) * p**k * (1-p)**(n-k)
print(f'問1 {p1:.2%}')

なお、f文字列の「書式指定子」に % が存在することは既に学習済みです。例えば print(f'p={0.1519:.1%}')p=15.2% を出力します。

余裕がある学生は、前回講義で学習したGoogleColabのフォーム機能を利用して、UI (User Interface) / UX (User Experience) を強化したバージョンを作成してみてください。

3 組込み関数 (数学系)

組み込み関数とは、import の宣言をせずに利用できる関数で「標準関数」や「ビルトイン関数」とも呼ばれます。既に何度も利用している printinput は組み込み関数になります。

ここでは、利用頻度が高い 数学系 の組み込み関数について記載します。

このほかにも、様々な組み込み関数が存在します。それらの詳細は リファレンス で確認してください。

3.1 とても重要なこと

「教わったこと」を教わった範囲で無条件に受け入れて終わるのではなく 好奇心や興味関心を持って、発展的に「試す」「調べる」「考える」に取り組めるように なってください。 例えば、次のような「疑問」や「興味」が湧いて、それに対して「試す」「調べる」「考える」ができるようになってください。

このような習慣が身に付いた人が、結果的に (4年生になる頃には) 高いプログラミングスキルを習得しています。

4 繰り返し構文におけるループ変数の参照

次のような C/C++言語プログラム をコンパイルして実行すると…

// これはC/C++言語のプログラムです。
#include<stdio.h>
main(){
  for(int i=0; i<5; i++){
    printf("i=%d\n",i);
  }
}

次のような出力を得ることができます。

i=0
i=1
i=2
i=3
i=4

一方で、Pythonでは 次のようなプログラムで同様の出力 を得ることができます。Colab.ノートブックに貼り付けて、実際に同じ出力が得られることを確認してください。

for i in range(5):
  print(f'i={i}')

ここで、変数 i「ループ変数」や「ループカウンタ」 と言います。range(5) のとき、i はループ毎に 01234 に変化します。

ゼロオリジン (ゼロから開始、zero-based) であるため 最終値が「5」ではなく「4」になる点 に注意してください。

4.1 ループ変数の参照

ループ変数は、繰返し構文の内部で 参照して (=読み取りして) 計算式に利用 したり、関数の引数に与えたり することができます。

%reset -f
import math
for i in range(5):
  a = i + 5         # ループ変数 i を計算式に利用
  b = math.sqrt(i)  # ループ変数 i を関数の引数に利用
  print(f'i={i}, i+5={a}, sqrt{i}={b:.2f}') # i+5 を出力
print('---')    # for終了
print(f'i={i}') # for終了後も参照可能(最終値が格納されている)

実行結果は、次のようになります。

i=0, i+5=5, sqrt0=0.00
i=1, i+5=6, sqrt1=1.00
i=2, i+5=7, sqrt2=1.41
i=3, i+5=8, sqrt3=1.73
i=4, i+5=9, sqrt4=2.00
---
i=4

ループを抜けた後も、第08行目 のように 変数 i が参照 (読み取り) できること に注意してください。

4.1.1 演習2 ( 目標時間: 5分)

次のような標準出力を得たいとします。

X座標 : -4
X座標 : -3
X座標 : -2
X座標 : -1
X座標 :  0
X座標 :  1
X座標 :  2
X座標 :  3
X座標 :  4

次のプログラムの 第02行目第04行目 を書き換えることで対応してください。

まずは 以下のプログラムを、そのまま実行して動作確認してから、そのあとに書き換えに取り組んでください。

%reset -f
n = 5   # ここを書き換え
for i in range(n):
  x = i # ここを書き換え
  print(f'X座標 : {x:>2}')

演習が完了したら、次のプログラムでも同等の結果が得られることを確認してください。

%reset -f
n = 4
for i in range(-n,n+1): # ◀ ここがポイント
  print(f'X座標 : {i:>2}')

4.1.2 演習3 ( 目標時間: 9分)

非負の整数値を格納する変数 n の値が \(N\) のとき、\(-N\)\(-N+1\)\(\cdots\)\(0\)\(\cdots\)\(N-1\)\(N\) のような数列を得るようなプログラムを作成してください。

例えば、n\(3\) のときは次のような文字列を出力します。1文字のスペース区切り、正の値にはプラスの符号がついている、ゼロには符号を付けない点に注意してください。

-3 -2 -1 0 +1 +2 +3

例えば、n\(5\) のときは次のような文字列を出力します。

-5 -4 -3 -2 -1 0 +1 +2 +3 +4 +5

次のプログラムに追記する形式で作成してください。

%reset -f
#@markdown 0より大きい整数値を与えてください 
n = 3    #@param {type:"slider", min:1, max:20, step:1}
# ここから先のコードを記述

4.2 等差数列の生成

初項 \(a\)、公差 \(d\)等差数列の第 \(1\) 項から第 \(n\) 項は、次のプログラムで生成することができます。

%reset -f
a = 3  # 初項
d = 4  # 公差
n = 8  # 第n項まで出力
for i in range(n):
  print( a+d*i ,end=' ')

実行結果は以下のようになります。

3 7 11 15 19 23 27 31 

ループ変数 i はゼロオリジン (最初の値が \(0\) である) ため、数学の教科書に記載されているような式 (1オリジン、one-based) を参考にする場合は読み替えが必要です。

数学の教科書に記載されている等差数列の一般項

\[ a_{n} = a_{1} + (n-1)d \]

4.2.1 フォーム機能を使用して整形

上記プログラムを Colab.のフォーム機能 を使って整形・装飾したプログラム例を示します。Colab.ノートブックに貼り付けて実行してみてください。

%reset -f
#@markdown # **等差数列の出力** 
#@markdown **初項** $a$ を入力してください。 
a = 3  # @param {type:"number"}
#@markdown **公差** $d$ を入力してください。 
d = 4  # @param {type:"number"}
#@markdown 第何項までの数列を出力するか設定してください。 
n = 8 #@param {type:"slider", min:1, max:20, step:1}
for i in range(n):
  print( a+d*i ,end=' ')
img

4.3 等比数列の生成

初項 \(a\)、公比 \(d\)等比数列の第 \(1\) 項から第 \(n\) 項は、次のプログラムで生成することができます。

%reset -f
a = 3  # 初項
r = 2  # 公比
n = 8  # 第n項まで出力
for i in range(n):
  print( a*r**i,end=' ')

実行結果は以下のようになります。

3 6 12 24 48 96 192 384 

等差数列と同様に、数学の教科書に記載されている式を利用する場合は「0オリジン」と「1オリジン」の違いに注意してください。以下は「微分積分1」の教科書に記載されている式です。

\[ a_{n} = a \cdot r^{(n-1)} \]

4.3.1 演習4 ( 目標時間: 6分)

上記の等比数列生成プログラムColabのフォーム機能 を使って整形・装飾したプログラムに変更してください (先に例として示した「等差数列」のフォーム対応プログラムをベースに作成してください)。また、その動作確認してください。

4.4 累積値の計算

for文は 累積値 を計算する場合にも利用されます。

例えば、次式のように定義される \(S\) は、

\[ S = 1^2 + 2^2 +3^2 +4^2 + 5^2 \]

以下のようなプログラムで計算ができます。

%reset -f
S = 0  # 初期値0
for i in range(5):
  S = S + i**2
print(f'S={S}')

上記の 第04行目S = S + i**2S += i**2 のように省略表記できます。

なお S =+ i**2 では意図した動作をしないので注意してください。+= のような演算子を「累算代入演算子」といいます。

4.4.1 演習5 ( 目標時間: 8分)

あるスマホゲームのガチャにおいて「URアイテム」の提供割合(排出率)は \(0.25\%\) となっています。

\(300\) 回のガチャをしたとき、「URアイテム」の入手個数が \(0\) 個の確率(\(\%\))、入手個数が \(1\) 個以上の確率(\(\%\))、\(2\) 個以上の確率(\(\%\))、\(3\) 個以上の確率(\(\%\))、\(4\) 個以上の確率(\(\%\)) を求めてください。

5 繰返し構文のネスト

条件分岐構文 (if-elif-else) と同様に、繰返し構文 (for) についてもネスト (入れ子) が構成できます。このようなネストした繰返し構文を「多重ループ (2重ループ)」や「多重繰り返し文 (2重繰返し文)」のようにいいます。

例えば「掛け算の九九の表」は、次のように2重ループで出力できます。

%reset -f
print('-'*27)
for x in range(9):
  for y in range(9):
    p = (x+1) * (y+1)
    print(f'{p:>2d}',end=' ')
  print('') # 1段ごとに改行
print('-'*27)

実行すると次のような出力が得られます。

---------------------------
 1  2  3  4  5  6  7  8  9 
 2  4  6  8 10 12 14 16 18 
 3  6  9 12 15 18 21 24 27 
 4  8 12 16 20 24 28 32 36 
 5 10 15 20 25 30 35 40 45 
 6 12 18 24 30 36 42 48 54 
 7 14 21 28 35 42 49 56 63 
 8 16 24 32 40 48 56 64 72 
 9 18 27 36 45 54 63 72 81 
---------------------------

5.1 応用例1

第05回講義 で利用した タートルグラフィックス (Turtle Graphics) を再び使用します。独立したコードセルを作成して、以下を実行してください。これは、接続している仮想マシン (Linux) に「ColabTurtle」というPythonモジュール (ライブラリ) をインストールするコマンドになります。

!pip install ColabTurtle

Colab.の場合、インストールしたライブラリは、ウェブブラウザと仮想マシンと接続が切れると、仮想マシンと共に破棄されます (再度、Colab.ノートブックを開いたときは、再度、上記コマンドを実行する必要があります) 。

pip install とは (ChatGPT先生に聞いてみた)

pip install はPythonのプログラミングツールのコマンドです。これを使うと、特定の機能を持つコードの集まり(ライブラリと呼ばれる)をダウンロードしてPythonプログラムのなかで利用できるようにすることができます。例えば pip install pandas とすると、データ分析に使われる pandas というライブラリをインストールできます。つまり pip install は、Pythonで新しい機能を手軽に追加するための道具として使用されます。

多重ループを利用して、次のようなドットマトリクスを描画してみます。

matrix

次のプログラムをColab.ノートブックに貼り付けて、実行してください。

%reset -f
import ColabTurtle.Turtle as t

# 初期設定
w, h = 400,200
t.initializeTurtle(initial_speed=13,initial_window_size=(w,h))
t.shape('circle');t.hideturtle();t.bgcolor(0,0,0)
t.width(12) # 軌跡幅(ペンの太さ)

# 2重繰り返し(2重ループ)
for x in range(19):
  for y in range(9):
    # 色設定 RGB
    t.color(255-x*10, 255-y*25, 100)
    # 点描画(現在位置から現在位置に移動軌跡を描く)
    t.up();   t.goto(x*20+20,y*20+20)
    t.down(); t.goto(x*20+20,y*20+20)

ColabTurtleの詳細は、こちら を参照してください。

5.1.1 演習6 ( 目標時間: 8分)

上記の ドットマトリックスの描画.py を書き換えて、次のようなドットマトリクスを描画してください。明るい赤は t.color(255,0,0) で、暗い赤は t.color(100,0,0) で色設定ができます。

matrix

5.2 応用例2

多重ループを使うことで、次ようなサイバーなロゴを描くこともできます。

ERROR-LOGO

上記を描くためのプログラムは以下のようになります。

同心多角形の描画の観点では第34行目がポイントとなります (1年の「基礎数学」で学んだ三角関数の実用例です。例えば、第34行目t.goto(cx-r/2,cy+r/2) に書き換えると (実際に試してみて!) 美しい同心多角形にならないことが分かると思います) 。

%reset -f
import math
import ColabTurtle.Turtle as t

n=6  # N角形
r=50 # 一片の長さ
cx,cy = 250,200 # 中心座標

# 初期設定
t.initializeTurtle(initial_speed=13,initial_window_size=(cx*2,cy*2))
t.shape('circle');t.hideturtle();t.bgcolor(0,0,0)

# N角形の外角を計算しておく
deg = 180-360/n
rad = math.radians(deg)

# 色の初期値
red = 255
pen = 10

# 文字
t.color(red,0,0)
t.up()
t.goto(cx, cy+6)
t.write('ERROR',align='center',font=(20,'Arial','bold'))

# 2重ループ
for p in range(10):
  t.up()
  t.width(pen)
  t.color(red,0,0)
  t.goto(cx, cy)
  t.setheading(0) # 向きを0°(3時方向)に設定
  t.goto(cx-r/2,cy+math.tan(rad/2)*r/2)
  t.down();

  # N角形を描画
  for i in range(n):
    t.forward(r)
    t.left(180-deg);

  # 辺の長さと色を更新
  r+=18
  red-=25

5.2.1 演習7

美しく魅力的な画像を生成せよ。

%reset -f
import ColabTurtle.Turtle as t

# 初期設定
w, h = 500,500
t.initializeTurtle(initial_speed=13,initial_window_size=(w,h))
t.shape('circle');t.hideturtle();t.bgcolor(0,0,0)
t.width(1)
# 2重繰り返し(2重ループ)
for i in range(40):
  t.up();   
  t.goto(20,10*i+20)
  t.down(); 
  t.goto(410-10*i,20)

6 数学に基づく計算 v.s. PGによるゴリ押し計算

微分積分1」の授業では、等比数列の第 \(1\) 項から第 \(n\) 項までの和 \(S_{n}\) が、次式で計算できることを学びました (数学的な証明についても示されたはずです) 。

\[ S_{n} = \frac{a(1-r^{n})}{1-r}= \frac{a(r^{n}-1)}{r-1}\]

一方で、プログラムで繰返し構文を使ったシンプルなゴリ押し計算でも \(S\) を求めることができます。

両者について比較・考察してみたいと思います。例として、初項 \(a=16\)、公比 \(r=-1/2\) の初項 (第 \(1\) 項) から第 \(50\) 項までの和 \(S_{50}\) を求めてみます。

6.1 計算の正確度を比較

Colab. のノートブックを使って実際に、2つのプログラムを実行して、その結果を確認してみてください。

%reset -f
a = 16
r = -1/3
n = 50
S = a*(r**n-1)/(r-1)
print(f'S = {S:.20f}')
%reset -f
a = 16
r = -1/3
n = 50
S = 0
for i in range(n):
  S += a*r**i
print(f'S = {S:.20f}')

非常に小さな値ですが計算結果が違っていることが分かると思います。証明は省略しますが、数学公式を使って計算した結果のほうが正しい (正確な) 答えになります。

プログラムでゴリ押し計算した値にズレが生じるのは「情報2」の授業で習ったように 丸め誤差桁落ち誤差 の影響によるものです (情報2の講義ノートの p.76-77 を参照)。公式を使った場合でも、それら誤差は避けられませんが、繰返し構文よって累積しないため影響が小さく抑えられています。

6.2 計算時間を比較

GoogelColab.では %%timeit というマジックコマンドを使ってコードセル単位の実行時間を計測することができます。

%%timeit では、何回もコードセルを実行し、その実行時間の平均標準偏差 (=「情報1」で学習したように「ばらつき」の指標 ) を出力します。そのため、%%timeit を使用する場合には print などの出力はコメントしておきます。また、%reset -f もコメントアウトしておきます。

次のコードを Colab. に貼り付けて実行してみてください。

%%timeit
# %reset -f
a = 16
r = -1/3
n = 50
S = a*(r**n-1)/(r-1)
# print(f'S = {S:.20f}')
%%timeit
# %reset -f
a = 16
r = -1/3
n = 50
S = 0
for i in range(n):
  S += a*r**i
# print(f'S = {S:.20f}')

実行結果の一例を示します。これらの実行結果は、各自に割り当てられた仮想マシンのスペックに依存するので注意してください。

「method-1b.py」の実行結果

208 ns ± 9.64 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

「method-2b.py」の実行結果

9.63 µs ± 3.1 µs per loop (mean ± std. dev. of 7 runs, 100000 loops each)

これは \(1,000,000\) 回のループを \(7\) 回実行したとき、その \(7\) 回の実行についての 平均実行時間\(208(\mathrm{ns})\) で、その標準偏差\(9.64(\mathrm{ns})\) であった、という意味になります。