20211009のPythonに関する記事は28件です。

Tweepyで 'API' object has no attribute 'search'

エラー内容 今回はこちらの記事を参考にいいね自動化を行ったところ 'API' object has no attribute 'search' というエラーが出ました。 解決策 search_results=api.search(q=q,count=count) #countの数だけ検索結果を表示 のapi.searchをapi.search_tweetsに変更します。 search_results=api.search_tweets(q=q,count=count) #countの数だけ検索結果を表示 これで作動しました。 search_tweetsはタブ補完機能で見つけました。 まとめ 公式のAPIメソッドについてはこちらの記事を参照してください。 searchで見ると API.search_30_day() API.search_full_archive() などが目につきますが、こちらは有料版なので気をつけてください。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

numpy100本ノック 99,100

通称 numpy100本ノック の 99 と 100だけ。 https://github.com/rougier/numpy-100/blob/master/100_Numpy_exercises.md import numpy as np import matplotlib.pyplot as plt import seaborn as sns 99 Given an integer n and a 2D array X, select from X the rows which can be interpreted as draws from a multinomial distribution with n degrees, i.e., the rows which only contain integers and which sum to n. (★★★) 与えられる二次元配列Xは、なんらかのresource(候補群)があって、その中からrandom生成されるものとした def multi_dist_n_pick(resource,size_2d,n): X = np.random.choice(X_resource,size_2d) # 抽出対象は行なので行数でループを回す for i in range(X.shape[0]): # 合計がnになる判定と配列を1で割ったあまりが全て0になることを利用した整数判定 if (np.sum(X[i]) == n) and (np.sum(X[i]%1)==0): print(X[i]) np.random.seed(5020126) # X に割り当てる候補の配列を作る # 初項:0 末項:4 交差:0.5の等差数列を作る X_resource = np.arange(start = 0, stop = 4.1, step = 0.5) multi_dist_n_pick(resource=X_resource, size_2d=(1000,3), n=8) 100 Compute bootstrapped 95% confidence intervals for the mean of a 1D array X (i.e., resample the elements of an array with replacement N times, compute the mean of each sample, and then compute percentiles over the means). (★★★) # bootstrapped 95% confidence intervals for the means を返す # 関数を作る def bootstrap_for_mean_percentile(sample,resample_try): """ bootstrap method sample : make 1D array resample_try : resample the elements of an array with replacement N times return: - N resample's mean - 95% confidence intervals """ resample_means = [ ] # resampleごとの平均保存 # resample_tryの数だけresampleを行う for i in range(resample_try): resample = np.random.choice(sample, len(sample), replace=True) # 復元抽出 resample_mean = np.mean(resample) # 各resampleの平均 resample_means.append(resample_mean) # 平均のパーセンタイルを計算する用に保存 # resample_meansの平均 mu_hat = np.mean(np.array(resample_means)) # 平均のパーセンタイルを計算 resample_conf = np.percentile(resample_means, [2.5, 97.5]) # 描画しとく sns.set() fig = plt.figure(figsize=(20, 10)) ax = fig.add_subplot(1, 2, 1) sns.distplot(sample, kde=False, bins=10, color='blue') plt.title(f"sample 1D array, n={len(sample)}", fontsize=16) ax = fig.add_subplot(1,2,2) sns.distplot(resample_means, kde=False, bins=10, color='red') plt.title(f"boot strap {resample_try} resample_means", fontsize=16) plt.show() return mu_hat,resample_conf[0],resample_conf[1] import numpy as np np.random.seed(5020126) # 1Darrayを決める,なんでもいい。 X = np.random.normal(50,30,10) # resample回数を決める resample_try = 1000 # 関数で平均のpercentileの部分を受け取る _ ,per025, per975 = bootstrap_for_mean_percentile(sample=X, resample_try=resample_try) print("95% confidence intervals") print("2.5%:{}".format(per025)) print("97.5%:{}".format(per975)) 95% confidence intervals 2.5%:40.214046546995746 97.5%:58.339760706475964 t検定で確認 import scipy.stats as stats np.random.seed(5020126) X = np.random.normal(50,30,10) X_mean = np.mean(X) X_var = np.var(X, ddof=1) stats.t.interval(alpha=0.95, loc=X_mean, scale=np.sqrt(X_var/len(X)), # 標準誤差 df=len(X)-1) (37.62043452526278, 60.19822262397027) うる覚えだが、確か同じサンプルサイズならブートストラップの方が信頼区間は小さくなるはず。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Pythonで空白(半角/全角)、スペース、タブを削除する方法

はじめに ユーザーに名前の入力を促すと、どうしても半角スペースや全角スペースが名前の間に入ってくることがあります。 例)理想:田中太郎 現実:田中 太郎 こちら側で設定できる場合には強制すればいいのですが、手元にあるデータだとそうもいきません。 そこで、今回はPythonを用いて空白文字を削除する方法を紹介したいと思います。 環境 macOS Big Sur バージョン11.5.2 python 3.7.10 サンプルデータ タブ、全角半角スペース、入力前の謎のスペース色々なパターンの入力を想定してみました。 sample_list = ["山本 大地", "坂本 卓郎", "鈴木 美奈子", " 高田栄作", "国枝 正樹"] # 結果 ['山本 大地', '坂本 卓郎', '鈴木\u3000美奈子', '\u3000高田栄作', '国枝 正樹'] (サンプルデータの人名は実在の人物とは関係ありません。) split()で空白(半角/全角)、スペース、タブを削除する まず思いつくのが組み込み関数str.split()を使うことです。 [''.join(name.split()) for name in sample_list] # 結果 ['山本大地', '坂本卓郎', '鈴木美奈子', '高田栄作', '国枝正樹'] split()で区切り文字無しで分割した後にjoinでくっつけるということを行なっています。 正規表現の利用して空白(半角/全角)、スペース、タブを削除する 正規表現であれば他の言語でも似たような動作をすると思うのでこちらも紹介しておきます。 reモジュールを利用します。 import re [re.sub("[\u3000 \t]", "", name) for name in sample_list] # 結果 ['山本大地', '坂本卓郎', '鈴木美奈子', '高田栄作', '国枝正樹'] replace()を利用して空白(半角/全角)、スペース、タブを削除する 最初検索した時に見つけたページを参考に書いたものです。 こちらはstr.replace()を利用しております。求めていた処理はできるのですが、上記と比べるとかなりみっともない出来ですので、上のものをご利用ください。 import pandas as pd result_list = pd.Series(sample_list).str.replace("[\u3000 \t]", "", regex=True) list(result_list) # 結果 ['山本大地', '坂本卓郎', '鈴木美奈子', '高田栄作', '国枝正樹'] 最後に Pythonで空白(半角/全角)、スペース、タブを削除する方法を紹介しました。 自動化する際には、スペースなどが人によって違いがあるとかなり手こずることも多いと思うので、こういった地味な処理も大事にしていきたいです。 この記事は以下の情報を参考にして執筆しました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

BiopythonでPubMedから論文情報を抽出して語の出現頻度を可視化・キーワード抽出

モチベーション 研究者のみなさま、今年も科研費執筆お疲れ様です。科研費申請書の「関連する国内外の研究動向と本研究の位置づけ」欄を書く(助けにする)ために、PubMedの論文情報を自然言語処理で簡単に解析したいです。語の頻度の可視化と、キーワード抽出を試してみます。 環境 MacOS Mojave 10.14.5 Python 3.7.10 Jupyter Notebook 6.4.3 準備 bash pip install Bio pip install nltk pip install rake-nltk pip install yake pip install spacy python3 -m spacy download en BiopythonでPubMedから論文情報をダウンロードする まず検索して件数を出します。Chlamydomonasで検索してみましょう。ここの9.14を参考にやっていきます。 Python from Bio import Entrez Entrez.email = "sample@email" #メールアドレスを入力 handle = Entrez.egquery(term="Chlamydomonas") record = Entrez.read(handle) for row in record["eGQueryResult"]: if row["DbName"]=="pubmed": number = row["Count"] print(number) #9335 次にesearchを使ってIDリストを取得します。 Python handle = Entrez.esearch(db="pubmed", term="Chlamydomonas", retmax=number, usehistory="y") #取得上限をさっき検索した数に設定 record = Entrez.read(handle) idlist = record["IdList"] print(idlist) efetchで論文情報を取得します。 Python from Bio import Medline handle = Entrez.efetch(db="pubmed", id=idlist, rettype="medline", retmode="text") records = Medline.parse(handle) records = list(records) 論文情報が取得できました。タイトル(TI)とアブストラクト(AB)を抽出して操作します。 Python titles = [] abstracts = [] for record in records: title = record.get("TI", "?") abstract = record.get("AB", "?") titles.append(title) abstracts.append(abstract) texts = titles + abstracts #リストを結合 キーワードの抽出と可視化 日本語で前にやったのとほぼ同じです。前処理として形態素解析をしないだけ日本語より楽です。 単純に語の出現頻度を数える textsをバラバラにしてみます。まずはテストから。 Python import re print(re.split('\.|,| |:|;|\(|\)|\[|\]|\?|\!', abstracts[0])) いい感じですが、空の要素が含まれるので削除します。 Python words = re.split('\.|,| |:|;|\(|\)|\[|\]|\?|\!', abstracts[0]) words = [word for word in words if word != ''] print(words) これをリストtextsに対して繰り返します。 Python words_list = [] for text in texts: words = re.split('\.|,| |:|;|\(|\)|\[|\]|\?|\!', text) words = [word for word in words if word != ''] words_list.extend(words) 最後に、大文字を小文字に変換します。長さは1760627語でした。 Python lower_list = list(map(str.lower, words_list)) 可視化 準備ができたので可視化していきます。以下、上位50位の頻度をグラフにします。 Python import collections import matplotlib.pyplot as plt words, counts = zip(*collections.Counter(lower_list).most_common()) plt.figure(figsize=[12, 6]) plt.bar(words[0:50], counts[0:50]) plt.xticks(rotation =90) plt.ylabel('count') plt.savefig('PubMed_bar', dpi=200, bbox_inches="tight") the, of, and, in, a, ...という結果になったので、ストップワードを除去してみます。 ストップワード除去 nltkからストップワードを呼び出し、リストlower_listから削除します。 Python from nltk.corpus import stopwords stop_words = stopwords.words('english') lower_list2 = [word for word in lower_list if word not in stop_words] ストップワードを除去した場合の語の頻度を可視化した結果を以下に示します。 Chlamydomonasで論文検索しましたが、chlamydomonas, reinhardtiiが最も多いという当然の結果になりました(Chlamydomonas reinhardtiiの2語で種名です)。他にもprotein(s), cell(s), gene(s), chloroplastなどそれらしい単語が上位に来ています。 spaCy ここからキーワード抽出ツールを使っていきます。 ここを参考に、spaCy, YAKE, rake-nltkの3種類を試してみました。まずspaCyです。 Python import spacy nlp = spacy.load("en") keywords_spacy = [] for text in texts: doc = nlp(text) keywords_spacy.extend(list(doc.ents)) keywords_spacy = [str(keyword) for keyword in keywords_spacy] タイトルの大文字がそのまま入っているのかなと思いますが、このまま多い順に集計してみましょう。 102207キーワードが抽出されました。Chlamydomonas, C. reinhardtiiは当然入っているとして、PSII, RNA, ATP, PSIなど、大文字だけの略語が目立ちます。Arabidopsis, Escherichia, Chlorella, Volvoxなど、他の生物の属名も入っています。大文字があると重要と判定するのかもしれません。 YAKE 次にYAKEでキーワード抽出してみます。 Python import yake kw_extractor = yake.KeywordExtractor() keywords_yake = [] for text in texts: keywords_y = kw_extractor.extract_keywords(text) keywords_y = [t[0] for t in keywords_y] keywords_yake.extend(keywords_y) なんだか途中でValueError: max() arg is an empty sequenceが出て止まってしまい、原因が特定できなかったのですが、144272キーワードを抽出できていたのでこのまま可視化してみます。 chlamydomonas, chlamydomonas reinhardtii, reinhardtiiが上位3つを占めています。chlamydomonas reinhardiなどの表記ゆれが入っているのも特徴的です。数字は含まれていません。 rake-nltk 最後にrake-nltkです。 Python from rake_nltk import Rake rake_nltk_var = Rake() keywords_rake = [] for text in texts: rake_nltk_var.extract_keywords_from_text(text) keyword_extracted = rake_nltk_var.get_ranked_phrases() keywords_rake.extend(keyword_extracted) chlamydomonas reinhardtii, green alga chlamydomonas reinhardtiiはフレーズで抽出されているものの、他は単語のみが上位に来るという結果になりました。found, used, show, identified, observedなどの動詞が多いのが特徴的です。printした結果を見ると、一般性の乏しい長いフレーズが抽出される傾向があり、それで集計すると上位に単語が多くなるのかなと思います。 まとめと所感 PubMedのデータから語の頻度を可視化し、spaCy, YAKE, rake-nltkでキーワード抽出を試してみました。今回はYAKEが一番キーワードっぽい結果になっているかなと思いますが、それぞれ一長一短ありそうです。個人的には、ストップワード除去して語の頻度だけ出すで研究の動向欄を書く(助けにする)という目的には十分かなという感じもありました。 参考 【自然言語処理】科研費データベースからMeCab-ipadic-neologdとtermextractでキーワードを抽出する Biopython を利用したNCBIのEntrez データベースへのアクセス Keyword Extraction process in Python with Natural Language Processing(NLP) 自然言語のpythonでの前処理のかんたん早見表(テキストクリーニング、分割、ストップワード、辞書の作成、数値化) spacy.load('en')が実行できない Python リストの要素を違う型に変換する Python: リスト内の文字列を全て大文字or小文字にする リストの空の要素を駆逐する Pythonで文字列を分割(区切り文字、改行、正規表現、文字数)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【物体検出】YOLOv5で電車の車両形式判定モデルを自作する

はじめに 任意のデータセットで物体検出のモデルを作成する手順について備忘録としてまとめました。 今回はGoogle ColabでYOLOv5を使用して物体検出モデルを作成しますので、Googleアカウントがあれば誰でも簡単に試すことができます。なお、ローカル環境でのモデル作成は以前の記事を参照してください。 (参考記事)ローカル環境でのYOLOv5モデル作成 成果物 以下のように動画または画像から電車の車両形式を判定するモデルを作成します。 今回は一般型(E231,E233など)、特急(E257,E353など)、機関車(EH200,EH500など)など14種類の車両形式を対象とします。 物体検出とは 物体検出(object detection)とは、画像内の「どこに」「何が」写っているかを検出する技術のことです。 物体検出としては、SSDやYOLOといったものがよく使用されます。 YOLOv5とは YOLOv5とは、物体検出をするアルゴリズムですがYOLOv3の後継にあたり、2020年に公開された最新のモデルです。YOLOv4という高精度化したYOLOv3の後継モデルもありますが、YOLOv5は推論処理時間がより速くなっているのが特徴です。 準備 YOLOv5でモデル作成の際に必要となるのは、教師データ・アノテーションデータ・yamlファイルです。これらをGoogleドライブにアップします。 教師画像とアノテーションファイルを作成したうえで、それぞれ指定の場所に格納していきます。 ①教師画像 /data/imagesに格納する。 ②アノテーションファイル /data/labelsに格納する。 ③yamlファイル coco.yamlをコピーしてdata.yamlと名前を付けて、ファイル内を以下のように書き換えます。 data.yaml train: /data/images val: /data/images #number of classes nc: 14 names: [ 'traincar'] number of classes:物体検出対象のクラス数です。今回は14種類なので14とします。 class names:物体検出対象の名前です。任意の名前にすることが可能です。 環境構築 まずはGoogleドライブをマウントします。 cmd from google.colab import drive drive.mount('/content/drive') クローンしてきます。 !git clone https://github.com/ultralytics/yolov5 次に必要なライブラリをインポートします。 !pip install -qr requirements.txt python import torch model = torch.hub.load('ultralytics/yolov5', 'yolov5x', pretrained=True) 以上で準備完了です。 学習 学習は以下のコマンドを実行すれば開始します。 初めから学習するときは以下のコマンドを入力を実行して学習を進めます。 !python train.py --img 640 --batch 4 --epochs 1000 --data traincar.yaml --weights yolov5x.pt --name traincar 上記の引数は以下の通りです。 batch:バッチサイズを指定します。 epochs:学習回数を指定します。 data.yaml:先程作成したyamlファイルを指定します。 weights:ここではyolov5x.ptを指定します。 前回の続きから学習 YOLOv5では学習するごとに結果がGoogleドライブに自動で保存されます。 --resumeを付け加えることで、前回までの結果の途中から学習することができます。 !python train.py --resume --img 640 --batch 4 --epochs 1000 --data traincar.yaml --weights yolov5x.pt --name traincar 結果 学習が終わるごとに/runs/train/traincar/weights/にbest.ptと保存されます。 まとめ 実際にbest.ptでテストしてみます。 電車の車両形式判定モデルを作ることができました。 おわりに 最後までご覧いただきありがとうございました。 訂正要望・ご意見等がございましたら、ご連絡頂けますと幸いです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

WSLgとOpenCVを使って動画と画像を表示する

はじめに この記事ではWSL2の新機能 WSLg と OpenCV を使用してWSLから画像と動画を表示する。 使用言語は C++ と Python3。 必要パッケージをインストールする $ sudo apt update && sudo apt upgrade $ sudo apt install git # C++ $ sudo apt install libopencv-dev build-essential # Python $ sudo apt install python3 python3-pip レポジトリをcloneし、動画ファイルを配置する まずは今回使うコードと画像が含まれたレポジトリをクローンする。 $ git clone https://github.com/nashinium/Miku-on-OpenCV.git 次に、レポジトリに含まれていない動画ファイルを所定のディレクトリに配置する。適当な動画を入手したら video.mp4に名前を変更し、次のディレクトリに配置する。./Miku-on-OpenCV/src/media Miku.pngと同じディレクトリに動画ファイルを置けたらOK。 C++ で コンパイル Compile $ cd Miku-on-OpenCV/ $ g++ src/main.cpp `pkg-config --cflags --libs opencv4` 実行 動画と画像のどちらを表示するか選ばされるのでp又はvを入力してEnterする。 パソコン起動後、初めてWSLgを使う場合メディアが表示されるまで少し時間がかかる。 Run $ ./a.out # Prompt Select media type which you want to display... ('v' for video, 'p' for Picture) >> 次のように画像、又は動画が表示されればOK。 Python で pipを使用して必要なモジュールをインストールする $ pip install opencv-python 実行 画像を表示したい場合はPicMiku.py、動画はVideoMiku.py。 $ cd Miku-on-OpenCV/src # 画像を表示する $ python3 PicMiku.py #動画を表示する $ python3 VideoMiku.py 次のように画像、又は動画が表示されればOK。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Python Elasticsearchクライアントの実装

はじめに ESがいくつかのクライアントとのAPIを提供し、ここでpythonを使って、実装しながら、使い方を解説する。 事前準備 Elasticsearch7.15.0 インストール Python3.9.7 インストール kibana7.15.0 インストール(なくてもよい、postmanでもなんでも好きなのがあれば) CentOS8.3(linuxじゃなくてもよい) ※Elasticsearchとkibanaバージョン一致しないと動かない。 Elasticsearchパッケージインストール # python -m pip install elasticsearch クライアントに接続 ESにデーターを詰めたいため、ESのサーバーに接続 以下のサンプルはES三台構成とする、SSL通信ならhttp_authが必要。 from elasticsearch import Elasticsearch es = Elasticsearch( [ {"host": "172.x.x.x", "port": 9200}, {"host": "172.x.x.x", "port": 9200}, {"host": "172.x.x.x", "port": 9200} ], http_auth=("username", "secret"), timeout=3600 ) es = Elasticsearch()だけならローカルの9200を接続しに行く。 インデックス作成 # インデックス名はnewsのインデックスを作成 result = es.indices.create(index='my-index', ignore=400) print(result) 実際のプログラミングにはエラーをキャッチするように、ignore=400などを使用した方がよい。 ignoreを使うと、同じ名前のインデックスが作成されたらエラーを無視する意味。 インデックス削除 result = es.indices.delete(index='my-index', ignore=[400, 404]) print(result) 404は存在してないインデックスを削除しようとするとエラーが発生する。 そのエラーを無視(ignore)する データー挿入 indexメソッドを使用する。DB作成みたいなもので、引数にはindex、doc_type、bodyが必要です。 # bodyとして使う doc = { 'level': 'info', 'timestamp': datetime.now(), 'detail': 'connect', } # インデックスmy-indexを作成する res = es.index(index="my-index", doc_type="log", id="100001", body=doc) # 結果出力 print(res['result']) index()メソッド使う時にid指定しなくても良いです。その場合、自動にid番号が振られる。 create()メソッドもデーターの挿入できる。create()メソッド使う時idは必須となる。 配列型の挿入方法 datas = [ { 'title': 'hello morning', 'date': '2021-11-16' }, { 'title': 'hello morning2', 'date': '2021-12-16' }, { 'title': 'hello morning3', 'date': '2021-12-17' }, { 'title': 'hello morning4', 'date': '2021-12-18' } ] for data in datas: es.index(index='my-index', doc_type='log', body=data) データー更新 doc = { 'level': 'info', 'timestamp': datetime.now(), 'detail': 'connect', } result = es.update(index='my-index', doc_type='log', body=doc, id=1) print(result) idを指定し、update()メソッドで更新する データー削除 result = es.delete(index='my-index', doc_type='log', id=1) print(result) id指定で削除する インデックス取得 res = es.get(index="my-index", id='100001') print(res['_source']) 検索 # 検索(最初の100件) query = { "size": 100, "query": { "match_all": {} } } res = es.search(index="my-index", body=query) for hit in res['hits']['hits']: print(hit['_source']) res['hits']['hits']ヒットした情報の一ページ目の情報が取得できる res['hits']['total']ヒットした件数が取得できる
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GraphCMSとHugoを連携してGithub Pagesで公開する(2)〜Hugoの設定

概要 GraphCMSとHugoの連携二回目。 今回はHugoの設定の話。 Hugoによるサイト構築 インストール ここではHugoのインストール方法は割愛する。 最初にサイトを作成する。 hugo new site my-site 上記コマンドで作成した場合は以下のようなファイル構成となる。 my-site ├── archetypes │   └── default.md ├── config.toml ├── content ├── data ├── layouts ├── static └── themes テーマ(themes)は既存のものを導入するとして最小限編集する必要があるのはconfig.tomlとcontentである。 その名の通り後者が具体的なサイトのコンテンツとなる。 なのでここにGraphCMSのコンテンツを流し込むようにする。 その前にまずGitレポジトリの初期化を行う。 cd my-site git init echo '*~' >> .gitignore echo '*.bak' >> .gitignore echo '*.orig' >> .gitignore echo '.env' >> .gitignore echo 'public' >> .gitignore echo 'resources' >> .gitignore 次にテーマの導入。 テーマはお好みのものを使ってもらえればいいが、本例ではAnankeを使う。 git submodule add https://github.com/budparr/gohugo-theme-ananke.git themes/ananke cp themes/ananke/exampleSite/config.toml . そしてconfig.tomlの編集 title = "Hugo GraphCMS Site" baseURL = "" languageCode = "en-us" theme = "ananke" #themesDir = "../.." #resourceDir = "../resources" DefaultContentLanguage = "ja" SectionPagesMenu = "main" Paginate = 3 # this is set low for demonstrating with dummy content. Set to a higher number googleAnalytics = "" enableRobotsTXT = true [languages] #[languages.en] # title = "My blog" # weight = 2 # contentDir = "content/en" [languages.ja] title = "私のブログ" weight = 1 #contentDir = "content/ja" contentDir = "content" # 以下省略 重要なのはthemeとthemesDirあたり。これを適切に設定しないエラーになるか真っ白な画面になる。 またlanguages.enの設定は不要だが、後で多言語化にトライしてみるつもりなので残してある。 では確認 huge serve ここまでのソース → Release v1.0 · higebobo/hugo-graphcms-blog Hugoのコンテンツ形式 Hugoのコンテンツは一般的にはcontentディレクトリにMarkdown形式のファイルをおいてビルドする。だけである。 コンテンツのメタ情報はフロントマターと呼ばれるyaml形式またはtoml形式で記述する。 なのでものすごくシンプルにするとこんな感じになる。 --- title: はじめまして date: "2021-09-16T12:58:01+09:00" --- # こんににちは 私もブログをはじめました。 コンテンツの雛形はarchetypeと呼ばれ、コマンドでコンテンツを作成する時に呼び出される。 (Hugoのサイト作成時にarchetypes/default.mdが生成される) 例えばcontentディレクトリにhello.mdを作成したい場合は以下を実行する。 hugo new content/post/hello.md するとフロントマターが生成された状態のコンテンツが作成されるので後は本文を書けばよい。 --- title: "Hello" date: 2021-09-24T08:23:45+09:00 draft: true --- Anankeテーマの場合 (他のテーマを使う場合は不要または適時読み替え) Anankeテーマを使う場合はarchetypeもテーマ用のものを使う必要がある。 デフォルトではarchetypes/default.mdが呼び出されるのでテーマのものに替える。 コピーして上書きでもよいが、archetypesの検索順(参照記事)を利用して呼び出す順位を変える。 # 変更する名前はdefault.mdやpost.mdでなければよい mv archetypes/default.md archetypes/_default.md またAnankeテーマのコンテンツは_index.mdというインデックスページ(参照記事)を利用しているので以下のように準備する。 content ├── _index.md └── post └── _index.md content/_index.md --- title: "Hugo GraphCMS Blog" featured_image: '/images/gohugo-default-sample-hero-image.jpg' description: "HugoとGraphCMSを連携したブログです" --- みなさん、こんにちは。 私のブログへようこそ content/post/_index.md --- title: "投稿記事" date: 2021-09-24T09:18:44+09:00 --- ブログの投稿記事一覧です。 準備ができたら記事を作成する。 hugo new content/post/first-post.md 以上がAnankeを使う場合の作法だが、テーマごとのコンテンツ作成ルールなどは正直初見ではわかりにくい。 まずはテーマがたいていexampleSiteを用意しているのでその中のcontentをまるっとコピーしてくるとよい。 Pythonによるコンテンツの雛形生成 archetypeのところを長々と説明したけども、今回はHugoのarchetypeの仕組みは利用せずPythonで作る。 この段階ではわざわざPythonで作る必要は無い。しかし後で述べるGraphCMSとの連携を考慮した。 Go言語がわかればHugo内で直接WEB-APIを叩いてMarkdownを作成すればよいと思うが、Pythonistaの私はPythonで実装するほうが手っ取り早い。 「Python使い」の意味だったと思うPythonistaがいつの間にかアプリ名になっている。。。 ざっと実装するとこんな感じ。 (Markdown作成の考えた方は同じなので好きな言語で実装してもらえればよいかとは思う) app/__main__.py import argparse import datetime import os ROOT_DIR = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) CONTENT_DIR = os.path.join(ROOT_DIR, 'content', 'post') def check_args(): parser = argparse.ArgumentParser() parser.add_argument('-t', '--title', default='My Post', help='blog title') parser.add_argument('-i', '--image', help='featured image') parser.add_argument('--tags', help='blog tags') return parser.parse_args() def main(content_dir=CONTENT_DIR): args = check_args() now = datetime.datetime.now() timestamp = now.strftime('%Y%m%dT%H%M%S') # ファイル名をタイムスタンプで生成 filepath = os.path.join(content_dir, f'{timestamp}.md') if os.path.exists(filepath): # 同一ファイル名が存在する場合は終了 print(f'{filepath} is exists') return # フロントマターのメタ情報設定 front_matter_map = { 'title': f'"{args.title}"', 'date': now.strftime('%Y-%m-%dT%H:%M:%S+09:00'), } # カバー写真の処理 if args.image: front_matter_map.update({"featured_image": f'"{args.image}"'}) # タグの処理 if args.tags: front_matter_map.update({"tags": str(args.tags.split(','))}) # 出力内容の生成 output = '---\n' for k, v in front_matter_map.items(): output += f'{k}: {v}\n' output += '---\n\n<!--more-->\n' # ファイルへの書き込み with open(filepath, 'w') as f: f.write(output) print(f'create {filepath}') if __name__ == "__main__": main() オプションでフロントマターの情報を与えられるようにしたので例えば以下のように実行すると python -m app -t "Pythonからこんにちは" -i https://www.python.org/static/community_logos/python-logo-master-v3-TM.png --tags="Python,挨拶" こんなMarkdownが生成される。 --- title: "Pythonからこんにちは" date: 2021-09-27T10:00:27+09:00 featured_image: "https://www.python.org/static/community_logos/python-logo-master-v3-TM.png" tags: ['Python', '挨拶'] --- <!--more--> 適当に本文を作成してサーバ起動する。 こんな感じでできあがる。 Pythonで実装しているのでフロントマターのみならず本文もオプションで自由に設定することはできる。 例えばyoutube動画を埋め込むとかいろいろ。 しかし現実的には長々とコマンドラインに引数を与えて実行するよりも単純に python -m app として後は自由に編集するのが一番よいかと思う。 ここまでのソース → Release v1.1 · higebobo/hugo-graphcms-blog まとめ 今回はHugoのコンテンツをわざわざPythonで生成する方法を解説した。 が、これは次回説明する本題のGraphCMSとHugoの連携についての前置きであるのでご了承いただきたい。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

機械学習の可視化を簡単に美しく

はじめに 機械学習は、いろんな切り口でパフォーマンスが見たくなる。 分類問題か、回帰問題かによって見方を変わる必要があるのはすこしややこしくもあるが、こればかりは一緒にはならない。 『「⚪︎×クイズ」(分類問題)ならパフォーマンスのよしあしを正解率で見るし、「100点満点のテスト」(回帰問題)なら点数でよしあしを判断するよね』と聞くと「そりゃそうだ」となる。 分類問題であっても回帰問題であっても、データを「学習データ」と「テストデータ」に分割し、モデルを構築したあと「テストデータ」で、いざ精度を検証!・・・ここまでは一緒だ。 回帰問題なら、構築したモデルにテストデータを投入して結果を予測、テストデータの結果と比較して、あてはまりが見たくなる。 テストデータと予測値のあてはまり あてはまりの度合いも把握したい。「予測値からの差はどの程度だろう?」、「この差は正規分布といえるだろうか?」「学習データのばらつきだけが極端に小さいということはないか?(過学習)」 予測値の差と分布(残差) 学習回数と評価尺度の関係も気になる。 例えば、「学習回数を増やせばまだ向上が期待できそうだ」ということがわかるかもしれないし、「そもそも期待誤差のレベルに達していないのでモデル構築から見直そう」といった場合もあるかもしれません。 学習回数と評価尺度の関係 分類問題なら、まずは混合行列で精度が確認したくなる。 AIをセンサに例えるなら「どれだけ正しく検出できたか」という総合評価だけではなく、「検出すべき時に検出しなかった(失報)」、「検出すべきでない時に検出した(誤報)」も少なくありたい。 混合行列なら、正解率も再現率も適合率もわかる。 混合行列 「⚪︎×クイズ」があたるかあたらないかは五分五分。 ROC曲線なら、誤りなく再現性高くといった状態をより正確に把握できる。 ROC曲線 決定木なら、木の深さと評価尺度が把握したくなる。むやみに深くしたくはないが少なすぎると誤差が大きいかもしれない、一方多くしたところで誤差は減らないかもしれない。 特徴量は重要なものを意識しておきたいから特徴量重要度の可視化も押さえておきたい。 決定木の深さと評価尺度の関係 特徴量重要度 といった具合にいろいろありますが、これらを確認できるようにするにはそれぞれコードを組み立てないといけないので、なかなか大変だな・・・と思っていた矢先に「Yellowbrick」という機械学習のパフォーマンスを可視化するライブラリがあることを知り、適用してみました。 この「Yellowbrick」、とにかく、すくないコードで実行でき、見た目もとても美しいです。 「Yellowbrick」使用前後のコードと見た目の違いを見て、ご興味を持たれた方はぜひどうぞ。 評価指標について この記事では、さまざまある評価指標の説明はしません。 ご存じない方は以下の記事がとても分かりやすいので、ぜひどうぞ。 実行条件など ・Google colabで実行 ・ボストン住宅価格のデータセットで実行 ・機械学習手法は決定木。デシジョンツリーはdtreevizで、機械学習の各種パフォーマンスは「Yellowbrick」で描かせます ※手元データを読込んで実行する場合も記載していますので、簡単にできるはずです。 ボストン住宅価格のデータセットについて 以下サイト(Kaggle)の「Boston.csv」を使わせていただいた。 データ数:506, 項目数:14のデータセットで、住宅価格を示す「MEDV」という項目と、住宅価格に関連するであろう項目が「CRIM:犯罪率」「RM:部屋数」「B:町の黒人割合」「RAD:高速のアクセス性」・・・等、13項目で構成されたデータとなっています。 これだけ項目があると、データ傾向を掴むだけでも、なかなか骨が折れるだろうと想像できますね。 ボストン住宅価格データの項目と内容 項目 内容 CRIM 町ごとの一人当たり犯罪率 ZN 25,000平方フィート以上の住宅地の割合 INDUS 町ごとの非小売業の面積の割合 CHAS チャールズ川のダミー変数(川に接している場合は1、そうでない場合は0) NOX 窒素酸化物濃度(1,000万分の1) RM 1住戸あたりの平均部屋数 AGE 1940年以前に建てられた持ち家の割合 DIS ボストンの5つの雇用中心地までの距離の加重平均 RAD 高速道路(放射状)へのアクセス性を示す指標 TAX 10,000ドルあたりの固定資産税の税率 PTRATIO 町ごとの生徒数と教師数の比率 B 町ごとの黒人の割合 LSTAT 人口の下層階級の比率 MEDV 住宅価格の中央値(1000㌦単位)    ライブラリのインストールおよびインポート graphvizインストール pip install graphviz dtreevizインストール pip install dtreeviz yellowbrickインストール pip install -U yellowbrick ライブラリインポート %matplotlib inline import pandas as pd import numpy as np import matplotlib.pyplot as plt from scipy.stats import norm from sklearn import tree from dtreeviz.trees import * import graphviz import yellowbrick from yellowbrick.model_selection import ValidationCurve from yellowbrick.model_selection import LearningCurve from yellowbrick.model_selection import FeatureImportances from yellowbrick.regressor import ResidualsPlot from yellowbrick.regressor import PredictionError import warnings warnings.filterwarnings('ignore') ファイル読込み データ読込み from google.colab import files uploaded = files.upload() df = pd.read_csv(target) ※kaggleサイトからダウンロードしたboston.csvは、1列目がindexコラムとなっています。私は読み込む前に削除しました。 データ読込みと変数設定 FEATURES = df.columns[:-1] TARGET = df.columns[-1] X = df.loc[:, FEATURES] y = df.loc[:, TARGET] Yellowbrickで各説明変数の目的変数との相関をグラフ化 from yellowbrick.target import FeatureCorrelation visualizer = FeatureCorrelation(labels=X.columns) visualizer.fit(X, y) visualizer.poof(); デシジョンツリー 決定木(木の深さ3) dtree = tree.DecisionTreeRegressor(max_depth=3) dtree.fit(X,y) viz = dtreeviz(dtree,X,y, target_name = TARGET, feature_names = FEATURES, #orientation='LR', #X = [3,3,5,3] ) viz モデル構築 決定木モデル from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split( X, y, test_size = 0.20, random_state = 1) from sklearn.tree import DecisionTreeRegressor from sklearn.model_selection import GridSearchCV dtr = DecisionTreeRegressor(random_state=0) gs_dtr = GridSearchCV(dtr, param_grid = {'max_depth': [2,3,4,5]}, cv = 10) gs_dtr.fit(X_train, y_train) y_train_pred = gs_dtr.predict(X_train) y_test_pred = gs_dtr.predict(X_test) gs_dtr.best_estimator_ モデル評価 予測プロット まずはYellowbrick使用前の予測値プロットのグラフ化です。 同じグラフに散布図+直線表示したいのでsubplotを使用。 予測プロットグラフ化 plt.figure(figsize = (5,5)) plt.title('Prediction Accuracy') ax = plt.subplot(111) ax.scatter(y_test, y_test_pred,alpha=0.9) ax.set_xlabel('y_test') ax.set_ylabel('y_test_pred') ax.plot(y_test,y_test,color='red',alpha =0.5) plt.show() つぎにYellowbrickを使用したのが以下です。 R²も表示される等、グラフ表示が充実しました。コードもとてもシンプルになりました。 予測プロットグラフ化(Yellowbrick) visualizer = PredictionError(dtree) visualizer.fit(X_train, y_train) visualizer.score(X_test, y_test) visualizer.poof(); 特徴量重要度 つぎは、Yellowbrick使用前の特徴量重要度のグラフです。 特徴量重要度 fea_clf_imp = pd.DataFrame({'imp': dtree.feature_importances_, 'col': FEATURES}) fea_clf_imp = fea_clf_imp.sort_values(by='imp', ascending=False) plt.figure(figsize=(7,5)) sns.barplot('imp','col',data=fea_clf_imp,orient='h') plt.title('Decision Tree - Feature Importance',fontsize=18) plt.ylabel('Features',fontsize=14) plt.xlabel('Importance',fontsize=14) plt.show() これも、Yellowbrickの場合、とてもシンプルなコードで済みます。 特徴量重要度(Yellowbrick) visualizer = FeatureImportances(dtree) visualizer.fit(X_train, y_train) visualizer.poof(); 残差プロット つぎは残差プロットです。 これはなかなかの力技です。散布図とヒストグラムを隣り合わせています。0点は合わせないといけないのにズレています。これら含め、細かな設定をしないといけません。 残差プロット fig = plt.figure(linewidth=1.5) grid = plt.GridSpec(1, 4) ax1 = fig.add_subplot(grid[0, 0:3]) ax2 = fig.add_subplot(grid[0, 0:3]) ax3 = fig.add_subplot(grid[0, 0:3]) ax4 = fig.add_subplot(grid[0, 3]) ax5 = fig.add_subplot(grid[0, 3]) ax1.set_xlabel('Predicted values') ax1.set_ylabel('Residuals') ax1.set_xlim([-10, 50]) ax1.scatter(y_train_pred, y_train_pred - y_train, c='steelblue', marker='o', edgecolor='white', label='Train data') ax2.scatter(y_test_pred, y_test_pred - y_test, c='limegreen', marker='s', edgecolor='white', label='Test data') ax1.legend(loc='upper right', borderaxespad=1, frameon=False) #ax4.set_facecolor('whitesmoke') ax3.hlines(y=0, xmin=-10, xmax=50, color='r', lw=1.5, alpha=0.5) ax4.hist((y_train-y_train_pred), orientation="horizontal",color='steelblue',alpha=0.40) ax5.hist((y_test-y_test_pred), orientation="horizontal",color='limegreen',alpha=0.40) plt.show() 上のコードを見たあとにこれをみると、「これだけでいいの?」と言いたくなるほどシンプルなコードで実行できます 学習曲線 最後に学習曲線です。 木の深さを増やすとR²はどうなる? scores = [] for i in range(1, 21): tree = DecisionTreeRegressor(random_state=1, max_depth=i) tree.fit(X_train, y_train) score = tree.score(X_test, y_test) scores.append(tree.score(X_test, y_test)) sns.set_context('talk') sns.set_palette('dark') sns.set_style('ticks') plt.plot(range(1, 21), scores) #plt.ylim(0.5,1.0) #y軸範囲指定 plt.xlabel("Depth of Tree") plt.ylabel("R²") plt.title("Decision Tree Regressor R²") plt.show() これもYellowbrickなら、簡単かつきれいです。 以下、2種類で描いてみました。 グラフは、交差検証(CV)の誤差の幅まで表示してくれるので、とても分かりやすいです。 木の深さでMSE(平均二乗誤差)はどうなる? visualizer = ValidationCurve( dtree, param_name="max_depth", param_range=np.arange(1, 11), cv=10, scoring='neg_mean_squared_error' ) visualizer.fit(X_train, y_train) visualizer.poof(); 学習回数でMSE(平均二乗誤差)はどうなる? visualizer = LearningCurve( dtree, cv=10, scoring='neg_mean_squared_error' ) visualizer.fit(X_train, y_train) visualizer.poof(); 最後に Yellowbrick、もっと早く存在に気がついてりゃよかったと思いました。 とてもシンプルなコードで、とても美しく機械学習のパフォーマンスが可視化できます。 おすすめです。 参考サイト
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RaspberryPiの死活監視システムを Azure IoT Central で構築した

はじめに 自宅で運用しているRaspberryPiの死活監視システムを構築したいと思い、GUIを含めたシステムを簡単に構築が出来そうなAzure IoT Centralを使用してみようと思います。 やりたいこと リソース情報をダッシュボードに表示 CPU・メモリ使用率 等のリソース増加の通知 デバイスのシステムダウン(ラズパイからの通信無し) の通知 環境 Raspberry Pi 4 Model B Python : 3.7.3 azure-iot-device : 2.7.1 <pipパッケージ> sysstat : 11.5.2 <debパッケージ> インストール $ pip3 install azure-iot-device $ sudo apt install sysstat システム構成 ラズパイからCPU・メモリ 等のデバイスリソース情報をAzure IoT Centralに送信して、ダッシュボードにリアルタイムでリソース情報を表示します。また、リソース情報が一定条件を満たすとメールを送信します。 ダッシュボード ラズパイから送信された、CPU使用率、メモリ使用率、ディスク使用率を数値とグラフで表示しています。変動が多いCPUとメモリのみをグラフ表示しています。 数値を表示しているパネルの色は、数値によって変更できるようになっていて、「0~70は青、70~90は黄、90~100は赤」みたいな感じで、ぱっと見で状態が分かるようになっています。 通知メール 「CPU使用率:90%以上」or「メモリ使用率:85%」の状態を検知すると以下のメールが送られ、デバイスのリソース使用率増加をすぐに知ることが出来ます。 ラズパイの設計 送信リソース情報 デバイスのリソース情報をjson形式に整形してAzure IoT Centralに送信します。 ※ コア毎のCPU使用率を送信していますが、今回は「All」のみを使用してます。 { "CpuUsage": { "All": 25.63, "Core0": 58.16, "Core1": 16.32, "Core2": 11.34, "Core3": 16.67 }, "MemoryUsage": 51.70, "DiskUsage": 92, "MsgId": 7 } Pythonプログラム Azure IoT Centralの「SCOPE_ID」,「DEVICE_ID」,「DEVICE_KEY」が環境変数に設定されていることが前提条件のプログラムとなっています。main.pyでは通信コネクション及び、取得したリソース情報の定期送信を行っています。 main.py import os import sys import json import asyncio from systemMoniter import SystemMoniter from deviceProvisioningService import Device from azure.iot.device.aio import IoTHubDeviceClient def message_received_handler(message): # ログ出力のみ print("the data in the message received was ") print(message.data) print("custom properties are") print(message.custom_properties) async def main(): scopeID = None deviceId = None key = None # 環境変数の取得 scopeID = os.getenv('SCOPE_ID') deviceId = os.getenv('DEVICE_ID') key = os.getenv('DEVICE_KEY') if scopeID is None or deviceId is None or key is None: sys.exit(1) # Azureとの通信に必要な「connection string」の取得 dps = Device(scopeID, deviceId, key) conn_str = await dps.connection_string # コネクション確立 device_client = IoTHubDeviceClient.create_from_connection_string(conn_str) await device_client.connect() # 受信ハンドラの設定、Azureからコマンドを受信するとこの関数が呼び出される device_client.on_message_received = message_received_handler systemMoniter = SystemMoniter() while True: try: # リソース情報取得 telemetry = await systemMoniter.getSystemStatus() if telemetry is not None: # ログ出力、json形式に変換し送信 print(telemetry) data = json.dumps(telemetry) await device_client.send_message(data) await asyncio.sleep(10) except: print("Unexpected error:", sys.exc_info()[0]) # コネクション切断 await device_client.disconnect() if __name__ == "__main__": asyncio.run(main()) # If using Python 3.6 or below, use the following code instead of asyncio.run(main()): # loop = asyncio.get_event_loop() # loop.run_until_complete(main()) # loop.close() deviceProvisioningService.pyでは通信に必要な「connection string」の生成を行います。Azure IoT Centralは内部的にはAzure IoT Hubを使用しているようで、Device Provisioning Serviceからデバイスに紐づけられたhub名を取得して「connection string」を生成する必要があります。 deviceProvisioningService.py import asyncio from azure.iot.device.aio import ProvisioningDeviceClient class Device(): def __init__(self, scope, device_id, key): self.scope = scope self.device_id = device_id self.key = key async def __register_device(self): provisioning_device_client = ProvisioningDeviceClient.create_from_symmetric_key( provisioning_host='global.azure-devices-provisioning.net', registration_id=self.device_id, id_scope=self.scope, symmetric_key=self.key, ) return await provisioning_device_client.register() @property async def connection_string(self): results = await asyncio.gather(self.__register_device()) registration_result = results[0] # build the connection string conn_str = 'HostName=' + registration_result.registration_state.assigned_hub + \ ';DeviceId=' + self.device_id + \ ';SharedAccessKey=' + self.key return conn_str systemMoniter.pyではラズパイのリソース情報の取得を行います。LinuxコマンドをPythonから取得し、後からjson型に変換しやすいようにdict型にデータをまとめています。 systemMoniter.py import subprocess class SystemMoniter(): def __init__(self) -> None: self.msgId = 0 async def getSystemStatus(self) -> dict: telemetry = {} self.msgId += 1 telemetry.update(self._cpuUpuUsage()) telemetry.update(self._memoryUsage()) telemetry.update(self._diskUsage()) telemetry["MsgId"] = self.msgId return telemetry def _cpuUsage(self) -> dict: ret = {} try: usages = {} cmd = R"LC_ALL=C sar -P ALL 1 1 | grep Average" cpuUsage = subprocess.check_output(cmd, shell=True).decode('utf-8') list = cpuUsage.splitlines()[1:] for num in range(0, len(list)): value = list[num].split() if(num == 0): property = "All" else: property = R"Core" + str(num-1) # cpuUsage = %user + %nice + %system + %iowait + %steal usages[property] = float(value[2])+float(value[3])+float(value[4])+float(value[5])+float(value[6]) ret["CpuUsage"] = usages except: print("Failed get the cpu usages.") ret = {} return ret def _memoryUsage(self) -> dict: ret = {} try: cmd = R"free -k | grep Mem:" cpuUsage = subprocess.check_output(cmd, shell=True).decode('utf-8') list = cpuUsage.split() ret["MemoryUsage"] = (float(list[2])*100)/float(list[1]) except: print("Failed get the memory usage.") ret = {} return ret def _diskUsage(self) -> dict: ret = {} try: cmd = R"df | grep /dev/root | awk '{print $5}' | sed 's/%//'" diskUsage = subprocess.check_output(cmd, shell=True).decode('utf-8') ret["DiskUsage"] = float(diskUsage) except: print("Failed get the disk usage.") ret = {} return ret Azure IoT Central の設計 デバイステンプレート ラズパイから送信するjsonデータに合わせてパラメータを設定します。 規則(アクション) 以下のように条件を設定してメールを送るようにしています。 パラメータの数値によって条件を付けることが出来るが、「受信できなかった」等の条件付けは無く、デバイスからデータが送信されていることが前提の条件付けになっている。その為、やりたかった「デバイスのシステムダウン(ラズパイからの通信無し) の通知」は現状できなさそう。 最後に 今回はAzure IoT Centralを使用して死活監視システムを構築しました。想像していたより簡単に作ることが出来たため、かなり驚いています。これぐらいのシステムなら構想から構築まで1~2日あれば実現できると思います。 気になる点としては、ダッシュボードや規則(アクション)についてはカスタマイズ性がないため、複雑なGUIや規則は設計できませんでした。複雑なことがしたいなら自前で作る必要があります。PoCレベルでさっと作る分ならかなり使えるサービスだと感じました。 やりたかった「デバイスのシステムダウン(ラズパイからの通信無し) の通知」が出来なかったのは残念でしたが、今後のアップデートに期待したいと思います。 参考 以下の情報を参考にさせていただきました。 Raspberry-Pi-Python-Environment-Monitor-with-the-Pimoroni-Enviro-Air-Quality-PMS5003-Sensor
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Pillow(PIL)で生成するgifの画質を上げる

単刀直入 im.quantize() するだけ はじめに Pythonの画像処理ライブラリのPillowを使えば、簡単にgifアニメーションを作ることができます。 詳しいことはみんな大好きnote.nkmk.meに譲りますが、適当に画像を数枚開いてオプションを決めるだけです。 makegif.py from PIL import Image im1 = Image.open("hoge.png") im2 = Image.open("huga.png") images = [im1, im2] images[0].save('test.gif', save_all=True, append_images=images[1:], optimize=False, duration=500, loop=0) たのしいアニメができました。 写真の場合 では、いらすとやのようなシンプルなイラストではなく、写真でも試してみましょう。 監視カメラを通して見たような酷い画質になってしまいました。 何故……。 *追記 スマホから見るとそんなに画質が悪くないように見えるかもしれません。 PCからだと結構酷さがわかります。(追記終わり) gifの制限と回避 2色モノクロから16777216色 (= 224) 中256色 (= 28) までの色サポート そうだったんですね。昔から私を楽しませてくれていたgifたちは256色の制限の中で戦っていたらしいです。 しかしこれでは困ります。というかガビガビじゃない写真のgifも見たことある気がします。 調べてみると以下のような記述を見つけました。(Deepl訳に少し追記) もうひとつの方法は、「量子化(the quantize method)」です。現在、マルチフレームGIFを作成する際、PillowはWebパレットを使って画像を変換しています。量子化を使って画像を最初にPに戻した方が、使われている正確な色をよりよく表現できるように思えます。 https://github.com/python-pillow/Pillow/issues/3660 いまいちピンと来ませんが、Pillowで開いたイメージに.quantize()することでいい感じになるらしいです。 やってみましょう。 makegif.py from PIL import Image im1 = Image.open("gohan1.jpg").quantize() im2 = Image.open("gohan2.jpg").quantize() im3 = Image.open("gohan3.jpg").quantize() images = [im1, im2, im3] images[0].save('test2.gif', save_all=True, append_images=images[1:], optimize=False, duration=800, loop=0) !!! 美味しそうです!これなら食えますね!! なにが起こったの? Pillowのquantizeによって、飯の画像を256色までいい感じに減色しました。 今回はデフォルトのままなので、メジアンカット(option=0)という方法で減色されたようです。 他にもmaximum coverage, fast octreeの方法が選べるようです。(libimagequantという方法もあるようですが、実行できませんでした) それぞれを比較してみましょう。 maximum coverageの劣化が酷いですね。ほか2つはかなり似ていますが、手前の白飯を見ると若干fast octreeのほうが上手く減色できている気がします。 まとめ Pillowでgifがボケたときはとりあえずquantize!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

std::setが欲しくなる問題をPythonで解く

はじめに この記事は投稿者のメモ代わりに書かれています。不具合があったらごめんね。 ACコードもあまりリーダブルじゃないので参考にしないように。 この記事は何? 競プロでは、配列の最大/最小値を求めたり、配列上で二分探索をしたりすることがよくあります。配列の要素が変わらなければ簡単ですが、要素が挿入されたり削除されたりすることもあります。頻繁ではありませんが。こんなとき、いちいちソートしていては間に合いません。C++ならstd::setを使えますがPythonにはありません。 以下ではstd::setの代わりに使えるかもしれない方法を書きます。 heapqを知っていますか? ABC137D - Summer Vacation いきなり脱線しますが優先度付きキューを紹介します。優先度付きキューとは、appendが$O(\log N)$、popが$O(\log N)$、最小値の参照が$O(1)$で済むデータ構造です。 Pythonでは標準ライブラリのheapqが使えます。Aをheapifyしたとき、$A[0]$がAの最小値です。最大値を求めたい場合はappendする値とpopされた値に-1を掛けます。 公式解説が一番わかりやすいので読んでください。 ACコードの例 類題 ABC141D - Powerful Discount Tickets ABC212D - Querying Multiset ABC217E - Sorting Queries heapqから削除したつもりリストを作って管理する ABC170E - Smart Infants この問題についてわかりやすい解説をしていたブログがあったはずなのですが失念してしまいました。 幼児iが幼稚園Aの最強園児である時、iがAからBへ転園すると、Aの最強園児が入れ替わります。つまりAからiを削除したあとの最強園児を求めなくてはなりません。 これを解決するのが、削除したつもりの園児のリストを作成する方法です。 つまり、最強園児を求める際に、heapqの先頭に来た園児に対して削除したつもりかチェックし、そうならば本当に削除します。そうでないならば、その園児が最強です。最強園児が決まるか、園児が0人になるまで繰り返します。listでは本当に削除する時の処理が遅いため、dictで要素ごとに個数を管理します。 クエリを処理するごとに平等さを求める必要がありますが、同様にheapqと「削除したつもりリスト」で管理できます。 ACコードの例 配列を木構造に落として総和をメモする ARC033C - データ構造 BIT(Fenwick Treeとも)というデータ構造を使えば、区間和を$O(logN)$で求めることができます。また、平方分割を使えば、区間和を$O(\sqrt{N})$で求めることができます。 $A_{X_i} = (S \in X_i) \ ?\ 1 :0$として、右端のindexが0で総和がxとなる最小の区間を求めれば解けます。 $1\le X_i \le 2 \times 10^5$なので、$X_i-1$をそのままindexとして配列に格納しても間に合います。 二分探索で求めれば間に合うそうですが、私のコーディングスキルでは残念ながらTLEになってしまいました。平方分割で線形探索を行うと通りました。 ACコードの例 類題 ABC157E - Simple String Queries 座標圧縮+木 ABC217D - Cutting Woods 簡単のために$x=0,L$にも切れ目があることにします。 $1\le L \le10^9$なので、$L$をそのままindexとして配列に格納すると間に合いません。 そこで、考えるべき線の数が高々$Q+2$本であること、線の左右関係は問題を通じて変わらないことを利用して、座標圧縮を行います。座標圧縮とは、値の上下関係を維持したまま圧縮し、圧縮後の値について操作を行うテクニックです。ただし、あらかじめ全ての値を知っている必要があるため、クエリ先読みを行います。 抽象的な書き方をすると$f(X)=[0,\#X)\cap\mathbb{Q}$かつ$a_i<a_j\Leftrightarrow f(a_i)<f(a_j)$ を満たす$f$と$f^{-1}$を考えるのですが、具体的には重複を取り除いた後にソートしてindexとvalueを入れ替えてdictに入れればよいです。提出したコードでは難解な書き方になっていますが、簡単に書くとこうなります。 X = set() for c,x in Q: X.add(x) finv = list(sorted(X)) f = dict() for k,v in enumerate(X): f[v] = k あとはBITなり平方分割なりに乗せてください。BITも二分探索もしんどいので私は平方分割と線形探索を行いました。だいぶループ回してるはずなのに通るとは思わなかった。 ACコードの例 巨人の肩に乗る likein12さんがPython向けのordered setとmultisetのwrapperを公開しています。 バイナリを埋め込む手法が使われているため、コンテストのルールをよく確認して使用してください。 自力で実装する 頑張って実装しても遅いことが稀によくあります。 いかがでしたか C++書けるならそっちのほうがいいよ。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Pythonエンジニアが雰囲気でJavascript使ったらinでハマった

普段はPythonを使っているが、JS/TSを雰囲気で触ってみたら思わぬ罠にハマったので残しておく ハマった罠 Pythonでlistの要素存在を確認する時、以下のように書く 「そもそもリスト内包表記([i for i in range(3)])使えよ」とか細かい点は一旦無視する numbers = [1, 2, 3] print(1 in numbers) JS/TSでも同じノリで描いてみたら const array = [1, 2, 3] console.log(1 in array) // True console.log(2 in array) // True console.log(3 in array) // False Why!? JS/TS People!!! 原因 ちゃんとMozillaの基本的な使い方のとこに書いてあった // Arrays let trees = ['redwood', 'bay', 'cedar', 'oak', 'maple']; (中略) 'bay' in trees // false を返す (インデックスの指す値ではなく、インデックスの数字を指定しなければならない) インデックスは0,1,2になるため3だけFalseになるわけね 解決策 includesを使う const array = [1, 2, 3] console.log(array.includes(1)) // True console.log(array.includes(2)) // True console.log(array.includes(3)) // True
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

DataFrameの中身を等間隔で取り出す

試験データ、例えば引張試験の伸びと荷重をとても細かいサンプリング間隔で取得してしまうと、その後のデータ処理やグラフ化する際に重くて扱いにくい場合があります。等間隔でデータを間引く方法を知っておくと便利です。 仮に、100行のデータを10分の1に間引きたいとします。まずは100行分のダミーデータをDataFrame型で作成します。 import numpy as np import pandas as pd # データ作成 np.random.seed(seed=1) df = pd.DataFrame(np.random.rand(100, 2), columns=['elongation', 'load']) つぎのようなデータができます。 elongation load 0 0.417022 0.720324 1 0.000114 0.302333 2 0.146756 0.092339 3 0.186260 0.345561 4 0.396767 0.538817 .. ... ... 95 0.263297 0.065961 96 0.735066 0.772178 97 0.907816 0.931972 98 0.013952 0.234362 99 0.616778 0.949016 [100 rows x 2 columns] DataFrameのインデックスラベルをスライス構文で指定することで、等間隔で行を取り出すことができます。 # 一定間隔で取り出し step = 10 df_selected = df.loc[::step] ここで、「::」に続く数字が、取り出しのステップとなります。取り出し後のDataFrame(df_selected)の中身は以下のようになります。 elongation load 0 0.417022 0.720324 10 0.800745 0.968262 20 0.988861 0.748166 30 0.102334 0.414056 40 0.883306 0.623672 50 0.326645 0.527058 60 0.019880 0.026211 70 0.556240 0.136455 80 0.239848 0.493770 90 0.629718 0.210174 もともと100行あったものが、10行になりました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Docker + Django-rest-fremework 構築メモ

前提 Docker, Docker Composeはインストールされている バージョン情報 Django 3.0 Djangorestfremework 3.12.4 Docker 20.10.8(build 3967d7d) Python 3.8 ホスト端末: Windows10 (64bit) 1.とりあえず環境作る 1.1 フォルダ構成とファイル内容 フォルダ構成 フォルダ:restapifw-sample F:..restapifw-sample │ docker-compose.yml │ Dockerfile │ requirements.txt └─workspace(フォルダ) ファイル内容 docker-compose.yml # Compose.ymlファイル書式のバージョン version: '3' services: # サービス名定義 django-restapi: # ComposeFileを実行しビルドされるときのルートパス # このフォルダにDockerファイルを置く build: . # Docker 起動 # command: python manage.py runserver 0.0.0.0:8000 # コンテナ名 container_name: 'django-sample.v1.0.0' # コンテナ内のワーキングディレクトリ # working_dir: '/workspace/tutorial' # コンテナを終了させたくない場合に設定 tty: true ports: - 8082:8000 # ローカルフォルダとコンテナ内のマウントディレクトリの関連づけ volumes: - ./workspace:/workspace Dockerfile. FROM python:3.8 USER root RUN apt-get update RUN apt-get -y install locales && \ localedef -f UTF-8 -i ja_JP ja_JP.UTF-8 ENV LANG ja_JP.UTF-8 ENV LANGUAGE ja_JP:ja ENV LC_ALL ja_JP.UTF-8 ENV TZ JST-9 ENV TERM xterm # コンテナ側へコピー COPY requirements.txt requirements.txt # python ライブラリインストール RUN pip install -r ./requirements.txt #viインスト RUN apt-get install -y vim less requirements.txt setuptools pip django==3.0 djangorestframework==3.12.4 1.2 Docker起動 プロジェクトフォルダのカレントで実施 コマンド. docker-compose build docker-compose up -d #起動確認 docker-compose ps docker-compose ps Name Command State Ports --------------------------------------------------------------------------------- django-sample.v1.0.0 python3 Up 0.0.0.0:8082->8000/tcp,:::8082->8000/tcp 1.3 Docker内でDjango設定 Docker内に入る(そんなイメージで通じる?) マウントしているworkspaceフォルダ配下にDnagoプロジェクトを作ります コマンド. docker exec -it django-sample.v1.0.0 /bin/bash cd workspace ここからは、下記URLを参照に実施 https://www.django-rest-framework.org/tutorial/quickstart/ コマンド. # Create the project directory mkdir tutorial cd tutorial # Create a virtual environment to isolate our package dependencies locally # 既にDocker内にインストール済みなので不要 # python3 -m venv env # source env/bin/activate # On Windows use `env\Scripts\activate` # Install Django and Django REST framework into the virtual environment # 既にDocker内にインストール済みなので不要 #pip install django #pip install djangorestframework # Set up a new project with a single application # Note the trailing '.' character django-admin startproject tutorial . cd tutorial django-admin startapp quickstart cd .. #作成されたファイルを確認(やらなくてもOK) find . . ./manage.py ./tutorial ./tutorial/asgi.py ./tutorial/quickstart ./tutorial/quickstart/admin.py ./tutorial/quickstart/apps.py ./tutorial/quickstart/migrations ./tutorial/quickstart/migrations/__init__.py ./tutorial/quickstart/models.py ./tutorial/quickstart/tests.py ./tutorial/quickstart/views.py ./tutorial/quickstart/__init__.py ./tutorial/settings.py ./tutorial/urls.py ./tutorial/wsgi.py ./tutorial/__init__.py Django内部のSqliteのデータベース作成 コマンド. python manage.py migrate Operations to perform: Apply all migrations: admin, auth, contenttypes, sessions Running migrations: Applying contenttypes.0001_initial... OK Applying auth.0001_initial... OK Applying admin.0001_initial... OK Applying admin.0002_logentry_remove_auto_add... OK Applying admin.0003_logentry_add_action_flag_choices... OK Applying contenttypes.0002_remove_content_type_name... OK Applying auth.0002_alter_permission_name_max_length... OK Applying auth.0003_alter_user_email_max_length... OK Applying auth.0004_alter_user_username_opts... OK Applying auth.0005_alter_user_last_login_null... OK Applying auth.0006_require_contenttypes_0002... OK Applying auth.0007_alter_validators_add_error_messages... OK Applying auth.0008_alter_user_username_max_length... OK Applying auth.0009_alter_user_last_name_max_length... OK Applying auth.0010_alter_group_name_max_length... OK Applying auth.0011_update_proxy_permissions... OK Applying sessions.0001_initial... OK Djangoプロジェクト内のスーパユーザ作成 パスワードは自由だけどここでは、admin にしちゃった (サイトでは、admin:password123かな) コマンド. python manage.py createsuperuser --email admin@example.com --username admin ここからは、サイト通りDjango内のソースや設定ファイルをいじる (Windowsからフォルダを参照して直接作成してもOK。コンテナ内で作業する必要なし。但し文字コードはUTF-8で保存) tutorial/quickstart/serializers.py from django.contrib.auth.models import User, Group from rest_framework import serializers class UserSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = User fields = ['url', 'username', 'email', 'groups'] class GroupSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = Group fields = ['url', 'name'] tutorial/quickstart/views.py from django.contrib.auth.models import User, Group from rest_framework import viewsets from rest_framework import permissions from tutorial.quickstart.serializers import UserSerializer, GroupSerializer class UserViewSet(viewsets.ModelViewSet): """ API endpoint that allows users to be viewed or edited. """ queryset = User.objects.all().order_by('-date_joined') serializer_class = UserSerializer permission_classes = [permissions.IsAuthenticated] class GroupViewSet(viewsets.ModelViewSet): """ API endpoint that allows groups to be viewed or edited. """ queryset = Group.objects.all() serializer_class = GroupSerializer permission_classes = [permissions.IsAuthenticated] tutorial/urls.py from django.urls import include, path from rest_framework import routers from tutorial.quickstart import views router = routers.DefaultRouter() router.register(r'users', views.UserViewSet) router.register(r'groups', views.GroupViewSet) # Wire up our API using automatic URL routing. # Additionally, we include login URLs for the browsable API. urlpatterns = [ path('', include(router.urls)), path('api-auth/', include('rest_framework.urls', namespace='rest_framework')) ] tutorial/settings.py 量が多いのでかいつまんで 28行目付近 ALLOWED_HOSTS = ['*'] 40行目付近 INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'rest_framework', ←これ追加 ] 52行目あたりに追加 REST_FRAMEWORK = { 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', 'PAGE_SIZE': 10 } 1.4 Django手動起動 Dockerコンテナ内で実行 コマンド. cd /workspace/tutorial python manage.py runserver #うまく動作するとこんな感じ Watching for file changes with StatReloader Performing system checks... System check identified no issues (0 silenced). October 09, 2021 - 02:08:21 Django version 3.0, using settings 'tutorial.settings' Starting development server at http://127.0.0.1:8000/ Quit the server with CONTROL-C. 確認 もう一つコンソールを起動してDockerコンテナ内に入り稼働確認 パスワードは自分で作成したスーパユーザのパスワード (サイトでは、admin:password123かな) コマンド. cd ...作成したプロジェクトのrestapifw-sampleフォルダへ移動 docker exec -it django-sample.v1.0.0 /bin/bash curl -H 'Accept: application/json; indent=4' -u admin:admin http://127.0.0.1:8000/users/ こんなのが出たら起動している コマンド. { "count": 1, "next": null, "previous": null, "results": [ { "url": "http://127.0.0.1:8000/users/1/", "username": "admin", "email": "admin@example.com", "groups": [] } ] } 1.5 Django停止 CTRL + C で終わらせる 2.Docker自動起動 + ホストからのアクセス設定 環境が作成出来たのでコンテナを起動したらDjangoを自動起動するように設定する Docker停止(exit でコンテから抜けてから実行) コマンド. docker-compose down Docker-compose.ymlファイルのコメントを2つ外す(Docker起動と、コンテナ内のワーキングディレクトリ) ※Djangoの 0.0.0.0で起動するのが肝これをやらないと外からアクセスは出来ないらしい?本当はnginx立ててリバースプロキシ経由でやりたかったけど出来なかったのでこれで… Docker-compose.yml # Compose.ymlファイル書式のバージョン version: '3' services: # サービス名定義 django-restapi: # ComposeFileを実行しビルドされるときのルートパス # このフォルダにDockerファイルを置く build: . # Docker 起動 command: python manage.py runserver 0.0.0.0:8000 # コンテナ名 container_name: 'django-sample.v1.0.0' # コンテナ内のワーキングディレクトリ working_dir: '/workspace/tutorial' # コンテナを終了させたくない場合に設定 tty: true ports: - 8082:8000 # ローカルフォルダとコンテナ内のマウントディレクトリの関連づけ volumes: - ./workspace:/workspace 再ビルド # 再ビルド docker-compose build # 起動 docker-compose up -d # 起動確認 docker-compose ps Name Command State Ports -------------------------------------------------------------------------------------------------------- django-sample.v1.0.0 python manage.py runserver ... Up 0.0.0.0:8082->8000/tcp,:::8082->8000/tcp Windows10のコンソールから実行 結果が返って来る(レイアウトは少々悪いけど) curl -H 'Accept: application/json; indent=4' -u admin:admin http://127.0.0.1:8082/users/ curl: (6) Could not resolve host: application curl: (6) Could not resolve host: indent=4' {"count":1,"next":null,"previous":null,"results":[{"url":"http://127.0.0.1:8082/users/1/","username":"admin","email":"admin@example.com","groups":[]}]} ブラウザから確認 http://127.0.0.1:8082/users/ 下記画面が表示されればOK 右上のLoginから遷移した画面 最後に 以前AWSでRestapi作って見たけど本当に運用を考えると維持費が高い・・。 もっと安いクラウドサービスなどで簡単にRestApiアプリを作れる方法を検討してDjangoのRestfremeworkに辿り着いた。 またDjango付属のサーバは開発用簡易サーバなはずだから本番ではやはりNginx経由などで構築しないとダメなのかな。 参照リンク https://www.django-rest-framework.org/tutorial/quickstart/ https://tech.mokelab.com/infra/docker/tips/empty_reply_from_server.html リバースプロキシの仕組みとは?nginxを使った設定
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

とりあえずたくさんの単語が欲しいときのプログラム

注意 オートコンプリートのAPIポリシー等を検索していみましたが、こういう使い方がいけないみたいな記述はみつけられませんでした、、が実行は自己責任でお願いします。 ※正直自分が見つけられないだけかもしれません。 こんな感じのもの テストデータなんかを作るときに、とりあえず全角の単語が欲しい時に使用 コンソール上に単語がたくさん出ます(440くらい) こちらの方のプログラムを半分以上流用していますm(_ _)m 環境 windows10 python3.9.4 pip実行(以下パッケージが必要) python -m pip install requests python -m pip install lxml python -m pip install numpy python -m pip install pandas ソース test.py import pprint import requests from lxml import etree import numpy as np import pandas as pd qstrs = ["あ", "い", "う", "え", "お", "か", "き", "く", "け", "こ", "さ", "し", "す", "せ", "そ", "た", "ち", "つ", "て", "と", "な", "に", "ぬ", "ね", "の", "は", "ひ", "ふ", "へ", "ほ", "ま", "み", "む", "め", "も", "や", "ゆ", "よ", "ら", "り", "る", "れ", "ろ", "わ" ] # 単語格納用 google_suglist = [] # ひらがな分ループ for qstr in qstrs: # オートコンプリートのデータを取得する google_r = requests.get("https://www.google.com/complete/search", params={'q':qstr, 'hl':'ja', 'ie':'utf_8', 'oe':'utf_8', 'output': 'toolbar'}) google_root = etree.XML(google_r.text) google_sugs = google_root.xpath("//suggestion") google_sugstrs = [s.get("data") for s in google_sugs] for ss in google_sugstrs: google_suglist.append(ss) # print(ss) # コンソール出力 for word in google_suglist: print(word) 実行(要らんでしょうが…) python test.py 最後に ランダムで1文字生成してとかにすると、もうちょっと良いものになるかも、、
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【AI・機械学習入門】よく使う Google Colaboratory のショートカットキー

はじめに 今回は、Google Colaboratory (以後、Colab)のショートカットキーについてです。 いきなり多くのショートカットキーを詰め込んでも、最初は大変なので、よく使うショートカットキーに絞りこんで説明させていただきます。 環境 今回紹介するショートカットキーの動作環境は以下の通りです。 Windows 10 Google Colaboratory Macをご使用の方は、Ctrl を Cmd に置き換えてください。 Colab のショートカットキー一覧 ショートカットキーの一覧については、 Colab を開き、ツール > キーボードショートカット から確認することができます。 または、Ctrl + M H でも同様に確認することができます。 Colab でよく使うショートカットキー 今回紹介するよく使うショートカットキーは以下の通りです。 ショートカットキー 内容 Ctrl + M H キーボード ショートカットを表示 Ctrl + S ノートブック1を保存 Ctrl + M A 上にコードセル2を挿入 Ctrl + M B 下にコードセル2を挿入 Ctrl + M D 選択したセル3を削除 Ctrl + M Y コードセル2に変換 Ctrl + M M テキストセル4に変換 Esc 現在のセル3の選択を解除 Ctrl + Enter 現在のセル3を実行 Shift + Enter セル3を実行して次のセル3を選択 Ctrl + F9 ノートブック1内のすべてのセル3を実行 Ctrl + M I 実行を中断 Ctrl + / 現在の行をコメントにします Shift + Tab 現在の行のインデント5を解除 Tab オートコンプリート6 Ctrl + Space オートコンプリート6(同上) Ctrl + Z セル内の変更を戻す Ctrl + Shift + Z 最後のセル操作を元に戻す Ctrl + M Z 最後のセル操作を元に戻す(同上) Ctrl + D 同じコード/テキストを選択 Ctrl + H グローバル検索/置換 覚える順番のオススメ ショートカットキーは、いきなりすべてを詰め込むのではなく、実際に使ってみて徐々に慣れていくと自然に覚えます。 はじめのうちは、3つくらいを1日打つ練習をすると、すぐに慣れると思いますよ。 Ctrl + S, Ctrl + M A, Ctrl + M B, Ctrl + M D, Ctrl + M Y, Ctrl + M M, Esc まずは、これらのショートカットキーを使って、セルの操作に慣れてください。セルの操作はよく行うので、ショートカットキーを覚えることで格段に操作が速くなりますよ。 Ctrl + Enter, Shift + Enter, Ctrl + F9, Ctrl + M I つぎに、セルの実行に関するショートカットキーに慣れてください。こちらもセルの操作と同じくらい多く使います。Ctrl + M I は、実行を中断します。セルを実行して、実行が完了しないおかしいなと思ったときに使います。 Ctrl + /, Shift + Tab, Tab, Ctrl + Space セルの中にコードを記述する際に、よく使うショートカットキーです。 Tab と Ctrl + Space は、オートコンプリートを行うことができます。どちらか片方使いやすい方を選んで覚えれば大丈夫です。 ※私はTabで覚えています。 Ctrl + Z, Ctrl + Shift + Z, Ctrl + M Z, Ctrl + D, Ctrl + H セルの変更や操作を戻したり、検索/置換を行うショートカットキーです。 Ctrl + D と Ctrl + H は組み合わせて使用すると、一括置換ができるので便利です。 Ctrl + Shift + Z と Ctrl + M Zは、最後のセル操作を元に戻すことができます。実験的にセルを追加して実行したが不要なので消したい場合に使用したりします。どちらか片方使いやすい方を選んで覚えれば大丈夫です。 ※私はCtrl + Shift + Zで覚えています。 さいごに 前回の記事と合わせて、ショートカットキーについては以上となります。 次回は、いよいよPythonの記事になります。 まずは、Pythonでよく使うライブラリの紹介と各ライブラリはなにができるのかということを身近なものに例えてみたいと思います。 ノートブックとは、現在開いているColab環境そのものを指します。ノートブックは、複数のセルと呼ばれるブロックで構成されています。 ↩ コードセルとは、Python のコードを書き込み、実行するためのセルです。 ↩ セルには、コードセルとテキストセルの 2 種類があります。 ↩ テキストセルとは、Markdown 形式で文章を書くためのセルです。 ↩ インデントとは、文章の行頭に空白を挿入して先頭の文字を右に押しやる字下げのことです。プログラミングでは、Tabで字下げを行い、コードの可読性を向上させるために用いられます。 ↩ オートコンプリートとは、過去に入力した文字を記憶し、次に入力される内容を予想して表示する機能のことです。プログラミングでは、設定されたコードを途中まで入力すると、該当する候補がオートコンプリートによって表示されます。 ↩
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

時系列データ分析

この記事の狙い・目的 機械学習を取り入れたAIシステムの構築は、 ①データセット作成(前処理)→ ②モデルの構築 → ③モデルの適用 というプロセスで行っていきます。 その際「データセット作成(前処理)」の段階では、正しくモデル構築できるよう、事前にデータを整備しておくことが求めらます。 このブログでは、時系列を含むデータに対しての解析方法、および前処理段階で用いられる「時系列データ分析」の手法について解説していきます。 プログラムの実行環境 Python3 MacBook pro(端末) PyCharm(IDE) Jupyter Notebook(Chrome) Google スライド(Chrome) 時系列データとは 時系列データとは、時間の順序にしたがって並べられたデータのことをいいます。 例えば、株価の値動き、気温や降水量などの気象情報、交通量の推移などがこれにあたります。 時系列データがもつ情報 時系列データには大きく分けて「傾向変動」「季節変動」「循環変動」「不規則変動」の4つの情報(要因)を持っています。ひとつひとつ解説していきます。 1.傾向変動(トレンド) トレンドとは、時系列の⻑期的傾向のことです。時間の経過とともに増加・減少する傾向とも言えます。 トレンドの推定方法には「移動平均」などがあります。詳しくは後述します。 2.季節変動(シーズナル) 季節変動とは、1年周期(12ヵ月間で繰り返す)で季節ごとに繰り返される変動(季節成分)のことです。 3.循環変動(サイクル) 1年以上の長い期間で、ある周期性をもって現れる変動のことです。 4.不規則変動(ノイズ) トレンド、季節変動、循環変動では説明できない、短期的かつ不規則な変動のことです。 これらを確認するため「季節調整」という手法をまずは簡単に見ていきましょう。 # 成分分解(季節調整) import pandas as pd import statsmodels.api as sm import matplotlib.pyplot as plt %matplotlib inline # データ取得 stock_df = pd.read_csv("./Tesla.csv") # Dataをインデックスに設定 time_series = stock_df.copy() time_series["Date"] = pd.to_datetime(stock_df['Date']) time_series = time_series.set_index(['Date']).sort_index(ascending=True) # 表示サイズ調整 plt.rcParams["figure.figsize"] = [20,10] plt.rcParams["font.size"] = 15 # 原系列、傾向(トレンド)、季節変動(季節性)、不規則変動(残差) def plt_seasonal_decompose(df, col_name, period): res = sm.tsa.seasonal_decompose(df[col_name], period=period) fig = res.plot() plt.show() # 原系列をインプットして確認 plt_seasonal_decompose(time_series, col_name='Close', period=100) 1番目のグラフが原系列、2番目のグラフが傾向変動(Trend)、3番目のグラフが季節変動(Seasonal)、4番目のグラフが不規則変動(Resid(残差))を表しています。 時系列データにおける重要な概念 時系列データ分析を始める前に、まずは時系列データにおける重要な概念を解説していきます。 「定常性」「単位根(過程)」「自己共分散」の3つを紹介します。 1.定常性 定常性とは、時間経過による影響を(強く)受けない状態のことです。 また、定常過程とは、その確率的な変動の性質が、各時点に依存せず一定であるような確率過程のことを言います。 具体的には、各期の平均が近似しており、上昇(下降)の傾向がないデータになります。 例えば、携帯アラームの周波数など、平均0の周辺で一定の分散をもってランダムに振動を続けるデータは、定常性があると言えます。反対に、明らかな上昇傾向が存在し変化し続けている株価の値動きなどは、定常性があるとは言えません。 ・定常過程の例 ・非定常過程の例 非定常過程のグラフは、グラフの右側へ進むにつれて変動(上昇傾向)があることがわかります。 2.単位根(過程)とは 単位根とは、値が足し合わされて出来上がったデータのことを言います。 そして単位根を持つデータを「単位根過程」と呼びます。 $ ?? $が非定常過程、また差分系列 $ {\Delta}y_t = y_t – y_{t-1} $が定常過程である時、$ y_t $は単位根過程である。 データの原系列は非定常だが、差分を取ると(弱)定常になるようなデータとも言えます。 最も代表的な単位根過程として「ランダムウォーク」があります。 ランダム・ウォーク過程とは、$ X_1, X_2, · · ·, X_n $ の系列が、$ Xt = Xt−1 + ut $ と表すことができます。 3.自己共分散 ある時系列 $ y_{1:t} $ に関して、各時点t における期待値を E[$y_t$]$ $= μt$ としたとき、 時点差$j$ における $yt$ と $y$_{t-j}$の共分散を、時差jの自己共分散といいます。 つまり、同一の時系列データにおける異なる時点間での共分散を指します。 (※$Cov$ = Covariance(共分散)、$E$ = Expect(期待値)) Cov(y_t, y_{t-j}) = E[ (y_t - {\mu}_t)(y_{t-j} - {\mu}_{t-j})] 時系列データ分析とは それでは時系列データ分析について解説していきましょう。 時系列データ分析とは、ある一定間隔の時間に対して観測されているデータの周期性や傾向から将来を予測する手法のことを言います。これにより近い将来を把握し、施策の検討や対策を打つことが可能となります。 時系列データ分析の目的 時系列データ分析には、大きく分けて下記の3つの目的があります。 1.データの可視化 2.情報の抽出 3.将来の予測 それぞれ具体的に見ていきましょう。 1.データの可視化 データの特徴を捉える為、「周期性」および「時間的な相関」を可視化していくことがあります。 ①周期性 周期性(周期的変動)とは、時系列が⼀定の間隔で同じような変動を繰り返す成分、またはその状態を指します。 周期性を可視化する方法として「コレログラム」というものがあります。 コレログラムとは、ラグと自己相関を表したグラフです。 ラグとは、元データからどれほど時間をずらしているかを表す指標のことです。 つまり、元データxから時間をずらしたデータyとの相関係数を表すグラフであり、横軸にラグ、縦軸に自己相関をとります。 青で塗られた領域は、データが無相関としたときの95%信頼区間で、この領域の外にある場合この仮定は棄却され、相関があると判断できます。 # 表示サイズ調整 plt.rcParams["figure.figsize"] = [20,2] plt.rcParams["font.size"] = 10 # コレログラム def correlogram(cicle, lags, df, feature): sm.graphics.tsa.plot_acf(df.resample(cicle)[feature].sum(), title=feature, lags=lags) # まずは原系列をインプットして確認 correlogram(cicle='Y', lags='7', df=time_series, feature='Close') # 年ベース correlogram(cicle='M', lags=str(12*6), df=time_series, feature='Close') # 月ベース correlogram(cicle='D', lags=str(30*12*6), df=time_series, feature='Close') # 日ベース 1番目が年ベースのグラフ、2番目が月ベースのグラフ、3番目が日ベースのグラフです。 これは原系列をコレログラムで表したグラフのため、本来このままでは正確な情報を得ることはできませんが、全体的に緩やかな変化が見て取れ、グラフの後方程、大きな周期変動がないように見られます。 ②時間的な相関 時間的な相関を確認する方法として、「相関関数」を用いる方法があります。 これにより離れた時点の時系列との相関を⾒てることができます。 また、この相関にも種類があり、ここでは「自己相関」「偏自己相関」について解説します。 自己相関( ACF : Autocorrelation Function ) 自己相関とは、過去の値が現在のデータにどのくらい影響しているかを表しています。 時系列のある時刻と$k$ だけ離れた時刻との相関をみると相関の観点から時系列の特徴を捉えることができるます。 ラグとは、ズラしたデータのステップ数のことです。 自己相関は $|{\rho}(j)|{\leq}1$を満たし、自分自身との自己相関係数は1となり、相関の強いデータほど自己相関係数は1に近づきます。 ※ ${\gamma}(0)$ は分散0を表し、${\gamma}(j)$の強度により自己相関の強さを測ることができます。 {\rho}(j) = \frac{Cov(y_t, t_{t-j})}{\sqrt{Var(y_t)Var(y_{t-j})}} = \frac{{\gamma}(j)}{{\gamma}(0)} 偏自己相関( PACF : Partial Autocorrelation Function) ある時点$j$ の偏自己相関とは、時点$t$ と時点$t-j$ の間に存在している$j-1$個のデータ $y_{t-(j-1)}$, $y_{t-(j-2)}$, ..., $y_{t-1}$ の影響を取り除いた後の時点$t$ と時点$t-j$ の間の相関のことです。 例えば、今日と2日前の関係を見たい際、間接的に1日前の影響が含まれる為、偏自己相関を用いることで、1日前の影響を除いて今日と2日前だけの関係を調べる事ができるようになります。 また、時点$t$ と時点$t-2$ の偏自己相関は、「時点$t$ のうち時点$t-1$ で説明できなかった情報$ y_t - {\alpha}y_{t-1} $」と「時点$t-2$ のうち、時点$t-1$ で説明できなかった情報$ y_{t-2}-{\beta}y_{t-1} $」の相関を表しています。通常 ${\alpha}, {\beta}$ は、$E[ (y_t - {\alpha}y_{t-1})^2 ]$ が最小となるように選ようにします。計算式で表すと以下の通りです。 \frac{Cov(y_t - {\alpha}y_{t-1}, y_{t-2} - {\beta}y_{t-1})}{\sqrt{Var(y_t - {\alpha}y_{t-1})Var(y_{t-2} - {\beta}y_{t-1})}} 2.情報(要因)の抽出 次に時系列データの情報抽出の方法として「季節調整」について解説していきます。 季節調整とは、何らかの原因で特定の周期で繰り返す成分を除去して本質的な現象を抽出する手法です。 具体的には、時系列データを「傾向変動」「季節変動」「不規則変動」に3つに分解していきます。これにより時系列データの傾向をはっきり⾒ることができます。 ①傾向変動(トレンド)の推定 「移動平均」を取ることで傾向変動を推定する方法があります。 移動平均により、時系列データを平滑化(変動を小さく)し、なめらかな傾向変動(トレンド)を取り出すことができます。 具体的には、時系列データにおいて、ある一定区間ごとの平均値を区間をずらしながら求めていきます。 ある時点$t$ と時点$t-s$ の比の計式は下記の通りです。 Z_t = \frac{y_t}{y_{t-s}} \\ ある時点$t$ の前後のデータを使って時点$t$ の値を調整し、時点$t$ の前後の値との差を小さくする流れを考えてみましょう。 具体的には、時系列全体に対して、ある時点$t$ のデータを、$t-k$ から $t+k$ までのデータの平均をとっていきます。 Z_t = \frac{1}{k + 1}(y_{t-k} + y_{t-(k+1)} + … + y_t … + y_{t+(k-1)} + y_{t+k}) 移動平均を用いてグラフを作成すると、長期的な傾向を表す滑らかな曲線が得られます。 時系列データの変化のブレが細かく、解析への悪影響が懸念される際に使用されることがあります。 # インデックスに['Date'](時間情報)を設定 stock = stock_df.copy() stock["Date"] = pd.to_datetime(stock['Date']) stock = stock.set_index(['Date']).sort_index(ascending=True) # 移動平均 stock['Close_move_mean'] = stock['Close'].rolling(100).mean().round(1) plt.plot(stock['Close'], label='終値') plt.plot(stock['Close_move_mean'], label='移動平均') plt.legend(loc='lower right') plt.show(); 各時点の平均を取ることで、ゆるやかな変化を捉えられるようになったことがわかります。 ②季節変動(シーズナル)の推定 季節変動を推定する方法として「季節階差」をとる方法があります。 季節階差とは、トレンド除去後の時系列データから季節変動(季節成分)を求める方法です。 階差とは今期と前期の値の差のことです。そして季節階差は周期pの階差を取ることで、時系列データからゆっくりした傾向変動を除去することができます。 また、季節成分を分離することにより,単なるトレンド推定よりトレンドの微妙な変化を捉えることができるようになります。 周期pの1周期をsとした場合、計算式は下記のように表すことができます。 {\Delta}_sy_t = y_t - y_{t-s} ③不規則変動(ノイズ)の推定 傾向変動と季節変動を除去することで、不規則変動を推定することができます。 3.将来の予測 時系列データ分析により「将来の予測」が可能となります。 その為に様々なモデルが考案されています。代表的なものとして「MAモデル」「ARモデル」「ARMAモデル」「ARIMAモデル」などがあります。時系列データの予測モデルについては、また別記事で紹介させていただきます。 定常性の確認 時系列データが定常過程を単位根過程かを確認する手法に、ADF(Augmented Dickey-Fuller)検定というものがあります。 多くの時系列モデルでは定常過程を前提としているため、時系列に対してまず最初に単位根を確認する事が多いです。 具体的には、帰無仮説:単位根過程, 対立仮説 : 定常過程、とし、P値(有意水準)が0.05(5%)以下なら帰無仮説が棄却され、定常過程とみなしていきます。実際に実装と結果を見ていきましょう。 def adf_test(series): adf_df = pd.DataFrame( [ stattools.adfuller(series)[1] ], columns=['P値'] ) adf_df['P値'] = adf_df['P値'].round(decimals=3).astype(str) print(adf_df) # 原系列のp値を算出 adf_test(series=time_series['Close']) # 結果:p値 = 0.815 p値が81.5%となり、原系列は非定常性であると判断できます。 定常がない場合、そのままの状態ではデータの特徴や関係性を正しく捉えられないため、定常性をもつようデータの変換が必要になります。データの変換方法にもいくつかあるため、以下で解説していきます。 時系列データの前処理 検定により時系列データが単位根過程と判断された場合、データの関係性を正しく解析ができない為、定常性を持たせるための変換を行う必要があります。 ここでは原系列に対する、各種の変換方法を解説していきます。 原系列とは、変換前のもともとの時系列データのことです。 1.差分変換 差分変換とは、ある時点$t$ のときの値とその直前の時点$t-1$ のときの値の差分 ${\delta}t = y_t - y_{t-1}$ を計算し、その差を新しいデータとする手法です。 つまり、1時点離れたデータとの差を取る手法です。その結果を差分系列、または階差系列と言います。 例えば、今日の株価と昨日の株価の差を考えたい時などに使用します。 上昇あるいは下降のトレンドを持つ時系列データ$(y_t = a_t + b)$ のとき、差分を計算することで、トレンドを取り除くことができます。計算式は以下の通りです。 Z_t = y_t - y_{t-1} = (a_t + b) - (a(t - 1) + b) = a # Dataをインデックスに設定 time_series = stock_df.copy() time_series["Date"] = pd.to_datetime(stock_df['Date']) time_series = time_series.set_index(['Date']).sort_index(ascending=True) # 原系列のプロット plt.plot(time_series['Close']) # 差分変換後のプロット time_proc = time_series.copy() Passengers_diff_c = time_proc["Close"].diff(periods = 1) Passengers_diff_c.plot(figsize=[20,5]); 上のグラフが原系列(比較用)、下のグラフが差分変換後のグラフです。 差分を取ることで、上昇傾向が取り除かれ、平均が0の正常過程の状態に近づけることができるようになりました。 y軸のスケールが変換前後で変わっているのもポイントです。 2.対数変換 対数変換とは、変動が著しく大きな時系列データに対し、対数値変換を施すことで原系列の傾向を保持したまま、値を小さく変換する手法です。 変動の分散を一様にし、複雑な時系列でも変数変換で分析が簡単になることがあります。 対数変換された時系列データは対数系列と呼ばます。数式では${\log}(y_t)$ と表します。 例えば、ビットコインの価格などの極端な動きをするデータに対数系列を用います。 Passengers_log_c = np.log(time_proc["Close"]) Passengers_log_c.plot(figsize=[20,5]); 上のグラフが原系列(比較用)、下のグラフが対数変換後のグラフです。 際立っていた上下運動がおしなべてなめらかになり、原系列のトレンドに比例した値のばらつきを解消できていることがわかります。 3.対数差分変換 対数差分変換とは、対数変換と差分変換の両方の変換を施す手法です。 時系列データが変化率や成長率などである場合、対数差分 ${\Delta}{\log}(y_t) = {\log}(y_t / y_{t-1}) $ を計算して解析することがあります。 Passengers_log_diff_c = Passengers_log_c.diff(periods = 1) Passengers_log_diff_c.plot(figsize=[20,5]); 1番目が原系列(比較用)、2番目が対数変換後の時系列(比較用)、3番番目が対数差分変換後の時系列です。 全体を通して平均と分散はのばらつきは解消し、年ごとの周期性がゆるやかになっていることがわかります。 再度ADF検定 変換後のデータで再度ADF検定を実施した結果を見てみましょう。 # 対数変換 Passengers_log_c = np.log(time_proc["Close"]) Passengers_log_c.plot(figsize=[20,5]); # 季節差分変換 Passengers_log_sdiff_c = Passengers_log_c.diff(periods = 12) # 季節:periods = 12 Passengers_log_sdiff_c.plot(figsize=[20,5]); # ラグ1の差分を取る Passengers_sdiff2_c = Passengers_log_sdiff_c.diff() Passengers_sdiff2_c.plot(figsize=[20,5]); # 変換済みデータの欠損の削除 Passengers_sdiff2_c2 = Passengers_sdiff2_c.dropna() # 成分分解(季節調整) res = sm.tsa.seasonal_decompose(Passengers_sdiff2_c2, period=100) fig = res.plot() plt.show() # 原系列のp値を算出 adf_test(series=Passengers_sdiff2_c2) # 結果:p値 = 0.534 変換前のp値は「0.815」でしたが、変換後は「0.534」と下り、定常過程に近づいたことがわかります。 ただし、有意水準(0.05以下)には程遠く、まだこのままでは解析が行えません。 その為、定常性となりえなかった要因を、更に取り除いていく必要があります。 そのために、まずはなぜ定常性がないのかを考察する必要があります。 また、今回は特徴量が非常にシンプルなデータセットを用いていますが、本来はそれ以外の特徴量を追加・生成するなどの対応も必要と思われます。 もしくは「定常・否定常」に左右されない分析手法を採用する、などの代替案を検討してもよいでしょう。 時系列データにおける特徴量作成 今回は参考程度ですが、時系列データでは下記の特徴量(生成)が有効となるケースがあります。 月、曜日、週末フラグ、祝日、休日、コロナ患者数、などなど。 まとめ 今回は時系列データ分析について、基本的な事項を解説してきました。 ただし今回用いたデータセットの情報量(特徴量)が少なく、もう少しバリエーションのあるデータで特徴量生成や分析を行っても良いと思いました。 また別途、時系列モデリングとそのアルゴリズムについて解説したいと思います。 参考文献 東京大学 数理・情報教育研究センター - 時系列データ解析 東京⼤学 - 数理⼿法VII(時系列解析) 学習院大学 - 時系列解析入門 大阪大学 - 時系列分析 解析結果 実装結果:github/chronological_order_analysis.ipynb データセット:Tesla Stock Price - kaggle 参考資料
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

pandasでクリックできるリンクを表示する

結論 次のようにして、指定した列をクリックできるリンクとして表示することができます。 df = pd.DataFrame({'name': ['google', 'pandas'], 'url':['https://www.google.com/?hl=ja', 'https://pandas.pydata.org/']}) def make_clickable(val): return '<a href="{}" target="_blank">{}</a>'.format(val,val) df.style.format(formatter={'url': make_clickable}) 詳細 Colabなどnotebook環境で、URLを含むDataFrameを表示しようとすると、以下のようになると思います。 できればこのURLをクリックして開けるようにしたいです。 stackoverflowに回答がありました。 次のコードは、上のstackoverflowからの引用です。 df = pd.DataFrame(['http://google.com', 'http://duckduckgo.com']) def make_clickable(val): return '<a href="{}">{}</a>'.format(val,val) df.style.format(make_clickable) このコードを実行すると、次のように表示されます。 クリックできるように見えますが、Colab上ではクリックしても反応しませんでした。(httpのせい?) httpをhttpsに修正すると、クリックできるようになりましたが、出力セル内でページを開いてしまいました。 新しいタブでページを開く そこで、関数make_clickableで返しているaタグにtarget="_blank"の属性を追加してあげます。 これで新しいタブでページを開くことができます。 def make_clickable(val): return '<a href="{}" target="_blank">{}</a>'.format(val,val) 列を指定する 次の例は複数の列を持つDataFrameの場合です。 同じ方法では、全ての列がクリックできるようになってしまいます。 適用したい列はDataFrame.style.formatの引数formatterで指定できます。 辞書形式で列と関数を指定します。 df.style.format(formatter={'url': make_clickable}) おわりに DataFrame.style.formatでフォーマットを指定すると、デフォルトのフォーマットが崩れてしまうため、ここまでの例のように見た目が変わってしまいます。 必要に応じて、以下のドキュメントを参考にフォーマットを調整することができます。 また、今回の内容をまとめたColabを参考として添付しておきます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【環境構築】32bit C++からPythonを利用する(VisualStudio)

はじめに 本記事では、Boostライブラリを利用し、C++からPythonを呼び出す方法をまとめる x86 ビルド(32bit)での環境構築に限定し、x64 ビルド(64bit)の解説は行わない 64bit版は、こちらの記事へ。 32bit版での環境構築の際は、64bit版Python, Anacondaを全て削除することをおすすめします。(ハマるポイントが少なくなります) 何を実現したいのか? VisualStudioのC++プロジェクトから、Pythonクラスを利用したい 速度の求められる処理をC++で実行し、一部の複雑な処理はPythonを利用したい 32bitマシンを制御する必要がある(シーケンサ、マイコン、ロボットなど) 32bit 環境のセットアップ 環境構築の流れは64bitと同じだが、少々クセがあるため以下で説明する。 1. Pythonインストール Python本家から、Python 3.8(32bit) をインストールする Anaconda は使用しない方が良い。 Anacondaではうまくいかないとのコメントがありました。( こちらのリンクを押し、一番下までスクロール) 複数バージョンのPythonを入れると設定がめんどくさくなります。PC内に、32bit版Pythonのみが存在する状態にしておいた方が良いです。 2. Boostセットアップ Boostをダウンロードする(筆者は Boost 1.76.0) 本記事ではCドライブ直下にダウンロード 解凍したBoostのファイル中で、bootstrap.batを実行する cd C:\boost_1_76_0 #解凍したフォルダパスへGO C:\boost_1_76_0>bootstrap.bat 実行後、project-config.jamというフォルダが出来上がっているので、このファイルを変更する これが、Boostのビルドオプションの設定ファイル project-config.jam # Boost.Build Configuration # Automatically generated by bootstrap.bat import option ; using msvc ; option.set keep-going : false ; using python : 3.8 : C:/Users/Tanaka.Taro/AppData/Local/Programs/Python/Python38-32 : C:/Users/Tanaka.Taro/AppData/Local/Programs/Python/Python38-32/include : C:/Users/Tanaka.Taro/AppData/Local/Programs/Python/Python38-32/libs : ; ポイント:自分の環境に合わせ、下記のように設定する using python : [Pythonのバージョン] : [Pythonのルートディレクトリ] : [Pythonのルートディレクトリ]/include : [Pythonのルートディレクトリ]/libs : ; ↑上記のコードの空白文字(スペース)は省略しないこと!(libs : ; などの箇所です) project-config.jam の設定後、b2.exeを実行する コマンドプロントで下記のコマンドを実行 cmd b2.exe toolset=msvc-14.2 link=static,shared runtime-link=static,shared address-model=32 -j12 install --build-dir=C:\boost_1_76_0\build\x86 --includedir=C:\boost_1_76_0\include --libdir=C:\boost_1_76_0\stage\lib\x86 b2.exe コンパイルオプションの解説 toolset= "コンパイラを指定" runtime-link="Visual Studioで使用する際のBoostライブラリのリンク形式" Boostライブラリ使用時に下記の違いがある runtime-link=static : Visual Studioで(MT or MTd)を指定する runtime-link=shared : Visual Studioで(MD or MDd)を指定する 注意: sharedの場合、実行時に.libと同名のdllを置かなければならない address-model="ビット数: 32 or 64 を指定" -j12: "ビルド時の並列スレッド数。(数値を大きくするほどコンパイルが早くなるが、いずれ頭打ちになる) --build-dir="ビルド後のBoost実行ファイルが生成される場所" --includedir="ビルド後のBoostライブラリのヘッダファイルが生成される場所" --libdir="ビルド後のBoostライブラリのライブラリファイルが生成される場所" コマンド中に --with-python オプションはつけない(すべてのパッケージをインストールする) --with-python: "Boost提供ライブラリのうち、Pythonのみ利用しますよ。という意味" ポイント msvc-14.2 の数値は、自身のVisualStudioのバージョンによって変えなければならない 確認方法 VisualStudioを開く > プロジェクト > プロパティ > プラットフォームツールセット b2.exeの実行オプションは、こちらの記事 にまとまっているので、一読すること 3. Visual Studio のセットアップ Boostのビルドが完了したら、次はVisualStudioでBoostPythonを利用するための設定を行う 今回は32bitビルドなので、 ビルドプラットフォームを x86 にすること。 プロジェクトのプロパティを設定する C/C++ --> 全般 --> 追加のインクルードディレクトリ C:\boost_1_76_0\include\boost-1_76 [Pythonのルートディレクトリ]/include リンカー --> 全般 --> 追加のライブラリディレクトリ C:\boost_1_76_0\stage\lib\x86 [Pythonのルートディレクトリ]/libs PCの環境変数の設定 筆者の環境では特にいじる必要がなかったが、もしうまくいかない場合は環境変数を確認するとよいかも PCに複数のPythonバージョンがインストールされていたり、Anacondaを利用している場合、環境変数あたりで苦戦するはず。 C++からPythonを利用する Boost Python 使用時の作法 プログラムを作成する際に気を付ける点をまとめる プログラムの初めに Py_Initialize(); を、 プログラムの終わりにPy_Finalize(); (必須ではない)をつけること Boost::numpy を使用する際は、np::initialize(); もつけること プログラム全体を try-catchで囲み、キャッチした例外をPrint()できるようにすること main.py int main() { //名前空間 namespace py = boost::python; try { //初期化 Py_Initialize(); // Do something with Python ! // } catch (py::error_already_set& e) { // Pythonの例外を標準出力にPrintします PyErr_Print(); } Py_Finalize(); } ※ 補足 Python から 返り値で渡されたnumpy配列の値をC++で取得しようとすると変な値になった。要確認。 BoostPythonで実行中、Pythonのカレントディレクトリは、C++ソースファイルの場所になることに注意。(ファイルパスが通らない場合確認のこと) 参考記事 C++ で Python を実行 on Visual Studio 2017 Anaconda仮想環境について BootStrapインストール方法 Python 32bit と64bit の話
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

コンピュータとオセロ対戦15 ~強化学習~

前回 今回の目標 今までこのシリーズで「カスタムスコア」と呼んでいたものですが、調べたところこの概念は既にあり、一般に「評価値」と呼ぶようです。なので以降の記事では、これまで「カスタムスコア」と呼んでいたものを「評価値」と呼び改めます。 さて、この評価値ですが、ネットで調べたところ有用とされるパターンがすでにいくつかあるようです。しかし私の感覚とは少し違う気がするので、いくつかの記事を使って評価値の検証をしていきたいと思います。 今回は前回作成したプログラムを改造して遺伝的アルゴリズム(っぽいこと)を行い、最適な評価値を探し出します。 ここから本編 今回のアルゴリズムは以下に示すとおりです。 評価値をランダムで初期化したものを30通り作る。 それぞれの評価値を用いて1handでランダムに打ち返すプログラムと対戦する。 勝利した際の評価値を次代につなぐ。 なお、5%の確率で突然変異を起こさせます。 遺伝的アルゴリズム(っぽいこと)に使用したプログラム ヘッダファイル publicにread_goal、mode、そしてplayerとcomputerを追加しました。 それぞれ「何手先まで読むか」「学習モードか、そうでないか」「プレイヤーとコンピュータがどちらのターン(黒ないし白)なのか」を指定します。 evaが評価値です。具体的な値はコンストラクタによって指定されます。 oesro_genetic.h #ifndef osero_genetic_h #define osero_genetic_h #include <stdio.h> #include <stdlib.h> #include <time.h> const bool BLACK = false; const bool WHITE = true; typedef unsigned long BOARD; enum class TURN{ black, white }; enum class PLAY_WAY{ nhand, random, sentinel }; class osero_genetic{ private: // other const int SIZE = 8; bool turn = BLACK; int bmethod, wmethod; void (osero_genetic:: * play_method[static_cast<int>(PLAY_WAY::sentinel)]) (int *, int *) = { &osero_genetic::nhand, &osero_genetic::random }; // board BOARD bw[2]; double eva[2][64]; // function bool check(BOARD * now, int line, int col); bool check_all(void); bool count_last(void); void put(BOARD * now, int line, int col, bool turn); void printb(void); void nhand(int * line, int * col); void random(int * line, int * col); int popcount(BOARD now); double count(BOARD * now); double board_add(BOARD * now, int num, bool turn); public: osero_genetic(int player_b, int player_w); osero_genetic(int player_b, double * eva, int player_w); osero_genetic(int player_b, int player_w, double * eva); osero_genetic(int player_b, double * eva1, int player_w, double * eva2); ~osero_genetic(); int read_goal = 1; int mode = 0; int player, computer; bool print = false; bool play(void); }; #endif ソースファイル count関数以外は今までのプログラムのつぎはぎなので詳細は省きます。 また、play関数を呼ぶことで、実行ファイルで指定した条件での試合が一回実行されます。 osero_genetic.cpp double osero_genetic::count(BOARD * now){ int my = static_cast<int>(this -> turn); int opp = static_cast<int>(!(this -> turn)); int i = 0; double score = 0; double * myeva = this -> eva[my]; BOARD place = 1; while (place){ if (now[my] & place) score += myeva[i]; else if (now[opp] & place) score -= myeva[i]; i++; place = place << 1; } return score; } 実行ファイル epocが一世代の数、entireが進化回x数です。 30x1000で三万試合行います。 ここで、具体的なアルゴリズムについて説明します。用語や詳しい説明はその下に書きます。 評価値をランダムで初期化したものを30通り作る(なお、数字はすべて-1~1の実数です。その範囲にした理由はプログラムが作りやすかったというだけです) クラスからインスタンスを作る。今回は黒がコンピューターで白がプレイヤー(役)です。黒は評価値に従った1hand、白はランダムで打ち返します。 read_goalを設定し、試合を行い、勝敗を記録する。 勝利した場合、その時の評価値をnew_evaに保存し、また勝利回数(win_sum)を更新します。 30試合終了後、5%の確率で評価値を初期化し、95%の確率で勝利した際に使っていた評価値を、ランダムで次の評価値に分配します。 1000回進化後、評価値をファイル出力し終了。 「評価値に従った1hand」について説明します。これまでの1handは「このターンで最もひっくりかえせる場所を選ぶ」という思考方法でしたが、これを「評価値が盤面全て1の状態で、次のターンで自分の評価値が最も高くなる場所を選ぶ」と解釈し、これを「評価値に従った1hand」と呼ぶことにします。 この評価値をいじることで思考方法自体は変えずとも強くなれるのではと考えました。 また、なぜこんな進化方法をとるのか説明します。 遺伝的アルゴリズムの例題としてよく上げられるナップザック問題では0か1で表現された遺伝子を交叉によって進化させるのが一般的です。これは各箇所の遺伝子情報よりも、0と1の並び方が重要だからなのではないかと考えました。 オセロでは評価値の並び方よりも、各箇所の具体的な評価値のほうが重要です。そのため、勝利した際に使っていた評価値の、いくつか存在する「右上の数字」からランダムで、新しい評価値の右上にその値をコピーする、右上以外も同様、という手法をとりました。一世代で30試合もするので、何もしなくても平均15試合は勝ちます。 以下は使用したプログラムです。 run_1hand_rand.cpp #include "osero_genetic.h" const int epoc = 30; const int entire = 1000; int main(void){ int win, win_sum; int i, j, k; double eva[epoc][64], new_eva[epoc][64]; osero_genetic * run; FILE * fp = fopen("data_1hand_rand.csv", "w"); printf("progress "); for (i = 0; i < epoc; i++) printf("."); printf("\n"); fprintf(fp, "num,win_per\n"); // srand(static_cast<unsigned int>(time(NULL))); srand(0); for (i = 0; i < epoc; i++) for (j = 0; j < 64; j++) eva[i][j] = static_cast<double>(rand()) / (RAND_MAX >> 1) - 1; for (i = 0; i < entire; i++){ printf("%3d/%3d: ", i + 1, entire); win_sum = 0; for (j = 0; j < epoc; j++){ printf("."); run = new osero_genetic( static_cast<int>(PLAY_WAY::nhand), eva[j], static_cast<int>(PLAY_WAY::random) ); run -> read_goal = 1; win = static_cast<int>(run -> play()); if (win) { for (k = 0; k < 64; k++) new_eva[win_sum][k] = eva[j][k]; win_sum++; } delete run; } if (win_sum){ for (j = 0; j < epoc; j++){ for (k = 0; k < 64; k++){ if (rand() % 100 > 5) eva[j][k] = new_eva[rand() % win_sum][k]; else eva[j][k] = static_cast<double>(rand()) / (RAND_MAX >> 1) - 1; } } } printf("\n"); fprintf(fp, "%d,%d\n", i, win_sum); } fclose(fp); fp = fopen("eva_1hand_rand.csv", "w"); for (i = 0; i < epoc; i++){ for (j = 0; j < 64; j++){ fprintf(fp, "%2.4f", eva[i][63 - j]); if ((j + 1) % 8 == 0) fprintf(fp, "\n"); else fprintf(fp, ","); } fprintf(fp, "\n"); } fclose(fp); return 0; } 実行結果 勝利した回数の世代ごとの記録をpythonで見てみました。 data.py import pandas as pd import matplotlib.pyplot as plt df = pd.read_csv("data_1hand_rand.csv") x = df["num"] y = df["win_per"] fig = plt.figure() plt.plot(x, y) plt.show() plt.clf() plt.close() 上のプログラムの実行結果が下の写真です。 一切勝率上がってませんでした。 学習後の評価値も以下の通り。なお、実際には30通り出力されますが、すべては載せきれないため三つだけ表示します。また、色がついているのはExcelの機能を使っています。 初期値かな? と思うくらいにランダムな値が入っているだけでした。 改善1 run_2hand_rand 1handでダメなら2handを試します。 実行に時間がかかったため100世代しかしておりません。 ほとんど学習しておりませんでした。 改善案2 run_1hand_1hand では、コンピュータが評価値に従った1hand、プレイヤーが1handで打ち合った場合どうなるかを試しました。 勝率は向上しています。 一方の評価値は・・・ ほとんど学習しておりません。 フルバージョン 実際にやってみた やってません。 次回は 遺伝的アルゴリズムの改良または今回の考察を行いたいと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

コンピュータとオセロ対戦15 ~遺伝的アルゴリズム~

前回 今回の目標 今までこのシリーズで「カスタムスコア」と呼んでいたものですが、調べたところこの概念は既にあり、一般に「評価値」と呼ぶようです。なので以降の記事では、これまで「カスタムスコア」と呼んでいたものを「評価値」と呼び改めます。 さて、この評価値ですが、ネットで調べたところ有用とされるパターンがすでにいくつかあるようです。しかし私の感覚とは少し違う気がするので、いくつかの記事を使って評価値の検証をしていきたいと思います。 今回は前回作成したプログラムを改造して遺伝的アルゴリズム(っぽいこと)を行い、最適な評価値を探し出します。 ここから本編 今回のアルゴリズムは以下に示すとおりです。 評価値をランダムで初期化したものを30通り作る。 それぞれの評価値を用いて1handでランダムに打ち返すプログラムと対戦する。 勝利した際の評価値を次代につなぐ。 なお、5%の確率で突然変異を起こさせます。 遺伝的アルゴリズム(っぽいこと)に使用したプログラム ヘッダファイル publicにread_goal、mode、そしてplayerとcomputerを追加しました。 それぞれ「何手先まで読むか」「学習モードか、そうでないか」「プレイヤーとコンピュータがどちらのターン(黒ないし白)なのか」を指定します。 evaが評価値です。具体的な値はコンストラクタによって指定されます。 oesro_genetic.h #ifndef osero_genetic_h #define osero_genetic_h #include <stdio.h> #include <stdlib.h> #include <time.h> const bool BLACK = false; const bool WHITE = true; typedef unsigned long BOARD; enum class TURN{ black, white }; enum class PLAY_WAY{ nhand, random, sentinel }; class osero_genetic{ private: // other const int SIZE = 8; bool turn = BLACK; int bmethod, wmethod; void (osero_genetic:: * play_method[static_cast<int>(PLAY_WAY::sentinel)]) (int *, int *) = { &osero_genetic::nhand, &osero_genetic::random }; // board BOARD bw[2]; double eva[2][64]; // function bool check(BOARD * now, int line, int col); bool check_all(void); bool count_last(void); void put(BOARD * now, int line, int col, bool turn); void printb(void); void nhand(int * line, int * col); void random(int * line, int * col); int popcount(BOARD now); double count(BOARD * now); double board_add(BOARD * now, int num, bool turn); public: osero_genetic(int player_b, int player_w); osero_genetic(int player_b, double * eva, int player_w); osero_genetic(int player_b, int player_w, double * eva); osero_genetic(int player_b, double * eva1, int player_w, double * eva2); ~osero_genetic(); int read_goal = 1; int mode = 0; int player, computer; bool print = false; bool play(void); }; #endif ソースファイル count関数以外は今までのプログラムのつぎはぎなので詳細は省きます。 また、play関数を呼ぶことで、実行ファイルで指定した条件での試合が一回実行されます。 osero_genetic.cpp double osero_genetic::count(BOARD * now){ int my = static_cast<int>(this -> turn); int opp = static_cast<int>(!(this -> turn)); int i = 0; double score = 0; double * myeva = this -> eva[my]; BOARD place = 1; while (place){ if (now[my] & place) score += myeva[i]; else if (now[opp] & place) score -= myeva[i]; i++; place = place << 1; } return score; } 実行ファイル epocが一世代の数、entireが進化回数です。 30x1000で三万試合行います。 ここで、具体的なアルゴリズムについて説明します。用語や詳しい説明はその下に書きます。 評価値をランダムで初期化したものを30通り作る(なお、数字はすべて-1~1の実数です。その範囲にした理由はプログラムが作りやすかったというだけです) クラスからインスタンスを作る。今回は黒がコンピューターで白がプレイヤー(役)です。黒は評価値に従った1hand、白はランダムで打ち返します。 read_goalを設定し、試合を行い、勝敗を記録する。 勝利した場合、その時の評価値をnew_evaに保存し、また勝利回数(win_sum)を更新します。 30試合終了後、5%の確率で評価値を初期化し、95%の確率で勝利した際に使っていた評価値を、ランダムで次の評価値に分配します。 1000回進化後、評価値をファイル出力し終了。 「評価値に従った1hand」について説明します。これまでの1handは「このターンで最もひっくりかえせる場所を選ぶ」という思考方法でしたが、これを「評価値が盤面全て1の状態で、次のターンで自分の評価値が最も高くなる場所を選ぶ」と解釈し、これを「評価値に従った1hand」と呼ぶことにします。 この評価値をいじることで思考方法自体は変えずとも強くなれるのではと考えました。 また、なぜこんな進化方法をとるのか説明します。 遺伝的アルゴリズムの例題としてよく挙げられるナップザック問題では0か1で表現された遺伝子を交叉によって進化させるのが一般的です。これは各箇所の遺伝子情報よりも、0と1の並び方が重要だからなのではないかと考えました。 オセロでは評価値の並び方よりも、各箇所の具体的な評価値のほうが重要です。そのため、勝利した際に使っていた評価値の、いくつか存在する「右上の数字」からランダムで、新しい評価値の右上にその値をコピーする、右上以外も同様、という手法をとりました。一世代で30試合もするので、何もしなくても平均15試合は勝ちます。 (追記) 上の進化方法の説明が分かりづらいと思うので、順序だてて説明しなおします。 30通りの評価値がある。それぞれの評価値は8x8の盤面に対応すべく64個の、範囲-1~1のdouble型実数を持つ。64個の実数一つ一つが盤面の中の64か所ある位置に対応する。 それぞれの評価値に従って、30回ランダムとの試合を行う。 勝ったパターンの評価値のみ取り出す。例えば10回勝った場合、64x10で640個のdouble型実数が保存される。配列としては10個である。 新しい評価値は30個の配列である。この30個の配列の0番目には、保存しておいた10個の配列の、10通りある0番目の中からランダムで選ぶ。 1~63番目についても同様。また、5%の確率で初期化(乱数を入れること)をする。 以下は使用したプログラムです。 run_1hand_rand.cpp #include "osero_genetic.h" const int epoc = 30; const int entire = 1000; int main(void){ int win, win_sum; int i, j, k; double eva[epoc][64], new_eva[epoc][64]; osero_genetic * run; FILE * fp = fopen("data_1hand_rand.csv", "w"); printf("progress "); for (i = 0; i < epoc; i++) printf("."); printf("\n"); fprintf(fp, "num,win_per\n"); // srand(static_cast<unsigned int>(time(NULL))); srand(0); for (i = 0; i < epoc; i++) for (j = 0; j < 64; j++) eva[i][j] = static_cast<double>(rand()) / (RAND_MAX >> 1) - 1; for (i = 0; i < entire; i++){ printf("%3d/%3d: ", i + 1, entire); win_sum = 0; for (j = 0; j < epoc; j++){ printf("."); run = new osero_genetic( static_cast<int>(PLAY_WAY::nhand), eva[j], static_cast<int>(PLAY_WAY::random) ); run -> read_goal = 1; win = static_cast<int>(run -> play()); if (win) { for (k = 0; k < 64; k++) new_eva[win_sum][k] = eva[j][k]; win_sum++; } delete run; } if (win_sum){ for (j = 0; j < epoc; j++){ for (k = 0; k < 64; k++){ if (rand() % 100 > 5) eva[j][k] = new_eva[rand() % win_sum][k]; else eva[j][k] = static_cast<double>(rand()) / (RAND_MAX >> 1) - 1; } } } printf("\n"); fprintf(fp, "%d,%d\n", i, win_sum); } fclose(fp); fp = fopen("eva_1hand_rand.csv", "w"); for (i = 0; i < epoc; i++){ for (j = 0; j < 64; j++){ fprintf(fp, "%2.4f", eva[i][63 - j]); if ((j + 1) % 8 == 0) fprintf(fp, "\n"); else fprintf(fp, ","); } fprintf(fp, "\n"); } fclose(fp); return 0; } 実行結果 勝利した回数の世代ごとの記録をpythonで見てみました。 data.py import pandas as pd import matplotlib.pyplot as plt df = pd.read_csv("data_1hand_rand.csv") x = df["num"] y = df["win_per"] fig = plt.figure() plt.plot(x, y) plt.show() plt.clf() plt.close() 上のプログラムの実行結果が下の写真です。 一切勝率上がってませんでした。 学習後の評価値も以下の通り。なお、実際には30通り出力されますが、すべては載せきれないため三つだけ表示します。また、色がついているのはExcelの機能を使っています。 初期値かな? と思うくらいにランダムな値が入っているだけでした。 改善1 run_2hand_rand 1handでダメなら2handを試します。 実行に時間がかかったため100世代しかしておりません。 ほとんど学習しておりませんでした。 改善案2 run_1hand_1hand では、コンピュータが評価値に従った1hand、プレイヤーが1handで打ち合った場合どうなるかを試しました。 勝率は向上しています。 一方の評価値は・・・ ほとんど学習しておりません。 フルバージョン geneticフォルダに入っています。 実際にやってみた やってません。 次回は 遺伝的アルゴリズムの改良または今回の考察を行いたいと思います。 次回
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【AtCoder】ABC221をPython3で解説(ABCD

ABCのA-D問題の解説。 目次 A - Seismic magnitude scales B - typo C - Select Mul D - Online games A - Seismic magnitude scales 解説 マグニチュードAの地震のエネルギーの大きさはマグニチュードBの自身のエネルギーの大きさの何倍かを求める問題。 このときAはB以上であることがわかっているので、AからBを引いてあげた値分、32倍してあげるとよい。 コード def main(): a, b = map(int, input().split()) print(32**(a-b)) if __name__ == '__main__': main() B - typo 解説 文字列SとTが与えられ、Sの隣り合う2文字を入れ替える操作を1回以下行うことで、Tと一致するか求める問題。 まず、そもそもS == Tであれば、入れ替える必要がないため、この時点で答えはYesとなる。 次に、入れ替える操作であるが、連続する2文字をいれかえればいいだけなので、S[i], S[i+1] = S[i+1], S[i]とし、値を入れ替えて判定する。SとTが合わなければ、また文字列をもとに戻し、文字列の長さまでこの操作を繰り返す。 コード def main(): S = list(input()) T = list(input()) ans = 'No' if S == T: ans = 'Yes' for i in range(len(S)-1): S[i], S[i+1] = S[i+1], S[i] if S == T: ans = 'Yes' S[i], S[i+1] = S[i+1], S[i] print(ans) if __name__ == '__main__': main() C - Select Mul 解説 入力した数字の文字列の中から、値を2つにわけ、その積の最大値を求める問題。 まず、積の最大値を求めたいので、入力した数字を降順に並び替える。さらに、積の最大値をとるためには、それぞれの桁数を最大にする必要がある。例えば、$1234$という数字があれば、$1 \times 234$よりも$12 \times 34$のほうが大きいことから明白である。 したがって、先程降順にソートした数字を1つずつ順番に、新たに2つの数字として振り分ける。 振り分けられた2つ数字の積では、まだ最大値とはえいえない。このとき桁数の少ない方の数字をできるだけ最大化してあげることで、積の最大値を求めることができる。つまり、2つの数字を桁数が高い方から順番に見ていき、その桁の値が異なる場合、値を入れ替えてあげることで、積の最大値となる2つの数字を求めることができる。 コード1(for) def main(): n = input() nsort = sorted(n, reverse=True) # 降順に並び替え a = nsort[0::2] # インデックス0から1つ飛ばしでリストわけ b = nsort[1::2] # インデックス1から1つ飛ばしでリストわけ for i in range(min(len(a), len(b))): if a[i] != b[i]: a[i], b[i] = b[i], a[i] break print(int(''.join(a)) * int(''.join(b))) if __name__ == '__main__': main() コード2(bit全探索) def main(): n = input() nsort = sorted(n, reverse=True) ans = 0 for i in range(1 << len(nsort)): l = 0 r = 0 for j in range(len(nsort)): if i & (1 << j): l = l * 10 + ord(nsort[j]) - ord('0') else: r = r * 10 + ord(nsort[j]) - ord('0') ans = max(ans, l * r) print(ans) if __name__ == '__main__': main() D - Online games 解説 i番目のプレイヤーは、$A_i$日目から$B_i$日間連続でログインしている。このとき、1日目からN日目までのなかで、ちょうどk人がログインしていた日数を求める問題。 これは、ログインした日とログアウトした日に注目すると問題がとける。 まず、入力値であるaとbについて、i番目の人がログインした日を$a$で日数を+1、ログアウトした日を$a+b$で日数を-1とし、タプルとして管理する。 これら入力した値を昇順にソートしてあげることで、ログイン・ログアウトの情報が日付順になっているため、一つの配列で管理することができる。 この配列内のタプルを前から一つずつ見ていく。 まず、その日は、ログインしたのか、ログアウトしたのかを確認する。つまり、配列内のタプルの2番目の要素が+1か-1かを考える。これをある変数(cnt)で管理してあげることで、その日は何人がログインしているかを把握することができる。 このcntは、次の日、つまり、配列内の次のタプルの1番目の値までの日数続くので、差分を計算して、事前に用意しておいた各日の人数を保存する配列ansのcnt番目に値をいれてあげるとよい。このときansは、Nの最大値である、$2 \times 10 ^ 5$よりも多く要素を準備しておく。 これで求めたansを1人からn人まで分出力するとAC。 コード def main(): n = int(input()) x = [] temp = 200010 for _ in range(n): a, b = map(int, input().split()) x.append((a, 1)) x.append((a+b, -1)) xsort = sorted(x) cnt = 0 ans = [0 for _ in range(temp)] for i in range(len(xsort)-1): cnt += xsort[i][1] # cnt人がログイン ans[cnt] += xsort[i+1][0] - xsort[i][0] ans = ans[1:n+1] # 1人からn人までの答えを出力 print(*ans) if __name__ == '__main__': main() 編集後記 復習サボったらいけないですね...。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Pythonのデータ検証ライブラリCerberusを使ってみよう

はじめに この資料はデータ検証用ライブラリ Cerberus のドキュメントを抄訳したものです。 コミュニティーで Hands-On を行うときの資料として作成したため、 Cerberus の変更履歴などについては触れていません。 また、ソースコードの例示と実行には、IPython を使っています。 $ ipython Python 3.9.7 | packaged by conda-forge | (default, Sep 29 2021, 19:23:19) Type 'copyright', 'credits' or 'license' for more information IPython 7.28.0 -- An enhanced Interactive Python. Type '?' for help. In [1]: IPython の %load コマンドでソースコードを読み込んで実行させています。 実行した結果と期待する出力を assert 文を使って記述しています。 実行してみて欲しいコードがあるときは末尾部分にコメントで記述しています。 このときにキー入力がすくなくなるような変数にしています。 サンプルコードは github で公開していますので、自習などでご利用ください。 Cerberus について Cerberusは、Pythonで実装されたデータ検証ライブラリです。Cerberusは、パワフルでありながらシンプルで軽量なデータ検証機能を提供しています。また、カスタム検証を可能にする拡張性の高い設計になっています。 ケルベロス - Wikipedia より引用 ギリシャ神話に登場する冥府の入り口を守護する番犬のこと。 Cerberus はラテン語表記で、Kérberos はギリシャ語表記です。 ラテン語読みはケルベルス、英語読みはサーベラス。 インストール cerberus は pip コマンドでインストールすることができます。 bash $ pip install cerberus cerberus には依存関係のある外部モジュールはありません。 基本的な使用方法 細かなことは後回しにして、まずどんな具合にデータ検証を行うのかを見てみましょう。 まず。検証スキーマを定義し、それをValidatorクラスに渡してインスタンスを生成します。 検証したいデータをValidatorクラスのvalidate()メソッドに渡すとブール値が返ってきます。 In [2]: # %load 01_validate.py ...: from cerberus import Validator ...: ...: schema = {'name': {'type': 'string'}} ...: document = {'name': 'Jack Bauer'} ...: ...: v = Validator(schema) ...: check = v.validate(document) ...: assert check == True ...: ...: # v ...: In [3]: v Out[3]: <cerberus.validator.Validator at 0x10707ee50> validate()メソッドにデータとスキーマを与えて検証することもできます。 In [2]: # %load 02_validate_alternate.py ...: from cerberus import Validator ...: ...: schema = {'name': {'type': 'string'}} ...: document = {'name': 'Jack Bauer'} ...: ...: v = Validator() ...: check = v.validate(document, schema) ...: assert check == True ...: ...: # v.types ...: In [3]: これは、インスタンスの使用期間中にスキーマが変更される場合に便利です。 検証スキーマの詳細については後述しますが、データのキーがどのようなルールに従うのかを定義した辞書です。ルールに指定できる型はtypesプロパティーで参照できるものが使用できます。 In [3]: v.types Out[3]: ('binary', 'boolean', 'container', 'date', 'datetime', 'dict', 'float', 'integer', 'list', 'number', 'set', 'string') In [4]: 他の検証ツールとは異なり、Cerberusは検証を行って問題があるときに停止したり、例外を発生させたりはしません。ドキュメント全体が常に処理され、検証に失敗した場合は False が返されます。その後、errors プロパティにアクセスして、エラーの原因のリストを取得することができます。検証に問題がなく成功したときはTrueが返され、errorsは空の辞書がセットされます。 In [2]: # %load 03_validate_error.py ...: from cerberus import Validator ...: ...: schema = {'name': {'type': 'string'}} ...: document = {'name': 12345 } ...: ...: v = Validator() ...: check = v.validate(document, schema) ...: ...: assert check == False ...: assert v.errors == {'name': ['must be of string type']} ...: In [3]: スキーマで定義するルールで制限を与えることもできます。これも詳しくは後述します。 次の例では、ageの値は 10 以上でないと検証が失敗します。 In [2]: # %load 04_validate_complex.py ...: from cerberus import Validator ...: ...: schema = {'name': {'type': 'string'}, ...: 'age': {'type': 'integer', 'min': 10}} ...: document = {'name': 'Little Joe', 'age': 5} ...: ...: v = Validator() ...: c = (v.validate(document, schema), v.errors) ...: ...: assert c[0] == False ...: assert c[1] == {'age': ['min value is 10']} ...: ...: # v.validation_rules ...: In [3]: ルールは validation_rules プロパティーのものが使用できます。 In [3]: v.validation_rules Out[3]: {'allof': {'type': 'list', 'logical': 'allof'}, 'allow_unknown': {'oneof': [{'type': 'boolean'}, {'type': ['dict', 'string'], 'check_with': 'bulk_schema'}]}, 'allowed': {'type': 'container'}, 'anyof': {'type': 'list', 'logical': 'anyof'}, 'check_with': {'oneof': [{'type': 'callable'}, {'type': 'list', 'schema': {'oneof': [{'type': 'callable'}, {'type': 'string', 'allowed': ()}]}}, {'type': 'string', 'allowed': ()}]}, 'contains': {'empty': False}, 'dependencies': {'type': ('dict', 'hashable', 'list'), 'check_with': 'dependencies'}, 'empty': {'type': 'boolean'}, 'excludes': {'type': ('hashable', 'list'), 'schema': {'type': 'hashable'}}, 'forbidden': {'type': 'list'}, 'items': {'type': 'list', 'check_with': 'items'}, 'keysrules': {'type': ['dict', 'string'], 'check_with': 'bulk_schema', 'forbidden': ['rename', 'rename_handler']}, 'max': {'nullable': False}, 'maxlength': {'type': 'integer'}, 'meta': {}, 'min': {'nullable': False}, 'minlength': {'type': 'integer'}, 'noneof': {'type': 'list', 'logical': 'noneof'}, 'nullable': {'type': 'boolean'}, 'oneof': {'type': 'list', 'logical': 'oneof'}, 'readonly': {'type': 'boolean'}, 'regex': {'type': 'string'}, 'require_all': {'type': 'boolean'}, 'required': {'type': 'boolean'}, 'schema': {'type': ['dict', 'string'], 'anyof': [{'check_with': 'schema'}, {'check_with': 'bulk_schema'}]}, 'type': {'type': ['string', 'list'], 'check_with': 'type'}, 'valuesrules': {'type': ['dict', 'string'], 'check_with': 'bulk_schema', 'forbidden': ['rename', 'rename_handler']}} ドキュメントがマッピングでない場合には DocumentError が発生します。 In [2]: # %load 05_document_error.py ...: from cerberus import Validator, DocumentError ...: ...: schema = {'name': {'type': 'string'}, ...: 'age': {'type': 'integer', 'min': 10}} ...: document = {'name': 'Little Joe', 'age': 5} ...: ...: v = Validator() ...: doc = f'{document}' ...: ...: try: ...: c = (v.validate(doc, schema), v.errors) ...: except DocumentError as e: ...: print(e) ...: '{'name': 'Little Joe', 'age': 5}' is not a document, must be a dict In [3]: `` `Validator`クラスとそのインスタンスは呼び出し可能(Callable)で、次のような短縮構文が可能です。 ```python In [2]: # %load 06_validate_callable.py ...: from cerberus import Validator ...: ...: schema = {'name': {'type': 'string'}, ...: 'age': {'type': 'integer', 'min': 10}} ...: document = {'name': 'David Coverdale', 'age': 70} ...: ...: v = Validator(schema) ...: c = v(document) ...: assert c == True ...: In [3]: スキーマで定義したキーがデータになくてもエラーにはなりませんが。スキーマーで定義していないキーが存在するとエラーになります。 In [2]: # %load 07_validate_keys.py ...: from cerberus import Validator ...: ...: schema = {'name': {'type': 'string'}, ...: 'age': {'type': 'integer', 'min': 10}} ...: doc1 = {'name': 'David Coverdale'} ...: doc2 = {'name': 'David Coverdale', 'country': 'USA'} ...: ...: v = Validator(schema) ...: c1 = (v.validate(doc1), v.errors) ...: assert c1[0] == True ...: assert c1[1] == {} ...: ...: c2 = (v.validate(doc2), v.errors) ...: assert c2[0] == False ...: assert c2[1] == {'country': ['unknown field']} ...: In [3]: 未知なキーを許可する デフォルトでは、スキーマで定義されたキーのみが許可されます。allow_unknownプロパティをTrueにセットすると、 これをスキーマで定義されていないキーも受け入れるようになります。 In [2]: # %load 08_unknown_key.py ...: from cerberus import Validator ...: ...: schema = {'name': {'type': 'string'}, ...: 'age': {'type': 'integer', 'min': 10}} ...: document = {'name': 'David Coverdale', 'country': 'USA'} ...: ...: v = Validator(schema) ...: assert v.allow_unknown == False ...: ...: c1 = (v.validate(document), v.errors) ...: assert c1[0] == False ...: assert c1[1] == {'country': ['unknown field']} ...: ...: v.allow_unknown = True ...: ...: c2 = (v.validate(document), v.errors) ...: assert c2[0] == True ...: In [3]: また、allow_unknown に検証スキーマに設定すると、未知のフィールドはそのスキーマに対して検証されます。 In [2]: # %load 09_unknown_validate.py ...: from cerberus import Validator ...: ...: schema = {'name': {'type': 'string'}, ...: 'age': {'type': 'integer', 'min': 10}} ...: ...: v = Validator(schema) ...: ...: v.schema = {} ...: v.allow_unknown = {'type': 'string'} ...: document = {'an_unknown_field': 'john'} ...: c1 = (v.validate(document), v.errors) ...: assert c1[0] == True ...: ...: document = {'an_unknown_field': 1} ...: c2 = (v.validate(document), v.errors) ...: assert c2[0] == False ...: assert c2[1] == {'an_unknown_field': ['must be of string type']} ...: In [3]: allow_unknownプロパティはいつでも元に戻せます。 In [2]: # %load 10_allow_unknown_reset.py ...: from cerberus import Validator ...: ...: ...: schema = {'name': {'type': 'string'}, ...: 'age': {'type': 'integer', 'min': 10}} ...: document = {'name': 'David Coverdale', 'country': 'USA'} ...: ...: v = Validator(schema) ...: assert v.allow_unknown == False ...: ...: v.allow_unknown = True ...: c1 = (v.validate(document), v.errors) ...: assert c1[0] == True ...: ...: v.allow_unknown = False ...: c2 = (v.validate(document), v.errors) ...: assert c2[0] == False ...: assert c2[1] == {'country': ['unknown field']} ...: In [3]: allow_unknownをルールとして設定することで、スキーマルールと照合される入れ子のマッピングのバリデータを設定することもできます。 In [2]: # %load 11_allow_unknown_rule.py ...: from cerberus import Validator ...: ...: v = Validator() ...: assert v.allow_unknown == False ...: ...: schema = { ...: 'name': {'type': 'string'}, ...: 'a_dict': { ...: 'type': 'dict', ...: 'allow_unknown': True, # 注目:この定義でプロパティを上書きする ...: 'schema': { ...: 'address': {'type': 'string'} ...: } ...: } ...: } ...: ...: document = {'name': 'john', ...: 'a_dict': {'an_unknown_field': 'is allowed'}} ...: c1 = (v.validate(document, schema), v.errors) ...: assert c1[0] == True ...: ...: document = {'name': 'john', ...: 'an_unknown_field': 'is not allowed', ...: 'a_dict': {'an_unknown_field': 'is allowed'}} ...: c2 = (v.validate(document, schema), v.errors) ...: assert c2[0] == False ...: assert c2[1] == {'an_unknown_field': ['unknown field']} ...: In [3]: c1 でのバリデーションでは、スキーマで a_dictキーのデータには allow_unknownがTrueに上書きされるのでエラーにはなりませんが、c2では、スキーマーのallow_unknown プロパティは False のままなので、親ドキュメントの未知なキーはエラになります。 すべてを要求するスキーマ デフォルトでは、スキーマで定義されたすべてのキーは必須ではありません。しかし、バリデータの初期化時に require_all を True に設定するか、require_allプロパティを後からTrueに変更すると、スキーマーで定義されているすべてのキーのペアを要求することができます。 require_all をルールとして設定し、スキーマルールと照合するサブドキュメント用のバリデータを設定することもできます。 In [2]: # %load 12_require_all.py ...: from cerberus import Validator ...: ...: schema = {'name': {'type': 'string'}, ...: 'age': {'type': 'integer', 'min': 10}} ...: document = {'name': 'David Coverdale'} ...: ...: v = Validator(schema) ...: assert v.require_all == False ...: ...: c1 = (v(document), v.errors) ...: assert c1[0] == True ...: ...: v.require_all = True ...: ...: c2 = (v(document), v.errors) ...: assert c2[0] == False ...: assert c2[1] == {'age': ['required field']} ...: In [3]: In [2]: # %load 13_require_all_schema.py ...: from cerberus import Validator ...: ...: v = Validator() ...: assert v.require_all == False ...: ...: schema = { ...: 'name': {'type': 'string'}, ...: 'a_dict': { ...: 'type': 'dict', ...: 'require_all': True, # 注目:この定義でプロパティを上書きする ...: 'schema': { ...: 'address': {'type': 'string'} ...: } ...: } ...: } ...: ...: document = {'name': 'john', 'a_dict': {}} ...: c1 = (v.validate(document, schema), v.errors) ...: assert c1[0] == False ...: assert c1[1] == {'a_dict': [{'address': ['required field']}]} ...: ...: document = {'a_dict': {'address': 'foobar'}} ...: c2 = (v.validate(document, schema), v.errors) ...: assert c2[0] == True ...: In [3]: 処理済ドキュメントの取得 正規化(Normalize)と強制(coerce)は元のドキュメントのコピーに対して実行され、結果のドキュメントは documentプロパティで取得できます。 In [2]: # %load 14_doc_property.py ...: from cerberus import Validator ...: ...: v = Validator() ...: v.schema = {'amount': {'type': 'integer', 'coerce': int}} ...: ...: document = {'amount': 1} ...: c = v.validate(document) ...: assert c == True ...: assert v.document == document ...: In [3]: Validator インスタンスには、documentプロパティーのほかに、 ドキュメントを処理したり処理結果を取得したりするための省略可能なメソッドがあります。 validated() メソッド ラッパーメソッド validated() があり、検証済みのドキュメントを返します。ドキュメントが認証されなかった場合は None を返します。ただし、キーワード引数 always_return_document を True に設定してこのメソッドを呼び出した場合はこの限りではありません。次のような処理をしたいときに便利です。 In [2]: # %load 15_validated.py ...: from cerberus import Validator ...: ...: schema = {'name': {'type': 'string'}, ...: 'age': {'type': 'integer', 'max': 45 }} ...: ...: documents = [ ...: { 'name': 'David', 'age': 70 }, ...: { 'name': 'Brian', 'age': 75 }, ...: { 'name': 'Roger', 'age': 75 }, ...: { 'name': 'Jack', 'age': 51 }, ...: { 'name': 'Anthony', 'age': 29 }, ...: { 'name': 'Chloe', 'age': 28 }, ...: ] ...: ...: v = Validator(schema) ...: valid_docs = [x for x in [v.validated(y) for y in documents] ...: if x is not None] ...: ...: print(valid_docs) ...: [{'name': 'Anthony', 'age': 29}, {'name': 'Chloe', 'age': 28}] In [3]: 強制(coerce)させる呼び出し可能(Callable)オブジェクトまたはメソッドが例外を発生させた場合、その例外はキャッチされ、検証は失敗することに注意してください。 normalized ()メソッド normalized()メソッドは、検証を行わずにドキュメントの正規化されたコピーを返します。 In [2]: # %load 16_normalized.py ...: from cerberus import Validator ...: ...: schema = {'amount': {'coerce': int}} ...: document = {'model': 'consumerism', 'amount': '1'} ...: ...: v = Validator() ...: normalized_document = v.normalized(document, schema) ...: assert normalized_document == {'model': 'consumerism', 'amount': 1} ...: assert type(normalized_document['amount']) == int ...: In [3]: 警告(Warnings) 非推奨事項やトラブルの原因となりそうなものなどの警告(warning)は、Python標準ライブラリのwarningsモジュールを通じて発行されます。logging モジュールの logging.captureWarnings() を使って、これらの警告をキャッチするように設定することができます。 検証スキーマ 検証スキーマとはマッピングのことで、通常は辞書になります。スキーマのキーは、ターゲット辞書で許可されるキーです。スキーマの値は、対応するターゲットの値と一致しなければならないルールを表します。 In [2]: # %load 20_schema.py ...: from cerberus import Validator ...: ...: schema = {'name': {'type': 'string', 'maxlength': 10}} ...: ...: documents = [ ...: {'name': 'Jack Bauer'}, ...: {'name': 'David Coverdale'}, ...: {'name': 77}, ...: ] ...: ...: v = Validator(schema) ...: ...: valid_docs = [x for x in [v.validated(y) for y in documents] ...: if x is not None] ...: ...: c1 = (v.validate(documents[0]), v.errors) ...: assert c1[0] == True ...: ...: c2 = (v.validate(documents[1]), v.errors) ...: assert c2[0] == False ...: assert c2[1] == {'name': ['max length is 10']} ...: ...: c3 = (v.validate(documents[2]), v.errors) ...: assert c3[0] == False ...: assert c3[1] == {'name': ['must be of string type']} ...: ...: print(f'Valid_docs: {valid_docs}') ...: Valid_docs: [{'name': 'Jack Bauer'}] In [3]: この例では、name という1つのキーだけを持つターゲット辞書を定義しています。{name': 'Jack Bauer'} のようなものは検証に失敗しませんが、{'name': 'David Coverdale'} や{'name': 77} のようなものは検証が失敗します。 デフォルトでは、ドキュメントのすべてのキーはオプションで省略可能です。ただし、個々のフィールドに対してルール requiredを True に設定したり、Validatorインスタンスのrequire_allプロパティ を True に設定して、ドキュメントにすべてのスキーマ定義のフィールドが存在していなければ検証が失敗とすることもできます。設定に方法の詳細については後述します。 レジストリ cerberus モジュールの名前空間には 2つのデフォルトレジストリがあります。ここにスキーマやルールセットの定義を保存し、検証スキーマで参照することができます。さらに、より多くのレジストリオブジェクトをインスタンス化し、 Validatorクラスの rules_set_registry や schema_registry にバインドすることができます。初期化時にこれらをキーワード引数として設定することもできます。 レジストリの使用は、次のような場合に特に有効です。 スキーマが自分自身への参照を含む場合 (スキーマの再帰) スキーマには再利用される部分が多く、シリアル化されていることが望ましいとき In [2]: # %load 21_schema_registry.py ...: from cerberus import schema_registry, Validator ...: ...: schema_registry.add('non-system user', ...: {'uid': {'min': 1000, 'max': 0xffff}}) ...: ...: schema = {'sender': {'schema': 'non-system user', ...: 'allow_unknown': True}, ...: 'receiver': {'schema': 'non-system user', ...: 'allow_unknown': True}} ...: ...: v = Validator(schema) ...: ...: document = {'sender': {'uid': 0}} ...: c1 = (v.validate(document), v.errors) ...: assert c1[0] == False ...: assert c1[1] == {'sender': [{'uid': ['min value is 1000']}]} ...: ...: document = {'sender': {'uid': 1000}} ...: c2 = (v.validate(document), v.errors) ...: assert c2[0] == True ...: ...: document = {'sender': {'uid': 1001}} ...: c3 = (v.validate(document), v.errors) ...: assert c3[0] == True ...: In [3]: In [2]: # %load 22_rules_set_registry.py ...: from cerberus import rules_set_registry, Validator ...: ...: rules_set_registry.extend((('boolean', {'type': 'boolean'}), ...: ('booleans', {'valuesrules': 'boolean'}))) ...: schema = {'foo': 'booleans'} ...: ...: v = Validator(schema) ...: r = v.rules['valuesrules'] ...: ...: c = v.validate({'foo': 1}) ...: assert c == True ...: ...: c = v.validate({'foo': True}) ...: assert c == True ...: ...: c = v.validate({'foo': {'enable': True}}) ...: assert c == True ...: ...: c = v.validate({'foo': {'name': 'Jack'}}) ...: assert c == False ...: assert v.errors == {'foo': [{'name': ['must be of boolean type']}]} ...: ...: # r ...: In [3]: r Out[3]: {'type': ['dict', 'string'], 'check_with': 'bulk_schema', 'forbidden': ['rename', 'rename_handler']} In [4]: ルールvaluesrulesは全ての値が指定したルールに従うことを制約するものです(詳しくは後述します) この例では、値には辞書か文字列を与えることができ、値の型はブール値となっていれば検証が失敗しません。 検証(バリデーション) Validatorクラスに渡されたときや、ドキュメントのフィールドに新しいルールが設定されたときに、バリデーションスキーマ自体が検証されます。無効な検証スキーマに遭遇した場合は SchemaError が発生します。 ただし、そのレベル以下のすべての変更や、レジストリ内の使用済みの定義が変更された場合には、検証がトリガーされないことに注意してください。そのため、検証をトリガーして例外をキャッチすることができます。 In [2]: # %load 23_schema_validation.py ...: from cerberus import Validator, SchemaError ...: ...: schema = {'foo': {'allowed': []}} ...: v = Validator(schema) ...: ...: ...: try: ...: v.schema['foo'] = {'allowed': 1} ...: except SchemaError as e: ...: print(f'1st: {e}') ...: ...: v.schema['foo']['allowed'] = 'strings are no valid constraint for allowe ...: d' ...: ...: try: ...: v.schema.validate() ...: except SchemaError as e: ...: print(f'2nd: {e}') ...: 1st: {'foo': [{'allowed': ['must be of container type']}]} 2nd: {'foo': [{'allowed': ['must be of container type']}]} In [3]: シリアル化 cerberus のスキーマは、dict、list、strなどの純粋なPythonの型で構築されます。ユーザーが定義した検証ルールも、文字列としての名前でスキーマ内で呼び出されます。この定義を文字列で行えるという設計の便利な副次的効果は、スキーマをPyYAMLなどの様々な方法で定義できることです。 In [2]: # %load 24_serialization.py ...: import yaml ...: from cerberus import Validator ...: ...: schema_text = ''' ...: name: ...: type: string ...: age: ...: type: integer ...: min: 10 ...: ''' ...: ...: schema = yaml.load(schema_text, Loader=yaml.SafeLoader) ...: document = {'name': 'Little Joe', 'age': 5} ...: ...: v = Validator(schema) ...: c = (v.validate(document), v.errors) ...: assert c[0] == False ...: assert c[1] == {'age': ['min value is 10']} ...: In [3]: もちろんYAMLに限定されているわけではなく、JSONなどの好きなシリアライザを使うことができます。ネストされたdictを生成できるデコーダがあればよいので、それらを使ってスキーマを定義することができます。 レジストリの入力とダンプには、extend() と all() を使います。 検証ルール Validatorインスタンスの validation_rulesプロパティーを参照すると検証ルールを知ることができます。 ここでは、そらについて説明してゆくことにします。 allow_unknown このルールは、サブドキュメントを検証するためのValidatorインスタンスのallow_unknownプロパティを設定することで、マッピングの検証時にスキーマルールと一緒に使用することができます。このルールは、purge_unknown(詳しくは後述します)よりも優先されます。 allowed このルールは、許容値を collections.abc のContainerで受け取ります。対象の値が許容値に含まれる場合、その値を検証します。ターゲットの値が反復可能である場合、そのすべてのメンバが許容値に含まれていなければなりません。 In [2]: # %load 30_allowed.py ...: from cerberus import Validator ...: ...: schema_list = {'role': {'type': 'list', ...: 'allowed': ['agent', 'client', 'supplier']}} ...: schema_string = {'role': {'type': 'string', ...: 'allowed': ['agent', 'client', 'supplier']}} ...: schema_integer = {'a_restricted_integer': {'type': 'integer', ...: 'allowed': [-1, 0, 1]}} ...: v = Validator() ...: ...: v.schema = schema_list ...: c1 = (v.validate({'role': ['agent', 'supplier']}), v.errors) ...: assert c1[0] == True ...: ...: c2 = (v.validate({'role': ['intern']}), v.errors) ...: assert c2[0] == False ...: assert c2[1] == {'role': ["unallowed values ('intern',)"]} ...: ...: v.schema = schema_string ...: c3 = (v.validate({'role': 'supplier'}), v.errors) ...: assert c3[0] == True ...: ...: c4 = (v.validate({'role': 'intern'}), v.errors) ...: assert c4[0] == False ...: assert c4[1] == {'role': ['unallowed value intern']} ...: ...: v.schema = schema_integer ...: c5 = (v.validate({'a_restricted_integer': -1}), v.errors) ...: assert c5[0] == True ...: ...: c6 = (v.validate({'a_restricted_integer': 2}), v.errors) ...: assert c6[0] == False ...: assert c6[1] == {'a_restricted_integer': ['unallowed value 2']} ...: In [3]: allof 提供された制約のすべてがそのフィールドを有効にするかどうかを検証します。詳細は *of-rules を参照してください。 anyof 提供された制約条件のいずれかがそのフィールドを有効にするかどうかを検証します。詳細は *of-rules をご覧ください。 check_with 関数またはメソッドを呼び出して、フィールドの値を検証します。 この関数は次のスニペットのように実装しなければなりません。 def functionnname(field, value, error): if value is invalid: error(field, 'error message') error 引数は、呼び出したバリデータの _error メソッドを指します。エラーを送信する方法については、 「Cerberusの拡張」を参照してください。 ここでは、整数が奇数かどうかをテストする例を示します。 def oddity(field, value, error): if not value & 1: error(field, "Must be an odd number") 次のようにしてデータを検証することができます。 In [2]: # %load 40_check_with.py ...: from cerberus import Validator ...: ...: def oddity(field, value, error): ...: if not value & 1: ...: error(field, "Must be an odd number") ...: ...: schema = {'amount': {'check_with': oddity}} ...: v = Validator(schema) ...: ...: c1 = (v.validate({'amount': 10}), v.errors) ...: assert c1[0] == False ...: assert c1[1] == {'amount': ['Must be an odd number']} ...: ...: c2 = (v.validate({'amount': 9}), v.errors) ...: assert c2[0] == True ...: In [3]: ルールの制約条件が文字列の場合、 Validator のインスタンスには、その名前が _check_with_ で始まるメソッドを持つ必要があります。上記の関数ベースの例に相当するものとしては、「check_withルールで参照できるメソッド」を参照してください。 制約条件は、シーケンスにすることで連続して呼び出されるようにすることもできます。 schema = {'field': {'check_with': (oddity, 'prime number')}} contains このルールは、コンテナオブジェクトに定義されたすべてのアイテムが含まれているかどうかを検証します。 In [2]: # %load 41_contains.py ...: from cerberus import Validator ...: ...: document = {'states': ['peace', 'love', 'inity']} ...: schema = {'states': {'contains': 'peace'}} ...: ...: v = Validator() ...: ...: c1 = (v.validate(document, schema), v.errors) ...: assert c1[0] == True ...: ...: schema = {'states': {'contains': 'greed'}} ...: c2 = (v.validate(document, schema), v.errors) ...: assert c2[0] == False ...: assert c2[1] == {'states': ["missing members {'greed'}"]} ...: ...: schema = {'states': {'contains': ['love', 'inity']}} ...: c3 = (v.validate(document, schema), v.errors) ...: assert c3[0] == True ...: ...: schema = {'states': {'contains': ['love', 'respect']}} ...: c4 = (v.validate(document, schema), v.errors) ...: assert c4[0] == False ...: assert c4[1] == {'states': ["missing members {'respect'}"]} ...: In [3]: dependencies このルールでは、定義されたフィールドがドキュメントに存在する場合、ドキュメントで必要とされる単一のフィールド名、一連のフィールド名、またはフィールド名と一連の許容値のマッピングを定義することができます。 In [2]: # %load 42_dependencies.py ...: from cerberus import Validator ...: ...: schema = {'field1': {'required': False}, ...: 'field2': {'required': False, ...: 'dependencies': 'field1'}} ...: ...: v = Validator() ...: ...: document = {'field1': 7} ...: c1 = v.validate(document, schema) ...: assert c1 == True ...: ...: document = {'field2': 7} ...: c2 = v.validate(document, schema) ...: assert c2 == False ...: assert v.errors == {'field2': ["field 'field1' is required"]} ...: In [3]: 複数のフィールド名が依存関係として定義されている場合、対象となるフィールドが検証されるためには、これらがすべて存在する必要があります。 In [2]: # %load 43_dependencies_multiple.py ...: from cerberus import Validator ...: ...: schema = {'field1': {'required': False}, ...: 'field2': {'required': False}, ...: 'field3': {'required': False, ...: 'dependencies': ['field1', 'field2']}} ...: ...: v = Validator() ...: ...: document = {'field1': 7, 'field2': 11, 'field3': 13} ...: c = v.validate(document, schema) ...: assert c == True ...: ...: document = {'field2': 11, 'field3': 13} ...: c = v.validate(document, schema) ...: assert c == False ...: assert v.errors == {'field3': ["field 'field1' is required"]} ...: In [3]: マッピングが提供されると、すべての依存関係が存在するだけでなく、それらの許容値のいずれかがマッチしなければなりません。 In [2]: # %load 44_dependencies_with_mapping.py ...: from cerberus import Validator ...: ...: schema = {'field1': {'required': False}, ...: 'field2': {'required': True, ...: 'dependencies': {'field1': ['one', 'two']}}} ...: ...: v = Validator() ...: ...: document = {'field1': 'one', 'field2': 7} ...: c = (v.validate(document, schema), v.errors) ...: assert c[0] == True ...: ...: document = {'field1': 'three', 'field2': 7} ...: c = (v.validate(document, schema), v.errors) ...: assert c[0] == False ...: assert c[1] == {'field2': ...: ["depends on these values: {'field1': ['one', 'two']}"]} ...: ...: # dependencies のリストを使うのと同じ ...: document = {'field2': 7} ...: c = (v.validate(document, schema), v.errors) ...: assert c[0] == False ...: assert c[1] == {'field2': ...: ["depends on these values: {'field1': ['one', 'two']}"]} ...: ...: ...: # 単一のdependencies を渡すこともできます。 ...: schema = {'field1': {'required': False}, ...: 'field2': {'dependencies': {'field1': 'one'}}} ...: document = {'field1': 'one', 'field2': 7} ...: c = (v.validate(document, schema), v.errors) ...: assert c[0] == True ...: ...: document = {'field1': 'two', 'field2': 7} ...: c = (v.validate(document, schema), v.errors) ...: assert c[0] == False ...: assert c[1] == {'field2': ...: ["depends on these values: {'field1': 'one'}"]} ...: ...: In [3]: サブドキュメントのフィールドの依存関係をドット表記で宣言することもサポートされています。 In [2]: # %load 45_dependencies_dot_notation.py ...: from cerberus import Validator ...: ...: schema = { ...: 'test_field': {'dependencies': ['a_dict.foo', 'a_dict.bar']}, ...: 'a_dict': { ...: 'type': 'dict', ...: 'schema': { ...: 'foo': {'type': 'string'}, ...: 'bar': {'type': 'string'} ...: } ...: } ...: } ...: ...: document = {'test_field': 'foobar', ...: 'a_dict': {'foo': 'foo'}} ...: ...: v = Validator() ...: ...: c = (v.validate(document, schema), v.errors) ...: assert c[0] == False ...: assert c[1] == {'test_field': ["field 'a_dict.bar' is required"]} ...: In [3]: サブドキュメントが処理されると、問題のあるフィールドの検索はそのドキュメントのレベルで始まります。処理されたドキュメントをルートレベルとして扱うためには、宣言はキャレット記号(^) で始まる必要があります。2 つのキャレット (^^) ので始まっていると、特別な意味を持たない1文字のキャレット(^) として解釈されます。 In [2]: # %load 46_dependencies_carets.py ...: from cerberus import Validator ...: ...: schema = { ...: 'test_field': {}, ...: 'a_dict': { ...: 'type': 'dict', ...: 'schema': { ...: 'foo': {'type': 'string'}, ...: 'bar': {'type': 'string', ...: 'dependencies': '^test_field'} ...: } ...: } ...: } ...: ...: v = Validator() ...: document = {'a_dict': {'bar': 'bar'}} ...: c = (v.validate(document, schema), v.errors) ...: assert c[0] == False ...: assert c[1] == {'a_dict': ...: [{'bar': ["field '^test_field' is required"]}]} ...: In [3]: 備考: ドット記法のセマンティクスを拡張したい場合は、_lookup_field()メソッドをオーバーライドしてください。 注意点: このルールの評価では、requireルールで定義された制約は考慮されません。 empty このルールがFalseとして制約されている場合、反復可能な値が空であれば検証に失敗します。デフォルトでは、フィールドが空であるかどうかはチェックされないため、ルールが定義されていない場合は許可されます。しかし、制約をTrueで定義すると、値が空であるとみなされた場合、そのフィールドに定義されている可能性のあるルールallowed、fobidden、items、minlength、maxlength、regex、validatorをスキップします。 In [2]: # %load 47_empty.py ...: from cerberus import Validator ...: ...: schema = {'name': {'type': 'string', 'empty': False}} ...: document = {'name': ''} ...: ...: v = Validator() ...: c = (v.validate(document, schema), v.errors) ...: assert c[0] == False ...: assert c[1] == {'name': ['empty values not allowed']} ...: In [3]: excludes このルールは除外するフィールドを宣言することができます。 In [2]: # %load 48_excludes.py ...: from cerberus import Validator ...: ...: v = Validator() ...: schema = {'this_field': {'type': 'dict', ...: 'excludes': 'that_field'}, ...: 'that_field': {'type': 'dict', ...: 'excludes': 'this_field'}} ...: ...: c1 = (v.validate({'this_field': {}, 'that_field': {}}, schema), v.errors ...: ) ...: assert c1[0] == False ...: assert c1[1] == {'that_field': ...: ["'this_field' must not be present with 'that_field'"], ...: 'this_field': ...: ["'that_field' must not be present with 'this_field'"]} ...: ...: c2 = v.validate({'this_field': {}}, schema) ...: assert c2 == True ...: ...: c3 = v.validate({'that_field': {}}, schema) ...: assert c3 == True ...: ...: c4 = v.validate({}, schema) ...: assert c4 == True ...: In [3]: 両方のフィールドをrequiredとし、排他的論理和を構築することができます。 In [2]: # %load 49_excludes_exclusive_or.py ...: from cerberus import Validator ...: ...: schema = {'this_field': {'type': 'dict', ...: 'excludes': 'that_field', ...: 'required': True}, ...: 'that_field': {'type': 'dict', ...: 'excludes': 'this_field', ...: 'required': True} ...: } ...: ...: v = Validator(schema) ...: document = {'this_field': {}, 'that_field': {}} ...: c1 = (v.validate(document), v.errors) ...: assert c1[0] == False ...: assert c1[1] == {'that_field': ...: ["'this_field' must not be present with 'that_field'"], ...: ...: 'this_field': ...: ["'that_field' must not be present with 'this_field'"]} ...: ...: ...: document = {'this_field': {}} ...: c2 = (v.validate(document), v.errors) ...: assert c2[0] == True ...: ...: document = {'that_field': {}} ...: c3 = (v.validate(document), v.errors) ...: assert c3[0] == True ...: ...: document = {} ...: c4 = (v.validate(document), v.errors) ...: assert c4[0] == False ...: assert c4[1] == {'that_field': ['required field'], ...: 'this_field': ['required field']} ...: In [3]: フィールドをリストで与えて、複数のフィールドを除外することできます。 In [2]: # %load 50_excludes_multiple_fields.py ...: from cerberus import Validator ...: ...: schema = {'this_field': {'type': 'dict', ...: 'excludes': ['that_field', 'bazo_field']}, ...: 'that_field': {'type': 'dict', ...: 'excludes': 'this_field'}, ...: 'bazo_field': {'type': 'dict'}} ...: ...: v = Validator(schema) ...: ...: document = {'this_field': {}, 'bazo_field': {}} ...: c = (v.validate(document), v.errors) ...: assert c[0] == False ...: assert c[1] == {'this_field': ...: ["'that_field', 'bazo_field' must not be present with 'this_field'"]} ...: In [3]: フィールドをリストで与えて、複数のフィールドを除外することできます。 In [2]: # %load 50_excludes_multiple_fields.py ...: from cerberus import Validator ...: ...: schema = {'this_field': {'type': 'dict', ...: 'excludes': ['that_field', 'bazo_field']}, ...: 'that_field': {'type': 'dict', ...: 'excludes': 'this_field'}, ...: 'bazo_field': {'type': 'dict'}} ...: ...: v = Validator(schema) ...: ...: document = {'this_field': {}, 'bazo_field': {}} ...: c = (v.validate(document), v.errors) ...: assert c[0] == False ...: assert c[1] == {'this_field': ...: ["'that_field', 'bazo_field' must not be present with 'this_field'"]} ...: In [3]: forbidden このルールは、allowedとは逆に、値が定義された値以外のものであるかどうかを検証します。 In [2]: # %load 51_forbidden.py ...: from cerberus import Validator ...: ...: schema = {'user': {'forbidden': ['root', 'admin']}} ...: v = Validator(schema) ...: ...: document = {'user': 'root'} ...: c1 = (v.validate(document), v.errors) ...: assert c1[0] == False ...: assert c1[1] == {'user': ['unallowed value root']} ...: ...: document = {'user': 'jack'} ...: c2 = (v.validate(document), v.errors) ...: assert c2[0] == True ...: In [3]: items 任意の反復可能な項目(iitem)を、インデックスに対応する各項目を検証しなければならない一連の規則に対して検証します。項目は、与えられた反復可能のサイズが定義のものと一致する場合にのみ評価されます。これは正規化の際にも適用され、長さが不一致の場合、値の項目は正規化されません。 In [2]: # %load 52_items.py ...: from cerberus import Validator ...: ...: schema = {'list_of_values': { ...: 'type': 'list', ...: 'items': [{'type': 'string'}, {'type': 'integer'}]} ...: } ...: v = Validator(schema) ...: ...: document = {'list_of_values': ['hello', 100]} ...: c1 = (v.validate(document), v.errors) ...: assert c1[0] == True ...: ...: document = {'list_of_values': [100, 'hello']} ...: c2 = (v.validate(document), v.errors) ...: assert c2[0] == False ...: assert c2[1] == {'list_of_values': ...: [{0: ['must be of string type'], ...: 1: ['must be of integer type']}]} ...: In [3]: keysrules このルールは、マッピングのすべてのキーが検証される制約としてルールのセットを受け取ります。 In [2]: # %load 53_keysrules.py ...: from cerberus import Validator ...: ...: schema = {'a_dict': { ...: 'type': 'dict', ...: 'keysrules': {'type': 'string', 'regex': '[a-z]+'}} ...: } ...: v = Validator(schema) ...: ...: document = {'a_dict': {'key': 'value'}} ...: c1 = (v.validate(document), v.errors) ...: assert c1[0] == True ...: ...: document = {'a_dict': {'KEY': 'value'}} ...: c2 = (v.validate(document), v.errors) ...: assert c2[0] == False ...: assert c2[1] == {'a_dict': ...: [{'KEY': ["value does not match regex '[a-z]+'"]}]} ...: In [3]: meta これは実際には検証ルールではなく、ルールセットの中のフィールドであり、ドキュメントフィールドのために記述されたアプリケーション固有のデータのために慣習的に使用することができます。 {'id': {'type': 'string', 'regex': r'[A-M]\d{,6}', 'meta': {'label': 'Inventory Nr.'}}} 割り当てられたデータの種類は問いません。 min、max 比較演算(__gt__() と __lt__())を実装しているクラスのオブジェクトに許される最小値と最大値で検証します。 In [2]: # %load 54_min_max.py ...: from cerberus import Validator ...: ...: schema = {'weight': {'min': 10.1, 'max': 10.9}} ...: ...: v = Validator(schema) ...: ...: document = {'weight': 10.3} ...: c1 = (v.validate(document), v.errors) ...: assert c1[0] == True ...: ...: document = {'weight': 12} ...: c2 = (v.validate(document), v.errors) ...: assert c2[0] == False ...: assert c2[1] == {'weight': ['max value is 10.9']} ...: In [3]: minlength、 maxlength サイズを得る__len__() を実装する型に許される最小と最大の長さで検証します。 In [2]: # %load 55_min_max_length.py ...: from cerberus import Validator ...: ...: schema = {'numbers': {'minlength': 1, 'maxlength': 3}} ...: v = Validator(schema) ...: ...: document = {'numbers': [256, 2048, 23]} ...: c1 = (v.validate(document), v.errors) ...: assert c1[0] == True ...: ...: document = {'numbers': [256, 2048, 23, 2]} ...: c2 = (v.validate(document), v.errors) ...: assert c2[0] == False ...: assert c2[1] == {'numbers': ['max length is 3']} ...: In [3]: noneof 提供された制約の中にフィールドを検証するものがない場合に検証します。詳細は *of-rules を参照してください。 nullable True の場合、フィールドの値は None であることが許可されます。ルールは、定義されているかどうかにかかわらず、すべてのフィールドでチェックされます。ルールの制約のデフォルトはFalseです。 In [2]: # %load 56_nullable.py ...: from cerberus import Validator ...: ...: schema = {'a_nullable_integer': {'type': 'integer','nullable': True}, ...: 'an_integer': {'type': 'integer'}} ...: v = Validator(schema) ...: ...: document = {'a_nullable_integer': 3} ...: c1 = v.validate(document) ...: assert c1 == True ...: ...: document = {'a_nullable_integer': None} ...: c2 = v.validate(document) ...: assert c2 == True ...: ...: document = {'an_integer': 3} ...: c3 = v.validate(document) ...: assert c3 == True ...: ...: document = {'an_integer': None} ...: c4 = (v.validate(document), v.errors) ...: assert c4[0] == False ...: assert c4[1] == {'an_integer': ['null value not allowed']} ...: In [3]: *of-rules これらのルールでは、検証するための異なるセットを定義することができます。ロジックを表すall、any、one、noneで始まるルール名です。これらのルールに従ってリスト内のセットに対して検証された場合に有効となります。 ルール 説明 allof すべての制約条件がそのフィールドを有効にするかどうかを検証する anyof 制約条件のいずれかがフィールドを有効にする場合に有効 noneof 制約条件のいずれもそのフィールドを有効にしない場合に有効 oneof 制約条件のうち、正確に1つが適用される場合に有効 備考: これらのルールの制約の中で、ルールセットに正規化を使用することはできません。 注意: これらのルールを使用する前に、Cerberusを使用する場合と使用しない場合の問題に対する他の可能な解決策を調べておく必要があります。これらのルールを使用すると、スキーマが複雑になりすぎることがあります。 例えば、フィールドの値が0~10または100~110の数字であることを確認するには、次のようにします。 In [2]: # %load 57_anyof.py ...: from cerberus import Validator ...: ...: schema = {'prop1': ...: {'type': 'number', ...: 'anyof': [{'min': 0, 'max': 10}, ...: {'min': 100, 'max': 110}] ...: } ...: } ...: ...: v = Validator(schema) ...: ...: document = {'prop1': 5} ...: c = v.validate(document) ...: assert c == True ...: ...: document = {'prop1': 105} ...: c = v.validate(document) ...: assert c == True ...: ...: document = {'prop1': 55} ...: c = (v.validate(document), v.errors) ...: assert c[0] == False ...: assert c[1] == {'prop1': ['no definitions validate', ...: {'anyof definition 0': ['max value is 10'], ...: 'anyof definition 1': ['min value is 100']}]} ...: In [3]: anyofルールは、リスト内の各ルールセットをテストします。したがって、上記のスキーマは2つの別々のスキーマを作ることと同じです。 In [2]: # %load 58_anyof_alternate.py ...: from cerberus import Validator ...: ...: schema1 = {'prop1': {'type': 'number', 'min': 0, 'max': 10}} ...: schema2 = {'prop1': {'type': 'number', 'min': 100, 'max': 110}} ...: ...: v = Validator() ...: ...: document = {'prop1': 5} ...: c = v.validate(document, schema1) or v.validate(document, schema2) ...: assert c == True ...: ...: document = {'prop1': 105} ...: c = v.validate(document, schema1) or v.validate(document, schema2) ...: assert c == True ...: ...: document = {'prop1': 55} ...: c = v.validate(document, schema1) or v.validate(document, schema2) ...: assert c == False ...: assert v.errors == {'prop1': ['min value is 100']} ...: In [3]: *of-rules の入力を簡単にする of-ruleをアンダースコア(_)で連結したり、別のルールをルールの値(rule-values)のリストで連結したりすることで、入力の手間を省くことができます。 {'foo': {'anyof_regex': ['^ham', 'spam$']}} # 上記は次と同じ {'foo': {'anyof': [{'regex': '^ham'}, {'regex': 'spam$'}]}} # これとも同じ # {'foo': {'regex': r'(^ham|spam$)'}} これを使えば、独自のロジックを実装することなく、複数のスキーマに対してドキュメントを検証することができます。 In [2]: # %load 59_of_rules_concatenate.py ...: from cerberus import Validator ...: ...: schemas = [ ...: {'department': {'required': True, 'regex': '^CTU$'}, ...: 'phone': {'nullable': True} }, ...: {'department': {'required': True}, ...: 'phone': {'required': True}} ...: ] ...: ...: employee_schema = {'employee': {'oneof_schema': schemas, ...: 'type': 'dict'}} ...: ...: employee_vldtr = Validator(employee_schema, allow_unknown=True) ...: ...: employees = [ ...: { 'employee': { 'name': 'Jack Bauer', ...: 'department': 'CTU', 'phone': None }}, ...: { 'employee': { 'name': "Chloe O'Brian", ...: 'department': 'CTU', 'phone': '001022' }}, ...: { 'employee': { 'name': 'Anthony Tony', ...: 'department': 'CTU', 'phone': '001023' }}, ...: { 'employee': { 'name': 'Ann Wilson', ...: 'department': 'Heart', 'phone': '002001' }}, ...: { 'employee': { 'name': 'Nacy Wilson', ...: 'department': 'Heart', 'phone': None }} ...: ] ...: ...: invalid_employees_phones = [] ...: for employee in employees: ...: if not employee_vldtr.validate(employee): ...: invalid_employees_phones.append(employee) ...: ...: from pprint import pprint as print ...: print(invalid_employees_phones) ...: [{'employee': {'department': 'CTU', 'name': "Chloe O'Brian", 'phone': '001022'}}, {'employee': {'department': 'CTU', 'name': 'Anthony Tony', 'phone': '001023'}}, {'employee': {'department': 'Heart', 'name': 'Nacy Wilson', 'phone': None}}] In [3]: oneof 提供された制約条件のうち、正確に1つが適用されるかどうかを検証します。詳細は *of-rules を参照してください。 readonly Trueの場合、値は読み取り専用になります。このフィールドがターゲット辞書に存在する場合、バリデーションは失敗します。これは、例えば、データストアに送信する前に検証されるべきデータを受信した場合などに便利です。このフィールドはデータストアから提供されるかもしれませんが、書き込み可能であってはなりません。 Validatorクラスに引数purge_readonly与えてインスタンスを作成するか、インスタンスオブジェクトの同名のプロパティを設定することで、このルールが積極的に定義されているすべてのフィールドを削除することができます。 defaultおよびdefault_setterと組み合わせて使用することができます。 regex フィールドの値が指定した正規表現に一致しない場合、検証は失敗します。これは文字列の値に対してのみテストされます。 In [2]: # %load 60_regex.py ...: from cerberus import Validator ...: ...: schema = { ...: 'email': { ...: 'type': 'string', ...: 'regex': '^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$' ...: } ...: } ...: ...: v = Validator(schema) ...: ...: document = {'email': 'john@example.com'} ...: c = v.validate(document) ...: assert c == True ...: ...: document = {'email': 'john_at_example_dot_com'} ...: c = (v.validate(document, schema), v.errors) ...: assert c[0] == False ...: assert c[1] == {'email': ...: ["value does not match regex \ ...: '^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$'"]} ...: In [3]: すべてのパターンで末尾の $が保証されているのは、ユーザーが文字列のマッチング(検索ではなく)のために完全なパターンを書くことを奨励するためです。先頭の ^ については、実装に一貫性がなく、強制されていません。この不整合は 1.3.x リリースシリーズでは修正されません。正規表現の構文の詳細については、標準ライブラリの re のドキュメントを参照してください。 式の一部として動作フラグを設定できることに注意してください。これは、例えばre.compile()関数にフラグを渡すことと同じです。つまり、制約 '(?i)holy grail' は re.I フラグに相当するものを含んでおり、 'holy grail' またはその変形を大文字のグリフで含むすべての文字列にマッチします。記載されているライブラリのドキュメントで(?aiLmsux)を探すと、そこに記述があります。 require_all これは、サブドキュメントのバリデータのrequire_allプロパティを設定するために、マッピングを検証する際のスキーマルールと組み合わせて使用することができます。「すべてを要求するスキーマ」での13_require_all_schema.py の動作をもう一度確認してみてください。 required Trueの場合、そのフィールドは必須です。update=True でvalidate() が呼ばれない限り、このフィールドがないと検証が失敗します。 In [2]: # %load 61_required.py ...: from cerberus import Validator ...: ...: schema = {'name': {'required': True, 'type': 'string'}, ...: 'age': {'type': 'integer'}} ...: ...: v = Validator(schema) ...: ...: document = {'age': 10} ...: c1 = (v.validate(document), v.errors) ...: assert c1[0] == False ...: assert c1[1] == {'name': ['required field']} ...: ...: c2 = v.validate(document, update=True) ...: assert c2 == True ...: In [3]: 備考: ドキュメントのすべてのフィールドを必須(required)として定義する方法は、「すべてを要求するスキーマ」でのサンプルコード12_require_all.py を参照してください。 注意: required が True に設定されていても、値が空(empty)の文字列フィールドは検証されます。空の値を受け入れたくない場合は、emptyルールを参照してください。 注意: このルールの評価では、dependenciesルールで定義された制約条件は考慮されません。 schema(dict) schemaルールが定義されているフィールドの値としてマッピングがある場合、そのマッピングは制約として提供されているスキーマに対して検証されます。 In [2]: # %load 62_schema_rule_dict.py ...: from cerberus import Validator ...: ...: schema = {'a_dict': {'type': 'dict', ...: 'schema': {'address': {'type': 'string'}, ...: 'city': {'type': 'string', ...: 'required': True}} ...: } ...: } ...: ...: v = Validator(schema) ...: ...: document = {'a_dict': {'address': 'my address', 'city': 'my town'}} ...: c = v.validate(document) ...: assert c == True ...: In [3]: マッピングの任意のキーを検証するには keysrulesルール、マッピングの任意の値を検証するには valuesrulesルールを参照してください。 schema(list) schemaバリデーションが値として不規則なサイズのシーケンスに遭遇した場合、シーケンスのすべてのアイテムは、スキーマの制約で提供されたルールに対して検証されます。 In [2]: # %load 63_schema_rule_list.py ...: from cerberus import Validator ...: ...: schema = {'a_list': {'type': 'list', ...: 'schema': {'type': 'integer'}}} ...: ...: v = Validator(schema) ...: ...: document = {'a_list': [3, 4, 5]} ...: c = v.validate(document) ...: assert c == True ...: In [3]: タイプ(type) キーの値に使用できるデータタイプ。次のいずれかの名前を指定できます。 Cerberus のタイプはValidatorインスタンスの types プロパティーで参照することができます。 In [3]: v.types Out[3]: ('binary', 'boolean', 'container', 'date', 'datetime', 'dict', 'float', 'integer', 'list', 'number', 'set', 'string') Type Python2 Type Python3 Type boolean bool bool binary bytes, bytearray bytes, bytearray date datetime.date datetime.date datetime datetime.datetime datetime.datetime dict collections.Mapping collections.abc.Mapping float float float integer int, long int list collections.Sequence, excl. string collections.abc.Sequence, excl. string number float, int, long, excl. bool float, int, excl. bool set set set string basestring() str このリストを拡張して、カスタムタイプをサポートすることができます。 タイプのリストを使用して、異なる値を許可することができます。 In [2]: # %load 64_type.py ...: from cerberus import Validator ...: ...: schema = {'quotes': {'type': ['string', 'list']}} ...: v = Validator() ...: ...: document = {'quotes': 'Hello world!'} ...: c = v.validate(document, schema) ...: assert c == True ...: ...: document = {'quotes': ['Do not disturb my circles!', 'Heureka!']} ...: c = v.validate(document, schema) ...: assert c == True ...: ...: schema = {'quotes': {'type': ['string', 'list'], ...: 'schema': {'type': 'string'}}} ...: ...: document = {'quotes': 'Hello world!'} ...: c = v.validate(document, schema) ...: assert c == True ...: ...: document = {'quotes': [1, 'Heureka!']} ...: c = (v.validate(document, schema), v.errors) ...: assert c[0] == False ...: assert c[1] == {'quotes': [{0: ['must be of string type']}]} ...: In [3]: 備考: 型規則の設定は必須ではありませんが、特に schema のような複雑な規則を使用する場合は、設定しないことをお勧めします。それでも明示的な型を設定したくないと判断した場合、schemaなどのルールは、そのルールを実際に使用できる値(dictやlistなど)にのみ適用されます。また、schemaの場合、Cerberus は list と dict のどちらの型のルールが適切かを判断し、schemaのルールがどのようなものかに応じて推論します。 注意点: 型の検証は、同じフィールドに存在する他のほとんどの型よりも先に実行されることに注意してください(事前に考慮されるのはnullable と readonly のみ)。型の失敗が発生した場合、そのフィールドに対する後続の検証ルールはスキップされ、他のフィールドの検証が続行されます。これにより、他の(標準またはカスタム)ルールが呼び出されたときに、フィールドの型が正しいと安全に仮定することができます。 valuesrules このルールは、マッピングのすべての値が検証される制約としてルールのセットを受け取ります。 In [2]: # %load 65_valuesrules.py ...: from cerberus import Validator ...: ...: schema = {'numbers': ...: {'type': 'dict', ...: 'valuesrules': {'type': 'integer', 'min': 10}} ...: } ...: ...: v = Validator(schema) ...: ...: document = {'numbers': {'an integer': 10, 'another integer': 100}} ...: c = v.validate(document) ...: assert c == True ...: ...: document = {'numbers': {'an integer': 9}} ...: c = (v.validate(document), v.errors) ...: assert c[0] == False ...: assert c[1] == {'numbers': [{'an integer': ['min value is 10']}]} ...: In [3]: 正規化ルール 正規化ルール(Nomalization Rules) はフィールドに適用されます。マッピング用のスキーマでも適用されますし、スキーマ(シーケンス用)、allow_unknown、keysrules、valuesrules で一括操作として定義された場合にも適用されます。anyof のようなテスト用バリアントの定義における正規化ルールは処理されません。 正規化は、マッピングの各レベルに対して、深さ優先で、このドキュメントに記載されているとおりに適用されます。 フィールドの名前変更 処理を行う前に名前を変更するフィールドを定義することができます。 フィールドのリネーム 次の処理を行う前に名前を変更するフィールドを定義できます。 In [2]: # %load 70_renaming_of_fields.py ...: from cerberus import Validator ...: ...: schema = {'foo': {'rename': 'bar'}} ...: v = Validator(schema) ...: ...: document = {'foo': 0} ...: c = v.normalized(document) ...: ...: keys = c.keys() ...: assert ('foo' in keys) == False ...: assert ('bar' in keys) == True ...: assert c != document ...: assert c == {'bar': 0} ...: In [3]: callableがフィールドや任意のフィールドの名前を変更できるようにするには、名前変更用のハンドラを定義します。制約が文字列の場合は、カスタム・メソッドを指します。制約が反復可能であれば、値はそのチェーンで処理されます。 In [2]: # %load 71_rename_handler.py ...: from cerberus import Validator ...: ...: v = Validator({}, allow_unknown={'rename_handler': int}) ...: ...: document = {'0': 'foo'} ...: c1 = v.normalized(document) ...: ...: keys = c1.keys() ...: assert (0 in keys) == True ...: assert ('0' in keys) == False ...: assert c1 != document ...: assert c1 == {0: 'foo'} ...: ...: even_digits = lambda x: '0' + x if len(x) % 2 else x ...: v = Validator({}, allow_unknown={'rename_handler': [str, even_digits]}) ...: ...: document = {1: 'foo'} ...: c2 = v.normalized(document) ...: ...: keys = c2.keys() ...: assert (1 in keys) == False ...: assert ('1' in keys) == False ...: assert ('01' in keys) == True ...: assert c2 != document ...: assert c2 == {'01': 'foo'} ...: In [3]: 未知のフィールドの除去 名前を変更した後、 Validator インスタンスの purge_unknown プロパティが True であれば、 未知のフィールドは除去されます(デフォルトは False)。このプロパティは、初期化時にキーワード・引数ごとに設定することもできますし、 allow_unknown のようにサブドキュメントのルールとして設定することもできます(「未知なキーを許可する」を参照)。既定値は False です。サブドキュメントに allow_unknown ルールが含まれている場合、そのサブドキュメントでは未知のフィールドは除去されません。 In [2]: # %load 72_purge_unknown_fields.py ...: from cerberus import Validator ...: ...: schema = {'foo': {'type': 'string'}} ...: v = Validator(schema, purge_unknown=True) ...: ...: c = v.normalized({'bar': 'foo'}) ...: assert c == {} ...: ...: c = v.normalized({'foo': 'bar'}) ...: assert c == {'foo': 'bar'} ...: In [3]: デフォルト値 ドキュメント内の欠落しているフィールドのデフォルト値を、defaultルールを使って設定することができます。 In [2]: # %load 73_default_values.py ...: from cerberus import Validator ...: ...: schema = {'amount': {'type': 'integer'}, ...: 'kind': {'type': 'string', 'default': 'purchase'}} ...: v = Validator(schema) ...: ...: c1 = v.normalized({'amount': 1}) ...: assert c1 == {'amount': 1, 'kind': 'purchase'} ...: ...: c2 = v.normalized({'amount': 1, 'kind': None}) ...: assert c2 == {'amount': 1, 'kind': 'purchase'} ...: ...: c3 = v.normalized({'amount': 1, 'kind': 'other'}) ...: assert c3 == {'amount': 1, 'kind': 'other'} ...: In [3]: デフォルト値を動的に設定するために、default_setterにcallable を定義することもできます。この callable は、現在の(サブ)ドキュメントを唯一の引数として呼び出されます。callableは互いに依存することもできますが、解決できない/循環する依存関係がある場合、正規化は失敗します。制約が文字列の場合は、カスタムメソッドを指します。 In [2]: # %load 74_default_setter.py ...: from cerberus import Validator ...: ...: v = Validator() ...: v.schema = {'a': {'type': 'integer'}, ...: 'b': {'type': 'integer', ...: 'default_setter': lambda doc: doc['a'] + 1}} ...: ...: ...: c1 = v.normalized({'a': 1}) ...: assert c1 == {'a': 1, 'b': 2} ...: ...: v.schema = {'a': {'type': 'integer', ...: 'default_setter': lambda doc: doc['not_there']}} ...: ...: c = v.normalized({}) ...: assert c == None ...: assert v.errors == {'a': ["default value for 'a' cannot be set: Circular ...: dependencies of default setters."]} ...: In [3]: 同じフィールドにdefaultとreadonlyの両方を使用することもできます。これにより、手動で値を割り当てることができないフィールドが作成されますが、Cerberusによって自動的にデフォルト値が提供されます。もちろん、default_setterにも同じことが言えます。 値の強制 coercion(強制)は、ドキュメントが検証される前に、 callable(オブジェクトまたはカスタムcoercionメソッドの名前として与えられる)を値に適用することができます。callableの戻り値は、ドキュメント内の新しい値に置き換わります。これは、バリデーションを受ける前に値を変換したり、データから不適切な部分を取り除いたりする(sanitize)のに使用できます。制約がcallableと名前のイテレート可能なものである場合、値はその一連のCoercion Callable を通して処理されます。 In [2]: # %load 75_value_coercion.py ...: from cerberus import Validator ...: ...: v = Validator() ...: v.schema = {'amount': {'type': 'integer'}} ...: c = v.validate({'amount': '1'}) ...: assert c == False ...: ...: v.schema = {'amount': {'type': 'integer', 'coerce': int}} ...: c = v.validate({'amount': '1'}) ...: assert c == True ...: assert v.document == {'amount': 1} ...: ...: to_bool = lambda v: v.lower() in ('true', '1') ...: v.schema = {'flag': {'type': 'boolean', 'coerce': (str, to_bool)}} ...: c = v.validate({'flag': 'true'}) ...: assert c == True ...: assert v.document == {'flag': True} ...: In [3]: エラーとエラー処理 エラーは、Pythonインターフェースを介して評価されたり、エラーハンドラを使ってさまざまな出力形式に処理されます。 エラーハンドラ エラーハンドラは、ドキュメントの処理後にバリデータの errors プロパティを通じて異なる出力を返します。エラーハンドラは必須のインターフェイスを定義した BaseErrorHandler をベースにしています。使用するエラーハンドラは、キーワード引数 error_handler としてValidatorクラスの初期化時に渡すか、 同じ名前のプロパティをいつでも設定することができます。初期化の際には、インスタンスかクラスのどちらかを指定します。クラスの初期化にキーワード引数を渡すには、 エラーハンドラクラスと引数を含む辞書の 2 つの値のタプルを指定します。 次のようなハンドラが用意されています。 BasicErrorHandler:これは辞書を返すデフォルトのものです。キーはドキュメントのものを参照し、値はエラー・メッセージを含むリストです。ネストされたフィールドのエラーは、これらのリストの最後の項目として辞書に保存されます。 Python インターフェース エラーは以下のプロパティを持つ ValidationError として表現されます。 document_path:ドキュメント内のパス。フラットな辞書の場合、これは単にタプル内のキーの名前であり、ネストされた辞書の場合は、すべてのトラバースされたキーの名前です。シーケンス内の項目はインデックスで表されます。 schema_path:スキーマ内のパスです。 code:エラーの一意な識別子です。リストは「エラーコード」を参照してください。 rule:ルール エラーの発生時に評価されたルールです。 constraint:制約。そのルールの制約条件です。 value:検証されている値です。 info: このタプルには、エラーとともに送信された追加情報が含まれます。ほとんどのエラーでは、これは実際には何もありません。一括して検証する場合 (アイテムや keysrules を使用する場合など) は、このプロパティに個々のエラーがすべて記録されます。追加のロギングを確認するには、ソースコードのルールの実装を参照してください。 ドキュメントを処理した後で、以下のValidatorインスタンスのプロパティでエラーにアクセスできます。 _errors:このErrorsListインスタンスは、送信されたすべてのエラーを保持します。この属性でエラーを直接操作するつもりはありません。特定のエラー定義を持つエラーが少なくともひとつ、このリストに含まれているかどうかを調べることができます。 document_error_tree:ドキュメントに対応するノードを問い合わせることができるディクテーションのようなオブジェクトです。ノードの添え字表記により、指定された ErrorDefinition に一致する特定のエラー、または指定されたキーを持つ子ノードのいずれかをフェッチできます。一致するエラーがそれぞれノード以下で発生していない場合は、代わりに None が返されます。ノードは、ErrorDefinitionまたは子ノードのキーにマッチする可能性のあるin演算子でテストすることもできます。ノードのエラーは、ErrorsListでもあるそのerrorsプロパティに含まれています。ノードのエラーは、そのエラー・プロパティに含まれます。 schema_error_tree:使用されているスキーマと同じです。 n [2]: # %load 80_error_handling.py ...: from cerberus import Validator ...: from cerberus.errors import BAD_TYPE ...: ...: schema = {'cats': {'type': 'integer'}} ...: document = {'cats': 'two'} ...: ...: v = Validator() ...: ...: c = v.validate(document, schema) ...: assert c == False ...: assert BAD_TYPE in v._errors ...: ...: c = v.document_error_tree['cats'].errors ...: assert c == v.schema_error_tree['cats']['type'].errors ...: ...: assert BAD_TYPE in v.document_error_tree['cats'] ...: ...: c = v.document_error_tree['cats'][BAD_TYPE] ...: assert c == v.document_error_tree['cats'].errors[0] ...: ...: error = v.document_error_tree['cats'].errors[0] ...: assert error.document_path == ('cats',) ...: assert error.schema_path == ('cats', 'type') ...: assert error.rule == 'type' ...: assert error.constraint == 'integer' ...: assert error.value == 'two' ...: In [3]: エラーコード codeプロパティーは、具体的なエラーのコードとして使用される ErrorDefinition を一意に識別します。いくつかのコードは、異なるエラーの共有プロパティをマークするために実際に予約されています。これらは、エラーを処理する際のビットマスクとして役立ちます。 次の表は、予約されているコードです。 ビット 16進表記 10進表記 意味 0110 0000 0x60 96 正規化の際に発生したエラー 1000 0000 0x80 128 子エラーを含んだエラー 1001 0000 0x90 144 いずれかの*of-rulesが発したエラー 次の表は、cerberus.errors モジュールに同梱されているすべてのエラー定義の一覧です。 10進数 16進数 コード名 ルール 0 0x0 CUSTOM None 2 0x2 REQUIRED_FIELD required 3 0x3 UNKNOWN_FIELD None 4 0x4 DEPENDENCIES_FIELD dependencies 5 0x5 DEPENDENCIES_FIELD_VALUE dependencies 6 0x6 EXCLUDES_FIELD excludes 34 0x22 EMPTY_NOT_ALLOWED empty 35 0x23 NOT_NULLABLE nullable 36 0x24 BAD_TYPE type 37 0x25 BAD_TYPE_FOR_SCHEMA schema 38 0x26 ITEMS_LENGTH items 39 0x27 MIN_LENGTH minlength 40 0x28 MAX_LENGTH maxlength 65 0x41 REGEX_MISMATCH regex 66 0x42 MIN_VALUE min 67 0x43 MAX_VALUE max 68 0x44 UNALLOWED_VALUE allowed 69 0x45 UNALLOWED_VALUES allowed 70 0x46 FORBIDDEN_VALUE forbidden 71 0x47 FORBIDDEN_VALUES forbidden 72 0x48 MISSING_MEMBERS contains 96 0x60 NORMALIZATION None 97 0x61 COERCION_FAILED coerce 98 0x62 RENAMING_FAILED rename_handler 99 0x63 READONLY_FIELD readonly 100 0x64 SETTING_DEFAULT_FAILED default_setter 128 0x80 ERROR_GROUP None 129 0x81 MAPPING_SCHEMA schema 130 0x82 SEQUENCE_SCHEMA schema 131 0x83 KEYSRULES keysrules 131 0x83 KEYSCHEMA keysrules 132 0x84 VALUESRULES valuesrules 132 0x84 VALUESCHEMA valuesrules 143 0x8f BAD_ITEMS items 144 0x90 LOGICAL None 145 0x91 NONEOF noneof 146 0x92 ONEOF oneof 147 0x93 ANYOF anyof 148 0x94 ALLOF allof Cerberusの拡張 coerce や check_with ルールは関数と組み合わせて使うことができますが、 Validator クラスをカスタムルール、タイプ、check_with ハンドラ、coerce、default_setter で簡単に拡張することができます。関数ベースのスタイルは、特殊な用途や一度きりの使用に適していますが、カスタムクラスでの拡張は次のようなメリットがあります。 カスタムルールをスキーマの制約条件で定義できる 利用可能な型を拡張する 追加のコンテクストデータを使用できる スキーマはシリアライズ可能 これらのカスタムメソッドへのスキーマ内の参照には、アンダースコア記号(_)の代わりにスペース文字を使用できます。 {'foo': {'check_with': 'is odd'}} は {'foo': {'check_with': 'is_odd'}} と同じことになります。 カスタムルール 今回のユースケースでは、奇数の整数でしか表現できない値があるので、検証スキーマに新しい is_odd ルールのサポートを追加することにしたとします。 schema = {'amount': {'is odd': True, 'type': 'integer'}} これを実現するためには次のスニペットのようなコードになります。 from cerberus import Validator class MyValidator(Validator): def _validate_is_odd(self, constraint, field, value): """ Test the oddity of a value. The rule's arguments are validated against this schema: {'type': 'boolean'} """ if constraint is True and not bool(value & 1): self._error(field, "Must be an odd number") Cerberus の Validator クラスをサブクラス化し、カスタム _validate_<rulename> メソッドを追加することで、Cerberus を拡張することができます。カスタムルール is_odd がスキーマで利用可能になり、さらに重要なことに、このルールを使ってすべての奇数値を検証することができます。 In [2]: # %load 90_custom_validatr.py ...: from cerberus import Validator ...: ...: class MyValidator(Validator): ...: def _validate_is_odd(self, constraint, field, value): ...: """ Test the oddity of a value. ...: ...: The rule's arguments are validated against this schema: ...: {'type': 'boolean'} ...: """ ...: if constraint is True and not bool(value & 1): ...: self._error(field, "Must be an odd number") ...: ...: schema = {'amount': {'is odd': True, 'type': 'integer'}} ...: ...: v = MyValidator(schema) ...: c = v.validate({'amount': 10}) ...: assert c == False ...: assert v.errors == {'amount': ['Must be an odd number']} ...: ...: c = v.validate({'amount': 9}) ...: assert c == True ...: In [3]: スキーマ自体が検証されるように、そのルールのスキーマで与えられた引数を検証するために、ルールの実装メソッドの docstring で Pythonのリテラル表現として制約を提供することができます。docstring にリテラルのみが含まれるか、リテラルが docstring の一番下に置かれ、その前に The rule's arguments are validated against this schema. が付けられます。 カスタムデータタイプ Cerberusはいくつかの標準的なデータタイプをサポートし、検証します。カスタムバリデータを作成する際には、独自のデータタイプを追加して検証することができます。 types_mapping で指定した型の名前に TypeDefinition を割り当てることで、その場で型を追加することができます。 from decimal import Decimal decimal_type = cerberus.TypeDefinition('decimal', (Decimal,), ()) Validator.types_mapping['decimal'] = decimal_type 注意: types_mapping プロパティは mutable型なので、インスタンスの項目を変更すると、そのクラスにも影響を与えます。 Validatorのサブクラスに定義することもできます。 from decimal import Decimal decimal_type = cerberus.TypeDefinition('decimal', (Decimal,), ()) class MyValidator(Validator): types_mapping = Validator.types_mapping.copy() types_mapping['decimal'] = decimal_type check_withルールで参照可能なメソッド 検証テストがスキーマの指定された制約に依存しない場合や、 ルールよりも複雑にする必要がある場合は、 ルールではなく値のチェッカとして定義することができます。check_with ルールを使うには2つの方法があります。 ひとつは、Validator を拡張して、先頭に _check_with_ をつけたメソッドを作ることです。これにより、任意の設定値や状態を含むバリデータインスタンスのコンテキスト全体にアクセスできます。check_withルールを使ってこのようなメソッドを参照するには、接頭辞なしのメソッド名を文字列制約として渡すだけです。 たとえば、奇数番目のバリデータのメソッドを次のように定義することができます。 class MyValidator(Validator): def _check_with_oddity(self, field, value): if not value & 1: self._error(field, "Must be an odd number") 使い方は以下のようになります。 schema = {'amount': {'type': 'integer', 'check_with': 'oddity'}} ルールを使用する2つ目の方法は、スタンドアローンの関数を定義し、それを制約条件として渡すことです。この場合、Validatorを拡張する必要がないという利点があります。この実装についての詳細や例を見るには、ルールのドキュメントを参照してください。 カスタム強制 強制された結果を返すカスタム・メソッドや、 rename_handler としてメソッドを指定するカスタム・メソッドも定義できます。メソッド名の前には _normalize_coerce_ を付ける必要があります。 class MyNormalizer(Validator): def __init__(self, multiplier, *args, **kwargs): super(MyNormalizer, self).__init__(*args, **kwargs) self.multiplier = multiplier def _normalize_coerce_multiply(self, value): return value * self.multiplier In [2]: # %load 91_custom_coercers.py ...: from cerberus import Validator ...: ...: class MyNormalizer(Validator): ...: def __init__(self, multiplier, *args, **kwargs): ...: super(MyNormalizer, self).__init__(*args, **kwargs) ...: self.multiplier = multiplier ...: ...: def _normalize_coerce_multiply(self, value): ...: return value * self.multiplier ...: ...: schema = {'foo': {'coerce': 'multiply'}} ...: document = {'foo': 2} ...: ...: c = MyNormalizer(2).normalized(document, schema) ...: assert c == {'foo': 4} ...: In [3]: カスタムデフォルトセッター カスタムリネームハンドラと同様に、カスタムデフォルトセッタを作成することも可能です。 from datetime import datetime class MyNormalizer(Validator): def _normalize_default_setter_utcnow(self, document): return datetime.utcnow() In [2]: # %load 92_custom_default_setters.py ...: from cerberus import Validator ...: from datetime import datetime ...: ...: class MyNormalizer(Validator): ...: def _normalize_default_setter_utcnow(self, document): ...: return datetime.utcnow() ...: def _normalize_default_setter_anniversary(self, document): ...: return datetime(2020, 10, 2) ...: ...: schema = {'creation_date': {'type': 'datetime', ...: 'default_setter': 'anniversary'}} ...: ...: c = MyNormalizer().normalized({}, schema) ...: assert c == {'creation_date': datetime(2020, 10, 2, 0, 0)} ...: In [3]: よく使用してる特定のルールを上書きするのは良くないかもしれません。 設定データの添付とカスタム・バリデータのインスタンス化 Validator やそのサブクラスをインスタンス化する際に、任意の構成値をキーワード引数 (Cerberus では使用しない名前) として渡すことができます。これらの値は、そのインスタンスにアクセスできる、このドキュメントで説明するすべてのハンドラーで使用できます。Cerberusは、処理中に生成される可能性のあるすべての子インスタンスで、このデータが利用可能であることを保証します。カスタマイズされたバリデータの __init__()を実装する際には、すべての位置引数とキーワード引数が親クラスの初期化メソッドにも渡されるようにしなければなりません。以下にパターンの例を示します。 class MyValidator(Validator): def __init__(self, *args, **kwargs): # インスタンス・プロパティに構成値を割り当てて利便性を高める self.additional_context = kwargs.get('additional_context') # すべてのデータをベースクラスに渡す super(MyValidator, self).__init__(*args, **kwargs) # また、ダイナミック・プロパティを定義することで、 # この例では__init__()が不要になります。 @property def additional_context(self): return self._config.get('additional_context', 'bar') # 状態を扱う場合のオプションのプロパティセッター @additional_context.setter def additional_context(self, value): self._config["additional_context"] = value def _check_with_foo(self, field, value): make_use_of(self.additional_context) 警告 上記で説明した以外の状況で _config プロパティにアクセスすることや、ドキュメントの処理中にその内容を変更することは推奨されません。これらのケースはテストされておらず、公式にサポートされる可能性は低いです。 関連するValidatorクラスの 属性 カスタムバリデータを書く際に注意すべき、Validatorクラスの属性があります。 Validator.document バリデータは、検証用のフィールドを取得する際に document プロパティにアクセスします。これにより、フィールドの検証をドキュメントの他の部分と関連づけて行うことができます。 Validator.schema 同様に、schema プロパティは使用するスキーマを保持します。 この属性は、ある時点でバリデータにスキーマとして渡されたオブジェクトとは異なります。また、その内容も異なる可能性がありますが、 初期の制約を表していることには変わりありません。この属性は、dict と同じインターフェイスを提供します。 Validator._error Validator のエラー・スタッシュにエラーを提出するために受け入れられる署名は3つあります。必要に応じて、与えられた情報は解析されて新しいValidationErrorのインスタンスが作成されます。 完全な開示 エラーの内容を後から完全に把握するためには、 _error() に 2 つの必須の引数を指定する必要があります。 エラーが発生したフィールド ErrorDefinition のインスタンス カスタムルールでは、一意の ID を持つ ErrorDefinition としてエラーを定義し、 違反したルールの原因を特定する必要があります。拠出されたエラー定義のリストは、errorsを参照してください。ビット7はグループエラーを示し、ビット5は異なるルールセットに対する検証で発生するエラーを示すことに注意してください。 必要に応じて、さらなる引数を情報として提出することができます。人間を対象としたエラーハンドラは、str.format() でメッセージをフォーマットする際に、これらを位置引数として使用します。シリアライズハンドラは、これらの値をリストにしておきます。 シンプルなカスタムエラー よりシンプルな方法は、フィールドとメッセージとしての文字列を指定して _error() を呼び出すことです。しかし、結果として生じるエラーには、違反した制約に関する情報は含まれません。これは後方互換性を維持するためのものですが、詳細なエラー処理が必要でない場合にも使用できます。 複数のエラー チャイルドバリデーターを使用する際には、そのエラーをすべて提出すると便利です。 これは ValidationError インスタンスのリストです。 Validator._get_child_validator 自分のサブクラスである Validator の別のインスタンスが必要な場合は、_get_child_validator()メソッドで self と同じ引数を指定して別のインスタンスを返します。キーワード引数をオーバーライドして指定することもできます。document_path と schema_path (下記参照) のプロパティは子バリデータに継承されるので、 document_crumb と schema_crumb というキーワードで単一の値または値のタプルを渡すことで、これらを拡張することができます。 Validator.root_document,、.root_schema,、.root_allow_unknown、 .root_require_all] 子バリデータ (スキーマを検証するときに使用するもの) は、 root_document、 root_schema、root_allow_unknown および root_require_all プロパティを使って、 第一世代のバリデータが処理しているドキュメントやスキーマ、 未知のフィールドに対する制約にアクセスできます。 Validator.document_path、Validator.schema_path これらのプロパティは、親バリデータが通過したドキュメント内のキーのパスと、スキーマのパスを保持します。これらのプロパティは、エラーが発生したときのベースパスとして使われます。 Validator.recent_error 最後に送信されたエラーは、recent_errorプロパティでアクセスできます。 Validator.mandatory_validations、Validator.priority_validations、Validator._remaining_rules これらのクラス・プロパティやインスタンス・プロパティは、各フィールドの検証ロジックを調整したい場合に使うことができます。 mandatory_validations は、スキーマのフィールドにルールが定義されているかどうかにかかわらず、各フィールドに対して検証されるルールを含むタプルです。 priority_validations は、他のルールよりも先に検証される順序付けられたルールのタプルです。_remaining_rules は、これらを考慮して作成されたリストで、次に評価されるべきルールを追跡します。したがって、ルールハンドラで操作して、現在のフィールドに対する残りの検証を変更することができます。できれば、_drop_remaining_rules()を呼んで特定のルールを削除したり、一度にすべてのルールを削除したりしたいところです。 エラーメッセージの日本語化 カスタムエラーハンドラーを設定することで、エラーメッセージを日本語化することができます。 BaasicErrorHandlerを継承した JapanseErrorHandlerを作成します。 cerberus_extend.py from cerberus.errors import BasicErrorHandler class JapaneseErrorHandler(BasicErrorHandler): def __init__(self, tree = None): super(JapaneseErrorHandler, self).__init__(tree) self.messages = { 0x00: "{0}", 0x01: "ドキュメントが見つかりません", 0x02: "必須項目です", 0x03: "不明な項目が指定されています", 0x04: "'{0}'は必須項目です", 0x05: "これらの値に依存します: {constraint}", 0x06: "{0} はフィールド'{field}'にセットされてる必要があります", 0x21: "'{0}'はドキュメントではありません。辞書でなければなりません", 0x22: "必須項目です", 0x23: "必須項目です", 0x24: "{constraint}型でなければなりません", 0x25: "辞書型でなければなりません。", 0x26: "リストの長さは{constraint}なければなりませんが、{0}です", 0x27: "{constraint}文字以上入力してください", 0x28: "{constraint}文字以内で入力してください", 0x41: "値が正規表現に一致しません:'{constraint}'", 0x42: "{constraint}以上の値を入力してください", 0x43: "{constraint}以下の値を入力してください", 0x44: "{value}は指定できません", 0x45: "{0}は指定できません", 0x46: "{value}は指定できません", 0x47: "{0}は指定できません", 0x48: "{0}メンバーが見つかりません", 0x61: "フィールド'{field}'は強制できません: {0}", 0x62: "フィールド'{field}'はリネームできません: {0}", 0x63: "フィールドはリードオンリーです", 0x64: "フィールド'{field}'にデフォルト値{0}をセットできません", 0x81: "マッピングがサブスキーマを検証しません: {0}", 0x82: "ひとつ以上のシーケンス要素が検証されていません: {0}", 0x83: "マッピングのひとつ以上のキーが検証されていません: {0}", 0x84: "マッピングのひとつ以上の値が検証されていません: {0}", 0x81: "マッピングがサブスキーマを検証しません: {0}", 0x91: "ひとつまたは複数の定義を有効です", 0x92: "ゼロもしくは複数のルールの検証です", 0x93: "定義されていない検証です", 0x94: "1つまたは複数の定義が検証されません" } 利用するときはValidator() の error_handler引数にこのクラスを与えます。 v = Validator(schema, error_handler=JapaneseErrorHandler()) 動作確認をしてみましょう。 In [2]: # %load 93_localization.py ...: from cerberus import Validator ...: from cerberus_extend import JapaneseErrorHandler ...: ...: # 参照: 52_items.py ...: ...: schema = {'list_of_values': { ...: 'type': 'list', ...: 'items': [{'type': 'string'}, {'type': 'integer'}]} ...: } ...: v = Validator(schema, error_handler=JapaneseErrorHandler()) ...: ...: document = {'list_of_values': ['hello', 100]} ...: c1 = (v.validate(document), v.errors) ...: assert c1[0] == True ...: ...: document = {'list_of_values': [100, 'hello']} ...: c2 = (v.validate(document), v.errors) ...: assert c2[0] == False ...: assert c2[1] != {'list_of_values': ...: [{0: ['must be of string type'], ...: 1: ['must be of integer type']}]} ...: assert c2[1] == {'list_of_values': ...: [{0: ['string型でなければなりません'], ...: 1: ['integer型でなければなりません']}]} ...: In [3]: 参考 Cerberus ドキュメント Cerberus ソースコード Wikipadia Cerberus
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

球面調和関数の値をPython3/matplotlibで3D表示する

2次元の調和関数の値を、r=1の円上で均等にスキャンして合計するとゼロになるらしい。 3次元の調和関数の値を、r=1の球面上で均等にスキャンして合計するとゼロになるかPythonで試してみた。 その前に、値を3次元でmatplotlibを使い表示させてみた。 3次元調和関数 {{\displaystyle {\partial ^{2} \over \partial x^{2}}\phi (x,y,z)+{\partial ^{2} \over \partial y^{2}}\phi (x,y,z)+{\partial ^{2} \over \partial z^{2}}\phi (x,y,z)=0.} } これ満たす式で、一番簡単なのをネットで見つける。 f(x,y,z) = - xy - yz - zx この式の値を r=1 の球面上の点で値を取り、色付けをしてプロットする。普通に、緯度、経度で分割すると極部の点が多くなるので、経線の長さに応じて点の数を調整している。真ん中の白い部分は、極です。 濃い赤が大きなプラス、濃い青が大きなマイナスを示しており、球面全体の平均を取るとゼロになりそうな配置である。実際にPython3でプログラムを実行してもらうと、スクロールして見れます。 プログラムは、こちら harmony.py import math from mpl_toolkits import mplot3d import numpy as np import matplotlib.pyplot as plt from matplotlib import cm def f(x,y,z): return - x*y - y*z - z*x fig = plt.figure() ax = plt.axes(projection='3d') c = 0 N = 50 zdata = np.linspace(0,0,2*N*N) xdata = np.linspace(0,0,2*N*N) ydata = np.linspace(0,0,2*N*N) udata = np.linspace(0,0,2*N*N) for j in range(0,N): ps = math.pi*(j+0.5)/N w = int(N*math.sin(ps)) for i in range(-1*w,w): th = math.pi*(i+0.5)/w z = math.cos(ps) x = math.sin(ps)*math.cos(th) y = math.sin(ps)*math.sin(th) xdata[c] = x ydata[c] = y zdata[c] = z u = f(x,y,z) udata[c] = u c = c + 1 ax.set_aspect('equal') ax.scatter3D(xdata, ydata, zdata, c=udata, cmap=cm.coolwarm); plt.show() 関数f()を、いろいろ変えると、きれいな調和関数を3D表示できるので、次回。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

プログラミング学習Day3(変数・演算子、条件分岐、文字列変換)

 変数、演算子 変数 1文字以上の名前を持っていて値が変わるもの プログラムは上から下に実行される プログラミングは英語でエラーが表示されるのでなんとなく英語でエラーの意味が分かるように。例えばEOL=End Of Line(最後の行) 比較演算子 >  より大きい <  より小さい >=  以上 =<  以下 ==  等値 !=  非等値 論理演算子 and 左右の条件がTrueであればTrueになる or  左右の条件のどちらかがTrueであればTruenになる not True,Falseのを逆にする ちっちゃすぎす少数は無視? 論理演算子 == で下記の事を発見しました。 a = 1 b = 1.000000000000001 c = 1.0000000000000001 d = 1.0000000000000011 e = 1.0000000000000002 print(a == b) print(a == c) print(b == d) print(a == e) 実行結果 False #わかる True #あ、少数第16位以下は無視されるのか? True #うん、やっぱり少数第16位以下は無視されるんだな False #いや、少数第16位以下でも2やったら無視されへんのかい このへん、なんか奥が深そうなのでそっとしておきます。 条件分岐 if 条件式:を使うと条件式に当てはまるものだけその後のアクションを実行できる。例えばaという変数があって、aが10の時だけOKと出力したい場合 a = 10 if a == 10: print("OK") 出力結果は下記の通り a = 10 の時 OK a = 1 の時 なにも出力されません。 何も出力されないのが不安だという場合。条件に当てはまらないときの実行をelseを使って指示することも出来ます。 aが10の場合はOK, aが10 でない場合はNGと出すなら下記のように書けます。elseを付けてあげるとifに当てはまらない場合の実行結果を指示できます。 a = 10 if a == 10: print("OK") else: print("NG") aが10の場合 OK aが10以外の場合 NG その他にも条件を複数設定した場合はelifというものもある。 文字列操作 文字をつなげる + 文字列を探す find 文字列を大文字にするupper 文字列を小文字にするlower 文字列を変換するreplace 以下使用例 str = "I am happy." #例文 print(str + str) #文字列を繋げる print(str.upper()) #全部大文字に変換 print(str.lower()) #全部小文字に変換 print(str.replace("I am", "You are")) #文字列を置き換え 出力結果 I am happy.I am happy. #strの中身が繋げられて繰り返し表示される I AM HAPPY. #全部大文字になっている i am happy. #全部小文字になっている You are happy. #"I am"が"You are"に変化されている 疑問 下記の書き方では出力は元の文字列のままになってしまう str = "Happy" str.upper() print(str) 出力結果 Happy 2行目の大文字変換の関数が3行目のプリント関数に影響しないということだと思うがなぜこんな仕様なのだろうか。もっと勉強していけばわかるのだろうか。。。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

高精度気圧センサーモジュール - DPS310 をPythonで動くようにした

よく調べようという教訓が得られます。 初めに Raspberry Piで気圧ロガーを作るためにセンサーモジュールを買ったが、販売元のページにはArduino用のCライブラリしか無かったので、仕方なくPythonのドライバを書いたら、実はチップメーカーのGithubにすでにPythonのコードが公開されていた。というオチの記録です。 目標 気圧センサを使用したデータロガーを作る。 環境 Raspberry Pi 4 model B 4GB(Raspberry Pi OS arm64 Release:10 Codename:buster ) この記事で作成したDockerコンテナ Python 3.9.2 RPi.GPIO, i2c-tools, smbus :などが利用可能 grove.py :Grove Base Hat for Raspberry PiとGroveシステムのセンサ類のライブラリ Grove Base Hat for Raspberry Pi GPIO,I2C,ADC,URAT,PWMを手軽に使えるようにするコネクターがいっぱい生えたHAT、便利 Grove - High Precision Barometric Pressure Sensor DPS310 高精度な気圧センサモジュール infineon社のDPS310を搭載 I2C接続で使用 リソース 販売店のページには以下のリンクがある。 製造元Wiki:Seeed Technology社解説Wikiページへのリンク Githubライブラリ:搭載センサーのメーカーinfineonのDPS310ライブラリリポジトリ(Arduino用) データシート:搭載センサーのメーカーinfineonのDPS310データシートへのリンク(リンク先はSeeed社) 回路図:モジュールの回路図やCADデータ 製造元WikiにはArduino用のリソースはあるがRaspberry Pi用はToDoとなっており記載がない 製造元Wikiに記載があるSeeed社のリポジトリもArduino用でした。 ここまで見て仕方ないので自分で書くしかないかなと思いセンサーを注文。 Pythonコードの作成 データシートとにらめっこしながらPythonでドライバを書きます。 ついでにCSVファイルを保存してデータロガーにしてしまいます。 infineon社ライブラリのCのコードを多分に参考してます。参考:ライセンス pressure_DPS310_csv.py import time from grove.i2c import Bus import multi_timer import csv import datetime READ_INTERVAL = 1/128 # [sec] FILE_SAVE_INTERVAL = 3600 # [sec] SAVE_DIR = 'press_log/' # i2c address setting ADDRESS = 0x77 # Pressure Configuration (PRS_CFG) PRESS_CONF = 0x71 # Pressure measurement rate: 0111 XXXX- 128 measurements pr. sec. # Pressure oversampling rate: XXXX 0001 - 2 times (Low Power). Precision : 1 PaRMS. # 0111 0001 = 0x71 highest measurements rate # DPS310 data sheet P.29 - 30 # Temperature Configuration(TMP_CFG) TEMP_CONF = 0xF0 # Temperature measurement: 1XXX XXXX - External sensor (in pressure sensor MEMS element) # Temperature measurement rate: X111 XXXX - 128 measurements pr. sec. # Temperature oversampling (precision): 0000 - single. (Default) - Measurement time 3.6 ms. # 1111 0000 = 0xF0 highest measurements rate # DPS310 data sheet P.31 # Interrupt and FIFO configuration (CFG_REG) INT_AND_FIFO_CONF = 0x00 # T_SHIFT: bit3: Temperature result bit-shift: 0 - no shift result right in data register. # P_SHIFT: bit2: Pressure result bit-shift: 0 - no shift result right in data register. # not use interrupts.: 0000 XXXX # not use FIFO. : XXXX XX0X # not use SPI. : XXXX XXX0 # 0000 1100 = 0x00 # DPS310 data sheet P.33 # Sensor Operating Mode and Status (MEAS_CFG) OP_MODE = 0x07 # 111 - Background Mode Continous pressure and temperature measurement # 0000 0111 = 0x07 # DPS310 data sheet P.32 # Compensation Scale Factors SCALE_FACTOR_KP = 1572864 # Oversampling Rate 2 times (Low Power) SCALE_FACTOR_KT = 524288 # Oversampling Rate 1 (single) # DPS310 data sheet P.15 class pressure_sensor_DPS310(): def __init__(self): time.sleep(0.1) # I2C bus self.bus = Bus(None) # Measurement Settings self.bus.write_byte_data(ADDRESS, 0x06, PRESS_CONF) self.bus.write_byte_data(ADDRESS, 0x07, TEMP_CONF) self.bus.write_byte_data(ADDRESS, 0x09, INT_AND_FIFO_CONF) self.bus.write_byte_data(ADDRESS, 0x08, OP_MODE) def __getTwosComplement(self, raw, length): value = raw if raw & (1 << (length - 1)): value = raw - (1 << length) return value def read_calibration_coefficients(self): # Read Calibration Coefficients reg = {} for i in range(0x10, 0x22): reg[i] = self.bus.read_byte_data(ADDRESS,i) Factors = {} Factors['c0'] = self.__getTwosComplement(((reg[0x10]<<8 | reg[0x11])>>4), 12) Factors['c1'] = self.__getTwosComplement(((reg[0x11] & 0x0F)<<8 | reg[0x12]), 12) Factors['c00'] = self.__getTwosComplement((((reg[0x13]<<8 | reg[0x14])<<8 | reg[0x15])>>4), 20) Factors['c10'] = self.__getTwosComplement((((reg[0x15] & 0x0F)<<8 | reg[0x16])<<8 | reg[0x17]), 20) Factors['c01'] = self.__getTwosComplement((reg[0x18]<<8 | reg[0x19]), 16) Factors['c11'] = self.__getTwosComplement((reg[0x1A]<<8 | reg[0x1B]), 16) Factors['c20'] = self.__getTwosComplement((reg[0x1C]<<8 | reg[0x1D]), 16) Factors['c21'] = self.__getTwosComplement((reg[0x1E]<<8 | reg[0x1F]), 16) Factors['c30'] = self.__getTwosComplement((reg[0x20]<<8 | reg[0x21]), 16) return Factors def __calc_temp(self, raw_temp, Factors): scaled_temp = raw_temp / SCALE_FACTOR_KT # Traw_sc = Traw/kT compd_temp = Factors['c0'] * 0.5 + Factors['c1'] * scaled_temp # Tcomp (°C) = c0*0.5 + c1*Traw_sc return scaled_temp, compd_temp def read_temperature(self, Factors): reg = {} # read raw temperature for i in range(0x03, 0x06): reg[i] = self.bus.read_byte_data(ADDRESS,i) raw_temp = self.__getTwosComplement(((reg[0x03]<<16) | (reg[0x04]<<8) | reg[0x05]), 24) # calculate temperature scaled_temp, compd_temp = self.__calc_temp(raw_temp, Factors) return scaled_temp, compd_temp def __calc_press(self, raw_press, scaled_temp, Factors): # Praw_sc = Praw/kP scaled_press = raw_press / SCALE_FACTOR_KP # Pcomp(Pa) = c00 + Praw_sc*(c10 + Praw_sc *(c20+ Praw_sc *c30)) # + Traw_sc *c01 + Traw_sc *Praw_sc *(c11+Praw_sc*c21) compd_press = Factors['c00'] + scaled_press * (Factors['c10'] + scaled_press\ * (Factors['c20'] + scaled_press * Factors['c30']))\ + scaled_temp * Factors['c01'] + scaled_temp * scaled_press\ * (Factors['c11'] + scaled_press * Factors['c21']) return compd_press def read_pressure(self, scaled_temp, Factors): reg = {} for i in range(0x00, 0x03): reg[i] = self.bus.read_byte_data(ADDRESS,i) raw_press = self.__getTwosComplement(((reg[0x00]<<16) | (reg[0x01]<<8) | reg[0x02]), 24) compd_press = self.__calc_press(raw_press,scaled_temp, Factors) return compd_press def main(): INTERVAL = READ_INTERVAL timer = multi_timer.multi_timer(INTERVAL) dps310 = pressure_sensor_DPS310() # Instance creation Factors = dps310.read_calibration_coefficients() # Read Calibration Coefficients while True: today = datetime.date.today() filename = str(SAVE_DIR + today.strftime('%Y%m%d') + '-' + time.strftime('%H%M%S') + '.csv') try: with open(filename, 'w', newline='') as f: writer = csv.writer(f) file_start_time = time.time() while True: timer.timer() if timer.up_state == True: timer.up_state = False # process scaled_temp ,temperature = dps310.read_temperature(Factors) # read and compensation temperature press = dps310.read_pressure(scaled_temp, Factors) # read and compensation pressure data = [str(time.time()),str(press)] writer.writerow(data) if file_start_time+FILE_SAVE_INTERVAL <= time.time(): break finally: bus = Bus(None) bus.write_byte_data(ADDRESS, 0x08, 0x00) if __name__ == "__main__": main() 上記コードは、時間分解能を上げたかったのでハイレートなセッティングです。 せっかくの精度を生かすなら4サンプル/secくらいで定数を READ_INTERVAL = 1/4 # [sec] PRESS_CONF = 0x2E # 4spp 64 times oversampling TEMP_CONF = 0xA0 # 4spp INT_AND_FIFO_CONF = 0x04 # Pressure result bit-shift enable SCALE_FACTOR_KP = 1040384 # 64 times (High Precision) こんな感じに設定するといいと思います。 importしているタイマーは自作のクラスで単純なポーリングタイマーです。 multi_timer.py import time class multi_timer(): ''' Multiple timers can be used simultaneously in this class. Create an instance with the interval [sec] as an argument. ''' def __init__(self, interval): self.last_time = 0.0 self.up_state = False self.interval = interval def timer(self): ''' The timer method compares the current time with the interval and updates the up_state at each call. ''' if self.last_time + self.interval <= time.time(): self.last_time = time.time() self.up_state = True # Usage Example def main(): INTERVAL_1s = float(1.0) # Enter the interval time in seconds INTERVAL_10s = float(10.0) # Enter the interval time in seconds # timer instance creation timer_1s = multi_timer(INTERVAL_1s) timer_10s = multi_timer(INTERVAL_10s) while True: timer_1s.timer() # Call the method to update the timer if timer_1s.up_state == True: timer_1s.up_state = False # required to manually rewrite 'up_state' to False # Write the process here print("1sec: " + str(time.time())) timer_10s.timer() if timer_10s.up_state ==True: timer_10s.up_state = False # required to manually rewrite 'up_state' to False # Write the process here print("10sec: " + str(time.time())) return if __name__ == "__main__": main() 実行するとカレント配下のpress_log/にCSVファイルが出力されます。 date-time.csv 1633533232.8374667,97542.54109208909 1633533232.8451686,97542.48074125986 1633533232.8530242,97543.69100189407 1633533232.8607929,97543.91064694956 1633533232.8689406,97543.04499677646 1633533232.8765638,97544.49214130614 1633533232.884714,97543.75997487147 1633533232.8925848,97543.13983414807 1633533232.9000964,97543.26033221664 1633533232.9077988,97542.63161833295 書いている途中(ほぼ完成)で見つけてしまったリポジトリ あるじゃないですか(涙) おわりに デバイスの仕様書を読みながらセンサを使えるようにするのは良いトレーニングになります。(涙) 動作はしましたが、精度の検証とかテストはしていないのでバグがあったらすみません。 参考文献 探すのを止めた時見つかることはよくある話で - Infineon/RaspberryPi_DPS - 高精度気圧センサDPS310の実力を試してみた 【前編】 - 高精度気圧センサDPS310の実力を試してみた 【後編】
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Herokuの使い方を勉強した。【HerokuでPythonを定期実行したい】

Pythonを定期実行したかったので、Herokuの使い方を勉強してみました!   具体的には、Herokuの公式チュートリアル 「Getting Started on Heroku with Python(PythonでHerokuをはじめよう)」 を日本語に翻訳しつつ、勉強した内容をまとめました。 Herokuの公式チュートリアルを勉強する セットアップ Gitを使えるようにする。 まず、Heroku CLIを使うには、バージョン管理システムであるGitが必要です。 まだインストールしていない場合は、インストールして設定をします。 Heroku Command Line Interface (CLI)をインストールする。 Herokuのインストーラーはこちらのページから入手できます。 僕はWindowsを使っているので、Windows用のインストーラーの表示に従ってインストールしました。 インストールが完了すると、コマンドシェルからherokuコマンドを使用可能になります。 Herokuへログインする。 PowerShellにheroku loginコマンド $ heroku login を入力します。 すると、ブラウザが起動して「Herokuにログインしますか?」的な確認画面が出てくるので、受け入れます。 アプリを準備する Heroku側が、勉強用サンプルアプリ↓を作ってくれているので これを自分のローカル環境にclone(コピー)します。 具体的には、まずサンプルアプリを保存したい任意の場所(デスクトップとか)に、PowerShellにcdコマンドを入力して移動します。 ※例 $ cd 任意の場所 その後 $ git clone https://github.com/heroku/python-getting-started.git を実行して、ローカル環境にサンプルアプリのクローンを作成します。 「python-getting-started」というサンプルアプリのディレクトリ(フォルダ)が作成されるはずです。 生成されたファイルについて 「python-getting-started」内には、いろいろなファイルが生成されているはずです。 主なファイルの役割を調べてみました。 Procfile(プロクファイル) Heroku 上で動かしたいコマンドの一覧を指定するファイル。(拡張子は無し) Procfile.windows Windowsでローカル開発を行うためのProcfile(プロクファイル)。 runtime.txt インストールするPythonのバージョンを書いておくファイル。 (Herokuへデプロイ時に指定したバージョンのPythonがインストールされる。) requirements.txt ローカルにインストールしてあるPythonのライブラリを書いておくファイル。 (Herokuへデプロイ時に指定したPythonのライブラリがインストールされる。) アプリをデプロイする 続いて、そのままcdコマンドでディレクトリ(フォルダ)の中へ移動します。 $ cd python-getting-started そこでheroku createコマンドを実行すると… $ cd heroku create Creating app... done, ⬢ serene-caverns-82714 https://serene-caverns-82714.herokuapp.com/ | https://git.heroku.com/serene-caverns-82714.git みたいな感じで、gitリモート(herokuと呼ばれる)も作成され、ローカルのgitリポジトリに関連付けられます。 Herokuはアプリにランダムな名前(ここではseren-caverns-82714)を生成します。 ちなみに $ heroku create アプリ名 とすると任意のアプリ名を指定できます。 (しかし、数字始まりのアプリ名や、既に他のユーザー使用されているアプリ名と被っているのはダメっぽいです。) コードをデプロイする そのままpush heroku mainコマンドを実行すると、コードをデプロイしてくれます。 $ git push heroku main 実行が終わったらheroku ps:scale web=1コマンドで $ heroku ps:scale web=1 アプリのインスタンスが、少なくとも1つ実行されているかを確認します。 「now running」みたいなメッセージが返ってきたら成功だと思います。 実際に確認する 次に、先ほどアプリ名で生成されたURL 例 https://serene-caverns-82714.herokuapp.com/ でアプリにアクセスします。 ショートカットとして、heroku openコマンドでもWebサイトを開けます。 $ heroku open こんな感じの画面になっていたら成功だと思います。 ログの表示方法を知る 公式チュートリアルによると 「Herokuはログを、すべてのアプリとHerokuコンポーネントの出力ストリームから集約された、時間順に並べられたイベントのストリームとして扱い、すべてのイベントに対して単一のチャンネルを提供します。」 とのこと。 …正直、ちょっと何言ってるかよく分からない。笑 「アプリを複数作っても、ログはまとめて時系列順で表示されるよ。」ってことでしょうか。 とりあえずheroku logs --tailコマンド $ heroku logs --tail で直近の実行中アプリのログが見れるっぽいです。 Procfile(プロクファイル)を定義する 先ほども紹介した「python-getting-started」の中にある「Procfile(プロクファイル)」。 Heroku 上で動かしたいコマンドの一覧を指定するファイルみたいです。(拡張子は無し) ※Procfileは必ずルートディレクトリに置く。 アプリの拡張 ここまで確認してきたアプリは、"Dyno(仮想サーバー)"上で動作しているみたいです。 いくつの"Dyno"が稼働しているかは、psコマンドで確認できます。 $ heroku ps "Dyno"は複数使えるので、アプリの負荷が高くなってきたら"Dyno"を増して対応します。 (スケールアウトって手法だと思います。) ※"Dyno"の数は、こんな感じ↓で設定・変更する。 $ heroku ps:scale web=1 無料用Dynoと課金について デフォルトでは、アプリは"無料用Dyno"にデプロイされるようです。 "無料用Dyno"は、30分ほど活動しないと(トラフィックを受け取らないと)スリープします。 そして、スリープ解除後の最初のリクエストに数秒の遅延が生じるようです。 「そうならない"Dyno"を使いたい場合は課金してね」ってわけです。 ※"無料用Dyno"でも使用時間が1000時間/月以上使いたい場合は、課金が必要みたいです。 アプリの依存関係を宣言する 先ほども紹介した「python-getting-started」の中にある「requirements.txt」。 ローカルにインストールしてあるPythonのライブラリを書いておくファイル。 サンプルアプリには django gunicorn django-heroku が記述されています。 Herokuへデプロイ時に指定したPythonのライブラリがインストールされるみたいです。 ローカル環境にも同じライブラリをインストールしたい場合は $ pip install -r requirements.txt で実行できるみたいです。 (上手くいかない場合、Postgresを正しくインストールしてある必要があるみたいです。) アプリをローカルで実行する ここらへんは、HerokuというよりDjangoの使い方です。 まず、collectstatic を実行します。 $ python manage.py collectstatic Djangoの使い方詳しくないので分かりませんけど、必要なファイルを生成するコマンドだと思います。 そして、以下のコマンドを実行します。 heroku local web -f Procfile.windows (この時に、Windowsでローカル開発を行うためのProcfileである「Procfile.windows」を使うわけですね。) しばらくすると、ローカルサーバーが立ち上がって「localhost:5000」から先ほどと同じページが閲覧できます。 ローカルサーバーの停止はCtrl+Cで行えます。 ローカル環境での変更をHerokuに反映させる方法 試しに変更してみる まず、公式チュートリアル通りにローカルにpythonのライブラリ「requests」をインストールします。 (僕は既に入れてありました。) $ pip install requests 次に、requirements.txtに「requests」を書き加えます。 そして、hello/views.pyを書き換えます。 ここにある↑ファイルですね。 requestsをインポートして、関数「index」を指定通りに書き換えます。 views.py from django.shortcuts import render from django.http import HttpResponse from .models import Greeting import requests # ← 「requests」をインポートする!!!! # Create your views here. # def index(request): # # return HttpResponse('Hello from Python!') # return render(request, "index.html") # ↓関数「index」を書き換える!!!!! def index(request): r = requests.get('http://httpbin.org/status/418') print(r.text) return HttpResponse('<pre>' + r.text + '</pre>') 再びローカルサーバーを立ち上げて、localhost:5000 にアクセスすると… ページの表示がティーポットのアスキーアートになっています。 変更をHerokuへデプロイ この状態をHerokuへデプロイします。 まず、変更した全てのファイルをローカルの git リポジトリに追加します。 $ git add . 次に、変更内容をリポジトリにコミットします。 コミットメッセージは「Demo」とします。 $ git commit -m "Demo" Herokuへデプロイします。 $ git push heroku main Herokuへデプロイが完了したら、確認します。 $ heroku open で、アプリのURLへアクセスできましたよね。 アクセスすると… ちゃんとティーポットになっていたら成功! git cloneしない場合 ここまで公式チュートリアル通りにgit cloneして進めてきました。 任意のフォルダのアプリをHerokuへデプロイしたい場合は $ heroku create アプリ名 で新規アプリを作った後、 自分でruntime.txt と requirements.txtを用意して $ git init $ git remote add heroku https://git.heroku.com/アプリ名.git $ git add . $ git commit -m "コミットメッセージ" $ git push heroku master でもいけるはずです。 ファイルの定期実行がしたい さて。なんとなくHerokuの概念は理解できてきた気がします。 しかし、今回はDjangoで作ったWebアプリをデプロイしたいのではなく、Pythonファイルを定期実行したいわけです。 定期実行をするための設定をしていきたいと思います。 定期実行するファイルを用意する 僕は自分にLINEを送るプログラムを定期実行させてみます。 LineSend.py import requests import datetime TOKEN = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' #トークン # APIのURL(エンドポイント)を変数に代入。 api_url = 'https://notify-api.line.me/api/notify' # ------------------------------------------- # メッセージを送信する関数 def send_message(send_text, token): # 情報を辞書型にする TOKEN_dic = {'Authorization': 'Bearer' + ' ' + token} # 送りたいコンテンツ send_contents = f'{send_text}' send_dic = {'message': send_contents} # LINE通知を送る(200: 成功時、400: リクエストが不正、401: アクセストークンが無効:公式より) requests.post(api_url, headers=TOKEN_dic, data=send_dic) # 現在時刻を取得 time = datetime.datetime.now() time = time.strftime('%Y年%m月%d日 %H:%M:%S') # 送りたいテキスト send_text = f'\n{time}\nテスト送信' # メッセージを送信する send_message(send_contents, TOKEN) ※アクセストークンを発行するためには、LINE Notifyに登録する必要があります。 ↓詳しくは、こちらの動画が参考になると思います。 Herokuへデプロイする 先ほどと同じ要領で、用意したpyファイルをHerokuへデプロイします。 (もう一度新たに定期実行用のフォルダを作ってHerokuの設定をしても良いですけど… とりあえず、練習ならそのまま「python-getting-started」の中に入れると手っ取り早そうです。) アドオン(拡張機能)を設置する方法 Herokuへデプロイしたファイルの定期実行をするには、「スケジューラ」のアドオンを使います。 まず、スケジューラーを使うには、クレジットカード情報の登録が必要です。 HerokuのAccount settings→Billingタブを開いてクレジットカード情報を登録します。 (※登録したからといってプランを変えない限り、勝手には課金されないみたいです。) コマンドを使って「スケジューラ」のアドオンを追加します。 $ heroku addons:add scheduler:standard コマンドを実行して、「Created scheduler-アプリ名」みたいなのが出てきたらOKだと思います。 スケジューラのジョブを設定をする スケジューラのアドオンを追加したら、ジョブを設定するページへ移動します。 $ heroku addons:open scheduler を実行すると、ブラウザで設定ページが開かれます。 Create jobボタンを押して、ジョブを作成します。 ここに実行間隔と、実行したいpyファイルを設定するわけですね。 時刻はUTC(協定世界時)で指定します。 (日本標準時から9時間マイナスした時刻になります。) これで指定通りに通知が来れば成功です! (微妙に実行時間がズレるのは仕様のようです。) 参考にさせていただいたページ
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む