Oct 04, 2020

Roland HP-145の鍵を修理した

先週、我が家の電子ピアノ、Roland HP-145の3つの鍵(C5, C♯5, D5)が下がりっ放しで戻らなくなった。鍵を押すとクッションに当たるのでなく、ガチ、ガチと硬い物に当たる音がして、スピーカーからは通常より小さな音しか出なくなった。

現象が発生した瞬間は見ていないので、直接の原因はわからない。
この電子ピアノは買って21年が経過したが、のべ1,000時間くらいしか使っていない。これの10倍使われた同型機は沢山あると思う。それに、筆者は打鍵圧がかなり弱い(最初にキーがめちゃくちゃ軽いキーボードを使って練習したので、アップライトピアノを弾くとほとんど音が出ないくらいである)。購入後に7回の引っ越しを経たことはあるが、この使い方で何かが折れたり割れたりするとは思えなかった。それに、同時に3つの鍵がおかしくなったので、異物が挟まったのだろうかと思った。

もう古いので、新しい物に買い換えようかとも思ったが、何よりも、まず捨てるのが面倒で億劫である。しかし、そのままだと鍵の位置的に使い物にならない。古すぎて、お金を払って修理してもらう気にはなれない。

Rolandの電子ピアノの鍵を自力で修理したという話は、探せばいくつも見つかったし、大抵の人は簡単に分解したようだったので、やってみることにした。
分解のやり方がなかなかわからず、思いの外時間がかかったが、結果として、ドライバー1本で、5分もあれば分解できることがわかった。
またやるかも知れないので、鍵を修理する為の分解方法をここに控えておく。

  1. 天板を開ける
    天面の2つのネジと背面の上部の4つのネジを外して、天板を手前に少しずらすと、外れる。


  2. スライド蓋を外す
    スライド蓋のレールの途中にある、歯車の出入り口(【写真1】、左右両方)と、天面を支える金具(【写真2】、片方でOK)を外すと、スライド蓋を外すことができる。
    【写真1】
    【写真2】


  3. 鍵の上のクッション付きの横棒を外す
    左右それぞれ、1つは上から、1つは下からの2つのネジ(【写真3】)を外す。
    【写真3】


  4. 問題の鍵を外す
    鍵の上部のU字部分(【写真4】)を少し広げると外せる。(ただ、折れそうで、かなり恐い。)
    外すと、ハンマーのような部品が見える。
    【写真4】

鍵を外すと、C♯5の黒鍵のハンマー部品が折れており(【写真5】)、先端の部分が3つの鍵の下に跨るように落ちていた。
【写真5】
折れたハンマー部品を取り除き、多くの人がやっているように、最も使わなさそうな左端の黒鍵(A♯0)のハンマー部品を移植すると、直った。

See more ...

Posted at 19:07 in 雑記 | WriteBacks (0)
WriteBacks

Sep 22, 2020

[Pandas] sharex=Trueで時系列データの折れ線グラフと棒グラフを共存させる

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

N = 10
np.random.seed(8)
df = pd.DataFrame({
    'A': np. random.randint(10, size=N),
    'B': np. random.randint(10, size=N),
    'C': np. random.randint(10, size=N)
}, index=pd.date_range('2020-09-01', periods=N))
df
A B C
2020-09-01 3 1 5
2020-09-02 4 3 5
2020-09-03 1 9 7
2020-09-04 9 2 9
2020-09-05 5 2 2
2020-09-06 8 6 6
2020-09-07 3 8 9
2020-09-08 8 9 5
2020-09-09 0 3 1
2020-09-10 5 4 6
このような時系列データがあるとし、A列を折れ線グラフ、B,C列を別のグラフに棒グラフで描画したものを、時間軸を合わせて縦に並べたいとする。
そこで、次のようなコードを実行すると、2段のグラフの内、上のグラフが描画されなかった。
fig, ax = plt.subplots(2, 1, sharex=True)
df['A'].plot(ax=ax[0], grid=True)
df[['B', 'C']].plot(kind='bar', ax=ax[1], grid=True)
plt.show()

下のグラフを描画しなければ、上のグラフが描画される。

fig, ax = plt.subplots(2, 1, sharex=True)
df['A'].plot(ax=ax[0], grid=True)
plt.show()

原因は、次のコードを実行するとわかった。

fig, ax = plt.subplots(2, 1, sharex=True)
df['A'].plot(ax=ax[0], grid=True)
print("xlim after 1st plot:", ax[0].get_xlim())
df[['B', 'C']].plot(kind='bar', ax=ax[1], grid=True)
print("xlim after 2nd plot:", ax[0].get_xlim())
実行結果
xlim after 1st plot: (18506.0, 18515.0)
xlim after 2nd plot: (-0.5, 9.5)
つまり、 pandas.DataFrame.plot は kind='line' と kind='bar' とで描画した後のX座標の範囲(xlim)が全く異なり、 matplotlib.pyplot.subplots(sharex=True) した状態でこの2つを描画すると、先に描画した座標系(Axes)のxlimが書き換えられてしまうのが原因である。
色々調べまくったが、棒グラフ表示にこだわると、pandas.DataFrame.plotを使って解決する方法は見つからなかった。(kind='scatter'の点グラフなら同じ問題が起こらないことを確認した。コードは省略)

Seabornを使っても、結果は同じだった。

import seaborn as sns
fig, ax = plt.subplots(2, 1, sharex=True)
df['A'].plot(ax=ax[0], grid=True)
print("xlim after 1st plot:", ax[0].get_xlim())
_ = df[['B', 'C']].melt(ignore_index=False).reset_index()
sns.barplot(x='index', y='value', hue='variable', data=_)
print("xlim after 2nd plot:", ax[0].get_xlim())
plt.show()
実行結果
xlim after 1st plot: (18506.0, 18515.0)
xlim after 2nd plot: (-0.5, 9.5)

結局、棒グラフだけ直接matplotlib APIを使って描画すると解決した。

fig, ax = plt.subplots(2, 1, sharex=True)
df['A'].plot(ax=ax[0], grid=True)

width = pd.Timedelta('0.4d')
ax[1].bar(df.index - width/2, df['B'], width=width, label='B')
ax[1].bar(df.index + width/2, df['C'], width=width, label='C')
ax[1].set_xlim(df.index[0] - width*2, df.index[-1] + width*2)
ax[1].grid(True)
ax[1].legend()

fig.autofmt_xdate()
plt.show()

See more ...

Posted at 16:41 in PC一般 | WriteBacks (0)
WriteBacks

Aug 15, 2020

[Pandas] 系列データの同じ値が連続する区間毎にgroupbyする方法

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

# サンプルデータ作成
np.random.seed(666)
df = pd.DataFrame({'value': np.random.randn(365).cumsum()},
                  index=pd.date_range('2019-1-1', periods=365))
df
value
2019-01-01 0.824188
2019-01-02 1.304154
2019-01-03 2.477622
2019-01-04 3.386670
2019-01-05 2.814949
... ...
2019-12-27 -1.935362
2019-12-28 -1.170606
2019-12-29 -0.112895
2019-12-30 0.490068
2019-12-31 -0.184056
# 描画
fig = plt.figure(figsize=(15, 3))
df['value'].plot()
plt.axhline(0, color='r')
plt.show()

こういうデータがあり、値が0より大きい日が5日以上連続する区間をグラフ上で示したいとする。
筆者は過去に似たようなことをしたい時があり、適当な方法がわからなかったので、効率が悪いと知りつつ、次のように、DataFrameの各行をループ処理で1行ずつ調べて該当区間を求めるようにした。

●改良前のコード
# 'value' > 0 が5日以上連続する区間を求める
df['cont_days'] = 0  # 'value' > 0 が連続する日数
df['ge_5d'] = False  # 連続する日数が5日以上(greater than or equal to)かどうか
flag = False  # 1つ前が 'value' > 0 がどうか
for i in range(len(df)):
    if df.iloc[i]['value'] > 0:
        if flag == False:
            start_i = i    # value' > 0 の開始位置を保存
            flag = True
    else:
        if flag == True:
            end_i = i    # value' > 0 の終了位置
            if end_i - start_i >= 5:
                print("{} - {} ({} days)".format(
                    df.index[start_i].date(), df.index[end_i - 1].date(), end_i - start_i))
                df.loc[df.index[start_i:end_i], ['cont_days', 'ge_5d']] = end_i - start_i, (end_i - start_i >= 5)
            flag = False

df
●実行結果
2019-01-01 - 2019-01-16 (16 days)
2019-03-17 - 2019-03-29 (13 days)
2019-04-20 - 2019-05-01 (12 days)
2019-05-14 - 2019-05-26 (13 days)
2019-06-12 - 2019-07-20 (39 days)
2019-07-22 - 2019-07-26 (5 days)
2019-07-29 - 2019-08-04 (7 days)
2019-08-13 - 2019-10-18 (67 days)
value cont_days ge_5d
2019-01-01 0.824188 16 True
2019-01-02 1.304154 16 True
2019-01-03 2.477622 16 True
2019-01-04 3.386670 16 True
2019-01-05 2.814949 16 True
... ... ... ...
2019-12-27 -1.935362 0 False
2019-12-28 -1.170606 0 False
2019-12-29 -0.112895 0 False
2019-12-30 0.490068 0 False
2019-12-31 -0.184056 0 False
●結果の描画コード
# 描画
fig = plt.figure(figsize=(15,3))
ax1 = fig.gca()

# 'value' > 0 が5日以上連続する区間を塗り潰す
ax2 = ax1.twinx()
ax2.fill_between(df.index, 0, df['ge_5d'], color='r', alpha=0.2, linewidth=0, step='post')
ax2.axes.yaxis.set_visible(False)

# 'value'の描画、先にするとX軸のラベルのフォーマットが変わるので後でする
df['value'].plot(ax=ax1)
ax1.axhline(color='r')

plt.show()
●描画結果

後日、そういうのは次のようにshift()とcumsum()をうまく使えばgroupby()で処理できるということを教えてもらった。

●改良後のコード
# 'value' > 0 が5日以上連続する区間を求める
df['flag'] = df['value'] > 0
df['cont_days'] = df.groupby((df['flag'] != df['flag'].shift()).cumsum())['flag'].transform(sum)
df['ge_5d'] = df['cont_days'] >= 5
df
●実行結果
value flag cont_days ge_5d
2019-01-01 0.824188 True 16 True
2019-01-02 1.304154 True 16 True
2019-01-03 2.477622 True 16 True
2019-01-04 3.386670 True 16 True
2019-01-05 2.814949 True 16 True
... ... ... ... ...
2019-12-27 -1.935362 False 0 False
2019-12-28 -1.170606 False 0 False
2019-12-29 -0.112895 False 0 False
2019-12-30 0.490068 True 1 False
2019-12-31 -0.184056 False 0 False
※結果の描画コードと描画結果は上と同じなので省略

改良後のコード中の groupby((df['flag'] != df['flag'].shift()).cumsum()) は初見ではややこしいが、次の例で説明すると、df['flag'].shift()が1つ前の値、df['flag'] != df['flag'].shift()が1つ前と同じかどうかで、それを累積(cumsum)することにより、'flag'が前と同じ値の所は同じ番号、変化があった所で次の番号となり、これをgroupby()のキーにすることにより、'flag'の同じ値が連続する区間毎にグループ分けされる。

# groupby((df['flag'] != df['flag'].shift()).cumsum()) の解説用
df = pd.DataFrame({
    'flag': [False, False, True, True, False, True, True, True, False, False]})
df['shift'] = df['flag'].shift()
df['diff'] = df['flag'] != df['shift']
df['cont_group'] = df['diff'].cumsum()
df
flag shift diff cont_group
0 False NaN True 1
1 False False False 1
2 True False True 2
3 True True False 2
4 False True True 3
5 True False True 4
6 True True False 4
7 True True False 4
8 False True True 5
9 False False False 5

改良前のコードと改良後のコードを比較すると、改良後のコードは断然短いし、処理時間も圧倒的に短く(筆者の環境では改良前約200ms、改良後約7.5ms)、しかもデータサイズが100倍になっても処理時間が少ししか伸びない(改良前約14秒、改良後約12.5ms)。

教えてもらった所の他の人のコメントを見ると、その筋では「shiftを使えばいい」だけで以上のことが通じるらしいことになっていた。
pandas documentation"Cookbook"の"Grouping like Python's itertools.groupby"の所に載っているし、stackoverflowのあるページに"uses some common idioms"と書かれているので、きっとよく知られたパターンなのだろう。

See more ...

Posted at 17:21 in PC一般 | WriteBacks (0)
WriteBacks

Jun 14, 2020

[Pandas] groupby.aggのnested renamingの代替手段

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

np.random.seed(9)
df = pd.DataFrame({
    '組': np.random.randint(3, size=20),
    '身長': np.random.randint(950, 1050, size=20) / 10,
    '地域': np.random.randint(5, size=20)})
df['組'] = df['組'].map({0: 'もも', 1: 'さくら', 2: 'ばら'})
df['地域'] = df['地域'].map({0: 'A町', 1: 'B町',  2: 'C町',  3: 'D町',  4: 'E町'}) 
df.head()
Out [1]:
身長 地域
0 ばら 101.0 B町
1 もも 100.9 A町
2 ばら 103.8 D町
3 さくら 102.4 C町
4 ばら 100.6 B町

こういうDataFrameがあり、組ごとに、
・身長の最低値と最高値
・A町の子が含まれているか、B町の子が含まれているか
を求めたいとする。
あまりいい例題では無いが、筆者が仕事で実際に必要になった処理と等価な、他にましな例題を思い付かなかった。

筆者は当初、df.groupby('組')['地域'].agg(lambda x: ...)のようにして集計結果を1列ずつ求め、後で結合していたのだが、先週、1回のgroupby.agg()でできる、次のような書き方があることを知った。

In [2]:
df.groupby('組')[['身長', '地域']].agg({
    '身長': {
        '最低身長': np.min,
        '最高身長': np.max
    },
    '地域': {
        'A町の子あり': lambda x: any(x == 'A町'),
        'B町の子あり': lambda x: any(x == 'B町')
    }})
Out [2]:
最低身長 最高身長 A町の子あり B町の子あり
さくら 95.9 104.9 True False
ばら 100.6 103.8 False True
もも 95.1 104.3 True False

同じ列に複数の集約関数を適用し、しかもそれぞれの結果の列に任意の列名を付与できるのである。
これは便利、と思って早速これを使うように書き直して、ローカルPCで動作確認して別PCにコピーして実行すると、

SpecificationError: nested renamer is not supported
というエラーになってしまった。

調べてみると、上のdict-of-dictを渡す書き方(nested renamingというらしい)はPandas v0.20.0でdeprecatedとされ、v1.0で廃止されたらしい。
What's new in 1.0.0より:
Removed support for nested renaming in DataFrame.aggregate(), Series.aggregate(), core.groupby.DataFrameGroupBy.aggregate(), ...
ローカルPCのPandasはv0.25.3だったので、nested renamingが動いた。

それでは代わりの方法は無いのかと思って探すと、"named aggregation"が推奨と書かれているのを見つけた。
What's new in 0.25.0より:

Named aggregation is the recommended replacement for the deprecated "dict-of-dicts" approach to naming the output of column-specific aggregations
他に、aggに列と関数のリストだけのdictを与えて、後で列名をrenameする方法もあるが、通常はaggに渡す関数名が結果の列名になるのに対し、lambda関数を渡すと列名が勝手に付けられるので、面倒なことになる。

Named aggregationを使うと、上のv1.0でエラーになったコードは次のように書ける。

In [3]:
df.groupby('組').agg(
    最低身長=('身長', np.min),
    最高身長=('身長', np.max),
    A町の子あり=('地域', lambda x: any(x == 'A町')),
    B町の子あり=('地域', lambda x: any(x == 'B町')))
Out [3]:
最低身長 最高身長 A町の子あり B町の子あり
さくら 95.9 104.9 True False
ばら 100.6 103.8 False True
もも 95.1 104.3 True False

列名をクォーテーションマークで括ったり括らなかったりするのが統一感に欠けるが、得られる結果が少しわかりやすくなったと思う。それから、前のコードでは[['身長', '地域']]でやっていた、aggに渡す前に列を絞るのが不要になった(絞らないとnested renamingでは列がMultiIndexになってしまう)ので、すっきりしたと感じる。

See more ...

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

May 24, 2020

(Pandas)複数の列を使ってtransformする方法で悩む

In [1]:
import pandas as pd
df = pd.DataFrame({'name': ['a', 'b', 'c', 'd'] * 3,
                               'value': range(12),
                               'flag': [1, 0, 0, 0, 1, 0] * 2})
df
Out [1]:
name value flag
0 a 0 1
1 b 1 0
2 c 2 0
3 d 3 0
4 a 4 1
5 b 5 0
6 c 6 1
7 d 7 0
8 a 8 0
9 b 9 0
10 c 10 1
11 d 11 0

こういうDataFrameがあり、'name'でgroupbyして、'flag'に1つでも1があるグループの、'flag'が1の行がグループ内の'value'の平均、それ以外の行は0という列(この例ではインデックスが0と4の所が'a'の平均、6と10の所が'c'の平均、それ以外は0という列)を追加したいとする。
大体そういう感じのことをtransformでやりたかったが、スマートなやり方がわからず、悩んでいる。

目的の列をグループ毎に返す関数を、transformの代わりにapplyを使って、

In [2]:

def func(groupdf):
    ret = pd.Series(0, index=groupdf.index)
    if any(groupdf['flag']):
        ret[groupdf['flag'] == True] = groupdf['value'].mean()
    return ret
    
df.groupby('name').apply(func)

又は

In [3]:

df.groupby('name').apply(lambda x: x['value'].mean() * x['flag'] * any(x['flag']))

Out [3]:

name    
a     0     4.0
      4     4.0
      8     0.0
b     1     0.0
      5     0.0
      9     0.0
c     2     0.0
      6     6.0
      10    6.0
d     3     0.0
      7     0.0
      11    0.0
Name: flag, dtype: float64

というように作ることができたのだが、[2]のfuncも[3]のlambda関数も、transformに与えると、'flag'という列が無いというエラーになる。
applyなら呼び出される関数に複数列のDataFrameが渡されるので複数列を参照しながら計算ができるが、aggregateやtransformだと呼び出される関数に1列分のSeriesしか渡されないので、複数列を参照しながら計算ができない。

もし、flagが1の行だけがグループの平均という条件を外し、'flag'に1つでも1があるグループはグループ内の'value'の平均、それ以外の行は0という列で良い、つまりグループ内は全て同じ値になるなら、Webでサンプルコードがいくつか見つかり、大きく分けて2つの方法が見つかった。
1つは、列毎にtransformした結果を組み合わせて何とかするという方法である。

In [4]:

grouped = df.groupby('name')
df['ave'] = grouped['value'].transform(np.mean) * grouped['flag'].transform(any)
df
Out [4]:
(省略)

もう1つは、applyを使ってグループ毎に計算した結果をmergeする方法である。

In [5]:

_ = df.groupby('name').apply(lambda x: x['value'].mean() if any(x['flag']) else 0).rename('ave')
df = df.merge(_, how='left', on='name')
df
Out [5]:
name value flag ave
0 a 0 1 4
1 b 1 0 0
2 c 2 0 6
3 d 3 0 0
4 a 4 1 4
5 b 5 0 0
6 c 6 1 6
7 d 7 0 0
8 a 8 0 4
9 b 9 0 0
10 c 10 1 6
11 d 11 0 0

前者は列毎にtransformした後で何とかなる場合しか使えないのに対し、後者は複数の列を参照しながら計算できるので、後者の方が汎用的だと思う。
速度面では、前者は中間データを作成して処理時間がかかりがちなtransformを複数回実行するので不利なように思えたが、筆者のJupyter Notebookの%timeitで計測した限り、DataFrameのサイズを10,000倍とかにしても、処理時間は大差なかった。

元のやりたいことについては、前者(In [4]の例)を応用すると、次のようなのができた。

In [6]:

grouped = df.groupby('name')
df['ave'] = grouped['value'].transform(np.mean) * grouped['flag'].transform(any) * df['flag']
df
Out [6]:
name value flag ave
0 a 0 1 4.0
1 0 0 0 0.0
2 0 0 0 0.0
3 0 0 0 0.0
4 a 4 1 4.0
5 0 0 0 0.0
6 c 6 1 6.0
7 0 0 0 0.0
8 0 0 0 0.0
9 0 0 0 0.0
10 c 10 1 6.0
11 0 0 0 0.0

しかし、transformを2回使うし、式が場当たり的で汎用的でないのが不満である。
後者(In [5]の例)を応用すると、次のようにするしか思い付かない。

In [7]:

_ = df.groupby('name').apply(lambda x: x['value'].mean() if any(x['flag']) else 0).rename('ave')
df = df.merge(_, how='left', on='name')
df[df['flag'] == 0] = 0
df
Out [7]:
(Out [6] と同じ)

これも、後のboolean indexing部分はグループを無視して処理しているので、そのようにできない時は同じようにできないし、これによって処理時間が大幅に伸びるし、transformの出番と思われるのにtransformを使ってないのが不満である。

See more ...

Posted at 15:04 in PC一般 | WriteBacks (0)
WriteBacks

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

Feb 23, 2020

Movable Type 7にバージョンアップ

このweblogはMovable Typeで運用している。現在ブログツールとしてはWordPressが圧倒的に人気だが、それでもMovable Typeを使い続けているのは、Movable TypeがHTMLを静的に生成でき、このRaspberry Piのような非力なサーバーに適していると思うからである。

これまで、aptでインストールできたmovabletype-opensource 5.2.7を使っていたが、とっくの昔にサポート終了状態で、ある時にaptitudeで更新パッケージをインストールするとパッケージの依存関係が解決されなくなって、その時にaptitudeが提示する解決方法を訳も分からずに選択して延命したが、aptでの管理が困難な状態になっていた。
加えて、コメントを投稿すると、裏で投稿に成功するものの、"wide character in syswrite"というエラーメッセージが表示されるようになっていたことに今月気付いたので、Movable Typeをパージョンアップすることにした。

前回のバージョン3→5への移行は相当大変で、コメントフォームの日本語化など、やり切れず諦めて放置した部分もあり、今回もかなり不安だったが、今回はMovable Typeの
個人無償版ダウンロードのページからMovable Type 7 r.4605のパッケージをダウンロードして、Apacheの設定を切り替えながら「旧バージョンから Movable Type 7 へアップグレードする」のページの手順に従って、ほぼ一発で成功した。

ただ、全体的に動作が遅くなり、サイトの再構築をすると、"500 Internal Server Error"というエラーになった。Apacheのログを見ると、

[fcgid:warn] mod_fcgid: read data timeout in 40 seconds, ...
[core:error] End of script output before headers: ...

というのが残っており、FastCGIのタイムアウト設定を変えれば解決することがわかったので、/etc/apache2/mods-enabled/fcgid.confに
FcgidIOTimeout 100
を加えると、成功した。

また、「Q.ウェブサイトが英語で表示される」のページのA.の手順に従って、テンプレートの日本語化にも一発で成功した。

ただ、これまでに自分でテンプレートをカスタマイズしてきたのがリセットされてしまい、どこをどういじったのかを控えてなかったので、バックアップされたテンプレートと比較しながら元に戻すのが結構面倒だった。以前に行ったテーマ切り替えでも同じような苦労があったので、今後はテンプレートに対して手動で行ったカスタマイズ内容を控えておくことにした。

See more ...

WriteBacks

Jan 12, 2020

Jupyter Notebookのリモートアクセス設定をしてみた

ガラケー+格安SIM差しタブレットの2台持ちに憧れて数年、完全に時代遅れ感があった昨秋に、遂にiPad(Wi-Fi + Cellularモデル)を買ってしまった。データ通信専用の格安SIMも差した。定期的に見たいが我が家の旧式PCでは見れないWebサイトがあったので、この機会にと思って勢いで買ったのだが、購入して2ヶ月、予想通り、Webブラウザ以外ほとんど使っていない。折角なので何か他にも活用したいと思って、まず思いついたのはPythonのプログラミング環境構築だった。
外出先でプログラミングすることはまず無いのだが、筆者は、目の前にあるiPadのような高スペックのコンピューターにプログラミング環境が無いと、気になって仕方がないのである。
そして、筆者が今一番興味ある言語はPythonであり、iPadでやるならPython一択である。

少し調べた限り、今iPadでPythonプログラミングするならPythonista3の一択のようであるが、Pythonista3ではPandasを使えないようで、しかも有料だったので、今回は見送った。
仕方なく、Jupyter Notebookを使うことにした。

自宅のLANでiPadのSafariからJupyter Notebookに接続してみると、表示もコード入力も実行もできた。ソフトウェアキーボードではかなり辛く、特にTABキーが無いので補完が効かないのが致命的であるが、必要であればBluetoothキーボードを使ったりJunoという有料のアプリを使えば解決しそうなので置いておくとして、とりあえず自宅のPCに起動したJupyter Notebookに外出先からアクセスできるようにすれば、目的が達成できそうである。

筆者はプロバイダーから動的に付与される自宅のIPアドレスをDDNSに登録するようにしているので、既に外から自宅内PCへのアクセスは可能である。実際、ルーターのポートフォワーディング(静的NAT)設定をするだけで、iPadからセルラー網経由で自宅PCのJupyter NotebookへHTTP接続できた。
しかし、HTTPだとパスワードが平文で送られてしまうのが問題である。
そこで、以下の3通りの方法を考えてみた。

(1) 自宅PCにSSHサーバーを設けてSSHトンネリングでJupyter Notebookに接続する
(2) ApacheにHTTPS接続し、Reverse ProxyでJupyter Notebookに接続する
(3) Jupyter Notebookに直接HTTPS接続する

(1)は筆者が過去にそうやって接続するしかないクラウドサービスを使ったことがあるので真っ先に思い付いた方法で、PCからは難なく成功したのだが、iPadで安全にSSHトンネリングしてキープさせるには有料アプリを使うしかなさそうなので、やめた。
(2)(3)を今回やってみたので、ここに記録する。

HTTPS接続したApacheのReverse ProxyでJupyter Notebookに接続する環境の作成方法

ここでは、https://ynomura.dip.jp/desktop/ipython/でデスクトップPC(192.168.0.80)のポート8888のJupyter Notebookに接続できるようにするものとする。

■Apache(2.4)の設定
HTTPSのVirtualHostは設定済、SSL/TLSサーバ証明書もインストール済みとする。
結果として、以下の設定で成功した。

  • proxy, proxy_http, proxy_wstunnelモジュールの有効化
    sudo a2enmod proxy
    sudo a2enmod proxy_http
    sudo a2enmod proxy_wstunnel
  • sites-enabled/*の<VirtualHost *:443>の所に以下のように/desktop/ipython/の設定を追加
    
    <VirtualHost *:443>
    	...
    
    	<Location /desktop/ipython/>
    		ProxyPass http://192.168.0.80:8888/desktop/ipython/
    		ProxyPassReverse http://192.168.0.80:8888/desktop/ipython/
    		ProxyPreserveHost On
    
    		Order deny,allow
    		Deny from all
    		Allow from 192.168. pXXXXXXX-mobac01.tokyo.ocn.ne.jp
    	</Location>
    	<Location /desktop/ipython/api/kernels/>
    		ProxyPass ws://192.168.0.80:8888/desktop/ipython/api/kernels/
    		ProxyPassReverse ws://192.168.0.80:8888/desktop/ipython/api/kernels/
    	</Location>
    
    </VirtualHost>
    
    "pXXXXXXX-mobac01.tokyo.ocn.ne.jp"というのは、筆者のiPadのFQDN(アドレス)である。これは時々変化するのだが、"-mobac01.tokyo.ocn.ne.jp"の部分は変わらないようなので、Allowの所に"-mobac01.tokyo.ocn.ne.jp"と書きたいのだが、残念ながらそれは通らないらしい。(https://httpd.apache.org/docs/2.4/ja/mod/mod_access_compat.htmlに、"Allow from apache.org"と書くと foo.apache.org にはマッチするが、 fooapache.org にはマッチしないと書かれている。マッチすると困るので当たり前であるが)
    "ProxyPreserveHosts On"としないと、Jupyter Notebookで"Blocking Cross Origin API request. Origin: https://ynomura.dip.jp, Host: 192.168.0.80:8888"というエラーになる。
参考: Deploying behind a reverse proxy - jupyterhub/jupyterhub

■Jupyter Notebook側の設定

  • リモートアクセスを可能にする設定
    jupyter_notebook_config.pyにて
    c.NotebookApp.ip = '*'
    とする。
  • パスワード設定
    Jupyter Notebookにて
    from notebook.auth import passwd
    passwd()
    としてハッシュ化されたパスワードを得て、jupyter_notebook_config.pyにて
    c.NotebookApp.password = u'(ハッシュ化されたパスワード)'
    とする。
    参考: Securing a notebook server - Jupyter Notebook documentation
  • URL変更
    jupyter_notebook_config.pyに
    c.NotebookApp.base_url = '/desktop/ipython/'
    を追加する。
    これをせず、https://ynomura.dip.jp/desktop/ipython/をhttp://192.168.0.80:8888/にリレーする方法がわからなかった。

■Jupyter NotebookをHTTPSサーバーにする設定方法

Jupyter Notebook DocumentationのUsing SSL for encrypted communicationの所に全て書いてあった。
  • リモートアクセスを可能にする設定
    上記と同じ
  • パスワード設定
    上記と同じ
  • SSL/TLSサーバ証明書と秘密鍵の設定
    jupyter_notebook_config.pyに
    c.NotebookApp.certfile = u'/absolute/path/to/your/certificate/mycert.pem'
    c.NotebookApp.keyfile = u'/absolute/path/to/your/certificate/mykey.key'
    の2行を追加する。
  • ポート番号変更
    ポート8888をインターネットに解放するのはすぐ狙われそうで抵抗があるので、変更する。ルーターのNAPT機能でポート番号を変換できれば良いのだが、筆者宅のルーターはNATしか対応していないので、Jupyter Notebookの待ち受け番号を変更する。
    jupyter_notebook_config.pyに
    c.NotebookApp.port = XXXXX
    を追加する。
  • ポートの解放、NAT設定
    Jupyter Notebookを起動するPCのFirewallのポートXXXXXを解放し、ポートXXXXX宛のパケットをそのPCに転送するようにルーターのNAT設定を変更する。
  • URL変更
    インターネットからhttps://既知のドメイン名:ポート番号/でアクセス可能だと気になるので、せめてhttps://既知のドメイン名:ポート番号/何らかのパス/としておく。
    Jupyter Notebookを起動するPCでもApacheを立ち上げるなどしてhttps://未知のホスト名.既知のサーバー名:ポート番号/とする方が無難だが、動作確認した方法の1つとして控えておく。
    jupyter_notebook_config.pyに
    c.NotebookApp.base_url = '/desktop/ipython/'
    を追加する。

See more ...

Posted at 23:33 in PC一般 | WriteBacks (0)
WriteBacks