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

コロナ・ショック後から株価上昇し続けている15銘柄

はじめに

新型コロナウイルス感染症が世界的に流行し始めたことによって,2020年2月末頃から3月にかけて,世界的に株価が大暴落しました(コロナ・ショック).日経平均株価は直前ピークと比べ,3割も暴落し約1万6千円の安値を付けました.

景気対策として行われた各国の中央銀行による大規模な金融緩和の影響もあり,5月以降株価の上昇傾向がはじまり,最近では日経平均株価は約2万8500円の値をつけ,30年5か月ぶりの高値となりました.

最近の高値の要因としては,米国の追加の財政出動などへの期待が高まっていることもあるようです(【日経新聞】日経平均、30年ぶり2万8000円台回復 米財政出動期待で).

このように,日経平均株価はコロナ・ショック後から現在にかけて,常に上昇し続けています.そこで,個別銘柄についても同じように常に価格上昇しているものは存在しないのかについて疑問に思いました.

今回の記事では,「コロナ・ショック後から株価上昇し続けている銘柄は存在するのか」を東京証券取引所に上場している全銘柄の中から調査しました.その結果,株価上昇し続けている銘柄は15銘柄存在することがわかりました.

さらに,その15銘柄のうち,コロナショック後営業利益が上昇し続けている,またはV字回復している銘柄を選定したところ,より今後の株価上昇が期待できる銘柄として10銘柄に絞ることができました.

株価上昇し続けている銘柄は?

【検証方法】
東京証券取引所に上場している全銘柄の株価データを,Yahoo FinanceのAPIを利用し,取得します.
・月ごとに株価上昇率を,データを取得した全銘柄について計算します.計算方法は,

株価上昇率=\frac{今月初日終値-前月初日終値}{前月初日終値}

です.株価上昇率の計算には休日を除く月初日の株価終値を利用しています.
・2020年5月から2021年1月まで (8か月間)の期間において,常に株価上昇率がプラスであるものを選択していきます.
※ データ取得,分析はすべてPythonでおこなっています.詳しい検証方法は記事の一番下の付録を参照してください.

【検証結果】
2020年5月から2021年1月までの期間において,株価が上昇し続けていた銘柄は15銘柄あることがわかりました.以下はその15銘柄のリストです.

  • 株価上昇し続けている15銘柄
証券コード 銘柄名 1か月の株価上昇率(%)
1400 ルーデン・ホールディングス 10.03
1431 Lib Work 13.95
2413 エムスリー 14.33
2484 出前館 16.72
3922 PR TIMES 19.12
3948 光ビジネスフォーム 4.44
4673 川崎地質 7.32
5217 テクノクオーツ 12.71
6365 電業社機械製作所 7.60
6391 加地テック 11.79
6563 みらいワークス 12.36
6622 ダイヘン 6.90
6723 ルネサスエレクトロニクス 13.37
7975 リヒトラブ 2.77
9468 KADOKAWA 12.0

PR TIMES,エムスリー,KADOKAWA,出前館(情報通信サービス業を展開)は特に高い株価上昇率となっており,株価上昇率だけを見ると,今後も株価上昇が大きく期待できる銘柄と言えるでしょう.そのほかには,建築業,機械,電気機器メーカーなど様々な業種がリスト入りしています.

以下のグラフは,各銘柄の株価上昇率の推移です.

図3.png

2-4月はコロナショックで,ほとんどの銘柄に関して,株価が大きく下落しています.しかし,その後,5月以降は常に株価が上がっていることがわかると思います.

特徴的なものとして,医療情報系ベンチャー企業であるエムスリー(2413)はコロナショックでも,ほとんど株価が下がることはなく,その後常に株価を上げてきました.ルネサスエレクトロニクス(6723)はコロナショックによって,40%以上株価を下げましたが,その後40%株価を上げ,コロナショック後,常に株価を上げてきました.出前館(7975)は9月の株価上昇率が60%を超えており,株価が急上昇しました.

さらに,15銘柄の営業利益の状況をみて,今後株価上昇が期待できそうな銘柄を考えていきます.

  • 四半期売上・営業利益の状況

さらに,株価上昇し続けている15銘柄の四半期売上・営業利益の状況です.

証券コード 銘柄名 売上
1Q
売上
2Q
売上
3Q
営業利益
1Q
営業利益
2Q
営業利益
3Q
1400 ルーデン・ホールディングス 722 577 623 35 -9 28
1431 Lib Work 962 1230 1011 -68 -77 -195
2413 エムスリー 34654 35478 39544 7404 11253 12678
2484 出前館 2994 3484 4228 -619 -1015 -3194
3922 PR TIMES 819 922 1028 277 354 472
3948 光ビジネスフォーム 1695 2045 1705 57 190 123
4673 川崎地質 2979 1549 1980 345 -58 39
5217 テクノクオーツ 2341 3020 3175 334 613 662
6365 電業社機械製作所 10875 2938 3517 2322 -152 53
6391 加地テック 2263 772 1234 242 2 177
6563 みらいワークス 1095 1014 1034 26 14 13
6622 ダイヘン 44770 30546 32485 4521 1150 2532
6723 ルネサスエレクトロニクス 178743 166672 178678 13313 17256 17249
7975 リヒトラブ 2466 1920 2006 210 47 129
9468 KADOKAWA 54476 47023 50530 -373 3581 4266

(単位:百万円)
1Q:1~3月,2Q:4~6月,3Q:7~9月(川崎地質のみ、1Q:3~5月、2Q:6~8月、3Q:9~11月)

以下は各銘柄の営業利益をグラフにしたものです.営業利益の絶対値が大きいものと小さいもので分けています.

画像1.png

画像2.png

表やグラフから,15銘柄にうち、営業利益が右肩上がり,または,V字回復している銘柄は以下の10銘柄です.

・ルーデン・ホールディングス
・エムスリー
・PR TIMES
・光ビジネスフォーム
・テクノクオーツ
・加地テック
・ダイヘン
・ルネサスエレクトロニクス
・リヒトラブ
・KADOKAWA

また,出前館は,上記で示したように,高い平均株価上昇率を示していましたが,今回提示したリストには含まれておらず,営業利益の状況を見てみると赤字が拡大し続けているので,危険な投資先とも言えるかもしれません.

おわりに

コロナショック後(2020年5月から2021年1月までの期間),東証に上場している全銘柄の中で,株価が上昇し続けていた銘柄を調査したところ,15銘柄あることがわかりました.さらに,そのうちの4半期の営業利益を調べたところ,10銘柄は右肩上がり,またはV字回復しているということがわかりました.その10銘柄は以下の通りです.

  • ルーデン・ホールディングス
  • エムスリー
  • PR TIMES
  • 光ビジネスフォーム
  • テクノクオーツ
  • 加地テック
  • ダイヘン
  • ルネサスエレクトロニクス
  • リヒトラブ
  • KADOKAWA

株価が上昇し続けている,営業利益が右肩上がり,またはV字回復であることを考えると,この10銘柄が今後も株価上昇が期待できます.
逆に,残りの5銘柄は上がり続けてはいますが,少し注意が必要でしょう。


<過去記事>
【5年分データ分析】ゴールデンクロスの数日後に株価は上がっているのか
日経225全銘柄の投資効率を検証
日経平均株価が上がった次の日に上がる銘柄を見つけたい
どの暗号資産が効率よく稼げるか
1株1千円以下の価格変動が大きい銘柄に投資してパフォーマンスをあげる
第1回緊急事態宣言のときに上がった銘柄TOP10を調査

付録(検証方法)

  • 準備

・フォルダ「data」を作成します.これは株価データを保存するための場所です.
東証上場銘柄一覧(2020年12月末)にあるdata_j.xlsをダウンロードします.data_j.xlsをエクセルなどで開き,csvファイル(data_j.csv)として,保存しなおします.Pythonではcsvファイルを読み込めるようにしているので,この操作をしています.

  • 株価時系列データ取得プログラム

今回はPythonのライブラリpandas_datareaderを用いて,Yahoo Financeからデータを取得します.以下のプログラムは,data_j.csvを参照して,東京証券取引所に上場している全銘柄の株価データを取得します.

全銘柄の時系列データを取得するので,かなり時間がかかります.途中でプログラムを止めても問題ないように,データ取得していない銘柄の時系列データのみダウンロードするように設計しています.したがって,タイムエラーなどによって,株価データ取得に失敗することがありますが,その場合はもう1度プログラムを実行すれば,その未取得データのみが取得できます.

get_data_price.py
import pandas as pd
import glob
import pandas_datareader.data as web

# 東証全銘柄リスト
def get_tokyo():
    tokyo = pd.read_csv("data_j.csv", engine="python", names=("date","code", "name","market", "CodeIndustry33", "ClassificationIndustry33", "CodeIndustry17", "ClassificationIndustry17", "CodeScale", "ClassificationScale"), skiprows=1, usecols=[1,2], encoding="utf-8")
    return tokyo

# 株価取得(日足) csv保存
def get_price_csv(stock_name,start='2015-1-1',end=None):
    data = web.DataReader(stock_name,"yahoo",start,end)
    data.to_csv("./data_price/"+stock_name+".csv")

def get_all(list_stock_name, dir_name, func, filetype='csv'):
    for code in list_stock_name.code:
        code = str(code)
        if not ('.T' in code):
            code+='.T'
        filename = '{}\\{}.{}'.format(dir_name, code, filetype)
        if filename in glob.glob(f"{dir_name}/*"):
            pass
        else:
            print(code, 'GET... ', end='')
            try:
                func(str(code))
                print('SUCCESS')
            except Exception as e:
                print('FAIL:', e)

if __name__ == '__main__':
    list_stock_name = get_tokyo()
    get_all(list_stock_name, './data_price', get_price_csv) # 株価取得

  • 分析プログラム

以下のプログラムは,ダウンロードした時系列データを読み込み,各月の株価上昇率を計算し,2020年5-2021年1月において,常に株価が上昇している銘柄を選択します.

最後に各月の株価上昇率,その平均値の結果がcsvファイルとして保存されます.このとき,各月の株価上昇率のグラフの作成されます.

stock_price_rise.py
import pandas as pd

# 東証上場全銘柄のリスト
def get_tokyo():
    tokyo = pd.read_csv("data_j.csv", engine="python", names=("date","code", "name","market", "CodeIndustry33", "ClassificationIndustry33", "CodeIndustry17", "ClassificationIndustry17", "CodeScale", "ClassificationScale"), skiprows=1, usecols=[1,2], encoding="utf-8")
    return tokyo

# 価格の時系列データフレーム作成
def make_df_price(list_stock_name, item):
    data   = {} # 自己資本
    stock_name = str(list_stock_name.code[0])
    if not ('.T' in stock_name):
            stock_name+='.T'
    dummy = pd.read_csv('./data_price/' + stock_name + '.csv')[item]
    for code in list_stock_name.code:
        stock_name = str(code)
        if not ('.T' in stock_name):
            stock_name+='.T'
        try:
            data[stock_name] = pd.read_csv('./data_price/' + stock_name + '.csv', index_col=0)[item]
        except:
            data[stock_name] = dummy # エラー発生時はダミーを入れる
    return pd.DataFrame(data)

# 東証上場全銘柄のリスト作成
list_stock_name = get_tokyo()
# 価格の時系列データフレーム作成
df_closes = make_df_price(list_stock_name, 'Close')['2020-01-06':'2021-01-04']

# 特定の日の株価データフレーム作成
df_closes2 = df_closes.loc[['2020-01-06',
                            '2020-02-03',
                            '2020-03-02',
                            '2020-04-01',
                            '2020-05-01',
                            '2020-06-01',
                            '2020-07-01',
                            '2020-08-03',
                            '2020-09-01',
                            '2020-10-02',
                            '2020-11-02',
                            '2020-12-01',
                            '2021-01-04',]]
# 株価上昇率の計算
result = ((df_closes2-df_closes2.shift(1))/df_closes2.shift(1)*100).T
# 2020年5月から収益率がプラスのものを検索
result_rise = result[#(result['2020-02-03']>0)&\
                     #(result['2020-03-02']>0)&\
                     #(result['2020-04-01']>0)&\
                     (result['2020-05-01']>0)&\
                     (result['2020-06-01']>0)&\
                     (result['2020-07-01']>0)&\
                     (result['2020-08-03']>0)&\
                     (result['2020-09-01']>0)&\
                     (result['2020-10-02']>0)&\
                     (result['2020-11-02']>0)&\
                     (result['2020-12-01']>0)&\
                     (result['2021-01-04']>0)
                     ]

# 証券コードと銘柄名の対応表を作成
result_rise_name = pd.DataFrame(index=list(result_rise.index), columns=['name'])
for stock_code in list(result_rise.index):
    result_rise_name.loc[stock_code] = list_stock_name[list_stock_name.code == int(stock_code[0:-2])]['name'].iloc[0]

# 結果リスト
mean = pd.DataFrame(result_rise.T['2020-05-01':'2021-01-04'].mean(), columns=['ave'])
result = pd.concat([result_rise, result_rise_name, mean], axis=1)
# 結果を出力
result = result.drop(['1329.T', '1365.T', '1626.T', '2068.T'])# ETFは除く
result.to_csv('result.csv', encoding="cp932")
print(result)

# グラフ作成
result_rise.T.plot(figsize=(9, 6), title='Return')
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Python環境構築

OS Windows10 Pro
Vagrant 2.2.14
Ubuntu 16.04.4 LTS

Dockerで今後管理したいため、Pythonをインストールし、pipをインストール、numpyをインストールしようとしたらエラーが発生。
````

 python3 -m pip install numpy
Collecting numpy
  Using cached https://files.pythonhosted.org/packages/51/60/3f0fe5b7675a461d96b9d6729beecd3532565743278a9c3fe6dd09697fa7/numpy-1.19.5.zip
    Complete output from command python setup.py egg_info:
    Traceback (most recent call last):
      File "<string>", line 1, in <module>
      File "/tmp/pip-build-ayy5xg9l/numpy/setup.py", line 68
        f"NumPy {VERSION} may not yet support Python "
                                                     ^
    SyntaxError: invalid syntax

    ----------------------------------------
Command "python setup.py egg_info" failed with error code 1 in /tmp/pip-build-ayy5xg9l/numpy/
You are using pip version 8.1.1, however version 20.3.3 is available.
You should consider upgrading via the 'pip install --upgrade pip' command.

調べて
pipをアップグレードした。
 sudo pip install --upgrade pip
The directory '/home/vagrant/.cache/pip/http' or its parent directory is not owned by the current user and the cache has been disabled. Please check the permissions and owner of that directory. If executing pip with sudo, you may want sudo's -H flag.
The directory '/home/vagrant/.cache/pip' or its parent directory is not owned by the current user and caching wheels has been disabled. check the permissions and owner of that directory. If executing pip with sudo, you may want sudo's -H flag.
Collecting pip
  Downloading https://files.pythonhosted.org/packages/54/eb/4a3642e971f404d69d4f6fa3885559d67562801b99d7592487f1ecc4e017/pip-20.3.3-py2.py3-none-any.whl (1.5MB)
    100% |████████████████████████████████| 1.5MB 110kB/s
Installing collected packages: pip
  Found existing installation: pip 8.1.1
    Not uninstalling pip at /usr/lib/python2.7/dist-packages, outside environment /usr
Successfully installed pip-20.3.3

再度、numpyをインストール
````

agrant@vagrant-ubuntu-trusty-64:~$ pip install numpy
WARNING: pip is being invoked by an old script wrapper. This will fail in a future version of pip.
Please see https://github.com/pypa/pip/issues/5599 for advice on fixing the underlying issue.
To avoid this problem you can invoke Python with '-m pip' instead of running pip directly.
DEPRECATION: Python 2.7 reached the end of its life on January 1st, 2020. Please upgrade your Python as Python 2.7 is no longer maintained. pip 21.0 will drop support for Python 2.7 in January 2021. More details about Python 2 support in pip can be found at https://pip.pypa.io/en/latest/development/release-process/#python-2-support pip 21.0 will remove support for this functionality.
Defaulting to user installation because normal site-packages is not writeable
Collecting numpy
  Downloading numpy-1.16.6-cp27-cp27mu-manylinux1_x86_64.whl (17.0 MB)
     |████████████████████████████████| 17.0 MB 116 kB/s
Installing collected packages: numpy
  WARNING: The scripts f2py, f2py2 and f2py2.7 are installed in '/home/vagrant/.local/bin' which is not on PATH.
  Consider adding this directory to PATH or, if you prefer to suppress this warning, use --no-warn-script-location.


無事インストールできました。

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

RocketChatをAPI/Pythonでイジる

RokcetChatのREST APIをPythonでなしかいじったものです。

Public,Privateで使い分けが必要になります(いまいっぽ。。。)。なので利用者にはその使い分けを意識せずに情報をとれるようにしてみました。

原理としては
responseの存在により

  • publicチャンネル→private用API
  • privateチャンネル→public用API

の組み合わせになった際にはresponseの有無で
格納処理をスルーするようにすることで
なんとかしている仕組みにしてみました。

#!/opt/anaconda3/bin/python3
# -*- coding: utf-8 -*-

'''RocketChat Channelメンテナンス

  RocketChatのチャンネル管理を行う 

  Todo:
     * まだRedmineとRocketChatのみ。他のOSSに対しても同様に作る

    def __init__(self, HEADERS, URL):
    def _getChannelPublicMap(self):
    def _getChannelPrivateMap(self):
    def exchangeMapkeyToList(self, map):
    def getChannelMap(self):
    def getChannelUserMap(self, list_channelname):
    def getDifftimeLastUpdateSec(self, _targetTime):
    def _getChannel_id(self, channelname):
    def sendMessageToRocketChat(self, channel, msg):
    def closeTargetChannel(self, roomname):
    def _ISOtimeToDatetime(self, target):
    def _CreateMapFromChannelIDtoChannelname(self):
    def _CreateMapFromChannelnameToChannelID(self, self._CreateMapFromChannelIDtoChannelname()):
    def _judgeRocketChatMessage(self, target_date, limit):
    def _JudgeDeleteChannelMessages(self, roomname, LIMIT):
    def JudgeDeleteChannelMessages(self, LIMIT):


'''

################################################
# library
################################################

import dateutil
import json
import pandas as pd
import requests
import sys

from datetime import date
from datetime import datetime
from datetime import timedelta
from dateutil import parser
from pprint import pprint
from pytz import timezone

################################################
# 環境変数取得 
################################################


################################################
# RocketChatChannelManager 
################################################
class RocketChatChannelManager(object):
    def __init__(self, HEADERS, URL):
        '''RESTを呼ぶ形式

        classの __init__処理
        REST APIでCallするために HEADERSとURLを共有する。

        RedmineXXXXXManager classとはことなりインスタンスは
        生成しない。

        '''
        # 引数チェック 型    
        if not isinstance(HEADERS, dict):
            print(f'引数:HEADERSの型が正しくありません dict <-> {type(HEADERS)}')
            raise TypeError

        # 引数チェック 型    
        if not isinstance(URL, str):
            print(f'引数:URLの型が正しくありません str <-> {type(URL)}')
            raise TypeError

        # パラメータ共有 
        self.HEADERS = HEADERS
        self.URL = URL


    def _getChannelPublicMap(self):
        '''パブリックチャネルのリストと最終更新時間のマップ

        パブリックチャンネル名とチャンネル最終更新時間のマップを作成する。

        Args:

        Returns:
           map:  パブリックチャンネル名と最終更新時間のマップ 

        Raises:
           API実行時のエラー 

        Examples:
            >>> map = self._getChannelPublicMap() 

        Note:
            publicとprivateで取得関数が異なるという。。。

        '''

        # 結果格納
        _map = {}

        # API定義
        API = f'{self.URL}/api/v1/channels.list'

        # 取得処理
        response = None 
        try:
            response = requests.get(
                API,
                headers=self.HEADERS,)    
        except Exception as e:
            print(f'API実行エラー: {API}')
            print(f'Error: {e}')
            return False
        else:
            for l in response.json()['channels']:
                _map[l['name']] = l['_updatedAt']

            # mapを返す
            return _map


    def _getChannelPrivateMap(self):
        '''プライベートチャネルのリストと最終更新時間のマップ

        プライベート名とチャンネル最終更新時間のマップを作成する。

        Args:

        Returns:
           map:  プライベートチャンネル  名と最終更新時間のマップ 

        Raises:
           API実行時のエラー 

        Examples:
            >>> map = self._getChannelPrivateMap() 

        Note:
            publicとprivateで取得関数が異なるという。。。

        '''

        # 結果格納
        _map = {}

        # API定義
        API = f'{self.URL}/api/v1/groups.listAll'

        # 取得処理
        try:
            response = requests.get(
                API,
                headers=self.HEADERS,)    
        except Exception as e:
            print(f'API実行エラー: {API}')
            print(f'Error: {e}')
            return False
        finally:
            for l in response.json()['groups']:
                _map[l['name']] = l['_updatedAt']

            # mapを返す
            return _map    


    def exchangeMapkeyToList(self, map):
        '''mapのkeyを要素とするlistを生成する

        ちょっとめんどい変換なのでヘルパー関数として作成したもの

        '''
        # 引数チェック 型    
        if not isinstance(map, dict):
            print(f'引数:mapの型が正しくありません dict <-> {type(map)}')
            raise TypeError

        # 入れ物
        _list = []

        # mapループ
        for key in map.keys():
            _list.append(key)

        return _list


    def getChannelMap(self):
        '''チャンネル一覧およびチャンネルの最終更新時間を取得する

        パブリック、プライベート両方のチャンネルをまとめて処理する

        Args:

        Returns:
           map: チャンネル名と所属ユーザリストのマップ 

        Raises:
           API実行時のエラー 

        Examples:
            >>> map_ = R.getChannelMap()

        Note:
            self._getChannelPubliclist()
            self._getChannelPrivatelist()
            パブリック、プライベートまとめて取得

        '''

        # public,privateそれぞれ取得
        _map_public = self._getChannelPublicMap()
        _map_private = self._getChannelPrivateMap()

        # mapを結合して返す
        if ((_map_public) and (_map_private)):
            _map_public.update(_map_private)
            return _map_public
        # public Channelのみの場合
        elif _map_public :
            return _map_public 
        # private Channelのみの場合
        elif _map_private :
            return _map_private 
        else:
            return {}


    def getChannelUserMap(self, list_channelname):
        '''指定チャンネルの登録ID一覧

        listに格納したチャンネルに所属するユーザ一覧を
        チャンネル名と参加しているユーザリストのマップを返す
        パブリック、プライベートをまとめて実施

        Args:
           list_channelname(list): 探索対象のチャンネル名リスト

        Returns:
           map: チャンネル名をKeyとする所属ユーザリストのマップ 

        Raises:
           API実行時のエラー 

        Examples:
            >>> map = getChannelUserMap(['aaaa','bbbb'])

        Note:

        '''

        # 引数チェック 型    
        if not isinstance(list_channelname , list):
            print(f'引数:list_channelnameの型が正しくありません list  <-> {type(list_channelname)}')
            raise TypeError

        # 結果全体格納するMap
        _map = {}

        # MSG送信API定義
        # パブリックもプライベートもまとめて実施
        APIS = [f'{self.URL}/api/v1/channels.members',
                f'{self.URL}/api/v1/groups.members']

        # 1000人は超えないだろう。。。から
        COUNT = '1000'

        # 対象チャンネル名リストでループ
        for channel in list_channelname:

            # MSG組み立て 
            msg = (('roomName', channel),('count',COUNT),) 

            # API発行
            for api in APIS:
                try:
                    response = requests.get(
                        api,
                        params=msg,
                        headers=self.HEADERS,)
                except Exception as e:
                    print(f'API実行エラー: {API}')
                    print(f'Error: {e}')
                    return False
                else:
                    # ユーザたちを格納するList
                    _list = []

                    # 結果を得られた場合のみ格納
                    if response:
                        # 所属するユーザlistを生成
                        for l in response.json()['members']:
                            _list.append(f'{l["username"]}')

                        # mapにchannel名をKeyにしてユーザリストを格納
                        _map[channel] = _list

        # mapを返す
        return _map


    def getDifftimeLastUpdateSec(self, _targetTime):
        '''最終更新時間からの経過秒を返す

        Public,Privateそれぞれ指定が可能

        Args:
           _targetTime(str): 比較したい時間 ISO時間フォーマット

        Returns:
           list: ユーザ一覧を格納したlist 

        Raises:
           API実行時のエラー 

        Examples:
            >>> list_AllUser = R.getAllUserList() 

        Note:

        '''

        # 引数チェック 型    
        if not isinstance(_targetTime, str):
            print(f'引数:_targetTimeの型が正しくありません str  <-> {type(_targetTime)}')
            raise TypeError

        # 今時間生成
        jst_now = datetime.now(timezone('Asia/Tokyo'))
        target = parser.parse(_targetTime).astimezone(timezone('Asia/Tokyo'))

        # いま時間とターゲット時間の差分を秒で返す
        return (jst_now - target).total_seconds()


    def _getChannel_id(self, channelname):
        '''Channel名の _id情報を取得する

        チャンネル名からチャンネルIDを取得する
        RocketChatAPIではチャンネル名ではなくチャンネルIDを
        要求するケースが多数ある。 

        Args:
           channelname: チャンネル名

        Returns:
           str: チャンネル名に対するチャンネルID 

        Raises:
           API実行時のエラー 

        Examples:
            >>> R._getChannel_id('general') 

        Note:

        '''

        # 引数チェック 型    
        if not isinstance(channelname, str):
            print(f'引数:channelの型が正しくありません str  <-> {type(channelname)}')
            raise TypeError

        # ユーザ情報取得API定義
        API = f'{self.URL}/api/v1/rooms.info'

        # MSG組み立て
        msg = {'roomName': channelname,}

        # MSG送信
        try:
            response = requests.get(
                API,
                params=msg,
                headers=self.HEADERS,)
        except Exception as e:
            print(f'API実行エラー: {API}')
            print(f'Error: {e}')
            return False
        else:
            if response.json()['success']:
                return response.json()['room']['_id']
            else:
                return False


    def sendMessageToRocketChat(self, channel, msg):
        '''指定チャネルにメッセージを送る

        指定チャンネルにメッセージを送信する        

        Args:
           channel: チャンネル名
           msg:     送信メッセージ 

        Returns:
           処理結果, HTTP ステータスコード

        Raises:
           API実行時のエラー 

        Examples:
            '>>> R.getUser_id('geneal', 'こんにちわ') 

        Note:

        '''

        # 引数チェック 型    
        if not isinstance(channel, str):
            print(f'引数:channelの型が正しくありません str  <-> {type(channel)}')
            raise TypeError

        if not isinstance(msg, str):
            print(f'引数:msgの型が正しくありません str  <-> {type(msg)}')
            raise TypeError

        # MSG送信API定義
        API = f'{self.URL}/api/v1/chat.postMessage'

        # MSG組み立て
        msg = {'channel': channel,
               'text'   : msg,}

        # 指定チャンネルが存在する場合のみ実行 
        if self._getChannel_id(channel):

            # MSG送信
            try:
                response = requests.post(
                    API,
                    data=json.dumps(msg),
                    headers=self.HEADERS,)
            except Exception as e:
                print(f'API実行エラー: {API}')
                print(f'Error: {e}')
                return False
            else:
                pprint(f'Status code: {response.status_code}') 
                return True
        else:
            print(f'指定したチャンネルが存在しません: {channel}')
            return False


    def closeTargetChannel(self, roomname):
        '''パブリック、プライベート区別なくチャンネルを削除する

        指定したチャンネル名を削除する 

        Args:
           roomname(str): 削除するチャンネル名

        Returns:

        Raises:
           API実行時のエラー 

        Examples:
            >>> R.closeTargetChannel('テストチャンネル')

        Note:
            まとめて消す仕様ではない、1チャンネルづつターゲットで

        ''' 

        # 引数チェック 型    
        if not isinstance(roomname, str):
            print(f'引数:roomnameの型が正しくありません str  <-> {type(roomname)}')
            raise TypeError

        # 削除API定義
        # パブリックもプライベートも区別なくまとめて実施
        APIS = [f'{self.URL}/api/v1/channels.delete',
                f'{self.URL}/api/v1/groups.delete']

        # MSG組み立て
        msg = {'roomId': self._getChannel_id(roomname)}

        # まとめてチャンネル削除を遂行
        for API in APIS:
            try:
                response = requests.post(API,
                                         data=json.dumps(msg),
                                         headers=self.HEADERS,)
            except Exception as e:
                print(f'API実行エラー: {API}')
                print(f'Error: {e}')
            else:
                # 結果を得られた場合のみ処理コードを返す
                if response:
                    return response.json()['success']


    def _ISOtimeToDatetime(self, target):
        '''ISOフォーマット時刻文字列をJST変換してdatetime型で返す

        Args:
            target: str   ISO形式のUTC時刻文字列

        Returns:
            datetime: JST変換後

        Raises:
           API実行時のエラー 

        Examples:
            >>> self._ISOtimeToDatetime('2021-01-20T00:23:10.256Z')

        Note:

        '''

        return parser.parse(target).astimezone(timezone('Asia/Tokyo'))


    def _CreateMapFromChannelIDtoChannelname(self):
        '''チャンネルIDに対するチャンネル名をもつmapを生成する。

        チャンネルIDをkeyにしてチャンネル名をValueに持つmapを生成する。
        RocketChatから還元される情報が何かとチャンネルIDで返してくるのだが
        還元する立場だとチャンネルIDだとわかりにくい問題がある。
        チャンネル名に置き換えることでデータ利便性を上げる。

          -> cf. _getChannel_id(self, channelname): チャンネル名からチャンネルIDを取得

        Args:

        Returns:
           map: Key:チャンネルID、Value: チャンネル名

        Raises:
           API実行時のエラー 

        Examples:
           >>> _MapChannelIDtoChannelName = self._CreateMapFromChannelIDtoChannelname()

        Note:
           つどつどAPIを叩いて情報収集する仕掛けだとレスポンス懸念あり。
           mapを予め作成し変換パフォーマンスを向上させる。

           TODO: チャンネル名 -> チャンネルIDのmapも作っておくべきかもしれない。

        ''' 
        # パブリック、プライベート合算でチャンネル名を取得する 
        ## Class内メソッドを使ってチャンネル名をまとめて取得
        _map = self.getChannelMap()

        # 蓄積するDataFrame生成
        channelMap = {}

        # 処理ループ
        for key in _map.keys():
           _key = self._getChannel_id(key)
           channelMap[_key] = key

        # 蓄積結果を返す
        return channelMap 


    def _CreateMapFromChannelnameToChannelID(self, self._CreateMapFromChannelIDtoChannelname()):
        '''ChannelID->ChannelNameのmapを利用してChannelName->ChannelID mapを生成する

        内包を使用してkey/valueを反転させる


        Args:
          map: map ChannelID->ChannelKeyマップ

        Returns:
           map: map  key/valueを反転させたmap

        Raises:
           API実行時のエラー 

        Examples:
           >>> _map = self._CreateMapFromChannelnameToChannelID(self._CreateMapFromChannelIDtoChannelname())

        Note:

        '''
        # ChannelID -> Channel NameMap
        _map = self._CreateMapFromChannelIDtoChannelname

        # ChannelID -> Channel NameMapのKey/Valueを反転させる
        swap_map = {v: k for k, v in _map.items()}

        return swap_map


    def _judgeRocketChatMessage(self, target_date, limit):
        '''メッセージ作成日付から保管する、しない判定を行う

        limitで指定した期間のMSGを保管する、しない判定を行いTrue/Falseで返す。
        日付差分計算はdatetime型のサポートにより行う。timedeltaオブジェクトを使用し
        差分日付けに対する判定処理を行う。

        Args:
          target_date: datetime  判定対象の時間データ
          limit      : int       RocketChatメッセージ保存期間

        Returns:
          True/False: Boolean    True 保存、False 削除対象

        Raises:
           API実行時のエラー 

        Examples:
            >>> self._judgeRocketChatMessage(target_datetime, 10)

        Note:

        '''

        today = date.today()
        diff_date = timedelta(limit)
        return (today - target_date.date() > diff_date)


    def _JudgeDeleteChannelMessages(self, roomname, LIMIT):
        '''roomnameに対しLIMIT超過日数を超えたメッセージに削除判別フラグを設定したデータを生成する。

        指定したroomnameに対し、メッセージ作成日からの日数が
        LIMITを超過している場合に削除判定フラグをつけて
        DataFrameを生成する。

        Args:
           roomname: str  探索対象のチャンネル名
           LIMIT:    int  保存期間(日数)

        Returns:
           df: DataFrame: ['チャンネル','MSG_ID','更新時間','削除対象','MSG']

        Raises:
           API実行時のエラー 

        Examples:
           >>> _df = self._JudgeDeleteChannelMessages(key, LIMIT)

        Note:
          _CreateMapFromChannelIDtoChannelname
          _MapChannelIDtoChannelName
          _ISOtimeToDatetime
          _judgeRocketChatMessage

        ''' 

        # 引数チェック 型    
        if not isinstance(roomname, str):
            print(f'引数:roomnameの型が正しくありません str  <-> {type(roomname)}')
            raise TypeError

        if not isinstance(LIMIT, int):
            print(f'引数:LIMITの型が正しくありません int  <-> {type(LIMIT)}')
            raise TypeError

        # MSG抽出API定義
        # パブリックもプライベートも区別なくまとめて実施
        APIS = [f'{self.URL}/api/v1/channels.messages',
                f'{self.URL}/api/v1/groups.messages']

        # MSG組み立て
        channel_id = self._getChannel_id(roomname)
        params = (
            ('roomId', channel_id),
        )
        ## この書き方だと失敗する
        #params = (
        #    ('roomId', channel_id)
        #)

        # 変換Map作成
        _MapChannelIDtoChannelName = self._CreateMapFromChannelIDtoChannelname()

        # 両パターンに対応する入れ物を用意
        _list = []

        for API in APIS:
            pprint(f'API={API}')
            try:
                response = requests.get(API,
                                        headers=self.HEADERS, 
                                        params=params,)
            except Exception as e:
                print(f'API実行エラー: {API}')
                print(f'Error: {e}')
            else:
                # 結果をログっぽく返す
                # 結果を得られた場合のみログを返す
                pprint(f'response={response}')
                if response:
                    # 結果チェック
                    pprint(response)
                    pprint(len(response.json()['messages']))

                    # 削除対象判定結果をDataFrameに組み込んでで返す
                    for _ in response.json()['messages']:
                        _list.append([_MapChannelIDtoChannelName[_['rid']],
                                      _['_id'], 
                                      self._ISOtimeToDatetime(_['_updatedAt']),
                                      self._judgeRocketChatMessage(self._ISOtimeToDatetime(_['_updatedAt']), LIMIT),
                                      _['msg']])

        # DataFrameにして結果を返す
        df= pd.DataFrame(_list)
        df.columns = ['チャンネル','MSG_ID','更新時間','削除対象','MSG']

        return df


    def JudgeDeleteChannelMessages(self, LIMIT):
        '''削除対象フラグを持ったDataFrameをまとめて1つのDataFrameにする。 

        サブメソッド _JudgeDeleteChannelMessagesから得られる
        パブリック、プライベート両方から得られたDataFrameを蓄積する。

        Args:
            LIMIT:    int  保存期間(日数)

        Returns:
           df: DataFrame 蓄積したDataFrame

        Raises:
            API実行時のエラー 

        Examples:

        Note:
           self._JudgeDeleteChannelMessages(key, LIMIT):  削除対象フラグをつけたDataFrameを作成

        ''' 

        if not isinstance(LIMIT, int):
            print(f'引数:LIMITの型が正しくありません int  <-> {type(LIMIT)}')
            raise TypeError

        # パブリック、プライベート合算でチャンネル名を取得する 
        ## Class内メソッドを使ってチャンネル名をまとめて取得
        _map = self.getChannelMap()

        # 蓄積するDataFrame生成
        df = pd.DataFrame(index=[])

        # 処理ループ
        for key in _map.keys():
           _df = self._JudgeDeleteChannelMessages(key, LIMIT)
           df = pd.concat([df, _df], axis=0)

        # 蓄積結果を返す
        return df.reset_index(drop=True)


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

sudachipyで簡易的に同義語辞書をつかう

TL;DR

  • sudachiの同義語辞書(synonym.txt)から同義語グループidと代表語の組み合わせを生成
  • 生成した組み合わせをつかってsudachipyで簡易的に同義語辞書を使えるようにする
  • 例としてわかちがき後同義語辞書をつかって正規化する

目的

テキストからの情報抽出やテキストの類似度計算などのタスクを行う際に、sudachiでの形態素解析で同義語を使いたかったのですが、sudachipyではsudachiの同義語辞書を利用できませんでした。
簡易的でいいので、sudachipyで簡易的に同義語辞書が使えるようにします。
今回の目的は、あくまでも形態素解析後の正規化です。特に、わかちがき後に同義語を同じ見出しに揃えることを目的としています。したがって、同義語の展開は行いません。

sudachiの同義語辞書

sudachiの同義語辞書はドキュメントによると、

Sudachi 辞書に登録されている語に対して同義語情報を付与したものです。 Sudachi 辞書と同じライセンスで提供されます。

とのことです。

同義語辞書のソースはテキストファイルで公開されています。
https://github.com/WorksApplications/SudachiDict/blob/develop/src/main/text/synonyms.txt

pythonでsudachiを使う

pythonでsudachiを使う場合は、pipを使ってinstallすることができます。

pip install sudachipy sudachidict_core

以下の要領で形態素を取得できます。

from sudachipy import tokenizer
from sudachipy import dictionary

tokenizer_obj = dictionary.Dictionary().create()
mode = tokenizer.Tokenizer.SplitMode.B

token = tokenizer_obj.tokenize("食べ", mode)[0]

token.surface() # => '食べ'
token.dictionary_form() # => '食べる'
token.reading_form() # => 'タベ'
token.part_of_speech() # => ['動詞', '一般', '*', '*', '下一段-バ行', '連用形-一般']

また、sudachiは文字の正規化ができます。

token.normalized_form()

同義語辞書の編集

ここからが本題。

同義語ファイルを一部抜粋すると、以下のように形式で作成されています。

000001,1,0,1,0,0,0,(),曖昧,,
000001,1,0,1,0,0,2,(),あいまい,,
000001,1,0,2,0,0,0,(),不明確,,
000001,1,0,3,0,0,0,(),あやふや,,
000001,1,0,4,0,0,0,(),不明瞭,,
000001,1,0,5,0,0,0,(),不確か,,

000002,1,0,1,0,0,0,(),宛て先,,
000002,1,0,1,0,0,2,(),あて先,,
000002,1,0,1,0,0,2,(),宛先,,
000002,1,0,2,0,0,0,(),送り先,,
000002,1,0,3,0,0,0,(),送付先,,
000002,1,0,4,0,0,0,(),届け先,,
000002,1,0,5,0,0,0,(),発送先,,
000002,1,0,6,0,0,0,(),配送先,,

1語1行で記述し、同義語グループ間は空行で区切られて、フォーマットは以下の通りです。

0 : グループ番号
1 : 体言/用言フラグ (省略可)
2 : 展開制御フラグ (省略可)
3 : グループ内の語彙番号 (省略可)
4 : 同一語彙素内での語形種別 (省略可)
5 : 同じ語形の語の中での略語情報 (省略可)
6 : 同じ語形の語の中での表記ゆれ情報 (省略可)
7 : 分野情報 (省略可)
8 : 見出し
9 : 予約
10 : 予約

詳細な説明は、ドキュメントを参照してください。

今回重要なのは

  • 0 : グループ番号
  • 3 : グループ内の語彙番号
  • 6 : 同じ語形の語の中での表記ゆれ情報

の3つです。

グループ番号は、ソース内で同義語の管理・識別に使用する6桁の数字です。
グループ内の語彙番号は、グループ内における、語彙素の管理番号です。"1"始まりで連番を付与します。
同じ語形の語の中での表記ゆれ情報は、同じ略語・略称形の語 (3、4、5の番号が同じもの) における、表記の関連性を示す情報です。0がその略語・略称形の語の代表語になります。

すべてを確認したわけではないですが、synonym.txtでは各値の昇順に並んでいます。
つまり、各同義語グループの先頭は複数ある語彙素のうちいずれかの代表語です。
また、管理番号1の語彙素を同義語グループの代表的な語彙素であるとすることで、各同義語グループの先頭の語をその同義語グループの代表語として扱うことができます。

このルールに則って、グループ番号と同義語グループの代表語の見出しの組み合わせを作成します。

import csv

with open("synonyms.txt", "r") as f:
    reader = csv.reader(f)
    data = [r for r in reader]

output_data = []
synonym_set = []
synonym_group_id = None
for line in data:
    if not line:
        if synonym_group_id:
            base_keyword = synonym_set[0]
            output_data.append([
                synonym_group_id, base_keyword
            ])
        synonym_set = []
        continue
    else:
        synonym_group_id = line[0]
        synonym_set.append(line[8])

with open("synonyms_base.csv", "w") as f:
    writer = csv.writer(f)
    writer.writerows(output_data)

sudachipyで同義語を使う準備

sudachipyでは同義語辞書を取得できませんが、トークンが該当する同義語グループのidを取得することができます。

token.synonym_group_ids()
# => [1]

この取得した同義語グループidで先程生成した組み合わせから同義語の代表語を取得します。
一点注意するのは、synonym.txtの同義語グループのidは6桁の数字の文字列ですが、取得できるidはintであることです。

import csv
with open('synonym_base.csv', "r") as f:
    reader = csv.reader(f)
    data = [[int(r[0]), r[1]] for r in reader]
    synonyms = dict(data)

synonym_group_ids = token.synonym_group_ids()
if synonym_group_ids:
    # 複数ありうるけどとりあえず先頭を選択
    surface = synonyms[synonym_group_ids[0]]

わかちがき

生成した同義語のデータを使って、わかちがきを正規化します。
fetch_synonym_surfaceでは同義語があった場合は同義語グループの代表語を、同義語がない場合は正規化された見出し語を返すようにします。

import csv
with open('synonym_base.csv', "r") as f:
    reader = csv.reader(f)
    data = [[int(r[0]), r[1]] for r in reader]
    synonyms = dict(data)

def fetch_synonym_surface(token):
    synonym_group_ids = token.synonym_group_ids()
    if synonym_group_ids:
        # 複数ありうるけどとりあえず先頭を選択
        surface = synonyms[synonym_group_ids[0]]
    else:
        surface = token.normalized_form()
    return surface

以下は、
1. わかちがきだけ
2. わかちがき + 正規化
3. わかちがき + 同義語正規化

のコードと結果の比較です。

def wakati(sentence):
    tokenizer_obj = dictionary.Dictionary().create()
    mode = tokenizer.Tokenizer.SplitMode.C
    return " ".join([m.surface() for m in tokenizer_obj.tokenize(sentence, mode)])

def wakati_normalized(sentence):
    tokenizer_obj = dictionary.Dictionary().create()
    mode = tokenizer.Tokenizer.SplitMode.C
    return " ".join([m.normalized_form() for m in tokenizer_obj.tokenize(sentence, mode)])

def wakati_synonym_normalized(sentence):
    tokenizer_obj = dictionary.Dictionary().create()
    mode = tokenizer.Tokenizer.SplitMode.C
    return " ".join([fetch_synonym_surface(m) for m in tokenizer_obj.tokenize(sentence, mode)])

sentence = "アドビはアメリカのお金であるmoneyを生み出す会社"

print("1:", wakati(sentence))
print("2:", wakati_normalized(sentence))
print("3:", wakati_synonym_normalized(sentence))

1: アドビ は アメリカ の お金 で ある money を 生み出す 会社
2: アドビ は アメリカ の お金 だ 有る マネー を 生み出す 会社
3: アドビシステムズ は アメリカ合衆国 の お金 だ 有る お金 を 生み出す 会社
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

1分で基礎をおさらい!Python 高速で最小値を取れる優先度付きキュー

概要

リストから最小値を高速で取り出したいときに用いる優先度付きキューがPythonにはheapqという便利なものがあるのでさくっと見直しましょう。AtCoderでも頻出内容

急いでる人向け

この記事のすべて。三行目を
q = heapq.heapify(a)
としても動かないため注意

a = [1,2,3,4,6,7,8]
#list をheapifyでヒープ化する。
heapq.heapify(a)

#heappush で5を追加する。
heapq.heappush(a,5)

#heap内のの要素を小さい順にheappopで取り出す。
while a:
  print(heapq.heappop(a))

######実行結果######
1
2
3
4
5
6
7
8

かんたんな説明

・heap*.heapify(list)*
リストを与えるとヒープ化してくれる

・heap.heappush(heap,要素)
heapに新しい要素を追加する(listにおけるappendのheap版)

・heap.heapop(heap)
heap内の最小の値を取り出す。取り出した要素はheapから削除される。

応用

最大値を取り出す優先度付きキューを作りたい

すべての値の符号を逆転させればよい。

a = [1,2,3,4,6,7,8]

#要素の符号を逆転
for i in range(len(a)):
  a[i] = a[i]*(-1)
heapq.heapify(a)

while a:
  #出力の符号を戻してやる
  print(heapq.heappop(a)*(-1))

######実行結果######

8
7
6
4
3
2
1

参考にしたサイト

より詳しい解説はこっち
https://docs.python.org/ja/3/library/heapq.html

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

教師あり学習 ~入門者のメモ~ (scikit-learn)

本記事の内容

東京大学のデータサイエンティスト育成講座」を読んで、scikit-learn の各モデルについての概要が掴めたので、忘れないうちにメモ。
書籍で言うと:Chapter 8 機械学習の基礎(教師あり学習)

初心者の自分にとっては、機械学習モデルの種類が多く感じてしまうので、シンプルに整理してみた。
実装サンプルのパラメータは、本の中で使ったもののみ。

scikit learn の機械学習モデル全体像

チートシート
ここでのポイントは、ザックリ、上が教師あり学習、下が教師なし学習。
今回は上の部分の説明。
sklearn-sheet.png

教師あり学習
  • classification: 分類=予測したい変数がクラス (例:「合格/不合格」、「晴れ/曇り/雨/雪」)
  • regression: 回帰=予測したい変数が値 (例:体重「66.6kg, 32.3kg, ...」)
教師なし学習
  • clustering: クラスタリング (似ているデータのグループ化)
  • dimensionality reduction: 次元削減 (多数の特徴=>少数の本質的な特徴に減らす、主成分分析とも)

教師あり学習

説明変数(または特徴量:X)から目的変数(y)を予測するモデルを求める手法。
Supervised learning.png

教師あり学習モデルの全体像(例の本の8章)

雑に言えば、以下4つ。
1. 線形モデル
2. 決定木
3. kNN (k近傍法)
4. SVM (サポートベクターマシン)

それぞれに分類と回帰のモデルがある。
Models.png
右側の図がポイント。

各論

モデルの詳細や数学的背景は他の記事が沢山あるので割愛(←まだ上手く説明できないだけ

1. 線形モデル

イメージ

linear.png

線形なので、一次多項式(y=ax+by+cz)のイメージ
本当はこうみたい。

y = w_0x_0+w_1x_1+w_2x_2+\cdots

基本的なものだけでも複数のモデルがある。
正則化項のある回帰とか、名前がかっこよすぎて、どっちがどっちかいつも忘れる。

単回帰、重回帰

単回帰:説明変数が1つ。

y = ax+b

重回帰:説明変数が複数。

y = w_0x_0+w_1x_1+w_2x_2+\cdots

最小二乗法(「正解-予測値」の2乗の総和=正解からどれぐらいかけ離れているかというペナルティを最小にする方法)で各項の最適な係数(重み)を求める。
以下の損失関数(loss function)が最小になれば勝ち。

loss = \sum_{i=1}^n(y_i-f(x_i))^2
モデル作成
from sklearn.linear_model import LinearRegression
model = LinearRegression()

パラメータ:あれ、何も設定しなかったや。。

ラッソ回帰、リッジ回帰

正則化の話。
正則化とは、ザックリ言うと、モデルが複雑になり過ぎるのを防ぐ手法。
訓練データに適応し過ぎるが故に未知のデータをいい感じに予測できない現象(=過学習)を防ぎたい。ほんで、汎化性能(テストデータのような未知のデータに対する予測精度)を高めたい。
ペナルティに正則化項(モデルが複雑になると大きくなる)を加えることで、複雑になることを防ぐ。

loss = \sum_{i=1}^n(y_i-f(x_i))^2+\lambda\sum_{j=1}^m|w_j|^q

ラッソ(Lasso): q=1: 正則化項(L1ノルム)=重みの(1乗の)総和を加える
リッジ(Ridge): q=2: 正則化項(L2ノルム)=重みの2乗の総和
ルンバ(roomba):ロボット掃除機
思わず「あいうえお作文」したくなる。

ついでに、本の内容超えるけど、以下のポイントを風の噂で聞きました。知らんけど。

Lasso は、不要な説明変数(特徴量)の係数を0にしてくれるので、不要なパラメータを削りたい時に便利らしい(スパース推定だってさ)。おや、教師なし学習の次元削減と、どう使い分けるのだろう・・・???
Ridge は、多重共線性(相関の高い特徴量が含まれるため、それらの係数がいい感じに決まらない)により通常の線形回帰では上手くいかない場面でも、いい感じに推定できるようにしてくれるらしい。

因みに、L1ノルムとL2ノルムを合体させると Elastic Net !だってさ。知らんけど。

ついでに、正則化項(Lpノルム)をグラフにしたらこんな感じらしいっす。
ついでに、ここの「Ridge回帰」のとこにある図が気に入った。(って、おい、上からw)

ついでのついでに、なんで、Lasso の場合だけ係数(パラメータ)を0にできるかと言うと・・・パラメーターの値を変える意味合いが違うんだって。
Ridge の場合、パラメーターの二乗がペナルティ=大きい値のパラメータを小さくするほうが、元々小さい値のパラメータをさらに小さくするより効果ある(そりゃそうだね)。だから、特定のパラメータを0に近づけるより、別の大きなパラメータを減らす方向に動く。なので、係数が0になりにくいとのこと。
Lasso の場合、パラメーターの絶対値なので、元々の値の大きさに関わらず、係数を1小さくしたら、ペナルティが1減る。なので、簡単に0まで減らせる。だってさ。(そうだよね)

モデル作成(Lasso)
from sklearn.linear_model import Lasso
model = Lasso(alpha=1.0, random_state=0)

パラメータ:alpha(上の loss にある「λ(ラムダ)」の値、default=1)

モデル作成(Ridge)
from sklearn.linear_model import Ridge
model = Ridge(random_state=0)

パラメータ:random_state

ロジスティック回帰

線形モデルで分類問題を解くにはコイツ。
シグモイド(sigmoid)関数とか使うと、二値分類ができる。多クラス分類は、それを「1対その他」に拡張するだけ。グラフだと分かりやすいが、以下の x の部分にさっきの一次多項式が入る予定。
sigmoid function.png
何となく、yが50%以上の奴らは上、それ以外は下って分類すればよさそうだけど、この閾値(50%)をどこに設定するかというのも重要。

モデル学習の過程では、予測値が正解に近づくように、正解から遠い予測値が出されるとペナルティが大きくなるように定義された損失関数を使う。ペナルティが最小になるようにモデルを構築できれば(=いい感じの係数を見つけられれば)勝ち。
ペナルティ(交差エントロピー誤差)はこんな感じ。
cross-entropy.png
関数にするなら、
(教えて!google先生「y=- log x graph」)

-log(予測値)

0が正解の場合は、関数を左右反転。
(教えて!google先生「- log -(x-1) graph」)

-log(-(予測値-1))
モデル作成
from sklearn.linear_model import LogisticRegression
model = LogisticRegression()

パラメータ:あれ、何も設定しなかったや。。

2. 決定木

tree-classification2.png

前処理が楽。(今回、前処理とか詳細割愛)
スケーリングが不要(分岐条件が値の大小関係なので影響がない)
何なら欠損値処理も不要(欠損値を特別な値として処理してくれる)

コンペでよく使われる xgboost とか LightGBM とかのベースになるやつやから、頑張って勉強しとこ。表面ツラだけ

てか、ここ見たらええよ、マジで。

分類

リンクの「分類木」参照、マジで。

モデル作成
from sklearn.tree import DecisionTreeClassifier
model = DecisionTreeClassifier(criterion='entropy', max_depth=5, random_state=0)

パラメータ:
- criterion : {“gini”, “entropy”}, default=”gini”
- max_depth : 木の最大深さ

回帰

リンクの「回帰木」参照、マジで。

モデル作成
from sklearn.tree import DecisionTreeRegressor
model = DecisionTreeRegressor()

パラメータ:あれ、何も設定しなかったや。。

3. kNN (k近傍法)

属性が近いk個による多数決
knn.png
てか、ここ見たらええよ。マジで。(キノコの話、おもろ)

分類

キノコの種類、教えて~(リンク先PDF参照)

モデル作成
from sklearn.neighbors import KNeighborsClassifier
model = KNeighborsClassifier(n_neighbors=5)

パラメータ:n_neighbors : k個の数

回帰

キノコの直径、教えて~(リンク先PDF参照)

モデル作成
from sklearn.neighbors import KNeighborsRegressor
model = KNeighborsRegressor(n_neighbors=5)

パラメータ:n_neighbors : k個の数

4. SVM (サポートベクターマシン)

カテゴリを識別する境界線を、マージンが最大になるように引く手法。
図で言うと、左でも右でもなく、真ん中。
svm.png

てか、ここ見たらええよ。マジで。

分類

上の記事そのまんまやな。

モデル作成1
from sklearn.svm import LinearSVC
model = LinearSVC()

パラメータ:あれ、何も・・・(ry

モデル作成2
from sklearn.svm import SVC
model = SVC(probability=True)

パラメータ:
- probability : 予測の結果として、最終的に判断された分類クラスではなく、各分類クラスに該当する確率を取得できる関数 predict_proba を使うには True にする必要がある。

回帰

だからここ見たらええって。マジで。

モデル作成
from sklearn.svm import SVR
model = SVR()

パラメータ:あれ、何も・・・(ry

実装

処理の流れ

  1. モデル作成(上の各論参照)
  2. モデルの訓練
  3. モデルの評価
# モデルは何でもいいよ。ここでは重回帰。
model = LinearRegression()
# 訓練
model.fit(X_train, y_train)
# 評価
print('train:', model.score(X_train, y_train))
print('test:', model.score(X_test, y_test))

model.score について

0 ~ 1 の値で、実際は以下を計算している。
分類の場合、accuracy
回帰の場合、決定係数: R^2

もっと全体像

import pandas as pd
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression

# データロード
iris = load_iris()

# テストデータを分割
X_train, X_test, y_train, y_test = train_test_split(iris.data, iris.target, random_state=0)

# モデルの構築~評価
model = LogisticRegression()
model.fit(X_train, y_train)
print('train:', model.score(X_train, y_train))
print('test:', model.score(X_test, y_test))

結果:
train: 0.9821428571428571
test: 0.9736842105263158

モデル比較

import pandas as pd
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.tree import DecisionTreeClassifier
from sklearn.neighbors import KNeighborsClassifier
from sklearn.svm import SVC

iris = load_iris()

X_train, X_test, y_train, y_test = train_test_split(iris.data, iris.target, random_state=0)

models = {
    'linear': LogisticRegression(),
    'tree': DecisionTreeClassifier(),
    'knn': KNeighborsClassifier(n_neighbors=3),
    'svm': SVC()
}

scores = {}

for name, model in models.items():
  # モデルの構築~評価
  model.fit(X_train, y_train)
  scores[(name, 'train')] = model.score(X_train, y_train)
  scores[(name, 'test')] = model.score(X_test, y_test)

pd.Series(scores).unstack()

結果:
モデル比較.png

まとめ

今回学んだ教師あり学習のモデルは以下4つ。

  1. 線形モデル
  2. 決定木
  3. kNN (k近傍法)
  4. SVM (サポートベクターマシン)

それぞれに、回帰と分類のためのモデルがあるよ。

感想

こうやって整理してみると結構シンプル。
説明書いたのほぼ線形モデルのみで、あとはリンク集(苦笑)。
今後、気が向いたら深堀していけばいいと思うけど、理論の詳細を追いかけるより、xgboost とか LightGBM とか使ってスコア改善する方が楽しそう。
近いうちに9章の教師なし学習についてもメモしたい。

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

Django - Qiita上のtutorial appを概観し、機能をプラスする(2)

はじめに

前回Django - Qiita上のtutorial appを概観し、機能をプラスする(1)の続きです。

Djangoにはログイン機能について標準でユーザー認証(login)機能がついていますが、ユーザー登録(signup)機能は備わっていません。

今回はユーザー認証機能とユーザー登録機能とそれら画面を実装していきます。
ログアウト画面も作ってよいのですが、今回はログイン画面に遷移させることで対応します。

参照記事は以下です。
Django2 でユーザー認証(ログイン認証)を実装するチュートリアル -2- サインアップとログイン・ログアウト

以下前回の機能も付与してgitにあげたものです。
https://github.com/Rio157/crud-image-accounts.git

完成型

b2b6f6da-fefe-4019-bbc4-56761af38df4-1920x913r.png
ログイン画面

97b64710-5e1c-4585-ae49-0bfcad2c5fa6-1920x900r.png
ユーザー登録画面

ユーザー認証機能(login)とその画面

まずはurlの指定から。

settings.py(project下)
LOGIN_REDIRECT_URL = '/'
LOGOUT_REDIRECT_URL='/'
# login後、logout後にリダイレクトするページを指定します。
# ここでは一覧画面に飛ぶようにします。
urls.py(project下)
    path('accounts/', include('django.contrib.auth.urls')), 
    # urlspatternsに追加。これでdjangoの標準でついている認証機能が有効になりました。

次にtemplatesをつくっていきます。

_base.html(templates/app下)
    {% block customcss %}
    {% endblock customcss %}
    <!--headタグに追加。_base.htmlを継承したときにcssファイルを読み込むことができます。-->
    <!--また、bodyタグ内で管理サイト、ログアウトと並んで、-->
    <li class="nav-item">
        <a class="nav-link" href="{% url 'accounts:signup'%}">ユーザー登録</a>
    </li>
    <!--を追加。また、ログアウトでは-->
  <li class="nav-item">
        <a class="nav-link" href="{% url 'logout'%}">ログアウト</a>
   </li>
  <!--{% url 'admin:logout'%}→{% url 'logout' %}とします。管理者用の認証機能によるログアウト画面に遷移させないためです。-->
login.html(templates下にregistrationフォルダ作成、registration下)
{% extends 'app/_base.html' %}
{% load static %}

{% block customcss %}
<link rel='stylesheet' type='text/css' href="{% static 'app/css/style.css' %}">
{% endblock customcss %}

{% block content %}
<section class="common-form">
    {% if form.errors %}
    <p class="error-msg">Your username and password didn't match. Please try again.</p>
    {% endif %}

    {% if next %}
    {% if user.is_authenticated %}
    <p class="error-msg">Your account doesn't have access to this page. To proceed,
        please login with an account that has access.</p>
    {% endif %}
    {% endif %}

    <form class="form-signin" method="POST" action="{% url 'login'%}">{% csrf_token %}
        <h1 class="h3 mb-3 font-weight-normal">Please login</h1>
        <table>
            <tr>
                <td>{{ form.username.label_tag }}</td>
                <td>{{ form.username }}</td>
            </tr>
            <tr>
                <td>{{ form.password.label_tag }}</td>
                <td>{{ form.password }}</td>
            </tr>
        </table>
        <div class="checkbox mb-3">
        </div>
        <button class="btn btn-lg btn-primary btn-block" type="submit">Log in</button>
        <p class="mt-5 mb-3 text-muted">&copy; 2017-2020</p>
    </form>
</section>
{% endblock %}

{% extends 'ファイル名' %}で継承。cssを読み込むときは{% load static %}と追加します。

先にユーザー登録画面も含めたcssファイルを作成しておきます。

style.css(app/static/app/css下)
    div, p, ul, ol, li, dl, dt, dd, h1, h2, h3, h4, h5, h6 label, input, textarea, select, button {
    margin: 0;
    padding: 0;
    color: #555555;
    font-size: 1rem;
    line-height: 1.8;
    box-sizing: border-box;
  }


  h1{
      font-size: 2rem;
      padding-top: 20px;
      padding-bottom: 15px;
  }

  section {
    margin: 0 0 8px;
  }

  .container {
    margin: 0 auto;
    width: 100%;
    max-width: 800px;
  }

  .content {
    padding: 0 8px;
  }
  .common-form label {
    display: block;
  }

  .common-form p {
    margin-bottom: 8px;
  }

  .common-form input, .common-form textarea {
    padding: 4px;
    width: 100%;
    margin-bottom: 8px;
  }

  .common-form select {
    padding: 4px;
    margin-bottom: 8px;
  }

  .common-form .submit {
    margin-top: 8px;
    margin-bottom: 8px;
    padding: 8px 36px;
    border: none;
    color: #ffffff;
    text-align: center;
    text-decoration: none;
    display: inline-block;
    background-color: #4CAF50;
    border-radius: 2px;
  }

  .common-form .delete {
    background-color: #f44336;
  }

  .form-signin {
    width: 100%;
    max-width: 315px;
    padding: 15px;
    margin: auto;
  }
  .form-signin .checkbox {
    font-weight: 400;
  }

  .form-signin h1{
    margin-top: 50px;
  }

ログイン画面と同様に、ユーザー登録画面でもstyle.cssを読み込みます。
ちなみにこのcssはbootstrapのサンプルページ画面からとってきたものです。

ユーザー登録機能(signup)とその画面

まずは新たにアプリを立ち上げます。
terminalで以下を実行。

django-admin startapp accounts
python3 manage.py makemigrations
python3 manage.py migrate

プロジェクトにおいて新しいアプリを認証してもらいます。

settings.py(project下)
  INSTALLED_APPS = [...,
                    'accounts.apps.AccountsConfig',
                   ]
urls.py(project下)
    path('accounts/', include('accounts.urls')), 
    #urlpatternsに追加。
    #これでaccountsアプリのurlがブラウザで探せるようになりました。

accountsアプリに戻ります。
accountsアプリ下にurls.pyを新たに作成します。

urls.py(accounts下)
from django.urls import path

from . import views

# set the application namespace
# https://docs.djangoproject.com/en/2.0/intro/tutorial03/
app_name = 'accounts'

urlpatterns = [
    # ex: /accounts/signup/
    path('signup/', views.SignUpView.as_view(), name='signup'),
]
views.py(accounts下)
from django.contrib.auth.forms import UserCreationForm
from django.urls import reverse_lazy
from django.views import generic


class SignUpView(generic.CreateView):
    form_class = UserCreationForm
    success_url = reverse_lazy('login')
    template_name = 'accounts/signup.html'
    #ユーザー登録のフォーム自体はDjangoに標準で備わっているので、
    #UserCreationFormを指定、reverse_lazyでログイン画面に遷移すると追加、templatesのディレクトリを指定します。

accounts下にtemplates/accountsディレクトリを作成後、

signup.html
{% extends 'app/_base.html' %}
{% load static %}

{% block customcss %}
<link rel='stylesheet' type='text/css' href="{% static 'app/css/style.css' %}">
{% endblock customcss %}

{% block content %}
<h1>Sign up</h1>
<section class="common-form">
    <form method="post">
        {% csrf_token %}
        {{ form.as_p }}
        <button type="submit" class="submit">Sign up</button>
    </form>
</section>
{% endblock %}

ログイン画面と同様style.cssを読み込みます。

振り返り

お疲れさまでした。

今回は最小限の機能と見栄えを実装しました。諸々改善できる点(ログイン失敗時の画面はcssできれいにしていない、signupにある注意事項が嘘になっているとか)はありますが、ひとまず作業フロー全体を捉えることはできました。

個人的には昨年末あたりにやっていた作業をまとめたので、いい復習になりました。その点、書き漏れなどあるかもしれませんが、悪しからず。

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

この2つの書籍を同じ人が出されているって、知りませんでした。

知らなかった

以下のような記事を書いたことがある。

良書「ゼロから作るDeep Learning -- Pythonで学ぶディープラーニングの理論と実装」を読む

良書『入門 Python3』、説明に失敗しているところN選(N=3)。

つまり、対象としている書籍は、

  • ゼロから作るDeep Learning -- Pythonで学ぶディープラーニングの理論と実装
  • 入門 Python3

これ、両方!

斎藤康毅 :tada:

という方が、(後者は、訳本なので監訳であるが、)出されているよう。
巨人ですね。。。

それをふまえ

以下のような記事も書いたが。。。

良書「ゼロから作るDeep Learning 」のGitHub

「ゼロから作るDeep Learning 」でModuleNotFoundError: No module named 'dataset.mnist'が出る理由。

この辺のPythonコードは、かなり、しっかりしたものと考えていいようです!!

ちょっと、ひっかかっていた部分

「ゼロから作るDeep Learning -- Pythonで学ぶディープラーニングの理論と実装」
の1章(ページ数:20)が、
Python入門
となっており、
20ページで、Pythonをざくっと説明されているのだが、
そんな量の説明では、無理でしょーーー、他の書籍とかに任せればーーー
と気になっていましたが、、、ド、プロなんですね。

この記事の目的

これらの書籍、良書です。

良書×良書 :tada: なので、かなり信頼できるのでは?という情報の共有。

「本」は、重要です。<--買いましょう。

ただ、お金がかかるし、紙の本は場所もとるし、重いし、
もとをとるために、良書だった場合には、筆者がネットで公開している良い情報をタダで得て、元を取りましょう!!

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

ゼロから! 入門 Python3! この2つの書籍を同じ人が出されているって。

知らなかった

以下のような記事を書いたことがある。

良書「ゼロから作るDeep Learning -- Pythonで学ぶディープラーニングの理論と実装」を読む

良書『入門 Python3』、説明に失敗しているところN選(N=3)。

つまり、対象としている書籍は、

  • ゼロから作るDeep Learning -- Pythonで学ぶディープラーニングの理論と実装
  • 入門 Python3

これ、両方!

斎藤康毅 :tada:

という方が、(後者は、訳本なので監訳であるが、)出されているよう。
巨人ですね。。。

それをふまえ

以下のような記事も書いたが。。。

良書「ゼロから作るDeep Learning 」のGitHub

「ゼロから作るDeep Learning 」でModuleNotFoundError: No module named 'dataset.mnist'が出る理由。

この辺のPythonコードは、かなり、しっかりしたものと考えていいようです!!

ちょっと、ひっかかっていた部分

「ゼロから作るDeep Learning -- Pythonで学ぶディープラーニングの理論と実装」
の1章(ページ数:20)が、
Python入門
となっており、
20ページで、Pythonをざくっと説明されているのだが、
そんな量の説明では、無理でしょーーー、他の書籍とかに任せればーーー
と気になっていましたが、、、Pythonの ド、プロ なんですね。

この記事の目的

これらの書籍、良書です。

良書 ×? 良書:tada: なので、かなり信頼できるのでは?!

という情報の共有。

以下、余談。
「本」は、いまだ、重要です。<--買いましょう。
ただ、お金がかかるし、紙の本は場所もとるし、重いし、
もとをとるために、
良書だった場合には、
筆者がネットで公開している良い情報をタダで得て、元を取りましょう!!
(というような活動を実践しようとした、2つの書籍を同じ人が出されていることに気づいた(驚いた)ので記事にした次第です。)

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

Django - Qiita上のtutorial appを概観し、機能をプラスする(1)

1年ほど前に書いた記事です。

Publicに公開できるようにQiitaに投稿いたします。

はじめに

この記事では
- Djangoアプリケーションのそれぞれのファイルの相関と大まかな役割を理解できる
- 既存のアプリケーションを概観し、適切な変更を加えられるようになる
- Djangoを知らない人には、ほう、そんな感じか、というイメージを持ってもらえる

ことが目的です。個人的には復習のつもりで取り組みます。

以下の記事のアプリを概観し、
[Python] Djangoチュートリアル - 汎用業務Webアプリを最速で作る

今回は、
ログイン機能とその画面の作成(2)と
画像ファイルのアップロード・表示を可能にする(1)作業を行います。

以下は今回の(1)の作業を終えたもの
https://github.com/Rio157/crud-image-app.git

開発環境

  • Windows10
  • Ubuntu LTS18.04 - Windows for Linux Terminal
  • Visual Studio Code にてUbuntuを開いています。

完成型

93f06bcd-4570-4d5c-ab3d-5d14c993de7f-1920x910r.png
アイテムの一覧画面

7eb35643-b63b-4d38-8557-272241099b27-1920x912r.png
アイテムの詳細画面

機能

今回は画像のアップロードと一覧画面、詳細画面にてアップロードされた画像の表示を施します。

今回は便宜上サーバー側の処理とフロントエンド側の操作をはっきり分けて書きます。

また、画像に関してはまずterminalで

pip3 install pillows

として画像をDjangoで扱えるようにしておきます。

画像のアップロード処理

画像アップロードの処理はほとんど定型的です。
コード操作とその意図を#コメントアウトに書いていきます。

サーバー側で画像を受け取れるようにしていきます。

settings.py(project下)
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/media/'

#まずは画像ファイルの保管、受取先を指定します。最終行当たりに追加。
#MEDIA_ROOT:projectと同じ階層にmediaフォルダがある
#MEDIA_URL:(Djangoでは画像ファイルに1対1でURLが対応するため)メディアファイル公開時のURL指定です。
urls.py(project下)
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
#メディアファイル公開用のURLを追加します。最終行当たりに追加。
forms.py
class ItemForm(forms.ModelForm):

    class Meta:
        model = Item
        fields = ('name','age','sex','memo','images')
        widgets = {
                    'name': forms.TextInput(attrs={'placeholder':'記入例:山田 太郎'}),
                    'age': forms.NumberInput(attrs={'min':1}),
                    'sex': forms.RadioSelect(),
                    'memo': forms.Textarea(attrs={'rows':4}),
                    'images': forms.ClearableFileInput(attrs={'multiple': False}),
                  }
                  #imagesを追加します。これによりメディアファイルをデータベース化し、'images'として引き出すことができるようになりました。
models.py
    images = models.ImageField(
        verbose_name='画像ファイル',
        upload_to='',
        default='defo.png'
    )
    #class Item(models.Model):に追加。upload_to=''でsettings.py上に書いた保存先が指定される。
    #defaultにメディアファイルをアップロードしていないときに表示する画像を指定します。mediaフォルダ下にdefo.pngを配置。

画像の表示

データベース化された画像を引き出し、一覧画面と詳細画面に表示します
フロントエンドをいじるので、templates下の操作になります。

item_filter.html(一覧画面の各アイテムの項目)
                    <!--一覧において画像を表示したい-->
                    <div class="row">
                        <div class="col-3">
                            <p>画像</p>
                        </div>
                        <div class="col-9">
                            <img src="{{ item.images.url }}" width=150>
                        </div>
                    </div>
                    <!--同階層に並ぶ名前、登録日の下に追加します。-->
                    <!--Djangoではサーバー側のデータを引き出すときに{{}}や{%%}で囲むことでディレクトリを指定したりします。-->
item_card.html(アイテムの詳細画面)
<div class="row">
    <div class="col-3">
        <p>画像ファイル</p>
    </div>
    <div class="col-9">
        <form method="post" enctype="multipart/form-data">
            <!--入力した画像を表示させたい-->
            <img src="{{ object.images.url }}" width=300>
        </form>
        {{ form.media }}
    </div>
</div>
<!--項目名:備考の上に追加します。-->
item_forms.html(アイテム作成画面)
    <div class="row">
        <div class="col-12">
            <!-- enctype=...の追加-->
            <form method="post" id="myform" enctype="multipart/form-data">
                {% crispy form%}
            </form>
        </div>
    </div>
<!--{%crispy form%}にメディアファイルを含むデータが格納されてますので、enctypeを指定する必要があります。-->
<!-- なおcrispyについて詳しくは省きますが、crispy_forms appにより入力項目をきれいに整理してくれます。-->

振り返り

今回は画像アップロード機能とその画像表示についてDjangoで実装しました。
ほんとは作成画面、編集画面においてプレビュー機能の実装と、複数画像のアップロード・表示についてもやりたかったですが、(プレビュー機能はJavaScriptでできるようです)今回は簡易な実装で簡易な機能をという趣旨だったので省かせてもらいました。

また、今回の以下記事がメディアファイルでなく、他ファイルのアップロードについて書いていますが、操作はほとんど同じですので比較の参考にしてください。
[Django] ファイルアップロード機能の使い方 [基本設定編]

次回

次回はDjangoでログイン機能の実装を行います。
Djangoではログイン機能が標準で備わっているわけですが、プロジェクト内に新しくアプリケーションをつくる形で実装します。

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

Streamlitで作ったデータ閲覧用のWebアプリをherokuで公開する

概要

  • 前回、Streamlitを使ってWebアプリを作れるようになった
  • せっかくだから公開しよう
  • ということで、できたもの → Chocolate Ball Viewer

前提

  • herokuにアカウントを作っておく(無料でOK)
  • githubのアカウントがある

アプリ作成

こちらなどを参考にアプリを作成します。本記事ではStreamlitの記法などについては触れません。

$ streamlit run [python-file]

で、期待通りにアプリが作成されていることを確認しときます。

herokuにデプロイ

ここからが本記事の本編です。
手順の概要としては次の通りです。

  1. herokuデプロイに必要なファイルの用意
  2. herokuにアプリの作成
  3. デプロイ
  4. 自動デプロイの確認をしてみよう

必要なファイルの用意

herokuにpythonアプリを公開するには次の2つのファイルが必要です。

  • Procfile
    • 実行コマンドを書く
  • requirements.txt
    • 必要なライブラリなどを書いておく

これだけでも十分ですが、streamlitの設定ファイルを作るためのスクリプトを作っておきます。port番号などを指定するものです。(Procfileで起動オプションとして指定してもOK)

  • setup.sh

これらをまとめて、Streamlitアプリをherokuで公開するためのテンプレートを作ってくれている方がいます。
https://github.com/patryk-oleniuk/streamlit-heroku-template

今回はこんな感じで作りました。ほぼほぼstreamlit-heroku-templateからコピーしてきました。

Procfile

web: sh setup.sh && streamlit run src/choco_view.py

requirements.txt(バージョン固定したほうが安心っすね)

pylint
pandas
scipy
matplotlib
seaborn
streamlit

setup.sh

mkdir -p ~/.streamlit/
echo "[server]
headless = true
port = $PORT
enableCORS = false
" > ~/.streamlit/config.toml

これらをまとめてgithubにpushしておきます。プライベートリポジトリでもOKです。

herokuにアプリの作成

herokuのアプリ作成はCLIを使ってコマンドで実行もできますが(herokuチュートリアルはCLIですね)、githubリポジトリと連携させるのであればWeb管理画面からも簡単です。今回はWebから作成します。

右上の「Create New App」からサービス名を設定して「Create app」します。

スクリーンショット 2021-01-15 11.27.25.png

これだけ

デプロイ

createすると、デプロイ方法の選択になります。今回は「GitHub」タブを選択して連携するリポジトリを指定します。

スクリーンショット 2021-01-20 0.01.10.png

連携できると、デプロイ画面に遷移します。

ここで、「Enable Automatic Deploys」を押して自動デプロイを有効にすると、連携したGitHubリポジトリの指定したブランチに更新があれば自動でデプロイされます。

準備ができたらManualDeployメニューから「Deploy Branch」を押してデプロイ開始です。

自動デプロイの確認

データを更新などしてgithubにpushしてみよう。

herokuの管理画面を眺めているとデプロイされているのが見えます(楽しい)。

できた

僕は趣味でチョコボールデータの計測をやっているので、その計測データを可視化するアプリを公開しました(詳しくはチョコボール統計をみてね)。
https://chocolate-view.herokuapp.com/

Flavorを選択できる

ezgif.com-gif-maker.gif

銀のエンゼルが当たるまでに必要な購入数のシミュレーション

ezgif.com-gif-maker (3).gif

おわり

参考

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

正方形の二次配列を渦状に取得する方法!

1. はじめに

皆さん、初めまして!
今、大学でソフトウェア工学を専攻しているモナ坊です。

読まれる前に、読者にはコードよりも考え方を参考として読んで欲しいという希望があります。

突然ですが、皆さん配列の流れに困惑したことはありますでしょうか?
そのくらい分かるわ!とつっこまれるかも知れませんが、もう少し読んでください。

では、正方形の配列をポインターで渦状に移動させる方法はご存知でしょうか?

詳しく説明致します。

本題

では、さっそく本題に入ります。

正方形の二次配列が存在するとします。
例.)

array_traversal.py
array = [
    [1, 2, 3, 4]
    [12, 13, 14, 5]
    [11, 16, 15, 6]
    [10, 9, 8, 7]
]

この 縦x横の二次配列を渦状に一つ一つの要素を渡り、一次配列を取得してください。(時間計算量はO(n))

出力例
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]

考え方

まずは考え方からです。

1. 少し視点を変えて見てみる

1つ目の画像:
Screen Shot 2021-01-20 at 17.56.17.png

1つ目の画像をみるとわかりやすいかも知れません。
渦巻状に取得はするものの、そこで少し視点を変えて問題に取り組んでみると
外側の四角と内側の四角に分けることができます。

そう見てみると少し、複雑さがなくなりました。ね?

次にこの四角を迂回するにあたって、ポインターが2つ(ロウとカラム用)あると考えてください。
ここでのポインターは、簡潔に言うと処理を指示する為に「この処理をここでしてね!」とコンピューターに伝えるためのものと捉えてください。

このポインター2つを渦状に取得させるために考慮する点が1つあります。
それは、二重計上(double counting)の制御です。


補足:
ここでは、ロウは横列、カラムは縦列を指しているます。
二重計上(double counting) は簡潔に言いますと、処理にあたって同じ要素が被ってしまことです。


2. 二重計上の制御

次の画像をご覧ください。

2つ目:
Screen Shot 2021-01-20 at 18.18.11.png

こうすることによってお互いに境界線を作ることができ、また二重計上も防ぐことができるので効率の良い解き方になってくると思います


*SC, EC, SR, ERとは??
SCがカラムのはじめ、ECがカラムの終わり
SRがロウのはじめ、ERがロウの終わり
となっています。


3. 一連の処理の流れ(外側)

それでは、最後に準備が整ったところで流れを説明します。

3.1 トップロウ([1, 2, 3, 4])をカラム用のポインターで取得します

Screen Shot 2021-01-20 at 20.30.51.png

ここは、普段使われているfor loopの仕方でできますね。

サンプルコード:

   for col in range(sc, ec + 1):
       result.append(array[sr][col])

3.2 ECが指しているカラムをERまでロウ用のポインターで取得します。(ここでは、[5, 6, 7]ですね。)

Screen Shot 2021-01-20 at 20.35.16.png

サンプルコード:

   for row in range(sr + 1, er + 1):
       result.append(array[row][ec])

3.3 ボトムロウ([10, 9, 8])を逆方向にカラム用のポインターで取得します。

ここは、reverseというbuilt-in function を使って逆方向のfor loop を実装します。
Screen Shot 2021-01-20 at 20.37.16.png

サンプルコード:

   for col in reversed(range(sc, ec)):
       result.append(array[er][col])

3.4 最後に、2行目の[12]と3行目の[11]を取得したいのでSR+1 ~ ER区間で逆方向にロウ用のポインターで取得します。

ここも、reverse を使います。
Screen Shot 2021-01-20 at 20.39.20.png

サンプルコード:

   for row in reversed(range(sr + 1, er)):
       result.append(array[row][sr])


これで、やっと外側の四角の要素を全て渦状に取得できました。


4. 一連の処理の流れ(内側)

次は、内側の四角です。
Screen Shot 2021-01-20 at 17.56.17.png
でも、このまま処理を同じように回すとhard codingになってしまいますね?

なので、SRとSCを1つ足して内側に寄せます。また、ECとERを1つ足して内側に寄せることによって1~5で作ったものを再利用することができます!ECOですね〜笑

#内側の正方形の処理
sr += 1 
er -= 1
sc += 1
ec -= 1     

以下からは、画像とコードでの説明を省略致します。

  1. トップロウ([13, 14])をカラム用のポインターで取得します
    こちらも同様に、普段使われているfor loopの仕方でできますね。

  2. ECが指しているカラムをERまでロウ用のポインターで取得します。(ここでは、[15]ですね。)

  3. ボトムロウ([16])を逆方向にカラム用のポインターで取得します。
    ここは、reverseというbuilt-in function を使って逆方向のfor loop を実装します。

  4. 最後に、SRがERと等しくなるのでrangeが0になり何も取得できないようになります。

完成コード

array_traversal.py

def spiralTraverse(array):
    result = []
    sr, er = 0, len(array) - 1
    sc, ec = 0, len(array[0]) - 1

    while sr <= er and sc <= ec:
        for col in range(sc, ec + 1):
            result.append(array[sr][col])

        for row in range(sr + 1, er + 1):
            result.append(array[row][ec])

        for col in reversed(range(sc, ec)):
            result.append(array[er][col])

        for row in reversed(range(sr + 1, er)):
            result.append(array[row][sr])

        sr += 1 
        er -= 1
        sc += 1
        ec -= 1

    return result 

終わりに

あくまで、1つの考え方なので、他にも解答方法はいくつもあります。例えば、再帰法を使った処理など。
注意点として、エッジケースをここでは考慮していないのでそこは問題に合わせて対応してください。

長くなりましたが、以上となります!お疲れ様でした!

LGTM よろしくお願いします!

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

PySImpleGUIでファイルの更新履歴や簡単なリリースノートを作成するGUI

はじめに

仕事ではチーム開発などはなく、個人でコードを作成することが多いので、
いままでほとんどコードの更新履歴やリリースノートを書いたことがありませんでした。
(リリースしてないのでリリースノートを書いたことがない)

ふとリリース前にバージョン管理はしっかりしておいた方がいいと思い立ちました。

GitHubをローカルで使う方法もありますが、
使うOSがばらばらで使い方を覚えるのも億劫だったので、
簡易的なバージョン管理ツールを作るに至りました。

開発環境

Windows10
PySImpleGUI 4.29.0
Python 3.7.6
pyinstaller(exe化で使用) 4.1

GUI

GUIは以下のようになります。今回はテーマがLightBrown3です。

更新履歴は事細かに記載する予定なので、リリースより細かい更新が入ります。
そのため、同じファイル名で二度以上更新する場合もありますので、
更新時間をファイル名に追加し、更新したファイルを別のフォルダにコピーします。
それにより、テキストの履歴と実際のファイルの紐づけを取ります。

リリースノートの「タイトル」は、なんとなく変更の概要が書けるようにしました。
makereleasenote_maindisp.png
[更新] … 履歴保存時の時間を更新します。(※押し忘れ防止のために5分おきの自動更新を追加予定)
[選択] … 更新対象のファイルを選択します。
[反映] … 更新履歴をhistory.txtに書き込みます。開発者が具体的に変更点がわかるように書きます。
      同時に、更新時間をファイル名に追加した更新対象ファイルをhistoryフォルダにコピーします。
[書き込み] … release.txtにアップデート内容を書き込みます。ユーザーに分かりやすい言葉で書きます。

コード

以下コードです。

make_note_app.py
import PySimpleGUI as sg
import datetime
import os
import shutil

class MainDisp:
    def __init__(self,now):
        sg.theme("LightBrown3")  #好きなテーマ設定

        self.layout = [
        [sg.Text(text="更新時間"),
         sg.Input(default_text=now, size=(20,1), key=("-TIME-"),
                  use_readonly_for_disable=True),
         sg.Button(button_text="更新", key=("-UPDATE-"))],

        [sg.Text(text="ファイル"),
         sg.InputText(size=(50,1), key='-FILE-'),
         sg.FileBrowse("選択", target=("-FILE-"))],

        [sg.Text(text="更新履歴")],

        [sg.Multiline(size=(200,5), key=("-HIST-"))],

        [sg.Button(button_text="反映", key=("-HANEI-"))],

        [sg.Text(text="リリースノート")],

        [sg.Text(text="タイトル"),
         sg.Input(size=(50,1), key=("-TITLE-"))],

        [sg.Multiline(size=(200,5), key=("-RELEASE-"))],

        [sg.Button(button_text="書き込み", key=("-WRITE-"))]
        ]

        self.window = sg.Window(title="MakeReleaseNote",
                                layout=self.layout,
                                size=(500,400))

    def writefile(self,type):
        time = self.window["-TIME-"].get()
        filename = self.window["-FILE-"].get()

        #更新履歴ver
        if type == "history":
            hist = self.window["-HIST-"].get()
            path = "../history.txt"

            if os.path.exists(path):
                '''
                更新した情報をファイルの上部に書き込みたいので、
                一度ファイルの中身を全部読んでから、頭に新しい情報を付け足して再度書き込む
                '''
                with open(path,"r") as f:
                    all = f.read()

                write = "更新時間:{}\n更新ファイル:{}\n変更内容:\n{}\n".format(
                    time,os.path.basename(filename),hist) + all
            else:
                write = "更新時間:{}\n更新ファイル:{}\n変更内容:\n{}\n".format(
                    time,os.path.basename(filename),hist)

            #更新したファイルをhistoryフォルダにコピー
            #ただしファイル名に時刻を反映するために一回形を整え直す
            s_time = datetime.datetime.strptime(time,"%y/%m/%d %H:%M:%S")
            shutil.copy2(filename,"../history/"+
                s_time.strftime("%y%m%d_%H%M%S_")+os.path.basename(filename))

        #リリースノートver
        elif type == "release":
            hist = self.window["-RELEASE-"].get()
            title = self.window["-TITLE-"].get()
            path = "../release.txt"

            '''
            更新した情報をファイルの上部に書き込みたいので、
            一度ファイルの中身を全部読んでから、頭に新しい情報を付け足して再度書き込む
            '''
            if os.path.exists(path):
                with open(path,"r") as f:
                    all = f.read()

                write = "File:{}\nTitle:{}\nComment:\n{}\n".format(
                    os.path.basename(filename),title,hist) + all
            else:
                write = "File:{}\nTitle:{}\nComment:\n{}\n".format(
                    os.path.basename(filename),title,hist)

        with open(path,"w") as f:
            f.write(write)

    def main(self):
        while True:
            event, values = self.window.read()
            if event == None:  #closeボタン
                break
            elif event == "-UPDATE-":  #時刻更新ボタン
                self.window["-TIME-"].update(gettimenow())
            elif event == "-HANEI-":  #更新履歴反映ボタン
                self.writefile("history")
            elif event == "-WRITE-":  #リリースノート書き込みボタン
                self.writefile("release")

        self.window.close()


#現在時刻をstrで返す関数
def gettimenow():
    time = datetime.datetime.now()
    return time.strftime("%y/%m/%d %H:%M:%S")

if __name__ == "__main__":
    maindisp = MainDisp(gettimenow())
    maindisp.main()

出力ファイル

更新履歴のテキストファイルはこのように最新の情報が上部に記載されます。
history_txt.png

historyフォルダ内は上記の履歴の更新時間と同じ時刻で
更新されたファイルの名前が変更され、コピーされています。
history_folder.png

リリースノートのテキストファイルは下記のようになります。
タイトルがある分、ちょっと体裁が違います。
リリースノートでは本来ファイル名にバージョンを付与した状態で作成する予定だったので、
更新時間は記載していません。ファイル名が更新時間のようなものです。
(下記の例ではファイル名を同じにしてしまったのでわかりにくいですが…)
release.png

おまけ:exe化

pyinstallerをインストールします。

pip install pyinstaller

--noconsoleを入れると起動時にコンソールがでません。

pyinstaller [exe化したいファイルのパス] --noconsole

exe化するとdistと言うフォルダの中に、ファイル名と同じフォルダが作成されます。
その中に、ずらっと必要なファイルがあり、そこにexeも存在します。

本来の位置からずれたディレクトリに作成されるので、
相対パスを使用していた場合エラーになりますのでご注意ください。

相対パスでなければならない場合は、exeと同じディレクトリにあるファイル全てをコピーして、
あってほしい場所に展開すれば実行可能になります。
exeだけの移動ではだめです。

私はアプリ自体がどこにおいても使えるように汎用的であってほしかったので、
絶対パスに書き換えるのではなく、相対パスのまま環境を移動させました。

おわりに

GitHubなどちゃんとバージョン管理している人にはこんなもの関係ないですが、
何が言いたいかと言うと、このくらいのことならPySImpleGUIでならすぐにできるということです。
思いついたことをすぐに実現できる、PythonとPySImpleGUIの組み合わせは本当におすすめです。

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

PySImpleGUIでファイルの更新履歴や簡単なリリースノートを作成

はじめに

仕事ではチーム開発などはなく、個人でコードを作成することが多いので、
いままでほとんどコードの更新履歴やリリースノートを書いたことがありませんでした。
(リリースしてないのでリリースノートを書いたことがない)

ふとリリース前にバージョン管理はしっかりしておいた方がいいと思い立ちました。

GitHubをローカルで使う方法もありますが、
使うOSがばらばらで使い方を覚えるのも億劫だったので、
簡易的なバージョン管理ツールを作るに至りました。

開発環境

Windows10
PySImpleGUI 4.29.0
Python 3.7.6
pyinstaller(exe化で使用) 4.1

GUI

GUIは以下のようになります。今回はテーマがLightBrown3です。

更新履歴は事細かに記載する予定なので、リリースより細かい更新が入ります。
そのため、同じファイル名で二度以上更新する場合もありますので、
更新時間をファイル名に追加し、更新したファイルを別のフォルダにコピーします。
それにより、テキストの履歴と実際のファイルの紐づけを取ります。

リリースノートの「タイトル」は、なんとなく変更の概要が書けるようにしました。
makereleasenote_maindisp.png
[更新]
履歴保存時の時間を更新します。
(※押し忘れ防止のために5分おきの自動更新を追加予定)

[選択]
更新対象のファイルを選択します。

[反映]
更新履歴をhistory.txtに書き込みます。
開発者が具体的に変更点がわかるように書きます。
同時に、更新時間をファイル名に追加した更新対象ファイルを
historyフォルダにコピーします。

[書き込み]
release.txtにアップデート内容を書き込みます。
ユーザーに分かりやすい言葉で書きます。

コード

以下コードです。

make_note_app.py
import PySimpleGUI as sg
import datetime
import os
import shutil

class MainDisp:
    def __init__(self,now):
        sg.theme("LightBrown3")  #好きなテーマ設定

        self.layout = [
        [sg.Text(text="更新時間"),
         sg.Input(default_text=now, size=(20,1), key=("-TIME-"),
                  use_readonly_for_disable=True),
         sg.Button(button_text="更新", key=("-UPDATE-"))],

        [sg.Text(text="ファイル"),
         sg.InputText(size=(50,1), key='-FILE-'),
         sg.FileBrowse("選択", target=("-FILE-"))],

        [sg.Text(text="更新履歴")],

        [sg.Multiline(size=(200,5), key=("-HIST-"))],

        [sg.Button(button_text="反映", key=("-HANEI-"))],

        [sg.Text(text="リリースノート")],

        [sg.Text(text="タイトル"),
         sg.Input(size=(50,1), key=("-TITLE-"))],

        [sg.Multiline(size=(200,5), key=("-RELEASE-"))],

        [sg.Button(button_text="書き込み", key=("-WRITE-"))]
        ]

        self.window = sg.Window(title="MakeReleaseNote",
                                layout=self.layout,
                                size=(500,400))

    def writefile(self,type):
        time = self.window["-TIME-"].get()
        filename = self.window["-FILE-"].get()

        #更新履歴ver
        if type == "history":
            hist = self.window["-HIST-"].get()
            path = "../history.txt"

            if os.path.exists(path):
                '''
                更新した情報をファイルの上部に書き込みたいので、
                一度ファイルの中身を全部読んでから、頭に新しい情報を付け足して再度書き込む
                '''
                with open(path,"r") as f:
                    all = f.read()

                write = "更新時間:{}\n更新ファイル:{}\n変更内容:\n{}\n".format(
                    time,os.path.basename(filename),hist) + all
            else:
                write = "更新時間:{}\n更新ファイル:{}\n変更内容:\n{}\n".format(
                    time,os.path.basename(filename),hist)

            #更新したファイルをhistoryフォルダにコピー
            #ただしファイル名に時刻を反映するために一回形を整え直す
            s_time = datetime.datetime.strptime(time,"%y/%m/%d %H:%M:%S")
            shutil.copy2(filename,"../history/"+
                s_time.strftime("%y%m%d_%H%M%S_")+os.path.basename(filename))

        #リリースノートver
        elif type == "release":
            hist = self.window["-RELEASE-"].get()
            title = self.window["-TITLE-"].get()
            path = "../release.txt"

            '''
            更新した情報をファイルの上部に書き込みたいので、
            一度ファイルの中身を全部読んでから、頭に新しい情報を付け足して再度書き込む
            '''
            if os.path.exists(path):
                with open(path,"r") as f:
                    all = f.read()

                write = "File:{}\nTitle:{}\nComment:\n{}\n".format(
                    os.path.basename(filename),title,hist) + all
            else:
                write = "File:{}\nTitle:{}\nComment:\n{}\n".format(
                    os.path.basename(filename),title,hist)

        with open(path,"w") as f:
            f.write(write)

    def main(self):
        while True:
            event, values = self.window.read()
            if event == None:  #closeボタン
                break
            elif event == "-UPDATE-":  #時刻更新ボタン
                self.window["-TIME-"].update(gettimenow())
            elif event == "-HANEI-":  #更新履歴反映ボタン
                self.writefile("history")
            elif event == "-WRITE-":  #リリースノート書き込みボタン
                self.writefile("release")

        self.window.close()


#現在時刻をstrで返す関数
def gettimenow():
    time = datetime.datetime.now()
    return time.strftime("%y/%m/%d %H:%M:%S")

if __name__ == "__main__":
    maindisp = MainDisp(gettimenow())
    maindisp.main()

出力ファイル

更新履歴のテキストファイルはこのように最新の情報が上部に記載されます。
history_txt.png

historyフォルダ内は上記の履歴の更新時間と同じ時刻で
更新されたファイルの名前が変更され、コピーされています。
history_folder.png

リリースノートのテキストファイルは下記のようになります。
タイトルがある分、ちょっと体裁が違います。
リリースノートでは本来ファイル名にバージョンを付与した状態で作成する予定だったので、
更新時間は記載していません。ファイル名が更新時間のようなものです。
(下記の例ではファイル名を同じにしてしまったのでわかりにくいですが…)
release.png

おまけ:exe化

pyinstallerをインストールします。

pip install pyinstaller

--noconsoleを入れると起動時にコンソールがでません。

pyinstaller [exe化したいファイルのパス] --noconsole

exe化するとdistと言うフォルダの中に、ファイル名と同じフォルダが作成されます。
その中に、ずらっと必要なファイルがあり、そこにexeも存在します。

本来の位置からずれたディレクトリに作成されるので、
相対パスを使用していた場合エラーになりますのでご注意ください。

相対パスでなければならない場合は、exeと同じディレクトリにあるファイル全てをコピーして、
あってほしい場所に展開すれば実行可能になります。
exeだけの移動ではだめです。

私はアプリ自体がどこにおいても使えるように汎用的であってほしかったので、
絶対パスに書き換えるのではなく、相対パスのまま環境を移動させました。

おわりに

GitHubなどちゃんとバージョン管理している人にはこんなもの関係ないですが、
何が言いたいかと言うと、このくらいのことならPySImpleGUIでならすぐにできるということです。
思いついたことをすぐに実現できる、PythonとPySImpleGUIの組み合わせは本当におすすめです。

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

長野県の市町村別コロナ発生状況をグラフで見れるサイトの作成(コード付き)

自分

 経理屋です。35才から趣味でpythonを始めて3年が経ちました。最近は徐々にwebアプリなんかも作れるようになってきてすごく楽しいです。よろしくお願いします。
 まだまだ未熟ですが、誰かの役に立てばいいなと思って記事を書いてみました。

とっかかり

 ここ最近まで長野県は新型コロナの発生が少なかったんですが、最近になってかなり増えてきました。出かける時もどこに出かければいいか少々不安です。
 そんなタイミングで長野県のホームページにコロナ発生状況のCSVデータが公開されているのを見つけたので(今さら)グラフで見れるサイトを作って見ました。
出来上がりはこちら →covid19 in nagano

全体イメージ

Screen Shot 2021-01-20 at 9.13.14.png

手順

目次

  1. データ確認
  2. streamlitでアプリケーション作成
  3. herokuへアップ
  4. GASでherokuを起こす設定

番外編: 継続管理

1.データ確認

 下記リンクに長野県内のコロナ発生状況がアップされているので、お手軽にgoogle colaboratoryで内容を確認します。

https://www.pref.nagano.lg.jp/hoken-shippei/kenko/kenko/kansensho/joho/documents/200000_nagano_covid19_patients.csv

 今回は最終的にstreamlitでアプリを作成するので、データ分析に使ったpythonコードはstreamlit用のファイルでも使います。

import pandas as pd
df = pd.read_csv('https://www.pref.nagano.lg.jp/hoken-shippei/kenko/kenko/kansensho/joho/documents/200000_nagano_covid19_patients.csv', encoding='cp932', header=1)
df.head()

Screen Shot 2021-01-20 at 9.01.27.png

 いろんな項目がありますが、必要なデータだけに絞り込みます。
 項目名をわかりやすい名前に変更します。
 ついでに日付のデータ型をdatetimeに変換します。

df.columns
df = df[['No', '事例確定_年月日', '患者_居住地', '備考']]
df.rename(columns={'事例確定_年月日': '日付', '患者_居住地': '市町村'}, inplace=True)
df['日付'] = pd.to_datetime(df['日付'])
df.head(10)

Screen Shot 2021-01-20 at 9.25.05.png

 東京に住んでて長野で発症したみたいな人は居住地が東京で備考に県内の市町村名が記載されているので、そういった人については市町村を「帰省先:」の後の文字列に変換します。

def change_location(x):
    if str(x['備考'])[:3] == '帰省先':
        x['市町村'] = str(x['備考']).replace('帰省先:', '')
    return x
df = df.apply(change_location, axis=1)
df.head(10)

Screen Shot 2021-01-20 at 9.29.00.png
 変換された市町村を見ると、まだ市町村名でないものが含まれているので、仕方ないので手作業でリストを作って市町村名のものだけ抽出します。それ以外のものは「その他」に変換します。
 市町村名も'南箕輪村'だったり'上伊那郡南箕輪村'だったり'上伊那郡\r\n南箕輪村'だったりしているので、バシッと1本に統一します。
 また、このリストに含まれていない市町村もあるので、たまにそういった市町村が新たに出てきていないか確認してリストを更新してやる必要があります。
 この辺は少し大変です。もっといいアイデアがあれば教えて欲しいです。

df['市町村'].unique()
towns = ['長野市', '山ノ内町',
       '上田市', '松本市', '筑北村', '安曇野市', '佐久穂町', '諏訪市', '須坂市', '南箕輪村',
       '小諸市', '飯田市', '中野市', '軽井沢町', '御代田町', '坂城町', '大町市', '岡谷市',
       '生坂村', '佐久市', '東御市', '千曲市', '長和町', '茅野市', '青木村',
       '原村', '飯山市', '信濃町', '富士見町', '下諏訪町', '伊那市', '栄村',
       '木島平村', '小布施町', '立科町', '宮田村', '塩尻市', '上伊那郡南箕輪村',
       '南佐久郡川上村', '駒ヶ根市', '野沢温泉村', '木曽町', '飯綱町', '飯島町', '辰野町',
       '南木曽町', '白馬村', '髙山村', '箕輪町', '小谷村', '上松町',
       '天龍村', '高森町', '中川村', '朝日村', '山形村', '池田町', '下條村',
       '北佐久郡御代田町', '上伊那郡\r\n南箕輪村', '下高井郡\r\n野沢温泉村', '阿南町',
       '駒ケ根市', '小川村', '喬木村', '松川町']

def change_towns(x):
    if x['市町村'] not in towns:
        x['市町村'] = 'その他'
    return x
df = df.apply(change_towns, axis=1)

df['市町村'] = df['市町村'].str.replace('上伊那郡南箕輪村', '南箕輪村')
df['市町村'] = df['市町村'].str.replace('上伊那郡\r\n南箕輪村', '南箕輪村')
df['市町村'] = df['市町村'].str.replace('下高井郡\r\n野沢温泉村', '野沢温泉村')
df['市町村'] = df['市町村'].str.replace('南佐久郡川上村', '川上村')
df['市町村'] = df['市町村'].str.replace('北佐久郡御代田町', '御代田町')
df['市町村'].unique()

Screen Shot 2021-01-20 at 9.34.00.png
Screen Shot 2021-01-20 at 9.34.25.png
 pivot_tableすると無事に市町村別の発生件数が集計されました。

pivot_daily = df.pivot_table(index='日付', columns='市町村', values='No', aggfunc=len, dropna=False).fillna(0)
pivot_daily.tail()

Screen Shot 2021-01-20 at 9.39.41.png
 累計はcumsumです。

pivot_daily_cum = df.pivot_table(index='日付', columns='市町村', values='No', aggfunc=len, dropna=False).fillna(0).cumsum()
pivot_daily_cum.tail()

Screen Shot 2021-01-20 at 9.42.36.png

2.streamlitでアプリケーション作成

 データが確認できたので、streamlitで実際のアプリケーションを作成します。
 streamlitをインストールし、pythonファイルを作成します。streamlitの使い方に関しては私よりわかりやすく説明されている方がネット上に大勢いるのでそちらにお任せします。
 またstreamlitは公式のtutorialが非常に充実しているので、そちらを一通りやるだけでわかった気になれます。大丈夫です。

view.py
import streamlit as st
import plotly.express as px
import pandas as pd
import datetime

df = pd.read_csv('https://www.pref.nagano.lg.jp/hoken-shippei/kenko/kenko/kansensho/joho/documents/200000_nagano_covid19_patients.csv', encoding='cp932', header=1)
df = df[['No', '事例確定_年月日', '患者_居住地', '備考']]

# 項目名を変更、日付のデータ型変更
df.rename(columns={'事例確定_年月日':'日付', '患者_居住地':'市町村'}, inplace=True)
df['日付'] = pd.to_datetime(df['日付'])

# 備考に「帰省先:」と入っているデータは市町村を変換
def change_location(x):
    if str(x['備考'])[:3] == '帰省先':
        x['市町村'] = str(x['備考']).replace('帰省先:', '')
    return x

df = df.apply(change_location, axis=1)

# 市町村欄に市町村名以外のものが入っていたら変換
towns = ['長野市', '山ノ内町',
       '上田市', '松本市', '筑北村', '安曇野市', '佐久穂町', '諏訪市', '須坂市', '南箕輪村',
       '小諸市', '飯田市', '中野市', '軽井沢町', '御代田町', '坂城町', '大町市', '岡谷市',
       '生坂村', '佐久市', '東御市', '千曲市', '長和町', '茅野市', '青木村',
       '原村', '飯山市', '信濃町', '富士見町', '下諏訪町', '伊那市', '栄村',
       '木島平村', '小布施町', '立科町', '宮田村', '塩尻市', '上伊那郡南箕輪村',
       '南佐久郡川上村', '駒ヶ根市', '野沢温泉村', '木曽町', '飯綱町', '飯島町', '辰野町',
       '南木曽町', '白馬村', '髙山村', '箕輪町', '小谷村', '上松町',
       '天龍村', '高森町', '中川村', '朝日村', '山形村', '池田町', '下條村',
       '北佐久郡御代田町', '上伊那郡\r\n南箕輪村', '下高井郡\r\n野沢温泉村', '阿南町',
       '駒ケ根市', '小川村', '喬木村', '松川町']

def change_towns(x):
    if x['市町村'] not in towns:
        x['市町村'] = 'その他'
    return x
df = df.apply(change_towns, axis=1)

df['市町村'] = df['市町村'].str.replace('上伊那郡南箕輪村', '南箕輪村')
df['市町村'] = df['市町村'].str.replace('上伊那郡\r\n南箕輪村', '南箕輪村')
df['市町村'] = df['市町村'].str.replace('下高井郡\r\n野沢温泉村', '野沢温泉村')
df['市町村'] = df['市町村'].str.replace('南佐久郡川上村', '川上村')
df['市町村'] = df['市町村'].str.replace('北佐久郡御代田町', '御代田町')

# sidemenu
st.sidebar.markdown(
    '# Covid-19 in Nagano'
)
town_selected = st.sidebar.selectbox(
    "市町村", list(df['市町村'].unique()), 1 # デフォルトではリスト番号1の'長野市'を表示
)
st.sidebar.markdown(
    '「その他」には、「松本保健所管内」や「東京都」などの、市町村名でない表記のものが含まれます。'
)

today = datetime.date.today()
start_date = st.sidebar.date_input('開始日', df['日付'].min())
end_date = st.sidebar.date_input('終了日', today)
if start_date < end_date:
    st.sidebar.success('OK')
else:
    st.sidebar.error('Error:終了日は開始日より後の日付にしてください。')

df = df[df['日付'].between(pd.to_datetime(start_date), pd.to_datetime(end_date))]

# body
# 1日あたり発生件数グラフ
st.markdown(
    '# 市町村別発生件数(日付別)'
)
st.markdown(
    'チャートをドラッグすると拡大できます。'
)
pivot_daily = df.pivot_table(index='日付', columns='市町村', values='No', aggfunc=len, dropna=False).fillna(0)
st.write(
    px.bar(pivot_daily, x=pivot_daily.index, y=town_selected)
)
# 発生件数累計推移のグラフ
st.markdown(
    '# 市町村別発生件数(累計)'
)
pivot_daily_cum = df.pivot_table(index='日付', columns='市町村', values='No', aggfunc=len, dropna=False).fillna(0).cumsum()
st.write(
    px.area(pivot_daily_cum, x=pivot_daily_cum.index, y=town_selected)
)

# 市町村別累計発生件数の全市町村比較
st.markdown(
    '# 市町村別発生件数(累計, 全市町村比較)'
)
data_span = st.radio(
    "集計期間",
    ('直近30日', '全期間')
)
if data_span == '直近30日':
    df = df[df['日付'] >= str(today - datetime.timedelta(days=30))]

town_cum = pd.DataFrame(df.groupby('市町村')['No'].count().sort_values())
town_cum.rename(columns={'No':'発生件数累計'}, inplace=True)

st.write(
    px.bar(town_cum, x='発生件数累計', y=town_cum.index, orientation='h', height=1500, hover_data=['発生件数累計', town_cum.index])
)

 Terminalでview.pyのあるディレクトリにcdしてrunするとアプリケーションが立ち上がります。

$ streamlit run view.py

3.herokuへアップ

 herokuへのアップする方法は全然わからなかったので、下記のサイト様を参考にさせていただきました。
→【簡単爆速第2弾】Streamlitをherokuにデプロイ
 他にも色々なサイト様を見せていただいたんですが覚えていません。すみません。

 よくわかっていませんが、下記の手順でやったら動きました。

・必要なディレクトリとファイルを自分のPC内に準備
Screen Shot 2021-01-20 at 10.25.48.png

Porocfile
web: sh setup.sh && streamlit run view.py
requirements.txt
streamlit==0.74.1
plotly==4.14.3
pandas

※下記のメールアドレス部分はご自分のものに変更してください。

setup.sh
mkdir -p ~/.streamlit/

echo "\
[general]\n\
email = \"mailaddress@dmain.com\"\n\
" > ~/.streamlit/credentials.toml

echo "\
[server]\n\
headless = true\n\
enableCORS=false\n\
port = $PORT\n\
" > ~/.streamlit/config.toml

・herokuにアカウント作成
・Terminalを立ち上げて下記を実行
※appnameの部分は自分の好きなアプリ名に変えてください。

$ heroku login
$ heroku create appname
$ git init
$ heroku git:remote -a sample
$ heroku buildpacks:set heroku/python
$ git add .
$ git commit -m "1st commit"
$ git push heroku master
$ heroku open

 上手くいっていればこれでCSVをグラフ化したページが見れると思います。
 間違えてたらすみません。

4.GASでherokuを起こす設定

 herokuは30分間何も実行されないと自動的にスリープになってしまい、次の立ち上がりの時に時間がかかってしまうようです。そこで定期的にherokuにアクセスしてherokuをスリープさせないようにしました。
 いろんなやり方があるようですが、自分はherokuにクレジット登録をしないでおこうと思ったのでGASを選びました。「GAS heroku」で検索すると、非常に丁寧に説明してくれているサイトがたくさん出てきました。ありがたや。

継続管理

 データ分析の中で少し触れましたが、今回のプロジェクトでは市町村のリスト(towns)を手で作成して使っています。なのでこの先townsに入っていない市町村名がCSVに入力されてくるとグラフ用の集計から漏れてしまいます。
 そのためどうしても手作業で市町村名の漏れがないかどうかの確認が必要になってしまいます。めんどいですね。
 1/20の例だと、早速松川町が新たに出てきてました。

import pandas as pd
towns = ['長野市', '山ノ内町',
       '上田市', '松本市', '筑北村', '安曇野市', '佐久穂町', '諏訪市', '須坂市', '南箕輪村',
       '小諸市', '飯田市', '中野市', '軽井沢町', '御代田町', '坂城町', '大町市', '岡谷市',
       '生坂村', '佐久市', '東御市', '千曲市', '長和町', '茅野市', '青木村',
       '原村', '飯山市', '信濃町', '富士見町', '下諏訪町', '伊那市', '栄村',
       '木島平村', '小布施町', '立科町', '宮田村', '塩尻市', '上伊那郡南箕輪村',
       '南佐久郡川上村', '駒ヶ根市', '野沢温泉村', '木曽町', '飯綱町', '飯島町', '辰野町',
       '南木曽町', '白馬村', '髙山村', '箕輪町', '小谷村', '上松町',
       '天龍村', '高森町', '中川村', '朝日村', '山形村', '池田町', '下條村',
       '北佐久郡御代田町', '上伊那郡\r\n南箕輪村', '下高井郡\r\n野沢温泉村', '阿南町',
       '駒ケ根市', '小川村', '喬木村']

df = pd.read_csv('https://www.pref.nagano.lg.jp/hoken-shippei/kenko/kenko/kansensho/joho/documents/200000_nagano_covid19_patients.csv', encoding='cp932', header=1)
df[~df['患者_居住地'].isin(towns)]['患者_居住地'].unique()

Screen Shot 2021-01-20 at 20.43.51.png
 目視で漏れを確認
  ↓
 手元でview.pyを編集してherokuにpush
 もしくはherokuで直接編集でしょうか?
 自分は前者でやってます。

終わりに

 今回は長野県のCSVデータでやってますが、他県でも似たようなデータを公開している(そして自動的に毎日更新されていく)ものがあれば今回の手順がすぐに応用できると思います。
 たいしたコードではないですがもし参考になる部分があれば使って、他県のグラフも作っていただければと思います。
 そして今より少しでも世の中便利になっていけばいいなと思います。

 また、自分は独学で勉強してきたので、もっといいやり方あるよとかここのコードはこう書いた方がいいよとかありましたら教えていただけるとありがたいです。よろしくお願いします。
 終わり。

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

streamlitとherokuで市町村別コロナ発生状況グラフをリリース

自分

 経理屋です。35才から趣味でpythonを始めて3年が経ちました。最近は徐々にwebアプリなんかも作れるようになってきてすごく楽しいです。よろしくお願いします。
 まだまだ未熟ですが、誰かの役に立つかなーと思って記事を書いてみました。

とっかかり

 ここ最近まで長野県は新型コロナの発生が少なかったんですが、最近になってかなり増えてきました。出かける時もどこに出かければいいか少々不安です。
 そんなタイミングで長野県のホームページにコロナ発生状況のCSVデータが公開されているのを見つけたので(今さら)グラフで見れるサイトを作って見ました。
出来上がりはこちら →covid19 in nagano

全体イメージ

Screen Shot 2021-01-20 at 9.13.14.png

手順

目次

  1. データ確認
  2. streamlitでアプリケーション作成
  3. herokuへアップ
  4. GASでherokuを起こす設定

番外編: 継続管理

1.データ確認

 下記リンクに長野県内のコロナ発生状況がアップされているので、お手軽にgoogle colaboratoryで内容を確認します。

https://www.pref.nagano.lg.jp/hoken-shippei/kenko/kenko/kansensho/joho/documents/200000_nagano_covid19_patients.csv

 今回は最終的にstreamlitでアプリを作成するので、データ分析に使ったpythonコードはstreamlit用のファイルでも使います。

import pandas as pd
df = pd.read_csv('https://www.pref.nagano.lg.jp/hoken-shippei/kenko/kenko/kansensho/joho/documents/200000_nagano_covid19_patients.csv', encoding='cp932', header=1)
df.head()

Screen Shot 2021-01-20 at 9.01.27.png

 いろんな項目がありますが、必要なデータだけに絞り込みます。
 項目名をわかりやすい名前に変更します。
 ついでに日付のデータ型をdatetimeに変換します。

df.columns
df = df[['No', '事例確定_年月日', '患者_居住地', '備考']]
df.rename(columns={'事例確定_年月日': '日付', '患者_居住地': '市町村'}, inplace=True)
df['日付'] = pd.to_datetime(df['日付'])
df.head(10)

Screen Shot 2021-01-20 at 9.25.05.png

 東京に住んでて長野で発症したみたいな人は居住地が東京で備考に県内の市町村名が記載されているので、そういった人については市町村を「帰省先:」の後の文字列に変換します。

def change_location(x):
    if str(x['備考'])[:3] == '帰省先':
        x['市町村'] = str(x['備考']).replace('帰省先:', '')
    return x
df = df.apply(change_location, axis=1)
df.head(10)

Screen Shot 2021-01-20 at 9.29.00.png
 変換された市町村を見ると、まだ市町村名でないものが含まれているので、仕方ないので手作業でリストを作って市町村名のものだけ抽出します。それ以外のものは「その他」に変換します。
 市町村名も'南箕輪村'だったり'上伊那郡南箕輪村'だったり'上伊那郡\r\n南箕輪村'だったりしているので、バシッと1本に統一します。
 また、このリストに含まれていない市町村もあるので、たまにそういった市町村が新たに出てきていないか確認してリストを更新してやる必要があります。
 この辺は少し大変です。もっといいアイデアがあれば教えて欲しいです。

df['市町村'].unique()
towns = ['長野市', '山ノ内町',
       '上田市', '松本市', '筑北村', '安曇野市', '佐久穂町', '諏訪市', '須坂市', '南箕輪村',
       '小諸市', '飯田市', '中野市', '軽井沢町', '御代田町', '坂城町', '大町市', '岡谷市',
       '生坂村', '佐久市', '東御市', '千曲市', '長和町', '茅野市', '青木村',
       '原村', '飯山市', '信濃町', '富士見町', '下諏訪町', '伊那市', '栄村',
       '木島平村', '小布施町', '立科町', '宮田村', '塩尻市', '上伊那郡南箕輪村',
       '南佐久郡川上村', '駒ヶ根市', '野沢温泉村', '木曽町', '飯綱町', '飯島町', '辰野町',
       '南木曽町', '白馬村', '髙山村', '箕輪町', '小谷村', '上松町',
       '天龍村', '高森町', '中川村', '朝日村', '山形村', '池田町', '下條村',
       '北佐久郡御代田町', '上伊那郡\r\n南箕輪村', '下高井郡\r\n野沢温泉村', '阿南町',
       '駒ケ根市', '小川村', '喬木村', '松川町']

def change_towns(x):
    if x['市町村'] not in towns:
        x['市町村'] = 'その他'
    return x
df = df.apply(change_towns, axis=1)

df['市町村'] = df['市町村'].str.replace('上伊那郡南箕輪村', '南箕輪村')
df['市町村'] = df['市町村'].str.replace('上伊那郡\r\n南箕輪村', '南箕輪村')
df['市町村'] = df['市町村'].str.replace('下高井郡\r\n野沢温泉村', '野沢温泉村')
df['市町村'] = df['市町村'].str.replace('南佐久郡川上村', '川上村')
df['市町村'] = df['市町村'].str.replace('北佐久郡御代田町', '御代田町')
df['市町村'].unique()

Screen Shot 2021-01-20 at 9.34.00.png
Screen Shot 2021-01-20 at 9.34.25.png
 pivot_tableすると無事に市町村別の発生件数が集計されました。

pivot_daily = df.pivot_table(index='日付', columns='市町村', values='No', aggfunc=len, dropna=False).fillna(0)
pivot_daily.tail()

Screen Shot 2021-01-20 at 9.39.41.png
 累計はcumsumです。

pivot_daily_cum = df.pivot_table(index='日付', columns='市町村', values='No', aggfunc=len, dropna=False).fillna(0).cumsum()
pivot_daily_cum.tail()

Screen Shot 2021-01-20 at 9.42.36.png

2.streamlitでアプリケーション作成

 データが確認できたので、streamlitで実際のアプリケーションを作成します。
 streamlitをインストールし、pythonファイルを作成します。streamlitの使い方に関しては私よりわかりやすく説明されている方がネット上に大勢いるのでそちらにお任せします。
 またstreamlitは公式のtutorialが非常に充実しているので、そちらを一通りやるだけでわかった気になれます。大丈夫です。

view.py
import streamlit as st
import plotly.express as px
import pandas as pd
import datetime

df = pd.read_csv('https://www.pref.nagano.lg.jp/hoken-shippei/kenko/kenko/kansensho/joho/documents/200000_nagano_covid19_patients.csv', encoding='cp932', header=1)
df = df[['No', '事例確定_年月日', '患者_居住地', '備考']]

# 項目名を変更、日付のデータ型変更
df.rename(columns={'事例確定_年月日':'日付', '患者_居住地':'市町村'}, inplace=True)
df['日付'] = pd.to_datetime(df['日付'])

# 備考に「帰省先:」と入っているデータは市町村を変換
def change_location(x):
    if str(x['備考'])[:3] == '帰省先':
        x['市町村'] = str(x['備考']).replace('帰省先:', '')
    return x

df = df.apply(change_location, axis=1)

# 市町村欄に市町村名以外のものが入っていたら変換
towns = ['長野市', '山ノ内町',
       '上田市', '松本市', '筑北村', '安曇野市', '佐久穂町', '諏訪市', '須坂市', '南箕輪村',
       '小諸市', '飯田市', '中野市', '軽井沢町', '御代田町', '坂城町', '大町市', '岡谷市',
       '生坂村', '佐久市', '東御市', '千曲市', '長和町', '茅野市', '青木村',
       '原村', '飯山市', '信濃町', '富士見町', '下諏訪町', '伊那市', '栄村',
       '木島平村', '小布施町', '立科町', '宮田村', '塩尻市', '上伊那郡南箕輪村',
       '南佐久郡川上村', '駒ヶ根市', '野沢温泉村', '木曽町', '飯綱町', '飯島町', '辰野町',
       '南木曽町', '白馬村', '髙山村', '箕輪町', '小谷村', '上松町',
       '天龍村', '高森町', '中川村', '朝日村', '山形村', '池田町', '下條村',
       '北佐久郡御代田町', '上伊那郡\r\n南箕輪村', '下高井郡\r\n野沢温泉村', '阿南町',
       '駒ケ根市', '小川村', '喬木村', '松川町']

def change_towns(x):
    if x['市町村'] not in towns:
        x['市町村'] = 'その他'
    return x
df = df.apply(change_towns, axis=1)

df['市町村'] = df['市町村'].str.replace('上伊那郡南箕輪村', '南箕輪村')
df['市町村'] = df['市町村'].str.replace('上伊那郡\r\n南箕輪村', '南箕輪村')
df['市町村'] = df['市町村'].str.replace('下高井郡\r\n野沢温泉村', '野沢温泉村')
df['市町村'] = df['市町村'].str.replace('南佐久郡川上村', '川上村')
df['市町村'] = df['市町村'].str.replace('北佐久郡御代田町', '御代田町')

# sidemenu
st.sidebar.markdown(
    '# Covid-19 in Nagano'
)
town_selected = st.sidebar.selectbox(
    "市町村", list(df['市町村'].unique()), 1 # デフォルトではリスト番号1の'長野市'を表示
)
st.sidebar.markdown(
    '「その他」には、「松本保健所管内」や「東京都」などの、市町村名でない表記のものが含まれます。'
)

today = datetime.date.today()
start_date = st.sidebar.date_input('開始日', df['日付'].min())
end_date = st.sidebar.date_input('終了日', today)
if start_date < end_date:
    st.sidebar.success('OK')
else:
    st.sidebar.error('Error:終了日は開始日より後の日付にしてください。')

df = df[df['日付'].between(pd.to_datetime(start_date), pd.to_datetime(end_date))]

# body
# 1日あたり発生件数グラフ
st.markdown(
    '# 市町村別発生件数(日付別)'
)
st.markdown(
    'チャートをドラッグすると拡大できます。'
)
pivot_daily = df.pivot_table(index='日付', columns='市町村', values='No', aggfunc=len, dropna=False).fillna(0)
st.write(
    px.bar(pivot_daily, x=pivot_daily.index, y=town_selected)
)
# 発生件数累計推移のグラフ
st.markdown(
    '# 市町村別発生件数(累計)'
)
pivot_daily_cum = df.pivot_table(index='日付', columns='市町村', values='No', aggfunc=len, dropna=False).fillna(0).cumsum()
st.write(
    px.area(pivot_daily_cum, x=pivot_daily_cum.index, y=town_selected)
)

# 市町村別累計発生件数の全市町村比較
st.markdown(
    '# 市町村別発生件数(累計, 全市町村比較)'
)
data_span = st.radio(
    "集計期間",
    ('直近30日', '全期間')
)
if data_span == '直近30日':
    df = df[df['日付'] >= str(today - datetime.timedelta(days=30))]

town_cum = pd.DataFrame(df.groupby('市町村')['No'].count().sort_values())
town_cum.rename(columns={'No':'発生件数累計'}, inplace=True)

st.write(
    px.bar(town_cum, x='発生件数累計', y=town_cum.index, orientation='h', height=1500, hover_data=['発生件数累計', town_cum.index])
)

 Terminalでview.pyのあるディレクトリにcdしてrunするとアプリケーションが立ち上がります。

$ streamlit run view.py

3.herokuへアップ

 herokuへのアップする方法は全然わからなかったので、下記のサイト様を参考にさせていただきました。
→【簡単爆速第2弾】Streamlitをherokuにデプロイ
 他にも色々なサイト様を見せていただいたんですが覚えていません。すみません。

 よくわかっていませんが、下記の手順でやったら動きました。

・必要なディレクトリとファイルを自分のPC内に準備
Screen Shot 2021-01-20 at 10.25.48.png

Porocfile
web: sh setup.sh && streamlit run view.py
requirements.txt
streamlit==0.74.1
plotly==4.14.3
pandas

※下記のメールアドレス部分はご自分のものに変更してください。

setup.sh
mkdir -p ~/.streamlit/

echo "\
[general]\n\
email = \"mailaddress@dmain.com\"\n\
" > ~/.streamlit/credentials.toml

echo "\
[server]\n\
headless = true\n\
enableCORS=false\n\
port = $PORT\n\
" > ~/.streamlit/config.toml

・herokuにアカウント作成
・Terminalを立ち上げて下記を実行
※appnameの部分は自分の好きなアプリ名に変えてください。

$ heroku login
$ heroku create appname
$ git init
$ heroku git:remote -a sample
$ heroku buildpacks:set heroku/python
$ git add .
$ git commit -m "1st commit"
$ git push heroku master
$ heroku open

 上手くいっていればこれでCSVをグラフ化したページが見れると思います。
 間違えてたらすみません。

4.GASでherokuを起こす設定

 herokuは30分間何も実行されないと自動的にスリープになってしまい、次の立ち上がりの時に時間がかかってしまうようです。そこで定期的にherokuにアクセスしてherokuをスリープさせないようにしました。
 いろんなやり方があるようですが、自分はherokuにクレジット登録をしないでおこうと思ったのでGASを選びました。「GAS heroku」で検索すると、非常に丁寧に説明してくれているサイトがたくさん出てきました。ありがたや。

継続管理

 データ分析の中で少し触れましたが、今回のプロジェクトでは市町村のリスト(towns)を手で作成して使っています。なのでこの先townsに入っていない市町村名がCSVに入力されてくるとグラフ用の集計から漏れてしまいます。
 そのためどうしても手作業で市町村名の漏れがないかどうかの確認が必要になってしまいます。めんどいですね。
 1/20の例だと、早速松川町が新たに出てきてました。

import pandas as pd
towns = ['長野市', '山ノ内町',
       '上田市', '松本市', '筑北村', '安曇野市', '佐久穂町', '諏訪市', '須坂市', '南箕輪村',
       '小諸市', '飯田市', '中野市', '軽井沢町', '御代田町', '坂城町', '大町市', '岡谷市',
       '生坂村', '佐久市', '東御市', '千曲市', '長和町', '茅野市', '青木村',
       '原村', '飯山市', '信濃町', '富士見町', '下諏訪町', '伊那市', '栄村',
       '木島平村', '小布施町', '立科町', '宮田村', '塩尻市', '上伊那郡南箕輪村',
       '南佐久郡川上村', '駒ヶ根市', '野沢温泉村', '木曽町', '飯綱町', '飯島町', '辰野町',
       '南木曽町', '白馬村', '髙山村', '箕輪町', '小谷村', '上松町',
       '天龍村', '高森町', '中川村', '朝日村', '山形村', '池田町', '下條村',
       '北佐久郡御代田町', '上伊那郡\r\n南箕輪村', '下高井郡\r\n野沢温泉村', '阿南町',
       '駒ケ根市', '小川村', '喬木村']

df = pd.read_csv('https://www.pref.nagano.lg.jp/hoken-shippei/kenko/kenko/kansensho/joho/documents/200000_nagano_covid19_patients.csv', encoding='cp932', header=1)
df[~df['患者_居住地'].isin(towns)]['患者_居住地'].unique()

Screen Shot 2021-01-20 at 20.43.51.png
 目視で漏れを確認
  ↓
 手元でview.pyを編集してherokuにpush
 もしくはherokuで直接編集でしょうか?
 自分は前者でやってます。

終わりに

 今回は長野県のCSVデータでやってますが、他県でも似たようなデータを公開している(そして自動的に毎日更新されていく)ものがあれば今回の手順がすぐに応用できると思います。
 たいしたコードではないですがもし参考になる部分があれば使って、他県のグラフも作っていただければと思います。
 そして今より少しでも世の中便利になっていけばいいなと思います。

 また、自分は独学で勉強してきたので、もっといいやり方あるよとかここのコードはこう書いた方がいいよとかありましたら教えていただけるとありがたいです。よろしくお願いします。
 終わり。

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

manimの作法 その23

概要

manimの作法、調べてみた。
VectorScene使ってみた。

サンプルコード

from manimlib.imports import *

class test(VectorScene):
    def construct(self):
        two_dot = TexMobject("2\\cdot")
        equals = TexMobject("=")
        self.add_axes()
        v = self.add_vector([3, 1])
        v_coords, vx_line, vy_line = self.vector_to_coords(v, clean_up = False)
        self.play(ApplyMethod(v_coords.to_edge, UP))
        two_dot.next_to(v_coords, LEFT)
        equals.next_to(v_coords, RIGHT)
        two_v = self.add_vector([6, 2], animate = False)
        self.remove(two_v)
        self.play(Transform(v.copy(), two_v), Write(two_dot, run_time = 1))
        two_v_coords, two_v_x_line, two_v_y_line = self.vector_to_coords(two_v, clean_up = False)
        self.play(ApplyMethod(two_v_coords.next_to, equals, RIGHT),Write(equals, run_time = 1))
        self.wait(2)
        x, y = v_coords.get_mob_matrix().flatten()
        two_v_elems = two_v_coords.get_mob_matrix().flatten()
        x_sym, y_sym = list(map(TexMobject, ["x", "y"]))
        two_x_sym, two_y_sym = list(map(TexMobject, ["2x", "2y"]))
        #VMobject(x_sym, two_x_sym).set_color(X_COLOR)
        #VMobject(y_sym, two_y_sym).set_color(Y_COLOR)
        syms = [x_sym, y_sym, two_x_sym, two_y_sym]
        #VMobject(*syms).scale(VECTOR_LABEL_SCALE_FACTOR)
        for sym, num in zip(syms, [x, y] + list(two_v_elems)):
            sym.move_to(num)
        #self.play(Transform(x, x_sym), Transform(y, y_sym), FadeOut(VMobject(*two_v_elems)))
        self.wait()
        #self.play(Transform(VMobject(two_dot.copy(), x.copy()), two_x_sym), Transform(VMobject(two_dot.copy(), y.copy() ), two_y_sym))
        #self.wait(2)





生成した動画

https://www.youtube.com/watch?v=Kq7Ptpt6YZQ

以上。

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

python codeの自動整形をするアクションを作ってみた

Why

  • チーム開発をしていると、コードフォーマットの差が大きい
    • やる人もいれば、やらない人もいる
    • フォーマッタが異なる
  • コード整形のみのコミットを入れたくない
  • 開発者に手間を掛けさせず、且つローカル環境の依存をなくしたい

    • black, autopepなどのライブラリをローカル環境に依存したくない
    • vscodeやemacsなどエディタに依存したくない
  • github actionsを触ってみたかった

    • 初めてのお題としては丁度いいと思った

What

  • 作ったもの
    • 所定のbranchにpushした時に、所定(今回の例だと、workspace/python以下)の*.pyコードをサーチし、対象となったpythonファイルにblackによるコード整形を実行する
    • プルリク→マージも想定して、マージ先のブランチにマージされた時にも、上記コード整形を実行する
    • リポジトリ
  • 前提
    • コード整形にはblackを用いる
    • ブランチは適当に、main/developの2つ
  • ディレクトリ構成
    • code-format.ymlが今回のアクションの中身
├─.github
│  └─workflows
│          code-format.yml
│
└─python
    │  pyproject.toml
    │
    └─source
            a.py
            b.py

How

  • code-format.ymlは以下のようにしました
code-format.yml
name: python auto code format

on:
  push:
    branches: 
      - main
      - develop
    paths:
      - 'python/**'

  workflow_dispatch:

jobs:
  black:
    name: Check code style with Black
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: ./python
    steps:    
      - name: Checkout
        uses: actions/checkout@v2
      - name: Set up Python 3.6.8
        uses: actions/setup-python@v2
        with:
          python-version: 3.6.8
      - name: Install black
        run: pip install black
      - name: run code format
        run: find . -type f -name "*.py" | xargs black
      - name: git settings
        run: |
          git config --local user.email "github-actions[bot]@users.noreply.github.com"
          git config --local user.name "github-actions[bot]"
      - name: Extract branch name
        id: extract_branch
        run: echo "##[set-output name=BRANCH_NAME;]$(echo ${GITHUB_REF#refs/heads/})"
      - name: commit
        run: git commit -m "python modules changed auto format." -a || echo "No changes to commit."
      - name: push
        run: |
          echo target branch is ${{ steps.extract_branch.outputs.BRANCH_NAME }}
          git pull
          git push origin ${{ steps.extract_branch.outputs.BRANCH_NAME }}

  • 確認方法

    1. developブランチから、適当なtopicブランチを作成する
    2. 適当なtopicブランチで、適当なコード(今回はb.py)編集(フォーマットを崩す)
    3. commit & push
    4. github上でプルリク生成
    5. 適当なtopicブランチ→developにmerge
    6. アクションが走る
    7. developブランチのb.pyが、コード整形された状態でpushされている
  • actionsの様子
    image.png

  • developブランチのコミット履歴

    • ちょっとわかりにくいですが以下、github actionsがコミットした履歴が残っており、きれいにフォーマットされているのが確認できます image.png

lastly

今回はtopicブランチ→developブランチを想定して行いましたが、develop→mainも同様です
また、このケースだと、main or developブランチにダイレクトコミットした場合もactionsが走ります

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

manimの作法 その22

概要

manimの作法、調べてみた。
ZetaTransformationScene使ってみた。

サンプルコード

from manimlib.imports import *
#import mpmath

#mpmath.mp.dps = 7
def zeta(z):
    max_norm = FRAME_X_RADIUS
    try:
        return np.complex(mpmath.zeta(z))
    except:
        return np.complex(max_norm, 0)
def d_zeta(z):
    epsilon = 0.01
    return (zeta(z + epsilon) - zeta(z))/epsilon

class ZetaTransformationScene(ComplexTransformationScene):
    CONFIG = {
        "anchor_density" : 35,
        "min_added_anchors" : 10,
        "max_added_anchors" : 300,
        "num_anchors_to_add_per_line" : 75,
        "post_transformation_stroke_width" : 2,
        "default_apply_complex_function_kwargs" : {
            "run_time" : 5,
        },
        "x_min" : 1,
        "x_max" : int(FRAME_X_RADIUS+2),
        "y_min" : 1,
        "y_max" : int(FRAME_X_RADIUS+2),
        "extra_lines_x_min" : -2,
        "extra_lines_x_max" : 4,
        "extra_lines_y_min" : -2,
        "extra_lines_y_max" : 2,
    }
    def prepare_for_transformation(self, mob):
        for line in mob.family_members_with_points():
            if not isinstance(line, Line):
                line.insert_n_curves(self.min_added_anchors)
                continue
            p1 = line.get_start()+LEFT
            p2 = line.get_end()+LEFT
            t = (-np.dot(p1, p2-p1))/(get_norm(p2-p1)**2)
            closest_to_one = interpolate(line.get_start(), line.get_end(), t)
            diameter = abs(zeta(complex(*closest_to_one[:2])))
            target_num_curves = np.clip(int(self.anchor_density*np.pi*diameter),self.min_added_anchors,self.max_added_anchors,)
            num_curves = line.get_num_curves()
            if num_curves < target_num_curves:
                line.insert_n_curves(target_num_curves-num_curves)
            line.make_smooth()
    def add_extra_plane_lines_for_zeta(self, animate = False, **kwargs):
        dense_grid = self.get_dense_grid(**kwargs)
        if animate:
            self.play(ShowCreation(dense_grid))
        self.plane.add(dense_grid)
        self.add(self.plane)
    def get_dense_grid(self, step_size = 1./16):
        epsilon = 0.1
        x_range = np.arange(max(self.x_min, self.extra_lines_x_min),min(self.x_max, self.extra_lines_x_max),step_size)
        y_range = np.arange(max(self.y_min, self.extra_lines_y_min),min(self.y_max, self.extra_lines_y_max),step_size)
        vert_lines = VGroup(*[Line(self.y_min*UP,self.y_max*UP,).shift(x*RIGHT) for x in x_range if abs(x-1) > epsilon])
        vert_lines.set_color_by_gradient(self.vert_start_color, self.vert_end_color)
        horiz_lines = VGroup(*[Line(self.x_min*RIGHT,self.x_max*RIGHT,).shift(y*UP) for y in y_range if abs(y) > epsilon])
        horiz_lines.set_color_by_gradient(self.horiz_start_color, self.horiz_end_color)
        dense_grid = VGroup(horiz_lines, vert_lines)
        dense_grid.set_stroke(width = 1)
        return dense_grid
    def add_reflected_plane(self, animate = False):
        reflected_plane = self.get_reflected_plane()
        if animate:
            self.play(ShowCreation(reflected_plane, run_time = 5))
        self.plane.add(reflected_plane)
        self.add(self.plane)
    def get_reflected_plane(self):
        reflected_plane = self.plane.copy()
        reflected_plane.rotate(np.pi, UP, about_point = RIGHT)
        for mob in reflected_plane.family_members_with_points():
            mob.set_color(Color(rgb = 1-0.5*color_to_rgb(mob.get_color())))
        self.prepare_for_transformation(reflected_plane)
        reflected_plane.submobjects = list(reversed(reflected_plane.family_members_with_points()))
        return reflected_plane
    def apply_zeta_function(self, **kwargs):
        transform_kwargs = dict(self.default_apply_complex_function_kwargs)
        transform_kwargs.update(kwargs)
        self.apply_complex_function(zeta, **kwargs)

class test(ZetaTransformationScene):
    CONFIG = {
        "anchor_density" : 15,
    }
    def construct(self):
        self.add_transformable_plane()
        self.add_extra_plane_lines_for_zeta()
        self.prepare_for_transformation(self.plane)
        print(sum([mob.get_num_points() for mob in self.plane.family_members_with_points()]))
        print(len(self.plane.family_members_with_points()))
        #self.apply_zeta_function()
        self.wait()





生成した動画

https://www.youtube.com/watch?v=glq_Jw-U0qE

以上。

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

manimの作法 その21

概要

manimの作法、調べてみた。
ZoomedScene使ってみた。

サンプルコード

from manimlib.imports import *

class test(ZoomedScene):
    CONFIG = {
        "zoom_factor": 0.3,
        "zoomed_display_height": 1,
        "zoomed_display_width": 6,
        "image_frame_stroke_width": 20,
        "zoomed_camera_config": {
            "default_frame_stroke_width": 3,
        },
    }
    def construct(self):
        dot = Dot().shift(UL * 2)
        image = ImageMobject(np.uint8([[0, 100, 30, 200], [255, 0, 5, 33]]))
        image.set_height(7)
        frame_text = TextMobject("Frame", color = PURPLE).scale(1.4)
        zoomed_camera_text = TextMobject("Zoomed camera", color = RED).scale(1.4)
        self.add(image, dot)
        zoomed_camera = self.zoomed_camera
        zoomed_display = self.zoomed_display
        frame = zoomed_camera.frame
        zoomed_display_frame = zoomed_display.display_frame
        frame.move_to(dot)
        frame.set_color(PURPLE)
        zoomed_display_frame.set_color(RED)
        zoomed_display.shift(DOWN)
        zd_rect = BackgroundRectangle(zoomed_display, fill_opacity = 0, buff = MED_SMALL_BUFF)
        self.add_foreground_mobject(zd_rect)
        unfold_camera = UpdateFromFunc(zd_rect, lambda rect: rect.replace(zoomed_display))
        frame_text.next_to(frame, DOWN)
        self.play(ShowCreation(frame), FadeInFrom(frame_text, direction = DOWN))
        self.activate_zooming()
        self.play(self.get_zoomed_display_pop_out_animation(), unfold_camera)
        zoomed_camera_text.next_to(zoomed_display_frame, DOWN)
        self.play(FadeInFrom(zoomed_camera_text, direction = DOWN))
        scale_factor = [0.5, 1.5, 0]
        self.play(frame.scale, scale_factor, zoomed_display.scale, scale_factor, FadeOut(zoomed_camera_text), FadeOut(frame_text))
        self.wait()
        self.play(ScaleInPlace(zoomed_display, 2))
        self.wait()
        self.play(frame.shift, 2.5 * DOWN)
        self.wait()
        self.play(self.get_zoomed_display_pop_out_animation(), unfold_camera, rate_func = lambda t: smooth(1 - t))
        self.play(Uncreate(zoomed_display_frame), FadeOut(frame))
        self.wait()


生成した動画

https://www.youtube.com/watch?v=GetSZncd4lY

以上。

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

過少学習と過学習

過少学習と過学習

過少学習 モデルがデータの裏にある時ロジックをとらえる事ができていない
過学習 訓練がトレーニングデータに対して過剰に適合しており本質から逸脱している

ライブラリのインポート

import numpy as np 
from sklearn.model_selection import train_test_splita

/train_test_splita = 名前を見てわかる通り訓練用のモジュール/

分割するデータの作成

a = np.arange(1,101)
a

分割したデータの出力

array([ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,
14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26,
27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39,
40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52,
53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65,
66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78,
79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91,
92, 93, 94, 95, 96, 97, 98, 99, 100])

1~100までの値が配列として格納されているのが分かる
/ここでデータが配列であることがポイントである 
 →データを二つに区切る手順が楽になる
/

b = np.arange(501,601) 
b
array([501, 502, 503, 504, 505, 506, 507, 508, 509, 510, 511, 512, 513,
       514, 515, 516, 517, 518, 519, 520, 521, 522, 523, 524, 525, 526,
       527, 528, 529, 530, 531, 532, 533, 534, 535, 536, 537, 538, 539,
       540, 541, 542, 543, 544, 545, 546, 547, 548, 549, 550, 551, 552,
       553, 554, 555, 556, 557, 558, 559, 560, 561, 562, 563, 564, 565,
       566, 567, 568, 569, 570, 571, 572, 573, 574, 575, 576, 577, 578,
       579, 580, 581, 582, 583, 584, 585, 586, 587, 588, 589, 590, 591,
       592, 593, 594, 595, 596, 597, 598, 599, 600])

a .bと両方に分割データを作成

データの分割

train_test_split(a) #(b)にして同じデータの分割ができるか確認
[array([87, 32, 90,  1,  2,  8, 51, 73, 22, 95,  4, 57, 27, 58, 48, 99, 96,  
        74, 72, 29, 76, 64,  3, 12, 53,  6, 18, 16, 65, 66, 63, 46, 39, 17, 
        91, 25, 15, 78, 83, 19, 45, 68, 33, 98, 97, 14, 44, 86, 80, 34, 70,
        47, 54, 93, 94, 85, 42, 60, 92, 41, 61, 71, 89, 23, 21, 11, 84, 13,
        82, 59, 49, 79, 36, 55,  5]),
 array([ 24,  56,  40,   9,  69,  75,  10,  28,  38,  30,  62,  67, 100,
         88,  37,  20,   7,  31,  77,  43,  35,  26,  81,  52,  50])]

2つのデータ(オブジェクト)を4つに分割していきます

a_train, a_test = train_test_split(a, b, test_size=0.2, random_state=365)

結果の確認

形状の確認

a_train,shape.a_test,shape

((80,), (20,))

データの中身の確認

a_train

array([ 25,  32,  99,  73,  91,  66,   3,  59,  94,   1,   8,  15,  90,
        54,  31,  20,  77,  82,  30,  35,  95,  42,  38,   7,  11,  50,
        21,  48,   2,  17,  10,  58,  68,  43,  41,  16,  88,  72,  79,
       100,  80,  39,  24,  86,  22,  23,  62,  76,  18,  47,  55,  26,
        60,  19,  71,  64,  51,  63,  65,  28,  12,  78,  13,  44,  75,
        87,  40,   4,  29,  49,  37,  57,  27,  74,   6,  45,  92,  34,
        53,  83])

/この状態で出力した場合はデータがシャッフルされている状態
殆どの場合はデータはシャッフしています。
/

データの中身の確認

a_test

array([ 9, 69, 81, 56, 33, 93, 84, 61, 46, 89, 85, 67, 97,  5, 70, 36, 98,
       96, 14, 52])

/rain_test_splitのメリット 
 配列もしくわ行列おランダムな訓練用とテスト用データに分けることができる
/

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

manimの作法 その20

概要

manimの作法、調べてみた。
MovingCameraScene使ってみた。

サンプルコード

from manimlib.imports import *

class test(GraphScene, MovingCameraScene):
    def setup(self):
        GraphScene.setup(self)
        MovingCameraScene.setup(self)
    def construct(self):
        self.camera_frame.save_state()
        self.setup_axes(animate = False)
        graph = self.get_graph(lambda x: np.sin(x), color = BLUE, x_min = 0, x_max = 3 * PI)
        moving_dot = Dot().move_to(graph.points[0]).set_color(ORANGE)
        dot_at_start_graph = Dot().move_to(graph.points[0])
        dot_at_end_grap = Dot().move_to(graph.points[-1])
        self.add(graph, dot_at_end_grap, dot_at_start_graph, moving_dot)
        self.play(self.camera_frame.scale, 0.5, self.camera_frame.move_to, moving_dot)
        def update_curve(mob):
            mob.move_to(moving_dot.get_center())
        self.camera_frame.add_updater(update_curve)
        self.play(MoveAlongPath(moving_dot, graph, rate_func = linear))
        self.camera_frame.remove_updater(update_curve)
        self.play(Restore(self.camera_frame))


生成した動画

https://www.youtube.com/watch?v=63cCz1Rou3Q

以上。

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

PyInstallerでエラーが出るとき

PyInstallerを使うと、Pythonで作ったプログラムを実行ファイルに変換できるので、とても便利です。しかも、以下のような感じで書くと、ライブラリも全部まとめて一つのファイルになります。かなり便利。

test.pyをtest.exeに変換する方法:

$ pyinstaller test.py --onefile

PyInstallerのインストール

以下のようにpipを使って、PyInstallerをインストールできます。

$ pip install pyinstaller

エラーが出る場合 - PythonバージョンやmacOS

macOSや新しいPythonのバージョンの対応は遅れています。対応バージョンを使いましょう。

エラーが出る場合 - 暗黙的なインポート

暗黙的なインポートがある場合、エラーが出ます。以下のように、『--hidden-import=パッケージ名』を追記することで暗黙的なモジュールも追記します。

$ pyinstaller -F --hidden-import="sklearn.utils._cython_blas" --hidden-import="sklearn.neighbors.typedefs" --hidden-import="sklearn.neighbors.quad_tree" --hidden-import="sklearn.tree._utils" test.py

また、プログラム内に「import xxx」と暗黙的なモジュールを記載しておくこともできます。

EXE化すると __file__ や __dir__ は使えない

そこで、exeファイルのパスはsys.argv[0]で取得します。

test.py
import os, sys
exe = os.path.abspath(sys.argv[0])
exe_dir = os.path.dirname(exe)

参考

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

【Django】メモリを節約しつつ大容量ファイルをダウンロードする。

Django で大容量ファイルをダウンロードする際にメモリエラーを回避する方法。
StreamingHttpResponsewsgiref.util.FileWrapperを使う。

※関数ベースのviewの実装例を記載

HttpResponseを使う場合

from django.http import HttpResponse

def download_view(request, *args, **kwargs):
    with open('path/to/dir/small.csv', 'rb') as f:
        response = HttpResponse(f.read(), content_type='text/csv')
    response['Content-Disposition'] = 'attachment; filename=small.csv'
    return response

f.read()の部分でファイルの全容量をメモリに読み込んでしまう。

StreamingHttpResponseFileWrapperを使う場合

import os
from wsgiref.util import FileWrapper

from django.http import StreamingHttpResponse

# 8MB ずつ読み込む
chunksize = 8 * (1024 ** 2)

def streaming_download_view(request, *args, **kwargs):
    path = os.path.join('path/to/dir', 'large.csv')
    response = StreamingHttpResponse(
        FileWrapper(open(path, 'rb'), chunksize),
        content_type='text/csv'
    )
    response['Content-Length'] = os.path.getsize(path)
    response['Content-Disposition'] = 'attachment; filename=large.csv'
    return response

なお FileWrapper の第一引数には"filelike-object"を指定するが、コンテキストマネージャを使うと(なぜか)エラーが発生する。

あんまり調査していないが、ドキュメントの使用例と同じようにopen()を使うのが無難である。

失敗する例(コンテキストマネージャを使用)
...
def streaming_download_view(request, *args, **kwargs):
    path = os.path.join('path/to/dir', 'large.csv')
    with open(path, 'rb') as f:
        response = StreamingHttpResponse(
            FileWrapper(f, chunksize),
            content_type='text/csv'
        )
        ...
        return response
エラー出力
Traceback (most recent call last):
  File "/home/ec2-user/.pyenv/versions/3.7.7/lib/python3.7/wsgiref/handlers.py", line 138, in run
    self.finish_response()
  File "/home/ec2-user/.pyenv/versions/3.7.7/lib/python3.7/wsgiref/handlers.py", line 183, in finish_response
    for data in self.result:
  File "/home/ec2-user/.pyenv/versions/3.7.7/lib/python3.7/wsgiref/util.py", line 30, in __next__
    data = self.filelike.read(self.blksize)
ValueError: read of closed file

応用: ダウンロード後にファイルを削除する

ダウンロードするファイルを一時ファイル的に扱いたい場合にはTemporaryDirectoryと組み合わせることで実現できる。

import os
from tempfile import TemporaryDirectory
from wsgiref.util import FileWrapper

from django.http import StreamingHttpResponse

# 8MB ずつ読み込む
chunksize = 8 * (1024 ** 2)


def generate_return_file(target_dir):
    # ダウンロード対象のファイルを指定ディレクトリ配下に作成する何らかの処理
    pass


def streaming_download_view_with_tempdir(request, *args, **kwargs):
    with TemporaryDirectory() as tempdir:
        path = os.path.join(tempdir, 'large.csv')
        # tempdir 配下に対象ファイルを作成する
        generate_return_file(tempdir)

        response = StreamingHttpResponse(
            FileWrapper(open(path, 'rb'), chunksize),
            content_type='text/csv'
        )
        response['Content-Length'] = os.path.getsize(path)
        response['Content-Disposition'] = 'attachment; filename=large.csv'
        return response
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

開発未経験者が覆面算ソルバーを作ったはなし

はじめに

 こんにちは。0kayu806059です。
 この記事がQiita初投稿となります。
 タイトルにもある通り、この度私は、覆面算ソルバーを作りました。
 とても簡単なWebアプリですが、完成までに至った経緯をここに残したいと思います。

そもそも覆面算とは?

 覆面算とは、数字の一部(または全部)がアルファベットに置き換えられた数式が与えられるので、各アルファベットに当てはまる数字を見つけて数式が成り立つようにするパズルです。
 例えば、
ABC + BAC = CACA
といった数式が与えられます。この場合A、B、Cにどんな数字が入るかを考えるといった感じです。中学受験とかで見るやつですね。ちなみに答えは、A = 2, B = 9, C = 1 となります。実際に数を代入してみると、
291 + 921 = 1212 となり、確かに成り立っていることがわかります。
 基本的なルールとしては3つあります。
1. 各文字には0〜9までの1つの数字が入ります。
2. 同じ数字が別の文字にはいうことはありません。(つまり、A = 1, B = 1というのはダメ)
3. 各項の先頭にある文字に'0'が入ることはありません。(先程の例で言うと、A、B、Cはいずれも項の先頭にあるため'0'は入りません。)

作ろうと思ったきっかけ

 私は普段、競技プログラミングをしています。レートを上げるためにプログラミングの勉強をしているうちに、せっかくだから何か作ってみよう!と思ったことがきっかけです。
 何を作ろうかと考えたときに、そのとき読んでいたアルゴリズムの本に、虫食い算(覆面算のアルファベットが全て□に置き換わったもの)はDFSで作れると言うようなことが書いてありました。それを読んで、「虫食い算の作り方はわかった。じゃあ覆面算は作れるだろうか?」と思ったので作ってみました。

苦労した点

 苦労したところは2つあります。覆面算ソルバーのアルゴリズムとWebアプリにする際の答えの出力方法です。

 1. 覆面算ソルバーのアルゴリズム
 はじめに考えたのは、虫食い算と同様にDFSで解くことです。しかし、上手い実装方法が思い浮かばずにかなり悩みました。そこで、ルールを見返してみると、各文字には1つの数字しか入らず、違う文字に同じ数が入ることはないので、登場する文字の種類は高々10種類であることに気づきました。そしたら答えの候補は高々10!(= 3628800)個なので答えは数秒で求められることになります。実際に書いたコードがこれです。

def solver(s):
    s = s.replace(" ", "")
    left_terms = []
    now = 0
    alphabet = []
    equal = 0
    right_term = ""
    operation = ['+', '-', 'x', '/']
    ope_num = [0]
    ZeroNine = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']

    # 式に含まれる項と登場する文字をリスト化
    for i in range(len(s)):
        if s[i] not in alphabet and s[i] not in operation and s[i] != '=' and s[i] not in ZeroNine:
            alphabet.append(s[i])
        if s[i] in operation:
            tmp = s[now:i]
            left_terms.append(tmp)
            now = i + 1
            if s[i] == '+':
                ope_num.append(0)
            elif s[i] == '-':
                ope_num.append(1)
            elif s[i] == 'x':
                ope_num.append(2)
            else:
                ope_num.append(3)
        if s[i] == '=':
            equal = i
            left_terms.append(s[now:equal])
            right_term = s[equal+1:]

    # 登場した文字をソート
    alphabet = sorted(alphabet) 

    number = len(alphabet)
    # 0〜9の中から答えの候補を全探索
    for i in itertools.permutations(ZeroNine, number):
        cur = list(i)
        answer = {}

        # 各文字についての対応表を作る
        for j in range(number):
            answer[alphabet[j]] = cur[j]
        # 対応表に基づき、数式を作る
        ok = 1

        # 左辺の計算
        Leftside, Rightside = 0, 0
        for j in range(len(left_terms)):
            # 数式の復元
            tmp = ""
            for k in left_terms[j]:
                if k in ZeroNine:
                    tmp += k
                else:
                    tmp += answer[k]
            if tmp[0] == '0':
                ok = 0
            else:
                if ope_num[j] == 0:
                    Leftside += int(tmp)
                elif ope_num[j] == 1:
                    Leftside -= int(tmp)
                elif ope_num[j] == 2:
                    Leftside *= int(tmp)
                else:
                    Leftside /= int(tmp)

        # 右辺の計算
        tmp = ""
        for k in right_term:
            if k in ZeroNine:
                tmp += k
            else:
                tmp += answer[k]
        if tmp[0] == '0':
            ok = 0
        Rightside += int(tmp)

        # ok では、最高位が0出ないという条件を満たしているかを確認している
        if ok == 1:
            if Leftside == Rightside:
                res = ""
                for key in answer:
                    res += str(key)
                    res += ": "
                    res += str(answer[key])
                    res += " "
                return res
    return "答えが見つかりませんでした"

 2. Webアプリにしたときの答えの出力方法
 自分でWebアプリを作った経験がないため、どのようにアプリにするのかとても苦労しました。フレームワークにはFlaskを使いました。Pythonの有名なフレームワークとしてFlaskの他にDjangoがあり、どちらを使うか悩みましたが、小規模な開発にはFlaskを用いることが多いといったことや、今回の自分の開発に、フルスタックフレームワークは必要ないといった理由からFlaskを選びました。
 どう出力するかと言うのはググってなんとか解決しました。ググるの大事ですね〜(自力で解決しようとして1日溶かしました...)自分にはHTMLとCSSの知識が欠けているようです。Webアプリを作っていく上での今後の課題です。

最後に

 実際に動かしてみるとこのようになります。
 ここでは入力として、最初にあげた例を入れています。
覆面算.gif
 実はこの覆面算ソルバーには欠点があります。括弧がついた式はそのまま計算できないのです。括弧を外してやれば良いのだけなのですが入力する側からすると割と面倒かもしれません。
 今までWebアプリというと、本を写経したり、動画の真似をして作ったりといったことしかなく、自分で考えて作ったことはなかったので、非常に良い経験になりました。読んでいただきありがとうございました。

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

PyPIにパッケージをトークンを用いてアップロードする

はじめに

Pythonではよりお手軽にpipでインストールするためにPyPIに作成したライブラリをアップロードすることができます。

このとき、通常はユーザー名・パスワードをアップロード時に手動でコンソールに入力する必要がありますが、APIトークンを発行すればこれらユーザ名・パスワードを省略することができます。

PyPIへのアップロード (トークンを使用しない)

pipenv install --dev wheel twine

パッケージ化とアップロードのためにwheeltwineをdevで入れます。
wheelはパッケージの1形式であり、かつその形式を作成するためのライブラリです。 (gemみたい)
twineはPyPIにアップロードをするためのライブラリです。

アップロードするときは、以下のシェルスクリプトを自分は利用しています。

#!/usr/bin/env bash

rm -f -r {your-library-name}.egg-info/* dist/*
python setup.py bdist_wheel
twine upload dist/*

このとき、ユーザー名とパスワードをtwineを利用時に打つ必要があります。

APIトークン

Add API tokenから発行できます。
APIトークンを発行したら、以下の内容をホームディレクトリに~/.pypircとして置いてあげます。

.pypirc
[pypirc]
servers =
    pypi
[pypi]
username: __token__
password: your_token

__token__は、いかにも個々に別の名前のトークン入れる感じみたいな名前ですが、そのままプレーンテキストで__token__とそのまま打ってあげればいいです。
passwordの方に関しては設定が必要で、先程発行したAPIトークンを書く必要があります。

参考リンク

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

Djangoのテンプレートでパラメータの値を取得

やりたいこと

https://example.com/?year=2021&month=1

でアクセスしたときにパラメータのyearmonthの値をテンプレート側で取りたい。

解決方法

yearの値を取る場合は
{{ request.GET.year }}
で取得。

monthの値を取る場合は
{{ request.GET.month }}
で取得。

備考

{% for key, value in request.GET.items %}
    {{ key }} = {{ value }}
{% endfor %}

こんなふうに回しても取れる。

year = 2021 month = 1
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Pythonとは。何に使われているのか。

Pythonに関して話す機会ができたので、経験範囲(Pythonをゆるゆる2年)でのPython知識をまとめていこうと思います。
私の思うPythonな内容も多めです。
間違いなどがありましたらご指摘ください。

全3編を予定

  • Pythonとは。何に使われているのか。(←今ここ)
  • Pythonの環境構築
  • モジュールとパッケージ

基本情報

開発元・運営母体

Python とは言語仕様のこと

インタプリタ言語

Pythonはインタプリタ言語です。
Pythonが実行するまでに行われることは以下が参考になりました。
https://qiita.com/intermezzo-fr/items/3ae7645bd7d4414d9607

Python のバージョンに関して

  • 各種versionの開発ステータスとサポート期限などがここで見れます
  • 2021年現在Python2系のサポートは切れています

Python の使われ方に関して

Python は何に使われているのか (Python を使って仕事をする人々の種類)

Python=機械学習と言われがちなので、
いろいろな職種の人が色々なPython製のものを使っているよと言いたいのが趣旨です。

  • データサイエンティスト
  • MLエンジニア
    • テーブルデータ・自然言語・メディア(画像・音声・動画)
  • データエンジニア
  • アプリケーションエンジニア
  • インフラ・システム管理者
    • Ansible
    • Airflow
    • Pyhonスクリプトをグルーに使っている例も結構見る
  • 組み込み・ロボット
    • Raspberry Pi
    • ROS
  • 各種研究者
  • スクレイピングしている人達
    • Selenium, Beautiful Soup, Scrapy
    • 私がPython使い始めたのはスクレイピングするためです

PyconJp 2019のアンケートだと機械学習やwebアプリケーション開発が多いですね


参考: 引用 (https://pyconjp.blogspot.com/2020/03/questionnaire-2019-result.html)

どんなライブラリがあるかを見て何に使われているのかを推測するのも面白いです。
-> Pythonの人気ライブラリやアプリの紹介

Pythonと言えば..な海外企業

個人的に調べものをしていて度々お世話になる企業

個人的に調べものをしていてテックブログやQiitaや社員さんの個人ブログでお世話になっている企業。

  • ZOZOテクノロジーズ
  • リクルート 系列
  • JX通信社
  • エムスリー
  • ISID
  • ブレインパッド

Pythonとは。何に使われているのか。編 は以上です。
間違いなどがありましたらご指摘ください。

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

numpy : 単一型の ndarray を structured array に変換したい

これは何か

単一の型で定義された通常の ndarray を structured array に変換する方法です。

動機

方法 1 : タプルのリストを経由させる

コード例

  • 例えば以下のようなコードになります
import numpy

# d1, d2, d3 の 3 つのデータがあるとします
d1 = numpy.arange(0, 1000, dtype='int32')
d2 = numpy.arange(1000, 2000, dtype='int32')
d3 = numpy.arange(2000, 3000, dtype='int32')

# くっつけます
d = numpy.array([d1, d2, d3]).T

# d はこんな感じです
# array([[   0, 1000, 2000],
#        [   1, 1001, 2001],
#        [   2, 1002, 2002],
#        ...,
#        [ 997, 1997, 2997],
#        [ 998, 1998, 2998],
#        [ 999, 1999, 2999]], dtype=int32)

# dtype を定義しときます
dtype1 = [
    ('d1', 'int32'),
    ('d2', 'int32'),
    ('d3', 'int32'),
]

# structured array に変換します
sa1 = numpy.array(list(map(tuple, d)), dtype=dtype1)

# sa1 はこんな感じ
# array([(  0, 1000, 2000), (  1, 1001, 2001), (  2, 1002, 2002),
#        (  3, 1003, 2003), (  4, 1004, 2004), (  5, 1005, 2005),
#        (  6, 1006, 2006), (  7, 1007, 2007), (  8, 1008, 2008),
#        ...
#        (993, 1993, 2993), (994, 1994, 2994), (995, 1995, 2995),
#        (996, 1996, 2996), (997, 1997, 2997), (998, 1998, 2998),
#        (999, 1999, 2999)],
#        dtype=[('d1', '<i4'), ('d2', '<i4'), ('d3', '<i4')])

# 個別のデータに key でアクセスできるようになりました
sa1['d1']
# array([  0,   1,   2,   3,   4,   5,   6,   7,   8,   9,  10,  11,  12,
#         13,  14,  15,  16,  17,  18,  19,  20,  21,  22,  23,  24,  25,
#         ...
#        975, 976, 977, 978, 979, 980, 981, 982, 983, 984, 985, 986, 987,
#        988, 989, 990, 991, 992, 993, 994, 995, 996, 997, 998, 999],
#       dtype=int32)

パフォーマンス

  • numpy.ndarray から tuple への変換をしており、やや遅いです
  • 手元の環境では 3 ms ほどかかっていました
%time numpy.array(list(map(tuple, d)), dtype=dtype1)
# CPU times: user 2.63 ms, sys: 0 ns, total: 2.63 ms
# Wall time: 2.64 ms

方法 2 : バッファを経由させる

コード例

  • 例えば以下のようなコードになります
import numpy

# data を作ります
d1 = numpy.arange(0, 1000, dtype='int32')
d2 = numpy.arange(1000, 2000, dtype='int32')
d3 = numpy.arange(2000, 3000, dtype='int32')

d = numpy.array([d1, d2, d3]).T

# dtype を定義します
dtype1 = [
    ('d1', 'int32'),
    ('d2', 'int32'),
    ('d3', 'int32'),
]

### ここまでは、方法 1 と同じです ###

# structured array に変換します
sa2 = numpy.frombuffer(d.tobytes(), dtype=dtype1)

# sa1 と sa2 の値は全く同じです
all(sa2 == sa1)
# >> True

パフォーマンス

  • 手元の環境では 80 us になりました
  • タプル経由に比べて、30 倍ほど速いです
  • 大満足です
%time numpy.frombuffer(d.tobytes(), dtype=dtype1)
# CPU times: user 75 µs, sys: 0 ns, total: 75 µs
# Wall time: 83.9 µs

もう少しまじめに比較

  • 方法 1 と、方法 2 の計算時間を計測してみました
  • データ点数 n を変化させます
  • 各点数に対して、100 回計算させて所要時間を平均しました

results.png

コード

import numpy
import time

dtype1 = [
    ('d1', 'int32'),
    ('d2', 'int32'),
    ('d3', 'int32'),
]

def run(num, func):
    d = numpy.arange(num*3, dtype='int32').reshape((3, num)).T
    t0 = time.time()
    [func(d) for i in range(100)]
    t1 = time.time()
    return (t1 - t0) / 100

func1 = lambda x: numpy.array(list(map(tuple, x)), dtype=dtype1)
func2 = lambda x: numpy.frombuffer(x.tobytes(), dtype=dtype1)

# 計測します
nums = numpy.logspace(2, 5, 10, dtype=int)
t1 = [run(i, func1) for i in nums]
t2 = [run(i, func2) for i in nums]

# プロットします
import matplotlib.pyplot

fig = matplotlib.pyplot.figure()
ax = fig.add_subplot(111, aspect=1)
ax.plot(nums, t1, 'o-', label='tuple')
ax.plot(nums, t2, 'o-', label='bytes')
ax.set_xscale('log')
ax.set_yscale('log')
ax.set_xlabel('# of data points')
ax.set_ylabel('Calculation time [s]')
ax.grid(True, color='#555555')
ax.grid(True, which='minor', linestyle=':', color='#aaaaaa')
ax.legend()
fig.savefig('results.png', dpi=200)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Google App EngineにPythonアプリをデプロイしGitHubと連携させる

動機

Heroku、AWS Beanstalk、Azureに同じPythonのアプリをデプロイしてみて、ここまできたらGoogle Cloud Platformでも同じことをやってみるかと思いやってみた。が、他のサービスに比べて圧倒的にややこしかった。
GitHubと連携させるところで随分悩んだが、最終的にとても簡単にできることがわかったので、その顛末をここに記す。

前提

  • ローカル、あるいは他のPaaS, IaaSなどで動くことを確認済みのPythonアプリケーションがあること
  • 上記アプリがGitHubのリポジトリに上がっていること
  • Google Cloud Platformのアカウントがあること

手動でデプロイ

最初はGitHubとの連携はさせず、ローカルから手動でデプロイしてみようと思った。公式のチュートリアル通り、Google Cloud SDKをインストールし、コマンドラインからgcloud app deployでデプロイ。初めはapp.yamlがなくて動かなかったり、app.yamlを追加した後もentrypointのアプリケーション名がappじゃなくてmainになっていてエラーになったりしたが、そのあたりはわりとすぐに解決できた。(エラーログを見つけるのに少々時間がかかったが。)

GitHub連携〜ハマる

じゃあ次はGitHubと連携させてみるかとやってみるが、まずネットでググるとGoogle Cloud RunとGoogle Cloud Buildを使ってGitHubと連携させる方法が出てくる。これがいけなかった。
とりあえず、Cloud Runのサービス作ってGitHubと連携するCloud Buildのトリガーを作ったら、pushのたびにちゃんとトリガーが動いて、正常にデプロイされるんだけど、App Engineの方でWebページを開いてみても一向に反映されず、???となる。Cloud Runが何なのかよくわかっていなかったので、App Engineとは別個のものとして動いていることに気づいていなかったのだ。
Cloud Runのサービス詳細ページを見ていて、URLというリンクがあることに気づき、クリックしたら最新のGitHubリポジトリの内容が反映されたページが表示された。ああ、そういうことかとようやく理解する。

公式ドキュメントを見よ

結論としては公式ドキュメントに全て書いてあった。
Cloud Runなんて最初からいらなかったんだ。
https://cloud.google.com/source-repositories/docs/quickstart-triggering-builds-with-source-repositories?hl=ja

必要なのは

  1. Cloud Buildの設定でGAEとの連携を許可、

  2. Cloud Buildでデプロイするための構成情報を記述したcloudbuild.yamlをプロジェクトに追加、

  3. ビルドトリガーを作成し、[ビルド構成] - [Cloud Build 構成ファイル] に上記で作成したcloudbuid.yamlを指定する。

これであとはGitHubのリポジトリにpushするたびに自動的にApp Engineの方にデプロイされる。
なぜ初めからこのページにたどり着けなかったのか・・・。

残る疑問

GitHubにあるソースを動かすんならCloud RunとCloud Buildだけでよくね?という疑問が湧く。
App Engineは何のためにあるんや?他のPaasと同じイメージで使うんならApp EngineとCloud Buildなのかな?
使い分けがわからんです。

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