【要注意】PandasでExcelワークシート内の日付データを読み込む時の落とし穴

先日とある書籍でデータ前処理技術について学んでいた時に奇妙な現象に遭遇したので検証した内容を記事にまとめておこうと思います。

# 実際に遭遇した現象
権利の関係上書籍に付いていたファイルをそのまま掲載出来ない為、別途説明用に作成したファイルを使って説明したいと思います。以下のファイルは2020年4月の日付を様々な表記方法で表記した一覧のExcelファイル(xlsxファイル)です。

![202004.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/469079/ed81f241-807e-bab0-66b0-1c869d703678.png)

2020年4月1日〜2020年4月30日までの日付が表記方法に従った書式で記載されています。データ分析の現場では綺麗な状態のデータが入手出来ることは稀で、表記方法がバラバラで有ることはしばしば起こり得ます[^format]。このデータをPandasで読み込ませてみます。PandasはPythonベースのライブラリとしてデータ分析の世界ではしばしば用いられます。Pandasには`read_excel`関数という専用の関数が有る為、簡単にExcelファイルを読み込ませることが出来ます。読み込ませて表示させると下図の様になりました。

![jupyter.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/469079/6369773d-8ffa-703e-4345-f4796d915b8e.png)

/区切り、-区切り、年月日表示のデータはタイムスタンプ形式で読み込まれました。残りの数値化、文字列化されているデータについてタイムスタンプに変換してみます。数値化、文字列化されているデータについては実際はシリアル値という数値となっています。Pandasでは読み込んだ時点で文字列になってしまっているので、数値に変換してその個数を算出してみます。

![flag_is_serial.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/469079/f3981317-6ccb-ed02-2b33-b28ce536b761.png)

合計12と出力されました。上の図における数値化、文字列化したデータ数と一致します。それではこのデータを日付に変換してみましょう。下図の様にシリアル値を日付に変換します。シリアル値を日付データにする場合、シリアル値は通常コンピュータの日付の開始日(1970/01/01)[^unixtime]から何日経った日付かという数値である為、以下の様に変換します。

<img width="1087" alt="fromSerial.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/469079/d2148460-067b-39b6-b185-ee9ef5983415.png">

無事変換が終わったとホッとしたのも束の間。。。奇妙な結果になっています。最初のデータは2020-04-03の筈が、変換結果では2090-04-05となっています。70年と2日ズレてしまっています。他のデータも想定していた日付＋70年2日の結果になっています。最初は何故だろうと首を傾げました。調べてみた所、以下の2つの事が分かりました。以下の2点をPandasが吸収出来ず2日ズレる結果を生んでしまっているとのことです。

1. Excelではシリアル値の基準日が1900/01/00を基準[^serial]にしている。(つまり1900/01/01がシリアル値1となる)
2. Excelにはバグが有り、閏年では無い1900年が閏年(つまり2月29日が存在する)として扱われている。

* 参考: https://teratail.com/questions/186070

本記事では折角なのでこれらのことを1つずつ検証したいと思います。

# 検証1: シリアル値の基準値について
まず、Pandasのシリアル値の0点がUNIX時間の0点(1970/1/1)であることを確認します。結果`0.0`と出力されました。因みにExcelのシステム時刻の基準となっているとされる`1900/1/1`は`-2208988800.0`と出力されました。

<img width="559" alt="unixandexcel.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/469079/a4935e21-7346-2973-e838-30bbab583ee0.png">

続いてExcelで`1970/1/1`と`1990/1/1`のシリアル値を算出してみます。

![excelserial.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/469079/5f952f0f-9362-03b5-b96a-a48f2173f90c.png)

1900/1/1が1となり、1970/1/1(UNIX時間)が25569となっています。つまり19世紀最後の年がExcelで言うところのシステム時刻の開始日ということになります。よって先述した2つの事実のうち1つ目は正しいということが分かりました。

# 検証2: Excelでは1900年が閏年扱いとなっている
そもそも閏年は4年に1回やって来る年だから4で割り切れる(1900も4で割り切れます)年だと思われがちですが、厳密に言うとそれは間違っています。本来の閏年の条件は<font color="RED">400で割り切れる年または、100では割り切れずかつ4で割り切れる年を閏年とする</font>[^leapyear]です。

```python:閏年判定プログラム
def judge_leap_year(y):
    if y % 400 == 0 | (y % 100 != 0 & y % 4 == 0):
        print('{} is leap year'.format(y))
    else:
        print('{} is not leap year'.format(y))
```

条件から自明な様な気もしますが、念の為にプログラムで判定させてみると1900年は閏年ではないことが分かります。

<img width="281" alt="leapjudge.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/469079/45cf0ec3-4f6f-ae44-9e9b-fcf28582009c.png">

## 検証方法: シリアル値から検証
Excelの関数には日付を入力すると閏年か判定してくれるという様な便利な物は有りません。。。そこでシリアル値を求めることによって検証したいと思います。まずPandasで`1900/2/29`のシリアル値を算出してみます。案の上エラー出力となりました。`day is out of range for month`と有る様に想定されている範囲内に存在しない日付を与えてしまっている旨が表示されています。

<img width="1132" alt="pandaserror.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/469079/447b151f-7c26-3170-8e9f-3f3cd33ce765.png">

続けてExcelでシリアル値を算出してみます。

![excelserial2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/469079/b934508e-3fa8-746f-8e84-a4693afd55d1.png)

何と!?60と出力されました。よって上述の通り<font color='RED'>Excelでは1900年を閏年として扱っていることが証明出来ました。</font>暦上は存在しない日付を存在するものとして扱っていることが分かります。

#### 2020/5/13追記:(補足) 1900/2/28と1900/3/1のシリアル値
記事を読んだ方から1900/2/28と1900/3/1のシリアル値のどちらかと重複[^double]しているのではと思ったけど、検証したら違ってたというお話を頂きましたので筆者の方で検証した内容を追記しておきます。因みに先述の1899/12/31のシリアル値が算出出来ないという検証結果も合わせて掲載しています。

![serialdata.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/469079/a3a7308b-bafe-620d-3a6b-994706f26759.png)


# まとめを兼ねて今後の対策
データ分析の現場ではExcelファイル（xlsxファイル)を扱う場合が有ります。その時に日付データの表示がおかしいと思ったら以下のことを先ず思い出します。

1. Excelでは1900/1/0をシステム時刻の開始としており1900/1/1をシリアル値1としている。
2. Excelでは1900年を閏年として扱っている。これは明らかな<font color='RED'>Excelのバグ[^excelbug]</font>です。

そして実際に日付を処理する時はシリアル値を数値変換した値から2を引きます。上述の変換処理コードは以下の様になります。

```python
pd.to_timedelta(data.loc[flag_is_serial, '日付'].astype('float') - 2, unit='D') + pd.to_datetime('1900/1/1')
```

実行結果を下図に示します。正しい日付に変換されました。

<img width="972" alt="exelmodify.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/469079/de5f7c99-c3df-8272-98e6-736504f6d674.png">

今後Excelのバグが解消されることを願うばかりです。合わせてシステム時刻もUNIX時刻に合わせて貰えると大変嬉しいと思うのは筆者だけでは無いと思います。

# 2020/4/13追記: Excelの1900年閏年扱い問題について
記事を読んだ友人が追加情報を送ってくれました。Microsoftの公式情報です。

* [年がうるう年かどうかを判断する方法](https://docs.microsoft.com/ja-jp/office/troubleshoot/excel/determine-a-leap-year)

このバグは昔のコンピュータの一部のアプリケーションでは閏年を4で割り切れる年(つまり4年に1回)としていたことを引き摺っていることが原因の様です。なかなか根深い問題です。

[^format]: 今回用意した例は説明用のデータとして用意したため、現実にここまで表記が乱れた酷いデータが存在するかと問われると答えに窮してしまいます。

[^unixtime]: [UNIX時間 Wikipedia](https://ja.wikipedia.org/wiki/UNIX%E6%99%82%E9%96%93), [日付⇒UNIX時間変換](https://keisan.casio.jp/exec/system/1526003938)

[^serial]: 1899/12/31を基準(シリアル値0)にしているのではと思うかもしれませんが、Excel上で1899/12/31のシリアル値を算出することは出来ません。矢張り1900/1/0をシリアル値0としている様です。

[^leapyear]: [閏年 Wikipedia](https://ja.wikipedia.org/wiki/%E9%96%8F%E5%B9%B4)

[^double]: そもそも暦に存在しない日付なのでどちらかとシリアル値が等しいのではないかという意味

[^excelbug]: Excel97より前のバージョンでは。。。と後述のMS公式情報に有ることからこれはバグと言うよりExcelの長きに渡る歴史の力によって仕様となってしまっていると言った方が良いかもしれないですね。