教科書ではあまり習わないPythonの便利な文法と先進的パッケージを紹介する.

ここでは,実際問題を解決するときに便利なPythonの比較的進んだ文法やパッケージを紹介する.

  • ジェネレータ
  • simpyによるシミュレーション
  • 型ヒント
  • dataclasses
  • pydanticパッケージによる型の厳密化
  • 既定値をもつ辞書 defaultdict
  • map関数
  • 正規表現
  • JSON
  • Requestsパッケージ
  • OpenPyXLによるExcel連携
  • StreamlitによるWebアプリ作成

ジェネレータ

ジェネレータとは, 反復子(イテレータ)を生成するための仕組みであり, 関数の内部でyield文を入れることによって作ることができる.

def 関数名(引数:

  yield 生成したいもの

たとえば, 2,3,5,7 の数値を順番に生成するジェネレータ prime は,以下のようになり, for文でrange()の代わりにprime()とすると,素数を順に入れた反復ができる.

def prime():
    yield 2
    yield 3
    yield 5
    yield 7
for i in prime():  # [2,3,5,7]
    print(i)
2
3
5
7

関数の中でforやwhileを用いた反復文を使い,生成する数値をたくさん(無限個)生成しておいてもメモリに負担をかけることはない. 使った分だけ順に生成されるからだ. 例として,2のべき乗を無限に生成するジェネレータとその使用法を示す.

def expon():
    i = 1
    while True:
        i *= 2
        yield i
for i in expon():
    print(i)
    if i >= 100:
        break
2
4
8
16
32
64
128

問題

フィボナッチ数 $F_n$ は

$$ F_1 =1 $$$$ F_2 =1 $$$$ F_n= F_{n-1} +F_{n-2} $$

と定義される数列である. フィボナッチ数を順に生成するジェネレータを作り, 100未満のフィボナッチ数を順に出力せよ.

問題

itertoolsパッケージのpermutationsは,与えたリストの順列を順に生成するジェネレータ関数である. 適当なリストを作り,順列の最初の5個を表示せよ.

simpyによるシミュレーション

simpyはシミュレーションのためのPythonパッケージである. simpyは標準パッケージではないので, インストールしておく必要がある.

simpyでは,ジェネレータ関数によって生成されたイベントをもとにシミュレーションを行う.

基本的な使い方は以下の通りである.

import simpy
env = simpy.Environment()      #シミュレーション環境を定義する
env.process(ジェネレータ関数(env)) #ジェネレータを環境に登録する
                               #(ここではシミュレーションは実行されない)
env.run(制限時間)               #制限時間分だけシミュレーションを行う

10分給油すると60分走行する車を300分だけシミュレートしてみよう.

import simpy

def car(env):
    while True:
        print(f"{env.now} : 給油中")  # nowで現在時刻を得ることができる
        yield env.timeout(10)  # timeoutで時間を経過させることができる
        print(f"{env.now} : 走行中")
        yield env.timeout(60)


env = simpy.Environment()  # シミュレーション環境を定義する
env.process(car(env))  # ジェネレータを環境に登録する(ここではシミュレーションは実行されない)
env.run(300)  # 制限時間分だけシミュレーションを行う
0 : 給油中
10 : 走行中
70 : 給油中
80 : 走行中
140 : 給油中
150 : 走行中
210 : 給油中
220 : 走行中
280 : 給油中
290 : 走行中

問題

工場内で稼働するAGV(Autimated Guided Vehicle; 自動搬送車)の動きをシミュレートしたい. AGVは3秒直進すると,1秒かかって右折し,5秒直進する動作を繰り返す. 100秒までの動作を出力せよ.

クラスを使った在庫シミュレーション

simpyで複数のジェネレータ関数を定義する場合には,関数間で変数の受け渡しをする必要が出てくることが多い. 大域的変数を使うのを避けるために,シミュレーション用のクラスを使うことが推奨される.

簡単な在庫のシミュレータを作成しよう.

いま,顧客が$[1,10]$の一様な時間間隔でやってきて,1単位の需要が発生するものとする. 在庫ポジション(手持ち在庫に発注済の量を加えた量)が発注点 $ROP$ 以下になったときに,一定量 $Q$ を発注するものとする. 発注した量はリード時間 $LT$ 後に到着するものと仮定したときのシミュレーションを考える.

クラスは以下のメソッドから構成される.

  • コンストラクタ init: シミュレーション環境 env とパラメータを受け取り,内部変数に保存する.以下の需要ジェネレータを環境に登録する.
  • 需要ジェネレータ demand: 時間間隔 $[1,10]$ で需要を発生させ, 手持ち在庫 inventory ならびに在庫ポジション inv_positionを1減らし,在庫ポジションが発注点未満なら,在庫ポジションを $Q$ 増やし, 以下の発注処理を行う.

  • 発注処理 order: リード時間 $LT$ 後に手持ち在庫を $Q$ 増やす.

import simpy
import random

class inv_simulation:
    def __init__(self, env, LT, ROP, Q):
        self.env = env
        self.env.process(self.demand())
        self.LT = LT
        self.ROP = ROP
        self.Q = Q
        self.inventory = self.ROP
        self.inv_position = self.inventory

    def demand(self):
        while True:
            yield (self.env.timeout(random.randint(1, 11)))
            print(f"{env.now} : inventory={self.inventory}")
            self.inventory -= 1
            self.inv_position -= 1
            if self.inv_position < self.ROP:
                print(f"{self.env.now} : order")
                self.inv_position += self.Q
                self.env.process(self.order())

    def order(self):
        yield (self.env.timeout(self.LT))
        print(f"{self.env.now} : reprenishment")
        self.inventory += self.Q
env = simpy.Environment()
sim = inv_simulation(env, LT=100, ROP=50, Q=30)
sim.env.run(150)
1 : inventory=50
1 : order
3 : inventory=49
4 : inventory=48
11 : inventory=47
14 : inventory=46
15 : inventory=45
21 : inventory=44
30 : inventory=43
34 : inventory=42
35 : inventory=41
38 : inventory=40
46 : inventory=39
50 : inventory=38
55 : inventory=37
57 : inventory=36
65 : inventory=35
69 : inventory=34
75 : inventory=33
85 : inventory=32
90 : inventory=31
95 : inventory=30
101 : reprenishment
106 : inventory=59
108 : inventory=58
116 : inventory=57
121 : inventory=56
125 : inventory=55
126 : inventory=54
130 : inventory=53
139 : inventory=52
147 : inventory=51

問題

リード時間 $LT$ がランダムな値(確率分布は自分で適当に決める)をとるように拡張せよ.

問題

平均在庫量や品切れ量をカウントするように変更せよ.

問題

発注方策を,発注点をきったときに「基在庫レベル $-$ 在庫ポジション」だけ発注するように変更せよ.基在庫レベルはパラメータである.これを色々変えたときの平均在庫量と品切れ量を調べてみよ.

型ヒント

Pythonでは型は指定しなくても動く. しかし,最近では型を指定して記述することができるようになった.

これを型ヒント(type hint)と呼ぶ. 上で,「できる」という部分が重要で,型ヒントを記述しなくても良いが,しておくことによって,分かりやすく,かつエラーをしにくくなる. また, いくつかの先進的なパッケージはこれを積極的に利用して,より堅牢なプログラムを目指している.

型ヒントは,「変数名:型名」と指定する. 関数の返値の指定は,関数の定義の括弧の後に 「-> 型名」を追加する.

以下の例は,整数 $x$ を2倍した整数を返す関数である.

def f(x: int) -> int:
    return x * 2
f(12)
24

ただし,型ヒントは,プログラム自体には何の影響も与えないので,引数に文字列を入れてもエラーしない.

f("hello")
'hellohello'

問題

以下のプログラムに型ヒントを追加せよ.

def gcd(m, n):
    if n == 0:
        return m
    return gcd(n, m % n)

m = 100
n = 8
print(gcd(m, n))
4
def palindrome(s):
    if len(s) <= 1:
        return True
    elif s[0] == s[-1]:
        return palindrome(s[1:-1])
    else:
        return False

print(palindrome("たけやぶやけた"))
print(palindrome("たけやぶやけてない"))
True
False

dataclassesパッケージ

dataclassesパッケージのdataclassデコレータと型ヒントを使うと, 簡易的にクラスを生成することができる (デコレータとは,関数やクラスを修飾する関数であり,@を先頭につける).

デコレータ dataclassの引数 order をTrueにすることによって,比較関連の演算子を定義したクラスを生成できる.

以下の例では,名前(文字列)と身長(整数)と体重(浮動小数点数)を属性として定義したクラス Customer を自動生成している.

比較演算子を定義してあるので,身長,体重,名前のタプルでソートすることができる.

from dataclasses import dataclass

@dataclass(order=True)
class Customer:
    height: int = 180
    weight: float = 70.0
    name: str = "No Name"

c1 = Customer(name="Kitty")
c2 = Customer(150, 40.0, "Daniel")
c3 = Customer(150, 60.0, "Dora")
print(c1, c2, c3)
L = [c1, c2, c3]
L.sort()
print(L)
Customer(height=180, weight=70.0, name='Kitty') Customer(height=150, weight=40.0, name='Daniel') Customer(height=150, weight=60.0, name='Dora')
[Customer(height=150, weight=40.0, name='Daniel'), Customer(height=150, weight=60.0, name='Dora'), Customer(height=180, weight=70.0, name='Kitty')]

問題

上で作成した Customer クラスに「体重/身長の2乗」で定義されるボディマス指数を表す属性 bmi (不動小数点数)を追加せよ.

問題

地名(文字列)と座標 $x,y$ (2つの浮動小数点数) を属性としてもつクラスを作れ. 3つの地点のインスタンスをリストに入れたとき, $x$ 座標(同点の場合には $y$ 座標)の小さい順に並べるには, どうしたら良いか?

pydanticパッケージによる型の厳密化

pydanticパッケージを使うことによって, 型指定の厳密化ができる. pydanticは標準パッケージではないので, インストールしておく必要がある.

以下の例では, 名前(文字列),身長(整数),体重(浮動小数点数),友人リスト(リストの要素は文字列)のクラス Customerを生成している.

既定値が指定されていない場合には,属性は必須になり,クラス生成時に省略できない.

from typing import List
from pydantic import BaseModel, ValidationError

class Customer(BaseModel):
    name: str
    height: int
    weight: float
    friends: List[str] = []

# c1 = Customer(name = "Kitty") #身長,体重がないので, エラー
c1 = Customer(name="Kitty", height=180, weight=60.0)  # friendsは既定値が指定されているので,省略可
print(c1)
name='Kitty' height=180 weight=60.0 friends=[]

引数の型が異なる場合には, 指定された型に合うように変換される.

c2 = Customer(name=123, height=180.0, weight=60, friends=["Kitty"])
print(c2)
name='123' height=180 weight=60.0 friends=['Kitty']

変換できない場合には ValidationError が発生する. このエラーを try ... except ... でキャッチすることにより,より詳細なエラー情報を得ることができる.

try:
    c2 = Customer(height="Heigh", weight=60, friends="Kitty")
except ValidationError as e:
    print(e)
3 validation errors for Customer
name
  field required (type=value_error.missing)
height
  value is not a valid integer (type=type_error.integer)
friends
  value is not a valid list (type=type_error.list)

問題

顧客名(文字列), 緯度(浮動小数点数),経度(浮動小数点数),商品(整数のリスト)からなるクラスをpydanticで作れ.

データを色々と与えて,どのようなエラーがでるか,エラーを把握できるかを確認せよ.

既定値をもつ辞書 defaultdict

通常の辞書は,存在しないキーに対して値を得ようとするとエラーする. これは,以下のように簡単に回避できるが,ちょっと面倒くさい.

既定値が $0$ である辞書 D を作りたいと仮定する.

D = dict(one=1, two=2)
print(D)
# print(D["zero"])       #エラー
print(D.get("zero", 0))  #getで回避
# 辞書に入っているかをif文で判定して回避
if "zero" in D:
    print(D["zero"])
else:
    print(0)
{'one': 1, 'two': 2}
0
0

collectionsパッケージにdefaultdict関数がある.これは,既定値を指定した辞書を生成する.

引数は関数であり,その返値が既定値になる. たとえばintとすると int() は $0$ を返すので,既定値は $0$ になり, listとすると空のリストになる.

from collections import defaultdict

D = defaultdict(int)
D["zero"]
0

defaultdictの使用例として,以下の文章の文字の出現回数を辞書に保管する.

sentense = """
I am, my lord, as well derived as he,
As well possess'd; my love is more than his;
My fortunes every way as fairly rank'd,
If not with vantage, as Demetrius';
And, which is more than all these boasts can be,
I am beloved of beauteous Hermia:
Why should not I then prosecute my right?
Demetrius, I'll avouch it to his head,
Made love to Nedar's daughter, Helena,
And won her soul; and she, sweet lady, dotes,
Devoutly dotes, dotes in idolatry,
Upon this spotted and inconstant man. 
"""

Count = defaultdict(int)
for s in sentense:
    Count[s] += 1
print(Count)
defaultdict(<class 'int'>, {'\n': 13, 'I': 5, ' ': 77, 'a': 30, 'm': 11, ',': 16, 'y': 11, 'l': 19, 'o': 28, 'r': 17, 'd': 21, 's': 32, 'w': 7, 'e': 44, 'i': 17, 'v': 8, 'h': 19, 'A': 3, 'p': 4, "'": 5, ';': 4, 't': 30, 'n': 21, 'M': 2, 'f': 4, 'u': 11, 'k': 1, 'g': 3, 'D': 3, 'c': 5, 'b': 4, 'H': 2, ':': 1, 'W': 1, '?': 1, 'N': 1, 'U': 1, '.': 1})

map関数

map関数は,以下のように使う.

map(関数のようなもの, リストのようなもの)

返値は,リストに含まれる要素ごとに関数を適用して得られた結果(マップオブジェクト)である.

例を示そう.

numbers = "12 56 46".split()
print(numbers)
m = map(int, numbers)
print(m)
print(list(m))
for i in map(int, numbers):
    print(i)
['12', '56', '46']
<map object at 0x7fc64892cbe0>
[12, 56, 46]
12
56
46

例として "'Taro', 180, 69 \n 'Jiro', 170, 50 \n 'Saburo', 160, 40" という文字列から, 人名をキーとして身長と体重のタプル(浮動小数点値に変換)を値とした辞書 D を生成する.

D = {}
data = "'Taro', 180, 69 \n 'Jiro', 170, 50 \n 'Saburo', 160, 40"
for line in data.split("\n"):
    L = line.split(",")
    height, weight = map(float, L[1:])
    D[L[0]] = (height, weight)
print(D)
{"'Taro'": (180.0, 69.0), " 'Jiro'": (170.0, 50.0), " 'Saburo'": (160.0, 40.0)}

問題

上の辞書Dを既定値を空のリストとしたdefaultdictとして生成し, 人名をキー, 身長と体重を入れたリストを値とせよ.

問題

OR Lib. (http://people.brunel.ac.uk/~mastjjb/jeb/info.html) にある問題例を1つ選択し,defaultdict(複数必要な問題例もある)にデータを格納せよ.

どのようなデータ構造を使えば,問題を解きやすいか考えて,設計せよ.

正規表現

正規表現 (regular expression, regex) はPythonに限らず,様々なプログラミング言語で使用される便利な仕組みである.

正規表現自体は先進的ではないが, 分かりにくい学習サイトが多いので,簡単に紹介しておく.

JupyterLabの検索 (EditメニューもしくはCommand Fで起動)やほとんどのモダンなエディタ(たとえばVisual Studio Code)でも, 正規表現が使える. 検索窓の .* を押すと,正規表現になる.

以下のサイトで様々な正規表現を試すことができる.

https://pythex.org/

正規表現の記法は膨大であるので,ここではPythonで使う場合の基礎を学ぶ.

正規表現パッケージは,標準パッケージ re である.

reパッケージには,文字列から決められたパターンに対して, (ほんの一部の例であるが) 以下のような操作ができる.

  • match: パターンを含むか否かの判定
  • findall: すべてのパターンの列挙
  • split: パターンによる分割
  • sub: 文字列による置き換え

上の関数に与えるのは,正規表現と呼ばれるパターン文字列であり,エスケープシークエンス \ を含む場合があるので,文字列の前にrを入れておく.

たとえば,アルファベットの大文字は, r"[A-Z]" と表現される. [ ] は括弧内のいずれかを表し, A-Z はAからZの範囲を表す.

"Kitty White"という文字列からアルファベットの大文字を探索するには,以下のようにする.

import re

re.findall(r"[A-Z]", "Kitty White")
['K', 'W']

上の例の[]はメタ文字と呼ばれ,他にも(ほんの一部であるが) 以下のようなものがある.

メタ文字 意味
. 1文字
^ 次の文字から始まる
$ 前の文字で終わる
* 前の文字の0回以上の繰り返し
+ 前の文字の1回以上の繰り返し
? 前の文字が0回か1回
[^A-Z] A-Z以外
( ) グループ化
| または

メタ文字を使いたい場合には,\(バックスラッシュ)を前に書く必要がある. たとえば, . を探したい場合には, \. と書く.

他にも,以下のような表現が準備されている.

文字 意味
\t タブ
\n 改行
\d すべての数字
\s 空白文字列
\w アルファベット,数字, アンダーバー _ 

例として, 空白文字列 (\s) で"Kitty White"を分割してみる.

re.split(r"\s", "Kitty White")
['Kitty', 'White']

問題

tという文字の1回以上の繰り返しで,"Kitty White"を分割せよ.

問題

以下の文字列 s から,数字だけを正規表現を用いて抽出せよ.

s = """
0          [0, 32400]
1     [[3600, 14400]]
2    [[14400, 25200]]
3    [[14400, 21600]]
"""

JSON

JSON(JavaScript Object Notation)は,テキストベースのデータフォーマットであり, データの受け渡しの際には,非常に便利である.

JSONのサンプルデータは,以下のサイトで生成したものを加工した.

https://www.json-generator.com/

JSONは単なる文字列であり, json.loads関数で読み込むことができる.

JSONオブジェクトはPythonの辞書に,arrayはリストに, trueはTrueに,nullはNoneに変換される.

import json

customer_string = """
{ "customers": [ 
   {
    "index": 0,
    "age": 25,
    "name": "Lisa Hansen",
    "gender": "female",
    "user": true,
    "email": "lisahansen@centregy.com",
    "phone": "+1 (884) 580-2826",
    "address": "914 Will Place, Avalon, Delaware, 4582",
    "friends": [
      {
        "id": 0,
        "name": "Holloway Stout"
      },
      {
        "id": 1,
        "name": "Suzette Gross"
      },
      {
        "id": 2,
        "name": "Madge Sexton"
      }
    ]
  }, 
  {
    "index": 1,
    "age": 29,
    "name": "Wise Greene",
    "gender": "male",
    "user": false,
    "email": "wisegreene@avit.com",
    "phone": "+1 (821) 552-3810",
    "address": "295 Story Court, Clay, Virgin Islands, 4262",
    "friends": null
  }
  ]
}
"""
data = json.loads(customer_string)
data
{'customers': [{'index': 0,
   'age': 25,
   'name': 'Lisa Hansen',
   'gender': 'female',
   'user': True,
   'email': 'lisahansen@centregy.com',
   'phone': '+1 (884) 580-2826',
   'address': '914 Will Place, Avalon, Delaware, 4582',
   'friends': [{'id': 0, 'name': 'Holloway Stout'},
    {'id': 1, 'name': 'Suzette Gross'},
    {'id': 2, 'name': 'Madge Sexton'}]},
  {'index': 1,
   'age': 29,
   'name': 'Wise Greene',
   'gender': 'male',
   'user': False,
   'email': 'wisegreene@avit.com',
   'phone': '+1 (821) 552-3810',
   'address': '295 Story Court, Clay, Virgin Islands, 4262',
   'friends': None}]}

読み込んだdataは辞書であり, "customers"がキー, 顧客のリストが値になっている. リストを反復して名前を表示し, 電話番号 phoneを辞書から削除する.

for i in data["customers"]:
    print(i["name"])
    del i["phone"]
data
Lisa Hansen
Wise Greene
{'customers': [{'index': 0,
   'age': 25,
   'name': 'Lisa Hansen',
   'gender': 'female',
   'user': True,
   'email': 'lisahansen@centregy.com',
   'address': '914 Will Place, Avalon, Delaware, 4582',
   'friends': [{'id': 0, 'name': 'Holloway Stout'},
    {'id': 1, 'name': 'Suzette Gross'},
    {'id': 2, 'name': 'Madge Sexton'}]},
  {'index': 1,
   'age': 29,
   'name': 'Wise Greene',
   'gender': 'male',
   'user': False,
   'email': 'wisegreene@avit.com',
   'address': '295 Story Court, Clay, Virgin Islands, 4262',
   'friends': None}]}

json.dumps関数で,Pythonの辞書をJSON形式のテキストに変換できる. 上で電話番号を除いた辞書をJSONに変えてみる.

引数のindentを設定すると読みやすくなる.

json_text = json.dumps(data, indent=2)
print(json_text)
{
  "customers": [
    {
      "index": 0,
      "age": 25,
      "name": "Lisa Hansen",
      "gender": "female",
      "user": true,
      "email": "lisahansen@centregy.com",
      "address": "914 Will Place, Avalon, Delaware, 4582",
      "friends": [
        {
          "id": 0,
          "name": "Holloway Stout"
        },
        {
          "id": 1,
          "name": "Suzette Gross"
        },
        {
          "id": 2,
          "name": "Madge Sexton"
        }
      ]
    },
    {
      "index": 1,
      "age": 29,
      "name": "Wise Greene",
      "gender": "male",
      "user": false,
      "email": "wisegreene@avit.com",
      "address": "295 Story Court, Clay, Virgin Islands, 4262",
      "friends": null
    }
  ]
}

上で作ったJSON形式のテキストを,ファイルに保存する.

with open("customer.json", "w") as f:
    f.write(json_text)

テキストファイルを読み込みPythonの辞書に変換するには,json.load関数(sがないことに注意)を用いる.

with open("customer.json") as f:
    new_data = json.load(f)
new_data
{'customers': [{'index': 0,
   'age': 25,
   'name': 'Lisa Hansen',
   'gender': 'female',
   'user': True,
   'email': 'lisahansen@centregy.com',
   'address': '914 Will Place, Avalon, Delaware, 4582',
   'friends': [{'id': 0, 'name': 'Holloway Stout'},
    {'id': 1, 'name': 'Suzette Gross'},
    {'id': 2, 'name': 'Madge Sexton'}]},
  {'index': 1,
   'age': 29,
   'name': 'Wise Greene',
   'gender': 'male',
   'user': False,
   'email': 'wisegreene@avit.com',
   'address': '295 Story Court, Clay, Virgin Islands, 4262',
   'friends': None}]}

住所addressを削除してから,JSONテキスト形式で保管する. それには, json.dump関数(sがないことに注意)を使えば良い.

for i in new_data["customers"]:
    del i["address"]
with open("new_customer.json", "w") as f:
    json.dump(new_data, f, indent=2)

Pandasのデータフレームは, to_json() メソッドでJSONに変換できる.

import pandas as pd

D = {"name": ["Pikacyu", "Mickey", "Kitty"], "color": ["Yellow", "Black", "White"]}
df = pd.DataFrame(D)
df
name color
0 Pikacyu Yellow
1 Mickey Black
2 Kitty White
txt = df.to_json()
print(txt)
{"name":{"0":"Pikacyu","1":"Mickey","2":"Kitty"},"color":{"0":"Yellow","1":"Black","2":"White"}}

逆に,JSONのテキストをデータフレームに変換するには, read_json関数を用いる.

pd.read_json(txt)
name color
0 Pikacyu Yellow
1 Mickey Black
2 Kitty White

問題

以下のサイトで適当なJSONファイルを生成し, 読み込んで加工してみよ.

https://www.json-generator.com/

Requestsパッケージ

RequestsはHTTPリクエストを簡単にするためのパッケージである. 標準パッケージではないので,インストールしておく必要がある.

例としてOSRMというAPIサービスを呼び出してみる.

http://project-osrm.org/docs/v5.5.1/api/#requests

import requests

response = requests.get(
    "http://router.project-osrm.org/route/v1/driving/139.792429,35.667864;139.768525,35.681010"
)
ret = response.json()  # レスポンスをJSON化すると,大学から東京駅までの移動距離が3708メートル,340秒(車で)かかることが分かる.
print(response, type(response))
ret
<Response [200]> <class 'requests.models.Response'>
{'code': 'Ok',
 'waypoints': [{'hint': 'Aw_pgxgP6YO2AAAAFAAAAAAAAADRAAAAJbyYQjyT_UAAAAAA8ratQrYAAAAUAAAAAAAAANEAAACbVAAAyQ9VCEs_IAItEFUImD8gAgAAjwjUAcIN',
   'distance': 12.447899,
   'location': [139.792329, 35.667787],
   'name': ''},
  {'hint': 'Ogb2g____38AAAAAFwAAACUAAAAGAAAAAAAAAIUgGUHOOXJBTx0mQAAAAAAXAAAAJQAAAAYAAACbVAAAF7RUCIByIALNslQI8nIgAgIAzwHUAcIN',
   'distance': 32.443406,
   'location': [139.768855, 35.680896],
   'name': 'タクシー・一般車降車場(一般車用)'}],
 'routes': [{'legs': [{'steps': [],
     'weight': 340.2,
     'distance': 3708.7,
     'summary': '',
     'duration': 340.2}],
   'weight_name': 'routability',
   'geometry': 'ujuxEaeftY{DxEe@k@}@lAiNeRkC_CgGhQaM`RsC`R{Rxa@wIdZlSlKyKt]cAk@e@x@',
   'weight': 340.2,
   'distance': 3708.7,
   'duration': 340.2}]}

urlにパラメータを埋め込むのではなく,引数paramsで辞書を渡すことができる.

例として,以下のサイト(A simple HTTP Request & Response Service)でテストをしてみる.

https://httpbin.org/

d = {"page": 1, "count": 1}
response = requests.get("http://httpbin.org/get", params=d)
ret = response.json()
print(response.url)
print(ret["args"])
http://httpbin.org/get?page=1&count=1
{'count': '1', 'page': '1'}

情報を得るためのgetメソッドだけでなく,postメソッドもできる.

ユーザー名とパスワードをpostすることによって,loginができるようになる.

d = {"username": "mikio", "password": "test"}
response = requests.post("http://httpbin.org/post", data=d)
response.json()["form"]
{'password': 'test', 'username': 'mikio'}

getメソッドでloginしてみる.

レスポンスのコード(200)はOKを意味する.

response = requests.get(
    "http://httpbin.org/basic-auth/mikio/test", auth=("mikio", "test")
)
print(response)
<Response [200]>

問題

Google Mapなどで自宅の緯度・経度を(右クリックで)調べて,大学までの距離をOSRMで求めよ. また,Google Mapの距離と時間と比較せよ.

OpenPyXL によるExcel連携

日本の企業の多くは業務をExcelで行っている. そのため,Excelと連携したシステムを作ることは,非常に重要である. PythonでExcelと連携するためのパッケージとしてOpenPyXL https://openpyxl.readthedocs.io/en/stable/index.html がある. 以下に,基本的な使用法を示す.

保存・シート追加・シート削除

import openpyxl
from openpyxl import Workbook
wb = Workbook()  # Bookインスタンス生成
ws = wb.active  # シートは1つ生成された状態になっている.
wb.create_sheet(title="Sample Sheet")  # 新しいシートの生成
wb.save("sample0.xlsx")  # 保存;2つのシート (Sheet1, Sample Sheet)がある
wb.remove(ws)  # 最初のwsの削除
print(wb.sheetnames)  # シート名の確認
['Sample Sheet']

読み込み

wb = openpyxl.load_workbook("sample0.xlsx")
print(wb.sheetnames)  # シート名の確認
['Sheet', 'Sample Sheet']

セル参照と値の代入

ws1 = wb["Sample Sheet"]  # もしくは wb.worksheets[1]
c1 = ws1["A1"]  # セルインスタンス; ws1.cell(1,1) でも同じ(番号は1から)
c1.value = "Hello"
wb.save("sample1.xlsx")  # 保存(セルの確認)

シートに列の追加

data = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
for i in range(3):
    ws1.append(data[i])  # ワークシートに追加
wb.save("sample2.xlsx")  # 保存(セルの確認) 2行目以降に行列が保管されていることの確認

セルのアドレス

print(c1.coordinate)
print(c1.row, c1.column, c1.column_letter, c1.value)
A1
1 1 A Hello

データの参照(rows, iter_rowsによる反復)

for row in ws1.rows:
    for i in row:
        print(i.value, end=" ")
    print()
print("2行目の2列目から")
for row in ws1.iter_rows(min_row=2, min_col=2):
    for i in row:
        print(i.value, end=" ")
    print()
Hello None None 
1 2 3 
4 5 6 
7 8 9 
2行目の2列目から
2 3 
5 6 
8 9 

データを一度に変換し,Pandasのデータフレームを生成

ワークシートオブジェクト.valuesで,データのジェネレータオブジェクトを得ることができる.これをリストに変換すると,行ごとのデータ(タプル)のリストになる.

ヘッダの行がある場合には,それを読んでから,DataFrameクラスのcolumns引数に与えれば良い. 以下の例では,自分で列名を指定して,データフレームを生成している.

import pandas as pd
df = pd.DataFrame(list(ws1.values), columns=["1列目", "2列目", "3列目"])
df
1列目 2列目 3列目
0 Hello NaN NaN
1 1 2.0 3.0
2 4 5.0 6.0
3 7 8.0 9.0

セルのフォントとボーダーの設定

from openpyxl.styles import Font
from openpyxl.styles.borders import Border, Side

ws1["A1"].font = Font(bold=True, color="FF0000")
side = Side(style="thin", color="FF0000")
ws1["A1"].border = Border(left=side, right=side, top=side, bottom=side)

関数の代入

for row in range(2, 5):
    cell = ws1.cell(row, 5)
    cell.value = f"=SUM(A{row}:C{row})"
wb.save("sample3.xlsx")  # 保存(和が計算されていることの確認)
wb = openpyxl.load_workbook("sample3.xlsx", data_only=True)
ws = wb["Sample Sheet"]
for row in ws.iter_rows(min_row=2):
    for i in row:
        print(i.value, end=" ")
    print()
1 2 3 None None 
4 5 6 None None 
7 8 9 None None 

チャート

from openpyxl.chart import BarChart, Reference

chart1 = BarChart()
chart1.style = 10
chart1.title = "Bar Chart"

data = Reference(ws, min_col=5, min_row=2, max_col=5, max_row=4)
chart1.add_data(data)
ws.add_chart(chart1, "A10")
wb.save("sample4.xlsx")

テーブル

from openpyxl.worksheet.table import Table, TableStyleInfo

wb = Workbook()
ws = wb.active

data = [
    ["Apples", 10000, 5000, 8000, 6000],
    ["Pears", 2000, 3000, 4000, 5000],
    ["Bananas", 6000, 6000, 6500, 6000],
    ["Oranges", 500, 300, 200, 700],
]

ws.append(["Fruit", "2011", "2012", "2013", "2014"])
for row in data:
    ws.append(row)

tab = Table(displayName="Table1", ref="A1:E5")

style = TableStyleInfo(
    name="TableStyleMedium9",
    showFirstColumn=False,
    showLastColumn=False,
    showRowStripes=True,
    showColumnStripes=True,
)
tab.tableStyleInfo = style

ws.add_table(tab)
wb.save("table.xlsx")

データバリデーション

from openpyxl.worksheet.datavalidation import DataValidation

wb = Workbook()
ws = wb.active

# リストから選択
dv1 = DataValidation(type="list", formula1='"Dog,Cat,Bat"', allow_blank=True)
ws.add_data_validation(dv1)
c1 = ws["A1"]
dv1.add(c1)

# 100より大きい数
dv2 = DataValidation(type="whole", operator="greaterThan", formula1=100)
ws.add_data_validation(dv2)
dv2.add("B1:B1048576")  # B列のすべて

# 0から1の小数
dv3 = DataValidation(type="decimal", operator="between", formula1=0, formula2=1)
ws.add_data_validation(dv3)
dv3.add("C1:C1048576")

# 日付
dv4 = DataValidation(type="date")  # 時間はtime
dv4.prompt = "日付を入力してください"
dv4.promptTitle = "日付選択"
ws.add_data_validation(dv4)
dv4.add("D1:D1048576")

wb.save("datavalid.xlsx")

カラースケールによる条件付きフォーマッティング

from openpyxl.formatting.rule import ColorScaleRule

wb = Workbook()
ws = wb.active
# 色はRRGGBB 000000=黒, FFFFFF=白   http://html.seo-search.com/reference/color.html 参照
ws.conditional_formatting.add(
    "A1:A10",
    ColorScaleRule(
        start_type="min", start_color="000000", end_type="max", end_color="FFFFFF"
    ),
)
wb.save("colorscale.xlsx")

問題 実務的なExcelファイルの読み込み

以下のサイトから「全国:年齢(各歳)、男女別人口 ・ 都道府県:年齢(5歳階級)、男女別人口」(2022年4月15日公表)データを読み込み,pandasのデータフレームに直せ.

https://www.stat.go.jp/data/jinsui/2021np/zuhyou/05k2021-3.xlsx

StreamlitによるWebアプリ作成

Streamlitは,Webアプリ作成のための最も簡単な方法であり,無料で公開も可能である.

インストール方法(自分の環境にあったものを1つ選ぶ)は,以下の通り.

pip install streamlit   
poetry add streamlit
conda install -c conda-forge streamlit

サーバー高速化のためには,以下もインストールする必要がある.

pip install watchdog
poetry add watchdog
conda install -c conda-forge watchdog

ローカル環境で「プログラム名.py」ファイルを作成し

streamlit run プログラム名.py

とすると,ブラウザのローカルホスト http://localhost:8501/ に結果が表示される.

以下のサイトに登録すると無料でWebアプリをデプロイできる.

https://streamlit.io/sharing

簡単な例

以下を入れた main.py ファイルを作成し,ターミナル(コマンドプロンプト)で,

streamlit run main.py

とする.

import streamlit as st
import pandas as pd
import plotly.express as px
st.title("タイトル")
st.write("何でも書ける")
st.markdown("## マークダウンで書く $x^2$")

データフレームや図も書ける

df = px.data.iris()
st.write(df.head())
st.table(df.head())
st.write(px.scatter(df, x="sepal_length", y="sepal_width", color="species"))

ボタンやチェックボックス

if st.button("Say hello"):
    st.write("こんにちわ")
else:
    st.write("Goodbye")

agree = st.checkbox("チェックしてください")
if agree:
    st.write("Great!")

対話をするためのウィジット

fruit = st.radio(label="好きなフルーツは?", options=["バナナ", "りんご", "いちご"], index=1)
st.write(fruit)

option = st.selectbox("今日はなにする?", ("なわとび", "野球", "ゲーム"))

def f(i):
    play = ("なわとび", "野球", "ゲーム")
    return play[i]

option = st.selectbox("今日はなにする(2)?", (0, 1, 2), format_func=f)
st.write("よし ", option, " をしよう!")

options = st.multiselect(
    "何色が好き?", options=["Green", "Yellow", "Red", "Blue"], default=["Yellow", "Red"]
)
st.write("You selected:", options)

age = st.slider("何歳?", min_value=0, max_value=130, value=25)
st.write(age)
from datetime import datetime, date, time

interval = st.slider(
    "計画期間は?",
    value=(datetime(2019, 1, 1, 9, 30), datetime(2020, 1, 1, 9, 30)),
    format="MM/DD/YY - hh:mm",
)

color = st.select_slider(
    "色を選んでね", options=["red", "orange", "yellow", "green", "blue", "indigo", "violet"]
)

title = st.text_input("お名前は?", value="ななしのごんべえ")

number = st.number_input("数字を入れてね", min_value=1.0, max_value=10.0, value=5.0, step=0.1)

d = st.date_input("誕生日は?", date(2019, 7, 6))

alarm = st.time_input("アラームセット", time(8, 45))

uploaded_file = st.file_uploader("ファイル選択")

add_selectbox = st.sidebar.selectbox("連絡方法は?", ("Email", "Home phone", "Mobile phone")) #サイドバーに表示

レイアウト(2段組み)

col1, col2 = st.beta_columns([1, 2])
with col1:
    st.header("A cat")
    st.image("https://static.streamlit.io/examples/cat.jpg", use_column_width=True)
with col2:
    st.header("A dog")
    st.image("https://static.streamlit.io/examples/dog.jpg", use_column_width=True)

キャッシュによる高速化

Streamlitはコードを上から順に実行するだけなので,簡単に書ける.

高速化のために関数デコレータを用いたキャッシュを使う.

キャッシュは引数に対する返値を辞書に覚えておいて,2回目以降に呼ばれたときには,それを返すだけである.

import time

@st.cache
def f(input):
    time.sleep(3)
    return input*10

st.write(f(10))  #3秒かかる
st.write(f(100)) #これも3秒かかる
st.write(f(10))  #キャッシュされているので一瞬で終わる

allow_output_mutation

同じ引数に対して返値が異なるような関数は,エラーとなる. 同じ引数に対しては,キャッシュを返すようにするには, 引数 allow_output_mutationをTrueにする.

引数のハッシュ値をhash_func引数で指定することができる.

import random
@st.cache(allow_output_mutation=True) # 返値が変わる場合は,この引数をTrueにする
def g(input):
    time.sleep(3)
    return input*random.random()

st.write(g(10))  #3秒かかる
st.write(g(100)) #これも3秒かかる
st.write(g(10))  #キャッシュされているので一瞬で終わる(ただし最初と同じ乱数を返す)

#データフレームのハッシュ値をidとする.
@st.cache(hash_funcs={pd.DataFrame: id})
def h(data):
    time.sleep(3)
    return data.values
df = px.data.iris()

セッション状態を用いた変数の保管

session_state名前空間に変数を記憶できる.

ウィジットのkeyで与えた名前(必ずユニークにする;上の例では省略したができれば全部つける)は,session_stateに保管されていて,ウィジットの返値が保管されている.

if 'count' not in st.session_state:
    st.session_state.count = 0

increment = st.button('Increment')
if increment:
    st.session_state.count += 1

st.write('Count = ', st.session_state.count)
if "celsius" not in st.session_state:
    st.session_state.celsius = 50.0

st.slider(
    "Temperature in Celsius",
    min_value=-100.0,
    max_value=100.0,
    key="celsius"
)

st.write(st.session_state.celsius)

Form入力

with st.form(key="basic_form"):
    n_job = st.number_input("最大ジョブ数", min_value=1, max_value=10000, value=100)
    n_shipment = st.number_input("最大輸送数", min_value=1, max_value=10000, value=100)
    submit = st.form_submit_button(label="データ更新")

if submit:
    st.write(n_job, n_shipment)

問題 アプリの作成

streamlitを使って何か役に立つWebアプリを作成せよ.