Pythonで与えられた時間データが決まった形ではなかった時の対処法(datetime)

初めて記事を書かせて頂きます。

[良い記事を書くためのガイドライン](https://help.qiita.com/ja/articles/qiita-article-guideline)を見ながら書きました:dragon:
時間を扱い始めて日が浅いので、間違っている点や追加点があればお願いします！

**2021/02/01 更新**
正規表現を用いてより分かりやすくより簡潔に修正してくださった方がいらっしゃいました！！
[コメント](https://qiita.com/YlePhan/items/a8fe312eda18829ac73f#comment-93062b8ae9714912b31d)をご参照ください！！

# きっかけ
あるデータの継続時間（〇〇h〇〇m〇〇s）をdatetime64[ns]に変換して
時間ごとの変化量を可視化したいな～～って思った時に
**1dayに変換されず24時間を超えるデータ(27h〇〇m〇〇s)**があったり、
**まず1時間に到達してなかったり(〇〇m〇〇s)**と、
与データの仕様で色々困ってしまったからです（詳しくは後述データ)。

綺麗にデータが与えてくれるのが普通だと思うけど！！

同じような境遇の人がいればいいな...:innocent:

以下に、**解決した**処理を書きます。

# バーションとか
```python
import sys
import pandas as pd
import datetime

# Pythonのバージョン
print(sys.version)
# pandasのバージョン
print(pd.__version__)
```
```
1.1.3
3.8.5 (default, Sep  3 2020, 21:29:08) [MSC v.1916 64 bit (AMD64)]
```
# 原因となったデータとその処理
ここでは簡潔に適当なデータを用意します。（ダミーです）

```python
test_data = pd.DataFrame(['1h2m3s', '10m30s', '25h23m10s'],
                          columns=['test'])
test_data.head()
```
```
	test
0	1h2m3s
1	10m30s
2	25h23m10s
```
見やすくしたもの↓

| 　 | 時間データ(test) |
|:-:|:-:|
|  0 | 1h2m3s  |
|  1 |  10m30s |
|  2 |  25h23m10s |
データを見ると、
index 1番目で**本来は時間(h)があって欲しいのに10m30sになって**たり、
index 2番目で**時間(h)において24時間を超えてしまっています**よね

よくある自分が知っている処理では

```python
test_data['test_converted'] = test_data['test'].map(lambda \
                    x:datetime.datetime.strptime(x, '%Hh%Mm%Ss'))

```
という処理が有名だと思いますが(mapで1要素ずつ取り出してそれに対して処理をしていく）、結果

```python
time data '10m30s' does not match format '%Hh%Mm%Ss'
```
と表示されてしまいます。1時間に到達していないデータは同様な結果になります。

## まずは与データの仕様に合わせる
まず、与データの仕様に合わせるため、
**データに対して処理を変えなければいけません**

ここで、.map属性には関数が適用できることを思い出します。
例えば、

```python
test_data['test_converted'] = test_data['test'].map(lambda x:func(x))
```
という風に、**関数func()を適用させることができます！**

実際に合わせてみましょう。

適用させる関数をtime_convert()とかにしてみます
ここでは**与えられる文字列にhが含まれているかを.findで探して**あげます。
**見つからなかった場合は、-1が返ります。**
そして存在する場合と存在しない場合で.strptimeの**処理を分けて**あげます。
(ここの処理はデータの型とかによって変えてみてください！128m20sだったら'm'で切ったほうが良いし...）

```python
def time_convert(x):
    if x.find('h') == -1:
        # ?m?s の場合
        result = datetime.datetime.strptime(x, "%Mm%Ss")
    else:
        # ?h?m?sの場合
        result = datetime.datetime.strptime(x, "%Hh%Mm%Ss")
    return result

# 処理部分
test_data['test_converted'] = test_data['test'].map(lambda x:time_convert(x))
```
わーいやったーできましたーーとなりましたが、新しく問題が発生します。

```ruby
time data '25h23m10s' does not match format '%Hh%Mm%Ss'
```
そうです、
%Hといったもの書式コードとかディレクティブとか言うらしいのですが、
**%Hは00, 01, ..., 23にしか対応していません。**

( https://docs.python.org/ja/3/library/datetime.html#strftime-and-strptime-format-codes から参照)

これが次に直面した問題で、
与データの仕様であり仕方がないので、変換する方法を探します。

## 24時間を超えるデータについて
25hというものを、1day 1h というdatetime型に変換してあげたいと思います

そのためには、まず**時間部分とそれ以外の部分**で分割してあげます。
これには.split属性を使います。'h'で分割させてあげることにします。
hoursは時間、restは残りの部分とします

```python
hours, rest = x.split('h')
print(hours, rest)
```

```
25 23m10s
```
と出力されました。この何で分割するなどはデータで変えてあげてください:expressionless:

次に行う処理は、25時間を1日1時間に分割させてあげる処理なのですが、
ここではdatetime.timedelta()を使います。
これを使うと、1時間が3600秒に変換され、残りの変換されたdatetime型と足し合わせることができます。
**datetime型は1日を超える時間加算が行われると自動的に1日進んだりしてくれます**。
( https://docs.python.org/ja/3/library/datetime.html#timedelta-objects )

実際に処理してみます。先ほどの関数をちょっと変えます。

```python
def time_convert(x):
    if x.find('h') == -1:
        # ?m?s の場合
        result = datetime.datetime.strptime(x, "%Mm%Ss")
        print(type(result))
    else:
        # ?h?m?sの場合
        hours, rest = x.split('h')
        print(hours, rest)
        # 以前のやつ
        # result = datetime.datetime.strptime(x, "%Hh%Mm%Ss")
        # 新しいバージョン
        # 時間のみの変換 + それ以外の変換
        result =  datetime.timedelta(hours=int(hours)) + \
                  datetime.datetime.strptime(rest, "%Mm%Ss")
    return result

# 処理部分
test_data['test_converted'] = test_data['test'].map(lambda x: \
                                                    time_convert(x))

```
( **\ を入れるだけでコード内改行が出来る**らしいです）

おっしゃー出来た！！ってなったらまた問題が出ます

```
	test	　　　 test_converted
0	1h2m3s        1900-01-01 01:02:03
1	10m30s	      1900-01-01 00:10:30
2	25h23m10s     1900-01-02 01:23:10
```
いや出来てるけど、1900っていつやねん！？てなります。
**変換の仕様上こうなる**のですが、私が得たい結果ではないので変換します。
(詳しくは検索を～）

## 見たい情報に合わせる(ここからは蛇足)
ここからは蛇足で、やらなくてもいいかも！

この処理は簡単で、**timedelta()は年や月、日も指定できる**ので、
逆に**減算**してあげます。つまり、

```python
result -= datetime.datetime(year=1900,month=1,day=1)
```
をします。

ここで、得たい情報は

```
1 days なんたら：なんたら：なんたら
```
ですので、またまた関数の中身を変更させてあげます。すると、

```python
def time_convert(x):
    if x.find('h') == -1:
        # ?m?s の場合
        result = datetime.datetime.strptime(x, "%Mm%Ss")
        print(type(result))
    else:
        # ?h?m?sの場合
        hours, rest = x.split('h')
        print(hours, rest)
        # 以前のやつ
        # result = datetime.datetime.strptime(x, "%Hh%Mm%Ss")
        # 新しいバージョン
        # 時間のみの変換 + それ以外の変換
        result =  datetime.timedelta(hours=int(hours)) + \
                  datetime.datetime.strptime(rest, "%Mm%Ss")
    
    # この部分
    result -= datetime.datetime(year=1900,month=1,day=1)
    return result

# 処理部分
test_data['test_converted'] = test_data['test'].map(lambda x: \
                                                    time_convert(x))

```
となります、結果は

```
	test	    test_converted
0	1h2m3s	    0 days 01:02:03
1	10m30s	    0 days 00:10:30
2	25h23m10s	1 days 01:23:10
```
と出来ました！！やったー！

# 終わりに
同じような悩みを持っていた方がこの記事を見て解決したーー！！ってなってくれたら嬉しいです！

datetime型にすることでプロットを簡単にすることが出来るのでいいですよね～
ここで、補足として、時間をx軸に表示させたいな～って時の方法を下に書きます

```python
import matplotlib.pyplot as plt
plt.figure(figsize=(16,8))
# ラベルとして表示させたいので文字列に変換～
time_index = [str(a) for a in test_data['test_converted'].tolist()]
# plt.plotのx軸とxticksのticks部分は一致させないといけないので、秒数に変換したりする
# 変換させないと '<' not supported between instances of 'numpy.float64' and 'Timedelta' ってエラーが出ちゃう...
# ↑ これもよく分からない
plt.plot(test_data['test_converted'].dt.total_seconds(), test_data.index, color = 'blue')
plt.xticks(ticks = test_data['test_converted'].dt.total_seconds(),
           labels= time_index,
           fontsize = 20,
           rotation = 45)
plt.yticks(fontsize = 20)
```

とすることで、 

![tekito.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/603708/fac70795-8b5b-4f53-742a-980970258c65.jpeg)


と変化が見れました！！！ダミーデータなのでの遷移意味わからんけどね！！
（ここも**綺麗に表示できる方法があれば教えてください**...ここのリファレンス読んで～とか）
日付云々ほんとに苦手過ぎて何回検索して何回沼にハマってるんだっていう感じになってる...



記事を見てくださり、ありがとうございました～！