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

グラフを描画してExcelに貼り付ける

import tkinter as tk import tkinter.filedialog as fl import tkinter.messagebox as mb import matplotlib.pyplot as plt import openpyxl as px import os import pandas as pd root = tk.Tk() #ファイルダイアログを表示するディレクトリ INI_DIR = "D:/Python/Hello" #書き込み先のExcelの一行目に読み込み元ファイルがどんな物理量かを記載するためのリスト LIST_TARGET = ["時間[sec]", "角度[deg]", "角速度[deg/sec]"] #GUIのボタンを押した時の処理 def get(*args): #CSVファイルを選択する ※複数選択可 filetype = [('excel file', '*.xlsx'), ("csv file",".csv"), ('excel file', '*.xls')] path_list = fl.askopenfilenames(initialdir=INI_DIR, filetypes=filetype, title="select file") #保存用Excelファイル名を指定する typ = [("Excel", ".xlsx")] savefile = fl.asksaveasfilename(title="保存ファイル名を指定", filetypes=typ, initialdir=INI_DIR) #書き込み&グラフ描画用のExcelを新規作成 wb = px.Workbook() #Excel内に新しいシートを作成してファイル名をつける wb.create_sheet(index=1, title='グラフ') ws = wb['グラフ'] # 読み取った複数のCSVファイルを1つのExcelファイルにまとめる for i, path in enumerate(path_list): read_data = pd.read_excel(path, skiprows=1) #データ数、系列数を変数に格納 data_num, series_num = read_data.shape #ファイル名をシート名に設定 filename = os.path.basename(path) basename = os.path.splitext(filename)[0] #x軸とするデータ x_values = read_data.iloc[:,0] #グラフの作成 fig = plt.figure() ax = fig.add_subplot(1,1,1) #カラーマップを定義 cmap = plt.get_cmap("tab10") for j in range(1, series_num): #y軸とするデータ y_values = read_data.iloc[:,j] ax.plot(x_values, y_values, label=basename, color=cmap(j)) #グラフを保存 image_file_path = os.path.dirname(savefile)+"\\"+basename+".png" plt.savefig(image_file_path) #グラフのメモリ解放 plt.clf() plt.close() #シートにグラフを追加 inColLetter = px.utils.get_column_letter(1) # 画像を貼り付ける位置をExcel形式で表す(列のアルファベット+行の番号) inCellLetter = inColLetter + str(28*(i)+1) img = px.drawing.image.Image(image_file_path) ws.add_image(img, inCellLetter) #Excelデータを保存 wb.save(savefile + ".xlsx") #処理が完了したことをユーザーに知らせる mb.showinfo("確認","Excelファイルの作成が完了しました") message["text"] = "処理が完了しました" message = tk.Label(root,text="ファイルを選択してください", width=30) message.grid(row=0, column=0) button = tk.Button(text="開く", command=get) button.grid(row=0, column=1) root.mainloop()
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

残プロ 第-9回 ~6回から8回のプログラムを組み込む~

各回の簡単なまとめ それぞれの回の詳しいまとめは日曜日の第-12回でやります! 第6回 pandasを使ってcsvファイルの読み込み・書き込み. 第7回 reを使って正規表現による文字列の分割. 第8回 datetime, calendarを使って文字列を日付へ変換.次の第N曜日を計算. あまり利用モジュールは増やしたくないんですが日に日に増えていってますね... タスク管理用に組み込み .csv name:タスク名 deadline_date, deadline_time:期限(日),期限(時間) isRoutine:習慣の場合は日数,又は既定の文章で指定 ["Every"や"Second"等の頻度? + "Monday"や"Sunday"等の曜日] priority:重要度 となっています.時間部分についてはまだ未実装です. 直貼りが一番分かりやすいですね! .py トークンはmytoken.pyにTOKEN="発行されたトークン"として保存しています.毎回編集するのが面倒なので. TaskNotify.py import requests import datetime import calendar import re import pandas as pd from mytoken import TOKEN class Task: def __init__(self, name, date, priority): self.name = name self.date = date self.priority = priority self.sentence = "{}まで残り{}日\n".format(name, abs(self.date-today).days) def str2date(str_list): date_list = [] for s in str_list: frequency = s[0] weekday = s[1] if frequency == "Every": if weekday == "Day": date = today else: delta = weekday_list.index(weekday) - today.weekday() if delta < 0: delta += 7 date = today + datetime.timedelta(days=delta) else: nth = frequency_list.index(frequency) + 1 weekday_first, _ = calendar.monthrange(year=today.year, month=today.month) day = 7 * (nth-1) + (weekday_list.index(weekday) - weekday_first) % 7 + 1 if day >= today.day: delta = day - today.day date = today + datetime.timedelta(days=delta) else: weekday_first, _ = calendar.monthrange(year=today.year, month=today.month+1) day = 7 * (nth-1) + (weekday_list.index(weekday) - weekday_first) % 7 + 1 date = datetime.date(year=today.year, month=today.month+1, day=1) + datetime.timedelta(days=day-1) date_list.append(date) return date_list def separateStr(string): separated_comma = string.split(',') separated_upper = [] for s in separated_comma: separated_upper.append(re.findall('[A-Z][a-z]+', s)) return separated_upper def loadTasks(path_csv): csv_tasks = pd.read_csv(path_csv, encoding='shift-jis') list_tasks =[[] for _ in range(len(priority_list)+1)] for row in csv_tasks.itertuples(): date = datetime.datetime.strptime(row.deadline_date, '%Y年%m月%d日').date() if type(row.isRoutine) is not float: try: routine = int(row.isRoutine) if date < today: date += datetime.timedelta(days=routine-1) except ValueError: if type(row.isRoutine) is str: str_list = separateStr(row.isRoutine) date_list = str2date(str_list) date = min(date_list) csv_tasks.at[row[0], 'deadline_date'] = date.strftime('%Y年%m月%d日') if type(row.priority) is str: list_tasks[priority_list.index(row.priority)].append(Task(row.name, date, row.priority)) else: list_tasks[-1].append(Task(row.name, date, row.priority)) csv_tasks.to_csv(path_csv, encoding='shift-jis', index=False) return list_tasks def createMessage(list_tasks): sentences = "\n" for i in range(len(priority_list)): sentences += "===Priority:{}===\n".format(priority_list[i]) for t in list_tasks[i]: if t.date >= today: sentences += t.sentence sentences += "===Routine===\n" for t in list_tasks[-1]: sentences += t.sentence return sentences if __name__ == '__main__': frequency_list = ['First', 'Second', 'Third', 'Fourth'] weekday_list = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'] priority_list = ['high', 'mid', 'low'] today = datetime.date.today() list_tasks = loadTasks("tasks.csv") message_dict = {'message': createMessage(list_tasks)} url = 'https://notify-api.line.me/api/notify' TOKEN_dict = {'Authorization': 'Bearer ' + TOKEN} requests.post(url, headers=TOKEN_dict, data=message_dict) LINE画面(実行結果) 次回 第-10回は LINE通知画面を工夫しようということで,絵文字や画像,スタンプなんかを使ってみます.
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ローカルのMarkdownファイルを全文検索

目的  会社で自分用のメモをMarkdownで書いています。エディタはVS Codeを使用しています。ファイル数が多くなってきて、目的のファイルを探すのが大変になってきたので、ローカル環境でmdファイルを全文検索できるwebアプリを作成しました。 デモ  ブラウザ上でキーワードを入力して検索すると、キーワードを含むmdファイルが出力されます。リンクを押すと、対象ファイルがブラウザで開きます(ブラウザはChromeを使用しており、mdファイルの表示はMarkdown Preview Plusを使っています。) 環境 Windows 10 Home Python 3.8.3 インストール bs4(スクレイピング用) markdown2(bs4でスクレイピングするためにmd形式をhtml形式に変換) flask(webアプリ作成) pip install beautifulsoup4 pip install markdown2 pip install Flask 検索対象  検索対象は、以下の構成中のarticleのフォルダ直下のmdファイルです。 md_search ├── article │ ├── sample1.md │ └── sample2.md │ └── sample3.md ├── templates │ ├── base.html │ └── index.html ├── md_search.py flaskのテンプレートの準備  検索画面用にflaskのテンプレートを準備します。 base.html <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous" /> <title>検索画面</title> {% block head %}{% endblock %} </head> <body> {% block body %}{% endblock %} </body> </html> 検索画面のhtmlファイルを用意します。検索用のキーワードはPOSTでpythonファイルへ渡します。 index.html {% extends 'base.html' %} {% block body %} <h1 class="m-3">mdファイル 全文検索</h1> <div class="input-group ml-3"> <form action="/" method="POST"> <input type="text" name="search_key" /> <input type="submit" value="Search" /> </form> </div> <p class="m-3">検索結果:{{ result | length }}件 ({{ elapsed_time }} 秒)</p> <ul> {% for i in result %} <h1> <a href="{{ i.url }}" style="text-decoration: none" >{{ i.file_name }}</a > </h1> <p>{{ i.contents }}</p> {% endfor %} </ul> {% endblock %} 検索用のpythonファイル  POSTで渡ってきた検索キーワードを使って、bs4でスクレイピングします。まず、検索対象フォルダを絶対パスで指定します。相対パスでも検索は可能ですが、相対パスではブラウザで検索結果のリンク先が正常に参照できませんでした。 md_search.py path = "C:/md_search/article/" # mdファイルの格納場所を絶対参照で指定 検索対象フォルダからmdファイルを探します。 md_search.py for file in os.listdir(path): # 指定したフォルダからmdファイルを検索 base, ext = os.path.splitext(file) if ext == '.md': print(file) f = open(path + file, encoding="utf-8_sig") md = f.read() md形式をhtml形式に変換します。 md_search.py htmlconv = markdown2.Markdown().convert(md) # markdown形式をhtml形式に変換 bs4で全文検索するために、bodyタグを無理やり付けます。 md_search.py html = "<body>" + htmlconv + "</body>" # bodyタグを付ける。検索対象をbodyタグ内と指定するため。 bs4でスクレイピングを行い、検索キーワードが含まれている場合ファイル名とパス、本文の最初の101文字を配列に格納します。 md_search.py soup = BeautifulSoup(html, "html.parser") elems = soup.find_all(text = search) judge = bool(elems) if judge: # 検索対象のキーワードがある場合は、検索結果を表示するための配列を作成 file_name = file url = path + file contents = soup.find('body').text[0:100].replace( '\n' , '' ) + "..." list = {"file_name":file_name, "url":url, "contents":contents} result.append(list) pythonファイルの全コードは以下です。 md_search.py from bs4 import BeautifulSoup import re import os from flask import Flask, render_template, request, redirect, flash import time import markdown2 app = Flask(__name__) @app.route('/', methods = ['GET', 'POST']) def index(): path = "C:/md_search/article/" # mdファイルの格納場所を絶対参照で指定 result = [] if request.method == "GET": return render_template("index.html") if request.method == "POST": if not request.form.get('search_key'): flash("検索ワードを入力してください。", "failed") print("空です!!!") return else: # 処理前の時刻 t1 = time.time() # 検索する print(request.form.get('search_key')) search_key = request.form.get('search_key') search = re.compile(search_key) for file in os.listdir(path): # 指定したフォルダからmdファイルを検索 base, ext = os.path.splitext(file) if ext == '.md': print(file) f = open(path + file, encoding="utf-8_sig") md = f.read() htmlconv = markdown2.Markdown().convert(md) # markdown形式をhtml形式に変換 html = "<body>" + htmlconv + "</body>" # bodyタグを付ける。検索対象をbodyタグ内と指定するため。 soup = BeautifulSoup(html, "html.parser") elems = soup.find_all(text = search) judge = bool(elems) if judge: # 検索対象のキーワードがある場合は、検索結果を表示するための配列を作成 file_name = file url = path + file contents = soup.find('body').text[0:100].replace( '\n' , '' ) + "..." list = {"file_name":file_name, "url":url, "contents":contents} result.append(list) print('検索keyは「{0}」'.format(search_key) ) print(result) # 処理後の時刻 t2 = time.time() # 経過時間を計算 elapsed_time = t2-t1 return render_template('index.html', result=result, elapsed_time='{:.1g}'.format(elapsed_time)) ## おまじない if __name__ == "__main__": app.run(debug=True) 実行  md_search.pyを実行すると、ローカルのwebサーバーが立ち上がるので、ブラウザに前記のwebサーバーのIPアドレスを打ち込むと検索画面が立ち上がります。 終わりに  改善点、間違い等あるかもしれないので、お気づきの方はご指摘お願いします。 参考にしたページ 【Python】Beautiful Soupを使ってローカルのhtmlファイルをスクレイピングしてCSV出力する方法
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Python】イテレータについて

この記事は【Python】ジェネレータについてという記事の補足として書いたものです。 といっても自分もイテレータについて結構ふんわりとしか理解していなかったのでそれも含めて書いていこうと思います。 目次 イテレータとは イテレータをクラスとして実装するとどうなるのか Pythonの内部ではどのように振舞っているのか 使用環境 Python 3.8.8 イテレータとは イテレータ: 要素を反復して取り出すことのできるインタフェース (引用:Pythonのイテレータとジェネレータ) イテレータとは値に含まれている要素を順に1個ずつ取り出せるオブジェクトです。 (引用:詳細! Python 3 入門ノート) ざっくりいうと、並んでるデータ(リストや、タプル)を順番に取り出すことができるモノ リスト、文字列、タプル、辞書などはイテラブルといい、イテレータではまだありません。 hoge = [1, 2, 3] hoge_iterator = iter(hoge) print(next(hoge_iterator)) print(next(hoge_iterator)) print(next(hoge_iterator)) このプログラムの実行結果は1 2 3と数字が連続で表示されます。 イテラブルはiter()関数でイテレータになります。そして、イテレータはnext()関数で順番に要素を取り出すことができます。 hoge = [1, 2, 3] for i in hoge: print(i) 実際にはこのようにfor文を使うことでiter()やnext()を明示的に使わなくても実行することもできます。ちなみに、for文はイテラブルでもイテレータでも値を順に取り出すことができます。 イテレータをクラスとして実装するとどうなるのか では、イテレータが内部でどのような処理を行なっているのかを理解するためにクラスとして定義してみます。 class MyIterator(object): def __init__(self, *numbers): self._numbers = numbers self._i = 0 def __iter__(self): return self def __next__(self): if self._i == len(self._numbers): raise StopIteration() value = self._numbers[self._i] self._i += 1 return value (引用:Pythonのイテレータとジェネレータ) こちらのプログラムは引用元の記事からお借りをしました。 特殊な文法があるのでいくつか解説しておきます(分かってる人は飛ばして大丈夫です)。 *args def func(*args): print(args, type(args)) for i in args: print(i) func(1, 2, 3) 実行結果: (1, 2, 3) <class 'tuple'> 1 2 3 こちらのプログラムを見てもらうとわかる通り、関数の引数に『*(アスタリスク)』を先頭につけてあげることで、複数の引数を受け取ることができる。 受け取った引数はタプル形式で、*を除いた変数名として扱われる。 raise raise ValueError('ごめんね!') print('hoge') 実行結果: --------------------------------------------------------------------------- ValueError Traceback (most recent call last) in ----> 1 raise ValueError('ごめんね!') 2 print('hoge') ValueError: ごめんね! このプログラムでは、raiseの後にエラー名と、引数としてエラーメッセージを渡すことで、任意のエラーを任意の場所で起こすことができる。 エラーが起きるため、その時点でプログラムの処理は停止する。 そのため、その後のhogeは表示されない。 以上で特殊な文法の解説を終わります。 クラスの解説 イテレータクラスの解説に移ります。 まず、__init__メソッドにてクラスの初期化処理を行うわけですが、そこで_numbers、_iというプロパティを定義しています。_numbersは引数にもあるとおり、*がついた可変長引数になってます。 そのため、_numbersは引数として渡された値のタプル(イテラブル)を保持していることになります。 続いて、__iter__メソッドです。ここでは、自身のインスタンスのみを返しています。実際の処理とは違うでしょうが、今回はこれで大丈夫です。現時点でこの処理の必要性が理解できなくても問題ないです。後で説明します。 最後に、__next__メソッドにてイテレータとして振る舞うための処理を書きます。まず、if文を通り過ぎると、valueにその回の要素を入れます。そして_iプロパティに1加算します。そして戻り値でvalueを返す。 これを繰り返すことによって、持っている要素を順番に送り出すことができます。 そしてif分のところですが、エラーを表示させています。そのエラーとは以下のプログラム例を見るとわかりますが過剰にイテレーションすると、StopIterationというエラーが出ます。それを再現するためにこのif分の処理が必要です。 hoge = [1, 2, 3] hoge_iterator = iter(hoge) print(next(hoge_iterator)) print(next(hoge_iterator)) print(next(hoge_iterator)) print(next(hoge_iterator))  実行結果: --------------------------------------------------------------------------- StopIteration Traceback (most recent call last) in 5 print(next(hoge_iterator)) 6 print(next(hoge_iterator)) ----> 7 print(next(hoge_iterator)) StopIteration: イテレータのクラスを利用してみる my_iter = MyIterator(1, 2, 3) my_iter = iter(my_iter) print(next(my_iter)) print(next(my_iter)) print(next(my_iter)) print(next(my_iter))  実行結果: 1 2 3 --------------------------------------------------------------------------- StopIteration Traceback (most recent call last) in 6 print(next(my_iter)) 7 print(next(my_iter)) ----> 8 print(next(my_iter)) <ipython-input-1-6ddc4f6f0785> in __next_(self) 9 def __next_(self): 10 if self._i == len(self._numbers): ---> 11 raise StopIteration() 12 value = self._numbers[self._i] 13 self._i += 1 StopIteration: ちゃんと、イテレーションをして、エラーも正しく実装できました。 これはfor文でも問題なく動作します。 my_iter = MyIterator(1, 2, 3) for i in my_iter: print(i) なぜこのような結果になるのかというと、iter()関数というのは__iter__メソッドを呼び出す関数で、next()関数は__next__メソッドを呼び出す関数だからです。 今回は__next__に直接イテレータとしての処理を書いたので、イテラブルをイテレータとする__iter__で自分のインスタンスを返していたのです。 Pythonの内部ではどのように振舞っているのか 最後に、Pythonの中で、オブジェクトたちがどのように振舞っているのかを確認してみましょう。 hoge = [1, 2, 3] print(iter(hoge), hoge.__iter__()) #どちらもイテレータオブジェクトが返される 実行結果: <list_iterator object at 0x7f0ad9a0b700> <list_iterator object at 0x7f0ad9a0bee0> iter()関数と、__iter__メソッドどちらを実行してもイテレータオブジェクトができてるのがわかると思います。 hoge = [1, 2, 3] hoge_str = 'hoge' print(iter(tuple(hoge)), iter(set(hoge)), iter(hoge_str)) #タプルとセットは違うイテレータオブジェクトを返される 実行結果: <tuple_iterator object at 0x7f0ae8624ac0> <set_iterator object at 0x7f0ad9ad5dc0> <str_iterator object at 0x7f0ae8624130> このようにタプルやセット、文字列のイテレータオブジェクトは区別される(同じ種類のイテレータオブジェクトではない)。 print(iter(MyIterator(hoge))) 実行結果: <__main__.MyIterator object at 0x7f0ae86243a0> 定義したイテレータクラスはiterを使っても自身のインスタンスを返しているだけなので、インスタンスとして振る舞う。 実際にはイテレータとして振る舞うが、システム上はあくまでインスタンスという部分をおさえておいてほしい。 まとめ 今回はPythonのイテレータについてまとめました。 要素を一つずつ取り出すためのオブジェクト for文を使って順番に取り出せる iter()関数と、next()関数でイテラブルをイテレータにして、イテレーションできる この辺りが重要だと思います。 この記事は【Python】ジェネレータについてを書くために書いた記事になるので、ジェネレータの方をみてもらえると嬉しいです。 参考文献 詳細! Python 3 入門ノート Pythonのイテレータとジェネレータ 初学者のためのPython講座 オブジェクト指向編7 カプセル化 Pythonで例外を発生させる:raise Pythonの可変長引数(*args, **kwargs)の使い方
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Python] moviepy + matplotlib で動画生成

これはなに matplotlibで描画した画像をつなぎ合わせてmp4ファイルを作ります。 準備 pip install matplotlib moviepy 使用例 まずは公式ドキュメントにならって、小さい例で基本的な使い方を示します。 import matplotlib.pyplot as plt from matplotlib.patches import Rectangle from moviepy.editor import VideoClip from moviepy.video.io.bindings import mplfig_to_npimage def make_frame(t): # 座標(t,t)に長方形を描画 fig, ax = plt.subplots(figsize=(6,6)) ax.add_patch(Rectangle((t, t), 1,2)) ax.set_xlim(0,10) ax.set_ylim(0,10) # bitmapに変換. bmp.shape==(432,432,3) bmp = mplfig_to_npimage(fig) plt.close() return bmp fname = "output/out2.mp4" # 10秒, 30fps の計300フレームからなる動画を生成 animation = VideoClip(make_frame, duration = 10) animation.write_videofile(fname, fps = 30) インポートすべきいものが多いのでゴツいですが、要するにVideoClipオブジェクトに画像を描画するmake_frame()関数を渡して、動画を生成しているだけです。 make_frame()関数は1フレームの描画のたびに呼び出され、引数には秒数tが与えられます。返り値は各ピクセルのRGBの値を表す3次元配列を必要がありますが、これはmplfig_to_npimage関数を使うことでmatplotlibのfigureオブジェクトから生成することができます。 出力された動画は以下のようなものになります: 実用的な使い方 前節の使い方そのままだと、実際に大きなプログラムの中で描画~動画生成を行いたい場合にmake_frame関数の部分にすべての処理を記述することになってしまい、コードが複雑になってしまいます。あるいは、既にある大きなコードに動画出力の機能を足したい場合に、make_frame関数の中に既存のコードを移植することになってしまいます。 そこで実用的にはマルチスレッドにして メイン処理を行い、figを作るスレッド 作られたfigから動画を生成するスレッド の2つに分けるのが見通しがよいです。 import matplotlib.pyplot as plt from matplotlib.patches import Rectangle from moviepy.editor import VideoClip from moviepy.video.io.bindings import mplfig_to_npimage import threading from queue import Queue def func(): """動画を生成 """ fname = "./out3.mp4" animation = VideoClip(make_frame, duration = 10) animation.write_videofile(fname, fps = 30) def make_frame(t): """figqにbitmapが入ってきたらそれを取り出して返す """ return figq.get() def main(): """メインの処理 + figqへのbitmapの追加 """ for i in range(310): fig, ax = plt.subplots(figsize=(6,6)) ax.add_patch(Rectangle((i/30, i/30), 1,2)) ax.set_xlim(0,10) ax.set_ylim(0,10) bmp = mplfig_to_npimage(fig) figq.put(bmp) plt.close() figq = Queue() thread = threading.Thread(target = func) thread.start() main() thread.join() 先ほどとの変更点は、 * スレッドセーフなQueue(figq)を用意 * 動画の生成部分は別スレッド(thread)に切り出し * make_frame()関数はfigqに入ってきたbitmapを取り出すだけの簡単なお仕事 (tは引数として与えられるが使わない) * メイン処理~bitmapの生成・Queueへの追加をmain()関数に切り出し といったあたりになります。結果としてはまったく同じ動画が出力されます。 既存のプログラムに動画生成の機能だけ付け加えたければ、main関数の figq.put(bmp) のあたりだけ追加すれば、あとの部分はコピペでOKだと思います。 実用例 AHC001のビジュアライザを作りました。 提出ではpypyで通るようにTEST=0としていますが、TEST=1とすると手元で動画を生成しながら動作します。 別途入力ファイルの準備も必要で、動かすのは面倒だと思うので眺める用にどうぞ。 できた動画は以下のような感じ: そもそもアルゴリズムの性能がイマイチだったり、あくまで本記事のために書いたのでこのコンテストで必要な情報(広告の必須枠の位置とか)が載ってませんが。。 おまけ jupyter notebookからであれば %%HTML <video width="640" height="480" controls> <source src="./out.mp4" type="video/mp4"> </video> のようにセルに書いて実行すればノートブック上から動画を確認できて便利です(参考)。 注意点 必要なフレーム枚数をfigqにputしないと動画生成のスレッドが完了しないためthread.join()でハングアップしてしまいます。 また、2番目の例では手元の試行だとなぜか(10秒*30fps=300枚ではなく)301枚読み込もうとするらしく、301枚以上にする必要がありました。make_frame()が呼ばれるtの値を確認したところ、t=0が2回呼ばれているようです。いずれにしても、bitmapは少し多めにputしておくのが無難かと思います。 大量のフレームを投げつける場合メモリ周りで注意が必要です。 figqに要素が溜まりすぎるとメモリ消費が激しくなるので、そのような場合はpushする側がsleepなりして調整しましょう。 デフォルトのmatplotlibの設定だと画像を出力しようとして(?)figureオブジェクトのメモリが解放されず(?) Fail to allocate bitmapで落ちることがあるようです。この場合は例えば下記のようにコードの先頭に書くことで回避できます(できた): import matplotlib matplotlib.use("Agg") (こちらのmatplotlibのissueを参考) おわり matplotlibだけでgif作る方が楽かなと思ってそっちも調べたのですが、汎用的な書き方があまりよくわからずでした。 mp4のほうがファイルサイズがずっと小さいのでmoviepy入れられるならこちらで良いのかなと。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Pythonから棒読みちゃんに読み上げさせる(Socket通信)

目的 Pythonとsocket通信を用いて棒読みちゃんにメッセージを読み上げさせる。 環境 python3.6 棒読みちゃんVer0.1.11.0 β21 Windows10 20H2 まず付属のサンプルソースを読み解く 2021年6月10日現在の最新の棒読みちゃんVer0.1.11.0 β21の配布ファイルの中には、socket通信で読み上げ指示を送るサンプルソースが付属している。内容は再配布禁止となっているため載せられないが、誰でもダウンロードできるので確認してほしい。 サンプルソースはC++版だが、socketさえ使えれば他の言語からでも読み上げ指示を送れるはずだ。 ありがたいことにPythonには標準ライブラリとしてsocketモジュールが付属している。 データのフォーマット 読み上げ指示に使われるデータのフォーマットは付属ファイルに記述されている。 大きく分けて2つのパートからなるようだ。 15Byte固定のヘッダー部 可変長のメッセージ部 ヘッダー部のフォーマット ヘッダー部はひと続きで15Byte固定だ。 サンプルソースを見るに、それぞれの要素は符号付き整数(2の補数を使う)と思われる。バイトオーダーはリトルエンディアンのようだ。 順番 名前 大きさ 値 1 コマンド 2Byte 読み上げ指示では1固定 2 読み上げ速度 2Byte 50~300, または-1で現在の設定値 3 読み上げ音程 2Byte 50~200, または-1で現在の設定値 4 音量 2Byte 0~100, または-1で現在の設定値 5 声質 2Byte 0で現在の設定値, 1~8でAquesTalk, 10001以上でSAPI5 6 メッセージ部の文字コード 1Byte 0でUTF-8, 1でUnicode, 2でShift-JIS 7 メッセージ部の長さ 4Byte 付属ファイルには書かれていないがサンプルを見るに0~2147483647だろう(signed intの最大値) メッセージ部のフォーマット メッセージ部はヘッダー部の直後に現れる可変長のデータだ。ここに読み上げさせたいメッセージを格納する。 文字コードはヘッダー部の6番で指定する。 メッセージの長さはヘッダー部の7番で指定する。単位はByteなので注意。 読み上げ指示の流れ 付属のサンプルソースでは socketを接続 ヘッダ部を送信 メッセージ部を送信 socketを閉じる という流れで通信していた。 読み上げ指示のみならデータを送信するだけで、何かを受信する必要はないようだ。 Pythonでコード作成 下記のようなコードを作成した。 実行すると変数MESSAGEの内容を読み上げてくれた。 python_to_bouyomi.py BOUYOMICHAN_IP = '127.0.0.1' BOUYOMICHAN_PORT = 50001 MESSAGE = 'ゆっくりしていってね' import socket def make_header(command=1, speed=-1, tone=-1, volume=-1, voice=0, char_code=0, msg_length=0): """ 棒読みちゃんへの読み上げ指示に使うヘッダー部を生成します。 長さ15のbytes型オブジェクトが返されます。 """ buffer = bytes() buffer += command.to_bytes(length = 2, byteorder = 'little', signed = True) buffer += speed.to_bytes(length = 2, byteorder = 'little', signed = True) buffer += tone.to_bytes(length = 2, byteorder = 'little', signed = True) buffer += volume.to_bytes(length = 2, byteorder = 'little', signed = True) buffer += voice.to_bytes(length = 2, byteorder = 'little', signed = True) buffer += char_code.to_bytes(length = 1, byteorder = 'little', signed = True) buffer += msg_length.to_bytes(length = 4, byteorder = 'little', signed = True) return buffer if __name__ == '__main__': message = MESSAGE.encode('utf-8') #文字コードをutf-8としてbytesオブジェクトに変換する header = make_header(msg_length = len(message)) #ヘッダー部を生成 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) #socket生成 s.connect((BOUYOMICHAN_IP, BOUYOMICHAN_PORT)) #socket接続 s.sendall(header) #ヘッダー部送信 s.sendall(message) #メッセージ部送信 s.close() #socketを閉じる str型のオブジェクトMESSAGEをbytes型のオブジェクトmessageに変換している点に注意してほしい。 これはヘッダー部を生成する際にlen(message)で文字数ではなくバイト数を得るためだ。 >>> message = "テスト" #これはstr型 >>> bytes_message = message.encode('utf-8') #これはbytes型 >>> print(len(message)) #3文字 3 >>> print(len(bytes_message)) #9バイト 9 他にも試して分かったこと 読み上げ指示は1件ごとにsocketを閉じなければならない。つまり1度の接続で複数の読み上げ指示を送ることはできない。複数読み上げさせたいときはその都度socketを生成し直そう。 ヘッダー部のコマンドの値を変更することで他にも色々な指示が送れる。詳しくは棒読みちゃん付属のサンプルを参照。 展望 Pythonから読み上げ指示を送ることができた。つまりPythonの豊富なライブラリ群を使って色々なものを読み上げさせられるということだ。夢が広がる。 また、socket通信を用いることによってリモートマシンに読み上げ指示を送ることができる。これはローカル専用のHTTP連携やIpcClientChannel連携にはない利点だ。使い所は思いつかないが...。 とりあえず筆者はfeedparserモジュールとrequestsモジュールを使ってニュースサイトのRSSと記事を読み上げさせたいと思っている。 参考文献 int.to_bytes(length, byteorder, *, signed=False) - 組み込み型 socket --- 低水準ネットワークインターフェース 棒読みちゃん付属のサンプルソース
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

テスト

こんにちわ
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Django公式チュートリアル(1~4)で分からない所、徹底的に調べた。

最初に 本格的なWebアプリケーションを作成したいのでPythonのフレームワークDjango(読み方:ジャンゴらしい。ディーどこ行った。)についてチュートリアルをこなしながら学んで行こうと思う。実際に作成するアプリは質問に対して回答して投票を表示するアプリになる。 この記事は公式チュートリアルの1〜4までに気になった事躓いた事をまとめていく。全部で1〜7まであるが4までにアプリは完成する。 5からはテストコード等を書いていくのでボリュームが多くなるため、前編として今回の記事を投稿する。後編も必ず書こうと思う。 今回のチュートリアルで作成したもの 質問一覧のページがあって、そこから質問に対して投票を行う。その後今まで投票された数を表示するページリダイレクトされる。 VSCodeのPython用の拡張機能をインストールする。 コードを書くにあたって構文エラーは事前に無くしたいので、拡張機能をインストールする。マイクロソフトがPython用に提供しているものがあるのでそちらをインストールする。 自分はanaconda環境での使用をしているので下記の通知が出てきた。 terminal.integrated.inheritEnv を false にした方が良いらしい。 We noticed you're using a conda environment. If you are experiencing issues with this environment in the integrated terminal, we recommend that you let the Python extension change "terminal.integrated.inheritEnv" to false in your user settings. VSCodeの code > preference > settings に terminal.integrated.inheritEnv と入力して出てきたチェックを外す。 【Mac/Python】VSCodeターミナル動作が通常ターミナルと違う時 | ゆうきのせかい それだけだと Import "django.contrib" could not be resolved from source という警告がでたままなので、赤枠の箇所をクリックしてDjangoをインストールした環境を選択すると警告文が消えます。 インストール 好きなフォルダーを作成して、そこに開発していく。Pythonの環境構築は自分は下記のように行っている。 https://techblog-pink.vercel.app/posts/cc111706c3167 anacondaで仮想環境を作成して、Djangoをインストールしていく。 pip install Django 開発を始めたいフォルダに移動して下記を実行する。 django-admin startproject mysite mysiteというディレクトリが生成される。 mysite/ manage.py mysite/ __init__.py settings.py urls.py asgi.py wsgi.py フォルダはこのような構成になっている。 mysite/mysite となっているのが不思議だ。外側の mysite はDjangoのシステム的には何でも良いらしい。 urls.py はプロジェクトのURLを宣言する。目次のような機能を提供する。 サーバを起動する。 実行すると http://127.0.0.1:8000/ でサイトにアクセス出来るようになる。 最初はデータベース等の設定をしていないので、ターミナルに警告が表示されるが他に問題がなければアクセス出来る。 ctr + c でサーバを終了することが出来る。 python manage.py runserver GitHubで管理する これから開発していくので、Githubにコードをあげて進捗を管理したい。しかし、このままだと settings.py に書かれた SECRET_KEY も一緒にアップロードしてしまうので別ファイル local_settings.py に SECRET_KEY を書く。ちなみに私は気付かずに一度GitHubに、そのままアップしてしまった。そのため新たに SECRET_KEY を作成する手間が掛かる。 local_settings.py SECRET_KEY = 'settings.pyにあったシークレットキーまたは、新たに作成したもの' この変数を settings.py で読み込んでいく。下記のコードを追加する。 try: # 同じ階層のlocal_settingsファイルからSECRET_KEYをkeyとして読み込む。 # 参考記事等では .local_settingsとなっているが、local_settingsはファイルなので必要ない # 将来的に複雑にファイルを分ける必要が出てフォルダにする場合は .が必要となる。 # ダメだった .local_settingsが正しかった。Django環境だとパッケージとして見なされるのか... from .local_settings import SECRET_KEY as key except ImportError: pass # シークレットキーの部分を読み込んだ変数に置き換える。 SECRET_KEY = key 実際に test.py と test2.py を同じ階層に作成してどのように読み込まれるか調べてみた。 test.py hello = "hello" test2.py # .testとするとImportError: attempted relative import with no known parent package # と表示される。 # asを付けない場合は helloで読み込まれる。 from test import hello as A print(A) settings.py と同じ環境に出来ていると思ったが、全然違った。 local_settings.py は パッケージとして認識されるが、 test.py はパッケージとして認識されないので . を使用するとエラーになる。そもそも python test2.py と直接実行しているので settings.py とは違う実行状況になる。 詳しくはここで回答を頂いている。 SECRET_KEYの作成 自分は知らずにシークレットキーをGitHubにあげてしまったので新たに作り直す必要があるのでシークレットキーを生成してくれるプログラムを実行する。これを直接ターミナルで実行するとシークレットキーが生成されるのでそれを local_settings.py に貼り付ける。 get_random_secret_key.py from django.core.management.utils import get_random_secret_key secret_key = get_random_secret_key() text = 'SECRET_KEY = \'{0}\''.format(secret_key) print(text) これでようやくチュートリアルに専念してコードを書き進める事が出来る。 プロジェクトとアプリ Djangoではプロジェクトの中にアプリが含まれる。なので特定のDjangoで作成されたWebサイト全体をプロジェクトと呼び、その中に含まれる小規模な投票アプリ、ログシステムをアプリと呼ぶ。 アプリを作成する。 manage.py と同じ階層に移動して python manage.py startapp polls を実行すると polls というフォルダが生成される。 polls/ __init__.py admin.py apps.py migrations/ __init__.py models.py tests.py views.py これでプロジェクトにpollsというアプリが作成された事になる。 Viewを作成する。 URLからパスにアクセスがあって、その際に実行する関数がViewになるここでHTMLファイルを返したりと処理を決めることが出来る。 views.py に記述する。 def detail(request, question_id): return HttpResponse("You're looking at question %s." % question_id) def results(request, question_id): response = "You're looking at the results of question %s." return HttpResponse(response % question_id) def vote(request, question_id): return HttpResponse("You're voting on question %s." % question_id) viewメソッドの第一引数には必ずHttpRequestクラスを受け取る。 引数 request には今ユーザがアクセスしているURLやIPアドレスなどの情報が入ってくる。そして戻り値としては HttpResponseクラスを返す必要がある。 実際にHttpResponseとHttpRequestの中身がどんな感じになっているのか気になる人は下記のページで確認できます。 ざっくりですが、 views.py があって実行されるとクラスの中身はこんな感じに格納されているみたいです。 def sample(request): print(request) response = HttpResponse('') print(response) return response # 実行結果 # <WSGIRequest: GET '/hello/'> # <HttpResponse status_code=200, "text/html; charset=utf-8"> Djangoはリクエストを受け取ってレスポンスを返しているだけです【詳しく解説】 views.py だけではURLと紐づいていないので URLconfを作成する。 URLconf urls.py を作成する URLconfを作成するには urls.py というファイルを views.py と同じ階層に作成する。mysiteフォルダ内には urls.py がすでにあるので pollsフォルダ内に作成する。中身はこんな感じになる。 include()を使ってアプリのURLを結び付ける。 polls/urls.py from django.urls import path from . import views urlpatterns = [ path('', views.index, name='index'), path('<int:question_id>/', views.detail, name='detail'), path('<int:question_id>/results/', views.results, name='results'), path('<int:question_id>/vote/', views.vote, name='vote'), ] <int:question_id> が views.py 関数の引数として渡される。この <> を使用すると、URLの一部がキャプチャされ渡される。文字列 :quesiton_id> は一致するパターンを定義し、 <int: の部分はURLパスに当てはまる値の型を指定している。なので <str:, <slug: などもある。 そして、これをmysiteフォルダ内の urls.py に結びつけてあげる必要がある。一応こっちが最初に読み込まれるのでここに後から追加したアプリのURLを include() を使って追加していくイメージになる。 mysite/urls.py from django.contrib import admin from django.urls import include, path urlpatterns = [ path('polls/', include('polls.urls')), path('admin/', admin.site.urls), ] これでpollsアプリのURLを結び付けることができた。サーバを起動して http://localhost:8000/polls/ にアクセスするとViewが返されるようになる。 path()の引数 4つの引数を受け取ることが出来る。そのうち route と view の2つは必須で残り kwargs と name は省略出来る。 route:URLパターンを含む文字列が入る。リクエストを処理する際に urlpatterns を順番にみて最初にマッチしたものを取り出す。このパターンはGET、POSTのパラメータに影響は受けないあくまでURLパスだけを見る。 view:URLがマッチしたら、そこに付随するView関数を返す仕組みになっている。 kwargs:任意のキーワード引数を辞書としてView関数に渡せる。 name:URLに名前を付ける事で reverse() を使って呼び出せるようになり、htmlのテンプレートでformに指定するURLを変更する際に動的にURLが変更されるようになる。 polls/urls.py がこんな感じだとして from django.urls import path from . import views import re urlpatterns = [ path('', views.index, name='index'), path('<int:number>', views.subView, name='suburl'), ] 試しに下記の views.py を実行してみる。 from django.http import HttpResponse from django.urls import reverse def index(request): urlName = reverse('index') print(urlName) return HttpResponse("Hello, world. You're at the polls index.{0}".format(urlName)) # 実行結果 # /polls/ こうすると例えば pollllllls/urls.py とURLを変更してもコードを変更する必要がないので、変更に強いコードになります。 ※pathのnameについて python、djangoのurls.pyで設定するnameってなんやねん?? - Qiita DjangoのURL ユーザがDjangoで作られたサイトにアクセスした際にどのような処理が走るのか。 ROOT_URLCONFに設定されているURLを確認する。(HttpRequestオブジェクトにurlconfという属性が設定されている場合はその値をROOT_URLCONFとする。) urlpatternsという名前の変数を探す。この変数値は django.urls.path() または django.urls.re_path() インスタンスのsequenceでなければならない。 urlpatternsから順番に要求されたURLパターンを探す。 マッチしたらViewを返す。 マッチしなかったらエラーハンドリングビューを返す。 URLconfのサンプル pathの左側がマッチするURL(route), 右側がマッチしたら呼び出される関数(view) from django.urls import path from . import views urlpatterns = [ # /articles/2003/にアクセスした場合 # Views.special_case_2003(request)を呼び出す。最後の/もしっかりないとマッチしない。 # 引数としてrequestが関数に渡る。 path('articles/2003/', views.special_case_2003), path('articles/<int:year>/', views.year_archive), path('articles/<int:year>/<int:month>/', views.month_archive), path('articles/<int:year>/<int:month>/<slug:slug>/', views.article_detail), ] /articles/2005/03/ というアクセスがあった場合上記のパターンの中からviews.month_archive(request, year=2005, month=3) という views.py (上記で記したviews.pyとは別で例としてあげてるurls.pyに対応するviews.pyがあったらという話で見てもらいたい。)に書かれたviewメソッドに引数を渡して呼び出す事になる。引数 request にはHttpRequestクラスが入り 今ユーザがアクセスしているURLやIPアドレスなどの情報が入ってる。 /articles/2003/03/building-a-django-site/ なら最後のパターンにマッチして、このように views.article_detail(request, year=2003, month=3, slug="building-a-django-site") 関数を呼び出す。 タイムゾーンの設定 デフォルトではUTCと世界標準時間になっているので、ここで日本時間に変更しておきたいと思う。 settings.py の TIME_ZONE = 'UTC' を TIME_ZONE = 'Asia/Tokyo' に変更する。 データベースの作成 ここでデータベース用語についてまとめて置く。 カラム:縦列の事を指し、別名では列と呼ばれる。 カラム名:列全体に付けられた名前 フィールド:データが入っている場所。 フィールド名:そのデータが入っているカラム名を指す。 項目名:フィールド名を指す。 テーブル:Excelでいうシートのようなもの。 レコード:データそのものを指す言葉になる。もう一つは横列の事を指し、行と呼ばれる。そして行をロウと呼ぶこともある。 これからデータベースを設定していく、チュートリアルの段階なのでまずは複雑な設定の必要がないSQLiteを使用していく。 python manage.py migrate 実行すると settings.py に書かれた INSTALLED_APPS の設定を参照して mysite/settings.py ファイルのデータベース設定に従って必要な全てのデータベーステーブルを作成する。 migrate(マイグレート)するとは データベースを削除してから作り直すと、DBに保存されている情報が全て削除されてしまう。こういった事態を回避する方法として、データベースマイグレーションを行う方法が生まれた。マイグレーションとは、DBに保存されているデータを保持したまま、テーブルの作成やカラムの変更などを行うことが出来る。 モデルの作成 models.py models.py はデータを保存したり、取り出したりするときの設定を記録するファイルになる。データベースの取扱説明書と書かれることが多い。 models.py からデータベースが作成される流れ models.pyファイルでモデル(データベースの型)を作成する。 migrationファイルを作成する。 migrate(最終的にはおそらくSQL文に変換されて、データベースに命令を出してデータベースを作成する。)する。ここで上記のファイルを元にデータベースが作成される。 つまりモデルはデータベースのレイアウトとそれに付随するメタデータになる。 簡単な例を示す。(チュートリアルとは関係ないモデル) # modelsモジュールを読み込むこれはデータベースを作成するのに必要な機能が格納されている。 from django.db import models # データベース作成機能を継承してモデルを書き込んでいく。 class BookModel(models.Model): # booknameという文字を入力することが出来るフィールドを作成する命令をだす。 # bookname = models.CharField(max_length = 50)とすると50文字までと制限をかけれる。 # CharFieldには必須の引数がありmax_lengthを設定しないといけない。 bookname = models.CharField(max_length = 50) # CharFieldとほぼ同じで文字列を扱うがTextFieldの方がデータの読み出し等にコストがかかるらしい。 summary = models.TextField() # 整数値を入れるフィールドを作成する。 rating = models.IntegerField() ForeignKey(外部制約キー) これを使ってこの後2つのモデルを双方向に参照できるようにするのだが、その前にデータベースにおいて外部制約キーまたは外部キーとも呼ばれるがどのような役割を果たしているのかみていこうと思う。 外部キーとは関係データベースにおいてデータの整合性を保つための制約(参照整合性制約) 外部キーに設定されている列(子テーブルのカラム)には、参照先となるテーブルの列内(親テーブルのカラム内)に存在している値しか設定できない。 そのため、外部キーに設定されている子テーブルの列内に親テーブルの列内に存在しない値を追加しようとするとエラーになる。 なので新しく値を追加したい場合は一度、親テーブルで追加する必要がある。 このように制約を結ぶ事でデータの整合性を保つ事ができる。 Djangoでは foreign keyが設定されている方が子テーブルになる。 今度は少し複雑なモデルを見ていく。 図のようなQuestionとChoiceという2つのモデルを作成する。 ChoiceにQuestionが ForeignKey を使って紐ずけられている。 on_delete=models.CASCADE は紐づけられたモデルが削除される際にどのような動作をするかを決める事が出来る。削除された後そのモデルだった部分をNullで埋めたり、そもそも削除できないようにしたりと出来る。 CASCADE は紐づけられた側のモデルで関連するオブジェクトも削除するという動きになる。なので Questionが削除されたら、Questionと関連のあるChoice側のオブジェクトも削除するような動作を取る。 詳しくはここの記事が分かりやすい。 Django2.0から必須になったon_deleteの使い方 - DjangoBrothers from django.db import models class Question(models.Model): # Question textというフィードを作成してそこに入る文字列は200文字までと制限している。 question_text = models.CharField(max_length=200) # 基本的には変数名がフィールド名として使用されるが、引数で文字列を渡す事でフィールド名設定する事が出来る。 pub_date = models.DateTimeField('date published') class Choice(models.Model): # Question ← → Choiseと双方向のやりとりが可能となる。 question = models.ForeignKey(Question, on_delete=models.CASCADE) choice_text = models.CharField(max_length=200) # Votesフィールドは整数値を受け付ける。最初は0が入る。 votes = models.IntegerField(default=0) モデル間の双方向やりとりについて 【Django】1対多の関係( related_name, _set.all() )について - Qiita モデルを作成したのでデータベースにマイグレート(モデルを元にデータベースのレイアウトを作成するデータを追加するわけではない。)していく migrationファイルを作成する前に、新たに作成したアプリpollsを伝える必要がある。 settings.py の INSTALLED_APPS の配列に 'polls.apps.PollsConfig', を追加する。 次にmigrationファイルを作成する。 新たにファイルを作成する必要はなくターミナルでコマンド実行する。 python manage.py makemigrations polls 実行すると models.py を元にmigrationファイルが生成される。 python manage.py migrate 実行するとmigrationファイルを元にデータベースが作成される。 試しにコマンドラインからDjango shellを通してデータベースを変更したりしてみる。 シェルに入るには下記のコマンドを実行する。 python manage.py shell するとPythonコード >>> を書ける状態になるのでここからデータベースにアクセスするコードを書く。 # 作成したモデルを読み込む from polls.models import Choice, Question # 格納されたデータを確認する。まだ追加していないので、空になっている。 Question.objects.all() from django.utils import timezone # モデルにデータを追加する。 q = Question(question_text="What's new?", pub_date=timezone.now()) # データベースに保存する。 q.save() q.id q.question_text q.pub_date # データの上書き q.question_text = "What's up?" q.save() # データが格納されているのが確認できる。 Question.objects.all() # 実行結果:<QuerySet [<Question: Question object (1)>]> このままではadminページでオブジェクト名が Question object (1) と表示され分かりにくいので 特殊メソッド __str__() をモデルに追加する。 あと追加で was_published_recently(self) メソッドを書きました。 データが最近追加されたかどうかを判定するメソッドで True or False で返します。 class Question(models.Model): # クラス変数を定義する。データベースフィールドを表現している。 # Charフィールドは文字のフィールド question_text = models.CharField(max_length=200) # 日時のフィールド pub_date = models.DateTimeField('date published') def __str__(self): # インスタンスを生成して、printした際にここが実行される。 # シェルで表示されるオブジェクトに質問名が使われるだけでなく # adminでオブジェクトを表現する際にも使用されるので追加する必要がある。 return self.question_text def was_published_recently(self): now = timezone.now() # now - datetime.timedelta(days=1)は今の時間から一日引いた日付を出す。 # 2021-05-19 23:29:56.216634こんな感じの値になる。 # pub_dateが現在時刻より過去で現在時刻から一日以内の場合はTrueを返すメソッド return now - datetime.timedelta(days=1) <= self.pub_date <= now class Choice(models.Model): # これはChoiceがQuestionに関連付けられている事を伝えている。 # データベースの多対一、多対多、一対一のようなデータベースリレーションシップに対応する。 # Question ← → Choiseと双方向のやりとりが可能となる。 question = models.ForeignKey(Question, on_delete=models.CASCADE) choice_text = models.CharField(max_length=200) votes = models.IntegerField(default=0) def __str__(self): return self.choice_text すると下記のように表示されるので、 何のデータが入っているのか分かり易くなった。 Djangoのshell内でも下記のようにオブジェクトの中身が分かり易くなった。 from polls.models import Choice, Question Question.objects.all() # 実行結果:<QuerySet [<Question: What's up?>]> # 続けて色々な関数を試してデータベースから # データを取得してみる。 # filterをかけてデータを取得する。 # idはデータベースにデータを追加した際に1から順番に自動で割り振られる。 Question.objects.filter(id=1) # 実行結果:<QuerySet [<Question: What's up?>]> # Questionオブジェクトのquestion_textフィールドで"What"から # 始まるデータを取得する。 Question.objects.filter(question_text__startswith="What") # 実行結果:<QuerySet [<Question: What's up?>]> from django.utils import timezone # 今年作成されたデータを取得する。 current_year = timezone.now().year Question.objects.get(pub_date__year=current_year) # id 2のデータを取得する。 # filterで指定しなくてもgetでも取得できる。 Question.objects.get(id=2) # 実行結果はない場合はエラーになります。 # プライマリーキーと呼ばれるものでいまいちidとの違いが分からない。 # 取得するデータはidの場合と同じになる。 q = Question.objects.get(pk=1) q.was_published_recently() # 実行結果:True # ChoiseはQuestionと関連付けられてるのでQuestionからも # データにアクセスする事ができる。 # Choice側にはまだデータを入れてないので結果は何も表示されない。 # Choicecは質問に対する回答の選択肢をデータとして持つ。 # qには今What's upという質問が入っているので、それに対しての # 選択肢を作成した。 q.choice_set.all() q.choice_set.create(choice_text="Not much", votes=0) q.choice_set.create(choice_text="The sky", votes=0) c = q.choice_set.create(choice_text='Just hacking again', votes=0) # 選択肢が関連づけられている質問を返す。 c.question # 実行結果:<Question: What's up?> q.choice_set.all() # 選択肢が何個あるか数える。 q.choice_set.count() Choice.objects.filter(question__pub_date__year=current_year) # 実行結果:<QuerySet [<Choice: Not much>, <Choice: The sky>, <Choice: Just hacking again>]> # just hackingの選択肢だけ削除する。 c = q.choice_set.filter(choice_text__startswith="Just hacking") c.delete() pkとidの違い pkは primary key の略で、データベースでは 主キー と呼ばれている。主キーはテーブルで一意の値を取る。 どのレコードを主キーにするかはフィールド名を定義する時に primary_key=True を付ければ設定できる。 codeというフィールドに入る値を主キーとする。 code = models.CharField(max_length=10, primary_key=True) djangoの場合はModel(=テーブル)には必ず1つの主キー用のフィールドが必要になる。ユーザが定義しない場合はidという名前のAutoFiekd(int型の連番1~n)が作成される。 そのため、pkキーを定義しない場合はpkはidのショートカットになる。 pkキーを上記の code のように定義した場合はidは作成されない。 SQLに直接アクセスする。(おまけ) sqllite3を使用してデータベースを作成している場合はmysiteフォルダ内にデータベースのファイルが生成されていると思うので、下記のコマンドからSQLで操作するシェルに入る事ができる。 sqlite3 db.sqlite3 # シェル内での操作 # 作成されたテーブル一覧を確認できる。 >>> .table auth_group django_admin_log auth_group_permissions django_content_type auth_permission django_migrations auth_user django_session # 先ほどモデルから作成されたテーブル auth_user_groups polls_choice auth_user_user_permissions polls_question # 構造を確認できるみたいだけど、見てもよく分からなかった。 >>> .schema # シェルから抜ける。 >>> .quit モデルからデータベースを操作する事ができたので、次に先ほど登場したadminページにアクセスしたいと思う。 adminページアクセスする。 ログインが必要なのでユーザを下記のコマンドから作成する。 python manage.py createsuperuser # 実行すると下記の入力画面が登場する。 Username:名前を入力 Email address:@と.comがあれば架空で良い Password: Password(again): ユーザを作成したら開発サーバを起動してhttp://127.0.0.1:8000/admin/ にアクセスするとログイン画面となるのでログインする。 するとそこから作成したモデルを閲覧したりデータを追加したりできる。 ChoiceモデルからはQuestionを選択して使用することしか出来ないのが確認できる。 右側にある + ボタンを押すと Questionのページに飛びそこから新しい質問を追加することはできる。 Views.pyからデータベースの値を取得する 先ほどDjango shellで使用したPythonコードを使って、 views.py にデータを取得していく。 # 最後はHttpResponseを返す必要があるのでimportする。 from django.http import HttpResponse # データベースを操作するためにモデルを読み込んでおく。 from .models import Question def index(request): # データベースから最新5件を取得する。 # こんな感じのデータになる。<QuerySet [<Question: test3>, <Question: hello>, <Question: what's up?>]> latest_question_list = Question.objects.order_by('-pub_date')[:5] # "test3, hello, what's up?"区切った文字列にしてHttpResponseに渡す。 output = ', '.join([q.question_text for q in latest_question_list]) return HttpResponse(output) # Leave the rest of the views (detail, results, vote) unchanged Viewとページデザインを切り離す Viewではデータの取得や操作を専門的に行ってもらい、そのデータをテンプレート(htmlにPythonの変数を入れられる。)に渡してページをレンダリングしてもらうようにする。 pollsディレクトリの中に、templatesディレクトリを作成する。システムでそのディレクトリを認識する。作成した templatesディレクトリにpollsフォルダを作成する。なので polls/templates/polls みたいなディレクトリが完成する。その中にテンプレート index.html を作成する。なぜ templates/polls とするのかというとDjangoは名前がマッチした最初のテンプレートを使用するので、もし異なるアプリケーションの中に同じ名前のテンプレートがあるとそちらを読み込む。それを回避するために名前空間(所属する領域)を与えている。 views.py をテンプレートにデータを渡せるように書き換える。 from django.http import HttpResponse from django.template import loader from .models import Question def index(request): latest_question_list = Question.objects.order_by('-pub_date')[:5] # テンプレートを読み込む template = loader.get_template('polls/index.html') # 辞書型に最新5件のデータを格納する。 context = { 'latest_question_list': latest_question_list, } # 辞書型のデータをテンプレートに渡してページを作成する。その結果をHttpResponseに返す。 return HttpResponse(template.render(context, request)) view.pyをさらに短くする。 from django.template import loader from django.http import HttpResponse を使わない書き方 より簡素にする事が出来る。 その場合、 render() 関数は第一引数に requestオブジェクト , 第二引数に テンプレート名 , 第三引数に 辞書型(テンプレートに渡したいデータ) を記述する。 from django.shortcuts import render from .models import Question def index(request): latest_question_list = Question.objects.order_by('-pub_date')[:5] context = {'latest_question_list': latest_question_list} return render(request, 'polls/index.html', context) 404エラーを出力する。 from django.http import Http404 from django.shortcuts import render from .models import Question def index(request): latest_question_list = Question.objects.order_by('-pub_date')[:5] context = {'latest_question_list': latest_question_list} return render(request, 'polls/index.html', context) # 質問の詳細ページのビュー def detail(request, question_id): try: # アクセスのあったURLでpkの値が変わる。/polls/1/なら1になる。 # データベースでエラーになるとHttp404を出力する。 question = Question.objects.get(pk=question_id) except Question.DoesNotExist: raise Http404("Question does not exist") return render(request, 'polls/detail.html', {'question': question}) 上記のdetail()を短くする。 django.shortcuts にはこうしたコードを省略する関数が多くあるので調べると面白いかもしれない。 from django.shortcuts import get_object_or_404, render from .models import Question def index(request): latest_question_list = Question.objects.order_by('-pub_date')[:5] context = {'latest_question_list': latest_question_list} return render(request, 'polls/index.html', context)。 # モデルにアクセスするobjects.get()とHttp404が一緒になっている。 def detail(request, question_id): question = get_object_or_404(Question, pk=question_id) return render(request, 'polls/detail.html', {'question': question}) index.html index() に対するテンプレートにはこのように記述する。 ビューで latest_question_list オブジェクトが辞書型に格納されて渡されているので、それを受け取ってテンプレート内でオブジェクトに格納された値を属性アクセス . して取得している。 <!-- 受け取った変数にデータがあるか確認する。 --> {% if latest_question_list %} <ul> {% for question in latest_question_list %} <li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</a></li> {% endfor %} </ul> {% else %} <p>No polls are available.</p> {% endif %} index.htmlのハードコード(直接記述している)を削除する このように直接URLを書き込むと変更に弱いコードになってしまうので、 polls.urlsモジュールのpath() 関数でname引数を定義したのでそれをURLに使用する。 {%url%} を使う。 <li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</a></li> 変更後 <li><a href="{% url 'detail' question.id %}">{{ question.question_text }}</a></li> そしてテンプレートを入れるディレクトリを作成した際のように名前空間を追加する。システムが別々のアプリ内で同じname引数を含んでいても区別が付けられるように template/polls/detail.html にアクセスしたい場合は polls:detail と記述する。 <!-- 受け取った変数にデータがあるか確認する。 --> {% if latest_question_list %} <ul> {% for question in latest_question_list %} <li><a href="{% url 'polls:detail' question.id %}">{{ question.question_text }}</a></li> {% endfor %} </ul> {% else %} <p>No polls are available.</p> {% endif %} そして、URLconf(urls.py)に名前空間を追加 app_name = 'polls' from django.urls import path from . import views # ここを新たに追加した。 app_name = 'polls' urlpatterns = [ path('', views.index, name='index'), path('<int:question_id>/', views.detail, name='detail'), path('<int:question_id>/results/', views.results, name='results'), path('<int:question_id>/vote/', views.vote, name='vote'), ] こうするとモジュールに指定されたURLの定義を検索出来る。例えば polls/specifics/12 のようにURLを変更した場合 urls.py に書かれたパスを変更する事でテンプレート側に変更を加える必要がない。 path('specifics/<int:question_id>/', views.detail, name='detail'), detail.html detail() に対するテンプレートはこのように記述する。 <h1>{{ question.question_text }}</h1> <ul> {% for choice in question.choice_set.all %} <li>{{ choice.choice_text }}</li> {% endfor %} </ul> フォームを使って質問に対して回答を送信する。 detail.html に <form> を追加してサーバにデータを送信して質問に対して、投票出来るようにする。 下記のように detail.html を変更する。 <!-- 質問の内容 --> <h1>{{ question.question_text }}</h1> <!-- もしデータベースから質問が取得出来ない場合エラーが表示される。 --> {% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %} <form action="{% url 'polls:vote' question.id %}" method="post"> <!-- セキュリティのため --> {% csrf_token %} <!-- 質問に対する選択肢を並べる --> {% for choice in question.choice_set.all %} <!-- forloop.counterはforタグのループが何度実行されたかを表す値です。 --> <input type="radio" name="choice" id="choice{{ forloop.counter }}" value="{{ choice.id }}"> <label for="choice{{ forloop.counter }}">{{ choice.choice_text }}</label><br> {% endfor %} <input type="submit" value="Vote"> </form> views.pyにvote()関数を追加する コードの流れとしては ユーザがdetailページの質問に対する選択肢を選択する。 Voteボタンをクリックする。 データがサーバに送信される。 選択された選択肢の投票数をインクリメントする。 results.htmlにリダイレクトする。Postデータが成功した後は基本的に HttpResponse ではなく HttpResponseRedirect を返す必要がある。 # 追加した。HttpResponse, HttpResponseRedirect from django.http import HttpResponse, HttpResponseRedirect from django.shortcuts import get_object_or_404, render # 追加した。reverse from django.urls import reverse # Choiceを追加した。 from .models import Choice, Question def index(request): latest_question_list = Question.objects.order_by('-pub_date')[:5] context = {'latest_question_list': latest_question_list} return render(request, 'polls/index.html', context)。 # モデルにアクセスするobjects.get()とHttp404が一緒になっている。 def detail(request, question_id): question = get_object_or_404(Question, pk=question_id) return render(request, 'polls/detail.html', {'question': question}) # 質問に対して選択して投票する。 def vote(request, question_id): # まず質問があるかどうか確認する。 question = get_object_or_404(Question, pk=question_id) try: # ユーザが選択した値からpk値を取得して、それを元にモデルから選択肢のオブジェクトを取得する。 # なければYou didn't...choiceと表示される。 selected_choice = question.choice_set.get(pk=request.POST['choice']) except (KeyError, Choice.DoesNotExist): # Redisplay the question voting form. return render(request, 'polls/detail.html', { 'question': question, 'error_message': "You didn't select a choice.", }) else: # 選択肢オブジェクトから何回投票されたか表示するvotesオブジェクトをインクリメントする。 selected_choice.votes += 1 # データベースに保存する。 selected_choice.save() # Always return an HttpResponseRedirect after successfully dealing # with POST data. This prevents data from being posted twice if a # user hits the Back button. # データの保存に成功したら、results.htmlにリダイレクトする。 return HttpResponseRedirect(reverse('polls:results', args=(question.id,))) reverse()関数とは この関数を使うと、vote関数中でのURLのハードコードを防ぐ事が出来る。 引数としては polls:results リダイレクト先のビュー名とそのビューに与えるURLパターン question.id を渡せる。 reverse('polls:results', args=(question.id,)) # 返り値 '/polls/3/results/' Post通信が成功した際のresults関数を作成する。 views.pyにresults関数を追加する。 # 先ほどまで書いてきたviews.pyにresults関数を追加する。 def results(request, question_id): # 指定したpkキーにデータがあれば返す、なければエラーを返す。 question = get_object_or_404(Question, pk=question_id) # 質問オブジェクトを引数で貰ってページを作成する。 return render(request, 'polls/results.html', {'question': question}) results.htmlを作成する。 <!-- 質問を表示する。 --> <h1>{{ question.question_text }}</h1> <!--質問の選択肢とそれに対する投票数を取得する。--> <ul> {% for choice in question.choice_set.all %} <!--choice.votes|pluralizeは投票数が2以上の場合vote s とsを追加してくれる。--> <li>{{ choice.choice_text }} -- {{ choice.votes }} vote{{ choice.votes|pluralize }}</li> {% endfor %} </ul> <a href="{% url 'polls:detail' question.id %}">Vote again?</a> Built-in template tags and filters | Django documentation | Django 汎用ビューを使って今まで書いたコードをさらに短くする。 views.py に書かれた index(), detail(), results() 関数は3つとも似たような機能でURLを介して渡されたパラメータに従ってデータベースからデータを取り出しページを作成する。これらの一連の動作はよくある事なのでDjangoでは汎用ビューというショートカットを用意してより簡素に機能を実装出来るようにしている。 汎用ビューを適用するにはいくつかこれまでに書いたコードを修正する必要がある。 URLconfを変換する。 古い不要なビューを削除する。 新しいビューにDjango汎用ビューを設定する。 URLconfの修正 変更前 from django.urls import path from . import views app_name = 'polls' urlpatterns = [ path('', views.index, name='index'), path('<int:question_id>/', views.detail, name='detail'), path('<int:question_id>/results/', views.results, name='results'), path('<int:question_id>/vote/', views.vote, name='vote'), ] 変更後 from django.urls import path from . import views app_name = 'polls' urlpatterns = [ path('', views.IndexView.as_view(), name='index'), path('<int:pk>/', views.DetailView.as_view(), name='detail'), path('<int:pk>/results/', views.ResultsView.as_view(), name='results'), path('<int:question_id>/vote/', views.vote, name='vote'), ] views.IndexView.as_view(), views.DetailView.as_view(), views.ResultsView.as_view() と as_view() と書くようになった。 そして、 question_id が pk に変更された。ここは同じでもいいような気もする。結局同じ数値が返り値として入るから。 ※後述する DetailView には pk キーを渡す必要があるので同じではダメなようだ。 viewsの修正 index(), detail(), results() 関数を削除しクラスベースに書き換える。 indexでは ListView を継承している。 detail, resultsでは DetailView を継承している。 ListView 「オブジェクトのリストを表示する。」 メソッドのフローチャート継承したメソッドが下記の順番で自動で実行される。 1.setup() 2.dispatch() 3.http_method_not_allowed() 4.get_template_names() 5.get_queryset() 6.get_context_object_name() 7.get_context_data() 8.get() 9.render_to_response() このようにメソッドが実行されるので継承したクラスに get_quesryset() メソッドを追加して内容を上書きする事が出来る。 template_name ListView ではデフォルトの場合 <app name>/<model name>_list.html を自動で生成して使用する。 その場合、テンプレート名は polls/question_list.html になる。 しかし元々作成してある polls/index.html を使用したい場合は template_name に 'polls/detail.html' を代入する事でDjangoがそちらを使用するように認識してくれる。 DetailView 「あるタイプのオブジェクト詳細ページを表示する。」 なので ListViewの詳細ページをDetailViewで表示するみたいな使われ方をする。 template_name そしてデフォルトでは DetailView は <app name>/<model name>_detail.html という名前のテンプレートを自動生成して使用する。 その場合、テンプレート名は polls/question_detail.html になるが、今回は自動生成されたものではなく元々作成してある polls/detail.html を使いたいので template_name を指定して元々のテンプレートを使用する。方法はListViewの時と同じで template_name に polls/detail.html を代入する。 model このクラス変数はビューが使用するモデルを指定している。 model = Question の場合は裏側で Question.objects.all() を行ってくれる。なので queryset = Question.objects.all() としても良い。 from django.http import HttpResponseRedirect from django.shortcuts import get_object_or_404, render from django.urls import reverse from django.views import generic from .models import Choice, Question class IndexView(generic.ListView): # デフォルトのビューを使用せず、元々作成してあったものを使用する。 template_name = 'polls/index.html' # 自動で渡されるquestion_listというコンテキスト変数の変数名を独自のものに変更している。 context_object_name = 'latest_question_list' def get_queryset(self): """最新の5件を取得する。""" return Question.objects.order_by('-pub_date')[:5] class DetailView(generic.DetailView): # 自分がどのモデルに対して動作するかを伝えている。 # おそらくget_object_or_404(Question, pk=question_id)のQuestion部分を担っている。 # pkの部分はurls.pyで先に指定してある。 model = Question template_name = 'polls/detail.html' class ResultsView(generic.DetailView): model = Question template_name = 'polls/results.html' def vote(request, question_id): ... # 前回のまま変更しない。 これでサーバを起動して特にエラーもなく質問のリストページ(index.html)、詳細ページ(投票するページdetail.html)、投票後の今ままでの投票数を表示するページ(results.html)が表示されていれば汎用ビューでのアプリ構築ができたと思う。 最後に 過去にRuby on railsのフレームワークの中身がどう動作しているのかイメージ出来ないのが苦痛(フレームワークは面倒な中身を気にしなくてもアプリが作れるように設計してあるので仕方ないかもしれない。)で挫折しているので今回Djangoのチュートリアルまだ途中ですが挫折せずにアプリ作成まで出来てよかったです。普段Jsonしか触らなかったので少しですがデータベースを作成して操作する経験が出来たのでこれを気にSQL構文をもう少し勉強しようと思う。多対多、多対一の関係とかも自分で作成出来るまでになります。自分の作成したアプリのER図をかける書けるようになりたい。 参照 SECRET_KEYを誤ってGitHubにプッシュしたときの対処法(Django編) - Qiita Pythonの相対インポートで上位ディレクトリ・サブディレクトリを指定 | note.nkmk.me ワイルドカードインポート(import *)は推奨されない 「カラム名」と「フィールド名」の違い|「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典 3. データベースマイグレーション | densan-labs.net モデル(データベース)の作成 プログラミングでよく見かける"コンテキスト(context)って何? - Qiita Python Django チュートリアル(3) - Qiita DJangoのお勉強(1) - Qiita ドキュメント Djangoの汎用ビュー入門(ListView) FOREIGN KEY制約(外部キー制約を設定する) 外部キー制約とは|「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典 Django ForeignKeyで1対多のモデルを構築 記事に関するコメント等は ?:Twitter ?:Youtube ?:Instagram ??‍?:Github ?:Stackoverflow でも受け付けています。どこかにはいます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Django公式チュートリアル(5~7)で分からない所、徹底的に調べた。

最初に 前回のチュートリアルで作成した投票アプリに対してテストコードを書く所から始める。内容はチュートリアル5〜7をカバーする予定になる。 Djangoでテストコードを書く チュートリアルが用意してくれたバグに対してテストコードを書いていく。 まずはバグを確認する。 Qustion.was_published_recently() のメソッドはQuestionが昨日以降に作成された場合に True を返すが 未来の日付になっている場合にもTrueを返す。これがバグになる。自分は前編の記事で登場した models.py に書かれた was_published_recently() はバグに対応済みなので下記のコードに入れ替えてバグを作り出す必要がある。 これだと pub_date が未来の場合も True を返す。 def was_published_recently(self): return self.pub_date >= timezone.now() - datetime.timedelta(days=1) バグの確認 コードに故意にバグを生み出した所でバグを確認したいと思う。 python manage.py shell データベース APIを叩いていく。 import datetime from django.utils import timezone from polls.models import Question # 投稿日を今から30日後に設定した Questionオブジェクトを作成する。 # この状態ではQuestionクラスからインスタンスを生成しただけでデータベースに保存はされていない。 future_question = Question(pub_date=timezone.now() + datetime.timedelta(days=30)) # 結果 True テストを作成する。 pollsアプリのディレクトリに tests.py というファイルがあると思うのでそこにテストコードを書いていく。 import datetime from django.test import TestCase from django.utils import timezone from .models import Question class QuestionModelTests(TestCase): def test_was_published_recently_with_future_question(self): """ was_published_recently()はpubdateが現在時刻より未来に設定された 場合はFalseを返さないといけない。 """ time = timezone.now() + detetime.timedelta(days=30) future_question = Question(pub_date=time) # ここで返却値がFalse出ない場合はテストに通らない事を設定している。 self.assertIs(future_question.was_published_recently(), False) テストを実行する。 python manage.py test polls # 実行結果 Creating test database for alias 'default'... System check identified no issues (0 silenced). F ====================================================================== FAIL: test_was_published_recently_with_future_question (polls.tests.QuestionModelTests) ---------------------------------------------------------------------- Traceback (most recent call last): File "/path/to/mysite/polls/tests.py", line 16, in test_was_published_recently_with_future_question self.assertIs(future_question.was_published_recently(), False) AssertionError: True is not False ---------------------------------------------------------------------- Ran 1 test in 0.001s FAILED (failures=1) Destroying test database for alias 'default'... テストは失敗したと出力されると思う。 バグを修正する models.py の記述された関数 was_published_recently() を元に戻してバグがない状態にしたいと思う。 def was_published_recently(self): now = timezone.now() return now - datetime.timedelta(days=1) <= self.pub_date <= now もう一度実行してみる。 python manage.py test polls # 実行結果 Creating test database for alias 'default'... System check identified no issues (0 silenced). . ---------------------------------------------------------------------- Ran 1 test in 0.001s OK Destroying test database for alias 'default'... 今度はテストはOKと出力され、テストに通った。 複数のテストを実行する 先ほど作成した QuestionModelTests クラスに別のテストも追加してみましょう。 import datetime from django.test import TestCase from django.utils import timezone from .models import Question class QuestionModelTests(TestCase): def test_was_published_recently_with_future_question(self): """ was_published_recently()はpubdateが現在時刻より未来に設定された 場合はFalseを返さないといけない。 """ time = timezone.now() + detetime.timedelta(days=30) future_question = Question(pub_date=time) # ここで返却値がFalse出ない場合はテストに通らない事を設定している。 self.assertIs(future_question.was_published_recently(), False) # 新しくテストを追加していく。 def test_was_published_recently_with_old_question(self): """ was_published_recently()はpub_dateが1日より過去の場合 Falseを返す """ # 現在時刻より一日1秒前の質問のインスタンスを作成する。 time = timezone.now() - timedelta(days=1, seconds=1) old_question = Question(pub_date=time) # 返り値がFalseならテストに通る。 self.assertIs(old_question.was_published_recently(), False) def test_was_published_recently_with_recent_question(self): """ was_published_recently()はpub_dateが1日以内ならTrueを返す """ # 一日以内の質問インスタンスを作成する。 time = timezone.now() - datetime.timedelta(hours=23, minutes=59, seconds=59) recent_question = Question(pub_date=time) self.assertIs(recent_question.was_published_recently(), True) これで過去、現在、未来に対してのテストが揃った。これで期待通りに動作する事を保証できるようになった。 Djangoのviewをテストする。 ビューレベルでのユーザ動作をシュミレートする事ができるClientを用意しているので、 tests.py や shellで使用する事ができる。 最初はshellから使用してみる。 python manage.py shell 下記のコードを一行ずつshellで実行する。 from django.test.utils import setup_test_environment # テンプレートのレンダラーをインストールする # response.context等の属性を調査できるようになる。 setup_test_environment() from django.test import Client # クライアントインスタンスを作成してページアクセスしたように操作する。 client = Client() response = client.get('/') # 実行結果 Not Found: / response.status_code # 実行結果 404 from django.urls import reverse response = client.get(reverse('polls:index')) response.status_code # 実行結果 200 # ページのhtmlが返ってくる。 response.content # 実行結果 b'\n <ul>\n \n <li><a href="/polls/3/">test3</a></li>\n \n <li><a href="/polls/2/">hello</a></li>\n \n <li><a href="/polls/1/">what&#x27;s up?</a></li>\n \n </ul>\n\n' response.context['latest_question_list'] # 実行結果 <QuerySet [<Question: test3>, <Question: hello>, <Question: what's up?>]> 現在の投票一覧は最新5件を取得しているため、未来の投稿日の質問も表示している。これを views.py の get_queryset() に変更を加えていく。 from django.http import HttpResponseRedirect from django.shortcuts import get_object_or_404, render from django.urls import reverse from django.views import generic # 新しく追加した from django.utils import timezone from .models import Choice, Question class IndexView(generic.ListView): # デフォルトのビューを使用せず、元々作成してあったものを使用する。 template_name = 'polls/index.html' # 自動で渡されるquestion_listというコンテキスト変数の変数名を独自のものに変更している。 context_object_name = 'latest_question_list' # 変更する箇所 def get_queryset(self): """ 最新の5件を取得する。ただし投稿日が現在時刻より前にある投稿のみ表示。 filter(pub_date__lte=timezone.now()) = if Question.pub_date <= timezone.now(): return Question.pub_date """ return Question.objects.filter( pub_date__lte=timezone.now() ).order_by('-pub_date')[:5] class DetailView(generic.DetailView): # 自分がどのモデルに対して動作するかを伝えている。 # おそらくget_object_or_404(Question, pk=question_id)のQuestion部分を担っている。 # pkの部分はurls.pyで先に指定してある。 model = Question template_name = 'polls/detail.html' class ResultsView(generic.DetailView): model = Question template_name = 'polls/results.html' def vote(request, question_id): ... # 前回のまま変更しない。 Djangoでは querySetの filter() を使用する際に変数を比較したい場合下記のような記述を取る事が出来る。 下記は Product.weight の値が2以下の場合 Trueになり返却されるオブジェクトになる。 # weight <= 2 products = Product.objects.filter(weight__lte=2) 他にも比較したり出来る、下記のサイトで説明されている。 Django逆引きチートシート(QuerySet編) - Qiita viewのテストを追加する import datetime from django.test import TestCase from django.utils import timezone from .models import Question # 新しく追加した。 from django.urls import reverse class QuestionModelTests(TestCase): def test_was_published_recently_with_future_question(self): """ was_published_recently()はpubdateが現在時刻より未来に設定された 場合はFalseを返さないといけない。 """ time = timezone.now() + detetime.timedelta(days=30) future_question = Question(pub_date=time) # ここで返却値がFalse出ない場合はテストに通らない事を設定している。 self.assertIs(future_question.was_published_recently(), False) # 新しくテストを追加していく。 def test_was_published_recently_with_old_question(self): """ was_published_recently()はpub_dateが1日より過去の場合 Falseを返す """ # 現在時刻より一日1秒前の質問のインスタンスを作成する。 time = timezone.now() - timedelta(days=1, seconds=1) old_question = Question(pub_date=time) # 返り値がFalseならテストに通る。 self.assertIs(old_question.was_published_recently(), False) def test_was_published_recently_with_recent_question(self): """ was_published_recently()はpub_dateが1日以内ならTrueを返す """ # 一日以内の質問インスタンスを作成する。 time = timezone.now() - datetime.timedelta(hours=23, minutes=59, seconds=59) recent_question = Question(pub_date=time) self.assertIs(recent_question.was_published_recently(), True)    # 新しく追加した def create_question(question_text, days): """ 引数から質問を作成する。過去に投稿された質問を作りたいなら-1~nの値を第二引数に取る、 まだ公開されてない質問を作成したいなら+1~nの値を第二引数に取る。 現在から10日後の投稿日の質問を作成したいなら 例: create_question('今日は何食べる?' , 10)     15日前の質問を作成したいなら create_question('今日は何食べる?' , -15) """ time = timezone.now() + datetime.timedelta(days=days) return Question.objects.create(question_text=question_text, pub_date=time) class QestionIndexViewTests(TestCase): def test_no_question(self): """ 質問がデータベースにない際に適切なメッセージを 表示出来てるか確認する。 """ response = self.client.get(reverse('polls:index')) # テスト合格条件 # ステータスコードが200である事 self.assertEqual(response.status_code, 200) # コンテンツに No polls are availableが含まれる事 self.assertContains(response, "No polls are available.") # データベースが空である事 self.assertQuerysetEqual(response.context['latest_question_list'], []) def test_past_question(self): """ 過去の投稿日の質問一覧が表示されるか確認する。 """ # 投稿日が30日前の質問を作成する。ダミーデータなので実際のデータベースにデータ作成されることはない。 # そしてメソッドが終了すればダミーデータは破棄される。 # なので新しくテストする際は質問は空の状態から始まる。 question = create_question(question_text="過去の質問", days=-30) response = self.client.get(reverse('polls:index')) # テスト合格条件 # 先ほど作成した質問が表示されているか表示する。 self.assertQuerysetEqual(response.context['latest_quesiton_list'], [question],) def test_future_question(self): """ 投稿日が未来の質問が表示されていないか確認する。 """ create_question(question_text="未来の質問", days=30) response = self.client.get(reverse('polls:index')) # テスト合格条件 # コンテンツに No polls are awailableが含まれる事 self.assertContains(response, "No polls are available.") # 最新の質問5件が質問が空な事 self.assertQuerysetEqual(response.context['latest_question_list'], []) def test_future_question_and_past_question(self): """ 過去・未来の質問の両方ある時に過去の質問だけ表示される。 """ # 片方だけ変数に入れるのはテストの合格条件を判別する際に過去質問が表示されているのを確認するため question = create_question(question_text="Past question.", days=-30) create_question(question_text="Future question.", days=30) response = self.client.get(reverse('polls:index')) self.assertQuerysetEqual( response.context['latest_question_list'], [question], ) def test_two_past_questions(self): """ 過去の質問2つが表示されているか確認する。 """ question1 = create_question(question_text="Past question 1.", days=-30) question2 = create_question(question_text="Past question 2.", days=-5) response = self.client.get(reverse('polls:index')) self.assertQuerysetEqual( response.context['latest_question_list'], [question2, question1], ) システムに問題がなければテストに全て合格する。作成された質問はデータベースに保存される事なく各テストが実行されて終わるたびに破棄される。 DetailViewのテスト 上記のテストは上手く動作して未来の質問はindexに表示されないが、 detail.html への正しいURLを知っていたり推測したユーザは、まだページに到達する事が出来る。そのため同じように未来の投稿日の場合はページを表示しないように polls/views.py コードを書き換える必要がある。 class DetailView(generic.DetailView): # テンプレートで変数にアクセスする際はquestionになる。 model = Question template_name = 'polls/detail.html' # 新しく追加した def get_queryset(self): """ まだ公開されていない質問は除外する。 """ return Question.objects.filter(pub_date__lte=timezone.now()) そして新たに追加した機能が動作するか確認するテストを書く。 tests.py に下記のコードを追加する。 class QuestionDataViewTests(TestCase): def test_future_question(self): """ detail.htmlの未来の日付のページにアクセスする場合は404を表示する、 """ # 現在から5日後の質問を作成する future_question = create_question(question_text = '未来の質問', days=5) url = reverse('polls:detail', args=(future_question.id,)) response = self.client.get(url) # 合格条件 # ページにアクセスした際のステータスコードが404 self.assertEqual(response.status_code, 404) def test_past_question(self): """ 過去の質問の場合はページを表示する。 """ past_question = create_question(question_text='過去の質問', days=-5) url = reverse('polls:detail', args=(past_question.id,)) response = self.client.get(url) # ページに過去の質問が含まれている。 self.assertContains(response, past_question.question_text) detailビューのテストも書いてきましたが、同様にresultsビューが必要になるが似たようなコードになるのでチュートリアルでは紹介されていない。 別の問題として現在の状態ではChoice(質問に対する選択肢)を持たない質問が公開されている。それを views.py で処理する事が出来るので機能を追加して、ChoicesがないQuestionを作成し、それが公開されないことをテスト、同じようにChoiceがあるQuestionを作成し、それが公開されることをテストをする。 get_queryset()に選択肢がない質問を表示しないようにfilterを追加する。 class IndexView(generic.ListView): template_name = 'polls/index.html' # テンプレート側でQuestion.objects.order_by('-pub_date')[:5]を呼び出す際の名前を設定している。 context_object_name = 'latest_question_list' def get_queryset(self):      # filter内の条件は現在より過去の質問かつ選択肢がある場合に質問オブジェクトを返すようになっている。 return Question.objects.filter(pub_date__lte=timezone.now(), choice__isnull=False).distinct().order_by('-pub_date')[:5] class DetailView(generic.DetailView): # テンプレートで変数にアクセスする際はquestionになる。 model = Question template_name = 'polls/detail.html' def get_queryset(self): print(Question.objects) return Question.objects.filter(pub_date__lte=timezone.now(), choice__isnull=False).distinct() choice__isnull=False で逆参照を行い 各質問にぶら下がる選択肢を確認する。 選択肢がある場合はおそらく内部でこんな感じに取得できると考えている。 Djangoのシェルに移動して直接データベースAPIを操作して選択肢があり参照関係になっている質問を確認する事ができる。 # 登録された選択肢を全て取り出して、それぞれがどこの質問に結びつけらているか表示している。 # 図でいう1, 1, 2を取り出しているのでそれに結びついたQuestionオブジェクトが表示されている。 [obj.question for obj in Choice.objects.all()] # 実行結果 [<Question: what's up?>, <Question: what's up?>, <Question: hello>] そして参照関係にない質問は obj.questtion しても空なので false になりその質問には選択肢がないと判断する事ができる。 distinct()で重複する結果を表示しないようにする。 親テーブルと子テーブルをJoinして作成された新しいテーブルになる。 そして同じフィールドに別の値を入れる事が出来ないので選択肢に対してどの質問が参照されているのかという表示方法になる。 そのため、1つの質問で複数の選択肢を参照している質問は参照する選択肢の数だけ表示されることになる。 http://127.0.0.1:8000/polls/ アクセスすると質問が重複して表示される。 この重複項目をなくすために distict() を使用する。 そうすると重複項目がなくなり、選択肢がない質問だけを表示する事ができる。 新しく追加した機能のテストコードを書いていく。 まず tests.py の create_question() で選択肢を含む質問を作成できるようにする。 choice_texts に値がある場合は、それを元に選択肢を作成する。複数作成することもできる。 def create_question(question_text, days, choice_texts=[]): """ 質問を `question_text` と投稿された日から作成する。現在より過去の時間で投稿したい場合は days= -days、 未来の時間で投稿したい場合は対してはdays= +daysとする。 """ time = timezone.now() + datetime.timedelta(days=days) q = Question.objects.create(question_text=question_text, pub_date=time) # 選択肢がある場合とない場合で変数に格納した際返ってくるモデルが変わるから注意が必要 # 選択肢があるとChoiceオブジェクトが変える。ないとQuestionオブジェクトになる。 if choice_texts: for choice_text in choice_texts: return q.choice_set.create(choice_text=choice_text, votes=0) else: return q これを使って、先ほど追加した indexページ、detailページで選択肢がない質問が表示されていないか確認するテストコードを書いていく、そして前回作成したテストも選択肢がない質問の場合ページが表示されなくなっているので、作成する質問に選択肢を付けてあげないとテストが通らなくなっている。 その修正も行う。このように一部変更を加えたために今まで通ってたテストを含めて、全体を修正しなくてはならないコードはとても修正が大変なので良いコードとは言えないかもしれない。もしもっと良いテストコードの書き方があったら教えて下さい。 tests.py これがテストの全体コードになる。 import datetime from django.http import response from django.test import TestCase from django.urls import reverse from django.utils import timezone from .models import Choice, Question # テストコードの書き方はTestCaseを継承する事 # メソッド名をtestから始める事でDjango側で実行してくれるようになる。 class QuestionModelTests(TestCase): def test_was_published_recently_with_future_question(self): """ was_published_recently()はpub_dateが未来の場合Falseを返す。 """ time = timezone.now() + datetime.timedelta(days=30) future_question = Question(pub_date=time) self.assertIs(future_question.was_published_recently(), False) def test_was_published_recently_with_recent_question(self): """ was_published_recently()はpub_dateが昨日までに投稿されたものなら Trueを返す。 """ time = timezone.now() - datetime.timedelta(hours=23, minutes=59, seconds=59) recent_question = Question(pub_date=time) self.assertIs(recent_question.was_published_recently(), True) def create_question(question_text, days, choice_texts=[]): """ 質問を `question_text` と投稿された日から作成する。現在より過去の時間で投稿したい場合は days= -days、 未来の時間で投稿したい場合は対してはdays= +daysとする。 """ time = timezone.now() + datetime.timedelta(days=days) q = Question.objects.create(question_text=question_text, pub_date=time) # 選択肢がある場合とない場合で変数に格納した際返ってくるモデルが変わるから注意が必要 # 選択肢があるとChoiceオブジェクトが変える。ないとQuestionオブジェクトになる。 if choice_texts: for choice_text in choice_texts: return q.choice_set.create(choice_text=choice_text, votes=0) else: return q class QuestionIndexViewTests(TestCase): def test_no_questions(self): # reverse('polls:index')でpollsのindexページURLを返している。それを利用してアクセスしている。 response = self.client.get(reverse('polls:index')) self.assertEqual(response.status_code, 200) self.assertContains(response, "No polls are available") self.assertQuerysetEqual(response.context['latest_question_list'], []) def test_past_question(self): question = create_question(question_text="Past question.", days=-30, choice_texts=['game set']) response = self.client.get(reverse('polls:index')) self.assertQuerysetEqual( response.context['latest_question_list'], [question.question], ) def test_future_question(self): create_question(question_text="Future question.", days=30, choice_texts=['game set']) response = self.client.get(reverse('polls:index')) self.assertContains(response, "No polls are available") self.assertQuerysetEqual(response.context['latest_question_list'], []) def test_future_question_and_past_question(self): # 片方だけ変数に入れるのはテストの合格条件を判別する際に過去質問が表示されているのを確認するため question = create_question(question_text="Past question.", days=-30, choice_texts=['game set']) create_question(question_text="Future question.", days=30, choice_texts=['game set']) response = self.client.get(reverse('polls:index')) self.assertQuerysetEqual( response.context['latest_question_list'], [question.question], ) def test_two_past_question(self): question1 = create_question(question_text="Past question 1.", days=-30, choice_texts=['game set']) question2 = create_question(question_text="Past qustion 2.", days=-5, choice_texts=['game set']) response = self.client.get(reverse('polls:index')) self.assertQuerysetEqual( response.context['latest_question_list'], [question2.question, question1.question], ) def test_choice_question(self): """ Indexページで 選択肢のある質問を表示する。 """ choice_question = create_question(question_text='Indexページでの選択肢のある質問', days=-1, choice_texts=['game set']) url = reverse('polls:index') response = self.client.get(url) self.assertContains(response, choice_question.question) def test_no_choice_question(self): """ Indexページで 選択肢がない質問は表示しない。 """ no_choice_question = create_question(question_text='Indexページでの選択肢のない質問', days=-1) url = reverse('polls:index') response = self.client.get(url) self.assertNotContains(response, no_choice_question) class QuestionDataViewTests(TestCase): def test_future_question(self): """ detail.htmlの未来の日付のページにアクセスする場合は404を表示する、 """ # 現在から5日後の質問を作成する future_question = create_question(question_text = '未来の質問', days=5) url = reverse('polls:detail', args=(future_question.id,)) response = self.client.get(url) # 合格条件 # ページにアクセスした際のステータスコードが404 self.assertEqual(response.status_code, 404) def test_past_question(self): """ Detailページ 過去の質問の場合はページを表示する。 """ past_question = create_question(question_text='過去の質問', days=-5, choice_texts=['geme set']) url = reverse('polls:detail', args=(past_question.id,)) response = self.client.get(url) # ページに過去の質問が含まれている。 self.assertContains(response, past_question.question) def test_choice_question(self): """ Detailページ 選択肢のある質問を表示する。 """ choice_question = create_question(question_text='detailページでの選択肢がある質問', days=-1, choice_texts=['game set']) url = reverse('polls:detail', args=(choice_question.id,)) response = self.client.get(url) self.assertContains(response, choice_question.choice_text) def test_no_choice_question(self): """ 選択肢がない質問は表示しない。 """ no_choice_question = create_question(question_text='detailページでの選択肢のない質問', days=-1) url = reverse('polls:detail', args=(no_choice_question.id,)) response = self.client.get(url) self.assertEqual(response.status_code, 404) まずは今まで動作していたテストが選択肢がない質問だったので、選択肢 ['game set'] を追加して再び動作するように変更する。 その際に choice_set.create で選択肢を追加した場合、返り値が Questionオブジェクトではなく Choiceオブジェクトになるので質問を取り出す際は 返り値.question とする必要がある。 問題なければ、テストが13個実行され OK と表示される。 スタイルシート・静的ファイルを追加する。 スタイルシートを追加 pollsディレクトリにstaticディレクトリを作成する。そうするとDjangoはそこから静的ファイルを探してくれる。 polls/static/polls と templateディレクトリを作成した時みたいになる。 先ほど追加したディレクトリに style.css を追加する。 polls/static/polls/style.css のようになる。 style.css li a { color: green; } polls/templates/polls/index.html の上部に下記のコードを追加する。 {% load static %} <link rel="stylesheet" type="text/css" href="{% static 'polls/style.css' %}"> 画像を追加する polls/static/polls/images/ とディレクトリを作成する。その中に 好きな画像をおく。 スタイルシートで背景画像として読み込む body { background: white url("images/background.gif") no-repeat; } li a { color: green; } adminのフォームをカスタマイズする 編集フォームでのフィールドの並び順を替える 質問の詳細ページでのフィールドの並び順を変更する。 polls/admin.py from django.contrib import admin from .models import Question class QuestionAdmin(admin.ModelAdmin): # この順番で表示されるようになる。 fields = ['pub_date', 'question_text'] # 第二引数で作成したclassを渡す admin.site.register(Question, QuestionAdmin) admin.site.register(Choice) 変更前 変更後 pub_dateとquestion_textの位置が入れ替わってる。 フィールドを分割する。 polls/admin.py from django.contrib import admin from .models import Question class QuestionAdmin(admin.ModelAdmin): fieldsets = [ (None, {'fields': ['question_text']}), ('Date information', {'fields': ['pub_date']}), ] admin.site.register(Question, QuestionAdmin) ChoiceオブジェクトをQuestionフォームから追加・編集する。 現在Choiceフォームから質問に選択肢を追加・編集可能ですが、これだとページを移動したりと効率が悪いので Questionフォームから追加・編集できるようにする。 polls/admin.py from django.contrib import admin from .models import Choice, Question class ChoiceInline(admin.StackedInline): model = Choice extra = 3 class QuestionAdmin(admin.ModelAdmin): fieldsets = [ (None, {'fields': ['question_text']}), ('Date information', {'fields': ['pub_date'], 'classes': ['collapse']}), ] inlines = [ChoiceInline] admin.site.register(Question, QuestionAdmin) コードを追加するとQuestionフォームに3つ(extraで数の調整ができる)の choice_text, votes を設定できる項目が追加される。 今のままだと多くの画面スペースを必要とするのでこれを小さくする。 class ChoiceInline の引数を TabularInline に変更する。 class ChoiceInline(admin.TabularInline): #... これでコンパクトになったと思う。 pollsの質問一覧ページをカスタマイズする。 チェンジリストページと呼ばれるページで(http://127.0.0.1:8000/admin/polls/question/)質問の一覧が表示されている。 現在は オブジェクトの名前(どんな質問が格納されているのがわかる)だけが表示されていますが、各フィールドの値を表示してより多くの情報をここで確認できるようにする。 polls/admin.py class QuestionAdmin(admin.ModelAdmin): # ... list_display = ('question_text', 'pub_date', 'was_published_recently') 各カラムのヘッダーをクリックすると並び替えを行えるが、 was_published_recently だけは並び替えをサポート出来ていないので、 @ デコレータを使用して並び替えの対応させていく。 デコレータなのでクラスメソッドの直前に追加する。 polls/models.py from django.contrib import admin class Question(models.Model): # ... # ここを新しく追加した。 @admin.display( boolean=True, ordering='pub_date', description='Published recently?', ) def was_published_recently(self): now = timezone.now() return now - datetime.timedelta(days=1) <= self.pub_date <= now 質問を日付でフィルター掛けれるようにする。 pub_date の日付を元に質問を絞れるようにする。フィルタは対象のフィールドの種類によって変化する。 pub_date は DateTimeField なので、Django はこのフィールドにふさわしいフィルタオプションが、「すべての期間 ("Any date")」「今日 ("Today")」「今週 ("Past 7 days")」「今月 ("This month")」 を用意してくれる。 polls/admin.py from django.contrib import admin from .models import Choice, Question class ChoiceInline(admin.StackedInline): model = Choice extra = 3 class QuestionAdmin(admin.ModelAdmin): fieldsets = [ (None, {'fields': ['question_text']}), ('Date information', {'fields': ['pub_date'], 'classes': ['collapse']}), ] inlines = [ChoiceInline] # 新しく追加した。 list_filter = ['pub_date'] admin.site.register(Question, QuestionAdmin) 質問の検索機能を追加する。 先ほどのコードにさらに変数を追加する。 question_text フィールドをユーザが入力した文字列を元に Likeクエリで検索するのでデータベースに割と負荷がかかるみたいで常識の範囲で使用しましょうとチュートリアルに記述されている。 # ... list_filter = ['pub_date'] # 新しく追加した search_fields = ['question_text'] 管理サイトの見た目をカスタマイズする。 管理サイトの上部に Django administration と書かれているのでこれを Polls administration と変更してみたいと思う。 manage.py が置かれているディレクトリに templates ディレクトリを作成する。その中に adminフォルダを作成する。 templates/admin みたいな構成になる。 その中にデフォルトのDjango adminのテンプレートをコピーして貼り付ける。 場所は 下記のコマンドから確認できる。anacondaの環境の場合は仮想環境内で実行する必要がある。 python -c "import django; print(django.__path__)" そして開いたファイルを下記のように編集する。 変更前 {% extends "admin/base.html" %} {% block title %}{% if subtitle %}{{ subtitle }} | {% endif %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %} {% block branding %} <h1 id="site-name"><a href="{% url 'admin:index' %}">{{ site_header|default:_('Django administration') }}</a></h1> {% endblock %} {% block nav-global %}{% endblock %} 変更後 {% extends "admin/base.html" %} {% block title %}{% if subtitle %}{{ subtitle }} | {% endif %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %} {% block branding %} <h1 id="site-name"><a href="{% url 'admin:index' %}">Polls Administration</a></h1> {% endblock %} {% block nav-global %}{% endblock %} 次に mysite/settings.py を開いて TEMPLATES 設定オプションの中にある DIRS オプションを下記のように変更する。 TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', # ここを新しく追加した。 'DIRS': [BASE_DIR / 'templates'], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ] これでデフォルトのテンプレートをオーバライドすることが出来た。 これでチュートリアル5~7の内容は終了した。 最後に チュートリアル5でテストコードを初めて書く経験が出来てよかったです。途中チュートリアルから外れた事をしようとした際に逆参照でモデルからデータを取得する方法がわからなくてかなり時間が掛かりました。SQLデータベースの理解がまだ乏しいのでもう少しデータベースに慣れてからDjangoでアウトプットとして、Webアプリを作成したいと思います。 参照 DjangoのModelからデータを取り出す方法をまとめとく - やる気がストロングZERO LEFT JOIN / INNER JOIN を実行すると同じ内容のレコードが複数含まれる - SQLの構文 ドキュメント 記事に関するコメント等は ?:Twitter ?:Youtube ?:Instagram ??‍?:Github ?:Stackoverflow でも受け付けています。どこかにはいます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

個人的メモ

venv を vscodeで読むときは ・リロードを忘れない ・日本語を使わない
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【初心者向け】Pythonで三目並べ(マルバツゲーム)を作る

はじめに この記事はプログラミング初心者さんがPythonで三目並べを作成するために作成されています。 学校での課題で出たよとか、とりあえずPythonで何か作ってみたいよって方を想定しています。 せっかくなのでエンジニア気分を味わってもらうべく、ちょっとカッコつけた(エンジニアっぽい)書き方を随所でしています。 ですが基本的には省略しすぎずにコードを書いていきます。 (全体的に、図少なめです。ご了承ください。) システム要件 このゲームは下記に示す様な、ターミナル上で動きます。 使用環境はPython3です。 よくある三目並べと比べると、かなり簡易化されています。 具体的には、今回紹介するゲームは以下の要件を満たします。 CPUとの対戦形式 自分が先攻 (o) でCPUが後攻 (x) CPUの手はランダムで確定 勝ち負け・引き分け判定がある 設計する コーディングを始める前に、設計をします。 初心者さんは特に設計に時間かけないとコードを書いている内に迷子になります。 ゲームの流れや細かい機能を考えていきます。 盤面管理 ゲームに利用する盤面はどのように管理する必要があると思いますか? この記事では盤面(gameBoard)は以下のように定義します。 gameBoard = [0,1,2,3,4,5,6,7,8] # ゲームに利用する盤面 普通のリストです。 番号と座標が対応しており、例えばプレイヤーが 1 座標に o を入力した場合は以下のように盤面が変化します。 gameBoard = [0,'o',2,3,4,5,6,7,8] # プレイヤーが座標1に'o'を入力 ゲームフローを考える 初めに「マルバツゲームを進行するのにどんな手順が必要だったっけ?」と考えます。 最初はざっくりとで大丈夫です。 例えば今回だと、 3x3の盤面を表示する ユーザーに入力をさせる 入力を盤面に反映させる 盤面表示 勝利判定をする CPUに入力をさせる 入力を盤面に反映させる 勝利判定をする 盤面表示 との具合で進めると、とりあえずプレイヤーとCPUがそれぞれ1手ずつ指すことができます。 機能の抽出 続いてはフローに基づいて「機能」を抽出していきます。 前節でのゲームフローでは9つのステップを表示しました。 ですが、必要な機能はもっとシンプルです。 3x3 盤面を表示 ユーザーまたはCPUに入力をさせて盤面に反映 勝利判定 これら 3つの機能を繰り返すことでゲームが成立させます 。 以上、設計終わり! 実装 それではお待ちかね、実装をしていきます。 前節でピックアップした3つの機能の実装 についてそれぞれ見ていきます! 3x3 盤面の表示 以下のリストがあったとすると、 gameBoard = [0,'o',2,3,4,5,6,7,8] # プレイヤーが座標1に'o'を入力 以下のように表示する機能を実装します。 0o2 345 678 (※補足) リストへのアクセス方法は下記を参考にしてください。 gameBoard = [0,'o',2,3,4,5,6,7,8] # こんな盤面の時 print(gameBoard[1]) # ← こうすると o が表示される 実装の方針は gameBoard[0] 、 gameBoard[1] ...と1マスずつ表示をさせ、 2番目と5番目 のときは表示後に改行させます。 言い換えると 2番目と5番目以外は改行しないで表示する ということになります。 ここはエンジニアっぽい書き方をしてみます。 とりあえず、コードを読んでみて下さい。 解説はコードの下に。 for i in range(0, len(gameBoard)): if(i%3 == 2): # 3回に1回は改行 print(gameBoard[i]) # 改行する else : print(gameBoard[i], end="") # 改行しない for で1マスずつ表示を行います。 i は0から始まり、ループのたびに1ずつ増え、9になったら forが終了します。 (つまり i は 0,1,2,3... と変化していく) ポイントは if(i%3 == 2): です。 i%3 は 「 i を3で割った数の余り」を示しています。 例えば、 i=2 のとき、 i%3 = 2、 i=4 のとき、 i%3 = 1 となります。 つまり、 for の中で i が 0,1,2,3,4,5,6,7,8 と増えるのに対して、 i%3 は 0,1,2,0,1,2,0,1,2, となります。 もうお分かり頂けたでしょうか? i%3 == 2の時というのは i が2,5,8の時を表しています。 これで、本節の冒頭で書いた 2番目と5番目 のときは表示後に改行させます。 という処理が書けました! ちなみに改行と改行しないの区別はそれぞれ print(表示するもの) と print(表示するもの, end="") の違いです。 後者が改行をさせない書き方になります 。 とりあえず実行 以下のコードを実行してみて下さい。 game.py gameBoard = [0,1,2,3,4,5,6,7,8] # ゲームに利用する盤面 for i in range(0, len(gameBoard)): if(i%3 == 2): # 3回に1回は改行 print(gameBoard[i]) # 改行する else : print(gameBoard[i], end="") # 改行しない 以下のように結果が出ましたか!? 012 345 678 関数定義 さて、これらの機能を関数として定義します。 関数とは、何かの入力に対して出力があるカタマリのことを指します。 よく自動販売機で例えられますね。 お金を入れる(入力)と飲み物が出てくる(出力)感じです。 とりあえず、以下を実行! game.py gameBoard = [0,1,2,3,4,5,6,7,8] # ゲームに利用する盤面 # 盤面を3x3で表示する関数 def displayBoard(): for i in range(0, len(gameBoard)): if(i%3 == 2): # 3回に1回は改行 print(gameBoard[i]) # 改行する else : print(gameBoard[i], end="") # 改行しない # ここが本命 displayBoard() # 盤面表示 print('Hello!') # 無駄にHelloを表示 displayBoard() # 盤面表示 以下の一文を書くだけで、 def displayBoard(): さっきの for が入った処理を displayBoard() として呼び出せるようになっています! 2回for文を書かなくても、下記のように盤面が2回表示されるはずです! 012 345 678 Hello!! 012 345 678 ターンを進める 続いて、ターンを進めるためにinputBoard という関数を作ります。 ターンを進めるのには3つのステップが考えられます。 0~8の座標を入力 入力座標に o か x が入っていたら再入力 それ以外ならgameBoardに反映 これだけ!簡単! ちなみに inputBoard は def inputBoard(playerType): とし、 inputBoard('o') とやるとプレイヤー、 inputBoard('x') とやるとCPU といった具合に、引数である playerType に o か x が入る想定です。 とりあえず完成形。 # ターンを進めるための関数 def inputBoard(playerType): # 1.座標を入力させる if(playerType == "o"): # o が渡されたら座標を入力 tgt = int(input("0~8の座標を入れてください: ")) else: # xが渡されたらランダムで座標を入力 tgt = random.randint(0,8) # 2.入力座標に 'o'か 'x' が入っていたら再入力 if(gameBoard[tgt] == 'o' or gameBoard[tgt] == 'x'): inputBoard(playerType) # 3.gameBoardに反映 else: gameBoard[tgt] = playerType inputBoard('o') # これでユーザーに入力をさせることができる inputBoard('x') # これでCPUに入力をさせることができる Step1: 0~8の座標を入力 プレイヤーかCPUに0~8の座標を入力させます。 プレイヤーからの座標入力を受け付ける それでは、プレイヤーが入力した座標を tgt という変数に格納します。 input() は文字列を受け取るので int() というので括って、入力値を数字に変換しています。 tgt = int(input("0~8の座標を入れてください: ")) # プレイヤーの入力座標受付 CPUはランダムな値を入力する プレイヤーと同様に、CPUの座標も tgt という変数に格納しましょう。 これもかなりシンプル。 tgt = random.randint(0,8) 整数の乱数はrandom.randint(最小値,最大値) という具合に使用します。 randomを使うために、コードの一番上に game.py import random を入れる必要があります。 プレイヤーかCPUか これら2つの機能が if 文で分割されており、 playerType を見ることで、どちらを呼び出すかを判定しています。 以下、Step1の完成形 def inputBoard(playerType): # 1.座標を入力させる if(playerType == "o"): # o が渡されたら座標を入力 tgt = int(input("0~8の座標を入れてください: ")) else: # xが渡されたらランダムで座標を入力 tgt = random.randint(0,8) Step2: 入力座標に o か x が入っていたら再入力 続いては、入力座標に o か x が入っていたら再入力機能を実装します。 Step2は以下の一文だけ。 # 2.入力座標に 'o'か 'x' が入っていないことを確認 if(gameBoard[tgt] == 'o' or gameBoard[tgt] == 'x'): inputBoard(playerType) Step1で tgt という座標をユーザーかCPUが入力しました。 そのため、 gameBoard[tgt] に何が入っているのかを if で検証します。 if では条件を or で繋ぐことできます。 if( 条件A or 条件B): 実行するもの を日本語に訳すなら もしも「条件A」または「条件B」を満たしていたら「実行するもの」を実行する といった具合です。 今回の場合は inputBoard をもう一度呼び出すことで実装しています。 Step3: 何も入ってないならgameBoardに反映 これもとてもシンプル。 # 3.gameBoardに反映 else: gameBoard[tgt] = playerType Step2の if と繋がっていることに注意して下さい。 ここまでで、表示と入力ができました。 全てをつなぎ合わせると、以下のようになります。 game.py import random gameBoard = [0,1,2,3,4,5,6,7,8] # ゲームに利用する盤面 # 盤面を3x3で表示する def displayBoard(): # gameBoardの要素を1つずつ表示 for i in range(0, len(gameBoard)): if(i%3 == 2): # 3回に1回は改行 print(gameBoard[i]) # 改行する else : print(gameBoard[i], end="") # 改行しない # ターンを進める def inputBoard(playerType): # 1.座標を入力させる if(playerType == "o"): # o が渡されたら座標を入力 tgt = int(input("0~8の座標を入れてください: ")) else: # xが渡されたらランダムで座標を入力 tgt = random.randint(0,8) # 2.入力座標に 'o'か 'x' が入っていないことを確認 if(gameBoard[tgt] == 'o' or gameBoard[tgt] == 'x'): inputBoard(playerType) # 3.gameBoardに反映 else: gameBoard[tgt] = playerType # ゲームの進行 displayBoard() inputBoard('o') # ユーザーのターン displayBoard() inputBoard('x') # CPUのターン displayBoard() これを実行すると、自分の入力をしたあとに、CPUがランダムで入力をしてくれます。 ちなみに最後の方にある、 # ゲームの進行 displayBoard() inputBoard('o') # ユーザーのターン displayBoard() inputBoard('x') # CPUのターン displayBoard() を # ゲームの進行 while(True): displayBoard() inputBoard('o') # ユーザーのターン displayBoard() inputBoard('x') # CPUのターン とすると while(True) の中が無限ループするので、盤面が埋まるまで入力をし続けることができます。 Step:3 勝敗判定 最後に勝敗判定を行います。 とりあえず、勝敗判定を行う winner() という関数の完成形をお見せします。 # 勝利判定 def winner(): # 勝ち手を列挙 lines = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6], ] # forで勝ち手を1パターンずつ見ていく for i in range(0, len(lines)): [a, b, c] = lines[i] # 勝ち手の場所に同じ記号が入っていないかを確認 if gameBoard[a] and gameBoard[a] == gameBoard[b] and gameBoard[a] == gameBoard[c]: # 同じ記号が入っていたら、入っている記号を返す return gameBoard[a] # どちらも勝っていない場合はNoneを返す return None winner() を呼び出すと、 o か x か None が returnされます。 linesに入っている勝ち手を全てチェックして、同一の記号が入っていないかチェックしているだけです。 1つ補足をすると、 [a, b, c] = lines[i] は a = lines[i][0] b = lines[i][1] c = lines[i][2] を省略しています。 lines[i] には [0, 1, 2] や [3, 4, 5] などの勝ち手のいずれか1つが格納されます。 もっと具体的に書くと lines[1] は [3, 4, 5] となり、 lines[1][0] は 3 です。 a,b,cに「勝ち手座標」が格納され、 gameBoard[a] と gameBoard[b] と gameBoard[c] に同じ記号が入ってたら、その記号のプレイヤーが勝利です。 以上で機能は全て完成! ゲームを進行する さて、上記で作成した機能をループで回していきましょう! 燃え尽きたので、細かい説明は省略! displayBoard() # 実行時に初めに表示される盤面 for turn in range(0,9): # 誰のターンかを判定する if(turn %2 == 0) : # あなたのターン print("You") inputBoard("o") else: # CPUのターン print("CPU") inputBoard("x") displayBoard() # 盤面表示 if winner(): # 勝敗判定 print(winner() + "の勝ち") break if turn == 8: # 8手目で引き分け print("引き分け") break 完成 おめでとうございます! 以上で完成です! 下記ソースは説明に使ったもの以外に、デコレーションが入っています。 それでは、良いエンジニアライフを! import random gameBoard = [0,1,2,3,4,5,6,7,8] # ゲームに利用する盤面 # 盤面を3x3で表示する def displayBoard(): # gameBoardの要素を1つずつ表示 print("--+---+--") # デコレーション for i in range(0, len(gameBoard)): if(i%3 == 2): # 3回に1回は改行 print(gameBoard[i]) # 改行する print("--+---+--") # デコレーション elif(i%3 == 1): print(" | " + str(gameBoard[i]) + " | ", end="") # 改行しない else : print(gameBoard[i], end="") # 改行しない print() # ターンを進める def inputBoard(playerType): # 1.座標を入力させる if(playerType == "o"): # o が渡されたら座標を入力 tgt = int(input("0~8の座標を入れてください: ")) else: # xが渡されたらランダムで座標を入力 tgt = random.randint(0,8) # 2.入力座標に 'o'か 'x' が入っていないことを確認 if(gameBoard[tgt] == 'o' or gameBoard[tgt] == 'x'): inputBoard(playerType) # 3.gameBoardに反映 else: gameBoard[tgt] = playerType # 勝利判定 def winner(): # 勝ち手を列挙 lines = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6], ] # forで勝ち手を1パターンずつ見ていく for i in range(0, len(lines)): [a, b, c] = lines[i] # 勝ち手の場所に同じ記号が入っていないかを確認 if gameBoard[a] and gameBoard[a] == gameBoard[b] and gameBoard[a] == gameBoard[c]: # 同じ記号が入っていたら、入っている記号を返す return gameBoard[a] # どちらも勝っていない場合はNoneを返す return None displayBoard() # 実行時に初めに表示される盤面 # ゲームを実行する for turn in range(0,9): # 誰のターンかを判定する if(turn %2 == 0) : # あなたのターン print("You") inputBoard("o") else: # CPUのターン print("CPU") inputBoard("x") displayBoard() # 盤面表示 if winner(): # 勝敗判定 print(winner() + "の勝ち") break if turn == 8: # 8手目で引き分け print("引き分け") break
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

遺伝的アルゴリズムについてコードも交えて説明する

はじめに この記事では遺伝的アルゴリズムについて説明する。初めにどのようなアルゴリズムなのか説明し、そこで紹介した操作を小分けにpythonでnumpyのみを使って実装する。遺伝的アルゴリズムというだけのことがあって高校の生物基礎程度の知識があると理解が容易にできる。 遺伝的アルゴリズム 遺伝子アルゴリズムは進化論の考え方に基づいたアルゴリズムで、データ(表現型)を遺伝子(遺伝子型)のように変形し選択や交叉、突然変異などの操作を繰り返すことによって最適な解を捜索するアルゴリズムである。表現型は遺伝子の評価などに使用し、遺伝子型は操作の対象として使われる。 フローチャート 厳密ではないがフローチャートは以下のようになる。Typoraでうまい作り方がわからず、随分下手なものになってしまったので参考程度に。 決められた世代までとあるが、評価がある閾値を超えるまで繰り返しを行うケースもある。また、遺伝子アルゴリズムでは最後に求められた解が最適な解とは限らないので過去も含め最も評価が良かったものを採用することもある。 遺伝子操作 次世代の遺伝子を生成する時、選択、交叉、突然変異の三つの動作のうちどれかを行う。それぞれが起きる確率の合計は1であり、交叉が起こる確率は突然変異よりもとても大きくすると良い。選択は学習、交叉と突然変異は捜索を行っていると考えられる。なので、交叉や突然変異を選択する確率は世代が進むにつれて小さくするとより良い結果を得られると考えている(私の意見なので参考程度に)。 選択 適応度を用いて優秀な遺伝子を増やしたり劣等な遺伝子を減らしたりする操作である。選択法にはルーレット選択、ランキング選択、トーナメント選択、エリート保存選択などがある。 ルーレット選択 ある確率$p_i$によって個体$i$を選択する方法である。確率$p_i$は個体の適応度によって決まり、以下のような式で表される。 $$ p_i = \frac{f_i}{\sum f_j} $$ この選択法は適応度が負の場合を考慮しない方法で、遺伝子間の適応度の差が大きいときは適応度の高い個体ばかりが選ばれることによって収束が早期に起きてしまうと言う問題がある(私の考えだが、前者はexpで和取れば解決できるのではと考えている)。 ランキング選択 適応度の大きさでソートし、その順番によってあらかじめ与えた確率で選択する方法である。なるべくシンプルな実験をしたいときはこの選択方法が採用される。あらかじめ与えられた確率で計算するので、適応度の差による確率の違いが出ない。これによって早期収束の問題はないが、適応度の差が極めて小さい場合も確率が大きく変わることがある。また、ソートを選択の都度行わなければならないので計算コストが高くなる。 トーナメント選択 集団から決まった個数だけランダムに遺伝子を選び出し、その中で最も適応度の高い遺伝子を選択するこれを元の遺伝子個数回行う方法である(選ばれた遺伝子は次世代の遺伝子となり、その後の選択に影響はない)。選び出す個数はパラメーターであり、大きくすることで適応度の大きい遺伝子が次世代に増えるが、その場合早期収束する可能性が高まる。 エリート保存選択 エリート保存選択はこれまで紹介した選択法と組み合わせて使う選択法である。これは、適応度の大きさでソートし上位幾つかの遺伝子を次世代の遺伝子としてあらかじめ残すのようにする方法である。世代数を小さく収束した解を得たいときに使われる手法であり、局所的な解に収束しやすくなることや、ソートしなければならない問題がある。 交叉 遺伝子間で染色体を組み替えることによって次世代の個体を生成する操作であり、生物の交配によって子孫を残すことをモデル化したものである。一般的には一点交叉、二点交叉、多点交叉、一様交叉などが使われる。この記事では多点交叉については説明しない(二点交叉より良い値が出ることが少なく使うメリットがほとんどないから)。 一点交叉 最初に提案された交叉で遺伝子を入れ替える場所を一つ決め、それ以降を入れ替える方法である。効率が悪いため、現在はあまり使われない。 例 |は決めた場所 遺伝子1: 10101|001 => 次世代1: 10101100 遺伝子2: 01110|100 => 次世代2: 01111001 二点交叉 遺伝子の入れ替えの始点と終点を決め、その間の染色体を入れ替える方法である。 例 |は決めた場所 遺伝子1: 1010|101|0 => 次世代1: 10100100 遺伝子2: 1010|010|0 => 次世代2: 10101010 上の例で最適解が10101110であるとき入れ替えによって最適となる確率は$\frac{2}{8(8-1)}=\frac{1}{28}$である。8は遺伝子のサイズであり、これが大きくなると最適解が得られる確率が小さくなってしまう。この性質をヒッチハイキングという。 一様交叉 遺伝子の染色体をそれぞれ$\frac{1}{2}$の確率で入れ替える方法である。二点交差で求められなかった(求めにくかった)問題に使うと有効なことが多い(逆も然り)。 例 |は決めた場所 遺伝子1: 10101010 => 次世代1: 10100010 遺伝子2: 10100100 => 次世代2: 10101100 二点交差ではヒッチハイキングの問題があったが、一様交差では最適となる確率は$\frac{2}{2^3}=\frac{1}{4}$となり遺伝子のサイズに依存しないのでこの問題は発生しない。 突然変異 任意で染色体の一部の値を反転させる操作である(2進数の場合、数値だとランダム)。これによって局所的な解に収束することがなくなる。先にも述べたが、突然変異を起こす確率は他の操作(特に選択)と比べるととても小さい。なぜなら頻繁に突然変異を起こしているとランダムに次世代の遺伝子を生み出しているだけであり、最適な解を得られないからである。通常0.1~1%で設定される。 実装 遺伝子数が$N$、遺伝子の長さが$length$、遺伝子は0または1の値を取る染色体で構成されているとして実装する。 紹介するコードを実行したいときは以下でサンプルを利用すると良い(他の変数は適宜追加)。 N = 10 length = 8 gene_arr = np.random.randint(2, size=length) fitness_arr = np.random.rand(N) 表現型と遺伝子型 表現型は遺伝子の適応度を計算するにあたって使用する型、遺伝子型は操作するにあたって使用する型である。表現型は一般的に10進数のように我々にも評価がわかりやすい型で、遺伝子型は2進数のようにわかりにくい型であることが多い。例えば関数の最大化なのでは実際その通りで、表現型として10進数、遺伝子型として2進数と変形して計算する(変形は2進数から10進数のみ)。2進数から10進数への変換だが、その場合のコードは以下のようになる。 def geno2pheno(gene_arr): """ 遺伝子型から表現型への変換 Attributes ---------- geno_tabel: ndarray     表現型へ変更するためのテーブル Parameters -------------- gene_arr: ndarray 遺伝子 Returns ---------- 表現型の遺伝子 """ geno_table = np.array([2**i for i in range(length)]) return np.sum(gene_arr * geno_table, axis=1) / (2 ** length - 1) 長さlengthによって表現型の大きさが変わらないように正規化する。  選択 選択の引数としてchoice_numを導入した。理由はエリート保存選択で次世代の遺伝子を先に決めた時とエリート保存選択を行わなかった時では選択数が異なるからだ。choice_num+fix_num=Nの条件が課せられている。 ルーレット選択 def roulette_choice(gene_arr, fitness_arr, choice_num): """ ルーレット選択 Attributes ------------- idx: ndarray 次の世代に選ばれた遺伝子のインデックス(重複可) Parameters -------------- gene_arr: ndarray 遺伝子 fitness_arr: ndarray 適応度が格納された配列 choice_num: int 選択する遺伝子数 Returns ---------- 次世代の遺伝子 """ idx = np.random.choice(np.arange(N), size=choice_num, p=fitness_arr/sum(fitness_arr)) return gene_arr[idx] np.random.choiceの引数pは確率であり渡す配列は合計が1でなければいけない。そのため適応度fitness_arrその合計で割る必要がある。 ランキング選択 def ranking_choice(gene_arr, fitness_arr, prob_arr, choice_num): """ ランキング選択 Attributes ------------- idx: ndarray 次の世代に選ばれた遺伝子のインデックス(重複可) Parameters -------------- gene_arr: ndarray 遺伝子 fitness_arr: ndarray 適応度が格納された配列 prob_arr: ndarray あらかじめ与えられた確率(適応度順) choice_num: int 選択する遺伝子数 Returns ---------- 次世代の遺伝子 """ gene_rank = np.argsort(fitness_arr) idx = np.random.choice(np.arange(N), size=choice_num, p=prob_arr) return gene_arr[gene_rank][idx] トーナメント選択 def tournament_choice(gene_arr, fitness_arr, choice_num): """ トーナメント選択 Attributes ------------- next_gene_arr: list 次世代の遺伝子 idx_chosen1: int ランダムに選ばれたインデックス idx_chosen2: int ランダムに選ばれたインデックス choice_num: int 選択する遺伝子数 Parameters -------------- gene_arr: ndarray 遺伝子 fitness_arr: ndarray 適応度が格納された配列 Returns ---------- 次世代の遺伝子 """ next_gene_arr = [] for i in range(choice_num): [idx_chosen1, idx_chosen2] = np.random.randint(N, size=2) if fitness_arr[idx_chosen1] > fitness_arr[idx_chosen1]: next_gene_arr.append(gene_arr[idx_chosen1]) else: next_gene_arr.append(gene_arr[idx_chosen2]) return np.array(next_gene_arr) エリート保存選択 def elite_choice(gene_arr, fitness_arr, fix_num): """ エリート保存選択 Attribute ----------- gene_rank: ndarray 遺伝子を適応度順に並び替えたときのインデックス Parameters -------------- gene_arr: ndarray 遺伝子 fitness_arr: ndarray 適応度が格納された配列 fix_num: int 次世代に残す遺伝子数 Returns ---------- 次世代の遺伝子 """ gene_rank = np.argsort(fitness_arr) return gene_arr[gene_rank][:fix_num]  交叉 一点交叉 def one_point_crossing(gene_arr, cross_point): """ 一点交叉 Attribute ----------- idx_chosen1: int ランダムに選ばれたインデックス idx_chosen2: int ランダムに選ばれたインデックス gene2: ndarray 二つ目のインデックスの遺伝子をコピーしたもの Parameters -------------- gene_arr: ndarray 遺伝子 cross_point: int 入れ替える場所を示す数字 Returns ---------- 次世代の遺伝子 """ [idx_chosen1, idx_chosen2] = np.random.randint(N, size=2) gene2 = gene_arr[idx_chosen2].copy() gene_arr[idx_chosen2][cross_point:] = gene_arr[idx_chosen1][cross_point:] gene_arr[idx_chosen1][cross_point:] = gene2[cross_point] return gene_arr 二点交叉 def two_point_crossing(gene_arr, begin_point, end_point): """ 二点交叉 Attribute ----------- idx_chosen1: int ランダムに選ばれたインデックス idx_chosen2: int ランダムに選ばれたインデックス gene2: ndarray 二つ目のインデックスの遺伝子をコピーしたもの Parameters -------------- gene_arr: ndarray 遺伝子 begin_point: int 入れ替える場所を示す数字(始点) begin_point: int 入れ替える場所を示す数字(終点) Returns ---------- 次世代の遺伝子 """ [idx_chosen1, idx_chosen2] = np.random.randint(N, size=2) gene2 = gene_arr[idx_chosen2].copy() gene_arr[idx_chosen2][begin_point: end_point] = gene_arr[idx_chosen1][begin_point: end_point] gene_arr[idx_chosen1][begin_point: end_point] = gene2[begin_point: end_point] return gene_arr 一様交叉 def uniform_crossing(gene_arr): """ 一様交叉 Attribute ----------- idx_chosen1: int ランダムに選ばれたインデックス idx_chosen2: int ランダムに選ばれたインデックス gene2: ndarray 二つ目のインデックスの遺伝子をコピーしたもの Parameters -------------- gene_arr: ndarray 遺伝子 Returns ---------- 次世代の遺伝子 """ [idx_chosen1, idx_chosen2] = np.random.randint(N, size=2) gene2 = gene_arr[idx_chosen2].copy() for i, chomosome in enumerate(gene2): if np.random.rand() < 0.5: gene_arr[idx_chosen2][i] = gene_arr[idx_chosen1][i] gene_arr[idx_chosen1][i] = chomosome return gene_arr 突然変異 突然変異は確率がいろいろ出てくるので整理する。まず、下記のコードにはない突然変異の操作をする確率。次に突然変異を起こすとき遺伝子が突然変異を起こす確率。最後に遺伝子が突然変異を起こすとして選ばれた時、染色体が突然変異を起こす確率である。 def mutation(gene_arr, gene_rate, chromosome_rate): """ 突然変異 Parameters -------------- gene_arr: ndarray 遺伝子 gene_rate: double 遺伝子が突然変異を起こす確率 chromosome_rate: double 染色体が突然変異を起こす確率 Returns ---------- 次世代の遺伝子 """ for i, gene in enumerate(gene_arr): if gene_rate > np.random.rand(): for j, chromosome in enumerate(gene): if chromosome_rate > np.random.rand(): gene_arr[i, j] = 1 - chromosome return gene_arr 以上のものを組み合わせることで遺伝的アルゴリズムのコードを書くことができる。それを書くと長すぎるのでここでは省略する。 まとめ 遺伝的アルゴリズムは生物の進化システムを模倣したようなアルゴリズムで、選択、交叉、突然変異などの操作を繰り返すことによって最適解に近い解を見つけることができる。選択、交叉、突然変異などはそれぞれ次世代の遺伝子を作り出すための操作であり、選択、交叉で学習を行い突然変異で捜索を行うようなアルゴリズムであった。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Python勉強記録 #7 Scikit-learnで重回帰分析を実装

Scikit-learnとは Scikit-learnは、Pythonの機械学習ライブラリです。「サイキット・ラーン」と読みます。 これを使って重回帰分析を行ってみます。 下準備 まず、使うライブラリをインポートする必要があります。 # Scikit-learnを使う時に一緒によく使うライブラリをインポート import numpy as np import pandas as pd import matplotlib.pyplot as plt import seaborn as sns それぞれが何かは、今までの記事で解説してきました。 データセットのインポート Scikit-learnの中にデータセットがあらかじめ色々と用意してあります。 今回はボストン近郊の住宅データセットを使います。 from sklearn.datasets import load_boston これでボストン近郊の住宅データセットを読み込みました。実際にどんなデータが入っているのが、みてみましょう。 print(load_boston()) """ 出力結果 : {'data': array([[6.3200e-03, 1.8000e+01, 2.3100e+00, ..., 1.5300e+01, 3.9690e+02, 4.9800e+00], [2.7310e-02, 0.0000e+00, 7.0700e+00, ..., 1.7800e+01, 3.9690e+02, 9.1400e+00], [2.7290e-02, 0.0000e+00, 7.0700e+00, ..., 1.7800e+01, 3.9283e+02, 4.0300e+00], ..., [6.0760e-02, 0.0000e+00, 1.1930e+01, ..., 2.1000e+01, 3.9690e+02, 5.6400e+00], [1.0959e-01, 0.0000e+00, 1.1930e+01, ..., 2.1000e+01, 3.9345e+02, 6.4800e+00], [4.7410e-02, 0.0000e+00, 1.1930e+01, ..., 2.1000e+01, 3.9690e+02, 7.8800e+00]]), 'target': array([24. , 21.6, 34.7, 33.4, 36.2, 28.7, 22.9, 27.1, 16.5, 18.9, 15. , 18.9, 21.7, 20.4, 18.2, 19.9, 23.1, 17.5, 20.2, 18.2, 13.6, 19.6, 15.2, 14.5, 15.6, 13.9, 16.6, 14.8, 18.4, 21. , 12.7, 14.5, 13.2, 13.1, 13.5, 18.9, 20. , 21. , 24.7, 30.8, 34.9, 26.6, 25.3, 24.7, 21.2, 19.3, 20. , 16.6, 14.4, 19.4, 19.7, 20.5, 25. , 23.4, 18.9, 35.4, 24.7, 31.6, 23.3, 19.6, 18.7, 16. , 22.2, 25. , 33. , 23.5, 19.4, 22. , 17.4, 20.9, 24.2, 21.7, 22.8, 23.4, 24.1, 21.4, 20. , 20.8, 21.2, 20.3, 28. , 23.9, 24.8, 22.9, 23.9, 26.6, 22.5, 22.2, 23.6, 28.7, 22.6, 22. , 22.9, 25. , 20.6, 28.4, 21.4, 38.7, 43.8, 33.2, 27.5, 26.5, 18.6, 19.3, 20.1, 19.5, 19.5, 20.4, 19.8, 19.4, 21.7, 22.8, 18.8, 18.7, 18.5, 18.3, 21.2, 19.2, 20.4, 19.3, 22. , 20.3, 20.5, 17.3, 18.8, 21.4, 15.7, 16.2, 18. , 14.3, 19.2, 19.6, 23. , 18.4, 15.6, 18.1, 17.4, 17.1, 13.3, 17.8, 14. , 14.4, 13.4, 15.6, 11.8, 13.8, 15.6, 14.6, 17.8, 15.4, 21.5, 19.6, 15.3, 19.4, 17. , 15.6, 13.1, 41.3, 24.3, 23.3, 27. , 50. , 50. , 50. , 22.7, 25. , 50. , 23.8, 23.8, 22.3, 17.4, 19.1, 23.1, 23.6, 22.6, 29.4, 23.2, 24.6, 29.9, 37.2, 39.8, 36.2, 37.9, 32.5, 26.4, 29.6, 50. , 32. , 29.8, 34.9, 37. , 30.5, 36.4, 31.1, 29.1, 50. , 33.3, 30.3, 34.6, 34.9, 32.9, 24.1, 42.3, 48.5, 50. , 22.6, 24.4, 22.5, 24.4, 20. , 21.7, 19.3, 22.4, 28.1, 23.7, 25. , 23.3, 28.7, 21.5, 23. , 26.7, 21.7, 27.5, 30.1, 44.8, 50. , 37.6, 31.6, 46.7, 31.5, 24.3, 31.7, 41.7, 48.3, 29. , 24. , 25.1, 31.5, 23.7, 23.3, 22. , 20.1, 22.2, 23.7, 17.6, 18.5, 24.3, 20.5, 24.5, 26.2, 24.4, 24.8, 29.6, 42.8, 21.9, 20.9, 44. , 50. , 36. , 30.1, 33.8, 43.1, 48.8, 31. , 36.5, 22.8, 30.7, 50. , 43.5, 20.7, 21.1, 25.2, 24.4, 35.2, 32.4, 32. , 33.2, 33.1, 29.1, 35.1, 45.4, 35.4, 46. , 50. , 32.2, 22. , 20.1, 23.2, 22.3, 24.8, 28.5, 37.3, 27.9, 23.9, 21.7, 28.6, 27.1, 20.3, 22.5, 29. , 24.8, 22. , 26.4, 33.1, 36.1, 28.4, 33.4, 28.2, 22.8, 20.3, 16.1, 22.1, 19.4, 21.6, 23.8, 16.2, 17.8, 19.8, 23.1, 21. , 23.8, 23.1, 20.4, 18.5, 25. , 24.6, 23. , 22.2, 19.3, 22.6, 19.8, 17.1, 19.4, 22.2, 20.7, 21.1, 19.5, 18.5, 20.6, 19. , 18.7, 32.7, 16.5, 23.9, 31.2, 17.5, 17.2, 23.1, 24.5, 26.6, 22.9, 24.1, 18.6, 30.1, 18.2, 20.6, 17.8, 21.7, 22.7, 22.6, 25. , 19.9, 20.8, 16.8, 21.9, 27.5, 21.9, 23.1, 50. , 50. , 50. , 50. , 50. , 13.8, 13.8, 15. , 13.9, 13.3, 13.1, 10.2, 10.4, 10.9, 11.3, 12.3, 8.8, 7.2, 10.5, 7.4, 10.2, 11.5, 15.1, 23.2, 9.7, 13.8, 12.7, 13.1, 12.5, 8.5, 5. , 6.3, 5.6, 7.2, 12.1, 8.3, 8.5, 5. , 11.9, 27.9, 17.2, 27.5, 15. , 17.2, 17.9, 16.3, 7. , 7.2, 7.5, 10.4, 8.8, 8.4, 16.7, 14.2, 20.8, 13.4, 11.7, 8.3, 10.2, 10.9, 11. , 9.5, 14.5, 14.1, 16.1, 14.3, 11.7, 13.4, 9.6, 8.7, 8.4, 12.8, 10.5, 17.1, 18.4, 15.4, 10.8, 11.8, 14.9, 12.6, 14.1, 13. , 13.4, 15.2, 16.1, 17.8, 14.9, 14.1, 12.7, 13.5, 14.9, 20. , 16.4, 17.7, 19.5, 20.2, 21.4, 19.9, 19. , 19.1, 19.1, 20.1, 19.9, 19.6, 23.2, 29.8, 13.8, 13.3, 16.7, 12. , 14.6, 21.4, 23. , 23.7, 25. , 21.8, 20.6, 21.2, 19.1, 20.6, 15.2, 7. , 8.1, 13.6, 20.1, 21.8, 24.5, 23.1, 19.7, 18.3, 21.2, 17.5, 16.8, 22.4, 20.6, 23.9, 22. , 11.9]), 'feature_names': array(['CRIM', 'ZN', 'INDUS', 'CHAS', 'NOX', 'RM', 'AGE', 'DIS', 'RAD', 'TAX', 'PTRATIO', 'B', 'LSTAT'], dtype='<U7'), 'DESCR': ".. _boston_dataset:\n\nBoston house prices dataset\n---------------------------\n\n**Data Set Characteristics:** \n\n :Number of Instances: 506 \n\n :Number of Attributes: 13 numeric/categorical predictive. Median Value (attribute 14) is usually the target.\n\n :Attribute Information (in order):\n - CRIM per capita crime rate by town\n - ZN proportion of residential land zoned for lots over 25,000 sq.ft.\n - INDUS proportion of non-retail business acres per town\n - CHAS Charles River dummy variable (= 1 if tract bounds river; 0 otherwise)\n - NOX nitric oxides concentration (parts per 10 million)\n - RM average number of rooms per dwelling\n - AGE proportion of owner-occupied units built prior to 1940\n - DIS weighted distances to five Boston employment centres\n - RAD index of accessibility to radial highways\n - TAX full-value property-tax rate per $10,000\n - PTRATIO pupil-teacher ratio by town\n - B 1000(Bk - 0.63)^2 where Bk is the proportion of blacks by town\n - LSTAT % lower status of the population\n - MEDV Median value of owner-occupied homes in $1000's\n\n :Missing Attribute Values: None\n\n :Creator: Harrison, D. and Rubinfeld, D.L.\n\nThis is a copy of UCI ML housing dataset.\nhttps://archive.ics.uci.edu/ml/machine-learning-databases/housing/\n\n\nThis dataset was taken from the StatLib library which is maintained at Carnegie Mellon University.\n\nThe Boston house-price data of Harrison, D. and Rubinfeld, D.L. 'Hedonic\nprices and the demand for clean air', J. Environ. Economics & Management,\nvol.5, 81-102, 1978. Used in Belsley, Kuh & Welsch, 'Regression diagnostics\n...', Wiley, 1980. N.B. Various transformations are used in the table on\npages 244-261 of the latter.\n\nThe Boston house-price data has been used in many machine learning papers that address regression\nproblems. \n \n.. topic:: References\n\n - Belsley, Kuh & Welsch, 'Regression diagnostics: Identifying Influential Data and Sources of Collinearity', Wiley, 1980. 244-261.\n - Quinlan,R. (1993). Combining Instance-Based and Model-Based Learning. In Proceedings on the Tenth International Conference of Machine Learning, 236-243, University of Massachusetts, Amherst. Morgan Kaufmann.\n", 'filename': '/usr/local/lib/python3.7/dist-packages/sklearn/datasets/data/boston_house_prices.csv'} """ このままでは非常に見辛いですね。 なので、これをpandasのデータフレーム型で表示しましょう。 まずは、x(入力変数)とt(目標値)を定義します。 dataset = load_boston() # dataset の中の data を x, target を t に代入 x,t = dataset.data, dataset.target # dataset の中の feature_names を columns に代入 columns = dataset.feature_names feature_namesというのは、CRIM,ZNなどが入っているリストですね。 そしてこれをpandasのデータフレーム型にして表示すると、 df = pd.DataFrame(x, columns=columns) df.head(3) """ 出力結果 : CRIM ZN INDUS CHAS NOX RM AGE DIS RAD TAX PTRATIO B LSTAT 0 0.00632 18.0 2.31 0.0 0.538 6.575 65.2 4.0900 1.0 296.0 15.3 396.90 4.98 1 0.02731 0.0 7.07 0.0 0.469 6.421 78.9 4.9671 2.0 242.0 17.8 396.90 9.14 2 0.02729 0.0 7.07 0.0 0.469 7.185 61.1 4.9671 2.0 242.0 17.8 392.83 4.03 """ このように見やすくなりました。 columns=columnsというのは、データフレーム内のcolumns(=列の名前)に、先ほど定義したcolumnsを代入するという感じです。名前が同じでややこしいですね。 僕はここを理解するのに時間がかかりましたが、一度load_boston()の中身を見れば、意味がわかりやすいと思います。 上のデータフレームには、targetの列がありませんので、追加します。 # tのデータを、'Target'という名前の列を追加してそこに代入 df['Target'] = t これで、Targetという列がデータフレームに追加されます。 データをndarray型に変換 機械学習アルゴリズムの実装を行う際には、numpyのndarray型にする必要があります。 それは、.valuesの属性で取り出すことができます。 そして、同時にデータフレーム内の入力変数と出力変数の切り分けを行います。 データフレーム内の'Target'が出力変数、それ以外が入力変数にあたります。 切り分ける時は、.dropメソッドを使います。 # 'Target'という列名の列の値をtに代入 t = df['Target'].values # dfの中の'Target'という列名の列の値以外の値をxに代入 x = df.drop(labels=['Target'], axis=1).values 学習用データセットとテスト用データセットへ分割 ボストン近郊の住宅データセットを学習用データセットとテスト用データセットに分けます。 学習用データセットで、適切な重みなどを求めて、それが本当に使えるかどうかをテスト用データセットでテストするためです。 from sklearn.model_selection import train_test_split x_train, x_test, t_train, t_test = train_test_split(x, t, test_size=0.3, random_state=0) train_test_splitを使うことで、データセットを x_train学習用入力変数 x_testテスト用入力変数 t_train学習用出力変数 t_testテスト用出力変数 と、ランダムに抽出して4つに分けます。 test_size=0.3というのは、データ全体の3割をテスト用データセットにするという意味です。 random_state=0は、ランダムに抽出する際のランダム性を指定することができます。同じ数字にすることで再現性を確保できます。 モデルの学習・検証 機械学習において、入力値を受け取り、何かしらの評価・判定をして出力値を出すものをモデルと言います。 例えば家賃を予測するモデルであれば、賃貸物件の面積と駅からの距離という入力値を受け取り、重回帰分析をして家賃という出力値を出すという働きをします。 モデルを定義し、学習させ、検証する。 この3ステップで、モデルを作っていきます。 1.モデルの定義 Scikit-learnで重回帰分析を行う時は、LinearRegressionを使います。 from sklearn.linear_model import LinearRegression # モデルの定義 model = LinearRegression() これで、modelは、重回帰分析をするモデルだと定義します。 2.モデルの学習 今回学習で使用するのは、先ほど分割したデータセットのうち、学習用のデータセットです。 # モデルの学習 model.fit(x_train, t_train) この.fitを使うだけで、モデルの学習が完了します。簡単ですね。 ここでパラメータ(重みwとバイアスbのこと)を確かめてみましょう。 # 重み print(model.coef_) """ 出力結果 : [-1.21310401e-01 4.44664254e-02 1.13416945e-02 2.51124642e+00 -1.62312529e+01 3.85906801e+00 -9.98516565e-03 -1.50026956e+00 2.42143466e-01 -1.10716124e-02 -1.01775264e+00 6.81446545e-03 -4.86738066e-01] """ # バイアス print(model.intercept_) # 出力結果 : 37.93710774183309 重みについて、棒グラフで可視化することもできます。(出力結果は割愛) plt.figure(figsize=(10, 7)) plt.bar(x=columns, height=model.coef_) 3.モデルの検証 モデルの検証は以下で行えます。 # 学習済みモデルを用いて、学習用データセットで計算した係数 print('train score : ', model.score(x_train, t_train)) # 出力結果 : train score : 0.7645451026942549 # 学習済みモデルを用いて、テスト用データセットで計算した係数 print('test score : ', model.score(x_test, t_test)) # 出力結果 : test score : 0.6733825506400171 出力された係数が、1に近ければ近いほど良いモデルです。 学習用データに対してのみ数値が高く、テスト用データに対して数値が低い状態のことを「過学習」といいます。 同じことばっかり勉強しちゃって、他の問題への応用がききにくくなってしまった、違う傾向を持つ問題について対応しにくくなった、そんな状態です。 過学習を防いで、テスト用データで計算した係数を1に近づけるようにしましょう。 推論 学習が終わったモデルに新たな入力値を加えて、予測値を計算させることもできます。それを「推論」と言います。 テスト用データからサンプルを一つ取り出し、推論してみます。 # x_testに対して、モデルで推論する y = model.predict(x_test) # 推論結果の1番目の予測値を表示 print('予測値 : ', y[0]) # 目標値の1番目はどうだったか確認 print('目標値 : ', t_test[0]) ''' 出力結果 : 予測値 : 24.935707898576915 目標値 : 22.6 ''' まぁまぁ近い数字が推論されていることがわかりました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

天文データ解析入門 その9 (イメージにコントアを重ねる)

この記事では、イメージにコントアを重ねる方法について記述します。 今回も例として、Spitzer 3.6 µm, 8.0 µm, 24 µm のアーカイブデータを使用します。 https://irsa.ipac.caltech.edu/data/SPITZER/GLIMPSE/images/I/1.2_mosaics_v3.5/ から GLM_03000+0000_mosaic_I1.fits GLM_03000+0000_mosaic_I4.fits をダウンロードします。 また、 https://irsa.ipac.caltech.edu/data/SPITZER/MIPSGAL/images/mosaics24/ から MG0290n005_024.fits MG0290p005_024.fits MG0300n005_024.fits MG0300p005_024.fits MG0310n005_024.fits MG0310p005_024.fits の計6つの fits をダウンロードし、つなぎ合わせた W43_24um.fits を用います そして、以前の記事 で regrid した 3 色分の fits GLM_03000+0000_mosaic_I1_reg.fits GLM_03000+0000_mosaic_I4_Reg.fits W43_24um.fits を用い、前回の記事 で作成した RGB 合成図の png ファイルも用います。 これらに、国立天文台の FUGIN プロジェクトで得られた野辺山45m電波望遠鏡の CO 輝線のアーカイブデータを重ねます。データは http://jvo.nao.ac.jp/portal/nobeyama/ から FGN_03100+0000_2x2_12CO_v1.00_cube.fits をダウンロードします (重いです)。 通常の 2D イメージの上に通常のコントアを重ねる 例として、8.0 µm のデータの上に CO 輝線の強度を plot します。 まずは必要なものを import します。 from astropy.io import fits import numpy as np from matplotlib import pyplot as plt import aplpy from astropy.wcs import WCS CO の cube を 2D fitsにするため、以下のような関数を定義します。 def v2ch(v, w): # v(km/s)をchに変える x_tempo, y_tempo, v_tempo = w.wcs_pix2world(0, 0, 0, 0) x_ch, y_ch, v_ch = w.wcs_world2pix(x_tempo, y_tempo, v*1000.0, 0) v_ch = int(round(float(v_ch), 0)) return v_ch def del_header_key(header, keys): # headerのkeyを消す import copy h = copy.deepcopy(header) for k in keys: try: del h[k] except: pass return h def make_new_hdu_integ(hdu, v_start_wcs, v_end_wcs, w): # 積分強度のhduを作る data = hdu.data header = hdu.header start_ch, end_ch = v2ch(v_start_wcs, w), v2ch(v_end_wcs, w) new_data = np.nansum(data[start_ch:end_ch+1], axis=0)*header["CDELT3"]/1000.0 header = del_header_key(header, ["CRVAL3", "CRPIX3", "CRVAL3", "CDELT3", "CUNIT3", "CTYPE3", "CROTA3", "NAXIS3"]) header["NAXIS"] = 2 new_hdu = fits.PrimaryHDU(new_data, header) return new_hdu def make_new_hdu_peak(hdu, v_start_wcs, v_end_wcs, w): # 積分強度のhduを作る data = hdu.data header = hdu.header start_ch, end_ch = v2ch(v_start_wcs, w), v2ch(v_end_wcs, w) new_data = np.nanmax(data[start_ch:end_ch+1], axis=0) header = del_header_key(header, ["CRVAL3", "CRPIX3", "CRVAL3", "CDELT3", "CUNIT3", "CTYPE3", "CROTA3", "NAXIS3"]) header["NAXIS"] = 2 new_hdu = fits.PrimaryHDU(new_data, header) return new_hdu 以下のように 2D fits を定義・作成します。 I4_hdu = fits.open("~/your/fits/dir/GLM_03000+0000_mosaic_I4_reg.fits")[0] CO_hdu = fits.open("~/your/fits/dir/FGN_03100+0000_2x2_12CO_v1.00_cube.fits")[0] w = WCS(CO_hdu.header) peak_hdu = make_new_hdu_peak(CO_hdu, 75.0, 125.0, w) 例えば以下のように plot します。 fig = plt.figure(figsize=(12, 12)) f = aplpy.FITSFigure(I4_hdu, figure=fig, slices=[0],convention='wells') f.show_colorscale(vmin=10, vmax=1000, stretch='log', cmap="Greens", aspect="equal") f.add_colorbar() f.colorbar.show() f.colorbar.set_axis_label_text("8.0 $\mu$m [Jy str$^{-1}$]") f.recenter(30.75, 0.0, width=0.5, height=0.5) f.show_contour(peak_hdu, levels=[20, 25, 30, 35, 40, 45, 50], convention='wells', colors=["w"], slices=[0], linewidths=[1,2,2,2,2,2,2,2]) 色や線幅をコントアレベルごとに指定できます。 通常の 2D イメージの上に半透明のコントアを重ねる 透過度 (alpha) を指定して、for文でレベルごとに引いていきます。 fig = plt.figure(figsize=(12, 12)) f = aplpy.FITSFigure(I4_hdu, figure=fig, slices=[0],convention='wells') f.show_colorscale(vmin=10, vmax=1000, stretch='log', cmap="Greys", aspect="equal") f.add_colorbar() f.colorbar.show() f.colorbar.set_axis_label_text("8.0 $\mu$m [Jy str$^{-1}$]") f.recenter(30.75, 0.0, width=0.5, height=0.5) levels = [20, 25, 30, 35, 40, 45, 50] for l in levels: f.show_contour(peak_hdu, levels=[l, 99999.9], convention='wells', colors="r", slices=[0], linewidths=[1,2,2,2,2,2,2,2], filled=True, alpha=0.15) RGB イメージの上にコントアを重ねる 前回の記事 で作成した RGB 合成図の png ファイルを用います。 color_min_R = 30.0 color_max_R = 1500.0 color_min_G = 30.0 color_max_G = 1500.0 color_min_B = 10.0 color_max_B = 100.0 colorval = "%.1f_%.1f_%.1f_%.1f_%.1f_%.1f"%(color_min_R, color_max_R, color_min_G, color_max_G, color_min_B, color_max_B) save_png_name = "RGB_%s"%(colorval)+'.png' fig = plt.figure(figsize=(12, 12)) f = aplpy.FITSFigure(I4_hdu, slices=[0], figure=fig, convention='wells') f.recenter(30.75, 0.0, width=0.5, height=0.5) f.show_rgb(save_png_name) f.ticks.set_color('w') f.show_contour(peak_hdu, levels=[20, 25, 30, 35, 40, 45, 50], convention='wells', colors="w", slices=[0], linewidths=[1,2,2,2,2,2,2,2]) 以上です。 リンク 目次
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【プログラムの解説動画?あり】Pythonでワードクラウド作成!〜SudachiPyを使った実装方法紹介〜

こんにちは!マーケティングリサーチプラットフォームを提供している、株式会社マーケティングアプリケーションズ(MApps)です。 本記事は、弊社でUPしている動画「【Python ワードクラウド】実装する方法を10分でシンプルに解説!【コードとサンプルデータもDLできます!】」を図解した内容になっています。 プログラムの解説動画もあります!(むしろ動画がメインです) ・動画だと2倍速でもご視聴できますので、素早くキャッチアップすることができます。 ・テレビの番組みたいに解説してくれる方が助かる!という方は、ぜひ動画をご利用ください。 (実は記事を書いている自分は、全く知らないことを勉強する時、動画の方が有り難いと感じたりします。そういう方に向いているかも?) 下記の画像をクリックすると、YouTubeの動画を再生することができます。 読んで良かった、参考になったという方は、ぜひLGTMボタンを押してください。??? 今後の執筆の励みになります? おわりに 図解だけだとわかりにくかった・・・という方は、? プログラムの解説動画も併せて、是非ご覧ください。 下記をクリックすると、YouTubeの動画を再生することができます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

天文データ解析入門 その8 (aplpy で RGB合成図を作成する)

この記事では、aplpy を用いたRGB画像の作り方について記述します。 今回も例として、Spitzer 3.6 µm, 8.0 µm, 24 µm のアーカイブデータを使用します。 https://irsa.ipac.caltech.edu/data/SPITZER/GLIMPSE/images/I/1.2_mosaics_v3.5/ から GLM_03000+0000_mosaic_I1.fits GLM_03000+0000_mosaic_I4.fits をダウンロードします。 また、 https://irsa.ipac.caltech.edu/data/SPITZER/MIPSGAL/images/mosaics24/ から MG0290n005_024.fits MG0290p005_024.fits MG0300n005_024.fits MG0300p005_024.fits MG0310n005_024.fits MG0310p005_024.fits の計6つの fits をダウンロードし、つなぎ合わせた W43_24um.fits を用います そして、前回の記事 で regrid した 3 色分の fits GLM_03000+0000_mosaic_I1_reg.fits GLM_03000+0000_mosaic_I4_Reg.fits W43_24um.fits を用います。 まずは必要なものを import します。 import aplpy これだけです。 fits の場所と名前を変数にしておきます。 data_fits_R = "~/your/fits/dir/W43_24um.fits" data_fits_G = "~/your/fits/dir/GLM_03000+0000_mosaic_I4_reg.fits" data_fits_B = "~/your/fits/dir/GLM_03000+0000_mosaic_I1_reg.fits" fitss = [data_fits_R, data_fits_G, data_fits_B] それぞれカラースケールを決めます。そして保存する名前も定義します。 color_min_R = 30.0 color_max_R = 1500.0 color_min_G = 30.0 color_max_G = 1500.0 color_min_B = 10.0 color_max_B = 100.0 colorval = "%.1f_%.1f_%.1f_%.1f_%.1f_%.1f"%(color_min_R, color_max_R, color_min_G, color_max_G, color_min_B, color_max_B) save_png_name = "RGB_%s"%(colorval)+'.png' 以下で png ファイルを生成します (少し時間がかかります)。 aplpy.make_rgb_image(fitss, save_png_name, vmin_r=color_min_R, vmax_r=color_max_R,vmin_g=color_min_G, vmax_g=color_max_G, vmin_b=color_min_B, vmax_b=color_max_B, stretch_r="log", stretch_g="log", stretch_b="log") もし何かエラー (大抵 PIL) が出た場合は、エラーメッセージで google 検索すると解決できる可能性が高いです。どうしてもダメな場合は ipython などで実行するといいかもしれません。 座標をつけて plot したい場合は以下のようにします。 import matplotlib.pyplot as plt fig = plt.figure(figsize=(16, 12)) f = aplpy.FITSFigure(data_fits_R, slices=[0], figure=fig, convention='wells') f.show_rgb(save_png_name) f.ticks.set_color('w') f.save('RGB_aplpy.pdf', dpi=300) 欠損部分について 24 µmのデータは強すぎるところがサチっていて NaN が入っています。 この NaN 部分をある値 (大抵 2000 Jy/str くらい) に置き換えたい場合は、 from astropy.io import fits import numpy as np hdu = fits.open("W43_24um.fits")[0] data = hdu.data data[data!=data] = 2000.0 # data!=dataは NaN の場所を示す fits.PrimaryHDU(data, hdu.header).writeto("W43_24um_nan2000.fits", overwrite=True) として、こちらを使用してください。 以上です。 リンク 目次
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

天文データ解析入門 その7 (fitsのregrid)

本記事では、fits の regrid の方法について紹介します。 例として、Spitzer 3.6 µm, 8.0 µm, 24 µm のアーカイブデータを使用します。 https://irsa.ipac.caltech.edu/data/SPITZER/GLIMPSE/images/I/1.2_mosaics_v3.5/ から GLM_03000+0000_mosaic_I1.fits GLM_03000+0000_mosaic_I4.fits をダウンロードします。 また、 https://irsa.ipac.caltech.edu/data/SPITZER/MIPSGAL/images/mosaics24/ から MG0290n005_024.fits MG0290p005_024.fits MG0300n005_024.fits MG0300p005_024.fits MG0310n005_024.fits MG0310p005_024.fits の計6つの fits をダウンロードし、つなぎ合わせた W43_24um.fits を用います (以前の記事 を参照)。 現状、casa でやる方法と miriad でやる方法が主流かと思います。それぞれインストール方法は公式のページを参照ください。 casa ターミナルで casa を起動します。パスが通っている場合は、casa と打つだけです。通していない場合は、 your/casa/dir/casa-X.X.X-XXX/bin/casa で起動します。 まず、fitsを読み込みます。 importfits(fitsimage="GLM_03000+0000_mosaic_I1.fits", imagename="GLM_03000+0000_mosaic_I1.im/", overwrite=True) importfits(fitsimage="GLM_03000+0000_mosaic_I4.fits", imagename="GLM_03000+0000_mosaic_I4.im/", overwrite=True) importfits(fitsimage="W43_24um.fits", imagename="W43_24um.im/", overwrite=True) これで casa 形式 (image) になりました。最後に true と表示されていればおそらく成功です。 この GLM_03000+0000_mosaic_I4.im を W43_24um.im と同じ grid にします。 (※ この場合、元々の grid サイズがほぼ等しいのでこのまま regrid を実行しますが、 grid サイズ (≈分解能) がかけ離れている場合には細かい方に smoothing (imsmooth) をかける必要があります。) imregrid を実行します。 imregrid(imagename="GLM_03000+0000_mosaic_I1.im/", template="W43_24um.im/", output="GLM_03000+0000_mosaic_I1_reg.im/", overwrite=True) imregrid(imagename="GLM_03000+0000_mosaic_I4.im/", template="W43_24um.im/", output="GLM_03000+0000_mosaic_I4_reg.im/", overwrite=True) imagename が regrid をかける image、template が合わせる先の image です。最後に true と表示されていればおそらく成功です。 これを fits に戻します。 exportfits(imagename="GLM_03000+0000_mosaic_I1_reg.im/", fitsimage="GLM_03000+0000_mosaic_I1_reg.fits", overwrite=True) exportfits(imagename="GLM_03000+0000_mosaic_I4_reg.im/", fitsimage="GLM_03000+0000_mosaic_I4_reg.fits", overwrite=True) 何も表示されなければおそらく成功です。 もし 3D fits (cube) の速度軸だけ regrid したい場合などは、imregrid の際にオプションで axes=2 を指定しましょう。 miriad miriad の場合もほぼ同様です。ターミナルで miriad と打って miriad を起動します。 まず fits を読み込みます。 fits in=GLM_03000+0000_mosaic_I1.fits out=GLM_03000+0000_mosaic_I1.mir op=xyin fits in=GLM_03000+0000_mosaic_I4.fits out=GLM_03000+0000_mosaic_I4.mir op=xyin fits in=W43_24um.fits out=W43_24um.mir op=xyin これで miriad 形式になりました。fits の名前が長すぎるとエラーを吐くので注意です。 regrid を実行します。 regrid in=GLM_03000+0000_mosaic_I1.mir out=GLM_03000+0000_mosaic_I1_reg2.mir tin=W43_24um.mir regrid in=GLM_03000+0000_mosaic_I4.mir out=GLM_03000+0000_mosaic_I4_reg2.mir tin=W43_24um.mir オプションで回転を入れたり GAL <=> RADec 変換ができたりなど、色々便利です。 (https://www.atnf.csiro.au/computing/software/miriad/doc/regrid.html) あとは fits に戻します。 fits in=GLM_03000+0000_mosaic_I1_reg2.mir out=GLM_03000+0000_mosaic_I1_reg2.fits op=xyout fits in=GLM_03000+0000_mosaic_I4_reg2.mir out=GLM_03000+0000_mosaic_I4_reg2.fits op=xyout これで grid の揃った 3 色の fits が出来上がりました。 以上です。 リンク 目次
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Sentinel2で衛星画像タイルを作成する(1)衛星画像ダウンロード編

Sentinel2 の衛星画像を使い衛星画像タイルを作成してみたので、何回かに分けて書いてみます。 まずこの記事では衛星画像を取得する Python ライブラリの sentinelsat について解説します。 sentinelsatで衛星画像をダウンロードしてみる。 sentinelsatでの条件にあった衛星画像を取得するクエリの書き方。 sentinelsat で衛星画像ダウンロード 1. Copernicus Open Access Hub でユーザー登録 まず、衛星画像をダウンロードするために、Copernicus Open Access Hub にアクセスしユーザー登録をします。「Sign up」をクリックしてユーザー登録をして下さい。 https://scihub.copernicus.eu/dhus/#/home ユーザー名とパスワードを sentinelsat で指定して衛星画像をダウンロードするので、忘れないようにメモしておいて下さい。 Sentinel2のプラットフォームについて ちなみにSentinel2の画像をダウンロードできるプラットフォームは色々ありますが、esa が運営しているプラットフォームは「Copernicus Open Access Hub」です。 「SentinelHub」など民間企業が運営している似たようなプラットフォームがあるので間違わないように気をつけて下さい。 2. sentinelsat で衛星画像をダウンロード 衛星画像のダウンロードに必要なライブラリをインストールします。 $ pip install sentinelsat $ pip install matplotlib $ pip install shapely main.py を作成して 必要なライブラリをインポートして下さい。 # manin.py import os from sentinelsat import SentinelAPI, read_geojson, geojson_to_wkt import matplotlib.pyplot as plt from shapely.geometry import MultiPolygon, Polygon 2.1. 衛星画像を取得したい範囲のGeoJSONを作成 衛星画像を取得したい範囲の緯度経度を取得し、GeoJSONを作成します。 キーン州立大学が公開しているツールを使い緯度経度を取得します。 https://www.keene.edu/campus/maps/tool/ GeoJSONを作成します。 # manin.py # 衛星画像取得する範囲を指定 AREA = [ [ 139.0240751, 36.1879383 ], [ 139.0570341, 34.7880413 ], [ 140.8093534, 34.8060848 ], [ 140.8642851, 36.1591156 ], [ 139.0240751, 36.1879383 ] ] from geojson import Polygon m=Polygon([AREA]) # GeoJSON ファイルを出力 object_name = 'Tokyo_Bay' import json with open(str(object_name) +'.geojson', 'w') as f: json.dump(m, f) footprint_geojson = geojson_to_wkt(read_geojson(str(object_name) +'.geojson')) 2.2. 衛星画像をダウンロード SentinelAPI.queryでパラメーターを指定して、衛星画像をダウンロードできます。 # manin.py # Copernicus Open Access Hub のユーザー情報を設定 user = '自分のユーザー名を貼り付け' password = '自分のパスワードを貼り付け' api = SentinelAPI(user, password, 'https://scihub.copernicus.eu/dhus') # 衛星画像を取得する条件を指定 products = api.query(footprint_geojson, date = ('20200608', '20210608'), #取得希望期間の入力 platformname = 'Sentinel-2', processinglevel = 'Level-2A', cloudcoverpercentage = (0,100)) #被雲率(0%〜100%) # 衛星画像のメタデータ product = list(products.values())[0] # ダウンロード api.download(product['uuid']) 上記のコードを main.pyに追加しターミナルで以下を実行すると、ダウンロードが始まります。 $ python main.py クエリの指定の仕方 SentinelAPI.query で条件を指定してクエリを書くことが出来ます。 パラメーター date: 検索する期間 yyyyMMdd yyyy-MM-ddThh:mm:ss.SSSZ (ISO-8601) yyyy-MM-ddThh:mm:ssZ NOW NOW-<n>DAY(S) (or HOUR(S), MONTH(S), etc.) NOW+<n>DAY(S) yyyy-MM-ddThh:mm:ssZ-<n>DAY(S) NOW/DAY (or HOUR, MONTH etc.) order_by limit その他にも以下のドキュメントにあるキーワードパラメーターとして使えます。 platformname: 衛星の名前 例 platformname:Sentinel-1 platformname:Sentinel-2 platformname:Sentinel-3 platformname:Sentinel-5 Precursor cloudcoverpercentage: 被雲率(0%〜100%) cloudcoverpercentage:95 cloudcoverpercentage:[0 TO 5] processinglevel: 衛星画像の処理のレベル Level-1C Level-2A 詳しくは公式ドキュメントをご参考ください。 https://sentinelsat.readthedocs.io/en/stable/api_reference.html#sentinelsat.sentinel.SentinelAPI.query processinglevel クエリのパラメーターの中に、processinglevel がありました。耳慣れない言葉ですが、衛星画像の処理レベルのことです。 今回は 'Level-2A' を指定していますが、 ここで取得するSentinel-2のプロダクトレベルは,L1-2Aを要求しています. これは,衛星画像の位置合わせを終えたL-1Cレベルに,大気のゆらぎ(モヤ)を補正した画像になります. 人工衛星(Sentinel-2)の観測画像をAPIを使って自動取得してみた. - Qiita より引用 詳しく、衛星画像の処理レベルについて知りたい場合は、以下をご参考ください。 まとめ 今回は衛星画像のダウンロードまでをやってみました。また、続編として指定した期間の中で、1番雲が少ない画像を取得する方法や、ダウンロードした画像をタイルに変換する方法を書こうと思います。 参考
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Django プロジェクトの始め方覚え書き(ローカルで Django の Welcome 画面が見れるまで)

Django プロジェクトを何度か立ち上げたので、自分用に立ち上げ方の作業を残しておきます。 環境はローカルが Windows10、本番が Ubuntu + Apache です。プロジェクト名をmypro、アプリ名をmyappとします。 今回はローカルで Django の Welcome 画面が見れるまでを記載します。 仮想環境の立ち上げ(venv) Python の仮想環境で有名なところですと、venvとvirtualenvがあります。 venvは Python3.3 以降では標準パッケージになっており、非常に使いやすいです。 venvとvirtualenvの最も大きな違いと私が認識しているのは、 venvは Python のパッケージ内のため Python 自体のバージョン管理ができない virtualenvはサードパーティのため Python 自体のバージョン管理ができる というところかなと。 現状、私は基本的には Python3.x で全て作っているので、簡単に使えるvenvを使用することにします。 $ mkdir mypro $ cd myapp $ mkdir myvenv $ py -m venv myvenv $ myvenv\Scripts\activate 1行目でプロジェクトのディレクトリ作成、2行目でプロジェクトのディレクトリに移動、3行目でvenvで作成する仮想環境のディレクトリを作成、4行目で仮想環境を作成、5行目で仮想環境立ち上げです。 Django のインストール 次にDjango のインストールをします。ディレクトリは ~/myproです。 $ pip install --upgrade setuptools $ pip install --upgrade pip $ pip install django $ django-admin startproject mypro . 1, 2行目でsetuptoolsとpipを最新版に更新し、2行目でdjangoのインストール、3行目で Django プロジェクトを立ち上げています。 現状はこのようなディレクトリ構成になっているはずです。 mypro/ │ manage.py ├─mypro/ │ asgi.py │ settings.py │ urls.py │ wsgi.py │ __init__.py └─myvenv/ settings.py の設定 mypro/settings.pyを修正します。 日本の環境に合わせる 言語と時間を日本に合わせます settings.py LANGUAGE_CODE = 'ja' TIME_ZONE = 'Asia/Tokyo' 本番用に環境変数を導入をする ローカルと本番用で環境をわけるために、環境変数を導入します。 秘密キーなど、知られてはならないものもこちらに書きます。最終的にサーバー側でも作成する必要があるものです。 django-environ をインストール $ pip install django-environ settings.py を修正 settings.py from pathlib import Path import os import environ BASE_DIR = Path(__file__).resolve().parent.parent env = environ.Env() env.read_env(os.path.join(BASE_DIR, '.env')) SECRET_KEY = env('SECRET_KEY', str) DEBUG = env.get_value('DEBUG', bool) if DEBUG: ALLOWED_HOSTS = [] else: ALLOWED_HOSTS = ['*'] .env を作成 manage.pyと同じディレクトリに.envファイルを作成し、settings.py内のSECRET_KEYを移す。 .env DEBUG=True SECRET_KEY=xxxxxxxxxx 空白があったり'などがあるとエラーになるので注意。 この.envをGitHubなどにアップしてしまうと大変なことになります。私は問い合わせのためのメールアドレスのパスワードをあげてしまって、数分後に気づいてすぐ削除しましたがとてつもない量のスパムメールを送信し始めてしまって、慌ててパスワードを変更した覚えがあります。 本番環境ではDEBUG=Falseとします。 runserver でエラーがないことを確認 ここまでで、ローカル環境で django のウェルカム画面が出ることを確認します。 $ py manage.py runserver エラーがないと、下記のメッセージが出ます。 Starting development server at http://127.0.0.1:8000/ http://127.0.0.1:8000/にアクセスし、下記の画面が出てこれば成功です。 次回はこちらをサーバーに移して動かすところを書きますね!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Python勉強記録 #5 重回帰分析

回帰と分類 回帰とは、入力変数xに対して、数値yを予測するものです。 「賃貸物件の面積、駅からの距離などに対応する家賃のデータ」という「教師」があり、その教師のデータを使って、面積や距離を入力した時に、未知のyを予測します。 同じ教師あり学習に「分類」というものがありますが、それは、入力変数xに対してカテゴリyを予測します。 これは、「犬はこんな特徴、猫はこんな特徴」という教師があり、その教師データにしたがって、顔の形や体重などを入力した時に、未知の動物を犬と猫に分類するという感じです。 重回帰分析とは 重回帰分析は、1つの目的変数を複数の説明変数で予測するというものです。 教師あり学習の「回帰」で使われます。 式にすると以下のようになります。 y=w_0x_0+w_1x_1+w_2x_2+...+w_nx_n *バイアス b を w0x0 と置いています。 具体例でわかりやすく 僕は「賃貸物件の家賃を予測する」という例で習ったので、その方法で説明します。 重回帰分析を、1つの目的変数(家賃)を複数の説明変数(面積、駅からの距離、寝室の数など)で予測するために使うとすると、 y = 家賃 x1 = 面積 x2 = 駅からの距離 が入ることになります。 では、w(重み:weight)は何を表すかというと、それぞれの説明変数xがどれぐらい家賃に影響するかを表す数字となります。 例えば、 y=w_1x_1 という関数だったとして、yが家賃、x1が面積とする時、家賃50000円の物件が面積10坪だったとしたら、w1は5000という値を取りますね。 このw1が重みです。 これに、「面積」以外にも、「駅からの距離」という要素も考慮して、家賃を計算したい時、要素が二つあるので、重みも2つになります。 y=w_1x_1+w_2x_2 駅からの距離は200メートルだとしたとき、 もしw2がw1と同じく5000だったら、y=50000+1000000=1050000となり、家賃105万円となってしまいます。 でも実際は、駅からの距離(メートル)がそんなに家賃に影響を与えることはありませんよね。 なので、w2を計算して、求める必要があります。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Python勉強記録 #6 重回帰分析

回帰と分類 回帰とは、入力変数xに対して、数値yを予測するものです。 「賃貸物件の面積、駅からの距離などに対応する家賃のデータ」という「教師」があり、その教師のデータを使って、面積や距離を入力した時に、未知のyを予測します。 同じ教師あり学習に「分類」というものがありますが、それは、入力変数xに対してカテゴリyを予測します。 これは、「犬はこんな特徴、猫はこんな特徴」という教師があり、その教師データにしたがって、顔の形や体重などを入力した時に、未知の動物を犬と猫に分類するという感じです。 重回帰分析とは 重回帰分析は、1つの目的変数を複数の説明変数で予測するというものです。 教師あり学習の「回帰」で使われます。 式にすると以下のようになります。 y=w_0x_0+w_1x_1+w_2x_2+...+w_nx_n *バイアス b を w0x0 と置いています。 具体例でわかりやすく 僕は「賃貸物件の家賃を予測する」という例で習ったので、その方法で説明します。 重回帰分析を、1つの目的変数(家賃)を複数の説明変数(面積、駅からの距離、寝室の数など)で予測するために使うとすると、 y = 家賃 x1 = 面積 x2 = 駅からの距離 が入ることになります。 では、w(重み:weight)は何を表すかというと、それぞれの説明変数xがどれぐらい家賃に影響するかを表す数字となります。 例えば、 y=w_1x_1 という関数だったとして、yが家賃、x1が面積とする時、家賃50000円の物件が面積10坪だったとしたら、w1は5000という値を取りますね。 このw1が重みです。 これに、「面積」以外にも、「駅からの距離」という要素も考慮して、家賃を計算したい時、要素が二つあるので、重みも2つになります。 y=w_1x_1+w_2x_2 駅からの距離は200メートルだとしたとき、 もしw2がw1と同じく5000だったら、y=50000+1000000=1050000となり、家賃105万円となってしまいます。 でも実際は、駅からの距離(メートル)がそんなに家賃に影響を与えることはありませんよね。 なので、w2を計算して、求める必要があります。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

天文データ解析入門 その6 (astrodendroの使い方: 基本編)

本記事では、astrodendro (Astronomical Dendrograms in Python) の基本的な使い方について記述します。 今回も、例として国立天文台の FUGIN プロジェクトで得られた野辺山45m電波望遠鏡の CO 輝線のアーカイブデータを用います。データは http://jvo.nao.ac.jp/portal/nobeyama/ から FGN_03100+0000_2x2_12CO_v1.00_cube.fits をダウンロードします (重いです)。 2D fits の場合 まずは例によって必要なものを import し、fitsを読み込みます。 from astropy.io import fits import numpy as np from matplotlib import pyplot as plt import aplpy from astropy.wcs import WCS hdu = fits.open("~/your/fits/dir/FGN_03100+0000_2x2_12CO_v1.00_cube.fits")[0] # 3D w = WCS(hdu.header) 2D fitsにするため、以下のような関数を定義します。 def v2ch(v, w): # v(km/s)をchに変える x_tempo, y_tempo, v_tempo = w.wcs_pix2world(0, 0, 0, 0) x_ch, y_ch, v_ch = w.wcs_world2pix(x_tempo, y_tempo, v*1000.0, 0) v_ch = int(round(float(v_ch), 0)) return v_ch def del_header_key(header, keys): # headerのkeyを消す import copy h = copy.deepcopy(header) for k in keys: try: del h[k] except: pass return h def make_new_hdu_integ(hdu, v_start_wcs, v_end_wcs, w): # 積分強度のhduを作る data = hdu.data header = hdu.header start_ch, end_ch = v2ch(v_start_wcs, w), v2ch(v_end_wcs, w) new_data = np.nansum(data[start_ch:end_ch+1], axis=0)*header["CDELT3"]/1000.0 header = del_header_key(header, ["CRVAL3", "CRPIX3", "CRVAL3", "CDELT3", "CUNIT3", "CTYPE3", "CROTA3", "NAXIS3"]) header["NAXIS"] = 2 new_hdu = fits.PrimaryHDU(new_data, header) return new_hdu 以下のように 2D fits を作成します。 integ_hdu = make_new_hdu_integ(hdu, 25.0, 125.0, w) Dendrogram と、統計量を出すためのものなどを import します。 from astrodendro import Dendrogram from astrodendro.analysis import PPStatistic import datetime 以下のように、3つのパラメータを設定します。詳しくはドキュメントを参照してください。 dendro_min_value = 10 dendro_min_delta = 50 dendro_min_npix = 100 以下でDendrogramを実行します。結果は全て d に格納されます。 (一応時間を計測しています。) t1 = datetime.datetime.now() print("[dendro] started......", t1) d = Dendrogram.compute(integ_hdu.data, min_value=dendro_min_value, min_delta=dendro_min_delta, min_npix=dendro_min_npix, verbose=False) t2 = datetime.datetime.now() print("[dendro] finished.....", t2) print("[dendro] total time... ", (t2 - t1).total_seconds(),"sec") ここまでやっておいて今更言うのもあれですが、以上を ipython で実行していると d.viewer() plt.show() を実行することで GUI で結果を確認できます。(Jupyter だとうまく動きません。) カタログ化 同定した構造をカタログ化したいと思います。標準では from astrodendro import pp_catalog というものが備わっているのですが、少し扱いづらいので、今回は pandas を使って独自にカタログを作る方法をとることにします。 import pandas as pd まず、どれだけの構造が同定されたか個数を確認します。 print(len(d)) # 1002 d.trunk や d.leaves と実行するとそれぞれ trunk と leaf の structure のリストが返ってきます (詳細はドキュメントを参照)。数を以下で確認します。 print(len(d.trunk)) # 6 print(len(d.leaves)) # 504 それでは、全 leaf に対してパラメータを出してカタログを作ろうと思います。 w_integ = WCS(integ_hdu.header) x_peak, y_peak, x_cen, y_cen, val_min, val_max = [], [], [], [], [], [] index_list, ancestor = [], [] n_vox, area_ellipse, area_exact, minor_sigma, major_sigma, position_angle = [], [], [], [], [], [] radius, total_flux = [], [] for s in d.leaves: stat = PPStatistic(s) index_list.append(s.idx) ancestor.append(str(s.ancestor.idx)) y_ch, x_ch = s.get_peak()[0] x_peak_, y_peak_ = w_integ.wcs_pix2world(x_ch, y_ch, 0) x_peak.append(round(float(x_peak_), 6)) y_peak.append(round(float(y_peak_), 6)) x_cen_ch, y_cen_ch = stat.x_cen.value, stat.y_cen.value x_cen_, y_cen_ = w_integ.wcs_pix2world(x_cen_ch, y_cen_ch, 0) x_cen.append(round(float(x_cen_), 6)) y_cen.append(round(float(y_cen_), 6)) val_min.append(round(s.vmin, 6)) val_max.append(round(s.vmax, 6)) ind = s.indices() n_vox.append(len(ind[0])) area_ellipse.append(round(stat.area_ellipse.value, 6)) area_exact.append(round(stat.area_exact.value, 0)) minor_sigma.append(round(stat.minor_sigma.value, 6)) major_sigma.append(round(stat.major_sigma.value, 6)) position_angle.append(round(stat.position_angle.value, 6)) radius.append(round(stat.radius.value, 6)) total_flux.append(round(stat.stat.mom0(), 6)) results_leaves = pd.DataFrame({ 'id':index_list, # ID 'ancestor':ancestor, # (最も)先祖のID 'x_peak':x_peak, # peak の位置のx座標 'y_peak':y_peak, # peak の位置のy座標 'x_cen':x_cen, # 重心位置のx座標 'y_cen':y_cen, # 重心位置のy座標 'val_min':val_min, # 構造の持つ最小値 'val_max':val_max, # 構造の持つ最大値 'n_vox':n_vox, # 構造が使っている pixel 数 'area_ellipse':area_ellipse, # 構造の重みつき 1 sigma の楕円面積 (pixel^2) 'area_exact':area_exact, # 構造が実際に使っている面積 (pixel^2) 'minor_sigma':minor_sigma, # 楕円の短軸 (pixel) 'major_sigma':major_sigma, # 楕円の長軸 (pixel) 'position_angle':position_angle, # 楕円の position angle (degree) 'radius':radius, # 楕円の長軸短軸の相乗平均 (pixel) 'total_flux':total_flux # 構造の持つ値の総和 }) results_leaves という名前の pandas DataFrame が出来上がりました。 試しに何かplotしてみます。 plt.scatter(results_leaves["area_exact"], results_leaves["total_flux"]) plt.xscale("log") plt.yscale("log") plt.xlabel("area_exact [pix^2]") plt.ylabel("total_flux [K]") plt.show() results_leaves.to_csv("leaves_test.csv") で保存できます。読み込みは results_leaves = pd.read_csv("leaves_test.csv") です。 3D fits (cube) の場合 2D の場合と使用方法は変わりません。 (ただし、もちろん計算は重いです。) カタログ化の際は from astrodendro.analysis import PPVStatistic を使って、v に関するパラメータも格納できるように上のコードを適宜書き換えましょう。 カタログの map 上への plot aplpy と、aplpy の show_markers を使えば簡単にできます。 fig = plt.figure(figsize=(8, 8)) f = aplpy.FITSFigure(integ_hdu, slices=[0], convention='wells', figure=fig) f.show_colorscale(vmin=1, vmax=500, stretch='log', cmap="Greys", aspect="equal") f.show_markers(results_leaves["x_peak"], results_leaves["y_peak"], marker="x", c="r") plt.show() 構造の輪郭のコントアを引きたい場合は、 mask = np.zeros(integ_hdu.data.shape, dtype=bool) for s in d.leaves: mask = mask | s.get_mask() mask_hdu = fits.PrimaryHDU(mask.astype('short'), integ_hdu.header) でコントアの hdu を作った後に、 fig = plt.figure(figsize=(8, 8)) f = aplpy.FITSFigure(integ_hdu, slices=[0], convention='wells', figure=fig) f.show_colorscale(vmin=1, vmax=800, stretch='log', cmap="Greys", aspect="equal") f.show_contour(mask_hdu, colors='r', linewidths=0.5) plt.show() 以上です。 リンク 目次
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PythonでPhotoshopのアクション自動化ツールを作ってみた

普段趣味で写真撮影をしているエンジニアです。 大量の写真を色味調整などをすると結構時間かかりますので自動化のツールを作ってみました。 今回はPhotoshopのアクション機能を自動的に実行、保存のツールを作ってみます。 という簡単の構造です。 今回はphotoshop-python-apiというオープンソースライブラリを使います。 Photoshop内作ったアクション(例) main.py import os import photoshop.api as ps import glob def main(): # APIインスタンス instance = ps.Application() # 画像のパス(今回は.pyファイル下のimageフォルダ内のJPGファイル) filespath = './image/*.jpg' # フォルダ内のファイル名を取り出す files = glob.glob(filespath) # メインループ for f in files: # パスを絶対パスに変更 abspath = os.path.abspath(f) # 画像をロード img = instance.load(abspath) # アクションを実行(アクション名,アクショングループ名) instance.doAction('P3', 'Skin') # 画像を保存(パス,保存のファイル形式,ファイルを置き換える) img.saveAs(abspath, ps.JPEGSaveOptions(), asCopy=False) # 画像を閉じる img.close() if __name__ == '__main__': main()
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

天文データ解析入門 その5 (位置–速度図の作り方)

この記事では位置–速度図の作り方を紹介します。 前回 は積分強度図の作り方を紹介しました。これは 3D データ (cube) を速度方向に足し合わせていました。今回はそれを座標軸方向 (空間 grid 方向) に足し合わせたものになります。 (斜め方向の位置-速度図に関してはまた別の記事で紹介します。) 今回も、例として国立天文台の FUGIN プロジェクトで得られた野辺山45m電波望遠鏡の CO 輝線のアーカイブデータを用います。データは http://jvo.nao.ac.jp/portal/nobeyama/ から FGN_03100+0000_2x2_12CO_v1.00_cube.fits をダウンロードします (重いです)。 全座標積分 まずは例によって必要なものを import し、fitsを読み込みます。 from astropy.io import fits import numpy as np from matplotlib import pyplot as plt import aplpy from astropy.wcs import WCS hdu = fits.open("~/your/fits/dir/FGN_03100+0000_2x2_12CO_v1.00_cube.fits")[0] w = WCS("~/your/fits/dir/FGN_03100+0000_2x2_12CO_v1.00_cube.fits") data の shape を確認します。 print(hdu.data.shape) # (462, 848, 848) これは、Z (速度), Y (銀緯), X (銀経) のチャンネル数を表しています。つまり、848 x 848 のピクセルがあって、その一つ一つが462 チャンネルのスペクトルを持っているということです。 これを今回は X 方向または Y 方向に足し合わせます。 yv_map = np.nansum(hdu.data, axis=2)*np.abs(hdu.header["CDELT1"]) # X方向に足し合わせ xv_map = np.nansum(hdu.data, axis=1)*np.abs(hdu.header["CDELT2"]) # Y方向に足し合わせ fig = plt.figure(figsize=(12, 6)) ax1 = fig.add_subplot(1, 2, 1) ax1.imshow(yv_map, vmin=0, vmax=6) ax1.set_xlabel("Y [pix]") ax1.set_ylabel("V [pix]") ax2 = fig.add_subplot(1, 2, 2) ax2.imshow(xv_map, vmin=0, vmax=6) ax2.set_xlabel("X [pix]") ax2.set_ylabel("V [pix]") plt.show() 軸を座標などにしたい場合は、前回 のようにやるか、または以下の指定範囲積分で hdu を作っているので、そちらで行いましょう。 指定範囲積分 大抵は指定した座標の範囲を積分したいかと思います。 まずは位置–速度の hdu を作る関数を定義します。Y 方向に積分するものと X 方向に積分する2つです。plot するのは 2D ですが、aplpy の仕様の都合上、あえて 3D にしています。 def make_xv_hdu(hdu, y_start_wcs, y_end_wcs, w): import copy data = hdu.data header = copy.deepcopy(hdu.header) x_tempo_wcs, y_tempo_wcs, v_tempo_wcs = w.wcs_pix2world(0, 0, 0, 0) x_start_ch, y_start_ch, v_start_ch = w.wcs_world2pix(x_tempo_wcs, y_start_wcs, v_tempo_wcs, 0) x_end_ch, y_end_ch, v_end_ch = w.wcs_world2pix(x_tempo_wcs, y_end_wcs, v_tempo_wcs, 0) y_start_ch, y_end_ch = int(round(float(y_start_ch))), int(round(float(y_end_ch))) data_xv = np.nansum(data[:, y_start_ch:y_end_ch+1, :], axis=1)*np.abs(header["CDELT2"]) data_xv = data_xv.reshape(header["NAXIS3"], 1, header["NAXIS1"]) header["CRVAL3"] = header["CRVAL3"]/1000.0 header["CDELT3"] = header["CDELT3"]/1000.0 header["CUNIT3"] = "km/s" header["CTYPE3"] = "VELOCITY" header["NAXIS2"] = 1 new_hdu = fits.PrimaryHDU(data_xv, header) return new_hdu def make_yv_hdu(hdu, x_start_wcs, x_end_wcs, w): import copy data = hdu.data header = copy.deepcopy(hdu.header) x_tempo_wcs, y_tempo_wcs, v_tempo_wcs = w.wcs_pix2world(0, 0, 0, 0) x_start_ch, y_start_ch, v_start_ch = w.wcs_world2pix(x_start_wcs, y_tempo_wcs, v_tempo_wcs, 0) x_end_ch, y_end_ch, v_end_ch = w.wcs_world2pix(x_end_wcs, y_tempo_wcs, v_tempo_wcs, 0) x_start_ch, x_end_ch = int(round(float(x_start_ch))), int(round(float(x_end_ch))) data_yv = np.nansum(data[:, :, x_start_ch:x_end_ch+1], axis=2)*np.abs(header["CDELT1"]) data_yv = data_yv.reshape(header["NAXIS3"], header["NAXIS2"], 1) header["CRVAL3"] = header["CRVAL3"]/1000.0 header["CDELT3"] = header["CDELT3"]/1000.0 header["CUNIT3"] = "km/s" header["CTYPE3"] = "VELOCITY" header["CTYPE1"] = "GLON-TAN" # 注意 header["CTYPE2"] = "GLAT-TAN" # header["NAXIS1"] = 1 new_hdu = fits.PrimaryHDU(data_yv, header) return new_hdu yv ですが、projection が SFL だとバグるようですので (2021年6月現在)、TAN に変更しています。RADec の場合は消してください。 以下のように使います。 xv_hdu = make_xv_hdu(hdu, -0.9, 0.9, w) # Y 座標 (degree) を下, 上の順で入れる yv_hdu = make_yv_hdu(hdu, 31.9, 30.1, w) # X 座標 (degree) を左, 右の順で入れる fig = plt.figure(figsize=(12, 6)) f = aplpy.FITSFigure(xv_hdu, slices=[0], figure=fig, convention='wells', dimensions=[0, 2]) f.show_colorscale(vmin=0.0, vmax=10.0, aspect='auto', stretch="linear", cmap="nipy_spectral") f.add_colorbar() f.axis_labels.set_xtext("Galactic Longitude") f.axis_labels.set_ytext("$V_{\mathrm {LSR}}$ (km s$^{-1}$)") plt.show() fig = plt.figure(figsize=(6, 12)) f = aplpy.FITSFigure(yv_hdu, slices=[0], figure=fig, convention='wells', dimensions=[2, 1], aspect='auto') f.show_colorscale(vmin=0.0, vmax=10.0, aspect='auto', stretch="linear", cmap="nipy_spectral") f.add_colorbar() f.axis_labels.set_xtext("$V_{\mathrm {LSR}}$ (km s$^{-1}$)") f.axis_labels.set_ytext("Galactic Latitude") f.tick_labels.set_yposition("left") plt.show() Python2系の時は位置–速度図でも recenter が使えたのですが、仕様が変わって機能しなくなりました。もしどうしても使いたい場合は 2系 で実行するか、matplotlib.pyplot で plot しましょう。show_markers 等に関しては、coords_frame="pixel" にして pixel 指定をすれば一応可能です。 以上です。 リンク 目次
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Spotify APIを用いた、印象語による楽曲検索(距離計算編)

上の続きです 今回はimpressionのテーブルの定義とmain.pyの部分を記事にします シェルスクリプトで関数を実行するために以下の記事を参考にしました main.pyの中身はこうなっています main.py import argparse import norm #距離計算 # python main.py music_impression -i 印象語 def music_impression(name): norm.distance(name) if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument('function_name', type=str, help='set fuction name in this file') parser.add_argument('-i', '--func_args', nargs='*', help='args in function', default=[]) args = parser.parse_args() # このファイル内の関数を取得 func_dict = {k: v for k, v in locals().items() if callable(v)} # 引数のうち,数値として解釈できる要素はfloatにcastする func_args = [float(x) if x.isnumeric() else x for x in args.func_args] # 関数実行 ret = func_dict[args.function_name](*func_args) 今回中にあるのはnorm.py(距離計算)だけです 実行環境 macOS Catlina 10.15.7 Python 3.7.5 impressionテーブル main.pyはimpressionテーブル(以下impression)とmusicテーブル(以下music)の距離計算で上位10曲を表示するというものになっています musicはSpotify APIを用いているのでimpressionの正確さが結果の正確さに直結します ここに客観性かつ正確性を持たせるために現在思案中です とりあえずは25人にそれぞれの印象に当てはまる曲を挙げてもらい、平均を出しました fc=# select * from impression; word | danceability | acousticness | energy | liveness | loudness | speechiness | tempo | valence ---------+--------------+--------------+----------+----------+-----------+-------------+------------+---------- excited | 0.559412 | 0.238991 | 0.753471 | 0.177212 | -6.361706 | 0.1073 | 128.399882 | 0.565765 cool | 0.533333 | 0.176618 | 0.796208 | 0.214154 | -5.769667 | 0.0718 | 124.457208 | 0.456937 relax | 0.5335 | 0.420415 | 0.604963 | 0.161387 | -7.346125 | 0.069258 | 121.235208 | 0.504838 sad | 0.509 | 0.403961 | 0.593177 | 0.183709 | -6.792652 | 0.046196 | 119.219565 | 0.408422 fierce | 0.53716 | 0.136491 | 0.823628 | 0.1822 | -5.7808 | 0.099324 | 132.39152 | 0.505112 距離計算 norm.py import psycopg2 def distance(name): conn = psycopg2.connect("host=" + "localhost" + " dbname=" + {データベース名} + " user=" + {自分のmacのユーザーネーム} + " password=" + "") cur = conn.cursor() cur.execute("SELECT * FROM music") m = cur.fetchall() conn.commit() print("The number of music is ", len(m)) conn.commit() cur.execute("SELECT music.name, music.artist, distance(music.danceability, music.acousticness, music.energy, music.liveness, music.loudness, music.speechiness, music.tempo, music.valence, impression.danceability, impression.acousticness, impression.energy, impression.liveness, impression.loudness, impression.speechiness, impression.tempo, impression.valence) AS score FROM music, impression WHERE impression.word = '{}' ORDER BY score ASC FETCH FIRST 10 ROWS ONLY".format(name)) n = cur.fetchall() conn.commit() print() print(name) for n_ in n: print(n_) print() cur.close() conn.close() return distanceという関数をsqlで定義しています これは距離計算をする関数です norm.sql CREATE FUNCTION distance(float, float, float, float, float, float, float, float, float, float, float, float, float, float, float, float) RETURNS float AS 'SELECT sqrt(($1 - $9)^2 + ($2 - $10)^2 + ($3 - $11)^2 + ($4 - $12)^2 + ($5 - $13)^2 + ($6 - $14)^2 + ($7 - $15)^2 + ($8 - $16)^2 ) AS distance' LANGUAGE 'sql'; 実行結果(fierce) こんな感じに結果が出てきます ~/music2imp$ python main.py music_impression -i 'fierce' The number of music is 2849 fierce ('Fxxker', 'CHANMINA', 0.4662540877118465) ('よー、そこの若いの', 'Takehara Pistol', 0.484637866437422) ('Tik Tik Tok (Rhythm of the Clock) - Original Mix', 'Kid Alina', 0.4947239347393736) ('Dark Horse', 'Katy Perry', 0.6637707380602147) ('ダンシング・ヒーロー(Eat You Up)', 'Yoko Oginome', 0.7221408093890002) ('インフルエンサー 〜off vocal ver.〜 - Off Vocal Version', 'Nogizaka46', 0.7878546612066344) ('pov', 'Ariana Grande', 0.7881882205317551) ('ONLY ONE', 'SPiCYSOL', 0.800737013372686) ('格好悪いふられ方', 'Senri Oe', 0.8075263077974683) ('19 GROWING UP -ode to my buddy-', 'PRINCESS PRINCESS', 0.8426145755830627)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Python】基本情報技術者試験の過去問

アルゴリズム問題をPythonで書き起こしたメモ.順次更新予定. H31年度 春期(午前) 問6 def f(): if len(A) != 0: C.append(A.pop()) f() B.append(C.pop()) A = [1,2,3] B = [1,2,3] C = [1,2,3] f() print("A=", A, "B=", B, "C=", C) 実行結果 A= [] B= [1, 2, 3, 1, 2, 3] C= [1, 2, 3] 問7 A = 876 B = 204 counter = 1 while A != B: counter += 1 if A > B: A -= B else: B -= A print(counter) 実行結果 11
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Spotify APIを用いた、印象語による楽曲検索(楽曲情報取得編)

印象語(cool, excited, relax, sad, fierce)からその印象に合った曲を検索します 構成図はこんな感じです 今回はadd_table.pyの部分を記事にしますが、impressionのテーブルの定義は次回にします。 add_table.pyの中身はこうなっています シェルスクリプトで関数を実行するために以下の記事を参考にしました add_table.py import argparse import create_table import save_db import music_info # 実行文 # python add_table.py add_table -i id #music tableの作成 def add_table(id): create_table.create_music_table() print("create table") ids = imusic_info.getTrackIDs({ユーザーID}, '{}'.format(id)) print("get API") save_db.save_df(music_info.make_df(ids)) print("save db") if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument('function_name', type=str, help='set fuction name in this file') parser.add_argument('-i', '--func_args', nargs='*', help='args in function', default=[]) args = parser.parse_args() # このファイル内の関数を取得 func_dict = {k: v for k, v in locals().items() if callable(v)} # 引数のうち,数値として解釈できる要素はfloatにcastする func_args = [float(x) if x.isnumeric() else x for x in args.func_args] # 関数実行 ret = func_dict[args.function_name](*func_args) 手順としては 1, DB作成(create_table) 2, 楽曲情報を取得(imusic_info) 3, データフレームに変換(imusic_info) 4, DBに保存(save_db) です 実行環境 macOS Catlina 10.15.7 Python 3.7.5 また今回は全ての作業をpythonで行いたかったのでpsycopg2を用いました 1, DB作成 create_table.py import psycopg2 #テーブル作成 def create_music_table(): conn = psycopg2.connect("host=" + "localhost" + " dbname=" + {データベース名} + " user=" + {自分のmacのユーザーネーム} + " password=" + "") cur = conn.cursor() cur.execute('CREATE TABLE IF NOT EXISTS music (name TEXT NOT NULL, artist TEXT NOT NULL, danceability FLOAT8 NOT NULL,acousticness FLOAT8 NOT NULL,energy FLOAT8 NOT NULL,liveness FLOAT8 NOT NULL,loudness FLOAT8 NOT NULL,speechiness FLOAT8 NOT NULL,tempo FLOAT8 NOT NULL,valence FLOAT8 NOT NULL);') cur.execute('CREATE TABLE IF NOT EXISTS music_normalization (name TEXT NOT NULL, artist TEXT NOT NULL, danceability FLOAT8 NOT NULL,acousticness FLOAT8 NOT NULL,energy FLOAT8 NOT NULL,liveness FLOAT8 NOT NULL,loudness FLOAT8 NOT NULL,speechiness FLOAT8 NOT NULL,tempo FLOAT8 NOT NULL,valence FLOAT8 NOT NULL);') conn.commit() cur.close() conn.close() return 2, 楽曲情報を取得 Spotify APIの取得は以下のサイトを参考にしました https://knowledge.insight-lab.co.jp/sisense/information/spotify_music_data_1 music_info.py import spotipy from spotipy.oauth2 import SpotifyClientCredentials import pandas as pd import time def getTrackIDs(user, playlist_id): #自分のユーザーID/取得したいプレイリストID ids = [] client_id = {自分の情報} client_secret = {自分の情報} client_credentials_manager = spotipy.oauth2.SpotifyClientCredentials(client_id, client_secret) spotify = spotipy.Spotify(client_credentials_manager=client_credentials_manager) client_credentials_manager = SpotifyClientCredentials(client_id, client_secret) sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager) playlist = sp.user_playlist(user, playlist_id) while playlist['tracks']['next']: for item in playlist['tracks']['items']: track = item['track'] ids.append(track['id']) playlist['tracks'] = sp.next(playlist['tracks']) else: for item in playlist['tracks']['items']: track = item['track'] ids.append(track['id']) return ids def getTrackFeatures(id): client_id = {自分の情報} client_secret = {自分の情報} client_credentials_manager = SpotifyClientCredentials(client_id, client_secret) sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager) meta = sp.track(id) features = sp.audio_features(id) # data name = meta['name'] artist = meta['album']['artists'][0]['name'] acousticness = features[0]['acousticness'] danceability = features[0]['danceability'] energy = features[0]['energy'] liveness = features[0]['liveness'] loudness = features[0]['loudness'] speechiness = features[0]['speechiness'] tempo = features[0]['tempo'] valence = features[0]['valence'] track = [name, artist, danceability, acousticness, energy, liveness, loudness, speechiness, tempo, valence] return track getTrackIDs = 曲のIDを取得 getTrackFeatures = 曲の情報を取得 みたいな感じです 3, データフレームに変換 後々正規化などをする予定なのでdfに変換しておく music_info.py def make_df(ids):#getTrackIDsで返ってきたID tracks = [] for i in range(len(ids)): time.sleep(.5) track = getTrackFeatures(ids[i]) tracks.append(track) df = pd.DataFrame(tracks, columns = ['name', 'artist', 'danceability', 'acousticness', 'energy', 'liveness', 'loudness', 'speechiness', 'tempo', 'valence']) return df 4, DBに保存 楽曲名とアーティスト名が重複したら保存しないようにしました save_db.py import psycopg2 #重複を避けてDBに保存 def save_df(df): conn = psycopg2.connect("host=" + "localhost" + " dbname=" + {データベース名} + " user=" + {自分のmacのユーザーネーム} + " password=" + "") cur = conn.cursor() for text in df.values: values = (text[0], text[1], text[2], text[3], text[4], text[5], text[6], text[7], text[8], text[9]) cur.execute('SELECT * FROM music WHERE name = %s AND artist = %s', (text[0], text[1])) record = cur.fetchone() if record is not None: continue else: try: cur.execute ('INSERT INTO music (name, artist, danceability, acousticness, energy, liveness, loudness, speechiness, tempo, valence)' 'VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)' ,values) except Exception as err: print("Encountered exception. {}".format(err)) conn.commit() cur.execute("SELECT * FROM music") m = cur.fetchall() conn.commit() print("The number of music is ", len(m)) cur.close() conn.close() 実行結果 ~/music2imp$ python add_table.py add_table -i '5gNu9eJF7yg4xHQBXWVVaN' create table get API The number of music is 2849 save db musicテーブルの中身のはこんな感じです(一部) fc=# select * from music; name | artist | danceability | acousticness | energy | liveness | loudness | speechiness | tempo | valence -----------------------------------------------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------+--------------+--------------+---------+----------+----------+-------------+---------+--------- 道 | EXILE | 0.478 | 0.00209 | 0.592 | 0.245 | -5.975 | 0.0265 | 139.607 | 0.271 桜 | KOBUKURO | 0.462 | 0.531 | 0.414 | 0.119 | -7.392 | 0.0251 | 83.474 | 0.342 桜 | FUNKY MONKEY BABYS | 0.65 | 0.291 | 0.734 | 0.153 | -3.746 | 0.0302 | 81.993 | 0.726 蕾(つぼみ) | KOBUKURO | 0.355 | 0.203 | 0.441 | 0.155 | -5.785 | 0.0253 | 75.894 | 0.283 流星 | KOBUKURO | 0.464 | 0.49 | 0.553 | 0.32 | -5.314 | 0.0272 | 77.022 | 0.254 遥か | GReeeeN | 0.516 | 0.258 | 0.428 | 0.213 | -5.53 | 0.0242 | 84.005 | 0.368 ここにしか咲かない花 | KOBUKURO 続きは下にあります
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

大量のExcelファイルを簡単に一括でCSVに変換する方法

はじめに この記事は大量のエクセルのデータをCSVに簡単、迅速、綺麗に一括変換する方法の紹介記事です。 手順等を詳しく書くように心がけましたので非エンジニアの人にも広く使ってもらえると幸いです。 動作確認環境 Windows 10 ※Macでは動作確認ができていませんが、シェルスクリプトを使った変換であるため動作すると思います 達成できる内容 指定フォルダ内にあるExcelブックを一括でCSVに変換することができます 変換後のCSVはExcelで文字化けなく開くことができます 変換途中の状況を画面上で確認することができます ※シェルスクリプトの改変によって上記を拡張、変更することが可能です 使用技術 Git Bash(Windows環境でコマンドを実行するため) pip(Python用のパッケージ管理システム) xlsx2csv(Pythonのライブラリ) シェルスクリプト 下準備 1.Git Bashのダウンロード Windows環境でシェルスクリプトを実行するためのツールとしてGit Bashを使います。 Git BashはGitをインストールすることで入手することができます。 下記の「step1.Git Bashのダウンロード」を参考にインストールします。 2.pipのインストール CSVの一括変換にはPythonのライブラリを使用します。 Pythonのライブラリをインストールするためには、pipが必要な為、先にpipをインストールします。 なお、pipはPythonをインストールすることで自動的に付属してきます。 下記の「1. Pythonのインストール」を参考にPythonをインストールします。 3.xlsx2csvのインストール 上記の「step2.Git Bashの実行」を参考にGit Bashを起動します。 Git Bashの画面に下記のコマンドを打ち込み、xlsx2csvをインストールします。 pip install xlsx2csv なお、正常にインストールできている(すでにインストール済みの)場合は再度上のコマンドを実行した際、 Requirement already satisfied というメッセージが最初に表示されますので、インストール済みであることが確認できます。 以上で下準備は終了です。 ※なお、pipのバージョンが古いとWARNINGが出る場合がありますが、今回のインストールには直接影響はありません Excelを一括でCSVに変換するためのシェルスクリプト フォルダ構成例 Sample(親フォルダ) ├── convert.sh(変換用のシェルスクリプトファイル) ├── edit(Excelファイルを保存しているフォルダ) │ ├── hogehoge1.xlsx │ ├── hogehoge2.xlsx │ └── hogehoge3.xlsx └── main(CSVファイルの出力先フォルダ) シェルスクリプト メモ帳(テキストエディタ)を開き、下記シェルスクリプトをコピー&貼り付けします。 ファイル名をconvert.shとして、上記の配置になるようにファイルを保存します。 #!/bin/bash save_dir="./main/" file="data_timestamps.csv" if [ -f $file ] ; then rm data_timestamps.csv fi touch data_timestamps.csv echo "table_name,last_updated_datetime" >> data_timestamps.csv for filepath in `find ./edit/ -name '*.xlsx'` do filename=`basename ${filepath} .xlsx` filename_offset=${filename:0:2} if [ ${filename_offset} = "~\$" ] ; then echo -e "\033[0;31mError:${filename}\033[0;39m" continue fi xlsx2csv -s 1 $filepath $save_dir$filename.csv sed -i '1s/^/\xef\xbb\xbf/' $save_dir$filename.csv touch -t `date "+%Y%m%d%H%M.%S" -r $filepath` $save_dir$filename.csv echo "$filename,`date "+%Y-%m-%d %H:%M:%S" -r $filepath`" >> data_timestamps.csv echo -e "\033[0;32mSuccess:${filename}\033[0;39m" done echo ${SECONDS} seconds read -p "Complete. Press any key: " 使い方 該当のシェルスクリプトをダブルクリックすることでCSV変換処理を実行することができます。 なお、シェルスクリプトは実行する度にCSVファイルを上書きします。 詳細説明(※エンジニア向け) save_dir="./main/" file="data_timestamps.csv" ファイルの出力先、出力詳細のファイル名を指定します。 if [ -f $file ] ; then rm data_timestamps.csv fi 出力詳細のファイルは毎回生成し直したいため、存在していた場合あらかじめ削除します。 touch data_timestamps.csv echo "table_name,last_updated_datetime" >> data_timestamps.csv 出力詳細のファイルを作成、ヘッダーとなる内容をあらかじめ作成しています。 for filepath in `find ./edit/ -name '*.xlsx'` do  繰り返したい処理 done editファイル内にある.xlsxファイルを取得し、1ファイルずつループ処理しています。 filename=`basename ${filepath} .xlsx` filename_offset=${filename:0:2} if [ ${filename_offset} = "~\$" ] ; then echo -e "\033[0;31mError:${filename}\033[0;39m" continue fi ファイル名の先頭から2文字を変数に代入しています。 先頭からの二文字が ~$ であった場合はCSV変換の対象外としています。 Excelはファイル編集中の場合、~$hogehogeというファイルが別途作られるため除外しています。 xlsx2csv -s 1 $filepath $save_dir$filename.csv sed -i '1s/^/\xef\xbb\xbf/' $save_dir$filename.csv touch -t `date "+%Y%m%d%H%M.%S" -r $filepath` $save_dir$filename.csv echo "$filename,`date "+%Y-%m-%d %H:%M:%S" -r $filepath`" >> data_timestamps.csv echo -e "\033[0;32mSuccess:${filename}\033[0;39m" 上記の1行目でExcelファイルをCSVに変換し出力しています。 2行目で出力したCSVが文字化けしないようにUTF-8 BOM付きにしています。 3行目で生成したCSVの更新日時をExcel側の更新日時と同期しています。 4行目で出力詳細のファイルへ出力ログとして内容を書き込みしています。 5行目でCSV変換が成功したことを色付きテキストで表示しています。 echo ${SECONDS} seconds read -p "Complete. Press any key: " 実行にかかった時間を表示し、ユーザーのボタン入力があるまでは画面が勝手に閉じないようにしています。 補足資料及び調べた時に役に立ったページ
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ゼロから作るDeep Learning 1章 Python入門 備忘録

はじめに どうもお久しぶりです、ろんです(名前変えた)。 研究やインターン、Web開発で機械学習に興味が出てきて勉強したいなと思い機械学習を勉強することにしました。 何やらオーム社の「ゼロから作るDeep Learning」が初学者におすすめということなので、こちらを読んで備忘録を書いていこうと思います。 なんとPython未経験でも読み進められるとかなんとか。 ゼミ発表この記事見せておしまいがいいなあ笑 1章 Python入門 本書ではPythonを用いて実装していくので、1章ではPython基礎文法についてまとめてあります。 あやふやだったやつをメモします。 Pythonとは Pythonはシンプルで可読性が高く、初学者におすすめの言語である。また、数値計算や統計処理等ライブラリが豊富であるため機械学習、データサイエンスの分野でもよく用いられる。 クラス ユーザが新しいクラスを定義し独自のデータ型を作成する場合、以下のように定義する。 example.py class クラス名 # コンストラクタ def __init__(self, 引数, ...) # メソッド def メソッド名(self, 引数, ...) コンストラクタという初期化を行う特殊なメソッドがある。クラスのインスタンスが作成される際に一度だけ呼ばれる。 以下にclass.pyの実行例を示す。 class.py class Man: def __init__(self, name): self.name = name print("Initialized") def hello(self): print("Hello " + self.name) m = Man("Lon") m.hello() $ python class.py Initialized Hello Lon class.pyではManというクラスを定義し、mというインスタンス(オブジェクト)を生成する。 コンストラクタによって、mの生成時にself.nameを"Lon"で初期化する。 この変数をインスタンス変数と呼ぶ。 NumPy NumPyとは数値計算のためのライブラリで、数学アルゴリズムや配列処理を簡易に行える。 NumPy配列作成 NumPyの配列を作成するには、リストを引数に取り、np.array()というメソッドを用いる。 これによりNumPy用の配列である(numpy.ndaray)を作成する。 >>> x = np.array([1,2,3]) >>> print(x) [ 1 2 3 ] >>> type(x) <class 'numpy.ndarray'> NumPyのN次元配列 NumPyは多次元の配列が作成可能であり、2✕2の2次元配列であれば以下のように作成できる。 行列Aの形状はA.shape,データ型はA.dtypeにて参照できる。 また、数学で用いられる行列のように演算が可能である。 >>> a = np.array([[1,2], [3,4]]) >>> print(a) [[1 2] [3 4]] >>> a.shape (2, 2) >>> a.dtype dtype('int64') 要素のアクセス 配列での各要素へのアクセスは以下のように行う。 >>> x = np.array([[1, 2], [3, 4], [5, 6]]) >>> print(x) [[1 2] [3 4] [5 6]] >>> x[0] # 0行目 array([1, 2]) >>> x[0][1] # (0,1)の要素 2 for文を使うこともできる。 >>> for row in x: ... print(row) ... [1 2] [3 4] [5 6] また、以下のようなアクセスも可能である。 >>> x = x.flatten(): # xを1次元配列へ変換 >>> print(x) [1 2 3 4 5 6] >>> x[np.array([0, 2, 4]) # インデックスが0,2,4番目の要素 array([1, 3, 5]) >>> x[x>3] array([4, 5, 6]) Matplotllib グラフ描画のためのライブラリで、実験結果やデータの可視化が可能である。 単純なグラフの描画 グラフを描画するためにはmatplotlibのpyplotというモジュールを利用する。 sin関数を描画する例を以下に示す。 配列xの各要素に対しNumPyのsin関数であるnp.sin()を適用し、x,yのデータ列をplt.plotメソッドに与えグラフを描画する。 最後にplt.show()でグラフを表示する。 sin.py import numpy as np import matplotlib.pyplot as plt # データの作成 # 0から6まで0.1刻みで生成 [0.1, 0.2, 0.3, ..., 5.9] x = np.arange(0, 6, 0.1) # 配列xの各要素にsin関数を適用 y = np.sin(x) # グラフの描画 plt.plot(x, y) # グラフの表示 plt.show() pyplotの機能 タイトルやx軸ラベルの付与等、他のpyplotの機能の使用例を以下に示す。 sincos.py import numpy as np import matplotlib.pyplot as plt # データの作成 # 0から6まで0.1刻みで生成 x = np.arange(0, 6, 0.1) y1 = np.sin(x) y2 = np.cos(x) # グラフの描画 # ラベル名指定 plt.plot(x, y1, label="sin") # 破線の指定・ラベル名指定 plt.plot(x, y2, linestyle = "--", label="cos") # x軸のラベル plt.xlabel("x") # y軸のラベル plt.ylabel("y") # グラフタイトル plt.title('sin & cos') # labelの名称表示 plt.legend() # グラフ表示 plt.show() 画像の表示 pyplotには画像表示メソッドimshow()が用意されている。 画像の読み込みは以下のようにmatplotlib.imageモジュールのimread()を利用する。 imshow.py import matplotlib.pyplot as plt from matplotlib.image import imread # 画像の読み込み img = imread('lena.png') plt.imshow(img) plt.show() おわりに Pythonは研究とか競プロとか、画像処理100本ノックで分からなくなったら調べるって感じの使い方だったので意外と知らなかったとこが多かったですね笑 次はいよいよ2章のパーセプトロンについて読み進めていきたいと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ゼロから作るDeep Learning 備忘録

はじめに どうもお久しぶりです、ろんです(名前変えた)。 研究やインターン、Web開発で機械学習に興味が出てきて勉強したいなと思い機械学習を勉強することにしました。 何やらオーム社の「ゼロから作るDeep Learning」が初学者におすすめということなので、こちらを読んで備忘録を書いていこうと思います。 なんとPython未経験でも読み進められるとかなんとか。 ゼミ発表この記事見せておしまいがいいなあ笑 1章 Python入門 本書ではPythonを用いて実装していくので、1章ではPython基礎文法についてまとめてあります。 あやふやだったやつをメモします。 Pythonとは Pythonはシンプルで可読性が高く、初学者におすすめの言語である。また、数値計算や統計処理等ライブラリが豊富であるため機械学習、データサイエンスの分野でもよく用いられる。 クラス ユーザが新しいクラスを定義し独自のデータ型を作成する場合、以下のように定義する。 example.py class クラス名 # コンストラクタ def __init__(self, 引数, ...) # メソッド def メソッド名(self, 引数, ...) コンストラクタという初期化を行う特殊なメソッドがある。クラスのインスタンスが作成される際に一度だけ呼ばれる。 以下にclass.pyの実行例を示す。 class.py class Man: def __init__(self, name): self.name = name print("Initialized") def hello(self): print("Hello " + self.name) m = Man("Lon") m.hello() $ python class.py Initialized Hello Lon class.pyではManというクラスを定義し、mというインスタンス(オブジェクト)を生成する。 コンストラクタによって、mの生成時にself.nameを"Lon"で初期化する。 この変数をインスタンス変数と呼ぶ。 NumPy NumPyとは数値計算のためのライブラリで、数学アルゴリズムや配列処理を簡易に行える。 NumPy配列作成 NumPyの配列を作成するには、リストを引数に取り、np.array()というメソッドを用いる。 これによりNumPy用の配列である(numpy.ndaray)を作成する。 >>> x = np.array([1,2,3]) >>> print(x) [ 1 2 3 ] >>> type(x) <class 'numpy.ndarray'> NumPyのN次元配列 NumPyは多次元の配列が作成可能であり、2✕2の2次元配列であれば以下のように作成できる。 行列Aの形状はA.shape,データ型はA.dtypeにて参照できる。 また、数学で用いられる行列のように演算が可能である。 >>> a = np.array([[1,2], [3,4]]) >>> print(a) [[1 2] [3 4]] >>> a.shape (2, 2) >>> a.dtype dtype('int64') 要素のアクセス 配列での各要素へのアクセスは以下のように行う。 >>> x = np.array([[1, 2], [3, 4], [5, 6]]) >>> print(x) [[1 2] [3 4] [5 6]] >>> x[0] # 0行目 array([1, 2]) >>> x[0][1] # (0,1)の要素 2 for文を使うこともできる。 >>> for row in x: ... print(row) ... [1 2] [3 4] [5 6] また、以下のようなアクセスも可能である。 >>> x = x.flatten(): # xを1次元配列へ変換 >>> print(x) [1 2 3 4 5 6] >>> x[np.array([0, 2, 4]) # インデックスが0,2,4番目の要素 array([1, 3, 5]) >>> x[x>3] array([4, 5, 6]) Matplotllib グラフ描画のためのライブラリで、実験結果やデータの可視化が可能である。 単純なグラフの描画 グラフを描画するためにはmatplotlibのpyplotというモジュールを利用する。 sin関数を描画する例を以下に示す。 配列xの各要素に対しNumPyのsin関数であるnp.sin()を適用し、x,yのデータ列をplt.plotメソッドに与えグラフを描画する。 最後にplt.show()でグラフを表示する。 sin.py import numpy as np import matplotlib.pyplot as plt # データの作成 # 0から6まで0.1刻みで生成 [0.1, 0.2, 0.3, ..., 5.9] x = np.arange(0, 6, 0.1) # 配列xの各要素にsin関数を適用 y = np.sin(x) # グラフの描画 plt.plot(x, y) # グラフの表示 plt.show() pyplotの機能 タイトルやx軸ラベルの付与等、他のpyplotの機能の使用例を以下に示す。 sincos.py import numpy as np import matplotlib.pyplot as plt # データの作成 # 0から6まで0.1刻みで生成 x = np.arange(0, 6, 0.1) y1 = np.sin(x) y2 = np.cos(x) # グラフの描画 # ラベル名指定 plt.plot(x, y1, label="sin") # 破線の指定・ラベル名指定 plt.plot(x, y2, linestyle = "--", label="cos") # x軸のラベル plt.xlabel("x") # y軸のラベル plt.ylabel("y") # グラフタイトル plt.title('sin & cos') # labelの名称表示 plt.legend() # グラフ表示 plt.show() 画像の表示 pyplotには画像表示メソッドimshow()が用意されている。 画像の読み込みは以下のようにmatplotlib.imageモジュールのimread()を利用する。 imshow.py import matplotlib.pyplot as plt from matplotlib.image import imread # 画像の読み込み img = imread('lena.png') plt.imshow(img) plt.show() おわりに Pythonは研究とか競プロとか、画像処理100本ノックで分からなくなったら調べるって感じの使い方だったので意外と知らなかったとこが多かったですね笑 次はいよいよ2章のパーセプトロンについて読み進めていきたいと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む