Pythonで日付データが数値データになっている場合の処理をまとめてみた

公開:2021/01/12
更新:2021/02/08
9 min読了の目安(約8500字TECH技術記事

Pythonで読み込んだcsvの日付データが、8桁の数字、例えば 2020/12/31 が 20201231 だったり、12桁の数字、例えば 2020/12/31 10:00 が 202012311000 だった場合の処理に手間取ったのでまとめます。

GitHubはこちら、Google Colabはこちらになります。

なお、読み込むcsvデータは、モック用のデータを作れるmockarooというサイトで作成しています。

詳しいmackarooの使い方は下記サイトをご参照ください。

https://yoshitaku-jp.hatenablog.com/entry/2020/10/18/190000

また、設定は下記のとおり行っております。

作成したcsvデータはコード上、Google Drive の直下に保存して使用する前提となっています。

日付データへの変換

特定の桁数の 文字列データ を pd.to_datetime() で読み込むことで、日付データに変換することができます。

https://deepage.net/features/pandas-to-datetime.html

数値データではない点に注意しましょう。

8桁を読み込むと年月日として読み込むことができます。

pd.to_datetime("20201218")

Timestamp('2020-12-18 00:00:00')

12桁を読み込むと年月日、時分として読み込むことができます。

pd.to_datetime("202012180150")

Timestamp('2020-12-18 01:50:00')

日付データとして読み込むことで、数値データと違って特定の日から特定の日までの差を正しく計算することができます。

なお、2020/12/18 1:50 の 時分 の部分が 150 となっている(0150となっていない)と、エラーとなって読み込むことができません。

pd.to_datetime("20201218150")
OverflowError: signed integer is greater than maximum

また、数値データの場合は文字列に変換してやる必要があります。

# 年月日を数値データを設定
time = 202012181050

# 数値データを読み込んだ場合
pd.to_datetime(time)

Timestamp('1970-01-01 00:03:22.012181050')

このように、数値データのまま読み込むと、おかしな日付データとして認識してしまいます。

そこで str() で文字列に変換してやります。

# 文字列に変換して読み込んだ場合
pd.to_datetime(str(time))

Timestamp('2020-12-18 10:50:00')

ただしく年月日を読み込むことができました。

DataFrame 型の場合の日付データへの変換

次に、DataFrame型として読み込んだ場合の日付データの処理を見てみましょう。

下記のようなデータを想定します。

df.head()

date1 カラムは数値データのため、そのまま読み込むとおかしな日付データになってしまいます。

pd.to_datetime(df["date1"])

0 1970-01-01 00:00:00.020201103
1 1970-01-01 00:00:00.020201109
2 1970-01-01 00:00:00.020201129
3 1970-01-01 00:00:00.020201119
4 1970-01-01 00:00:00.020201124
...
995 1970-01-01 00:00:00.020201113
996 1970-01-01 00:00:00.020201124
997 1970-01-01 00:00:00.020201128
998 1970-01-01 00:00:00.020201123
999 1970-01-01 00:00:00.020201105
Name: date1, Length: 1000, dtype: datetime64[ns]

そこで .astype(str) を使って文字列データに変換してやります。

pd.to_datetime(df["date1"].astype(str))

0 2020-11-03
1 2020-11-09
2 2020-11-29
3 2020-11-19
4 2020-11-24
...
995 2020-11-13
996 2020-11-24
997 2020-11-28
998 2020-11-23
999 2020-11-05
Name: date1, Length: 1000, dtype: datetime64[ns]

正しく日付データを認識することができました。

また、このcsv は、date1 カラムに年月日、time1 カラムに時分データが入っています(date2、time2 も同様)。

ただ、time1 は 例えば 2時10分が 170 と、分の相当部分に60以上の数値が入っていたり、8時50分が 0850 ではなく 850 と3桁の数値になっていたりとおかしなデータとなっているため、前処理が必要となってきます。

たとえば 3桁の場合は先頭に 0 を付けて 4桁にしたり、分が 70 になっている場合は 1時間10分 になるよう修正、具体的には時間部分を +1、分部分を -60 してやる必要があります。

DataFrame 型でこのような処理を行う場合、内包表現を使うと便利です。

また、内包表現では if、else は使えますが、elif は使えない点も留意しましょう。

https://qiita.com/kokorinosoba/items/eb72dac6b68fccbac04d

https://esu-ko.hatenablog.com/entry/2016/02/13/Pythonのリスト内包表記とif%2Celse

まず最初に、文字列データに変換します。

df["date1"] = df["date1"].astype(str)
df["time1"] = df["time1"].astype(str)
df["date2"] = df["date2"].astype(str)
df["time2"] = df["time2"].astype(str)

time1(time2も同様)の時間部分、分部分の抽出はスライスで行います。

# 時間部分の抽出
display(df["time1"].str[:-2])

# 分部分の抽出
display(df["time1"].str[-3:-1])

内包表現を使って、1桁しかない場合(例:0時5分が 5)、3桁しかない場合(例:1時30分が 130)と、条件に応じて先頭に 0 を付けてやります。

# 1桁の場合
df["time1"] = ["000" + i if len(i)==1 else i for i in df["time1"]]
df["time2"] = ["000" + i if len(i)==1 else i for i in df["time2"]]

# 2桁の場合
df["time1"] = ["00" + i if len(i)==2 else i for i in df["time1"]]
df["time2"] = ["00" + i if len(i)==2 else i for i in df["time2"]]

# 3桁の場合
df["time1"] = ["0" + i if len(i)==3 else i for i in df["time1"]]
df["time2"] = ["0" + i if len(i)==3 else i for i in df["time2"]]

次に2時10分が 170 となっているなど、分の相当部分に60以上の数値が入ってる場合の処理です。

なお、先ほどの前処理で time1 が4桁になっているので、スライスの指定がさきほどと変わっている点に注意しましょう。

また、分の部分が 69 以下の場合、計算のために数値データに変換するとまた1桁に戻ってしまうため、下記のように 70以上の場合、60以上の場合と場合分けをする必要があります。

df["time1"] = [str(int(i[:2])+1) + str(int(i[-2:])-60) if int(i[-2:])>=70 else i for i in df["time1"]]
df["time1"] = [str(int(i[:2])+1) + "0" + str(int(i[-2:])-60) if int(i[-2:])>=60 else i for i in df["time1"]]

df["time2"] = [str(int(i[:2])+1) + str(int(i[-2:])-60) if int(i[-2:])>=70 else i for i in df["time2"]]
df["time2"] = [str(int(i[:2])+1) + "0" + str(int(i[-2:])-60) if int(i[-2:])>=60 else i for i in df["time2"]]

時間の部分も1桁に戻ってしまっているので、もう一度先頭に0を付ける前処理を行いましょう。

# 2桁の場合
df["time1"] = ["00" + i if len(i)==2 else i for i in df["time1"]]
df["time2"] = ["00" + i if len(i)==2 else i for i in df["time2"]]

# 3桁の場合
df["time1"] = ["0" + i if len(i)==3 else i for i in df["time1"]]
df["time2"] = ["0" + i if len(i)==3 else i for i in df["time2"]]
df_date_time1 = df["date1"] + df["time1"]
df_date_time2 = df["date2"] + df["time2"]

df["date_time1"] = pd.to_datetime(df_date_time1)
df["date_time2"] = pd.to_datetime(df_date_time2)

df.head()

ようやく日付データとして読み込むことができました。

日付の差をとる

date_time2 と date_time1 の間にどれだけの日付があるのか、差を取ってみましょう。

df["elapsed_time"] = df["date_time2"] - df["date_time1"]
df["elapsed_time"]

0 41 days 02:40:00
1 34 days 08:30:00
2 4 days 00:30:00
3 19 days 16:56:00
4 20 days 21:55:00
...
995 43 days 09:56:00
996 34 days 03:56:00
997 6 days 06:46:00
998 37 days 19:26:00
999 54 days 10:06:00

日付データの差は、timedelta という型として認識されます。

timedelta 型 を日数や時間数に変更する場合の処理の詳細は、下記を参照ください。

https://www.it-swarm-ja.tech/ja/python/pythonでdatetimetimedeltaを分、時間に変換するにはどうすればよいですか?/1069892817/

また、DataFrame の場合は、.apply() と無名関数 lambda を組み合わせましょう。

https://www.366service.com/jp/qa/664f6721f2fcc9b3359d5d5a091d1c72
# 経過時間を分に修正する
df["elapsed_minute"] = df["elapsed_time"].apply(lambda x: x.seconds//60)

# 経過時間を日に修正する
df["elapsed_day"] = df["elapsed_time"].apply(lambda x: x.days)

df.head()

ただし上記の処理では、例えば 24日と5時間の差があった場合、df["elapsed_minute"] には 5時間部分が、df["elapsed_day"] には 24日間部分が、と別々に処理されてしまいます。

日数と時間数、あわせて xxx 分、としたい場合は、下記のように計算してやる必要があります。

df["elapsed_total_minute"] = df["elapsed_day"] * 24 * 60 + df["elapsed_minute"]
df.head()

以上になります、最後までお読みいただきありがとうございました。