Excel Python:飞速搞定数据分析与处理

在这里插入图片描述

第二部分 pandas 入门

第六章 使用 pandas 进行时序分析

时序(time series)是时间轴上的一系列数据点,它们在很多场景中扮演着重要角色:交 易员用历史股价计算风险;天气预报基于测量温度、湿度和气压的传感器生成的时序来预测天气;数字市场部依靠网页生成的时序(比如每小时访问量)来得出营销活动所需的结论。

时序分析方面的需求使得数据科学家和分析师开始寻找更优秀的技术来替代Excel。他们的动机概括起来有以下几点。

  • 大型数据集

    ​ 时序的快速增长可能会使得数据量超过每张Excel数据表的容量上限——大约1 000 000 行。如果要在报价数据层面上处理盘中股价,那么你通常需要处理成千上万条记录,因 为每天、每只股票都会产生一条记录。

  • 日期和时间

    ​ Excel 在处理时序的基石,即日期和时间时有很多限制。举例来说,Excel 缺少对时区和毫秒时间格式的支持。而 pandas 支持时区,且使用了 NumPy 的 datetime64[ns] 数据类型,这种数据类型的时间精度可以精确到纳秒。

  • 缺少功能

    ​ Excel 甚至缺少处理时序数据的基本工具,例如,将每日时序转换为每月时序本来是一项十分常见的工作,但是在 Excel 中并没有一种方便的方法来完成。

利用 DataFrame 可以处理多种基于时间的索引:DatetimeIndex 是最常见的一种,表示带有时间戳的索引。其他的索引类型,比如 PeriodIndex,是基于时间间隔(比如每小时、每月)的索引。本章只会研究DatetimeIndex。

6.1 DatetimeIndex

本节会学习如何构造 DatetimeIndex,如何筛选属于特定时间范围的索引,以及如何处理时区。

6.1.1 创建 DatetimeIndex

pandas 为构造 DatetimeIndex 提供了 date_range 函数。它会接受一个开始日期、一个频率参数,以及周期数或者结束日期:

In [1]: # 首先导入本章中会用到的包, 
        # 并将绘图后端设置为Plotly 
        import pandas as pd 
        import numpy as np 
        pd.options.plotting.backend = "plotly" 
In [2]: # 通过起始时间戳、周期数和频率 
        #("D"=daily)创建DatetimeIndex 
        daily_index = pd.date_range("2020-02-28", periods=4, freq="D") 
        daily_index 
Out[2]: DatetimeIndex(['2020-02-28', '2020-02-29', '2020-03-01', '2020-03-02'], 
        dtype='datetime64[ns]', freq='D') 
In [3]: # 通过起始/结束时间戳创建DatetimeIndex 
        # 将频率设置为每周星期日("W-SUN") 
        weekly_index = pd.date_range("2020-01-01", "2020-01-31", freq="W-SUN") 
        weekly_index 
Out[3]: DatetimeIndex(['2020-01-05', '2020-01-12', '2020-01-19', '2020-01-26'], 
        dtype='datetime64[ns]', freq='W-SUN') 
In [4]: # 通过weekly_index构造DatetimeIndex 
        # 可以作为只在星期日开放的博物馆的游客人数 
        pd.DataFrame(data=[21, 15, 33, 34], 
                     columns=["visitors"], index=weekly_index) 
Out[4]:             visitors 
        2020-01-05        21 
        2020-01-12        15 
        2020-01-19        33 
        2020-01-26        34

在这里插入图片描述

现在回到第 5 章中微软股价的时序。如果仔细观察列的数据类型,你会注意到 Date列是 object 类型,也就是说pandas 会将时间戳作为字符串来解释:

In [5]: msft = pd.read_csv("csv/MSFT.csv") 
In [6]: msft.info() 
<class 'pandas.core.frame.DataFrame'> 
RangeIndex: 8622 entries, 0 to 8621 
Data columns (total 7 columns): 
 #   Column     Non-Null Count  Dtype   ---  ------     --------------  -----   
 0   Date       8622 non-null   object  
 1   Open       8622 non-null   float64 
 2   High       8622 non-null   float64 
 3   Low        8622 non-null   float64 
 4   Close      8622 non-null   float64 
 5   Adj Close  8622 non-null   float64 
 6   Volume     8622 non-null   int64   
dtypes: float64(5), int64(1), object(1) 
memory usage: 471.6+ KB

在这里插入图片描述

有两种方法可以修复这个问题并将其转换为 datetime 数据类型。一种方法是在该列上执行 to_datetime 函数。如果你想直接对原本的 DataFrame 进行修改,那么一定要记得将修改后的列赋值给原本的 DataFrame:

In [7]: msft.loc[:, "Date"] = pd.to_datetime(msft["Date"]) 
In [8]: msft.dtypes 
Out[8]: Date         datetime64[ns] 
        Open                float64 
        High                float64 
        Low                 float64 
        Close               float64 
        Adj Close           float64 
        Volume                int64 
        dtype: object

在这里插入图片描述

另一种方法是通过 parse_dates 参数告诉 read_csv 这一列包含时间戳。parse_dates 会接受一个列名列表或者索引作为参数。另外,你可能总是需要将时间戳转换为 DataFrame 的索引,因为我们马上会看到,以时间戳为索引可以让筛选数据更加简单。在 index_col 参数中提供想要用作索引的列名或索引可以省去一次 set_index 调用:

In [9]: msft = pd.read_csv("csv/MSFT.csv", 
                            index_col="Date", parse_dates=["Date"]) 
In [10]: msft.info() 
<class 'pandas.core.frame.DataFrame'> 
DatetimeIndex: 8622 entries, 1986-03-13 to 2020-05-27 
Data columns (total 6 columns): 
 #   Column     Non-Null Count  Dtype   ---  ------     --------------  -----   
 0   Open       8622 non-null   float64 
 1   High       8622 non-null   float64 
 2   Low        8622 non-null   float64 
 3   Close      8622 non-null   float64 
 4   Adj Close  8622 non-null   float64 
 5   Volume     8622 non-null   int64   
dtypes: float64(5), int64(1) 
memory usage: 471.5 KB

在这里插入图片描述

注意,第二次调用 info 输出的内容发生了变化,第二行的内容从 RangeIndex: 8622 entries, 0 to 8621 变为DatetimeIndex: 8622 entries, 1986-03-13 to 2020-05-27。这就表明你现在正在处理一个包含 DatetimeIndex 的 DataFrame。如果想转换其他的数据类型(比如想将 Volume 从 int 转换为 float),你也有两种选择:要么为read_csv 函数提供参数 dtype={“Volume”: float};要么像下面这样使用 astype 方法。

In [11]: msft.loc[:, "Volume"] = msft["Volume"].astype("float") 
         msft["Volume"].dtype 
Out[11]: dtype('float64')

在这里插入图片描述

处理时序时,在开始分析之前最好确保索引井然有序:

In [12]: msft = msft.sort_index()

如果只需要访问 DatetimeIndex 的一部分,比如只需要访问日期部分而不需要访问时间, 那么可以像下面这样访问它的 date 属性:

In [13]: msft.index.date 
Out[13]: array([datetime.date(1986, 3, 13), datetime.date(1986, 3, 14),  
                datetime.date(1986, 3, 17), ..., datetime.date(2020, 5, 22), 
                datetime.date(2020, 5, 26), datetime.date(2020, 5, 27)], 
               dtype=object)

在这里插入图片描述

除了 date,也可以访问 year、month、day 等属性。要在 datetime 类型的列上使用同样的功能,可以使用 dt 属性,比如 df[“column_name”].dt.date。

6.1.2 筛选 DatetimeIndex

如果你的 DataFrame 包含 DatetimeIndex,为 loc 传递 YYYY-MM-DD HH:MM:SS 格式的字符串作为参数可以轻松选取属于特定时间周期的行。pandas 会将这个字符串转换为一个包含整个时间周期的切片。如果要选取属于2019 年的所有行,则需要传递一个字符串,而不是数字:

In [14]: msft.loc["2019", "Adj Close"] 
Out[14]: Date 
         2019-01-02     99.099190 
         2019-01-03     95.453529 
         2019-01-04     99.893005 
         2019-01-07    100.020401 
         2019-01-08    100.745613 
                          ... 
         2019-12-24    156.515396 
         2019-12-26    157.798309 
         2019-12-27    158.086731 
         2019-12-30    156.724243 
         2019-12-31    156.833633 
         Name: Adj Close, Length: 252, dtype: float64

现在更进一步,为2019年6月和2020年5月之间的数据绘制一张图像:

In [15]: msft.loc["2019-06":"2020-05", "Adj Close"].plot()

在这里插入图片描述

将鼠标指针悬停在图表上可以显示一条有关该值的提示信息,用鼠标在图上画一个矩形可以进行缩放。双击图表可以返回默认视图。

6.1.3 处理时区

微软在纳斯达克上市,纳斯达克位于纽约,每天下午 4 点闭市。要将这些额外的信息添加到 DataFrame 的索引中,首先要通过 DateOffset 添加闭市时间,然后通过 tz_localize 为时间戳添加正确的时区。由于闭市时间只对收盘价有用,因此需要创建一个新的 DataFrame:

In [16]: # 将时间信息添加到日期中 
         msft_close = msft.loc[:, ["Adj Close"]].copy() 
         msft_close.index = msft_close.index + pd.DateOffset(hours=16) 
         msft_close.head(2) 
Out[16]:                      Adj Close 
         Date 
         1986-03-13 16:00:00   0.062205 
         1986-03-14 16:00:00   0.064427 
In [17]: # 令时间戳包含时区信息 
         msft_close = msft_close.tz_localize("America/New_York") 
         msft_close.head(2) 
Out[17]:                            Adj Close  
         Date 
         1986-03-13 16:00:00-05:00   0.062205 
         1986-03-14 16:00:00-05:00   0.064427

在这里插入图片描述

在这里插入图片描述

如果你想将时间戳转换为 UTC 时区,可以使用 DataFrame 的 tz_convert 方法。UTC 代表协调世界时(Coordinated Universal Time),它是格林尼治标准时间(GMT)的后继者。要注意 UTC 闭市时间的变化依赖于纽约夏令时(daylight saving time,DST)的生效情况:

In [18]: msft_close = msft_close.tz_convert("UTC") 
         msft_close.loc["2020-01-02", "Adj Close"]  # 21:00,不启用DST 
Out[18]: Date 
         2020-01-02 21:00:00+00:00    159.737595 
         Name: Adj Close, dtype: float64 
In [19]: msft_close.loc["2020-05-01", "Adj Close"]  # 20:00,启用DST 
Out[19]: Date 
         2020-05-01 20:00:00+00:00    174.085175 
         Name: Adj Close, dtype: float64

在这里插入图片描述

像这样处理时序之后可以让你比较位于不同时区的股票交易所的收盘价,即使数据中缺少这样的信息或者数据是以当地时间表示的也没有关系。

6.2 常见时序操作

本节会展示如何执行常见的时序分析任务,比如计算股票收益、绘制各种股票的表现,以 及在热度图中对其收益的相关性进行可视化。本节还会介绍如何更改时序的频率以及如何计算滚动统计信息。

6.2.1 移动和百分比变化率

在金融领域,通常假定股票的对数收益率(log return)服从正态分布。这里的对数收益率指的是当前股价和之前的股价之比的对数。为了体会每日对数收益率的分布情况,我们来绘制一个直方图。不过首先需要算出对数收益率。

Excel 和 Python 中的对数:Excel 用 LN 表示自然对数,用 LOG 表示以 10为底的对数。而在 Python 的 math 模块和 NumPy 中,自然对数用 log 表示,以 10 为底的对数用 log10 表示。

在pandas 中需要使用 shift 方法将值下移一行。这个方法允许你在单行上进行操作,因此可以利用向量化。shift会接受一个正整数或负整数作为参数,如果参数为正,就下移对应行;如果参数为负,则上移对应行。先来看看shift 如何工作:

In [20]: msft_close.head() 
Out[20]:                            Adj Close 
         Date 
         1986-03-13 21:00:00+00:00   0.062205 
         1986-03-14 21:00:00+00:00   0.064427 
         1986-03-17 21:00:00+00:00   0.065537 
         1986-03-18 21:00:00+00:00   0.063871 
         1986-03-19 21:00:00+00:00   0.062760 
In [21]: msft_close.shift(1).head() 
Out[21]:                            Adj Close  
         Date 
         1986-03-13 21:00:00+00:00        NaN 
         1986-03-14 21:00:00+00:00   0.062205 
         1986-03-17 21:00:00+00:00   0.064427 
         1986-03-18 21:00:00+00:00   0.065537 
         1986-03-19 21:00:00+00:00   0.063871

在这里插入图片描述

现在可以编写易于阅读和理解的基于向量的公式了。NumPy 的 log ufunc 会被应用到每一个元素上以求出其自然对数。然后就可以绘制出这个直方图了。

In [22]: returns = np.log(msft_close / msft_close.shift(1)) 
         returns = returns.rename(columns={"Adj Close": "returns"}) 
         returns.head() 
Out[22]:                             returns 
         Date 
         1986-03-13 21:00:00+00:00       NaN 
         1986-03-14 21:00:00+00:00  0.035097 
         1986-03-17 21:00:00+00:00  0.017082 
         1986-03-18 21:00:00+00:00 -0.025749 
         1986-03-19 21:00:00+00:00 -0.017547 
In [23]: # 绘制每日对数收益率的直方图 
         returns.plot.hist()

在这里插入图片描述

要获得简单收益率(simple return),可以使用 pandas 内置的 pct_change 方法。在默认情况 下,它会计算相对于前一行数据的百分比变化率,这也正是简单收益率的定义:

In [24]: simple_rets = msft_close.pct_change() 
         simple_rets = simple_rets.rename(columns={"Adj Close": "simple rets"}) 
         simple_rets.head() 
Out[24]:                            simple rets  
         Date 
         1986-03-13 21:00:00+00:00           NaN 
         1986-03-14 21:00:00+00:00      0.035721 
         1986-03-17 21:00:00+00:00      0.017229 
         1986-03-18 21:00:00+00:00     -0.025421 
         1986-03-19 21:00:00+00:00     -0.017394

在这里插入图片描述

6.2.2 基数的更改和相关性

当处理多个时序时,事情就变得更加有趣了。下面来读取一些其他公司的调整收盘价,这些数据来自亚马逊(AMZN)、谷歌(GOOGL)和 Apple(AAPL),它们也是从雅虎财经上下载下来的:

In [25]: parts = []  # 保存各个DataFrame的列表 
         for ticker in ["AAPL", "AMZN", "GOOGL", "MSFT"]: 
             # usecols参数可以让我们只读取Date列和Adj Close列 
             adj_close = pd.read_csv(f"csv/{ticker}.csv", 
                                     index_col="Date", parse_dates=["Date"], 
                                     usecols=["Date", "Adj Close"]) 
             # 将列重命名为股票代码 
             adj_close = adj_close.rename(columns={"Adj Close": ticker}) 
             # 将股价DataFrame添加到parts列表中 
             parts.append(adj_close) 
In [26]: # 将4个DataFrame组合成单个DataFrame 
         adj_close = pd.concat(parts, axis=1) 
         adj_close 
Out[26]:                   AAPL         AMZN        GOOGL        MSFT 
         Date 
         1980-12-12    0.405683          NaN          NaN         NaN 
         1980-12-15    0.384517          NaN          NaN         NaN 
         1980-12-16    0.356296          NaN          NaN 
         1980-12-17    0.365115          NaN          NaN         NaN 
         1980-12-18    0.375698          NaN          NaN         NaN 
         ...                ...          ...          ...         ... 
         2020-05-22  318.890015  2436.879883  1413.239990  183.509995 
         2020-05-26  316.730011  2421.860107  1421.369995  181.570007 
         2020-05-27  318.109985  2410.389893  1420.280029  181.809998 
         2020-05-28  318.250000  2401.100098  1418.239990         NaN 
         2020-05-29  317.940002  2442.370117  1433.520020         NaN 
         [9950 rows x 4 columns]

在这里插入图片描述

见识到 concat 的威力了吗?pandas 将每个时序都沿日期进行了自动对齐。这就是为什么那些历史没有 Apple 悠久的公司股价会包含 NaN。将时序沿日期对齐是一种很典型的操作,但是在 Excel 中很难实现,因而也很容易出错。丢弃所有包含缺失值的行可以保证所有股价都有同样的数据点:

In [27]: adj_close = adj_close.dropna() 
         adj_close.info() 
<class 'pandas.core.frame.DataFrame'> 
DatetimeIndex: 3970 entries, 2004-08-19 to 2020-05-27 
Data columns (total 4 columns): 
 #   Column  Non-Null Count  Dtype ---  ------  --------------  ----- 
 0   AAPL    3970 non-null   float64  
 1   AMZN    3970 non-null   float64 
 2   GOOGL   3970 non-null   float64 
 3   MSFT    3970 non-null   float64 
dtypes: float64(4) 
memory usage: 155.1 KB

在这里插入图片描述

现在更改股价的基数让所有的时序都从 100 开始。这样做可以在图表中对它们的相关表现进行比较,如图 6-4 所示。要更改时序的基数,首先应将每个值除以起始值,然后再乘以新的基数 100。如果在 Excel 中做过同样的事,那么你一般会写一个结合了绝对值和相对单元格引用的公式,然后把这个公式复制并粘贴到每一行和每一个时序。多亏了向量化和广播技术,在 pandas 中,只需写一个公式即可。

In [28]: # 使用一个从2019年6月到2020年5月的样本 
         adj_close_sample = adj_close.loc["2019-06":"2020-05", :] 
         rebased_prices = adj_close_sample / adj_close_sample.iloc[0, :] * 100 
         rebased_prices.head(2) 
Out[28]:  
                           AAPL        AMZN      GOOGL        MSFT 
         Date 
         2019-06-03  100.000000  100.000000  100.00000  100.000000 
         2019-06-04  103.658406  102.178197  101.51626  102.770372 
In [29]: rebased_prices.plot()

在这里插入图片描述

图 6-4:更改基数后的时序

通过 corr 方法获取数据的相关性,可以了解到不同公司股价之间的独立性。不幸的是,由 于 pandas 没有提供内置的图像类型来将相关性矩阵可视化为热度图,因此需要直接使用 Plotly 的 plotly.express 接口(参见图 6-5)。

In [30]: # 每日对数收益率的相关性 
         returns = np.log(adj_close / adj_close.shift(1)) 
         returns.corr() 
Out[30]:  
                    AAPL      AMZN     GOOGL      MSFT 
         AAPL   1.000000  0.424910  0.503497  0.486065 
         AMZN   0.424910  1.000000  0.486690  0.485725 
         GOOGL  0.503497  0.486690  1.000000  0.525645 
         MSFT   0.486065  0.485725  0.525645  1.000000 
In [31]: import plotly.express as px 
In [32]: fig = px.imshow(returns.corr(), 
                         x=adj_close.columns, 
                         y=adj_close.columns, 
                         color_continuous_scale=list( 
                             reversed(px.colors.sequential.RdBu)), 
                         zmin=-1, zmax=1) 
          fig.show()

在这里插入图片描述

执行上面的单元格后出现错误,提示的原因是 np.bool 的别名在当前版本中已经无效了,因此我们需要检查当前环境中的 Numpy 的版本,然后安装指定版本 numpy==1.23.2 以继续使用 np.bool。

print(f"NumPy version: {np.__version__}")
np.__version__
pip install numpy==1.23.2

在这里插入图片描述

安装成功后,需要重启内核。直接点击页面中工具栏的 Cell 中的 Run All,即可运行成功:

在这里插入图片描述

6.2.3 重新采样

向上/向下采样(up and down sampling)是处理时序时的一项常见任务。向上采样指的是将时序的频率提高,而向下采样指的是将时序的频率降低。例如,在财务概况介绍中,你通常需要展示出月度或季度的状况。要将每日时序转换为每月时序,可以使用 resample 方法,该方法接受字符串形式的频率参数,比如 M 代表日历月底(end-of-calendar-month), BM 代表经营月底(end-of-business-month)。你可以在 pandas 文档中找到所有频率字符串的 列表。和 groupby 类似,可以紧接着调用一个方法指定如何重采样。这里使用了 last 来获得当月最后几个观察值:

In [33]: end_of_month = adj_close.resample("M").last() 
         end_of_month.head() 
Out[33]:                 AAPL       AMZN      GOOGL       MSFT 
         Date 
         2004-08-31  2.132708  38.139999  51.236237  17.673630 
         2004-09-30  2.396127  40.860001  64.864868  17.900215 
         2004-10-31  3.240182  34.130001  95.415413  18.107374 
         2004-11-30  4.146072  39.680000  91.081078  19.344421 
         2004-12-31  3.982207  44.290001  96.491493  19.279480

在这里插入图片描述

除了 last,你也可以选择任何能够在 groupby 上使用的方法,比如 sum 或者 mean。还有一 个 ohlc 方法,它可以方便地获得周期内的开盘价、最高价、最低价和收盘价。这些数据可以用作股价分析中常用的K线图的数据来源。

如果只有月底时序而你想要从中生成每周时序,则必须对时序进行向上采样。asfreq 不会让 pandas 应用任何转换,因此你会看到大部分的值变成了 NaN。如果想向前填充(forward fill)最后的已知值,那么可以使用 ffill 方法:

In [34]: end_of_month.resample("D").asfreq().head()  # 无转换 
Out[34]:                 AAPL       AMZN      GOOGL      MSFT 
         Date 
         2004-08-31  2.132708  38.139999  51.236237  17.67363 
         2004-09-01       NaN        NaN        NaN        NaN 
         2004-09-02       NaN        NaN        NaN        NaN 
         2004-09-03       NaN        NaN        NaN        NaN 
         2004-09-04       NaN        NaN        NaN        NaN 
In [35]: end_of_month.resample("W-FRI").ffill().head()  # 向前填充 
Out[35]:                 AAPL       AMZN      GOOGL       MSFT 
         Date 
         2004-09-03  2.132708  38.139999  51.236237  17.673630 
         2004-09-10  2.132708  38.139999  51.236237  17.673630 
         2004-09-17  2.132708  38.139999  51.236237  17.673630 
         2004-09-24  2.132708  38.139999  51.236237  17.673630 
         2004-10-01  2.396127  40.860001  64.864868  17.900215

在这里插入图片描述

向下采样是平滑时序的方式之一,而利用滚动窗口计算统计量是另一种方式,下一节会对此进行介绍。

6.2.4 滚动窗口

在计算时序的统计量时,你通常想要算出滚动统计量,比如移动平均值(moving average)。 移动平均值会关注时序(比如 25 天)的一个子集,先算出这个子集的均值,然后再将窗口向前移动一天。这样就可以产生一个更平滑的新时序,且不容易产生异常值。如果你是做算法交易的,那么你可以关注股价移动平均值发生重叠的地方,然后以此(或是它的某 种变体)作为交易的信号。DataFrame 有一个 rolling 方法,该方法会接受观测数量作为参数。你可以在 rolling 后面链式调用所需的统计量方法——对于移动平均值来说就是在 rolling 后面调用 mean。观察图6-6,你可以轻松地将原本的时序和平滑的移动平均值进行对比。

In [36]: # 用2019年的数据为MSFT绘制移动平均值 
         msft19 = msft.loc["2019", ["Adj Close"]].copy() 
         # 将25天的移动平均值作为一个新列添加到DataFrame中 
         msft19.loc[:, "25day average"] = msft19["Adj Close"].rolling(25).mean() 
         msft19.plot()

在这里插入图片描述

图 6-6:移动平均值的图像

除了 mean,还可以使用很多其他的统计量方法,其中包括 count、sum、median、min、max、 std(标准差)和 var(方差)。

6.3 pandas 的局限性

在你的 DataFrame 不断增长的过程中,应当注意到 DataFrame 的容量上限。Excel 每张工作表能保存的数据有严格的限制,最多只能保存大概 1000000 行 12000 列。而 pandas 只有一个不那么严格的限制:所有的数据必须能够被设备的可用内存装下。如果实际情况并不允许,那么也有一些简单的解决方案:只加载数据集中你需要的部分,或是删去中间结果来释放一些内存。如果还是解决不了内存不足的问题,那么还有不少可以处理大型数据的项目,它们都是 pandas 用户的“老熟人”。构建在 NumPy 和 pandas 之上的 Dask 就是其 中之一,它会将大型数据集分割成多个 pandas DataFrame,将工作负载分布到多个 CPU 核心或多台设备上,从而让你可以处理大型数据集。还有一些其他的大数据项目也可以在某些 DataFrame 中使用,比如 Modin、Koalas、Vaex、PyShark、cuDF、Ibis 和 PyArrow。

更多推荐