由读写arrow引发的对时间时区的思考

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

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

永远的麦田

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值