arrow是apache开发的一种高压缩的数据结构,发现用来存储K线还是很不错的选择。
测试用python读写很方便,关键是足够小,A股1支票1分钟的数据,1个月大约是140多K吧。
结果从数据库取出来存入arrow中,再用C++进行读取,发现总有8小时的时差问题,估计就是东八区的问题。
c++读取arrow数据,时间默认是按格林尼治时间来读取,然后存入进去的没有带时区信息,因此读,比如数据是2024年5月15日9点31分,存入应先将其转成格林时间,即减掉8小时,即2024年5月15日1点31分,然后再消除时区印迹(貌似有时间印记c++读取时会出错),这样C++那边读取的时间才能够和这里写入的时间对得上。
好,总结一下时间转换,df中的时间一般从数据库中读取或是csv中读取默认都没有时间印记,但因为我们是在中国,所有人都认为看到的时间为东八区,因此存入arrow前先做时区调整,将之本地化东八区,然后转成格林时间,再消除时间印记,存入arrow中,这样所有人从arrow中读取的时间都是格林时间,互相就对得上了。
demo示例:
读取csv数据,注意,由于已经解析了时间格式,因此不需要再额外进行时间转换:
df['time'] = pd.to_datetime(df['time']) # 一般都是要做时间转换的,但这时已经通过parse_dates已经成功解析了时间成为了datetime64[ns]这样的类型
def read_csv(file_name):
# 定义列名列表
columns = ["time", "open"]
data_types = {
'open': 'float64'
}
# 读取 CSV 文件,指定列名
df_read = pd.read_csv(file_name, names=columns, dtype=data_types, header=None, parse_dates=["time"])
# 打印读取后的 DataFrame
print(df_read)
return df_read
写入arrow:
def write_arrow(df: DataFrame, file_name):
import pyarrow as pa
import pyarrow.parquet as pq
if not df['time'].dt.tz:
pd.to_datetime(df['time']).dt.tz_localize('Asia/Shanghai').dt.tz_convert('UTC').dt.tz_localize(None)
table = pa.Table.from_pandas(df, preserve_index=False)
pq.write_table(table, file_name)
if not df['time'].dt.tz: 表达的为如果没有时区印记,即没带上时区信息,就先默认为东八区的时间本地化,再将之转换为格林时间,最后将时区印记抹除(抹除的原因是防止C++读取时出错)
至于C++如何读取这一块,是有一点复杂,稍后有时间再补充上来。
实际上真正的读取csv有很多坑,比如明明要求的是浮点,结果读出来int型了,或者明明要的是时间型,结果硬是读出来解析错了,对于python一旦错了,程序会崩的。因此,在解析时的出错处理尤其重要。如下代码,如果时间错了,时间那一列就置为NaN,后面删除NaN相应的一行,即可达到对错乱数据的清洗,当然,hold那一列异常直接就置0,忽略了。
def read_min_from_csv(csv_file_name):
data_types = {
'market': 'Int64',
'code': 'object',
'volume': 'float64',
'amount': 'float64',
'hold': 'float64',
'avg': 'float64',
'up_count': 'Int64', # 暂时改为float64
'down_count': 'Int64' # 暂时改为float64
}
column_names = ['market', 'code', 'time', 'open', 'high', 'low', 'close', 'volume', 'amount', 'hold', 'avg',
'up_count', 'down_count']
df = DataFrame()
try:
df = pd.read_csv(csv_file_name, names=column_names, header=None, dtype=data_types, parse_dates=['time'])
except Exception as e:
logging.error(e)
df['time'] = pd.to_datetime(df['time'], errors='coerce')
df['hold'] = df['hold'].astype('Int64', errors='ignore').fillna(0).astype(int)
return df
如下代码实现将df按特定列放置的功能:
def trans_min_csv_to_df(df) -> DataFrame:
df = df[['time', 'market', 'code', 'open', 'high', 'low', 'close', 'volume', 'amount', 'hold', 'avg', 'up_count',
'down_count']]
return df
如下代码,实现了缺失数据的清洗,使得不会因为乱数据引发python程序的崩溃
# 找到包含 NaN 值的行
nan_rows = df[df.isna().any(axis=1)]
# 打印包含 NaN 值的行并记录错误信息
if not nan_rows.empty:
logging.error(f"Found rows with NaN values:\n{nan_rows}")
# 删除包含 NaN 值的行
df = df.dropna()
如下代码,实现了将不同的股票分门别类的放置于各自的df中,再装入到以market,code为关键的字典中(通常csv是装所有的股票数据),便于后面各个股票的单独处理
def group_market_code(df: DataFrame) -> Dict[Any, DataFrame]:
# 根据market和code分组,并生成相应的df
grouped = df.groupby(['market', 'code'])
# 创建一个字典来存储每个组的DataFrame
dfs = {}
for name, group in grouped:
dfs[name] = group
这两行代码:去重后再排序,而且去重是去掉前面的,保留后面的值,常理是最后的数据总是最新的,理应替换前面的数据:
combined_df = combined_df.drop_duplicates(subset=['time'], keep='last') # 去重
combined_df = combined_df.sort_values(by='time') # 排序
另外,去重后索引需要重置,否则读取出来后发现索引是断开的不连续,很怪异
combined_df = combined_df.reset_index(drop=True) # 重置索引
当然读取文件的过程中也有一些有趣的东西,比如有一个文件类,用来记录文件信息,默认所有的文件的修改时间都会大于1980年,然后给定一个字段update_date设置成1980年,在后续的更新中只要发现某个文件的更新时间大于last_modified_date,则说明已经更新过了,不再更新。
定义如下:
class FileInfo:
def __init__(self, name_only):
self.folder = ""
self.name_only = name_only
self.file_type = "min" if "min-" in self.name_only else "tick" # 根据name_only得出文件类型
self.file_size = 0
self.last_modified_date = ""
self.update_date = "1980-01-01 01:01:01"
然后就是遍历某目录中的文件信息,填充到定义的前5项中,并将之存于df中,每次更新某个文件,即将当前时间存于update_date,然后存于配置文件.csv中,
以下两行即为更新update_date字段,由于folder和name_only构成记录关键字,因此这样写:
df.loc[(df['folder'] == folder) & (df['name_only'] == name_only), 'update_date'] = new_update_date
df.to_csv(check_file, index=False)
特别注意的是,index=False,否则在反复读取的过程中会发生奇怪的事。
程序中断后,重新启动,一边是先获取cfg中的存储信息,这里面最主要的是update_date字段,总是以cfg中的记录为准,一边是获取指定目录中的文件列表信息,此时文件的更新时间可能会发生变化,文件大小也可能发生变化,因此这些信息要以实际的文件信息为准。也就是说取cfg的更新时间和实际的文件的信息进行合并。如果cfg中有的,实际的列表中没有,取cfg的,如果cfg没有,实际列表多出了几行,则取实际列表的,且将update_date那一项取默认值 1980年
代码很有意思,看实际的函数:
def merge_csv_cfg(df1, df2):
"""
df1为配置文件cfg的,df2则是通过遍历文件夹获取的文件信息集合,
意图是文件更新时间是事后做的,本身遍历的文件信息中不存在,因此不能覆盖df1,而文件大小,文件修改时间则是df2独有的。
以df1为蓝本,只保留df1的update_date,然后将df2更新到df1中
"""
# 使用 'how=outer' 确保所有行都被保留,'_df1' 和 '_df2' 后缀帮助区分同名列
combined_df = pd.merge(df1, df2, on=['folder', 'name_only'], suffixes=('_df1', '_df2'), how='outer')
# 确定哪些列需要更新(除了 'update_date', 'folder', 'name_only')
columns_to_update = [col for col in df2.columns if col not in ['update_date', 'folder', 'name_only']]
# 对于 df2 的每个更新列,使用 df2 的值,如果 df2 为 NaN,则保留 df1 的值
for column in columns_to_update:
combined_df[column] = combined_df[column + '_df2'].combine_first(combined_df[column + '_df1'])
# 处理 'update_date' 列,保留 df1 的值,除非 df1 为 NaN,此时取 df2 的值
combined_df['update_date'] = combined_df['update_date_df1'].fillna(combined_df['update_date_df2'])
# 清除所有辅助列
combined_df.drop(columns=[col for col in combined_df.columns if '_df1' in col or '_df2' in col], inplace=True)
return combined_df