ここでは,実際問題を解決するときに便利なPythonの比較的進んだ文法やパッケージを紹介する.
- ジェネレータ
- simpyによるシミュレーション
- 型ヒント
- dataclasses
- pydanticパッケージによる型の厳密化
- 既定値をもつ辞書 defaultdict
- map関数
- 正規表現
- JSON
- Requestsパッケージ
- OpenPyXLによるExcel連携
- StreamlitによるWebアプリ作成
def prime():
yield 2
yield 3
yield 5
yield 7
for i in prime(): # [2,3,5,7]
print(i)
関数の中でforやwhileを用いた反復文を使い,生成する数値をたくさん(無限個)生成しておいてもメモリに負担をかけることはない. 使った分だけ順に生成されるからだ. 例として,2のべき乗を無限に生成するジェネレータとその使用法を示す.
def expon():
i = 1
while True:
i *= 2
yield i
for i in expon():
print(i)
if i >= 100:
break
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) # 制限時間分だけシミュレーションを行う
クラスを使った在庫シミュレーション
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)
def f(x: int) -> int:
return x * 2
f(12)
ただし,型ヒントは,プログラム自体には何の影響も与えないので,引数に文字列を入れてもエラーしない.
f("hello")
def gcd(m, n):
if n == 0:
return m
return gcd(n, m % n)
m = 100
n = 8
print(gcd(m, n))
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("たけやぶやけてない"))
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)
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)
引数の型が異なる場合には, 指定された型に合うように変換される.
c2 = Customer(name=123, height=180.0, weight=60, friends=["Kitty"])
print(c2)
変換できない場合には ValidationError が発生する. このエラーを try ... except ... でキャッチすることにより,より詳細なエラー情報を得ることができる.
try:
c2 = Customer(height="Heigh", weight=60, friends="Kitty")
except ValidationError as e:
print(e)
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)
collectionsパッケージにdefaultdict関数がある.これは,既定値を指定した辞書を生成する.
引数は関数であり,その返値が既定値になる. たとえばintとすると int() は $0$ を返すので,既定値は $0$ になり, listとすると空のリストになる.
from collections import defaultdict
D = defaultdict(int)
D["zero"]
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)
numbers = "12 56 46".split()
print(numbers)
m = map(int, numbers)
print(m)
print(list(m))
for i in map(int, numbers):
print(i)
例として "'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)
問題
OR Lib. (http://people.brunel.ac.uk/~mastjjb/jeb/info.html) にある問題例を1つ選択し,defaultdict(複数必要な問題例もある)にデータを格納せよ.
どのようなデータ構造を使えば,問題を解きやすいか考えて,設計せよ.
正規表現
正規表現 (regular expression, regex) はPythonに限らず,様々なプログラミング言語で使用される便利な仕組みである.
正規表現自体は先進的ではないが, 分かりにくい学習サイトが多いので,簡単に紹介しておく.
JupyterLabの検索 (EditメニューもしくはCommand Fで起動)やほとんどのモダンなエディタ(たとえばVisual Studio Code)でも, 正規表現が使える. 検索窓の .* を押すと,正規表現になる.
以下のサイトで様々な正規表現を試すことができる.
正規表現の記法は膨大であるので,ここでは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")
上の例の[]はメタ文字と呼ばれ,他にも(ほんの一部であるが) 以下のようなものがある.
メタ文字 | 意味 |
---|---|
. | 1文字 |
^ | 次の文字から始まる |
$ | 前の文字で終わる |
* | 前の文字の0回以上の繰り返し |
+ | 前の文字の1回以上の繰り返し |
? | 前の文字が0回か1回 |
[^A-Z] | A-Z以外 |
( ) | グループ化 |
| | または |
メタ文字を使いたい場合には,\(バックスラッシュ)を前に書く必要がある. たとえば, . を探したい場合には, \. と書く.
他にも,以下のような表現が準備されている.
文字 | 意味 |
---|---|
\t | タブ |
\n | 改行 |
\d | すべての数字 |
\s | 空白文字列 |
\w | アルファベット,数字, アンダーバー _ |
例として, 空白文字列 (\s) で"Kitty White"を分割してみる.
re.split(r"\s", "Kitty White")
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
読み込んだdataは辞書であり, "customers"がキー, 顧客のリストが値になっている. リストを反復して名前を表示し, 電話番号 phoneを辞書から削除する.
for i in data["customers"]:
print(i["name"])
del i["phone"]
data
json.dumps関数で,Pythonの辞書をJSON形式のテキストに変換できる. 上で電話番号を除いた辞書をJSONに変えてみる.
引数のindentを設定すると読みやすくなる.
json_text = json.dumps(data, indent=2)
print(json_text)
上で作った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
住所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
txt = df.to_json()
print(txt)
逆に,JSONのテキストをデータフレームに変換するには, read_json関数を用いる.
pd.read_json(txt)
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
urlにパラメータを埋め込むのではなく,引数paramsで辞書を渡すことができる.
例として,以下のサイト(A simple HTTP Request & Response Service)でテストをしてみる.
d = {"page": 1, "count": 1}
response = requests.get("http://httpbin.org/get", params=d)
ret = response.json()
print(response.url)
print(ret["args"])
情報を得るためのgetメソッドだけでなく,postメソッドもできる.
ユーザー名とパスワードをpostすることによって,loginができるようになる.
d = {"username": "mikio", "password": "test"}
response = requests.post("http://httpbin.org/post", data=d)
response.json()["form"]
getメソッドでloginしてみる.
レスポンスのコード(200)はOKを意味する.
response = requests.get(
"http://httpbin.org/basic-auth/mikio/test", auth=("mikio", "test")
)
print(response)
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) # シート名の確認
wb = openpyxl.load_workbook("sample0.xlsx")
print(wb.sheetnames) # シート名の確認
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)
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()
import pandas as pd
df = pd.DataFrame(list(ws1.values), columns=["1列目", "2列目", "3列目"])
df
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()
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アプリをデプロイできる.
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")) #サイドバーに表示
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)
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)) #キャッシュされているので一瞬で終わる
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()
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)
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)