pandasのdatetimeの操作いろいろ

しょっちゅうやってる処理なのに毎回やり方を忘れてその都度考えるのがいい加減面倒になってきたので残しておく。

サンプルデータの生成

やりたいことは色々とあるが、とりあえずサンプルデータを本当に適当に作る。numpy.randomで整数を吐くのにrandint関数のhighは未満であることに注意する。てか整数吐くのに未満である必要あるか?

import numpy as np
import pandas as pd
d = pd.DataFrame({
    'year': np.random.randint(low=1990, high=2030, size=100),
    'month': np.random.randint(low=1, high=13, size=100),
    'day': np.random.randint(low=1, high=28, size=100),
    'hour': np.random.randint(low=0, high=24, size=100),
    'min': np.random.randint(low=0, high=60, size=100),
    'sec': np.random.randint(low=0, high=60, size=100),
})
d['datetime'] = pd.to_datetime(d['year'].astype(str) + '-' + d['month'].astype(str).str.zfill(2) + '-' + d['day'].astype(str).str.zfill(2) + ' ' + d['hour'].astype(str).str.zfill(2) + ':' + d['min'].astype(str).str.zfill(2) + ':' + d['sec'].astype(str).str.zfill(2) )
d = d[['datetime']]

これぐらいの適当さがたまらない。

こんな感じで山のような量のdatetimeの観測値があったとして、とりあえずその月の特に日数が知りたいことが多い。月によって日数が違うとかなんかいろいろめんどい。pandasのdatetime形式で変数は得られているので、mapとか使ってdatetime組みなおしてやればまず月初は簡単に出せる。

from datetime import datetime
func = lambda x: datetime(x.year, x.month, 1)
d['first'] = d['datetime'].map(func)

月末はこの形で指定しようと思うと結局その月に何日あるのか知らないといけなくなるが、特に年まで考慮しようとすると大変面倒なので翌月の月初から1日引く。1日引くにはtimedeltaでdays=1を指定しておけば普通に演算できる。

from datetime import datetime, timedelta
def yearmonth(x):
    y = x.year
    m = x.month
    y  = y if m != 12 else y + 1
    m  = 1 if m == 12 else m + 1
    dt = datetime(y, m, 1) - timedelta(days=1)
    return dt
d['last']  = d['datetime'].map(yearmonth)
d['ndays'] = (d['last'] - d['first']).dt.days + 1

無名関数使わなかったからダサい感じになってしまったが。

まあ本当のことをいえば、timedeltaでmonths=1が設定できればいいんだけどね。それができれば

func = lambda x: datetime(x.year, x.month, 1) + timedelta(month=1) - timedelta(days=1)

だけで終わるんだけどね。

…とはいえmonths + 1の処理ではそれこそ月によって加算される日数が変わるからバグの温床になりそうだし単純に嫌だろうね。

ze felt like itだそうなので仕方ないね(datetime.pyより)。

日付の差分

ちなみにもう一個datetimeの変数作って差をとると、表現によってこんな感じで差が出る。

_ = pd.DataFrame({
    'year': np.random.randint(low=1990, high=2030, size=100),
    'month': np.random.randint(low=1, high=13, size=100),
    'day': np.random.randint(low=1, high=28, size=100),
    'hour': np.random.randint(low=0, high=24, size=100),
    'min': np.random.randint(low=0, high=60, size=100),
    'sec': np.random.randint(low=0, high=60, size=100),
})
_['datetime2'] = pd.to_datetime(_['year'].astype(str) + '-' + _['month'].astype(str).str.zfill(2) + '-' + _['day'].astype(str).str.zfill(2) + ' ' + _['hour'].astype(str).str.zfill(2) + ':' + _['min'].astype(str).str.zfill(2) + ':' + _['sec'].astype(str).str.zfill(2) )
_ = _[['datetime2']]

d2 = d.join(_)
d2['date1'] = d2['datetime2'] - d2['datetime']
d2['date2'] = d2['datetime2'].dt.date - d2['datetime'].dt.date
d2['date3'] = d2['datetime2'].dt.normalize() - d2['datetime'].dt.normalize()

d2['date2==3'] = d2['date2'] == d2['date3']

display(d2.sort_values('date2==3'))

当たり前といえば当たり前だけどpandasのdatetime変数からdateで日付だけ抜き出すのと時刻の情報を初期化するnormalizeをかけたのでは差分の結果は同じ。

datetimeの距離の算出

あともうひとつ、わざと距離と表現したけど。とりあえず最初のコードでdatetime変数だけを持ってるデータフレームを作り直しておく。面倒くさいので関数化した。

def createdf(i=0):
    d = pd.DataFrame({
        'year': np.random.randint(low=1990, high=2030, size=100),
        'month': np.random.randint(low=1, high=13, size=100),
        'day': np.random.randint(low=1, high=28, size=100),
        'hour': np.random.randint(low=0, high=24, size=100),
        'min': np.random.randint(low=0, high=60, size=100),
        'sec': np.random.randint(low=0, high=60, size=100),
    })
    d['datetime'] = pd.to_datetime(d['year'].astype(str) + '-' + d['month'].astype(str).str.zfill(2) + '-' + d['day'].astype(str).str.zfill(2) + ' ' + d['hour'].astype(str).str.zfill(2) + ':' + d['min'].astype(str).str.zfill(2) + ':' + d['sec'].astype(str).str.zfill(2) )
    return d[['datetime']].rename(columns={'datetime':'datetime'+str(i)})
d = createdf().join(createdf(1))

これを適当に差分とった時にdatetimeがすごいのは(まあやってくれなきゃ困るんだけど)、絶対値とるとちゃんと表現を裏返してくれることです。

d['diff0'] = d['datetime1'] - d['datetime0']
d['absdiff0'] = np.abs(d['diff0'])

d['diff1'] = d['datetime0'] - d['datetime1']
d['absdiff1'] = np.abs(d['diff1'])

d['abs'] = ( d['absdiff0'] == d['absdiff1'] )

d.sort_values('abs')

プラマイ外せば終わりだと思いませんでした?それじゃだめです。

時間の算出

そんなpandasのdatetimeの信じられない挙動といえば、(とりあえずdatetime変数2つを時間単位でnormalizeした上で)差をとったときにdt.accessorでsecondsを指定すると、

from datetime import datetime
d = createdf().join(createdf(1))

norm = lambda x: datetime(x.year, x.month, x.day, x.hour, 0, 0)
d['n0'] = d['datetime0'].map(norm)
d['n1'] = d['datetime1'].map(norm)

d['diff'] = np.abs(d['n1'] - d['n0'])
d['diffsec'] = d['diff'].dt.seconds

日付のとこ見ねえ。くそやば。

つまりこれを解決するには(時間単位の距離を出すには)、

d['diff'] = np.abs(d['n1'] - d['n0'])
d['diffday'] = d['diff'].dt.days
d['diffsec'] = d['diff'].dt.seconds
d['diftotal'] = d['diffday'] * 24 + d['diffsec'] / 60 / 60

いやこれは逆に聞きたい。これしか解決策ないの?(変数をいくつも作っているのはわかりやすくするためであってそこは問題としていない)

まあこんな感じでやればいい。

またこの話になるけど(つまり俺は常々思っているわけだけど)、pandasの使い方を覚えるのがデータサイエンスなわけではない。別にこんなものはどうでもいい。学部向けデータ分析入門とかでimport pandas as pdまで教えて一コマ終わるみたいな日は本当につらい気持ちになる。

来年度の僕のマーケティング・リサーチ論は回帰(線形回帰、GLM、ロジスティック…)に分散分析やら各種検定もやるし、マーケティング情報システム論では機械学習側からクラスタリングに決定木にといろいろやります。