Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Table of Contents

import pandas as pd


def print_ht(df):
    print(df.head())
    print(df.tail())

Time series

  • Anything that is observed or measured at many points in time

    • fixed frequency
    • irregular
  • One can refer to time series in terms of

    1. timestamps: specific instant in times
    2. intervals of time: indicated by start / end timestamps
    3. fixed periods: e.g., months of 2010, years
      • This is a special case of “interval of time”
    4. delta times with respect to a beginning of time

Date and time data types and tools

  • date
  • time
  • datetime
  • timedelta
from datetime import datetime, timedelta

now = datetime.now()
print("now=", now)
print(now.year, now.month, now.day)
now= 2018-05-13 10:54:40.633243
2018 5 13
delta = datetime(2011, 1, 7) - datetime(2008, 6, 25, 8, 15)
print("delta=", delta)
print(delta.days, delta.seconds)
delta= 925 days, 15:45:00
925 56700
start = datetime(2011, 1, 7)
print(start + timedelta(12))
2011-01-19 00:00:00

Converting between string and datetime

ts = datetime(2011, 1, 3)

print("str=", str(ts))
# Format using ISO C89.
# strftime = str Format time
print("strftime=", ts.strftime("%Y-%m-%d"))
str= 2011-01-03 00:00:00
strftime= 2011-01-03
# strptime = str Parse time
# It is useful to parse a date in a known format.
value = "2011-01-03"
datetime.strptime(value, "%Y-%m-%d")
datetime.datetime(2011, 1, 3, 0, 0)
# To parse data in unknown format.
import dateutil

print(dateutil.parser.parse("2011-01-03"))
print(dateutil.parser.parse("Jan 31, 1997 10:45 PM"))

# Using pandas.
print(pd.to_datetime("2011-07-06 12:00:00"))
2011-01-03 00:00:00
1997-01-31 22:45:00
2011-07-06 12:00:00
# pd.to_datetime works also with array of strings.
datestrs = ["2011-07-06 12:00:00", "2011-08-06 00:00:00"]
print(pd.to_datetime(datestrs))
DatetimeIndex(['2011-07-06 12:00:00', '2011-08-06 00:00:00'], dtype='datetime64[ns]', freq=None)
# None gets converted into a Not-A-Time.
print(pd.to_datetime(datestrs + [None]))
DatetimeIndex(['2011-07-06 12:00:00', '2011-08-06 00:00:00', 'NaT'], dtype='datetime64[ns]', freq=None)

Time series basics

dates = [
    datetime(2011, 1, 2),
    datetime(2011, 1, 5),
    datetime(2011, 1, 7),
    datetime(2011, 1, 8),
    datetime(2011, 1, 10),
    datetime(2011, 1, 12),
]

ts = pd.Series(np.random.randn(6), index=dates)

ts
2011-01-02 -0.195830 2011-01-05 -0.813747 2011-01-07 0.967337 2011-01-08 -0.868878 2011-01-10 2.101715 2011-01-12 0.558378 dtype: float64
ts.index
DatetimeIndex(['2011-01-02', '2011-01-05', '2011-01-07', '2011-01-08', '2011-01-10', '2011-01-12'], dtype='datetime64[ns]', freq=None)
# Arithmetic operations between differently indexed time series
# automatically align on dates.

ts + ts[::2]
2011-01-02 -0.391659 2011-01-05 NaN 2011-01-07 1.934673 2011-01-08 NaN 2011-01-10 4.203429 2011-01-12 NaN dtype: float64
ts[::2]
2011-01-02 -0.195830 2011-01-07 0.967337 2011-01-10 2.101715 dtype: float64
# Timestamps are stored using np datetime64 at ns resolution.
print("ts.index.dtype=", ts.index.dtype)

# Timestamp are derived from datetime objects.
timestamp = ts.index[0]
print(type(timestamp))
print(timestamp)
ts.index.dtype= datetime64[ns]
<class 'pandas._libs.tslib.Timestamp'>
2011-01-02 00:00:00

Indexing, selection, subsetting

# Time series behaves as any pd.Series.

print("ts=\n", ts)

stamp = ts.index[2]
print("\nstamp=", stamp)
ts=
2011-01-02   -0.195830
2011-01-05   -0.813747
2011-01-07    0.967337
2011-01-08   -0.868878
2011-01-10    2.101715
2011-01-12    0.558378
dtype: float64

stamp= 2011-01-07 00:00:00
# You can pass a string that will be interpreted as a date.

print(ts["1/10/2011"])

print(ts["20110110"])
2.10171465932
2.10171465932
longer_ts = pd.Series(
    np.random.randn(1000), index=pd.date_range("1/1/2000", periods=1000)
)

print_ht(longer_ts)
2000-01-01    0.953442
2000-01-02    2.228868
2000-01-03   -1.962444
2000-01-04   -0.484121
2000-01-05   -0.616139
Freq: D, dtype: float64
2002-09-22    0.499749
2002-09-23   -0.365132
2002-09-24   -2.176054
2002-09-25   -0.294013
2002-09-26   -1.913276
Freq: D, dtype: float64
# Filter by year.
print_ht(longer_ts["2001"])
2001-01-01   -0.026238
2001-01-02    0.174582
2001-01-03   -0.522434
2001-01-04    0.246643
2001-01-05    0.622896
Freq: D, dtype: float64
2001-12-27    0.538356
2001-12-28    0.995585
2001-12-29    0.536215
2001-12-30   -0.026049
2001-12-31   -1.058872
Freq: D, dtype: float64
# Filter by month.
print_ht(longer_ts["2001-05"])
2001-05-01   -1.382761
2001-05-02    0.364104
2001-05-03   -0.243860
2001-05-04    1.489501
2001-05-05    0.469970
Freq: D, dtype: float64
2001-05-27   -0.039531
2001-05-28    0.911845
2001-05-29   -1.164597
2001-05-30   -2.296297
2001-05-31    0.173897
Freq: D, dtype: float64
# Slice by datetime.
ts[datetime(2011, 1, 7) :]
2011-01-07 0.967337 2011-01-08 -0.868878 2011-01-10 2.101715 2011-01-12 0.558378 dtype: float64
# One can use date, datetime, or timestamp to slice.
# Note that slicing creates a np view on the object, and there
# is no data copied.
ts["1/6/2011":"1/11/2011"]
2011-01-07 0.967337 2011-01-08 -0.868878 2011-01-10 2.101715 dtype: float64
ts.truncate(after="1/9/2011")
2011-01-02 -0.195830 2011-01-05 -0.813747 2011-01-07 0.967337 2011-01-08 -0.868878 dtype: float64
# Everything works also for DataFrame indexed on datetimes.

dates = pd.date_range("1/1/2000", periods=100, freq="W-WED")

long_df = pd.DataFrame(
    np.random.randn(100, 4),
    index=dates,
    columns="Colorado Texas NY Ohio".split(),
)

print(long_df.index.is_unique)

print_ht(long_df)
True
            Colorado     Texas        NY      Ohio
2000-01-05  0.228642  0.203085 -0.322465 -1.242223
2000-01-12  0.882768 -0.119918  0.345217  2.635342
2000-01-19 -1.286971 -1.705926 -0.597030  0.063292
2000-01-26 -1.317127  0.366290  0.492232  1.966448
2000-02-02 -0.784780  0.629830 -0.646747 -0.466076
            Colorado     Texas        NY      Ohio
2001-10-31  0.039872 -0.905562 -0.188492  0.116938
2001-11-07 -1.212310  0.267614  1.345367 -0.770098
2001-11-14  0.901416 -1.286568 -0.711707 -1.612438
2001-11-21 -0.546983 -0.653516  0.026749 -1.293937
2001-11-28  0.577820 -0.985672 -1.704768  0.277601
long_df.loc["5-2001"]
Loading...

Time series with duplicated indices.

dates = pd.DatetimeIndex(
    ["1/1/2000", "1/2/2000", "1/2/2000", "1/2/2000", "1/3/2000"]
)

dup_ts = pd.Series(np.arange(5), index=dates)

dup_ts
2000-01-01 0 2000-01-02 1 2000-01-02 2 2000-01-02 3 2000-01-03 4 dtype: int64
dup_ts.index.is_unique
False
# Indexing can produce scalar values or slices, depending on whether
# a timestamp is duplicated.

# Not dup.
print(dup_ts["1/3/2000"])

# Dup.
print(dup_ts["1/2/2000"])
4
2000-01-02    1
2000-01-02    2
2000-01-02    3
dtype: int64
# To aggregate the data, you can use groupby using level=0.
grouped = dup_ts.groupby(level=0)

# Count elements per date.
print(grouped.count())

# Compute mean.
print(grouped.mean())
2000-01-01    1
2000-01-02    3
2000-01-03    1
dtype: int64
2000-01-01    0
2000-01-02    2
2000-01-03    4
dtype: int64

Date ranges, freqs, shifting

# Time series in Pandas are assumed to be "irregular", i.e., having
# no fixed frequency.
# Often we want to work with time series with fixed frequency.
# Pandas has functions for:
# - resampling
# - inferring frequencies
# - generating fixed-frequency ranges
dates = [
    datetime(2011, 1, 2),
    datetime(2011, 1, 5),
    datetime(2011, 1, 7),
    datetime(2011, 1, 8),
    datetime(2011, 1, 10),
    datetime(2011, 1, 12),
]
ts = pd.Series(np.random.randn(6), index=dates)
ts
2011-01-02 -1.420990 2011-01-05 0.674290 2011-01-07 0.712650 2011-01-08 -0.635353 2011-01-10 0.732992 2011-01-12 0.904861 dtype: float64
ts.resample("D")
DatetimeIndexResampler [freq=<Day>, axis=0, closed=left, label=left, convention=start, base=0]

Generating date ranges

# Sample daily (by default) a (closed) interval of dates.
pd.date_range("2012-04-01", "2012-06-01")
DatetimeIndex(['2012-04-01', '2012-04-02', '2012-04-03', '2012-04-04', '2012-04-05', '2012-04-06', '2012-04-07', '2012-04-08', '2012-04-09', '2012-04-10', '2012-04-11', '2012-04-12', '2012-04-13', '2012-04-14', '2012-04-15', '2012-04-16', '2012-04-17', '2012-04-18', '2012-04-19', '2012-04-20', '2012-04-21', '2012-04-22', '2012-04-23', '2012-04-24', '2012-04-25', '2012-04-26', '2012-04-27', '2012-04-28', '2012-04-29', '2012-04-30', '2012-05-01', '2012-05-02', '2012-05-03', '2012-05-04', '2012-05-05', '2012-05-06', '2012-05-07', '2012-05-08', '2012-05-09', '2012-05-10', '2012-05-11', '2012-05-12', '2012-05-13', '2012-05-14', '2012-05-15', '2012-05-16', '2012-05-17', '2012-05-18', '2012-05-19', '2012-05-20', '2012-05-21', '2012-05-22', '2012-05-23', '2012-05-24', '2012-05-25', '2012-05-26', '2012-05-27', '2012-05-28', '2012-05-29', '2012-05-30', '2012-05-31', '2012-06-01'], dtype='datetime64[ns]', freq='D')
# If only one point of the interval is specified, periods is
# required
print(pd.date_range(start="2012-04-01", periods=5))
print(pd.date_range(end="2012-06-01", periods=5))
DatetimeIndex(['2012-04-01', '2012-04-02', '2012-04-03', '2012-04-04',
               '2012-04-05'],
              dtype='datetime64[ns]', freq='D')
DatetimeIndex(['2012-05-28', '2012-05-29', '2012-05-30', '2012-05-31',
               '2012-06-01'],
              dtype='datetime64[ns]', freq='D')
pd.date_range("2000-01-01", "2000-06-01", freq="BM")
DatetimeIndex(['2000-01-31', '2000-02-29', '2000-03-31', '2000-04-28', '2000-05-31'], dtype='datetime64[ns]', freq='BM')
# date_range() preserves the time, if present.
pd.date_range("2012-05-02 12:56:31", periods=5)
DatetimeIndex(['2012-05-02 12:56:31', '2012-05-03 12:56:31', '2012-05-04 12:56:31', '2012-05-05 12:56:31', '2012-05-06 12:56:31'], dtype='datetime64[ns]', freq='D')
# normalize=True aligns on midnights.
pd.date_range("2012-05-02 12:56:31", periods=5, normalize=True)
DatetimeIndex(['2012-05-02', '2012-05-03', '2012-05-04', '2012-05-05', '2012-05-06'], dtype='datetime64[ns]', freq='D')

Frequencies and date offsets

  • frequencies are composed of a base frequency and a multiplier
from pandas.tseries.offsets import Hour, Minute

hour = Hour()
print(hour)
<Hour>
four_hours = Hour(4)
print(four_hours)
<4 * Hours>
pd.date_range("2000-01-01", "2000-01-02 23:59", freq="4h")
DatetimeIndex(['2000-01-01 00:00:00', '2000-01-01 04:00:00', '2000-01-01 08:00:00', '2000-01-01 12:00:00', '2000-01-01 16:00:00', '2000-01-01 20:00:00', '2000-01-02 00:00:00', '2000-01-02 04:00:00', '2000-01-02 08:00:00', '2000-01-02 12:00:00', '2000-01-02 16:00:00', '2000-01-02 20:00:00'], dtype='datetime64[ns]', freq='4H')
# Frequencies can be combined.
Hour(2) + Minute(30)
<150 * Minutes>
pd.date_range("2000-01-01", periods=10, freq="1h30min")
DatetimeIndex(['2000-01-01 00:00:00', '2000-01-01 01:30:00', '2000-01-01 03:00:00', '2000-01-01 04:30:00', '2000-01-01 06:00:00', '2000-01-01 07:30:00', '2000-01-01 09:00:00', '2000-01-01 10:30:00', '2000-01-01 12:00:00', '2000-01-01 13:30:00'], dtype='datetime64[ns]', freq='90T')
# Week of month: e.g., 3rd Friday of each month.
pd.date_range("2012-01-01", "2012-09-01", freq="WOM-3FRI")
DatetimeIndex(['2012-01-20', '2012-02-17', '2012-03-16', '2012-04-20', '2012-05-18', '2012-06-15', '2012-07-20', '2012-08-17'], dtype='datetime64[ns]', freq='WOM-3FRI')

Shifting data

  • shift is about moving data back and forward through time
ts = pd.Series(
    np.random.randn(4), index=pd.date_range("1/1/2000", periods=4, freq="M")
)
ts
2000-01-31 0.137219 2000-02-29 -0.441725 2000-03-31 -0.025440 2000-04-30 -0.647758 Freq: M, dtype: float64
# Shift backward 2 lags (delay).
# Note that no new timestamps is inferred, so data is lost.
ts.shift(2)
2000-01-31 NaN 2000-02-29 NaN 2000-03-31 0.137219 2000-04-30 -0.441725 Freq: M, dtype: float64
# Shift forward 2 lags.
ts.shift(-2)
2000-01-31 -0.025440 2000-02-29 -0.647758 2000-03-31 NaN 2000-04-30 NaN Freq: M, dtype: float64
# If frequency of data is known, new timestamps can be inferred.
ts.shift(2, freq="M")
2000-03-31 0.137219 2000-04-30 -0.441725 2000-05-31 -0.025440 2000-06-30 -0.647758 Freq: M, dtype: float64
ts.shift(3, freq="D")
2000-02-03 0.137219 2000-03-03 -0.441725 2000-04-03 -0.025440 2000-05-03 -0.647758 dtype: float64
# Shift back of 4 * 90 mins = 6hrs.
ts.shift(4, freq="90T")
2000-01-31 06:00:00 0.137219 2000-02-29 06:00:00 -0.441725 2000-03-31 06:00:00 -0.025440 2000-04-30 06:00:00 -0.647758 Freq: M, dtype: float64

Shifting dates with offsets

from pandas.tseries.offsets import Day, MonthEnd

now = datetime(2011, 11, 17)

print(now + 3 * Day())
2011-11-20 00:00:00
# Anchored offsets allow to align a period of time.
print(now + MonthEnd())
# This is equivalent to:
print(MonthEnd().rollforward(now))
2011-11-30 00:00:00
2011-11-30 00:00:00
print(now + MonthEnd(2))
2011-12-31 00:00:00
# One can use date offsets with groupby.
ts = pd.Series(
    np.random.randn(20), index=pd.date_range("1/15/2000", periods=20, freq="4d")
)
ts
2000-01-15 0.081999 2000-01-19 0.864714 2000-01-23 0.284556 2000-01-27 0.433515 2000-01-31 1.967312 2000-02-04 -0.461309 2000-02-08 -0.939936 2000-02-12 -0.393808 2000-02-16 -0.057009 2000-02-20 -0.408874 2000-02-24 1.047092 2000-02-28 -1.146174 2000-03-03 2.388882 2000-03-07 0.578028 2000-03-11 -0.825274 2000-03-15 -0.332605 2000-03-19 -2.258880 2000-03-23 1.960624 2000-03-27 -0.228699 2000-03-31 -0.369610 Freq: 4D, dtype: float64
offset = MonthEnd()


def print_group(df):
    print("[%s, %s]" % (df.index[0], df.index[-1]))


# print ts.groupby(offset.rollforward).first()
# print ts.groupby(offset.rollforward).last()
_ = ts.groupby(offset.rollforward).agg(print_group)
[2000-01-15 00:00:00, 2000-01-31 00:00:00]
[2000-02-04 00:00:00, 2000-02-28 00:00:00]
[2000-03-03 00:00:00, 2000-03-31 00:00:00]
# This is equivalent to resample.
ts.resample("M").agg(print_group)
[2000-01-15 00:00:00, 2000-01-31 00:00:00]
[2000-02-04 00:00:00, 2000-02-28 00:00:00]
[2000-03-03 00:00:00, 2000-03-31 00:00:00]

Time zone handling

  • Working with time zones is unpleasant
    • One can work with UTC (Coordinated Universal Time), which is an international standard
    • Time zones are expressed as offsets from UTC
    • E.g., NY is +4 during daylight saving time (DST), +5 otherwise
  • pytz is 3rd party library
    • Olson db
    • DST and UTC offsets have been changed many times
  • pandas wraps pytz
import pytz

pytz.common_timezones[-7:]
['US/Arizona', 'US/Central', 'US/Eastern', 'US/Hawaii', 'US/Mountain', 'US/Pacific', 'UTC']
tz = pytz.timezone("America/New_York")

print(tz)
print(repr(tz))
America/New_York
<DstTzInfo 'America/New_York' LMT-1 day, 19:04:00 STD>

Time zone localization and conversion

  • By default time series are time zone naive
rng = pd.date_range("3/9/2012 9:30", periods=6, freq="D")

print(rng)

# Note that tz field is none.
print(rng.tz)
DatetimeIndex(['2012-03-09 09:30:00', '2012-03-10 09:30:00',
               '2012-03-11 09:30:00', '2012-03-12 09:30:00',
               '2012-03-13 09:30:00', '2012-03-14 09:30:00'],
              dtype='datetime64[ns]', freq='D')
None
ts = pd.Series(np.random.randn(len(rng)), index=rng)
print(ts)

# Note that tz field is none.
print("index.tz=", ts.index.tz)
2012-03-09 09:30:00    0.659952
2012-03-10 09:30:00   -0.277512
2012-03-11 09:30:00   -0.584728
2012-03-12 09:30:00   -0.039433
2012-03-13 09:30:00    0.790273
2012-03-14 09:30:00   -0.326480
Freq: D, dtype: float64
index.tz= None
# Generate data range with a UTC time zone.
rng = pd.date_range("3/9/2012 9:30", periods=6, freq="D", tz="UTC")

print(rng)
print(rng.tz)
DatetimeIndex(['2012-03-09 09:30:00+00:00', '2012-03-10 09:30:00+00:00',
               '2012-03-11 09:30:00+00:00', '2012-03-12 09:30:00+00:00',
               '2012-03-13 09:30:00+00:00', '2012-03-14 09:30:00+00:00'],
              dtype='datetime64[ns, UTC]', freq='D')
UTC
# Generate data range with a time zone.
rng = pd.date_range("3/9/2012 9:30", periods=6, freq="D", tz="US/Pacific")

print(rng)
print(rng.tz)
DatetimeIndex(['2012-03-09 09:30:00-08:00', '2012-03-10 09:30:00-08:00',
               '2012-03-11 09:30:00-07:00', '2012-03-12 09:30:00-07:00',
               '2012-03-13 09:30:00-07:00', '2012-03-14 09:30:00-07:00'],
              dtype='datetime64[ns, US/Pacific]', freq='D')
US/Pacific
# Naive times can be converted in "localized".
ts_utc = ts.tz_localize("UTC")
print(ts_utc)
print(ts_utc.index.tz)

# Once they are localized, dates can be converted into other time zones.
ts_edt = ts_utc.tz_convert("America/New_York")
print("\n", ts_edt)
print(ts_edt.index.tz)
2012-03-09 09:30:00+00:00    0.659952
2012-03-10 09:30:00+00:00   -0.277512
2012-03-11 09:30:00+00:00   -0.584728
2012-03-12 09:30:00+00:00   -0.039433
2012-03-13 09:30:00+00:00    0.790273
2012-03-14 09:30:00+00:00   -0.326480
Freq: D, dtype: float64
UTC

2012-03-09 04:30:00-05:00    0.659952
2012-03-10 04:30:00-05:00   -0.277512
2012-03-11 05:30:00-04:00   -0.584728
2012-03-12 05:30:00-04:00   -0.039433
2012-03-13 05:30:00-04:00    0.790273
2012-03-14 05:30:00-04:00   -0.326480
Freq: D, dtype: float64
America/New_York
# Add a ET timezone.
ts_edt = ts.tz_localize("America/New_York")
print(ts_edt)
print(ts_edt.index.tz)
2012-03-09 09:30:00-05:00    0.659952
2012-03-10 09:30:00-05:00   -0.277512
2012-03-11 09:30:00-04:00   -0.584728
2012-03-12 09:30:00-04:00   -0.039433
2012-03-13 09:30:00-04:00    0.790273
2012-03-14 09:30:00-04:00   -0.326480
Freq: D, dtype: float64
America/New_York
# tz_localize and tz_convert are also methods of DateTimeIndex

Operations with time zone-aware timestamp objects

  • Also Timestamp can be handled in terms of time-zones
ts = pd.Timestamp("2011-03-12 04:00")
print("ts=", ts)

ts_utc = ts.tz_localize("UTC")
print("\nts_utc=", ts_utc)

print("ts_convert=", ts_utc.tz_convert("America/New_York"))
ts= 2011-03-12 04:00:00

ts_utc= 2011-03-12 04:00:00+00:00
ts_convert= 2011-03-11 23:00:00-05:00
ts_moscow = pd.Timestamp("2011-03-12 04:00", tz="Europe/Moscow")
print(ts_moscow)
2011-03-12 04:00:00+03:00
# Tz-aware Timestamp objects are stored a nanoseconds since Unix epoch (1970-01-01).
# This is not changed when tz conversions are applied.

print(ts_utc)
print(type(ts_utc))
print(ts_utc.value)
print(ts_utc.tz_convert("America/New_York").value)
2011-03-12 04:00:00+00:00
<class 'pandas._libs.tslib.Timestamp'>
1299902400000000000
1299902400000000000

Operations between different time zones

  • If two time series with different time zones are combined, the result is UTC
rng = pd.date_range("3/7/2012 9:30", periods=6, freq="B")
ts = pd.Series(np.random.randn(len(rng)), index=rng)
print(ts)
2012-03-07 09:30:00    0.023061
2012-03-08 09:30:00    0.285329
2012-03-09 09:30:00   -1.310567
2012-03-12 09:30:00    2.882604
2012-03-13 09:30:00    0.921431
2012-03-14 09:30:00    0.277645
Freq: B, dtype: float64
ts1 = ts.tz_localize("Europe/London")
ts2 = ts1.tz_convert("Europe/Moscow")

print(ts1)
print(ts2)
2012-03-07 09:30:00+00:00    0.023061
2012-03-08 09:30:00+00:00    0.285329
2012-03-09 09:30:00+00:00   -1.310567
2012-03-12 09:30:00+00:00    2.882604
2012-03-13 09:30:00+00:00    0.921431
2012-03-14 09:30:00+00:00    0.277645
Freq: B, dtype: float64
2012-03-07 13:30:00+04:00    0.023061
2012-03-08 13:30:00+04:00    0.285329
2012-03-09 13:30:00+04:00   -1.310567
2012-03-12 13:30:00+04:00    2.882604
2012-03-13 13:30:00+04:00    0.921431
2012-03-14 13:30:00+04:00    0.277645
Freq: B, dtype: float64
print(ts1 + ts2)
2012-03-07 09:30:00+00:00    0.046122
2012-03-08 09:30:00+00:00    0.570658
2012-03-09 09:30:00+00:00   -2.621134
2012-03-12 09:30:00+00:00    5.765209
2012-03-13 09:30:00+00:00    1.842862
2012-03-14 09:30:00+00:00    0.555290
Freq: B, dtype: float64

Periods and period arithmetic

  • Periods represent timespans (e.g., days, months, quarters, years)
# Freq is the same frequencies used for other applications.

# E.g., 'A-DEC' means annual dates anchored to the end last cander of Dec.
p = pd.Period(2007, freq="A-DEC")
p
Period('2007', 'A-DEC')
p + 5
Period('2012', 'A-DEC')
p - 2
Period('2005', 'A-DEC')
# Distance of periods in terms of periods.
p2 = pd.Period("2014", freq="A-DEC")
print("p2=", p2)
print("p =", p)
print("p2-p=", p2 - p)
p2= 2014
p = 2007
p2-p= 7
# Date range with monthly cadence.
rng1 = pd.date_range("2000-01-01", "2000-06-30", freq="M")
print(rng1)
DatetimeIndex(['2000-01-31', '2000-02-29', '2000-03-31', '2000-04-30',
               '2000-05-31', '2000-06-30'],
              dtype='datetime64[ns]', freq='M')
ts1 = pd.Series(np.random.randn(len(rng1)), index=rng1)
print(ts1)

print(type(ts1.index))
print(type(ts1.index[0]))
2000-01-31   -0.241163
2000-02-29   -0.304942
2000-03-31   -0.107074
2000-04-30   -0.572908
2000-05-31   -0.165864
2000-06-30    1.062376
Freq: M, dtype: float64
<class 'pandas.core.indexes.datetimes.DatetimeIndex'>
<class 'pandas._libs.tslib.Timestamp'>
# Period range with monthly cadence.
rng2 = pd.period_range("2000-01-01", "2000-06-30", freq="M")
print(rng2)
PeriodIndex(['2000-01', '2000-02', '2000-03', '2000-04', '2000-05', '2000-06'], dtype='period[M]', freq='M')
# A PeriodIndex stores a sequence of periods
# It can be used as an axis index, like a DatetimeIndex.
ts2 = pd.Series(np.random.randn(len(rng2)), index=rng2)
print(ts2)

print(type(ts2.index))
print(type(ts2.index[0]))
2000-01    0.993906
2000-02   -0.645846
2000-03    1.465864
2000-04    0.334810
2000-05   -0.035259
2000-06    0.064194
Freq: M, dtype: float64
<class 'pandas.core.indexes.period.PeriodIndex'>
<class 'pandas._libs.period.Period'>
values = ["2001Q3", "2002Q2", "2003Q1"]

index = pd.PeriodIndex(values, freq="Q-DEC")
print(index)
PeriodIndex(['2001Q3', '2002Q2', '2003Q1'], dtype='period[Q-DEC]', freq='Q-DEC')

Period frequency conversion

  • One can convert a PeriodIndex to another frequency
# A period for 2007 anchored to end of Dec.
p = pd.Period("2007", freq="A-DEC")
p
Period('2007', 'A-DEC')
p.asfreq("M", how="start")
Period('2007-01', 'M')
p.asfreq("M", how="end")
Period('2007-12', 'M')
# Consider a year ending at the end of June.
p = pd.Period("2007", "A-JUN")
p
Period('2007', 'A-JUN')
p.asfreq("M", how="start")
Period('2006-07', 'M')
p.asfreq("M", how="end")
Period('2007-06', 'M')
# Also PeriodIndex can be converted to

rng = pd.period_range("2006", "2009", freq="A-DEC")
ts = pd.Series(np.random.randn(len(rng)), index=rng)
print(ts)
2006   -1.079111
2007   -1.036980
2008    0.597151
2009    1.686564
Freq: A-DEC, dtype: float64
# Pick the first month of the year period ending in Dec.
ts.asfreq("M", how="start")
2006-01 -1.079111 2007-01 -1.036980 2008-01 0.597151 2009-01 1.686564 Freq: M, dtype: float64
# Use the last business day of the period.
ts.asfreq("B", how="end")
2006-12-29 -1.079111 2007-12-31 -1.036980 2008-12-31 0.597151 2009-12-31 1.686564 Freq: B, dtype: float64

Quarterly period frequencies

  • Quarterly data is standard in accounting and finance
  • Quarterly data is reported relative to fiscal year end (typically the last calendar or business day of one of the 12 months of the year)
# 4th quarter, with fiscal year end in Jan.
p = pd.Period("2012Q4", freq="Q-JAN")
p
Period('2012Q4', 'Q-JAN')
# The 4th quarter is:
p.asfreq("D", "start"), p.asfreq("D", "end")
(Period('2011-11-01', 'D'), Period('2012-01-31', 'D'))
# Get a timestamp at 4pm on second-to-last business day of the quarter.

ts = (p.asfreq("B", "end") - 1).asfreq("T", "s") + 16 * 60
print(ts)
2012-01-30 16:00

Converting timestamps to periods (and back)

  • periods always refer to non-overlapping timestamps
  • a timestamp can only belong to a single period for a given frequency
rng = pd.date_range("1/1/2000", periods=3, freq="M")
rng
DatetimeIndex(['2000-01-31', '2000-02-29', '2000-03-31'], dtype='datetime64[ns]', freq='M')
ts = pd.Series(randn(3), index=rng)
ts
2000-01-31 -0.456340 2000-02-29 -0.400942 2000-03-31 0.585983 Freq: M, dtype: float64
# to_period() infers the frequency automatically
# In practice it uses the same frequency of the timeseries.
pts = ts.to_period()
print(pts)
2000-01   -0.456340
2000-02   -0.400942
2000-03    0.585983
Freq: M, dtype: float64
rng = pd.date_range("1/29/2000", periods=6, freq="D")
ts2 = pd.Series(randn(6), index=rng)

print(ts2)
2000-01-29   -0.785957
2000-01-30    1.272619
2000-01-31    0.180610
2000-02-01    0.700050
2000-02-02   -0.423228
2000-02-03    0.260495
Freq: D, dtype: float64
# Convert each timestamp to the including period.
# Note that the frequency of timestamp and periods are different.
ts2.to_period("M")
2000-01 -0.785957 2000-01 1.272619 2000-01 0.180610 2000-02 0.700050 2000-02 -0.423228 2000-02 0.260495 Freq: M, dtype: float64
pts2 = ts2.to_period()
print(pts2)
2000-01-29   -0.785957
2000-01-30    1.272619
2000-01-31    0.180610
2000-02-01    0.700050
2000-02-02   -0.423228
2000-02-03    0.260495
Freq: D, dtype: float64
pts2.index
PeriodIndex(['2000-01-29', '2000-01-30', '2000-01-31', '2000-02-01', '2000-02-02', '2000-02-03'], dtype='period[D]', freq='D')
# Converting back to timestamps.
pts2 = ts2.to_period("M")
print(pts2)

print(pts2.to_timestamp(how="end"))
2000-01   -0.785957
2000-01    1.272619
2000-01    0.180610
2000-02    0.700050
2000-02   -0.423228
2000-02    0.260495
Freq: M, dtype: float64
2000-01-31   -0.785957
2000-01-31    1.272619
2000-01-31    0.180610
2000-02-29    0.700050
2000-02-29   -0.423228
2000-02-29    0.260495
dtype: float64

Creating a PeriodIndex from arrays

  • If the timespan information are stored in multiple columns, the constructor of PeriodIndex can gather them and use them
!ls /Users/gp/src/github/pydata-book/examples
array_ex.txt               macrodata.csv
csv_mindex.csv             out.csv
ex1.csv                    segismundo.txt
ex1.xlsx                   spx.csv
ex2.csv                    stinkbug.png
ex3.csv                    stock_px.csv
ex3.txt                    stock_px_2.csv
ex4.csv                    test_file.csv
ex5.csv                    tips.csv
ex6.csv                    tseries.csv
ex7.csv                    volume.csv
example.json               yahoo_price.pkl
fdic_failed_bank_list.html yahoo_volume.pkl
ipython_bug.py
data = pd.read_csv("~/src/github/pydata-book/examples/macrodata.csv")
data.head()
Loading...
print(data.head()[["year", "quarter"]])
     year  quarter
0  1959.0      1.0
1  1959.0      2.0
2  1959.0      3.0
3  1959.0      4.0
4  1960.0      1.0
index = pd.PeriodIndex(year=data.year, quarter=data.quarter, freq="Q-DEC")

print(index)
PeriodIndex(['1959Q1', '1959Q2', '1959Q3', '1959Q4', '1960Q1', '1960Q2',
             '1960Q3', '1960Q4', '1961Q1', '1961Q2',
             ...
             '2007Q2', '2007Q3', '2007Q4', '2008Q1', '2008Q2', '2008Q3',
             '2008Q4', '2009Q1', '2009Q2', '2009Q3'],
            dtype='period[Q-DEC]', length=203, freq='Q-DEC')
data.index = index
data.infl.head()
1959Q1 0.00 1959Q2 2.34 1959Q3 2.74 1959Q4 0.27 1960Q1 2.31 Freq: Q-DEC, Name: infl, dtype: float64

Resampling and frequency conversion

  • Resampling = converting a time series from one frequency to another
  • Downsampling = aggregating higher frequency data to lower frequency
  • Upsampling = converting lower frequency data to higher frequency
  • Some operations are not down or upsampling
    • E.g., converting from W-WED (weekly on Wednesday) to W-FRI
rng = pd.date_range("1/1/2000", periods=100, freq="D")

ts = pd.Series(randn(len(rng)), index=rng)
ts.head()
2000-01-01 0.290105 2000-01-02 -0.219713 2000-01-03 0.995451 2000-01-04 1.093139 2000-01-05 1.152817 Freq: D, dtype: float64
# Resample daily data to monthly using mean to aggregate.
# ts.resample('M', how="mean")
ts.resample("M").mean()
2000-01-31 -0.122909 2000-02-29 -0.286706 2000-03-31 0.178215 2000-04-30 0.096529 Freq: M, dtype: float64
ts.resample("M", kind="period").mean()
2000-01 -0.122909 2000-02 -0.286706 2000-03 0.178215 2000-04 0.096529 Freq: M, dtype: float64
  • options for resample()
  • freq: string (e.g., ‘M’, ‘5min’) or DateOffset (e.g., Second(15)) indicating resampled frequency
  • how: function name or function aggregating values
    • e.g., mean, ohlc, np.max, first, last, median
  • fill_method: how to interpolate when upsampling
    • e.g., ffill, bfill
  • limit: when filling the maximum number of periods to fill
  • closed: in downsampling which end of the interval is closed, i.e., inclusive
    • e.g., right, left
  • label: in downsampling how to label the aggregated result with the right or left bin edge
    • e.g., we can sample 5-min interval [9:30, 9:35) and label it 9:30 or 9:35
  • loffset: time adjustment to bin labels to shift aggregate labels

Downsampling

  • Aggregating data to a regular, lower frequency is a normal time series task

  • The data doesn’t need to be fixed

    • The desired frequency defines bin edges used to aggregate
    • Partition
      • Each interval is half-open, since each point can only belong to one interval
      • The union of all intervals must make up the whole time frame
  • When sampling

    • which side of each interval is closed
    • how to label each aggregated bin (beginning or end)
# Generate minute data.
rng = pd.date_range("1/1/2000", periods=12, freq="T")
ts = pd.Series(np.arange(len(rng)), index=rng)

ts
2000-01-01 00:00:00 0 2000-01-01 00:01:00 1 2000-01-01 00:02:00 2 2000-01-01 00:03:00 3 2000-01-01 00:04:00 4 2000-01-01 00:05:00 5 2000-01-01 00:06:00 6 2000-01-01 00:07:00 7 2000-01-01 00:08:00 8 2000-01-01 00:09:00 9 2000-01-01 00:10:00 10 2000-01-01 00:11:00 11 Freq: T, dtype: int64
# Aggregate data into 5-min chunks or bars by taking the sum.
# ts.resample('5min', how="sum")
print(ts.resample("5min", closed="left", label="left").sum())

# This is the default.
print(ts.resample("5min").sum())
2000-01-01 00:00:00    10
2000-01-01 00:05:00    35
2000-01-01 00:10:00    21
Freq: 5T, dtype: int64
2000-01-01 00:00:00    10
2000-01-01 00:05:00    35
2000-01-01 00:10:00    21
Freq: 5T, dtype: int64
ts.resample("5min", closed="left", label="right").sum()
2000-01-01 00:05:00 10 2000-01-01 00:10:00 35 2000-01-01 00:15:00 21 Freq: 5T, dtype: int64
ts.resample("5min", closed="right", label="left").sum()
1999-12-31 23:55:00 0 2000-01-01 00:00:00 15 2000-01-01 00:05:00 40 2000-01-01 00:10:00 11 Freq: 5T, dtype: int64
print(ts.resample("5min", closed="right", label="right").sum())
2000-01-01 00:00:00     0
2000-01-01 00:05:00    15
2000-01-01 00:10:00    40
2000-01-01 00:15:00    11
Freq: 5T, dtype: int64
# If you want to shift the index.
# This is equivalent to calling shift() after resampling
ts.resample("5min", how="sum", loffset="-1s")
/Users/gp/.conda/envs/jupyter/lib/python2.7/site-packages/ipykernel_launcher.py:3: FutureWarning: how in .resample() is deprecated
the new syntax is .resample(...).sum()
  This is separate from the ipykernel package so we can avoid doing imports until
1999-12-31 23:59:59 10 2000-01-01 00:04:59 35 2000-01-01 00:09:59 21 Freq: 5T, dtype: int64
OHLC resampling
ts.resample("5min").ohlc()
Loading...
Resampling with GroupBy
  • One can group by and then compute mean
rng = pd.date_range("1/1/2000", periods=100, freq="D")
ts = pd.Series(np.arange(100), index=rng)
print(ts.head())
2000-01-01    0
2000-01-02    1
2000-01-03    2
2000-01-04    3
2000-01-05    4
Freq: D, dtype: int64
ts.groupby(lambda x: x.month).mean()
1 15 2 45 3 75 4 95 dtype: int64

Upsampling

  • When converting from lower to higher frequency, no aggregation is needed
frame = pd.DataFrame(
    np.random.randn(2, 4),
    index=pd.date_range("1/1/2000", periods=2, freq="W-WED"),
    columns="Colorado Texas NY Ohio".split(),
)
frame[:5]
Loading...
# When resampling to daily frequency, missing values are introduced.

# frame.resample("D", fill_method="ffill")
frame.resample("D").ffill()
Loading...
frame.resample("D").ffill(limit=2)
Loading...

Resampling with periods

frame = pd.DataFrame(
    np.random.randn(24, 4),
    index=pd.period_range("1-2000", "12-2001", freq="M"),
    columns="Colorado Texas NY Ohio".split(),
)
frame[:5]
Loading...
annual_frame = frame.resample("A-DEC").mean()
annual_frame
Loading...

Time series plotting

!ls ~/src/github/pydata-book/examples
array_ex.txt               macrodata.csv
csv_mindex.csv             out.csv
ex1.csv                    segismundo.txt
ex1.xlsx                   spx.csv
ex2.csv                    stinkbug.png
ex3.csv                    stock_px.csv
ex3.txt                    stock_px_2.csv
ex4.csv                    test_file.csv
ex5.csv                    tips.csv
ex6.csv                    tseries.csv
ex7.csv                    volume.csv
example.json               yahoo_price.pkl
fdic_failed_bank_list.html yahoo_volume.pkl
ipython_bug.py
close_px_all = pd.read_csv(
    "~/src/github/pydata-book/examples/stock_px.csv",
    index_col=0,
    parse_dates=True,
)

close_px_all.head()
Loading...
close_px = close_px_all["AAPL MSFT XOM".split()]

close_px.head()
Loading...
close_px = close_px.resample("B", fill_method="ffill")
/Users/gp/.conda/envs/jupyter/lib/python2.7/site-packages/ipykernel_launcher.py:1: FutureWarning: fill_method is deprecated to .resample()
the new syntax is .resample(...).ffill()
  """Entry point for launching an IPython kernel.
close_px.head()
Loading...
close_px["AAPL"].plot()
<matplotlib.figure.Figure at 0x107150c50>
close_px["2009"].plot()
<matplotlib.figure.Figure at 0x107833e90>
close_px["AAPL"].loc["01-2011":"03-2011"].plot()
<matplotlib.figure.Figure at 0x107d29110>
# Quarterly data is nicely formatted with quarterly markers.
# aapl_q = close_px["AAPL"].resample("Q-DEC", fill_method="ffill")
aapl_q = close_px["AAPL"].resample("Q-DEC").ffill()
aapl_q["2009":].plot()
<matplotlib.figure.Figure at 0x107f8ee90>

Moving window functions

  • Common operations are statistics evaluated over a sliding window, with exponentially decaying weights
close_px.AAPL.plot()

# One needs to specify the number of non-na observations.
# pd.rolling_mean(close_px.AAPL, 250).plot()
close_px.rolling(window=250, center=False).mean().plot()
<matplotlib.figure.Figure at 0x108aaddd0>
<matplotlib.figure.Figure at 0x108aadd50>
# pd.rolling_std(close_px.AAPL, 250, min_periods=10).plot()
close_px.AAPL.rolling(min_periods=10, window=250, center=False).std().plot()
<matplotlib.figure.Figure at 0x1089f12d0>
expanding_mean = lambda x: pd.rolling_mean(x, len(x), min_periods=1)
# pd.rolling_mean(close_px, 60).plot(logy=True)
close_px.rolling(window=60, center=False).mean().plot(logy=True)
<matplotlib.figure.Figure at 0x1085e2dd0>
close_px.AAPL.rolling(window=60, center=False).count().plot()
<matplotlib.figure.Figure at 0x108c425d0>
close_px.AAPL.rolling(window=60, center=False).skew().plot()
<matplotlib.figure.Figure at 0x1094ce810>
close_px.AAPL.rolling(window=60, center=False).kurt().plot()
<matplotlib.figure.Figure at 0x1096f5210>
# rolling.apply(): apply a generic function over a moving window
# ewma, ewmvar, ewmstd, ewmcorr, ewmcov: exp-weighted moving *

Exponentially-weighted functions

  • Instead of specifying a static window size with equally-weighted observations, specify a constant decay factor to give more weight to more reent observations

    mat=amat1+(a1)xtma_t = a \cdot ma_{t-1} + (a-1) \cdot x_t
fig, axes = plt.subplots(
    nrows=1, ncols=1, sharex=True, sharey=True, figsize=(12, 5)
)

aapl_px = close_px.AAPL["2005":"2009"]
ma60 = aapl_px.rolling(60, min_periods=50).mean()
ewma60 = aapl_px.ewm(span=60, min_periods=60).mean()

aapl_px.plot(style="k-", ax=axes)
ma60.plot(style="b--", ax=axes, label="ma")
ewma60.plot(style="r--", ax=axes, label="ewma")

plt.legend()
<matplotlib.figure.Figure at 0x10b6c3090>

Binary moving window functions

# Some operators (e.g., corr, cov) operate on 2 time series.
spx_px = close_px_all.SPX["2005":"2009"]
spx_rets = spx_px / spx_px.shift(1) - 1

returns = close_px.pct_change()

display(spx_rets.head())
display(returns.head())

# corr = pd.rolling_corr(returns.AAPL, spx_rets, 125, min_periods=100)
corr = returns.AAPL.rolling(125, min_periods=100).corr(spx_rets)
corr.dropna().plot(figsize=(15, 6))
2005-01-03 NaN 2005-01-04 -0.011671 2005-01-05 -0.003628 2005-01-06 0.003506 2005-01-07 -0.001431 Name: SPX, dtype: float64
Loading...
<matplotlib.figure.Figure at 0x10cae1610>
# Multiple columns of a data-frame vs a series.
display(returns.head())
display(spx_rets.head())

corr = returns.rolling(125, min_periods=100).corr(spx_rets)
corr.dropna().plot(figsize=(15, 6))
Loading...
2005-01-03 NaN 2005-01-04 -0.011671 2005-01-05 -0.003628 2005-01-06 0.003506 2005-01-07 -0.001431 Name: SPX, dtype: float64
<matplotlib.figure.Figure at 0x10ca4fd10>

User-defined moving window functions

# rolling_apply)() allows to apply an array function over a moving function.
# The function needs to produce a single function

from scipy.stats import percentileofscore

score_at_2pct = lambda x: percentileofscore(x, 0.02)
# result = pd.rolling_apply(returns.AAPL, 250, score_at_2pct)
# pandas 0.22
result = returns.AAPL.rolling(250).apply(score_at_2pct)

result.plot()
<matplotlib.figure.Figure at 0x1138a1050>
TODO
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-318-9b7105f44b18> in <module>()
----> 1 TODO

NameError: name 'TODO' is not defined