20211022のPythonに関する記事は24件です。

pythonを使ってRestAPIでファイルを取得する

はじめに pythonでRestAPIを実行し、テキストファイルを取得し、保存するコードです。 環境 python3.9.6 サンプルコード import requests url = 'https://[対象のURL]' headers = {'Content-Type':'application/json','xxx-key':'<API認証キーなど>'} try: #APIで隔離メールをダウンロード、失敗したらエラーを出力 response = requests.get(url , headers=headers) response.raise_for_status() except requests.exceptions.RequestsException as e: print("エラー:",e) #ファイルを取得できた場合に、filename.txtとしてファイルを保存する。 if response.status_code == 200: with open("filename.txt" ,mode='wb') as file: file.write(response.content)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Dashオブジェクトをインスタンス変数にしたときの、コールバックの書き方

Dashのソースコード量が多くなって、変数がグローバル汚染してしまうのを避けるために、Dashオブジェクトをクラス内部に保持したい場合のコールバックの書き方。 普通の書き方。 app = dash.Dash(__name__) app.layout = html.Div([ dcc.Input(id='input_id', value=None, type='text'), html.Div(id='output_div') ]) @app.callback( Output(component_id='output_div', component_property='children'), [Input(component_id='input_id', component_property='value')] ) def update_output_div(input_value): if input_value: return html.Div(className='output-area', children=[ html.Span(input_value) ]) else: html.Div() クラス内部に書くやり方。 class MyDash(): def __init__(self, app_name): self.app = dash.Dash(app_name) self.app.layout = html.Div([ dcc.Input(id='input_id', value=None, type='text'), html.Div(id='output_div') ]) # デコレータは使わない self.app.callback( Output(component_id='output_div', component_property='children'), [Input(component_id='input_id', component_property='value')] )(self.update_output_div) def update_output_div(self, input_value): if input_value: return html.Div(className='output-area', children=[ html.Span(input_value) ]) else: html.Div()
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

pygameでテキストを中央寄せにする

いろいろ調べてもやり方がよく分からず、分解して考えることで理解できる気がしたのでまとめてみた。 全体像はこんな感じ。 import pygame WIDTH = 500 HEIGHT = 500 pygame.init() screen = pygame.display.set_mode((WIDTH, HEIGHT)) font = pygame.font.SysFont(None, 120) text = font.render("Test", False, (255,255,255)) text_rect = text.get_rect(center=(WIDTH//2, HEIGHT//2)) endFlag = False while endFlag == False: for event in pygame.event.get(): if event.type == pygame.QUIT: endFlag = True screen.blit(text, text_rect) pygame.display.update() pygame.quit() 動かしてみるとこんな感じになる。 逆から重要なところだけ抜き取って読み解いていく screen.blit(画像, (x,y)) ↓ 画像と位置が必要 ↓ まずは画像を用意(テキストをレンダリングして画像とする) text = font.render("Test", False, (255,255,255)) ↓ このレンダリングされた情報を元に中央の位置を決める ↓ text_rect = text.get_rect(center=(WIDTH//2, HEIGHT//2)) 見ての通りWIDTH//2で横の中央、HEIGHT//2で縦の中央の位置を取得できる。 結局人から見てわかりやすいものができたかは分からないがとりあえず動くので自分はこれで覚えることにする。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

めちゃはやPython環境構築(よくわからん人おすすめ)

初投稿です.よろしくお願いします. 今回はPythonの環境構築をscoopでやっていきます Python環境構築 まずはPythonの環境構築をします.いつも私はscoopというツールを使って環境構築をしています.WindowsはUbuntu等と違って,インストールメディアを使ってインストールすることができますが,インストールする場所がぐちゃぐちゃになってしまったりuninstallするときにどれをuninstallすればいいかわからなかったりと,Windowsの中身を汚してしまいます(俺も無駄なストレージをいっぱい使ってしまったりしました)でもこのscoopを使えば,PythonなどのインストールをUbuntuのaptみたいに管理できるので非常に便利です.今回はそのscoopを使ってPython環境を作っていきます. scoopとは 以下の記事が参考になります ここの記事を見てやってもらってもいいですが一応位置から説明していきます. 要件 PowerShell 5 .Net Framework 4.5以上 Windows10だったら問題ないです! scoop install 上記要件を満たしていたらWindowsPowerShellから以下を実行する. Set-ExecutionPolicy RemoteSigned -scope CurrentUser invoke-Expression (New-Object System.Net.WebClient).DownloadString('https://get.scoop.sh') 以下のメッセージが出るのでYを押してください 実行ポリシーの変更 実行ポリシーは、信頼されていないスクリプトからの保護に役立ちます。実行ポリシーを変更すると、about_Execution_Policies のヘルプ トピック (https://go.microsoft.com/fwlink/?LinkID=135170) で説明されているセキュリティ上の危険にさらされる可能性があります。実行ポリシーを変更しますか? [Y] はい(Y) [A] すべて続行(A) [N] いいえ(N) [L] すべて無視(L) [S] 中断(S) [?] ヘルプ (既定値は "N"): このメッセージが出てインストール完了です Initializing... Downloading scoop... Extracting... Creating shim... Downloading main bucket... Extracting... Adding ~\scoop\shims to your path. 'lastupdate' has been set to '2020-08-19T11:27:12.6461118+09:00' Scoop was installed successfully! Type 'scoop help' for instructions. scoopでは,以下のようなコードでいろんなツールをインストールできます. scoop install ~ ここで追加でインストールしておくとよいモノを紹介しときます git scoop install git ついでに7zipもインストールされます.ファイルをめちゃめちゃ圧縮できます. aria2 scoop install aria2 scoopのファイルダウンロードがマルチセッションになってめっちゃはやくなります. Python install もうわかるかもしれませんが以下でもうオッケーです. scoop install python このコードではPythonの最新バージョンがインストールされます.2021/10/22時点ではpython 3.9.6でした. でも,ライブラリによってはPython37が必要だったり,人によってはバージョンを管理したい人もいるかもしれません.(Open3Dを使いたいときは,3.6,3.7,3.8のどれかとか) そんなときにもscoopはめちゃ便利です. ここからはpython 2.7,python 3.6,python 3.8をインストールしていきます! まずはこれからの作業で必要なため,以下を実行してください. scoop install git scoop bucket add versions これができたらそれぞれのバージョンをインストールしていきます! scoop install python38 scoop install python36 scoop install python27 これでインストールは完了です! Pythonバージョン管理 Pythonは,pipというツールを使って大量に存在するPythonライブラリをinstallすることができます. pip install numpy でもバージョンごとに使えるライブラリがちがうのでこれを管理したい人がいるはずです(俺はそうでした) でもこれまでで複数のバージョンをインストールしましたが,これらをどう使い分けたらいいかがわからない... もしそんな人がいたら以下で説明する内容でめちゃめちゃきれいにバージョン管理できます.もうよくわからんところにインストールしちゃったりなんかしちゃうことはなくなります! 今回はCドライブ直下にPython仮想環境を管理する\venvフォルダを作り,そこでバージョンを管理することにします.フォルダの場所は皆さんの好きなところで大丈夫です. まずは仮想環境用のフォルダを作ります mkdir C:\venv 次にPython最新バージョンの仮想環境を作ります 以下のコードでいけます C:\Users\your_name\scoop\apps\python\3.9.6\python -m venv C:\venv\python ここで,your_nameの部分はご自身のユーザーネームです.また3.9.5の部分も最新バージョンでインストールされたバージョンを入れてください. さらに,Python3.8,3.6の仮想環境を作ります. C:\Users\your_name\scoop\apps\python38\3.8.10\python -m venv C:\venv\python38 #Pyhotn3.8 C:\Users\your_name\scoop\apps\python36\3.6.8\python -m venv C:\venv\python36 # Python3.6 また,2.7はvenvが使えなかったのでvirtualenvをインストールします. C:\Users\your_name\scoop\apps\python27\2.7.18\python -m pip install virtualenv # Python2.7 C:\Users\your_name\scoop\apps\python27\2.7.18\python -m virtualenv C:\venv\python27 これを全部やるとC:\venv内がこんな感じになります これで環境完成です. この環境を利用するときはこんな感じで仮想環境をactivateします C:\venv\python\Scripts\activate.bat #最新バージョン C:\venv\python38\Scripts\activate.bat #3.8 C:\venv\python36\Scripts\activate.bat #3.6 C:\venv\python27\Scripts\activate.bat #2.7 それぞれの仮想環境に入った後pipコマンドでいろいろインストールすれば環境出来ちゃいます (例) C:\venv\python38\Scripts\activate.bat #3.8 pip install numpy pip install matplotlib
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

pythonでDBSCANアルゴリズムを実装

はじめに クラスタリングアルゴリズムの中でもk-meansと並んで有名なのがDBSCANです. 今回は理解を深めるためにできるだけシンプルな構成で,実装してみます. 単純に使いたいだけなら,scilit-learnの実装などを利用する方が簡単です. 目次 DBSCANアルゴリズム 実装 まとめ ソースコード DBSCANアルゴリズム DBSCANは密度ベースのクラスタリングアルゴリズムです. 多くの密度ベースアルゴリズムの基本となっており,比較的シンプルなアルゴリズムになっています. DBSCANではパラメータ$\epsilon$と,$MinPts$を与えることによって動作します.k-meansと違ってクラスタ数を事前に指定する必要はありません. core point データ点$p$に対して,距離半径$\epsilon$以内にある点を$\epsilon$-近傍点と呼び, $$N_{\epsilon}(p)= \lbrace q \mid q \in P,d_{p,q} \leq \epsilon \rbrace$$ と表記します.$d_{p,q}$は$p$と$q$の間の距離です.距離はユークリッド距離やチェビシェフ距離などが用いられます.このような$\epsilon$-近傍点に対して $$\lvert N_{\epsilon}(p) \rvert \geq MinPts$$ を満たすような点をcore pointと定義します. 直感的にはcore pointはクラスターの中核となるような点です. クラスタの定義 core pointである点$q$に対して,$p \in N_{\epsilon(q)}$は点$q$から点$p$へと直接到達可能な点と定義します.また,$p_1,...,p_n$に対して$p_i$から$p_{i+1}$がそれぞれ連鎖的に直接到達可能な時,$p_1$は$p_n$から到達可能な点であると言います. 到達可能は非対称な関係なので,対象な関係に拡張したものを密度連結であると言います $q$がcore pointである時に$q$から直接到達可能な点$p$はborder pointといいます. 一方で,core pointの定義を満たさず,かつ全てのcore pointから到達可能で無いような点のことをnoiseと呼びます. 直感的にborder pointはクラスタの境界付近に存在する点で,noiseは外れ値となるような点のことです. これらを踏まえて以下の二つのような条件を満たす空で無い集合のことをクラスタと定義します. 1. 全ての$p,q$に対して,$p \in C$から$q$に到達可能な時,$q \in C$を満たす.(Maximarity) 2. 全ての$p,q \in C$に対して,$p$は$q$と密度連結である.(Connectivity) 実装 では実際に実装してみます. アルゴリズム DBSCAN.py def fit(self, X): self.points = [Point(x) for x in X] for p in self.points: if not p.visited: p.visit() neighborPts = self.rangeQuery(p) if len(neighborPts) < self.min_samples: p.pointType = "NOISE" else: p.pointType = "CORE" self.expandCluster(p, neighborPts) self.label += 1 ここでは受け取ったデータ配列を順にスキャンしていきます.まだスキャンしていないに対して,core pointの定義を満たさなければnoise,満たしていればcore pointとしていきます. core pointとなるような点に対してはexpandClusterでクラスタを拡大していきます. DBSCAN.py def expandCluster(self, p, neighborPts): p.label = self.label for q in neighborPts: if not q.visited: q.visit() neighborPts_q = self.rangeQuery(q) if len(neighborPts_q) >= self.min_samples: q.pointType = "CORE" neighborPts.extend(neighborPts_q) if q.label == -1: q.label = self.label expandClusterでは受け取ったcore pointに対してrangeQueryを用いて$\epsilon$-近傍点を探索し,到達可能な点を同じクラスタに割り当てていきます.noiseで場合にはクラスタ番号は-1になっています. このようにexpandClusterを用いてcore pointから到達可能な点を全て探索していくことによって同じクラスタに属している点の集合を拡大していきます.呼び出し元のfitに戻った時には与えたcore pointの属するクラスタの点は全て探索しきっているので,次のクラスタに移ります.   実験 上で実装したアルゴリズムの中を使って簡単な実験を行います. 実験では上図のような2次元,3クラスタの簡単な合成データセットを用います. $\epsilon=0.2,MinPts=10$とした時のDBSCANの結果が下図のようになっています. 各クラスタがうまく抽出できていることがわかります. また,分散の大きいクラスタでは外側の外れ値がnoiseとして灰色の点になっています.DBSCANはこのように外れ値に対してロバストなクラスタリングアルゴリズムでもあります. まとめ DBSCANアルゴリズムは, クラスタ数を指定しなくて良い 任意の形状のクラスタを抽出できる 外れ値に対してロバスト などの利点がある優秀なアルゴリズムでありながら,割と簡単に実装できました. 発展手法では木構造を用いたrangeQueryの高速化や,全ての点を探索せずにクラスタリングする手法などいろいろ提案されているので注目です. ソースコード ソースコード全体はgithub上にあります. https://github.com/kotaYkw/DBSCAN
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Ubuntu 20.04でVirtualenvwrapperを使ってVirtualenvをセットアップする方法

この記事では、pip3(pip for Python 3)を使ってvirtualenvwrapperをセットアップする方法を紹介します。 このチュートリアルを成功させるには、Ubuntu 20.04がインストールされたコンピュータと、インターネット接続が必要です。また、ターミナルとnanoエディタについての知識があると便利です。システムのアップデートやアップグレードは済ませているものとします。 仮想環境のセットアップ ホームディレクトリで、右クリックして「ターミナルで開く」を選択し、ターミナルを開きます。また、キーボードのCTRL、ALT、Tキーを同時に押すと、ターミナルアプリケーションが自動的に開きます。 まず、すべての仮想環境を格納するための特別なディレクトリを作成する必要があります。virtualenv」という新しい隠しディレクトリの作成を進めます。 mkdir .virtualenv 次に、Python3用のpipをインストールします。 sudo apt install python3-pip pip3のインストールを確認します。 pip3 --version virtualenvをpip3でインストールします。 pip3 install virtualenv virtualenvがどこにインストールされたかは、次のように入力します。 which virtualenv virtualenvwrapperをpip3でインストールします。 pip3 install virtualenvwrapper これから .bashrc ファイルを修正して、すべての新しい仮想環境が Python 3 を使用するように調整する行を追加します。仮想環境には上で作成したディレクトリ(.virtualenv)を指定し、virtualenvとvirtualenvwrapperの場所も指定します。 では、nanoエディタで.bashrcファイルを開いてみましょう。 nano .bashrc nanoエディタをまだ使ったことがない、あるいはコンピュータにインストールしていない場合は、今すぐインストールしてください。nanoは Linux のエディタとして広く使われていますが、それには理由があります。 sudo apt install nano nano をインストールしたら、ターミナルで nano .bashrc コマンドを入力して、.bashrc ファイルを開きます。.bashrc ファイルの一番下に移動し、以下の行を追加します。 Virtualenvwrapperの設定。 export WORKON_HOME=$HOME/.virtualenvs export VIRTUALENVWRAPPER_PYTHON=/usr/bin/python3 export VIRTUALENVWRAPPER_VIRTUALENV_ARGS=' -p /usr/bin/python3 ' export PROJECT_HOME=$HOME/Devel source /usr/local/bin/virtualenvwrapper.sh 完了したら、ctrl-oキーを押します。次に「yes」と入力してエンターキーを押します。このコマンドはnanoエディタを保存します。ctrl-xを押して終了したらターミナルを閉じて、再度開いてください。 Python3で仮想環境を作成し、すぐに有効にするには、ターミナルで次のコマンドを使います。 mkvirtualenv name_of_your_env この環境がPython3用に設定されていることを確認してください。 Python -V 環境を無効にするにはdeactivateコマンドを使用します。 deactivate 利用可能なすべての仮想環境をリストアップするには、ターミナルで workon または lsvirtualenv コマンドを使用します(workon と同じ結果になりますが、格好良く表示されます)。 workon lsvirtualenv 特定の環境をアクティブにするには、workon + 環境の名前を入力します。 workon name_of_your_env この他にも便利なコマンドがいくつかありますので、いつか使ってみてください。 Rmvirtualenv は .virtualenv ディレクトリにある特定の仮想環境を削除します。 rmvirtualenv name_of_your_env Cpvirtualenv」は既存の仮想環境を新しい仮想環境にコピーして起動します。 cpvirtualenv old_virtual_env new_virtual_env エイリアスの登録 .bashrcファイルにエイリアスを登録することによって、毎回起動時に面倒な仮想環境に起動やディレクトリの移動などを簡単にすることができます。 nano .bashrc で、ファイルを開き、以下のエイリアスを挿入する。 alias your_aliasname='workon name_of_your_env;cd /name_of_your_directory' 次回、ターミナル起動時に、 your_aliasname を入力するだけで、name_of_your_envが起動し、name_of_your_directoryに移動してくれます。 お疲れ様でした。これで最初の孤立したPython 3環境ができあがりました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

機械学習でよく使われる数学記号(1)

1 はじめに 機械学習、ディープラーニング関連の論文を読む時に、出てくる数式を理解するため、理解しておくべき数学記号を整理しました。 2. 代数 (Algebra) 2.1 総和(Summation) 記号&例題 \sum_{k=1}^{n} x^k 例題の意味 x^1 +x^2+ ... +x^n 要するに足し算です。 2.2 総積(Production) 記号&例題 \prod_{k=1}^{n} x^k 例題の意味 x^1 \times x^2\times ... +\times ^n 要するに掛け算です。少し息が上がりました。 2.3 関数値が最大になる元(argument) 記号&例題 argmax \quad f(x) 例題の意味 f(x)を最大にするようなx プログラミング用語かと思ったら、ちゃっと数学記号でした。 2.4 関数値が最小になる元(argument) 記号&例題 argmin \quad f(x) 例題の意味 f(x)を最小にするようなx プログラミング用語かと思ったら、ちゃっと数学記号でした。(小梅太夫の声で) 3. 線形代数 (Linear Algebra) 3.1 ノルム(Norm) 記号&例題 \begin{Vmatrix} (3,4) \end{Vmatrix} 例題の意味 \sqrt{3^2+4^2} ベクトルの長さor距離 3.2 写像(mapping) 記号&例題 f:x \Longrightarrow y 例題の意味 y = f(x) xからyへの対応付ける 3.3 合成写像(composite function, composite) 記号&例題 f \circ g 例題の意味 y = f(g(x)) 順番に注意 4. 論理演算 (Logical Operation) 4.1 論理和(Logical OR) 記号&例題 A \bigvee B 例題の意味 AまたはB OR 4.2 論理積(Logical AND) 記号&例題 A \bigwedge B 例題の意味 AかつB AND 4.3 論理包含/条件 記号&例題 A \Rightarrow B \\ A \rightarrow B 例題の意味 AならばB if文より「断言」のニュアンスが強い。 4.4 全称記号 記号&例題 \forall x (x<0) 例題の意味 すべての x は、0より小さい for Allと読む。 4.5 存在記号 記号&例題 \exists x (x<0) 例題の意味 0より小さい xが存在する。 Existsと読む。Eをひっくり返した表記 5. 数の記号 5.1 実数 記号&例題 \mathbb{R} 5.2 有理数 記号&例題 \mathbb{Q} 英語で商を意味するQuotinentの頭文字 5.3 整数 記号&例題 \mathbb{Z} ドイツ語で数を意味するZahlenの頭文字 5.4 自然数 記号&例題 \mathbb{N} Natural Numberの頭文字 参考資料 http://www27.cs.kobe-u.ac.jp/~masa-n/misc/cmc/j-kiso2001/iabasic/jlshort/node34.html https://qiita.com/PlanetMeron/items/63ac58898541cbe81ada#%E5%9B%9B%E5%89%87%E6%BC%94%E7%AE%97 https://qiita.com/Qiita/items/c686397e4a0f4f11683d#emphasis---%E5%BC%B7%E8%AA%BF%E5%BC%B7%E5%8B%A2
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

データ分析、前処理、モデル構築・適用

この記事の狙い・目的 機械学習を取り入れたAIシステムの構築は、 ①データ分析→ ②データセット作成(前処理)→ ③モデルの構築・適用 というプロセスで行っていきます。 このブログでは、①②③の全工程ついて解説していきます。 プログラムの実行環境 Python3 MacBook pro(端末) PyCharm(IDE) Jupyter Notebook(Chrome) Google スライド(Chrome) データ分析 Kaggleのボストンの住宅価格予測のデータセットを用いてデータ分析を行なった全手順を解説しています。 前処理(特徴量エンジニアリング) 前処理の各手順を解説しています。 クレンジングなどのデータ整備は割愛させていただきます。 モデルの構築・適用 モデルの構築、パラメータ・チューニング、アンサンブル学習までを解説しています。 まとめ ①データ分析→ ②データセット作成(前処理)→ ③モデルの構築・適用、までを通して行なってきました。 これまで2値分類問題として解くことが多かったため、今回は回帰問題で解いてみることにしました。 実際に色々な手法を試して精度検証を行って見て、やはりアルゴリズムごとに向き不向きがあり、その使い分けを今回学ぶことができました。次はまた別のデータを用いて分析から前処理、モデリングまでを行なって見たいと思います。 最後に 他の記事はこちらでまとめています。是非ご参照ください。 解析結果 実装結果:GitHub/boston_regression データセット:Boston House Prices-Advanced Regression Techniques 参考資料
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

モデル構築・適用

この記事の狙い・目的 機械学習を取り入れたAIシステムの構築は、 ①データ分析→ ②データセット作成(前処理)→ ③モデルの構築・適用 というプロセスで行っていきます。 その際「前処理」の段階では、データ分析の考察を踏まえて、精度の高いデータセットが作れるよう様々な対応が必要となります。 このブログでは、「前処理(特徴量エンジニアリング)」の工程について初めから通して解説していきます。 プログラムの実行環境 Python3 MacBook pro(端末) PyCharm(IDE) Jupyter Notebook(Chrome) Google スライド(Chrome) 評価比較 from sklearn.model_selection import train_test_split # データ分割 # アルゴリズム from sklearn.neighbors import KNeighborsRegressor as KNR # K近傍法(回帰) from sklearn.linear_model import LinearRegression as LR # 線形回帰 from sklearn.linear_model import Ridge, Lasso, ElasticNet # Ridge回帰、Lasso回帰、ElasticNet回帰 from sklearn.svm import SVR # サポートベクトルマシン from sklearn.ensemble import RandomForestRegressor as RFR # ランダム・フォレスト(回帰) from xgboost import XGBRFRegressor as XGBRFR # XGBoost from sklearn.ensemble import GradientBoostingRegressor as GBR # GBDT(勾配ブースティング木) # 各評価指標 from sklearn.metrics import r2_score as R2 from sklearn.metrics import mean_absolute_error as MAE from sklearn.metrics import mean_squared_error as MSE # 評価 def evaluation(model, y_test, y_pred): print('='*40) print(model.__class__.__name__) print('決定係数(R2) = ', R2(y_test, y_pred).round(decimals=3)) mae = MAE(y_test, y_pred) print('平均絶対誤差(MAE) = ', mae.round(decimals=3)) rmse = np.sqrt(MSE(y_test, y_pred)) print('平均二乗平方根誤差(RMSE) = ', rmse.round(decimals=3)) print('RMSE / MAE = ', (rmse / mae).round(decimals=3)) # <, =, > 1.253 aic = AIC(y_test, y_pred, num_feature=len(boston_df.columns)) print('AIC = ', aic.round(decimals=3)) return mae, rmse, aic # AIC(赤池情報量規準) def AIC(y_test, y_pred, num_feature): num_data = len(y_test) # サンプル数 mse = MSE(y_test, y_pred) # MSE rss = (0.5 * np.sum((y_test - y_pred)**2) / num_data) # 残差平方和 return (num_data * (np.log(2 * np.pi) * (rss / num_data) + 1)) + 2 * (num_feature + 1) # データ分割 def data_devide(df, test_size=0.2): target = 'MEDV' X = df.drop(columns=target, axis=1) y = df[target] X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=test_size, random_state=42) return X_train, X_test, y_train, y_test # モデリング def modeling(model, df): # ホールドアウト法 X_train, X_test, y_train, y_test = data_devide(df, test_size=0.2) # 学習、予測 model = model model.fit(X_train, y_train) y_pred = model.predict(X_test).round(decimals=2) # 評価 mae, rmse, aic = evaluation(model, y_test, y_pred) # モデル rfr_model = RFR(random_state=42) # ランダムフォレスト lr_model = LR() # 線形回帰 ridge_model = Ridge() # Ridge lasso_model = Lasso() # Lasso gbr_model = GBR(random_state=42) # 勾配ブースティング木 xgbrfr_model = XGBRFR(random_state=42) # XGBoost knr_model = KNR() # K近傍法(回帰) svr_model = SVR() # サポートベクトルマシン models = [rfr_model, lr_model, ridge_model, lasso_model, gbr_model, xgbrfr_model, knr_model, svr_model] # 調整前のデータで評価 for model in models: modeling(model, boston_before) # 調整後のデータで評価 for model in models: modeling(model, boston_df) R2 全体的にあてはまりの良さは改善 RMSE / MAE Ridge、LinearRegressionが一番大きく外していない。ただしR2が高くない RandomForestRegressorは、大きく外している値もある。外れ値をもっと除く。パラメータを変更する。 SVRは、一番大きく外している。 AIC 全体的に改善。複数項目を取り除いたのが効いている。 $$ AIC = n (\log{(2\pi\frac{S_e}{n}+1)} + 2(p + 1)) $$ $n$ = サンプル数 : len(y_test) $S_e$ = 残差平方和 $p$ = 項目数 : num_feature 学習状況の確認 from sklearn.model_selection import learning_curve # 学習曲線 def plt_learn_curve(model, df): # ホールドアウト法 X_train, X_test, y_train, y_test = data_devide(df, test_size=0.2) # データ準備 train_sizes, train_scores, val_scores = learning_curve(model, X=X_train, y=y_train, train_sizes = np.linspace(0.1, 1.0, 10), cv=5, n_jobs=1) train_scores_mean = np.mean(train_scores, axis=1) train_scores_std = np.std(train_scores, axis=1) val_scores_mean = np.mean(val_scores, axis=1) val_scores_std = np.std(val_scores, axis=1) # 訓練スコア と 検証スコア をプロット print(model.__class__.__name__) plt.figure(figsize=[10,4]) plt.title("学習曲線") plt.xlabel("訓練サンプル") plt.ylabel("スコア") plt.plot(train_sizes, train_scores_mean, 'o-', color="r", label="訓練スコア") plt.plot(train_sizes, val_scores_mean, 'o-', color="g", label="検証スコア") # 標準偏差の範囲を色付け plt.fill_between(train_sizes, train_scores_mean - train_scores_std, train_scores_mean + train_scores_std, color="r", alpha=0.2) plt.fill_between(train_sizes, val_scores_mean - val_scores_std, val_scores_mean + val_scores_std, color="g", alpha=0.2) # Y軸の範囲指定 plt.ylim(0.5, 1.0) # 凡例の表示位置 plt.grid() plt.legend(loc="best") plt.show() # 調整前のデータで確認 models = [rfr_model, ridge_model, lasso_model, gbr_model, xgbrfr_model] for model in models: plt_learn_curve(model, boston_before) # 調整後のデータで確認 for model in models: plt_learn_curve(model, boston_df) 調整後の結果 訓練データに適合しているが、検証データに適合しておらず、過学習が起きているものと思われる。 調整前後の比較・考察 非線形モデルは調整後の数値が向上している。ただし線形モデルは、調整後の方がスコアが若干下がっている。線形モデル向けに評価が低下した理由を考察し、改善を図ることとする。 調整した意味はありそうだが、過学習を要抑制。「交差検証」で精度の見積もりを行い、「パラメーター・チューニング」で調整を行う必要がありそう。 交差検証 KFold from sklearn.model_selection import KFold # Fold数を変化させた評価を比較・考察する。 def fold(model, df, fold): # 交差検証 target = 'MEDV' for train_index, val_index in kf.split(df.index): X_train = df.drop(columns=target, axis=1).iloc[train_index] y_train = df[target].iloc[train_index] X_test = df.drop(columns=target, axis=1).iloc[val_index] y_test = df[target].iloc[val_index] # 学習、予測 model.fit(X_train, y_train) y_pred = model.predict(X_test).round(decimals=2) # 評価 mae, rmse, aic = evaluation(model, y_test, y_pred) return rmse_mae, aic # 実行 models = [rfr_model, ridge_model, xgbrfr_model] kf = KFold(n_splits=11, shuffle=True) # インスタンス rmse_maes = [] aics = [] for model in models: rmse_mae, aic = fold(model, boston_df, fold=kf) rmse_maes.append(rmse_mae) aics.append(aic) print('='*20) print('(RMSE / MAE)平均 = ', np.mean(rmse_maes).round(decimals=3)) print('AIC平均 = ', np.mean(aics).round(decimals=3)) fold=11で、(RMSE / MAE)=1.253に一番近づくため、これらの結果を精度目標とする。 AICも100以下を目指して、不要なパラメータを除くようにする。 パラメータ・チューニング # パラメータ・チューニング from sklearn.model_selection import RandomizedSearchCV # 探索用関数 def params_search(df, model, params): # データ分割 X_train, X_test, y_train, y_test = data_devide(df, test_size=0.2) # パラメータの探索 random = RandomizedSearchCV(estimator=model, param_distributions=params) random_result = random.fit(X_train, y_train) print(random_result) print(random.best_params_) print(random.best_score_) ランダム・サーチ ランダムフォレスト回帰 # ランダムフォレスト(回帰) params_rfr = { 'n_estimators' : [i for i in range(10, 100, 10)], 'max_features' : [i for i in range(1, boston_df.shape[1]-1)], 'n_jobs' : [i for i in range(1, 10)], 'min_samples_split' : [i for i in range(10, 100, 10)], 'max_depth' : [i for i in range(3, 15)] } model_rfr = RFR() params_search(boston_df, model_rfr, params_rfr) # 結果:{'n_jobs': 1, 'n_estimators': 80, 'min_samples_split': 30, 'max_features': 6, 'max_depth': 11} Ridge回帰 # Ridge回帰 params_ridge = { 'alpha' : [i/10 for i in range(1, 10)], 'solver' : ['auto','cholesky','sag','saga'] } model_ridge = Ridge() params_search(boston_df, model_ridge, params_ridge) # 結果:{'solver': 'cholesky', 'alpha': 0.9} 勾配ブースティング木 # 勾配ブースティング木 params_gbr = { 'max_depth' : [i for i in range(5, 15)], 'subsample' : [i/10 for i in range(1, 10)], 'n_estimators' : [i for i in range(900, 1100, 50)], 'learning_rate' : [i/100 for i in range(1, 10)] } model_gbr = GBR() params_search(boston_df, model_gbr, params_gbr) # 結果:{'subsample': 0.6, 'n_estimators': 1050, 'max_depth': 6, 'learning_rate': 0.01} グリッド・サーチ # パラメータ・チューニング from sklearn.model_selection import GridSearchCV # 探索用関数 def params_search(df, model, params): # データ分割 X_train, X_test, y_train, y_test = data_devide(df, test_size=0.2) # パラメータの探索 grid = GridSearchCV(estimator=model, param_grid=params) grid_result = grid.fit(X_train, y_train) print(grid_result) print(grid.best_params_) print(grid.best_score_) ランダムフォレスト回帰 params_rfr = { 'n_estimators' : [i for i in range(80, 85)], 'max_features' : [i for i in range(10, 13)], 'n_jobs' : [i for i in range(8, 10)], 'min_samples_split' : [i for i in range(25, 30)], 'max_depth' : [i for i in range(10, 12)] } model_rfr = RFR() params_search(boston_df, model_rfr, params_rfr) # 結果:{'n_estimators': 87, 'max_depth': 11, 'max_features': 12, 'min_samples_split': 25, 'n_jobs': 9} Ridge回帰 params_ridge = { 'alpha' : [i/10 for i in range(5, 9)], 'solver' : ['cholesky','sag','saga'] } model_ridge = Ridge() params_search(boston_df, model_ridge, params_ridge) # 結果:{'alpha': 0.8, 'solver': 'cholesky'} 勾配ブースティング木 params_gbr = { 'max_depth' : [i for i in range(12, 15)], 'subsample' : [i/10 for i in range(4, 8)], 'n_estimators' : [i for i in range(1030, 1070, 5)], 'learning_rate' : [i/100 for i in range(4, 8)] } model_gbr = GBR() params_search(boston_df, model_gbr, params_gbr) # 結果:{'learning_rate': 0.04, 'max_depth': 14, 'subsample': 0.5} 結果 ランダムフォレスト回帰 {'n_jobs': 8, 'n_estimators': 87, 'min_samples_split': 27, 'max_features': 10, 'max_depth': 8} Ridge回帰 {'solver': 'cholesky', 'alpha': 0.8} 勾配ブースティング木 {'subsample': 0.5, 'n_estimators': 1030, 'max_depth': 12, 'learning_rate': 0.05} 再評価 # モデリング def modeling(model, df): # データ分割 X = df.drop('MEDV',axis=1) y = df['MEDV'] X_train,X_test,y_train,y_test = train_test_split(X, y, test_size=0.2, random_state=42) # 学習、予測 model = model model.fit(X_train, y_train) y_pred = model.predict(X_test).round(decimals=2) # 評価 mae, rmse, aic = evaluation(model, y_test, y_pred) return model # ランダムフォレスト rfr_model = RFR( n_estimators=87, max_features=10, n_jobs=8, min_samples_split=27, max_depth=87 ) # Ridge ridge_model = Ridge( alpha=0.8, solver='cholesky' ) # 勾配ブースティング木 gbr_model = GBR( max_depth=12, subsample=0.5, n_estimators=1030, learning_rate=0.05 ) models = [rfr_model, ridge_model, gbr_model] # 評価 mdls = {} for model in models: mdl = modeling(model, boston_before) mdls[model.__class__.__name__] = mdl # 学習曲線 for name, model in mdls.items(): plt_learn_curve(model, boston_df) Ridge回帰が精度低め。(Ridgeの学習時に)データセットをRidge向けに変換する。 XGBoostは過学習をおこしているため、過学習対策を行う。 学習データの調整 from sklearn.pipeline import make_pipeline from sklearn.preprocessing import StandardScaler # ランダムフォレスト回帰 rfr_model = RFR( n_estimators=87, n_jobs=8, min_samples_split=27, max_depth=87 ) # Ridge回帰 ridge_model = make_pipeline( StandardScaler(),# 標準化 Ridge(alpha=0.8, solver='cholesky') ) # 勾配ブースティング木 gbr_model = GBR( max_depth=12, subsample=0.5, n_estimators=1030, learning_rate=0.05, validation_fraction=0.1, # 過学習対策 n_iter_no_change=3, # 過学習対策 tol=0.001 # 過学習対策 ) models = [rfr_model, ridge_model, gbr_model] # 評価 mdls = {} for model in models: mdl = modeling(model, boston_before) mdls[model.__class__.__name__] = mdl # 学習曲線 for name, model in mdls.items(): plt_learn_curve(model, boston_df) 訓練スコアと検証スコアの差が全体的に縮まり、過学習が改善されている。 ただい検証スコア自体にあまり大きな変化はない。この後のアンサンブル学習の評価でどこまで良くなるか確認する。 アンサンブル # スタッキング # ホールドアウト法 X_train, X_val, y_train, y_val = data_devide(boston_df, test_size=0.3) X_train = X_train.reset_index(drop=True) X_val = X_val.reset_index(drop=True) y_train2 = y_train.reset_index(drop=True) y_val = y_val.reset_index(drop=True) # モデル models = [rfr_model, ridge_model, gbr_model] preds = [] for model in models: cv = KFold(n_splits=11, random_state=42, shuffle=True) pred_model = np.zeros(len(y_train)) for trn_index, val_index in cv.split(X_train): X_trn, X_val = X_train.iloc[trn_index], X_train.iloc[val_index] y_trn, y_val = y_train.iloc[trn_index], y_train.iloc[val_index] model.fit(X_trn, y_trn) pred_model[val_index] = model.predict(X_val) preds.append(pred_model) # 2層目の学習データとして用いる為、学習データの予測値を取得 kf_train = pd.DataFrame({"RFR_予測":preds[0], "RIDGE_予測":preds[1], "DGBR_予測":preds[2]}) # display(kf_train) # テストデータの予測値を取得 pred_tests = [] for model in models: model.fit(X_train, y_train) pred_test = model.predict(X_val) pred_tests.append(pred_test) # 2層目の予測データとして用いるため、テストデータの予測値を取得 kf_tests = pd.DataFrame({"RFR_予測":pred_tests[0], "RIDGE_予測":pred_tests[1], "DGBR_予測":pred_tests[2]}) # display(kf_tests) # 2層目は「ランダムフォレスト回帰」で予測 model2 = rfr_model model2.fit(kf_train, y_train) pred_stack = model2.predict(kf_tests) # 1層目の予測値 kf_train.head() kf_tests.head() # スタッキングの評価 mae, rmse, aic = evaluation(model2, y_val, pred_stack) 最高スコア。交差検証で当初見積もっていた目標値よりも良い結果を出せました。 まとめ モデルの構築・適用を行ってみて、まだまだデータ分析、前処理の工程の進め方が甘いと認識しました。特に線形モデルの精度が上がらず改善が必要そうです。過学習の対策も行っていますが、まだ少し訓練スコアと検証スコアに差があり、パラメータの調整でもう少し改善できるかもしれません。Kaggleの他の方の実装なども参考にしながら改善点を模索していきたいと思います。 最後に 他の記事はこちらでまとめています。是非ご参照ください。 解析結果 実装結果:GitHub/boston_regression_modeling.ipynb データセット:Boston House Prices-Advanced Regression Techniques 参考資料
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Qittaにマークダウン方式でデータフレームを載せたい

Qittaにマークダウン方式でPythonのデータフレームを載せたい まずJupiter notebookのセルに以下を打ち込み必要なモジュールであるtabulateをインストールする。 !pip install pytablewriter あとはマークダウン化したいデータフレーム(df)に対して以下を実行する。 import pytablewriter writer = pytablewriter.MarkdownTableWriter() writer.from_dataframe(df) writer.write_table()
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

前処理(特徴量エンジニアリング)

この記事の狙い・目的 機械学習を取り入れたAIシステムの構築は、 ①データ分析→ ②データセット作成(前処理)→ ③モデルの構築・適用 というプロセスで行っていきます。 その際「前処理」の段階では、データ分析の考察を踏まえて、精度の高いデータセットが作れるよう様々な対応が必要となります。 このブログでは、「前処理(特徴量エンジニアリング)」の工程について初めから通して解説していきます。 プログラムの実行環境 Python3 MacBook pro(端末) PyCharm(IDE) Jupyter Notebook(Chrome) Google スライド(Chrome) データ確認 # データ取得 boston_df = pd.read_csv("./boston.csv", sep=',') # データ確認 boston_df.shape boston_df.head() boston_df.info() boston_df.describe() 精度評価 from sklearn.ensemble import RandomForestRegressor as RFR # ランダム・フォレスト(回帰) from sklearn.model_selection import train_test_split # データ分割 from sklearn.metrics import r2_score, mean_squared_error, mean_absolute_error, mean_squared_log_error # 各評価指標 def learning(model, df): # データ分割 X = df.drop('MEDV',axis=1) y = df['MEDV'] X_train,X_test,y_train,y_test = train_test_split(X, y, test_size=0.2, random_state=42) # 学習、予測 rfr_model = model rfr_model.fit(X_train, y_train) y_pred = rfr_model.predict(X_test).round(decimals=1) # 評価 print('決定係数(R2) = ', r2_score(y_test, y_pred).round(decimals=3)) print('平均絶対誤差(MAE) = ', mean_absolute_error(y_test, y_pred).round(decimals=3)) print('平均二乗誤差(MSE) = ', mean_squared_error(y_test, y_pred).round(decimals=3)) print('対数平均二乗誤差(MSLE) = ', mean_squared_log_error(y_test, y_pred).round(decimals=3)) print('平均二乗平方根誤差(RMSE) = ', np.sqrt(mean_squared_error(y_test, y_pred)).round(decimals=3)) print('対数平方平均二乗誤差(RMSLE) = ', np.sqrt(mean_squared_log_error (y_test, y_pred)).round(decimals=3)) return rfr_model, y_test, y_pred # 実行 rfr_model, y_test, y_pred = learning(RFR(random_state=42), boston_df) # 結果 # 決定係数(R2) = 0.892 # 平均絶対誤差(MAE) = 2.04 # 平均二乗誤差(MSE) = 7.902 # 対数平均二乗誤差(MSLE) = 0.02 # 平均二乗平方根誤差(RMSE) = 2.811 # 対数平方平均二乗誤差(RMSLE) = 0.142 非線形モデルでまずまずの評価 特徴量重要度 ランダム・フォレスト版 def rfr_importance(df): # データ分割 X = df.drop(columns='MEDV', axis=1) y = df['MEDV'] X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=0) # モデリング clf_rf = RFR() clf_rf.fit(X_train, y_train) y_pred = clf_rf.predict(X_test) # 評価 print('平均二乗平方根誤差(RMSE) = {:>.3f}'.format(np.sqrt(mean_squared_error(y_test, y_pred)))) # 重要度 fimp = clf_rf.feature_importances_ # データフレームに変換 imp_df = pd.DataFrame() imp_df['項目名'] = df.columns[:-1] imp_df['重要度'] = fimp.round(decimals=3).astype(str) return imp_df # 実行 imp_df = rfr_importance(boston_df) imp_df.sort_values(by='重要度', ascending=False) XGBoost版 import xgboost as xgb def boost_importance(df): # データ分割 X = df.drop(columns='MEDV', axis=1) y = df['MEDV'] X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=0) # パラメータ xgb_params = {"objective": "reg:linear", "eta": 0.1, "max_depth": 6, "silent": 1} num_rounds = 100 # XGBoost用のデータセットの作成 dtrain = xgb.DMatrix(X_train, label=y_train) # 学習 gbdt = xgb.train(xgb_params, dtrain, num_rounds) # 重要度 _, ax = plt.subplots(figsize=(12, 4)) # パラメーター:gain 予測精度をどれだけ改善させたることができるできたか(平均値) xgb.plot_importance(gbdt, ax=ax, importance_type='gain') # 実行 boost_importance(boston_df) 線形回帰モデル版 from sklearn.linear_model import Ridge, RidgeCV, ElasticNet, LassoCV, LassoLarsCV from sklearn.model_selection import cross_val_score def linear_importance(df): X = df.drop(columns='MEDV', axis=1) y = df['MEDV'] X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=0) def rmse_cv(model): rmse= np.sqrt(-cross_val_score(model, X_train, y_train, scoring="neg_mean_squared_error", cv = 5)) return(rmse) # model_ridge = Ridge().fit(X_train, y_train) model_lasso = LassoCV().fit(X_train, y_train) print(f'スコア: {rmse_cv(model_lasso).mean().round(decimals=3)}') coef = pd.Series(model_lasso.coef_, index = X_train.columns) print("選択項目数: " + str(sum(coef != 0)) + "、除外項目数: " + str(sum(coef == 0))) plt.figure(figsize=[8,4]) imp_coef = coef.sort_values() imp_coef.plot(kind = "barh").grid() plt.title("Lassoモデルの回帰係数") # 実行 linear_importance(boston_df) ランダム・フォレスト + LIME版 import lime import lime.lime_tabular def explain_importance(df): # データ分割 X = df.drop(columns='MEDV', axis=1) y = df['MEDV'] X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.3, random_state = 0) # ランダム・フォレスト(回帰) model = RFR(max_depth=6, random_state=0, n_estimators=10) model.fit(X_train, y_train) # パラメータ RFR(bootstrap=True, criterion='mse', max_depth=6, max_features='auto', max_leaf_nodes=None, min_impurity_decrease=0.0, min_impurity_split=None, min_samples_leaf=1, min_samples_split=2, min_weight_fraction_leaf=0.0, n_estimators=10, n_jobs=None, oob_score=False, random_state=0, verbose=0, warm_start=False) # 予測 y_pred = model.predict(X_test) # 評価 mse = mean_squared_error(y_test, y_pred)**(0.5) print(f'MSE: {mse}') # 予測の判断根拠を示す(LIME) explainer = lime.lime_tabular.LimeTabularExplainer(X_train.values, feature_names=X_train.columns.values.tolist(), class_names=['MEDV'], verbose=True, mode='regression', random_state=0) # 解釈結果(予測結果への影響)を表示する j = 5 exp = explainer.explain_instance(X_test.values[j], model.predict, num_features=6) exp.show_in_notebook(show_table=True) # 実行 from IPython.display import Image explain_importance(boston_df) plt.savefig("newplot_lime.png") Image("./newplot_lime.png") 分析通り、RM、LSTATは重要度が高い数値を出している。 CHASは重要度が高いと見ていたが、低い数値となっている。単体では低い数値のため何かしらの変換が必要と思われる。 特徴量エンジニアリング 対応方針 正規分布への変換 MEDV: 予測残差の正規分布性を期待する分析アルゴリズム(線形回帰など)を使用するため、必要と考える。 また、目的変数にマイナス値が既に含まれてしまっているため、Box-Cox変換を用いる。 変換結果にマイナス値を含まれてしまい、線形モデルの評価時にエラーとなるため、Yeo-Johnson変換は行わない。   外れ値 CRIM: (大きく外れた)外れ値(80)を含んでいる。犯罪率80%は疑わしい値。線形モデルを使用する(予定)のため、この外れ値を除去する。 スケーリング NOX, RM, DIS, PTRATIO, LSTAT, MEDV: (微妙な)外れ値の影響を軽減させたい 線形回帰に対しては有効でないと思われるが、非線形アルゴリズムをアンサンブルで使用する(予定)のため、有効であると考える。 特徴量生成 クラスタの重心からの距離: (まだ試してみたことがないため)実験的に効果検証してみる。 正規分布への変換 # 変換前の状態 import warnings warnings.filterwarnings('ignore') from scipy.stats import norm plt.figure(figsize=[8,3]) sns.distplot(boston_df['MEDV'], kde=True, fit=norm, fit_kws={'label': '正規分布'}).grid() plt.legend(); Yeo-Johnson変換 # Yeo-Johnson変換 from sklearn.preprocessing import PowerTransformer yeo_johnson_df = boston_df.copy() sk_yeojohnson = PowerTransformer(method='yeo-johnson') # インスタンス生成 yeojohnson_data = sk_yeojohnson.fit_transform(yeo_johnson_df[['MEDV']]) # 変換 yeo_johnson_df['MEDV'] = yeojohnson_data import warnings warnings.filterwarnings('ignore') from scipy.stats import norm plt.figure(figsize=[8,3]) sns.distplot(yeo_johnson_df['MEDV'], kde=True, fit=norm, fit_kws={'label': '正規分布'}).grid() plt.legend() ランダムフォレストには効果があったが、線形モデルには適用できない。評価の過程で目的変数に負値を扱えないため。 Box-Cox変換 # Box-Cox変換 from scipy.special import boxcox1p boxcox_df = boston_df.copy() lam=0.15 boxcox_df['MEDV'] = boxcox1p(boxcox_df['MEDV'], lam) # 描画 plt.figure(figsize=[8,3]) sns.distplot(boxcox_df['MEDV'], kde=True, fit=norm, fit_kws={'label': '正規分布'}).grid() 分布の偏りが軽減された。しかし非線形モデルには逆効果だった。 外れ値処理 # 確認 plt.figure(figsize=[10,2]) sns.boxplot(data=boston_df, x='CRIM').grid() # 外れ値除去 boston_df.shape boston_df = boston_df[boston_df['CRIM']<40] boston_df = boston_df.reset_index() boston_df.shape # (506, 15) # (500, 15) plt.figure(figsize=[10,2]) sns.boxplot(data=boston_df, x='B').grid() # 外れ値除去 boston_df.shape boston_df = boston_df[boston_df['B']>10] boston_df = boston_df.reset_index() boston_df.shape # (500, 15) # (493, 15) スケーリング col = ['NOX', 'RM', 'DIS', 'PTRATIO', 'LSTAT', 'MEDV'] boston_df[col].hist(bins=10, figsize=(10,8)) from sklearn.preprocessing import StandardScaler # 標準化 scaler = StandardScaler() boston_df['NOX'] = scaler.fit_transform(boston_df[['NOX']]) # 描画 plt.figure(figsize=[8,3]) sns.distplot(boston_df['NOX'], kde=True, fit=norm, fit_kws={'label': '正規分布'}).grid(); RM, DIS, PTRATIO, LSTAT, MEDVは、標準化しても精度に影響がなかったため、そのままとする。 特徴量生成 クラスタリング、主成分分析 # クラスター数の探索(k-means++) from sklearn.cluster import KMeans def elbow(df): wcss = [] for i in range(1, 10): kmeans = KMeans(n_clusters = i, init = 'k-means++', max_iter = 300, n_init = 30, random_state = 0) kmeans.fit(df.iloc[:, :]) wcss.append(kmeans.inertia_) plt.figure(figsize=[8,4]) plt.plot(range(1, 10), wcss) plt.title('エルボー法') plt.xlabel('クラスター数') plt.ylabel('クラスター内平方和(WCSS)') plt.grid() plt.show() # 実行 elbow(boston_df) # 主成分分析 from sklearn.decomposition import PCA # 寄与率 pca = PCA(n_components=3) pca.fit(boston_df) plt.figure(figsize=[8,4]) plt.grid() plt.bar([n for n in range(1, len(pca.explained_variance_ratio_)+1)], pca.explained_variance_ratio_) # 累積寄与率 contribution_ratio = pca.explained_variance_ratio_ accumulation_ratio = np.cumsum(contribution_ratio) cc_ratio = np.hstack([0, accumulation_ratio]) plt.figure(figsize=[8,4]) plt.plot(accumulation_ratio, "-o") plt.xlabel("主成分") plt.ylabel("累積寄与率") plt.grid() plt.show() contribution_ratios = pd.DataFrame(pca.explained_variance_ratio_) contribution_ratios.round(decimals=2).astype('str').head() print(f"累積寄与率: {contribution_ratios[contribution_ratios.index<5].sum().round(decimals=2).astype('str').values}"); # 結果:累積寄与率: ['0.98'] 第3主成分までで寄与率は98%。クラスター数=3とする。 # クラスタリング(k-means) from sklearn.preprocessing import StandardScaler def clustering(df, num): sc = StandardScaler() sc.fit_transform(df) data_norm = sc.transform(df) cls = KMeans(n_clusters = num) result = cls.fit(data_norm) pred = cls.fit_predict(data_norm) plt.figure(figsize=[8, 4]) sns.scatterplot(x=data_norm[:,0], y=data_norm[:,1], c=result.labels_) plt.scatter(result.cluster_centers_[:,0], result.cluster_centers_[:,1], s=250, marker='*', c='blue') plt.grid('darkgray') plt.show() # 実行 clustering(boston_df, 3) うまく分割できないため、主成分分析を行う。 ## クラスタリング(k-means)+主成分分析 from sklearn.decomposition import PCA def cross(df, num): df_cls = df.copy() sc = StandardScaler() clustering_sc = sc.fit_transform(df_cls) # n_clusters:クラスター数 kmeans = KMeans(n_clusters=num, random_state=42) clusters = kmeans.fit(clustering_sc) df_cls['cluster'] = clusters.labels_ x = clustering_sc # n_components:削減結果の次元数 pca = PCA(n_components=num) pca.fit(x) x_pca = pca.transform(x) pca_df = pd.DataFrame(x_pca) pca_df['cluster'] = df_cls['cluster'] for i in df_cls['cluster'].unique(): tmp = pca_df.loc[pca_df['cluster'] == i] plt.scatter(tmp[0], tmp[1]) plt.grid() plt.show() # 実行 cross(boston_df, 3) 今度はうまく分割することができた。 3つのクラスタと、その重心からの距離を算出する。 # 重心からの距離 def get_center_distance(df): num_cluster=3 # cluster数 clusters = KMeans(n_clusters=num_cluster, random_state = 42) clusters.fit(df) centers = clusters.cluster_centers_ columns = df.columns clust_features = pd.DataFrame(index = df.index) for i in range(len(centers)): clust_features['クラスタ' + str(i + 1) + 'との距離'] = (df[columns] - centers[i]).applymap(abs).apply(sum, axis = 1) return clust_features # 実行 clust_features = get_center_distance(boston_df) boston_df[clust_features.columns] = clust_features boston_df.head() オーバーサンプリング # SMOTE from imblearn.over_sampling import SMOTE column = 'CHAS' sm = SMOTE(random_state=42) X = boston_df.drop(columns=column, axis=1) y = boston_df[column] X_sample, Y_sample = sm.fit_resample(X, y) over_sampling = pd.DataFrame() over_sampling = X_sample over_sampling[column] = Y_sample value_counts = over_sampling[column].value_counts() df = pd.DataFrame() df['ラベル'] = value_counts.index df['件数'] = value_counts.values ratio=[] ratio.append((value_counts.values[0] / len(over_sampling[column]) * 100).round(decimals=2).astype('str')) ratio.append((value_counts.values[1] / len(over_sampling[column]) * 100).round(decimals=2).astype('str')) df['割合'] = [f'{ratio[0]}%', f'{ratio[1]}%'] print(f"全レコード数:{len(over_sampling[column])}") df # 全レコード数:916 「CHAS」に対するオーバーサンプリングは、ランダムフォレストに対しては効果がある。その際「重心からの距離」がない方が精度が良い。 ただし、線形モデルに対しては、大幅な精度の悪化を招く。一旦、不採用とする。 カウント・エンコーディング count_map = boston_df['CHAS'].value_counts().to_dict() count_map df = boston_df.copy() df['CHASカウント'] = df['CHAS'].map(count_map) df[['CHAS', 'CHASカウント']].head() df = df.drop(columns='CHAS', axis=1) sns.distplot(df['CHASカウント']) # 結果:{0: 458, 1: 35} CAHSは住宅価格帯が異なるため、影響があると思ったが、得に影響はなかった。 出現頻度を学習させ、大小を明確にすれば影響が出ると考えたが、むしろ精度が悪化した。 一旦、このカウント・エンコーディングを採用するかは保留とする。 再評価 # 実行 rfr_model, y_test, y_pred = learning(RFR(random_state=42), boston_df) 決定係数(R2) = 0.924 平均絶対誤差(MAE) = 1.761 平均二乗誤差(MSE) = 5.588 対数平均二乗誤差(MSLE) = 0.018 平均二乗平方根誤差(RMSE) = 2.364 対数平方平均二乗誤差(RMSLE) = 0.134 # 線形回帰(重回帰) from sklearn.linear_model import LinearRegression as LR rfr_model, y_test, y_pred = learning(LR(), boston_df) 決定係数(R2) = 0.811 平均絶対誤差(MAE) = 2.919 平均二乗誤差(MSE) = 13.928 対数平均二乗誤差(MSLE) = 0.048 平均二乗平方根誤差(RMSE) = 3.732 対数平方平均二乗誤差(RMSLE) = 0.219 # 線形モデル from sklearn.linear_model import Ridge rfr_model, y_test, y_pred = learning(Ridge(), boston_df) 決定係数(R2) = 0.81 平均絶対誤差(MAE) = 2.924 平均二乗誤差(MSE) = 13.977 対数平均二乗誤差(MSLE) = 0.048 平均二乗平方根誤差(RMSE) = 3.739 対数平方平均二乗誤差(RMSLE) = 0.219 # 線形モデル from sklearn.linear_model import Lasso rfr_model, y_test, y_pred = learning(Lasso(), boston_df) 決定係数(R2) = 0.745 平均絶対誤差(MAE) = 3.172 平均二乗誤差(MSE) = 18.792 対数平均二乗誤差(MSLE) = 0.042 平均二乗平方根誤差(RMSE) = 4.335 対数平方平均二乗誤差(RMSLE) = 0.204 # 線形回帰(重回帰) from sklearn.linear_model import ElasticNet rfr_model, y_test, y_pred = learning(ElasticNet(), boston_df) 決定係数(R2) = 0.759 平均絶対誤差(MAE) = 3.112 平均二乗誤差(MSE) = 17.745 対数平均二乗誤差(MSLE) = 0.04 平均二乗平方根誤差(RMSE) = 4.212 対数平方平均二乗誤差(RMSLE) = 0.201 全体的に精度の向上が見られる。 特徴選択 # 実行 imp_df = rfr_importance(boston_df) imp_df = imp_df.sort_values(by='重要度', ascending=False).reset_index().set_index("項目名") imp_df = imp_df.drop(columns="index", axis=1) imp_df # 実行 boost_importance(boston_df) # 実行 linear_importance(boston_df) explain_importance(boston_df) 結果を踏まえて再評価 features = boston_df.drop(columns=['INDUS', 'CRIM', 'level_0', 'index', 'クラスタ3との距離']).columns # 実行 rfr_model, y_test, y_pred = learning(RFR(random_state=42), boston_df[features]) 決定係数(R2) = 0.929 平均絶対誤差(MAE) = 1.712 平均二乗誤差(MSE) = 5.248 対数平均二乗誤差(MSLE) = 0.017 平均二乗平方根誤差(RMSE) = 2.291 対数平方平均二乗誤差(RMSLE) = 0.129 # 線形回帰(重回帰) from sklearn.linear_model import LinearRegression as LR rfr_model, y_test, y_pred = learning(LR(), boston_df[features]) 決定係数(R2) = 0.827 平均絶対誤差(MAE) = 2.803 平均二乗誤差(MSE) = 12.766 対数平均二乗誤差(MSLE) = 0.036 平均二乗平方根誤差(RMSE) = 3.573 対数平方平均二乗誤差(RMSLE) = 0.189 # 線形モデル from sklearn.linear_model import Ridge rfr_model, y_test, y_pred = learning(Ridge(), boston_df[features]) 決定係数(R2) = 0.827 平均絶対誤差(MAE) = 2.799 平均二乗誤差(MSE) = 12.736 対数平均二乗誤差(MSLE) = 0.036 平均二乗平方根誤差(RMSE) = 3.569 対数平方平均二乗誤差(RMSLE) = 0.189 # 線形モデル from sklearn.linear_model import Lasso rfr_model, y_test, y_pred = learning(Lasso(), boston_df[features]) 決定係数(R2) = 0.767 平均絶対誤差(MAE) = 3.124 平均二乗誤差(MSE) = 17.171 対数平均二乗誤差(MSLE) = 0.032 平均二乗平方根誤差(RMSE) = 4.144 対数平方平均二乗誤差(RMSLE) = 0.179 # 線形回帰(重回帰) from sklearn.linear_model import ElasticNet rfr_model, y_test, y_pred = learning(ElasticNet(), boston_df[features]) 決定係数(R2) = 0.773 平均絶対誤差(MAE) = 3.073 平均二乗誤差(MSE) = 16.698 対数平均二乗誤差(MSLE) = 0.031 平均二乗平方根誤差(RMSE) = 4.086 対数平方平均二乗誤差(RMSLE) = 0.176 新宿の例といい、犯罪率は住宅価格との関係はないのかもしれない。 CHASは「0⇆1」で住宅価格が異なっていたため、影響はあると思われたが、「CHAS」単体では影響があまりなかった。 まとめ 実際に様々な手法を試して見て、効果があったもの、なかったものがあった。特に外れ値はどこまでを外れ値とするのかが判断が付けられず、モデル構築時の評価も加味して決めていきたい。 また過学習かどうかをまだ評価できていないため、モデル構築時(前)に確認することにします。 最後に 他の記事はこちらでまとめています。是非ご参照ください。 解析結果 実装結果:GitHub/boston_regression_preprocessing.ipynb データセット:Boston House Prices-Advanced Regression Techniques 参考資料
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

機械学習(分類問題)の最適なモデルを選択したい!!

データの中身確認 機械学習の練習の際頻繁に使われるsklearnの乳癌のデータ(breast_cancer)。 まずはこのデータの中身がどうなっているか見ていきましょう。 dataset.py #sklearnからdatasetを読み込む from sklearn import datasets #datasetの中から乳癌のデータを取得する cancer = datasets.load_breast_cancer() #乳癌データのデータフレームを作成して上位5つのデータを表示する breast_cancer_df = pd.DataFrame(cancer.data, columns=cancer.feature_names) breast_cancer_df.head() こんな感じ mean radius mean texture mean perimeter mean area mean smoothness mean compactness mean concavity mean concave points mean symmetry mean fractal dimension radius error texture error perimeter error area error smoothness error compactness error concavity error concave points error symmetry error fractal dimension error worst radius worst texture worst perimeter worst area worst smoothness worst compactness worst concavity worst concave points worst symmetry worst fractal dimension 17.99 10.38 122.8 1001 0.11840 0.27760 0.3001 0.14710 0.2419 0.07871 1.0950 0.9053 8.589 153.40 0.006399 0.04904 0.05373 0.01587 0.03003 0.006193 25.38 17.33 184.6 2019 0.1622 0.6656 0.7119 0.2654 0.4601 0.11890 20.57 17.77 132.9 1326 0.08474 0.07864 0.0869 0.07017 0.1812 0.05667 0.5435 0.7339 3.398 74.08 0.005225 0.01308 0.01860 0.01340 0.01389 0.003532 24.99 23.41 158.8 1956 0.1238 0.1866 0.2416 0.1860 0.2750 0.08902 19.69 21.25 130.0 1203 0.10960 0.15990 0.1974 0.12790 0.2069 0.05999 0.7456 0.7869 4.585 94.03 0.006150 0.04006 0.03832 0.02058 0.02250 0.004571 23.57 25.53 152.5 1709 0.1444 0.4245 0.4504 0.2430 0.3613 0.08758 30個の特長量(説明変数)が準備されていますが、mean radius :平均半径、mean texture :テクスチャをグレースケールにした際の平均 のような内容になっています。 詳細は https://ensekitt.hatenablog.com/entry/2018/08/22/200000  ↑上記リンクを確認してみてください。 さて、それでは次に目的変数である乳がんの腫瘍が良性or悪性を判断するtargetの中身を確認してみましょう。 dataset.py breast_cancer_df_tgt = pd.DataFrame(cancer.target, columns=['target']) breast_cancer_df_tgt.head(3) target 0 0 0 頭から3つのデータだけ確認するとtargetが'0'しかありませんが、breast_cancer_df_tgt.head(20)で20番目のデータまで確認すると'1'もあることが見て取れます。 targetのデータですが、良性の腫瘍の場合は'0'、悪性の腫瘍の場合は'1'を表しています。 最適なモデルの選択 さて、ここからが本題です。 targetが良性か悪性か判断するための分類モデル(ロジスティック回帰? 決定木? サポートベクターマシン? K近傍法?)はさまざまありますが、どのモデルが最もこの問題を解決するのに適しているかみていきましょう! モデルを確認するためのコードはざっとこんな感じ。 model_decision.py #必要モデル作成に必要なライブラリのインポート #上からK近傍法,決定木,ロジスティック回帰,線形SVM,SVM from sklearn.neighbors import KNeighborsClassifier from sklearn.tree import DecisionTreeClassifier from sklearn.linear_model import LogisticRegression from sklearn.svm import LinearSVC from sklearn.svm import SVC #乳癌データの読み込み cancer = datasets.load_breast_cancer() #説明変数'X',目的変数'y'に定義 X = cancer.data y = cancer.target #訓練用データとテストデータを分割する X_train, X_test, y_train, y_test = train_test_split( cancer.data, cancer.target, stratify = cancer.target, random_state = 50) #StandardScalerを使用してデータを標準化 sc = StandardScaler() sc.fit(X_train) X_train_std = sc.transform(X_train) X_test_std = sc.transform(X_test) #複数のモデルの設定 models = {'knn' : KNeighborsClassifier(), 'tree' : DecisionTreeClassifier(random_state=0), 'logistic' : LogisticRegression(random_state=0), 'svc1' : LinearSVC(random_state=0), 'svc2' : SVC(random_state=0)} #スコアを持たせる空の辞書 scores = {} for model_name, model in models.items(): model.fit(X_train_std, y_train) scores[model_name, 'train'] = model.score(X_train_std, y_train) scores[model_name, 'test'] = model.score(X_test_std, y_test) print('{}:訓練データ正答率(train):{:.3f}'.format(model_name,model.score(X_train_std, y_train))) print('{}:テストデータ正答率(train):{:.3f}'.format(model_name,model.score(X_test_std, y_test))) さて、実行結果をみてみましょう。 knn:訓練データ正答率(train):0.969 knn:テストデータ正答率(train):0.972 tree:訓練データ正答率(train):1.000 tree:テストデータ正答率(train):0.944 logistic:訓練データ正答率(train):0.988 logistic:テストデータ正答率(train):0.986 svc1:訓練データ正答率(train):0.986 svc1:テストデータ正答率(train):0.979 svc2:訓練データ正答率(train):0.986 svc2:テストデータ正答率(train):0.979 print文では上記のように表示されました。 次にデータフレームを使用して結果を整理していきます。 model_decision.py pd.Series(scores).unstack().sort_values('train', ascending = False) test train tree 0.9440559440559441 1.0000000000000000 logistic 0.9860139860139860 0.9882629107981221 svn1 0.9790209790209791 0.9859154929577465 svn2 0.9790209790209791 0.9859154929577465 knn 0.9720279720279720 0.9694835680751174 この結果を確認して、最も今回のケースに適しているモデルは何か一概に決定することは難しいですが、testとtrainデータの正答率の差が最も低く、正答率がどちらとも高いロジスティック回帰が最も適しているのではないか。。。と判断することができそうですね。。。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

pythonのnumpyとxarrayにおけるdeep copyの作成法

はじめに python の numpy を使って ndarray をコピーする場合、そのやり方によってコピー元・先のオブジェクトが束縛される場合と別のオブジェクトとなる場合があり注意が必要。代入演算子ではオブジェクトを共有して同じメモリ領域を参照するためコピー先のオブジェクトとコピー元のオブジェクトに互いに影響される。コピー先のオブジェクトをコピー元のオブジェクトとリンクさせたくない場合は深いコピー(deep copy)を行う必要がある。 一方、xarray の copy ではデフォルトで深いコピーになる。 x が numpy の場合 Pythonの標準モジュールである copy をインポートし、copy.deepcopyを使う。 import copy y = copy.deepcopy(x) x が xarrayの場合 xarray.DataArray.copyはデフォルトで深いコピーになる。 y = x.copy() 参考HP
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

わかったつもりのχ二乗をもう一度ちゃんと理解する

統計において、母分散の区間を求めたい時などに出てくるχ二乗分布。 χ二乗検定などでクロス表を扱ったりと、ちらほら「χ二乗」には関わることがあります。 実用性において、「χ二乗分布の公式やt分布との関係性を熟知しておく必要がない」と 個人的な意見はありますが、それでも統計検定2級とかでちゃんと出てくる(そして、私は自宅で初見で解いて爆死した)ので、「しっかりと」χ二乗分布についてみていこうと思います。 標本分散とχ二乗分布 標本(不偏)分散 まずは標本(不偏)分散から。 母集団の推測を目的とするため、標本から分散を求めるには「不偏分散」を用いますが、参考書によっては当たり前のように「標本分散」と記載があったりします。 流派があるかもなんですが、ここでの標本から求める分散は以下で定義するものとします $$ \hat{σ} = \frac{1}{n-1} \sum_{i=1}^{n}(x_i - \bar{x})^2$$ この$\hat{σ^2}$は性質として、平均的に母分散$σ^2$と等しくなる(E[$\hat{σ}^2$] = $σ^2$ これを不偏性と言います)わけですが、もちろん数回だけではバラつきます 修正χ二乗 まず、母分散を求めたいとか区間推定したい、という話の前に、母分散$σ^2$と$\hat{σ^2}$を使って、 $$C^2 = \frac{\hat{σ^2}}{σ^2}$$ を考えます。 この$C^2$というのは修正χ二乗と言われて、平均的には1に落ち着きます (なぜなら不偏分散の期待値は$σ^2$なので) しかし、いつも1ではなく、当然1よりも大きな値も小さい値(あくまで非負の範囲)も取りうることになり、これは一般的に自由度(今回ならn-1)により、グラフが変わってきます。 (自分が不慣れにkeynoteで作ったグラフなので、精緻なグラフでもなんでもないですw あくまでイメージ) しかし、この$C^2$はおそらく普通の参考書とかに出てこないです。 数値として0以上でかつ1のとき完全一致するというわかりやすいんですが、基本的には$χ^2$が使われます χ二乗 今、母集団(N(μ, $σ^2$))において、n個のサンプル($x_1, x_2,,, x_n$)とすると、$χ^2$は $$χ^2 = \sum_{i-1}^{n}(\frac{x_i - μ}{σ})^2$$ で表現されます。(μ、σはそれぞれ母数) これだけ書いても「あ、そーなんですね!」で会話が終わらせられるので、噛み砕いていうと、 「サンプルから平均を引いて標準偏差で割ったものであり、これは標準化そのもの。 つまり、これはN(0, 1)のn個の独立な要素の平方和は$χ^2$分布をする」 って感じです。 ちなみに、母数を使ったこの$χ^2$分布の自由度はnです(xiのどれもμで表したりできないので、そのままn) この母平均μを標本平均$\bar{x}$に置き換えると、 $$χ^2 = \sum_{i-1}^{n}(\frac{x_i - \bar{x}}{σ})^2 = \frac{(n-1)\hat{σ}^2}{σ^2} = (n-1)C^2$$ となり、一般的な書籍と同じ形になりました。 この$χ^2$分布に関しては、自由度はn-1となります (たとえば、$x_nはx_1,・・・, x_{n-1}と\bar{x}$で表現できるので、n-1) つまり、母数を使う場合と標本平均を使う場合とでは、自由度の違う$χ^2$分布になるということです。 χ二乗の性質 確率変数X~$χ^2(n)$のとき、 - 期待値(平均値)は$E[X] = n $ - 分散は$V[X] = 2n$ という性質があります (導出はχ二乗の確率密度関数の積分とかなので、割愛。 さすがにχ二乗の確率密度関数は覚えるものではないw) 加法性 χ二乗は分散との共通性質もあり、例えば、$χ_1^2: 自由度n_1, χ_2^2: 自由度n_2$であるとき、 $χ^2 = χ_1^2 + χ_2^2の自由度はn_1+n_2$ となります(3つ以上でもOK,χ_i^2は独立) t分布との関係性 χ二乗もt分布も標本から計算される数値を扱っているため、共通点を持っています。 実際導出まではしないのですが、一般的にt分布と自由度nのχ^2変数を用いて、以下のような等式が成り立つことが知られています。 $$t(n) = \frac{N(0, 1)}{\sqrt{\frac{χ^2(n)}{n}}}$$ (導出はt分布の統計量、標準正規分布の統計量を用いれば、上記で扱ってきたχ^2の組み合わせで算出できます) Pythonでχ二乗をプロット 我々は手計算でχ二乗をゴリゴリ計算することはほとんどないので、pythonをつかってχ二乗を扱ってみます。 まずば確率密度関数を見ます import numpy as np import matplotlib.pyplot as plt from scipy import stats x = np.linspace(0, 20, 100) for df in range(1, 20, 3): plt.plot(x, stats.chi2.pdf(x, df), label=f'degree of freedom: {df}') plt.plot(x, stats.chi2.pdf(x, 100), label='degree of freedom: 100') plt.legend() plt.show() 検定をしたい場合はchi2_contigencyメソッドとかがありますが、今回は検定の話ではないので、省略 今回参考にさせていただいた記事や書籍 2021年6月統計検定2級の問題の解説(その3) 基本統計学 22-1. カイ二乗分布
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【スクレイピングツール】scrape.doの使い方

はじめに 過去にスクレイピング(PythonでBeautifulSoup)で収集した情報をブログに投稿するツールを作成したことがあります。 現在はスクレイピングのwebサービスもあり、スクレイピング導入における敷居も低くなったのかな?、という疑問からその代表格?であるscrape.doを実際に触った結果をまとめていきます。 scrape.doのポイント 高度なカスタマイズを可能とするWebスクレイピングAPI SPAサイトもスクレイピング可能 リクエストごとにIPアドレスをローテーション JavaScriptをレンダリングし、CAPTCHAを処理可能 ターゲットWebサイトへの失敗したリクエストを自動的に再試行 リクエストごとに2MBの応答サイズ制限があります scrape.doの無料枠 サインアップすると、毎月1000のAPIリクエストを含む無料枠がありますので、安心して試すことができます。 ※無料枠利用において、クレジットカードの登録が不要なのはGood! アカウント登録 私はGoogleのアカウントで登録しました。 登録は簡単でGood! ※ログイン後の画面イメージ 利用していきます! 今回使用するツールなどのバージョン Windows 10 Pro:21H1 vscode:1.61.2 Docker:20.10.8 Python: 3.8.12 環境構築 詳細なソースはGitHub イメージの作成 docker build -t scrape_do:1.0 /mnt/c/wsl/docker/dev/scrape.do-python-sample/.devcontainer --no-cache コンテナの作成 docker run -v /mnt/c/wsl/docker/dev/scrape.do-python-sample:/opt/python/scrape_do --name scrape_do_01 -i -t scrape_do:1.0 /bin/bash スクレイピングAPIの実行 scrape.doのAPIレスポンスであるhtml情報を出力 scrape.doのAPIレスポンスのhtml情報からBeautifulSoupオブジェクトを作成して簡易的なスクレイピング結果を出力 httpのサイトはアクセスできない? ターゲットのURLが「http:XX」の場合、「502 Bad Gateway」となりました。 httpsのサイトのみが対象のようなので、httpのサイトにアクセスしないといけない要件の場合は残念ながら利用不可かなと。 ※回避策あればコメントいただきたいです! 利用した感想 利用しやすさ 細かい機能は有料オプションじゃないと利用できないものが多くて試せなかったのですが、APIの実行なので総じて利用は楽でした! 料金 スクレイピング処理の実行回数が少なく無料枠(1000回/月)内で利用可能あれば、scrape.doを利用したほうがIP自動変更もしてくれるのでオススメです。 逆に実行回数が多い場合は、bright dataを利用してIP自動変更をしつつリクエスト処理をしたほうが安くつくかなと。 しかし、Seleniumの代替となるJavascriptレンダリング機能は有料となるため、料金を意識するなら自前で作成したほうがよいかなと。 今後 bright dataとScrapyを利用して、スクレイピングをしていきたいです。 まずは10分で理解する Scrapyを見て進めてみます。 参考資料 参考とさせていただきました。ありがとうございます。 VSCodeでWSL2上のDockerコンテナ内コードをデバッグ 10分で理解する Beautiful Soup
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Python】for文で辞書(dictionary)繰り返し処理

辞書また、辞書型変数(dictionary)はkey-valueで格納されるデータです。 自分の定義した辞書 myDict = {'John' : 180, 'Naomi': 156, 'Terry': 177, 'Stella': 160} 1. キー(key)だけ取る 1.1 デフォルトでアクセス for key in myDict: print(key) """ 結果: John Naomi Terry Stella """ 1.2 dictionary.keys()でアクセス for key in myDict.keys(): print(key) """ 結果: John Naomi Terry Stella """ 2. 値(value)だけ取る for value in myDict.values(): print(value) """ 結果: 180 156 177 160 """ 3. キー、値(key, value)両方とも取る 3.1 デフォルトでアクセス(1.1と一緒) for key in myDict: print(key, myDict[key]) """ 結果: John 180 Naomi 156 Terry 177 Stella 160 """ 3.2 dictionary.items()でアクセス for key, value in myDict.items(): print(key, value) """ 結果: John 180 Naomi 156 Terry 177 Stella 160 """ 以上、簡単にメモしました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

データ分析

この記事の狙い・目的 機械学習を取り入れたAIシステムの構築は、 ①データ分析→ ②データセット作成(前処理)→ ③モデルの構築・適用 というプロセスで行っていきます。 その際「データ分析」の段階では、正しく前処理が行えるよう、事前にデータを分析を行い、データの構造、データの傾向を解析し、その後の方針を決定していることが求められます。 このブログでは、「データ分析」の工程について初めから通して解説していきます。 プログラムの実行環境 Python3 MacBook pro(端末) PyCharm(IDE) Jupyter Notebook(Chrome) Google スライド(Chrome) 対象データの確認 # データ取得 boston_df = pd.read_csv("./boston.csv", sep=',') # データ確認 boston_df.shape boston_df.head().astype(str) boston_df.info() boston_df.describe().astype(str) # 地図で確認 from IPython.display import Image Image("./boston_area.png") Image("./boston_river.png") 属性 説明変数 CRIM: 町ごとの一人当たりの犯罪率 ZN: 25,000平方フィートを超える区画にゾーニングされた住宅用地の割合。 INDUS: 町ごとの非小売ビジネスエーカーの割合 CHAS:(int) チャールズリバーダミー変数(路が川に接している場合は1、それ以外の場合は0) NOX: 一酸化窒素濃度(1000万あたりのパーツ)[パーツ/10M] RM: 住居あたりの平均部屋数 AGE: 1940年以前に建設された持ち家の割合 DIS: ボストンの5つの雇用センターまでの加重距離 RAD:(int) 放射状高速道路へのアクセスの指標 TAX: 全額-価値資産、10000ドルあたりの税率($/10k) PTRATIO: 町ごとの生徒と教師の比率 B: 方程式B = 1000(Bk-0.63)^2の結果。ここで、Bkは町ごとの黒人の割合です。 LSTAT: 人口の%低いステータス 目的変数 MEDV: 持ち家の中央値(1000ドル単位) データ型、尺度 質的データ 名義尺度:CHAS(2値) 順序尺度:なし 量的データ 間隔尺度:RM、DIS、RAD 比例尺度:CRIM、ZN、INDUS、NOX、AGE、TAX、PTRATIO、B、MEDV 目的変数の確認 import warnings warnings.filterwarnings('ignore') # 正規分布を重ねて描画する from scipy.stats import norm plt.figure(figsize=[8,3]) sns.distplot(boston_df['MEDV'], kde=True, fit=norm, fit_kws={'label': '正規分布'}).grid() plt.legend() # QQ Plot plt.figure(figsize=[8,3]) plt.grid() stats.probplot(boston_df['MEDV'], dist="norm", plot=plt) # コルモゴロフ・スミルノフ検定(K-S検定) from scipy import stats stats.kstest(boston_df['MEDV'], 'norm') 帰無仮説=正規分布と一致している 対立仮説=正規分布と一致していない p値=0.0(四捨五入した値)のため、帰無仮説が棄却される。(正規分布と一致していない) 線形モデルの場合は、正規分布に変換した方が良さそう。 欠損値の確認 boston_df.isnull().sum() 欠損値は含まれない。 外れ値の確認 # 箱ひげ図 def hige_graph(cols): fig, ax = plt.subplots() data_ = [] for col in cols: data_.append(boston_df[col]) ax.set_title('箱ひげ図') ax.boxplot(data_, labels=cols) plt.grid() plt.show() # 全項目 all_col = boston_df.columns hige_graph(cols=all_col) plt.grid() sns.countplot(boston_df["ZN"]) plt.figure(figsize=[10,3]) sns.boxplot(data=boston_df, x='CRIM').grid() plt.figure(figsize=[10,3]) sns.boxplot(data=boston_df, x='B').grid() sns.lmplot(x='B',y='CRIM',data=boston_df,aspect=5,height=5) sns.lmplot(x='CRIM',y='B',data=boston_df,aspect=5,height=5) CRIM 外れ値(80)を含む。犯罪率80%は疑わしい値。線形モデルを使用する場合はこの外れ値を除去する。 B 何%で切るか難しいところだが、0.1〜10%台は全体の1.5%。除外しても良いかもしれない。 単・多変量データ解析 単変量データ解析 boston_df.hist(bins=10, figsize=(20,15)) plt.figure(figsize=[20,5]) sns.distplot(boston_df['CRIM']).grid() plt.figure(figsize=[20,5]) sns.distplot(boston_df['B'], bins=100).grid() plt.figure(figsize=[8,3]) sns.distplot(boston_df[boston_df['B']<200], bins=10).grid() def sns_distplot(col_name, bins): import warnings warnings.filterwarnings('ignore') plt.figure(figsize=[8,3]) sns.distplot(boston_df[col_name], bins=bins).grid() sns_distplot(col_name='INDUS', bins=10) sns_distplot(col_name='CHAS', bins=10) sns_distplot(col_name='NOX', bins=10) sns_distplot(col_name='RM', bins=10) sns_distplot(col_name='AGE', bins=10) sns_distplot(col_name='DIS', bins=10) sns_distplot(col_name='RAD', bins=10) sns.countplot(boston_df["RAD"]).grid() sns_distplot(col_name='TAX', bins=10) sns_distplot(col_name='PTRATIO', bins=10) sns_distplot(col_name='LSTAT', bins=10) NOX, RM, DIS, PTRATIO, LSTAT, MEDV モデリングに線形モデルを用いる場合、正規分布に変換(近づける)する処理を施す。 多変量データ解析 相関分析 # 散布図行列 pd.plotting.scatter_matrix(boston_df, c='blue', figsize=(20, 20)) # ヒートマップ1 plt.figure(figsize=(7, 7)) sns.heatmap(pd.DataFrame(boston_df).corr(method='pearson'), annot=False, vmin=-1, vmax=1, cmap='bwr', cbar=True) # ヒートマップ2 plt.figure(figsize=(7, 7)) sns.heatmap(pd.DataFrame(boston_df).corr(method='spearman'), annot=False, vmin=-1, vmax=1, cmap='bwr', cbar=True) 非線形で相関のありそうな変数 CRIM: INDUS, NOX, RM, RAD, TAX INDUS: NOX, DIS NOX: AGE, DIS AGE: DIS RAD: TAX MEDV: LSTAT 相関の弱い変数 CHAS: 単体では不要そう # 目的変数との関係性を確認 def target_relation(tgt, data): y_train = data[tgt] # ヒートマップの表示数 k = len(data.columns) fig = plt.figure(figsize=(20,20)) # 各変数間の相関係数 corrmat = data.corr() # リストの最大値から順にk個の要素を取得 cols = corrmat.nlargest(k, tgt)[tgt].index # 全て可視化 for i in np.arange(1, k): X_train = data[cols[i]] ax = fig.add_subplot(4,4, i) sns.regplot(x=X_train, y=y_train) plt.tight_layout() plt.show() sns.jointplot(x='MEDV', y='RM', data=boston_df) sns.jointplot(x='MEDV', y='LSTAT', data=boston_df) RM, LSTATは目的変数と線形性が関係がある。 fig = px.box(boston_df, x="CHAS", y="MEDV", color="CHAS", width=600, height=400) fig.show() 川に面している住宅(CAHS=1)価格の方が、少し高い。 CAHS=0で高級住宅は、外れ値となっている。 fig = px.box(boston_df, x="RAD", y="MEDV", color="RAD") fig.show() 低価格帯は、RAD=24しかカバーできていない。 fig = px.scatter (boston_df, x = "MEDV", y = "RM", color = "CHAS", trendline="ols") fig.show() fig = px.scatter(boston_df, x = "MEDV", y = "RM", color = "RAD", trendline="ols") fig.show() CHAS=0はノイズを含んでいる。 fig = px.scatter (boston_df, x = "MEDV", y = "LSTAT", color = "CHAS", trendline="ols") fig.show() fig = px.scatter (boston_df, x = "MEDV", y = "LSTAT", color = "RAD", trendline="ols") fig.show() fig = px.scatter (boston_df, x = "MEDV", y = "LSTAT", color = "AGE", trendline="ols") fig.show() RAD=24は外れる傾向がある。 AGE=80代以上は外れる傾向がある。 割合の確認 # 積み立て棒グラフ def stack_graph(columns): tgt_df = pd.DataFrame() for i in range(24): tgt_df['RAD' + str(i+1)] = boston_df[boston_df['RAD'] == i+1][columns].mean() plt.figure(figsize=[20,5]) my_plot = tgt_df.T.plot(kind='bar', stacked=True, title="各クラスタの平均") my_plot.set_xticklabels(my_plot.xaxis.get_majorticklabels(), rotation=0) plt.grid() columns = ['RAD', 'MEDV', 'CRIM', 'DIS'] stack_graph(columns) columns = ['RAD', 'MEDV', 'AGE'] stack_graph(columns) RAD1~8は傾向が似ているが、各要素の割合は若干異なる。 RAD24は要素の割合が大きく異なる。 まとめ 今回はボストンの住宅価格データを用いてデータの解析を行なってきました。 項目数はあまり多くはありませんが、その中で目的変数と関係性の高そうな項目、他の項目と組み合わせて影響のありそうな項目、関係性の低い効かなそうな項目が見えてきました。 この考察を元に、次回前処理を行なっていきます。 最後に 他の記事はこちらでまとめています。是非ご参照ください。 解析結果 実装結果:GitHub/boston_regression_analytics.ipynb データセット:Boston House Prices-Advanced Regression Techniques 参考資料
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PytorchのDataLoaderのshuffleについて

DataLoaderについて PytorchのDataLoaderはよく機械学習モデルで使用されています。これを用いることで,エポック毎に異なる組み合わせや順番で ミニバッチ学習を行うことができます。しかし,それには再現性があるのでしょうか。今まであまり確認していなかったので確認してみることにします。 (また誤りや質問などありましたらコメントお願いいたします。 追記 : GPUなどを用いた実際の深層学習においては, こちらが参考になると思います。 north_rewing様の記事『Pytorchの再現性に関して』 makaishi2様の記事『PyTorch CNNモデル再現性問題』 を参照してみてください。) 深層学習における再現性(seed)の重要性に関して Takahiro Kuboさんのこちらのスライドには強化学習においてseedが異なるだけで汎化性能が大きく異なる論文を紹介されており, 汎化性能を担保したいモデルを運用したい場合はこの辺りの話は極めて重要となります。 結論 学習時のエポックのfor文が始まる前に seed=42 torch.manual_seed(seed) # for GPU # torch.cuda.manual_seed(seed) といった文言をつけましょう。 実験 import numpy as np import torch #サンプル x = np.array([[1], [2], [3], [4], [5], [6]]) y = np.array([[10], [20], [30], [40], [50], [60]]) x = torch.tensor(x) y = torch.tensor(y) dataset = torch.utils.data.TensorDataset(x, y) batch_size=3 data_loader1 = torch.utils.data.DataLoader(dataset, batch_size=batch_size, shuffle=True) data_loader2 = torch.utils.data.DataLoader(dataset, batch_size=batch_size, shuffle=True) print(f"--- dataloader{1}") for _ in range(2): print(f"{_}番目のエポック") for i,(x,y) in enumerate(data_loader1): print(f" {i+1}番目のバッチ : x:{x.tolist()}, y:{y.tolist()}") print(f"--- dataloader{2}") for _ in range(2): print(f"{_}番目のエポック") for i,(x,y) in enumerate(data_loader2): print(f" {i+1}番目のバッチ : x:{x.tolist()}, y:{y.tolist()}") --- dataloader1 0番目のエポック 1番目のバッチ : x:[[3], [2], [1]], y:[[30], [20], [10]] 2番目のバッチ : x:[[5], [4], [6]], y:[[50], [40], [60]] 1番目のエポック 1番目のバッチ : x:[[1], [2], [5]], y:[[10], [20], [50]] 2番目のバッチ : x:[[4], [3], [6]], y:[[40], [30], [60]] --- dataloader2 0番目のエポック 1番目のバッチ : x:[[6], [3], [2]], y:[[60], [30], [20]] 2番目のバッチ : x:[[5], [1], [4]], y:[[50], [10], [40]] 1番目のエポック 1番目のバッチ : x:[[1], [6], [4]], y:[[10], [60], [40]] 2番目のバッチ : x:[[3], [2], [5]], y:[[30], [20], [50]] dataloader1とdataloade2の結果が異なるため,再現性がありません...。shuffle=Falseではもちろん再現性がありますが,念の為,確かめてみましょう。 data_loader3 = torch.utils.data.DataLoader(dataset, batch_size=batch_size, shuffle=False) data_loader4 = torch.utils.data.DataLoader(dataset, batch_size=batch_size, shuffle=False) print(f"--- dataloader{3}") for _ in range(2): print(f"{_}番目のエポック") for i,(x,y) in enumerate(data_loader3): print(f" {i+1}番目のバッチ : x:{x.tolist()}, y:{y.tolist()}") print(f"--- dataloader{4}") for _ in range(2): print(f"{_}番目のエポック") for i,(x,y) in enumerate(data_loader4): print(f" {i+1}番目のバッチ : x:{x.tolist()}, y:{y.tolist()}") --- dataloader3 0番目のエポック 1番目のバッチ : x:[[1], [2], [3]], y:[[10], [20], [30]] 2番目のバッチ : x:[[4], [5], [6]], y:[[40], [50], [60]] 1番目のエポック 1番目のバッチ : x:[[1], [2], [3]], y:[[10], [20], [30]] 2番目のバッチ : x:[[4], [5], [6]], y:[[40], [50], [60]] --- dataloader4 0番目のエポック 1番目のバッチ : x:[[1], [2], [3]], y:[[10], [20], [30]] 2番目のバッチ : x:[[4], [5], [6]], y:[[40], [50], [60]] 1番目のエポック 1番目のバッチ : x:[[1], [2], [3]], y:[[10], [20], [30]] 2番目のバッチ : x:[[4], [5], [6]], y:[[40], [50], [60]] 再現性ありました。 ではsheffle=Trueにしつつ再現性を保つ i.e. dataloader1とdataloader2の結果を同じにするにはどのようにすれば良いのでしょうか。 試み1 dataloaderが定義されるときにseedを固定する. torch.manual_seed(42) data_loader_new1 = torch.utils.data.DataLoader(dataset, batch_size=batch_size, shuffle=True) torch.manual_seed(42) data_loader_new2 = torch.utils.data.DataLoader(dataset, batch_size=batch_size, shuffle=True) print(f"--- dataloader{1}") for _ in range(2): print(f"{_}番目のエポック") for i,(x,y) in enumerate(data_loader_new1): print(f" {i+1}番目のバッチ : x:{x.tolist()}, y:{y.tolist()}") print(f"--- dataloader{2}") for _ in range(2): print(f"{_}番目のエポック") for i,(x,y) in enumerate(data_loader_new2): print(f" {i+1}番目のバッチ : x:{x.tolist()}, y:{y.tolist()}") --- dataloader1 0番目のエポック 1番目のバッチ : x:[[5], [6], [2]], y:[[50], [60], [20]] 2番目のバッチ : x:[[1], [4], [3]], y:[[10], [40], [30]] 1番目のエポック 1番目のバッチ : x:[[5], [6], [2]], y:[[50], [60], [20]] 2番目のバッチ : x:[[3], [4], [1]], y:[[30], [40], [10]] --- dataloader2 0番目のエポック 1番目のバッチ : x:[[3], [2], [1]], y:[[30], [20], [10]] 2番目のバッチ : x:[[5], [4], [6]], y:[[50], [40], [60]] 1番目のエポック 1番目のバッチ : x:[[1], [2], [5]], y:[[10], [20], [50]] 2番目のバッチ : x:[[4], [3], [6]], y:[[40], [30], [60]] だめでした。 試み.2 dataloaderから取り出す際にseedを固定する. print(f"--- dataloader{1}") torch.manual_seed(42) for _ in range(2): print(f"{_}番目のエポック") for i,(x,y) in enumerate(data_loader1): print(f" {i+1}番目のバッチ : x:{x.tolist()}, y:{y.tolist()}") print(f"--- dataloader{2}") torch.manual_seed(42) for _ in range(2): print(f"{_}番目のエポック") for i,(x,y) in enumerate(data_loader2): print(f" {i+1}番目のバッチ : x:{x.tolist()}, y:{y.tolist()}") --- dataloader1 0番目のエポック 1番目のバッチ : x:[[5], [6], [2]], y:[[50], [60], [20]] 2番目のバッチ : x:[[1], [4], [3]], y:[[10], [40], [30]] 1番目のエポック 1番目のバッチ : x:[[5], [6], [2]], y:[[50], [60], [20]] 2番目のバッチ : x:[[3], [4], [1]], y:[[30], [40], [10]] --- dataloader2 0番目のエポック 1番目のバッチ : x:[[5], [6], [2]], y:[[50], [60], [20]] 2番目のバッチ : x:[[1], [4], [3]], y:[[10], [40], [30]] 1番目のエポック 1番目のバッチ : x:[[5], [6], [2]], y:[[50], [60], [20]] 2番目のバッチ : x:[[3], [4], [1]], y:[[30], [40], [10]] このままでは,全てのエポックで同じ組み合わせを持ってきているため,shuffleしている感じを確認できません。現状何とも言えないため,エポック数を増やしてみましょう。 print(f"--- dataloader{1}") torch.manual_seed(42) for _ in range(5): print(f"{_}番目のエポック") for i,(x,y) in enumerate(data_loader1): print(f" {i+1}番目のバッチ : x:{x.tolist()}, y:{y.tolist()}") print(f"--- dataloader{2}") torch.manual_seed(42) for _ in range(5): print(f"{_}番目のエポック") for i,(x,y) in enumerate(data_loader2): print(f" {i+1}番目のバッチ : x:{x.tolist()}, y:{y.tolist()}") --- dataloader1 0番目のエポック 1番目のバッチ : x:[[5], [6], [2]], y:[[50], [60], [20]] 2番目のバッチ : x:[[1], [4], [3]], y:[[10], [40], [30]] 1番目のエポック 1番目のバッチ : x:[[5], [6], [2]], y:[[50], [60], [20]] 2番目のバッチ : x:[[3], [4], [1]], y:[[30], [40], [10]] 2番目のエポック 1番目のバッチ : x:[[3], [2], [1]], y:[[30], [20], [10]] 2番目のバッチ : x:[[5], [4], [6]], y:[[50], [40], [60]] 3番目のエポック 1番目のバッチ : x:[[1], [2], [5]], y:[[10], [20], [50]] 2番目のバッチ : x:[[4], [3], [6]], y:[[40], [30], [60]] 4番目のエポック 1番目のバッチ : x:[[6], [3], [2]], y:[[60], [30], [20]] 2番目のバッチ : x:[[5], [1], [4]], y:[[50], [10], [40]] --- dataloader2 0番目のエポック 1番目のバッチ : x:[[5], [6], [2]], y:[[50], [60], [20]] 2番目のバッチ : x:[[1], [4], [3]], y:[[10], [40], [30]] 1番目のエポック 1番目のバッチ : x:[[5], [6], [2]], y:[[50], [60], [20]] 2番目のバッチ : x:[[3], [4], [1]], y:[[30], [40], [10]] 2番目のエポック 1番目のバッチ : x:[[3], [2], [1]], y:[[30], [20], [10]] 2番目のバッチ : x:[[5], [4], [6]], y:[[50], [40], [60]] 3番目のエポック 1番目のバッチ : x:[[1], [2], [5]], y:[[10], [20], [50]] 2番目のバッチ : x:[[4], [3], [6]], y:[[40], [30], [60]] 4番目のエポック 1番目のバッチ : x:[[6], [3], [2]], y:[[60], [30], [20]] 2番目のバッチ : x:[[5], [1], [4]], y:[[50], [10], [40]] これで確かに,再現性が保てることがわかりました。 この辺りを含めdropoutや深層学習の初期値に関してもseedを気にする必要があります。 今後機会があればこの辺りもまとめてみたいと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Pandas入門

・Pandas入門 今現在、プロジェクトでPythonのライブラリ「Pandas」を使っているので、覚書も含め纏めたいと思います。 不備などありましたら、ご指摘をお願い致します。 ・Pandasとは Pandasとはデータ解析、操作を迅速で快適かつ簡単に行なうことができるPythonで作られたオープンソースライブラリになります。 開発者であるWes McKinneyは、財務データを定量分析するための高性能で柔軟なツールを欲しており、AQR Capital Managementにて2008年にpandasの開発を開始した。 AQRを去る前に上司を説得し、ライブラリの一般公開が可能となった。 別のAQR従業員であるChang Sheは、2012年からこのライブラリの2番目の主要コントリビューターとなった。同時期に、Pythonコミュニティでライブラリが普及し、さらに多くのコントリビューターがプロジェクトに加わることになった。 2015年、pandasはNumFOCUS(アメリカ合衆国における501(c)(3)非営利慈善団体)の財政出資プロジェクトとして署名した。 ・Pandasのインストール pip install pandas ・使い方 Pandasを利用するには通例、以下のようにPandasライブラリを読み込みます。 import pandas as pd ・Pandasのデータ構造 Pandasでは1次元配列のSeries、2次元配列のDataframe、3次元配列のPanelが用意されています。 今回はよく利用するSeries、Dataframeについてまとめていきます。 ・Series生成 Seriesの生成は以下のようになります。 pandas.Series(data=None, index=None, dtype=None, name=None, copy=False, fastpath=False) 各パラメータの説明 引数 内容 ・data データ:配列相当のもの、ディクショナリ、数値データがディクショナリの場合には引数の順序は維持されます。 ・index データ:配列相当のもの、または1次元配列のインデックス。デフォルトとしてRangeIndexが0から1, 2, …, nの順番で設定される。ディクショナリ相当のデータでインデックスが指定されていない場合、キーがインデックスとして使用される。 ・dtype データ:str型、numpy.dtype、ExtensionDtype出力されるSeriesのデータ型を指定したい場合に設定。Noneの場合は型推論で指定される。 ・name データ:str型Seriesの列名を設定 ・copy データ:bool型 デフォルト:False入力されたデータの参照を利用するか、コピーを利用するかを指定。 pd.Series(["name1","name2","name3"]) index col1 0 name1 1 name2 2 name3 dtype: object ・DataFrame生成 DataFrameの生成は以下のようになります。 pandas.DataFrame(data=None, index=None, columns=None, dtype=None, copy=None) 各パラメータの説明 引数 内容 ・data データ:ndarray、Iterable、ディクショナリ、DataFrameディクショナリにはSeries、配列、定数、dataclass、リスト相当のオブジェクトを含めれます。データがディクショナリの場合、列の順番は挿入の順番に準拠します。 ・index データ:インデックスまたは配列相当のものインデックスを配列などで指定。インデックスを指定していない場合は、デフォルトとしてRangeIndexが0から1, 2, …, nの順番で設定される。 ・columns データ:インデックスまたは配列相当のものデータにカラム名がない場合、結果にカラム名を設定。デフォルトとしてRangeIndexが0から1, 2, …, nの順番で設定される。 ・dtype データ:dtype デフォルト None出力されるDataframeのデータ型を指定したい場合に設定。Noneの場合は型推論で指定される。 ・copy データ:bool型 デフォルト:None入力データのコピー。ディクショナリのデフォルトの設定はNoneでcopy=Trueの振る舞いをする。データフレーム、二次元配列のデータのデフォルトの設定はNoneでcopy=Falseの振る舞いをする。version 1.3.0.で変更されているようです。 data_list = [["name1", "address1", "M", pd.Timestamp("20000101"), 170.0, 60.0],["name2", "address2", "F", pd.Timestamp("20000102"), 160.0, 50.0],["name3", "address3", "M", pd.Timestamp("20000103"), 180.0, 70.0]] df = pd.DataFrame(data_list, columns=["col1", "col2", "col3", "col4", "col5", "col6"]) index col1 col2 col3 col4 col5 col6 0 name1 address1 M 2000-01-01 170.0 60.0 1 name2 address2 F 2000-01-02 160.0 50.0 2 name3 address3 M 2000-01-03 180.0 70.0 ・列データの追加 new_data_list = [1,2,1] df["col7"] = new_data_list index col1 col2 col3 col4 col5 col6 col7 0 name1 address1 M 2000-01-01 170.0 60.0 1 1 name2 address2 F 2000-01-02 160.0 50.0 2 2 name3 address3 M 2000-01-03 180.0 70.0 1 要素の選択 列を指定した取得方法(二つは同一結果) df["col1"] index col1 0 name1 1 name2 2 name3 Name: col1, dtype: object df.col1 index col1 0 name1 1 name2 2 name3 Name: col1, dtype: object 行を指定した取得方法 df[0:2] index col1 col2 col3 col4 col5 col6 0 name1 address1 M 2000-01-01 170.0 60.0 1 name2 address2 F 2000-01-02 160.0 50.0 数値で位置を指定した行の取得方法 df.iloc[2] Name Value col1 name3 col2 address3 col3 M col4 2000-01-0300:00:00 col5 180 col6 70 Name: 2, dtype: object 行および列を数値位置で指定しての取得方法。 df.iloc[[0, 2], [0, 2]] index col1 col3 0 name1 M 2 name3 M 行を指定しての取得方法 df.iloc[[0,2], :] index col1 col2 col3 col4 col5 col6 0 name1 address1 M 2000-01-01 170.0 60.0 2 name3 address3 M 2000-01-03 180.0 70.0 Boolean indexing df[df["col5"] > 160] index col1 col2 col3 col4 col5 col6 0 name1 address1 M 2000-01-01 170.0 60.0 df[df["col6"].isin([50.0, 70.0])] index col1 col2 col3 col4 col5 col6 1 name2 address2 F 2000-01-02 160.0 50.0 2 name3 address3 M 2000-01-03 180.0 70.0 ・グループ化 データ data_list = [["name1", "address1", "M", pd.Timestamp("20000101"), 170.0, 60.0,1],["name2", "address2", "F", pd.Timestamp("20000102"), 160.0, 50.0,2],["name3", "address3", "M", pd.Timestamp("20000103"), 180.0, 70.0,1],["name4", "address4", "F", pd.Timestamp("20000104"), 150.0, 40.0,1],["name5", "address5", "M", pd.Timestamp("20000105"), 190.0, 80.0,2],["name6", "address6", "M", pd.Timestamp("20000106"), 200.0, 90.0,1],["name7", "address7", "F", pd.Timestamp("20000107"), 140.0, 40.0,1],["name8", "address8", "F", pd.Timestamp("20000108"), 150.0, 50.0,2],["name9", "address9", "F", pd.Timestamp("20000109"), 170.0, 60.0,1],["name10", "address10", "M", pd.Timestamp("20000110"), 170.0, 60.0,2]] df = pd.DataFrame(data_list, columns=["col1", "col2", "col3", "col4", "col5", "col6", "col7"]) index col1 col2 col3 col4 col5 col6 col7 0 name1 address1 M 2000-01-01 170.0 60.0 1 1 name2 address2 F 2000-01-02 160.0 50.0 2 2 name3 address3 M 2000-01-03 180.0 70.0 1 3 name4 address4 F 2000-01-04 150.0 40.0 1 4 name5 address5 M 2000-01-05 190.0 80.0 2 5 name6 address6 M 2000-01-06 200.0 90.0 1 6 name7 address7 F 2000-01-07 140.0 40.0 1 7 name8 address8 F 2000-01-08 150.0 50.0 2 8 name9 address9 F 2000-01-09 170.0 60.0 1 9 name10 address10 M 2000-01-10 170.0 60.0 2 列を指定してグループ化した結果を取得する方法。 gp_df = df.groupby("col3") df.loc[gp_df['col5'].idxmax(),:] index col1 col2 col3 col4 col5 col6 col7 8 name9 address9 F 2000-01-09 170.0 60.0 1 5 name6 address6 M 2000-01-06 200.0 90.0 1 複数列を指定してグループ化した結果を取得する方法。 gp_df = df.groupby(["col3","col7"]) df.loc[gp_df['col5'].idxmax(),:] index col1 col2 col3 col4 col5 col6 col7 8 name9 address9 F 2000-01-09 170.0 60.0 1 1 name2 address2 F 2000-01-02 160.0 50.0 2 5 name6 address6 M 2000-01-06 200.0 90.0 1 4 name5 address5 M 2000-01-05 190.0 80.0 2 ・マージ処理 concat 縦方向に単純に結合する方法。 データ1 data_list = [["101","name1", "address1", "M", pd.Timestamp("20000101"), 170.0, 60.0,1],["102","name2", "address2", "F", pd.Timestamp("20000102"), 160.0, 50.0,2],["103","name3", "address3", "M", pd.Timestamp("20000103"), 180.0, 70.0,1],["104","name4", "address4", "F", pd.Timestamp("20000104"), 150.0, 40.0,1],["105","name5", "address5", "M", pd.Timestamp("20000105"), 190.0, 80.0,2]] df = pd.DataFrame(data_list, columns=["ID","col1", "col2", "col3", "col4", "col5", "col6", "col7"]) index ID col1 col2 col3 col4 col5 col6 col7 0 101 name1 address1 M 2000-01-01 170.0 60.0 1 1 102 name2 address2 F 2000-01-02 160.0 50.0 2 2 103 name3 address3 M 2000-01-03 180.0 70.0 1 3 104 name4 address4 F 2000-01-04 150.0 40.0 1 4 105 name5 address5 M 2000-01-05 190.0 80.0 2 データ2 data_list2 = [["106","name6", "address6", "M", pd.Timestamp("20000106"), 200.0, 90.0,1],["107","name7", "address7", "F", pd.Timestamp("20000107"), 140.0, 40.0,1],["108","name8", "address8", "F", pd.Timestamp("20000108"), 150.0, 50.0,2],["109","name9", "address9", "F", pd.Timestamp("20000109"), 170.0, 60.0,1],["110","name10", "address10", "M", pd.Timestamp("20000110"), 170.0, 60.0,2]] df2 = pd.DataFrame(data_list2, columns=["ID","col1", "col2", "col3", "col4", "col5", "col6", "col7"]) index ID col1 col2 col3 col4 col5 col6 col7 0 106 name6 address6 M 2000-01-06 200.0 90.0 1 1 107 name7 address7 F 2000-01-07 140.0 40.0 1 2 108 name8 address8 F 2000-01-08 150.0 50.0 2 3 109 name9 address9 F 2000-01-09 170.0 60.0 1 4 110 name10 address10 M 2000-01-10 170.0 60.0 2 結合結果 pd.concat([df,df2],ignore_index=True) index ID col1 col2 col3 col4 col5 col6 col7 0 101 name1 address1 M 2000-01-01 170.0 60.0 1 1 102 name2 address2 F 2000-01-02 160.0 50.0 2 2 103 name3 address3 M 2000-01-03 180.0 70.0 1 3 104 name4 address4 F 2000-01-04 150.0 40.0 1 4 105 name5 address5 M 2000-01-05 190.0 80.0 2 5 106 name6 address6 M 2000-01-06 200.0 90.0 1 6 107 name7 address7 F 2000-01-07 140.0 40.0 1 7 108 name8 address8 F 2000-01-08 150.0 50.0 2 8 109 name9 address9 F 2000-01-09 170.0 60.0 1 9 110 name10 address10 M 2000-01-10 170.0 60.0 2 merge 横方向に結合する方法。 データ3 data_list = [["101","name1", "address1", "M", pd.Timestamp("20000101"), 170.0, 60.0,1],["102","name2", "address2", "F", pd.Timestamp("20000102"), 160.0, 50.0,2],["104","name4", "address4", "F", pd.Timestamp("20000104"), 150.0, 40.0,1],["105","name5", "address5", "M", pd.Timestamp("20000105"), 190.0, 80.0,2],["106","name6", "address6", "M", pd.Timestamp("20000106"), 200.0, 90.0,1],["107","name7", "address7", "F", pd.Timestamp("20000107"), 140.0, 40.0,1],["110","name10", "address10", "M", pd.Timestamp("20000110"), 170.0, 60.0,2]] df = pd.DataFrame(data_list, columns=["ID","col1", "col2", "col3", "col4", "col5", "col6", "col7"]) index ID col1 col2 col3 col4 col5 col6 col7 0 101 name1 address1 M 2000-01-01 170.0 60.0 1 1 102 name2 address2 F 2000-01-02 160.0 50.0 2 2 104 name4 address4 F 2000-01-04 150.0 40.0 1 3 105 name5 address5 M 2000-01-05 190.0 80.0 2 4 106 name6 address6 M 2000-01-06 200.0 90.0 1 5 107 name7 address7 F 2000-01-07 140.0 40.0 1 6 110 name10 address10 M 2000-01-10 170.0 60.0 2 データ4 data_list2 = [["101",100,10,50],["102",90,20,60],["103",80,30,70],["105",60,50,90],["106",50,60,100],["108",30,80,20],["109",20,90,30]] df2 = pd.DataFrame(data_list2, columns=["ID","science","english","math"]) index ID science english math 0 101 100 10 50 1 102 90 20 60 2 103 80 30 70 3 105 60 50 90 4 106 50 60 100 5 108 30 80 20 6 109 20 90 30 INNER JOINでの結合方法 pd.merge(df,df2,how="inner",on="ID") 結合結果 index ID col1 col2 col3 col4 col5 col6 col7 science english math 0 101 name1 address1 M 2000-01-01 170.0 60.0 1 100 10 50 1 102 name2 address2 F 2000-01-02 160.0 50.0 2 90 20 60 2 105 name5 address5 M 2000-01-05 190.0 80.0 2 60 50 90 3 106 name6 address6 M 2000-01-06 200.0 90.0 1 50 60 100 LEFT JOINでの結合方法 pd.merge(df,df2,how="left",on="ID") 結合結果 index ID col1 col2 col3 col4 col5 col6 col7 science english math 0 101 name1 address1 M 2000-01-01 170.0 60.0 1 100.0 10.0 50.0 1 102 name2 address2 F 2000-01-02 160.0 50.0 2 90.0 20.0 60.0 2 104 name4 address4 F 2000-01-04 150.0 40.0 1 NaN NaN NaN 3 105 name5 address5 M 2000-01-05 190.0 80.0 2 60.0 50.0 90.0 4 106 name6 address6 M 2000-01-06 200.0 90.0 1 50.0 60.0 100.0 5 107 name7 address7 F 2000-01-07 140.0 40.0 1 NaN NaN NaN 6 110 name10 address10 M2000-01-10 170.0 60.0 2 NaN NaN NaN RIGHT JOINでの結合方法 pd.merge(df,df2,how="right",on="ID") 結合結果 index col1 col2 col3 col4 col5 col6 col7 science english math 0 101 name1 address1 M 2000-01-01 170.0 60.0 1.0 100 10 1 102 name2 address2 F 2000-01-02 160.0 50.0 2.0 90 20 2 105 name5 address5 M 2000-01-05 190.0 80.0 2.0 60 50 3 106 name6 address6 M 2000-01-06 200.0 90.0 1.0 50 60 4 103 NaN NaN NaN NaT NaN NaN NaN 80 30 5 108 NaN NaN NaN NaT NaN NaN NaN 30 80 6 109 NaN NaN NaN NaT NaN NaN NaN 20 90 ・データの入出力 CSVデータの入出力方法 df.to_csv("foo.csv") pd.read_csv("foo.csv") Excelデータの入出力方法 df.to_excel("foo.xlsx", sheet_name="Sheet1") pd.read_excel("foo.xlsx", "Sheet1", index_col=None, na_values=["NA"]) まとめ 基本的なpandasの使い方をまとめました。 pandasは他にも色々便利な機能も用意されているので、今後利用した際にまとめたいと思います。 参考資料
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GPT-3 & OpenAI Codexの使用方法。文章からプログラムを生成する方法

本記事ではOpenAIのGPT-3およびCodexの使用方法と、文章生成、文章からプログラムの生成、プログラムから文章を生成する方法を紹介します。 本記事の内容 GPT-3、Codex、Copilotの違いとは GPT-3、Codex、Copilotを使用するための申請方法 GPT-3の使用例:広告文書の作成 Codexの使用例:Pythonのコードからコードのコメント文を生成 Codexの使用例:Pythonのコードからdocstringを生成 Codexの使用例:文章からPythonプログラムの生成 さいごに 1. GPT-3、Codex、Copilotの違いとは GPT-3、Codex(コーデックス)、Copilot(コパイロット)の違いについて説明します。 初め、私にはCodexとCopilotの違いが分かりづらかったです。 GPT-3 GPT-3 [link]の正式名称は「Generative Pretrained Transformer version3」です。 その名の通り、BERTなどでも使用されるTransformerを使用した言語モデルとなります。 GPT-3では入力に文章を与え、何らかを出力させます。 基本的には、文章を出力させることが多く、言語生成モデルとなります。 Codex Codex [link]の正式名称は「code extension model」です(だと思われます)。 当初のGPT-3の発表ではGPT-3に文章を入力し、プログラムコードを生成させるデモなどがありました。 CodexはGPT-3の、入力もしくは出力がプログラミングコードである部分を取り出したものです(code extension model)。 おそらく、OpenAIがAPIを開放する際の整理のために、 自然言語を出力するものは「GPT-3 API」にして、プログラミングコードが関係するAPI機能は「Codex」に整理したのだと思います。 Copilot Copilot[link]の正式名称は「GitHub Copilot」です。 Copilotは普通の英単語で、飛行機などの副操縦士を意味します。 CopilotはCodexを使用し、GitHubの(おそらく利用権限に問題がない)各種コードを学習させた「VS Code」の拡張機能です。 コーディングを支援してくれるAI機能になります。 正式名称にGiHubが含まれており、使用プラットフォームが「VS Code」であることから分かるように、Copilotを管轄しているのはOpenAIではなく、GitHub(Microsoft)です。 2. GPT-3、Codex、Copilotを使用するための申請方法 GPT-3、Codex GPT-3とCodexはOpenAIのAPIページの申請フォーム(APIページの右上のJOIN)から申請します。 申請するとwait listに追加され、時期が来ると「使用できるようになりましたー」というメールが届きます。 上記の図の通り、GPT-3(language models)とCodex(code models)を両方同時に(Both)申請することができます。 ただし、「使用できるようになりましたー」メールは別々のタイミングで届きます。 私の場合、GPT-3が1日後、Codexが2日後くらいでした。 申請フォームには、「What is your GitHub profile?」とGitHubのURLを記入する部分、 そして、「If you have an idea you want to use the API for, what is it?」と、どんな風に使用しますか?というのを記入する部分があります。 そのため、育っているGitHubアカウントを持ち、良い感じにアイデアを書いておくと、申請が通るのが早くなるかもしれません。 Copilot Copilotは、GPT-3とCodexとは違い、OpenAIではなく、GitHubのページ こちら から申請します。 ページ中央にある「Sign UP」ボタンをクリックします。 私もまだCopilotはwaiting listのままで、使用できる状態にありません。 Copilotが使用できるようになったら、また報告したいと思います。 3. GPT-3の使用例:広告文書の作成 GPT-3およびCodexの使用例を紹介します。 まずは広告文書の作成例です。 OpenAIのAPIページにログインすると、各種exampleが用意されています(約60個)。 まずは、ここのGPT-3の権限だけで実行できる「Ad from product description」(商品説明からの広告文生成)を紹介します。 実行方法は複数あります。 一番簡単なのは上図の右上にある「Open in Playground」をクリックして、Webページ上で体験する方法です。 その他にも、上図の下側のようにAPIを各種言語から叩いてresponseとして取得することもできます。 OpenAIとしては公式にはCurlで叩けるものと、Python SDKしか提供していませんが、各種言語でのラッパーが用意されており(C#.net、Java、Javascript、GO、Rubyなどなど)、公式ページで以下のようにリンクが案内されています。 「Playground」での使用の様子は以下の通りです。 はじめに入力文を与えておき、そして「Submit」ボタンをクリックします。 すると、広告文が生成されます。 上記の例の場合、 入力は以下です。 Write a creative ad for the following product to run on Facebook: """""" Airee is a line of skin-care products for young women with delicate skin. The ingredients are all-natural. """""" This is the ad I wrote for Facebook aimed at teenage girls: """""" 商品説明の文章が 最初に目的として、「Facebook用の広告として、以下の製品のad文章を生成せよ」と指令しています。 商品説明は、 「Aireeは、デリケートな肌を持つ若い女性のためのスキンケア製品ラインです。成分はすべて天然のものを使用しています。」 です。 そして、「10代の女性向けにFacebook広告としては以下となります」。 と記入して、入力が終了します。 あとは、GPT-3の出力です。 出力は、以下のような感じです(上記の図とは少し異なります。毎回異なる生成になるので) Airee is a line of skin-care products for young women with delicate skin. The ingredients are all-natural. You need to be gentle with your skin. It's important to take care of it. Airee is perfect for you. It's gentle and Aireeは、デリケートな肌を持つ若い女性のためのスキンケア製品ラインです。 成分はすべて天然のものを使用しています。 自分の肌に優しくすることが必要です。ケアすることが大切なのです。 Aireeはあなたにぴったりです。穏やかで 生成するたびに変わるので、 You know how it's always hard to find a moisturizer that won't make your skin break out? Well, Airee is the answer to your prayers. With all-natural ingredients and a light, moisturizing formula, Airee will keep your skin looking and feeling great. 肌が荒れない保湿クリームを見つけるのはいつも大変なんですよね。 Aireeはそんなあなたの願いを叶えてくれます。 すべての天然成分と、軽い保湿剤を使用しています。 Aireeは、あなたの肌の見た目と感触を保ちます。 です。 APIを叩くこともでき、Pythonで上記を実現する場合は以下の通りです。 import os import openai openai.api_key = os.getenv("OPENAI_API_KEY") response = openai.Completion.create( engine="davinci-instruct-beta", prompt="Write a creative ad for the following product to run on Facebook:\n\"\"\"\"\"\"\nAiree is a line of skin-care products for young women with delicate skin. The ingredients are all-natural.\n\"\"\"\"\"\"\nThis is the ad I wrote for Facebook aimed at teenage girls:\n\"\"\"\"\"\"", temperature=0.5, max_tokens=60, top_p=1.0, frequency_penalty=0.0, presence_penalty=0.0, stop=["\"\"\"\"\"\""] ) 事前にpip install でopenaiをインストールしておきます(openai gymとは違うので注意)。 responseは以下のような感じになっており、textに生成された文が入っています。 <OpenAIObject text_completion id=cmpl-3utxvRERchBUwZCIAPbzilXmUJFE4 at 0x7ff061f40d10> JSON: { "choices": [ { "finish_reason": "length", "index": 0, "logprobs": null, "text": "\nAiree is a line of skin-care products for young women with delicate skin. The ingredients are all-natural.\n\nYou need to be gentle with your skin. It's important to take care of it.\n\nAiree is perfect for you. It's gentle and" } ], "created": 1634690511, "id": "cmpl-3utxvRERchBUwZCIAPbzilXmUJFE4", "model": "if-davinci-v2", "object": "text_completion" } 重要点 OpenAIのAPIを使ううえで重要な点は以下の6つかと思います。 現在の対応は英語のみで、日本語での入力はできません アカウントには事前に18ドルくらい?の使用料金が付与されていますが、それを超えると払う必要あります 料金は入力する文字数(正確には分かち書きしたトークン数)で決まります さらに料金は使用するパラメータengine(上記の例ではdavinci-instruct-beta)でも変わります。 賢さ(モデルサイズや事前訓練の量)がエンジンによって異なっており、Davinci、Curie、Babbage、Adaの順番で賢いが処理が遅くなります。 させたいタスクはengineでモデル名のあとにつなげます。Base series、Instruct series、Codex series、Content filterの4種類があります。多くは、instructにして、入力文にさせたいタスクを実際に入力することになります(上記のad文章の生成の入力のように) 上記のCodex seriesがCodexを使用するための設定ですが、Codexの権限許可が下りるまでは実行できません 4. Codexの使用例:Pythonのコードからコードのコメント文を生成 続いて、Exampleの「Python to natural language」を試します。 以下のような入力を与え、エンジンにdavinci-codexを使用すると、以下の図のような動作をします。 以下の関数は、 「変数ws_prefixがTrueなら、変数xの接頭語prefixを空白スペースに変換する」 という内容です。 # Python 3 def remove_common_prefix(x, prefix, ws_prefix): x["completion"] = x["completion"].str[len(prefix) :] if ws_prefix: # keep the single whitespace as prefix x["completion"] = " " + x["completion"] return x # Explanation of what the code does # 出力は The code above is a function that takes three arguments: と、 「上記の関数は3つの引数をとります」でした。 ちょっと、使い物にならないですね。。。 Playgroudの右端での設定が問題です。 Stop sequencesに、"#"が設定されているのを除去して、2文以上のコメント文を生成できるようにします。 さらにResponse lengthが64では短文しか作れないので、256に増やしてあげます。 これで実行すると以下の通りです。 出力結果は以下の通りです。 最初に各引数を説明しているのですが、きちんと各引数が関数の動作の説明となる正確な文章です。 とくにxがdataframeであることを説明しており、かつ、ws_prefixがフラグとなっていて、しかもスペースに接頭語を置換する点まで説明してくれています。 # The code above is a function that takes three arguments: # # * `x`: a row of the dataframe # * `prefix`: the prefix to remove # * `ws_prefix`: whether or not to keep the single whitespace character that was added to the prefix # # The function returns a modified version of the row, where the `completion` column has the prefix removed. # # The function is then passed to the `apply` method of the dataframe, which applies the function to each row. # # The `apply` method is a very powerful method of dataframes, and there are many use cases. # # For more information, see the [documentation](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.apply.html). # The code above is a little verbose, and it can be simplified using a lambda function. # # A lambda function is a function without a name. # # The code below is equivalent to the code above, but uses a lambda function instead. 入力した関数は型ヒントがないのですが、xがdictなどでは、x["completion"].str[が実行できないので、xはdataframe型である必要があります。 その点まで説明し、さらにリファレンスとして、 https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.apply.html を書き出し、dataframeに対する関数のひっかけかた(ここでは、.str[])の説明のURLまで提示してくれました。 5. Codexの使用例:Pythonのコードからdocstringを生成 続いて、Exampleの「Write a Python docstring」を試します。 入力は以下の通りです。engineはdavinci-codexです。 フォルダ名とファイル名を引数に、訓練とテストのdataframeを分割し、それぞれをJSONにして保存しています。 # Python 3.7 def randomly_split_dataset(folder, filename, split_ratio=[0.8, 0.2]): df = pd.read_json(folder + filename, lines=True) train_name, test_name = "train.jsonl", "test.jsonl" df_train, df_test = train_test_split(df, test_size=split_ratio[1], random_state=42) df_train.to_json(folder + train_name, orient='records', lines=True) df_test.to_json(folder + test_name, orient='records', lines=True) randomly_split_dataset('finetune_data/', 'dataset.jsonl') # An elaborate, high quality docstring for the above function: """ 動作は以下の通りです。 出力は Randomly splits a dataset into train and test sets. :param folder: The folder where the dataset is located. :param filename: The name of the dataset file. :param split_ratio: The ratio of train to test samples. :return: None きちんとした関数と引数の説明、そして返り値がないことまで説明してくれています。 上記はreStructuredTextのstyleのdocstringであって、次はNumpy styleにしたいとします。 次は、「先ほどコメント文を生成させた接頭語をスペースに変換する関数」を入力、そしてdocstringはNumpy Styleにしたいと思います。 GPT-3 Codexへの入力は次の通りです。 # Python 3.7 def remove_common_prefix(x, prefix, ws_prefix): x["completion"] = x["completion"].str[len(prefix) :] if ws_prefix: # keep the single whitespace as prefix x["completion"] = " " + x["completion"] return x # An elaborate, high quality docstring for the above function in Numpy style: """ さきほどは # An elaborate, high quality docstring for the above function: であった部分の最後に、in Numpy styleと足してあげます。 動作は次の通りです。 出力は Remove a common prefix from a column in a dataframe. Parameters ---------- x : pandas.DataFrame The dataframe containing the column to be modified. prefix : str The prefix to be removed. ws_prefix : bool Whether to keep the single whitespace character after the prefix. Returns ------- x : pandas.DataFrame The modified dataframe. Numpy Styleで説明、引数、返り値を記述するとともに、ws_prefixが接頭語を空白スペースに変換するかのフラグである点の説明など、各変数の内容についてもきちんと説明してくれています。 6. Codexの使用例:文章からPythonプログラムの生成 最後に文章からプログラムを生成します。 Exampleの「Natural language to Stripe API」をしてみます。 Stripeは決済サービスのプラットフォームでいろいろな言語のSDKが用意されています。 今回はExample通りではなく、Pythonのコードを生成させてみましょう。 まず「Natural language to Stripe API」のPlaygroundを開きます。 右側のResponse lengthを200にして、長い文章を生成できるように設定します。 入力する内容は元の、 """ Util exposes the following: util.stripe() -> authenticates & returns the stripe module; usable as stripe.Charge.create etc """ import util """ Create a Stripe token using the users credit card: 5555-4444-3333-2222, expiration date 12 / 28, cvc 521 """ ではなく、 """ Create a Stripe token using the users credit card: 5555-4444-3333-2222, expiration date 12 / 28, cvc 521 in python sdk """ に変更します。 動作は次の通りです。 出力は import stripe stripe.api_key = "sk_test_4eC39HqLyjWDarjtT1zdp7dc" token = stripe.Token.create( card={ "number": "5555-4444-3333-2222", "exp_month": 12, "exp_year": 2020, "cvc": "521", }, ) print(token) となりました。 私はStripeのPython SDKを使用したことがないので、正しいか確認してみます。 リンクはこちら importするライブラリはstripeで合っています。 また使用する関数がstripe.Token.createで、これも合っています。 生成されたAPI Tokenが誰かがGitHubに誤って公開したものだと恐ろしかったのですが、Stripeの公式ページの見本と一致しており、見本例をきちんと引っ張ってきてくれたのだと分かりました。 続いて、私は、stripe.Token.create関数のパラメータ名をもちろん知らないのですが、"number"や"exp_month"を正しく使用してくれています。 そして"exp_month"には、入力文章で「expiration date 12 / 28」と与えた通り、きちんと12月が与えられています。 よって、私の知らないPython SDKでのコードを文章からうまく自動生成できたと判断します。 「本例は、1関数だけで構成される、SDK Documentsに見本がある簡単なレベルの内容ではありますが、文章を入力すればプログラムが生成されるという凄い世界になったものです」。 最後に、API keyも指定して、コードを自動生成させましょう。 入力は以下です。 """ Create a Stripe token using the users credit card: 5555-4444-3333-2222, expiration date 12 / 28, cvc 521 in python sdk. My api key is sk_test_yutaroogawa. """ 動作は以下の通りです。 出力は import stripe stripe.api_key = "sk_test_yutaroogawa" token = stripe.Token.create( card={ "number": "5555-4444-3333-2222", "exp_month": 12, "exp_year": 2020, "cvc": "521", }, ) print(token) となり、api_keyにsk_test_yutaroogawaを代入してくれました。 これで完全に、自然言語から、プログラムの自動生成が完了できました。 7. さいごに 以上、OpenAIのGPT-3およびCodexの使用方法と、GPT-3およびCodexを使用した文章生成、プログラムから文章を生成する方法、そして文章からプログラム生成を紹介しました。 ご一読いただき、ありがとうございました。 【記事執筆者】 電通国際情報サービス(ISID)AIトランスフォーメーションセンター 製品開発Gr 小川 雄太郎 主書「つくりながら学ぶ! PyTorchによる発展ディープラーニング」 自己紹介(詳細はこちら) 【情報発信】 Twitterアカウント:小川雄太郎@ISID_AI_team IT・AIやビジネス・経営系情報で、面白いと感じた記事やサイトを、Twitterで発信しています。 【免責】 本記事の内容そのものは執筆者の意見/発信であり、執筆者が属する企業等の公式見解ではございません
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GPT-3 & OpenAI Codexの使用方法。文章からプログラムを自動生成する方法

本記事ではOpenAIのGPT-3およびCodexの使用方法と、文章自動生成、文章からプログラムの自動生成、プログラムから文章を自動生成する方法を紹介します。 また最後にGPT-3での素数生成の再現も試みます。 本記事の内容 GPT-3、Codex、Copilotの違いとは GPT-3、Codex、Copilotを使用するための申請方法 GPT-3の使用例:広告文書の作成 Codexの使用例:Pythonのコードからコードのコメント文を生成 Codexの使用例:Pythonのコードからdocstringを生成 Codexの使用例:文章からPythonプログラムの生成 GPT-3で素数を生成してみる さいごに 1. GPT-3、Codex、Copilotの違いとは GPT-3、Codex(コーデックス)、Copilot(コパイロット)の違いについて説明します。 初め、私にはCodexとCopilotの違いが分かりづらかったです。 GPT-3 GPT-3 [link]の正式名称は「Generative Pretrained Transformer version3」です。 その名の通り、BERTなどでも使用されるTransformerを使用した言語モデルとなります。 GPT-3では入力に文章を与え、何らかを出力させます。 基本的には、文章を出力させることが多く、言語生成モデルとなります。 Codex Codex [link]の正式名称は「code extension model」です(だと思われます)。 当初のGPT-3の発表ではGPT-3に文章を入力し、プログラムコードを生成させるデモなどがありました。 CodexはGPT-3の、入力もしくは出力がプログラミングコードである部分を取り出したものです(code extension model)。 おそらく、OpenAIがAPIを開放する際の整理のために、 自然言語を出力するものは「GPT-3 API」にして、プログラミングコードが関係するAPI機能は「Codex」に整理したのだと思います。 Copilot Copilot[link]の正式名称は「GitHub Copilot」です。 Copilotは普通の英単語で、飛行機などの副操縦士を意味します。 CopilotはCodexを使用し、GitHubの(おそらく利用権限に問題がない)各種コードを学習させた「VS Code」の拡張機能です。 コーディングを支援してくれるAI機能になります。 正式名称にGiHubが含まれており、使用プラットフォームが「VS Code」であることから分かるように、Copilotを管轄しているのはOpenAIではなく、GitHub(Microsoft)です。 2. GPT-3、Codex、Copilotを使用するための申請方法 GPT-3、Codex GPT-3とCodexはOpenAIのAPIページの申請フォーム(APIページの右上のJOIN)から申請します。 申請するとwait listに追加され、時期が来ると「使用できるようになりましたー」というメールが届きます。 上記の図の通り、GPT-3(language models)とCodex(code models)を両方同時に(Both)申請することができます。 ただし、「使用できるようになりましたー」メールは別々のタイミングで届きます。 私の場合、GPT-3が1日後、Codexが2日後くらいでした。 申請フォームには、「What is your GitHub profile?」とGitHubのURLを記入する部分、 そして、「If you have an idea you want to use the API for, what is it?」と、どんな風に使用しますか?というのを記入する部分があります。 そのため、育っているGitHubアカウントを持ち、良い感じにアイデアを書いておくと、申請が通るのが早くなるかもしれません。 Copilot Copilotは、GPT-3とCodexとは違い、OpenAIではなく、GitHubのページ こちら から申請します。 ページ中央にある「Sign UP」ボタンをクリックします。 私もまだCopilotはwaiting listのままで、使用できる状態にありません。 Copilotが使用できるようになったら、また報告したいと思います。 3. GPT-3の使用例:広告文書の作成 GPT-3およびCodexの使用例を紹介します。 まずは広告文書の作成例です。 OpenAIのAPIページにログインすると、各種exampleが用意されています(約60個)。 まずは、ここのGPT-3の権限だけで実行できる「Ad from product description」(商品説明からの広告文生成)を紹介します。 実行方法は複数あります。 一番簡単なのは上図の右上にある「Open in Playground」をクリックして、Webページ上で体験する方法です。 その他にも、上図の下側のようにAPIを各種言語から叩いてresponseとして取得することもできます。 OpenAIとしては公式にはCurlで叩けるものと、Python SDKしか提供していませんが、各種言語でのラッパーが用意されており(C#.net、Java、Javascript、GO、Rubyなどなど)、公式ページで以下のようにリンクが案内されています。 「Playground」での使用の様子は以下の通りです。 はじめに入力文を与えておき、そして「Submit」ボタンをクリックします。 すると、広告文が生成されます。 上記の例の場合、 入力は以下です。 Write a creative ad for the following product to run on Facebook: """""" Airee is a line of skin-care products for young women with delicate skin. The ingredients are all-natural. """""" This is the ad I wrote for Facebook aimed at teenage girls: """""" 商品説明の文章が 最初に目的として、「Facebook用の広告として、以下の製品のad文章を生成せよ」と指令しています。 商品説明は、 「Aireeは、デリケートな肌を持つ若い女性のためのスキンケア製品ラインです。成分はすべて天然のものを使用しています。」 です。 そして、「10代の女性向けにFacebook広告としては以下となります」。 と記入して、入力が終了します。 あとは、GPT-3の出力です。 出力は、以下のような感じです(上記の図とは少し異なります。毎回異なる生成になるので) Airee is a line of skin-care products for young women with delicate skin. The ingredients are all-natural. You need to be gentle with your skin. It's important to take care of it. Airee is perfect for you. It's gentle and Aireeは、デリケートな肌を持つ若い女性のためのスキンケア製品ラインです。 成分はすべて天然のものを使用しています。 自分の肌に優しくすることが必要です。ケアすることが大切なのです。 Aireeはあなたにぴったりです。穏やかで 生成するたびに変わるので、 You know how it's always hard to find a moisturizer that won't make your skin break out? Well, Airee is the answer to your prayers. With all-natural ingredients and a light, moisturizing formula, Airee will keep your skin looking and feeling great. 肌が荒れない保湿クリームを見つけるのはいつも大変なんですよね。 Aireeはそんなあなたの願いを叶えてくれます。 すべての天然成分と、軽い保湿剤を使用しています。 Aireeは、あなたの肌の見た目と感触を保ちます。 です。 APIを叩くこともでき、Pythonで上記を実現する場合は以下の通りです。 import os import openai openai.api_key = os.getenv("OPENAI_API_KEY") response = openai.Completion.create( engine="davinci-instruct-beta", prompt="Write a creative ad for the following product to run on Facebook:\n\"\"\"\"\"\"\nAiree is a line of skin-care products for young women with delicate skin. The ingredients are all-natural.\n\"\"\"\"\"\"\nThis is the ad I wrote for Facebook aimed at teenage girls:\n\"\"\"\"\"\"", temperature=0.5, max_tokens=60, top_p=1.0, frequency_penalty=0.0, presence_penalty=0.0, stop=["\"\"\"\"\"\""] ) 事前にpip install でopenaiをインストールしておきます(openai gymとは違うので注意)。 responseは以下のような感じになっており、textに生成された文が入っています。 <OpenAIObject text_completion id=cmpl-3utxvRERchBUwZCIAPbzilXmUJFE4 at 0x7ff061f40d10> JSON: { "choices": [ { "finish_reason": "length", "index": 0, "logprobs": null, "text": "\nAiree is a line of skin-care products for young women with delicate skin. The ingredients are all-natural.\n\nYou need to be gentle with your skin. It's important to take care of it.\n\nAiree is perfect for you. It's gentle and" } ], "created": 1634690511, "id": "cmpl-3utxvRERchBUwZCIAPbzilXmUJFE4", "model": "if-davinci-v2", "object": "text_completion" } 重要点 OpenAIのAPIを使ううえで重要な点は以下の6つかと思います。 現在の対応は英語のみで、日本語での入力はできません アカウントには事前に18ドルくらい?の使用料金が付与されていますが、それを超えると払う必要あります 料金は入力する文字数(正確には分かち書きしたトークン数)で決まります さらに料金は使用するパラメータengine(上記の例ではdavinci-instruct-beta)でも変わります。 賢さ(モデルサイズや事前訓練の量)がエンジンによって異なっており、Davinci、Curie、Babbage、Adaの順番で賢いが処理が遅くなります。 させたいタスクはengineでモデル名のあとにつなげます。Base series、Instruct series、Codex series、Content filterの4種類があります。多くは、instructにして、入力文にさせたいタスクを実際に入力することになります(上記のad文章の生成の入力のように) 上記のCodex seriesがCodexを使用するための設定ですが、Codexの権限許可が下りるまでは実行できません 4. Codexの使用例:Pythonのコードからコードのコメント文を生成 続いて、Exampleの「Python to natural language」を試します。 以下のような入力を与え、エンジンにdavinci-codexを使用すると、以下の図のような動作をします。 以下の関数は、 「変数ws_prefixがTrueなら、変数xの接頭語prefixを空白スペースに変換する」 という内容です。 # Python 3 def remove_common_prefix(x, prefix, ws_prefix): x["completion"] = x["completion"].str[len(prefix) :] if ws_prefix: # keep the single whitespace as prefix x["completion"] = " " + x["completion"] return x # Explanation of what the code does # 出力は The code above is a function that takes three arguments: と、 「上記の関数は3つの引数をとります」でした。 ちょっと、使い物にならないですね。。。 Playgroudの右端での設定が問題です。 Stop sequencesに、"#"が設定されているのを除去して、2文以上のコメント文を生成できるようにします。 さらにResponse lengthが64では短文しか作れないので、256に増やしてあげます。 これで実行すると以下の通りです。 出力結果は以下の通りです。 最初に各引数を説明しているのですが、きちんと各引数が関数の動作の説明となる正確な文章です。 とくにxがdataframeであることを説明しており、かつ、ws_prefixがフラグとなっていて、しかもスペースに接頭語を置換する点まで説明してくれています。 # The code above is a function that takes three arguments: # # * `x`: a row of the dataframe # * `prefix`: the prefix to remove # * `ws_prefix`: whether or not to keep the single whitespace character that was added to the prefix # # The function returns a modified version of the row, where the `completion` column has the prefix removed. # # The function is then passed to the `apply` method of the dataframe, which applies the function to each row. # # The `apply` method is a very powerful method of dataframes, and there are many use cases. # # For more information, see the [documentation](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.apply.html). # The code above is a little verbose, and it can be simplified using a lambda function. # # A lambda function is a function without a name. # # The code below is equivalent to the code above, but uses a lambda function instead. 入力した関数は型ヒントがないのですが、xがdictなどでは、x["completion"].str[が実行できないので、xはdataframe型である必要があります。 その点まで説明し、さらにリファレンスとして、 https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.apply.html を書き出し、dataframeに対する関数のひっかけかた(ここでは、.str[])の説明のURLまで提示してくれました。 5. Codexの使用例:Pythonのコードからdocstringを生成 続いて、Exampleの「Write a Python docstring」を試します。 入力は以下の通りです。engineはdavinci-codexです。 フォルダ名とファイル名を引数に、訓練とテストのdataframeを分割し、それぞれをJSONにして保存しています。 # Python 3.7 def randomly_split_dataset(folder, filename, split_ratio=[0.8, 0.2]): df = pd.read_json(folder + filename, lines=True) train_name, test_name = "train.jsonl", "test.jsonl" df_train, df_test = train_test_split(df, test_size=split_ratio[1], random_state=42) df_train.to_json(folder + train_name, orient='records', lines=True) df_test.to_json(folder + test_name, orient='records', lines=True) randomly_split_dataset('finetune_data/', 'dataset.jsonl') # An elaborate, high quality docstring for the above function: """ 動作は以下の通りです。 出力は Randomly splits a dataset into train and test sets. :param folder: The folder where the dataset is located. :param filename: The name of the dataset file. :param split_ratio: The ratio of train to test samples. :return: None きちんとした関数と引数の説明、そして返り値がないことまで説明してくれています。 上記はreStructuredTextのstyleのdocstringであって、次はNumpy styleにしたいとします。 次は、「先ほどコメント文を生成させた接頭語をスペースに変換する関数」を入力、そしてdocstringはNumpy Styleにしたいと思います。 GPT-3 Codexへの入力は次の通りです。 # Python 3.7 def remove_common_prefix(x, prefix, ws_prefix): x["completion"] = x["completion"].str[len(prefix) :] if ws_prefix: # keep the single whitespace as prefix x["completion"] = " " + x["completion"] return x # An elaborate, high quality docstring for the above function in Numpy style: """ さきほどは # An elaborate, high quality docstring for the above function: であった部分の最後に、in Numpy styleと足してあげます。 動作は次の通りです。 出力は Remove a common prefix from a column in a dataframe. Parameters ---------- x : pandas.DataFrame The dataframe containing the column to be modified. prefix : str The prefix to be removed. ws_prefix : bool Whether to keep the single whitespace character after the prefix. Returns ------- x : pandas.DataFrame The modified dataframe. Numpy Styleで説明、引数、返り値を記述するとともに、ws_prefixが接頭語を空白スペースに変換するかのフラグである点の説明など、各変数の内容についてもきちんと説明してくれています。 6. Codexの使用例:文章からPythonプログラムの生成 最後に文章からプログラムを生成します。 Exampleの「Natural language to Stripe API」をしてみます。 Stripeは決済サービスのプラットフォームでいろいろな言語のSDKが用意されています。 今回はExample通りではなく、Pythonのコードを生成させてみましょう。 まず「Natural language to Stripe API」のPlaygroundを開きます。 右側のResponse lengthを200にして、長い文章を生成できるように設定します。 入力する内容は元の、 """ Util exposes the following: util.stripe() -> authenticates & returns the stripe module; usable as stripe.Charge.create etc """ import util """ Create a Stripe token using the users credit card: 5555-4444-3333-2222, expiration date 12 / 28, cvc 521 """ ではなく、 """ Create a Stripe token using the users credit card: 5555-4444-3333-2222, expiration date 12 / 28, cvc 521 in python sdk """ に変更します。 動作は次の通りです。 出力は import stripe stripe.api_key = "sk_test_4eC39HqLyjWDarjtT1zdp7dc" token = stripe.Token.create( card={ "number": "5555-4444-3333-2222", "exp_month": 12, "exp_year": 2020, "cvc": "521", }, ) print(token) となりました。 私はStripeのPython SDKを使用したことがないので、正しいか確認してみます。 リンクはこちら importするライブラリはstripeで合っています。 また使用する関数がstripe.Token.createで、これも合っています。 生成されたAPI Tokenが誰かがGitHubに誤って公開したものだと恐ろしかったのですが、Stripeの公式ページの見本と一致しており、見本例をきちんと引っ張ってきてくれたのだと分かりました。 続いて、私は、stripe.Token.create関数のパラメータ名をもちろん知らないのですが、"number"や"exp_month"を正しく使用してくれています。 そして"exp_month"には、入力文章で「expiration date 12 / 28」と与えた通り、きちんと12月が与えられています。 よって、私の知らないPython SDKでのコードを文章からうまく自動生成できたと判断します。 「本例は、1関数だけで構成される、SDK Documentsに見本がある簡単なレベルの内容ではありますが、文章を入力すればプログラムが生成されるという凄い世界になったものです」。 最後に、API keyも指定して、コードを自動生成させましょう。 入力は以下です。 """ Create a Stripe token using the users credit card: 5555-4444-3333-2222, expiration date 12 / 28, cvc 521 in python sdk. My api key is sk_test_yutaroogawa. """ 動作は以下の通りです。 出力は import stripe stripe.api_key = "sk_test_yutaroogawa" token = stripe.Token.create( card={ "number": "5555-4444-3333-2222", "exp_month": 12, "exp_year": 2020, "cvc": "521", }, ) print(token) となり、api_keyにsk_test_yutaroogawaを代入してくれました。 これで完全に、自然言語から、プログラムの自動生成が完了できました。 7. GPT-3で素数を生成してみる 最後に、GPT-3で素数を生成してみます。 以下の記事にて、松尾先生がGPT-3の素数生成能力の話をしています。 さらに、GPT-3には素数が列挙できるという特徴もある。最初の12個ぐらいの素数を「2、3、5、7、11……」と列挙すると、その後の数字を続け始めるというのだ。 もしかしたら、インターネット上にはたくさんのデータがあるため、「素数を列挙したページから丸覚えしたのではないか?」と思われるかもしれない。しかし、GPT-3は誤って「3693」など素数ではない数字もはじき出しているという。 松尾豊さんは「間違えているということは、計算をちゃんとしているということです。つまり、たくさんのデータを入力して学習させるだけで、(GPT-3には)素数を計算するようなアルゴリズムが学習されています」と解説する。 GPT-3はビジネスの世界も変え得る。松尾豊さんは「非常に多くのタスクが今後、GPT-3によって実現される可能性があります。法務、人事、調達などなどはもちろん、ヘルスケアの分野でも、たくさんテキストを使う仕事があります。こういったものを次々と自動化していく可能性があると思います」と話した。 上記の松尾先生の見解は、Aravind Srinivas(OpenAI所属)のTwitterに由来します。 確かに、129のような素数でない数字も生成しているのですが、ほとんどが素数で、たまに素数を飛ばしてしまったりもしています(263など)。 ただ、Aravind Srinivasのこの結果を再現する方法がどこにもないので、本当か?と疑問があります。 ここで試してみましょう。 入力するのは、109までの素数とし、その続きを生成させることにします(109までにしたのは、100以下の素数を羅列したサイトは多いため、それを引用する可能性を考慮)。 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, engineには一番賢いDavinci、そして2番目のCurieの2つを使用して比べてみます。 まずCurieでの生成の様子を以下に示します。Response lengthは70に設定しています。 出力は 113, 127, 131, 137, 139, 149, 151, 157, 161, 163, 167, 173, 179, 181, 187, 191, 193, 197, 199, 211, 223, 227, 229, 233, 235, 237, 239, 241, 243, 245, 247, 249, 251, 253, 255, となりました。 113から233のうち、ほとんど(22個)は素数ですが、161(7×23)と187(11×17)のみ素数ではありません。 233からその先は単純に2ずつ増えていて素数生成になっていません。 凄いのは、与えた数字が109までの素数なので、入力した数値の最大値の2倍である233あたりまでは素数をきちんと出力していることです。そして松尾先生の指摘通り、素数表を引用しているわけではなさそうで、161と187という素数でない誤った数字も出力しています。 松尾豊さんは「間違えているということは、計算をちゃんとしているということです。つまり、たくさんのデータを入力して学習させるだけで、(GPT-3には)素数を計算するようなアルゴリズムが学習されています」と解説する。 が再現できました。 それでは続いて、engineを最高レベルのDavinciで試してみましょう。 先ほどと同じく入力するのは、109までの素数とし、その続きを生成させることにします。 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, Davinciでの生成の様子を以下に示します。 出力は 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211, 223, 227, 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, 283, 293, 307, 311, となりました。 これは全て完全に素数です。誤りもなく、抜けもなく、きちんと生成されています。 Curieと違って、Davinciは入力した数列から、素数の数列であることを把握し、続きの素数をどこかから引用して羅列している可能性があります・・・ 109までの入力で素数と判断しているなら、それはそれで凄いのですが、その場合、続きの生成はCurieとは違い、GPT-3にとくにアルゴリズムはなく素数表の引用になります。 上記仮説が本当かどうかを検証するために、当人であるGPT-3のDavinciとCurieに聞いてみることにします。 入力は以下の通りです。上記の数列は何ですか?と尋ねてみます。 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, Q: What are these sequential numbers? Response lengthは20にします。 まずはDavinciを試します。 動作は以下の通りです。 今回は一度20 lengthの文章を生成したあと、再度もう一度続きを生成してみました。 出力は1回目が、 A: They are the prime numbers. Q: What are the prime numbers? A: 続きが、 A: They are the numbers that can only be divided by themselves and 1. Q: What are the であり、入力が素数(prime numbers)であると把握していることが分かります。 また、丁寧に素数の説明もしてくれました。 よって、GPT-3 Davinciは入力を素数と判断していることが確定し、数字生成は素数表をどこかから引用しているだけと思われます。 では、同じ質問をGPT-3 Curieに尋ねるとどうでしょうか? 以下が動作の様子です。 出力は A: They are the numbers that are used to identify the different parts of the sequence. (これらはシーケンスの異なる部分を特定するために使用される数字です。) となり、GPT-3 Curieの場合は素数であることを把握できていません。 Curieの方がDavinciよりもモデルサイズが小さく、性能が低いため、入力を素数と把握できなかったのでしょう。 よってGPT-3の、engineDavinciは「素数を列挙したページから丸覚え」(入力を素数の数列と判定できていることも凄いですが)、そしてengineCurieは松尾先生が指摘した通り 「間違えているということは、計算をちゃんとしているということです。つまり、たくさんのデータを入力して学習させるだけで、(GPT-3には)素数を計算するようなアルゴリズムが学習されています」 と考えられます。 いったい、もう、どうなっているのだろう。。。 GPT-3Curieの、ちょっと間違うこともあるけど、ほぼほぼきちんと素数を生成できる能力に驚きです。 長くなりましたが、以上、GPT-3での素数生成の再現実験でした。 8. さいごに 以上、OpenAIのGPT-3およびCodexの使用方法と、GPT-3およびCodexを使用した文章生成、プログラムから文章を生成する方法、文章からプログラム生成、そして最後に素数生成を紹介しました。 長文をご一読いただき、ありがとうございました。 【記事執筆者】 電通国際情報サービス(ISID)AIトランスフォーメーションセンター 製品開発Gr 小川 雄太郎 主書「つくりながら学ぶ! PyTorchによる発展ディープラーニング」 自己紹介(詳細はこちら) 【情報発信】 Twitterアカウント:小川雄太郎@ISID_AI_team IT・AIやビジネス・経営系情報で、面白いと感じた記事やサイトを、Twitterで発信しています。 【免責】 本記事の内容そのものは執筆者の意見/発信であり、執筆者が属する企業等の公式見解ではございません
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

python + OpenCVで画像を加工① 画像を取り込んで大きさを変える

pythonとOpenCVを使って画像の加工をやっています。最終的には動画で人の顔を自動で認識→自動でモザイクをかけるところまでやりたいと思っています。 とりあえず今回は画像を取り込んで大きさを変えて出力するまでをまとめようと思います。 OpenCVのインストールは各所に記事があるので割愛いたします。 ①画像の取り込み 画像取り込みのためにはcv2.imread()を使います(OpenCVのチュートリアル に大変分かりやすく解説してあります)。画像の保存場所とコードの保存場所が異なる場合はファイルパスで指定してあげる必要あり。 img = cv2.imread('sample.jpg') ②画像の加工(拡大・縮小) cv2.resizeを使います。OpenCVのDocumentationに解説がありますが、これが非常に分かりにくい。resize()は引数をいくつか指定する必要があるみたいです。 cv2.resize(①加工する画像, ②加工する際の縮尺, ③縮尺の仕方) こんな感じになるかと思います。実際のコードを下に。 cv2.resize(img, dsize=(100, 200), interpolation = cv2.INTER_LINEAR) #"dsize="は省略可能 cv2.resize(img, (100, 200), interpolation = cv2.INTER_LINEAR) ①加工する対象の画像がimg、②加工する際の縮尺がdsize=(100,200)、③縮尺の仕方がinterpolation = cv2.INTER_LINEAR、になります。 ちなみに③について調べた&試してみた結論としては、縮尺の方法については個人的にはほとんど変わらないという印象でした。。。なので最初のうちは③はあまり気にせず、慣れてきたらいろいろ比較するのがよろしいかと思います。この記述自体省略可能です。 ちょっとややこしいのが②で、上記のサンプルコードでは出力の際の大きさをピクセルで直接指定しています。元の写真の縦横の比率は無視しているので、これだと上下や左右に画像がつぶれてしまうと思います。 そこで使うのがfx, fyで縮尺を指定するという方法です。縦と横を半分の大きさにしたい場合は下記のようなコードになります(その場合dsize=Noneを指定しないとエラーになってしまいますので忘れずに)。 cv2.resize(image, dsize=None, fx=0.5,fy=0.5, interpolation = cv2.INTER_LINEAR) #"dsize="は省略可能 cv2.resize(image, None, fx=0.5,fy=0.5, interpolation = cv2.INTER_LINEAR) 4分の1の大きさにする場合はfx,fyに0.25を指定。 cv2.resize(image, dsize=None, fx=0.25,fy=0.25, interpolation = cv2.INTER_LINEAR) ただ、0.5とか0.25と打ち込むとコードとして分かりにくくなるので、もうちょっと工夫できるかも。例えば少し工夫をして、resizeのコードの前に%で指定して、fxとfyを記述する際にその数値を100で割るという手もありそうです。 最終的なコードは下の通りです。imshow()、WaitKey()、cv2.destroyAllWindows()についてはimread()と同じチュートリアルのページに分かりやすく解説がありました。このページ本当に分かりやすいです。 import cv2 image = cv2.imread('Hiro.jpg') #比較用にオリジナル画像を出力する場合は下記コード #cv2.imshow('Original Image', image) #cv2.waitKey() #縦300ピクセル、横200ピクセルに大きさを指定 resized = cv2.resize(image, dsize=(300,200), interpolation = cv2.INTER_LINEAR) #縦横半分に大きさを指定 resized_half = cv2.resize(image, dsize=None, fx=0.5, fy=0.5, interpolation = cv2.INTER_LINEAR) #縦横にピクセルを指定した画像を出力 cv2.imshow('Resized', resized) cv2.waitKey() #縦横半分の大きさにした画像を出力 cv2.imshow('Resized half', resized_half) cv2.waitKey() cv2.destroyAllWindows()
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Pyinstaller で実行ファイル化する場合のプログラムパスの取得

まずはコードのみ .pyおよび.exeどちらでも親フォルダを取得する def get_source_path(): if getattr(sys, 'frozen', False): # The application is frozen exe_path = Path(sys.executable).resolve() main_path = exe_path.parent dist_path = main_path.parent src_path = dist_path.parent else: # The application is not frozen # Change this bit to match where you store your data files: py_path = Path(__file__).resolve() src_path = py_path.parent return src_path 参照:https://qiita.com/Authns/items/f3cf6e9f27fc0b5632b3
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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

今まで複雑なデータ操作・分析などはPythonでやっており、SQLは普通のアプリ開発程度のライトなものしか触って来なかったのですが、やはり分析用の長いSQLなども書けた方がやりとり等で便利・・・という印象なので、復習も兼ねて記事にしておきます。 また、SQLに加えて検算も兼ねてPythonやPandasなどを使ったコードもSQLと併記していきます(Pythonで書くとどういった記述が該当するのかの比較用として使います)。 ※長くなるのでいくつかの記事に分割します。本記事は6記事目となります。 他のシリーズ記事 Athenaとはなんぞやという方はこちらをご確認ください: ※過去の記事で既に触れたものは本記事では触れません。 #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: 文字列操作全般・正規表現関係など。 #4: コメント関係・配列操作全般・ラムダ式など。 #5: 辞書(STRUCT, MAP, ROW)やJSON関係全般。 この記事で触れること 窓関数関係全般 環境の準備 以下の#1の記事でS3へのAthena用のデータの配置やテーブルのCREATE文などのGitHubに公開しているものに関しての情報を記載していますのでそちらをご参照ください。 窓関数 この節以降では窓関数(Window Functions)について全体的に触れていきます。窓関数はGROUP BYなどに少し似ていますが、特定の条件でデータをセットにして集計用などの関数を通したり、任意の日数分で行をひとまとめに扱って計算を行ったりすることができます。例えば移動平均値などを出す場合などに使います。 窓関数の基本 基本的な書き方は<対象の集計などの関数> OVER(<データの区切りの設定> <ORDER BYの設定> <フレーム設定>)となります。OVER内の各指定は省略されたりもします。それぞれ詳細は後述する各節で順番に触れていきます(ここでは基本のみ触れます)。 データの区切りはPARTITION BY <カラム名>といったように書きます。 例えば日付区切りにして(PARTITION BY dt)、salesカラムに対して合計値を出したい場合にはSUM(sales)といったように書き、SQLは以下のようになります。 SELECT device_type, dt, sales, SUM(sales) OVER(PARTITION BY dt) AS summed_sales FROM athena_workshop.total_sales_per_device_daily ORDER BY dt, device_type 同一の日付のデータの売り上げの合計値のカラムをsummed_salesという名前で追加することができました(今回アクセスしたテーブルではdevice_typeというカラムの端末種別値ごとに各日で2行ずつ存在します)。 こうしてみるとGROUP BYに少し似ているように思えます。一方で、GROUP BYと異なりSELECTで指定できるカラムに制約が少ないですし、他にも独特な機能が色々あります(少しずつ触れていきます)。 Pythonでの書き方 Python(Pandas)で書きたい場合には基本的な書き方は以下のようになります。 シリーズなどでrollingメソッドを使うとRollingオブジェクトが返ってくるので、そのオブジェクトでさらにsumなどの集計関数を使うことで計算が行えます。 別カラムによるデータ区切り等に関してはPandasでスライスを行うことで対応ができます。 rollingメソッドにはさらに第一引数に窓のデータの個数の指定が必要になります。例えば3を指定すれば3行ずつのデータ区切りとなります。 また、集計関数などを挟んだ際に必要なデータ件数が確保できない行に関しては欠損値になったりします。例えば3行ずつのデータ区切りとした場合には1行目と2行目は必要な行数が確保できないので欠損値として表示されます。 from typing import List import pandas as pd date_range = pd.date_range(start='2021-01-01', end='2021-01-05') dfs: List[pd.DataFrame] = [] for date in date_range: df: pd.DataFrame = pd.read_json( 'https://github.com/simon-ritchie/athena_workshop/blob/main/workshop/total_sales_per_device_daily/' f"dt%3D{date.strftime('%Y-%m-%d')}/data.json.gz?raw=true", lines=True, compression='gzip') dfs.append(df) df = pd.concat(dfs, ignore_index=True) ios_df: pd.DataFrame = df[df['device_type'] == 1] rolling = ios_df['sales'].rolling(window=3) summed = rolling.sum() print(summed) 0 NaN 2 NaN 4 1038831.0 6 962325.0 8 963163.0 必要な行数が確保できないものの、少ない行数でも計算をしてしまいたい場合にはmin_periods引数に任意の整数を指定します。この整数以上の行であれば欠損値ではなく計算が実行されるようになります。 以下の例ではmin_periods=1と指定しているため、1行あれば計算されるので欠損値が無くなります。 from typing import List import pandas as pd date_range = pd.date_range(start='2021-01-01', end='2021-01-05') dfs: List[pd.DataFrame] = [] for date in date_range: df: pd.DataFrame = pd.read_json( 'https://github.com/simon-ritchie/athena_workshop/blob/main/workshop/total_sales_per_device_daily/' f"dt%3D{date.strftime('%Y-%m-%d')}/data.json.gz?raw=true", lines=True, compression='gzip') dfs.append(df) df = pd.concat(dfs, ignore_index=True) ios_df: pd.DataFrame = df[df['device_type'] == 1] rolling = ios_df['sales'].rolling(window=3, min_periods=1) summed = rolling.sum() print(summed) 0 368880.0 2 766560.0 4 1038831.0 6 962325.0 8 963163.0 窓関数での集計関数について OVERの前に記述する集計関数(SUMやCOUNTなど)は一通り利用することができるそうです。 集計関数の他にもランキング関数や個別の値に対する関数などが窓関数では利用することができます。これらは別途後々の節で触れていきます。 Pythonでの集計関数について Python(Pandas)での窓関数での集計関数はcountやsum、meanなど基本的なものに加えて他にも様々な関数が用意されていたり、applyで独自の関数やラムダ式などを指定できるようです。一覧に関して詳しくは以下のPandasのドキュメントをご確認ください。 Rolling window functions PARTITION BYによるデータの区切り設定 PARTITION BYはGROUP BYによるグループ化のカラム指定と似たような挙動をします。両方ともキーワードの後にカラム名を指定します(例 : PARTITION BY date)。窓関数の処理はここで指定され、データ区切りが指定されたカラムに対して実行されます。 例えば端末種別(device_typeカラム)ごとに処理をしたい場合は以下のようにPARTITION BY device_typeと書きます。 SELECT device_type, dt, sales, SUM(sales) OVER(PARTITION BY device_type) AS summed_sales_2 FROM athena_workshop.total_sales_per_device_daily ORDER BY dt, device_type 省略した場合は全行が1つのまとまりとして扱われます。例えば以下のようにOVER内の記述を省略してSUM関数を使うと全行の合計値を取得することができます。通常の集計関数やGROUP BYなどを絡めた場合と異なり他のカラムなども同時に表示することができます。 SELECT device_type, dt, sales, SUM(sales) OVER() AS summed_sales_2 FROM athena_workshop.total_sales_per_device_daily ORDER BY dt, device_type Pythonでの書き方 他にも色々やり方はあると思いますが、Pandasでやる場合は一例としてスライスしてしまうのがシンプルかもしれません。 特定カラム(シリーズ)のuniqueメソッドで一意な値が取れるので以下のコードではそちらでスライスを行っています。 from typing import List import pandas as pd import numpy as np date_range = pd.date_range(start='2021-01-01', end='2021-01-05') dfs: List[pd.DataFrame] = [] for date in date_range: df: pd.DataFrame = pd.read_json( 'https://github.com/simon-ritchie/athena_workshop/blob/main/workshop/total_sales_per_device_daily/' f"dt%3D{date.strftime('%Y-%m-%d')}/data.json.gz?raw=true", lines=True, compression='gzip') dfs.append(df) df = pd.concat(dfs, ignore_index=True) dfs: List[pd.DataFrame] = [] unique_vals: np.ndarray = df['device_type'].unique() for unique_val in unique_vals: dfs.append(df[df['device_type'] == unique_val]) print(dfs[0]) print(dfs[1]) date device_type sales 0 2021-01-01 1 368880 2 2021-01-02 1 397680 4 2021-01-03 1 272271 6 2021-01-04 1 292374 8 2021-01-05 1 398518 date device_type sales 1 2021-01-01 2 399620 3 2021-01-02 2 430820 5 2021-01-03 2 307029 7 2021-01-04 2 497826 9 2021-01-05 2 288582 ORDER BYによるデータ区切り内でのソート条件の指定 OVER内ではORDER BYでソートの指定を行うことができます。使い方は通常のORDER BYと同じような感じで、ORDER BY <カラム名>といったように指定していきます。降順にしたい場合には最後にDESCと付ける点も同様です。ただし通常のORDER BYと異なり、PARTITION BYによって区切られたデータの単位ごとにソートが実行されます。 以下のSQLでは日付別のデータの区切り(PARTITION BY dt)ごとに売り上げの降順でソート(ORDER BY sales DESC)を行っています。結果はFIRST_VALUEというデータの区切りの中で先頭の値を取得する関数を使っています(この関数については後の節で触れます)。 SELECT device_type, dt, sales, FIRST_VALUE(sales) OVER(PARTITION BY dt ORDER BY sales DESC) AS first_value FROM athena_workshop.total_sales_per_device_daily ORDER BY dt, device_type 実行してみると、各日付内で一番高い売り上げの値がfirst_valueカラムとして設定されていることを確認することができます。 Pythonでの書き方 前節くらいのソートであれば、スライスを行う形でデータ区切りを設定する場合は事前にsort_valuesメソッドなどでソートするなどで対応ができます。元のデータフレームをそのまま保持したければコピーを取ったり、もしくはデータ区切り数が少なければスライス後のデータフレームに対してソートする形でもいいかもしれません。 from typing import List import pandas as pd import numpy as np date_range = pd.date_range(start='2021-01-01', end='2021-01-05') dfs: List[pd.DataFrame] = [] for date in date_range: df: pd.DataFrame = pd.read_json( 'https://github.com/simon-ritchie/athena_workshop/blob/main/workshop/total_sales_per_device_daily/' f"dt%3D{date.strftime('%Y-%m-%d')}/data.json.gz?raw=true", lines=True, compression='gzip') dfs.append(df) df = pd.concat(dfs, ignore_index=True) df.sort_values(by='sales', ascending=False, inplace=True) dfs: List[pd.DataFrame] = [] unique_vals: np.ndarray = df['device_type'].unique() for unique_val in unique_vals: dfs.append(df[df['device_type'] == unique_val]) print(dfs[0]) date device_type sales 7 2021-01-04 2 497826 3 2021-01-02 2 430820 1 2021-01-01 2 399620 5 2021-01-03 2 307029 9 2021-01-05 2 288582 フレームの設定 フレームの指定は(PARTITION BYによる)データ区切りを設定した範囲のデータで、どの行を対象とするか・・・といった追加の条件を設定することができます。例えばデータ区切り内のデータで2行目~5行目を対象とするとか、数値が50~100の値のみを対象にするとか、もしくは対象行の値の前後2日間に該当する行のみを対象にする・・・といったような細かい制御ができます。 フレームの指定はORDER BYなどの後に記述します。基本的に行範囲の算出などの都合で行の順番が大切になってきたりもするのとAthenaではORDER BYを指定しないとクエリの度に行の順番が変わったりするためフレームを使用する場合はOVER内でORDER BYの指定を省略せずに指定する形が無難かと思われます。例えば<集計などの関数> OVER(PARTITION BY <カラム名> ORDER BY <カラム名> <フレーム設定>)といったように書きます。 フレーム設定には行範囲(例 : 対象のデータ区切り内の2行目から5行目など)を設定するROWSと値の範囲(例 : 前日~翌日の範囲を満たす行全てなど)を設定するRANGEの2種類が存在します。 ROWSもしくはRANGEの後には、開始値の設定のみを行う場合もしくは開始値と終了値の範囲の指定の2パターンが存在します。 例えば開始値側のみをROWSで設定する場合にはROWS <開始値の設定>といったように書き、開始値と終了値の範囲を指定する場合にはROWS BETWEEN <開始値の設定> AND <終了値の設定>といったようにBETWEENとANDが必要になります。 開始値と終了値の指定に関しては以下のように5パターンが存在します。 UNBOUNDED PRECEDING: 対象のデータ区切り内の先頭の行 <n行> PRECEDING: 現在の行からn行分前の行(ROWSの場合のみ使用可) CURRENT ROW: 現在行 <n行> FOLLOWING: 現在の行からn行分前の行(ROWSの場合のみ使用可) UNBOUNDED FOLLOWING: 対象のデータ区切り内の最後の行 <n行> PRECEDINGと<n行> FOLLOWINGという書き方は他のDB(MySQLやOracleなど)ではRANGEでも利用ができると書かれている記事があり恐らく使えるのですが、Athena(Presto)ではROWSでのみ使えるという記述がPrestoのドキュメントにあるためRANGEでは使えません。 色々要素が多いので、以降の節でSQLを実行しつつ個別に細かく見ていきます。 Pythonでの書き方 この辺のフレーム制御に関しては・・・色々やり方があってどんな対応だとシンプルになるか自信がありませんが、シンプルにn行ずつの処理が必要というケースであれば前節までで触れたrollingメソッドの引数で対応ができます。 他の要件が必要な場合にはapplyやループを回したりなど他の制御を使って色々と対応する必要が出てくるかもしれません。要件次第かなと思います。 現在行の値をCURRENT ROWで取ってみる 試しにCURRENT ROWを指定して現在行の値を取ってみます。現在行の値は窓関数関係を使わなくても取れるので以下のSQL自体は何かに使える・・・というものではないのですが、挙動の確認用となります。 なお、このCURRENT ROW単体での指定(BETWEENなどを使わない指定)では「開始値の位置」の指定となるため、先頭の行(現在行)のみを取得するためFIRST_VALUE関数を使っています。 SELECT device_type, dt, sales, FIRST_VALUE(sales) OVER(PARTITION BY device_type ORDER BY dt ROWS CURRENT ROW) AS current_row_sales FROM athena_workshop.total_sales_per_device_daily ORDER BY dt, device_type 現在行の値(salesカラム)と窓関数で取った現在行の値(current_row_salesカラム)の値が一致していることが確認できます。 ROWS ... PRECEDINGを使って2行前の値を取得してみる 今度はROWS ... PRECEDINGを使って2行前の値を取得してみます。2行前の値を取りたいのでFIRST_VALUE関数とROWS 2 PRECEDINGという書き方を組み合わせています。 SELECT device_type, dt, sales, FIRST_VALUE(sales) OVER(PARTITION BY device_type ORDER BY dt ROWS 2 PRECEDING) AS window_func_sales FROM athena_workshop.total_sales_per_device_daily ORDER BY device_type, dt 2行前の参照ができない1行目と2行目はどうなるのだろう・・・と思いましたが、どうやらそのような行ではNULLなどにはならずに遡れる位置の行の値が設定されるようです。例えば1行目であれば1行目の値、2行目であれば1行目の値、3行目であれば1行目の値、4行目であれば2行目の値・・・といったようになるようです。 ROWS UNBOUNDED PRECEDING で最初の行の値を取得してみる こちらも特に何か分析に使う・・・というものでもありませんが、挙動の確認用にUNBOUNDED PRECEDINGでデータ区切り内の最初の行の値を取得してみます。 SELECT device_type, dt, sales, FIRST_VALUE(sales) OVER(PARTITION BY device_type ORDER BY dt ROWS UNBOUNDED PRECEDING) AS window_func_sales FROM athena_workshop.total_sales_per_device_daily ORDER BY device_type, dt この処理は特に違和感が無く、window_func_salesカラムの各行の値が最初の行の368880という値になっていることが確認できます。 UNBOUNDED FOLLOWING はBETWEENとセットで使わないとエラーになる UNBOUNDED FOLLOWINGはBETWEENと一緒に使わないとエラーになってしまいます。正確には範囲の終了値側(BETWEEN ... AND ...のANDの後)で使わないとエラーになります。 SELECT device_type, dt, sales, FIRST_VALUE(sales) OVER(PARTITION BY device_type ORDER BY dt ROWS UNBOUNDED FOLLOWING) AS window_func_sales FROM athena_workshop.total_sales_per_device_daily ORDER BY device_type, dt ROWS BETWEEN で前後の行も含めた合計を出してみる ROWS BETWEEN ... AND ...で指定した行の範囲の合計を計算して挙動を確認してみます。この節のサンプルでは現在行に加えて前の1行分と後の1行分を対象とし、合計3行で計算するようにしてみます。 1行前を対象とするにはANDの前に1 PRECEDINGという記述が必要になり、1行後を対象とするにはANDの後に1 FOLLOWINGという記述が必要になります。また、合計を出すため関数部分をSUM(sales)としています。 SELECT device_type, dt, sales, SUM(sales) OVER(PARTITION BY device_type ORDER BY dt ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) AS window_func_sales FROM athena_workshop.total_sales_per_device_daily ORDER BY device_type, dt 1行目を見てみると、1行目と2行目のみの合算となっており1行目のwindow_func_salesカラムの値だけ低くなっています。2行目からは1行目・2行目・3行目…といったように3行分の合計にちゃんとなっているようです。平均などであればあまり問題にはならなそうですが、PRECEDINGを使いつつ合計などを計算する場合には最初の方の行は要件によっては少し注意する必要がありそうです。 フレームのデフォルトの挙動 最初誤解していたのですが、ORDER BYを使った場合フレームの指定を省略した場合データ区切りの最初(UNBOUNDED PRECEDING)~現在の行(CURRENT ROW)という挙動になるそうです。データ区切りの終わりは最後の行にはならず現在の行で止まってしまうのでフレームの指定を省略する場合には注意が必要です。 紛らわしいことに窓関数内でORDER BYを使わなかった場合にはデータ区切りの最初(UNBOUNDED PRECEDING)~データ区切りの最後の行(UNBOUNDED FOLLOWING)になる・・・と挙動が変わるようです。うっかりしているとミスしそうなので毎回フレームを明示する・・・とかでもいいかもしれません。 試しにCOUNT関数で各データ区切り内の行数をカウントしつつフレームの指定を省略した場合、以下のようにPARTITION BYで区切った範囲でも各行数が一致せず下にいくほど行数が増えていっています。 SELECT device_type, dt, sales, COUNT(*) OVER(PARTITION BY device_type ORDER BY dt) AS window_func_sales FROM athena_workshop.total_sales_per_device_daily ORDER BY device_type, dt PARTITION BYで指定した区切りごとに各行数を統一したい場合(データ区切り全体で統一したい場合)はBETWEENによるUNBOUNDED PRECEDINGとUNBOUNDED FOLLOWINGでのフレームの指定が必要になります。 SELECT device_type, dt, sales, COUNT(*) OVER(PARTITION BY device_type ORDER BY dt ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS window_func_sales FROM athena_workshop.total_sales_per_device_daily ORDER BY device_type, dt また、ORDER BYを窓関数内で使用していない場合には行数はフレームの指定を省略してもデータ区切り全体が対象になってくれます。 SELECT device_type, dt, sales, COUNT(*) OVER(PARTITION BY device_type) AS window_func_sales FROM athena_workshop.total_sales_per_device_daily ORDER BY device_type, dt 移動平均を計算してみる ここまでに学んだことを利用して移動平均を計算してみます。対象の日付に対して前方方向に7日分を対象として平均を取ります。例えば2021-01-07の日付であれば2021-01-01~2021-01-07の7日間の平均で計算されるようにします。 7日分の行を確保するには6日前~現在の行の日付という指定が必要になるためROWS BETWEEN ...を使い、開始行は6 PRECEDING、終了行はCURRENT ROWとなります。 SELECT device_type, dt, sales, AVG(sales) OVER(PARTITION BY device_type ORDER BY dt ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS window_func_sales FROM athena_workshop.total_sales_per_device_daily ORDER BY device_type, dt 2021-01-07の日付の部分を見てみると361299.0になっています。2021-01-01~2021-01-07で計算してみると(368880 + 397680 + 272271 + 292374 + 398518 + 425370 + 374000) / 7 = 361299.0となるので合っていそうです。 Pythonでの書き方 古いPandasバージョンではrolling_mean関数があったようですが、0.18.0のバージョン以降では切り落としになっているようです。 rolling(n).mean()とする必要があります。 from typing import List import pandas as pd import numpy as np date_range = pd.date_range(start='2021-01-01', end='2021-01-14') dfs: List[pd.DataFrame] = [] for date in date_range: df: pd.DataFrame = pd.read_json( 'https://github.com/simon-ritchie/athena_workshop/blob/main/workshop/total_sales_per_device_daily/' f"dt%3D{date.strftime('%Y-%m-%d')}/data.json.gz?raw=true", lines=True, compression='gzip') dfs.append(df) df = pd.concat(dfs, ignore_index=True) ios_df: pd.DataFrame = df[df['device_type'] == 1].copy() ios_df.sort_values(by='date', inplace=True) mean: pd.Series = ios_df['sales'].rolling(window=7, min_periods=1).mean() print(mean) 0 368880.000000 2 383280.000000 4 346277.000000 6 332801.250000 8 345944.600000 10 359182.166667 12 361299.000000 14 354562.857143 16 343387.857143 ... RANGEでの<n> PRECEDINGや<n> FOLLOWINGはエラーになる 前節で少し触れたように、Athena(Presto)ではRANGEでは<n> PRECEDINGや<n> FOLLOWINGといったような数値や日付範囲の指定による書き方がサポートされていません。結構便利そうなのですが・・・。今後のPrestoなどのアップデートに期待です。 UNBOUNDED PRECEDINGやCURRENT ROW、UNBOUNDED FOLLOWINGなどはサポートされていますが、一方でそれらを使う場合はROWSを使っても同じことでできる?気がするので現状RANGEを使うケースがあまり無い・・・?感じも少ししています(ぱっと浮かばないだけで、良く考えたら何かRANGEじゃないと対応ができないケースがあるかもしれませんが・・・)。 たとえば以下のようにRANGEと<n> PRECEDINGなどを組み合わせて使ってみるとエラーになることを確認できます。 SELECT total_sales, power, AVG(total_sales) OVER(PARTITION BY power ORDER BY power RANGE BETWEEN 1000 PRECEDING AND 1000 FOLLOWING) FROM athena_workshop.user_total_sales_and_power WHERE total_sales != 0 ORDER BY power 現状で特定の値の範囲でデータ区切りをしたい場合は、事前にWITH句や一時テーブル(中間テーブル)を作ったりして区切り用の種別のようなカラムを設けておいて、そちらのカラムをPARTITION BYで指定する・・・といった対応になりそうです。 窓関数で追加で使える関数 窓関数では前節までで色々触れてきたように集計用の関数(COUNTやSUM)などは一通り使えます。加えて窓関数専用のランキング関数(Ranking Functions)や単一値用の関数(Value Functions)が用意されています。この節以降ではそれらの各関数について触れていきます。 データ区切り内の先頭の値を取得する: FIRST_VALUE 前節まででも何度か先行して使っていましたが、FIRST_VALUE関数ではデータ区切り内で先頭の値を取得できます。 以下のSQLでは端末種別(device_type)ごとの売り上げのデータ区切りで、先頭の行の売り上げの値をFIRST_VALUE関数で取得しています。window_func_salesカラムの各値が最初の行の368880になっていることを確認できます。 SELECT device_type, dt, sales, FIRST_VALUE(sales) OVER(PARTITION BY device_type ORDER BY dt) AS window_func_sales FROM athena_workshop.total_sales_per_device_daily ORDER BY device_type, dt Pythonでの書き方 rollingメソッドの返却値はapplyメソッドを持っているのでそちらを利用して対応ができます。applyメソッドへは窓関数のデータ区切りごとのシリーズが渡されるので、x.values[0]といった記述で先頭の値が取れます(処理が遅いかもしれませんがx.iloc[0]などでも同じような制御にはなります)。 from typing import List import pandas as pd import numpy as np date_range = pd.date_range(start='2021-01-01', end='2021-01-10') dfs: List[pd.DataFrame] = [] for date in date_range: df: pd.DataFrame = pd.read_json( 'https://github.com/simon-ritchie/athena_workshop/blob/main/workshop/total_sales_per_device_daily/' f"dt%3D{date.strftime('%Y-%m-%d')}/data.json.gz?raw=true", lines=True, compression='gzip') dfs.append(df) df = pd.concat(dfs, ignore_index=True) ios_df: pd.DataFrame = df[df['device_type'] == 1].copy() ios_df.sort_values(by='date', inplace=True) first_vals: pd.Series = ios_df['sales'].rolling( window=7, min_periods=1).apply(lambda x: x.values[0]) print(first_vals) window=7としているので7行目までは同じ値(先頭の値が同じ)で、8行目から値が変わっていることを確認することができます。 0 368880.0 2 368880.0 4 368880.0 6 368880.0 8 368880.0 10 368880.0 12 368880.0 14 397680.0 16 272271.0 18 292374.0 データ区切り内の最後の値を取得する: LAST_VALUE LAST_VALUE関数はFIRST_VALUE関数とは逆にデータ区切りの中の最後の行の値を取得できます。前の節で触れたように、窓関数内でORDER BYを指定した場合にはフレームの指定が無いとデータ区切りが最後の行までになってくれないのでその辺のフレームの指定(ROWS BETWEEN ...)を行っています。 SELECT device_type, dt, sales, LAST_VALUE(sales) OVER(PARTITION BY device_type ORDER BY dt ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS window_func_sales FROM athena_workshop.total_sales_per_device_daily ORDER BY device_type, dt Pythonでの書き方 FIRST_VALUEの節のPythonコードのapplyメソッド部分のインデックス参照を0から-1に変更するだけです。 from typing import List import pandas as pd import numpy as np date_range = pd.date_range(start='2021-01-01', end='2021-01-10') dfs: List[pd.DataFrame] = [] for date in date_range: df: pd.DataFrame = pd.read_json( 'https://github.com/simon-ritchie/athena_workshop/blob/main/workshop/total_sales_per_device_daily/' f"dt%3D{date.strftime('%Y-%m-%d')}/data.json.gz?raw=true", lines=True, compression='gzip') dfs.append(df) df = pd.concat(dfs, ignore_index=True) ios_df: pd.DataFrame = df[df['device_type'] == 1].copy() ios_df.sort_values(by='date', inplace=True) last_vals: pd.Series = ios_df['sales'].rolling( window=7, min_periods=1).apply(lambda x: x.values[-1]) print(last_vals) 0 368880.0 2 397680.0 4 272271.0 6 292374.0 8 398518.0 10 425370.0 12 374000.0 14 321727.0 16 319455.0 18 283038.0 データ区切り内の任意の位置の値を取得する: NTH_VALUE NTH_VALUE関数は引数に指定されたn番目の窓関数のデータ区切り内の値を取得します。第一引数には対象のカラム、第二引数に位置の整数を指定します。第二引数は1からスタートします。 以下のSQLでは第二引数に3を指定しているため、3行目の272271の値にwindow_func_salesカラムの値がなっています。 SELECT device_type, dt, sales, NTH_VALUE(sales, 3) OVER(PARTITION BY device_type ORDER BY dt ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS window_func_sales FROM athena_workshop.total_sales_per_device_daily ORDER BY device_type, dt Pythonでの書き方 FIRST_VALUEやLAST_VALUE関数の節のコードのインデックス参照でのインデックス指定を任意の整数値(n)に書き換えることで対応ができます。 例えば3番目の値であればPython(NumPy)ではインデックスが0からスタートするのでインデックスには2を指定します。 また、n番目の値が存在しなければnanを返却するようにしています。 from typing import Any, List import pandas as pd import numpy as np date_range = pd.date_range(start='2021-01-01', end='2021-01-10') dfs: List[pd.DataFrame] = [] for date in date_range: df: pd.DataFrame = pd.read_json( 'https://github.com/simon-ritchie/athena_workshop/blob/main/workshop/total_sales_per_device_daily/' f"dt%3D{date.strftime('%Y-%m-%d')}/data.json.gz?raw=true", lines=True, compression='gzip') dfs.append(df) df = pd.concat(dfs, ignore_index=True) ios_df: pd.DataFrame = df[df['device_type'] == 1].copy() ios_df.sort_values(by='date', inplace=True) def nth_value(x: pd.Series) -> Any: n: int = 3 if len(x) < n: return np.nan return x.values[n - 1] nth_vals: pd.Series = ios_df['sales'].rolling( window=7, min_periods=1).apply(nth_value) print(nth_vals) 0 NaN 2 NaN 4 272271.0 6 272271.0 8 272271.0 10 272271.0 12 272271.0 14 292374.0 16 398518.0 18 425370.0 現在の行から任意の行数だけ後の位置の値を取得する: LEAD LEAD関数はNTH_VALUE関数に少し似ていますが、基準位置がデータ区切りの先頭ではなく現在の行になり、現在の行から第二引数に指定された整数分の後の行の値を取得することができます。 引数はNTH_VALUEと同じように第一引数が対象のカラム、第二引数がオフセット分の整数(いくつ後の行を参照するかの整数)となります。第二引数は0からスタートで、0を指定した場合は現在の行の値がそのまま返却されます。 以下のSQLではデータ区切り内で1行後の行の値を取得しています。 SELECT device_type, dt, sales, LEAD(sales, 1) OVER(PARTITION BY device_type ORDER BY dt ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS window_func_sales FROM athena_workshop.total_sales_per_device_daily ORDER BY device_type, dt また、第二引数を省略した場合にはデフォルト値として1が設定されます(1行後の値が取得されます)。 SELECT device_type, dt, sales, LEAD(sales) OVER(PARTITION BY device_type ORDER BY dt ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS window_func_sales FROM athena_workshop.total_sales_per_device_daily ORDER BY device_type, dt 結果は第二引数に1を指定した時と同じになります。 第三引数には値が取れない行に対するデフォルト値を指定することもできます(例 : 最後の行は次の行の値が存在しないためNULLとなるため、第三引数を指定しているとその値でNULLが補正されます)。この辺は次のLAG関数で説明がしやすいので触れていきます。 Pythonでの書き方 rollingを使わずにスライスでPARTITION BYのような制御をしたとして、例のごとく色々書き方はありますが数行後の値を取りたい場合の書き方の一例としては以下のコードでは データフレームのカラムにインデックスを設定する(事前にreset_indexでインデックスをリセットしておく) インデックスのカラムでapplyメソッドを使い、且つapplyメソッドではキーワード引数なども指定できるので対象の配列を指定する n行後の行が存在すればその値、存在しなければnanを返却というようにapplyメソッドで指定した関数内を実装する といった形で対応しています。 from typing import Any, List import pandas as pd import numpy as np date_range = pd.date_range(start='2021-01-01', end='2021-01-10') dfs: List[pd.DataFrame] = [] for date in date_range: df: pd.DataFrame = pd.read_json( 'https://github.com/simon-ritchie/athena_workshop/blob/main/workshop/total_sales_per_device_daily/' f"dt%3D{date.strftime('%Y-%m-%d')}/data.json.gz?raw=true", lines=True, compression='gzip') dfs.append(df) df = pd.concat(dfs, ignore_index=True) ios_df: pd.DataFrame = df[df['device_type'] == 1].copy() ios_df.sort_values(by='date', inplace=True) def lead(index: int, values: np.ndarray) -> Any: n: int = 3 if len(values) - 1 < n + index: return np.nan return values[n + index] sales_vals: np.ndarray = ios_df['sales'].values ios_df.reset_index(drop=True, inplace=True) ios_df['index'] = ios_df.index lead_vals: pd.Series = ios_df['index'].apply(lead, values=sales_vals) print('original df:\n', ios_df) print('lead values:\n', lead_vals) original df: date device_type sales index 0 2021-01-01 1 368880 0 1 2021-01-02 1 397680 1 2 2021-01-03 1 272271 2 3 2021-01-04 1 292374 3 4 2021-01-05 1 398518 4 5 2021-01-06 1 425370 5 6 2021-01-07 1 374000 6 7 2021-01-08 1 321727 7 8 2021-01-09 1 319455 8 9 2021-01-10 1 283038 9 lead values: 0 292374.0 1 398518.0 2 425370.0 3 374000.0 4 321727.0 5 319455.0 6 283038.0 7 NaN 8 NaN 9 NaN 現在の行から任意の行数だけ前の位置の値を取得する: LAG LAG関数はLEADとは逆の挙動をする関数で、現在の行から第二引数で指定された整数分前の行の値を取得できます。 他の引数の構成や挙動はLEAD関数と同様です。第一引数は対象のカラム、第二引数に何行前の値を参照するかの値(デフォルト値は1で省略可)、第三引数は参照する前の行が存在しない場合の欠損値(NULL)の場合の補正値としてのデフォルト値となります。 以下のSQLでは第二引数に2を指定して、2行前の値を取得しています。 SELECT device_type, dt, sales, LAG(sales, 2) OVER(PARTITION BY device_type ORDER BY dt ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS window_func_sales FROM athena_workshop.total_sales_per_device_daily ORDER BY device_type, dt 2行前の行が存在しない場合にはNULLとなるため、1行目と2行目の値がNULL(UI上では空)になっていることが確認できます。 値が取れない行に対してNULLではなく別の値を指定したい場合には第三引数に値を設定するとその値で補正されます。以下のSQLでは値が取れない行に関しては0を設定しています。 SELECT device_type, dt, sales, LAG(sales, 2, 0) OVER(PARTITION BY device_type ORDER BY dt ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS window_func_sales FROM athena_workshop.total_sales_per_device_daily ORDER BY device_type, dt Pythonでの書き方 LEAD関数の節と似たような感じですが、こちらはn行分前にインデックスをした際に対象インデックスが0未満になった場合にはnanを返却する形にapplyで指定した関数の内容を調整しています。 from typing import Any, List import pandas as pd import numpy as np date_range = pd.date_range(start='2021-01-01', end='2021-01-10') dfs: List[pd.DataFrame] = [] for date in date_range: df: pd.DataFrame = pd.read_json( 'https://github.com/simon-ritchie/athena_workshop/blob/main/workshop/total_sales_per_device_daily/' f"dt%3D{date.strftime('%Y-%m-%d')}/data.json.gz?raw=true", lines=True, compression='gzip') dfs.append(df) df = pd.concat(dfs, ignore_index=True) ios_df: pd.DataFrame = df[df['device_type'] == 1].copy() ios_df.sort_values(by='date', inplace=True) def lag(index: int, values: np.ndarray) -> Any: n: int = 3 if len(values) - 1 < n + index: return np.nan return values[n + index] sales_vals: np.ndarray = ios_df['sales'].values ios_df.reset_index(drop=True, inplace=True) ios_df['index'] = ios_df.index lead_vals: pd.Series = ios_df['index'].apply(lag, values=sales_vals) print('original df:\n', ios_df) print('lead values:\n', lead_vals) original df: date device_type sales index 0 2021-01-01 1 368880 0 1 2021-01-02 1 397680 1 2 2021-01-03 1 272271 2 3 2021-01-04 1 292374 3 4 2021-01-05 1 398518 4 5 2021-01-06 1 425370 5 6 2021-01-07 1 374000 6 7 2021-01-08 1 321727 7 8 2021-01-09 1 319455 8 9 2021-01-10 1 283038 9 0 292374.0 1 398518.0 2 425370.0 3 374000.0 4 321727.0 5 319455.0 6 283038.0 7 NaN 8 NaN 9 NaN データ区切り内の行番号を取得する: ROW_NUMBER ROW_NUMBER関数はデータ区切りの中の行数を取得します。引数は特に無く、返却される行数は1以降で設定されます。 SELECT device_type, dt, sales, ROW_NUMBER() OVER(PARTITION BY device_type ORDER BY dt) AS row_number FROM athena_workshop.total_sales_per_device_daily ORDER BY device_type, dt Pythonでの書き方 PandasではSQLと異なり行番号の割り振りなどはインデックスを参照するだけなので、PARTITION BY的なことをしたければスライス → 行番号を取りたければreset_indexでインデックスを割り振り直した後にindex属性にアクセスすれば連番が取れます(SQLと異なり0からスタートします)。カラムに設定したければdf['<カラム名>'] = df.indexとすれば設定することができます。 from typing import Any, List import pandas as pd import numpy as np date_range = pd.date_range(start='2021-01-01', end='2021-01-05') dfs: List[pd.DataFrame] = [] for date in date_range: df: pd.DataFrame = pd.read_json( 'https://github.com/simon-ritchie/athena_workshop/blob/main/workshop/total_sales_per_device_daily/' f"dt%3D{date.strftime('%Y-%m-%d')}/data.json.gz?raw=true", lines=True, compression='gzip') dfs.append(df) df = pd.concat(dfs, ignore_index=True) ios_df: pd.DataFrame = df[df['device_type'] == 1].copy() ios_df.sort_values(by='date', inplace=True) ios_df.reset_index(drop=True, inplace=True) ios_df['row_number'] = ios_df.index print(ios_df) date device_type sales row_number 0 2021-01-01 1 368880 0 1 2021-01-02 1 397680 1 2 2021-01-03 1 272271 2 3 2021-01-04 1 292374 3 4 2021-01-05 1 398518 4 データ区切り内のランク(順位)を取得する: RANK, DENSE_RANK RANK関数は窓関数内のORDER BYで指定した順番でのランキングを割り振ります。ROW_NUMBERに似た挙動をし、結果は1から割り振られます。ただし全体の行の順番(窓関数ではない最後の方のORDER BY)の指定によって、窓関数内の順番と結果の順番が異なっていてもランキングの順番を計算することができます。 SELECT device_type, dt, sales, RANK() OVER(PARTITION BY device_type ORDER BY sales DESC ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS ranking FROM athena_workshop.total_sales_per_device_daily ORDER BY device_type, sales DESC RANKに似た関数としてDENSE_RANKという関数も存在します。基本的な使い方は同じなのですが、RANKに関しては同値の複数の行があった場合にはそれらのランキングは同じになり、その次の行のランキングが同値の行数分飛んだ値になります。 例えば1位と2位が100で同値、3位が80だった場合には1位と2位は両方ともRANK関数の値は1となり、3位のRANK関数の値は3位となります。2位の行は欠落します。 一方でDENSE_RANK関数の方は2位の行が欠落したりせずに、1位と2位が1となり3位が2となります。間の欠落した順位が詰められる形となります。 Pythonでの書き方 Pandasでランキングの値を取る場合にはrankメソッドを使うとシンプルです。RANK関数に合わせるなら引数にmethod='min'と指定します。また、降順で計算するにはascending=Falseと引数を指定します。 ※以下のコードでは動作確認のサンプルとして2行目と4行目を同じ値(350000)にしています。 from typing import List import pandas as pd date_range = pd.date_range(start='2021-01-01', end='2021-01-05') dfs: List[pd.DataFrame] = [] for date in date_range: df: pd.DataFrame = pd.read_json( 'https://github.com/simon-ritchie/athena_workshop/blob/main/workshop/total_sales_per_device_daily/' f"dt%3D{date.strftime('%Y-%m-%d')}/data.json.gz?raw=true", lines=True, compression='gzip') dfs.append(df) df = pd.concat(dfs, ignore_index=True) ios_df: pd.DataFrame = df[df['device_type'] == 1].copy() ios_df.reset_index(drop=True, inplace=True) ios_df.at[1, 'sales'] = 350000 ios_df.at[3, 'sales'] = 350000 ios_df['rank'] = ios_df['sales'].rank(method='min', ascending=False) print(ios_df) date device_type sales rank 0 2021-01-01 1 368880 2.0 1 2021-01-02 1 350000 3.0 2 2021-01-03 1 272271 5.0 3 2021-01-04 1 350000 3.0 4 2021-01-05 1 398518 1.0 DENSE_RANK関数に合わせたい場合にはmethod='dense'と引数に指定します。 from typing import List import pandas as pd date_range = pd.date_range(start='2021-01-01', end='2021-01-05') dfs: List[pd.DataFrame] = [] for date in date_range: df: pd.DataFrame = pd.read_json( 'https://github.com/simon-ritchie/athena_workshop/blob/main/workshop/total_sales_per_device_daily/' f"dt%3D{date.strftime('%Y-%m-%d')}/data.json.gz?raw=true", lines=True, compression='gzip') dfs.append(df) df = pd.concat(dfs, ignore_index=True) ios_df: pd.DataFrame = df[df['device_type'] == 1].copy() ios_df.reset_index(drop=True, inplace=True) ios_df.at[1, 'sales'] = 350000 ios_df.at[3, 'sales'] = 350000 ios_df['dense_rank'] = ios_df['sales'].rank(method='dense', ascending=False) print(ios_df) date device_type sales dense_rank 0 2021-01-01 1 368880 2.0 1 2021-01-02 1 350000 3.0 2 2021-01-03 1 272271 4.0 3 2021-01-04 1 350000 3.0 4 2021-01-05 1 398518 1.0 ランクの比率値を取得する: PERCENT_RANK PERCENT_RANK関数はRANK関数で取れるランキングの値を使ったパーセンテージを取得することができます。関数内容としては以下のような計算になります。分子側はRANK関数で取れる値となります。 \frac{関数によるランキングの値 - 1}{データ区切り全体の件数 - 1} SELECT device_type, dt, sales, RANK() OVER(PARTITION BY device_type ORDER BY sales DESC ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS ranking, PERCENT_RANK() OVER(PARTITION BY device_type ORDER BY sales DESC ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS percent_ranking FROM athena_workshop.total_sales_per_device_daily ORDER BY device_type, sales DESC Pythonでの書き方 rankメソッドでpct=Trueと引数を指定することでパーセンテージ表示となります。シンプルで良いですね。 from typing import List import pandas as pd date_range = pd.date_range(start='2021-01-01', end='2021-01-05') dfs: List[pd.DataFrame] = [] for date in date_range: df: pd.DataFrame = pd.read_json( 'https://github.com/simon-ritchie/athena_workshop/blob/main/workshop/total_sales_per_device_daily/' f"dt%3D{date.strftime('%Y-%m-%d')}/data.json.gz?raw=true", lines=True, compression='gzip') dfs.append(df) df = pd.concat(dfs, ignore_index=True) ios_df: pd.DataFrame = df[df['device_type'] == 1].copy() ios_df['percent_rank'] = ios_df['sales'].rank(ascending=False, pct=True) print(ios_df) date device_type sales percent_rank 0 2021-01-01 1 368880 0.6 2 2021-01-02 1 397680 0.4 4 2021-01-03 1 272271 1.0 6 2021-01-04 1 292374 0.8 8 2021-01-05 1 398518 0.2 指定された件数ごとにデータ区切り内のデータを振り分ける: NTILE NTILE関数はデータ区切りごとのデータグループを引数に指定されたn個分にデータを振り分ける関数です。 例えば特定のデータ区切りに100個データがあってnを10とした場合、10個ずつのグループに分けられます(グループの番号のカラムが設定されます)。別のデータ区切りで50個のデータがあれば5個ずつのグループに分けられます。 第一引数にはnの整数が必要になり、返却値も整数となります(1以降で順番に設定されていきます)。 以下のSQLではnの引数に18を設定しています。データ区切りには90個の時系列データがあるので1グループ辺り5件となり、1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, ...という値になっていきます。 SELECT device_type, dt, sales, NTILE(18) OVER(PARTITION BY device_type ORDER BY sales DESC ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS tile FROM athena_workshop.total_sales_per_device_daily ORDER BY device_type, sales DESC Pythonでの書き方 Pandasのqcut関数で近いことができます。q引数にNTILE関数のnに該当する個数を指定し、且つlabels=Falseと引数を指定します(文字列のラベルではなくFalseを指定すると連番が割り振られます)。 xの引数には今回はデータフレームのインデックスを指定しています。qcut関数では指定されたシリーズなどの昇順などで順番に割り振られるようなので、今回は行の順番通りに割り振りたかったためインデックスを指定しています。 from typing import List import pandas as pd date_range = pd.date_range(start='2021-01-01', end='2021-01-10') dfs: List[pd.DataFrame] = [] for date in date_range: df: pd.DataFrame = pd.read_json( 'https://github.com/simon-ritchie/athena_workshop/blob/main/workshop/total_sales_per_device_daily/' f"dt%3D{date.strftime('%Y-%m-%d')}/data.json.gz?raw=true", lines=True, compression='gzip') dfs.append(df) df = pd.concat(dfs, ignore_index=True) ios_df: pd.DataFrame = df[df['device_type'] == 1].copy() ios_df.reset_index(drop=True, inplace=True) ios_df['ntile'] = pd.qcut(x=ios_df.index, q=5, labels=False) print(ios_df) date device_type sales ntile 0 2021-01-01 1 368880 0 1 2021-01-02 1 397680 0 2 2021-01-03 1 272271 1 3 2021-01-04 1 292374 1 4 2021-01-05 1 398518 2 5 2021-01-06 1 425370 2 6 2021-01-07 1 374000 3 7 2021-01-08 1 321727 3 8 2021-01-09 1 319455 4 9 2021-01-10 1 283038 4 データ区切り内での累積分布を取得する: CUME_DIST CUME_DIST関数は累積分布(cumulative distribution)を得ることができる関数です。 ここでは累積分布自体には説明は引用程度で詳しくは省きます。必要な方は検索などで色々記事がヒットしますのでそちらをご参照ください。 累積分布関数(るいせきぶんぷかんすう、英: cumulative distribution function, CDF)や分布関数(ぶんぷかんすう、英: distribution function)とは、確率論において、実数値確率変数 X が x 以下になる確率の関数のこと。連続型確率変数では、負の無限大から x まで確率密度関数を定積分したもの。 累積分布関数 - Wikipedia 引数は特に無く、DOUBLE型の浮動小数点数が返却されます。 SELECT date, device_type, sales, CUME_DIST() OVER(PARTITION BY device_type ORDER BY sales DESC ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS cume_dist FROM athena_workshop.total_sales_per_device_daily ORDER BY device_type, sales DESC Pythonでの書き方 そのままプロット・・・するのであればPandasのシリーズのhistメソッドでcumulative=True, density=1, bins=100と引数を指定するとシンプルそうです。 数値がデータフレームで欲しい場合はcumsumメソッドなどで何ステップか計算を挟む必要がありそうです。ここではリンクだけ貼って省きますので必要な場合はリンク先をご確認ください。 参考文献・参考サイトまとめ The Applied SQL Data Analytics Workshop: Develop your practical skills and prepare to become a professional data analyst, 2nd Edition Window Functions 分析関数(ウインドウ関数)をわかりやすく説明してみた 累積分布関数 - Wikipedia pandasで窓関数を適用するrollingを使って移動平均などを算出 Rolling window functions module 'pandas' has no attribute 'rolling_mean' pandas.DataFrame, Seriesを順位付けするrank pandasのcut, qcut関数でビニング処理(ビン分割) Plotting CDF of a pandas series in python
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む