diadia

興味があることをやってみる。自分のメモを残しておきます。

DB設計について

テーブルの設計について学習している。

メモを残す。

 

テーブル設計には正規化という概念が重要らしい。正規化には5つの種類がある。通常は3つまでチェックして修正すれば良いらしい。

第1正規形の定義:1つのセルの中には1つの値しか含まないこと

第2正規形の定義:テーブル内で部分関数従属を解消し、完全関数従属のみ

テーブルを作ること

 

ある座標点からxkm以内に存在するデータオブジェクトをmapに表示したい

PostGISを使う場合はST_DWithin()関数を使えばよいだろう。
ではgeodjangoではどうやって特定の範囲内のデータオブジェクトを取得するか?
psycopg2を使ってデータオブジェクトのみ取得することももやり方としてはできるだろう。
しかしここではgeodjangoの備え付けの機能を使うとしたらどのように書くかを記しておく。

参考:https://docs.djangoproject.com/en/2.2/ref/contrib/gis/tutorial/#automatic-spatial-transformations
https://docs.djangoproject.com/en/2.2/ref/contrib/gis/db-api/#distance-lookups

特定の範囲内に存在するデータオブジェクトはdistance_ltを使う

djangoではデータオブジェクトはfilterを使って取得してきた。icontainsとか使うやり方でdistance_ltやdistance_lteを使えば特定の範囲内のデータオブジェクトは取得できる。
問題はどのように使うかだ。ここで引っかかってしまったのできちんと記録しておく。

ドキュメントの例文では以下のように書いてある。

qs = SouthTexasCity.objects.filter(point__distance_lte=(pnt, D(km=7)))

はじめにこの意味を確認するとpntから7km以内のSouthTexasCityのクエリ結果を示している。Dはfrom django.contrib.gis.measure import Dで別途インポートする必要がある。そして当初pntはデータオブジェクトを用いるのだろうと思っていた。しかしながら使うのはデータオブジェクトではなくgeometryのオブジェクトで、GEOSGeometryやPointクラスのインスタンスでなければならないことが分かった。GEOSGeometryやPointについては現状違いがわからない。少なくともpointのデータを作ることに関しては両者どちらでも作成できると思われる。

GEOSGeometryの使い方

from django.contrib.gis.geos import GEOSGeometry, Point
my_place    = UserPointModel.objects.get(id=1)
point = 'POINT({lon} {lat})'.format(lon=my_place.point.x,lat=my_place.point.y)
pnt = GEOSGeometry(point, srid=4326)

Pointの使い方

from django.contrib.gis.geos import GEOSGeometry, Point
my_place    = UserPointModel.objects.get(id=1)
pnt = Point(my_place.point.x, my_place.point.y, srid=4326)

本題のdistance_lteの使い方

サンプル
class MapsView(View):
    def get(self, request):
        my_place    = UserPointModel.objects.get(id=1)
        pnt = Point(my_place.point.x, my_place.point.y, srid=4326)

        all_objects = UserPointModel.objects.filter(point__distance_lte=(pnt, D(km=2)))
        context = {"all_objects":all_objects}
        return render(request, "maps/list.html",context)    

geodjangoのメモ

 

models.pyでpointを使う場合はmodels.PointFieldを使う。しかしデフォルトの from django.db import models にはPointFieldがない。

from django.contrib.gis.db import models

を使うこと。

https://docs.djangoproject.com/en/2.2/ref/contrib/gis/model-api/#module-django.contrib.gis.db.models

同様にadmin.pyでもデフォルトのfrom django.contrib import adminを使わずに

from django.contrib.gis import admin

を使うことでadmin画面で空間データの入力が可能になる。

テンプレートについて

djangoのいつも通りにtemplate書いてview書いてもgeometry型のデータは文字でしか表示されない。つまりテンプレートに表示させるには何らかの処理が必要になる。template系についての情報を見つけた。もしかしてこれを使えばよいのかも!

https://medium.com/@h4k1m0u/displaying-a-map-in-a-django-webapp-2-3-develop-a-gis-webapp-with-geodjango-c831522ccf79

この記事によるとブラウザでデータをマップにプロットする場合にはdjango leafletを使う、と書いてある。leafletは1javascriptのライブラリのようでそのdjnagoバージョンらしい。
djnago leafletのドキュメント:
https://django-leaflet.readthedocs.io/en/latest/

インストール

pip install django-leaflet

settings.pyの編集

INSTALLED_APPS = [
    ...
    'leaflet',
    ]

テンプレート上での使い方

sample_template
{% load leaflet_tags %}
{% leaflet_css %}
{% leaflet_js %}


<h1>list</h1>
{{ object.point.x }}<br />
{{ object.point.y }}<br />
{% leaflet_map "main" callback="map_init_basic" %}


<script type="text/javascript">
    function map_init_basic(map, options) {
        var lon = "{{ object.point.x }}";
        var lat = "{{ object.point.y }}";
        #マップの拡大率を調整
        map.setView([lat, lon], 18);
        #地図に座標点をプロットする lon,latではなくlat,lonの順
        L.marker([lat, lon]).addTo(map);
        
    }	
</script>    

pointを複数表示させる方法(ListView)

views.pyはいつもどおりデータオブジェクトをリスト表示させるロジックを書けば良い。templateをgeodjango仕様にすればpointを複数リスト表示することができる。

sample_template
{% load leaflet_tags %}
{% leaflet_css %}
{% leaflet_js %}


<h1>list</h1>

{% leaflet_map "main" callback="map_init_basic" %}


<script type="text/javascript">
    function map_init_basic(map, options) {
        {% for object in objects %}
        var lon = "{{ object.point.x }}";
        var lat = "{{ object.point.y }}";
        #マップの拡大率を調整
        map.setView([lat, lon], 10);
        #地図に座標点をプロットする lon,latではなくlat,lonの順
        L.marker([lat, lon]).addTo(map);
        {% endfor %}
        
    }	
</script>    

javascriptのscriptタグ内でもdjangoのテンプレート制御構文を使うことができるようだ。これを使えばデータオブジェクトを全て地図上に表示することができる。

次にpointをクリックしたらデータ詳細が表示されるようにするには

地図上に位置情報をプロットするだけではなくて、マーカーをクリックしたらその他のプロパティを表示したい。これには.bindPopup()メソッドを用いれば良いようだ。
参考:https://qiita.com/amagasu1234/items/baf15da6655b74b4723a#%E3%81%BE%E3%81%9A%E3%81%AF%E5%8D%98%E7%B4%94%E3%83%9E%E3%83%BC%E3%82%AB%E3%83%BC%E3%82%92%E8%BF%BD%E5%8A%A0%E3%81%97%E3%81%A6%E3%81%BF%E3%81%BE%E3%81%99

  sample_template
{% load leaflet_tags %}
{% leaflet_css %}
{% leaflet_js %}


<h1>list</h1>

{% leaflet_map "main" callback="map_init_basic" %}


<script type="text/javascript">
    function map_init_basic(map, options) {
        {% for object in objects %}
        var lon = "{{ object.point.x }}";
        var lat = "{{ object.point.y }}";
        #マップの拡大率を調整
        map.setView([lat, lon], 10);
        #地図に座標点をプロットする lon,latではなくlat,lonの順
        L.marker([lat, lon]).bindPopup("{{ object.user_name }}").addTo(map);
        {% endfor %}
        
    }	
</script>   

f:id:torajirousan:20190915131850p:plain

user_nameの表示ができた

 

半径10km以内のpointのみを表示するにはどうすれば実現できるか

Distance Queriesというものがあるようだ。これでできるか?
https://docs.djangoproject.com/en/2.2/ref/contrib/gis/db-api/#distance-queries

  • distance_lt
  • distance_lte
  • distance_gt
  • distance_gte
  • dwithin

distance_lt:ルックアップジオメトリからジオメトリフィールドまでの距離が指定された距離値よりも小さいモデルを返します。
https://docs.djangoproject.com/en/2.2/ref/contrib/gis/geoquerysets/#distance-lt

distance_lte:ルックアップジオメトリからジオメトリフィールドまでの距離が指定された距離値以下であるモデルを返します。
https://docs.djangoproject.com/en/2.2/ref/contrib/gis/geoquerysets/#distance-lte

distance_gt:ルックアップジオメトリからジオメトリフィールドまでの距離が、指定された距離値よりも大きいモデルを返します。
https://docs.djangoproject.com/en/2.2/ref/contrib/gis/geoquerysets/#distance-gt

distance_gte:ルックアップジオメトリからジオメトリフィールドまでの距離が、指定された距離値以上であるモデルを返します。
https://docs.djangoproject.com/en/2.2/ref/contrib/gis/geoquerysets/#distance-gte

dwithin:ルックアップジオメトリからジオメトリフィールドまでの距離が互いに指定された距離内にあるモデルを返します。 ターゲットジオメトリが投影システムにある場合にのみ、距離オブジェクトを提供できることに注意してください。 地理的なジオメトリの場合、ジオメトリフィールドの単位(WGS84の度数など)を使用する必要があります。
https://docs.djangoproject.com/en/2.2/ref/contrib/gis/geoquerysets/#dwithin

メモ:https://codeday.me/jp/qa/20190526/901886.html

https://docs.djangoproject.com/en/2.2/ref/contrib/gis/db-api/#distance-lookups

models.GeoManager()について

https://stackoverflow.com/questions/47690134/django-has-no-attribute-geomanager-issue
https://docs.djangoproject.com/en/2.0/releases/2.0/#features-removed-in-2-0

どうやらGeoManager()はdjango2.0以降では使われていないようだ。

https://docs.djangoproject.com/en/2.2/ref/contrib/gis/functions/

unittestのsys.exit()の抜け方

def tashizan(a,b):
    try:
        return int(a)+int(b)
    except ValueError:
        print("a,bは整数ではなく文字列の可能性があります")
        sys.exit(1)
import unittest , tashizan

class TestTashizan(unittest.TestCase):

    def test_tashizan_1(self):
        expected = 8
        actual = tashizan(2, 6)
        self.assertEqual(expected, actual)

    def test_tashizan_2(self):
        expected = 8
        actual = tashizan(6, "2")
        self.assertEqual(expected, actual)


    def test_tashizan_3(self):
        with self.assertRaises(SystemExit):
            tashizan("a", "6")
    


if __name__ == "__main__":
    unittest.main(exit=False)

どうやらエラーのときはwith を使ってassertRaises()を使い、エラーの出る使い方をくるむようだ。

デバッグについて

デバッグの方法論

今までプログラムを走らせてエラーが出たところでデバッグを行っていた。これではあまり具合が良くないと分かった。 それは設計(ロジック)が正しくて、コードが正しくて、でもインプットデータが問題であるかもしれない。インプットデータが正しくてもインプットデータを一種類しか使ってないと、エラー原因を正確に確定することができない。したがってエラーが出たならまずはインプットデータを複数使って正確なエラーの特定をすることが大事。

python csvファイルを結合

csvを結合する方法

  • ライブラリpandasを使う方法
  • open関数を使う方法

ライブラリpandasを使う方法

import pandas as pd

csv_list = ['csv1_path', 'csv2_path', 'csv3_path', 'csv4_path']
pd_list  = [pd.read_csv(file) for file in csv_list]
df       = pd.concat(pd_list)
df.to_csv("hoge.csv", index=False, encoding="utf-8")

この方法は時間がかかる恐れがある。以下の方法もあるので試してみる。どちらが速いか分かったら追記しようと思う。

open関数を使う方法

下記の方法はpandasを使った場合と同様、一番目のファイルのヘッダーは残し、追加するファイルのヘッダーは削除して追加するやり方になる。ヘッダーを排除するためにreadlines()で各行をリスト化し、ヘッダーを削除したリストとして[1:-1]でスライスしている。この方法を使うとpandasより速くできるか検証が必要である。

merge_csv_list = ['csv1_path', 'csv2_path', 'csv3_path', 'csv4_path']

for n in range(0,len(merge_csv_list)):
    if n == 0:  
        base_file = open(merge_csv_list[0], "a", encoding="utf-8")
        continue
    else:
        add_file = open(merge_csv_list[n], "r", encoding="utf-8")
        base_file.write(",".join(add_file.readlines()[1:-1]))
        add_file.close()
        continue
base_file.close()
    

geodjangoの環境構築

https://docs.djangoproject.com/ja/2.2/ref/contrib/gis/install/ geodjangoを使うには、python,django,空間データベース、地理空間ライブラリが必要。 PostgreSQLで空間データベースを使うには、ライブラリであるGEOS,GDAL,PROJ.4,PostGISが必要である。

windows postgresqlのデータのディレクトリを変更したい

postgresqlwindowsにインストールする場合

windowsの場合はインストーラーを起動するとdataの置き場所をウィザードに従って自分で指定することができた。 しかしながらインストール後にデータディレクトリを変更(新たにデータクラスタも作成)する場合にはどのように実行するのかわからない。どうすればよいのか

memoを残しておく

まずservices smcでpostgresqlを見ると、実行ファイルパスは以下のようになっていた。

"C:\Program Files\PostgreSQL\11\bin\pg_ctl.exe" runservice -N "postgresql-x64-11" -D "C:\pgdata\11" -w

で、 -Dはデータの読込み先を表す。またpg_ctl.exeはpostgresqlのサービスの起動、停止、再起動を実行するためのコマンドである。だからdataの読込み先を新しく作ったdataの場所に変更してpostgresqlを起動しなおせば、新しいデータクラスタでdataを書くことができると思っていた。だが今のところできない・

成功した方法

 


現在稼働しているpostgresqlのサービスを停止する。これはservices mscを使ってpostgresqlの停止を行った。その他の方法としてGUIではなくCUI

pg_ctl stop -U postgres

でやることができると思う。

ドロップボックス上にpostgres_dataディレクトリを作成した。
ここにテーブルやテーブルデータを入れることになる。 ディレクトリを作成しただけなので、このディレクトリをデータベースクラスタ化する。これはinitdbコマンドを使う。

initdb --encoding=UTF8 --no-locale --username=postgres -W -D "C:\Users\USER\Dropbox\postgres_data"
#コマンドの結果
データベースシステム内のファイルの所有者は"USER"となります。
このユーザがサーバプロセスも所有する必要があります。

データベースクラスタロケール"C"で初期化されます。
デフォルトのテキスト検索設定はenglishに設定されました。

データベージのチェックサムは無効です。

新しいスーパユーザのパスワードを入力してください:
再入力してください:

既存のディレクトリC:/Users/USER/Dropbox/postgrestestの権限を修正します ... 完了
サブディレクトリを作成します ... 完了
max_connectionsのデフォルト値を選択します ... 100
shared_buffersのデフォルト値を選択します ... 128MB
selecting default timezone ... Asia/Tokyo
動的共有メモリの実装を選択します ... windows
設定ファイルを作成します ... 完了
ブートストラップスクリプトを実行します ... 完了
ブートストラップ後の初期化を行っています ... 完了
データをディスクに同期します...完了

警告: ローカル接続で"trust"認証を有効にします。
この設定はpg_hba.confを編集するか、次回のinitdbの実行の際であれば-Aオプ
ション、または、--auth-localおよび--auth-hostを使用することで変更するこ
とができます。

成功しました。以下のようにしてデータベースサーバを起動できます。

    pg_ctl -D ^"C^:^\Users^\USER^\Dropbox^\postgrestest^" -l <ログファイル> start

次にpostgresqlを稼働させる。これはpg_ctl start コマンドで動かす事ができる。

pg_ctl start -U postgres -D "C:\Users\USER\Dropbox\postgres_data" -w

これで動かすことができた。 参考http://koshian2.hatenablog.jp/entry/2018/02/17/213446

https://kenpg.bitbucket.io/blog/201506/04.html

人にコードを見せるときには

まずコードだけ見せない。設計書、コード、テストをセットにする。

どの順番にモジュールを使うのかを図とか絵で説明するのは設計書のレベル。

設計書でもっと詳しく確認したいときにコードを確認する流れとなる。

感覚としてはコード無しで構造が分かるものを準備すること。

またコードに関しては、パラメータやDB情報などを記載した設定ファイルを準備すること、そしてコード全体としてメンテナンス性に優れた構造にする。

また設計書、コードの信頼性を高めるテストを添付する。

 

まずフローチャートを書く。これは人にどのような論理でコードを書いているか説明できるとともに、自分が書こうとするコードの論理構造を明確に理解できる。フローチャートを用いると、同じフローを視覚を通して発見できるので関数化しやすくなる。また関数化しなくとも冗長な部分を視覚的に発見するメリットもある。

納品物のドキュメントはエクセルなどのファイルにしておく。

納品物の構成もPROGRAM、設計書、INPUT_DIRにしておくと良い。 

postgres create

    
import pandas as pd
import zipfile,os,time

ZIP_DIR     = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + "\\ZIP_DIR\\"
UNZIP_DIR   = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + "\\UNZIP_DIR\\"
MERGED_CSVS = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + "\\DIR_DIR\\MERGED_CSVS\\"
zip_files = os.listdir(ZIP_DIR)


#zipファイルを解凍してrenameを実行。すべて解凍する。
userid_list = []
for zf in zipfiles:
	with zipfile.ZipFile(ZIP_DIR+zf) as zip:
		time.sleep(0.0000001)
		timestamp = str(time.time()).replace(".", "_")
		ziplst = zip.namelist()
		for f in ziplst:
			#チェック
			print(f)
			sys.exit()
			userid = f.split("_","_")[X]
			zip.extract(f, UNZIP_DIR)
			os.rename("".join([UNZIP_DIR, f]), "".join([UNZIP_DIR, userid, span, timestamp, ".csv"]))
			userid_list.append(userid)



#同一useridのcsvファイルをUNZIPディレクトリから取得
unzip_list = os.listdir(UNZIP_DIR)

for userid in set(userid_list):
	
	csv_list = [ file for file in unzip_list if userid in file ]
	csv_list.sort()
	print(csv_list)
	for f in csv_list:
		unzip_list.pop(f)

	sys.exit()

	#マージファイルを作成
	pd_list = [ pd.read_csv(csv_file) for csv_file in csv_list ]
	df = pd.concat(pd_list)
	#マージファイルタイトルの作成
	merge_file = "".join([userid, span, ".csv" ]) 
	df.to_csv("".join([MERGED_CSVS, merge_file]), index=False, encoding="utf-8")