日付と時間

このページではPythonでの時間と日付の扱いについてまとめておく(ここはまだ書きかけ)。

コンピュータの時計

コンピュータの内部には独立した時計の機能がハードウェアとして組み込まれており、そこから日時を読み出したり、設定したりすることができるようになっている場合がほとんどである。こうしたコンピュータ内の時計は、一般的な時計と同様、水晶発振器など正確な時間間隔を刻む素子によって駆動されてはいるものの、単体では少しずつずれてしまう。

現在では、多くのコンピュータがネットワークに接続されているため、正確な時計を内蔵したサーバーに問い合わせることで、自らの時計を自動修正するようになっている場合が多い。 そのための標準的な手順がNTP(Network Time Protocol)である。 Windows 10では、Windowsの設定 → 時刻と言語 → 日付と時刻 を開き、「時刻を自動的に設定する」によって、NTPサーバーとの同期が行われる。 macOSの場合は、システム環境設定 → 日付と時刻 の「日付と時刻を自動的に設定」の項目がそれに相当する。 NTPサーバーは世界中に多数存在しており、OSの種類や地域毎に推奨されている設定に従う(通常は、適切なデフォルト値が設定されている)。

時刻の表現

時刻の自然な表現方法は、起点となる日時を設け、そこからの長さ(秒数)を数える方法である。 実際に多くのソフトウェア では、1970日1月1日午前0時を起点として、そこからの経過秒数によって時間を表現している(UNIX時間)。 ところが、実際の暦ではときおり閏秒が発生しているため、実際の経過秒数から日時を計算すると、暦とずれてしまう。 そこで、UNIX時間から日時を算出する場合には、その間のうるう秒を考慮して、暦と整合するよう計算するような仕組みが組み込まれているのが通常である。

従来型のハードウェアで多く用いられていた(符号付き)32ビット整数で秒数を表すと、2038年で、その表現できる最大値を超えてしまうため、旧型の情報通信機器の誤動作が懸念されている(2038年問題)

Pythonでの日付と時間の扱い

Pythonで日付と時間を扱う際には、標準的な機能である timeモジュールdatetimeモジュールが利用できる。

これらを使った、誕生日を入力すると、現在まで何日が経過したか、およびその時点のUnix時間を表示するコードの例を以下に示す:

# coding:utf-8

from datetime import datetime

birthday = input("input your birthday (yyyy/dd/mm):")

bd = datetime.strptime(birthday,'%Y/%m/%d')

now = datetime.now()

delta = now - bd

print("You lived",delta.days,"days since you were born.")

print("Current Unix time is",now.timestamp())

icon-teacher 解説: Pythonは人類滅亡をカウントダウンできるか

Pythonのdatetimeモジュールで表現できる西暦の最大値は datetime.MAXYEARで得られる:

import datetime
print(datetime.MAXYEAR)

実際にPython 3で実行すると 9999 と表示される。

また、Unix時間の秒数を符号付きの64ビット整数で表現したとすると、最大で 263=9223372036854775808 秒、 それを年数に換算すると、約2.9×1011 年で、これが現代のコンピュータで(秒精度で)計時できる最大値と言える。

人類滅亡までのカウントダウンプログラムを作成するとして、Pythonの処理系や現在のコンピュータはそれに必要な時間を扱うことができるのだろうか。そのためには、人類滅亡までの時間を見積もる必要がある。

例えば、核戦争による人類の危機を象徴的に表現した世界終末時計は、警鐘を鳴らすことが主な目的なので、その針は進んだり戻ったりしており、我々の残り時間の予測には使えそうもない。

ここから先は、定説というわけでは無いので、話半分で読んでください。

そこで、色々な事物の余命を推定したGottによる議論を少し焼き直して考察してみよう。

種の絶滅が何らかのランダムなイベント(パンデミックや環境の急激な変動等)で生じるとすれば、イベント間の時間間隔は(第0近似としては)指数分布に従うと考えるのが自然である(不安定原子核の崩壊のようなもの、と、考える)。 人類に対してそういったイベントがどれくらいの頻度で生じるかは不明であるが、その平均間隔をL万年と置き、 種の誕生からT万年目にイベントが実際に生じて絶滅する確率を (1)P(T)=1Lexp(T/L) と書いてみる。

人類に近いネアンデルタール人は40万年程度の期間で絶滅したらしい。一方、チンパンジーは約500万年前に、系統樹の上で、枝分かれしたと言われ、現在まで生存している。とすれば、上記のLは、少なくとも100万年から1000万年程度かそれ以上と考えて良さそうである。

次に、人類誕生から、我々はt万年目に差し掛かっているとする。 我々がこの瞬間に生を受けたのは全く偶然と考えるほか無さそうであるから、 種の寿命Tを知っているとき、その確率は (2)P(t|T)=1T と書ける(ただし、t>T に対しては P(t|T)=0)。

他方で、 (3)P(t)=tP(t|T)P(T)dT=1LEi1(tL) である。 ここで、積分で定義される関数を Ei1(x)=1t1etxdt と置いた。

(1), (2), (3)式を使うと、ベイズの定理から (4)P(T|t)=P(t|T)P(T)P(t)=1Texp(TL)/Ei1(tL) を得る。

(4)式より、人類が t(万年)だけ生存していたことを知った上で、そのN倍のNt 万年までに絶滅する確率は (5)T=tT=NtP(T|t)tNtP(T|t)dT と表現できる。

言うまでもなく、ここでの推定は、Lの値に大きく依存する。

ここで、仮に、L=100万年として、 人類の誕生からの経過時間 t=20万年を入れ (6)20N×20P(T|20)dT=0.95 を数値的に解くと、 N9.2 が得られる。

つまり、上記の見積もりが正しければ、95%の確率でこれまでの9倍程度の期間内に、すなわち 20×9.2=180 万年くらい先までには人類は絶滅するであろう、という結果が得られた。

SciPyの機能を使って、(6)式の根を数値的に求めるコードの例。

# coding: utf-8

import math
import numpy as np
from scipy import optimize,integrate

def Li1(x):
    val,err = integrate.quad(lambda t:1/t*math.exp(-t*x),1,np.inf)
    return val

def G(x):
    L=100
    t=20
    val,err = integrate.quad(lambda T:1/T*math.exp(-T/L), t,t*x)
    return val/Li1(t/L) - 0.95

roots = optimize.fsolve(G,1)
print("Roots=",roots)

以上をまとめると、Python 3のdatetimeモジュールで表現できる年数(9999)は、未来の我々にとっては不十分と思われる。 一方、64ビットの整数で表現したUnix時間を使えば、人類滅亡までのカウントダウン(アップ?)は十分に可能と予想できる。

別の議論の例

人類の存続期間Tがあらかじめ定められており、我々はその期間の中のどこかの時点で等しい確率で生を受けたとする。 T全体を、最初の5%の区間と、残りの95%の区間に分けると、我々は95%の確率で「後半」の区間に生存していることになる。 それが95%の区間の最初の時点、すなわち t=T×0.05(万年目)とすれば、人類はこれからさらにt×0.950.05万年生存できることになる。 他方で、95%の区間の最後に位置する場合は、明日にでも人類は滅亡することになる。

ここで、tを人類誕生からの年数である20万年と置くと、 (7)20×0.950.05=20×19=380 万年が、95%の確率で人類の(最大限の)持ち時間ということになる。

icon-teacher 解説: 原子の寿命と無記憶性

時刻t=0n0個の不安定な原子核があったとすると、 崩壊によって別の核種に変化することで、その数は指数関数的に減少することが知られている。 例えば、核実験や原発で生成される放射性のセシウム137は、半減期が約30年である。 そうすると、ここにセシウム137の原子がn0個あったとすると、それがt年後には (8)n(t)=n0(12)t30=n0elog230t に減少することになる。

半減期をThとして、τ=Th/log2と置くと、 それぞれの原子核が時間あたりに崩壊する確率は(経過年数には依らず)常に 1/τ になる。 すなわち、tからt+Δtの間に、n(t)個の原子核のうちのδ/τだけが崩壊して個数を減らすとすれば (9)n(t+Δt)=n(t)Δtτn(t) となる。Δt0で、これは微分方程式 (10)dn(t)dt=n(t)τ に帰着でき、その解が (8)式に他ならない。

よく考えると、これは少なからず不思議な気がする。 というのは、原発などで生成されたばかりの「真新しい」放射性の原子と、崩壊しないまま長年残存していた原子核との間に全く違いはなく、 「長生き」した原子核のほうが崩壊しやすい、といった傾向は一切ない、と言っているからである。 言い換えれば、原子は自分の過去を全く覚えていない、ということになる(無記憶性)。

この様子をもう少し詳しく考えてみよう。

時間の原点をどこかに設け、原子の崩壊のようなイベント起こる時刻をTとする。 イベントがt以後に発生する確率を P(T>t)と書くことにする。

そして、t以降にイベントが発生したことを知った上で、tから起算してs(0)以降にイベントが発生する条件付き確率 P(T>s+t|T>t) を考えると、条件付き確率の定義から、 (11)P(T>s+t|T>t)=P(T>s+t,T>t)P(T>t) となる。 ここで同時確率 P(T>s+t,T>t) の意味するところ考えると、t以降にイベントが発生し、かつ、t+s以降にイベントが発生する確率は (12)P(T>s+t,T>t)=P(T>s+t) であることは明らかである。よって (11)式は (13)P(T>s+t|T>t)=P(T>s+t)P(T>t) となる。

イベントが発生する確率密度が指数分布の場合、すなわち (14)p(t)dt=τetτdt の場合、累積確率分布は (15)P(T>t)=tp(t)dt=etτ であるから、これを (13)式に代入すると (16)P(T>s+t|T>t)=exp(s+tτ)exp(tτ)=esτ=P(T>s) となる。

すなわち、指数分布に従ってランダムに生じるイベントでは、条件付き確率はtに関係せず、「これから先」のことは「これまで」と全く無関係に起こる。

逆に、無記憶性の条件 P(T>s+t|T>t)=P(T>s+t)P(T>t)=P(T>s) から出発して、f(t)=P(T>t)と表記し直すと、 関数方程式 f(s+t)=f(t)f(s) が得られるが、f(t)の連続性を仮定すると、その解は指数関数 f(t)=aexp(bt) となる。

icon-pc 練習:バスの待ち時間

あるバス停では、バスが到着するまでの時間間隔が指数分布に従っていることが分かっている(平均間隔は10分とする)。 あなたがバス停に来た時点ですでに人が並んでいたので、列の先頭の人に尋ねると、「5分間そこで待っている」と教えてもらった。

あなたはそのバス停で平均あと何分待たなければならないか、考えてみなさい。