20210827のPythonに関する記事は30件です。

Algorithm | 二分探索をPython3で解説(例題あり)

二分探索とは  二分探索(バイナリサーチ)とは、かんたんにいうと、数字がソートされたリストのなかから、求めたい数を効率的に求めるための手法である。  例えば、次のようなリストがあったとしよう。 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]  このとき、このリストから7を求めたいとする。  通常は、0から前から順番に見ていって探し出す、という手法をとる。このとき、0, 1, 2,...とみてくので、探すまでに8回の操作が必要である。    しかし、これを二分探索をもちいることでより効率的に値を探し出すことができる。  二分探索は、まず真ん中の値から探索を開始する。このリストであれば、リストの最小値である0と最大値である9を足して2で割った数を真ん中の値midとする。つまり、4の値が取れる。ここで、求めたい値である7がこの値よりも大きいかどうかを判定する。大きければleftに値を入れ、小さければrightに値を代入する。この操作で1回分である。このときは、7は4よりも大きいので、leftに4を代入する。  これをleftがrightの値を超える(そもそもリストに値が存在しない)、もしくは、midの値が見つかるまで、続ける。  続きを見ていこう。今、left == 4, right == 9となっているので、これらを足して2で割った値、つまり、mid == 6となる。このとき、7は6よりも大きいので、left = 6とする。これで2回目の操作である。  left == 6, right == 9なので、mid == 7を追加する。このときmid == 7で値が見つかったので、この値を返す。  このようにすると、はじめの操作では8回かかっていたのに比べて、3回の操作で目的の値を探し出すことができる。たった5回かと思うが、リストが長くなるほど、効果を発揮する。通常の全探索では、計算量$O(N)$かかるが、二分探索は$O(logN)$で目的の値を探し出すことができる。 どうやって実装するか  二分探索には、大きく分けて3つの手法があると考えている。  ライブラリを使う、めぐる式二分探索を使う、自分で実装するの3つである。 手法1: ライブラリ(import bisect)  Python3には、二分探索をするために便利なライブラリが存在する。これがbisectだ。  これを用いることで、コードを書かなくてもかんたんな二分探索なら実装できる。  ライブラリのbisectは、大きく分けて2つの操作ができる。bisect(_left, _right)とinsortである。 bisect_right, bisect_left  まず、bisect_right, bisect_leftについて説明する。これは、bisect_right(リスト, 目的の数)と書き、リストから目的の値が入るインデックスを返してくれるというものである。bisect_rightとbisect_leftの2つあるのは、重複した値に対する操作である。以下のようなコードでは、同じ値がある場合、それぞれの端の値を返すようになっている。 from bisect import bisect_right, bisect_left numbers = [0, 1, 2, 3, 4, 5, 6, 7, 7, 8, 9] print(bisect_left(numbers, 7)) print(bisect_right(numbers, 7)) # output >> 7 >> 9 insort  次に、insortについて説明する。これは、insort(リスト、挿入したい値)と書き、リストの順序を崩さずに、値を挿入することができる。例えば、次のようにだ。 from bisect import insort numbers = [0, 1, 3, 4, 5, 6, 8, 9] insort(numbers, 2) print(numbers) insort(numbers, 7) print(numbers) # output >> [0, 1, 2, 3, 4, 5, 6, 8, 9] >> [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] その他メソッドはこちらの公式ドキュメントに記載されている。 手法2: めぐる式二分探索 めぐる式二分探索とは、次のようなコードの二分探索のことをいう。 def is_ok(arg): # 条件を満たすかどうか?問題ごとに定義 pass def meguru_bisect(ng, ok): ''' 初期値のng,okを受け取り,is_okを満たす最小(最大)のokを返す まずis_okを定義すべし ng ok は とり得る最小の値-1 とり得る最大の値+1 最大最小が逆の場合はよしなにひっくり返す ''' while (abs(ok - ng) > 1): mid = (ok + ng) // 2 if is_ok(mid): ok = mid else: ng = mid return ok  これに、問題文に応じて条件を設定してあげると、ACすることができる。  めぐる式二分探索のメリットは以下の通りである。 定式化されている 実行してもバグりにくい リストではなくても、二分探索できる 二分探索ということが見抜ければ、あとは条件を考えてあげるだけで良い 手法3: 自分で実装  3つ目は、自分で実装する方法である。参考までに、一般的な二分探索のコードを以下に書き記しておく。 whileで実装 def binary_search(numbers, value): left, right = 0, len(numbers) - 1 while left <= right: mid = (left + right) // 2 if numbers[mid] == value: return mid elif numbers[mid] < value: left = mid + 1 else: right = mid - 1 return -1 recursible(再帰関数)で実装 def binary_search(numbers, value): def _binary_search(numbers, value, left, right): if left > right: return -1 mid = (left + right) // 2 if numbers[mid] == value: return mid elif numbers[mid] < value: return _binary_search(numbers, value, mid + 1, right) else: return _binary_search(numbers, value, left, mid - 1) return _binary_search(numbers, value, 0, len(numbers) - 1) 二分探索を見分けるポイント  二分探索の手法について述べてきたが、これをどうやって見分ければよいのか。大きく分けて以下の2つがあると考えている。 制約が異常に大きい ex) $1 \leq A \leq 10^9$, $1 \leq X \leq 10^{18}$ 「次の成約を満たす、最大or最小の数を求めてください」  問題文にこれらの文章が入っていれば、二分探索である可能性が高い。 二分探索の解き方 数直線を考えて、境界線がどこかを調べる 条件式を立式する 何を二分探索で求めたいのかを明確にする  この3つを考えることができれば、二分探索で問題を解くことができる。  次からは、各手法の例題をみていこう。answerの下に回答があるので、見たくない方は途中で止めていただけると幸いだ。 0- 二分探索の基本問題 例題0: AOJ ALDS1 Search II answer def binary_search(numbers, value): left, right = 0, len(numbers) - 1 while left <= right: mid = (left + right) // 2 if numbers[mid] == value: return mid elif numbers[mid] < value: left = mid + 1 else: right = mid - 1 return -1 n = int(input()) slist = list(map(int, input().split())) q = int(input()) tlist = list(map(int, input().split())) cnt = 0 for t in tlist: if binary_search(slist, t) != -1: cnt += 1 print(cnt) 1- import bisectを用いた例題 例題1-1: ABC030 C - 飛行機乗り answer def main(): from bisect import bisect_left n, m = map(int, input().split()) x, y = map(int, input().split()) alist = list(map(int, input().split())) blist = list(map(int, input().split())) now_idx = 0 cnt = 0 while True: now = alist[now_idx] + x now_idx = bisect_left(blist, now) # blistの時刻からnowを超えないblistのインデックス番号を返す if now_idx == len(blist): print(cnt) break now = blist[now_idx] + y now_idx = bisect_left(alist, now) # alistの時刻からnowを超えないalistのインデックス番号を返す if now_idx == len(alist): cnt += 1 print(cnt) break cnt += 1 if __name__ == '__main__': main() 例題1-2: ABC061 C - Big Array answer def main(): from itertools import accumulate import bisect n, k = map(int, input().split()) numbers = [0]*(10**5+1) for i in range(n): a, b = map(int, input().split()) numbers[a] += b # 各数字が何個追加されるかを数える sum_numbers = list(accumulate(numbers)) # 累積和 ans = bisect.bisect_left(sum_numbers, k) # k以上となる累積和を探す print(ans) if __name__ == '__main__': main() 例題1-3: ABC077 C - Snuke Festival answer def main(): import bisect n = int(input()) alist = list(map(int, input().split())) blist = list(map(int, input().split())) clist = list(map(int, input().split())) sort_alist = sorted(alist) sort_clist = sorted(clist) cnt = 0 for b in blist: low = bisect.bisect_left(sort_alist, b) # 昇順のalistからbがはいるインデックスを返す high = bisect.bisect_right(sort_clist, b)# 昇順のclistからbがはいるインデックスを返す cnt += low *(n - high) # bよりも下、bよりも上の数を数えてカウント print(cnt) if __name__ == '__main__': main() 例題1-4: ABC113 C - ID answer def main(): from bisect import bisect_left n, m = map(int, input().split()) city = [[] for _ in range(n+1)] plist = [] ylist = [] for i in range(m): p, y = map(int, input().split()) plist.append(p) ylist.append(y) city[p].append(y) for i in range(n+1): city[i].sort() for i in range(m): print(str(plist[i]).zfill(6) + (str(bisect_left(city[plist[i]], ylist[i])+1)).zfill(6)) if __name__ == '__main__': main() 例題1-5: ABC172 C - Tsundoku answer def main(): from itertools import accumulate import bisect n, m, k = map(int, input().split()) alist = [0]+list(map(int, input().split())) blist = list(map(int, input().split())) acc_alist = list(accumulate(alist)) acc_blist = list(accumulate(blist)) ans = 0 # NlogN for i in range(n+1): # Aの累積和を順番に a = acc_alist[i] check = k - a # Bの本を読む時間 if check < 0: # 読む時間がなければループを抜ける break ans = max(bisect.bisect_right(acc_blist, check)+i, ans) # Bの本を読む時間がBの本を読む時間にかかる累積和のリストのどこにあてはまるかを二分探索し、インデックス番号を返す print(ans) if __name__ == '__main__': main() 例題1-6: ABC119 D - Lazy Faith answer def main(): import bisect a, b, q = map(int, input().split()) INF = 10**18 slist = [-INF] + list(int(input()) for _ in range(a)) + [INF] tlist = [-INF] + list(int(input()) for _ in range(b)) + [INF] for i in range(q): x = int(input()) b, d = bisect.bisect_right(slist, x), bisect.bisect_right(tlist, x) res = INF for s in [slist[b-1], slist[b]]: # xのslist左右 for t in [tlist[d-1], tlist[d]]: # xのtlist左右 d1, d2 = abs(s-x) + abs(t-s), abs(t-x) + abs(s-t) # sが近いとき, tが近いとき res = min(res, d1, d2) # 最小の移動距離を保存 print(res) if __name__ == '__main__': main() 例題1-7: ABC143 D - Triangles answer def main(): from bisect import bisect_right n = int(input()) llist = list(map(int, input().split())) sort_llist = sorted(llist) ans = 0 for i in range(2, n): # 一番長い辺 for j in range(1, i): # 2番目に長い辺 ans += max(0, j - bisect_right(sort_llist, sort_llist[i]-sort_llist[j])) # 一番短い辺を二分探索する print(ans) if __name__ == '__main__': main() 例題1-8: ARC037 C - 億マス計算 answer def main(): from bisect import bisect_right def is_ok(x): cnt = 0 for a in alist: a = x // a # a_i * b_j <= Xの判定 cnt += bisect_right(blist, a) return cnt >= k # k番目以上なら def meguru_bisect(ng, ok): # めぐる式二分探索 while (abs(ok - ng) > 1): mid = (ok + ng) // 2 if is_ok(mid): ok = mid else: ng = mid return ok n, k = map(int, input().split()) alist = list(map(int, input().split())) blist = list(map(int, input().split())) alist.sort() blist.sort() print(meguru_bisect(-1, 10 ** 18 + 1)) if __name__ == '__main__': main() 2- めぐる式二分探索を用いた例題 例題2-1: ABC023 D - 射撃王 answer def main(): def is_ok(x): limit = [] for i in range(n): limit.append((x-hlist[i])//slist[i]) # x-hlist[i]は現在の高さ、slist[i]でわると、いつまでに割らないといけないかがわかる limit.sort() for i in range(n): # limitのi番目の時刻が時刻i以内になっているか if limit[i] < i: # 割らないといけない時刻までに、iが来てしまったらアウト return False return True def meguru_bisect(ng, ok): while abs(ok - ng) > 1: # okとngの時刻さが1または0になったらループを抜ける mid = (ok + ng) // 2 if is_ok(mid): ok = mid # okの時刻より下 else: ng = mid # ngの時刻より上 return ok n = int(input()) hlist = [] slist = [] for i in range(n): h, s = map(int, input().split()) hlist.append(h) slist.append(s) print(meguru_bisect(0, int(1e+14))) # H,Sの最大値1,000,000,000に注意 if __name__ == '__main__': main() 例題2-2: ABC063 D - Widespread answer def main(): def is_ok(x): c = 0 for i in range(n): monster = monsters[i] - x * b # monsters全体にbがx回分のダメージ if monster <= 0: # monsterが倒せているかどうか continue c += monster // a_b # 残りHPに対してa-bのダメージを if monster % a_b: # a-bのダメージでHPがまだ残っていたらもう1回分 c += 1 return c <= x # a-bの攻撃回数がbの攻撃回数以下であればTrue def meguru_bisect(ng, ok): # 二分探索 while (abs(ok - ng) > 1): mid = (ok + ng) // 2 if is_ok(mid): ok = mid else: ng = mid return ok n, a, b = map(int, input().split()) a_b = a - b monsters = list(int(input()) for _ in range(n)) ng, ok = 0, 1010101010 print(meguru_bisect(ng, ok)) if __name__ == '__main__': main() 例題2-3: ARC050 B - 花束 answer def main(): def is_ok(k): if r-k < 0 or b-k < 0: # 各色の花は必ず一本は必要なので、それがどこまで絞れるか確認する return False return ((r-k)//(x-1))+((b-k)//(y-1)) >= k # 各色に一本ずつ使えることが確認できたら、余っている花に対して、花束の数を数える def meguru_bisect(ng, ok): while (abs(ok - ng) > 1): mid = (ok + ng) // 2 if is_ok(mid): ok = mid else: ng = mid return ok r, b = map(int, input().split()) x, y = map(int, input().split()) print(meguru_bisect(10**18+1, 0)) if __name__ == '__main__': main() 例題2-4: ABC174 E - logs answer def main(): # 最大値の最小化 def is_ok(x): now = 0 for i in range(n): now += (alist[i]-1)//x # それぞれの丸太を何回切ればよいかをカウント return now <= k # 切った回数がk以下であれば def meguru_bisect(ng, ok): while (abs(ok - ng) > 1): mid = (ok + ng) // 2 if is_ok(mid): ok = mid else: ng = mid return ok n, k = map(int, input().split()) alist = list(map(int, input().split())) ng, ok = 0, 10**9+1 print(meguru_bisect(ng, ok)) if __name__ == '__main__': main() 3- 自分で実装する例題 例題3-1: ABC146 C - Buy an Integer answer def main(): a, b, x = map(int, input().split()) left, right = 0, 10**9+1 def d(n): # 桁数確認 return len(list(str(n))) while left + 1 != right: # 二分探索 mid = (left+right)//2 result = a * mid + b * d(mid) if result <= x: left = mid else: right = mid return left if __name__ == '__main__': print(main()) 例題3-2: ARC054 B - ムーアの法則 answer def main(): import math P = float(input()) def f(x): # f(x) return x + P / (2 ** (x / 1.5)) def fd(x): # f(x)の微分 return 1 + P * math.log(2**(-1/1.5)) * (2**(-x/1.5)) left, right = 0, P cnt = 10**5 # lim(n->cnt)に近づける while cnt > 0: # 二分探索 cnt -= 1 mid = (left + right) / 2 if fd(mid) == 0: break elif fd(mid) < 0: left = mid else: right = mid print(f(mid)) if __name__ == '__main__': main() まとめ  これだけの問題をやっていたら、競技プログラミングの基本的な二分探索の問題に対応できるだろう。もし、忘れてしまったら、また、この記事に立ち返って復習をしていただけると幸いだ。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

シリアルポートからくるデータをリアルタイムにプロットする

作ったもの Pythonの標準機能とMatplotlibとPySerialなどよく使われるライブラリだけで、シリアルポートからくるデータをリアルタイムにプロットしてみました。 コードはGitHubに。 なぜ作ったか Arduinoなどのマイコンにセンサを繋ぎ、測定値をシリアルでパソコンに送り込むことがよくあります。Arduino IDEにはSerialPlotterの機能があるので、リアルタイムに測定値を眺めることがすぐにできてとても便利です。 ところが表示のレンジが勝手に変わってしまうのを止めたかったり、何かしらの計算をしながら表示したかったり。自分でちょっとしたカスタマイズをしてリアルタイムにグラフを眺めたいことがあります。 Instructablesなどをみていると、Processingを利用して自作アプリを作る例を見かけます。例えばオープンソースの脈波計 Pulse Sensorのアプリ できれば普段使い慣れたPythonでやりたい。しかもなるべく特殊なライブラリは入れずに。 方針 なるべく特別なライブラリを使わずにやりたい。とりあえずシンプルな機能だけにして、ボイラープレートとして使えるコードにする。 シリアルポートを扱うのはPySerial、グラフ表示は定番のmatplotlibを使う。 ぱっとできそうですが、いくつか工夫が必要です。 * 絶え間なくシリアルからくるデータを受け取りつつGUIを止めないために、標準のthreadingモジュールを使って別スレッドでデータを受け取る * Matplotlibはもともと静的グラフを描画するライブラリなので、それなりの速度でリアルタイムにアップデートし続けるにはanimation機能を使う * 別スレッドで受け取ったデータをanimationのスレッドで安全に使うための処理 * 一定数の最新のデータを保持するためのデータ構造をどうするか 私自身ソフトのプロではなく、いろいろ調べながらやっています。間違ったり余計なことをやっていることもあると思うので、アドバイスお願いします。 コード コード全体はGitHub上で確認してください。 シリアルからデータを受けるワーカー class DataWorker(threading.Thread): """ Worker to get date from serial, parse data and put into numpy array """ def __init__(self, ser, data): """ ser: PySerial object data: Numpy array to hold data """ threading.Thread.__init__(self) self._data = data self._ser = ser self._running = False self._lock = threading.Lock() def stop(self): self._running = False def run(self): """ Worker loop """ logging.debug("Start running worker") self._running = True line = "" while self._running: try: if self._ser.is_open: line = self._ser.readline() except Exception as e: logging.error("Serial port Exception: "+ str(e)) if line: line2 = line.decode("utf-8").strip() logging.info(line2) if line2 == '': #logging.info("Emtpy line") pass elif line2.startswith('#'): logging.info("Found comment line") else: self.parseLine(line2) logging.debug("Finished running worker") 別スレッドで処理するためにthreading.Threadを継承したクラスを作ります。 runメソッドにループでシリアルポートから1行ずつ読み込んで処理。 本当はシリアルポートのバッファ見ながら自分で改行コードで区切ってとかやるのでしょうが、PySerialのreadline()でラクしてます。 self._running変数を介してループすることで、外部からstop()できるようにするのが常套手段(?) データはnumpy.ndarrayに格納して受け渡す def parseLine(self, line): """ Implement your line parsing code here This example expects all comma separated values are float """ try: values = [float(_) for _ in line.split(',')] # make sure the number of values are same as expected if len(values) == self._data.shape[0]: with self._lock: self._data = np.roll(self._data, -1) # move all data to left by 1 self._data[:,-1] = values # replace last data with new values else: logging.error("Wrong number of values") logging.error(line) except Exception as e: logging.error("error parsing: "+str(e)) logging.error(line) @property def snapshot(self): """ make a copy of current data and return """ with self._lock: data = self._data return data データのラインが来たらパースしてnumpy.ndarrayに格納します。 とりあえずコンマ区切りですべてfloatだとして処理してますが、必要に応じて変更してください。 プロットするために、決まった個数の最新データだけを変数に入れます。リングバッファとしてPython標準ではcollections.dequeが使えるようです。でもnumpy.roll()でスライドさせていくのも悪くないようなのでnumpy.ndarrayを使うことにします。ndarrayの方が2次元配列で一気に処理できるし、maptplotlibに渡すと時もラクそうなので。 スレッド間でデータを受け渡しすることになるので、アクセスするときにはthreading.Lock()を使い保護する。 animation用のクラスを定義 class Plotter(): """ Class to hold plot figure provides functions for animation """ def __init__(self, fig, ax, x_data, data_worker): """ fig: Figure ax: Axes x_data: array of x values data_worker: instance of DataWorker that provide data """ self._x_data = x_data self.data_worker = data_worker self.fig = fig self.ax = ax def initial_plot(self): """ Adjust plot command Make sure to return lines to update """ # obtain latest data from worker data = self.data_worker.snapshot # draw a line with 1st row data self.lineRed, = self.ax.plot(self._x_data, data[0,:], 'r-') # adjust plot self.ax.set_xlim(0, 500) self.ax.set_ylim(100000,200000) return self.lineRed, def update_plot(self, frame): # obtain latest data from worker data = self.data_worker.snapshot # update line's ydata self.lineRed.set_ydata(data[0,:]) return self.lineRed, matplotlibで動的なプロットをするにはanimationの仕組みを使います。FuncAnimationクラスには、最初のプロットをする関数と各フレームで描画する関数を渡すので、それを1つのクラスにまとめて定義しておきます。 animationのポイントは、アップデートしたいArtistを最初のプロットをするときに変数に入れておくこと。ここではinitial_plot()の中でself.ax.plot()で作られるLine2Dをself.lineRedに格納したうえでreturnします。 アニメーションの各フレームで呼ばれる update_plot()の中では、DataWorkerから最新データを受け取り、Line2D.set_ydataに最新データを渡すことで高速にアップデートします。 セットアップ if __name__ == "__main__": fmt = "%(message)s" logging.basicConfig(format=fmt, level=logging.INFO) port = "/dev/tty.usbmodem14201" baudrate = 115200 ser = serial.Serial(port, baudrate, timeout=1) # we expect two values from serial port # ValueA,ValueB n_values = 2 # expecting number of values in a line n_points = 500 # number of latest data points to hold # make numpy array to hold data arr = np.empty(n_values * n_points) arr[:] = np.nan # fill with NaN as default data = np.reshape(arr, (n_values, n_points)) arr_x = np.arange(n_points) # make array for x axis fig = plt.figure() ax = fig.add_subplot(111) dataworker = DataWorker(ser, data) plotter = Plotter(fig, ax, arr_x, dataworker) dataworker.start() ani = FuncAnimation(fig, plotter.update_plot, frames=None, init_func=plotter.initial_plot, blit=True, interval=50, ) plt.show() dataworker.stop() シリアルポートやボーレートを決めてPySerial.Serialオブジェクトをインスタンス化。 1行に何個の値が来るかと、何データ分プロットするかを決めたらndarrayを作ります。 とりあえずnp.nanにしておけばグラフの初期化がラク。 FuncAnimationにもろもろ渡してあげれば出来上がりです。 今後 ボイラープレートなのであまり複雑にする気はないですが、パーサーは外から与えるようにするなどDataWorkerクラスは変更せずに使い回せるようにしたいところ。受け取ったデータをファイルに保存するコードも入れていきたい。loggingの機能を使えばすぐできるはず。 あとはちゃんとしたGUIアプリにしようとすると、QtなりのGUIライブラリを使いたくなる。QtとMatplotlibの組み合わせも癖があるので、Qt版のボイラープレートも作っておきたいところ。Qt6+PySide6に対応したmatplotlib v3.5が出たらやってみたい。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

matplotlibで生成したグラフを直接エクセルに出力する方法

概要 matplotlibで生成したグラフをエクセルに出力する時、自分のローカルにグラフ画像を保存せずにそのままエクセルに出力したい事があると思います。 今回は「既存のエクセルファイルに新しいタブを追加し、そこにグラフを出力する」という場面を想定し、上記を実現するコードを書いていこうと思います。 サンプルコード import io import openpyxl import matplotlib.pyplot as plt fig = plt.figure() # ~~~~~~ グラフを作っていく処理は省略 ~~~~~~~~ # 既存のエクセルファイルを開き、「chart」という名前のタブを追加する file = 'path/to/excel.xlsx' workbook = openpyxl.load_workbook(file) new_sheet = workbook.create_sheet('chart') # PCのメモリに画像を保存する img_data = io.BytesIO() fig.savefig(img_data, format='png') # 表示するための画像を生成し、エクセルに出力する img = openpyxl.drawing.image.Image(img_data) new_sheet.add_image(img, 'G1') # 変更を保存 workbook.save(file) まとめ これで作成したグラフがエクセルの新しいタブに表示されるはずです。 ローカルに不必要なデータが生成されなくていいですね。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Python - FizzBuzz

def fb(n): result = '' if n % 3 == 0: result = 'Fizz' if n % 5 == 0: result += 'Buzz' if result == '': result = n return result i = 1 while i < 100: print(fb(i)) i = i + 1
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

DjangoをDEBUG=FalseでHerokuにデプロイするとServer Error(500)が出てきて悩んでいる人向けの小ネタ

DjangoがHerokuで動かない HerokuにDjangoをDEGUB=Falseでデプロイが成功しても、接続するとServer Error(500)が表示されて悲しい気持ちになってしまい、いやいやながら不安な心を抱いてDEBUG=Trueにしている人(初心者)のための情報です。 私のような英語がよく理解できず、かつ翻訳内容すら正しく読みとる読解力が足りない人間がDjango公式などの文書を読むと、とかく勘違いしたり理解しないままやっちまうのでServer Error(500)を出しがちです。 そんな私がなんとか動かすことに成功した環境設定について、その注意点まとめてみた。 Herokuへのデプロイはコマンドを叩くんじゃなくて、Githubからデプロイする機能を使ってで行ってます。こっちのほうがボタン一発だから超カンタン。なんならGithubのソースが更新されたら、自動的にデプロイしてくれる(やったことないけど)ので超ベンリ(のはず) 静的ファイルの配信のやり方(whitenoiseを使いかた) まずは公式文書を読んでみよう。 Heroku公式(Djangoと静的アセット) https://devcenter.heroku.com/ja/articles/django-assets WhiteNoise公式 (Using WhiteNoise with Django) https://whitenoise.evans.io/en/stable/django.html setting.pyの設定 whitenoise公式に書いてあること 'whitenoise.middleware.WhiteNoiseMiddleware'は必ず'django.middleware.security.SecurityMiddleware'の直下に書け Heroku公式をみると単にMIDDLEWAREの最初に書け的な事が書いてあるけど、本家Whitenoise公式では 「SecurityMiddleware以外の他のどのミドルウエアよりも上に書け」とあります。 しかし試しに一番下に書いてもServer Error(500)にはなりませんでした。 とはいうもののHeroku公式にも「最上段にかけ」と書いていますし、どこでどんな影響が出かがわからないので、言われて通りに設定するのがいいと思います。 こんなかんじですね MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', <= ここにSecurityMiddlewareがある 'whitenoise.middleware.WhiteNoiseMiddleware', # ... <= その他もろもろの定義が続く ] ちなみにHeroku公式の説明では MIDDLEWARE_CLASSES = ( # Simplified static file serving. # https://warehouse.python.org/project/whitenoise/ 'whitenoise.middleware.WhiteNoiseMiddleware', ... とかいてあって、django.middleware.security.SecurityMiddlewareについては これは、settings.py の (先頭にある) ミドルウェアのセクションで実行されます。 というようなよくわからん書き方がされているうえに、 MIDDLEWARE_CLASSES = ( という新しい定数をMIDDLEWAREとは別に追加する必要があるのかと思ってしまった。(結果的にそうじゃなかった) staticファイル関連の設定 staticファイル関係の設定は以下の通り。(おそらくmediaも同じように定義するんじゃないかな(こちらは未確認)) STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') STATIC_URL = '/static/' そんなHeroku公式には同じ場所に STATICFILES_DIRS = ( os.path.join(BASE_DIR, 'static'), ) を追加しろと記述があるけれど、こいつを設定するとデプロイ(具体的にはmanage.py collectstaticが)失敗します。 削除しててもデプロイ成功するし静的ファイル配信も問題なしだったから、STATICFILES_DIRSは追加しなくてもオーケー。(これについては他の方も書かれていたのでおそらく正しい) ちなみにHerouはデプロイ時に manage.py collectstatic --noinputを自動的に実行してくれます。失敗するとデプロイにも失敗するのですぐに分かります。 で、結局どうするのか 一番の問題点はこれ(ではないか) STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage' Heroku公式の説明では「gzip機能(HTML要素を圧縮する機能らしい)を有効にしたい場合」に必要らしいので、そんな機能は使わないひとは設定してはいけません。(すると動きません) 公式の英語をよく理解しないままに書いてあるとおり何も考えず安易にSTATICFILE_STRAGEを追加してしまうとServer Error(500)になってしまいます。削除するとServer Error(500)は発生しないので根本原因はこいつではないかと思われます。 デプロイは成功するのにServer Error(500)になってしまって悩んでいる人は、とりあえずこいつを削除またはコメント化してみればいかがでしょうか。 上のスクリーンショットはクマ(別にサルでもイノシシでもUMAでもなんでもいいんです)を目撃した場所をGoogleMap上に記録することができるというやつです。アイコンに使った画像ファイルがちゃんと反映されているし地図表示のJavascriptは動いているので、staticフォルダが正しく認識し処理されているってことでしょう。 おなじみの管理サイトのデザインも正しく表示されてましたのでCSS参照も問題なし。 すでにテスト環境でSTATICFILES_STORAGEが設定されていて問題なく動いている場合は、whitenoise用への変更が必要みたいです。(gzip機能を使ってないので(設定がよくわからないので未確認) Heroku+Django DEBUG=FalseでServer Error(500)が発生する問題の解決のために様々な人の様々な試みを書いたブログを読んでると、templateなどでstaticファイル自体の定義が正しくなかった(存在しないファイルを指定していた、存在場所が正しく指定されていなかった)というような意見もあり(ならばServer Errorじゃなくて表示されないだけなじゃないか?)、上のやり方で治らなかったら頑張って原因を見つけてください。そしてその結果を公開してください。 結論としては 「Heoku公式はもっとわかりやすい日本語にしてくれたらいいな」と「自分の文章読解力をもっと疑え」  でした。 現場からは以上です。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Python】スクレイピングしたデータで、日本地図を爆速で塗り分けるためのTips

はじめに ふとしたきっかけで、サクッと日本地図を塗り分けたくなる場面がありますよね(多分) ライブラリが非常に豊富な Python を使って スクレイピング → 日本地図の塗り分け のステップを爆速にするためのコツを紹介します。 どんな雰囲気の可視化ができるのか、47 都道府県の最低賃金を例に取って、YouTube の動画にしたのでぜひご覧ください。 1. 可視化したいデータを収集する このステップの目標は、47 都道府県の数値データを格納したリストを作成することです。 ここでは Web スクレイピングの例を紹介しますが、その他の方法でも目標が達成できれば問題ありません。 (1) Web スクレイピング Web スクレイピングが初めての人は、 図解!Python BeautifulSoupの使い方を徹底解説!(select、find、find_all、インストール、スクレイピングなど) - AI-interのPython3入門 という非常に分かりやすい記事があるので参考にしてください。 注意点として、スクレイピングで抽出したままの状態では文字列型なので、必ず数値型に変換するようにしましょう。 以下のサンプルコードでは、厚生労働省のサイトから都道府県別の最低賃金を取得しています。 from bs4 import BeautifulSoup # pip install beautifulsoup4 import requests # スクレイピングしたい Web サイトの URL url = 'https://www.mhlw.go.jp/stf/seisakunitsuite/bunya/koyou_roudou/roudoukijun/minimumichiran/' # 指定した Web サイトの情報を取得し,変数に格納する r = requests.get(url) # BeautifulSoup() に Web サイトの情報と解析に使用するパーサーを渡す soup = BeautifulSoup(r.text, "html.parser") # HTML タグで該当する箇所を検索してリストに格納する minimums = [int(i.text.replace(',','')) for i in soup.find_all('td', attrs={'class':'xl65'})][:-1] # リストの長さが 47 であることを確認する print(len(minimums)) print(minimums) >>> [861, 793, 793, 825, 792, 793, 800, 851, 854, 837, 928, 925, 1013, 1012, 831, 849, 833, 830, 838, 849, 852, 885, 927, 874, 868, 909, 964, 900, 838, 831, 792, 792, 834, 871, 829, 796, 820, 793, 792, 842, 792, 793, 793, 792, 793, 793, 792] (2) その他の方法 Web スクレイピングの他にも、様々なデータ収集の方法が考えられます。 例えば、Python で Excel ファイルのデータを読み込みたいときには、 pandasでExcelファイル(xlsx, xls)の読み込み(read_excel) | note.nkmk.me が参考になります。 いずれの方法でも、47 都道府県の数値データを格納したリストが作成できれば問題ありません。 2. 白地図を塗り分ける ステップ1で作成した数値リストを用いて、真っ白な日本地図を塗り分けていきます。 (1) japanmap のインストール 日本地図を県別に色分けする Python のライブラリが提供されているのでインストールします。 pip install japanmap japanmap の活用例として、もっと色々なことにチャレンジしている 県別データの可視化 - Qiita という記事がとても分かりやすかったので、詳しく知りたい人は参考にしてください。 以下では、数値データで白地図を塗り分けることに焦点を当てたいと思います。 (2) ヒートマップの作成 原理としては、 カラーマップの選択(Choosing Colormaps — Matplotlib 2.0.2 documentation) 標準化関数の作成(matplotlib.colors.Normalize — Matplotlib 3.4.3 documentation) mappable オブジェクトの作成(Customized Colorbars Tutorial — Matplotlib 3.4.3 documentation) 数値データ → カラーコード への変換 都道府県名(or都道府県番号)と対応させたディクショナリの作成 japanmap による白地図の塗り分け(japanmap · PyPI) の6ステップから成り立っています。 手順は複雑に見えますが、よく分からなければコピペで OK です。 カラーマップはかなり好みが分かれる部分なので、私の好きな'plasma'以外も試してみてください。 import matplotlib.pyplot as plt # pip install matplotlib import numpy as np # pip install numpy from japanmap import picture # pip install japanmap # ステップ1で作成したリストを NumPy 配列に変換する minimum_wages = np.array(minimums) # カラーマップを選択する # 参考:https://matplotlib.org/2.0.2/users/colormaps.html cmap = plt.get_cmap('plasma') # 最小値を 0,最大値を 1 とする標準化関数を作成する norm = plt.Normalize(vmin=minimum_wages.min(), vmax=minimum_wages.max()) # 標準化関数とカラーマップを指定した "mappable" オブジェクトを作成する plt.colorbar(plt.cm.ScalarMappable(norm, cmap)) # 数値データをカラーコードに変換する colors = ['#' + bytes(cmap(norm(x), bytes=True)[:3]).hex() for x in minimum_wages] # 都道府県番号(1~47)をキー,カラーコードを値としたディクショナリを作成する datas = dict(zip([i for i in range(1,48)], colors)) # print(datas) # 作成したディクショナリをもとに,白地図を塗り分ける plt.imshow(picture(datas)) 最低賃金の差が一目で分かるように可視化できました。 低賃金の都道府県は東北地方、四国地方、九州地方に密集していることが分かります。 おわりに Python には便利なライブラリがたくさんあるので、データビジュアライゼーションに使える手段が増えると嬉しくなりますよね。 心が躍る可視化の方法をたくさん模索していきましょう! Enjoy Pythoning!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Pythonパッケージ中にsymlinkを含めたい

Pythonパッケージ中にsymlinkを含めたいことがある。 現行の状況では、 ファイルのsymlinkは実体がコピーされる ディレクトリのsymlinkはエラーを起こす(include_package_data)か無視される(package_data) という状況。 どうしてもsymlinkのまま扱いたいことがあると思います。 https://github.com/pypa/pip/issues/5856 で議論されているように見えますが結論が見えない。 https://github.com/cielavenir/distutils_symlink_enabler ライブラリを作ってしまいました。 bdistは非対応ですが、build_pyとsdistであればこれで対応できます。 (ただしWindowsでは確認してません)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ネイティブモジュールを含む Python プロジェクトの開発イテレーションを改善したい

setup.py でビルドする + C/C++ ネイティブモジュールな Python プロジェクトがある. 動かすと不都合があるので .py をいじって修正して再度実行 -> おかしい -> print デバッグ -> ... みたいなイテレーションをする. python ソースコードだけであれば, pip インストール先のファイルを直編集という手もあるが, ネイティブモジュール含んでいて, ネイティブモジュール部分を編集した場合ビルドしなおしが発生. 毎回 python setup.py install して動かして... とかだとめんどい. とりあえずの方法 python setup.py build でネイティブモジュールも含めビルド一式が build/lib.linux-x86-64-.../ とします. ここにそこにパスを通して動かす. # ビルドしなおす (cd <repo>; python setup.py build) export PYTHONPATH=<repo>/build/lib.linux-x86-64...:$PYTHONPATH python mytest.py .py など変えたら上記のようなスクリプトで毎回ビルドしなおしてから実行, みたいな感じになるでしょうか... TODO libtorch みたいにでかいプロジェクトだと再コンパイルも時間かかってめんどい. みんなどうしているのかしらね.
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

初心者による初心者のためのDiscord.py【2021】

前書き この初心者による初心者のためのDiscord.py入門では、 Discord.pyを習得したいけどよく分からない! という人のために、初心者が、初心者のための解説をしていきます。 書き方、使い方、構文などを記載していきます。 この記事を書こうと思った理由は、動くけど情報が古かったり、だいぶ昔に作成された記事がヒットしたりと、最近の記事が全くなかったので、先駆者様への恩返しなども込めた記事となっております。 ちなみに初投稿です。至らない部分もあると思いますが、問題点などあれば教えて下さると幸いです。 対象:初心者(パソコンのタイピングができて、pythonを触ったことがある程度の人) 基本構文 まず初めに、Discord.pyとは、Discord BotをPythonで動かすためのライブラリのことを指します。 まず、 ターミナル # Windows環境 py -3 -m pip install discord.py # その他 python3 -m pip install discord.py でdiscord.pyをpythonで使えるようにインストールします。 では、次は実際にコードの例を見ていきましょう。 main.py import discord #Discord.pyをimport client = discord.Client() #クライアントを定義。 token = "ここにdiscord tokenを貼り付ける" #処理 client.run(token) #botを走らせる と言った形です。これでとりあえずbotはオンラインになります。 では、次に実際に処理を書いていきましょう。 処理は、 main.py @client.event async def 使いたいもの: #処理1 という構文で書いていきます。 例として、次のものは、!helloと発言した際に、こんにちは。と返すコードです。 main.py import discord client = discord.Client() token = "" @client.event async def on_message(message): if message.content == "!hello": #もしメッセージがhelloだったら await message.channel.send("こんにちは。") #こんにちはを送信 では、詳しい解説をしていきます。 main.py async def on_message(message): このコードは、メッセージが送られてきた時に反応するという意味のコードです。 async defにはそれぞれ名前があるのですが、初心者がこれを最初から詰め込んでしまうと多分パンクしてしまうので、飛ばします。 main.py if message.content == "!hello": このコードは、message.content(送信されたメッセージの内容。)が、もしも!helloと等しかったら(==という演算子。!=などもある。)処理を実行するというif文です。 このif文というのはよく使うので、覚えておきましょう。 main.py await message.channel.send("こんにちは。") このコードは、message.channel.sendで、このメッセージが送信されたチャンネルに、こんにちはとメッセージを送信するコードです。 この message.channel.sendというのは、Discord.pyをする上ですごくよく使うコードなので、これも覚えておきましょう。 よくやってしまう間違い 次に、よくやってしまう間違いを見ていきましょう。 main.py @client.event async def on_message(message): #処理 @client.event async def on_message(message): #処理2 のように、同じイベントを2つ書いてしまう人がめちゃくちゃ多いのですが、やめましょう。 どちらかが動きませんし、もし動いたとしても可読性が悪く、コードのバグが見つけにくいです。 最後に ここまで読んでいただいてありがとうございます。 今回は構文を説明しただけですが、次回からはコードを動かすサーバーや、実用性のあるコードを記述していきます! 役に立った方は、LGTMやストックなどよろしくお願いします!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

初心者による初心者のためのDiscord.py

前書き この初心者による初心者のためのDiscord.pyでは、 Discord.pyを習得したいけどよく分からない! という人のために、初心者が、初心者のための解説をしていきます。 書き方、使い方、構文などを記載していきます。 この記事を書こうと思った理由は、動くけど情報が古かったり、だいぶ昔に作成された記事がヒットしたりと、最近の記事が少なかったので、先駆者様への恩返しなども込めた記事となっております。 ちなみに初投稿です。至らない部分もあると思いますが、問題点などあれば教えて下さると幸いです。 対象:初心者(パソコンのタイピングができて、pythonを触ったことがある程度の人) 基本構文 まず初めに、Discord.pyとは、Discord BotをPythonで動かすためのライブラリのことを指します。 まず、 ターミナル # Windows環境 py -3 -m pip install discord.py # その他 python3 -m pip install discord.py でdiscord.pyをpythonで使えるようにインストールします。 では、次は実際にコードの例を見ていきましょう。 main.py import discord #Discord.pyをimport client = discord.Client() #クライアントを定義。 token = "ここにdiscord tokenを貼り付ける" #処理 client.run(token) #botを走らせる と言った形です。これでとりあえずbotはオンラインになります。 では、次に実際に処理を書いていきましょう。 処理は、 main.py @client.event async def 使いたいもの: #処理1 という構文で書いていきます。 例として、次のものは、!helloと発言した際に、こんにちは。と返すコードです。 main.py import discord client = discord.Client() token = "" @client.event async def on_message(message): if message.content == "!hello": #もしメッセージがhelloだったら await message.channel.send("こんにちは。") #こんにちはを送信 では、詳しい解説をしていきます。 main.py async def on_message(message): このコードは、メッセージが送られてきた時に反応するという意味のコードです。 async defにはそれぞれ名前があるのですが、初心者がこれを最初から詰め込んでしまうと多分パンクしてしまうので、飛ばします。 main.py if message.content == "!hello": このコードは、message.content(送信されたメッセージの内容。)が、もしも!helloと等しかったら(==という演算子。!=などもある。)処理を実行するというif文です。 このif文というのはよく使うので、覚えておきましょう。 main.py await message.channel.send("こんにちは。") このコードは、message.channel.sendで、このメッセージが送信されたチャンネルに、こんにちはとメッセージを送信するコードです。 この message.channel.sendというのは、Discord.pyをする上ですごくよく使うコードなので、これも覚えておきましょう。 よくやってしまう間違い 次に、よくやってしまう間違いを見ていきましょう。 main.py @client.event async def on_message(message): #処理 @client.event async def on_message(message): #処理2 のように、同じイベントを2つ書いてしまう人がめちゃくちゃ多いのですが、やめましょう。 どちらかが動きませんし、もし動いたとしても可読性が悪く、コードのバグが見つけにくいです。 次のよくある間違いは、 main.py @client.event async def on_message(message): if message.content == "!hello": await channel.send("こんにちは。") はい。インデントエラーですね。 ここまで極端な例は無いと思いますが、もし動かなかったときのエラー文にindentという文字があったら、まっさきに確認しましょう。 次のよくある間違いは、 main.py @client.event async def on_ready(): channel = 定義 await message.channel.send("起動しました!") はい。この何がダメかと言うと、messageが定義されていないのに、message.channel.sendを使おうとしていますね。 これだとエラーを吐いてしまいます。 正しくは、channel.sendです。間違えないようにしましょう。 最後に ここまで読んでいただいてありがとうございます。 今回は構文を説明しただけですが、次回からはコードを動かすサーバーや、実用性のあるコードを記述していきます! 役に立った方は、LGTMやストックなどよろしくお願いします!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

tf.data、TFRecordsを使った画像読み込み (tensorflow 2.6.0~)

チュートリアルとかにある全てnumpy配列に格納するタイプではなく、大規模データなどで使われる、バッチ毎に読み込む方法。 あまり最近のが無かったので、Qiitaで記事にした。 tensorflowは2.6.0を前提。 import glob import math import os from typing import List, Tuple, Union import albumentations as albu # 必要に応じて import tensorflow as tf import numpy as np # 必要に応じて AUTOTUNE = tf.data.experimental.AUTOTUNE tf.data 画像の読み込み tf.dataを使う場合、まずは読み込む用の関数を用意する。 簡単に書けば、こういうものになる。 @tf.function(experimental_follow_type_hints=True) def read_and_preprocess(img_path: tf.Tensor) -> tf.Tensor: # tf.ioでファイルを読み込んでから画像にする _read = tf.io.read_file(img_path) img = tf.image.decode_image(_read, channels=3, expand_animations=False) return img 高速化したいので、tf.functionでラッパーする。 (tf.dataを使うからargは絶対Tensorになるんだけど)、experimental_follow_type_hints=Trueを入れてtype hintsにtf.Tensorを入れておくと、たとえpythonのstringが入ってきても自動的にTensor型に変換してくれるので、再トレーシングとかが気にしなくても良くなる。これは今回のようなtf.dataに限らず、いろんな場面で使えるので、覚えておいて損はない。 tf.decode_imageは自動的にファイル形式を識別して読み込んでくれるので、decode_jpegとかdecode_pngとかよりも使いやすい。expand_animations=Falseでgifとか入ってきてもちゃんと3次元で返ってくれるようになる。 前処理 やり方は主に2種類。KerasのPreprocessing Layerも入るかもしれない。 1. tensorflow公式が準備している前処理関数を使う(tf.image、tensorflow addonsのtfa.image ) 2. 他モジュールやImageDataGeneratorを、tf.py_function経由で前処理をする(※) バリエーションが多い順に並べると後者が強い。ただし、TPUやマルチGPU環境ではうまく動作しないらしい。 1の例で、ランダムクロップと正規化を反映させたい場合、こんな感じ。 def preprocess(img: tf.Tensor) -> tf.Tensor: crop_size = tf.constant((128,128, img.shape[-1])) cropped = tf.image.random_crop(img, crop_size) normalized = tf.cast(cropped, tf.float32) / 255. return normalized @tf.function(experimental_follow_type_hints=True) def read_and_preprocess(img_path: tf.Tensor) -> tf.Tensor: # tf.ioでファイルを読み込んでから画像にする _read = tf.io.read_file(img_path) img = tf.image.decode_image(_read, channels=3, expand_animations=False) return preprocess(img) 他にも、いろいろな前処理があるので、公式ドキュメントや画像の水増し方法をTensorFlowのコードから学ぶなどを参照。 2の場合、albumentationsを使ってみるとこんな感じ。transformsはだいぶ昔に使ってたものを引っ張ってきた。 # callの際に、適当な前処理をどれか1つ選んで行う。 transforms = albu.OneOf([ albu.ShiftScaleRotate(shift_limit=0.2, scale_limit=0.2, rotate_limit=90), albu.GaussNoise(), albu.Equalize(), albu.ElasticTransform(), albu.GaussianBlur(), albu.HueSaturationValue(hue_shift_limit=20, sat_shift_limit=30, val_shift_limit=20), albu.RandomBrightnessContrast(brightness_limit=0.5, contrast_limit=0.5), albu.HorizontalFlip()]) def preprocess(img) -> tf.Tensor: augmented = transforms(image=img.numpy())['image'] return tf.convert_to_tensor(augmented, tf.float32) @tf.function(experimental_follow_type_hints=True) def read_and_preprocess(img_path: tf.Tensor) -> tf.Tensor: # tf.ioでファイルを読み込んでから画像にする _read = tf.io.read_file(img_path) img = tf.image.decode_image(_read, channels=3, expand_animations=False) img = tf.py_function(preprocess, [img], [tf.float32]) return preprocess(img) Datasetへ ここは公式のチュートリアルの方が良いので、公式を見るべし。 自分の場合、こういう形をよく作っている。 def generate_dataset(image_list: List[str], batch_size: int = 32, name: str = 'train'): """ Parameters -------- image_list: list (str, ...) 読み込む画像のパスが含まれるリスト batch_size: int, default = 32 バッチサイズ name: str, default = `train` データセットの名前。trainだと、シャッフルされる。 """ ds = tf.data.Dataset.from_tensor_slices(image_list) ds = ds.map(read_and_preprocess, num_parallel_calls=AUTOTUNE) if name == 'train': ds = ds.shuffle(2048) ds = ds.batch(batch_size) # check size size = ds.cardinality().numpy() if size > 0: print(f'{name} steps per epoch: {size}') ds = ds.prefetch(AUTOTUNE) return ds 例: p = glob.glob('samples/*') # 適当なフォルダを指定 dataset = generate_dataset(image_list = p, batch_size = 2) # データセット生成 x = iter(dataset) print(next(x)) 出力: train steps per epoch: 15 <tf.Tensor: shape=(2, 128, 128, 3), dtype=float32, numpy= array([[[[0.9490196 , 0.93333334, 0.92941177], [0.9529412 , 0.9372549 , 0.93333334], [0.95686275, 0.9411765 , 0.9372549 ], ..., [0.9137255 , 0.91764706, 0.89411765], [0.90588236, 0.9098039 , 0.8862745 ], [0.9019608 , 0.90588236, 0.88235295]]]], dtype=float32)> これでデータセットができた。あとはkerasのmodel.fitに入れるなり、train loopで使うなり。 TFRecord こちらは、「ネットワーク経由で、画像を1枚1枚読み込むのは非効率なため、バッチ毎にデータを取得して読み込みたい」といったことをしたいときに使える(ストレージサーバからのデータ取得等)。分散学習(Tensorflow Federated)だったり、TPUだと良く使われるらしい。 データの書き出し 前処理とシリアライズ まずは前処理とシリアライズ用の関数を準備。今回はSemantic Segmentationなどで使われることを想定して書く(画像とマスクがセットになる)。 以下は例で、前処理をカスタマイズしたい場合は、preprocess_imageとpreprocess_maskを好きに変える。学習はしないので、型はnumpyでもok。よってalubumentationやcv2等が使える。 # 学習はしないので、tf.functionはつけなくて良い def as_tensor(func): def function(*args, **kwargs) -> tf.Tensor: ret = func(*args, **kwargs) if not isinstance(ret, tf.Tensor): return tf.convert_to_tensor(ret) return ret return function @as_tensor def preprocess_image(img_path: Union[tf.Tensor, str], resize_shape: Tuple[int, int]=(256,256)): """ 画像の前処理関数 """ img = tf.io.read_file(img_path) img = tf.image.decode_image(img, channels=3, expand_animations=False) img = tf.image.resize(img, resize_shape) # ここに水増し用の関数を入れる # e.g. # img = transforms(image=img.numpy())['image'] img = tf.convert_to_tensor(img, tf.float32) img /= 255. # 水増し用の関数で正規化した場合、ここは不要 return img @as_tensor def preprocess_mask(img_path: Union[tf.Tensor, str], resize_shape: Tuple[int, int]=(256,256)): """ マスク画像の前処理関数 """ img = tf.io.read_file(img_path) img = tf.image.decode_image(img, channels=3, expand_animations=False) img = tf.image.resize(img, resize_shape) img = tf.cast(img, tf.float32) img /= 255. return img def serialize(base_path: tf.Tensor, mask_path: tf.Tensor): # 前処理とシリアライズ base_img = preprocess_image(base_path) base_img = tf.io.serialize_tensor(base_img) mask_img = preprocess_mask(mask_path) mask_img = tf.io.serialize_tensor(mask_img) bytes_base = tf.train.BytesList(value=[base_img.numpy()]) bytes_mask = tf.train.BytesList(value=[mask_img.numpy()]) features = tf.train.Features( feature={ 'base': tf.train.Feature(bytes_list = bytes_base), 'mask': tf.train.Feature(bytes_list = bytes_mask) } ) proto = tf.train.Example(features=features) return proto.SerializeToString() 特に前処理をしないなら、こういうのでok. def simply_load(img_path: Union[tf.Tensor, str]) -> tf.Tensor: """ マスク画像の前処理関数 """ img = tf.io.read_file(img_path) img = tf.image.decode_image(img, channels=3, expand_animations=False) return img def serialize(base_path: tf.Tensor, mask_path: tf.Tensor): # 前処理とシリアライズ base_img = simply_load(base_path) base_img = tf.io.serialize_tensor(base_img) mask_img = simply_load(mask_path) mask_img = tf.io.serialize_tensor(mask_img) ... 書き出し バッチサイズを指定して、(全ファイル数/バッチサイズ)個のファイルを作成する。 公式チュートリアルでも使われているtf.data.experimental.TFRecordWriterは2.6.0以降Deprecatedになったようで、tf.io.TFRecordWriterか、tf.data.experimental.save/tf.data.experimental.loadを使ってやってくれとのこと。後者は仕組みがよくわかっていないので、今回は前者で行う。 def split_path(filepathes: List[str], batch_size: int) -> List[List[str]]: total = math.ceil(len(filepathes)/batch_size) ret = [filepathes[i*batch_size:(i+1)*batch_size] for i in range(total)] if len(ret[-1]) == 0: ret = ret[:-1] return ret def write_to_tfrecord( image_list: List[str], mask_list: List[str], batch_size: int = 32, output_path: str = 'tfr_outputs', output_path_exist_ok: bool=False): """ Parameters -------- image_list: list (str, ...) 読み込む画像のパスが含まれるリスト mask_list: list (str, ...) 読み込む画像(マスク)のパスが含まれるリスト。image_listと同じ長さでなければならない。 batch_size: int, default = 32 バッチサイズ output_path: str, default = `tfr_outputs/` tfrecordファイルの保存先 output_path_exist_ok: bool, default = `False` output_pathの重複作成を許可するかどうか。Falseかつすでにフォルダが存在する場合。エラーが出る。 """ if len(image_list) != len(mask_list): raise ValueError('The sizes of image_list and mask_list do not match({} vs {}).'.format(len(image_list), len(mask_list))) if len(image_list) == 0: raise ValueError('Empty dataset.') os.makedirs(output_path, exist_ok=output_path_exist_ok) filepathes = list(zip(image_list, mask_list)) split_fs = split_path(filepathes, batch_size) size = len(split_fs) adjust_size = len(list(str(size))) for i, fs in enumerate(split_fs): output = os.path.join(output_path, 'batch_{}.tfrecord'.format(str(i+1).rjust(adjust_size, '0'))) print('\rWriting {}/{} to {}...'.format(str(i+1).rjust(adjust_size), size, output), end='') with tf.io.TFRecordWriter(output) as writer: for targets in fs: writer.write(serialize(*targets)) print('\nDone.') 例: write_to_tfrecord(image_list = p, mask_list = p, batch_size = 2) 出力: Writing 15/15 to tfr_outputs/batch_15.tfrecord... Done. データの読み込み tf.recordのファイルを読み込んでdeserializeする。 ここで前処理したいなら、deserializeの最後で前処理する関数などを入れる。 def deserialize(proto): parsed = tf.io.parse_example( proto, { 'base': tf.io.FixedLenFeature([], tf.string), 'mask': tf.io.FixedLenFeature([], tf.string) }) base_img = tf.io.parse_tensor(parsed['base'], out_type=tf.float32) mask_img = tf.io.parse_tensor(parsed['mask'], out_type=tf.float32) # 前処理したいならここにその操作を挿入 return base_img, mask_img def generate_dataset_from_tfrecord(path: str, batch_size: int = 32, name: str = 'train'): """ Parameters -------- path: str 読み込むtfrecordがあるフォルダ batch_size: int, default = 32 バッチサイズ name: str, default = `train` データセットの名前。trainだと、シャッフルされる。 """ file_list = [p for p in glob.glob(path + '/*') if os.path.isfile(p) and os.path.splitext(p)[1] == '.tfrecord'] if len(file_list) == 0: raise ValueError('Empty Dataset.') else: print(f'{name} steps per epoch: {len(file_list)}') ds = tf.data.TFRecordDataset(file_list) ds = ds.map(deserialize, num_parallel_calls=AUTOTUNE).batch(batch_size) if name == 'train': ds = ds.shuffle(2048) ds = ds.prefetch(AUTOTUNE) return ds 例: ds = generate_dataset_from_tfrecord('tfr_outputs', batch_size=2) x = next(iter(ds)) print(len(x)) print(x[0]) 出力: train steps per epoch: 15 2 tf.Tensor( [[[[0.775337 0.802788 0.8263174 ] [0.75114125 0.7785922 0.80212164] [0.7315334 0.7589844 0.7825138 ] ... [0.84117645 0.8372549 0.8215686 ] [0.84117645 0.8372549 0.8215686 ] [0.84432447 0.8404029 0.8247166 ]]]], shape=(2, 256, 256, 3), dtype=float32) ちゃんと型が復元されていることが分かる。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AndroidでDiscord Botを動かす

家にAndroid 5とかいう化石を搭載しているタブレットが2台ある。 片方はイヤホンジャックがタヒんでてYouTube専用機としても使いづらい。 なにか有効活用できないか。探した結果、Discord Botにたどり着いた。 動作環境 Android 5 UserLand 2.7.2 Python 3.6.9 Discord.py 1.7.3 本題 この記事ではBotアカウントの取得方法やBotのコードの書き方は書きません。環境構築の方法を超適当に書きます。 てことで、AndroidにDiscord Botを構築しよう! 1. UserLandのインストール これが無いとなにも始まりまらない。 ちなみに何故Termuxじゃないのかだが、TermuxはAndroid 5&6のサポートが打ち切られてしまったのと、Discord.pyが入れれなかった。 F-droidかGoogle Play(https://play.google.com/store/apps/details?id=tech.ula)でインストールしてくる。 2. UserLandの準備 アプリを起動してリビジョンを選択する。 今回はトラブりたくなかったので無難にUbuntuを選択 ユーザー名とパスワードを設定してCONTINUEを2回押してしばらく待てば起動する。 儀式 $ sudo apt update $ sudo apt upgrade 必要なものをインストール $ sudo apt install python3 $ sudo apt install python3-pip $ sudo python3 -m pip install discord.py get-pip.pyでpipをインストールするとDiscord.pyがインストールできなかった。 テキストエディタについては論争に巻き込まれたくないので各自好きなのをインストールしてください。操作が簡単なのはnano Requestsとかも入れといても損はない。(というか入れたほうがいい) 日本語対応 デフォルトだと日本語に対応してない。 $ sudo apt install locales $ sudo dpkg-reconfigure locales //なんかいっぱい出てくるのでその中からja_JP.UTF-8ってのを見つけてその番号を入力 //環境変数にja_JP.UTF-8を設定 $ echo "export LANG=ja_JP.UTF-8" >> ~/.profile これで再ログインすれば日本語に対応してる。 環境構築はこれで終わり。 3. Wi-Fi接続が切れる スリープ状態にするとなんか接続が切れるのでこれhttps://m.apkpure.com/jp/advanced-wifi-lock-free/opotech.advancedwifilockfreeいれたら解決した。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

機械学習(分類問題)

データの作成 from sklearn.model_selection import train_test_split target_col = '' exclude_cols = ['', '', '', ''] feature_cols = [] for col in dataset.columns: if col not in exclude_cols: feature_cols.append(col) X = dataset[feature_cols] y = dataset[target_col] X_train, X_test, y_train, y_test = train_test_split(X , y , test_size=0.3, random_state=1234) print('X_test Features Shape: ', X_test.shape) print('y_test Target Shape: ', y_test.shape) print('X_train Features Shape: ', X_train.shape) print('y_train Target Shape: ', y_train.shape) 分類問題 ロジスティック回帰 線形回帰によって求めた値をシグモイド関数に適応させ、確率を予測する。 P = \frac{1}{1+e^{-(\beta_0 + \beta_1 X_1 + \cdots + \beta_K X_K)}}          重要度(オッズ比)の求め方 回帰係数を基にオッズ比で解釈する。 オッズ比が1に近い → 重要度が低い オッズ比が1より大きい → 説明変数が大きいほど、事象が起こりやすい オッズ比が1より小さい → 説明変数が小さいほど、事象が起こりやすい \begin{align} p &= \frac{1}{1+e^{-(\beta_0+ \beta_1X)}} \cdots 事象が起こる確率\\ \\ オッズ \cdots \frac{p}{1-p} &= \frac{\frac{1}{1+e^{-(\beta_0+ \beta_1X)}}}{1-\frac{1}{1+e^{-(\beta_0+ \beta_1X)}}} = e^{\beta_0+ \beta_1 X} = e^{\beta_0} e^{\beta_1 X}\\ \\ X_0 \underset{+1}{\longrightarrow} X_1とする\\ オッズ比 \cdots \frac{\frac{p_0}{1-p_0}}{\frac{p_1}{1-p_1}} &=\frac{e^{\beta_0} e^{\beta_1 X_0}}{e^{\beta_0} e^{\beta_1 X_1}} =e^{\beta_1(X_1 - X_0)} =e^{\beta_1} \end{align} from sklearn.linear_model import LogisticRegression from sklearn.metrics import confusion_matrix, accuracy_score, classification_report, auc, roc_curve logi = LogisticRegression() logi = LogisticRegression(penalty = "l1",solver = "liblinear", C=1) #Lasso logi = LogisticRegression(penalty = "l2",solver = "liblinear", C=1) #Ridge logi.fit(X_train, y_train) y_pred = logi.predict(X_test) y_proba = logi.predict_proba(X_test)[:,1] #混同行列 def show_evaluation_metrics(y_true, y_pred): print("Accuracy:") print(accuracy_score(y_true, y_pred)) print() print("Report:") print(classification_report(y_true, y_pred)) print("Confusion matrix:") print(confusion_matrix(y_true, y_pred)) show_evaluation_metrics(y_test, y_pred) #AUC def AUC_and_ROC_curve(y_true, y_proba): fpr, tpr, thresholds = roc_curve(y_true, y_proba) AUC = auc(fpr, tpr) plt.plot(fpr, tpr, label='ROC curve (AUC={:.3f})'.format(AUC)) plt.legend() plt.title('ROC curve') plt.xlabel('False Positive Rate') plt.ylabel('True Positive Rate') plt.grid(True) plt.show() AUC_and_ROC_curve(y_test, y_proba) #オッズ比 df_odds = pd.DataFrame({'feature':X.columns, 'odds':np.exp(logi.coef_)[0]}) sns.barplot(x='odds', y='feature', data=df_odds.sort_values('odds', ascending=False)) plt.show() #パラメーターの調整 from sklearn.model_selection import GridSearchCV logi = LogisticRegression(penalty = "l1 or l2",solver = "liblinear") params = {"C": [0.01*i for i in range(1,10,1)]} gscv = GridSearchCV(logi, param_grid=params, verbose=1, #詳細を表示 cv=3, #交差検証の分割数 scoring='neg_log_loss', n_jobs=-1 #全てのプロセッサを使用する。 ) gscv.fit(X_train, y_train) print('best params:', gscv.best_params_) print('best logloss:', gscv.best_score_) #best_paramsの可視化 gscv_df = pd.DataFrame(gscv.cv_results_['mean_test_score'], index=params['C']) plt.plot(gscv_df) plt.xlabel('C') plt.ylabel('mean_test_score') plt.grid(True) plt.show() 決定木 分類問題の場合、ジニ係数を元に値が最小となるように分類する。 重要度の求め方 ある特徴量で分岐させることでどのくらい値を下げられるかを示す。 回帰問題の場合はMSE、分類問題の場合はジニ係数 (例)分類問題 \begin{align}  (Pclassの重要度) &= I(Pclass) \\ &= (623*0.474)-((275*0.492)+(348*0.369)) \\ \\  (Parchの重要度) &= I(Parch)_1 + I(Parch)_2 \\ I(Parch)_1 &= (275*0.492)-((203*0.5)+(72*0.346)) \\ I(Parch)_2 &= (348*0.369)-((272*0.344)+(76*0.441)) \end{align} from sklearn.tree import DecisionTreeClassifier, plot_tree from sklearn.metrics import confusion_matrix, accuracy_score, classification_report, auc, roc_curve dt = DecisionTreeClassifier(random_state=1234, max_depth=None #全てのリーフがPureになるまで分岐する ) dt.fit(X_train, y_train) y_pred = dt.predict(X_test) y_proba = dt.predict_proba(X_test)[:,1] #混同行列 def show_evaluation_metrics(y_true, y_pred): print("Accuracy:") print(accuracy_score(y_true, y_pred)) print() print("Report:") print(classification_report(y_true, y_pred)) print("Confusion matrix:") print(confusion_matrix(y_true, y_pred)) show_evaluation_metrics(y_test, y_pred) #AUC def AUC_and_ROC_curve(y_true, y_proba): fpr, tpr, thresholds = roc_curve(y_true, y_proba) AUC = auc(fpr, tpr) plt.plot(fpr, tpr, label='ROC curve (AUC={:.3f})'.format(AUC)) plt.legend() plt.title('ROC curve') plt.xlabel('False Positive Rate') plt.ylabel('True Positive Rate') plt.grid(True) plt.show() AUC_and_ROC_curve(y_test, y_proba) #特徴量の重要度 df_feature_importance = pd.DataFrame({'feature':X.columns, 'importance':dt.feature_importances_}) sns.barplot(x='importance', y='feature', data=df_feature_importance.sort_values('importance', ascending=False)) plt.show() #決定木の可視化 plot_tree(dt, feature_names=X.columns, filled=True, rounded=True) plt.show() #パラメータ調整 from sklearn.model_selection import GridSearchCV dt = DecisionTreeClassifier(random_state=1234) params = {"max_depth": [i for i in range(4,10,1)]} gscv = GridSearchCV(dt, param_grid=params, verbose=1, cv=3,scoring='neg_log_loss', n_jobs=-1) gscv.fit(X_train, y_train) print('best params:', gscv.best_params_) print('best logloss:', gscv.best_score_) #best_paramsの可視化 gscv_df = pd.DataFrame(gscv.cv_results_['mean_test_score'], index=params['max_depth']) plt.plot(gscv_df) plt.xlabel('max_depth') plt.ylabel('mean_test_score') plt.grid(True) plt.show() ランダムフォレスト 決定木をたくさん作り、多数決を行う。 サンプリングはデータと特徴量両方で行われる。 重要度の求め方 OOBのデータを使用。 ある特徴量(列)をシャッフルし、ランダムフォレストの精度の差を求める。 (重要度)=(シャッフル前の精度)-(シャッフル後の精度) 精度が下がる→その特徴量が重要であった。 精度があまり変わらない→その特徴量があまり必要ではない。 from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import confusion_matrix, accuracy_score, classification_report, auc, roc_curve rf = RandomForestClassifier(random_state=1234, n_estimators=100, max_depth=None #全てのリーフがPureになるまで分岐する ) rf.fit(X_train, y_train) y_pred = rf.predict(X_test) y_proba = rf.predict_proba(X_test)[:,1] #混同行列 def show_evaluation_metrics(y_true, y_pred): print("Accuracy:") print(accuracy_score(y_true, y_pred)) print() print("Report:") print(classification_report(y_true, y_pred)) print("Confusion matrix:") print(confusion_matrix(y_true, y_pred)) show_evaluation_metrics(y_test, y_pred) #AUC def AUC_and_ROC_curve(y_true, y_proba): fpr, tpr, thresholds = roc_curve(y_true, y_proba) AUC = auc(fpr, tpr) plt.plot(fpr, tpr, label='ROC curve (AUC={:.3f})'.format(AUC)) plt.legend() plt.title('ROC curve') plt.xlabel('False Positive Rate') plt.ylabel('True Positive Rate') plt.grid(True) plt.show() AUC_and_ROC_curve(y_test, y_proba) #特徴量の重要度 df_feature_importance = pd.DataFrame({'feature':X.columns, 'importance':rf.feature_importances_}) sns.barplot(x='importance', y='feature', data=df_feature_importance.sort_values('importance', ascending=False)) plt.show() #パラメータ調整 from sklearn.model_selection import GridSearchCV rf = RandomForestRegressor(random_state=1234) params = {"n_estimators":[10*i for i in range(8,12,1)], "max_depth":[5*i for i in range(1,5,1)]} gscv = GridSearchCV(rf, param_grid=params, verbose=1, #詳細を表示 cv=3, #交差検証の分割数 scoring='neg_log_loss', n_jobs=-1 #全てのプロセッサを使用する ) gscv.fit(X_train, y_train) print('best params:', gscv.best_params_) print('best logloss:', gscv.best_score_) #グリッドサーチの可視化 gscv_score = [] for i in range(len(gscv.grid_scores_)): gscv_score.append(gscv.grid_scores_[i][1]) gscv_df = pd.DataFrame(np.reshape(gscv_score,(len(params['n_estimators']),len(params['max_depth']))), index=gscv.param_grid['n_estimators'], columns=gscv.param_grid['max_depth']) sns.heatmap(gscv_df, annot=True) plt.show() XGBoost 勾配Boosting木の一種で精度が高く、使いやすい。 勾配Boosting木(Gradient Boosting Decision Tree) 〜特徴〜 ・特徴量は数値         → 特徴量がある値より大きいか小さいかで分岐するため ・欠損値を扱うことができる   → 欠損値かどうかで分岐もすることができる ・変数間の相互作用が反映される → 分岐の繰り返しによりできる ・スケーリングする必要がない  → 値の大小関係のみが重要 ・カテゴリー変数をone-hot-encodingする必要がない                 → 決定木の分岐の際に、勝手に特徴量を抽出してくれる ・疎行列への対応がサポートされている 〜仕組み〜 目的変数$y_i$と予測値$\hat{y_i}$の差について学習し、逐次的に決定木を作成する。 ハイパーパラメータである決定木の本数分、上記を繰り返す。 最終的に各決定木のウェイト$W_m$の総和が予測値となる。 木の作成の仕方 Level-wise tree growth・・・深さ単位で木を成長させる。 重要度の求め方 gain → 特徴量によって得られた損失の減少を示し、基本的な手法。 他にも、"weight","cover","total_gain","total_cover"がある。 #sklearnAPI ver. from xgboost import XGBClassifier from sklearn.metrics import confusion_matrix, accuracy_score, classification_report, auc, roc_curve xgb = XGBClassifier(random_state=1234, learning_rate=0.3, # 学習率 max_depth=6, #木の深さ(最も重要) importance_type='gain' #特徴量の算出方法 ) xgb.fit(X_train, y_train) y_pred = xgb.predict(X_test) y_proba = xgb.predict_proba(X_test)[:,1] #混同行列 def show_evaluation_metrics(y_true, y_pred): print("Accuracy:") print(accuracy_score(y_true, y_pred)) print() print("Report:") print(classification_report(y_true, y_pred)) print("Confusion matrix:") print(confusion_matrix(y_true, y_pred)) show_evaluation_metrics(y_test, y_pred) #AUC def AUC_and_ROC_curve(y_true, y_proba): fpr, tpr, thresholds = roc_curve(y_true, y_proba) AUC = auc(fpr, tpr) plt.plot(fpr, tpr, label='ROC curve (AUC={:.3f})'.format(AUC)) plt.legend() plt.title('ROC curve') plt.xlabel('False Positive Rate') plt.ylabel('True Positive Rate') plt.grid(True) plt.show() AUC_and_ROC_curve(y_test, y_proba) #特徴量の重要度 df_feature_importance = pd.DataFrame({'feature':X.columns, 'importance':xgb.feature_importances_}) sns.barplot(x='importance', y='feature', data=df_feature_importance.sort_values('importance', ascending=False)) plt.show() #ハイパーパラメータ調整 from sklearn.model_selection import GridSearchCV xgb =XGBClassifier(random_state=1234) params = {"learning_rate":[np.round(0.05*i,3) for i in range(0,5)], "max_depth":[i for i in range(6,10,1)]} gscv = GridSearchCV(xgb, param_grid=params, verbose=1, #詳細を表示 cv=3, #交差検証の分割数 scoring='neg_log_loss', n_jobs=-1 #全てのプロセッサを使用する。 ) gscv.fit(X_train, y_train) print('best params:', gscv.best_params_) print('best logloss:', gscv.best_score_) #グリッドサーチの可視化 gscv_df = pd.DataFrame(np.reshape(gscv.cv_results_['mean_test_score'],(len(params['learning_rate']),len(params['max_depth']))), index=gscv.param_grid['learning_rate'], columns=gscv.param_grid['max_depth']) sns.heatmap(gscv_df, annot=True) plt.show() LGBM XGBoostと比較して、高速かつ精度が同等程度と考えられている。 木の作成の仕方 Leaf-wise tree growth・・・葉単位で木を成長させる 重要度の求め方 split → 分岐に使われた回数 他にも、"gain"を使うことができる。 #sklearnAPI ver. from lightgbm import LGBMClassifier from sklearn.metrics import confusion_matrix, accuracy_score, classification_report, auc, roc_curve lgbm = LGBMClassifier(random_state=1234, num_leaves=31, #葉の数 min_child_samples=20, #各ノードに入る最小データ数 importance_type='split' #特徴量の算出方法 ) lgbm.fit(X_train, y_train) y_pred = lgbm.predict(X_test) y_proba = lgbm.predict_proba(X_test)[:,1] #混同行列 def show_evaluation_metrics(y_true, y_pred): print("Accuracy:") print(accuracy_score(y_true, y_pred)) print() print("Report:") print(classification_report(y_true, y_pred)) print("Confusion matrix:") print(confusion_matrix(y_true, y_pred)) show_evaluation_metrics(y_test, y_pred) #AUC def AUC_and_ROC_curve(y_true, y_proba): fpr, tpr, thresholds = roc_curve(y_true, y_proba) AUC = auc(fpr, tpr) plt.plot(fpr, tpr, label='ROC curve (AUC={:.3f})'.format(AUC)) plt.legend() plt.title('ROC curve') plt.xlabel('False Positive Rate') plt.ylabel('True Positive Rate') plt.grid(True) plt.show() AUC_and_ROC_curve(y_test, y_proba) #特徴量の重要度 df_feature_importance = pd.DataFrame({'feature':X.columns, 'importance':lgbm.feature_importances_}) sns.barplot(x='importance', y='feature', data=df_feature_importance.sort_values('importance', ascending=False)) plt.show() #ハイパーパラメータ調整 from sklearn.model_selection import GridSearchCV lgbm = LGBMClassifier(random_state=1234) params = {"num_leaves":[i for i in range(28,34,1)], "min_child_samples":[i for i in range(17,23,1)]} gscv = GridSearchCV(lgbm, param_grid=params, verbose=1, #詳細を表示 cv=3, #交差検証の分割数 scoring='neg_log_loss', n_jobs=-1 #全てのプロセッサを使用する。 ) gscv.fit(X_train, y_train) print('best params:', gscv.best_params_) print('best logloss:', gscv.best_score_) #グリッドサーチの可視化 gscv_df = pd.DataFrame(np.reshape(gscv.cv_results_['mean_test_score'],(len(params['num_leaves']),len(params['min_child_samples']))), index=gscv.param_grid['num_leaves'], columns=gscv.param_grid['min_child_samples']) sns.heatmap(gscv_df, annot=True) plt.show() 損失関数 以下の尤度関数$\,f(\lambda)$を用いた交差エントロピー誤差関数$\,E(\lambda)$を使用する。 \begin{align} \lambda &= \frac{1}{1+e^{-(\beta_0+ \beta_1 X_1\cdots +\beta_K X_K)}}\\ \lambda_i &= \frac{1}{1+e^{-(\beta_0+ \beta_1 x_{1,i}\cdots +\beta_K x_{K,i})}} \cdots i\,が1と判断される確率 \\ \\ f(\lambda ) &= \displaystyle \prod_{i=1}^{n}\lambda_i^{t_i}(1-\lambda_i)^{(1-t_i)} \quad(t_i\cdots0\,or\,1) \\ \end{align} 交差エントロピー誤差関数 \begin{align} E(\lambda) &= -log\,f(\lambda)\\ &= -log \displaystyle \prod_{i=1}^{N}\lambda^{t_i}(1-\lambda)^{(1-t_i)}\\ &=-\displaystyle \sum_{ i = 1 }^{ n } (t_i log\lambda + (1-t_i)log(1-\lambda))\\ \end{align} 他にも『Binary Cross Entropy Loss』を使用することもできる。 精度評価指標 混同行列        予測されたクラス実際のクラス Positive Negative Positive True Positive False Negative Negative False Positive True Negative \begin{align} Accuracy(正解率) \cdots \frac{True Positive + True Negative}{(全データ)}\\ \\ Precision(適合率) \cdots \frac{True Positive}{TruePositive+FalsePositive}\\ \\ Recall(再現率) \cdots \frac{True Positive}{TruePositive+FalseNegative}\\ \\ F1Score \cdots \frac{2*Recall*Precision}{Recall+Precision} \quad\quad\quad \\ \\ FbetaScore \cdots \frac{(1+\beta^2)*Recall*Precision}{\beta^2Recall+Precision} \end{align} AUC 閾値を変化させ、ROC曲線を描く。 AUCはその下側の面積となる。 AUCは0.5に近いほどモデルの精度が低く、1.0に近いほどモデルの精度が高いことを示している。        予測されたクラス実際のクラス Positive Negative Positive True Positive False Negative Negative False Positive True Negative (真陽性率)=\frac{True Positive}{True Positive + False Negative} \\ \\ (偽陽性率)=\frac{False Positive}{False Positive + True Negative} あとがき KaggleやSignateのコンペティションに熱中している間に、モデルの内容や指標の解釈を忘れてしまうので、すぐ概要を確認できるようにまとめました。 詳細を突き詰めると、間違えている箇所はあるかもしれませんがご容赦ください。 <参考文献> ・scikit-learnホームページ(https://scikit-learn.org/stable/) ・XGBoostホームページ(https://xgboost.readthedocs.io/en/latest/) ・LightGBMホームページ(https://lightgbm.readthedocs.io/en/latest/) ・Kaggleで勝つデータ分析の技術(技術評論社/著:門脇大輔,阪田隆司,保坂桂佑,平松雄司)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【100 numpy exercises】でnumpy力を鍛える!(51〜60問目)

こんにちは! 前回はnumpy力を鍛えるために41~50問をやっていきました。 前回の記事はこちら それでは前回に引き続き100 numpy exercisesを使ってnumpyの学習をしていきたいと思います! 今回は51~60問をやっていきます。 51. Create a structured array representing a position (x,y) and a color (r,g,b) (★★☆) 51. 位置(x,y)と色(r,g,b)を表す構造化された配列を作成する 100_numpy_exercises.ipynb(51) Z = np.zeros(10, [ ('position', [ ('x', float, 1), ('y', float, 1)]), ('color', [ ('r', float, 1), ('g', float, 1), ('b', float, 1)])]) print(Z) 上記のコードはfloat型の数値を持つ配列を要素に持つ構造化配列を作成します。構造化配列を作っておけば、簡単なコードで色々なデータを取り出すことができます。 実行結果は以下の通りです。 100_numpy_exercises.ipynb(51)-output [((0., 0.), (0., 0., 0.)) ((0., 0.), (0., 0., 0.)) ((0., 0.), (0., 0., 0.)) ((0., 0.), (0., 0., 0.)) ((0., 0.), (0., 0., 0.)) ((0., 0.), (0., 0., 0.)) ((0., 0.), (0., 0., 0.)) ((0., 0.), (0., 0., 0.)) ((0., 0.), (0., 0., 0.)) ((0., 0.), (0., 0., 0.))] 52. Consider a random vector with shape (100,2) representing coordinates, find point by point distances (★★☆) 52. 座標を表す形状(100,2)のランダムなベクトルを考え、点と点の距離を求める 100_numpy_exercises.ipynb(52) Z = np.random.random((10,2)) X,Y = np.atleast_2d(Z[:,0], Z[:,1]) D = np.sqrt( (X-X.T)**2 + (Y-Y.T)**2) print(D) np.random.randomで(10, 2)の二次元配列を生成します。np.atleast_2dで入力された配列をX,Yの2つの二次元配列として生成します。2点間の距離の公式を使って(X,Y),(X.T, Y.T)間の距離を求めます。そうすると2点間の距離が100個求まります。 実行結果は以下の通りです。 100_numpy_exercises.ipynb(52)-output [[0. 0.6060765 0.40413331 0.49307252 0.10412311 0.36386293 0.52039787 0.33679798 0.41173638 0.63203753] [0.6060765 0. 0.34389387 0.63998092 0.6437951 0.69211375 0.98402659 0.3863142 0.34481216 0.2826146 ] [0.40413331 0.34389387 0. 0.30181901 0.48790107 0.35431183 0.6527074 0.42000667 0.00828523 0.23475134] [0.49307252 0.63998092 0.30181901 0. 0.59708376 0.17966471 0.43859746 0.66966905 0.29895121 0.44140839] [0.10412311 0.6437951 0.48790107 0.59708376 0. 0.4655664 0.59479412 0.31852055 0.49588384 0.70715854] [0.36386293 0.69211375 0.35431183 0.17966471 0.4655664 0. 0.30030062 0.61592342 0.3557692 0.55594391] [0.52039787 0.98402659 0.6527074 0.43859746 0.59479412 0.30030062 0. 0.84115716 0.65469067 0.85487456] [0.33679798 0.3863142 0.42000667 0.66966905 0.31852055 0.61592342 0.84115716 0. 0.42755679 0.55940182] [0.41173638 0.34481216 0.00828523 0.29895121 0.49588384 0.3557692 0.65469067 0.42755679 0. 0.22856936] [0.63203753 0.2826146 0.23475134 0.44140839 0.70715854 0.55594391 0.85487456 0.55940182 0.22856936 0. ]] ちなみにscipyを使うとより早い計算ができるらしいので、実際に使う際にはそちらを使った方がいいかもしれないです。 53. How to convert a float (32 bits) array into an integer (32 bits) in place? (★★☆) 53. float(32ビット)の配列をinteger(32ビット)に逐次変換するには? 100_numpy_exercises.ipynb(53) Z = (np.random.rand(10)*100).astype(np.float32) Y = Z.view(np.int32) Y[:] = Z print(Y) ここではviewというものを使います。viewは元の配列と同じメモリを参照している配列要素を複製します。似ているものとしてcopyがあるのですが、copyは元の配列と違うメモリを使用しているので、データ型を変えてもメモリに存在する元の配列に影響はありません。 Z.viewでint型に変えられた値は小数点以下の数値も全て整数に変換されてしまうため、Yの要素にZを代入することによってZの整数部分だけの配列とすることができます。 実行結果は以下の通りです。 100_numpy_exercises.ipynb(53)-output [97 73 73 3 11 36 90 61 95 64] 54. How to read the following file? (★★☆) 54. 次のファイルを読むには? 100_numpy_exercises.ipynb(54) from io import StringIO s = StringIO('''1, 2, 3, 4, 5 6, , , 7, 8 , , 9,10,11 ''') Z = np.genfromtxt(s, delimiter=",", dtype=np.int) print(Z) 文字列をファイルとして扱うには、StringIOが使えます。まずioモジュールをインポートし、StringIOインスタンスを作成します。これで文字列ファイルを作成できました。ファイルを読み込むには、np.genfromtxtを使います。np.genfromtxtは欠損値を含んでいたり複数の異なるデータ型を含んでいたりする、より複雑な構造のCSVファイルの読み込みが可能です。区切り文字に,を設定しています。 実行結果は以下の通りです。 100_numpy_exercises.ipynb(54)-output [[ 1 2 3 4 5] [ 6 -1 -1 7 8] [-1 -1 9 10 11]] 数値が入力された所には、-1が入ってますね。よくわかりませんが笑笑 55. What is the equivalent of enumerate for numpy arrays? (★★☆) 55. numpyの配列でenumerateに相当するものは何ですか? 100_numpy_exercises.ipynb(55) Z = np.arange(9).reshape(3,3) for index, value in np.ndenumerate(Z): print(index, value) for index in np.ndindex(Z.shape): print(index, Z[index]) np.ndenumerate,np.indexでenumerateと同様の動きができるようです。2つの違いはnp.ndenumerateは配列、np.ndindexは配列の形状を引数に使うようです。 実行結果は以下の通りです。 100_numpy_exercises.ipynb(55)-output (0, 0) 0 (0, 1) 1 (0, 2) 2 (1, 0) 3 (1, 1) 4 (1, 2) 5 (2, 0) 6 (2, 1) 7 (2, 2) 8 (0, 0) 0 (0, 1) 1 (0, 2) 2 (1, 0) 3 (1, 1) 4 (1, 2) 5 (2, 0) 6 (2, 1) 7 (2, 2) 8 56. Generate a generic 2D Gaussian-like array (★★☆) 56. 一般的な2Dガウス状の配列を生成する 100_numpy_exercises.ipynb(56) X, Y = np.meshgrid(np.linspace(-1,1,10), np.linspace(-1,1,10)) D = np.sqrt(X*X+Y*Y) sigma, mu = 1.0, 0.0 G = np.exp(-( (D-mu)**2 / ( 2.0 * sigma**2 ) ) ) print(G) np.meshgridはx,y,...の各座標の要素列から格子座標を作成するために使います。2Dガウス状の配列を生成するにはガウス分布を持つ一次元ベクトルの多重配列をガウスの式に当てはめればいいので上記のようになります。 実行結果は以下の通りです。 100_numpy_exercises.ipynb(56)-output [[0.36787944 0.44822088 0.51979489 0.57375342 0.60279818 0.60279818 0.57375342 0.51979489 0.44822088 0.36787944] [0.44822088 0.54610814 0.63331324 0.69905581 0.73444367 0.73444367 0.69905581 0.63331324 0.54610814 0.44822088] [0.51979489 0.63331324 0.73444367 0.81068432 0.85172308 0.85172308 0.81068432 0.73444367 0.63331324 0.51979489] [0.57375342 0.69905581 0.81068432 0.89483932 0.9401382 0.9401382 0.89483932 0.81068432 0.69905581 0.57375342] [0.60279818 0.73444367 0.85172308 0.9401382 0.98773022 0.98773022 0.9401382 0.85172308 0.73444367 0.60279818] [0.60279818 0.73444367 0.85172308 0.9401382 0.98773022 0.98773022 0.9401382 0.85172308 0.73444367 0.60279818] [0.57375342 0.69905581 0.81068432 0.89483932 0.9401382 0.9401382 0.89483932 0.81068432 0.69905581 0.57375342] [0.51979489 0.63331324 0.73444367 0.81068432 0.85172308 0.85172308 0.81068432 0.73444367 0.63331324 0.51979489] [0.44822088 0.54610814 0.63331324 0.69905581 0.73444367 0.73444367 0.69905581 0.63331324 0.54610814 0.44822088] [0.36787944 0.44822088 0.51979489 0.57375342 0.60279818 0.60279818 0.57375342 0.51979489 0.44822088 0.36787944]] 57. How to randomly place p elements in a 2D array? (★★☆) 57. 2D配列にp個の要素をランダムに配置するには? 100_numpy_exercises.ipynb(57) n = 10 p = 3 Z = np.zeros((n,n)) np.put(Z, np.random.choice(range(n*n), p, replace=False),1) print(Z) まず特定の範囲からランダムな数字を選ぶためにnp.random.choiceを使います。第一引数に値の範囲、第二引数に何個選ぶか、第3引数のreplaceはデフォルトではTrueですが、同じ値を選ばないためにFalseにしておきます。値を置き換える関数として、np.putを使います。第一引数に置き換えたい配列、第二引数にランダムに選ばれた数字の配列、第三引数に置き換える数字が入ります。 実行結果は以下の通りです。 100_numpy_exercises.ipynb(57)-output [[0. 0. 0. 0. 0. 0. 0. 0. 0. 0.] [0. 0. 0. 0. 0. 0. 0. 0. 0. 0.] [1. 0. 0. 0. 0. 0. 0. 0. 0. 0.] [0. 0. 0. 0. 0. 0. 0. 0. 0. 0.] [0. 0. 0. 0. 0. 0. 0. 0. 0. 0.] [0. 0. 0. 0. 0. 0. 0. 0. 0. 0.] [0. 0. 0. 0. 0. 0. 0. 1. 0. 0.] [0. 0. 0. 0. 0. 0. 0. 0. 1. 0.] [0. 0. 0. 0. 0. 0. 0. 0. 0. 0.] [0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]] 58. Subtract the mean of each row of a matrix (★★☆) 58. 行列の各行の平均値を引きます 100_numpy_exercises.ipynb(58) X = np.random.rand(5, 10) Y = X - X.mean(axis=1, keepdims=True) print(Y) 各行の平均値はnp.meanのaxisを1にすることで求められます。keepdimsをTrueにすればreshapeをする必要がなくなりそのまま計算に移れます。 実行結果は以下の通りです。 100_numpy_exercises.ipynb(58)-output [[ 0.37333021 0.14818213 -0.17776424 -0.02976235 -0.30472966 0.21828251 -0.0952619 0.19063885 -0.42395379 0.10103823] [ 0.10851469 -0.48779044 -0.23011103 -0.24256646 0.11941821 0.37498574 -0.47534218 0.30533606 0.37791314 0.14964229] [-0.13118259 -0.32867068 0.18038018 0.02444958 -0.04725876 -0.26589785 0.37444314 0.33571181 -0.31668501 0.17471018] [ 0.12539266 0.03421996 -0.16533064 0.25065169 0.11262873 -0.250109 0.05621285 0.14263656 -0.13638187 -0.16992094] [ 0.39663391 -0.23377007 0.12086686 -0.08139474 -0.1618067 -0.39339115 -0.15129964 -0.25654682 0.22949898 0.53120939]] 59. How to sort an array by the nth column? (★★☆) 59. 配列をn番目の列でソートするには? 100_numpy_exercises.ipynb(59) Z = np.random.randint(0,10,(3,3)) print(Z) print(Z[Z[:,1].argsort()]) 任意の列でソートするにはargsortを使うのが良いです。上記のコードは2番目の列を基準に昇順にソートされています。 実行結果は以下の通りです。 100_numpy_exercises.ipynb(59)-output Z = np.random.randint(0,10,(3,3)) print(Z) print(Z[Z[:,1].argsort()]) 60. How to tell if a given 2D array has null columns? (★★☆) 60. 与えられた2D配列にNULL列があるかどうかを見分けるには? 100_numpy_exercises.ipynb(60) Z = np.random.randint(0,3,(3,10)) print((~Z.any(axis=0)).any()) anyはNULL列があるかどうか見分けるのに使えます。 実行結果は以下の通りです。 100_numpy_exercises.ipynb(60)-output False 今回は以上になります。 次回は61~70問をやっていきます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

NFCpyにUIをつけたい

動機 リーダーの上に何か載せたら情報が表示されるものがつくりたい 例えば、薬を載せたら何に使うものか表示されたり、いろいろ使えそう 構成 簡単にできるかと思ったのですが、技量不足のため難儀しました 結果から話すと、UIとReaderを分け、watchdogでテキスト監視し変化があったら内容を読み取る方式にしました ダメだったのは、1ファイルで同じループ内に書くとどちらかが止まる、Scocket通信でも止まりました スクリプト 実行ファイルと同じ階層にtemp/nfc.txtがあり、 Reader -> nfc.txt 書き込み UI   -> nfc.txt 監視 変化があれば内容を読む tag1,2,3であれば対応した静止画を表示 Reader nfcsimple.py import nfc import binascii import sys import os globalDirpath = os.path.dirname(__file__) txtpath = globalDirpath+'/temp/nfc.txt' def on_connect(tag): if tag.ndef: if tag.ndef.length > 0: print("NDEF Message:") for i, record in enumerate(tag.ndef.records): print(record.text) #tag1 with open(txtpath, mode='w') as f: f.write(record.text) return True def main(): while True: with nfc.ContactlessFrontend("usb") as clf: rdwr = { 'on-connect': on_connect } clf.connect(rdwr=rdwr) if __name__ == '__main__': main() UI ui.py import pygame import sys import os from pygame.locals import * from watchdog.observers import Observer from watchdog.events import FileSystemEventHandler import threading imgBtlBG = pygame.image.load("img/bg.jpg") imgBtl1 = pygame.image.load("img/01.jpg") imgBtl2 = pygame.image.load("img/02.jpg") imgBtl3 = pygame.image.load("img/03.jpg") globalDirpath = os.path.dirname(__file__) dirpath = globalDirpath+'/temp/' class ChangeHandler(FileSystemEventHandler): isPlay = False NfcTag = "None" def StopOverlay(self): self.isPlay = False self.NfcTag = "None" def on_modified(self, event): filepath = event.src_path filename = os.path.basename(filepath) if filename=="nfc.txt": with open(filepath) as f: s = f.read() print(s) self.isPlay = True self.NfcTag = s t=threading.Timer(3,self.StopOverlay) t.start() event_handler = ChangeHandler() observer = Observer() observer.schedule(event_handler, dirpath, recursive=True) observer.start() def draw(bg): global isPlay,NfcTag if event_handler.isPlay: if event_handler.NfcTag=="tag1": bg.blit(imgBtl1, [0,0]) elif event_handler.NfcTag=="tag2": bg.blit(imgBtl2, [0,0]) elif event_handler.NfcTag=="tag3": bg.blit(imgBtl3, [0,0]) else: bg.blit(imgBtlBG, [0, 0]) def main(): pygame.init() pygame.display.set_caption("Title") flags = pygame.DOUBLEBUF | pygame.HWSURFACE | pygame.FULLSCREEN screen = pygame.display.set_mode((0, 0), flags) clock = pygame.time.Clock() while True: for event in pygame.event.get(): if event.type == pygame.QUIT: observer.stop() observer.join() pygame.quit() sys.exit() draw(screen) pygame.display.update() clock.tick(30) if __name__ == '__main__': main() 実行 cd /projectdir python3 nfcsimple.py & python3 ui.py gif youtube 全パターン タグに書き込む場合、こちら
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Python で Webスクレイピングする

Selenium を使ってPython で WEBスクレイピングしてみる 1. 環境 内容 バージョン OS Windows 10 Pro (64bit) Chrome 92.0.4515.159 ChromeDriver 92.0.4515.107 Python 3.9.0 selenium 3.141.0 Visual Studio Code アイテム バージョン バージョン 1.59.1 (user setup) コミット 3866c3553be8b268c8a7f8c0482c0c0177aa8bfa 日付 2021-08-19T11:56:46.957Z Electron 13.1.7 Chrome 91.0.4472.124 Node.js 14.16.0 V8 9.1.269.36-electron.0 OS Windows_NT x64 10.0.19042 Python 3.9.0 64bit 2. 初期設定 Seleniumは、PythonのコードからWEBブラウザを操作する。 操作するためには、WebDriverが必要。 2-1. インストール 2-1-1. Selenium インストール pip install selenium 2-1-2. ChromeDriver のインストール 一度、以下のコマンドでインストールしましたが、 pip install chromedriver-binary Chrome と Chromedriver のバージョンが一致しない問題で動かなかったので、 Chrome のバージョンチェック > 92.0.4515.159 Chrome Driver のサイト で Chrome と近いバージョンのものをダウンロード > ChromeDriver 92.0.4515.107 Zip を解凍、中身の chromedriver.exe をプログラムのあるディレクトリに置いてダブルクリック。 以上でバージョンを揃えて動かすことが可能に。 以下参考サイト。 3. サンプルコードから内容について確認 sample.py import time from selenium import webdriver from selenium.webdriver.common.keys import Keys #Chromeを操作 driver = webdriver.Chrome() driver.get('https://www.google.com/') time.sleep(5) search_box = driver.find_element_by_name("q") search_box.send_keys('ChromeDriver') search_box.submit() time.sleep(5) driver.quit() 以下、内容について。 from selenium.webdriver.common.keys import Keys Keys クラスで、RETURN、F1、ALTなどのキーボードのキーを提供 driver = webdriver.Chrome() Chrome WebDriver のインスタンスを作成。 Chrome が立ち上がる。 driver.get('https://www.google.com/') driver.get メソッドは、URLによって指定されたページに移動。 WebDriverは、テストまたはスクリプトが実行する前に、ページが完全にロードされる(つまり、「onload」イベントが発生する)まで待機。 ※ページがロード時にたくさんのAJAXを使用すると、WebDriverは完全にロードされたかどうかわからないということに注意する必要あり。 search_box = driver.find_element_by_name("q") WebDriverでは、find_element_by_* メソッドのひとつを使って要素を見つけることができる。 今回の入力テキスト要素は、find_element_by_name メソッドを使用して name 属性で検索。 コード 機能 by_id(id) id属性がidである要素 by_class_name(name) クラス名がnameの要素 by_name(name) タグ内のname属性が一致する要素 by_tag_name(name) タグ名が一致する要素 by_link_text(text) テキストに完全一致する要素 by_partial_link_text(text) テキストに部分一致する要素 by_xpath(path) xpathが一致する要素 search_box.send_keys('ChromeDriver') キーを送信。これはキーボードを使用してキーを入力するのと同じ。 特殊キーは、 selenium.webdriver.common.keys からインポートした Keys クラスを使用して送信。 安全のため、あらかじめ入力フィールド(例:「検索」)に入力したテキストをクリアする。 そうすれば、検索結果には影響を及ぼさない。 今回で言えば、ChromeDriver という文字列でGoogle検索開始。 search_box.submit() なくても動きました。 よくわかりませんでしたが、フォームを送信するときに利用する? driver.quit() すべてのウィンドウを閉じる。 4. 要素の取得の仕方 Chrome の場合、Ctrl + Shift + I 検証コンソール左上の選択矢印クリック 知りたい要素クリック 参考にさせて頂いたサイト
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

paizaラーニング 「長テーブルのうなぎ屋」の考え方【初級】 (Python)

この記事はネタバレを含みます。 まずは自分で挑戦してみましょう 問題へのリンク:長テーブルのうなぎ屋 この問題は「paizaラーニング」問題集の問題のため、解説・解答の公開が許されています。 スキルチェックの問題は内容に関わる一切が公開を禁止されています。 気をつけましょう。 この記事で作成するコードは不完全です。 あくまで「考え方」と「考えをコードにする」ことが目的です。 「処理の最適化」までは考えないものとしています。 目次 0. 読み方 1. 問題理解 2. やること 3. 処理組み立て 4. 構築 5. 動作確認 6. おわりに 0. 読み方  深く考えず書いたら「3. 処理組み立て」がえらく長くなりました。  そのため前書きとして一つご忠告します。  基礎的なことはわかってる! という方は「3. 処理組み立て」は飛ばしていいと思います。かなーり基礎的なことから触れています。  レベルとして目安は「2. やること」を読んで、もう頭にソースコードがふわふわと浮かんでくるなら「3. 処理組み立て」は読んでも面白くないと思います。  2 → 4と読んで、「なんだこれ?」となったら、その項目だけ3の該当箇所を読めばいいと思います。(飛ばして読んだ場合「おさらい」の中身がメインになると思います)。  また、そもそもとしてこの記事のターゲットは「わからなかった人」としています。わかってる人は最後のコードだけさらっと見て、筆者を鼻で笑う程度にしてください。  また、ページ内ジャンプを実装するため、ナンバリングがぐちゃぐちゃになりました。申し訳ない。 1. 問題理解  プログラムを作るのに「うなぎ屋」だの「江戸っ子」だのは必要ないです。何が必要かを判断し、単純な形にできるところは単純にして考えます。   最終的に必要な情報 = 椅子に座っているお客さんの人数  では椅子に座っている人数はどうすれば分かるか?  国語・算数の問題です、文章から以下のことが読み取れます。   ①椅子はN個、ひとつのテーブルの周りに置かれている   ②客はグループ毎に案内される   ③グループは案内された場所に時計回りに順番に座る   ④座ろうとした場所に先客がいたら、その人は座れない   ⑤グループで1人でも座れなかったら、グループ全員が即帰る  こんなところでしょうか。  で、難しいのは”テーブルの周りに置かれている”ところでしょうか。円形で周りをグルグル回る処理とは…よく分かりません。  なので、こう考えてください。1からNの椅子は四角いテーブルの1辺に1列で並んでいると。  そうすると、朧気ながら見えてくるはずです、配列という文字が…  簡略化した考えでも「隣である」ことは、「そのように認識していれば」変わっていません。  プログラミングでは「こう考えれば同じこと」という発想が大事です。 ※あ、図が反時計回りになってる...ゴメンゴ( ´_ゝ`) 2. やること  paizaの問題に限って言えば、やることは大きく3つです。   1.入力を受け取る   2.処理をする   3.出力する  時にはそれぞれが重なったり1つにまとまったりしますが、本筋は変わりません。  この3つを問題に合わせて細分化していきます。  まず入力を受け取ります。入力では以下のものが来ることがわかています。 入力はm+1行から成ります。 1行目にはn(座席数)とm(グループ数)が半角スペース区切りで入力されます。 i+1行目(1≦i≦m)には2個の整数a_i(グループの人数)とb_i(着席開始座席番号)が半角スペース区切りで入力されます。  2行目以降は何行来るのかはこの時点ではわからないので、最初に1行目のn, mだけを受け取ります。  やること1:n, m を受け取る  次は、まだ入力が残っているので、それを受け取らないといけません。  mを受け取ったのでこれで何行来るのか把握できました。繰り返し処理で各グループの人数・座る位置を受け取ります。  やること2:m回の繰り返しを行う  やること3:繰り返しの中でa_i, b_i を受け取る  さてこれで全部の入力を受け取りました。では処理を考えます。  グループの数はmですので、これもまた繰り返しを使います。  やること4:m回の繰り返しを行う  そして座席に着席できるかチェックして、全員が座れるなら座ってもらい、座れない人がいたら特に何もしません(勝手に帰るだけ)。  ここでポイントとして、座ってもらえたらそれを記録していかないといけません。次のグループでのチェックができないためです。  やること5-0:事前に着席結果を記録するものを作る  やること5-1:全員が座れるかチェック  やること5-2:座れたら着席済みの席を記録  5-1について少し深堀します。  座れるかのチェックでは何をするかというと、グループの一人一人について着席予定の椅子に人がいないか確認します。一人一人ということはこれもまた繰り返し処理です。全部が空席だった時だけ成功です。逆に言えば途中一人でも座れなかったら失敗です。  やること5-1-1:a_i回の繰り返しを行う  やること5-1-2:着席結果の記録から着席予定の場所に人がいるか確認する  やること5-1-3:全員が座れる状態なら成功  全グループの案内を繰り返し終わったら、処理は終了です。  取っておいた記録から座った人数をカウントして出力します。  やること6-1:記録内容から着席済みの席カウントする  やること6-2:標準出力に表示  以上です。  最後にもう一度やることをおさらいしておきます。  やること1:n, m を受け取る  やること2:m回の繰り返しを行う  やること3:繰り返しの中でa_i, b_i を受け取る  やること4:m回の繰り返しを行う  やること5-0:事前に着席結果を記録するものを作る  やること5-1:全員が座れるかチェック  やること5-1-1:a_i回の繰り返しを行う  やること5-1-2:着席結果の記録から着席予定の席に人がいるか確認する  やること5-1-3:全員が座れる状態なら成功  やること5-2:座れたら着席済みの席を記録  やること6-1:記録内容から着席済みの席カウントする  やること6-2:標準出力に表示 ☆★ 完璧に把握した、4に飛ぶ ★☆ 3. 処理の組み立て  やることもわかったので、ここから処理を考えていきます。  Pythonで記載していきます。 3-1. やること1:n、mを受け取る  n, mは1行にまとまって空白区切りの数字でやってきます。  数字ではありますが受け取った瞬間は”文字列”として受け取っている状態のため、数値型に変換してから受け取ります。 n, m = ( int( x ) for x in input().split() )  input() で標準入力を見て1行を持ってきます。この時は文字列です。  split() で文字列を分割します。()の中を空にするとデフォルト値として空白で区切ってくれます。この時もまだ文字列ですが、分割したことで”文字列の配列”になっています。  for x in 配列 で配列の中身を一つ一つ取り出してxに入れています。つまりxもまだ文字列です。  ( int(x) for ~ ) で文字列だったxを数値に変えるint()を使います。外側の()はジェネレータというものを作っています、なんか便利なやつぐらいの認識でいいです。  n, m = ~ 変数に値を格納します。~の部分が配列など複数要素を持つ値で、左辺の変数の個数と要素の数が一致していれば、それぞれの要素を変数に入れてくれます。 3-X. 整理   やること2:m回の繰り返しを行う   やること3:繰り返しの中でa_i, b_i を受け取る   やること4:m回の繰り返しを行う   やること5:繰り返しの中でなんかいろいろ  ここで一旦やることを振り返ってみます。「m回の繰り返し」が2回出てきますが、実はこれ、まとめることができます。  なぜかといえば「やること5」の中で使う情報は「着席の記録」「iグループ目の人数」「iグループ目の座る位置」なので、全部を持っておく必要はないです。  つまりは   やること2 & 4:m回の繰り返しを行う   やること3:繰り返しの中でa_i, b_i を受け取る   やること5:繰り返しの中でなんかいろいろ  と読み替えることができます。  では繰り返しを書こう・・と思いきや、もう一つ見直す部分があります。   やること5-0:事前に着席結果を記録するものを作る  プログラムには「スコープ」という考えがあり、下部の処理が勝手に作ったものは上の処理・横の処理からは見えない、となっています。 ※追記 Pythonでは図の場合はエラーにならないそうです。 ifやforの場合は「処理が通過していれば定義され抜けた後も保存される。通過しなかった場合は定義が発生していないので未定義でエラー。」だそうです。 Javaのつもりで書いてしまいました、申し訳ありません。  なので繰り返しの中で共有して使いたい「着席結果を記録するモノ」を、繰り返しに入る前に作っておく必要があります。 3-2. やること5-0:事前に着席結果を記録するものを作る  作るものは↓です。  赤枠部分がメモできるようになっていればいいです。ここでは人がいない状態を'0'、人が座っている状態を'1'として記録することとします。(自分がわかりやすいように設定していいです。)  図ではMax14になってますが、プログラムでは椅子の総数は n で渡されています。 chair = [0] * n  [初期値] * 要素の数 ですべての要素に初期値を入れた状態の配列が作れます。  ついでにもう一つ変数を用意します。最後に出すのは座っている人の合計人数で、座席の着席状態ではありません。数字だけ集計するための変数を作ります。 result = 0 3-3. やること2、4:m回の繰り返しを行う  繰り返しを表現します。forを使うのが一般的です。 for _ in range( m ): #~ forの中 #~ forの中 #~ forの中 #~ forの外  range(数値) で指定した数の要素数を持つ配列が入手できます。  要素は0から始まり指定した数値の手前までの数値が入っています。  range(3) なら [ 0, 1, 2 ] が入手できます。  for 変数 in 配列 で配列の中身を個別に取り出せることは前述しました。ここでは変数に _ (アンダースコア)を使っています。これには特別な意味があり、「とってきた値そのものは使わないよ」と表現しています。  for ~: と:(コロン)を付けたことにより「次からの行がforによって管理される」という表現になります。  範囲は「次の行のインデントが保たれている間」になります。Pythonのインデントは意味づけがされているので誤ってスペース1個消しただけでエラー(もしくは予期せぬ動き)になります。気を付けましょう。 3-4. やること3:繰り返しの中でa_i、b_iを受け取る  これは「やること1」と同じですのでさらっと。 g, s = ( int( x ) for x in input().split() )  ここの変数g, sは繰り返し毎に新しいものを取ってくるため、繰り返しの中で定義して大丈夫です。gにa_i、sにb_iが入っています。 3-5. やること5-1:全員が座れるかチェック  チェックを始める前に準備を入れます。  次から繰り返しが始まりますが、一度繰り返しに入ってしまうと相互の連携が取れなくなります。  座れるかどうかの最終結果がわかる変数を作っておきます。この値が最後までTrueだったらこのグループは座ることができます。 flg = True 3-6. やること5-1-1:a_i回の繰り返しを行う  座席のチェックのため、グループの人数で繰り返し処理します。  先ほども繰り返しが出ましたが、一つ異なる点があります。  図を見て動きをイメージしてください。  例えば座席の番号が7の場所に案内されたら、そのグループは7,8,9,...と順番に座れるかを確認します。そのためにはまず7,8,9,...の番号全部を入手しないといけません。今現在渡されているのは最初の番号7と人数だけです。  そこで、7を基準に相対位置を考えると、図のように0,1,2,...という並びになっています。先ほどの range(数値) が有効活用できます。  そのため、今度のforはこう書きます。 for i in range(g): これでiに「案内位置からの相対位置」が入るようになります。 3-7. やること5-1-2:着席結果の記録から着席予定の席に人がいるか確認する  ようやく実際に人がいるか確認していきます。  記録処理としての登場はまだですが、確認後に着席状態を記録している変数がありました。その値がどうなっているか確認しましょう。 index = (s + i - 1) % n if chair[index] == 1: #~ if判定が真だった時に入れる flg = False else: # 判定が偽だった場合の処理、今回は不要 #~ if判定が偽だった時に入れる、今回は不要 #~ if判定が偽だった時に入れる、今回は不要 #~ ifを抜けた後  s + i - 1で記録用に作った配列での参照場所を計算しています。  ん?-1ってなんだ?と思いますね。実はプログラミングの世界ではイメージとずれてしまう部分があります。  それは配列の番号が0始まりであることです。  つまり   arr = [ 5, 2, 8 ] という配列があったとすると   arr[0] => 5   arr[1] => 2   arr[2] => 8  というように値が取れてきます。  座席の番号は1から順番につけられているので、配列から取り出すときは番号としては1ずれるのです。  (数値) % n は何をしているのかというと、最後の番号nから1につながるようにmod計算を入れています。  modとは何かというと”割り算の余り”のことです。  例えば「5 ÷ 2 = 2 余り 1」なので、「5 mod 2 = 1」となります。このmodをプログラミングでは%で表しています。  なんでmodなんかが出てきたかというと、「余り」が「nをこえたら1から再出発」と同じ意味になるからです。 ※追記:10を超えていない場合はmod計算しても値が"変わらない"ことも注目です。 例)7 ÷ 10 = 0 余り 7  長くなりましたが  index = (s + i -1) % n によって座席の記録用に作った配列の正しい位置を入手しています。わかりやすいように変数に入れておきます。  if chair[index] == 1: で配列の記録が1と等しいかを判定しています、1はその席に人が座っている場合の数字でした。つまり「その席に人がいたらifの中に入るよ」という意味になります。  flg = False は座ってる人がいたときのみ行われる処理です、ここで値をTrue(座れる)からFalse(座れない)に変更します。  else: はifの条件にあわなかった場合の処理ですが、「座れるなら次の人の確認に進む」ため、何も処理しなくていいです。  逆に「座れるからflg = Trueにしよう」なんてしてしまうと、座れなかった時の確認結果が上書きされるのでやってはいけません。 3-8. やること5-1-3:全員が座れる状態なら成功  「やること5-1-1」で作った繰り返しが終われば、グループ全員の座れるかチェックが完了したことになります。  for を抜けた後にflgの値を確認します。 if flg: # 全員が座れる時の処理  値の確認ですが、記号==を使わなくても大丈夫です。理由はflgに使ったデータ型にあります。  bool型といいif文の判定に使われる真偽値を表します。判定結果が真のことをTrue、偽のことをFalseという値で表しています。これは文字列や変数ではなく”値そのもの”です。  変数flgには「判定結果そのもの」が入っているので、ifに直接読み込んでもらうことができます。 3-9. やること5-2:座れたら着席済みの席を記録  「やること5-1-3」のifの中に着席に成功したグループの処理を書きます。  つまり「着席結果の記録」を「新たに今チェックしたグループが座った状態」に更新ます。  もう一度「やること5-1-1」の繰り返しと同じものを作ります。さらに「やること5-1-2」の確認処理を、今度は更新処理としてアレンジします。 for i in range(g): index = (s + i - 1) % n chair[index] = 1  すでに確認済みのため、ただただ着席完了(=1)に更新していきます。 3-10. やること6-1:記録内容から着席済みの席をカウントする  着席済みの人数のカウントですが、記録した変数があるのでこれを全部見渡せば人数はわかります。  でも、めんどくさいです。嫌だやりたくない、そう感じざるを得ない。全くの無知識でこの感覚になったなら、あなたはプログラミング向いているかもしれません。  「着席する毎に」「グループの人数=着席した人数」がわかっているのですから、座ったタイミングでカウントアップしていけば人数がわかりますね?  そういえばまだ使ってない変数を最初に作りましたね、ここで使います。 result += g  += は”インクリメント”という表現で、次の2行は同じ処理になります。   num = num + 3   num += 3  はいわかりづらい、どういうことかといえば「現在のnum(変数)に+3を処理して”更新する”」という記号です。その変数は使いまわしたいけど値は随時更新していきたい、そういう感じです。 3-11. やること6-2:標準出力に表示  ...ついに...戦いは...終わった...。  集計した人数を標準出力に表示します。 print(result)  print(表示したいもの) でPython君は標準出力にんべって出してくれます。  引数に表示したいものだけを入れると改行を追加して出してくれます。「指定してねーのに勝手に出すなや!」なんて言わないでください。 4. 構築  終わったと思った?惜しい残念、まだ完成ではないです(ほぼ終わってますが)。  今はやりたいことをPythonで書き並べただけです。これをちゃんと動くように組み立ててあげないといけません。  途中で説明したようにPythonはインデントで処理範囲を判断しています。  この記事ではここまでわざとできるだけインデントを書かないようにバラバラに説明してきました。  これらの処理を「適切に」並べていく必要があります。  長い説明で頭が混乱していると思いますが、もう一度おさらいしてみましょう。  ...もういい?了解です、振り返りたい人だけどうぞ。 おさらい やること1:n, m を受け取る 3の説明を見る n, m = ( int( x ) for x in input().split() ) やること5-0:事前に着席結果を記録するものを作る 3の説明を見る chair = [0] * n result = 0 やること2 & 4:m回の繰り返しを行う 3の説明を見る for _ in range( m ): #~ forの中 #~ forの中 #~ forの中 #~ forの外 やること3:繰り返しの中でa_i, b_iを受け取る 3の説明を見る g, s = ( int( x ) for x in input().split() ) やること5-1:全員が座れるかチェック 3の説明を見る flg = True やること5-1-1:a_i回の繰り返しを行う 3の説明を見る for i in range(g): やること5-1-2:着席結果の記録から着席予定の席に人がいるか確認する 3の説明を見る index = (s + i - 1) % n if chair[index] == 1: #~ if判定が真だった時に入れる flg = False else: # 判定が偽だった場合の処理、今回は不要 #~ if判定が偽だった時に入れる、今回は不要 #~ if判定が偽だった時に入れる、今回は不要 #~ ifを抜けた後 やること5-1-3:全員が座れる状態なら成功 3の説明を見る if flg: # 全員が座れる時の処理 やること5-2:座れたら着席済みの席を記録 3の説明を見る for i in range(g): index = (s + i - 1) % n chair[index] = 1 やること6-1: 記録内容から 着席済みの席をカウントする 3の説明を見る result += g やること6-2:標準出力に表示 3の説明を見る print(result)  はい、長かったですが今までのことをちゃんと組み上げたら終わりです。  次が組みあがったものになります。 完成コード # 処理実装部 def main(): ## ↓もし動かすだけならここからでいい↓ ## ## これだけにするときはインデントに気を付けて ## # input取得 n, m = ( int( x ) for x in input().split() ) # 変数定義 chair = [0] * n result = 0 # グループ単位繰り返し開始 for _ in range( m ): # グループ情報取得 g, s = ( int( x ) for x in input().split() ) # チェック結果変数 flg = True # チェック開始 for i in range( g ): index = ( s + i - 1 ) % n if chair[index] == 1: # 席に人がいたら更新 flg = False # チェック結果確認 if flg: # 座れる場合のみ処理 for i in range( g ): index = ( s + i - 1 ) % n chair[index] = 1 result += g # 結果表示 print( result ) ## これだけにするときはインデントに気を付けて ## ## ↑もし動かすだけならここまででいい↑ ## # main処理呼出部 if __name__ == '__main__': main()  以上で本当に完成です。 5. 動作確認  では動くのを確認します。  入力例1  入力例2  よさそうですね。  ではpaizaラーニングに投げてみます。  クリアです。 6. おわりに  冒頭で注意書きしたように、実はこの処理は提出コードとしては質が悪いものです。  なぜかといえば無駄があるからです。それは座れるかのチェックの際に「座れない人がいることが分かった時点で帰る」という条件を使えていないためです。  今回の問題では別に全探査してもそれほど差は出ないですが、Aランク・Sランクでは無駄なことを処理していると時間制限に引っ掛かり、出るはずの出力結果が正解でもタイムオーバーで×になります。 とある男の敗退結果 おそらく最終的な結果はあいますが、あまりにも無駄が多く処理に時間がかかる。 言語はJavaのため1回の処理の制限時間は各5秒(各言語の制限時間 ※Time limit欄を参照)。  ここで書いた処理はあくまで「考えたことをトレースした」処理です。  いったんそれでも大丈夫ですが、まだゴールは先にあるということだけは、忘れないでください。  ページ内ジャンプを実装するため、ナンバリングがぐちゃぐちゃになりました。申し訳ない。 おまけコード これは解説しません。 # coding: UTF-8 ch = [] def main(): global ch n, m = ( int( x ) for x in input().split() ) r = 0 ch =[False] * n for i in range(m): g, s = ( int( x ) for x in input().split() ) if (check(n, g, s - 1)): r += g print(r) def check(n, g, s): global ch if (g == 0): return True if (ch[s % n]): return False if (check(n, g - 1, s + 1)): ch[s % n] = True return True return False if __name__ == "__main__": main()
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[初心者向け]WindowsにPythonをインストールする前に読んでほしい

はじめに WindowsでPython環境を構築する際、いろんな方法が出てくるので、初心者にとって何がベストか分かりません。 Pythonの場合は開発コミュニティが活発であるため、Python本体やライブラリも含めて頻繁にバージョンアップされます。 ただバージョンアップすることで、環境起因のバグが発生して、沼にハマってしまうことがあります。 そこで、このような問題を避けるため、バージョン別のPython仮想環境を構築するのが定石です。 今回、Python仮想環境を構築するシンプルな方法について、まとめます。 Python環境構築の流れ 1.インストーラを入手 2.インストール先ディレクトリを作成してPythonをインストール 3.仮想環境用のディレクトリを作成して仮想環境を構築 4.仮想環境にライブラリをインストール 1. インストーラを入手 こちらのサイトからインストーラをダウンロードします。 2. インストール先ディレクトリを作成してPythonをインストール 任意の場所にインストール先ディレクトリを作成します。 (今回はユーザディレクトリにpyverというフォルダを作りました。) インストーラを起動してCustomize installationをクリックします。 インストール先ディレクトリを指定して、更にPythonバージョンがわかるようにディレクトリを指定します。(今回はpy383とした) 3. 仮想環境用のディレクトリを作成して仮想環境を構築 仮想環境用のディレクトリを作成します。 (今回はユーザディレクトリにpyprojというフォルダを作りました。) コマンドプロンプトを起動して、作成したディレクトリに移動します。 その後、先程インストールしたpythonのコマンドで仮想環境を作成します。 (今回はmy_envという仮想環境名として作成しました) c:\Users\[ユーザ名]\pyver\python -m venv my_env [仮想環境名]\Scripts\activateと実行することで、仮想環境に切り替えができます。 pythonと実行することで、Python Shellが表示されます。 4. 仮想環境にライブラリをインストール pip installコマンドでPythonライブラリをインストールします。 インストール後、importすることができます。 まとめ WindowsにPython仮想環境を構築して、ライブラリをインストールする手順をまとめました。 新しいバージョンのPythonが出た場合も、同様の手順で環境を構築することが可能です。 余計なシステムファイルが増えたり、環境変数が変更されるなど、PC環境を汚さず構築できるため、おすすめです。 以上です。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Flask2でGET/POSTの値を取得する

はじめに 前回の続きで、EC2上に構築したFlask2でGET/POSTの動作確認の防備録。 REST APIでは必須のJSONの連携もまとめていきます。 EC2にPython/Flask2を構築 GET Flask 1.*までの書き方 引数のmethodsを付けないとデフォルトはGETです hoge.py @app.route('/') def index(): return "世界のみなさん、こんにちは" @app.route('/get1x', methods=["GET"]) def get1x(): return "GETです" Flask起動 flask run curlで動作確認 curl http://127.0.0.1:5000/get1x GETです Flask2の新しい書き方 getメソッドが増えた(laravelっぽい書き方?) hoge.py @app.get('/get2x') def get2x(): return "GET2です" curlで動作確認 curl http://127.0.0.1:5000/get2x GET2です パラメーター hoge.py @app.get('/get2x') def get2x(): return request.args["name"] + "です" curl http://127.0.0.1:5000/get2x?name=yamada yamadaです URLパラメーター hoge.py @app.get('/get2x/<name2>/myname') def get2_myname(name2): return f"{name2}です" curl http://127.0.0.1:5000/get2x/tarou/myname tarouです 小まとめ hoge.py @app.get('/get2x/<name2>/myname') def get2_myname(name2): if "name" in request.args: name = request.args["name"] else: name = "ななし" return f"{name} {name2}です" curl curl http://127.0.0.1:5000/get2x/tarou/myname?name=yamada yamada tarouです POST hoge.py @app.post('/post2x') def post2x(): if "name" in request.form: name = request.form["name"] else: name = "ななし" if "name2" in request.form: name2 = request.form["name2"] else: name2 = "ななし" return f"{name} {name2}です" ''' curl -X POST --data 'name=yamada&name2=tarou' http://127.0.0.1:5000/post2x ``` yamada tarouです JSON取得 jsonで受け取ったデータをjsonで返す importにjson追加 hoge.py from flask import Flask, json, jsonify hoge.py @app.post('/post2x/json') def post2x_json(): # bodyを文字列で取得 body = request.data.decode("UTF-8") # jsonをdict型に変換 json_data = json.loads(body) # 応答メッセージ追加 json_data["message"] = "ok" return jsonify(json_data), 200 curl -X POST -H 'Content-Type: application/json' -d '{"name":"yamada", "name2":"tarou"}' http://127.0.0.1:5000/post2x/json {"message":"ok","name":"yamada","name2":"tarou"} エラー処理を入れてみる hoge.py @app.post('/post2x/json') def post2x_json(): # headersにjsonが指定されていなかったらエラー if "Content-Type" not in request.headers or request.headers["Content-Type"] != "application/json": return jsonify({"message": "json形式ではありません"}), 500 try: # bodyを文字列で取得 body = request.data.decode("UTF-8") # jsonをdict型に変換 json_data = json.loads(body) # 応答メッセージ追加 json_data["message"] = "ok" return jsonify(json_data), 200 except Exception: return jsonify({"message": "jsonの取得でエラーが発生しました"}), 500 curl -i -X POST -H 'Content-Type: application/json' http://127.0.0.1:5000/post2x/json HTTP/1.0 500 INTERNAL SERVER ERROR Content-Type: application/json Content-Length: 103 Server: Werkzeug/2.0.1 Python/3.7.10 Date: Fri, 27 Aug 2021 05:58:25 GMT {"message":"json\u306e\u53d6\u5f97\u3067\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f"} まとめ GET/POSTの使い方とパラメーターの値取得でしたが、PUT/DELETEでも基本は同じで行けると思う。 Flask2になってlaravelっぽい書き方が出来るようになったのかな Flask1の書き方でもOKだし、この辺はあまり変わってなさそうだ 次回はBlueprintでもまとめてみようかな 今回のサンプルコードはGitHubにアップしております github/flask2_demo
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

DataFrameでcolumn名の異なる列を縦に結合する。

状況 DataFrameのColumn名の異なる列を、ひとつの列にしたい。 まず仮のDataFrameを用意します。 import pandas as pd list_A = [["apple", 100], ["banana", 200], ["orange", 300]] list_B = [["potato", 30], ["carrot", 20], ["lettuce", 10]] df_A = pd.DataFrame(data=list_A, columns=["fruits", "price"]) df_B = pd.DataFrame(data=list_B, columns=["vegetables", "weight"]) fruits price 0 apple 100 1 banana 200 2 orange 300 vegetables weight 0 potato 30 1 carrot 20 2 lettuce 10 例えば、fruitsの列とvegetablesの列を一緒のfoodの列にしたいとします。 このままpandasの関数concatで2つのDataFrameを縦結合をしようとすると、以下のようにfruitsとvegetablesの列は別の列になってしまします。 # 縦結合 df_food = pd.concat([df_A, df_B], ignore_index=True) 絶妙に困りますよねこれ…。 解決策 名前を一致させてから、結合する。 # colum名変更 df_food_A = df_A.rename(columns={'fruits': 'food'}) df_food_B = df_B.rename(columns={'vegetables': 'food'}) # 縦結合 df_food = pd.concat([df_food_A, df_food_B], ignore_index=True) 解説 colum名変更 データ.rename(columns={'変更前の名前': '変更後の名前'})でDataFrameのcolum名を変更できます。 # colum名変更 df_food_A = df_A.rename(columns={'fruits': 'food'}) これは便利ですね。 縦結合 concatで縦に結合します。 名前が同じものは、統合されるようです。 このとき、ignore_index=Trueを引数に指定しておくことで、index番号をきれいに順番にすることができます。 df_food = pd.concat([df_food_A, df_food_B], ignore_index=True) 参考 下記を参考にさせていただきました。 ラベル名が異なる場合のmergeについて Pandas 指定した軸の方向にDataFrameを結合するPandasのconcat関数の使い方 読んでいただき、ありがとうございました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

NFCpyでタグに書き込む

hardware setup brew install libusb pip3 install nfcpy script 公式のものを改変 nfcwriter.py import nfc import sys def on_connect(tag): global data if tag.ndef is not None: for record in tag.ndef.records: print(record) if tag.ndef.is_writeable: from ndef import TextRecord tag.ndef.records = [TextRecord(data)] print("write complete") def main(): with nfc.ContactlessFrontend("usb") as clf: rdwr = { 'on-connect': on_connect } clf.connect(rdwr=rdwr) if __name__ == '__main__': global data if len(sys.argv) > 1: data=sys.argv[1] print("Put NFC-Tag") main() else: print("Need Record-Text") 実行 python3 nfcwriter.py helloworld
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Google Colaboratory+OpenCVでWebカメラ画像から顔検出

はじめに この記事は,OpenCVとGoogle Colaboratory (以下Colab) を使って,PCに接続されているWebカメラで画像を一枚キャプチャし,顔を検出するPythonプログラムを作成した備忘録です. Colabの詳細は他の情報源が多数存在するので譲りますが,ColabではPython+OpenCVが動作する上に,PCに接続されているWebカメラからの画像ストリームをキャプチャするコードスニペットを利用できます.OpenCV3.3以降のdnnモジュールでは学習済みモデルを提供しているため,学習に時間を費やすことなく手軽に物体検出などのプログラムを実行できます. 本記事のコードが書かれているColabノートブック 本記事のコードは下記のノートブックの形でも公開されています. 参考文献 本記事中のコードは,Colabにより提供されているコードスニペットと下記のFace detectionコードを改変しつつ利用しています. 動作環境 2021.08.27時点の最新版です. Google Colaboratory https://colab.research.google.com/ MacBook Air (M1, 2020) Chrome 92.0.4515.159 (Official Build) (arm64) 解説付きのコード 前処理 まず,ヘッダとして様々なパッケージをインポートします. # Google Driveをマウント from google.colab import drive drive.mount('/content/gdrive') # imshowサポートパッチのインポート from google.colab.patches import cv2_imshow # dnn用 from IPython.display import display, Javascript from google.colab.output import eval_js from base64 import b64decode # その他パッケージ import imutils import numpy as np import matplotlib.pyplot as plt import cv2 次に, Webカメラからストリームをキャプチャするコードスニペットです.この部分はGoogle Colabにより提供されています. def take_photo(filename='photo.jpg', quality=0.8): js = Javascript(''' async function takePhoto(quality) { const div = document.createElement('div'); const capture = document.createElement('button'); capture.textContent = 'Capture'; div.appendChild(capture); const video = document.createElement('video'); video.style.display = 'block'; const stream = await navigator.mediaDevices.getUserMedia({video: true}); document.body.appendChild(div); div.appendChild(video); video.srcObject = stream; await video.play(); // Resize the output to fit the video element. google.colab.output.setIframeHeight(document.documentElement.scrollHeight, true); // Wait for Capture to be clicked. await new Promise((resolve) => capture.onclick = resolve); const canvas = document.createElement('canvas'); canvas.width = video.videoWidth; canvas.height = video.videoHeight; canvas.getContext('2d').drawImage(video, 0, 0); stream.getVideoTracks()[0].stop(); div.remove(); return canvas.toDataURL('image/jpeg', quality); } ''') display(js) data = eval_js('takePhoto({})'.format(quality)) binary = b64decode(data.split(',')[1]) with open(filename, 'wb') as f: f.write(binary) return filename ネットワークと学習済みのモデルをダウンロードします.今回はCaffeのモデルを利用します. 以下の2行はColab,もしくはJupyter Notebook環境特有の記述です. !wget -N https://raw.githubusercontent.com/opencv/opencv/master/samples/dnn/face_detector/deploy.prototxt !wget -N https://raw.githubusercontent.com/opencv/opencv_3rdparty/dnn_samples_face_detector_20170830/res10_300x300_ssd_iter_140000.caffemodel メインプログラム まず,ストリームから画像を一枚撮影します.下記はColabにより提供されているカメラキャプチャのコードスニペットの改変です. try: filename = take_photo() img = cv2.imread(filename) print('Saved to {}'.format(filename)) # Show the image which was just taken. #display(Image(filename)) except Exception as err: # Errors will be thrown if the user does not have a webcam or if they do not # grant the page permission to access it. print(str(err)) 次に,画像からblobを作成し,検出器にblobを適用します. # ネットワークと学習済みモデルをロードする print("[INFO] loading model...") prototxt = 'deploy.prototxt' model = 'res10_300x300_ssd_iter_140000.caffemodel' net = cv2.dnn.readNetFromCaffe(prototxt, model) # 幅400画素になるようにリサイズする img = imutils.resize(img, width=400) (h, w) = img.shape[:2] blob = cv2.dnn.blobFromImage(cv2.resize(img, (300, 300)), 1.0, (300, 300), (104.0, 177.0, 123.0)) # 物体検出器にblobを適用する print("[INFO] computing object detections...") net.setInput(blob) detections = net.forward() 最後にネットワークの出力を描画し,出力結果を表示して保存します. for i in range(0, detections.shape[2]): # ネットワークが出力したconfidenceの値を抽出する confidence = detections[0, 0, i, 2] # confidenceの値が0.5以上の領域のみを検出結果として描画する if confidence > 0.5: # 対象領域のバウンディングボックスの座標を計算する box = detections[0, 0, i, 3:7] * np.array([w, h, w, h]) (startX, startY, endX, endY) = box.astype("int") # バウンディングボックスとconfidenceの値を描画する text = "{:.2f}%".format(confidence * 100) y = startY - 10 if startY - 10 > 10 else startY + 10 cv2.rectangle(img, (startX, startY), (endX, endY), (0, 0, 255), 2) cv2.putText(img, text, (startX, y), cv2.FONT_HERSHEY_SIMPLEX, 0.45, (0, 0, 255), 2) # 出力結果を表示して保存 cv2_imshow(img) cv2.imwrite("/content/gdrive/My Drive/Works_OpenCV/OpenCV_Colaboratory/dnn_face_out.jpg", img) 動作結果 confidenceが高すぎて逆に怪しい結果ですが. 今後の改善予定 動画への対応 他のモデルの利用
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Jupyter Notebookでコードの自動補完でエラーがでる

問題 Jupyter-notebookにて 以下画面でtabを押して補完しようとすると、エラーが出る。 [IPKernelApp] ERROR | Exception in message handler: Traceback (most recent call last): 以下略 File "/home/xxxxx/xxxxx/xxxxx/lib/python3.6/site-packages/IPython/core/completer.py", line 1374, in _jedi_matches text[:offset], namespaces, column=cursor_column, line=cursor_line + 1) File "/home/xxxxx/xxxxx/xxxxx/lib/python3.6/site-packages/jedi/api/__init__.py", line 726, in __init__ project=Project(Path.cwd()), **kwds) TypeError: __init__() got an unexpected keyword argument 'column' 原因 標準でpipでインストールされる、補完に使っている標準オプションのJediのversionがあってないのが原因らしい 環境 python 3.6 Jedi 0.18 Ipython 7.26 解決 以下で一発解決。 pip install jedi=0.17.2 イケてない解決 毎回以下のコマンドをいつ込み、Jediの使用をやめる。 毎回jupyter起動するたび、こんなコマンドを打ってられない。 %config Completer.use_jedi = False 以下のコマンドでは何も変わらない。 %config IPCompleter.greedy=True
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

mecab-ipadic-neologdがうまくインストールできないときの対処方法

mecab-ipadic-neologdのインストールがうまくできず、数日かけてやっと解決できたので手順を示します。 必要なパッケージのインストール mecab-ipadic-neologdを動かすにはいくつか必要なパッケージがあるので、それらをインストールします。 以下はMacOSでのコマンドです。(他のOSの方は、mecab-ipadic-neologdのreadmeファイルを見てください。) なお、Jyupter Notebook上ではうまくインストールできなかったので、ターミナル上で実行してください。順番が大事です。(ソフトが入ってないとうまく解凍できずやり直すハメになります。) $ xcode-select --install $ brew install libiconv $ brew install mecab mecab-ipadic git curl xz mecab-ipadic-neologdのインストール準備 必要なファイルをダウンロードします。 $ git clone --depth 1 https://github.com/neologd/mecab-ipadic-neologd.git mecab-ipadic-neologdのインストール ディレクトリを変更してからインストールを行います。 $ cd mecab-ipadic-neologd いよいよインストールです。 $ ./bin/install-mecab-ipadic-neologd -n インストールが順調にいくと、「yes」「no」を質問されますので、ターミナル上に「yes」と入力してエンターします。 動作確認 最後にインストールがうまくいっているか確認します。 ターミナルに次のコマンドを入力します。 $ echo `mecab-config --dicdir`"/mecab-ipadic-neologd" そうすると文字入力できるので試しに「鬼滅の刃っていいよね。」とでも入力してエンターキーを押してみましょう。 なにかしら出てくるはずです。controlキー+Cキーを押して抜けたら動作確認は完了です。 インストールがうまくいっていないときは、「そんなファイルありませんよ」的なメッセージが出てきます。 これでもつまづいてしまったらreadmeファイルを見てください。 以上です。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Qiita APIを使って直近100件のタイトルだけ取得する

技術トレンドを word Cloud で見せたいな、と思った時の技術トレンドの拾い上げ(メモ) 参考記事 Qiitaの新着記事を見たい。なのでQiita APIを叩こう。 from yyanoさん Qiita Developer - API v2 PythonでWeb APIを叩いてJSONをパースする from bow_arrowさん 結論 # python --version Python 2.7.5 # _*_ coding: utf-8 _*_ import requests import urllib import json import sys import codecs from bs4 import BeautifulSoup # webAPIからJSON形式の文字列の結果を取得し返却する def getData(): # proxy設定 proxies = { "http":"proxy.alpha.co.jp:8080", "https":"proxy.alpha.co.jp:8080" } # API url = 'http://qiita.com/api/v2/items?' # APIパラメータセット param = { 'page': '1', # ページ番号(1-100) 'per_page' : '100' # 1ページ要素数(1-100) } # パラメータ文字列作成 paramStr = urllib.urlencode(param) # APIを叩いて変数に結果を格納 response = requests.get(url + paramStr, proxies=proxies) # JSONに変換 jsonData = response.json() # 配列の中にJSONが入っているので配列要素毎にループ for jsonObj in jsonData: # 欲しいのはタイトルだけ print(jsonObj["title"]) return response # main if __name__ == '__main__': # webAPIからJSON形式の文字列の結果を取得する apiRes = getData() 結果 Wordpressで固定ページのURLを変えるには ASP.NET MVC CSV出力メモ [Ruby on rails]ゲストログイン機能 ゲストログインは削除、編集されないようにする GCP Data engineer 合格体験記 Dataiku:社名の由来とその読み方 [JS]簡単なフォームバリデーション 無心でコピペしてGitOpsを試してみた with Argo CD EC2に自動でEIPを割り当ててくれるシェルスクリプト [Mac]Windowのどこをドラッグしても移動できるアプリの実装について OpenSSL で複数のホスト名に対応した自己署名証明書を作成するには IBM Cloud internet services (CIS)からROKSのpodにアクセスしてみた Nest-js Headerデコレータのカスタム JSON [axios] ファイルダウンロードができない or 空ファイルになってしまう現象の解消 PowerAutomateDesktopのexcelまとめ記事はpowerhacksさんの所が良い予感…… flutter_flavorizr + flutter_flavorでFlutterの環境切り替えした後に、iOSでbuildした時の 「Unable to load contents of file list」の解決方法 Node.js(axios)からDirectCloud-BOX APIでファイル一覧の取得とnode話 【GCP】利用料割引しているパートナーを調査 OKtaの検証用アカウントを取得してみる Vue.js開発中のエラー対処 【Ruby on rails】JavaScript 非同期通信のコメント機能でエラーメッセージを出す バリデーション AWSのEC2インスタンスでAmazon Correttoを用いてJDK16を導入する Terraform+SAMでLambda+APIGatewayの環境構築 CodeceptJSのhelper、playWright vs WebDriver QNAPにgitea入れるメモ : :
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

最小二乗法をPythonでやってみる

大学の課題で最小二乗法に関する課題が出たのでGoogle先生に最小二乗法について聞いてみたが、どのように活用していくかを書いている"ロク"なサイトがなかった。なのでここに参考になる記事を書いておこうと思う。ぜひ大学の課題等に役立ててほしい。 最小二乗法 まずは最小二乗法について説明しておこう。最小二乗法とは簡単に言えばそれぞれの点に一番近いような式を求めるというものだ。今回では$y=w_1x+w_2$の一次方程式をそれぞれの点に近くなるように計算を行っていく。($y=ax+b$といったほうがイメージしやすいと思うだろうが今回は他サイトに出てくる変数名を用いるためにこのような式にした。意味は変わらないので安心してほしい。)$y$と$x$は変数で、$w_1$と$w_2$は定数である。つまり、それぞれの点に近くなるような$w_1$と$w_2$を求めていこうというのが最小二乗法である。 それでは例を示していく。まず適当に点をプロットする。 そしてこれらの点に一番近くなるような$w_1$と$w_2$を求め、$y=w_1x+w_2$の線を引くと以下のようになる。 以上が最小二乗法のイメージである。 詳しいところは他サイトを参考にしてほしい。 今回は2つ目に挙げた参考サイト(最小二乗法の下位の導出-機械学習に詳しくなりたいブログ)に出てくる(11)の式を用いて計算を行っていく。ちなみにこの式の導出方法は筆者もよくわからない。興味がある者は頑張って式を追ってみてほしい。 今回用いる式をちょっと変えてここに示しておく。 \begin{pmatrix} w_1 \\ w_2 \end{pmatrix}=\boldsymbol{w}=(\boldsymbol{X}^T\boldsymbol{X})^{-1}\boldsymbol{X}^T\boldsymbol{t} この式を計算することによって$w_1$と$w_2$の値を求めることができる。$\boldsymbol{X}$と$\boldsymbol{t}$にはプロットした点の座標の値が入る。$\boldsymbol{X}$には$x$軸の値、$\boldsymbol{t}$には$y$軸の値が入っている(厳密には異なるが、この時点では良いだろう)。 Numpy 最小二乗法では行列の計算を行う。そのため今回はPythonのNumpyという行列を扱えるライブラリを使う。Numpyを使うためにはライブラリをインポートする必要がある。 import numpy as np 行列を定義する 次に行列を定義する。例えば以下の行列をNumpyを使って定義してみよう \boldsymbol{X}= \begin{pmatrix} 1 & 2 \\ 3 & 4 \end{pmatrix} この行列のコードは以下のようになる。 X=np.matrix([[1,2],[3,4]]) Numpyでは行列だけでなくベクトルも定義することができる。 試しに以下のベクトルを定義してみよう。 \boldsymbol{t}= \begin{pmatrix} 5 \\ 6 \end{pmatrix} このベクトルのコードは以下のようになる。 t=np.matrix([[5],[6]]) 行列の演算 今回用いる行列の演算を以下の表に示す。 演算 例 Numpy表現 転置行列 $\boldsymbol{X}^T$ X.T 逆行列 $\boldsymbol{X}^{-1}$ X.I 掛け算 $\boldsymbol{Xt}$ X*t 転置行列をNumpyで表現したかったら行列の変数に.Tをつければよいし、逆行列を表現したかったら.Iをつければよい。掛け算はプログラム言語でおなじみの*だ。 これらを用いることによって(11)式を表現することができる。 w=(X.T*X).I*X.T*t 最小二乗法を用いてみる 最小二乗法を用いて日本のアイスの消費額と平均気温の関係性を求めて、平均気温からアイスの消費額を予想してみよう。まずはデータをそれぞれ見てみる。 年度 2017 2018 2019 アイスの消費額(億円) 5114 5186 5151 平均気温 15.8 16.8 16.5 参考文献 https://news.yahoo.co.jp/articles/88fb0dc4d63b45fd75ef498f98132de28957e03d/images/001 http://www.data.jma.go.jp/obd/stats/etrn/view/monthly_s3.php?%20prec_no=44&block_no=47662 とりあえずこれらのデータをプロットしてみよう。 import matplotlib.pyplot as plt plt.scatter([15.8,16.8,16.5],[5114,5186,5151]) plt.show() データをグラフにプロットさせるためにはmatplotlib.pyplotというライブラリを用いる。1行目はmatplotlib.pyplotをpltという名前で使うぞという感じだ。データをプロットするときはscatterメソッドを使う。scatterメソッドの中にプロットさせたい座標を入力する。具体的にはplt.scatter([x軸の値],[y軸の値])という感じだ。そしてplt.show()はそれらを表示させろというメソッドだ。今回のグラフはx軸に平均気温、y軸にアイスの消費額を割り当てている。 それではここに$y=w_1x+w_2$の線を引くために$w_1、w_2$を求めていこう。 $w_1、w_2$を求めるためには(11)式を用いなければならないし、(11)式を用いるためには$X$と$t$を定義しなければならない。 まず、$X$は以下の通りになる。 \boldsymbol{X}= \begin{pmatrix} 15.8 & 1 \\ 16.8 & 1 \\ 16.5 & 1 \\ \end{pmatrix} x軸の値(平均気温)の値が1列目に入っている。2列目には1が入っているが、なぜこうなるかは先に挙げた他サイトを参考にしてほしい。それではこれをNumpyで表現してみよう。 X=np.matrix([[15.8,1],[16.8,1],[16.5,1]]) 次に$t$は以下の通りになる。 \boldsymbol{t}= \begin{pmatrix} 5114 \\ 5186 \\ 5151 \\ \end{pmatrix} y軸の値(アイスの消費額)の値が入っている。これをNumpyで表現すると以下のようになる。 t=np.matrix([[5114],[5186],[5151]]) それでは(11)式の計算を用いて$w_1、w_2$を求めていこう。 w=(X.T*X).I*X.T*t print(w) この結果は以下のようになった。 [[ 68.60759494] [4027.4556962 ]] つまり、$w_1$=68.60759494、$w_2$=4027.4556962ということである。 これらのことからプロットされたそれぞれの点に一番近くなるような式は$y$=68.60759494$x$+4027.4556962ということがわかる。 この式もグラフに描いてみよう。 p=np.linspace(15.8,17,10) plt.plot(p,68.60759494*p+4027.4556962) np.linspaceは式を描くために必要な変数を作成してくれるメソッドである。ここでは15.8から17までの値を10等分する値を生成している。グラフを見たところ大体いい感じに描けたと思う。それでは求めた式を用いて2020年度の平均気温からアイスの消費額を求めてみよう。正解のデータは以下の通りだ。 年度 2020 アイスの消費額(億円) 5197 平均気温 16.5 2020年度の平均気温である16.5を$y$=68.60759494$x$+4027.4556962の$x$のところに代入してみよう。計算して出た答えは5159.48101271になる。正解が5197なのでまあ合っているだろう(適当)。 最後に 最小二乗法を用いて平均気温からアイスの消費額を予想してみた。答えはそこそこ近かったと思う。今回は一次方程式を用いたが、二次方程式を用いることでより精度を上げたりすることができる。興味があったらやってみてほしい。ただ、最小二乗法も限界があるのでより正確な予想を出したい場合はニューラルネットワークなどを使ったほうがいいと思う。機械学習の基礎としては最小二乗法が考え方がわかりやすいので初心者にはピッタリなのかもしれない。 最後に全体のソースコードを載せておく。これをこのままGoogleColaboratoryに貼り付ければ動くはずだ。 import numpy as np import matplotlib.pyplot as plt X=np.matrix([[15.8,1],[16.8,1],[16.5,1]]) t=np.matrix([[5114],[5186],[5151]]) print(X) print(t) w=(X.T*X).I*X.T*t print(w) p=np.linspace(15.8,17,10) plt.scatter([15.8,16.8,16.5],[5114,5186,5151]) plt.plot(p,68.60759494*p+4027.4556962) plt.show() print(68.60759494*16.5+4027.4556962)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

杉並区内の新型コロナウイルス感染者数(日ごと)のPDFをCSV変換

camelotは変換できなかったのでpdfplumberで変換 年月が省略 プログラム import pathlib import re from urllib.parse import urljoin import requests from bs4 import BeautifulSoup import pdfplumber import pandas as pd headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko" } def fetch_soup(url, parser="html.parser"): r = requests.get(url, headers=headers) r.raise_for_status() soup = BeautifulSoup(r.content, parser) return soup def fetch_file(url, dir="."): p = pathlib.Path(dir, pathlib.PurePath(url).name) p.parent.mkdir(parents=True, exist_ok=True) r = requests.get(url) r.raise_for_status() with p.open(mode="wb") as fw: fw.write(r.content) return p def str2date(s): lst = [None, None] + list(map(int, re.findall("\d+", s))) return lst[-3:] url = "https://www.city.suginami.tokyo.jp/news/kansensho/covid-19/1058987.html" soup = fetch_soup(url) tag = soup.select_one("ul.objectlink > li.pdf > a") link = urljoin(url, tag.get("href")) p = fetch_file(link) with pdfplumber.open(p) as pdf: dfs = [] for page in pdf.pages: for table in page.extract_tables(): df_tmp = pd.DataFrame(table).set_index(0).T if df_tmp.shape[0] > 2: # df_tmp.set_axis(["日にち", "感染者数"], axis=1, inplace=True) dfs.append(df_tmp) df0 = pd.concat(dfs).reset_index(drop=True) df1 = df0[df0["感染者数"].str.endswith("人") & (~df0["日にち"].str.endswith("計"))].copy() # 日付変換 df_date = ( df1["日にち"] .apply(str2date) .apply(pd.Series) .rename(columns={0: "year", 1: "month", 2: "day"}) .fillna(method="ffill") .astype(int) ) df_date["year"] = df_date["year"] + 2018 df1["日にち"] = pd.to_datetime(df_date, errors="coerce") df1["感染者数"] = df1["感染者数"].str.rstrip("人").str.replace(",", "").astype(int) df1.to_csv("suginami.csv", encoding="utf_8_sig")
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

dbtからAmazon Athenaにつないで使ってみる

概要 データ基盤でETL処理を効率的に行うdbtなんですが、標準ではAmazon Athenaが サポートされていません。有志によるAthena用のpluginは作成されているので、 それを使ってAtenaに接続する流れをまとめてみました。 前提 動作確認バージョン python 3.9.5 pythonの環境はpyenv使うなりご自由にどうぞ。 私はpyenvとvenvで構築しました。 手順 1. dbtのインストール 基本的には公式ドキュメントにそって行うだけですが、1点注意があります。 Athena用のモジュールであるdbt-athenaが 0.20.0 までしか対応していないので、 必ず下記のようにバージョン指定でインストールしてください。 指定せずにインストールすると、0.20.1 が入ります。 pip install dbt==0.20.0 終わったらバージョンを確認しておきます。 最新じゃないよ!と言われますが、これでOKです。 > dbt --version installed version: 0.20.0 latest version: 0.20.1 Your version of dbt is out of date! You can find instructions for upgrading here: https://docs.getdbt.com/docs/installation Plugins: - bigquery: 0.20.0 - snowflake: 0.20.0 - redshift: 0.20.0 - postgres: 0.20.0 2. dbt-athenaのインストール dbt-athenaのREADMEにそってインストールします。 # 最新のコマンドはGithub確認のこと pip install git+https://github.com/Tomme/dbt-athena.git インストール後、もう一度確認すると、athena用のpluginが入っている事がわかります。 > dbt --version installed version: 0.20.0 〜 省略 〜 Plugins: - bigquery: 0.20.0 - snowflake: 0.20.0 - redshift: 0.20.0 - postgres: 0.20.0 - athena: 0.20.0 3. profiles.ymlの作成 ここからはdbt動作のための設定です。 まず ~/.dbt/profiles.yml を編集します。 このファイルはdbtインストール時に自動的に作成されるもので、 今回はAthena用の接続設定を作成するので、下記のようなymlになります。 profiles.yml athena-conn: target: dev outputs: dev: type: athena s3_staging_dir: s3://dbt-sample-result/ region_name: ap-northeast-1 schema: dbt database: awsdatacatalog aws_profile_name: user-profile 「athena-conn」のところは、この接続の名前なので任意のもので構いません。 s3_stagion_dirは発行SQLの結果を保存するバケットになります。(実際のデータを保存するわけではないです) databaseは、少し紛らわしいですが、Athenaでいうところの「Datasource」で、標準だと awscatalogになると思います。この項目は英小文字しか許可されていないので注意ください。 aws_profile_nameはdatabaseに接続するためのprofileで、 dbt-athenaでは、credential情報は保持せず、profile指定で接続するようでした。 また、接続に使用するユーザーは athena:StartQueryExecution の権限が必要なので、 権限付与しておいてください。(もしかすると他にも必要な権限あるかもです) 4.dbtのプロジェクト作成 dbtのプロジェクトを作成します dbt init test-project サンプルのモデルやREADMEなどが作成されます。 5. dbt_project.ymlの修正 4で作成したプロジェクトにある dbt_project.yml がプロジェクトの設定ファイルになっていて、 この中に使用する接続情報を記載するところがあるので、profiles.ymlで作成した名前に修正します。 dbt_project.yml profile: 'athena-conn' 6. 接続テスト これでdbtの設定は完了なので、dbt debug で接続テストします。 > dbt debug Running with dbt=0.20.0 dbt version: 0.20.0 python version: 3.9.5 python path: /Users/hoge/dev/dbt-sample/.venv/bin/python os info: macOS-11.4-x86_64-i386-64bit Using profiles.yml file at /Users/hoge/.dbt/profiles.yml Using dbt_project.yml file at /Users/hoge/dev/dbt-sample/test-project/dbt_project.yml Configuration: profiles.yml file [OK found and valid] dbt_project.yml file [OK found and valid] Required dependencies: - git [OK found] Connection: s3_staging_dir: s3://dbt-sample-result/ work_group: None region_name: ap-northeast-1 database: awsdatacatalog schema: dbt poll_interval: 1.0 aws_profile_name: user-profile Connection test: OK connection ok 成功しました! 接続に使用するprofileの権限がおかしかったり、databaseがなかったりするとエラーになるので、 エラーメッセージを見て適宜対応ください。 7. サンプルモデルの作成 サンプルのモデルが含まれているので、下記コマンドで発行します。 dbt run 下記のとおり、データベースとテーブルができあがりました。 あとがき 非公式のpluginなのでもっと詰まるかなーと思いましたが、接続までは意外とすんなりいけました。 まだ深く使い込んでおらず、どんな落とし穴があるかわかりませんが、 Airflowの連携とかも試していきたいところではありますので、 より使い込んで、また知見を共有できればと思います! それでは! 参考リンク dbtのCLI版をインストールして使ってみた https://dev.classmethod.jp/articles/dbt-cli-first/ 公式ドキュメント https://docs.getdbt.com/dbt-cli/installation dbt-athena https://github.com/Tomme/dbt-athena/
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

熱海土石流災害を衛星画像と機械学習で解析

2021年7月に起こった熱海での土石流災害を衛星画像と機械学習を用いて分析してみました。 以前、下の記事で京都の土地被覆状況を衛星画像、機械学習とクラスター分析を用いて解析する方法を書いてみましたが、今回はそれを応用してより、実際起こっている環境問題などに取り組んでみました。 衛星画像データのダウンロード 今回の解析では、Sentinel2のデータを使って分析をしてみました。ダウンロードする方法などは、参考文献および注釈にその方法を説明している記事をリンクしましたのでそちらを参考にしてください。 Sentinel2 :  2021年8月5日 土石気流発生後、晴天で雲が少ない日のデータ 2020年8月15日 土石気流発生前と比較するため1年前の同時期の晴天で雲が少ない日のデータ 分解能: 10m (10メートル以上のものを見分けることとができます。) 解析都市: 熱海近郊 使用バンド: 2、3、4、8。10mの分解能のバンドのみを使用しています。 教師なしクラスター分析 解析範囲の切り出し 方法はいろいろとあると思いますが、今回は画像を表示してそこで解析範囲を指定しました。Sentinel2のデータにはTCI(True Color Image)が含まれていますので、それを使うとはっきりとした画像で解析範囲を探すことができます。 import numpy as np import matplotlib import rasterio # 画像データ with rasterio.open('T54SUD_20210805T012701_TCI.jp2') as src2: data2 = src2.read() plt.imshow(data2[0]) plt.show() ズームボタンで必要な大きさまで拡大して、右下に表示される座標を元に切り出し範囲を指定します。 また黄色で囲んだ部分の明るい部分は雲で、これにより解析を行う熱海市街の海岸近くは雲がかかっていないことが見て取れます。 x-meansで解析 x-meansを使って教師なしクラスター分析を行いました。x-meansの詳細に関しては参考文献および注釈から参考にさせていただいた記事をご覧ください。 データの読み取り 前項の画像を元にまずは、災害地区を含んだ全体像の解析をします。 範囲としては縦1000ピクセル、横500ピクセルです。1ピクセル10mなので、10Kmx5Kmの範囲となります。 コードは京都の記事とほぼ同じですが、使用バンドを指定できるように改良しました。 ''' # 10m x=2200 y=1000 h=1000 w=500 r =[ '02', '03', '04','08'] cluster_num = 15 ''' import numpy as np import rasteri def k_get_raw_data(city,dataset,r,x,y,h,w): mimage = np.zeros((h, w, len(r)), dtype=int) for i in range(len(r)): b = r[i] with rasterio.open(city + '\\' + dataset + b + '.jp2') as src: data = src.read() # 画像サイズの確認 print('Shape: ' + str(data.shape)) print('Dimension: ' + str(data.ndim)) print('Dataset:' + dataset + b ) org_img = data[0] img = org_img[y:y+h, x:x+w] print('Extracted Image Shape: ' + str(img.shape)) mimage[:, :, i] = img new_shape = (mimage.shape[0] * mimage.shape[1], mimage.shape[2]) X = mimage[:, :, :len(r)].reshape(new_shape) return X 機械学習 解析結果の保存部分でも、前回より少し改良してみました。 clusters.sort(key=len, reverse=True) 画像で表示した時に、クラスター番号順に色を表示できるようにしています。 np.save(imagefile + '.data',X_cluster_new) 座標ごとのクラスター番号の割り当てをndarrayデータとして後の解析のために保存しています。 import numpy as np import matplotlib.pyplot as plt from pyclustering.cluster.xmeans import xmeans, kmeans_plusplus_initializer def x_cal_plot(X,city,r,cluster_num,h,w,now): # 画像ファイル listToStr = ' '.join(map(str, r)) imagefile = 'images/x-' + city + '_bandnum_' + listToStr + '_clusternum_' + str(cluster_num) + '_' + now # 初期クラスター数2で開始 initial_centers = kmeans_plusplus_initializer(X, 2).initialize() instances = xmeans(X, initial_centers, kmax=cluster_num, tolerance=0.025, criterion=0, ccore=True) instances.process() clusters = instances.get_clusters() clusters.sort(key=len, reverse=True) X_cluster_new = np.zeros(X.shape[0], dtype=int) for i in range(len(clusters)): print('Cluster ' + str(i) + ': ' + str(len(clusters[i]))) X_cluster_info[0:,i] = i X_cluster_info[1:,i] = len(clusters[i]) # Insert cluster num into X_cluster_new for j in range(len(clusters[i])): X_cluster_new[clusters[i][j]] = i X_cluster_new = X_cluster_new.reshape([h,w]) ############################################# # 画像 fig = plt.figure(dpi=400) fig.suptitle('X-Means ' + city + ': cluster:' + str(len(clusters)) ) plt.get_current_fig_manager().full_screen_toggle() plt.rcParams["font.size"] = 6 import matplotlib.patches as mpatches im = plt.imshow(X_cluster_new,cmap='jet') colors = [ im.cmap(im.norm(value)) for value in range(len(clusters))] patches = [ mpatches.Patch(color=colors[i], label="Cluster {l}".format(l=i)) for i in range(len(clusters)) ] plt.legend(handles=patches, bbox_to_anchor=(1.05, 1), loc=2, borderaxespad=0. ) plt.savefig(imagefile + '.png') np.save(imagefile + '.data',X_cluster_new) 解析結果 熱海全域 2020年8月15日 2021年8月5日 2020年と2021年の解析結果を比較すると、赤で囲まれた部分に明らかな違いが出ています。熱海市 大規模土石流 現地調査 写真レポートを参考にすると、その範囲で土石流災害が起こっているのが分かります。また、海岸線に2020年ではなかった海水とは違うクラスターが見られます。これは、雨によって流れ出た土砂が原因で海水が濁っているからではと考えています。 True Color画像の表示 Sentinel2のバンド2(青)、バンド3(緑)、バンド4(赤)を組み合わせてTrue Colorの画像を作って先ほどの土石流災害の箇所や海岸線がどの様になっているか見てみます。 import rasterio from rasterio import plot from osgeo import gdal def get_color_pic(city,dataset,r,x,y,h,w,now): b = r[0] with rasterio.open(city + '\\' + dataset + b + '.jp2',driver='JP2OpenJPEG') as src0: data0 = src0.read() img0 = data0[0][y:y+h, x:x+w] b = r[1] print('image:' + str(b)) with rasterio.open(city + '\\' + dataset + b + '.jp2',driver='JP2OpenJPEG') as src1: data1 = src1.read() img1 = data1[0][y:y+h, x:x+w] b = r[2] print('image:' + str(b)) with rasterio.open(city + '\\' + dataset + b + '.jp2',driver='JP2OpenJPEG') as src2: data2 = src2.read() img2 = data2[0][y:y+h, x:x+w] # Set image file name listToStr = ' '.join(map(str, r)) imagefile = 'images/x-' + city + '_bandnum_' + listToStr + '_' + now + '_color' trueColor = rasterio.open(imagefile + '.tiff','w', driver='Gtiff', width=w, height=h, count=3, crs=src2.crs, transform=src2.transform, dtype=src2.dtypes[0]) trueColor .write(img0,3) # blue trueColor .write(img1,2) # green trueColor .write(img2,1) # red trueColor .close() scale = '-scale 0 255 0 25' options_list = [ '-ot Byte', '-of JPEG', scale ] options_string = " ".join(options_list) gdal.Translate(imagefile +'.jpg', imagefile +'.tiff', options=options_string) src = rasterio.open(imagefile + '.jpg') plot.show(src) 土石流が起こったところがかなり明確に表示されていますし、また海岸線も濁ったような色が出ています。 土石流発生箇所の解析 次に土石流が発生した箇所に絞って解析をしました。コードは同じですが、解析範囲を縦160ピクセル、横200ピクセルです。1ピクセル10mなので、1.6Kmx2Kmの範囲となります。 ''' # 10m x=2400 y=1165 h=160 w=200 ''' 2020年8月15日 2021年8月5日  クラスター 1年前の土地被覆と比較しても、土石流が山から発生して町の中に流れ込んでいる様子がはっきりと表れています。True Color表示で見て取れるように土石流は町中まで流れて、解析画像から見ても土地被覆が変わっているのが分かります。 土石流発生が流れた範囲の抽出 次に、土石流が流れた範囲がどれ位だったか調べてみました。方法としては先ほどの2つの解析画像で明らかにクラスター番号が大きく変化した箇所、つまり土地の被覆状況が大きく変わった個所を調べてみました。 import numpy as np import matplotlib.pyplot as plt from rasterio import plot import rasterio import cv2 # 読み込み data_before = np.load("images/x-atami_bandnum_02 03 04 08_clusternum_15_20210814-1903_20200815_.data.npy") data_after = np.load("images/x-atami_bandnum_02 03 04 08_clusternum_15_20210814-1903_20210805_.data.npy") # クラスター番号の置き換え data_load = np.uint8() data_load = np.where((5 < data_after) & (data_after < 10) & (-1 < data_before) & ( data_before < 5), 20,data_after) data_load = np.where(( data_after == 9) & (4 < data_before) & ( data_before < 9) , 20,data_load) # 土石流個所を白色 data_load = np.where(( data_load != 20), (data_load + 1 ) * 15, 255) # 白色のピクセル数 print(data_load.shape) print("White pixel: " + str(np.count_nonzero(data_load == 255))) # 画像の保存 data_load8 = (data_load).astype(np.uint8) data_load_cv = cv2.applyColorMap(data_load8, cv2.COLORMAP_BONE) cv2.imshow('image with colormap', data_load_cv) cv2.waitKey() cv2.imwrite('images/x-atami_bandnum_20210805_change_extract.png',data_load_cv) 2020年と2021年を比較し土石流で土地被覆が明らかに変わった個所のクラスター番号を変え、画像で保存するときに白色になるようにします。 2020年でクラスター0-4の森林地区と分類されているが、2021年ではクラスター6-9となっているところ。 2020年でクラスター5-9の住宅地区近郊と分類されているが、2021年ではクラスター9(おそらく土と分類されている)となっているところ。 置換が終わって保存した画像データは、以下の通りです。 土石流が発生したところが、白入りに代わっています。そのほかの箇所も変わっていますが、土石流がどれくらいの範囲で起こったか調べるため、今回は赤で囲ったところのみを調べます。 画像ツールで、赤で囲ったところ以外を切り取ります。 これで、土石流発生個所のみが白色で、それ以外は違う色で表すことができました。 土石流発生が流れた範囲 最後に、先ほど保存した画像で白色となった個所のピクセル数を計算します。 import numpy as np import matplotlib.pyplot as plt import rasterio with rasterio.open('images/x-atami_bandnum_02 03 04 08_clusternum_15_20210814-1903_20210805_change_extract_impact.png') as src2: data2 = src2.read() # データのサイズの確認 print('Loaded data'+ str(data2.shape)) plt.imshow(data2[0],cmap='gist_earth') plt.show() # 白色ピクセル数 print("White pixel: " + str(np.count_nonzero(data2[0] == 255))) データが先ほど置き換えた255となっているところのピクセル数を数えます。 Loaded data(3, 160, 200) White pixel: 245 白色のピクセル数は242個でした。 245 x 10 x 10 = 24,500 m2 つまり、土石流が流れた範囲は、約2万4千平方メートルとなります。 まとめ 解析結果によると、画像で示された土石流の範囲は約2万4千平方メートル、東京ドーム約半分の広さに影響が及んでいたことが分かりました。 解析画像でも見て取れるように、今回範囲を計算したところは、ほとんどが去年の段階では森林地区として分類されている部分です。そこで起こった2万平方メートル以上の範囲を流れたの土石流が、そのふもとの町に流れ込んだことを考えるといかに被害が大きかったか想像できます。 今回はSentinel2の分解能10mのデータを使用しました。以前使ったランドサットデータは30mだったので、それよりはかなり細かな画像を作ることができ解析もより正確になっていると思います。ただ、それでも1ピクセル10mなのである程度の誤差は出ていると思います。 この記事は以前から行っていた機械学習を用いて衛星画像を解析する、IT技術の紹介の目的で書いています。今回は応用してより実社会で起こっている問題を取り上げてみました。今回の記事が、環境問題や防災に役に立つかどうかはわかりませんが何か参考になれば幸いです。 参考文献および注釈 Sentinel2について - https://scihub.copernicus.eu/twiki/do/view/SciHubWebPortal/TermsConditions に指定されている通り、ダウンロードしたデータを元に作成された画像には"produced from ESA remote sensing data"と記載しています。 熱海市 大規模土石流 現地調査 写真レポート - http://www.bo-sai.co.jp/atami.html 【コード付き】Sentinelの衛星データをAPI経由で取得してみた - https://sorabatake.jp/9987/ 最後に 犠牲者の御冥福をお祈り申し上げますとともに、被災された方々に心よりお見舞いを申し上げます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

pysparkで線形回帰モデルを実行する

pysparkで線形回帰モデル作成までの一連の流れを実行する  統計を勉強している人で線形回帰モデルを知らない人はいないと思います。pythonでもscikit-learnなどで簡単にモデリングできます。しかし、仕事などで大規模なデータを扱うとき、pysparkを使って線形回帰モデルを作成したいときもあると思います。私もその一人ですが、ネットで調べてもなかなか情報が少なかったり、英語ばかりで辛かったです。そこで、私が調べた内容を備忘録的にまとめておきます。  今回は線形回帰モデルを下記のステップに分けて作成・評価しました。 モデル作成のための準備 基本統計量の確認 データの標準化 OR 正規化 多重共線性の排除 モデル作成 モデル評価 線形回帰モデルの仮定を満たしているかの確認 残差の分散は均一か? 残差が正規分布にしたがっているか? モデル選択基準 AICの計算 汎化性能の計算 各ステップで必要な関数はpysparkで実装されていなければ、自分で作成しました。作成した関数については、scikit-learnを用いて計算した場合の数値と比較し、出力値に差異がないことを確認していますが、使用は自己責任でお願いします。  実装時に使用したデータはボストンの住宅価格データセットです。下記のスクリプトを用いて、pyspark.DataFrameに変換しています。また実行環境は、google colabratory上のpyspark環境です。環境構築はこちらを参考にしました。 # pyspark関連のライブラリimport import findspark findspark.init() from pyspark import SparkContext sc = SparkContext.getOrCreate() import pyspark from pyspark.sql import SparkSession spark = SparkSession.builder.getOrCreate() #ボストンの住宅価格データセットの読み込み from sklearn.datasets import load_boston dataset = load_boston() # ボストンの住宅価格データセットをpandas.DataFrameに変換 import pandas as pd # 説明変数取得 data_x = pd.DataFrame(dataset.data,columns=dataset.feature_names) # 目的変数を取得 data_y = pd.DataFrame(dataset.target,columns=['target']) # 説明変数と目的変数を結合して、1つのDataFrameにする data = pd.merge(data_y, data_x, how="left", left_index=True, right_index=True) # pysparkDataFrameに変換 df = spark.createDataFrame(data) なお、私がgoogle colaboratoryで実行したところ途中でメモリが不足し一旦Googleドライブにデータを出力して再読み込みをした箇所がいくつかあります。どこで出力したかと、出力のためのスクリプトを記載すると本質から離れるため割愛しています。 モデル作成のための準備 基本統計量の確認  線形回帰モデルを作る前に、データに含まれる変数の基本統計量を確認します。今回は、基本統計量として、各カラムの行数・平均・分散・最小値・最大値・欠損値の個数を計算します。  まずは行数です。count関数を使うだけなので容易です。 # 行数の確認 print("行数:", df.count()) 次に、平均・分散・最小値・最大値を計算します。pyspark.mllib.stat.Statisticsを使うのですが、DataFrameをRDDに変換して使います。 #import from pyspark.mllib.stat import Statistics df_rdd = df.rdd #DataFrame → rddに変換 summary = Statistics.colStats(df_rdd) #colStatsインスタンス作成 #基本統計量をpandas.DataFrameに格納する summary_df = pd.DataFrame(columns = data.columns) summary_df.loc["mean"] = summary.mean() #各列の平均 summary_df.loc["variance"] = summary.variance() #各列の分散 summary_df.loc["min"] = summary.min() #各列の最小値 summary_df.loc["max"] = summary.max() #各列の最大値 実はこの方法は環境に依存するみたいで、上記の方法でできない場合は以下を試してみてください。 # import import numpy as np # colStatsに格納できる形に変換 np_data = df.toPandas().values # pyspark.Dataframe →pandas.DataFrame → np.arrayに変換 df_from_np = sc.parallelize(np_data) #np.array → rddに変換 summary = Statistics.colStats(df_from_np) #colStatsインスタンス作成 #基本統計量をpandas.DataFrameに格納する #一つ上のスクリプトと同じ この方法はpandasやnumpyを経由するので、これも環境に依存してできないこともあるかもしれません。そのときは残念ながら、groupbyを用いてカラムごとに基本統計量を計算する必要があります。「pyspark groupby」で調べると記事がたくさんあるので、ここでは説明を割愛します。 最後に欠損値の個数の計算です。 # 各列の欠損値の個数の確認 from pyspark.sql.functions import col null_count_list = [df.where(col(c).isNull()).count() for c in df.columns] #各列の欠損値の個数を計算 null_count_df = pd.DataFrame(columns = df.columns) #欠損値の個数を格納するdataFrameを作成 null_count_df.loc["null_count"] = null_count_list データの標準化 OR 正規化  ここでは、データの標準化と正規化の方法を説明します。これを行うことで線形回帰モデルに含まれるパラメータの大きさを適切に比較できるようになります。なお、ここでいう標準化はZスコア化、正規化はmin-maxスケーリングを行うことを指します。 まずデータにIDを振っておきます。これは後々便利になるための作業で、本質的ではないです。 # IDを振っておく(便利のため) # import from pyspark.sql.functions import monotonically_increasing_id #IDを連番で振る df_id = df.withColumn('ID', monotonically_increasing_id()) 標準化  標準化するためのスクリプトは下記です。 # import from pyspark.sql import functions as F from pyspark.sql.window import Window # 目的変数、説明変数の抽出 var = df_id.columns var.remove("ID") #ID列を排除 # 標準化 z_df = df_id for c in var: tmp1_z_df = ( df_id .withColumn('mean', F.mean(c).over(Window.orderBy())) .withColumn('std', F.stddev(c).over(Window.orderBy())) .withColumn(c+'_z_scaled', (F.col(c) - F.col('mean')) / F.col('std')) ) tmp2_z_df = tmp1_z_df.select("ID", c+"_z_scaled") z_df = z_df.join(tmp2_z_df, ["ID"], "left") ちなみに、標準化するためのクラスとして、pyspark.ml.feature.StandardScalerがあります。しかし、業務で使ったときに標準化後のデータをparquet形式で保存しようとしたところエラーを吐いて動かなくなりました。そのため、今回は時間がかかりますが、カラムを一つ一つ標準化していく方法を紹介しました。 正規化  正規化するためのスクリプトは下記です。 # 正規化 Mm_df = df_id for c in var: tmp1_Mm_df = ( df_id .withColumn('max', F.max(c).over(Window.orderBy())) .withColumn('min', F.min(c).over(Window.orderBy())) .withColumn(c+'_Mm_scaled', (F.col(c) - F.col('min')) / (F.col('max')-F.col('min'))) ) tmp2_Mm_df = tmp1_Mm_df.select("ID", c+"_Mm_scaled") Mm_df = Mm_df.join(tmp2_Mm_df, ["ID"], "left") これも標準化と同じようにpyspark.ml.feature.MinMaxScalerが存在しますが、StandardScalerと同じ理由で上記スクリプトを紹介しました。 多重共線性の排除  線形回帰モデルにおいて変数間に相関がある場合は、相関がある変数のうちの一つを取り除く必要があります。ここでは、VIFを用いて相関の有無を確認しようと思います。VIFについてはこちらを参照しました。なお、ここでは標準化したデータに対してVIFを求めています。途中でfeaturesうんぬんと出てくるのですが、これについては「モデル作成」の章で説明します。 # import from pyspark.ml.feature import VectorAssembler from pyspark.ml.regression import LinearRegression # VIFを格納するリスト VIF_intercept_list = [] # VIFを求める際の線形回帰モデルにおいて、定数項を入れる VIF_not_intercept_list = [] # VIFを求める際の線形回帰モデルにおいて、定数項を入れない # 説明変数の取得 tmp_ex_var = z_df.columns ex_var = [ev for ev in tmp_ex_var if "_z_scaled" in ev] ex_var.remove("target_z_scaled") # VIFの計算 for c in ex_var: # featuresの作成(pysparkのLinearRegressionにおいては、説明変数をベクトル化し一つのカラムにする必要がある) ##VIF計算時に説明変数となる変数の取得 VIF_ex_var = [c2 for c2 in ex_var if c2 != c] ## VectorAssemblerインスタンス作成 vector_assembler = VectorAssembler( inputCols = VIF_ex_var, #線形回帰モデルの説明変数 outputCol='features' # 説明変数をベクトル化したカラム )  ## features化したpyspark.DataFrameの作成 VIF_z_df = ( vector_assembler .transform(z_df) .withColumnRenamed(c, "label") ) # 線形回帰 ## 定数項ありバージョン lr_intercept = LinearRegression(regParam = 0.0, fitIntercept = True) lrModel_intercept = lr_intercept.fit(VIF_z_df) trainig_summary_intercept = lrModel_intercept.summary ## 定数項なしバージョン lr_not_intercept = LinearRegression(regParam = 0.0, fitIntercept = False) lrModel_not_intercept = lr_not_intercept.fit(VIF_z_df) trainig_summary_not_intercept = lrModel_not_intercept.summary ## VIFの計算 VIF_intercept_list.append(1/(1-trainig_summary_intercept.r2)) VIF_not_intercept_list.append(1/(1-trainig_summary_not_intercept.r2))  ## 今何を計算しているか可視化 print("目的変数:", c, "\n") print("説明変数:", VIF_ex_var, "\n") # pandasDataFrameに格納してVIFを見やすくする VIF_df = pd.DataFrame(index = ex_var) VIF_df["VIF_intercept"] = VIF_intercept_list # 定数項を入れた線形回帰モデルから得られたVIF VIF_df["VIF_not_intercept"] = VIF_not_intercept_list # 定数項を入れない線形回帰モデルから得られたVIF 今回は$VIF\geq10$の変数がなかったので、説明変数を取り除くことはせずに次に進みました。 モデル作成  pysparkで線形回帰モデルを作成する際に、ちょっと癖があると思ったのが説明変数をまとめたカラムを作成する必要があることです。下図の左側が元々のpyspark.DataFrameです(数値は適当です)。そして右側がfeaturesを含めたpyspark.DataFrameです。pysparkでLinearRegressionを使う場合には、このfeaturesを作成する必要があります。 featuresを作成するためのスクリプトが下記です。 # import from pyspark.ml.feature import VectorAssembler # featuresの作成 #ex_varは説明変数のカラム名を入れたリスト vector_assembler = VectorAssembler( inputCols = ex_var, # featuresに入れたいカラムをリストで指定 outputCol='features' ) feature_z_df = ( vector_assembler .transform(z_df) # featuresが含まれたpyspark.DataFrameの作成 .withColumnRenamed(target_var, "label") # LinearRegressionで指定する目的変数のdefaultがlabelなのでlabelにカラム名を変えた。 ) ここまできたら、feature_z_dfをLinearRegressionに突っ込めば線形回帰モデルが作成できます。LinearRegressionを使う上で迷った引数を下記にまとめておきます。本当は引数の意味をもっとちゃんと理解したいのですが、下記の設定でscikit-learnで線形回帰モデルを実行した結果と一致しました。 引数 意味 featuresCol 説明変数のカラム名を指定。defaultは"features"。 labelCol  目的変数のカラム名を指定。defaultは"label"。 regParam 正則化項の係数。defaultは0。 fitIntercept 定数項をいれるか否か。Trueだと定数項あり、Falseだと定数項なし。defaultはTrue。 今回については説明変数のカラム名はfeatures、目的変数のカラムはlabelなので、featuresColとlabelColは明示的に指定していません。また、RidgeやLassoをしたいわけではないので、正則化項は0にしました。また、定数項も入れました。線形回帰モデルを作成するスクリプトは下記です。 # 定数項ありで線形回帰モデルの作成 lr = LinearRegression(regParam = 0.0, fitIntercept = True) lrModel = lr_intercept.fit(feature_z_df) また、作成したモデルに対して予測値を計算するスクリプトは下記です。 # 予測値の計算 predict_summary = lrModel.evaluate(feature_z_df) モデル評価 ここでは作成した線形回帰モデルの評価を行います。最初に謝っておきますが、「線形回帰モデルの仮定を満たしているかの確認」内の「残差の分散は均一か?」「残差が正規分布にしたがっているか?」では、pandas.DataFrameを使っています。もし、pysparkで散布図を書けるのであればpandas.DataFrameを使う必要はないのですが...。私が調べたところそのようなパッケージを発見することはできませんでした。 線形回帰モデルの仮定を満たしているかの確認 残差の分散は均一か?  残差の分散が均一か確認するために散布図を作成しました。スクリプトは下記です。 # ダウンサンプリング & Pandasに変換 sampled_predict_residuals_pd_df = ( predict_residuals_df .select("prediction", "residuals") .sample(1.00) #ダウンサンプリング(今回はデータ量が少ないので、ダウンサンプリングしていない) .toPandas() #Pandasに変換 ) # 散布図(予測値×残差)作成 import matplotlib.pyplot as plt plt.scatter(sampled_predict_residuals_pd_df["prediction"], sampled_predict_residuals_pd_df["residuals"]) plt.axhline(y = 0, xmin = min(sampled_predict_residuals_pd_df["prediction"]), xmax = max(sampled_predict_residuals_pd_df["prediction"]), color = "black", linestyle = "dotted") このスクリプトで得られた散布図は下記です。この図を見ると予測値が-2以下と1.5以上で分散が均一ではなさそうに見えます。今回は、単純に線形回帰モデルの仮定を満たしているかどうか確認する図を作成したかったので、これ以上深追いしません。しかし、仮定を満たしているか微妙なところです。 残差が正規分布にしたがっているか?  ここではQQプロットを作成して確認します。スクリプトは下記です。 # QQプロット作成 ## 値の作成 import scipy.stats as stats stats.probplot(sampled_predict_residuals_pd_df["residuals"], dist = "norm", plot = plt) plt.show() このスクリプトから得られたQQプロットは下記です。 モデル選択基準  ここではAICと汎化性能を求めました。線形回帰モデルなので、係数が0かどうかの検定も作成したかったのですが、時間の都合から割愛します。係数が0かどうかの検定を実行するためのスクリプトはまた時間ができたときにでも作成します。 AICの計算 線形回帰モデルにおけるAICの計算をするスクリプトです。誤差項が平均0、分散が一定の正規分布にしたがっていることを仮定しています。今回は$AIC = 780.3$になりました。 # サンプルサイズ N= feature_z_df.count() # パラメータの個数 k = len(ex_var)+1 # 定数項がある場合 #k = len(ex_var) # 定数項がない場合 # 残差の2乗和の計算 var_hat = ( predict_residuals_df .withColumn("residulas_2", col("residuals")**2) .groupby() .agg( F.sum("residulas_2").alias("sum_residulas_2") ) .collect()[0] .sum_residulas_2 )/(N-k) # AICの計算 from math import log, sqrt, pi AIC = -2*N*log(1/sqrt(2*pi*var_hat))+N+k print("AIC = ", AIC) 汎化性能の測定  汎化性能を計算するためのスクリプトは下記です。今回はRMSEを計算しています。今回は$RMSE = 0.45$でした。 また、ホールドアウト法で実装しています。クロスバリデーションをしたい場合は、for文で全体を包むなどして実装してください。 # 訓練データとテストデータに分割 feature_z_df_train, feature_z_df_test = feature_z_df.randomSplit([0.8, 0.2], seed=2021) # 訓練データについて定数項ありで線形回帰モデルの作成 lr = LinearRegression(regParam = 0.0, fitIntercept = True) lrModel_train = lr_intercept.fit(feature_z_df_train) # テストデータについて予測値の計算 predict_summary_test = lrModel_train.evaluate(feature_z_df_test) predict_df_test = predict_summary_test.predictions # RMSEの計算 ## 予測値と実際の値を一つのdataFrameにまとめる feature_z_df_test_id = feature_z_df_test.select("ID", "label") predict_df_test_id = predict_df_test.select("ID", "prediction") # 実績値・予測値の両方が入ったDataFrameの結合 predict_act_df_test = ( feature_z_df_test_id .join(predict_df_test_id, ["ID"], "left" ) .select("label", "prediction") ) # RMSEの計算 from pyspark.mllib.evaluation import RegressionMetrics metrics = RegressionMetrics(predict_act_df_test.rdd) print("RMSE = %s" % metrics.rootMeanSquaredError) 参考文献 Google ColaboratoryでPySpark環境構築 多重共線性とVIF統計量の求め方 Boston Housing:ボストンの住宅価格(部屋数や犯罪率などの13項目)の表形式データセット LinearRegression
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む