Mar 29, 2020

(Emacs) python-shell-send-bufferではまった

筆者はPythonの短いプログラムを書く時によくEmacsのPython.elをよく使う。主な理由は長年使っていて手に馴染んでいるからであるが、何よりもプログラムの編集中にカーソルを移動させることなく都度C-c C-cで実行結果を別バッファで見れるのが便利であり、筆者には手放せない。UNIXのシェルからpythonと打って起動する対話モードは、計算機代わりに1行書くのにはよく使うが、2〜3行程度であっても複数行のブロックを書く気になれないし、Jupyter Notebookは起動に時間がかかるし、ブラウザの操作が面倒だし、重い。Emacsはすぐに開くし、Python.elのお陰で、ファイルを開いてコードを書いて、初回はC-c C-p, C-c C-c, C-c C-zで、2回目以降はC-c C-cですぐに実行結果が見れる。

さて、Emacsのpython.elのPython modeでC-c C-cすると if __name__ == '__main__': のブロックが実行されないが、筆者はそれが __name__ == '__main__' じゃないからだと思っていた。
ある日、何か延々と試行錯誤しながらプログラムを作っている途中で、テスト用にC-c C-cで実行する時とシェルから実行する時とで動作を変えたくなって、ふと if __name__ == '__main__':else: を追加すれば実現できるのではないかと思って、やってみたら成功した。その時、たまたま if False: のブロックが if __name__ == '__main__': の直前にあり、次のような形をしていた。

#!/usr/bin/python

def foo(a, b):
    return a + b

if False:
    def foo(a, b):
        return a - b

if __name__ == '__main__':
    print(foo(1, 2))
else:
    print(foo(3, 4))

これをC-c C-cで実行すると else: 側が実行されて 7 が出力され、シェルから実行すると if __name__ == '__main__': 側が実行されて 3 が出力される。

グッドアイデアと満足してしばらくそのようにしていた後、 if False:if True: に変えると、C-c C-cした時に if __name__ == '__main__': 側のコードも else: 側のコードも実行されなくなって、頭の中が???となった。
何が悪いのだろうと思って調べ始め、 if False: に戻して else: 側に入る時に print(__name__) としてみると予想に反して __main__ と表示されるし、 if False のブロックをコメントアウトしてC-c C-cすると

    else:
       ^
SyntaxError: invalid syntax
というエラーが出て、混乱した。

if True: にした時に末尾に if __name__ == '__main__': のブロックをもう1つ足すとそちらは実行されたので、それでようやく、C-c C-cでは if __name__ == '__main__': のブロックが消されるのだと気付いた。

C-c C-cで起動されるpython-shell-send-bufferのドキュメントをM-x describe-functionで見ると、

When optional argument SEND-MAIN is non-nil, allow execution of
code inside blocks delimited by "if __name__== '__main__':".
と書いてあるだけで、どういう仕組みで"if __name__== '__main__':"のブロックを実行しないようにしているのかが書いていないが、Python.elの中身を追っていくと、途中で実行されるpython-shell-buffer-substringのドキュメントに
  1. When optional argument NOMAIN is non-nil everything under an
     "if __name__ == '__main__'" block will be removed.
と書いてあった。"if __name__== '__main__':"のブロックを消す具体的な処理を見ると、確かにpython-nav-if-name-mainが if __name__ == (['"])__main__\1: を検索し、python-nav-forward-sexpがブロックの終端まで移動しており、"if __name__== '__main__':"のelse:ブロックは消えないようになっていた。

つまり、直前に if False: ブロックがある時に if __name__ == '__main__':else: 側のコードが実行されるのは、"if __name__== '__main__':"のブロックが消されて

if False:
    def foo(a, b):
        return a - b

else:
    print(foo(3, 4))
になるからであり、 if True: に変えると実行されなくなるのも、 if False: のブロックをコメントアウトすると else:SyntaxErrorになるのも当然であった。

残念...

See more ...

Posted at 12:00 in PC一般 | WriteBacks (0)
WriteBacks

Mar 20, 2020

(Pandas)pivot_tableでカテゴリ変数が欠損する対策を探る

area age positive
0 京都 17 False
1 大阪 15 True
2 京都 4 False
3 大阪 9 True
4 大阪 10 True
... ... ... ...
45 京都 16 True
46 大阪 3 False
47 大阪 2 False
48 奈良 10 True
49 京都 13 False

これは下記のプログラムの中で生成される架空のデータだが、このようなデータの地域別、年齢層(5歳未満、5歳以上、10歳以上、15歳以上)毎の人数の分布を、陽性と陰性とに分けて比較できるように、ヒートマップを並べようとして、次のようなプログラムを書いた。

In [1]:
import numpy as np
import pandas as pd

# サンプルデータ作成
np.random.seed(0)
df = pd.DataFrame({'area': [['京都', '大阪', '奈良'][i] for i in np.random.randint(3, size=50)],
                   'age': np.random.randint(20, size=50),
                   'positive': np.random.randint(2, size=50).astype(bool)})
df.loc[df['age'] < 5, 'positive'] = False  # 5歳未満の陽性は無しとする

# 年齢のビニング(区分け)
df['range'] = pd.cut(df['age'], bins=[0, 5, 10, 15, 20], labels=['5歳未満', '5歳以上', '10歳以上', '15歳以上'], right=False)

# 陽性、陰性の地域-年齢区分の分布を比較
df_positive = df[df.positive == True].pivot_table(index='range', columns='area', aggfunc='size', fill_value=0)
df_negative = df[df.positive == False].pivot_table(index='range', columns='area', aggfunc='size', fill_value=0)

# ヒートマップを並べて描画
import matplotlib.pyplot as plt
import seaborn as sns
plt.figure(figsize=(8, 2))
plt.subplot(121)
sns.heatmap(df_positive, cmap='Reds', cbar=False, annot=True)
plt.title("陽性")
plt.xlabel("")
plt.ylabel("")
plt.subplot(122)
sns.heatmap(df_negative, cmap='Blues', cbar=False, annot=True)
plt.title("陰性")
plt.xlabel("")
plt.ylabel("")
plt.subplots_adjust(wspace=0.4)
plt.show()

コード内のコメントに書いているように、5歳未満で陽性の人がいないものとする。
すると、次のように、陽性の5歳未満の行が抜けて、行の数が合わなくなってしまった。

Out [1]:

求める結果は、次のものである。

これが出るようにコードを修正しようとしたが、スマートな方法がわからなかった。

陽性のヒートマップの元になっているDataFrameを見ると、5歳未満の行が無い。

In [2]:
df_positive
Out [2]:
area 京都 大阪 奈良
range
5歳以上 2 2 2
10歳以上 0 4 3
15歳以上 2 2 1

しかし、このDataFrameのインデックスを見ると、データ型がカテゴリ変数で、そのカテゴリーには'5歳未満'が存在する。

In [3]:
df_positive.index
Out [3]:
CategoricalIndex(['5歳以上', '10歳以上', '15歳以上'], categories=['5歳未満', '5歳以上', '10歳以上', '15歳以上'], ordered=True, name='range', dtype='category')

であればこのインデックスを期待するものに書き換えれば良いのではないかと思って、次のようにインデックスのデータをカテゴリ変数のカテゴリーと同じになるように reindexしてみると、確かに望み通りの結果になった。

In [4]:
df_positive = df[df.positive == True].pivot_table(index='range', columns='area', aggfunc='size', fill_value=0)
df_positive = df_positive.reindex(index=pd.CategoricalIndex(data=df_positive.index.categories, categories=df_positive.index.categories, ordered=True), fill_value=0)
df_positive
Out [4]:
area 京都 大阪 奈良
range
5歳未満 0 0 0
5歳以上 2 2 2
10歳以上 0 4 3
15歳以上 2 2 1

しかし、何かやりたいことに対してコードが冗長だし、ordered=Trueまで面倒を見ないといけないなど、手間がかかり過ぎている感じがする。
次のようにインデックスに欠損が無いDataFrameを別に作ってインデックスをコピーする方が、コードがシンプルだし、考慮すべきことが少なくて楽だが、これだとプログラムの動作が冗長である。

In [5]:
df_all = df.pivot_table(index='range', columns='area', aggfunc='size', fill_value=0)
df_positive = df[df.positive == True].pivot_table(index='range', columns='area', aggfunc='size', fill_value=0)
df_positive = df_positive.reindex(index=df_all.index, fill_value=0)
df_positive
Out [5]:
area 京都 大阪 奈良
range
5歳未満 0 0 0
5歳以上 2 2 2
10歳以上 0 4 3
15歳以上 2 2 1

pivot_tableしてからreindexするのではなく、pivot_tableでインデックスに欠損を生じないようにできないかと思って調べると、dropnaという引数があり、次のように aggfunc='count', dropna=False とすればできることがわかった。

In [6]:
df_positive = df[df.positive == True].pivot_table(index='range', columns='area', values='positive', aggfunc='count', fill_value=0, dropna=False)
df_positive
Out [6]:
area 京都 大阪 奈良
range
5歳未満 0 0 0
5歳以上 2 2 2
10歳以上 0 4 3
15歳以上 2 2 1

See more ...

Posted at 22:50 in PC一般 | WriteBacks (0)
WriteBacks

Mar 03, 2020

IP_Set_ie.plの"IP Read ERROR"を修正した

このWebサーバーはieServer.NetのDDNSサービスを利用しており、このDDNSのIPアドレスの自動更新には、ai-lineが提供するIP_Set_ie.plというスクリプトを使わせてもらっている。

これまで13年半もの間、このスクリプトでたまにai-line.comに繋がらない以外のトラブルは無かったのだが、一昨日の3月1日から突然、何度やっても"IP Read ERROR!!"というエラーを出すようになった。
少し調べた所、このスクリプトがai-line.comのCGIにアクセスすると"400 Bad Request"が返されていた。ブラウザで同じCGIアクセスすると正常に応答が返って来るし、同じスクリプトで自前のCGIにアクセスすると"400 Bad Request"になったので、サーバー側でなくこのスクリプトの問題のようである。

LWPかwgetでCGIにアクセスするように書き換えようかと思ったが、試しにスクリプト中の

	print SOCKET "GET $ip_view_file HTTP/1.0\n";
	print SOCKET "HOST: $ip_view_host:$port\n";
	print SOCKET "\n";
の部分を
	print SOCKET "GET $ip_view_file HTTP/1.0\r\n";
	print SOCKET "HOST: $ip_view_host:$port\r\n";
	print SOCKET "\r\n";
に変えるとエラーが解消したので、適切な修正かどうかわからないが、ここに記しておく。

See more ...

WriteBacks