时间序列处理
🎯 目标:掌握 Pandas 处理时间序列数据的能力,这是金融、销售、IoT 等领域必备技能。
10.1 什么是时间序列?
10.1.1 时间序列的特点
#mermaid-svg-VGpWKTfQKej3qcGm{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-VGpWKTfQKej3qcGm .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-VGpWKTfQKej3qcGm .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-VGpWKTfQKej3qcGm .error-icon{fill:#552222;}#mermaid-svg-VGpWKTfQKej3qcGm .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-VGpWKTfQKej3qcGm .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-VGpWKTfQKej3qcGm .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-VGpWKTfQKej3qcGm .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-VGpWKTfQKej3qcGm .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-VGpWKTfQKej3qcGm .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-VGpWKTfQKej3qcGm .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-VGpWKTfQKej3qcGm .marker{fill:#333333;stroke:#333333;}#mermaid-svg-VGpWKTfQKej3qcGm .marker.cross{stroke:#333333;}#mermaid-svg-VGpWKTfQKej3qcGm svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-VGpWKTfQKej3qcGm p{margin:0;}#mermaid-svg-VGpWKTfQKej3qcGm .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-VGpWKTfQKej3qcGm .cluster-label text{fill:#333;}#mermaid-svg-VGpWKTfQKej3qcGm .cluster-label span{color:#333;}#mermaid-svg-VGpWKTfQKej3qcGm .cluster-label span p{background-color:transparent;}#mermaid-svg-VGpWKTfQKej3qcGm .label text,#mermaid-svg-VGpWKTfQKej3qcGm span{fill:#333;color:#333;}#mermaid-svg-VGpWKTfQKej3qcGm .node rect,#mermaid-svg-VGpWKTfQKej3qcGm .node circle,#mermaid-svg-VGpWKTfQKej3qcGm .node ellipse,#mermaid-svg-VGpWKTfQKej3qcGm .node polygon,#mermaid-svg-VGpWKTfQKej3qcGm .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-VGpWKTfQKej3qcGm .rough-node .label text,#mermaid-svg-VGpWKTfQKej3qcGm .node .label text,#mermaid-svg-VGpWKTfQKej3qcGm .image-shape .label,#mermaid-svg-VGpWKTfQKej3qcGm .icon-shape .label{text-anchor:middle;}#mermaid-svg-VGpWKTfQKej3qcGm .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-VGpWKTfQKej3qcGm .rough-node .label,#mermaid-svg-VGpWKTfQKej3qcGm .node .label,#mermaid-svg-VGpWKTfQKej3qcGm .image-shape .label,#mermaid-svg-VGpWKTfQKej3qcGm .icon-shape .label{text-align:center;}#mermaid-svg-VGpWKTfQKej3qcGm .node.clickable{cursor:pointer;}#mermaid-svg-VGpWKTfQKej3qcGm .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-VGpWKTfQKej3qcGm .arrowheadPath{fill:#333333;}#mermaid-svg-VGpWKTfQKej3qcGm .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-VGpWKTfQKej3qcGm .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-VGpWKTfQKej3qcGm .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-VGpWKTfQKej3qcGm .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-VGpWKTfQKej3qcGm .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-VGpWKTfQKej3qcGm .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-VGpWKTfQKej3qcGm .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-VGpWKTfQKej3qcGm .cluster text{fill:#333;}#mermaid-svg-VGpWKTfQKej3qcGm .cluster span{color:#333;}#mermaid-svg-VGpWKTfQKej3qcGm div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-VGpWKTfQKej3qcGm .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-VGpWKTfQKej3qcGm rect.text{fill:none;stroke-width:0;}#mermaid-svg-VGpWKTfQKej3qcGm .icon-shape,#mermaid-svg-VGpWKTfQKej3qcGm .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-VGpWKTfQKej3qcGm .icon-shape p,#mermaid-svg-VGpWKTfQKej3qcGm .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-VGpWKTfQKej3qcGm .icon-shape .label rect,#mermaid-svg-VGpWKTfQKej3qcGm .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-VGpWKTfQKej3qcGm .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-VGpWKTfQKej3qcGm .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-VGpWKTfQKej3qcGm :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 时间序列数据
时间戳索引
DatetimeIndex
时间间隔
固定或不固定
趋势性
长期走向
季节性
周期性波动
随机性
不可预测波动
时间序列 = 时间戳 + 观测值
10.1.2 创建时间序列数据
python
import pandas as pd
import numpy as np
# 🌟 创建日期范围
dates = pd.date_range(start='2024-01-01', periods=10, freq='D')
print("日期范围:", dates)
# 🌟 创建时间序列 DataFrame
np.random.seed(42)
ts = pd.DataFrame({
'日期': pd.date_range('2024-01-01', periods=30, freq='D'),
'销售额': np.random.randint(100, 500, 30) + np.linspace(0, 200, 30)
})
ts['日期'] = pd.to_datetime(ts['日期'])
ts = ts.set_index('日期')
print("\n=== 时间序列数据 ===")
print(ts.head(10))
10.2 日期时间基础
10.2.1 创建日期时间
python
# 🌟 字符串转日期
s = '2024-01-15'
dt = pd.to_datetime(s)
print(f"字符串 '{s}' → 日期时间: {dt}")
# 🌟 多种格式自动识别
dates = pd.to_datetime([
'2024-01-15',
'2024/02/20',
'15-03-2024',
'2024年04月15日',
'May 5, 2024'
], errors='coerce')
print("\n多种格式:", dates)
# 🌟 指定格式解析
dt = pd.to_datetime('15/01/2024', format='%d/%m/%Y')
print(f"\n指定格式: {dt}")
# 🌟 从时间戳创建
dt = pd.to_datetime(1609459200, unit='s') # Unix 时间戳
print(f"从时间戳: {dt}")
10.2.2 日期时间属性
python
dt = pd.Timestamp('2024-01-15 14:30:00')
print(f"日期时间: {dt}")
print(f"年份: {dt.year}")
print(f"月份: {dt.month}")
print(f"日: {dt.day}")
print(f"小时: {dt.hour}")
print(f"分钟: {dt.minute}")
print(f"秒: {dt.second}")
print(f"星期几: {dt.dayofweek}") # 0=周一
print(f"星期名称: {dt.day_name()}")
print(f"季度: {dt.quarter}")
print(f"是否闰年: {dt.is_leap_year}")
print(f"当月天数: {dt.days_in_month}")
print(f"年内第几天: {dt.dayofyear}")
10.2.3 日期范围生成
python
# 🌟 日频率
daily = pd.date_range('2024-01-01', periods=5, freq='D')
print("每日:", daily)
# 🌟 工作日频率
business_days = pd.date_range('2024-01-01', periods=5, freq='B')
print("\n工作日:", business_days)
# 🌟 周频率
weekly = pd.date_range('2024-01-01', periods=5, freq='W') # 周日
weekly_mon = pd.date_range('2024-01-01', periods=5, freq='W-MON') # 周一
print("\n每周日:", weekly)
print("每周一:", weekly_mon)
# 🌟 月频率
monthly = pd.date_range('2024-01-01', periods=5, freq='M') # 月末
monthly_start = pd.date_range('2024-01-01', periods=5, freq='MS') # 月初
print("\n每月末:", monthly)
print("每月初:", monthly_start)
# 🌟 季度频率
quarterly = pd.date_range('2024-01-01', periods=4, freq='Q')
print("\n每季度:", quarterly)
# 🌟 年频率
yearly = pd.date_range('2024-01-01', periods=3, freq='Y')
print("\n每年:", yearly)
# 🌟 小时频率
hourly = pd.date_range('2024-01-01', periods=5, freq='H')
print("\n每小时:", hourly)
10.3 时间索引操作
10.3.1 设置时间索引
python
df = pd.DataFrame({
'日期': pd.date_range('2024-01-01', periods=10, freq='D'),
'销售额': np.random.randint(100, 500, 10)
})
# 🌟 设置为索引
df_indexed = df.set_index('日期')
print(df_indexed)
# 🌟 直接创建带时间索引的 DataFrame
ts = pd.DataFrame(
{'销售额': np.random.randint(100, 500, 10)},
index=pd.date_range('2024-01-01', periods=10, freq='D')
)
print(ts)
10.3.2 时间索引选择
python
# 创建一年的数据
ts = pd.DataFrame(
{'销售额': np.random.randint(100, 500, 365)},
index=pd.date_range('2024-01-01', periods=365, freq='D')
)
# 🌟 选择特定日期
print("2024-01-15:", ts.loc['2024-01-15'])
# 🌟 选择月份
print("\n2024年1月:")
print(ts.loc['2024-01'])
# 🌟 选择日期范围
print("\n2024年1月1日到1月10日:")
print(ts.loc['2024-01-01':'2024-01-10'])
# 🌟 使用 truncate(截断)
print("\n2024年2月之后:")
print(ts.truncate(before='2024-02-01').head())
10.3.3 时间组件提取
python
ts = pd.DataFrame(
{'销售额': np.random.randint(100, 500, 100)},
index=pd.date_range('2024-01-01', periods=100, freq='D')
)
# 🌟 提取时间组件
ts['年'] = ts.index.year
ts['月'] = ts.index.month
ts['日'] = ts.index.day
ts['星期'] = ts.index.dayofweek
ts['季度'] = ts.index.quarter
ts['是否周末'] = ts.index.dayofweek.isin([5, 6])
print(ts.head(10))
10.4 重采样(Resample)
10.4.1 降采样(高频→低频)
python
# 创建小时数据
ts_hourly = pd.DataFrame(
{'销售额': np.random.randint(10, 100, 24*7)}, # 一周的小时数据
index=pd.date_range('2024-01-01', periods=24*7, freq='H')
)
print("=== 原始小时数据 ===")
print(ts_hourly.head(10))
# 🌟 按日汇总
daily = ts_hourly.resample('D').sum()
print("\n=== 日汇总 ===")
print(daily)
# 🌟 按日求平均
daily_mean = ts_hourly.resample('D').mean()
print("\n=== 日平均 ===")
print(daily_mean)
# 🌟 多种聚合
daily_stats = ts_hourly.resample('D').agg(['sum', 'mean', 'max', 'min'])
print("\n=== 日统计 ===")
print(daily_stats)
10.4.2 升采样(低频→高频)
python
# 创建日数据
ts_daily = pd.DataFrame(
{'销售额': [100, 150, 120, 180, 200]},
index=pd.date_range('2024-01-01', periods=5, freq='D')
)
print("=== 原始日数据 ===")
print(ts_daily)
# 🌟 升采样到小时(需要填充)
hourly = ts_daily.resample('H').asfreq() # 不填充
print("\n=== 升采样(无填充) ===")
print(hourly.head(10))
# 🌟 前向填充
hourly_ffill = ts_daily.resample('H').ffill()
print("\n=== 前向填充 ===")
print(hourly_ffill.head(10))
# 🌟 线性插值
hourly_interp = ts_daily.resample('H').interpolate()
print("\n=== 线性插值 ===")
print(hourly_interp.head(10))
10.4.3 常用重采样规则
python
# 创建分钟数据
ts_min = pd.DataFrame(
{'值': np.random.randint(1, 10, 60*24)}, # 一天的分钟数据
index=pd.date_range('2024-01-01', periods=60*24, freq='T')
)
# 🌟 常用重采样
print("5分钟:", ts_min.resample('5T').sum().head())
print("\n15分钟:", ts_min.resample('15T').sum().head())
print("\n30分钟:", ts_min.resample('30T').sum().head())
print("\n1小时:", ts_min.resample('H').sum().head())
print("\n4小时:", ts_min.resample('4H').sum().head())
print("\n1天:", ts_min.resample('D').sum())
10.5 时间偏移(Shift)
10.5.1 数据移动
python
ts = pd.DataFrame(
{'销售额': [100, 150, 120, 180, 200, 170]},
index=pd.date_range('2024-01-01', periods=6, freq='D')
)
# 🌟 向下移动(滞后)
ts['销售额_滞后1'] = ts['销售额'].shift(1)
# 🌟 向上移动(领先)
ts['销售额_领先1'] = ts['销售额'].shift(-1)
# 🌟 计算日环比
ts['日环比'] = ts['销售额'] / ts['销售额_滞后1'] - 1
print(ts)
10.5.2 时间索引移动
python
ts = pd.DataFrame(
{'销售额': [100, 150, 120, 180, 200]},
index=pd.date_range('2024-01-01', periods=5, freq='D')
)
# 🌟 索引向后移动1天
ts_shifted = ts.shift(periods=1, freq='D')
print("原索引:", ts.index.tolist())
print("移动后:", ts_shifted.index.tolist())
# 🌟 索引向前移动1月
ts_shifted_m = ts.shift(periods=1, freq='M')
print("\n月移动:", ts_shifted_m.index.tolist())
10.6 时间差计算
10.6.1 Timedelta
python
# 🌟 创建时间差
td = pd.Timedelta(days=5, hours=3, minutes=30)
print(f"时间差: {td}")
print(f"总天数: {td.days}")
print(f"总秒数: {td.total_seconds()}")
# 🌟 日期相减
dt1 = pd.Timestamp('2024-01-15')
dt2 = pd.Timestamp('2024-01-20')
diff = dt2 - dt1
print(f"\n日期差: {diff}")
print(f"天数: {diff.days}")
# 🌟 时间差运算
dt = pd.Timestamp('2024-01-15')
new_dt = dt + pd.Timedelta(days=5)
print(f"\n2024-01-15 + 5天 = {new_dt}")
10.6.2 日期间隔
python
df = pd.DataFrame({
'开始日期': pd.to_datetime(['2024-01-01', '2024-02-01', '2024-03-01']),
'结束日期': pd.to_datetime(['2024-01-15', '2024-02-20', '2024-03-10'])
})
# 🌟 计算间隔天数
df['间隔天数'] = (df['结束日期'] - df['开始日期']).dt.days
# 🌟 计算工作日间隔
df['工作日间隔'] = df.apply(
lambda x: len(pd.bdate_range(x['开始日期'], x['结束日期'])),
axis=1
)
print(df)
10.7 时区处理
10.7.1 时区设置
python
# 🌟 创建带时区的日期
dt_utc = pd.Timestamp('2024-01-15 12:00', tz='UTC')
print(f"UTC时间: {dt_utc}")
# 🌟 本地时间转 UTC
dt_local = pd.Timestamp('2024-01-15 12:00')
dt_utc = dt_local.tz_localize('Asia/Shanghai').tz_convert('UTC')
print(f"\n北京时间转UTC: {dt_utc}")
# 🌟 时区转换
dt_ny = dt_utc.tz_convert('America/New_York')
print(f"\n转纽约时间: {dt_ny}")
10.7.2 时间序列时区
python
# 创建时间序列
ts = pd.DataFrame(
{'值': [1, 2, 3, 4, 5]},
index=pd.date_range('2024-01-01', periods=5, freq='H')
)
# 🌟 设置时区
ts_utc = ts.tz_localize('UTC')
print("UTC:", ts_utc.index)
# 🌟 转换时区
ts_shanghai = ts_utc.tz_convert('Asia/Shanghai')
print("\n上海:", ts_shanghai.index)
# 🌟 去除时区
ts_naive = ts_shanghai.tz_localize(None)
print("\n无时区:", ts_naive.index)
10.8 实战案例:销售时间序列分析
场景
你是一名销售分析师,需要分析全年销售数据的时间趋势。
python
import pandas as pd
import numpy as np
# 创建一年的销售数据(带趋势和季节性)
np.random.seed(42)
dates = pd.date_range('2024-01-01', periods=365, freq='D')
# 添加趋势
trend = np.linspace(100, 200, 365)
# 添加季节性(周效应)
weekday_effect = [1.0, 1.1, 1.05, 1.0, 1.15, 1.3, 1.2] # 周末销量高
seasonal = np.array([weekday_effect[d.weekday()] for d in dates])
# 添加随机波动
noise = np.random.normal(0, 10, 365)
# 合成销售额
sales = (trend * seasonal + noise).clip(50, 500)
# 创建 DataFrame
df = pd.DataFrame({
'日期': dates,
'销售额': sales,
'订单数': np.random.randint(10, 50, 365)
})
df['日期'] = pd.to_datetime(df['日期'])
df = df.set_index('日期')
print("=== 销售数据概览 ===")
print(df.head(10))
print(f"\n数据统计:")
print(df.describe())
# 1. 月度汇总
print("\n=== 1. 月度销售额汇总 ===")
monthly = df.resample('M').agg({
'销售额': 'sum',
'订单数': 'sum'
})
monthly['客单价'] = monthly['销售额'] / monthly['订单数']
print(monthly)
# 2. 周汇总
print("\n=== 2. 周销售额汇总 ===")
weekly = df.resample('W').sum()
print(weekly.head(10))
# 3. 星期分析
print("\n=== 3. 星期销售分析 ===")
df['星期'] = df.index.dayofweek
df['星期名'] = df.index.day_name()
weekday_analysis = df.groupby('星期名').agg({
'销售额': ['mean', 'sum'],
'订单数': 'mean'
}).round(2)
print(weekday_analysis)
# 4. 季度分析
print("\n=== 4. 季度销售分析 ===")
df['季度'] = df.index.quarter
quarterly = df.groupby('季度')['销售额'].sum()
print(quarterly)
# 5. 7日移动平均
print("\n=== 5. 7日移动平均 ===")
df['MA7'] = df['销售额'].rolling(window=7).mean()
print(df[['销售额', 'MA7']].head(15))
# 6. 同比分析(假设有去年数据)
print("\n=== 6. 月度同比 ===")
current_year = df.resample('M')['销售额'].sum()
# 模拟去年数据(今年数据的90%)
last_year = current_year * 0.9
comparison = pd.DataFrame({
'今年': current_year.values,
'去年': last_year.values,
'同比增长': ((current_year.values - last_year.values) / last_year.values * 100).round(2)
}, index=current_year.index)
print(comparison)
# 7. 找出销售高峰和低谷
print("\n=== 7. 销售Top10和Bottom10 ===")
print("Top 10 销售日:")
print(df.nlargest(10, '销售额')[['销售额', '订单数']])
print("\nBottom 10 销售日:")
print(df.nsmallest(10, '销售额')[['销售额', '订单数']])
10.9 本章小结
核心要点
✅ 日期时间创建:
pd.to_datetime()------ 字符串转日期pd.date_range()------ 生成日期范围
✅ 时间索引:
set_index('日期')------ 设置时间索引ts.loc['2024-01']------ 时间选择ts.index.year/month/day------ 提取组件
✅ 重采样:
resample('D').sum()------ 降采样resample('H').ffill()------ 升采样填充
✅ 时间偏移:
shift(1)------ 数据移动shift(1, freq='D')------ 索引移动
✅ 时区处理:
tz_localize()------ 设置时区tz_convert()------ 转换时区
常用频率代码
| 代码 | 说明 |
|---|---|
D |
日 |
B |
工作日 |
W |
周(周日) |
M |
月末 |
MS |
月初 |
Q |
季末 |
A |
年末 |
H |
小时 |
T |
分钟 |
S |
秒 |