Pythonプログラミング(ステップ7・リスト・基本的なデータ処理)

このステップの目標

1.リストを使ったデータの整列(ソーティング)

リストを使ったデータ処理のうち、実用的にも重要な例として、リスト中の数値の並び順を、大きいものから(あるいは小さいものから)順に並べる方法を考えよう。 統計処理をする際に、データの順位付けが必要となる場合が多いが、そのような際、あらかじめデータを整列しておく必要がある。

以下は、リストを小さい方から大きい方へ順に(昇順に)並べ変え、出力するPythonコードの例である:

例題9a(ex9a.py)
shakyou
リストの整列(ソーティング)

# coding: utf-8

X=[2.1, 5.3, 7.3, 4.7, 9.2, 1.2, 3.1, 5.3, 8.3, 4.8]

for i in range(0,len(X)-1,1):
    for j in range(len(X)-1,i,-1):
        if X[j-1]>X[j]:
            w=X[j]
            X[j]=X[j-1]
            X[j-1]=w

for i in range(0,len(X),1):
    print(X[i])

例題 9aのアルゴリズム
Input: 10件のデータが入った配列 $\{x_i\}$
Output: 昇順に整列された配列 $\{x_i\}$
1: 配列$\{x_i\}$に10件のデータをセットする
2: 位置 $i$ を、先頭から末尾から1つ前まで、1ずつ「後方に」移動しながら、6:までを繰り返す:
3:   位置 $j$ を、末尾から $i+1$ まで、1ずつ「前に」移動しながら、5:までを繰り返す:
4:    もし 隣同士が $x_{j-1} \gt x_j$ ならば、$x_{j-1}$ と $x_j$ の値を入れ替える
5:   反復ここまで
6: 反復ここまで
7: 配列の値を先頭から順に出力する
8: 終了する

プログラムの見所

ソーティングについての 補足説明はこちら

トランプのカードをバブルソートで並べ替える様子の動画(亀場を使用)

例題9の5〜10行目にかけてが、リスト(配列)データの並べ変え(整列、ソーティング)を行っている部分である。 $i$ のループの中に $j$ のループが入れ子になった2重ループを形成しており、$j$ は $i$ より大きな値の範囲を動くようになっている($0 \le i \lt j \le 9$)。 ここで、リストの要素 x[j] で $x_j$ と表すと、 例題9では、隣り合う全ての $x_{j-1}$ と $x_{j}$ のペアを参照しながら、もし $x_{j-1} \gt x_j$ ならば値を入れ替える動作を繰り返している。 こうすることで、全ての$j$ について、最終的に $x_{j-1}\le x_j$ が成り立つようにできる。 これは、バブルソート(bubble sort)と呼ばれるアルゴリズムに基づいたソーティングの例である。

ソーティングの仕組みについては、別ページにもう少し詳しく説明した。 また、参考書 例題2.25 (p.30)も併せて参照のこと。

Pythonの標準機能

Pythonの機能を使うと、リストXの整列はX.sort()(降順の場合はX.sort(reverse=True))で済んでしまう。 ここではアルゴリズムの学習を兼ねて、敢えて、リストの要素毎に処理するコードを考えることにする。

sort()メソッドを使って書き直した例題9a

# coding: utf-8

X=[2.1, 5.3, 7.3, 4.7, 9.2, 1.2, 3.1, 5.3, 8.3, 4.8]

X.sort()

for i in range(0,len(X),1):
    print(X[i])

このように、リストを操作するための「便利機能」がPythonには沢山備わっている。 こちらのページにいくつかをまとめたので参照のこと。

icon-pc 練習:データの整列(降順)

例題9a(ex9a.py)のコードに変更を加え、以下の80件のデータ(東北楽天ゴールデンイーグルスに登録されている選手の身長)の並びについて、値が大きいものから小さくなる順(降順)に整列されるよう改修してみなさい。

X=[
  174,175,180,185,178,184,179,185,170,178,184,183,180,191,180,180,174,188,181,182,
  187,178,175,177,185,178,175,175,174,169,174,173,185,178,180,180,183,191,179,178,
  174,185,185,180,193,181,178,181,177,176,174,180,186,180,180,180,185,174,194,175,
  183,176,192,185,186,193,171,172,175,186,180,172,175,175,177,181,178,180,176,174
  ]
icon-hint ヒント

昇順から降順に切り替えるには、プログラム中の1箇所を変更するのみである。 データの件数が変わると、プログラムのどの箇所が影響されるかについても、よく考えること。

icon-pc 練習:平均とメジアン

上の課題(「データの整列」)で作成したプログラムにさらにコードを追加し、以下の例に倣って、平均値と中央値(median)を出力するよう改修してみなさい。

...
172
172
169
平均身長= ...
中央値= ...
icon-hint ヒント

平均の計算は、このページの例題9b(ex9b.py)も参照のこと。

あらかじめ昇順に整列された$N$件のデータ $x_0, x_1, \cdots, x_{N-1}$ が与えられたとき、$N$が奇数のときメジアン(中央地)は $x_{N/2}$、 $N$が偶数の場合は $\frac{1}{2}\left( x_{N/2-1} + x_{N/2} \right)$ である。 ここで $/$は整数の除算(Pythonの整数除算は //)。

icon-pc 練習:最頻値

上記の野球選手の身長データについて、最頻値(モード)を求めるコードを作成し、動作を確認してみなさい。

icon-hint ヒント

いろいろなアプローチが考えられるが、データが整列されていれば、データを順に調べ、同じ値が連続する回数(の最大値)を調べれば良さそうだ。

icon-pc 練習:バブルソートに要する手間

バブルソートを行なう際に、その「手間」あるいは「工数」を見積もる指標として

を考えるのが良さそうである。もちろん、回数が少ないほど効率よく短時間で仕事が完了するはずである。 上の データの整列 で作成したプログラムを元にして、1,2の回数をプログラム内でカウントし、

比較の回数= xxx回
入れ替えの回数 xxx回

のように出力するプログラムを作成しなさい。


2.リスト(配列)を使った統計計算

以前のステップで行った統計計算プログラムを、リストを使ったバージョンに変更してみよう。 先にまず、全データをリストに格納して、その後で、平均の計算を行っている。 リストが登場する以外には、特に目新しい箇所は無いはずだ。

例題9b(ex9b.py)
shakyou

# coding: utf-8

X=[ ]
n=int(input("データの件数を入れてください:"))

for i in range(n):
    data=float(input("{0}番目のデータ:".format(i+1)))
    X.append(data)

s_x=0
for i in range(len(X)):
    s_x=s_x + X[i]

print("データの総和は",s_x,"です")
mu = s_x/n
print("平均値は",mu,"です")
例題9bの見所

icon-pc 練習:標準偏差の計算

例題9bを発展させて,標本平均だけでなく(標本)標準偏差も計算して,両方を表示するプログラムを書いてみなさい。

icon-hint ヒント

標本標準偏差を計算は以下の手順で行えばよい:

  1. データの件数を $n$ とし、データが $x_i$ $(x=0, \cdots, n-1)$ に格納されているとする
  2. データの総和を求める $$S_x = \sum_{i=0}^{n-1} x_i$$
  3. データの2乗の総和を求める $$S_{xx} = \sum_{i=0}^{n-1} (x_i)^2$$
  4. 標本平均 $\mu$ を求める $$\mu = \frac{S_x}{n}$$
  5. 標本分散 $V$ を求める $$V = \frac{S_{xx}}{n} - \mu^2$$
  6. 標本標準偏差 $$\sigma = \sqrt{V}$$

標準偏差の計算方法については、総和のパターンの練習も参照のこと。

総和と二乗の総和は、ひとつのforループで「まとめて」計算できるはず。


icon-teacher 解説 ファイルに格納されたデータの利用

データの件数が多いと、数値をキーボードから入力するのは大変な手間となる。 そこであらかじめテキストエディタ等で、

80
174
175
180
185
178
184
...

のようなデータファイルをあらかじめ作成しておいて(ファイル名をsample.txtとすると)、 それをPythonのコードで処理させれば効率的である。 そんな場合は、ファイルのデータをリスト(配列)に読み込ませればよい。 例題9bを修正し、キーボードからではなく、ファイルからデータを読み込むようにした例を以下に示す:

ファイルからデータを読み込むには、まず 変数=open("ファイル名")で「ファイルを開いた」後、 内容を読み込む動作( 変数.readlines() ) を行う。
ファイルへのアクセスが終わったら、最後に閉じる( 変数.close() )。

データファイル: sample.txt

# coding: utf-8
import math

file=open("sample.txt")    # ファイル"sample.txt"を開く
lines=file.readlines()     # ファイルからリストに行を読み込む
file.close()               # ファイルを閉じる

n=int(lines[0])            # ファイルの1行目の数値をnにセットする
s_x=0.0
for i in range(1,n+1,1):
   x=float(lines[i])       # ファイルのi行目を実数に変換してxにセットする
   s_x=s_x+x

print("データの総和は",s_x,"です")
mu=s_x/n
print("平均値は",mu,"です")
補足

上のコードで、ファイルの読み込みの箇所は

with open("sample.txt") as file:
	lines=file.readlines()     

と書くのが今流である。


icon-teacher 解説: 分散(標準偏差)の推定

調査や実験から $n$ 個のサンプル $\{x_0, x_1, \cdots, x_{n-1}\}$ が得られたとしよう。 そのとき、サンプルの平均(標本平均)は $$ \bar{x}=\frac{1}{n} \sum_{i=0}^{n-1} x_i $$ で与えられる。一方、統計学の教科書を見ると、分散(不偏分散)$\sigma^2$は、 $$ \sigma^2 = \frac{1}{n-1} \sum_{i=0}^{n-1} \left( x_i - \bar{x}\right)^2 \tag{1} $$ であると書かれている(以下では、Pythonのリストの流儀に合わせて、添字は0から始める)。

ところが、上の練習問題や以前の練習問題では、サンプルの分散(標本分散)を $$ s^2 = \frac{1}{n} \sum_{i=0}^{n-1} \left( x_i - \bar{x}\right)^2 \tag{2} $$ として計算している。(1)式と(2)式では、分母のところが1だけ違っているが、この違いはどこから来るのだろうか?

平均$\mu$、分散$V$に従うような多量のデータの貯蔵庫があって、 観測の都度、そこから$n$個ずつデータを取り出すというイメージ。

ここの箇所、統計学の教科書には $$ \begin{eqnarray} E\left[\frac{X_1 + \cdots + X_n}{n}\right] & = & \mu \\ Var\left[\frac{X_1 + \cdots + X_n}{n}\right] & = & \frac{V}{n} \end{eqnarray} $$ 等と表記されているかもしれない。

いま、対象としているデータの「真の」平均(母平均)が$\mu$、「真の」分散(母分散)を$V$としよう。 そのとき、$n$ 個のデータ $\{x_i\}$ を独立に何回も取得(サンプリング)して得られたデータの平均値(標本平均)と真の値とのずれの2乗の期待値について $$ E\left[ \left( \frac{\sum_{i=0}^{n-1} x_i}{n} - \mu \right)^2 \right] = \frac{V}{n} \tag{3} $$ が成り立つことが知られている。 ここで、$E[\cdot]$ は $\cdot$ の期待値を表す。 (3)式は、『観測によって得た平均値は真の値の周りでばらついており、そのばらつきの程度はサンプル数$n$と共に$V/n$のように変化(減少)する』と言っているわけだ。

一方、データの本来の分散は、「真の平均値$\mu$からのずれ」を使って、 $$ \frac{1}{n} \sum_{i=0}^{n-1} \left( x_i - \mu \right)^2 $$

全てのデータが等しい重みでサンプルされるとすれば、この関係はほとんど自明であろう。

で評価できる。そして、$n$の多少にかかわらず、サンプルに渡ってこれを平均すれば$V$に等しくなるはずである: $$ V = E\left[ \frac{1}{n} \sum_{i=0}^{n-1} \left( x_i - \mu \right)^2 \right] \tag{4} $$

次に、上記の量$V$と、サンプルの平均を使って求めた分散(標本分散)の期待値 $$ \tilde{V} = E\left[ \frac{1}{n} \sum_{i=0}^{n-1} \left( x_i - \frac{\sum_{i=0}^{n-1} x_i}{n} \right)^2 \right] \tag{5} $$ との差((4)式ー(5)式)を計算してみると、 $$ V - \tilde{V} = E\left[ \left( \frac{\sum_{i=0}^{n-1} x_i}{n} \right)^2 \right] - 2 \mu \,\, E\left[ \frac{\sum_{i=0}^{n-1} x_i}{n} \right] + \mu^2 $$ が得られる。ところが、上式の右辺は、式(3)(データの平均値と真の値とのずれの2乗平均)の左辺を展開した式に他ならないので、結局 $$ V - \tilde{V} = \frac{V}{n} $$ となる。知りたかったのは$V$であるから、この式を変形して、 $$ V = \frac{n}{n-1} \tilde{V} = E\left[ \frac{1}{n-1} \sum_{i=0}^{n-1} \left( x_i - \frac{\sum_{i=0}^{n-1} x_i}{n} \right)^2 \right] \tag{6} $$

つまり、「分母を$n-1$」にして計算した分散は、その期待値が$V$に等しい、ということが分かった。

p-7-unbiased-variance

以上を定性的に再解釈すると

というわけである。

icon-pc 練習:標本分散と不偏分散のシミュレーションによる比較

以下は、『平均0、分散1のガウス(正規)分布に従う乱数を5回サンプルし、その標本分散と不偏分散を求める計算』を1000回試行し、 それぞれの分散の平均(期待値)を計算するシミュレーション用コードの例である。

# coding: utf-8

import random

n_trial=1000  # 試行回数
n_sample=5    # 標本の数

var_sample=0
var_unbiased=0
for k in range(n_trial):
    sample=[ ]
    s_x = 0
    for i in range(n_sample):
        x = random.gauss(0,1) # 平均0 分散1のガウス分布
        sample.append(x)
        s_x = s_x + x

    s_x = s_x/n_sample # 標本平均
    s_xx=0
    for i in range(n_sample):
        s_xx = s_xx + (sample[i]-s_x)**2

    var_sample = var_sample + s_xx/n_sample  # 標本分散(の総和)
    var_unbiased = var_unbiased + s_xx/(n_sample-1)  # 不偏分散(の総和)


var_sample = var_sample/n_trial # 標本分散の平均
var_unbiased = var_unbiased/n_trial # 不偏分散の平均

print("標本分散の平均=",var_sample)
print("不偏分散の平均=",var_unbiased)
  1. コードを実行し、母分散(この場合は1)に近い値が得られるを確認しなさい。また、標本の数(n_sampleの値)を小さく(大きく)して、 標本分散と不偏分散との違いにどのように反映されるかシミュレーションしてみなさい。
  2. 母集団の分布型を(ガウス分布ではなく)$[-1,1]$の一様分布に変えて、同様の比較を行いなさい。

randomのマニュアル

icon-hint ヒント

random.gauss(0,1)の箇所を、random.uniform(-1,1) に変更すると、 区間$[-1,1]$の一様乱数を生成することができる。