原文:
annas-archive.org/md5/7277c6f80442eb633bdbaf16dcd96fad
译者:飞龙
第四章:4 将时间序列数据持久化到文件
加入我们的书籍社区,访问 Discord
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/ts-anal-py-cb-2e/img/file0.png
在本章中,你将使用pandas库将你的时间序列 DataFrame持久化到不同的文件格式中,如CSV、Excel、Parquet和pickle文件。在对 DataFrame 进行分析或数据转换时,实际上是利用了 pandas 的内存分析能力,提供了极好的性能。然而,内存中的数据意味着它很容易丢失,因为它尚未被持久化到磁盘存储中。
在处理 DataFrame 时,你需要持久化数据以便将来取回、创建备份或与他人共享数据。pandas库附带了一套丰富的写入函数,可以将内存中的 DataFrame(或系列)持久化到磁盘上的不同文件格式中。这些写入函数使你能够将数据存储到本地驱动器或远程服务器位置,例如云存储文件系统,包括Google Drive、AWS S3、Azure Blob Storage和Dropbox。
在本章中,你将探索将数据写入不同的文件格式(本地存储)和云存储位置,如 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
文件中,以便未来分析。这应该类似于持久化仍在进行中的数据(就分析而言)的一种典型场景:
- 首先,让我们将 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/20到3/9/23。
- 假设分析的一部分是聚焦于美国,并且只使用 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")
- 检查
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。
- 假设你现在对数据集已经满意,准备将数据集进行 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
类型。
- 使用
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。
- 导入
pickle
库:
import pickle
- 然后,您可以使用
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”
,因为我们是以二进制模式写入(以原始字节写入)。
- 您可以使用
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)
其他支持的压缩模式包括 gzip、bz2、tar 和 xz。
第二个参数是 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()
上述示例展示了序列化如何有助于存储对象及附加的元数据信息。这在存储一个正在进行的工作或执行多个实验并希望跟踪它们及其结果时非常有用。在机器学习实验中也可以采用类似的方法,因为你可以存储模型及与实验和其输出相关的任何信息。
另见
-
有关
Pandas.DataFrame.to_pickle
的更多信息,请访问此页面:pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.to_pickle.html
。 -
有关 Python Pickle 模块的更多信息,请访问此页面:
docs.python.org/3/library/pickle.html
。
写入 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
,你将在接下来的步骤中使用这个方法:
- 你将使用 pandas DataFrame 写入方法来将 DataFrame 持久化为 CSV 文件。该方法有多个参数,但至少你需要传递文件路径和文件名:
output = Path('../../datasets/Ch4/df_movies.csv')
movies.to_csv(output)
默认情况下,创建的 CSV 文件是 逗号分隔的。
- 要更改分隔符,可以使用
sep
参数并传入不同的参数。在以下代码中,你将创建一个管道符(|)
分隔的文件:
output = Path('../../datasets/Ch4/piped_df_movies.csv')
movies.to_csv(output, sep='|')
- 读取管道分隔的文件并检查生成的 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_MINIMAL
:to_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
参数提供额外的参数,否则会抛出错误。
另见
-
欲了解更多关于
Pandas.DataFrame.to_csv()
函数的信息,请参考此页面:pandas.pydata.org/docs/reference/api/pandas.DataFrame.to_csv.html
。 -
欲了解更多关于 CSV 模块的信息,请参考此页面:
docs.python.org/3/library/csv.html
。
将数据写入 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 文件,你需要提供包含filename
和sheet_name
参数的写入函数。文件名包含文件路径和名称。确保文件扩展名为.xlsx
,因为你使用的是 openpyxl。
DataFrame.to_excel()
方法将根据文件扩展名来决定使用哪个引擎,例如.xlsx
或.xls
。你也可以通过engine
参数明确指定使用的引擎,示例如下:
- 确定文件输出的位置,并将文件路径、所需的工作表名称以及引擎传递给
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
列,符合预期。
- 为了实现这一点,你将使用另一个由 pandas 提供的类,
pandas.ExcelWriter
类为我们提供了两个用于日期格式化的属性:datetime_format
和date_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()
函数。
-
要了解更多关于
Pandas.DataFrame.to_excel()
的信息,请参考pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.to_excel.html
。 -
要了解更多关于
Pandas.ExcelWriter()
的信息,请参考pandas.pydata.org/pandas-docs/stable/reference/api/pandas.ExcelWriter.html#pandas.ExcelWriter.
-
要了解更多关于
openpyxl
的信息,请参考openpyxl.readthedocs.io/en/stable/index.html
。 -
要了解更多关于
openpyxl.utils.dataframe
的信息,请参考openpyxl.readthedocs.io/en/stable/pandas.html#working-with-pandas-dataframes
将数据存储到云存储(AWS、GCP 和 Azure)
在本教程中,您将使用 pandas 将数据写入云存储,如 Amazon S3、Google Cloud Storage 和 Azure Blob Storage。多个 pandas 写入函数支持通过storage_options
参数将数据写入云存储。
准备工作
在第二章的“从 URL 读取数据”中,您被要求安装boto3
和s3fs
来从 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_csv
、to_parquet
和to_excel
,允许你通过storage_accounts
参数传递 AWS S3 特定的凭证(key
和sercret
),这些凭证在s3fs
中有说明。以下代码展示了如何利用to_csv
和to_excel
将你的 movies DataFrame 写入tscookbook
S3 桶,分别保存为movies_s3.csv
和movies_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_csv
和to_excel
将你的 movies DataFrame 写入tscookbook
桶,分别保存为movies_gs.csv
和movies_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 需要传递key
和secret
,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 桶名称和对象键(文件名)来创建一个对象资源。一旦定义,你将可以访问多个方法,包括copy
、delete
、put
、download_file
、load
、get
和upload
等。put
方法将把一个对象添加到定义的 S3 桶中。
使用boto3.client("s3")
客户端 API 时,你可以访问许多 Bucket 和 Object 级别的方法,包括create_bucket
、delete_bucket
、download_file
、put_object
、delete_object
、get_bucket_lifecycle
、get_bucket_location
、list_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
方法指定该文件。你将使用bucket
和blob
方法创建一个引用,指向你希望放入 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 管理云存储的信息,请查看这些流行库的官方文档
-
Amazon S3 (Boto3)
boto3.amazonaws.com/v1/documentation/api/latest/reference/services/s3.html
-
Azure Blob Storage
learn.microsoft.com/en-us/azure/storage/blobs/storage-quickstart-blobs-python
-
Google Cloud Storage
cloud.google.com/python/docs/reference/storage/latest
写入大数据集
在本示例中,你将探索不同文件格式的选择如何影响整体的写入和读取性能。你将探索 Parquet、优化行列式(ORC)和 Feather,并将它们的性能与其他流行的文件格式,如 JSON 和 CSV,进行比较。
这三种文件格式 ORC、Feather 和 Parquet 是列式文件格式,适用于分析需求,并且总体上显示出更好的查询性能。这三种文件格式也得到了 Apache Arrow(PyArrow)的支持,后者提供了内存中的列式格式,优化了数据分析性能。为了将这种内存中的列式数据持久化并存储,你可以使用 pandas 的to_orc
、to_feather
和to_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
写入函数支持多种压缩算法,包括snappy
、GZIP
、brotli
、LZ4
、ZSTD
。您将使用DataFrame.to_parquet()
方法写入三个文件,以比较snappy
、LZ4
和ZSTD
压缩算法:
%%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()
方法,使用两个支持的压缩算法LZ4
和ZSTD
写入三个 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
- 注意未压缩文件、使用 LZ4 和 ZSTD 压缩算法之间的文件大小差异。您可以进一步探索
compression_level
来找到最佳输出。总体而言,LZ4 在写入和读取(压缩和解压缩速度)上提供了出色的性能。ZSTD 算法可能提供更高的压缩比,生成更小的文件,但其速度可能不如 LZ4。
写入为 ORC
类似于 Feather 和 Parquet 文件格式,ORC 支持不同的压缩算法,包括无压缩、snappy
、ZLIB
、LZ4
和 ZSTD
。你将使用 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。
-
Parquet
-
Feather
-
ORC
-
Apache Arrow:
arrow.apache.org/overview/
第五章:5 将时间序列数据持久化到数据库
加入我们在 Discord 上的书籍社区
https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/ts-anal-py-cb-2e/img/file0.png
在完成一个数据分析任务后,通常会从源系统提取数据,进行处理、转换并可能建模,最后将结果存储到数据库中以实现持久化。你总是可以将数据存储在平面文件中或导出为 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 配合使用,以支持其他数据库(方言),如 Snowflake
、Microsoft SQL Server
和 Google 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 数据库中的表:
- 从调用 技术要求 部分中创建的
get_stock_data
函数开始。
amzn_hist = get_stock_data('AMZN', '2019-01-01', '2023-12-31')
- 你需要创建一个 SQLAlchemy engine 对象。该引擎告诉 SQLAlchemy 和 pandas 我们计划与之交互的方言(后端数据库)以及运行中数据库实例的连接详情。利用
URL.create()
方法,通过提供必要的参数(drivername
、username
、password
、host
、port
和database
)来创建一个格式正确的 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)
- 让我们将
amz_hist
DataFrame 写入 PostgreSQL 数据库实例中的新amzn
表。这是通过使用DataFrame.to_sql()
写入函数来实现的,该函数利用 SQLAlchemy 的功能将 DataFrame 转换为合适的表模式,并将数据转换为适当的 SQL 语句(如CREATE TABLE
和INSERT 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 字符串。
- 通过查询
amzn
表并统计记录数来确认数据已写入数据库:
query = "select count(*) from amzn;"
with engine.connect() as conn:
result = conn.execute(text(query))
result.fetchone()
>>
(1258,)
- 接下来,使用
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')
- 统计记录的总数,以确保我们已经将 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
参数,并传递了两个不同的参数:
-
最初,你将值设置为
replace
,这会在表存在时覆盖该表。如果我们将此覆盖操作转换为 SQL 命令,它会执行DROP TABLE
,然后是CREATE TABLE
。如果你已经有一个包含数据的表并打算向其中添加记录,这可能会很危险。因此,如果不传递任何参数,默认值会设置为fail
。这种默认行为会在表已存在时抛出错误。 -
在食谱的第二部分,计划是将额外的记录插入到现有表中,并且你将参数从
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_sql
、read_sql_table
、read_sql_query
和 to_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
使用 ConfigParser
和 URL
提取参数并构建 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)不同的选项:overwrite
、append
或 upsert
。
另请参见
这里有一些额外的资源:
-
若要了解更多关于
DataFrame.to_sql()
函数的信息,可以访问pandas.pydata.org/docs/reference/api/pandas.DataFrame.to_sql.html
。 -
要了解更多关于SQLAlchemy的功能,你可以先阅读它们的功能页面:
www.sqlalchemy.org/features.html
。 -
要了解
awswrangler
,你可以访问他们的 GitHub 仓库:github.com/aws/aws-sdk-pandas
将时间序列数据写入 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 中,你需要创建一个数据库和一个集合。一个数据库包含一个或多个集合,这些集合类似于关系数据库中的表。一旦创建集合,你将以文档形式写入数据。集合包含文档,文档相当于关系数据库中的行。
- 首先,导入必要的库:
import pandas as pd
from pymongo import MongoClient
- 创建一个
MongoClient
实例以建立与数据库的连接:
client = MongoClient('mongodb://localhost:27017')
- 创建一个名为
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
引用来与时间序列集合进行交互。
- 你将利用在技术要求部分创建的
get_stock_data
函数,拉取从 2019 年 1 月 1 日到 2024 年 8 月 31 日的亚马逊股票数据:
amzn_hist = get_stock_data('AMZN', '2019-01-01', '2024-8-31')
- 在 pandas 中,我们以表格格式处理数据,其中每一列代表一个变量,每一行代表一个数据点。而 MongoDB 则将数据存储为类似 JSON 的格式(BSON),其中每个文档是一个独立的记录,可以包含时间戳、元数据和其他键值对。
在将数据插入 MongoDB 之前,你需要将 DataFrame 转换为一个字典列表,每个字典(或文档)表示一个股票数据点。每个字典将包含时间戳(Date
)、股票信息(例如High
、Low
)和元数据(例如"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
- 现在,你已经准备好使用
insert_many()
方法将数据写入时间序列daily_stock
集合中:
result = ts.insert_many(amzn_records)
- 你可以使用以下代码验证数据库和集合是否已创建:
client.list_database_names()
>>
['admin', 'config', 'local', 'stock_data']
db.list_collection_names()
>>
['daily_stock', 'system.buckets.daily_stock', 'system.views']
- 接下来,拉取微软的股票数据(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
- 现在,集合中包含了两个股票代码的数据。你可以使用元数据在查询中为每个代码进行筛选。你将首先查询
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
- 你还可以进行聚合计算每个股票代码的平均
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_one
和InsertOneResult
类来实现这一点:
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
属性,它可以是seconds
、minutes
或hours
。
还有更多内容…
在前面的示例中,如果你运行以下代码查询数据库以列出可用的集合,你将看到三个集合:
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,并添加两个额外的列:month 和 year:
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 添加了 month
和 year
列,并创建了一个名为 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 集合
另见
-
欲了解更多关于在 MongoDB 中存储时间序列数据和分桶的信息,你可以参考这篇 MongoDB 博客文章:
www.mongodb.com/blog/post/time-series-data-and-mongodb-part-2-schema-design-best-practices
-
查看 MongoDB 手册,了解更多关于时间序列集合的信息
www.mongodb.com/docs/manual/core/timeseries-collections/
写入时间序列数据到 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-23
到2016-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 做准备:
- 首先加载所需的库:
from influxdb_client import InfluxDBClient, WriteOptions
from influxdb_client.client.write_api import SYNCHRONOUS
import pandas as pd
from pathlib import Path
- 数据集由 60 个压缩 CSV 文件(
csv.gz
)组成,你可以使用pandas.read_csv()
读取这些文件。read_csv
的默认compression
参数设置为infer
,这意味着 pandas 会根据文件扩展名推断使用哪种压缩或解压协议。文件扩展名为(gz
),pandas 会使用这个扩展名来推断需要使用的解压协议。或者,你也可以通过compression='gzip'
明确指定使用哪种压缩协议。
在以下代码中,你将读取这些文件中的一个,选择timestamp
和watch_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
个传感器读数。
- 为了将数据写入 InfluxDB,你需要至少一个
measurement
列和一个timestamp
列。目前,时间戳是一个 Unix 时间戳(epoch),以秒为单位捕获,这是一个可接受的写入 InfluxDB 的数据格式。例如,2015-12-08 7:06:37 PM
在数据集中以1449601597
的形式存储。
InfluxDB 在磁盘上以纪元纳秒存储时间戳,但在查询数据时,InfluxDB 将数据显示为RFC3339 UTC 格式,以使其更易读。因此,在RFC3339中1449601597
将表示为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
,这将返回一个UTC的DatetimeIndex
类型。我们的 DataFrame 索引的dtype
现在是datetime64[ns, UTC]
。
您可以在第六章中的Chapter 6中的Working with Unix epoch timestamps中的食谱中了解有关 Unix 纪元时间戳的更多信息
- 接下来,您需要建立与运行的 InfluxDB 数据库实例的连接。您只需传递您的 API 读/写令牌即可。在写入数据库时,您需要指定 bucket 和组织名称:
bucket = "sensor"
org = "<yourorg>"
token = "<yourtoken>"
client = InfluxDBClient(url="http://localhost:8086",
token=token,
org=org)
- 初始化
write_api
并配置WriterOptions
。包括指定writer_type
为SYNCHRONOUS
,batch_size
和max_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=[])
- 要验证数据是否正确写入,您可以使用
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]
类型的列。
- 现在您完成了,可以关闭您的写入对象并关闭客户端,如下所示:
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=[])
另见
-
要了解更多关于 InfluxDB 行协议的信息,请参阅他们的文档:
docs.influxdata.com/influxdb/v2.0/reference/syntax/line-protocol/
。 -
要了解更多关于 InfluxDB 2.x Python API 的信息,请参阅官方文档:
docs.influxdata.com/influxdb/cloud/tools/client-libraries/python/
。
将时间序列数据写入 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 数据库。
- 导入本食谱所需的库:
import pandas as pd
from snowflake import connector
from snowflake.connector.pandas_tools import pd_writer, write_pandas
pands_tools
模块提供了几个用于处理 pandas DataFrame 的函数,其中包括两个写入方法(write_pandas
和 pd_writer
)。
write_pandas
是一个将 pandas DataFrame 写入 Snowflake 数据库的方法。背后,该函数会将数据存储为 Parquet 文件,将文件上传到 临时阶段,然后通过 COPY INTO
命令将数据从文件插入到指定的表中。
另一方面,pd_writer
方法是一个插入方法,用于通过 DataFrame.to_sql()
方法将数据插入到 Snowflake 数据库,并传递一个 SQLAlchemy 引擎。在本食谱后面的使用 SQLAlchemy 部分,你将探索 pd_writer
。
- 建立与 Snowflake 数据库实例的连接,并创建一个游标对象:
con = connector.connect(**params)
cursor = con.cursor()
cursor
对象将用于执行 SQL 查询,以验证数据集是否已正确写入 Snowflake 数据库。
- 使用 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=''
,则会创建一个 永久 表(默认行为)。
- 你可以进一步验证所有 1426 条记录是否已写入临时表:
cursor.execute('SELECT count(*) FROM AMAZON;')
count = cursor.fetchone()[0]
print(count)
>>
1426
实际上,你已将所有 1426 条记录写入 Snowflake 中的 AMAZON
表。
使用 SQLAlchemy
本节中的示例将使用 snowflake-sqlalchemy 和 snowflake-connector-python 库来连接和将数据写入 Snowflake 数据库。
- 导入本示例所需的库:
import pandas as pd
from snowflake.connector.pandas_tools import pd_writer
from snowflake.sqlalchemy import URL
from sqlalchemy import create_engine
- 你将使用 Snowflake SQLAlchemy 库中的
URL
函数来构建连接字符串并创建 SQLAlchemy 引擎,以便与 Snowflake 实例建立连接:
url = URL(**params)
engine = create_engine(url)
- 使用
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 数据库。
- 要读取并验证数据是否已写入,你可以使用
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 数据框架。
- 导入本示例所需的库:
from snowflake.snowpark import Session
import pandas as pd
- 通过与 Snowflake 数据库建立连接来创建一个会话。
session = Session.builder.configs(params).create()
- 在写入数据框架之前,你必须将 pandas 数据框架转换为 Snowpark 数据框架。
amzn_snowpark_df = session.create_dataframe(amzn_hist)
Snowpark 数据框架采用延迟计算,并提供了许多相对于 Panda 数据框架的优势。
- 要写入 Snowpark 数据框架,你可以使用
write
和save_as_table
方法:
amzn_snowpark_df.write.mode("overwrite").save_as_table("amazon_snowpark")
- 要读取并验证数据是否已写入,你可以使用
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 对象。
另见
-
访问 Snowflake 文档,了解更多关于
write_pandas
和pd_write
方法的信息:docs.snowflake.com/en/user-guide/python-connector-api.html#write_pandas
。 -
你可以在这里了解更多关于
pandas DataFrame.to_sql()
函数的信息:pandas.pydata.org/docs/reference/api/pandas.DataFrame.to_sql.html
。 -
要了解更多关于 Snowpark API 中
write_pandas
方法的信息,请参阅官方文档:docs.snowflake.com/en/developer-guide/snowpark/reference/python/1.22.1/snowpark/api/snowflake.snowpark.Session.write_pandas