IT業界における女性の社会進出はとくに遅れている: 米国における統計
1980年代になにが起こったのか? 「パソコン」の登場である。 当時、パソコンは「男の子向けのオモチャ」という位置づけで販売されていた:
この傾向は、消費者に対してある一定のメッセージを送っている。
Saturday Morning Breakfast Cereal (漫画)
- 男の子向けのオモチャ: 「メカブロックスだよ! 何通りにも組み合わせができて遠隔操作できるんだ!」
- 女の子向けのオモチャ: 「人形だよ!」
- 後年… 「なんでこの業界はみんな野郎ばっかりなんだ?」
しかし、女性でもコンピュータ・サイエンスのパイオニアは存在する:
CSV (Comma Separated Values、カンマ区切り値)
形式とは、テキスト形式の一種で、
カンマによって区切られた値を並べたものである。
まず、テキストファイルの各行は表の一行 (row) に相当する。
さらに各行のなかのカンマで囲まれた部分を
カラム (column、列または欄) または フィールド (field) という。
空白や特殊な文字が入ったフィールドはさらに "〜"
で囲まれる。
また CSVファイルは、Microsoft Excel で開くことができる:
Alice,USA,22 Bob,UK,25 "Yusuke Shinyama",Japan,13
エクセルで上の CSVファイルを開くと、以下のような表が表示される:
Alice | USA | 22 |
Bob | UK | 25 |
Yusuke Shinyama | Japan | 13 |
日本語のCSVをPythonでリスト(のリスト)として読み込むには、 以下のようにする:
import csv with open("ファイル名", encoding="utf-8") as fp: 変数名 = list(csv.reader(fp))
table1.csv
を作成し、
Excel で実際に開けることを確認せよ。
Python で上の CSV ファイルを読み込むと、 以下のような「リストのリスト」になっている:
(外側にある) リストの要素は、CSV (表) の各行に相当する。 各要素はさらにリストになっており (リストのリスト)、 その各要素は1行のカラム (列) に相当する。 なお、すべての内容は ([['Alice', 'USA', '22'], ['Bob', 'UK', '25'], ['Yusuke Shinyama', 'Japan', '13']]
22
のような数字でも)
文字列型になっていることに注意。これを数値として正しく処理するためには、
特定の要素に対して
int()
関数などを使う必要がある。
以下の zip ファイルをダウンロードせよ:
この表は全国の郵便番号一覧を記した、約12万行の巨大な CSV ファイルである。 以下のプログラムを使って、この表を1行ずつ表示せよ:
# CSVファイルを読み込む。 import csv with open("KEN_ALL_ROME.CSV", encoding="utf-8") as fp: table = list(csv.reader(fp)) # 表を1行(row)ずつ処理する。 for row in table: print(row)
(CSV の元のデータは郵便局の 郵便番号データダウンロードページから取得した)
演習 5-2. のプログラムを実行すると、以下のような内容が表示される:
['0600000', '北海道', '札幌市\u3000中央区', '以下に掲載がない場合', 'HOKKAIDO', 'SAPPORO SHI CHUO KU', 'IKANIKEISAIGANAIBAAI'] ['0640941', '北海道', '札幌市\u3000中央区', '旭ケ丘', 'HOKKAIDO', 'SAPPORO SHI CHUO KU', 'ASAHIGAOKA'] ['0600041', '北海道', '札幌市\u3000中央区', '大通東', 'HOKKAIDO', 'SAPPORO SHI CHUO KU', 'ODORIHIGASHI'] ... (Control + C を押して止める。)
このプログラムの中で、次の部分に着目しよう:
# 表を1行(row)ずつ処理する。
for row in table:
print(row)
これまではリストの要素をひとつずつ表示するのに、 以下のようにしていた:
実はa = [5,9,4,0] for i in range(len(a)): x = a[i] print(x)
for
文ではこうも書ける:
a = [5,9,4,0] for x in a: print(x)
これらは同じ原理にもとづいている。
range(n)
関数は、
[0, 1, 2, ..., n-1]
というリスト
(のようなもの) を返す。
したがって、
という文は、この中の要素ひとつひとつを変数for i in range(n): ....
i
に代入し、
「結果として i
が 0 から n-1 まで
変化しているように見えた」のである。
Python では、 リストと同じく、文字列もスライスが可能である。
s = "HEADACHE"
この場合、s
のスライスは、以下のようになる:
s[2:6]
の文字列を答えよ。
s[4:]
の文字列を答えよ。
s[:2]
の文字列を答えよ。
s[-2:]
の文字列を答えよ。
s[-2:-1]
の文字列を答えよ。
文字列に対してスライスが使えると、 ある文字列の中に含まれる部分文字列の検索をおこなうことができる。 これは、検索したい文字列をスライドさせながら各位置と比較することでおこなう:
# ある文字列に含まれる部分文字列を発見する。 s = "VACATION" # 検索対象の文字列 t = "CAT" # 含まれる文字列 for i in range(6): # 0〜5 if s[i:i+3] == t: print("found", i)
演習 5-2. でやった問題をふたたび考える。 郵便番号を入力すると、その住所を表示するプログラムを作りたい。
$ python postal.py 1520001 東京都目黒区中央町
このように、ある情報 (e.g. 郵便番号) を別の情報 (e.g. 住所) に関連づける処理は、 業務用コンピュータの代表的な利用例である。数学的には、 これは2つの集合 K から V への写像 K → V として定義される。 このとき、集合 K の要素を 「キー (key)」、集合 V の要素を 「バリュー (value)」と呼ぶ。
今回の例では郵便番号 (row[0]
) がキー、
住所 (row[1]+row[2]+row[3]
)
がバリューということになる。
これを使ってプログラム postal.py を改造すると、以下のようになる:
# CSVファイルを読み込む。 import csv with open("KEN_ALL_ROME.CSV", encoding="utf-8") as fp: table = list(csv.reader(fp)) # 郵便番号 (キー) を取得する。 import sys key = sys.argv[1] # 表を1行ずつ処理する。 for row in table: if row[0] == key: # キーが一致したら、それに対応するバリューを表示する。 value = row[1]+row[2]+row[3] print(value)
郵便番号データは 12万行以上あるので、 逐次探索をすると毎回 (最悪) 12万回のループを実行することになり、 効率が悪い。n件のデータに対して、O(n) の計算量が必要である。
二分探索を使えば、最悪 O(log n) の計算量ですむが、 データをあらかじめキーが昇順になるようにソートしておく必要がある。 またキーを新しく追加・更新するたびに正しく並べ直さなければならない。
郵便番号は 0
〜9
の数字から始まるので、
まずキーを「最初の1文字」で分類し、10個のリストに分割するという手がある。
# まず 10個の (空のリストからなる) リストを作成。 index = [[], [], [], [], [], [], [], [], [], []] for row in table: key = row[0] value = row[1]+row[2]+row[3] # キーの0文字目を数値に直す。 i = int(key[0]) # i番目のリストにキーとバリューの組を追加する。 a = index[i] a.append([key, value])
ここで作られたリストのリストを「索引」あるいは
「インデックス (index)」とよぶ。
インデックスを使って検索をする処理 lookup()
は以下のようになる:
def lookup(index, key): # キーの0文字目を数値に直す。 i = int(key[0]) # i番目のリストをとりだす。 a = index[i] # そのリストに対して逐次探索をおこなう。 for pair in a: # 一致するキーとバリューの組をとりだす。 k = pair[0] v = pair[1] if k == key: return v # 見つからなかったら、空の文字列を返す。 return ""
最後に、この関数を呼び出す:
import sys key = sys.argv[1] value = lookup(index, key) print(value)
この例では、最初のインデックスを作成するのに ある程度の時間がかかるので、全体の実行時間はそれほど速くならないが、 検索時間だけをみると、このようにリストを10個に分割すれば 時間は約 1/10 に短縮されると思われる、が…
上の方法のインデックスをより一般化したものが ハッシュテーブル (hash table) である。 これはインデックス作成時にキーの最初の1文字を使うかわりに、 ハッシュ関数 (hash function) が返す値を使う。 ハッシュ関数とは、任意の文字列をなんらかの値 (ハッシュ) に 変換するような関数である:
h("7038258")
= 0
h("7320001")
= 15731
h("4420029")
= 602
ハッシュ関数 h(x) はどんな関数でもよいが、 同じ文字列に対してはつねに同じ値を返すように設計しなければならない。 また、2つの異なる文字列ができるだけ異なる値になるように 設計することが望ましい。一般的なハッシュ関数の例として考えられるのは、 与えられたキー (文字列) に含まれる文字コードをすべて足す、 などの方法である。
ハッシュ関数ができたら、これを使って何番目のリストを使うべきか決定する。 全体的な流れは以下のようになる:
(h(x) % k)
番目の
リストを使用する。
ひとたびハッシュテーブルができれば、
関数 lookup()
は従来とほぼ同じ方法で検索がおこなえる:
def lookup(index, key): k = len(index) # キーをハッシュに変換し、k の剰余をとる。 i = h(key) % k # i番目のリストをとりだす。 a = index[i] # そのリストに対して逐次探索をおこなう。 for pair in a: # 一致するキーとバリューの組をとりだす。 k = pair[0] v = pair[1] if k == key: return v # 見つからなかったら、空の文字列を返す。 return ""
ハッシュテーブルの検索時間は二分探索ほど速くないが、 キーの追加や変更の処理が簡単なため広く利用されている。 また、ハッシュテーブルによる検索時間は k の値に調整できるところも有利である。
一般的に、ハッシュテーブルを使うと現実の検索時間は短縮されるが、 その計算量はたかだか 1/k になるだけである。 この k は定数であるので、Big-O 記法で表した計算量は変わらない。
前節の「郵便番号の最初の数字を 0 〜 9 の整数に変換する」処理は、ある意味 ハッシュ関数の一例であるが、郵便番号にしか使えないし、ハッシュの値は 0 から 9 までに固定されている。さらに、得られる値が一様に分布していない。 どのような文字列に対しても一様に分布するハッシュ関数を 設計するのは意外に難しく、これは現在でもさかんに研究されている テーマのひとつである。ちなみに最悪のハッシュ関数とは、 どんな文字列に対しても同じ値を返す関数である。 これではすべての文字に対して同じリストを使うことになってしまい、 事実上、逐次探索と変わらない:
# 最悪のハッシュ関数
def h(x):
return 123
ハッシュ値はどんなデータに対しても適用できる。 これを応用したものが電子署名で、あるデータのハッシュ値を使って、 そのデータが改竄されていないかチェックするものである。 ここでは、ハッシュ関数の以下のような特性を利用している:
ハッシュ関数の別の応用としてビットコインに代表される ブロックチェーン技術があるが、 ここでは触れない。
ブレイクアウトルーム演習の方法:
find(s, t)
として実現せよ。
これは与えられた文字列 s
の中に含まれる t
の位置を
整数として返し、見つからなければ -1
を返すものとする:
def find(s, t): ... return i print(find("VACATION", "CAT")) # 2 print(find("VACATION", "DOG")) # -1
find
を使って、
「東京都にあって、なおかつ『が丘
』が含まれる住所」を
表示させよ。全部でいくつあるか?
import csv with open("KEN_ALL_ROME.CSV", encoding="utf-8") as fp: table = list(csv.reader(fp)) # 表を1行(row)ずつ処理する。 for row in table: # 都道府県名 (1番目の要素) が東京都の住所を表示。 if row[1] == "東京都": print(row[1]+row[2]+row[3])
findcode.py
以下のプログラム findcode.py
は、授業中にやった
postal.py
とは逆に、住所から郵便番号を検索するプログラムである。
これは、100個のリストを使ったハッシュテーブルを作成し、
それを使って住所から郵便番号を検索している。
ところが、ここで使っているハッシュ関数 h は、
つねに同じ値を返す「最悪の関数」になっているため、
このままで実行すると1回の検索に時間がかかる。
ハッシュ関数を改良して、実行時間を 1/10以下に短縮せよ。
# キーに対応したハッシュを計算する関数 h を定義する。def h(key): return 123# 検索をおこなう関数 lookup を定義する。 def lookup(index, key): # キーをハッシュに変換し、k の剰余をとる。 i = h(key) % 100 # i番目のリストをとりだす。 a = index[i] # そのリストに対して逐次探索をおこなう。 for pair in a: # 一致するキーとバリューの組をとりだす。 k = pair[0] v = pair[1] if k == key: return v # 見つからなかったら、空の文字列を返す。 return "" # 郵便番号の一覧を読み込む。 import csv with open("KEN_ALL_ROME.CSV", encoding="utf-8") as fp: table = list(csv.reader(fp)) # 100個の (空のリストからなる) リストを作成。 index = [] for i in range(100): index.append([]) # このリストを使ったハッシュテーブルを作成する。 for row in table: key = row[1]+row[2]+row[3] # キー: 住所 value = row[0] # バリュー: 郵便番号 # キーをハッシュに変換する。 i = h(key) % 100 # i番目のリストにキーとバリューの組を追加する。 a = index[i] a.append([key, value]) # 指定された住所から郵便番号を検索し、実行時間を計測する。 import sys key = sys.argv[1] import time t0 = time.time() # 最初の時刻 (秒数) を取得する。 # 郵便番号を検索する (時間計測のため、1000回繰り返している)。 for i in range(1000): value = lookup(index, key) t1 = time.time() # 最後の時刻 (秒数) を取得する。 # 見つかった郵便番号を表示する。 print(value) # かかった時間を表示する。 print(t1 - t0)
実行例 (実行時間は正確にこの値になるとは限らない):
$ python findcode.py 東京都目黒区大岡山 1520033 0.021525859832763672 $ python findcode.py 長野県中野市中野 3830013 0.02087545394897461
crossmaze.py
中課題 2.でやった迷路生成プログラム genmaze.py
を
改造し「各通路が交差する迷路」を生成するプログラム
crossmaze.py
を作成せよ。
ここでは壁を #
で表すのではなく、
通路を -
、 |
、 +
で表すものとする:
$ python crossmaze.py 15 15 +-+-+-+---+-+-+ | | | | | | +-|-+---+-+ | | | | | + + + +-|-+ + + | | | | | +-+-+ + +-+-+-+ | | | +---+---+-+-+ | | | | | | | + + +-+ + + +-+ | | | | | | + +-|-+---+ + | | | | | + + + + +-+---+
genmaze.py
と同じであるが、
以下の変更を加える:
-
)
|
)
+
)
|
) の上を、
横向きの通路 (-
) が通ってもよいことにする。
ちなみに関数 showmaze()
は次のようになる:
# showmaze: 与えらえた迷路を表示する。 def showmaze(m): for y in range(len(m)): # 空の文字列を用意し、1文字ずつ足していく。 s = "" for x in range(len(m[y])): # 座標 (x,y) の情報を加える。 if m[y][x] == 1: s = s + "-" elif m[y][x] == 2: s = s + "|" elif m[y][x] == 3: s = s + "+" else: s = s + " " print(s) return