Python 时间序列分析秘籍第二版(二)

原文:annas-archive.org/md5/7277c6f80442eb633bdbaf16dcd96fad

译者:飞龙

协议:CC BY-NC-SA 4.0

第四章:4 将时间序列数据持久化到文件

加入我们的书籍社区,访问 Discord

https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/ts-anal-py-cb-2e/img/file0.png

packt.link/zmkOY

在本章中,你将使用pandas库将你的时间序列 DataFrame持久化到不同的文件格式中,如CSVExcelParquetpickle文件。在对 DataFrame 进行分析或数据转换时,实际上是利用了 pandas 的内存分析能力,提供了极好的性能。然而,内存中的数据意味着它很容易丢失,因为它尚未被持久化到磁盘存储中。

在处理 DataFrame 时,你需要持久化数据以便将来取回、创建备份或与他人共享数据。pandas库附带了一套丰富的写入函数,可以将内存中的 DataFrame(或系列)持久化到磁盘上的不同文件格式中。这些写入函数使你能够将数据存储到本地驱动器或远程服务器位置,例如云存储文件系统,包括Google DriveAWS S3Azure Blob StorageDropbox

在本章中,你将探索将数据写入不同的文件格式(本地存储)和云存储位置,如 Amazon Web Services(AWS)、Google Cloud 和 Azure。

以下是本章将涵盖的食谱:

  • 使用pickle进行时间序列数据序列化

  • 写入 CSV 和其他分隔符文件

  • 写入 Excel 文件

  • 将数据存储到云存储(AWS、GCP 和 Azure)

  • 写入大规模数据集

技术要求

在本章及之后的内容中,我们将广泛使用 pandas 2.2.2 版本(2023 年 4 月 10 日发布)。

在整个过程中,你将安装多个 Python 库,以与 pandas 协同工作。这些库在每个食谱的准备工作部分中都有突出说明。你还可以从 GitHub 仓库下载 Jupyter 笔记本(github.com/PacktPublishing/Time-Series-Analysis-with-Python-Cookbook)来跟随学习。你可以在这里下载本章使用的数据集:github.com/PacktPublishing/Time-Series-Analysis-with-Python-Cookbook./tree/main/datasets/Ch4

使用 pickle 序列化时间序列数据

在 Python 中处理数据时,你可能希望将 Python 数据结构或对象(如 pandas DataFrame)持久化到磁盘,而不是将其保留在内存中。一个方法是将数据序列化为字节流,以便将其存储在文件中。在 Python 中,pickle模块是一种常见的对象序列化与反序列化方法(序列化的反过程),也被称为pickling(序列化)和unpickling(反序列化)。

准备工作

pickle模块是 Python 自带的,因此无需额外安装。

在这个食谱中,我们将探索两种常见的序列化数据的方法,这些方法通常被称为pickling

你将使用由约翰·霍普金斯大学的*系统科学与工程中心(CSSE)*提供的 COVID-19 数据集,你可以从官方 GitHub 仓库下载该数据集,链接为:github.com/CSSEGISandData/COVID-19。请注意,约翰·霍普金斯大学自 2023 年 3 月 10 日起不再更新该数据集。

如何实现…

你将使用 pandas 的DataFrame.to_pickle()函数将数据写入pickle文件,然后使用pickle库直接探索另一种选择。

使用 pandas 写入 pickle 文件

你将从读取 COVID-19 时间序列数据到 DataFrame 开始,进行一些转换,然后将结果持久化到pickle文件中,以便未来分析。这应该类似于持久化仍在进行中的数据(就分析而言)的一种典型场景:

  1. 首先,让我们将 CSV 数据加载到 pandas DataFrame 中:
import pandas as pd
from pathlib import Path
file = \
Path('../../datasets/Ch4/time_series_covid19_confirmed_global.csv')
df = pd.read_csv(file)
df.head()

上述代码将显示 DataFrame 的前五行:

https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/ts-anal-py-cb-2e/img/file32.png

图 4.1:COVID-19 全球确诊病例的前五行

你可以从输出中观察到,这是一个宽格式 DataFrame,共有 1147 列,每列代表一个数据收集日期,从1/22/203/9/23

  1. 假设分析的一部分是聚焦于美国,并且只使用 2021 年夏季(6 月、7 月、8 月和 9 月)收集的数据。你将通过应用必要的过滤器转换 DataFrame,然后将数据反向旋转,使日期显示在行中而不是列中(从宽格式转换为长格式):
# filter data where Country is United States
df_usa = df[df['Country/Region'] == 'US']
# filter columns from June to end of September
df_usa_summer = df_usa.loc[:, '6/1/21':'9/30/21']
# unpivot using pd.melt()
df_usa_summer_unpivoted = \
    pd.melt(df_usa_summer,
            value_vars=df_usa_summer.columns,
            value_name='cases',
            var_name='date').set_index('date')
df_usa_summer_unpivoted.index = \
    pd.to_datetime(df_usa_summer_unpivoted.index, format="%m/%d/%y")
  1. 检查df_usa_summer_unpivoted DataFrame 并打印前五条记录:
df_usa_summer_unpivoted.info()
>>
<class 'pandas.core.frame.DataFrame'>
DatetimeIndex: 122 entries, 2021-06-01 to 2021-09-30
Data columns (total 1 columns):
 #   Column  Non-Null Count  Dtype
---  ------  --------------  -----
 0   cases   122 non-null    int64
dtypes: int64(1)
memory usage: 1.9 KB
df_usa_summer_unpivoted.head()
>>
               cases
date               
2021-06-01  33407540
2021-06-02  33424131
2021-06-03  33442100
2021-06-04  33459613
2021-06-05  33474770

你已对数据集进行了筛选,并将其从宽格式的 DataFrame 转换为长格式的时间序列 DataFrame。

  1. 假设你现在对数据集已经满意,准备将数据集进行 pickling(序列化)。你将使用DataFrame.to_pickle()函数将 DataFrame 写入covid_usa_summer_2020.pkl文件:
output = \
Path('../../datasets/Ch4/covid_usa_summer_2021.pkl')
df_usa_summer_unpivoted.to_pickle(output)

Pickling 保留了 DataFrame 的结构。当你再次加载 pickle 数据时(反序列化),你将恢复 DataFrame 的原始结构,例如,带有DatetimeIndex类型。

  1. 使用pandas.read_pickle()读取 pickle 文件并检查 DataFrame:
unpickled_df = pd.read_pickle(output)
unpickled_df.info()
>>
<class 'pandas.core.frame.DataFrame'>
DatetimeIndex: 122 entries, 2021-06-01 to 2021-09-30
Data columns (total 1 columns):
 #   Column  Non-Null Count  Dtype
---  ------  --------------  -----
 0   cases   122 non-null    int64
dtypes: int64(1)
memory usage: 1.9 KB

从之前的示例中,你能够使用pandas.read_pickle()反序列化数据到 DataFrame 中,并保留之前所做的所有转换和数据类型。

使用 pickle 库写入 pickle 文件

Python 自带pickle库,你可以导入并使用它来序列化(pickle)对象,使用dump(写入)和load(读取)。在接下来的步骤中,你将使用pickle.dump()pickle.load()来序列化并反序列化df_usa_summer_unpivoted DataFrame。

  1. 导入pickle库:
import pickle
  1. 然后,您可以使用 dump() 方法将 df_usa_summer_unpivoted DataFrame 持久化:
file_path = \
Path('../../datasets/Ch4/covid_usa_summer_2021_v2.pkl')
with open(file_path, "wb") as file:
    pickle.dump(df_usa_summer_unpivoted, file)

请注意,使用的模式是 “wb”,因为我们是以二进制模式写入(以原始字节写入)。

  1. 您可以使用 load() 方法读取文件并检查 DataFrame。请注意,在以下代码中,导入的对象是一个 pandas DataFrame,尽管您使用的是 pickle.load() 而不是 Pandas.read_pickle()。这是因为 pickling 保留了模式和数据结构:
with open(file_path, "rb") as file:
    df = pickle.load(file)
type(df)
>>
pandas.core.frame.DataFrame

请注意,使用的模式是 “rb”,因为我们是以二进制模式读取(作为原始字节读取)。

它是如何工作的……

在 Python 中,Pickling 是将任何 Python 对象序列化的过程。更具体地说,它使用一种二进制序列化协议将对象转换为二进制信息,这是一种不可人类读取的格式。该协议允许我们重新构建(反序列化)被 pickle 的文件(二进制格式),使其恢复到原始内容而不丢失宝贵的信息。如前面的示例所示,我们确认时间序列 DataFrame 在重新构建(反序列化)时,能够恢复到其精确的形式(模式)。

pandas 的 DataFrame.to_pickle() 函数有两个额外的参数,重要的是需要了解第一个参数是 compression,该参数在其他写入函数中也可用,如 to_csv()to_json()to_parquet() 等。

DataFrame.to_pickle() 函数的情况下,默认的压缩值设置为 infer,这让 pandas 根据提供的文件扩展名来确定使用哪种压缩模式。在前面的示例中,我们使用了 DataFrame.to_pickle(output),其中 output 被定义为 .pkl 文件扩展名,如 covid_usa_summer_2020.pkl。如果将其更改为 covid_usa_summer_2020.zip,则输出将是存储在 ZIP 格式中的压缩二进制序列化文件。您可以尝试以下示例:

zip_output =\
Path('../../datasets/Ch4/covid_usa_summer_2021.zip')
# Write the Dataframe
df_usa_summer_unpivoted.to_pickle(zip_output)
# Read the Dataframe
pd.read_pickle(zip_output)

其他支持的压缩模式包括 gzipbz2tarxz

第二个参数是 protocol。默认情况下,DataFrame.to_pickle() 写入函数使用最高协议,截至本文编写时,该协议设置为 5。根据 Pickle 文档,当进行 pickle 时,有六个(6)不同的协议可供选择,从协议版本 0 到最新的协议版本 5。

在 pandas 之外,您可以使用以下命令检查最高协议配置是什么:

pickle.HIGHEST_PROTOCOL
>> 5

同样,默认情况下,pickle.dump() 使用 HIGHEST_PROTOCOL 值,如果没有提供其他值的话。该构造如下所示:

with open(output, "wb") as file:
    pickle.dump(df_usa_summer_unpivoted,
                file,
                pickle.HIGHEST_PROTOCOL)
with open(output, "wb") as file:
    pickle.dump(df_usa_summer_unpivoted,
                file,
                5)

前面两个代码片段是等效的。

还有更多……

Pickling(二进制序列化方法)的一个优点是我们几乎可以 pickle 大多数 Python 对象,无论是 Python 字典、机器学习模型、Python 函数,还是更复杂的数据结构,如 pandas DataFrame。然而,某些对象(如 lambda 和嵌套函数)存在一些限制。

让我们来看一下如何将一个函数及其输出进行序列化。你将创建一个covid_by_country函数,该函数需要三个参数:要读取的 CSV 文件回溯的天数国家。该函数将返回一个时间序列 DataFrame。接着你将对函数、函数的输出以及其图形进行序列化:

def covid_by_country(file, days, country):
    ts = pd.read_csv(file)
    ts = ts[ts['Country/Region'] == country]
    final = ts.iloc[:, -days:].sum()
    final.index = pd.to_datetime(final.index,
                                format="%m/%d/%y")
    return final
file = \
Path('../../datasets/Ch4/time_series_covid19_confirmed_global.csv')
us_past_120_days = covid_by_country(file, 200, 'US')
plot_example = \
us_past_120_days.plot(title=f'COVID confirmed case for US',
                xlabel='Date',
                ylabel='Number of Confirmed Cases');

该函数将输出以下图形:

https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/ts-anal-py-cb-2e/img/file33.png

图 4.:– covid_by_country 函数的输出

在将对象序列化之前,你可以通过添加额外的信息来进一步增强内容,以提醒你内容的含义。在以下代码中,你将序列化函数以及返回的 DataFrame,并使用 Python 字典封装附加信息(称为元数据):

from datetime import datetime
metadata = {
    'date': datetime.now(),
    'data': '''
        COVID-19 Data Repository by the
        Center for Systems Science and Engineering (CSSE)
        at Johns Hopkins University'
        ''',
    'author': 'Tarek Atwan',
    'version': 1.0,
    'function': covid_by_country,
    'example_code' : us_past_120_days,
    'example_code_plot': plot_example
}
file_path = Path('../../datasets/Ch4/covid_data.pkl')
with open(file_path, 'wb') as file:
    pickle.dump(metadata, file)

为了更好地理解它是如何工作的,你可以加载内容并使用pickle.load()进行反序列化:

with open(output, 'rb') as file:
    content = pickle.load(file)
content.keys()
>>
dict_keys(['date', 'data', 'author', 'version', 'function', 'example_df', 'example_plot'])

你可以按照以下代码检索并使用该函数:

file_path =\
Path('../../datasets/Ch4/time_series_covid19_confirmed_global.csv')
loaded_func = content['function']
loaded_func(file_path, 120, 'China').tail()
>>
2023-03-05    4903524
2023-03-06    4903524
2023-03-07    4903524
2023-03-08    4903524
2023-03-09    4903524
dtype: int64

你也可以检索之前为美国存储的 DataFrame:

loaded_df = content['example_df']
loaded_df.tail()
>>
2023-03-05    103646975
2023-03-06    103655539
2023-03-07    103690910
2023-03-08    103755771
2023-03-09    103802702
dtype: int64

你也可以加载你刚刚存储的图形视图。以下代码将展示类似于图 4.2中的图形:

loaded_plot = content['example_plot']
loaded_plot.get_figure()

上述示例展示了序列化如何有助于存储对象及附加的元数据信息。这在存储一个正在进行的工作或执行多个实验并希望跟踪它们及其结果时非常有用。在机器学习实验中也可以采用类似的方法,因为你可以存储模型及与实验和其输出相关的任何信息。

另见

写入 CSV 及其他分隔符文件

在本例中,你将导出一个 DataFrame 为 CSV 文件,并利用DataFrame.to_csv()写入函数中的不同参数。

准备工作

该文件已提供在本书的 GitHub 代码库中,你可以在这里找到:github.com/PacktPublishing/Time-Series-Analysis-with-Python-Cookbook。你将使用的文件名为movieboxoffice.csv,首先读取该文件以创建你的 DataFrame。

为了准备本例,你将使用以下代码将文件读取为一个 DataFrame:

import pandas as pd
from pathlib import Path
filepath = Path('../../datasets/Ch4/movieboxoffice.csv')
movies = pd.read_csv(filepath,
                 header=0,
                 parse_dates=[0],
                 index_col=0,
                 usecols=['Date',
                          'Daily'],
                date_format="%d-%b-%y")
movies.info()
>>
<class 'pandas.core.frame.DataFrame'>
DatetimeIndex: 128 entries, 2021-04-26 to 2021-08-31
Data columns (total 1 columns):
 #   Column  Non-Null Count  Dtype
---  ------  --------------  -----
 0   Daily   128 non-null    object
dtypes: object(1)
memory usage: 2.0+ KB

你现在有了一个以DatetimeIndex为索引的时间序列 DataFrame。

如何做……

使用 pandas 将 DataFrame 写入 CSV 文件非常简单。DataFrame 对象可以访问许多写入方法,如 .to_csv,你将在接下来的步骤中使用这个方法:

  1. 你将使用 pandas DataFrame 写入方法来将 DataFrame 持久化为 CSV 文件。该方法有多个参数,但至少你需要传递文件路径和文件名:
output = Path('../../datasets/Ch4/df_movies.csv')
movies.to_csv(output)

默认情况下,创建的 CSV 文件是 逗号分隔的

  1. 要更改分隔符,可以使用 sep 参数并传入不同的参数。在以下代码中,你将创建一个管道符 (|) 分隔的文件:
output = Path('../../datasets/Ch4/piped_df_movies.csv')
movies.to_csv(output, sep='|')
  1. 读取管道分隔的文件并检查生成的 DataFrame 对象:
movies_df = pd.read_csv(output, sep='|')
movies_df.info()
>>
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 128 entries, 0 to 127
Data columns (total 2 columns):
 #   Column  Non-Null Count  Dtype
---  ------  --------------  -----
 0   Date    128 non-null    object
 1   Daily   128 non-null    object
dtypes: object(2)
memory usage: 2.1+ KB

从上面的输出可以看出,读取 CSV 文件时丢失了一些信息。例如,原始 DataFrame “movies” 中的 Date 列实际上不是列,而是 DatetimeIndex 类型的索引。当前的 DataFrame “movies_df” 并没有 DatetimeIndex 类型的索引(现在的索引是 RangeIndex 类型,仅为行号的范围)。这意味着你需要配置 read_csv() 函数并传递必要的参数,以便正确解析文件(这与读取 pickle 文件的情况不同,正如前面的示例中所演示的,使用 pickle 序列化时间序列数据)。

一般来说,CSV 文件格式不会保留索引类型或列数据类型信息。

它是如何工作的……

DataFrame.to_csv() 的默认行为是根据默认的 sep 参数(默认为 ",")写入一个 逗号分隔的 CSV 文件。你可以通过传递不同的分隔符来覆盖这个默认行为,例如制表符 ("\t")、管道符 ("|") 或分号 (";")

以下代码展示了不同 分隔符 及其表示方式:

# tab "\t"
Date    DOW Daily   Avg To Date Day Estimated
2019-04-26  Friday  157461641   33775   157461641   1   False
2019-04-27  Saturday    109264122   23437   266725763   2   False
2019-04-28  Sunday  90389244    19388   357115007   3   False
# comma ","
Date,DOW,Daily,Avg,To Date,Day,Estimated
2019-04-26,Friday,157461641,33775,157461641,1,False
2019-04-27,Saturday,109264122,23437,266725763,2,False
2019-04-28,Sunday,90389244,19388,357115007,3,False
# semicolon ";"
Date;DOW;Daily;Avg;To Date;Day;Estimated
2019-04-26;Friday;157461641;33775;157461641;1;False
2019-04-27;Saturday;109264122;23437;266725763;2;False
2019-04-28;Sunday;90389244;19388;357115007;3;False
# pipe "|"
Date|DOW|Daily|Avg|To Date|Day|Estimated
2019-04-26|Friday|157461641|33775|157461641|1|False
2019-04-27|Saturday|109264122|23437|266725763|2|False
2019-04-28|Sunday|90389244|19388|357115007|3|False

还有更多内容……

注意,在上面的示例中,逗号分隔的字符串值没有被双引号("")包围。如果我们的字符串对象包含逗号(,)并且我们将其写入逗号分隔的 CSV 文件,会发生什么呢?让我们看看 pandas 如何处理这个场景。

在以下代码中,我们将创建一个 person DataFrame:

import pandas as pd
person = pd.DataFrame({
     'name': ['Bond, James', 'Smith, James', 'Bacon, Kevin'],
     'location': ['Los Angeles, CA', 'Phoenix, AZ', 'New York, NY'],
     'net_worth': [10000, 9000, 8000]
    })
print(person)
>>
           name         location  net_worth
0   Bond, James  Los Angeles, CA      10000
1  Smith, James      Phoenix, AZ       9000
2  Bacon, Kevin     New York, NY       8000

现在,将 DataFrame 导出为 CSV 文件。你将指定 index=False 来忽略导出的索引(行号):

person.to_csv('person_a.csv', index=False)

如果你检查 person_a.csv 文件,你会看到以下表示方式(注意 pandas 添加的双引号):

name,location,net_worth
"Bond, James","Los Angeles, CA",10000
"Smith, James","Phoenix, AZ",9000
"Bacon, Kevin","New York, NY",8000

to_csv() 函数有一个 quoting 参数,默认值为 csv.QUOTE_MINIMAL。这个默认值来自 Python 的 csv 模块,它是 Python 安装的一部分。QUOTE_MINIMAL 参数只会为包含特殊字符的字段加上引号,例如逗号(",")。

csv 模块提供了四个常量,我们可以将它们作为参数传递给 to_csv() 函数中的 quoting 参数。这些常量包括以下内容:

  • csv.QUOTE_ALL:为所有字段加上引号,无论是数字型还是非数字型

  • csv.QUOTE_MINIMALto_csv()函数中的默认选项,用于引用包含特殊字符的值。

  • csv.QUOTE_NONNUMERIC:引用所有非数字字段

  • csv.QUOTE_NONE:不引用任何字段

为了更好地理解这些值如何影响输出的 CSV,你将在以下示例中测试传递不同的引用参数。这是通过使用person DataFrame 来完成的:

import csv
person.to_csv('person_b.csv',
               index=False,
               quoting=csv.QUOTE_ALL)
person.to_csv('person_c.csv',
               index=False,
               quoting=csv.QUOTE_MINIMAL)
person.to_csv('person_d.csv',
               index=False,
               quoting= csv.QUOTE_NONNUMERIC)
person.to_csv('person_e.csv',
               index=False,
               quoting= csv.QUOTE_NONE, escapechar='\t')

现在,如果你打开并检查这些文件,你应该能看到以下表示:

person_b.csv
"name","location","net_worth"
"Bond, James","Los Angeles, CA","10000"
"Smith, James","Phoenix, AZ","9000"
"Bacon, Kevin","New York, NY","8000"
person_c.csv
name,location,net_worth
"Bond, James","Los Angeles, CA",10000
"Smith, James","Phoenix, AZ",9000
"Bacon, Kevin","New York, NY",8000
person_d.csv
"name","location","net_worth"
"Bond, James","Los Angeles, CA",10000
"Smith, James","Phoenix, AZ",9000
"Bacon, Kevin","New York, NY",8000
person_e.csv
name,location,net_worth
Bond, James,Los Angeles , CA,10000
Smith, James,Phoenix , AZ,9000
Bacon, Kevin,New York, NY,8000

注意,在前面的示例中,使用csv.QUOTE_NONE时,你必须为escapechar参数提供额外的参数,否则会抛出错误。

另见

将数据写入 Excel 文件

在本配方中,你将把 DataFrame 导出为 Excel 文件格式,并利用DataFrame.to_excel()写入函数中可用的不同参数。

准备工作

第二章从 Excel 文件读取数据配方中,你需要安装openpyxl作为使用pandas.read_excel()读取 Excel 文件的引擎。在本配方中,你将使用相同的openpyxl作为使用DataFrame.to_excel()写入 Excel 文件的引擎。

要使用conda安装openpyxl,请运行以下命令:

>>> conda install openpyxl

你也可以使用pip

>>> pip install openpyxl

该文件已提供在本书的 GitHub 仓库中,你可以在这里找到:github.com/PacktPublishing/Time-Series-Analysis-with-Python-Cookbook。文件名为movieboxoffice.csv

为了准备这个配方,你将使用以下代码将文件读取到一个 DataFrame 中:

import pandas as pd
from pathlib import Path
filepath = Path('../../datasets/Ch4/movieboxoffice.csv')
movies = pd.read_csv(filepath,
                 header=0,
                 parse_dates=[0],
                 index_col=0,
                 usecols=['Date',
                          'Daily'],
                date_format="%d-%b-%y")

如何操作…

要将 DataFrame 写入 Excel 文件,你需要提供包含filenamesheet_name参数的写入函数。文件名包含文件路径和名称。确保文件扩展名为.xlsx,因为你使用的是 openpyxl。

DataFrame.to_excel()方法将根据文件扩展名来决定使用哪个引擎,例如.xlsx.xls。你也可以通过engine参数明确指定使用的引擎,示例如下:

  1. 确定文件输出的位置,并将文件路径、所需的工作表名称以及引擎传递给DataFrame.to_excel()写入函数:
output = \
Path('../../datasets/Ch4/daily_boxoffice.xlsx')
movies.to_excel(output,
               sheet_name='movies_data',
               engine='openpyxl', # default engine for xlsx files
               index=True)

前面的代码将在指定位置创建一个新的 Excel 文件。你可以打开并检查该文件,如下图所示:

https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/ts-anal-py-cb-2e/img/file34.png

图 4.3:来自 daily_boxoffice.xlsx 文件的示例输出

注意,工作表名称是movies_data。在 Excel 文件中,你会注意到Date列的格式与预期的不符。假设预期Date列是一个特定格式,比如MM-DD-YYYY

使用read_excel读取相同文件将正确读取Date列,符合预期。

  1. 为了实现这一点,你将使用另一个由 pandas 提供的类,pandas.ExcelWriter类为我们提供了两个用于日期格式化的属性:datetime_formatdate_format。这两个参数在使用xlsxwriter引擎时效果很好,但截至目前,openpyxl 集成存在一个已知的 bug。openpyxl 相较于 xlsxwriter 有几个优势,尤其是在追加现有 Excel 文件时。我们将利用 openpyxl 的number_format属性来修复这个问题。以下代码展示了如何实现这一点:
date_col = 'Date'
with pd.ExcelWriter(output, 
                    engine='openpyxl',
                    mode='a',
                    if_sheet_exists='replace') as writer:
    movies.to_excel(writer, sheet_name='movies_fixed_dates', index=True)

    worksheet = writer.sheets['movies_fixed_dates']
    for col in worksheet.iter_cols():
        header = col[0] # capture headers
        if header.value == date_col:
            for row in range(2, # skip first row
                             worksheet.max_row+1):
                    worksheet.cell(
                        row,
                        header.column
                                  ).number_format='MM-DD-YYYY'

以下是新输出的表现形式。这是通过将MM-DD-YYYY传递给writer对象的datetime_format属性实现的:

https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/ts-anal-py-cb-2e/img/file35.png

图 4.4:使用 pd.ExcelWriter 和 number_format 将 Date 列格式更新为 MM-DD-YYYY

它是如何工作的……

DataFrame.to_excel()方法默认会创建一个新的 Excel 文件(如果文件不存在)或覆盖文件(如果文件已存在)。要向现有的 Excel 文件追加内容或写入多个工作表,你需要使用Pandas.ExcelWriter类。ExcelWriter()类有一个mode参数,可以接受"w"(写入)或"a"(追加)。截至目前,xlsxwriter 不支持追加模式,而 openpyxl 支持两种模式。

请记住,在ExcelWriter中,默认模式设置为"w"(写入模式),因此,如果未指定"a"(追加模式),将导致覆盖 Excel 文件(任何现有内容将被删除)。

此外,在使用追加模式(mode="a")时,你需要通过if_sheet_exists参数指定如何处理现有的工作表,该参数接受以下三种值之一:

  • error,会引发ValueError异常。

  • replace,它会覆盖现有的工作表。

  • new,创建一个具有新名称的新工作表。如果重新执行前面的代码并更新if_sheet_exists='new',那么将创建一个新的工作表并命名为movies_fixed_dates1

还有更多……

如果你需要在同一个 Excel 文件中创建多个工作表,那么ExcelWriter可以帮助实现这一目标。例如,假设目标是将每个月的数据分开到自己的工作表中,并按月命名工作表。在下面的代码中,你将添加一个Month列并使用它按月拆分 DataFrame,使用groupby将每个组写入一个新工作表。

首先,让我们创建辅助函数sheet_date_format,将每个工作表中的Date列格式化为 MM-DD-YYYY 格式:

def sheet_date_format(sheet_name, writer, date_col):
    worksheet = writer.sheets[sheet_name]

    for col in worksheet.iter_cols():
        header = col[0]
        if header.value == date_col:
            for row in range(2, worksheet.max_row+1):
                    worksheet.cell(
                        row,
                        header.column).number_format='MM-DD-YYYY'

接下来的代码将向 movies DataFrame 添加一个“Month”列,然后将每个月的数据写入独立的工作表,并为每个工作表命名为相应的月份名称:

movies['Month'] = movies.index.month_name()
output = Path('../../datasets/Ch4/boxoffice_by_month.xlsx')
with pd.ExcelWriter(output,
                    engine='openpyxl') as writer:
    for month, data in movies.groupby('Month'):
        data.to_excel(writer, sheet_name=month)
        sheet_date_format(month, writer, date_col='Date')

上面的代码将创建一个名为boxoffice_by_month.xlsx的新 Excel 文件,并为每个月创建五个工作表,如下图所示:

https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/ts-anal-py-cb-2e/img/file36.png

图 4.5:movies DataFrame 中的每个月都被写入到各自的 Excel 工作表中

另见

pandas 的to_excel()方法和ExcelWriter类使将 DataFrame 写入 Excel 文件变得非常方便。如果您需要对 pandas DataFrame 以外的部分进行更精细的控制,您应该考虑使用已安装的openpyxl库作为读取/写入引擎。例如,openpyxl库有一个用于处理 pandas DataFrame 的模块(openpyxl.utils.dataframe)。一个例子是dataframe_to_rows()函数。

将数据存储到云存储(AWS、GCP 和 Azure)

在本教程中,您将使用 pandas 将数据写入云存储,如 Amazon S3、Google Cloud Storage 和 Azure Blob Storage。多个 pandas 写入函数支持通过storage_options参数将数据写入云存储。

准备工作

第二章的“从 URL 读取数据”中,您被要求安装boto3s3fs来从 AWS S3 桶读取数据。在本教程中,除了需要的 Google Cloud Storage(gcsfs)和 Azure Blob Storage(adlfs)库外,您还将使用相同的库。

使用pip安装,您可以使用以下命令:

>>> pip install boto3 s3fs
>>> pip install google-cloud-storage gcsfs
>>> pip install adlfs azure-storage-blob azure-identity

使用conda安装,您可以使用以下命令:

>>> conda install -c conda-forge boto3 s3fs -y
>>> conda install -c conda-forge google-cloud-storage gcsfs -y
>>> conda install -c conda-forge adlfs azure-storage-blob azure-identity -y

您将使用我们在前一个教程中创建的boxoffice_by_month.xlsx文件,将数据写入 Excel 文件。该文件可在本书的 GitHub 仓库中找到,链接如下:github.com/PacktPublishing/Time-Series-Analysis-with-Python-Cookbook

为了准备这个操作,你将使用以下代码将文件读取到一个 DataFrame 中:

import pandas as pd
from pathlib import Path
source = "../../datasets/Ch4/boxoffice_by_month.xlsx"
movies = pd.concat(pd.read_excel(source,
             sheet_name=None,
             index_col='Date',
             parse_dates=True)).droplevel(0)
print(movies.head())
                   Daily  Month
Date                           
2021-04-26   $125,789.89   April
2021-04-27    $99,374.01   April
2021-04-28    $82,203.16   April
2021-04-29    $33,530.26   April
2021-04-30    $30,105.24   April

请注意,movie DataFrame 有两列(Daily 和 Month),并且有一个 DatetimeIndex(Date)。

接下来,你将把 AWS、Google Cloud 和 Azure 的凭证存储在 Python 脚本外部的cloud.cfg配置文件中。然后,使用configparser读取并将值存储在 Python 变量中。你不希望将凭证暴露或硬编码在代码中:

# Example of configuration file "cloud.cfg file"
[AWS]
aws_access_key=<your_access_key>
aws_secret_key=<your_secret_key>
[GCP]
key_file_path=<GCPKeyFileexample.json>
[AZURE]
storage_account_key=<your_storageaccount_key>

然后,我们可以使用config.read()加载aws.cfg文件:

import configparser
config = configparser.ConfigParser()
config.read('cloud.cfg')
AWS_ACCESS_KEY = config['AWS']['aws_access_key']
AWS_SECRET_KEY = config['AWS']['aws_secret_key']
AZURE_ACCOUNT_KEY = config['AZURE']['storage_account_key']
GCP_KEY_FILE = config['GCP']['key_file_path']

如何实现…

多个 pandas 写入函数支持直接写入远程或云存储文件系统,例如 AWS 的s3://、Google 的gs://以及 Azure 的abfs://az://协议。这些写入函数提供了storage_options参数,支持与远程文件存储系统的协作。这部分得益于 pandas 使用fsspec来处理非 HTTP(s)的系统 URL,例如每个云存储专用的 URL。对于每个云存储,你需要使用特定的文件系统实现,例如,AWS S3 使用s3fs,Google Cloud 使用gcsfs,Azure 使用adlfs

storage_options参数接受一个 Python 字典,用于提供附加信息,如凭证、令牌或云提供商要求的任何信息,以键值对的形式提供。

使用 pandas 写入 Amazon S3

在本节中,你将使用 pandas 将movies DataFrame 写入tscookbook-private S3 桶,保存为 CSV 和 Excel 文件:

若干 pandas 写入函数,如to_csvto_parquetto_excel,允许你通过storage_accounts参数传递 AWS S3 特定的凭证(keysercret),这些凭证在s3fs中有说明。以下代码展示了如何利用to_csvto_excel将你的 movies DataFrame 写入tscookbook S3 桶,分别保存为movies_s3.csvmovies_s3.xlsx

# Writing to Amazon S3
movies.to_csv('s3://tscookbook-private/movies_s3.csv',
               storage_options={
                   'key': AWS_ACCESS_KEY,
                   'secret': AWS_SECRET_KEY
               })
movies.to_excel('s3://tscookbook-private/movies_s3.xlsx',
               storage_options={
                   'key': AWS_ACCESS_KEY,
                   'secret': AWS_SECRET_KEY
               })

以下图示展示了tscookbook-private桶的内容:

https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/ts-anal-py-cb-2e/img/file37.png

图 4.6: 使用 pandas 成功写入 AWS S3 的 movies_s3.csv 和 movie_s3.xlsx

使用 pandas 写入 Google Cloud Storage

在本节中,你将使用 pandas 将movies DataFrame 写入 Google Cloud Storage 的tscookbook桶,保存为 CSV 和 Excel 文件:

在使用 Google Cloud 时,你将使用存储为 JSON 文件的服务帐户私钥。这个文件可以从 Google Cloud 生成并下载。在storage_options中,你将传递文件路径。以下代码展示了如何使用to_csvto_excel将你的 movies DataFrame 写入tscookbook桶,分别保存为movies_gs.csvmovies_gs.xlsx

# Writing to Google Cloud Storage
movies.to_csv('gs://tscookbook/movies_gs.csv',
               storage_options={'token': GCP_KEY_FILE})
movies.to_excel('gs://tscookbook/movies_gs.xlsx',
               storage_options={'token': GCP_KEY_FILE})

以下图示展示了tscookbook桶的内容:

https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/ts-anal-py-cb-2e/img/file38.png

图 4.7: 使用 pandas 成功写入 Google Cloud Storage 的 movies_gs.csv 和 movie_gs.xlsx

使用 pandas 向 Azure Blob 存储写入数据

在这一部分,你将使用 pandas 将movies DataFrame 以 CSV 文件的形式写入 Azure Blob 存储中的名为objects的容器:

在使用 Azure Blob 存储时,你可以使用abfs://az://协议。在storage_options中,你将传递account_key,这是你在 Azure 存储账户中的 API 密钥。以下代码展示了如何利用to_csv将你的 movies DataFrame 写入objects容器。下面的三段代码是等效的,并展示了你需要传递的不同 URI 和storage_options

# Writing to Azure Blob Storage
movies.to_csv("abfs://objects@tscookbook.dfs.core.windows.net/movies_abfs.csv",
             storage_options={
                 'account_key': AZURE_ACCOUNT_KEY
             })
movies.to_csv("az://objects@tscookbook.dfs.core.windows.net/movies_az.csv",
             storage_options={
                 'account_key': AZURE_ACCOUNT_KEY
             })
movies.to_csv("az://objects/movies_az2.csv",
             storage_options={
                 'account_name': "tscookbook",
                 'account_key': AZURE_ACCOUNT_KEY
 })

下图展示了objects容器的内容:

https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/ts-anal-py-cb-2e/img/file39.png

图 4.8:movies_abfs.csv、movies_az.csv 和 movie_az2.csv 成功写入 Azure Blob 存储,使用 pandas

它是如何工作的……

在前面的代码部分,我们使用了DataFrame.to_csv()DataFrame.to_excel()方法,将数据写入 Amazon S3、Azure Blob 存储和 Google Cloud 存储。storage_options参数允许传递一个包含存储连接所需信息的键值对;例如,AWS S3 需要传递keysecret,GCP 需要token,而 Azure 需要account_key

支持storage_options的 pandas DataFrame 写入函数示例包括:

  • Pandas.DataFrame.to_excel()

  • Pandas.DataFrame.to_json()

  • Pandas.DataFrame.to_parquet()

  • Pandas.DataFrame.to_pickle()

  • Pandas.DataFrame.to_markdown()

  • Pandas.DataFrame.to_pickle()

  • Pandas.DataFrame.to_stata()

  • Pandas.DataFrame.to_xml()

还有更多……

为了更细粒度的控制,你可以使用 AWS(boto3)、Google Cloud(google-cloud-storage)或 Azure(azure-storage-blob)的特定 Python SDK 来写入数据。

首先,我们将把我们的电影 DataFrame 存储为 CSV 格式,以便将数据上传到不同的云存储服务。

data = movies.to_csv(encoding='utf-8', index=True)

注意,index=True是因为我们的日期列是索引,我们需要确保它在写入 CSV 文件时作为列被包含。

使用 boto3 库向 Amazon S3 写入数据

你将探索资源 API客户端 API。资源 API 是一个更高级的抽象,它简化了代码并与 AWS 服务的交互。与此同时,客户端 API 提供了一个低级别的抽象,允许对 AWS 服务进行更细粒度的控制。

当使用资源 API 与boto3.resource("s3")时,你首先需要通过提供 S3 桶名称和对象键(文件名)来创建一个对象资源。一旦定义,你将可以访问多个方法,包括copydeleteputdownload_fileloadgetupload等。put方法将把一个对象添加到定义的 S3 桶中。

使用boto3.client("s3")客户端 API 时,你可以访问许多 Bucket 和 Object 级别的方法,包括create_bucketdelete_bucketdownload_fileput_objectdelete_objectget_bucket_lifecycleget_bucket_locationlist_buckets等。put_object方法将把一个对象添加到定义的 S3 存储桶中。

import boto3
bucket = "tscookbook-private"
# Using the Resource API
s3_resource = boto3.resource("s3",
            aws_access_key_id = AWS_ACCESS_KEY,
            aws_secret_access_key = AWS_SECRET_KEY)
s3_resource.Object(bucket, 'movies_boto3_resourceapi.csv').put(Body=data)
# Using the Client API
s3_client = boto3.client("s3",
            aws_access_key_id = AWS_ACCESS_KEY,
            aws_secret_access_key = AWS_SECRET_KEY)
s3_client.put_object(Body=data, Bucket=bucket, Key='movies_boto3_clientapi.csv')
使用 google-cloud-storage 库写入 Google Cloud Storage

你首先需要创建一个客户端对象,这是来自存储模块的 Client 类的一个实例。你将使用服务账户的 JSON 密钥文件进行身份验证。通过from_service_account_json方法指定该文件。你将使用bucketblob方法创建一个引用,指向你希望放入 Google Storage 中tscookbook存储桶的 blob 对象。最后,你可以使用upload_from_string方法将数据上传到指定的 blob 对象中。

from google.cloud import storage
# Authenticate using the service account key
storage_client = storage.Client.from_service_account_json(GCP_KEY_FILE)
bucket_name = 'tscookbook'
file_path = 'movies_gsapi.csv'
blob = storage_client.bucket(bucket_name).blob(file_path)
blob.upload_from_string(data)
使用 azure-storage-blob 库写入 Azure Blob Storage

你将首先创建一个BlobServiceClient对象,并使用 Azure Storage Account API 密钥进行身份验证。然后,你将使用get_blob_client为指定的容器创建 blob 对象,并使用upload_blob方法将数据上传到指定的对象中。

from azure.storage.blob import BlobServiceClient
blob_service_client = BlobServiceClient(
        account_url="https://tscookbook.blob.core.windows.net",
        credential=AZURE_ACCOUNT_KEY)
blob_client = blob_service_client.get_blob_client(
    container='objects',
    blob='movies_blobapi.csv')
blob_client.upload_blob(data)

另见

若要了解更多关于如何使用 Python 管理云存储的信息,请查看这些流行库的官方文档

写入大数据集

在本示例中,你将探索不同文件格式的选择如何影响整体的写入和读取性能。你将探索 Parquet、优化行列式(ORC)和 Feather,并将它们的性能与其他流行的文件格式,如 JSON 和 CSV,进行比较。

这三种文件格式 ORC、Feather 和 Parquet 是列式文件格式,适用于分析需求,并且总体上显示出更好的查询性能。这三种文件格式也得到了 Apache Arrow(PyArrow)的支持,后者提供了内存中的列式格式,优化了数据分析性能。为了将这种内存中的列式数据持久化并存储,你可以使用 pandas 的to_orcto_featherto_parquet写入函数将数据持久化到磁盘。

Arrow 提供数据的内存表示,采用列式格式,而 Feather、ORC 和 Parquet 则允许我们将这种表示存储到磁盘中。

准备工作

在本示例中,您将使用来自(www.nyc.gov/site/tlc/about/tlc-trip-record-data.page)的纽约出租车数据集,我们将处理 2023 年的黄出租车行程记录。

在以下示例中,我们将使用这些文件之一,yellow_tripdata_2023-01.parquet,但您可以选择其他任何文件来跟随学习。在第二章从 Parquet 文件读取数据示例中,您安装了PyArrow。以下是使用 Conda 或 Pip 安装 PyArrow 的说明。

要使用conda安装 PyArrow,运行以下命令:

conda install -c conda-forge pyarrow

要使用pip安装 PyArrow,运行以下命令:

pip install pyarrow

为了准备本示例,您将使用以下代码将文件读取到 DataFrame 中:

import pandas as pd
from pathlib import Path
file_path = Path('yellow_tripdata_2023-01.parquet')
df = pd.read_parquet(file_path, engine='pyarrow')
df.info()
>>
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 3066766 entries, 0 to 3066765
Data columns (total 19 columns):
 #   Column                 Dtype        
---  ------                 -----        
 0   VendorID               int64        
 1   tpep_pickup_datetime   datetime64[us]
 2   tpep_dropoff_datetime  datetime64[us]
 3   passenger_count        float64      
 4   trip_distance          float64      
 5   RatecodeID             float64      
 6   store_and_fwd_flag     object       
 7   PULocationID           int64        
 8   DOLocationID           int64        
 9   payment_type           int64        
 10  fare_amount            float64      
 11  extra                  float64      
 12  mta_tax                float64      
 13  tip_amount             float64      
 14  tolls_amount           float64      
 15  improvement_surcharge  float64      
 16  total_amount           float64      
 17  congestion_surcharge   float64      
 18  airport_fee            float64      
dtypes: datetime64us, float64(12), int64(4), object(1)
memory usage: 444.6+ MB

如何实现

您将把 DataFrame 写入不同的文件格式,并随后比较压缩效率(文件大小)、写入和读取速度。

为了实现这一点,您需要创建一个返回文件大小的函数:

import os
def size_in_mb(file):
    size_bytes = os.path.getsize(file)
    size_m = size_bytes / (1024**2)
    return round(size_m,2)

该函数将获取您创建的文件并返回文件大小(单位:MB)。os.path.getsize()将返回文件大小(单位:字节),而size_bytes / (1024**2)这一行将其转换为兆字节(MB)。

我们将把这些文件写入formats文件夹,以便稍后可以从该文件夹读取以评估读取性能。

写入为 JSON 和 CSV

您将使用DataFrame.to_json()方法写入一个yellow_tripdata.json文件:

%%time
df.to_json('formats/yellow_tripdata.json', orient='records')
size_in_mb('formats/yellow_tripdata.json')
>>
CPU times: user 4.63 s, sys: 586 ms, total: 5.22 s
Wall time: 5.24 s
1165.21

请注意,文件大小约为 1.16 GB,耗时约为 5.24 秒。

您将使用DataFrame.to_csv()方法写入一个yellow_tripdata.csv文件:

%%time
df.to_csv('formats/yellow_tripdata.csv', index=False)
size_in_mb('formats/yellow_tripdata.csv')
>>
CPU times: user 16.7 s, sys: 405 ms, total: 17.1 s
Wall time: 17.1 s
307.04

请注意,文件大小约为 307 MB,耗时约为 17.1 秒。

写入为 Parquet

to_parquet写入函数支持多种压缩算法,包括snappyGZIPbrotliLZ4ZSTD。您将使用DataFrame.to_parquet()方法写入三个文件,以比较snappyLZ4ZSTD压缩算法:

%%time
df.to_parquet('formats/yellow_tripdata_snappy.parquet',
              compression='snappy')
size_in_mb('formats/yellow_tripdata_snappy.parquet')
>>
CPU times: user 882 ms, sys: 24.2 ms, total: 906 ms
Wall time: 802 ms
59.89
%%time
df.to_parquet('formats/yellow_tripdata_lz4.parquet',
              compression='lz4')
size_in_mb('formats/yellow_tripdata_lz4.parquet')
>>
CPU times: user 898 ms, sys: 20.4 ms, total: 918 ms
Wall time: 817 ms
59.92
%%time
df.to_parquet('formats/yellow_tripdata_zstd.parquet',
              compression='zstd')
size_in_mb('formats/yellow_tripdata_zstd.parquet')
>>
CPU times: user 946 ms, sys: 24.2 ms, total: 970 ms
Wall time: 859 ms
48.95

注意,三种压缩算法产生相似的压缩结果(文件大小)和速度。

写入为 Feather

您将使用DataFrame.to_feather()方法,使用两个支持的压缩算法LZ4ZSTD写入三个 feather 文件。最后一个文件格式将是未压缩的格式,以便进行比较:

%%time
df.to_feather('formats/yellow_tripdata_uncompressed.feather', compression='uncompressed')
size_in_mb('formats/yellow_tripdata_uncompressed.feather')
>>
CPU times: user 182 ms, sys: 75.5 ms, total: 257 ms
Wall time: 291 ms
435.84
%%time
df.to_feather('formats/yellow_tripdata_lz4.feather', compression='lz4')
size_in_mb('formats/yellow_tripdata_lz4.feather')
>>
CPU times: user 654 ms, sys: 42.1 ms, total: 696 ms
Wall time: 192 ms
116.44
%%time
df.to_feather('formats/yellow_tripdata_zstd.feather', compression='zstd', compression_level=3)
size_in_mb('formats/yellow_tripdata_zstd.feather')
>>
CPU times: user 1 s, sys: 39.2 ms, total: 1.04 s
Wall time: 243 ms
61.79
  1. 注意未压缩文件、使用 LZ4 和 ZSTD 压缩算法之间的文件大小差异。您可以进一步探索compression_level来找到最佳输出。总体而言,LZ4 在写入和读取(压缩解压缩速度)上提供了出色的性能。ZSTD 算法可能提供更高的压缩比,生成更小的文件,但其速度可能不如 LZ4。
写入为 ORC

类似于 Feather 和 Parquet 文件格式,ORC 支持不同的压缩算法,包括无压缩、snappyZLIBLZ4ZSTD。你将使用 DataFrame.to_orc() 方法写入三个 ORC 文件,以探索 ZSTD 和 LZ4 压缩算法,并将无压缩文件作为对比:

%%time
df.to_orc('formats/yellow_tripdata_uncompressed.orc',
          engine_kwargs={'compression':'uncompressed'})
size_in_mb('formats/yellow_tripdata_uncompressed.orc')
>>
CPU times: user 989 ms, sys: 66.3 ms, total: 1.06 s
Wall time: 1.01 s
319.94
%%time
df.to_orc(' formats /yellow_tripdata_lz4.orc',
          engine_kwargs={'compression':'lz4'})
size_in_mb('formats/yellow_tripdata_lz4.orc')
>>
CPU times: user 1 s, sys: 67.2 ms, total: 1.07 s
Wall time: 963 ms
319.65
%%time
df.to_orc('yellow_tripdata_zstd.orc',
          engine_kwargs={'compression':'zstd'})
size_in_mb('formats/yellow_tripdata_zstd.orc')
>>
CPU times: user 1.47 s, sys: 46.4 ms, total: 1.51 s
Wall time: 1.42 s
53.58

注意,LZ4 算法在与无压缩版本进行比较时并未提供更好的压缩效果。ZSTD 算法确实提供了更好的压缩效果,但执行时间稍长。

它的工作原理…

通常,在处理需要完成转换后持久化到磁盘的大型数据集时,决定选择哪种文件格式会显著影响整体的数据存储策略。

例如,JSON 和 CSV 格式是人类可读的格式,几乎任何商业或开源的数据可视化或分析工具都可以处理这些格式。CSV 和 JSON 格式不支持大文件的压缩,会导致写入和读取操作的性能较差。另一方面,Parquet、Feather 和 ORC 是二进制文件格式(不可读),但支持多种压缩算法,并且是基于列的,这使得它们非常适合用于分析应用程序,具有快速的读取性能。

pandas 库通过 PyArrow 支持 Parquet、Feather 和 ORC,PyArrow 是 Apache Arrow 的 Python 封装。

还有更多内容…

你已经评估了不同文件格式的写入性能(及大小)。接下来,你将比较读取时间性能以及各种文件格式和压缩算法的效率。

为此,你将创建一个函数(measure_read_performance),该函数会读取指定文件夹中的所有文件(例如,formats 文件夹)。该函数将评估每个文件扩展名(例如,.feather.orc.json.csv.parquet),以确定应使用哪种 pandas 读取函数。然后,该函数会捕获每个文件格式的性能时间,附加结果,并返回一个按读取时间排序的包含所有结果的 DataFrame。

import pandas as pd
import os
import glob
import time
def measure_read_performance(folder_path):
  performance_data = []
  for file_path in glob.glob(f'{folder_path}/*'):
    _, ext = os.path.splitext(file_path)
    start_time = time.time()

    if ext == '.csv':
      pd.read_csv(file_path, low_memory=False)
    elif ext == '.parquet':
      pd.read_parquet(file_path)
    elif ext == '.feather':
      pd.read_feather(file_path)
    elif ext == '.orc':
      pd.read_orc(file_path)
    elif ext == '.json':
      pd.read_json(file_path)
    end_time = time.time()
    performance_data.append({'filename': file_path,
                             'read_time': end_time - start_time})
    df = pd.DataFrame(performance_data)
  return df.sort_values('read_time').reset_index(drop=True)

你可以通过指定文件夹(例如,formats 文件夹)来执行该函数,以显示最终结果:

results =\
    measure_read_performance(folder_path='formats')
print(results)
>>
                                        filename  read_time
0            formats/yellow_tripdata_lz4.parquet   0.070845
1         formats/yellow_tripdata_snappy.parquet   0.072083
2           formats/yellow_tripdata_zstd.parquet   0.078382
3            formats/yellow_tripdata_lz4.feather   0.103172
4           formats/yellow_tripdata_zstd.feather   0.103918
5   formats/yellow_tripdata_uncompressed.feather   0.116974
6               formats/yellow_tripdata_zstd.orc   0.474430
7       formats/yellow_tripdata_uncompressed.orc   0.592284
8                formats/yellow_tripdata_lz4.orc   0.613846
9                    formats/yellow_tripdata.csv   4.557402
10                  formats/yellow_tripdata.json  14.590845 

总体而言,读取性能的结果表明,Parquet 文件格式表现最佳,其次是 Feather,然后是 ORC。时间 read_time 以秒为单位。

另见

若要了解更多有关高效数据存储的文件格式,可以参考 pandas。

第五章:5 将时间序列数据持久化到数据库

加入我们在 Discord 上的书籍社区

https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/ts-anal-py-cb-2e/img/file0.png

packt.link/zmkOY

在完成一个数据分析任务后,通常会从源系统提取数据,进行处理、转换并可能建模,最后将结果存储到数据库中以实现持久化。你总是可以将数据存储在平面文件中或导出为 CSV,但在处理大量企业数据(包括专有数据)时,你需要一种更强大且安全的存储方式。数据库提供了多个优势:安全性(静态加密)、并发性(允许多个用户查询数据库而不影响性能)、容错性、ACID合规性、优化的读写机制、分布式计算和分布式存储。

在企业环境中,一旦数据被存储在数据库中,它可以跨不同部门共享;例如,财务、市场营销、销售和产品开发部门现在可以根据自己的需求访问存储的数据。此外,数据现在可以实现民主化,供不同角色的组织人员应用于各种用例,如业务分析师、数据科学家、数据工程师、市场分析师和商业智能开发人员。

在本章中,你将把时间序列数据写入数据库系统以实现持久化。你将探索不同类型的数据库(关系型和非关系型),并使用Python推送你的数据。

更具体地说,你将使用pandas库,因为你会通过使用 pandas 的DataFrame进行大部分的分析。你将学习如何使用 pandas 库将你的时间序列 DataFrame 持久化到数据库存储系统中。许多数据库提供 Python API 和连接器,最近,许多数据库已经支持 pandas DataFrame(用于读取和写入),因为它们的流行和主流应用。在本章中,你将使用关系型数据库、文档数据库、云数据仓库和专门的时间序列数据库。

本章的目的是让你通过与不同方法连接到这些数据库系统,亲身体验如何持久化时间序列 DataFrame。

以下是本章将涵盖的内容列表:

  • 将时间序列数据写入关系型数据库

  • 将时间序列数据写入 MongoDB

  • 将时间序列数据写入 InfluxDB

  • 将时间序列数据写入 Snowflake

写入数据库和权限

记住,当你安装数据库实例或使用云服务时,写入数据是直接的,因为你是所有者/管理员角色。

在任何公司中,这种情况在他们的数据库系统中并不适用。你必须与数据库的所有者、维护者以及可能的 IT 人员、数据库管理员或云管理员对接。在大多数情况下,他们可以允许你将数据写入沙盒或开发环境。然后,一旦完成,可能是同一个团队或另一个团队(如 DevOps 团队)会检查代码并评估性能,之后才会将代码迁移到质量保证(QA)/用户验收测试(UAT)环境。一旦进入该环境,业务部门可能会参与测试并验证数据,以便获得批准。最终,它可能会被推广到生产环境,以便所有人都可以开始使用数据。

技术要求

本章将广泛使用 pandas 2.2.2(于 2024 年 4 月 10 日发布)。

在我们的旅程中,你将安装多个 Python 库来与 pandas 一起使用。这些库在每个配方的准备部分中都有说明。你还可以从 GitHub 仓库下载 Jupyter notebooks,网址为 github.com/PacktPublishing/Time-Series-Analysis-with-Python-Cookbook 来跟着一起练习。

你应该参考 第三章 中的 技术要求 部分,从数据库中读取时间序列数据。这包括创建一个 配置文件,如 database.cfg

本章中所有的配方将使用相同的数据集。该数据集基于 2019 年 1 月至 2023 年 12 月的亚马逊股票数据,通过 yfinance 库获取,并以 pandas DataFrame 的形式存储。

首先安装 yfinance 库,你可以通过 conda 安装,方法如下:

conda install -c conda-forge yfinance

你还可以通过 pip 安装,方法如下:

pip install yfinance

为了了解这个库的工作原理,你将从使用 yfinance 拉取亚马逊股票数据开始。

import yfinance as yf
amzn = yf.Ticker("AMZN")
amzn_hist = amzn.history(start="2019-01-01", end="2023-12-31")
amzn_hist.info()
>>
<class 'pandas.core.frame.DataFrame'>
DatetimeIndex: 1258 entries, 2019-01-02 00:00:00-05:00 to 2023-12-29 00:00:00-05:00
Data columns (total 7 columns):
 #   Column        Non-Null Count  Dtype 
---  ------        --------------  ----- 
 0   Open          1258 non-null   float64
 1   High          1258 non-null   float64
 2   Low           1258 non-null   float64
 3   Close         1258 non-null   float64
 4   Volume        1258 non-null   int64 
 5   Dividends     1258 non-null   float64
 6   Stock Splits  1258 non-null   float64
dtypes: float64(6), int64(1)
memory usage: 78.6 KB

生成的 DataFrame 有七(7)列和 1258 行。它包含一个 DatetimeIndex,格式为 2019-01-02 00:00:00-05:00。我们将重点关注几列(Open、High、Low、Close 和 Volume),并将 DatetimeIndex 的日期时间格式更改为 YYYY-MM-DD

amzn_hist.index = amzn_hist.index.strftime('%Y-%m-%d')
amzn_hist = amzn_hist[['Open', 'High', 'Low', 'Close', 'Volume']]
print(amzn_hist.head())
>>
                 Open       High        Low      Close     Volume
Date                                                            
2019-01-02  73.260002  77.667999  73.046501  76.956497  159662000
2019-01-03  76.000504  76.900002  74.855499  75.014000  139512000
2019-01-04  76.500000  79.699997  75.915497  78.769501  183652000
2019-01-07  80.115501  81.727997  79.459503  81.475502  159864000
2019-01-08  83.234497  83.830498  80.830498  82.829002  177628000

基于前面的示例,我们可以通过创建一个可以在本章中调用的函数来概括这种方法:

import yfinance as yf
def get_stock_data(ticker, start, end):
    stock_data = yf.Ticker(ticker)
    stock_data = stock_data.history(start=start, end=end)
    stock_data.index = stock_data.index.strftime('%Y-%m-%d')
    stock_data = stock_data[['Open', 'High', 'Low', 'Close', 'Volume']]
    return stock_data

get_stock_data 函数将返回一个包含选定列和格式化 DatetimeIndex 的 pandas DataFrame。它需要三个输入:一个 ticker 符号,一个 start 日期和一个 end 日期。如果你想获取从 2024 年 1 月 1 日到今天的股票数据,只需将 end 参数传递为 None。下面是一个示例:

msft = get_stock_data('MSFT', '2024-01-01', None)

这将提供从 2024 年 1 月 1 日到请求时的最新数据的股票数据。

将时间序列数据写入关系型数据库(PostgreSQL 和 MySQL)

在这个配方中,你将把 DataFrame 写入 PostgreSQL 等关系型数据库。对于 SQLAlchemy Python 库支持的任何关系型数据库系统,这种方法都是一样的。你将体验到 SQLAlchemy 如何使得切换后端数据库(称为 dialect)变得简单,而无需更改代码。SQLAlchemy 提供的抽象层使得你能够使用相同的代码在任何支持的数据库之间切换,例如从 PostgreSQL 切换到 Amazon Redshift。

SQLAlchemy 支持的关系型数据库(方言)示例包括以下内容:

  • Microsoft SQL Server

  • MySQL/MariaDB

  • PostgreSQL

  • Oracle

  • SQLite

此外,还可以安装并使用外部方言与 SQLAlchemy 配合使用,以支持其他数据库(方言),如 SnowflakeMicrosoft SQL ServerGoogle BigQuery。请访问 SQLAlchemy 的官方网站,查看可用的方言列表:docs.sqlalchemy.org/en/14/dialects/

准备工作

你应参考第 3 章 中的配方“从关系型数据库中读取数据”,以回顾连接 PostgreSQL 的不同方式。

在此配方中,你将使用 yfinance Python 库来拉取股票数据。

要使用 conda 安装这些库,请运行以下命令:

>> conda install sqlalchemy psycopg

要使用 pip 安装这些库,请运行以下命令:

>> pip install sqlalchemy
>> pip install pyscopg

本书的 GitHub 仓库中提供了文件,你可以在这里找到:github.com/PacktPublishing/Time-Series-Analysis-with-Python-Cookbook

如何操作……

在此配方中,你将使用 yfnance 库从 2019 年 1 月到 2023 年 12 月获取亚马逊的股票数据,并将其存入一个 pandas DataFrame 中,然后将该 DataFrame 写入 PostgreSQL 数据库中的表:

  1. 从调用 技术要求 部分中创建的 get_stock_data 函数开始。
amzn_hist = get_stock_data('AMZN', '2019-01-01', '2023-12-31')
  1. 你需要创建一个 SQLAlchemy engine 对象。该引擎告诉 SQLAlchemy 和 pandas 我们计划与之交互的方言(后端数据库)以及运行中数据库实例的连接详情。利用 URL.create() 方法,通过提供必要的参数(drivernameusernamepasswordhostportdatabase)来创建一个格式正确的 URL 对象。这些参数存储在 database.cfg 文件中。
from sqlalchemy import create_engine, URL
from configparser import ConfigParser
config = ConfigParser()
config.read('database.cfg')
config.sections()
params = dict(config['POSTGRESQL'])
url = URL.create('postgresql+psycopg', **params)
print(url)
>>
postgresql+psycopg://postgres:***@127.0.0.1/postgres

现在,你可以将 url 对象传递给 create_engine

engine = create_engine(url)
print(engine)
>>
Engine(postgresql+psycopg://postgres:***@127.0.0.1/postgres)
  1. 让我们将amz_hist DataFrame 写入 PostgreSQL 数据库实例中的新amzn表。这是通过使用DataFrame.to_sql()写入函数来实现的,该函数利用 SQLAlchemy 的功能将 DataFrame 转换为合适的表模式,并将数据转换为适当的 SQL 语句(如CREATE TABLEINSERT INTO),这些语句特定于方言(后端数据库)。如果表不存在,在加载数据之前会创建一个新表;如果表已存在,你需要提供如何处理表的指令。这是通过if_exists参数来完成的,参数可以接受以下选项之一:'fail''replace''append'
amzn_hist.to_sql('amzn',
            engine,
            if_exists='replace')

完成与前面的代码相同任务的另一种方法是利用with语句,这样你就无需管理连接。这通常是一个更优的做法。

with engine.connect() as connection:
    hist.to_sql('amzn',
                    connection,
                    if_exists='replace')

一旦执行上述代码,一个新的amzn表会在默认的postgres数据库的 public 模式下创建(默认)。

你可以通过以下方式验证数据库中的内容:

from sqlalchemy import text
query = """
SELECT EXISTS (
   SELECT FROM information_schema.tables
   WHERE  table_schema = 'public'
   AND    table_name   = 'amzn'
   );"""
with engine.connect() as conn:
    result = conn.execute(text(query))
print(result.fetchone())
>>
(True,)

注意使用text()函数包裹我们的查询。text()构造了一个新的TextClause,用来表示文本 SQL 字符串。

  1. 通过查询amzn表并统计记录数来确认数据已写入数据库:
query = "select count(*) from amzn;"
with engine.connect() as conn:
    result = conn.execute(text(query))
result.fetchone()
>>
(1258,)
  1. 接下来,使用get_stock_data请求额外的亚马逊股价数据,这次是 2024 年的数据(例如,2024 年 1 月 1 日到 2024 年 9 月 23 日),并将其附加到现有的amzn表中。在这里,你将利用to_sql()写入函数中的if_exists参数。
amzn_hist_2024 = get_stock_data('AMZN', '2024-01-01', None)
print(amzn_hist_2024.shape)
>>
(182, 5)

确保将append传递给if_exists参数,如以下代码所示:

with engine.connect() as connection:
    amzn_hist_2024.to_sql('amzn',
                    connection,
                    if_exists='append')
  1. 统计记录的总数,以确保我们已经将 182 条记录附加到原来的 1258 条记录中。你将运行与之前相同的查询,如以下代码所示:
query = "select count(*) from amzn;"
with engine.connect() as conn:
    result = conn.execute(text(query))
print(result.fetchone())
>>
(1440,)

确实,你可以观察到所有 1440 条记录都已写入amzn表。

它是如何工作的……

使用DataFrame.to_sql()写入函数,SQLAlchemy 在后台处理许多细节,比如创建表的模式、插入记录并提交到数据库。

使用 pandas 和 SQLAlchemy 向关系型数据库写入和读取数据非常相似。我们在第三章从关系型数据库读取数据章节中讨论了使用 SQLAlchemy 读取数据。许多讨论的概念同样适用于这里。

我们总是从create_engine开始,并指定方言(后端数据库)。to_sql()函数会将 DataFrame 的数据类型映射到适当的 PostgreSQL 数据类型。使用**对象关系映射器(ORM)**如 SQLAlchemy 的优势在于,它提供了一个抽象层,使你无需担心将 DataFrame 模式转换为特定数据库模式。

在前面的例子中,你在 DataFrame.to_sql() 函数中使用了 if_exists 参数,并传递了两个不同的参数:

  1. 最初,你将值设置为 replace,这会在表存在时覆盖该表。如果我们将此覆盖操作转换为 SQL 命令,它会执行 DROP TABLE,然后是 CREATE TABLE。如果你已经有一个包含数据的表并打算向其中添加记录,这可能会很危险。因此,如果不传递任何参数,默认值会设置为 fail。这种默认行为会在表已存在时抛出错误。

  2. 在食谱的第二部分,计划是将额外的记录插入到现有表中,并且你将参数从 replace 更新为 append

当你使用 yfinance 拉取股票数据时,它会自动将 Date 字段指定为 DatetimeIndex。换句话说,Date 不是一列,而是一个索引。在 to_sql() 中,默认行为是将 DataFrame 的索引作为数据库中的一列写入,这由 index 参数控制。这个参数是布尔类型,默认值为 True,表示将 DataFrame 索引作为列写入。

另一个非常有用的参数是 chunksize。默认值为 None,表示一次性将 DataFrame 中的所有行写入数据库。如果你的数据集非常庞大,可以使用 chunksize 参数批量写入数据库;例如,设置 chunksize 为 500 会一次性批量写入 500 行数据。

with engine.connect() as connection:
    amzn_hist.to_sql('amzn',
                    connection,
                    chunksize=500,
                    if_exists='append')

还有更多…

使用 pandas 的 read_sqlread_sql_tableread_sql_queryto_sql I/O 函数时,它们需要一个 SQLAlchemy 连接对象(SQLAlchemy 引擎)。要使用 SQLAlchemy 连接到目标数据库,你需要为特定的数据库(例如 Amazon Redshift、Google BigQuery、MySQL、MariaDB、PostgreSQL 或 Oracle)安装相应的 Python DBAPI(驱动程序)。这样你可以一次编写脚本,并且仍然能够与 SQLAlchemy 支持的其他方言(后端数据库)一起使用。为了演示这一点,我们将扩展最后一个例子。

Amazon Redshift 是一款流行的云数据仓库数据库,基于 PostgreSQL 架构,并在其基础上做了多项增强,包括列存储以加速分析查询。你将探索 SQLAlchemy 的简便性,以及其他将 pandas DataFrame 写入 Amazon Redshift 的选项。

使用 SQLAlchemy 向 Amazon Redshift 写入数据

这次你将使用相同的代码,但写入 Amazon Redshift 数据库。除了运行 MySQL 实例之外,唯一的要求是安装适用于的 Python DBAPI(驱动程序)。

Amazon Redshift。注意,sqlalchemy-redshift 需要 psycopg2

要使用 conda 安装,请运行以下命令:

conda install -c conda-forge psycopg2 sqlalchemy-redshift

要使用 pip 安装,请运行以下命令:

pip install pip install psycopg2 sqlalchemy-redshift

你将使用相同的代码来操作 PostgreSQL;唯一的不同是 SQLAlchemy 引擎,它使用的是 Amazon Redshift 的 DBAPI。首先从配置文件中加载连接参数。在这个示例中,配置存储在 database.cfg 文件中。

[AWS]
host=yourendpoint
port=5439
database=dev
username=username
password=password

使用 ConfigParserURL 提取参数并构建 URL:

from configparser import ConfigParser
config = ConfigParser()
config.read('database.cfg')
config.sections()
params = dict(config['AWS'])
from sqlalchemy import URL, create_engine
url = URL.create('redshift+psycopg2', **params)
print(url)
>>
redshift+psycopg2://awsuser:***@redshift-cluster-1.cltc17lacqp7.us-east-1.redshift.amazonaws.com:5439/dev

你现在可以使用以下代码创建引擎:

aws_engine = create_engine(url)

使用 yfinance 库创建一个基于过去 5 年 股票数据的新 amzn_hist DataFrame:

amzn = yf.Ticker("AMZN")
amzn_hist = amzn.history(period="5y")
amzn_hist = amzn_hist[['Open', 'High', 'Low', 'Close', 'Volume']]

在写入 DataFrame 之前,我们需要重置索引。这将使我们恢复 Date 列。我们这样做是因为 Amazon Redshift 不支持传统的索引,因为它是列式数据库(相反,你可以定义一个 排序键)。

amzn_hist = amzn_hist.reset_index()
with aws_engine.connect() as conn:
    amzn_hist.to_sql('amzn',
                    conn,
                    if_exists='replace', index=False)

请注意前面的代码中的 index=False。这是因为 to_sql 会写入 DataFrame 中的索引对象,默认情况下 index=True。当你重置 DataFrame 索引时,它会将 DatetimeIndex 移动到 Date 列,并用 RangeIndex(从 0 到 1257)替换索引。使用 index=False 确保我们不会尝试将 RangeIndex 写入 Amazon Redshift。

最后,你可以验证写入的记录总数:

from sqlalchemy import text
query = "select count(*) from amzn;"
with aws_engine.connect() as conn:
    result = conn.execute(text(query))
result.fetchone()
>>
(1258,)
使用 redshift_connector 写入 Amazon Redshift

在这个示例中,你将使用一个不同的库,即 redshift_connector。你首先需要安装该库。

你可以使用 conda 安装:

conda install -c conda-forge redshift_connector

你也可以使用 pip 安装它:

pip install redshift_connector

请注意,redshift_connector 需要一个 user 参数,这与 SQLAlchemy 需要 username 参数不同。为此,你可以在配置文件中创建一个新部分。下面是一个示例:

[AWS2]
host=yourendpoint
port=5439
database=dev
user=username
password=password

以下代码从 database.cfg 文件中读取参数,并将这些参数传递给 redshift_connector.connect() 来创建连接对象。

import redshift_connector
from configparser import ConfigParser
config = ConfigParser()
config.read('database.cfg')
config.sections()
params = dict(config['AWS2'])
conn = redshift_connector.connect(**params)

你将创建一个游标对象,它提供对 write_dataframe 方法的访问。

cursor = conn.cursor()
cursor.write_dataframe(amzn_hist, 'amzn')

最后,你将提交事务。

conn.commit()

请注意,write_dataframe 方法没有提供指定追加、替换/覆盖或失败行为的参数,正如你在 SQLAlchemy 中所看到的那样。write_dataframe 方法期望在 Amazon Redshift 中已存在的表进行追加。

使用 AWS SDK for pandas 写入 Amazon Redshift

awswrangler 库或 AWS SDK for pandas 可以轻松地与多个 AWS 服务(如 Athena、Glue、Redshift、Neptune、DynamoDB、EMR、S3 等)集成。

你可以使用 conda 安装该库:

conda install -c conda-forge awswrangler

你也可以使用 pip 安装:

pip install 'awswrangler[redshift]'

你可以利用在上一节中创建的 conn 对象,使用 redshift_connector 写入 Amazon Redshift

import awswrangler as wr
wr.redshift.to_sql(
    df=amzn_hist,
    table='amzn',
    schema='public',
    con=conn,
    mode='overwrite'
)

请注意,mode 参数支持三种(3)不同的选项:overwriteappendupsert

另请参见

这里有一些额外的资源:

将时间序列数据写入 MongoDB

MongoDB是一个文档数据库系统,它以BSON格式存储数据。当你从 MongoDB 查询数据时,数据将以 JSON 格式呈现。BSON 与 JSON 类似,它是 JSON 的二进制编码格式。不过,BSON 不像 JSON 那样是人类可读的格式。JSON 非常适合传输数据,且与系统无关,而 BSON 则专为存储数据并与 MongoDB 相关联。

在这个实例中,你将学习如何将一个 pandas DataFrame 写入 MongoDB。

准备工作

你应该参考第三章中“从文档数据库读取数据”这一实例,以便复习连接 MongoDB 的不同方法。

第三章中“从文档数据库读取数据”这一实例中,我们安装了pymongo。在本实例中,你将再次使用该库。

要通过conda安装,请运行以下命令:

$ conda install -c anaconda pymongo -y

要通过pip安装,请运行以下命令:

$ python -m pip install pymongo

本书的 GitHub 仓库中提供了该文件,你可以在此找到:github.com/PacktPublishing/Time-Series-Analysis-with-Python-Cookbook

如何操作…

要将数据存储到 MongoDB 中,你需要创建一个数据库和一个集合。一个数据库包含一个或多个集合,这些集合类似于关系数据库中的表。一旦创建集合,你将以文档形式写入数据。集合包含文档,文档相当于关系数据库中的行。

  1. 首先,导入必要的库:
import pandas as pd
from pymongo import MongoClient
  1. 创建一个MongoClient实例以建立与数据库的连接:
client = MongoClient('mongodb://localhost:27017')
  1. 创建一个名为stock_data的新数据库,并创建一个名为daily_stock时间序列集合

首先,我们在 MongoDB 中创建一个常规集合

db = client['stock_data']
collection = db.create_collection('amazon')

这将创建一个名为stock_data的新数据库,并创建一个名为 amazon 的集合。如果stock_data已存在,它将把amazon集合添加到现有数据库中。

然而,由于我们处理的是时间序列数据,我们可以通过创建一个时间序列集合来更高效地存储和查询数据。从 MongoDB 5.0 版本开始,时间序列集合已针对带时间戳的数据进行了优化。我们可以修改之前的代码来创建一个daily_stock时间序列集合。

db = client['stock_data']
ts = db.create_collection(
    name="daily_stock",
    timeseries={
        "timeField": "Date",
        "metaField": "symbol",
        "granularity": "hours"
    }
)

通过此次更新,我们现在使用时间序列集合,这提高了时间数据(如股票价格)的存储效率和查询性能。以后,我们将使用ts引用来与时间序列集合进行交互。

  1. 你将利用在技术要求部分创建的get_stock_data函数,拉取从 2019 年 1 月 1 日到 2024 年 8 月 31 日的亚马逊股票数据:
amzn_hist = get_stock_data('AMZN', '2019-01-01', '2024-8-31')
  1. 在 pandas 中,我们以表格格式处理数据,其中每一列代表一个变量,每一行代表一个数据点。而 MongoDB 则将数据存储为类似 JSON 的格式(BSON),其中每个文档是一个独立的记录,可以包含时间戳、元数据和其他键值对。

在将数据插入 MongoDB 之前,你需要将 DataFrame 转换为一个字典列表,每个字典(或文档)表示一个股票数据点。每个字典将包含时间戳(Date)、股票信息(例如HighLow)和元数据(例如"ticker": "AMZN")。

你将探索两个选项:第一个选项使用to_dict()方法,第二个选项是遍历 DataFrame。

让我们来探索第一个选项:

metadata = {"ticker": "AMZN"}
amzn_hist['metadata'] = [metadata] * len(amzn_hist)
amzn_hist = amzn_hist.reset_index()
amzn_hist['Date'] = pd.to_datetime(amzn_hist['Date'])
amzn_records = amzn_hist.to_dict(orient='records')
amzn_records[0:2]
>>
[{'Date': Timestamp('2019-01-02 00:00:00'),
  'Open': 73.26000213623047,
  'High': 77.66799926757812,
  'Low': 73.04650115966797,
  'Close': 76.95649719238281,
  'Volume': 159662000,
  'metadata': {'ticker': 'AMZN'}},
 {'Date': Timestamp('2019-01-03 00:00:00'),
  'Open': 76.00050354003906,
  'High': 76.9000015258789,
  'Low': 74.85549926757812,
  'Close': 75.01399993896484,
  'Volume': 139512000,
  'metadata': {'ticker': 'AMZN'}}]

在这里,我们假设所有数据都有相同的元数据信息(例如"ticker": "AMZN")。

to_dict()方法中orient参数的默认值是dict,它会生成一个字典,其格式为{column -> {index -> value}}

另一方面,使用records作为值,会生成一个列表,它遵循[{column -> value}, … , {column -> value}]的模式。

现在,让我们来探索第二个选项,它提供了更多的灵活性,可以为特定字段添加数据或对单个记录进行转换,例如,基于不同的股票代码值:

amzn_hist = amzn_hist.reset_index()
amzn_records = []
for idx, row in amzn_hist.iterrows():
    doc = {
        "Date": pd.to_datetime(row['Date']),
        "metadata": {"ticker": "AMZN"},
        "High": row['High'],
        "Low": row['Low'],
        "Close": row['Close'],
        "Open": row['Open'],
        "Volume": row['Volume']
    }
    amzn_records.append(doc)
amzn_records[0:2]
>>
[{'Date': Timestamp('2019-01-02 00:00:00'),
  'metadata': {'ticker': 'AMZN'},
  'High': 77.66799926757812,
  'Low': 73.04650115966797,
  'Close': 76.95649719238281,
  'Open': 73.26000213623047,
  'Volume': 159662000},
 {'Date': Timestamp('2019-01-03 00:00:00'),
  'metadata': {'ticker': 'AMZN'},
  'High': 76.9000015258789,
  'Low': 74.85549926757812,
  'Close': 75.01399993896484,
  'Open': 76.00050354003906,
  'Volume': 139512000}]

现在,你有了一个长度为1426的 Python 列表(每个记录是一个字典):

len(amzn_records)
>>
1426
  1. 现在,你已经准备好使用insert_many()方法将数据写入时间序列daily_stock集合中:
result = ts.insert_many(amzn_records)
  1. 你可以使用以下代码验证数据库和集合是否已创建:
client.list_database_names()
>>
['admin', 'config', 'local', 'stock_data']
db.list_collection_names()
>>
['daily_stock', 'system.buckets.daily_stock', 'system.views']
  1. 接下来,拉取微软的股票数据(MSFT)并将其添加到同一个daily_stock时间序列集合中。稍后,你将探索如何利用元数据在查询数据时区分不同的股票代码(如 AMZN 与 MSFT)。
msft_hist = get_stock_data('MSFT', '2019-01-01', '2024-8-31')
metadata = {"ticker": "MSFT"}
msft_hist['metadata'] = [metadata] * len(msft_hist)
msft_hist = msft_hist.reset_index()
msft_hist['Date'] = pd.to_datetime(msft_hist['Date'])
msft_records = msft_hist.to_dict(orient='records')
result = ts.insert_many(msft_records)

你可以通过查询数据库来检查写入的文档总数,如下面的代码所示:

ts.count_documents({})
>>
2852
  1. 现在,集合中包含了两个股票代码的数据。你可以使用元数据在查询中为每个代码进行筛选。你将首先查询daily_stock集合,检索仅包含微软(MSFT)股票数据的记录。这时,metadata字段变得非常有用,允许你按股票代码进行筛选。让我们首先定义一个日期范围,然后仅查询 MSFT 的数据。
from datetime import datetime
# Define date range
start_date = datetime(2019, 1, 1)
end_date = datetime(2019, 1, 31)
# Query for MSFT stock data within the date range
results = ts.find({
    "metadata.ticker": "MSFT",
    "Date": {"$gte": start_date, "$lte": end_date}
})
# Convert the query results to a DataFrame
msft_df = (pd.DataFrame(results)
             .set_index('Date')
             .drop(columns=['_id', 'metadata']))
print(msft_df.head())
>>
                Close       High       Open    Volume        Low
Date                                                           
2019-01-02  95.501335  96.096327  94.018571  35329300  93.442465
2019-01-03  91.988037  94.623014  94.538011  42579100  91.799146
2019-01-04  96.266327  96.814101  94.179125  44060600  93.433020
2019-01-07  96.389107  97.531873  95.992445  35656100  95.369122
2019-01-08  97.087975  98.192962  97.314637  31514400  96.058536
  1. 你还可以进行聚合计算每个股票代码的平均Close价格:
msft_avg_close = ts.aggregate([
    {"$group":
         {"_id": "$metadata.ticker",
          "avgClose":
                  {"$avg": "$Close"}}
    }
])
for doc in msft_avg_close:
    print(doc)
>>
{'_id': 'AMZN', 'avgClose': 133.4635473361022}
{'_id': 'MSFT', 'avgClose': 252.4193055419066}

它是如何工作的…

PyMongo提供了两个插入函数,用于将我们的记录作为文档写入集合。这些函数如下:

  • insert_one()会将一个文档插入到集合中。

  • insert_many()会将多个文档插入到集合中。

在前面的示例中,你使用了insert_many()并同时传入了要写入的数据作为文档。然而,在执行此操作之前,将 DataFrame 转换为符合[{column -> value}, … , {column -> value}]模式的字典列表格式是至关重要的。这是通过在to_dict() DataFrame 方法中使用orient='records'来完成的。

当文档插入数据库时,它们会被分配一个唯一的_id值。如果文档尚未拥有_id,MongoDB 会在插入操作中自动生成一个。你可以捕获生成的_id,因为插入函数会返回一个结果对象——对于单次插入是InsertOneResult,对于批量插入是InsertManyResult。以下代码演示了如何使用insert_oneInsertOneResult类来实现这一点:

one_record = amzn_records[0]
one_record
>>
{'Date': Timestamp('2019-01-02 00:00:00'),
 'metadata': {'ticker': 'AMZN'},
 'High': 77.66799926757812,
 'Low': 73.04650115966797,
 'Close': 76.95649719238281,
 'Open': 73.26000213623047,
 'Volume': 159662000}
result_id = ts.insert_one(one_record)
result_id
>>
InsertOneResult(ObjectId('66f2ed5efad8cbd88968d02e'), acknowledged=True)

返回的对象是InsertOneResult的一个实例;要查看实际的值,你可以使用insert_id属性:

result_id.inserted_id
>>
ObjectId('66f2ed5efad8cbd88968d02e')

如果你有分钟级的股票数据,你可以利用granularity属性,它可以是secondsminuteshours

还有更多内容…

在前面的示例中,如果你运行以下代码查询数据库以列出可用的集合,你将看到三个集合:

db = client['stock_data']
db.list_collection_names()
>>
['daily_stock', 'system.buckets.daily_stock', 'system.views']

你创建了daily_stock集合,那么另外两个集合是什么呢?让我们首先探讨一下 MongoDB 中的桶模式

桶模式是一种数据建模技术,用来优化数据在数据库中的存储方式。默认情况下,当你将 DataFrame 转换为字典列表时,实际上是将每个 DataFrame 记录(数据点)作为一个独立的 MongoDB 文档插入。这会在记录和文档之间创建一对一的映射。

然而,桶策略允许你将相关的数据点分组到一个文档中。例如,如果你有每小时的数据,你可以将它们分组到一个桶中,比如 24 小时周期,并将该时间范围内的所有数据存储在一个文档中。同样,如果我们有来自多个设备的传感器数据,你可以使用桶模式将数据(例如按设备 ID 和时间范围)分组,并将它们作为一个文档插入。这将减少数据库中文档的数量,提高整体性能,并简化查询。

当你创建一个时间序列集合时,MongoDB 会自动应用桶模式,以高效的格式存储数据。让我们分解一下:

  • daily_stock:这是你创建的主要时间序列集合。它充当视图,允许你使用标准的 MongoDB 操作与时间序列数据进行交互。

  • system.buckets.daily_stock:这是一个内部集合,MongoDB 使用桶模式存储实际的时间序列数据。MongoDB 会自动为时间序列集合实现这一策略,以提高存储和查询性能。它是如何工作的呢:

    • 文档会根据时间戳和元数据字段(例如,股票符号)被分组到“桶”中。

    • 每个桶包含时间上接近的数据点,并共享相同的元数据值。

    • 这种分桶策略显著减少了存储的文档数量,提高了查询效率并减少了磁盘使用。

  • system.views:这是一个系统集合,MongoDB 用来存储数据库中所有视图的信息,包括你的时间序列集合的视图。

为了更好地理解桶模式的应用,我们将通过创建一个新的集合(常规集合,而非时间序列集合),并按年和月将每日股票数据分桶:

db = client['stock_data']
bucket = db.create_collection(name='stock_bucket')

接下来,让我们创建一个新的 DataFrame,并添加两个额外的列:monthyear

amzn = yf.Ticker("AMZN")
amzn_hist = amzn.history(period="5y")
amzn_hist = amzn_hist[['Open',
                       'High',
                       'Low',
                       'Close',
                       'Volume']].reset_index()
amzn_hist['Date'] = pd.to_datetime(amzn_hist['Date'])
amzn_hist['month'] = amzn_hist['Date'].dt.month
amzn_hist['year'] = amzn_hist['Date'].dt.year

在前面的代码中,你向 DataFrame 添加了 monthyear 列,并创建了一个名为 stocks_bucket 的新集合。在接下来的代码段中,你将循环遍历数据,并将按年和月分组的数据作为一个单一文档写入:

for year in amzn_hist['year'].unique():
    for month in amzn_hist['month'].unique():
        record = {}
        record['month'] = int(month)
        record['year'] = int(year)
        record['symbol'] = 'AMZN'
        try:
            prices = amzn_hist[(amzn_hist['month'] == month) & (amzn_hist['year'] == year)]['Close'].values
            record['price'] = [float(price) for price in prices]
        except Exception as e:
            print(f"Error processing data for {month}/{year}: {str(e)}")
            continue
        else:
            bucket.insert_one(record)

在代码中,你遍历了唯一的年和月组合,然后为每个组合创建一个包含月、年、符号和收盘价列表的记录字典。然后,该记录被插入到 stock_bucket 集合中,有效地按月和年对数据进行了分桶。

为了说明文档数量的差异,请运行以下代码:

print('without bucketing: ',
      db.daily_stock.count_documents({}))
print('with bucketing: ',
      db.stock_bucket.count_documents({}))
>>
without bucketing:  2853
with bucketing:  72

请注意,stock_bucket 集合包含 72 个文档,代表按年和月分组的数据。

要查询 2024 年和 6 月的数据,并查看文档如何表示,请使用以下代码示例:

results = pd.DataFrame(bucket.find({'year':2024, 'month': 6}))
results['price'].to_dict()[0]
>>
[178.33999633789062,
 179.33999633789062,
 181.27999877929688,
 185.0,
 184.3000030517578,
 187.05999755859375,
 187.22999572753906,
 186.88999938964844,
 183.8300018310547,
 183.66000366210938,
 184.05999755859375,
 182.80999755859375,
 186.10000610351562,
 189.0800018310547,
 185.57000732421875,
 186.33999633789062,
 193.61000061035156,
 197.85000610351562,
 193.25]

你也可以使用 MongoDB Compass 运行相同的查询,结果应与图示中显示的类似:

https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/ts-anal-py-cb-2e/img/file41.png

图示 – 使用 MongoDB Compass 查询 stock_bucket 集合

另见

写入时间序列数据到 InfluxDB

在处理大规模时间序列数据时,如传感器或物联网IoT)数据,你需要一种更高效的方式来存储和查询这些数据,以便进行进一步的分析。这就是时间序列数据库的优势所在,因为它们专门为处理复杂且非常大的时间序列数据集而构建。

在这个示例中,我们将以InfluxDB为例,演示如何写入时间序列数据库。

准备工作

你应该参考第三章“从数据库中读取时间序列数据”中的教程“从时间序列数据库中读取数据”,以便复习连接 InfluxDB 的不同方式。

你将使用ExtraSensory数据集,这是由加利福尼亚大学圣地亚哥分校提供的一个移动感应数据集:Vaizman, Y., Ellis, K., 和 Lanckriet, G. “从智能手机和智能手表识别复杂的人类背景”。IEEE Pervasive Computing, vol. 16, no. 4, 2017 年 10 月至 12 月, pp. 62-74. doi:10.1109/MPRV.2017.3971131

你可以在这里下载数据集:extrasensory.ucsd.edu/#download

数据集包含 60 个文件,每个文件代表一个参与者,并通过唯一标识符(UUID)来识别。每个文件包含 278 列:225 列(特征)、51 列(标签)和 2 列(时间戳和标签来源)。

本教程的目标是演示如何将时间序列 DataFrame 写入 InfluxDB。在这个教程中,选择了两列:时间戳(日期范围从2015-07-232016-06-02,共覆盖 152 天)和手表加速度计读数(以毫 G 为单位测量)。

在你可以在 Python 中与 InfluxDB 交互之前,你需要安装 InfluxDB Python 库。

你可以通过运行以下命令,使用pip安装该库:

$ pip install 'influxdb-client[ciso]'

要使用conda安装,请使用以下命令:

conda install -c conda-forge influxdb-client

如何操作…

你将通过读取ExtraSensory数据集中的一个文件(针对特定的 UUID)来开始这个教程,重点关注一个特征列——手表加速度计。你将进行一些数据转换,为将时间序列 DataFrame 写入 InfluxDB 做准备:

  1. 首先加载所需的库:
from influxdb_client import InfluxDBClient, WriteOptions
from influxdb_client.client.write_api import SYNCHRONOUS
import pandas as pd
from  pathlib import Path
  1. 数据集由 60 个压缩 CSV 文件(csv.gz)组成,你可以使用pandas.read_csv()读取这些文件。read_csv的默认compression参数设置为infer,这意味着 pandas 会根据文件扩展名推断使用哪种压缩或解压协议。文件扩展名为(gz),pandas 会使用这个扩展名来推断需要使用的解压协议。或者,你也可以通过compression='gzip'明确指定使用哪种压缩协议。

在以下代码中,你将读取这些文件中的一个,选择timestampwatch_acceleration:magnitude_stats:mean两列,重命名这些列,最后,针对所有na(缺失)值执行回填操作:

path = Path('../../datasets/Ch5/ExtraSensory/')
file = '0A986513-7828-4D53-AA1F-E02D6DF9561B.features_labels.csv.gz'
columns = ['timestamp',
           'watch_acceleration:magnitude_stats:mean']
df = pd.read_csv(path.joinpath(file),
                usecols=columns,
                compression='gzip')
df = df.bfill()
df.columns = ['timestamp','wacc']
df.shape
>>
(3960, 2)

从前面的输出中,你有来自那个文件的3960个传感器读数。

  1. 为了将数据写入 InfluxDB,你需要至少一个measurement列和一个timestamp列。目前,时间戳是一个 Unix 时间戳(epoch),以秒为单位捕获,这是一个可接受的写入 InfluxDB 的数据格式。例如,2015-12-08 7:06:37 PM在数据集中以1449601597的形式存储。

InfluxDB 在磁盘上以纪元纳秒存储时间戳,但在查询数据时,InfluxDB 将数据显示为RFC3339 UTC 格式,以使其更易读。因此,在RFC33391449601597将表示为2015-12-08T19:06:37+00:00.000Z。请注意 InfluxDB 中的精度为纳秒

在以下步骤中,您将把 Unix 时间戳转换为在pandas中更易读的格式,这也是 InfluxDB 中可接受的格式:

df['timestamp'] = pd.to_datetime(df['timestamp'],
                                  origin='unix',
                                  unit='s',
                                  utc=True)
df.set_index('timestamp', inplace=True)
print(df.head())
>>
                                  wacc
timestamp                            
2015-12-08 19:06:37+00:00   995.369977
2015-12-08 19:07:37+00:00   995.369977
2015-12-08 19:08:37+00:00   995.369977
2015-12-08 19:09:37+00:00   996.406005
2015-12-08 19:10:55+00:00  1034.180063

在上述代码中,unit参数设置为's'用于。这指示 pandas 基于起点计算秒数。origin参数默认设置为unix,因此转换将计算到 Unix 纪元开始的秒数。utc参数设置为True,这将返回一个UTCDatetimeIndex类型。我们的 DataFrame 索引的dtype现在是datetime64[ns, UTC]

您可以在第六章中的Chapter 6中的Working with Unix epoch timestamps中的食谱中了解有关 Unix 纪元时间戳的更多信息

  1. 接下来,您需要建立与运行的 InfluxDB 数据库实例的连接。您只需传递您的 API 读/写令牌即可。在写入数据库时,您需要指定 bucket 和组织名称:
bucket = "sensor"
org = "<yourorg>"
token = "<yourtoken>"
client = InfluxDBClient(url="http://localhost:8086",
                        token=token,
                        org=org)
  1. 初始化write_api并配置WriterOptions。包括指定writer_typeSYNCHRONOUSbatch_sizemax_retries,在失败之前:
writer = client.write_api(WriteOptions(SYNCHRONOUS,
                     batch_size=500,
                     max_retries=5))
writer.write(bucket=bucket,
                record=df,
                write_precision='ns',
                data_frame_measurement_name='wacc',
                data_frame_tag_columns=[])
  1. 要验证数据是否正确写入,您可以使用query_data_frame方法查询数据库,如以下代码所示:
query = '''
         from(bucket: "sensor")
         |> range(start: 2015-12-08)
         |> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value")
         '''
result = client.query_api()
influx_df = result.query_data_frame(
                             org=org,
                             query=query,
                             data_frame_index='_time')

检查返回的 DataFrame:

Influx_df.info()
>>
<class 'pandas.core.frame.DataFrame'>
DatetimeIndex: 3960 entries, 2015-12-08 19:06:37+00:00 to 2015-12-11 18:48:27+00:00
Data columns (total 6 columns):
 #   Column        Non-Null Count  Dtype             
---  ------        --------------  -----             
 0   result        3960 non-null   object            
 1   table         3960 non-null   int64             
 2   _start        3960 non-null   datetime64[ns, UTC]
 3   _stop         3960 non-null   datetime64[ns, UTC]
 4   _measurement  3960 non-null   object            
 5   wacc          3960 non-null   float64           
dtypes: datetime64ns, UTC, float64(1), int64(1), object(2)
memory usage: 216.6+ KB

请注意 DataFrame 有两个datetime64[ns, UTC]类型的列。

  1. 现在您完成了,可以关闭您的写入对象并关闭客户端,如下所示:
writer.close()
client.close()

工作原理…

在使用write_api将 pandas DataFrame 写入 InfluxDB 之前,您需要在 InfluxDB 中定义几个必需的事项。包括以下内容:

  • 测量:这些是您要跟踪的值。InfluxDB 每个数据点接受一个测量。

  • 字段:我们不需要明确指定字段,因为未在标签定义中的任何列将被标记为字段。字段作为键值对存储的元数据对象。与标签不同,字段不被索引。

  • 标签(可选):一个元数据对象,您可以指定要索引以提高查询性能的列。这也存储为键值对。

WriteAPI 支持 同步异步 写入。此外,WriteAPI 在写入 InfluxDB 时还提供了多个选项(例如,行协议字符串、行协议字节、数据点结构、字典样式,以及对 pandas DataFrame 的支持)。在第三章的 从时间序列数据库读取数据 示例中,你使用了 query_data_frame() 方法,指定查询结果应该作为 pandas DataFrame 返回。

同样,write_api 在将 pandas DataFrame 写入 InfluxDB 时提供了额外的参数:

  • data_frame_measurement_name:用于写入 pandas DataFrame 的测量名称

  • data_frame_tag_columns:作为标签的 DataFrame 列表;其余列将作为字段

还有更多内容…

在前面的示例中,我们需要手动使用 writer.close() 刷新数据,并使用 client.close() 终止连接。为了更好的资源管理(例如,自动关闭连接)和异常处理,你可以使用 with 语句,享受它带来的便利。以下示例展示了如何以更清晰、更高效的格式重写相同的代码:

with InfluxDBClient(url="http://localhost:8086", token=token) as client:
    with client.write_api(WriteOptions(SYNCHRONOUS,
                     batch_size=500,
                     max_retries=5_000)) as writer:

        writer.write(bucket=bucket,
                        org=org,
                        record=df,
                        write_precision='ns',
                        data_frame_measurement_name='wacc',
                        data_frame_tag_columns=[])

另见

将时间序列数据写入 Snowflake

Snowflake 已成为构建大数据分析的热门云数据库选项,因其可扩展性、性能以及 SQL 导向(列存储关系数据库)。

Snowflake 的 Python 连接器简化了与数据库的交互,无论是读取数据还是写入数据,特别是对 pandas DataFrame 的内建支持。在这个示例中,你将使用在 将时间序列数据写入 InfluxDB 示例中准备的传感器物联网数据集。这项技术适用于任何你打算写入 Snowflake 的 pandas DataFrame。

准备工作

你可以参考 第三章 中的 从 Snowflake 读取数据 示例,作为回顾,了解连接到 Snowflake 的不同方式。

对于 snowflake-connector-python 库,推荐的安装方法是使用 pip,这允许你安装诸如 pandas额外 组件,如下所示:

pip install snowflake-sqlalchemy snowflake-snowpark-python
pip install "snowflake-connector-python[pandas]"

你也可以通过 conda 安装,但如果你想在使用 snowflake-connector-python 与 pandas 时,必须使用 pip 安装。

conda install -c conda-forge snowflake-sqlalchemy snowflake-snowpark-python
conda install -c conda-froge snowflake-connector-python

创建一个配置文件,例如 database.cfg,用于存储你的 Snowflake 连接信息,如下所示:

[SNOWFLAKE]
ACCOUNT=<YOURACCOUNT>
USER=<YOURUSERNAME>
PASSWORD= <YOURPASSWORD>
WAREHOUSE=COMPUTE_WH
DATABASE=TSCOOKBOOK
SCHEMA=CHAPTER5
ROLE=<YOURROLE>

使用 ConfigParser,提取 [SNOWFLAKE] 部分的内容,以避免暴露或硬编码你的凭证。读取 [SNOWFLAKE] 部分下的参数,并将其转换为 Python 字典,如下所示:

config = ConfigParser()
config.read('database.cfg)
config.sections()
params = dict(config['SNOWFLAKE'])

你将利用在技术要求部分创建的 get_stock_data 函数,拉取 Amazon 从 2019 年 1 月 1 日到 2024 年 8 月 31 日的股票数据:

amzn_hist = get_stock_data('AMZN', '2019-01-01', '2024-8-31')

amzn_hist DataFrame 没有 Date 列,而是有一个 DatetimeIndex。由于 API 不支持写入索引对象,你需要将索引转换为一列。

amzn_hist = amzn_hist.reset_index()
amzn_hist.shape
>>
(1426, 6)

在本食谱中,你将引用 amzn_hist DataFrame 和对象 params

如何操作…

我们将探索三种方法和库来连接到 Snowflake 数据库。你将首先使用 Snowflake Python 连接器,然后探索 Snowflake SQLAlchemy,最后探索 Snowpark Python API。让我们开始吧。

使用 snowflake-connector-python(write_pandas)

本节中的食谱将利用 snowflake-connector-python 库来连接并将数据写入 Snowflake 数据库。

  1. 导入本食谱所需的库:
import pandas as pd
from snowflake import connector
from snowflake.connector.pandas_tools import pd_writer, write_pandas

pands_tools 模块提供了几个用于处理 pandas DataFrame 的函数,其中包括两个写入方法(write_pandaspd_writer)。

write_pandas 是一个将 pandas DataFrame 写入 Snowflake 数据库的方法。背后,该函数会将数据存储为 Parquet 文件,将文件上传到 临时阶段,然后通过 COPY INTO 命令将数据从文件插入到指定的表中。

另一方面,pd_writer 方法是一个插入方法,用于通过 DataFrame.to_sql() 方法将数据插入到 Snowflake 数据库,并传递一个 SQLAlchemy 引擎。在本食谱后面的使用 SQLAlchemy 部分,你将探索 pd_writer

  1. 建立与 Snowflake 数据库实例的连接,并创建一个游标对象:
con = connector.connect(**params)
cursor = con.cursor()

cursor 对象将用于执行 SQL 查询,以验证数据集是否已正确写入 Snowflake 数据库。

  1. 使用 writer_pandas 方法将 amzn_hist DataFrame 写入 Snowflake。该方法接受连接对象 con、DataFrame、目标表名及其他可选参数,如 auto_create_table 和 table_type 等。
success, nchunks, nrows, copy_into = write_pandas(
                                            con,
                                            amzn_hist,
                                            auto_create_table=True,
                                            table_name='AMAZON',
                                            table_type='temporary')

使用 write_pandas 时,它返回一个元组。在之前的代码中,我们将元组解包为:success、nchunks、nrows 和 copy_into。让我们查看这些对象内部的值:

print('success: ', success)
print('number of chunks: ', nchunks)
print('number of rows: ', nrows)
print('COPY INTO output', copy_into)
>>
success:  True
number of chunks:  1
number of rows:  1426
COPY INTO output [('ntporcytgv/file0.txt', 'LOADED', 1426, 1426, 1, 0, None, None, None, None)]

success 对象是一个布尔值(True 或 False),用于指示函数是否成功将数据写入指定的表中。nchunks 表示写入过程中的块数,在本例中,整个数据作为一个块写入。nrows 表示函数插入的行数。最后,copy_into 对象包含 COPY INTO 命令的输出。

注意 auto_create_table=True 的使用,如果没有设置为 True 并且表 AMAZON 在 Snowflake 中不存在,write_pandas 会抛出一个错误。当设置为 True 时,我们明确要求 write_pandas 创建该表。此外,如果表已经存在,你可以使用 overwrite=True 参数指定是否希望覆盖现有的表。

table_type 支持 Snowflake 中的 永久临时瞬态 表类型。该参数可以采用以下值:'temp''temporary''transient'。如果传递空字符串 table_type='',则会创建一个 永久 表(默认行为)。

  1. 你可以进一步验证所有 1426 条记录是否已写入临时表:
cursor.execute('SELECT count(*) FROM AMAZON;')
count = cursor.fetchone()[0]
print(count)
>>
1426

实际上,你已将所有 1426 条记录写入 Snowflake 中的 AMAZON 表。

使用 SQLAlchemy

本节中的示例将使用 snowflake-sqlalchemy 和 snowflake-connector-python 库来连接和将数据写入 Snowflake 数据库。

  1. 导入本示例所需的库:
import pandas as pd
from snowflake.connector.pandas_tools import pd_writer
from snowflake.sqlalchemy import URL
from sqlalchemy import create_engine
  1. 你将使用 Snowflake SQLAlchemy 库中的 URL 函数来构建连接字符串并创建 SQLAlchemy 引擎,以便与 Snowflake 实例建立连接:
url = URL(**params)
engine = create_engine(url)
  1. 使用 to_sql() 写入函数将数据框架写入 Snowflake 数据库。你需要传递一个 插入方法;在此情况下,你将传递 pd_writer
try:
    amzn_hist.to_sql(
    'amazon_alchemy',
    engine,
    index=False,
    if_exists='replace'
)
except:
    print('failed to write')

上述代码使用标准的 SQL INSERT 子句,每一行一个。Snowflake 连接器 API 提供了一个插入方法 pd_writer,你可以将其传递给 to_sql 方法中的方法参数,如下所示:

try:
    amzn_hist.to_sql(
    'amazon_alchemy',
    engine,
    index=False,
    if_exists='replace',
    method=pd_writer
)
except:
    print('failed to write')

在幕后,pd_writer 函数将使用 write_pandas 函数将数据框架写入 Snowflake 数据库。

  1. 要读取并验证数据是否已写入,你可以使用 pandas.read_sql() 来查询表:
pd.read_sql_table('amazon_alchemy',
                  con=engine).info()
>>
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1426 entries, 0 to 1425
Data columns (total 6 columns):
 #   Column  Non-Null Count  Dtype 
---  ------  --------------  ----- 
 0   Date    1426 non-null   object
 1   Open    1426 non-null   float64
 2   High    1426 non-null   float64
 3   Low     1426 non-null   float64
 4   Close   1426 non-null   float64
 5   Volume  1426 non-null   float64
dtypes: float64(5), object(1)
memory usage: 67.0+ KB

新的数据框架包含所有 1426 条记录以及预期的确切列数。

使用 snowflake-snowpark-python

本节中的示例将使用 Snowpark API 来写入 pandas 数据框架。

  1. 导入本示例所需的库:
from snowflake.snowpark import Session
import pandas as pd
  1. 通过与 Snowflake 数据库建立连接来创建一个会话。
session = Session.builder.configs(params).create()
  1. 在写入数据框架之前,你必须将 pandas 数据框架转换为 Snowpark 数据框架。
amzn_snowpark_df = session.create_dataframe(amzn_hist)

Snowpark 数据框架采用延迟计算,并提供了许多相对于 Panda 数据框架的优势。

  1. 要写入 Snowpark 数据框架,你可以使用 writesave_as_table 方法:
amzn_snowpark_df.write.mode("overwrite").save_as_table("amazon_snowpark")
  1. 要读取并验证数据是否已写入,你可以使用 session.table 来查询表:
amzn_df = session.table("amazon_snowpark")

如果你更习惯使用 pandas,你可以使用 to_pandas 方法将 Snowpark 数据框架转换为 pandas 数据框架,如下所示:

df = amzn_df.to_pandas()
df.shape
>>
(1426, 6)

数据框架包含所有 1426 条记录和预期的六(6)列。

它是如何工作的…

Snowflake Python API 提供了两种将 pandas 数据框架写入 Snowflake 的机制,这些机制包含在 pandas_tools 模块中:

from snowflake.connector.pandas_tools import pd_writer, write_pandas

在这个食谱中,你使用了 pd_writer 并将其作为插入方法传递给 DataFrame.to_sql() 写入函数。当在 to_sql() 中使用 pd_writer 时,你可以通过 if_exists 参数来改变插入行为,该参数有三个选项:

  • fail,如果表格已存在,则引发ValueError错误

  • replace,在插入新值之前会删除表格

  • append,将数据插入到现有表格中

如果表格不存在,SQLAlchemy 会为你创建表格,并将 pandas DataFrame 中的数据类型映射到 Snowflake 数据库中的相应数据类型。通过 pandas.read_sql() 使用 SQLAlchemy 引擎从 Snowflake 读取数据时也适用此规则。

请注意,pd_writer 在后台使用了 write_pandas 函数。它们的工作方式都是将 DataFrame 转储为 Parquet 文件,上传到临时阶段,最后通过 COPY INTO 将数据复制到表格中。

当你使用 Snowpark API 写入 DataFrame 时,你使用了 write.mode() 方法。mode() 方法接受不同的写入模式选项:

  • append:将 DataFrame 的数据追加到现有表格。如果表格不存在,它将被创建。

  • overwrite:通过删除旧表来覆盖现有表格。

  • truncate:通过截断旧表来覆盖现有表格。

  • errorifexists:如果表格已存在,则抛出异常错误

  • ignore:如果表格已存在,则忽略该操作。

请记住,默认值是 errorifexists

还有更多…

在 Snowpark 中有一个有用的方法,可以直接写入 pandas DataFrame,而无需将其转换为 Snowpark DataFrame:

snowpark_df = session.write_pandas(amzn_hist,
                               table_name="amazon_temp",
                               auto_create_table=True,
                               table_type="temp")

write_pandas 函数将 pandas DataFrame 写入 Snowflake,并返回一个 Snowpark DataFrame 对象。

另见

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值