优化数据预处理

This commit is contained in:
2026-02-02 09:44:07 +08:00
parent c8fe5e6d6f
commit b033eb61cc
12 changed files with 516 additions and 39 deletions

View File

@@ -0,0 +1,89 @@
# 数据预处理模块
独立的数据清洗工具,用于在正式分析前准备数据。
## 功能
- **数据合并**:将多个 Excel/CSV 文件合并为单一 CSV
- **时间排序**:按时间列对数据进行排序
- **目录管理**:标准化的原始数据和输出数据目录
## 目录结构
```
project/
├── raw_data/ # 原始数据存放目录
│ ├── remotecontrol/ # 按数据来源分类
│ └── ...
├── cleaned_data/ # 清洗后数据输出目录
│ ├── xxx_merged.csv
│ └── xxx_sorted.csv
└── data_preprocessing/ # 本模块
```
## 使用方法
### 命令行
```bash
# 初始化目录结构
python -m data_preprocessing.cli init
# 合并 Excel 文件
python -m data_preprocessing.cli merge --source raw_data/remotecontrol
# 合并并按时间排序
python -m data_preprocessing.cli merge --source raw_data/remotecontrol --sort-by SendTime
# 指定输出路径
python -m data_preprocessing.cli merge -s raw_data/remotecontrol -o cleaned_data/my_output.csv
# 排序已有 CSV
python -m data_preprocessing.cli sort --input some_file.csv --time-col SendTime
# 原地排序(覆盖原文件)
python -m data_preprocessing.cli sort --input data.csv --inplace
```
### Python API
```python
from data_preprocessing import merge_files, sort_by_time, Config
# 合并文件
output_path = merge_files(
source_dir="raw_data/remotecontrol",
output_file="cleaned_data/merged.csv",
pattern="*.xlsx",
time_column="SendTime" # 可选:合并后排序
)
# 排序 CSV
sorted_path = sort_by_time(
input_path="data.csv",
output_path="sorted_data.csv",
time_column="CreateTime"
)
# 自定义配置
config = Config()
config.raw_data_dir = "/path/to/raw"
config.cleaned_data_dir = "/path/to/cleaned"
config.ensure_dirs()
```
## 配置项
| 配置项 | 默认值 | 说明 |
|--------|--------|------|
| `raw_data_dir` | `raw_data/` | 原始数据目录 |
| `cleaned_data_dir` | `cleaned_data/` | 清洗输出目录 |
| `default_time_column` | `SendTime` | 默认时间列名 |
| `csv_encoding` | `utf-8-sig` | CSV 编码格式 |
## 注意事项
1. 本模块与 `DataAnalysisAgent` 完全独立,不会相互调用
2. 合并时会自动添加 `_source_file` 列标记数据来源(可用 `--no-source-col` 禁用)
3. Excel 文件会自动合并所有 Sheet
4. 无效时间值在排序时会被放到最后

View File

@@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
"""
数据预处理模块
提供独立的数据清洗功能:
- 按时间排序
- 同类数据合并
"""
from .sorter import sort_by_time
from .merger import merge_files
from .config import Config
__all__ = ["sort_by_time", "merge_files", "Config"]

140
data_preprocessing/cli.py Normal file
View File

@@ -0,0 +1,140 @@
# -*- coding: utf-8 -*-
"""
数据预处理命令行接口
使用示例:
# 合并 Excel 文件
python -m data_preprocessing.cli merge --source raw_data/remotecontrol --output cleaned_data/merged.csv
# 合并并排序
python -m data_preprocessing.cli merge --source raw_data/remotecontrol --sort-by SendTime
# 排序已有 CSV
python -m data_preprocessing.cli sort --input data.csv --output sorted.csv --time-col SendTime
# 初始化目录结构
python -m data_preprocessing.cli init
"""
import argparse
import sys
from .config import default_config
from .sorter import sort_by_time
from .merger import merge_files
def main():
parser = argparse.ArgumentParser(
prog="data_preprocessing",
description="数据预处理工具:排序、合并",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
示例:
%(prog)s merge --source raw_data/remotecontrol --sort-by SendTime
%(prog)s sort --input data.csv --time-col CreateTime
%(prog)s init
"""
)
subparsers = parser.add_subparsers(dest="command", help="可用命令")
# ========== merge 命令 ==========
merge_parser = subparsers.add_parser("merge", help="合并同类文件")
merge_parser.add_argument(
"--source", "-s",
required=True,
help="源数据目录路径"
)
merge_parser.add_argument(
"--output", "-o",
default=None,
help="输出文件路径 (默认: cleaned_data/<目录名>_merged.csv)"
)
merge_parser.add_argument(
"--pattern", "-p",
default="*.xlsx",
help="文件匹配模式 (默认: *.xlsx)"
)
merge_parser.add_argument(
"--sort-by",
default=None,
dest="time_column",
help="合并后按此时间列排序"
)
merge_parser.add_argument(
"--no-source-col",
action="store_true",
help="不添加来源文件列"
)
# ========== sort 命令 ==========
sort_parser = subparsers.add_parser("sort", help="按时间排序 CSV")
sort_parser.add_argument(
"--input", "-i",
required=True,
help="输入 CSV 文件路径"
)
sort_parser.add_argument(
"--output", "-o",
default=None,
help="输出文件路径 (默认: cleaned_data/<文件名>_sorted.csv)"
)
sort_parser.add_argument(
"--time-col", "-t",
default=None,
dest="time_column",
help=f"时间列名 (默认: {default_config.default_time_column})"
)
sort_parser.add_argument(
"--inplace",
action="store_true",
help="原地覆盖输入文件"
)
# ========== init 命令 ==========
init_parser = subparsers.add_parser("init", help="初始化目录结构")
# 解析参数
args = parser.parse_args()
if args.command is None:
parser.print_help()
sys.exit(0)
try:
if args.command == "merge":
result = merge_files(
source_dir=args.source,
output_file=args.output,
pattern=args.pattern,
time_column=args.time_column,
add_source_column=not args.no_source_col
)
print(f"\n✅ 合并成功: {result}")
elif args.command == "sort":
result = sort_by_time(
input_path=args.input,
output_path=args.output,
time_column=args.time_column,
inplace=args.inplace
)
print(f"\n✅ 排序成功: {result}")
elif args.command == "init":
default_config.ensure_dirs()
print("\n✅ 目录初始化完成")
except FileNotFoundError as e:
print(f"\n❌ 错误: {e}")
sys.exit(1)
except KeyError as e:
print(f"\n❌ 错误: {e}")
sys.exit(1)
except Exception as e:
print(f"\n❌ 未知错误: {e}")
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,42 @@
# -*- coding: utf-8 -*-
"""
数据预处理模块配置
"""
import os
from dataclasses import dataclass
# 获取项目根目录
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@dataclass
class Config:
"""预处理模块配置"""
# 原始数据存放目录
raw_data_dir: str = os.path.join(PROJECT_ROOT, "raw_data")
# 清洗后数据输出目录
cleaned_data_dir: str = os.path.join(PROJECT_ROOT, "cleaned_data")
# 默认时间列名
default_time_column: str = "SendTime"
# 支持的文件扩展名
supported_extensions: tuple = (".csv", ".xlsx", ".xls")
# CSV 编码
csv_encoding: str = "utf-8-sig"
def ensure_dirs(self):
"""确保目录存在"""
os.makedirs(self.raw_data_dir, exist_ok=True)
os.makedirs(self.cleaned_data_dir, exist_ok=True)
print(f"[OK] 目录已就绪:")
print(f" 原始数据: {self.raw_data_dir}")
print(f" 清洗输出: {self.cleaned_data_dir}")
# 默认配置实例
default_config = Config()

View File

@@ -0,0 +1,83 @@
import pandas as pd
import glob
import os
def merge_excel_files(source_dir="remotecontrol", output_file="merged_all_files.csv"):
"""
将指定目录下的所有 Excel 文件 (.xlsx, .xls) 合并为一个 CSV 文件。
"""
print(f"[SEARCH] 正在扫描目录: {source_dir} ...")
# 支持 xlsx 和 xls
files_xlsx = glob.glob(os.path.join(source_dir, "*.xlsx"))
files_xls = glob.glob(os.path.join(source_dir, "*.xls"))
files = files_xlsx + files_xls
if not files:
print("[WARN] 未找到 Excel 文件。")
return
# 按文件名中的数字进行排序 (例如: 1.xlsx, 2.xlsx, ..., 10.xlsx)
try:
files.sort(key=lambda x: int(os.path.basename(x).split('.')[0]))
print("[NUM] 已按文件名数字顺序排序")
except ValueError:
# 如果文件名不是纯数字,退回到字母排序
files.sort()
print("[TEXT] 已按文件名包含非数字字符,使用字母顺序排序")
print(f"[DIR] 找到 {len(files)} 个文件: {files}")
all_dfs = []
for file in files:
try:
print(f"[READ] 读取: {file}")
# 使用 ExcelFile 读取所有 sheet
xls = pd.ExcelFile(file)
print(f" [PAGES] 包含 Sheets: {xls.sheet_names}")
file_dfs = []
for sheet_name in xls.sheet_names:
df = pd.read_excel(xls, sheet_name=sheet_name)
if not df.empty:
print(f" [OK] Sheet '{sheet_name}' 读取成功: {len(df)}")
file_dfs.append(df)
else:
print(f" [WARN] Sheet '{sheet_name}' 为空,跳过")
if file_dfs:
# 合并该文件的所有非空 sheet
file_merged_df = pd.concat(file_dfs, ignore_index=True)
# 可选:添加一列标记来源文件
file_merged_df['Source_File'] = os.path.basename(file)
all_dfs.append(file_merged_df)
else:
print(f"[WARN] 文件 {file} 所有 Sheet 均为空")
except Exception as e:
print(f"[ERROR] 读取 {file} 失败: {e}")
if all_dfs:
print("[LOOP] 正在合并数据...")
merged_df = pd.concat(all_dfs, ignore_index=True)
# 按 SendTime 排序
if 'SendTime' in merged_df.columns:
print("[TIMER] 正在按 SendTime 排序...")
merged_df['SendTime'] = pd.to_datetime(merged_df['SendTime'], errors='coerce')
merged_df = merged_df.sort_values(by='SendTime')
else:
print("[WARN] 未找到 SendTime 列,跳过排序")
print(f"[CACHE] 保存到: {output_file}")
merged_df.to_csv(output_file, index=False, encoding="utf-8-sig")
print(f"[OK] 合并及排序完成!总行数: {len(merged_df)}")
print(f" 输出文件: {os.path.abspath(output_file)}")
else:
print("[WARN] 没有成功读取到任何数据。")
if __name__ == "__main__":
# 如果需要在当前目录运行并合并 remotecontrol 文件夹下的内容
merge_excel_files(source_dir="remotecontrol", output_file="remotecontrol_merged.csv")

View File

@@ -0,0 +1,148 @@
# -*- coding: utf-8 -*-
"""
数据合并模块
合并同类 Excel/CSV 文件
"""
import os
import glob
import pandas as pd
from typing import Optional, List
from .config import default_config
def merge_files(
source_dir: str,
output_file: Optional[str] = None,
pattern: str = "*.xlsx",
time_column: Optional[str] = None,
add_source_column: bool = True
) -> str:
"""
合并目录下的所有同类文件
Args:
source_dir: 源数据目录
output_file: 输出 CSV 文件路径。如果为 None则输出到 cleaned_data 目录
pattern: 文件匹配模式 (e.g., "*.xlsx", "*.csv", "*.xls")
time_column: 可选,合并后按此列排序
add_source_column: 是否添加来源文件列
Returns:
输出文件的绝对路径
Raises:
FileNotFoundError: 目录不存在或未找到匹配文件
"""
if not os.path.isdir(source_dir):
raise FileNotFoundError(f"目录不存在: {source_dir}")
print(f"[SCAN] 正在扫描目录: {source_dir}")
print(f" 匹配模式: {pattern}")
# 查找匹配文件
files = glob.glob(os.path.join(source_dir, pattern))
# 如果是 xlsx也尝试匹配 xls
if pattern == "*.xlsx":
files.extend(glob.glob(os.path.join(source_dir, "*.xls")))
if not files:
raise FileNotFoundError(f"未找到匹配 '{pattern}' 的文件")
# 排序文件列表
files = _sort_files(files)
print(f"[FOUND] 找到 {len(files)} 个文件")
# 确定输出路径
if output_file is None:
default_config.ensure_dirs()
dir_name = os.path.basename(os.path.normpath(source_dir))
output_file = os.path.join(
default_config.cleaned_data_dir,
f"{dir_name}_merged.csv"
)
# 合并数据
all_dfs = []
for file in files:
try:
df = _read_file(file)
if df is not None and not df.empty:
if add_source_column:
df['_source_file'] = os.path.basename(file)
all_dfs.append(df)
except Exception as e:
print(f"[ERROR] 读取失败 {file}: {e}")
if not all_dfs:
raise ValueError("没有成功读取到任何数据")
print(f"[MERGE] 正在合并 {len(all_dfs)} 个数据源...")
merged_df = pd.concat(all_dfs, ignore_index=True)
print(f" 合并后总行数: {len(merged_df)}")
# 可选:按时间排序
if time_column and time_column in merged_df.columns:
print(f"[SORT] 正在按 '{time_column}' 排序...")
merged_df[time_column] = pd.to_datetime(merged_df[time_column], errors='coerce')
merged_df = merged_df.sort_values(by=time_column, na_position='last')
elif time_column:
print(f"[WARN] 未找到时间列 '{time_column}',跳过排序")
# 保存结果
print(f"[SAVE] 正在保存: {output_file}")
merged_df.to_csv(output_file, index=False, encoding=default_config.csv_encoding)
abs_output = os.path.abspath(output_file)
print(f"[OK] 合并完成!")
print(f" 输出文件: {abs_output}")
print(f" 总行数: {len(merged_df)}")
return abs_output
def _sort_files(files: List[str]) -> List[str]:
"""对文件列表进行智能排序"""
try:
# 尝试按文件名中的数字排序
files.sort(key=lambda x: int(os.path.basename(x).split('.')[0]))
print("[SORT] 已按文件名数字顺序排序")
except ValueError:
# 退回到字母排序
files.sort()
print("[SORT] 已按文件名字母顺序排序")
return files
def _read_file(file_path: str) -> Optional[pd.DataFrame]:
"""读取单个文件(支持 CSV 和 Excel"""
ext = os.path.splitext(file_path)[1].lower()
print(f"[READ] 读取: {os.path.basename(file_path)}")
if ext == '.csv':
df = pd.read_csv(file_path, low_memory=False)
print(f" 行数: {len(df)}")
return df
elif ext in ('.xlsx', '.xls'):
# 读取 Excel 所有 sheet 并合并
xls = pd.ExcelFile(file_path)
print(f" Sheets: {xls.sheet_names}")
sheet_dfs = []
for sheet_name in xls.sheet_names:
df = pd.read_excel(xls, sheet_name=sheet_name)
if not df.empty:
print(f" - Sheet '{sheet_name}': {len(df)}")
sheet_dfs.append(df)
if sheet_dfs:
return pd.concat(sheet_dfs, ignore_index=True)
return None
else:
print(f"[WARN] 不支持的文件格式: {ext}")
return None

View File

@@ -0,0 +1,45 @@
import pandas as pd
import os
def sort_csv_by_time(file_path="remotecontrol_merged.csv", time_col="SendTime"):
"""
读取 CSV 文件,按时间列排序,并保存。
"""
if not os.path.exists(file_path):
print(f"[ERROR] 文件不存在: {file_path}")
return
print(f"[READ] 正在读取 {file_path} ...")
try:
# 读取 CSV
df = pd.read_csv(file_path, low_memory=False)
print(f" [CHART] 数据行数: {len(df)}")
if time_col not in df.columns:
print(f"[ERROR] 未找到时间列: {time_col}")
print(f" 可用列: {list(df.columns)}")
return
print(f"[LOOP] 正在解析时间列 '{time_col}' ...")
# 转换为 datetime 对象,无法解析的设为 NaT
df[time_col] = pd.to_datetime(df[time_col], errors='coerce')
# 检查无效时间
nat_count = df[time_col].isna().sum()
if nat_count > 0:
print(f"[WARN] 发现 {nat_count} 行无效时间数据,排序时将排在最后")
print("[LOOP] 正在按时间排序...")
df_sorted = df.sort_values(by=time_col)
print(f"[CACHE] 正在保存及覆盖文件: {file_path} ...")
df_sorted.to_csv(file_path, index=False, encoding="utf-8-sig")
print("[OK] 排序并保存完成!")
except Exception as e:
print(f"[ERROR]处理失败: {e}")
if __name__ == "__main__":
sort_csv_by_time()

View File

@@ -0,0 +1,82 @@
# -*- coding: utf-8 -*-
"""
数据排序模块
按时间列对 CSV 文件进行排序
"""
import os
import pandas as pd
from typing import Optional
from .config import default_config
def sort_by_time(
input_path: str,
output_path: Optional[str] = None,
time_column: str = None,
inplace: bool = False
) -> str:
"""
按时间列对 CSV 文件排序
Args:
input_path: 输入 CSV 文件路径
output_path: 输出路径。如果为 None 且 inplace=False则输出到 cleaned_data 目录
time_column: 时间列名,默认使用配置中的 default_time_column
inplace: 是否原地覆盖输入文件
Returns:
输出文件的绝对路径
Raises:
FileNotFoundError: 输入文件不存在
KeyError: 时间列不存在
"""
# 参数处理
time_column = time_column or default_config.default_time_column
if not os.path.exists(input_path):
raise FileNotFoundError(f"文件不存在: {input_path}")
# 确定输出路径
if inplace:
output_path = input_path
elif output_path is None:
default_config.ensure_dirs()
basename = os.path.basename(input_path)
name, ext = os.path.splitext(basename)
output_path = os.path.join(
default_config.cleaned_data_dir,
f"{name}_sorted{ext}"
)
print(f"[READ] 正在读取: {input_path}")
df = pd.read_csv(input_path, low_memory=False)
print(f" 数据行数: {len(df)}")
# 检查时间列是否存在
if time_column not in df.columns:
available_cols = list(df.columns)
raise KeyError(
f"未找到时间列 '{time_column}'。可用列: {available_cols}"
)
print(f"[PARSE] 正在解析时间列 '{time_column}'...")
df[time_column] = pd.to_datetime(df[time_column], errors='coerce')
# 统计无效时间
nat_count = df[time_column].isna().sum()
if nat_count > 0:
print(f"[WARN] 发现 {nat_count} 行无效时间数据,排序时将排在最后")
print("[SORT] 正在按时间排序...")
df_sorted = df.sort_values(by=time_column, na_position='last')
print(f"[SAVE] 正在保存: {output_path}")
df_sorted.to_csv(output_path, index=False, encoding=default_config.csv_encoding)
abs_output = os.path.abspath(output_path)
print(f"[OK] 排序完成!输出文件: {abs_output}")
return abs_output