时间序列处理

时间序列处理

🎯 目标:掌握 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
相关推荐
light blue bird1 小时前
3C 数码电子BOM 协同工作台组件
java·开发语言·jvm·windows·.net·桌面端
喵叔哟1 小时前
第2周学习笔记
笔记·python·学习·langchain
落羽的落羽1 小时前
【项目】JsonRpc框架——功能测试、项目总结
linux·服务器·开发语言·c++·qt·算法·机器学习
ZC跨境爬虫1 小时前
跟着 MDN 学JavaScript day_6:JavaScript 中的基础数学——数字与运算符
开发语言·前端·javascript·学习·ecmascript
copyer_xyf1 小时前
Python 迭代器与生成器
前端·后端·python
小小测试开发8 小时前
安装 Python 3.10+
开发语言·人工智能·python
梦想不只是梦与想9 小时前
Python 中的装饰器
python·装饰器
我叫唧唧波9 小时前
Python+AI 全栈学习笔记
人工智能·python·学习
AAA大运重卡何师傅(专跑国道)10 小时前
【无标题】
开发语言·c#