20210908のPythonに関する記事は21件です。

サービスアカウントファイルを使ってアクセストークンを取得する

はじめに Google Cloud Platformの機能を使う場合、たいていは GOOGLE_APPLICATION_CREDENTIALS が設定されていることを前提とするので認証について深く考えず使えることが多い。 が、GOOGLE_APPLICATION_CREDENTIALSを使わず、サービスアカウントファイルを使って直接REST APIを叩きたい時もある。 この場合、REST APIのリクエストのヘッダーには Authorization: Bearer <アクセストークン> なるおまじないを埋め込まなくてはいけない。 このアクセストークンの取得の仕方をよく忘れるのでここでメモっておく。 サービスアカウントファイルを使ってアクセストークンを取得する 特にひねりもなく回答をのっける。 実行環境 Python 3.9 必要パッケージのインストール pip install google-auth サービスアカウントファイルを使ってアクセストークンを取得する関数 from typing import Any, Dict, Union from google.auth.transport.requests import Request from google.oauth2.service_account import IDTokenCredentials def get_access_token_from_service_account( service_account: Union[str, Dict[str, Any]], target_audience: str ) -> str: if type(service_account) is str: credentials = IDTokenCredentials.from_service_account_file( service_account, target_audience=target_audience, ) else: credentials = IDTokenCredentials.from_service_account_info( service_account, target_audience=target_audience, ) credentials.refresh(Request()) token = credentials.token return token 使い方の例 以下の例では、最終的にhttpトリガーのCloud Functions https://my_region-my-project.cloudfunctions.net/hello_world を実行したかったものとする。 hello_wolrd 関数は認証が必要な関数であるためアクセストークンが必要、というシチュエーションである。 import json import requests url = "https://my_region-my-project.cloudfunctions.net/hello_world" # サービスアカウントファイルのパスを与えてアクセストークンを取得する。 access_token = get_access_token_from_service_account("./my-project-xxxxxxxxxxxx.json", url) # 何らかの理由で既に service_account_obj = json.load(open("./my-project-xxxxxxxxxxxx.json")) のようにして # サービスアカウントファイルを自前で読み込んでJSONパーズ済みの場合は # access_token = get_access_token_from_service_account(service_account_obj, url) # のようにパース結果を直接指定してもいい。 # Cloud Functionsを実行する。 # ヘッダーの設定 headers = { "Content-Type": "application/json", "Authorization": f"Bearer {access_token}", } # リクエストボディの設定 data_encoded = json.dumps({...}).encode() response = requests.post( url, headers=headers, data=data_encoded, ) print(response.text) 補足? Cloud Functions以外でもGCPのREST APIなら get_access_token_from_service_account の第2引数にAPIのサービスエンドポイントを指定すればいいと思う(未確認)。 おわりに 本当は google-auth のようなラッパーも使わず全部 requests でやりたかったんだけど妥協した。 生REST APIを使う方法は情報が散逸していてよくわからんですね。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PythonのWhoisライブラリの闇

PythonでWhois情報を構造的に取得したい そう、やりたいのはこれだけです。 RDAPを使ってもいいのですが、JPドメインのRDAPとかみつからないし、まだ時期尚早かな、ということでWhoisで頑張ります。 なぜ難しいのか whoisは太古のインターネットのプロトコルのひとつです。 仕組み自体は、45番portにアクセスして、情報の欲しいドメイン名を入れる、というシンプルなものなのですが、まず、どこに問い合わせをしたらいいのかはっきりしません。次に、どんな情報をどんな形式で返すかは、それぞれのwhoisサーバー次第なのです。 基本的に人間が読みやすいように工夫されてるので、TLD毎にParserを書いてあげる必要があります。いや、JPの場合、.jpと.co.jpと.ne.jpとでは異なるので、TLD毎でさえないんですよね。 探してみる いくつかググってPythonで使えそうなwhoisのライブラリを探してみました。 とりあえず、pipでインストールできるものとして、whois、python-whois、pythonwhoisを見つけました。 まず、簡単な説明と、どのくらい人気があるのか、最近メンテされてるのかを調べてみます。 whois 0.9.13 PyPiで確認してみます。ここです。 このパッケージの作者による説明はありません。あぁ、そうですか。 しかし、最終更新日は今年の4月なので、そんなに悪くはなさそうです。 githubの方も確認してみます。DannyCork/python-whois です。 えっ?! さっき、python-whoisって別のパッケージありましたよね。 ま、それはおいといて、実装内容としては、Linuxのwhoisコマンドを呼んで結果を成形するようです。 Able to extract data for all the popular TLDs (com, org, net, biz, info, pl, jp, uk, nz, ...) とか書いてあるので、いいんじゃないでしょうか。 Starも160あります。 python-whois 0.7.3 次は、python-whoisを見てみます。ここです。 こちらは、whoisコマンド経由ではなく、直接whoisサーバーにアクセスしてデータを取得するようです。 githubも確認してみます。richardpenman/whois です。 おっと、こちらはpython-whoisではなく、whoisなのですね。 カオスですね・・・。 先月更新があったようですし、Starも149個あります。 pythonwhois 2.4.3 こちらは、pythonwhoisです。ここです。 ほぅ、UNKNOWNですと・・・。 最終リリースは2014年ですね。 IPアドレスのwhoisライブラリのipwhoisがオススメしてたので期待したのですが、新しいgTLDとかはないかもしれませんね。 githubは、joepie91/python-whois です。 もう、パッケージ名の突っ込みはよしましょう・・・。 READMEを見ると、お、なにやら立派なゴールが書いてあります。 100% coverage of WHOIS formats. Accurate and complete data. Consistently functional parsing; constant tests to ensure the parser isn't accidentally broken. すげ。 無理でしょ。 メンテが7年前で止まっていますが・・・。 ま、Starが367個のすばらしいプロジェクトだったようです。 あと、ライセンスが、WTFPL(Do What the Fuck You Want to Public License)です。聞いたことないのですが、何ですかね、これ。勝手にしやがれってことですかね。 ま、いやならCC0を使え、と。 使ってみる whois 0.9.13 まずは、今年4月にメンテされているwhoisです。 import whois import pprint pprint.pprint(whois.query("qiita.com").__dict__) 実行してみます。 {'creation_date': datetime.datetime(2011, 7, 18, 11, 47, 4), 'dnssec': False, 'expiration_date': datetime.datetime(2022, 7, 18, 11, 47, 4), 'last_updated': datetime.datetime(2021, 6, 13, 23, 21, 43), 'name': 'qiita.com', 'name_servers': {'ns-1049.awsdns-03.org', 'ns-171.awsdns-21.com', 'ns-1956.awsdns-52.co.uk', 'ns-772.awsdns-32.net'}, 'registrant': '', 'registrant_country': '', 'registrar': 'Amazon Registrar, Inc.', 'status': 'clientTransferProhibited ' 'https://icann.org/epp#clientTransferProhibited', 'statuses': ['clientTransferProhibited ' 'https://icann.org/epp#clientTransferProhibited']} あれ。registrantがないですね。 whoisコマンドで直接見てみるとわかるのですが、 Registrar WHOIS Server: whois.registrar.amazon.com のように、実はもう一段階whoisサーバを辿る必要があります。 ここまで、追いかけてはくれないようです。 汎用JPドメインも見てみましょう。hiragana.jpです。 {'creation_date': datetime.datetime(2002, 8, 4, 0, 0), 'dnssec': False, 'expiration_date': datetime.datetime(2022, 8, 31, 0, 0), 'last_updated': datetime.datetime(2021, 9, 1, 1, 5, 10), 'name': 'hiragana.jp', 'name_servers': {'ns1.qt-dns.jp', 'ns1.qt-dns.com', 'ns1.qt-dns.net'}, 'registrant': 'Yoshitaka Hirano', 'registrant_country': '', 'registrar': '', 'status': 'Active', 'statuses': ['Active']} いい感じのようですが、dnssecは登録してあるので、間違いですね。 レジストラはwhoisコマンで見えないので、出なくて仕方ありません。 co.jpはどうでしょうか。interlingua.co.jpです。 {'creation_date': None, 'dnssec': False, 'expiration_date': datetime.datetime(2022, 6, 30, 0, 0), 'last_updated': datetime.datetime(2021, 7, 1, 1, 8, 4), 'name': 'interlingua.co.jp', 'name_servers': None, 'registrant': '', 'registrant_country': '', 'registrar': '', 'status': 'Connected (2022/06/30)', 'statuses': ['Connected (2022/06/30)']} registrantもcreation_dateがないですね。 whoisコマンドの出力にはあるので、うまくparseできていないようですね。残念です。 ne.jpはどうでしょうか。imail.ne.jpです。 Traceback (most recent call last): File "/home/hirano/test/a.py", line 6, in <module> pprint.pprint(whois.query("imail.ne.jp").__dict__) AttributeError: 'NoneType' object has no attribute '__dict__' あらら。死んじゃいました。 うーん。なんか、全体的に残念な感じです。 whoisコマンドの出力にあるのに、結果に出ないものについては、whois/tld_regexpr.pyを修正すればいけそうです。 例えば、co.jpのcreation_dateが出ないのは、r'\[登録年月\]\s?(.+)'をr'\[登録年月日\]\s?(.+)'にすれば直ります。 ne.jpはそもそもコードが対応していないようです。 では、新しめので、xyzドメインとかはどうでしょうか。aaa.xyzで試してみます。 No whois server is known for this kind of object. Traceback (most recent call last): File "/root/jpaawg/tsuritai/tsuritai-scanner/a.py", line 6, in <module> pprint.pprint(whois.query("aaa.xyz").__dict__) File "/root/jpaawg/tsuritai/tsuritai-scanner/.venv/lib/python3.9/site-packages/whois/__init__.py", line 82, in query pd = do_parse(do_query(d, force, cache_file, slow_down, ignore_returncode), tld) File "/root/jpaawg/tsuritai/tsuritai-scanner/.venv/lib/python3.9/site-packages/whois/_2_parse.py", line 45, in do_parse raise FailedParsingWhoisOutput(whois_str) whois.exceptions.FailedParsingWhoisOutput: No whois server is known for this kind of object. これも、死んじゃいました。 OSのwhoisコマンドでも出力されませんでした。ま、whoisコマンドのラッパーなので仕方がないですね。 python-whois 0.7.3 最近でもメンテされているもう一つのwhoisライブラリです。 import whois import pprint pprint.pprint(whois.whois("qiita.com")) 実行してみます。 {'address': 'P.O. Box 81226', 'city': 'Seattle', 'country': 'US', 'creation_date': datetime.datetime(2011, 7, 18, 11, 47, 4), 'dnssec': 'unsigned', 'domain_name': ['QIITA.COM', 'qiita.com'], 'emails': ['abuse@amazonaws.com', 'owner-10669316@qiita.com.whoisprivacyservice.org', 'admin-10669316@qiita.com.whoisprivacyservice.org', 'tech-10669316@qiita.com.whoisprivacyservice.org'], 'expiration_date': datetime.datetime(2022, 7, 18, 11, 47, 4), 'name': 'On behalf of qiita.com owner', 'name_servers': ['NS-1049.AWSDNS-03.ORG', 'NS-171.AWSDNS-21.COM', 'NS-1956.AWSDNS-52.CO.UK', 'NS-772.AWSDNS-32.NET', 'ns-1049.awsdns-03.org', 'ns-171.awsdns-21.com', 'ns-1956.awsdns-52.co.uk', 'ns-772.awsdns-32.net'], 'org': 'Whois Privacy Service', 'referral_url': None, 'registrar': 'Amazon Registrar, Inc.', 'state': 'WA', 'status': ['clientTransferProhibited ' 'https://icann.org/epp#clientTransferProhibited', 'renewPeriod https://icann.org/epp#renewPeriod'], 'updated_date': [datetime.datetime(2021, 6, 13, 23, 21, 43), datetime.datetime(2021, 6, 13, 23, 21, 44, 397000)], 'whois_server': 'whois.registrar.amazon.com', 'zipcode': '98108-1226'} whoisと違い、python-whoisはレジストラのwhois情報も辿って取得しているようで、orgのところに登録者情報も表示されています。「Whois Privacy Service」ですがね。 name server情報は、なぜか、大文字小文字2つあって残念な感じです。 では、汎用JPドメインも見てみましょう。hiragana.jpです。 {'creation_date': None, 'domain_name': None, 'name_servers': None, 'registrant_org': None, 'status': None, 'updated_date': None} おっと、これは。やる気が感じられません。 co.jpはどうでしょうか。interlingua.co.jpです。 {'creation_date': None, 'domain_name': None, 'name_servers': None, 'registrant_org': 'Interlingua, Inc.', 'status': None, 'updated_date': None} こちらも残念な感じですね。かろうじて登録者情報だけ取得できています。 ne.jpも見てみましょう。imail.ne.jpです。 {'creation_date': None, 'domain_name': None, 'name_servers': None, 'registrant_org': None, 'status': None, 'updated_date': None} あらら。全滅ですね。 whois/parser.pyを見てみると、動きそうなのですが、記述が英語表記になっています。 実は、JPのwhoisサーバーはデフォルトで日本語で情報を返すので、期待した動作をしなかったようです。 もしかしたら、昔はJPドメインのwhoisサーバーも英語で返してその頃は動いていたのかも知れません。 JPRSのWhoisサーバーはドメイン名の後ろに/eを付けると英語で返すので、ソースをちょっと修正すれば取得できました。 が、対応しているのはco.jpのみで、汎用JPドメインやne.jpドメインは動作しません。 うーん。こちらも残念な感じです。 xyzドメインも見てみます。 {'address': None, 'city': None, 'country': None, 'creation_date': None, 'dnssec': None, 'domain_name': None, 'emails': None, 'expiration_date': None, 'name': None, 'name_servers': None, 'org': None, 'referral_url': None, 'registrar': None, 'state': None, 'status': None, 'updated_date': None, 'whois_server': None, 'zipcode': None} ほぅ。なぜか項目は増えましたが、Noneですね。 残念です。 pythonwhois 2.4.3 次は、7年前で開発が止まってるpythonwhoisです。 from pythonwhois import get_whois import pprint pprint.pprint(get_whois("qiita.com")) 実行してみます。 Traceback (most recent call last): File "/usr/local/lib/python3.9/sre_parse.py", line 1039, in parse_template this = chr(ESCAPES[this][1]) KeyError: '\\s' During handling of the above exception, another exception occurred: Traceback (most recent call last): File "/home/hirano/test/a.py", line 3, in <module> from pythonwhois import get_whois File "/home/hirano/test/.venv/lib/python3.9/site-packages/pythonwhois/__init__.py", line 1, in <module> from . import net, parse File "/home/hirano/test/.venv/lib/python3.9/site-packages/pythonwhois/parse.py", line 422, in <module> registrant_regexes = [preprocess_regex(regex) for regex in registrant_regexes] File "/home/hirano/test/.venv/lib/python3.9/site-packages/pythonwhois/parse.py", line 422, in <listcomp> registrant_regexes = [preprocess_regex(regex) for regex in registrant_regexes] File "/home/hirano/test/.venv/lib/python3.9/site-packages/pythonwhois/parse.py", line 215, in preprocess_regex regex = re.sub(r"\\s\*\(\?P<([^>]+)>\.\+\)", r"\s*(?P<\1>\S.*)", regex) File "/usr/local/lib/python3.9/re.py", line 210, in sub return _compile(pattern, flags).sub(repl, string, count) File "/usr/local/lib/python3.9/re.py", line 327, in _subx template = _compile_repl(template, pattern) File "/usr/local/lib/python3.9/re.py", line 318, in _compile_repl return sre_parse.parse_template(repl, pattern) File "/usr/local/lib/python3.9/sre_parse.py", line 1042, in parse_template raise s.error('bad escape %s' % this, len(this)) re.error: bad escape \s at position 0 うわ。いきなりimportで死んだよ・・・。 ま、7年前のコードなので仕方ないでしょう。 修正してみます。pythonwhois/parse.pyの215行目です。 pythonwhois/parse.pyの215行目 regex = re.sub(r"\\s\*\(\?P<([^>]+)>\.\+\)", r"\s*(?P<\1>\S.*)", regex)                  ↓ pythonwhois/parse.pyの215行目 regex = re.sub(r"\\s\*\(\?P<([^>]+)>\.\+\)", r"\\s*(?P<\1>\\S.*)", regex) 実行してみます。 長いので折りたたみ {'contacts': {'admin': None, 'billing': None, 'registrant': None, 'tech': None}, 'creation_date': [datetime.datetime(2011, 7, 18, 11, 47, 4)], 'emails': ['abuse@amazonaws.com'], 'expiration_date': [datetime.datetime(2022, 7, 18, 11, 47, 4)], 'id': ['1667512845_DOMAIN_COM-VRSN'], 'nameservers': ['NS-1049.AWSDNS-03.ORG', 'NS-171.AWSDNS-21.COM', 'NS-1956.AWSDNS-52.CO.UK', 'NS-772.AWSDNS-32.NET'], 'raw': [' Domain Name: QIITA.COM\n' ' Registry Domain ID: 1667512845_DOMAIN_COM-VRSN\n' ' Registrar WHOIS Server: whois.registrar.amazon.com\n' ' Registrar URL: http://registrar.amazon.com\n' ' Updated Date: 2021-06-13T23:21:43Z\n' ' Creation Date: 2011-07-18T11:47:04Z\n' ' Registry Expiry Date: 2022-07-18T11:47:04Z\n' ' Registrar: Amazon Registrar, Inc.\n' ' Registrar IANA ID: 468\n' ' Registrar Abuse Contact Email: abuse@amazonaws.com\n' ' Registrar Abuse Contact Phone: +1.2067406200\n' ' Domain Status: clientTransferProhibited ' 'https://icann.org/epp#clientTransferProhibited\n' ' Name Server: NS-1049.AWSDNS-03.ORG\n' ' Name Server: NS-171.AWSDNS-21.COM\n' ' Name Server: NS-1956.AWSDNS-52.CO.UK\n' ' Name Server: NS-772.AWSDNS-32.NET\n' ' DNSSEC: unsigned\n' ' URL of the ICANN Whois Inaccuracy Complaint Form: ' 'https://www.icann.org/wicf/\n' '>>> Last update of whois database: 2021-09-08T11:12:17Z <<<\n' '\n' 'For more information on Whois status codes, please visit ' 'https://icann.org/epp\n' '\n' 'NOTICE: The expiration date displayed in this record is the date ' 'the\n' "registrar's sponsorship of the domain name registration in the " 'registry is\n' 'currently set to expire. This date does not necessarily reflect the ' 'expiration\n' "date of the domain name registrant's agreement with the sponsoring\n" "registrar. Users may consult the sponsoring registrar's Whois " 'database to\n' "view the registrar's reported date of expiration for this " 'registration.\n' '\n' 'TERMS OF USE: You are not authorized to access or query our Whois\n' 'database through the use of electronic processes that are ' 'high-volume and\n' 'automated except as reasonably necessary to register domain names ' 'or\n' 'modify existing registrations; the Data in VeriSign Global Registry\n' 'Services\' ("VeriSign") Whois database is provided by VeriSign for\n' 'information purposes only, and to assist persons in obtaining ' 'information\n' 'about or related to a domain name registration record. VeriSign does ' 'not\n' 'guarantee its accuracy. By submitting a Whois query, you agree to ' 'abide\n' 'by the following terms of use: You agree that you may use this Data ' 'only\n' 'for lawful purposes and that under no circumstances will you use ' 'this Data\n' 'to: (1) allow, enable, or otherwise support the transmission of ' 'mass\n' 'unsolicited, commercial advertising or solicitations via e-mail, ' 'telephone,\n' 'or facsimile; or (2) enable high volume, automated, electronic ' 'processes\n' 'that apply to VeriSign (or its computer systems). The compilation,\n' 'repackaging, dissemination or other use of this Data is expressly\n' 'prohibited without the prior written consent of VeriSign. You agree ' 'not to\n' 'use electronic processes that are automated and high-volume to ' 'access or\n' 'query the Whois database except as reasonably necessary to register\n' 'domain names or modify existing registrations. VeriSign reserves the ' 'right\n' 'to restrict your access to the Whois database in its sole discretion ' 'to ensure\n' 'operational stability. VeriSign may restrict or terminate your ' 'access to the\n' 'Whois database for failure to abide by these terms of use. VeriSign\n' 'reserves the right to modify these terms at any time.\n' '\n' 'The Registry database contains ONLY .COM, .NET, .EDU domains and\n' 'Registrars.\n'], 'registrar': ['Amazon Registrar, Inc.'], 'status': ['clientTransferProhibited ' 'https://icann.org/epp#clientTransferProhibited'], 'updated_date': [datetime.datetime(2021, 6, 13, 23, 21, 43)], 'whois_server': ['whois.registrar.amazon.com']} お。動きました。 生データはうるさいので、静かにしてもらいましょう。 {'contacts': {'admin': None, 'billing': None, 'registrant': None, 'tech': None}, 'creation_date': [datetime.datetime(2011, 7, 18, 11, 47, 4)], 'emails': ['abuse@amazonaws.com'], 'expiration_date': [datetime.datetime(2022, 7, 18, 11, 47, 4)], 'id': ['1667512845_DOMAIN_COM-VRSN'], 'nameservers': ['NS-1049.AWSDNS-03.ORG', 'NS-171.AWSDNS-21.COM', 'NS-1956.AWSDNS-52.CO.UK', 'NS-772.AWSDNS-32.NET'], 'registrar': ['Amazon Registrar, Inc.'], 'status': ['clientTransferProhibited ' 'https://icann.org/epp#clientTransferProhibited'], 'updated_date': [datetime.datetime(2021, 6, 13, 23, 21, 43)], 'whois_server': ['whois.registrar.amazon.com']} こんな感じです。 whoisと同じようにレジストラのwhoisまでは追いかけてないですが、whois_serverに書いてあるので、頑張ったら取れそうです。 汎用JPはどうでしょうか。hiragana.jpで試します。 {'contacts': {'admin': None, 'billing': None, 'registrant': {'email': 'yo@hirano.cc', 'name': 'Interlingua. Inc.', 'phone': '03-3904-2501', 'postalcode': '177-0033', 'street': '2-7-13-201 Takanodai\nNerima-ku'}, 'tech': None}, 'creation_date': [datetime.datetime(2002, 8, 4, 0, 0), datetime.datetime(2002, 8, 4, 0, 0)], 'expiration_date': [datetime.datetime(2022, 8, 31, 0, 0), datetime.datetime(2022, 8, 31, 0, 0)], 'nameservers': ['ns1.qt-dns.com', 'ns1.qt-dns.jp', 'ns1.qt-dns.net'], 'status': ['Active'], 'updated_date': [datetime.datetime(2021, 9, 1, 1, 5, 10)]} 取れてますが、公開窓口情報がregistrantに入ってますね。本物のRegistrantは取れていません。 あと、creation_dateとexpiration_dataがなぜか2つありますね。 co.jpはどうでしょうか。interlingua.co.jpで試します。 {'contacts': {'admin': {'changedate': datetime.datetime(2004, 6, 14, 4, 31, 1), 'email': 'domain@interlingua.co.jp', 'firstname': 'Yoshitaka', 'handle': 'YH5591JP', 'lastname': 'Hirano', 'name': 'Yoshitaka Hirano', 'organization': 'Interlingua, Inc.'}, 'billing': None, 'registrant': {'organization': 'Interlingua, Inc.'}, 'tech': {'changedate': datetime.datetime(2004, 11, 17, 9, 10, 48), 'email': 'yo@hirano.cc', 'fax': '03-3904-2501', 'firstname': 'Yoshitaka', 'handle': 'YH5927JP', 'lastname': 'Hirano', 'name': 'Yoshitaka Hirano', 'organization': 'Interlingua, Inc.', 'phone': '03-3904-2501'}}, 'creation_date': [datetime.datetime(2004, 6, 14, 0, 0)], 'nameservers': ['ns1.qt-dns.com', 'ns1.qt-dns.jp', 'ns1.qt-dns.net'], 'status': ['Connected (2022/06/30)'], 'updated_date': [datetime.datetime(2021, 7, 1, 1, 8, 4)]} こちらはRegistrantは正しく取れていますね。 こりゃ、ne.jpもいけるかもしれません。imail.ne.jpで試してみます。 {'contacts': {'admin': {'changedate': datetime.datetime(2000, 7, 22, 4, 33, 1), 'firstname': 'Yoshitaka', 'handle': 'YH2576JP', 'lastname': 'Hirano', 'name': 'Yoshitaka Hirano', 'organization': 'Hirano, Yoshitaka'}, 'billing': None, 'registrant': None, 'tech': {'changedate': datetime.datetime(2000, 4, 7, 17, 9, 44), 'email': 'hirano@girigiri.ne.jp', 'firstname': 'Yoshitaka', 'handle': 'YH1048JP', 'lastname': 'Hirano', 'name': 'Yoshitaka Hirano', 'organization': 'Girigiri Group Company', 'phone': '03-5731-6270'}}, 'creation_date': [datetime.datetime(2000, 7, 24, 0, 0)], 'nameservers': ['ns1.hirano.cc', 'ukulele.orcaland.gr.jp'], 'status': ['Connected (2022/07/31)'], 'updated_date': [datetime.datetime(2021, 8, 1, 1, 4, 58)]} 残念。registrantは取れませんでした。 co.jpは「g. [Organization]」なんですが、ne.jpは「d. [Network Service Name]」なんですよね。 どうやら20年くらい更新を忘れている情報があるようです。直さないと。 では、xyzドメインはどうでしょうか。 {'contacts': {'admin': None, 'billing': None, 'registrant': None, 'tech': None}, 'creation_date': [datetime.datetime(2016, 3, 8, 21, 38, 58), datetime.datetime(2016, 3, 8, 21, 38, 58)], 'emails': ['domainabuse@service.aliyun.com'], 'expiration_date': [datetime.datetime(2022, 3, 8, 23, 59, 59)], 'id': ['D18453057-CNIC'], 'nameservers': ['DNS9.HICHINA.COM', 'DNS10.HICHINA.COM'], 'registrar': ['Alibaba Cloud Computing Ltd. d/b/a HiChina (www.net.cn)'], 'status': ['ok https://icann.org/epp#ok'], 'updated_date': [datetime.datetime(2021, 2, 8, 15, 24, 45)], 'whois_server': ['grs-whois.hichina.com']} おーーーーっ! 取れましたね。 7年前から開発が止まっているけど、もしかして、7年間メンテしなくていいくらいに完成してるのか?! まとめ whois, python-whois, pythonwhois 3つのライブラリを見てみました。 結局、これは、というものはなく、全部残念な感じでした。 日本で使うには、JPドメインが取れないと話にならないので、whoisやpython-whoisはそのままでは使えなさそうです。 pythonwhoisはJPドメインの取れますが、登録者情報が窓口情報になってしまっているので、そのままでは使えなさそうです。 新しめのドメインもxyzの例を見る限りでは、7年前のpythonwhoisが唯一結果を出したので、新しいものがいい、とも言えなさそうです。 結論 なんか、踏み入れてはいけない闇に1歩足を踏み入れてしまった気がします。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

WatchDogTimerを作る

WatchDogTimerを作る 仕様 日向で寛いでいる(という設定の)犬を撫でる! 一定時間撫でられなかった犬は,次に撫でようとしたときに吠えてくる 一度吠えた犬は二度と撫でさせてくれない コード watchdog.py from threading import Event, Thread class WatchdogBark(Exception): pass class Watchdog(Thread): def __init__(self, interval): Thread.__init__(self) self.interval = interval self.watching = True self.petted = Event() self.bark = False def stop(self): self.watching = False def pet(self): if self.bark: raise WatchdogBark() self.petted.set() def run(self): while self.watching: self.petted.wait(self.interval) if not self.petted.is_set(): self.bark = True break self.petted.clear() 使い方 watchdog_sample.py import random import time from watchdog import Watchdog, WatchdogBark def main(): # 3秒ごとに撫でないと怒っちゃう犬 dog = Watchdog(3) dog.start() try: for t in range(20): print() # なんか処理 (ここではランダム時間sleep) processing_time = random.random() * 4 print(f"{t+1}番目の処理は{processing_time:.1f}秒かかりそう...") time.sleep(processing_time) print("犬を撫でる!") dog.pet() print("撫でても吠えられなかった!!") except WatchdogBark: print("吠えられた.") else: print("吠えられることなく処理しきれました.") finally: dog.stop() dog.join() if __name__ == '__main__': main() 感想 この犬,永遠に放置しても撫でようとしなければ吠えてこないぞ!!!! (自分から吠えるようにするにはsignal使った割り込みをしましょう.)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Python】PythonからC/C++で書かれたソースコードを呼び出す

概要 Pythonで動いているプログラムから、C/C++で書かれたプログラムを呼び出す方法について記載する。 手法は以下の3パターン Python C API Cython boost.python 記事が長くなってしまいそうなので、手法ごとに記事を分けて記載することにする。 今回は「Python C API」について記載する。 ソースコード公開先 GitHubのリンク Python C API Python C APIとは、PythonモジュールをC/C++で記述可能な標準ライブラリ。 実際にPythonから呼び出す際は事前にビルドしておく必要がある。 モジュール化したいC/C++のソースコード まずはソースコード全体。 test_c_mod.c #include <Python.h> static PyObject* c_hello_method(PyObject* self, PyObject* args) { printf("HELLO TEST\n"); return Py_None; } int c_multiplication(int a, int b) { return a * b; } static PyObject* wrap_c_multiplication(PyObject* self, PyObject* args) { int a, b, c; if (!PyArg_ParseTuple(args, "ii", &a, &b) { return NULL; } c = c_multiplication(a, b); return Py_BuildValue("i", c); } static PyMethodDef Cmethods[] = { {"c_hello", c_hello_method, METH_NOARGS, "HELLO TEST"}, {"c_multiplication", wrap_c_multiplication, METH_VARARGS, "Multiply the two values"}, { NULL, NULL, 0, NULL} }; static struct PyModuleDef Cmodule = { PyModuleDef_HEAD_INIT, "test_C_module", "Python3 C API Module(test_C_module)", -1, Cmethods } PyMODINIT_FUNC PyInit_test_C_module(void) { return PyModule_Create(&Cmodule); } 解説 必要なヘッダファイルをインクルードする。 Python C APIを使用する際は、C/C++側のソースファイルで、Python.hをインクルードする必要がある。 #include <Python.h> // パスが通ってない場合は絶対パスでもOK #include "/usr/include/python3.8/Python.h" Pythonで実行したいC/C++の処理を実装する まずはPythonで実行したいC/C++の関数の内容を記述する。 関数はラッパー関数を用意してもよいし、PyObject型の関数に直接処理内容を記述する方法でもよい。 /* Pythonで実行したいC/C++の処理を記述する */ // ラッパー関数無しで直接記述する方法 static PyObject* c_hello_method(PyObject* self, PyObject* args) { printf("HELLO TEST\n"); return Py_None; //戻り値は無し } // ラッパー関数有りで呼び出す方法 int c_multiplication(int a, int b) { return a * b; } static PyObject* wrap_c_multiplication(PyObject* self, PyObject* args) { int a, b, c; // 型を決定しつつ引数を受け取る(PyObject型→int型 int型) if (!PyArg_ParseTuple(args, "ii", &a, &b) { return NULL; } // 目的の関数を実行 c = c_multiplication(a, b); // 型を決定しつつ戻り値を返却(int型→PyObject型) return Py_BuildValue("i", c); } Pythonから呼び出すための関数定義 実行したい関数が用意出来たら、それらをPython側に伝えるための定義を行う。 static PyMethodDef Cmethods[] = { // {"Pythonで使う際の関数名", Python側に実装したいC/C++関数, 引数指定, "関数の説明"} {"c_hello", c_hello_method, METH_NOARGS, "HELLO TEST"}, {"c_multiplication", wrap_c_multiplication, METH_VARARGS, "Multiply the two values"}, { NULL, NULL, 0, NULL} }; Pythonモジュールとして使用するための定義を行う Pythonモジュールとしてimportするために必要なモジュールの情報を定義する。 // モジュール名の定義 static struct PyModuleDef Cmodule = { PyModuleDef_HEAD_INIT, "test_C_module", // これがモジュール名となる "Python3 C API Module(test_C_module)", // モジュールについての説明文 -1, Cmethods // 関数定義の名前 } // モジュールの初期化処理 PyMODINIT_FUNC PyInit_test_C_module(void) { return PyModule_Create(&Cmodule); コンパイルする C/C++のソースコードが出来たらpythonで使用するために*.soファイルとしてコンパイルする。 setup.py from distutils.core import setup, Extension module= Extension( "test_C_module", # モジュール名 sources= ["test_c_mod.c"] # 対象のC/C++ソースファイル ) setup(name= "test_C_module", # pipに登録する際の名前 version= "1.0.0", # ライブラリのバージョン ext_modules= [module] # 上記で記述したモジュールの情報 ) コンパイルを実行 コンパイルを実行すると、./buildディレクトリが生成されその中に*.soファイルが生成される。 # コンパイル実行 $ python3 setup.py build # コンパイル実行→pipの管轄に追加 $ python3 setup.py install $ pip freeze 実際に使用する 生成された*.soファイルを参照可能なディレクトリに移動しておく。 (pipの管轄に追加した場合は不要) import test_C_module test_C_module.c_hello() c= test_C_module.c_multiplication(2, 3) print(c) 以上。 次回は、「Cython」を使用した方法について書く予定です。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Python3について(->とは,API通信の例外処理,指数関数的なリトライ処理)

アノテーション この関数はこれを返します とかの説明用->以降の文字は関数に影響しない def fetch(url : str) -> requests.Response: max_retries = 3 retries = 0 while True: try: print(f'Retrieving {url} ...') response = requests.get(url) print(f'Status: {response.status_code}') if response.status_code not in TEMPORARY_ERROR_CODES: return response ... API通信の例外処理 この方のページが参考になりました 指数関数的なリトライ間隔を求める この方のページが参考になりました イメージは、同じ間隔でスクレイピングを続けても、ずっとエラーが吐かれてしまうので、 リトライ間隔を開けて、サーバーの負荷を軽減している。と理解しました。 純粋に+1秒ずつでも良いのでは?とも思いましたが…
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Raspberry Pi ZeroでTensorFlow Liteランタイムを実行しようとしたらハマった話

はじめに タイトルのとおり、ハマってしまったので記事に残すことにしました。 ハマったこと Raspberry Pi Zero(armv6l)用のPython Wheelパッケージが提供されていないので、公式ドキュメントの通りにやってみたら、ビルドに失敗してしまう… 試してみたこと-1 こちらの記事を参考にして、Dockerコンテナ上で試してみても、ビルドに失敗してしまう(汗) 失敗したときの実行環境・実行したコマンドは、以下の通りです。 実行環境 OS: Ubuntu 20.04.2 LTS (ConoHa VPS) CPU: 2Core Mem: 1GB SSD: 100GB 実行したコマンド # Dockerのインストール(環境:ローカル) # apt update && apt upgrade # apt install \ apt-transport-https \ ca-certificates \ curl \ gnupg \ lsb-release # curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg # echo \ "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \ $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null # apt update # apt install docker-ce docker-ce-cli containerd.io # cmakeのインストール(環境:ローカル) # apt install cmake # TensorFlowリポジトリのクローンを作成 # cd ~ # git clone https://github.com/tensorflow/tensorflow.git tensorflow_src # Dockerコンテナの作成・起動 # docker pull tensorflow/tensorflow:devel # docker run -it -v /var/run/docker.sock:/var/run/docker.sock \ -v $PWD/tensorflow_src:/root/tensorflow_src -w /root/tensorflow_src \ tensorflow/tensorflow:devel bash # Dockerのインストール(環境:Dockerコンテナ) # apt update && apt upgrade # apt install \ apt-transport-https \ ca-certificates \ curl \ gnupg \ lsb-release # curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg # echo \ "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \ $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null # apt update # apt install docker-ce docker-ce-cli containerd.io # cmakeのインストール(環境:Dockerコンテナ) # apt install cmake # Wheelのビルド(環境:Dockerコンテナ) # tensorflow/tools/ci_build/ci_build.sh PI-PYTHON37 \ tensorflow/lite/tools/pip_package/build_pip_package_with_cmake.sh rpi0 ちなみに、ビルドに失敗すると、このようなメッセージが表示されます。 /workspace/tensorflow/lite/tools/cmake/toolchains/gcc-arm-8.3-2019.03-x86_64-arm-linux-gnueabihf/arm-linux-gnueabihf/include/c++/8.3.0/bits/deque.tcc:585:7: note: parameter passing for argument of type '__gnu_cxx::__normal_iterator<const double*, std::vector<double> >' changed in GCC 7.1 /workspace/tensorflow/lite/tools/cmake/toolchains/gcc-arm-8.3-2019.03-x86_64-arm-linux-gnueabihf/arm-linux-gnueabihf/include/c++/8.3.0/bits/deque.tcc:585:7: note: parameter passing for argument of type '__gnu_cxx::__normal_iterator<const double*, std::vector<double> >' changed in GCC 7.1 /workspace/tensorflow/lite/tools/cmake/toolchains/gcc-arm-8.3-2019.03-x86_64-arm-linux-gnueabihf/arm-linux-gnueabihf/include/c++/8.3.0/bits/deque.tcc:625:11: note: parameter passing for argument of type '__gnu_cxx::__normal_iterator<const double*, std::vector<double> >' changed in GCC 7.1 _M_insert_aux(__pos, __first, __last, __n); ^~~~~~~~~~~~~ In file included from /workspace/tensorflow/lite/tools/cmake/toolchains/gcc-arm-8.3-2019.03-x86_64-arm-linux-gnueabihf/arm-linux-gnueabihf/include/c++/8.3.0/deque:64, from /workspace/tensorflow/lite/kernels/internal/spectrogram.h:35, from /workspace/tensorflow/lite/kernels/internal/spectrogram.cc:16: /workspace/tensorflow/lite/tools/cmake/toolchains/gcc-arm-8.3-2019.03-x86_64-arm-linux-gnueabihf/arm-linux-gnueabihf/include/c++/8.3.0/bits/stl_deque.h: In member function 'bool tflite::internal::Spectrogram::GetNextWindowOfSamples(const std::vector<InputSample>&, int*) [with InputSample = double]': /workspace/tensorflow/lite/tools/cmake/toolchains/gcc-arm-8.3-2019.03-x86_64-arm-linux-gnueabihf/arm-linux-gnueabihf/include/c++/8.3.0/bits/stl_deque.h:2021:4: note: parameter passing for argument of type '__gnu_cxx::__normal_iterator<const double*, std::vector<double> >' changed in GCC 7.1 _M_range_insert_aux(__pos, __first, __last, ^~~~~~~~~~~~~~~~~~~ /workspace/tensorflow/lite/tools/cmake/toolchains/gcc-arm-8.3-2019.03-x86_64-arm-linux-gnueabihf/arm-linux-gnueabihf/include/c++/8.3.0/bits/stl_deque.h:2021:4: note: parameter passing for argument of type '__gnu_cxx::__normal_iterator<const double*, std::vector<double> >' changed in GCC 7.1 _M_range_insert_aux(__pos, __first, __last, ^~~~~~~~~~~~~~~~~~~ make[3]: Leaving directory '/workspace/tensorflow/lite/tools/pip_package/gen/tflite_pip/python3.7/cmake_build' make[2]: *** [CMakeFiles/tensorflow-lite.dir/all] Error 2 CMakeFiles/Makefile2:1145: recipe for target 'CMakeFiles/tensorflow-lite.dir/all' failed make[2]: Leaving directory '/workspace/tensorflow/lite/tools/pip_package/gen/tflite_pip/python3.7/cmake_build' CMakeFiles/Makefile2:1253: recipe for target 'CMakeFiles/_pywrap_tensorflow_interpreter_wrapper.dir/rule' failed make[1]: *** [CMakeFiles/_pywrap_tensorflow_interpreter_wrapper.dir/rule] Error 2 make[1]: Leaving directory '/workspace/tensorflow/lite/tools/pip_package/gen/tflite_pip/python3.7/cmake_build' make: *** [_pywrap_tensorflow_interpreter_wrapper] Error 2 Makefile:215: recipe for target '_pywrap_tensorflow_interpreter_wrapper' failed root@06a2eea49469:~/tensorflow_src# 試してみたこと-2 結論から言うと、こちらの方法でWheelのビルドに成功しました。 コンテナではなくRaspberry Pi Zero上で普通にビルドした方が良いのでは?、と思いbuild_pip_package_with_cmake.shの中身を解読して、一行ずつ端末から実行してみました。 成功したときの実行環境・実行したコマンドは、以下の通りです。 実行環境 Raspberry Pi Zero WH OS: Raspberry Pi OS Lite (Release data: May 7th 2021) SDカード: 64GB 実行したコマンド $ sudo apt update && sudo apt upgrade -y $ sudo apt install cmake $ cd ~ $ git clone https://github.com/tensorflow/tensorflow.git tensorflow_src $ mkdir tflite_build $ cd tflite_build $ sudo cmake ../tensorflow_src/tensorflow/lite \ -DCMAKE_C_FLAGS="-I/usr/include/python3.7m -I/home/pi/.local/lib/python3.7/site-packages/pybind11/include" \ -DCMAKE_CXX_FLAGS="-I/usr/include/python3.7m -I/home/pi/.local/lib/python3.7/site-packages/pybind11/include" \ -DCMAKE_SHARED_LINKER_FLAGS='-latomic' -DCMAKE_SYSTEM_NAME=Linux \ -DCMAKE_SYSTEM_PROCESSOR=armv6 \ -DTFLITE_ENABLE_XNNPACK=OFF $ sudo cmake --build . --verbose -j 1 -t _pywrap_tensorflow_interpreter_wrapper $ cd ~/tensorflow_src/tensorflow/lite/tools/pip_package/ $ sudo mkdir gen $ sudo mkdir gen/tflite_pip $ sudo mkdir gen/tflite_pip/python3.7 $ sudo mkdir gen/tflite_pip/python3.7/tflite_runtime $ cd gen/tflite_pip/python3.7 $ sudo cp -r ~/tensorflow_src/tensorflow/lite/tools/pip_package/debian/ ./ $ sudo cp -r ~/tensorflow_src/tensorflow/lite/tools/pip_package/MANIFEST.in ./ $ sudo cp -r ~/tensorflow_src/tensorflow/lite/python/interpreter_wrapper/ ./ $ sudo cp ~/tensorflow_src/tensorflow/lite/tools/pip_package/setup_with_binary.py ./setup.py $ sudo cp ~/tensorflow_src/tensorflow/lite/python/interpreter.py ./tflite_runtime/ $ sudo cp ~/tensorflow_src/tensorflow/lite/python/metrics_interface.py ./tflite_runtime/ $ sudo cp ~/tensorflow_src/tensorflow/lite/python/metrics_portable.py ./tflite_runtime/ $ sudo cp ~/tflite_build/_pywrap_tensorflow_interpreter_wrapper.so ./tflite_runtime/ $ sudo chmod u+w ./tflite_runtime/_pywrap_tensorflow_interpreter_wrapper.so $ sudo touch ./tflite_runtime/__init__.py $ sudo echo "__version__ = '2.7.0'" >> ./tflite_runtime/__init__.py $ sudo echo "__git_version__ = '$(git -C ~/tensorflow_src describe)'" >> ./tflite_runtime/__init__.py $ sudo python3 setup.py bdist --plat-name=linux_armv6l bdist_wheel --plat-name=linux-armv6l ビルドに成功すると、tensorflow_src/tensorflow/lite/tools/pip_package/gen/tflite_pip/python3.7/distに Wheelが出力されます。ちなみに、ビルド完了するまで、4時間くらいかかりました。 pi@raspberrypi:~/tensorflow_src/tensorflow/lite/tools/pip_package/gen/tflite_pip/python3.7/dist $ ls tflite_runtime-2.7.0-cp37-cp37m-linux_armv6l.whl tflite-runtime-2.7.0.linux_armv6l.tar.gz 動作確認 動作確認のため、学習済みモデル(test.tflite)を読み込み、get_input_details()とget_output_details()を実行してみました。結果、エラーは発生せず、入力層と出力層の情報を表示することができました。 pi@raspberrypi:~ $ python3 Python 3.7.3 (default, Jan 22 2021, 20:04:44) [GCC 8.3.0] on linux Type "help", "copyright", "credits" or "license" for more information. >>> import tflite_runtime.interpreter as tflite >>> interpreter = tflite.Interpreter(model_path='./test.tflite') >>> interpreter.get_input_details() [{'name': 'serving_default_flatten_input:0', 'index': 0, 'shape': array([ 1, 28, 28]), 'shape_signature': array([-1, 28, 28]), 'dtype': <class 'numpy.float32'>, 'quantization': (0.0, 0), 'quantization_parameters': {'scales': array([], dtype=float32), 'zero_points': array([], dtype=int32), 'quantized_dimension': 0}, 'sparsity_parameters': {}}] >>> interpreter.get_output_details() [{'name': 'StatefulPartitionedCall:0', 'index': 9, 'shape': array([ 1, 10]), 'shape_signature': array([-1, 10]), 'dtype': <class 'numpy.float32'>, 'quantization': (0.0, 0), 'quantization_parameters': {'scales': array([], dtype=float32), 'zero_points': array([], dtype=int32), 'quantized_dimension': 0}, 'sparsity_parameters': {}}] >>> おわりに あまり需要のない内容かと思いますが、誰かの参考になれば幸いです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SATソルバーでPodの配置を決定する

問題 Kubernetesを使った設計していて、「どのワーカーノードにどのPodを配置すると、ノードのハードウェアリソースに収まる範囲で、冗長性を確保できるのか」という問題に直面しました。Kubernetesのスケジューラーが良きに計らってくれると嬉しいですが、諸般の事情により設計者が手作業でPod配置を決定する必要に迫られてしまったのでこの問題を解くためのコードを作ってみました。 条件 それぞれのPodはCPU, メモリなどのリソースを要求する 配置したいPod数は既に決まっている ワーカーノードのリソース(CPUなど)は既に決まっており、その範囲内に収まるようにPodを配置する 1つのReplicaSetから作られるPodが1つのワーカーノードに偏ってしまうとそのノードの障害時に全滅してしまうので、冗長性を持たせて配置する 対応 Z3というソルバーを使ってみます。Z3はSMTソルバーというものの1つで上記のような条件を与えると、その条件を満たす解を求めてくれます。https://github.com/Z3Prover/z3 今回はCPUコア数をハードウェアリソースとして条件を与えることにします。以下はJupyterを用いたコードになっています(Python初心者のため変なところがたくさんあると思います)。Pod名は https://github.com/GoogleCloudPlatform/microservices-demo を真似ました。 !pip install z3-solver tabulate import numpy as np from z3 import * import functools import tabulate from IPython.display import HTML, display # 問題設定 nodes = [{'name':'node001', 'cpu': 7}, {'name':'node002', 'cpu': 5}, {'name':'node003', 'cpu': 6}, {'name':'node004', 'cpu': 5}] replica_sets = [{'name':'frontend', 'replicas':4, 'min_replicas': 2, 'cpu': 1.5}, {'name':'cart', 'replicas':4, 'min_replicas': 2, 'cpu': 1.3}, {'name':'email', 'replicas':4, 'min_replicas': 2, 'cpu': 0.5}, {'name':'ad', 'replicas':3, 'min_replicas': 1, 'cpu': 0.2}, {'name':'payment', 'replicas':4, 'min_replicas': 2, 'cpu': 0.2}, {'name':'currency', 'replicas':1, 'min_replicas': 0, 'cpu': 0.2}, {'name':'shipping', 'replicas':6, 'min_replicas': 3, 'cpu': 0.1}, {'name':'recommendation', 'replicas':2, 'min_replicas': 1, 'cpu': 2}] config = np.array([Real(f'v{i}') for i in range(len(nodes)*len(replica_sets))]).reshape((len(nodes), len(replica_sets))) solver = Solver() # solverへ制約を入力 for i in range(len(nodes)): for j in range(len(replica_sets)): # 配置数は0以上 solver.add(config[i][j] >= 0) # 配置数は整数(配置数をReal型ではなくIntで定義するとSumで小数点以下が切り落とされてしまうためReal型で宣言してこの制約で整数に絞る) solver.add(deployment[i][j] == ToInt(config[i][j])) # それぞれのノードに配置したpodの合計がreplica_setのreplicasになっている for j in range(len(replica_sets)): solver.add(Sum([deployment[i][j] for i in range(len(nodes))]) == replica_sets[j]['replicas']) # それぞれのノードに配置したpodが使用するリソース合計がノードのリソースに収まっている for i in range(len(nodes)): solver.add(Sum([replica_sets[j]['cpu'] * config[i][j] for j in range(len(replica_sets))]) <= nodes[i]['cpu']) # N+1の冗長性を持つ(どれか1ノードが停止したとしてもmin_replicas以上の稼働数となる) for k in range(len(nodes)): for j in range(len(replica_sets)): solver.add(Sum([config[i][j] for i in range(len(nodes))]) - config[k][j] >= replica_sets[j]['min_replicas']) # 結果表示 if solver.check() == sat: model = solver.model() print('Pod配置') # ndarrayへ変更 result = np.frompyfunc(lambda v: model[v].as_long(), 1, 1)(config) # ノード名の列を追加 display_result = np.insert(result, 0, 'name', axis=1) for i in range(len(nodes)): display_result[i][0] = nodes[i]['name'] # 表示 pod_headers = [] for r in replica_sets: pod_headers.append(r['name']) pod_headers.insert(0, 'node') display(HTML(tabulate.tabulate(display_result, pod_headers, tablefmt='html'))) print('各ノードのリソース使用状況') # 使用しているリソースを合計 table = [] for i in range(len(nodes)): sum = 0.0 for j in range(len(replica_sets)): sum = sum + replica_sets[j]['cpu'] * result[i][j] table.append([nodes[i]['name'], sum, nodes[i]['cpu']]) node_headers = ['name', 'used', 'total'] display(HTML(tabulate.tabulate(table, node_headers, tablefmt='html'))) else: print('解なし') 計算結果 こんな感じでどのノードにどのPodをいくつ配置すると良いかが出力されます。 参考 https://speakerdeck.com/ytaka23/cicd-conference-2021 https://qiita.com/fuloru169/items/de8c53402f4998177dfd
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Pythonの環境構築 on Mac m1

Homebrewのインストール /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" 次のステップとして、画面に出てきた通り、パスを通す。具体的には、 echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> /Users/kazuoy/.zprofile echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> /Users/kazuoy/.zprofile pyenvのインストール Pythonのバージョン管理のため、pyenvをインストールする。 brew install pyenv 以下のコマンド3つでパスを通す。 echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.zshrc echo 'export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.zshrc echo 'eval "$(pyenv init -)"' >> ~/.zshrc Pythonのインストール 以下のコマンドで最新バージョンのPythonをインストールできるようである。 brew install python しかしながら、バージョン指定してPythonをインストールするには、まず、以下でPythonバージョン一覧を取得する。 pyenv install --list その後、以下のコマンドにより目的のバージョンのPythonをインストールする(ここでの例は、Python 3.9.7である)。 pyenv install 3.9.7 グローバル環境で該当バージョンを使えるように、以下のコマンドを入力した。 pyenv global 3.9.7 これによってバージョンが3.9.7になったであろうと python --version と入力し確認したが、"Python 2.7.16"という、従来Macに入っていたバージョンが示された。 いろんな参考を見て、いろいろ試したが結果は変わらなかった。 なんやこれおかしいな、と思ったが、解決策は意外なところにあった。 python3 --version "python3"とすれば、"Python 3.9.7"が出てきたので、ひとまず環境構築が終了したと結論づけた。 正直"python3"とすれば良い理由はわからないが、様々なコマンドを打つときに"python3"を接頭辞にすればPython3が使えるのではないかと考えた。 参考文献 https://qiita.com/C2_now/items/c85be2ffeacd61cc7207 https://qiita.com/tuk19/items/7fbee949fbecc5cfd3db https://qiita.com/tuk19/items/7fbee949fbecc5cfd3db
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

requests.postのallow_redirectsでハマった話

ハマった状況 Flask-Loginを使用した簡単なアプリケーションを作成中。 ブラウザから操作した範囲ではOKであることを確認した。 テストを自動化するために、requestsパッケージを使用して、ログインを実装してみたが、 ログインに成功しているにも関わらず、その後の問合せに対してログイン状態が維持されず、 再度、ログインを要求するレスポンスが返ってくる。 最初のコーディング 作成したアプリケーションは、emailとpasswordで認証する。 import requests from bs4 import BeautifulSoup # GET /loginでformを取得して、csrfトークンを抽出 r = requests.get("http://127.0.0.1:5000/login") doc = BeautifulSoup(r.text, 'html.parser') csrf_token = doc.find(attrs={'name':'csrf_token'}).get('value') # credentialとsession cookieを指定して、POST /login cookies = {"session": r.cookies["session"]} credential = {"email": "user1@email.com", "password": "password1", "csrf_token": csrf_token} r = requests.post("http://127.0.0.1:5000/login", cookies=cookies, data=credential, allow_redirects=True) しかしながら、この後の呼び出しでは、ログイン状態が維持されていないため、 ログイン画面に飛ばされてしまう。 コマンドラインからcurlで実行した場合は、正しくログイン状態が維持されることは確認。 curl http://127.0.0.1:5000/login -i -c cookie.txt -b cookie.txt curl http://127.0.0.1:5000/login -i -c cookie.txt -b cookie.txt ^ -d "email=user1@email.com" -d "password=password1" ^ -d "csrf_token=・・・" -L 調査 何が違うのか色々と調べてみたが、原因分からず。 stackoverflowで、requestsのログを出力する方法を見つけて、ログを調査。 https://stackoverflow.com/questions/16337511/log-all-requests-from-the-python-requests-module import logging import http.client logging.basicConfig(level=logging.DEBUG) httpclient_logger = logging.getLogger("http.client") def httpclient_logging_patch(level=logging.DEBUG): """Enable HTTPConnection debug logging to the logging framework""" def httpclient_log(*args): httpclient_logger.log(level, " ".join(args)) # mask the print() built-in in the http.client module to use # logging instead http.client.print = httpclient_log # enable debugging http.client.HTTPConnection.debuglevel = 1 httpclient_logging_patch() 原因 最初にrequestsがPOST /loginすると、サーバー側で認証後、レスポンスが返る。 サーバー側がredirectで返しているので、戻り値が302 Foundとなり、 遷移先を取得しようとしてrequestsがGETを発行する。 この際、レスポンスにSet-Cookieが含まれているにも関わらず、 最初のPOSTで指定したcookieをそのまま送ってしまっている。 対応方法1 allow_redirectsをFalseに変更して、 戻り値が302の場合は、cookieを再設定してGETする。 # credentialとsession cookieを指定して、POST /login cookies = {"session": r.cookies["session"]} credential = {"email": "user1@email.com", "password": "password1", "csrf_token": csrf_token} r = requests.post("http://127.0.0.1:5000/login", cookies=cookies, data=credential, allow_redirects=False) if r.status_code == 302: cookies = {"session": r.cookies["session"]} r = requests.get(r.next.url, cookies=cookies) 対応方法2 最初から素直にsessionオブジェクトを使用する。 import requests from bs4 import BeautifulSoup session = requests.session() r = session.get("http://127.0.0.1:5000/login") doc = BeautifulSoup(r.text, 'html.parser') csrf_token = doc.find(attrs={'name':'csrf_token'}).get('value') credential = {"email": "user1@email.com", "password": "password1", "csrf_token": csrf_token} r = session.post("http://127.0.0.1:5000/login", data=credential, allow_redirects=True) おわりに 常識なのかもしれませんが、答えを求めてインターネットを三日間くらい彷徨いました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Python】pipとpip3で少しハマったので紹介します

はじめに 今回はpipとpip3でインストールされるライブラリの場所が異なるというお話です。 環境 macOS(Catalina10.15.7) Python3.9 Python3.7 Python2.7 (混在環境) 昨日ccxtという複数の仮想通貨取引所のAPI操作が集約されたライブラリのインストール方法をご紹介しました。 昨日は以下のコマンドを紹介しました。 $ sudo pip install ccxt しかしpython3.X系では以下が正しいコマンドです。(すでに修正済み) $ sudo pip3 install ccxt これがどう違うのかというと、インストール先が異なります。 python3.X系でpip installコマンドを打っても参照先にccxtライブラリがないので、以下のエラーが発生します。 ModuleNotFoundError: No module named 'ccxt' ●pipの場合のインストール先は以下です。 /Library/Python/2.7 ●pip3の場合のインストール先は以下です。 /Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages なので、Python3.X系をご利用の方は、必ずpip3でインストールする必要があるかと存じます。 なお、Python3.7とPython3.9が混在している場合、Python3.9側のsite-packagesにインストールされますのでご注意ください。 今回は凡ミス的な内容でしたが、ご参考になれば幸いです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

streamlitのボタンのカスタマイズはmarkdown()メソッドでdiv.stButtonのスタイルシートを強制設定する

はじめに  PythonのノンHTML+CSSフレームワークであるstreamlit。フロントエンドを全てPythonコードで実装出来てお手軽にWebアプリが作れます。その反面、細かな見た目の変更ができません。  そこで、ここでは細かな見た目の変更の一例として、ボタンのスタイルシートを設定する方法をご紹介します。 結論 streamlitのmarkdown()メソッドを利用します markdown()メソッドの 第1引数にスタイルシートを 第2引数としてunsafe_allow_htmlにTrueを設定します これでスタイルシートを強制設定することができます スタイルシートの設定は div.stButtonの子要素のbuttonタグに対して行います 実装例 streamlitでボタンの見た目を変更します import streamlit as st button_css = f""" <style> div.stButton > button:first-child {{ font-weight : bold ;/* 文字:太字 */ border : 5px solid #f36 ;/* 枠線:ピンク色で5ピクセルの実線 */ border-radius: 10px 10px 10px 10px ;/* 枠線:半径10ピクセルの角丸 */ background : #ddd ;/* 背景色:薄いグレー */ }} </style> """ st.markdown(button_css, unsafe_allow_html=True) action = st.button('このボタンを押してください') 書式 streamlitでボタンに任意のスタイルシートを適用する書式 スタイルシートHTML = f""" <style> div.stButton > button:first-child {{ 任意のスタイルシート設定 }} </style> """ st.markdown(スタイルシートHTML, unsafe_allow_html=True) ボタンの戻り値 = st.button('ボタンラベル名')
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PythonでC++のMultisetっぽいことをする(2本のSegment Treeを用いた実装)

はじめに PythonでC++のMultiset(多重集合)っぽいデータ構造を実装するシリーズ第2弾です。 前回のheapqを用いた実装では多重集合で ・値の挿入 ・最大値の取り出し ・最小値の取り出し ・ある値xが含まれているか といったクエリをそれぞれO(logN)で処理できるものを実装しました。(Nは多重集合のサイズ) この記事ではこれらのクエリに加え二分探索のようなことが行えるもの、つまり ・ある値x未満で最も大きい数は何か? ・ある値x以上で最も小さい数は何か? というクエリにもO(logN)で答えられるようなデータ構造を2本のSegment Treeを用いて実装していきたいと思います。 背景 実は先日のABC217のD問題でこの二分探索を行えるデータ構造を必要とする問題が出題され話題になりました。 C++で参加していた人からすると標準ライブラリのsetを使うだけで実装が楽だったかと思いますが Pythonを使っていた人(特に茶~緑の人)には水色以上のdiffに感じられたかもしれません。 Segment Treeとは 簡単にいうと区間Max,区間Min,区間和 etc.を高速に計算できる便利なデータ構造です。 競技プログラミングでは頻出なので知らない人は解説サイトを参照してください。 コード 長いですがとりあえずコードです。 #疑似multiset:segtreeを用いた実装 #####segfunc##### def segfunc_min(x, y): return min(x, y) #最小値 def segfunc_max(x, y): return max(x, y) #最大値 #####単位元(ide_ele)##### ide_ele_min = float('inf') #最小値 ide_ele_max = -float('inf') #最大値 class SegTree: """ 参考:https://qiita.com/takayg1/items/c811bd07c21923d7ec69 init(init_val, segfunc, ide_ele): 配列init_valで初期化 O(N) update(k, x): k番目の値をxに更新 O(N) query(l, r): 区間[l, r)をsegfuncしたものを返す O(logN) j番目の要素を取り出すとき->sgt.tree[sgt.num+j] """ def __init__(self, init_val, segfunc, ide_ele): """ init_val: 配列の初期値 segfunc: 区間にしたい操作 ide_ele: 単位元 n: 要素数 num: n以上の最小の2のべき乗 tree: セグメント木(1-index) """ n = len(init_val) self.segfunc = segfunc self.ide_ele = ide_ele self.num = 1 << (n - 1).bit_length() self.tree = [ide_ele] * 2 * self.num # 配列の値を葉にセット for i in range(n): self.tree[self.num + i] = init_val[i] # 構築していく for i in range(self.num - 1, 0, -1): self.tree[i] = self.segfunc(self.tree[2 * i], self.tree[2 * i + 1]) def update(self, k, x): """ k番目の値をxに更新 k: index(0-index) x: update value """ k += self.num self.tree[k] = x while k > 1: self.tree[k >> 1] = self.segfunc(self.tree[k], self.tree[k ^ 1]) k >>= 1 def query(self, l, r): """ [l, r)のsegfuncしたものを得る l: index(0-index) r: index(0-index) """ res = self.ide_ele l += self.num r += self.num while l < r: if l & 1: res = self.segfunc(res, self.tree[l]) l += 1 if r & 1: res = self.segfunc(res, self.tree[r - 1]) l >>= 1 r >>= 1 return res class QuasiMultiset: def __init__(self,N): self.N = N self.cnt = [0]*self.N self.sgt_min = SegTree([N-1]*N,segfunc_min,ide_ele_min) self.sgt_max = SegTree([0]*N,segfunc_max,ide_ele_max) def add(self,x): if self.cnt[x]==0: self.sgt_min.update(x,x) self.sgt_max.update(x,x) self.cnt[x] += 1 def delete(self,x): ''' 値がxの要素を一つ削除 ''' self.cnt[x] -= 1 if self.cnt[x] == 0: self.sgt_min.update(x,self.N-1) self.sgt_max.update(x,0) def popMin(self): mi = self.sgt_min.query(0,self.N) self.delete(mi) return mi def popMax(self): ma = self.sgt_max.query(0,self.N) self.delete(ma) return ma def searchMin(self): return self.sgt_min.query(0,self.N) def searchMax(self): return self.sgt_max.query(0,self.N) def lower_bound(self,x): ''' x未満で最大の数を返す ''' return self.sgt_max.query(0,x) def upper_bound(self,x): ''' x以上で最小の数を返す ''' return self.sgt_min.query(x,self.N) def contain(self,x): ''' xが存在するか->bool ''' return bool(self.cnt[x]) メゾットの説明 0. インスタンスの作成 インスタンスを作成します。 Nはデータのサイズです。セグメント木を使うのでデータサイズを予め決めなければいけないです。 つまり、multisetに出し入れできる値は0以上N未満に限られます。 multiset = QuasiMultiset(N) コンストラクタでは ・要素の個数管理用の配列cnt ・区間Minクエリ、区間Maxクエリを処理できるセグメント木1本ずつ を用意します。セグメント木の初期値は全ての要素が区間MinではN-1、区間Maxでは0です。 def __init__(self,N): self.N = N self.cnt = [0]*self.N self.sgt_min = SegTree([N-1]*N,segfunc_min,ide_ele_min) self.sgt_max = SegTree([0]*N,segfunc_max,ide_ele_max) 1. addクエリ 多重集合に値を追加します。 multiset.add(x) 具体的には両方のセグメント木のx番目の値をxに変更し、xのカウンタをインクリメントします。 def add(self,x): if self.cnt[x]==0: self.sgt_min.update(x,x) self.sgt_max.update(x,x) self.cnt[x] += 1 2. deleteクエリ 多重集合から要素xを一つ削除します。 multiset.delete(x) この操作で多重集合内のxの数が0個になると両方のセグメント木のx番目の値を初期化します。 def delete(self,x): self.cnt[x] -= 1 if self.cnt[x] == 0: self.sgt_min.update(x,N-1) self.sgt_max.update(x,0) 3. popMinクエリ 多重集合内の要素のうち最小の要素をpopします。 mi = multiset.popMin() 具体的には全体の最小値を取り、その値をdeleteします。 def popMin(self): mi = self.sgt_min.query(0,self.N) self.delete(mi) return mi 4. popMaxクエリ 多重集合内の要素のうち最大の要素をpopします。 ma = multiset.popMax() 具体的には全体の最大値を取り、その値をdeleteします。 def popMax(self): ma = self.sgt_max.query(0,self.N) self.delete(ma) return ma 5. searchMinクエリ popMinと違って最小値を返すだけです。 mi = multiset.searchMin() 6. searchMaxクエリ popMaxと違って最大値を返すだけです。 ma = multiset.searchMax() 7. lower_boundクエリ ある値x 未満で多重集合内に存在する最大の要素を返します。 lb = multiset.lower_bound(x) 具体的には[0,x)の区間Maxを取ります。 def lower_bound(self,x): return self.sgt_max.query(0,x) 8. upper_boundクエリ ある値x 以上で多重集合内に存在する最小の要素を返します。 ub = multiset.upper_bound(x) 具体的には[x,N)の区間Minを取ります。 def upper_bound(self,x): return self.sgt_min.query(x,self.N) 9. containクエリ 多重集合内に要素xが存在するかというクエリに対してbool型で返します。 b = multiset.contain(x) デメリット 今回実装した疑似multisetは前回heapqを用いて実装したものに比べ 二分探索ができるようになった分、上位互換に見えるかもしれませんがデメリットもいくつか存在します。 データサイズが限られている 実装にセグメント木を用いるため最初にデータサイズを指定しないといけないというデメリットが生じます。 しかし、昨今のAtCoderで出される問題はクエリ先読みができる形式のものが多く 座標圧縮を用いることでうまく対処できる可能性が高いです。 インスタンスの作れる数が限られている これも先ほどと同様にセグメント木の性質から生じるデメリットです。 データサイズNのインスタンスを作成する場合、セグメント木も同時に作成するのでO(N)メモリが必要となります。 その点heapqを用いた実装では空のheap木を作成するだけでいいのでメモリの心配はしなくていいです。 使用例 ABC217 D. Cutting Woods : 先日のABCで出題され、話題になった問題です。座標圧縮を用いることでクエリ数以下のデータサイズに収められます。あとはクエリを順番に消化、二分探索でxの左右の値を求めるだけです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

自動微分と勾配テープ(TensorFlow)入門

はじめに 自動微分と勾配テープでつまずいているので、調査しまとめることにしました。 主に以下のサイトを参考にしました。 公式より、「自動微分と勾配テープ」とは TensorFlow には、自動微分、すなわち、入力変数に対する計算結果の勾配を計算するためのtf.GradientTape API があります。TensorFlow は、tf.GradientTape のコンテキスト内で行われる演算すべてを「テープ」に「記録」します。その後 TensorFlow は、そのテープと、そこに記録された演算ひとつひとつに関連する勾配を使い、トップダウン型自動微分(リバースモード)を使用して、「記録」された計算の勾配を計算します。 レポジトリはこちらです。 import import tensorflow as tf 基礎 1回微分 y = x^2 を微分して見ます。 \frac{dy}{dx} = 2x \\ \frac{dy}{dx} |_{x=8} = 16 操作は、このコンテキストマネージャー内で実行され、その入力の少なくとも1つが「監視」されている場合に記録されます。 x = tf.constant(8.0) # 8.0, shape=() with tf.GradientTape() as t: t.watch(x) y = x * x # 元の入力テンソル x に対する z の微分 dy_dx = t.gradient(y, x) # 16.0, shape=() 2回微分 y = x^2 を微分して見ます。 \frac{dy}{dx} = 2x \\ \frac{dy^2}{d^2x} = 2 \\ x = tf.constant(3.0) # 3.0, shape=() print(x) with tf.GradientTape() as t: t.watch(x) with tf.GradientTape() as tt: tt.watch(x) y = x * x dy_dx = tt.gradient(y, x) # 6.0, shape=() dy2_dx2 = t.gradient(dy_dx, x) # 2.0, shape=() 定数で微分できない場合 公式サイトによれば、微分できない場合は, '1' のテンソルを返すそうです。 x = tf.constant(8.0) # 8.0, shape=() with tf.GradientTape() as t: t.watch(x) y = x dy_dx = t.gradient(y, x) # 1.0, shape=() 制御フローの記録 勾配テープは演算を実行の都度記録するため、(たとえば if や while を使った)Python の制御フローも自然に扱われます。 def f(x, y): output = 1.0 for i in range(y): if i > 1 and i < 5: output = tf.multiply(output, x) return output def grad(x, y): with tf.GradientTape() as t: t.watch(x) out = f(x, y) return t.gradient(out, x) x = tf.convert_to_tensor(2.0) grad(x, 6).numpy() # 12.0 勾配テープ内で使用するメソッド テンソルのまま処理できます。 よく使うメソッド reduce_sum = 配列内のすべての要素を足し算する関数 multiply = テンソルの要素の掛け算する関数 z = y^2 \\ \frac{dz}{dy} = 2x \\ \frac{dz}{dy} |_{x=4} = 8 より \frac{dz}{dx} = 8 \cdot \begin{bmatrix} 1 & 1 \\ 1 & 1 \end{bmatrix} =\begin{bmatrix} 8 & 8 \\ 8 & 8 \end{bmatrix} x = tf.ones((2, 2)) # x = [[1. 1.] [1. 1.]] with tf.GradientTape() as t: t.watch(x) y = tf.reduce_sum(x) # 4.0, shape=() z = tf.multiply(y, y) # 16.0, shape=() dz_dx = t.gradient(z, x) # [[8. 8.] [8. 8.]], shape=(2, 2) ループ add = 関数の足し算 less = 未満の条件式 i = tf.constant(0) c = lambda i: tf.less(i, 10) # b = lambda i: (tf.add(i, 1), ) def b(i): print(i) return (tf.add(i, 1), ) r = tf.while_loop(c, b, [i]) tf.Tensor(0, shape=(), dtype=int32) tf.Tensor(1, shape=(), dtype=int32) tf.Tensor(2, shape=(), dtype=int32) tf.Tensor(3, shape=(), dtype=int32) tf.Tensor(4, shape=(), dtype=int32) tf.Tensor(5, shape=(), dtype=int32) tf.Tensor(6, shape=(), dtype=int32) tf.Tensor(7, shape=(), dtype=int32) tf.Tensor(8, shape=(), dtype=int32) tf.Tensor(9, shape=(), dtype=int32) Out[27]: (<tf.Tensor: shape=(), dtype=int32, numpy=10>,) 指定したインデックスを出力 gather_nd = テンソルと、そのテンソル内の位置を表すインデックスを提供します。指定したインデックスに対応するテンソルの要素を返します。第一引数は、探すテンソル、第二引数は、そのインデックスを指定します。 一次元配列の場合 x = tf.constant([0.1, 0.1, 1.5, 4.5, 3.6, 1.2]) y = tf.constant([3.1, 2.3, 1.4, 2.3, 4.4, 3.1]) uniqueIndices = tf.constant([0, 1, 2, 3, 4, 5]) x = tf.gather_nd(x, uniqueIndices[:,None]) y = tf.gather_nd(y, uniqueIndices[:,None]) tf.multiply(x, y) # shape=(6,), array([ 0.31 , 0.23 , 2.1 , 10.349999, 15.84 , 3.72 ]) array([ 0.31 , 0.23 , 2.1 , 10.349999, 15.84 , 3.72 ]) の間が空いているのは、その分のメモリを開けているからだと思われます。 二次元配列の場合 x = [[1,2,3],[4,5,6]] y = tf.gather_nd(x, [[1,1],[1,2]]) # shape=(2,) numpy=array([5, 6], dtype=int32) 条件分岐 less = 大小比較のメソッド cond = いわゆるif。条件分岐します。 x = tf.constant(2) y = tf.constant(5) def f1(): return tf.multiply(x, 17) def f2(): return tf.add(y, 23) r = tf.cond(tf.less(x, y), f1, f2) rはf1()の値が設定されます。f2の操作(例:tf.add)は実行されません。 (x= 2, r = 34) 関数 公式より tf.functionを用いてある関数にアノテーションを付けたとしても、一般の関数と変わらずに呼び出せます。一方、実行時にはその関数はグラフへとコンパイルされます。これにより、より高速な実行や、 GPU や TPU での実行、SavedModel へのエクスポートといった利点が得られます。 @tf.function def mini_func(x): return x * x x = tf.constant(5.0) with tf.GradientTape() as t: t.watch(x) result = mini_func(x) dy_dx = t.gradient(result, x) print(dy_dx) # tf.Tensor(10.0, shape=(), dtype=float32) constantとVariableの違いについて TensorFlowにおけるconstantとVariableの違いは、constantを宣言した場合、その値は将来的に変更できない(また、初期化は操作ではなく値で行う必要がある)ことです。 また、デフォルトでは、GradientTapeは、コンテキスト内でアクセスされるすべてのtrainable変数を自動的に監視します。(t.watch(v)しなくてよい。) constantの場合 @tf.function def mini_func(x): return x * x x = tf.constant(5.0) with tf.GradientTape() as t: t.watch(x) result = mini_func(x) dy_dx = t.gradient(result, x) print(dy_dx) # tf.Tensor(10.0, shape=(), dtype=float32) Variableの場合 @tf.function def mini_func(x): return x * x x = tf.Variable(5.0) with tf.GradientTape() as t: result = mini_func(x) dy_dx = t.gradient(result, x) print(dy_dx) # tf.Tensor(10.0, shape=(), dtype=float32)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

パスを通す?環境変数とは…??command not findに打ち勝ちたくて調べたこと

入れたはずのPythonが認識されない… などなど、入れたはずのコマンド、ソフト、言語がチュートリアル通りにターミナルへ入力してもcommand not findと出力される問題。 こんなことがあった時に偉い人からよく聞くアドバイス「どうせパスが通ってないんでしょ?」 いやいや、パスってなんじゃい!と思って重い腰を上げて調べました。 ちなみに私は使っているmacOSが古く、bashを使ってます。 新しいmacOS(確かCatalina以降?)の人はzshなので、編集するファイルが違ったり、ちょこっと違うみたいなのでご了承ください。 パスを通すとはコマンドを追加してあげること コマンドやソフト、言語を使うためには、そこまでのファイルパスを環境変数に設定して、文字通り道を作ってあげないと使えない。 パスはそのコマンドなどの情報が保存されているファイルまでの道を登録するってことらしい。 LinuxやmacみたいなUNIX系OSは新しい言語とかツールとかを登録する時にこのパスを通す作業をしないとそのコマンドが使えないことがあるみたい。 「パスが通ってない」とは コマンドを入力したのに、command not found(コマンド見つからなかった)と出る状態。 $ アイウエオ -bash: アイウエオ: command not found そんなコマンド知らないよ。(=そんなんどこにあるの?)となる。 実際にはインストールしたのに!コマンドのファイルがあるのに!って思っても、そこまでのパスが通っていないので、そのファイルが見つからない時もこれが出る。 いざパスを通す(一時的ver.) パスを通すとは、具体的には$PATHという変数に、そのコマンドが入っているファイルへのファイルパスを登録することです。 ということで、まずはその$PATHの中に何が入っているのか、$ echo $PATHコマンドを入力して見てみましょう。 $ echo $PATH /usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin 出力された/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbinが$PATHの中身です。 :までで1区切りとして読み、左側にいくにつれて優先順位が高くなります。 そのため、上記出力例でいくと、以下の様になります。 優先順位ランキング 1位 /usr/local/binさん 2位 /usr/binさん 3位 /bin:/usr/sbinさん 4位 /sbinさん $PATHという変数の中身を確認したら、追加したいコマンドのファイルパスをFinderとかを使って予め調べておいて、ターミナルに以下を入力します。 $ export PATH=$PATH:加えたいコマンドのファイルパス けれども、これだとターミナルを閉じるとまた初期化されて元どおりになってしまい、再度開いてコマンドを入力するとcommand not foundになってしまいます。。 いざパスを通す(永遠にver.) 恒久的にパスを通すためには.bash_profileや.profile, .bsh, .zshrcみたいなファイルを編集します。 だいたい、.bash_profileや.profile, .bsh, .zshrcなどはホーム(macだとユーザー名の直下)に入っているみたいなので、今ターミナルで開いている階層がホームではない人は、何も考えずにターミナルでcdと入力するとホームに移動できるので、そこで以下のls -aコマンドを入力してどんなドットファイルが入っているのか、確認しましょう。 $ cd $ ls -a すると今いる階層(多分ホームにいますよね?)にある、色々なドットファイルなどそこの階層にあるものが一覧表示されます。 あとはviコマンドでそれらを開いて、編集しましょう。 $ vi 編集するファイル名 開いたら、少し変わった表示になったと思います。そのままだと入力ができないので、Iキーを押してインサートモードにして入力ができる状態にし、最終行にさっきの追加したいコマンドのファイルパスを入力します。 入力が終わったら、Escキーでインサートモードを解除し、:wqと入力して、編集内容を保存してエディタを閉じます。 ここまでくれば、あとはターミナルのウインドウを消して、ターミナルを立ち上げ直せば再起動したことになり、先ほどの編集内容が反映されるはずです。 再起動が面倒であれば以下のコマンドで編集内容を反映させる方法もあります。 $source ~/.bash_profile 「コマンドの情報が入っているファイルの場所を調べるコマンド」でパスが反映されてるか確認 先ほど通したパスが、きちんと通っているか確認します。 そのために、コマンドのパス情報がどこのファイルに入っているのか確認する以下のコマンドを入力してみましょう。 $ which コマンド名 実行例 $ which pyenv /usr/local/bin/pyenv
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

mayaがエラー・ワーニングを吐く時の対処 4 ~不死身の亀~

TurtleDefaultBakeLayerというノードが何もしてないのに出来てしまっいて、 「削除してくだい」と言われたので削除しようとしたけれども、deleteボタンを押すと // Error: file: C:/Program Files/Autodesk/Maya2019/scripts/others/doDelete.mel line 111: Cannot delete locked node 'TurtleDefaultBakeLayer'. と断られる。 TurtleDefaultBakeLayerは レンダリング設定で render using を turtleにすると生成されるturtleの為のノード。 ワーニングの通り、ノードがロックされているので通常の手順では削除できない。 1,カメなのにウサギ殺しのボタンを押す シェルフのturtleに登録されているこのボタン。 (カメを削除したいのに、ウサギにバッテン・・・・) これを押すとTurtleDefaultBakeLayerは削除される。他のturtle由来のノードも削除される。 2,プラグインをunloadする。 プラグインをunloadすると 特にシーンに対して何も影響がなければ、turtle由来のノードは削除されプラグインをunloadしてくれる。 ロードされていると何かの拍子に出てきてしまうので、プラグインマネージャーからload/autoLoadをoffにしておく。 それでもTurtleDefaultBakeLayerが存在しているデータが回ってくるとプラグインはloadされるし、TurtleDefaultBakeLayerはまた出てくるしで面倒なので上記2点を一括処理にして見つけたら即処理にしてしまうのがお勧め。 ただし、Turtleを実際に使用している場合は消すとより面倒になるのでその辺はしっかり確認してください。 ノードのlock "locked node" とあるように、ノードが lock状態にあるとmayaではノードを削除できない。 cmds.lockNode("pSphere1", lock =True) でノードをlockできる。 cmds.lockNode("pSphere1", lock =False) でlockを解除できる。 が、無暗に解除して削除するとデータが容易に壊れるので注意が必要。 例えば referenceノードもシステム上は lockNodeでlockされてるだけなので、lockを外せば削除できる。 必要な物を保護するためにlockしていることが多いので、何も考えずに nodes = cmds.ls() cmds.lockNode(nodes , lock =False) みたいなことをやるのはダメ、絶対。 unkownノードもロックを外せば削除できるが、そもそもunkownノードが生成されるという事はシーンが正確に再現できていない可能性が高いので これも何も考えずに削除すると、正しいシーンを再現することができなくなる可能性が非常に高い。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

OpenCVを使って緑被を抽出する

Pythonで使えるライブラリの1つとしてOpenCVがありますが、活用例として特定の色の抽出がよく紹介されます。 今回は、OpenCVと緑被分布図を使って、駅から半径1.5km以内の範囲にある緑被の抽出・緑被率の計算を行うプログラムを作成してみました。 緑被分布図の準備 緑被分布図は下記のリンクから。 首都圏と近畿圏のものがあるようです。 緑被分布図の公開 - 大都市圏整備(国土交通省) https://www.mlit.go.jp/crd/daisei_ryokuhi_index.html 駅から半径1.5km以内の範囲を示す緑被分布図の作成 現時点では気合いで作成します(いずれは自動化していきたい…)。 例えば新宿駅なら次のような図になります。 コード 下記のサイトを参考に、コードを書いてみました。 OpenCV 入門 (10) - 色の抽出 https://note.com/npaka/n/nc6764b99dbe0 【Python+OpenCV】特定の色を検出するプログラム https://craft-gogo.com/python-opencv-color-detection/ 今回は「緑被地(主に樹林地)」・「緑被地(主に草地)」の2種類を抽出します。 import cv2 import numpy as np #パスを指定 path = "" #適宜パスを指定する #駅を指定 station = "shinjuku" #上記パスの下に作成した円形の緑被分布図を置き、駅名を付けて保存しておく #画像の読み込み img = cv2.imread(f'{path}{station}.jpg') #周囲の余白を除去・計測・図示 img_array = np.asarray(img) white_min = np.array([235, 235, 235]) white_max = np.array([255, 255, 255]) mask_white = cv2.inRange(img, white_min, white_max) cv2.imwrite(f'{path}white_mask_{station}.png', mask_white) white_array = np.asarray(mask_white) white_count = np.count_nonzero(white_array == 255) #HSVに変換 hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) # 緑色のHSVの値域 hsv_min = np.array([30, 64, 0]) hsv_max = np.array([90, 255, 255]) mask = cv2.inRange(hsv, hsv_min, hsv_max) masked_img = cv2.bitwise_and(img, img, mask = mask) #緑色のピクセル範囲を図示 cv2.imwrite(f'{path}green_mask_{station}.png', mask) cv2.imwrite(f'{path}green_masked_img_{station}.png', masked_img) #緑色のピクセル比率を算出 hsv_array = np.asarray(mask) chi = np.count_nonzero(hsv_array == 255) mot = np.count_nonzero(hsv_array > -1) - white_count prob = chi / mot * 100 print(prob, "%") 出力 余白の判定図 緑被の判定図 緑被の抽出図 緑被率は下記の通り。 10.700869741784832 % 参考文献 緑被分布図の公開 - 大都市圏整備(国土交通省) https://www.mlit.go.jp/crd/daisei_ryokuhi_index.html OpenCV 入門 (10) - 色の抽出 https://note.com/npaka/n/nc6764b99dbe0 【Python+OpenCV】特定の色を検出するプログラム https://craft-gogo.com/python-opencv-color-detection/
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Athenaで基礎からしっかり入門 分析SQL(Pythonコード付き) #4

今まで複雑なデータ操作・分析などはPythonでやっており、SQLは普通のアプリ開発程度のライトなものしか触って来なかったのですが、やはり分析用の長いSQLなども書けた方がやりとり等で便利・・・という印象なので、復習も兼ねて記事にしておきます。 また、SQLに加えて検算も兼ねてPythonやPandasなどを使ったコードもSQLと併記していきます(Pythonで書くとどういった記述が該当するのかの比較用として使います)。 ※長くなるのでいくつかの記事に分割します。本記事は4記事目となります。 特記事項 本記事では配列関係を良く扱っていきます。その都合、Pandasなどでapplyメソッドなどを絡めた形で触れている節もあればビルトインでの記述のみしている節もあります(本記事からPandas色が少し薄くなります)。 他のシリーズ記事 ※過去の記事で既に触れたものは本記事では触れません。 #1: 用語の説明・SELECT、WHERE、ORDER BY、LIMIT、AS、DISTINCT、基本的な集計関係(COUNTやAVGなど)、Athenaのパーティション、型、CAST、JOIN、UNION(INTERSECTなど含む)など。 #2: GROUP BY・HAVING・サブクエリ・CASE・COALESCE・NULLIF・LEAST・GREATEST・四則演算などの基本的な計算・日付と日時の各操作など。 #3: 文字列操作全般・正規表現関係など。 この記事で触れること コメント関係 配列操作全般 ラムダ式 SQLを読みやすくする: コメント SQL中にコメントを残すことで、なんのSQLなのか、どういった処理なのかを各所に追加していくことができます。引き継ぎや他人との共有時などにSQLを読む負担が減ります。 Athena(Presto)では以下のようなコメントの書き方がサポートされています。 インラインコメント --以降の文字列はインラインコメントとして扱われ、SQLには影響しません。 以下のように「ログインログを10行読み込みます。」といったような説明を入れてもそのままSQLを実行できます。 -- ログインログを10行読み込みます。 SELECT * FROM athena_workshop.login LIMIT 10; 行中にコメントを追加したい場合には、行末であれば追加することができます。 SELECT * FROM athena_workshop.login LIMIT 10 -- ログインログを10行読み込みます。 ただし、セミコロンが付いている行に関してはその後にSQL関係を入力することができなくなり、コメントなども書かれているとエラーになります。Athenaではセミコロンが無くても動くので、そういった場合はセミコロンを消してしまってもいいかもしれません。 SELECT * FROM athena_workshop.login LIMIT 10; -- ログインログを10行読み込みます。 複数行のコメントを追加したい場合には複数の--の記述を各行にするか、もしくは後の節で触れるブロックコメントを利用します。 -- ログインログを10行読み込みます。 -- 日付は2021-01-01を対象とします。 SELECT * FROM athena_workshop.login WHERE dt = '2021-01-01' ブロックコメント /**/の記述のアスタリスクの間に挟んだコメントはブロックコメント(複数行に対応できるコメント)になります。例えば以下のように書きます。 /* ログインログを10行読み込みます。 */ SELECT * FROM athena_workshop.login LIMIT 10 アスタリスクの間は自由に改行などを含めることができます。毎行--などの記述をしなくて済みますし、コメント部分のテキストのコピーなどをしたい場合などにも--などが紛れ込まないので複数行の場合にはこちらの方が快適なケースがあります。 /* ログインログを10行読み込みます。 日付は2021-01-01を対象とします。 */ SELECT * FROM athena_workshop.login WHERE dt = '2021-01-01' 配列の操作 この節以降では配列(Array)操作について色々触れていきます。 配列の固定値の扱い方 配列の固定値を作りたい場合にはARRAY [配列の値]という書き方をすることでカラムを設定することができます。例えば1, 2という値を格納した配列と3, 4という値を格納した配列の2つを得たい場合には以下のようにSQLを実行することで配列の固定値を取得することができます。 SELECT ARRAY [1, 2] AS arr_1, ARRAY [3, 4] AS arr_2 特定のテーブルと一緒に使った場合、各行に対してこの固定値が設定されます。 SELECT ARRAY [1, 2] AS arr_1, ARRAY [3, 4] AS arr_2, user_id FROM athena_workshop.login LIMIT 10 Pythonでの書き方 Pandasを初期化する際にカラムに配列などが含まれていればそのまま配列の値を持ったデータフレームが作れるのですが、途中から配列の固定値のカラムを追加する場合、固定のスカラ値などを直接代入するケースと同じようにはストレートにいきません。配列の各値を各行に設定する・・・みたいな挙動になるため、リストなどを代入しようとするとデータフレームとリストの行数が一致していないとエラーになります。 例えば以下のように[1, 2]という配列の固定値をarr_1というカラムに設定しようとすると行数の不一致によるエラーになります。 import pandas as pd import numpy as np df: pd.DataFrame = pd.read_json( 'https://github.com/simon-ritchie/athena_workshop/blob/main/workshop/login/dt%3D2021-01-01/data.json.gz?raw=true', lines=True, compression='gzip') df['arr_1'] = [1, 2] ValueError: Length of values (2) does not match length of index (24453) 色々対策として書き方はあると思いますが、1つの対応としてリスト内包表記などを使って行数を揃えてしまう・・・というのがシンプルかもしれません。この辺は探せばもっとシンプルなPandasのインターフェイスがあるような気がしないでもないです。 import pandas as pd import numpy as np df: pd.DataFrame = pd.read_json( 'https://github.com/simon-ritchie/athena_workshop/blob/main/workshop/login/dt%3D2021-01-01/data.json.gz?raw=true', lines=True, compression='gzip') df['arr_1'] = [[1, 2] for _ in range(len(df))] print(df[['user_id', 'arr_1']].head()) user_id arr_1 0 8590 [1, 2] 1 568 [1, 2] 2 17543 [1, 2] 3 15924 [1, 2] 4 5243 [1, 2] ||記号による配列の連結 ※以降の節ではuser_weapon_and_armor_idsというテーブルを扱っていきます。基本的なカラムに加えてweapon_idsとarmor_idsという2つのカラムを持っており、それぞれ所持武器と所持防具のマスタIDを想定した整数の配列を格納させています。 以下のようなデータになっています(armor_idsカラムの方がスクショ内に収まらないのでスクロールして2枚スクショを貼っています)。分かりやすいようにarmor_idsの方が大きな整数値のIDを設定しています。 SELECT * FROM athena_workshop.user_weapon_and_armor_ids LIMIT 10; 配列同士は||記号で連結することができます。 例えば2つの配列のカラムを連結したい・・・といった場合には以下のように書きます(concatenated_idsという1つのカラムに変換しています)。 SELECT weapon_ids || armor_ids AS concatenated_ids FROM athena_workshop.user_weapon_and_armor_ids LIMIT 10; 後半がarmor_idsカラムの大きな値になっていることが確認できます。 また、固定値との連結もできます。 以下のSQLでは[1, 2, 3]という配列とweapon_idsというカラムの配列を連結しています。連結されて結果の配列の先頭が1, 2, 3からスタートしていることが確認できます。 SELECT ARRAY [1, 2, 3] || weapon_ids AS concatenated_ids FROM athena_workshop.user_weapon_and_armor_ids LIMIT 10; Pythonでの書き方 Pandasは基本的に各インターフェイスが行列前提のものが多く、リストの値を含んだデータフレームのための連結関係のインターフェイスは無い・・・?気がしますので、applyなりループを回したりで対応する必要があるかなという所感です(しっかり探せばその辺のインターフェイスもあるかもしれません)。 その辺は別の記事で書いたり、もしくはループで回したときのパフォーマンス関係をご共有いただいたりしているので必要に応じてそちらもご確認ください。 以下はループで回す場合の書き方例です。 from typing import List import pandas as pd df: pd.DataFrame = pd.read_json( 'https://github.com/simon-ritchie/athena_workshop/blob/main/workshop/user_weapon_and_armor_ids/dt%3D2021-01-01/data.json.gz?raw=true', lines=True, compression='gzip') weapon_ids: List[List[int]] = df['weapon_ids'].tolist() armor_ids: List[List[int]] = df['armor_ids'].tolist() concatenated_ids: List[List[int]] = [] for single_weapon_ids, single_armor_ids in zip(weapon_ids, armor_ids): single_weapon_ids.extend(single_armor_ids) concatenated_ids.append(single_weapon_ids) df['concatenated_ids'] = concatenated_ids print(df['concatenated_ids'].head()) print(df.at[0, 'concatenated_ids']) Name: concatenated_ids, dtype: object 0 [240, 65, 157, 257, 51, 266, 211, 208, 257, 35... 1 [45, 123, 75, 271, 87, 298, 156, 243, 80, 57, ... 2 [85, 30, 163, 151, 142, 93, 229, 258, 101, 6, ... 3 [7, 128, 288, 157, 277, 279, 257, 202, 230, 97... 4 [20, 216, 19, 7, 189, 283, 131, 63, 48, 281, 5... [240, 65, 157, 257, 51, 266, 211, 208, 257, 35, 187, 94, 264, 267, 18, 248, 70, 185, 156, 293, 96, 65, 143, 195, 115, 117, 107, 160, 299, 249, 85, 39, 259, 12, 207, 92, 290, 297, 152, 121, 57, 134, 182, 38, 60, 138, 244, 215, 223, 79, 208, 97, 87, 38, 160, 80, 170, 240, 206, 225, 155, 210, 245, 36, 55, 117, 14, 112, 215, 252, 236, 232, 284, ... 450, 343, 386, 425, 492, 456, 332, 475, 413, 452, 469, 477, 386, 452, 346, 444, 392, 418, 311, 466, 470, 442, 368, 319, 378, 476, 368, 491, 498, 497, 433, 337, 359, 346, 398, 446, 411, 333, 494, 500, 397, 402, 355, 317, 392, 498, 407, 499, 431, 466, 441, 458, 444, 338, 350, 440, 500, 392, 339, 447, 343, 326, 422, 345, 351] CONCAT関数による配列の連結 CONCAT関数でも||の記号を使った場合と同じように複数の配列を連結することができます。各引数に連結したい配列を指定します。2つ以上の配列も指定することができます。 以下のSQLでは[1, 2, 3]という固定値の配列とweapon_idsカラム、armor_idsカラムと3つの配列を連結しています。 SELECT CONCAT(ARRAY [1, 2, 3], weapon_ids, armor_ids) AS concatenated_ids FROM athena_workshop.user_weapon_and_armor_ids LIMIT 10; []記号による配列の特定の位置の値へのアクセス 配列のカラムや固定値などに対して[]の括弧と整数を指定することで、配列の特定の位置の値にアクセスすることができます(添え字 / インデックスによるアクセス)。 なお、Athena(Presto)では配列のインデックスは1スタートとなります。jsやPythonのように0スタートではなく、Juliaなどのような挙動になるので注意が必要です。 SELECT weapon_ids[1] AS first_value, weapon_ids FROM athena_workshop.user_weapon_and_armor_ids LIMIT 10; 以下のSQLでは配列の先頭の値を取得しています。 配列の固定値を使った場合にもインデックスの指定が効くようです。以下のSQLでは1, 2, 3という固定値の配列の2番目の値を取得しています。 SELECT ARRAY [1, 2, 3][2] AS second_value FROM athena_workshop.user_weapon_and_armor_ids LIMIT 10; なお、Pythonなどのように負のインデックスを指定して配列の末尾からアクセスするといったことはできません。エラーになります。 SELECT weapon_ids[-1] AS first_value, weapon_ids FROM athena_workshop.user_weapon_and_armor_ids LIMIT 10; ELEMENT_AT関数による配列の特定の位置の値へのアクセス []の括弧による特定の位置の配列の値へのアクセスと同じようなことがELEMENT_AT関数で出来ます。 第一引数に対象のカラムもしくは固定値、第二引数に値の位置のインデックスを指定します。こちらも値の位置のインデックスは1からスタートします。 SELECT ELEMENT_AT(weapon_ids, 1) AS first_value, weapon_ids FROM athena_workshop.user_weapon_and_armor_ids LIMIT 10 特筆すべき点として、[]の括弧では使えなかった負のインデックスの指定がPythonと同様に使えます。負の値を指定した場合は配列の末尾からカウントされます。例えば-1を指定すれば配列の最後の値が対象となり、-2を指定すれば配列の最後から二番目の値の位置となります。 SELECT ELEMENT_AT(weapon_ids, -1) AS last_value, weapon_ids FROM athena_workshop.user_weapon_and_armor_ids LIMIT 10 Pythonでの書き方 ※Pandasが基本的に行列を扱うという性質上、ここからしばらくはデータフレーム内の配列への処理としてPythonビルトインの処理とapplyメソッドなどを絡めた書き方が多くなると思います。 特定の配列のインデックスの値を取得する関数を定義し、データフレームのapplyメソッドでそちらの関数を指定することで同じようなことができます。applyメソッドには任意のキーワード引数で固定値も渡すことができるので、今回はindexという引数を設定しています。 from typing import List, Optional import pandas as pd df: pd.DataFrame = pd.read_json( 'https://github.com/simon-ritchie/athena_workshop/blob/main/workshop/user_weapon_and_armor_ids/dt%3D2021-01-01/data.json.gz?raw=true', lines=True, compression='gzip') def element_at(list_value: List[int], index: int) -> Optional[int]: """ 指定されたインデックス位置の値をリストから取得する。 Parameters ---------- list_value : list of int 対象のリスト。 index : int 対象のインデックス。 Returns ------- value : int or None 指定されたインデックス位置の値。インデックス範囲外の場合には Noneが返却される。 """ if index >= len(list_value): return None value: int = list_value[index] return value df['first_value'] = df['weapon_ids'].apply(element_at, index=0) print(df[['first_value', 'weapon_ids']].head()) first_value weapon_ids 0 240 [240, 65, 157, 257, 51, 266, 211, 208, 257, 35... 1 45 [45, 123, 75, 271, 87, 298, 156, 243, 80, 57, ... 2 85 [85, 30, 163, 151, 142, 93, 229, 258, 101, 6, ... 3 7 [7, 128, 288, 157, 277, 279, 257, 202, 230, 97... 4 20 [20, 216, 19, 7, 189, 283, 131, 63, 48, 281, 5... 重複を除いた形での2つの配列の連結を行う: ARRAY_UNION ARRAY_UNION関数を使うと2つの配列を重複を除いた形で連結した配列を取得することができます。第一引数に1つ目の配列、第二引数に2つ目の配列を指定します。 例えば以下のSQLでは1, 2, 3という値を含んだ配列と2, 3, 4という値を含んだ配列を指定しています。2と3の部分は重複していますが連結結果は1, 2, 2, 3, 3, 4といった値にはならずに1, 2, 3, 4という配列になります。 SELECT ARRAY_UNION(ARRAY [1, 2, 3], ARRAY [2, 3, 4]) AS union_array Pythonでの書き方 処理自体はapplyもしくはループで回すとして(以下のサンプルではループで回しています)、ビルトインのset関数にリストを渡すと一意な値が取れます。型はlistではなくなるので、setで変換後に再度listに変換しています。[*a, *b]といった記述で二つ以上のリストを連結したコピーを得られるのでそちらをsetの引数に使っています。 from typing import List import pandas as pd df: pd.DataFrame = pd.DataFrame( data=[{'a': [1, 2, 3], 'b': [2, 3, 4]}]) a_list: List[List[int]] = df['a'].tolist() b_list: List[List[int]] = df['b'].tolist() union_list: List[List[int]] = [] for a, b in zip(a_list, b_list): union_list.append(list(set([*a, *b]))) df['union_array'] = union_list print(df) a b union_array 0 [1, 2, 3] [2, 3, 4] [1, 2, 3, 4] 値を繰り返した配列を取得する: REPEAT REPEAT関数は第一引数で指定された値を第二引数に指定された回数分繰り返した配列を取得します。第一引数には色々な型の値を指定できます。第二引数には整数が必要になります。 例えば以下のSQLでは整数の3という値を5回繰り返すSQLを実行しています。 SELECT REPEAT(3, 5) AS repeated_array 整数以外でも、例えば配列の繰り返しなどもできます。以下のSQLでは[1, 2, 3]という配列を3回繰り返しています。結果は[1, 2, 3, 1, 2, 3, 1, 2, 3]という1次元の配列ではなく[[1, 2, 3], [1, 2, 3], [1, 2, 3]]という2次元の配列になります。 SELECT REPEAT(ARRAY [1, 2, 3], 3) AS repeated_array Pythonでの書き方 ※以降の節ではデータフレームを絡めない形で触れるケースも出てきますが、前節までのapplyやループなどでデータフレームに設定することでデータフレーム側にも同じようなことができます。 リストを乗算するとリストの値を繰り返した値になります。例えば以下のようにすると3の値を5回繰り返したリストになります。 print([3] * 5) [3, 3, 3, 3, 3] SQLのように[[1, 2, 3], [1, 2, 3], [1, 2, 3]]といった二次元配列を得たければ与えるリストも2次元にしておくことで対応ができます。 print([[1, 2, 3]] * 3) [[1, 2, 3], [1, 2, 3], [1, 2, 3]] ARRAY_AVERAGE や ARRAY_SUM などの配列への要約統計量用の関数について Prestoのドキュメントには配列の平均値を計算するARRAY_AVERAGEや合計値を計算するARRAY_SUMなどの関数があるのですが、これらはどうやらAthena上ではまだ利用できないようです。将来のアップデートで反映されるかもとは思いますが、本記事でもそれらの関数は触れずに進めます。 配列の各値の最大値を取る: ARRAY_MAX ※この節以降ではbattle_party_powerというテーブルを説明のために使っていきます。クエスト(バトル)ごとのパーティーメンバーのIDと装備やレベルに応じた戦闘力を想定した値を格納しています。基本的なカラムに加えて以下のようなカラムを持っています。 quest_id -> 対象のクエストのIDの整数。 user_ids -> パーティーメンバーのユーザーIDの整数の配列。 powers -> パーティーメンバーの戦闘力の整数の配列。 以下のようなデータになっています。 SELECT * FROM athena_workshop.battle_party_power LIMIT 10; ARRAY_MAX関数を使うと配列の値の中での最大値を取得することができます。 以下のSQLではパーティーの戦闘力の中で一番戦闘力が高い値を取得しています。 SELECT powers, ARRAY_MAX(powers) AS party_max_power FROM athena_workshop.battle_party_power LIMIT 10; Pythonでの書き方 シンプルにビルトインのmax関数をapplyメソッドに指定するだけで対応ができます。 import pandas as pd df: pd.DataFrame = pd.read_json( 'https://github.com/simon-ritchie/athena_workshop/blob/main/workshop/battle_party_power/dt%3D2021-01-01/data.json.gz?raw=true', lines=True, compression='gzip') df['max_power'] = df['powers'].apply(max) print(df[['max_power', 'powers']].head()) max_power powers 0 41013 [41013, 34983] 1 30252 [26151, 29020, 30252] 2 10269 [10269, 8786, 8179, 6633] 3 18561 [18561] 4 50078 [34067, 50078, 37768] 配列の各値の最小値を取る: ARRAY_MIN ARRAY_MIN関数を使うと配列内の最小値を取得することができます。 以下のSQLではパーティーの戦闘力の中で一番戦闘力が低い値を取得しています。 SELECT powers, ARRAY_MIN(powers) AS party_min_power FROM athena_workshop.battle_party_power LIMIT 10; Pythonでの書き方 こちらも最大値算出の時と同じように、ビルトインのmin関数をapplyメソッドの引数に指定するだけで対応ができます。 import pandas as pd df: pd.DataFrame = pd.read_json( 'https://github.com/simon-ritchie/athena_workshop/blob/main/workshop/battle_party_power/dt%3D2021-01-01/data.json.gz?raw=true', lines=True, compression='gzip') df['min_power'] = df['powers'].apply(min) print(df[['min_power', 'powers']].head()) min_power powers 0 34983 [41013, 34983] 1 26151 [26151, 29020, 30252] 2 6633 [10269, 8786, 8179, 6633] 3 18561 [18561] 4 34067 [34067, 50078, 37768] 配列の特定範囲の値の配列を取得する: SEQUENCE SEQUENCE関数は特定の値の範囲の配列を取得することができます。連番生成などに使います。 第一引数には開始値となる数値(後述しますが、日付や日時でも対応ができます)、第二引数に終了値となる数値を指定します。返却値は配列となります。 例えば第一引数に1、第二引数に5を指定すると[1, 2, 3, 4, 5]という配列が返ってきます。 SELECT SEQUENCE(1, 5) AS sequence_value 終了値が開始値よりも小さい場合は減算処理が実行される形で連番が生成されます。例えば開始値に3、終了値に-3を指定すれば[3, 2, 1, 0, -1, -2, -3]という配列結果が得られます。 SELECT SEQUENCE(3, -3) AS sequence_value また、第三引数に整数を指定した場合連番のステップを設定することができます。例えは2ずつ増加させるといった制御になります。 SELECT SEQUENCE(1, 5, 2) AS sequence_value Pythonでの書き方 ビルトインのrange関数を使うと連番の値を取得できます。主にループ処理などで使われますが、list関数を通してリストにキャストすれば連番の配列としても使えます。注意点として、第二引数に指定した値の1つ手前の値が最終値となるため、最終値に対して1大きい値を第二引数に指定する必要があります(Pythonだとインデックスが0からスタートするので、range(0, 5)とすれば最終値は4となり件数も5件となる・・・といった挙動になります)。 print(list(range(1, 6))) [1, 2, 3, 4, 5] 第三引数もSQL側と同様にステップの設定となります。 print(list(range(1, 6, 2))) [1, 3, 5] 特定の日付範囲の配列を取得する: SEQUENCE SEQUENCE関数には日付を開始値と終了値に指定することもできます。開始日~終了日の範囲の配列を得られます。ただし結果の配列の値はDATEではなくTIMESTAMPになるようです。 第三引数にはINTERVALで種類(日数や月数など)や数値(日数値など)を指定します。この辺は以前の記事で触れた日時関係の操作のものと同じなのでそちらをご確認ください。 SELECT SEQUENCE(DATE('2021-01-01'), DATE('2021-01-03'), INTERVAL '1' DAY) AS sequence_value なお、第三引数は整数を指定した時とは異なり省略できません(日付を指定したから1日ずつ・・・とはなりません)。省略するとエラーになります。 SELECT SEQUENCE(DATE('2021-01-01'), DATE('2021-01-03')) AS sequence_value Pythonでの書き方 Pandasのdate_range関数を使うと日付の範囲を取れます。 import pandas as pd print(pd.date_range(start='2021-01-01', end='2021-01-03')) DatetimeIndex(['2021-01-01', '2021-01-02', '2021-01-03'], dtype='datetime64[ns]', freq='D') 返却値はDatetimeIndexという特殊な値になっているので、リストの方が良ければシリーズやndarrayなどと同じようにtolistメソッドで変換できます。 import pandas as pd print(pd.date_range(start='2021-01-01', end='2021-01-03').tolist()) [Timestamp('2021-01-01 00:00:00', freq='D'), Timestamp('2021-01-02 00:00:00', freq='D'), Timestamp('2021-01-03 00:00:00', freq='D')] 特定の日時範囲の配列を取得する: SEQUENCE 日時(TIMESTAMP)を第一引数と第二引数に指定する形でもSEQUENCE関数は使えます。使い方と挙動はほぼ日付を指定した時と同じです。 以下のSQLでは10時~13時の間で1時間ごとに4件の配列を取得しています。 SELECT SEQUENCE(CAST('2021-01-01 10:00:00' AS TIMESTAMP), CAST('2021-01-01 13:00:00' AS TIMESTAMP), INTERVAL '1' HOUR) AS sequence_value Pythonでの書き方 日付と同様にdate_range関数を使います。日付の時に加えてfreq引数にHを指定すると1時間単位で値が返ってきます。 import pandas as pd date_range: pd.DatetimeIndex = pd.date_range( start='2021-01-01 10:00:00', end='2021-01-01 13:00:00', freq='H') print(date_range) DatetimeIndex(['2021-01-01 10:00:00', '2021-01-01 11:00:00', '2021-01-01 12:00:00', '2021-01-01 13:00:00'], dtype='datetime64[ns]', freq='H') 配列の特定の位置から特定の件数の値を格納した配列を取得する: SLICE SLICE関数は配列の特定の開始位置から任意の件数分の値を格納した配列を抽出することができます。第一引数には配列の開始位置の整数、第二引数には値の件数を指定します。 第一引数の開始位置は1からスタートします。0ではありません。 対象が存在しない場合(1件の値しかない配列で2番目以降の値を指定した場合など)には結果の行の値は空の配列となります。 以下のSQLでは配列の2番目の位置から最大2件分の値を抽出した配列を取得しています。 SELECT SLICE(powers, 2, 2) AS second_and_third_powers, powers FROM athena_workshop.battle_party_power LIMIT 10; 第二引数の開始位置には負の値も指定できます。Pythonと同じように-1を指定した場合は配列の最後、-2を指定した場合は最後から2番目の位置・・・といった挙動になります。 以下のSQLでは配列の最後から2番目の位置から1件分の値を抽出しています。 SELECT SLICE(powers, -2, 1) AS sliced_power, powers FROM athena_workshop.battle_party_power LIMIT 10; Pythonでの書き方 以下のコードのようにビルトインのリストでスライスの記法が使えるのでそちらをapplyメソッド用の関数で設定して対応しています。SQLと異なりPythonでは開始インデックスが0なのでそちらに合わせてあります。 from typing import List import pandas as pd def get_sliced_list( list_val: List[int], start: int, length: int) -> List[int]: """ 指定されたリストの特定の位置から任意の件数の値を格納したリストを 取得する。 Parameters ---------- list_val : list of int 対象のリスト。 start : int リストの開始位置。 length : int 取得件数。 Returns ------- list_val : list of int スライス処理後のリスト。 """ list_val = list_val[start:start + length] return list_val df: pd.DataFrame = pd.read_json( 'https://github.com/simon-ritchie/athena_workshop/blob/main/workshop/battle_party_power/dt%3D2021-01-01/data.json.gz?raw=true', lines=True, compression='gzip') df['second_and_third_powers'] = df['powers'].apply( get_sliced_list, start=1, length=2) print(df[['second_and_third_powers', 'powers']].head(10)) second_and_third_powers powers 0 [34983] [41013, 34983] 1 [29020, 30252] [26151, 29020, 30252] 2 [8786, 8179] [10269, 8786, 8179, 6633] 3 [] [18561] 4 [50078, 37768] [34067, 50078, 37768] 5 [34217, 22725] [25882, 34217, 22725, 21635] 6 [] [13968] 7 [1205, 2094] [1573, 1205, 2094, 2021] 8 [] [22217] 9 [] [19061] 一意な値のみに変換した配列を取得する: ARRAY_DISTINCT ARRAY_DISTINCT関数を使うと指定されたカラムもしくは固定値の配列の値から、一意(重複の存在しない)配列を取得できます。 SELECT ARRAY_DISTINCT(ARRAY [1, 2, 2, 1, 3, 4, 2]) AS unique_array Pythonでの書き方 ビルトインのリストであれば一度set関数を通すと一意な値になるので、そちらをさらにlist関数を通してリストに戻すと対応ができます。 from typing import List unique_list: List[int] = list(set([1, 2, 2, 1, 3, 4, 2])) print(unique_list) [1, 2, 3, 4] NumPyのndarrayなどであればunique関数が用意されています。 import numpy as np arr: np.ndarray = np.array([1, 2, 2, 1, 3, 4, 2]) arr = np.unique(arr) print(arr) [1 2 3 4] 配列が特定の値を含むかどうかの真偽値を取得する: CONTAINS CONTAINS関数は配列の値の中に特定の値が含まれているかどうかの真偽値を返します。 以下のSQLではパーティーのユーザーIDの配列内に25456のIDが含まれているかどうかの真偽値を取得しています。 SELECT CONTAINS(user_ids, 25456) AS user_id_25456_contains, user_ids FROM athena_workshop.battle_party_power WHERE dt = '2021-01-01' LIMIT 3; Pythonでの書き方 ビルトインのリストなどに対してinのキーワードを使うことで真偽値を取得することができます。 print(25456 in [96607, 14806]) False print(25456 in [82053, 25456, 34646]) True 2つの配列で値の重複が存在するかの真偽値を取得する: ARRAYS_OVERLAP ARRAYS_OVERLAP関数は2つの配列の値で重複している値が存在するかどうかの真偽値を返却します。それぞれの配列内に欠損値が含まれる場合にはtrueとはなりません。もし配列内の値が欠損値のみになっていれば返却値も欠損値(null)になります。 以下のSQLでは1つ目の配列の値が1, 2, 3で2つ目の配列の値が4, 5, 6であり、重複している値は存在しないのでfalseとなります。 SELECT ARRAYS_OVERLAP(ARRAY[1, 2, 3], ARRAY[4, 5, 6]) AS is_overlap 一方で以下のSQLでは1つ目の配列が1, 2, 3、2つ目の配列の値が3, 4, 5となっており3が重複しているのでtrueとなります。 SELECT ARRAYS_OVERLAP(ARRAY[1, 2, 3], ARRAY[3, 4, 5]) AS is_overlap Pythonでの書き方 シンプルにループを使ってinで判定ができます。 from typing import List arr_1: List[int] = [1, 2, 3] arr_2: List[int] = [3, 4, 5] is_overlap: bool = False for value in arr_1: if value in arr_2: is_overlap = True break print(is_overlap) True が、それぞれのリストが巨大になってくると1件ずつリストに対してinを使うのは結構処理時間が気になるようになってきます。そのためもしも対象が巨大なリストの場合には2つ目のリストの方を辞書に変換しておいて、辞書のキーに存在するかどうかをinで判定するようにすると計算時間をぐぐっと抑えられます(メモリは食いますが)。 from typing import Dict, List arr_1: List[int] = [1, 2, 3] arr_2: List[int] = [3, 4, 5] dict_val: Dict[int, None] = {value: None for value in arr_2} is_overlap: bool = False for value in arr_1: if value in dict_val: is_overlap = True break print(is_overlap) True 配列から特定の値を取り除く: ARRAY_REMOVE ARRAY_REMOVE関数は配列から指定された値を取り除いた配列を返却します。第一引数に対象のカラム名もしくは固定値、第二引数に任意の取り除きたい値を指定します。 以下のSQLでは配列から2の値を取り除いているため、結果は[1, 3]という配列になります。 SELECT ARRAY_REMOVE(ARRAY[1, 2, 3], 2) AS result 複数の削除対象の値が含まれている場合には全て削除されます。 SELECT ARRAY_REMOVE(ARRAY[1, 2, 2, 3], 2) AS result Pythonでの書き方 ビルトインのリストであれはremoveメソッドで削除が利きます。ただしリストに含まれていないとエラーになるのと1回呼んだだけでは先頭の値しか削除されないのでSQL側と同じようなことをしようとするとwhile文などで対象の値が含まれているかといった判定が必要になります。 from typing import Dict, List arr: List[int] = [1, 2, 2, 3] while 2 in arr: arr.remove(2) print(arr) [1, 3] 配列内で指定された値の最初の出現位置を取得する: ARRAY_POSITION ARRAY_POSITION関数は配列内で指定された値が初めて出現する位置を返します。複数対象の値が存在する場合は最初の値の位置のみが返却されます。 例えば以下のSQLでは配列内で2の値が配列の何番目に存在するかを確認しています。2番目に存在するので結果は2となります。 SELECT ARRAY_POSITION(ARRAY[1, 2, 3, 3, 2], 2) AS first_position 配列内に対象の値が存在しない場合返却値は0になります。 SELECT ARRAY_POSITION(ARRAY[1, 2, 3, 3, 2], 5) AS first_position ※Prestoのドキュメントでは第三引数で何番目の値かどうかが指定できるようになっていますがAthenaではまだ使えないようです。この記事を書いている時点では値が最初に出現する位置しか取れません。将来Athena側のアップデートで変わるかもしれません。 Pythonでの書き方 ビルトインのリストであればindexメソッドで同じようなことができます。他と同様Pythonではインデックスが0からスタートするのでSQLと比べて結果が-1した値になります。 from typing import List arr: List[int] = [1, 2, 3, 3, 2] first_position: int = arr.index(2) print(first_position) 1 特定の値が含まれない配列を取得する: ARRAY_EXCEPT ARRAY_EXCEPT関数は2つの配列を用いて、1つ目の配列の中で2つ目の配列に含まれない値の配列を返します。 以下のSQLでは1つ目の配列の値は1, 2, 3, 3, 2, 4, 5, 5、2つ目の配列の値は2, 3としています。結果的に2と3が取り除かれた1, 4, 5の値が結果の配列に設定されます。結果に重複分は含まれません(1, 4, 5, 5といったような値は返却されません)。 SELECT ARRAY_EXCEPT(ARRAY[1, 2, 3, 3, 2, 4, 5, 5], ARRAY[2, 3]) AS result Pythonでの書き方 ダイレクトにARRAY_EXCEPT関数に該当するビルトイン関数などは無い(?)ためループで回しています。前節と同じようにリストが巨大になった際のことを考えて2つ目の配列は一旦辞書にしています。また、ループの回数を減らすために最初の方でset関数を使って配列を一意にしています。2つ目の配列に含まれる値は結果に含めずにループ内でcontinueしています。 from typing import Dict, List arr_1: List[int] = [1, 2, 3, 3, 2, 4, 5, 5] arr_2: List[int] = [2, 3] arr_1 = list(set(arr_1)) arr_2 = list(set(arr_2)) result_list: List[int] = [] dict_value: Dict[int, None] = {value: None for value in arr_2} for value in arr_1: if value in dict_value: continue result_list.append(value) print(result_list) [1, 4, 5] 配列の値を連結して文字列を取得する: ARRAY_JOIN ARRAY_JOIN関数は配列の値を特定の文字列で連結した文字列を取得することができます。たとえばコンマで連結すればCSV的な文字列を取得することができます。 第一引数に対象の配列のカラム名もしくは固定値、第二引数には区切り文字を指定します。 以下のSQLではコンマ区切りで配列の値を連結しています。 SELECT ARRAY_JOIN(powers, ',') AS joined_powers, powers FROM athena_workshop.battle_party_power LIMIT 10 なお、配列に欠損値(NULL)が含まれている場合、その値は結果の文字列には含まれません。 以下のSQLでは1, NULL, 3という値の配列を指定していますが、結果は1,3とNULLを除いた値になっていることを確認できます。 SELECT ARRAY_JOIN(ARRAY[1, NULL, 3], ',') AS joined 欠損値部分を別の値で埋めたい場合には第三引数を指定します。以下ではNULL部分が取り除かれてしまうのではなく、NaNという文字列で置換しています。 SELECT ARRAY_JOIN(ARRAY[1, NULL, 3], ',', 'NaN') AS joined Pythonでの書き方 ビルトインの文字列がリストを引数に取るjoinメソッドを持っているためそちらで対応ができます。ただしリストの値を文字列に先に変換しておく必要があります。 from typing import List int_arr: List[int] = [1, 2, 3] str_arr: List[str] = [str(value) for value in int_arr] joined: str = ','.join(str_arr) print(joined) 1,2,3 配列のソートを行う: ARRAY_SORT ARRAY_SORT関数を使うと配列の値の昇順ソートを行うことができます。 SELECT ARRAY_SORT(powers) AS sorted_powers, powers FROM athena_workshop.battle_party_power LIMIT 10 配列に欠損値(NULL)が含まれている場合にはその欠損値は配列の末尾に配置される形でソートされます。 SELECT ARRAY_SORT(ARRAY[5, 3, 6, NULL, 7, NULL, 10]) AS sorted Pythonでの書き方 ビルトインのリストであればsortメソッドで昇順ソートすることができます。 from typing import List, Optional arr: List[int] = [5, 3, 6, 10, 7] arr.sort() print(arr) [3, 5, 6, 7, 10] もしくはコピーされたリストが欲しければビルトインのsorted関数で取得することができます。 from typing import List, Optional arr_1: List[int] = [5, 3, 6, 10, 7] arr_2: List[int] = sorted(arr_1) print(arr_2) [3, 5, 6, 7, 10] ラムダ式(Lambda Expressions) 以降の節の関数ではラムダ式が必要になってきたりするため先に軽く触れておきます。詳細は各関数で必要な分だけ説明を加えていきます。 ラムダ式は無名関数などとも呼ばれたりしますが、特定の値を受け取ってその値を使った返却値を記述することで、独自の関数定義のようなことをすることができます。 ->の記号を使い、特定の値を引数として左側に記述し、右側に返却値を記述します。例えばxという値を引数で受け取って、それを1加算する形の返却値としてx + 1という表現をしたい場合にはx -> x + 1という書き方になります。複数の引数(xとy)を受け付けて1つの返却値(z)を返す場合には(x, y) -> zといったように書きます。 このラムダ式は関数内でしか使えません。ラムダ式を引数に取る関数があるためそれらの関数で使います。 ソート条件にラムダ式を使う ARRAY_SORT関数の第三引数にはラムダ式を指定することができ、独自のソート方法を自身で組むことができます。 引数には2つの値が渡されます。ここではxとyという引数名を使っています。このxとyの2つの値での比較条件をラムダ式で書くことで比較条件を配列の各値に反映する形でソートが実行されます。ラムダ式の返却値には1つの整数が必要になり、-1, 0, 1のいずれかの値のみを受け付けます。xとyをソート用に比較した時にxの方を前にしたい条件であれば-1、xの方を後にしたい条件であれば1、xとyが同じ位置(同値条件など)になるべき条件であれば0を返すようにします。 条件文はIF関数を使って書きます。第一引数に条件式(x > yなど)、第二引数に条件を満たす場合の返却値、第三引数に条件を満たさない場合の返却値を指定します。第二引数や第三引数にはさらにIF関数もしくは他の関数などを入れ子にすることもできます。例えばA条件を満たす場合、さらにその中でB条件を満たすか満たさないか・・・といったように複数の条件を入れ子にしていくことができます。 例としてラムダ式を使わないARRAY_SORTの昇順ソートのと同じソートの計算をラムダ式を使って書いてみましょう(第三引数を省略する形で実務では問題無いのですがシンプルな説明用として使いえます)。 以下のようなSQLとなります。 SELECT ARRAY_SORT(ARRAY[5, 3, 6, 10, 7], (x, y) -> IF(x < y, -1, IF(x = y, 0, 1))) AS sorted ラムダ式部分は(x, y) -> IF(x < y, -1, IF(x = y, 0, 1)))が該当します。まず引数部分ですがソートで2つの値が必要になるので(x, y)という記述にしています。 IF関数の最初の条件と条件を満たす場合の指定ですがIF(x < y, -1,としています。これはxがyよりも小さければ-1を返す(xがyよりも小さければxをyよりも前に配置する)という制御になります。 前述の条件を満たさない場合にはさらにIF関数を入れ子にしてIF(x = y, 0, 1)と分岐を書いています。これは、 xがyよりも小さいという条件を満たさず、且つxとyが同値(x = y)である場合には0を返す(xとyはソートで同じ位置になる)。 xがyよりも小さいという条件を満たさず、且つxとyが同値(x = y)でもない場合には1を返す(xがyよりも大きいのでxがyの後の位置にする)。 という条件になっています。 今度は降順ソートを書いてみましょう。現在Athenaでは配列を逆順にするREVERSE関数が使えない(Presto側のドキュメントには追加されているのでその内アップデートで使える形になると思われます)ため自前で書く必要があります。 やることとしてはシンプルで、x < yの箇所をx > yと比較演算子の向きを変えているだけです。これでxがyよりも大きければ前の位置に配置するという挙動になります。 SELECT ARRAY_SORT(ARRAY[5, 3, 6, 10, 7], (x, y) -> IF(x > y, -1, IF(x = y, 0, 1))) AS sorted ラムダ式と関数などを組み合わせてソートする ARRAY_SORT関数の第二引数のラムダ式には関数などを組み合わせて使うこともできます。例えば文字列の配列に対して文字数でソートしたい・・・というケースには以下のように書くことができます。xやyの部分を文字数を取得するLENGTH関数で囲っています。 SELECT ARRAY_SORT( ARRAY['ねこ', 'うさぎ', 'いぬ', 'おおかみ', 'ひつじ', 'らいおん'], (x, y) -> IF(LENGTH(x) < LENGTH(y), -1, IF(LENGTH(x) = LENGTH(y), 0, 1))) AS sorted 配列をシャッフルする: SHUFFLE SHUFFLE関数は配列の内容をランダムにシャッフルします。結果は実行する度に変わります。 SELECT SHUFFLE(powers) AS shuffled_powers, powers FROM athena_workshop.battle_party_power LIMIT 10 Pythonでの書き方 ビルトインのrandomパッケージのshuffle関数でリストなどをシャッフルすることができます。 from typing import List from random import shuffle arr: List[int] = [5, 3, 6, 10, 7] shuffle(x=arr) print(arr) [7, 6, 3, 5, 10] 2つの配列で双方に存在する値を格納した配列を取得する: ARRAY_INTERSECT ARRAY_INTERSECT関数は第一引数と第二引数に指定された2つの配列間で、両方の配列に存在する値のみを格納した配列を返却します。返却される値は重複の無い形で設定されます。 以下のSQLでは1, 2, 3という値の配列と2, 3, 4という値の配列の2つを指定しています。2と3部分がそれぞれの配列に存在するため結果は2, 3という値の配列となります。片側にしかない値の1と4は含まれません。 SELECT ARRAY_INTERSECT(ARRAY[1, 2, 3], ARRAY[2, 3, 4]) AS result 余談ですが、Prestoのドキュメントを見ていると2次元配列を渡す形でのARRAY_INTERSECT関数の記述がありましたがまだAthenaでは使えない?ようです。 Pythonでの書き方 2つのsetを&記号で繋げばそれぞれのsetで両方に値があるもののみのsetが得られるのでそちらをリストにキャストすれば結果のリストが得られます。 以下のコードでは一度リストをset関数でsetに変換しています。 from typing import List arr_1: List[int] = [1, 2, 3] arr_2: List[int] = [2, 3, 4] result: List[int] = list(set(arr_1) & set(arr_2)) print(result) [2, 3] 任意の条件を満たした値のみを格納した配列を取得する: FILTER FILTER関数では特定の条件を満たす値のみを残した配列を返却します。第一引数には対象の配列のカラムもしくは配列の固定値、第二引数にはラムダ式を指定します。ラムダ式には1つの引数(サンプルではxという名前を使います)を受け付け、返却値には(等値や大なりなどの比較表現などによって)真偽値が返るようにします。返却値でtrueが返る条件の箇所のみ結果の配列に残ります。 以下のSQLではラムダ式でx >= 0という条件を指定しており、結果的に値が0以上のもののみ結果の配列に含まれるようにしています。 SELECT FILTER(ARRAY[-5, 3, 2, -1, 0, 8, -3], x -> x >= 0) AS result Pythonでの書き方 ビルトインのfilter関数で同じようなことができます。返却値はfilterオブジェクトになるのでリストにキャストする処理を加えてあります。 from typing import List arr: List[int] = [-5, 3, 2, -1, 0, 8, -3] arr = list(filter(lambda x: x >= 0, arr)) print(arr) [3, 2, 0, 8] 2次元配列を1次元の配列に変換する: FLATTEN FLATTEN関数は以下のような行列(2次元配列)を1, 2, 3, 4, 5, 6, 7, 8, 9といったように1次元の配列に変換します。 \begin{array}{ccc} 1 & 2 & 3\\ 4 & 5 & 6\\ 7 & 8 & 9 \end{array} SQL上では第一引数に2次元配列が必要になります。 SELECT FLATTEN(ARRAY[ARRAY[1, 2, 3], ARRAY[4, 5, 6], ARRAY[7, 8, 9]]) AS result Pythonでの書き方 ビルトインのリストの場合は今まで使ったことがなかったのですがitertools.chain.from_iterableを使うとビルトインのもので完結する形でストレートにいくようです。 from typing import List import itertools arr: List[List[int]] = [[1, 2, 3], [4, 5, 6], [7, 8, 9]] flattened: List[int] = list(itertools.chain.from_iterable(arr)) print(flattened) [1, 2, 3, 4, 5, 6, 7, 8, 9] NumPyなどであればflatternメソッドがあるのでそのままそれを呼ぶだけで対応ができます。こちらはデータ方面のお仕事などされている方々はビルトインよりも使う機会が結構あると思います。 import numpy as np arr: np.ndarray = np.array([ [1, 2, 3], [4, 5, 6], [7, 8, 9], ]) arr = arr.flatten() print(arr) [1 2 3 4 5 6 7 8 9] 他のカラムの値を残しつつ、配列のネストされたデータを行列にする: UNNEST ケースによっては配列のカラムではなく行列で扱った方が色々とシンプルな時があります。そういった場合にはUNNEST関数とCROSS JOINによる連結を使うと他のカラムの値を残したまま行列に変換することができます。 指定された配列の値を1行1行展開し、他のカラムの値は一通りそのまま残す(元の値を連結する)みたいな挙動になります。 例えば以下のような行列があったとします。武器のIDのカラムは配列の値を含んでいます。 \begin{array}{rr} ユーザーID & 武器のID\\ 1 & [10, 11]\\ 2 & [12, 13] \end{array} UNNEST関数と武器のIDを指定したCROSS JOINを使うと上記の行列が以下のような行列に変換されるイメージです。 \begin{array}{rr} ユーザーID & 武器のID\\ 1 & 10\\ 1 & 11\\ 2 & 12\\ 2 & 13 \end{array} CROSS JOIN自体は以前の記事で触れましたが、組み合わせによる連結を行う都合元の行数よりもぐぐっと行数が多くなります。 参考 : 左側のテーブルの行数がN行、右側のテーブルの行数がM行とすると交差結合の結果の行数は$N × M$行となります。 今回のUNNEST関数の例で言うとNが元々の行数、Mが対象とする配列の値の件数となります。例えば元々の行が10万行、配列が平均して10件の値を含んでいた場合は10万 × 10で結果は100万行となります。処理に時間がかかったり扱うデータが巨大化したりするケースがあるので扱う際には少し気を付ける必要があります。 書き方としてはCROSS JOIN UNNEST(<対象の配列のカラム名>) AS <UNNESTしたテーブルに付ける名前>(配列の値に付けるカラム名)みたいな書き方になります。少々独特です。 例えばbattle_party_powerテーブルのpowersという配列を展開したい場合に、展開後のテーブル名をunnested_powersとし、配列の値のカラム名にpowerという名前を付ける場合はCROSS JOIN UNNEST(athena_workshop.battle_party_power.powers) AS unnested_powers(power)といったような書き方になります。 実際にSQLで実行してみましょう。 SELECT * FROM athena_workshop.battle_party_power CROSS JOIN UNNEST(athena_workshop.battle_party_power.powers) AS unnested_powers(power) LIMIT 50 SQL実行結果では元々のカラムに加えて連結されたpowerというカラムが追加になっていることが分かります。また、powersの配列のカラムの各値が1行ずつに展開され、powerカラム以外のカラムの値はそのまま残っていることが確認できます。 辞書を格納した配列に関してはさらに色々と触れることがありますが、そちらは別の辞書について詳しく触れる記事で書いていこうと思います。 Pythonでの書き方 この辺は大分昔の記事となりますが、Pandasのjson_normalize関数などが用意されており記事にしていますのでそちらをご確認ください。 配列をn件ずつの2次元配列に変換する: NGRAMS NGRAMS関数は1次元の配列を2次元の配列に変換します。自然言語方面で結構出てきたりします。 n-gramとはn個の連続する単位 一言で説明すると、「n-gram」とは連続するn個の単語や文字のまとまりを表します。 まとまりごとに2次元目の配列化されます。例えば[1, 2, 3, 4, 5]という配列に対してn=2でNGRAM関数を反映すると、[[1, 2], [2, 3], [3, 4], [4, 5]]といった2次元配列に変換されます。n=3で反映すると[[1, 2, 3], [2, 3, 4], [3, 4, 5]]といった値になります。 第一引数には対象の配列のカラムもしくは配列の固定値、第二引数にはnの値を指定します。 SELECT NGRAMS(ARRAY[1, 2, 3, 4, 5], 3) AS result Pythonでの書き方 Pandas自体にはn-gram関係の機能は無さそうですが、nltkなどの別のライブラリを絡めるとシンプルにいきそうな印象です。試していないのでここではリンクのみ貼っておきます。 配列の各値で計算を行い、計算結果を取得する: REDUCE REDUCE関数は配列の値に対して1つ1つ計算を行い、計算結果を単一の値(次元数が減った状態の値)を取得します。ラムダ式をがっつり使う形となります。 第一引数には対象とする配列のカラム名もしくは配列の固定値、第二引数には初期値、第三引数には各値ごとの入力のラムダ式、第四引数には全体の出力時のラムダ式が必要になります。引数が複雑なので順番に触れていきます。 まずは第二引数の初期値です。これは集計処理の初期値です。0を指定して0からスタートするケースが結構多くなると思います。 第三引数は各値ごとの入力のラムダ式となります。各値ごとに処理がされます。引数はその値までの計算結果(今回の記事ではsと表記します)と対象の配列の値(今回の記事ではxと表記します)の2つです。先頭の値に関してはsの値は第二引数に指定した初期値になります。 例えば[2, 3, 4]という配列に対して処理を行うとして、第二引数の初期値が1だった場合は 1回目の処理 -> s = 1, x = 2 2回目の処理 -> s = 1回目の処理の計算結果, x = 3 3回目の処理 -> s = 2回目の処理の計算結果, x = 4 といったように3回計算がこのラムダ式で実行されます。返却値は1つの値となる必要があります(計算結果として次の処理のsに渡されます)。 第四引数は出力用のラムダ式です。このラムダ式は第三引数とは異なり配列の各値ごとに実行されたりはしません。各値の計算結果の最後の値に対してのみ反映されます(配列の件数に関わらず1回のみ実行されます)。引数は全体の計算結果のsのみ、返却値も1つの値のみとなります。 いくつかサンプルのSQLを書いていきます。まずはシンプルに配列の値の合計値を出すという記述をREDUCE関数を使ってしてみましょう。前節までで触れた通り、配列の合計値を出す関数自体はPrestoにはARRAY_SUM関数が存在しますが、Athenaではまだこの記事を執筆時点では使えない点とサンプルとしてシンプルなので使っていきます。 まずは第二引数の初期値ですが、これは合計値を出す計算なので0を指定します。 続いて第三引数のラムダ式ですが、引数の指定は2つの値が必要になるため(s, x)といった記述になります。返却値側は配列の各値の合計を出す必要があるため、前の処理までの計算結果sに対して今回の配列の値xを加えるという形でs + xとしています。 第四引数の処理に関しては今回は制御が必要ないため、s -> sとして値をそのまま返却するようにしています。 1, 2, 3という3つの値を含んでいる配列に対して試してみます。 SELECT REDUCE(ARRAY[1, 2, 3], 0, (s, x) -> s + x, s -> s) AS result 無事配列の合計値の6を得ることができました。 今度は第四引数の出力部分のラムダ式を調整していきましょう。このラムダ式は第三引数のラムダ式とは異なり配列の値1つ1つで計算はされずに最後の計算結果にのみ(最後の1回だけ)反映されます。 以下のSQLでは計算結果に対して4で割ったときの剰余(余り)をs % 4という記述で求めています。結果が2になることが確認できます。 SELECT REDUCE(ARRAY[1, 2, 3], 0, (s, x) -> s + x, s -> s % 4) AS result 他にも比較演算子を使って比較結果を真偽値を取得したり、関数を挟んだりと色々できます。以下のSQLでは計算結果が5よりも大きいか(s > 5)という判定にして結果の真偽値を取得しています。 SELECT REDUCE(ARRAY[1, 2, 3], 0, (s, x) -> s + x, s -> s > 5) AS result Pythonでの書き方 ビルトインのfunctoolsパッケージにreduce関数があり、そちらで同じような制御を行うことができます。第一引数には反映する関数もしくはラムダ式、第二引数にリストなどの値を指定します。 from typing import List from functools import reduce arr: List[int] = [1, 2, 3] result: int = reduce(lambda s, x: s + x, arr) print(result) 6 特定の変換処理を加えた配列を取得する: TRANSFORM TRANSFORM関数もREDUCE関数のように配列の値1件1件に対して処理を加えます。ただし結果の値はそのまま配列で返却され、配列の値の件数も基本的に変動しません。「配列の全ての値に対して加工を行う」といった制御に向いています。 第一引数には対象の配列のカラム名もしくは配列の固定値、第二引数には加工処理用のラムダ式を指定します。第二引数のラムダ式には1つの引数(配列内の個々の値)と1つの返却値を受け付けます。今回はxという名前を使っています。 以下のSQLでは1, 2, 3という値の配列に対して1加算するラムダ式(x + 1)を指定しています。結果が2, 3, 4となることを確認できます。 SELECT TRANSFORM(ARRAY[1, 2, 3], x -> x + 1) AS result Pythonでの書き方 色々書き方がありますが、1つ目としてはmap関数による処理がTRANSFORM関数に近い挙動になっています。 from typing import List arr: List[int] = [1, 2, 3] arr = list(map(lambda x: x + 1, arr)) print(arr) [2, 3, 4] 他にもリスト内包表記やもちろん普通にループを回してしまっても良いと思います。 from typing import List arr: List[int] = [1, 2, 3] arr = [x + 1 for x in arr] print(arr) [2, 3, 4] 2つ以上の配列を統合した配列を取得する: ZIP ZIP関数は2つ以上の配列の値をセットにした配列を返します。返却値はROWという型の値を格納した配列になるようです。ROWは辞書のような形の型となります(1行分のデータといった形になります)。 実行後のカラム名はfield0, field1, ... と設定されます。 SELECT ZIP(ARRAY[1, 2, 3], ARRAY['猫', '犬', '兎']) AS result ROW型の値はドット記号(.)でアクセスすることができます。例えばfield1のカラムにアクセスしたい場合には.field1といった書き方でその値を参照することができます。 配列へのアクセスは[]の括弧を使えば行えるため、例えば配列の最初のROWの値のfield1にアクセスしたい場合には[1].field1といった書き方になります(括弧内のインデックスの数字はAthena(Presto)上では1からのスタートになります)。 SELECT ZIP(ARRAY[1, 2, 3], ARRAY['猫', '犬', '兎'])[1].field1 AS result Pythonでの書き方 ループなどの際にビルトインのzip関数を使うことで複数のリストなどの値をひとまとめにしつつループを行うことができます。結果はタプルで渡されます。 from typing import List arr_1: List[int] = [1, 2, 3] arr_2: List[str] = ['猫', '犬', '兎'] for tpl in zip(arr_1, arr_2): print(tpl) (1, '猫') (2, '犬') (3, '兎') タプルはコンマ区切りでそのまま展開することもできるため、それぞれを変数に分けることもできます。 from typing import List arr_1: List[int] = [1, 2, 3] arr_2: List[str] = ['猫', '犬', '兎'] for id_, name in zip(arr_1, arr_2): print(id_, name) 1 猫 2 犬 3 兎 なお、Prestoのドキュメントには2つの配列に対してラムダ式で処理を加えつつ制御が可能なZIP_WITHという関数もあったのですがこの記事の執筆時点ではまだAthena上では利用できないようです。 参考文献・参考サイトまとめ The Applied SQL Data Analytics Workshop: Develop your practical skills and prepare to become a professional data analyst, 2nd Edition [AWS]Athenaのコメントアウトの仕方とフォーマットについて Array Functions and Operators Lambda Expressions pandas.date_range Pythonでリスト(配列)の要素を削除するclear, pop, remove, del How to find list intersection? Pythonでflatten(多次元リストを一次元に平坦化) Flatten arrays into rows with UNNEST Pandasのjson_normalizeを詳しく調べてみる 自然言語処理に出てくるn-gramとはなに? From DataFrame to N-Grams Python の基本的な高階関数( map() filter() reduce() ) How to Transform List Elements with Python map() Function Data Types
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

OPENAIのCodexがホラーすぎる件について

OPENAIのCodex 巷を賑わせている、OPENAIそのCodex betaの利用申請が通りましたので試しに使ってみます。 テスト 入力コード """Online This is a module of sequential calculation algorithm for finding moving average, moving standard deviation and moving variance in constraints. Author: Hideto Manjo Date: Aug 9, 2020 """ import time import random import numpy as np ここまでを入力します。ちなみにこの元になるコードはオンラインアルゴリズムに従って、逐次的に平均値や標準偏差を計算するモジュールになっています。今回はこのコードの説明の部分とインクルードの部分だけを入力に使いました。つまりあとはAIが適当に作ってみよというわけです。 さあどうなるか? Codexが出したコード いまから、ホラーをお見せしますね。 これまじで出力してきてますからね。 コードを見やすくするために、入力文も含まれています。 import numpy as npから下はすべてCodexが出した答えです。 それではどうぞ・・・ """Online This is a module of sequential calculation algorithm for finding moving average, moving standard deviation and moving variance in constraints. Author: Hideto Manjo Date: Aug 9, 2020 """ import time import random import numpy as np import pandas as pd import matplotlib.pyplot as plt class Online: """Online class """ def __init__(self, n=10): """Initialize """ self.n = n self.x = np.zeros(n) self.mean = 0 self.var = 0 self.std = 0 def update(self, x): """Update """ self.x = np.roll(self.x, 1) self.x[0] = x self.mean = np.mean(self.x) self.var = np.var(self.x) self.std = np.std(self.x) def get_mean(self): """Get mean """ return self.mean def get_var(self): """Get variance """ return self.var def get_std(self): """Get standard deviation """ return self.std def main(): """Main """ n = 10 x = np.zeros(n) mean = 0 var = 0 std = 0 online = Online(n) for i in range(n): x[i] = random.randint(0, 100) online.update(x[i]) print("x[{}]: {}".format(i, x[i])) print("mean: {}".format(online.get_mean())) print("var: {}".format(online.get_var())) print("std: {}".format(online.get_std())) print("") if __name__ == "__main__": main() もう人間要らなくね? なんか普通に動きそうです。 配列をずらす時にnumpy.rollを使ってるところなんかはかなり感心しました。 試しに動かしてみましょう。 AIが出したコードを実際に動かしてみる x[0]: 6.0 mean: 0.6 var: 3.2400000000000007 std: 1.8000000000000003 x[1]: 23.0 mean: 2.9 var: 48.09000000000001 std: 6.934695379034324 x[2]: 57.0 mean: 8.6 var: 307.44 std: 17.53396703544295 x[3]: 95.0 mean: 18.1 var: 956.2900000000002 std: 30.923938947035843 x[4]: 84.0 mean: 26.5 var: 1287.25 std: 35.878266401820476 x[5]: 14.0 mean: 27.9 var: 1230.69 std: 35.081191541907465 x[6]: 54.0 mean: 33.3 var: 1191.81 std: 34.522601292486634 x[7]: 78.0 mean: 41.1 var: 1219.8899999999999 std: 34.92692371223094 x[8]: 85.0 mean: 49.6 var: 1171.4399999999998 std: 34.226305672683985 x[9]: 60.0 mean: 55.6 var: 900.24 std: 30.003999733368882 普通に動きました。恐ろしいです。参りました。 近いうちに人間はコーディングする必要が無くなるでしょう。 使い方によってはこの技術はコーディング速度を飛躍的に高めるはずです。 細かな指摘 ところでAI君はなんで 不使用のインクルード import pandas as pd import matplotlib.pyplot as plt pandaとpyplotをインクルードしたんですかね? 少々おかしな挙動も見受けられます。(とりあえずこれをインクルードやってる人多い気もするが・・・) コレは無駄なんで良い子のみんなは真似しないでね。 結論 OPENAIのCodexは噂通りヤバイです。というか途中まで入れて出てくるコードを眺めると冷や汗が出てきます。怖いですコレ。みなさんも是非体験してみてください。 ところで、この記事利用許諾の範囲内ですよね?ミスってたら消します。あまりにもすごすぎるんで、記事を書かざるを得ませんでした。未だ複雑なプログラムは書けないとは思いますが、今回のテストだけでも相当の火力を秘めていることがわかります。 今から将来プログラマーになろうとしてる人は少し情報を集めたほうがいいかも知れませんよコレ。本気で必要なくなる可能性十分にありです。 付録:入力に使ったコードの続き 下記は実際に自分が書いていたコードです。ほぼほぼ同じことをやっていますがアルゴリズムは違うことをやっています。 移動平均、移動標準偏差モジュールとテスト用のメイン関数 """Online This is a module of sequential calculation algorithm for finding moving average, moving standard deviation and moving variance in constraints. Author: Hideto Manjo Date: Aug 9, 2020 """ import time import random import numpy as np class Online: def __init__(self): self.K = 0 self.n = 0 self.Ex = 0 self.Ex2 = 0 def add_variable(self, x): if (self.n == 0): self.K = x self.n += 1 self.Ex += x - self.K self.Ex2 += (x - self.K) * (x - self.K) def remove_variable(self, x): self.n -= 1 self.Ex -= (x - self.K) self.Ex2 -= (x - self.K) * (x - self.K) def get_mean(self): return self.K + self.Ex / self.n def get_variance(self): return (self.Ex2 - (self.Ex * self.Ex) / self.n) / self.n if __name__ == '__main__': # The main function is for calculation validation. TOTAL_NUM = 100 cnt = 0 data = [0] * TOTAL_NUM y = np.array([]) online = Online() for i in range(200): val = random.random() s = time.time() if len(y) == TOTAL_NUM: y = np.delete(y, 0) y = np.append(y, val) print("Numpy : {:f}, {:f}, {:f}, time: {:.3f} ms" .format(y.mean(), y.std(), y.var(), (time.time() - s) * 1000)) s = time.time() if cnt == TOTAL_NUM: cnt = 0 if online.n == TOTAL_NUM: online.remove_variable(data[cnt]) data[cnt] = val online.add_variable(data[cnt]) average = online.get_mean() var = online.get_variance() print("Online: {:f}, {:f}, {:f}, time: {:.3f} ms" .format(average, np.sqrt(var), var, (time.time() - s) * 1000)) cnt += 1 今回の例は特殊な計算をして高速化しているので、こちらのほうが速い(はず)ですが、AIの計算はかなり正攻法と言えます。 コードもAIの方が見やすいですよねw・・・、既に負けた感あり。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

画像解析実践入門_指紋の稜線分析 1(前処理編)

概要 主にpythonで画像処理について本格的に学び始めたのですが、実際に使う機会が欲しくてテーマを探していました。 研究室に埋もれていた書籍「MATLAB画像処理入門」に指紋解析について乗っていましたので,python風にコードを書き換えながら画像解析について理解を深めます。 もくじ 指紋の画像データセットについて 使用するpythonライブラリ 画像の読み込み~二値化 までを行います 1.指紋の画像データセットについて kaggleにデータセットがあったのでこちらを使います。 [https://www.kaggle.com/ruizgara/socofing] ダウンロードしたデータの /SOCOFing/Real/* の画像を使いましょう。 2.使用するpyhonライブラリ Numpy #行列計算に使用 matplotlib #画像とグラフのプロット Scikit-image #画像処理に利用 の3つをインポートします。 ちなみに画像処理に使うライブラリですがopencvではなくあえてSkimageを使っています。 import.py import numpy as np from skimage import io from skimage.util import crop import matplotlib.pyplot as plt 3. 画像の読み込み~二値化 早速画像を読み込んで処理を実行していきますが今回 画像の読み込みと確認 トリミング エッジを保持したノイズ処理 二値化 順で説明します。 画像の読み込みと確認 imread_and_plt.py #画像をグレースケールで読み込み img = io.imread('1__M_Left_thumb_finger.BMP',as_gray =True) print(img.shape) #(103,93) #画像を確認する plt.imshow(img,cmap='gray') 画像の読み込みと確認ができました。 トリミング 読み込んだ画像は左、上に縁が写ってしまっているのと右、下に余白ができています。解析に余計な画素はトリミングしてしまいます。 crop.py #縁を切り取り img_crop=crop(img,((7,6),(3,3)),copy=True) print(img_crop.shape) #(90,90) plt.imshow(img_crop,cmap='gray') crop関数について [https://scikit-image.org/docs/dev/api/skimage.util.html#skimage.util.crop] エッジを保持したノイズ処理(ノンローカルミーンフィルタ) 読み込んだ画像には暗い画素が指紋の山の部分で明るい画素が指紋の谷部位もしくはなにもない領域と言えます。この指紋の稜線は本来同じ値の画素が一続きに並んだ「連結成分」だがこれとは関係ない 散発的に存在する画素(つまりノイズ)があるためこれを除去(ノイズフィルタリング)したいところです。 ノイズフィルタリングとして有名なのは主にガウシアンフィルタだが、指紋においては山と谷の連続した線が非常に大切であるため、この線(エッジ)を残しつつフィルタリング処理をする必要がある。 そこでおすすめなのがノンローカルミーンフィルタによる処理になります。 ノンローカルミーンフィルタについて [https://scikit-image.org/docs/stable/auto_examples/filters/plot_nonlocal_means.html#sphx-glr-auto-examples-filters-plot-nonlocal-means-py] noise_reduction.py from skimage.filters import gaussian from skimage.restoration import denoise_nl_means, estimate_sigma #ガウシアンフィルタ gauss_img = gaussian(img_crop) #ノンローカルミーンフィルタ sigma_est = np.mean(estimate_sigma(img_crop)) nl_img = denoise_nl_means(img_crop,h = sigma_est*1.2) #画像を比較 plt.figure(figsize = (15,15)) plt.subplot(131) plt.imshow(img_crop,cmap = 'gray') plt.title('origin') plt.subplot(132) plt.imshow(gauss_img,cmap = 'gray') plt.title('gauusian') plt.subplot(133) plt.imshow(nl_img,cmap = 'gray') plt.title('NL_means') 左からオリジナル画像、ガウシアンフィルタ、ノンローカルミーンフィルタ による画像を表示しています。ノンローカルミーンフィルタによって明らかにノイズが減少し、かつ稜線もくっきりと残っていることが確認できます。  二値化 読み込んだ画像は0~255の値を取る8bitのデータです。これを 指紋の山(暗い画素) -> 1(解析対象) 指紋の谷(明るい画素) -> 0 (背景) となるようにする(二値化)必要があります。 二値化することで画像に対して適応できる処理アルゴリズムが数多くあるからです。 二値化する方法ですが有名なのは大津法で求めたしきい値による二値化です。 大津法について [https://scikit-image.org/docs/dev/api/skimage.filters.html#skimage.filters.threshold_otsu] 大津法による二値化 ootu.py from skimage.filters import threshold_otsu #大津法で二値化 bin_otsu = nl_img < threshold_otsu(nl_img) #画像を比較 plt.figure(figsize = (12,12)) plt.subplot(121) plt.title('origin') plt.imshow(nl_img,cmap = 'gray') plt.subplot(122) plt.title('thresh_ootu') plt.imshow(bin_otsu,cmap = 'gray') 残念ながら良い結果とは言えません。 画像の上部分の稜線が消えているのと下中心部が潰れて稜線がつながっています。 これは指紋を読み取るとき指圧が領域によって異なってしまうことが原因として考えられます。 この場合は指紋の読み取り面に対し指の下中心に強い指圧がかかり、逆に上部分に掛けて指圧が減少しているのでしょう。 このような画像に対しての二値化手法として適応的しきい値処理が効果的です。 適応的しきい値について [https://scikit-image.org/docs/stable/api/skimage.filters.html#skimage.filters.threshold_local] 適応的しきい値処理 adaptiveThreshold.py from skimage.filters import threshold_local #適応的しきい値処理による二値化 bin_locla=nl_img < threshold_local(nl_img,block_size=21) plt.figure(figsize=(12,12)) plt.subplot(121) plt.title('origin') plt.imshow(nl_img,cmap='gray') plt.subplot(122) plt.title('thresh_local') plt.imshow(bin_locla,cmap='gray') かなり良くなりました。 まとめ 今回は画像の読み込みから二値化という、画像分析のための前処理についてまとめました。 次回からこの二値化画像を用いて様々な分析を行いますのでお楽しみに。 続く
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PythonのJupyter NotebookでグラフだけRのggplot2で描きたい(ほぼ標準ライブラリ)

データ分析をしていて、基本的にはPythonでの作業が快適だけど、グラフだけはRのggplot2で描きたい、ということが私はよくあります。 そういうとき、普通は次の2つの手段を取ることが多いと思います。 ggpy ライブラリを使う。これは、ggplot2の機能をPythonに移植したものです。 rpy2 ライブラリを使い、Rのコードをセルに書く(例) 1つめの手段はPythonネイティブなのが嬉しいものの、完璧な移植とは限らないのと文法の差を覚え直す必要があるところが難点です。 2つめの方が個人的には好みですが、Rでやりたいことがggplot2のグラフを書くだけなら、もう少し簡単にできそうです。 そこで、ggplot2でグラフを書いて表示させるだけの関数です。IPythonとデータフレームを渡す場合にpandasの機能を使う以外は標準ライブラリで完結しています。 やっていることはシンプルです。 一時フォルダを作成 データフレームを利用する場合、それらを一時フォルダのCSVファイルに書き出し Rコードを生成 ライブラリの読込 データのロード ggplot2による描画を画像ファイルとして保存 保存した画像ファイルを表示 一時フォルダごと削除 import os import subprocess from tempfile import TemporaryDirectory from IPython.display import Image def ggplot(plotcode, libs=("magrittr", "ggplot2"), dispwidth=None, dispheight=None, width=None, height=None, showcode=False, **dataframes): with TemporaryDirectory() as tmpdir: importcode = ";".join(f"library({l})" for l in libs) readcode = [] for name, df in dataframes.items(): filename = os.path.join(tmpdir, f"__data_{name}.csv") df.to_csv(filename, index=False) readcode.append(f"{name} <- read.csv('{filename}', as.is=TRUE)") readcode = ";".join(readcode) graphfile = os.path.join(tmpdir, "__graph.png") if width is None: width = "NA" if height is None: height = "NA" code = f""" {importcode} {readcode} ..g <- {{ {plotcode} }} ggsave("{graphfile}", ..g, width={width}, height={height}) """ if showcode: print(code) subprocess.run(["Rscript", "-e", code]) display(Image(filename=graphfile, width=dispwidth, height=dispheight)) 上のセルを1回実行したうえで、下記のようなコードでグラフの描画ができます。 width, heightは保存時のサイズ、dispwidthは表示時のサイズです。 import pandas as pd import numpy as np df = pd.DataFrame({"x": np.linspace(-10, 10, 100)}) df["y"] = np.sin(df.x) df2 = pd.DataFrame({"x": np.linspace(-10, 10, 100)}) df2["y"] = 1.2*np.cos(0.6*df2.x) ggplot( """ ggplot(dat, aes(x=x, y=y)) + geom_line() + geom_line(aes(x=x, y=y), data=dat2, linetype="dotted") + theme_bw() """, dat=df, dat2=df2, width=4, height=2, dispwidth=640) こういった出力がでます。基本的にはRのコードを実行しているだけなので、自由度は高いです。 関数に少し変更を加えれば、Jupyter上で表示するのではなく指定の場所にファイルを書き出すようにもできます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

競プロ典型90問 001 - Yokan Party

問題 競プロ典型90問 001 Yokan Party 解法 使用する考え方 二分探索法 貪欲法 二分探索法 ソートされた複数のデータの中からあるデータを探す際に、中央値よりも大きいか小さいかで探す範囲を絞っていく方法。 また、何番目のデータか探すだけではなく、何番目と何番目の間に入るかを調べることもできる。 先頭から見ていくよりもかなり速い。 例 1,2,4,8,16の数字の中から8の場所を見つける場合について考える。 list = [1,2,4,8,16] lo = 0 hi = len(list) search = 1 while hi - lo > 1: mid = (lo+hi)//2 if list[mid] <= search: lo = mid else: hi = mid print(lo) #3 1回目は0番目~4番目を調べるため、中央値である2番目の4との大小を比較する。 4より大きいので次は3番目~4番目の数字を調べる。 3番目と4番目の中央値である8との大小を比較する。 統計学等で中央値という場合、データが偶数個の場合は中央の2つの平均を取る。 今回は何番目と何番目の間にあるかを調べたいため、インデックスの3と4の平均の3.5を切り捨てて3番目の8との大小比較を行なっている。 8以下なので3番目~3番目の数字を調べる。 調べる数字が1つになったため8の場所が3番目で確定する。 先頭から調べて行った場合最悪5回かかるが、この方法ならば最悪でも3回で済む。 データの数が大きくなって行った時に真価を発揮する。 注意 平均値ではなく中央値と比較すること。 例えば、1というデータの場所を探す際に、1回目に1と16の平均値で8.5と比較してしまった場合、2回目は1,2,4,8の中から探すことになってしまう。最終的に5回の確認が必要になる。 貪欲法 「このアルゴリズムは問題の要素を複数の部分問題に分割し、それぞれを独立に評価を行い、評価値の高い順に取り込んでいくことで解を得るという方法」らしい。 最初に聞いた時は意味がわからなかったし、今もよく分かってはいないが、例を見て自分の考えをまとめていく。 例 例として上がっていたのは以下の2つがあったのでこれらを見ていく。 なお以下の例は貪欲法で最適解が出るわけではないので注意する必要がある。 ある金額を払う時に最小の貨幣で払うための組み合わせを求める問題 評価値 = 1枚あたりの貨幣の価値 日本の貨幣であれば1枚あたりの価値が高い1万円札を出せるだけ出して、次は5千円札、千円札・・・と続いていく。 ナップサック問題 評価値 = 重さあたりの価値[v/w] 重さあたりの価値が高い順に、荷物を入れられるだけ入れていく。 実際の問題での使い方 貪欲法は、羊羹をある基準の長さ以上切り分ける時、切り分けられるだけ切り分けた時いくつになるかを調べるのに使っている。切り分けた数がK+1個以上なればその基準の長さは答えになりうる。 そしてK+1個以上に切り分けられる長さの中で最大のものが答えであるため、それを調べるために二分探索法を使っている。  ソースコード 以下が実際に解答したソースコードになる。 check関数に引数として基準の長さを渡すことで、K+1個以上に切り分けられるかをチェックしている。 そしてその範囲を二分探索法で絞っている。 N,L = map(int,input().split()) K = int(input()) A = list(map(int,input().split())) A.insert(0,0) A.append(L) maxLen = L//(K+1) minLen = 0 def check(halfLen): cnt = 0 work_a = 0 for a in A: if a - work_a < halfLen: continue else: work_a = a cnt += 1 if cnt >= K+1: return True else: return False while minLen != maxLen: halfLen = ((minLen+maxLen)//2) + 1 if check(halfLen): minLen = halfLen else: maxLen = halfLen -1 print(minLen) 最後に プログラミングに関して 初めは一番小さい切れ目を順番になくしていけば良いのかなとか考えて実装してみましたが全然ダメで、 L = 6 , A1 = 2 , A2 = 3 , A3 = 4 というテストデータを自分で作ってすぐ詰んでしまい、解法を調べながらコードを書いていきました。 二分探索法は考え方自体は知っていて、例の数字を探す時のように単体で使ったこともありましたが、実際に他の考え方と組み合わせて使うのが初めてだったため、かなり手間取ってしまいました。ただ、かなりの頻度で使うと思うので今回自分なりに整理できてよかったと思います。 貪欲法は一見速くなさそうに見えたんですが、実際に使ってみると計算量自体はデータ量に比例するため使い方によっては計算量がそこまで多くならないのかなと思いました。 記事に関して 正直、記事はまだ書き慣れていなくて全く読みやすい記事にはなっていないと思います。 そこは他の人の記事を読んでどういう構成にしたらわかりやすいとか、学んでいけたらと思います。あと、ここをこうしたら読みやすくなる等あれば教えていただけると幸いです。 あと自分で読み返して気になったのが常体じゃなくて敬体の方が良いかなと思いました。常体だと事実を述べているっていうイメージがあるんですが、その割にはふんわりとした説明だったり曖昧な理解だったりが多すぎるかなと。 なので次回からは敬体でもう少し人に説明するような形で書けたらと思います。 以上、最後まで読んでいただきありがとうございます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む