combine_cms_data.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. from __future__ import annotations
  2. from ast import main
  3. import multiprocessing
  4. import shutil
  5. from collections import defaultdict
  6. from datetime import datetime, timedelta
  7. from pathlib import Path
  8. import pandas as pd
  9. from log import logger
  10. # base_dir = Path('/home/wzl/下载/cms')
  11. base_dir = Path('/data/wind_files/headquarter/cms/')
  12. tmp_dir = base_dir / 'tmp'
  13. tmp_bak_dir = base_dir / 'tmp_bak'
  14. days_dir_30 = base_dir / '30days'
  15. days_dir_total = base_dir / 'total'
  16. GROUP_COLUMNS = ['pointName', 'indexCode', 'departmentName']
  17. REQUIRED_COLUMNS = [*GROUP_COLUMNS, 'localTime', 'value']
  18. def get_files_by_relative_path() -> tuple[dict[Path, list[tuple[str, Path]]], str | None]:
  19. """Collect all YYYYMMDD folders under tmp, ordered chronologically per file."""
  20. files_by_path: dict[Path, list[tuple[str, Path]]] = defaultdict(list)
  21. date_dirs: list[tuple[datetime, Path]] = []
  22. logger.info(f'扫描 tmp 目录: {tmp_dir}')
  23. for date_dir in tmp_dir.iterdir() if tmp_dir.exists() else []:
  24. if not date_dir.is_dir():
  25. continue
  26. try:
  27. date_dirs.append((datetime.strptime(date_dir.name, '%Y%m%d'), date_dir))
  28. except ValueError:
  29. continue
  30. for _, date_dir in sorted(date_dirs):
  31. for file_path in date_dir.rglob('*.parquet'):
  32. files_by_path[file_path.relative_to(date_dir)].append((date_dir.name, file_path))
  33. for files in files_by_path.values():
  34. files.sort(key=lambda item: item[0])
  35. latest_date = max(date_dirs, key=lambda item: item[0])[1].name if date_dirs else None
  36. logger.info(f'扫描到 {len(files_by_path)} 个相对路径,latest_date={latest_date}')
  37. return files_by_path, latest_date
  38. def read_parquet(file_path: Path) -> pd.DataFrame:
  39. df = pd.read_parquet(file_path)
  40. missing_columns = set(REQUIRED_COLUMNS) - set(df.columns)
  41. if missing_columns:
  42. raise ValueError(f'{file_path} 缺少字段: {sorted(missing_columns)}')
  43. return df[REQUIRED_COLUMNS].copy()
  44. def complete_minute_data_fast(
  45. df: pd.DataFrame,
  46. query_date: str,
  47. history: pd.DataFrame | None = None,
  48. ) -> pd.DataFrame:
  49. if df.empty:
  50. return df.copy()
  51. query_day = pd.Timestamp(query_date)
  52. next_day = query_day + pd.Timedelta(days=1)
  53. df = df.copy()
  54. df['localTime'] = pd.to_datetime(df['localTime']).dt.floor('min')
  55. df = df.loc[(df['localTime'] >= query_day) & (df['localTime'] < next_day)]
  56. if df.empty:
  57. return df
  58. df = df.sort_values([*GROUP_COLUMNS, 'localTime'])
  59. df = df.drop_duplicates([*GROUP_COLUMNS, 'localTime'], keep='last')
  60. minute_range = pd.date_range(query_day, periods=24 * 60, freq='min')
  61. unique_keys = df[GROUP_COLUMNS].drop_duplicates().reset_index(drop=True)
  62. full_template = unique_keys.merge(
  63. pd.DataFrame({'localTime': minute_range}), how='cross'
  64. )
  65. completed = full_template.merge(
  66. df, on=[*GROUP_COLUMNS, 'localTime'], how='left'
  67. )
  68. # 用前一天最后一分钟的数据补全当天 00:00 的缺失值
  69. if history is not None and not history.empty:
  70. prior = history.copy()
  71. prior['localTime'] = pd.to_datetime(prior['localTime'])
  72. prior = prior.loc[prior['localTime'] < query_day, [*GROUP_COLUMNS, 'localTime', 'value']]
  73. if not prior.empty:
  74. prior = prior.sort_values([*GROUP_COLUMNS, 'localTime']).drop_duplicates(GROUP_COLUMNS, keep='last')
  75. seed = prior[GROUP_COLUMNS + ['value']].rename(columns={'value': 'seed_value'})
  76. completed = completed.merge(seed, on=GROUP_COLUMNS, how='left')
  77. first_minute = completed['localTime'].eq(query_day)
  78. needs_seed = first_minute & completed['value'].isna()
  79. completed['value'] = completed['value'].mask(needs_seed, completed['seed_value'])
  80. completed = completed.drop(columns=['seed_value'])
  81. else:
  82. completed['value'] = completed.groupby(GROUP_COLUMNS, dropna=False, sort=False)[ 'value'].bfill()
  83. completed['value'] = completed.groupby(GROUP_COLUMNS, dropna=False, sort=False)['value'].ffill()
  84. completed['localTime'] = completed['localTime'].dt.strftime('%Y-%m-%d %H:%M:00')
  85. return completed
  86. def remove_dates(df: pd.DataFrame, dates: set[str]) -> pd.DataFrame:
  87. if df.empty:
  88. return df
  89. local_dates = pd.to_datetime(df['localTime']).dt.strftime('%Y%m%d')
  90. return df.loc[~local_dates.isin(dates)].copy()
  91. def combine_cms_data(
  92. relative_path: Path,
  93. files: list[tuple[str, Path]],
  94. latest_date: str,
  95. generate_total: bool = True,
  96. ) -> None:
  97. logger.info(f'{relative_path}: 开始处理,共 {len(files)} 天')
  98. total_path = days_dir_total / relative_path
  99. recent_path = days_dir_30 / relative_path
  100. existing_recent = read_parquet(recent_path) if recent_path.exists() else pd.DataFrame(columns=REQUIRED_COLUMNS)
  101. history = existing_recent.copy()
  102. completed_days: list[pd.DataFrame] = []
  103. for query_date, file_path in files:
  104. query_day = pd.Timestamp(datetime.strptime(query_date, '%Y%m%d'))
  105. completed = complete_minute_data_fast(read_parquet(file_path), query_day.strftime('%Y-%m-%d'), history)
  106. completed_days.append(completed)
  107. history = pd.concat([history, completed], ignore_index=True)
  108. bak_path = tmp_bak_dir / query_date / relative_path
  109. bak_path.parent.mkdir(parents=True, exist_ok=True)
  110. shutil.move(str(file_path), str(bak_path))
  111. logger.debug(f'{relative_path}: {query_date} 完成,已移动到 {bak_path}')
  112. input_dates = {date for date, _ in files}
  113. combined = pd.concat(
  114. [remove_dates(existing_recent, input_dates), *completed_days], ignore_index=True
  115. )
  116. combined['localTime'] = pd.to_datetime(combined['localTime'])
  117. combined = combined.sort_values([*GROUP_COLUMNS, 'localTime']).drop_duplicates(
  118. [*GROUP_COLUMNS, 'localTime'], keep='last'
  119. )
  120. combined['localTime'] = combined['localTime'].dt.strftime('%Y-%m-%d %H:%M:00')
  121. if generate_total:
  122. total_path.parent.mkdir(parents=True, exist_ok=True)
  123. combined.to_parquet(total_path, index=False)
  124. logger.info(f'{relative_path}: total 已写入,共 {len(combined)} 条')
  125. else:
  126. logger.info(f'{relative_path}: 跳过 total 写入')
  127. latest_day = pd.Timestamp(datetime.strptime(latest_date, '%Y%m%d'))
  128. recent_start = latest_day - timedelta(days=29)
  129. recent = combined.loc[pd.to_datetime(combined['localTime']) >= recent_start].copy()
  130. recent_path.parent.mkdir(parents=True, exist_ok=True)
  131. recent.to_parquet(recent_path, index=False)
  132. logger.info(f'{relative_path}: 完成,30days {len(recent)} 条')
  133. def main(generate_total: bool = False):
  134. days_dir_total.mkdir(parents=True, exist_ok=True)
  135. days_dir_30.mkdir(parents=True, exist_ok=True)
  136. files_by_path, latest_date = get_files_by_relative_path()
  137. logger.info(f'待处理文件: {len(files_by_path)},generate_total={generate_total}')
  138. if latest_date is None:
  139. raise SystemExit('tmp 下没有 YYYYMMDD 日期文件夹')
  140. with multiprocessing.Pool(processes=4, maxtasksperchild=5) as pool:
  141. pool.starmap(
  142. combine_cms_data,
  143. [
  144. (relative_path, files, latest_date, generate_total)
  145. for relative_path, files in files_by_path.items()
  146. ],
  147. )
  148. if __name__ == '__main__':
  149. main(generate_total=True)