先週、我が家の電子ピアノ、Roland HP-145の3つの鍵(C5, C♯5, D5)が下がりっ放しで戻らなくなった。鍵を押すとクッションに当たるのでなく、ガチ、ガチと硬い物に当たる音がして、スピーカーからは通常より小さな音しか出なくなった。
現象が発生した瞬間は見ていないので、直接の原因はわからない。
この電子ピアノは買って21年が経過したが、のべ1,000時間くらいしか使っていない。これの10倍使われた同型機は沢山あると思う。それに、筆者は打鍵圧がかなり弱い(最初にキーがめちゃくちゃ軽いキーボードを使って練習したので、アップライトピアノを弾くとほとんど音が出ないくらいである)。購入後に7回の引っ越しを経たことはあるが、この使い方で何かが折れたり割れたりするとは思えなかった。それに、同時に3つの鍵がおかしくなったので、異物が挟まったのだろうかと思った。
もう古いので、新しい物に買い換えようかとも思ったが、何よりも、まず捨てるのが面倒で億劫である。しかし、そのままだと鍵の位置的に使い物にならない。古すぎて、お金を払って修理してもらう気にはなれない。
Rolandの電子ピアノの鍵を自力で修理したという話は、探せばいくつも見つかったし、大抵の人は簡単に分解したようだったので、やってみることにした。
分解のやり方がなかなかわからず、思いの外時間がかかったが、結果として、ドライバー1本で、5分もあれば分解できることがわかった。
またやるかも知れないので、鍵を修理する為の分解方法をここに控えておく。
- 天板を開ける
天面の2つのネジと背面の上部の4つのネジを外して、天板を手前に少しずらすと、外れる。
- スライド蓋を外す
スライド蓋のレールの途中にある、歯車の出入り口(【写真1】、左右両方)と、天面を支える金具(【写真2】、片方でOK)を外すと、スライド蓋を外すことができる。
【写真1】
【写真2】
- 鍵の上のクッション付きの横棒を外す
左右それぞれ、1つは上から、1つは下からの2つのネジ(【写真3】)を外す。
【写真3】
- 問題の鍵を外す
鍵の上部のU字部分(【写真4】)を少し広げると外せる。(ただ、折れそうで、かなり恐い。)
外すと、ハンマーのような部品が見える。
【写真4】
鍵を外すと、C♯5の黒鍵のハンマー部品が折れており(【写真5】)、先端の部分が3つの鍵の下に跨るように落ちていた。
【写真5】
折れたハンマー部品を取り除き、多くの人がやっているように、最も使わなさそうな左端の黒鍵(A♯0)のハンマー部品を移植すると、直った。
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()
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"と書かれているので、きっとよく知られたパターンなのだろう。