1009 lines
35 KiB
Python
1009 lines
35 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
无人机数据处理脚本
|
||
|
||
处理Excel格式的无人机测量数据,转换为GasFlux标准输入格式。
|
||
|
||
支持两种使用方式:
|
||
1. 命令行调用:python data_processor.py input.xlsx
|
||
2. 直接调用:from data_processor import process_file; df = process_file('input.xlsx')
|
||
|
||
处理步骤:
|
||
1. 读取Excel文件
|
||
2. 删除不需要的列
|
||
3. 根据文件名修正时间格式
|
||
4. 坐标转换(经纬度除以10^7)
|
||
5. 计算气压(使用qiya.py)
|
||
6. 高度调整(减去最小高度)
|
||
7. 时间戳融合
|
||
8. 字段重命名
|
||
|
||
作者:GasFlux开发团队
|
||
"""
|
||
|
||
import pandas as pd
|
||
import numpy as np
|
||
from pathlib import Path
|
||
import re
|
||
from datetime import datetime
|
||
import sys
|
||
import os
|
||
from collections import Counter
|
||
import yaml
|
||
|
||
try:
|
||
from tqdm import tqdm
|
||
HAS_TQDM = True
|
||
except ImportError:
|
||
HAS_TQDM = False
|
||
print("WARNING: 未安装tqdm库,将不显示进度条。如需进度条,请运行: pip install tqdm")
|
||
|
||
|
||
def _safe_print(msg):
|
||
"""
|
||
安全打印函数,保证任何情况下不因打印而失败
|
||
避免 Windows 控制台编码问题导致的 OSError
|
||
"""
|
||
try:
|
||
print(msg)
|
||
except Exception:
|
||
try:
|
||
# 回退方案:移除非ASCII字符后打印
|
||
safe_msg = str(msg).encode('ascii', 'ignore').decode('ascii', 'ignore')
|
||
sys.stdout.write(safe_msg + '\n')
|
||
sys.stdout.flush()
|
||
except Exception:
|
||
# 最终回退:静默失败,不影响程序运行
|
||
pass
|
||
|
||
|
||
def create_height_bins(heights, bin_size=2.0):
|
||
"""
|
||
将高度数据按指定间隔分档
|
||
|
||
Args:
|
||
heights: 高度数据Series
|
||
bin_size: 每个档位的间隔(米)
|
||
|
||
Returns:
|
||
list: [(bin_min, bin_max, bin_center, count), ...] 每个档位的信息
|
||
"""
|
||
if len(heights) == 0:
|
||
return []
|
||
|
||
height_min = heights.min()
|
||
height_max = heights.max()
|
||
|
||
# 计算需要的档位数量
|
||
range_size = height_max - height_min
|
||
if range_size == 0:
|
||
# 所有高度相同
|
||
return [(height_min, height_max, height_min, len(heights))]
|
||
|
||
num_bins = max(1, int(np.ceil(range_size / bin_size)))
|
||
|
||
bins = []
|
||
for i in range(num_bins):
|
||
bin_min = height_min + i * bin_size
|
||
bin_max = min(height_min + (i + 1) * bin_size, height_max)
|
||
bin_center = (bin_min + bin_max) / 2
|
||
|
||
# 统计这个档位有多少数据点
|
||
count = ((heights >= bin_min) & (heights < bin_max)).sum()
|
||
if i == num_bins - 1: # 最后一个档位包含上限
|
||
count = ((heights >= bin_min) & (heights <= bin_max)).sum()
|
||
|
||
if count > 0: # 只保留有数据的档位
|
||
bins.append((bin_min, bin_max, bin_center, count))
|
||
|
||
return bins
|
||
|
||
# 导入qiya模块
|
||
try:
|
||
from .qiya import get_pressure_at_location
|
||
|
||
except ImportError as e:
|
||
print(f"导入qiya模块失败: {e}")
|
||
print("请确保GasFlux包结构完整")
|
||
sys.exit(1)
|
||
|
||
|
||
def load_excel_data(file_path):
|
||
"""
|
||
读取Excel文件并进行初步处理
|
||
|
||
Args:
|
||
file_path: Excel文件路径
|
||
|
||
Returns:
|
||
pd.DataFrame: 读取的数据
|
||
"""
|
||
try:
|
||
_safe_print(f"正在读取Excel文件: {Path(file_path).name}")
|
||
|
||
# 读取Excel文件
|
||
df = pd.read_excel(file_path)
|
||
print(f" 数据读取成功: {len(df):,} 行 × {len(df.columns)} 列")
|
||
print(f" 检测到的列: {', '.join(df.columns[:5])}{'...' if len(df.columns) > 5 else ''}")
|
||
|
||
return df
|
||
|
||
except Exception as e:
|
||
_safe_print(f" 文件读取失败: {Path(file_path).name}")
|
||
print(f" 错误详情: {e}")
|
||
sys.exit(1)
|
||
|
||
|
||
def remove_columns(df, columns_to_remove):
|
||
"""
|
||
删除指定的列
|
||
|
||
Args:
|
||
df: 输入DataFrame
|
||
columns_to_remove: 要删除的列名列表
|
||
|
||
Returns:
|
||
pd.DataFrame: 删除列后的DataFrame
|
||
"""
|
||
print(f"\n 删除不需要的列: {columns_to_remove}")
|
||
|
||
# 检查要删除的列是否存在
|
||
existing_columns = [col for col in columns_to_remove if col in df.columns]
|
||
missing_columns = [col for col in columns_to_remove if col not in df.columns]
|
||
|
||
if missing_columns:
|
||
print(f"以下列不存在(跳过): {missing_columns}")
|
||
|
||
if existing_columns:
|
||
df = df.drop(columns=existing_columns)
|
||
print(f"已删除 {len(existing_columns)} 列")
|
||
|
||
return df
|
||
|
||
|
||
def extract_hour_from_filename(filename):
|
||
"""
|
||
从文件名中提取小时信息
|
||
|
||
例如: "08_34_01_间隔高度5m.xlsx" -> "08"
|
||
|
||
Args:
|
||
filename: 文件名
|
||
|
||
Returns:
|
||
str: 小时字符串(两位数)
|
||
"""
|
||
# 使用正则表达式匹配小时部分
|
||
match = re.match(r'(\d{2})_', filename)
|
||
if match:
|
||
return match.group(1)
|
||
else:
|
||
print(f"无法从文件名 '{filename}' 中提取小时信息,使用默认值 '00'")
|
||
return "00"
|
||
|
||
|
||
def fix_time_column(df, filename):
|
||
"""
|
||
根据文件名修正时间列
|
||
|
||
Args:
|
||
df: 输入DataFrame
|
||
filename: 文件名
|
||
|
||
Returns:
|
||
pd.DataFrame: 修正后的DataFrame
|
||
"""
|
||
print(" 根据文件名修正时间格式...")
|
||
|
||
# 提取小时信息
|
||
hour_prefix = extract_hour_from_filename(filename)
|
||
print(f"从文件名提取的小时: {hour_prefix}")
|
||
|
||
# 检查时间列是否存在
|
||
if '时间' not in df.columns:
|
||
print("未找到 '时间' 列")
|
||
return df
|
||
|
||
# 修正时间格式
|
||
def fix_single_time(time_str):
|
||
if pd.isna(time_str):
|
||
return time_str
|
||
|
||
time_str = str(time_str).strip()
|
||
|
||
# 如果时间格式类似 "0:34:01" 或 "00:34:01"
|
||
if re.match(r'^\d{1,2}:\d{2}:\d{2}$', time_str):
|
||
parts = time_str.split(':')
|
||
original_hour = int(parts[0])
|
||
filename_hour = int(hour_prefix)
|
||
|
||
# 小时相加
|
||
new_hour = (filename_hour + original_hour) % 24 # 防止超过24小时
|
||
|
||
# 保持分钟和秒不变
|
||
new_time = f"{new_hour:02d}:{parts[1]}:{parts[2]}"
|
||
return new_time
|
||
else:
|
||
# 其他格式,使用默认时间
|
||
return f"{hour_prefix}:00:00"
|
||
|
||
# 应用时间修正
|
||
original_times = df['时间'].head(3).tolist()
|
||
df['时间'] = df['时间'].apply(fix_single_time)
|
||
corrected_times = df['时间'].head(3).tolist()
|
||
|
||
print(f"时间修正示例:")
|
||
for orig, corr in zip(original_times, corrected_times):
|
||
print(f" {orig} → {corr}")
|
||
|
||
return df
|
||
|
||
|
||
def convert_coordinates(df):
|
||
"""
|
||
转换经纬度坐标(除以10^7)
|
||
|
||
Args:
|
||
df: 输入DataFrame
|
||
|
||
Returns:
|
||
pd.DataFrame: 转换后的DataFrame
|
||
"""
|
||
print(" 转换经纬度坐标(GPS原始数据除以10^7)...")
|
||
|
||
if 'stGPSPositionX' in df.columns:
|
||
original_lon = df['stGPSPositionX'].head(3).tolist()
|
||
df['stGPSPositionX'] = df['stGPSPositionX'] / 1e7
|
||
converted_lon = df['stGPSPositionX'].head(3).tolist()
|
||
print(" 经度转换示例:")
|
||
for i, (orig, conv) in enumerate(zip(original_lon, converted_lon)):
|
||
print(f" 第{i+1}行: {orig:.6f} → {conv:.6f}")
|
||
|
||
if 'stGPSPositionY' in df.columns:
|
||
original_lat = df['stGPSPositionY'].head(3).tolist()
|
||
df['stGPSPositionY'] = df['stGPSPositionY'] / 1e7
|
||
converted_lat = df['stGPSPositionY'].head(3).tolist()
|
||
print(" 纬度转换示例:")
|
||
for i, (orig, conv) in enumerate(zip(original_lat, converted_lat)):
|
||
print(f" 第{i+1}行: {orig:.6f} → {conv:.6f}")
|
||
|
||
return df
|
||
|
||
|
||
def calculate_pressure(df, max_samples=None, height_tolerance=10.0, height_bin_size=10.0):
|
||
"""
|
||
计算气压数据(高度分档优化版)
|
||
|
||
Args:
|
||
df: 输入DataFrame
|
||
max_samples: 最大采样数量(None表示计算所有行)
|
||
height_tolerance: 高度变化容差(米),如果所有高度都在此范围内,只计算一次
|
||
height_bin_size: 高度分档间隔(米),每个档位使用中间高度计算气压
|
||
|
||
Returns:
|
||
pd.DataFrame: 添加气压列的DataFrame
|
||
"""
|
||
print(" 计算气压数据(使用qiya.py大气模型)...")
|
||
|
||
# 检查必要列是否存在(支持原始列名和新列名)
|
||
# 原始数据中的列名
|
||
original_cols = ['qStrDate', 'qStrTime', 'stGPSPositionX', 'stGPSPositionY', 'fAltitudeFused']
|
||
# 坐标转换后的列名(经纬度已被除以1e7)
|
||
converted_cols = ['qStrDate', 'qStrTime', 'stGPSPositionX', 'stGPSPositionY', 'fAltitudeFused']
|
||
|
||
# 优先使用转换后的列名(如果存在),否则使用原始列名
|
||
date_col = 'qStrDate' if 'qStrDate' in df.columns else '日期'
|
||
time_col = 'qStrTime' if 'qStrTime' in df.columns else '时间'
|
||
lon_col = 'stGPSPositionX' if 'stGPSPositionX' in df.columns else '经度'
|
||
lat_col = 'stGPSPositionY' if 'stGPSPositionY' in df.columns else '纬度'
|
||
height_col = 'fAltitudeFused' if 'fAltitudeFused' in df.columns else '融合高程'
|
||
|
||
required_cols = [date_col, time_col, lon_col, lat_col, height_col]
|
||
missing_cols = [col for col in required_cols if col not in df.columns]
|
||
|
||
if missing_cols:
|
||
print(f"缺少必要列: {missing_cols}")
|
||
return df
|
||
|
||
# 检查高度变化范围
|
||
height_min = df[height_col].min()
|
||
height_max = df[height_col].max()
|
||
height_range = height_max - height_min
|
||
|
||
print(f"高度范围: {height_min:.1f} - {height_max:.1f} 米 (变化: {height_range:.1f} 米)")
|
||
# 创建高度分档
|
||
height_bins = create_height_bins(df[height_col], height_bin_size)
|
||
print(f"高度分档: {len(height_bins)} 个档位 (间隔: {height_bin_size:.1f} 米)")
|
||
|
||
for i, (bin_min, bin_max, bin_center, count) in enumerate(height_bins):
|
||
print(f" 档位{i+1}: {bin_min:.1f}-{bin_max:.1f}m (中心: {bin_center:.1f}m, 数据: {count}行)")
|
||
# 决定计算策略
|
||
if height_range <= height_tolerance:
|
||
# 高度变化小,只计算一次气压
|
||
print("高度变化小,将使用平均高度计算一次气压")
|
||
use_single_calculation = True
|
||
mean_height = df[height_col].mean()
|
||
print(f"使用平均高度: {mean_height:.1f} 米")
|
||
elif len(height_bins) == 1:
|
||
# 只有一个高度档位,使用档位中心高度
|
||
print("只有一个高度档位,使用档位中心高度")
|
||
use_single_calculation = True
|
||
mean_height = height_bins[0][2] # bin_center
|
||
print(f"使用档位中心高度: {mean_height:.1f} 米")
|
||
else:
|
||
# 高度变化大,使用分档计算
|
||
print("使用高度分档策略,减少API调用")
|
||
use_single_calculation = False
|
||
|
||
# 确定要处理的行数
|
||
if max_samples is None or max_samples >= len(df):
|
||
# 计算所有行
|
||
sample_df = df.copy()
|
||
actual_samples = len(df)
|
||
if not use_single_calculation:
|
||
print(f"将计算所有 {len(df)} 行的气压数据")
|
||
else:
|
||
# 限制采样数量
|
||
print(f"数据量较大 ({len(df)} 行),只对前 {max_samples} 行计算气压")
|
||
sample_df = df.head(max_samples).copy()
|
||
actual_samples = max_samples
|
||
|
||
pressures = []
|
||
|
||
if use_single_calculation:
|
||
# 只计算一次气压
|
||
try:
|
||
# 使用第一行的日期和时间作为代表
|
||
first_row = sample_df.iloc[0]
|
||
|
||
# 转换日期格式 - 只提取日期部分,移除任何时间信息
|
||
date_str = str(first_row[date_col])
|
||
if ' ' in date_str:
|
||
date_str = date_str.split(' ')[0] # 处理 "2026-01-15 00:00:00" 格式
|
||
elif 'T' in date_str:
|
||
date_str = date_str.split('T')[0] # 处理ISO格式
|
||
|
||
if '/' in date_str:
|
||
date_str = date_str.replace('/', '-')
|
||
|
||
date_parts = date_str.split('-')
|
||
if len(date_parts) == 3 and len(date_parts[0]) == 4:
|
||
year, month, day = date_parts
|
||
formatted_date = f"{year}-{month.zfill(2)}-{day.zfill(2)}"
|
||
else:
|
||
raise ValueError(f"日期格式异常: {date_str}")
|
||
|
||
# 使用数据的代表性时间(整点小时,众数)
|
||
time_strings = []
|
||
for time_val in sample_df[time_col]:
|
||
time_str = str(time_val).strip()
|
||
|
||
# 处理时间字符串,提取正确的部分
|
||
# 如果时间字符串包含多个时间部分(如 "00:00:00 08:37:12"),取最后一个
|
||
if ' ' in time_str:
|
||
time_parts = time_str.split()
|
||
time_str = time_parts[-1] # 取最后一个有效的时间部分
|
||
|
||
if ':' in time_str:
|
||
# 确保是有效的 HH:MM 格式,然后取整点小时
|
||
parts = time_str.split(':')
|
||
if len(parts) >= 2:
|
||
try:
|
||
hour = int(parts[0])
|
||
# 确保小时在有效范围内 (0-23)
|
||
if 0 <= hour <= 23:
|
||
time_strings.append(f"{hour:02d}:00")
|
||
except ValueError:
|
||
continue
|
||
|
||
if time_strings:
|
||
time_counts = Counter(time_strings)
|
||
formatted_time = time_counts.most_common(1)[0][0]
|
||
occurrence_count = time_counts.most_common(1)[0][1]
|
||
print(f"数据时间选择: {formatted_time} ({occurrence_count}/{len(time_strings)} 次,{occurrence_count/len(time_strings)*100:.1f}%)")
|
||
else:
|
||
formatted_time = "12:00"
|
||
print("无有效时间数据,使用默认中午12:00")
|
||
|
||
print("正在计算平均气压...")
|
||
pressure = get_pressure_at_location(
|
||
lat=sample_df[lat_col].mean(),
|
||
lon=sample_df[lon_col].mean(),
|
||
altitude=mean_height,
|
||
date=formatted_date,
|
||
time=formatted_time
|
||
)
|
||
|
||
if pressure is not None:
|
||
pressures = [pressure] * len(sample_df)
|
||
print("平均气压计算成功,将应用到所有行")
|
||
else:
|
||
print("平均气压计算失败")
|
||
pressures = [None] * len(sample_df)
|
||
|
||
except Exception as e:
|
||
print(f"平均气压计算失败: {e}")
|
||
pressures = [None] * len(sample_df)
|
||
|
||
else:
|
||
# 使用高度分档策略
|
||
print("开始分档计算气压...")
|
||
|
||
# 为每个高度档位计算气压
|
||
bin_pressures = {} # bin_center -> pressure
|
||
|
||
# 设置进度条
|
||
iterator = height_bins
|
||
if HAS_TQDM:
|
||
iterator = tqdm(iterator, total=len(height_bins), desc="计算气压档位", unit="档")
|
||
|
||
for bin_min, bin_max, bin_center, count in iterator:
|
||
try:
|
||
# 使用第一行数据作为代表来获取日期和时间
|
||
# 找到这个档位中的一行数据
|
||
bin_rows = sample_df[(sample_df[height_col] >= bin_min) &
|
||
(sample_df[height_col] <= bin_max)]
|
||
if len(bin_rows) == 0:
|
||
continue
|
||
|
||
first_row = bin_rows.iloc[0]
|
||
|
||
# 转换日期格式 - 只提取日期部分,移除任何时间信息
|
||
date_str = str(first_row[date_col])
|
||
if ' ' in date_str:
|
||
date_str = date_str.split(' ')[0] # 处理 "2026-01-15 00:00:00" 格式
|
||
elif 'T' in date_str:
|
||
date_str = date_str.split('T')[0] # 处理ISO格式
|
||
|
||
if '/' in date_str:
|
||
date_str = date_str.replace('/', '-')
|
||
|
||
date_parts = date_str.split('-')
|
||
if len(date_parts) == 3 and len(date_parts[0]) == 4:
|
||
year, month, day = date_parts
|
||
formatted_date = f"{year}-{month.zfill(2)}-{day.zfill(2)}"
|
||
else:
|
||
print(f"档位高度 {bin_center:.1f}m 日期格式异常: {date_str}")
|
||
bin_pressures[bin_center] = None
|
||
continue
|
||
|
||
# 使用该档位数据的代表性时间(整点小时)
|
||
time_strings = []
|
||
for time_val in bin_rows[time_col]:
|
||
time_str = str(time_val).strip()
|
||
|
||
# 处理时间字符串,提取正确的部分
|
||
# 如果时间字符串包含多个时间部分(如 "00:00:00 08:37:12"),取最后一个
|
||
if ' ' in time_str:
|
||
time_parts = time_str.split()
|
||
time_str = time_parts[-1] # 取最后一个有效的时间部分
|
||
|
||
if ':' in time_str:
|
||
# 确保是有效的 HH:MM 格式,然后取整点小时
|
||
parts = time_str.split(':')
|
||
if len(parts) >= 2:
|
||
try:
|
||
hour = int(parts[0])
|
||
# 确保小时在有效范围内 (0-23)
|
||
if 0 <= hour <= 23:
|
||
time_strings.append(f"{hour:02d}:00")
|
||
except ValueError:
|
||
continue
|
||
|
||
if time_strings:
|
||
# 使用最常见的时间(众数)
|
||
time_counts = Counter(time_strings)
|
||
formatted_time = time_counts.most_common(1)[0][0]
|
||
occurrence_count = time_counts.most_common(1)[0][1]
|
||
print(f" 档位时间选择: {formatted_time} ({occurrence_count}/{len(time_strings)} 次,{occurrence_count/len(time_strings)*100:.1f}%)")
|
||
else:
|
||
# 如果没有有效时间,使用中午12点
|
||
formatted_time = "12:00"
|
||
print(" 无有效时间数据,使用默认中午12:00")
|
||
|
||
# 计算这个档位的气压(使用平均位置和档位中心高度)
|
||
avg_lat = bin_rows[lat_col].mean()
|
||
avg_lon = bin_rows[lon_col].mean()
|
||
|
||
pressure = get_pressure_at_location(
|
||
lat=avg_lat,
|
||
lon=avg_lon,
|
||
altitude=bin_center,
|
||
date=formatted_date,
|
||
time=formatted_time
|
||
)
|
||
|
||
bin_pressures[bin_center] = pressure
|
||
|
||
# 更新进度条
|
||
if HAS_TQDM:
|
||
success_count = sum(1 for p in bin_pressures.values() if p is not None)
|
||
iterator.set_description(f"计算档位 (成功: {success_count}/{len(bin_pressures)})")
|
||
|
||
except Exception as e:
|
||
print(f"计算高度档位 {bin_center:.1f}m 气压失败: {e}")
|
||
bin_pressures[bin_center] = None
|
||
|
||
# 为每一行分配对应档位的气压
|
||
pressures = []
|
||
for idx, row in sample_df.iterrows():
|
||
# 找到这个高度对应的档位
|
||
height = row[height_col]
|
||
assigned_pressure = None
|
||
|
||
for bin_min, bin_max, bin_center, count in height_bins:
|
||
if bin_min <= height <= bin_max:
|
||
assigned_pressure = bin_pressures.get(bin_center)
|
||
break
|
||
|
||
pressures.append(assigned_pressure)
|
||
|
||
print(f"完成分档气压计算,共 {len(bin_pressures)} 个档位,{len([p for p in bin_pressures.values() if p is not None])} 个成功")
|
||
|
||
# 添加气压列
|
||
df['pressure'] = None # 初始化
|
||
df.loc[sample_df.index, 'pressure'] = pressures
|
||
|
||
# 对于未计算的行,使用插值或平均值填充
|
||
if max_samples is not None and len(df) > max_samples:
|
||
# 只计算了部分行,用平均值填充其余行
|
||
valid_pressures_for_mean = [p for p in pressures if p is not None]
|
||
if valid_pressures_for_mean:
|
||
mean_pressure = sum(valid_pressures_for_mean) / len(valid_pressures_for_mean)
|
||
df['pressure'] = df['pressure'].fillna(mean_pressure)
|
||
print(f"使用平均气压填充其余 {len(df) - max_samples} 行: {mean_pressure:.1f} hPa")
|
||
|
||
# 统计信息
|
||
valid_pressures = [p for p in pressures if p is not None]
|
||
if valid_pressures:
|
||
avg_pressure = sum(valid_pressures) / len(valid_pressures)
|
||
print(f"成功计算 {len(valid_pressures)}/{actual_samples} 个气压值,平均值: {avg_pressure:.1f} hPa")
|
||
else:
|
||
print("未能计算出任何气压值")
|
||
return df
|
||
|
||
|
||
def adjust_altitude(df):
|
||
"""
|
||
创建调整后的高度字段(减去最小值)
|
||
|
||
Args:
|
||
df: 输入DataFrame
|
||
|
||
Returns:
|
||
pd.DataFrame: 添加调整后高度字段的DataFrame
|
||
"""
|
||
print(" 创建调整后的高度字段...")
|
||
|
||
if 'fAltitudeFused' in df.columns:
|
||
min_altitude = df['fAltitudeFused'].min()
|
||
print(f" 最小高度值: {min_altitude:.2f} 米")
|
||
|
||
# 创建新的调整后高度字段,而不是修改原始字段
|
||
df['height_ato'] = df['fAltitudeFused'] - min_altitude
|
||
|
||
original_alt = df['fAltitudeFused'].head(3).tolist()
|
||
adjusted_alt = df['height_ato'].head(3).tolist()
|
||
|
||
print(" 高度调整示例:")
|
||
for i, (orig, adj) in enumerate(zip(original_alt, adjusted_alt)):
|
||
print(f" 第{i+1}行: {orig:.2f}m → {adj:.2f}m")
|
||
else:
|
||
print(" 未找到 'fAltitudeFused' 列,跳过高度调整")
|
||
|
||
return df
|
||
|
||
|
||
def merge_timestamp(df):
|
||
"""
|
||
融合日期和时间列为时间戳(修正时间格式)
|
||
|
||
Args:
|
||
df: 输入DataFrame
|
||
|
||
Returns:
|
||
pd.DataFrame: 融合后的DataFrame
|
||
"""
|
||
print(" 融合日期和时间为时间戳...")
|
||
|
||
if 'qStrDate' in df.columns and 'qStrTime' in df.columns:
|
||
timestamps = []
|
||
|
||
for idx, row in df.iterrows():
|
||
try:
|
||
date_str = str(row['qStrDate']).strip()
|
||
time_str = str(row['qStrTime']).strip()
|
||
|
||
# 处理日期字符串,提取纯日期部分
|
||
# 如果日期字符串包含时间部分(如 "2026-01-15 00:00:00"),取日期部分
|
||
if ' ' in date_str:
|
||
date_parts = date_str.split()
|
||
date_str = date_parts[0] # 取第一个部分作为日期
|
||
|
||
# 处理时间字符串,提取正确的部分
|
||
# 如果时间字符串包含多个时间部分(如 "00:00:00 08:37:12"),取最后一个
|
||
if ' ' in time_str:
|
||
time_parts = time_str.split()
|
||
# 取最后一个有效的时间部分
|
||
time_str = time_parts[-1]
|
||
|
||
# 确保时间格式正确
|
||
if ':' in time_str and len(time_str.split(':')) >= 2:
|
||
# 组合日期和修正后的时间
|
||
timestamp = f"{date_str} {time_str}"
|
||
else:
|
||
timestamp = f"{date_str} 12:00:00" # 默认中午时间
|
||
print(f" 时间格式异常 '{row['qStrTime']}',使用默认时间")
|
||
|
||
timestamps.append(timestamp)
|
||
|
||
except Exception as e:
|
||
print(f"处理第 {idx+1} 行时间戳失败: {e}")
|
||
timestamps.append(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||
|
||
df['timestamp'] = timestamps
|
||
|
||
print("时间戳融合示例:")
|
||
for i in range(min(3, len(timestamps))):
|
||
print(f" {df.loc[i, 'qStrDate']} + {df.loc[i, 'qStrTime']} → {timestamps[i]}")
|
||
|
||
return df
|
||
|
||
|
||
def rename_columns(df):
|
||
"""
|
||
重命名字段为GasFlux标准格式
|
||
|
||
Args:
|
||
df: 输入DataFrame
|
||
|
||
Returns:
|
||
pd.DataFrame: 重命名后的DataFrame
|
||
"""
|
||
print(" 重命名字段为GasFlux标准格式...")
|
||
|
||
# 定义字段映射
|
||
column_mapping = {
|
||
'timestamp': 'timestamp', # 时间戳(已创建)
|
||
'stGPSPositionX': 'longitude', # 经度 → longitude
|
||
'stGPSPositionY': 'latitude', # 纬度 → latitude
|
||
# 'fAltitudeFused': 'height_ato', # 高度字段已在adjust_altitude中处理
|
||
'fFixedWindDirection': 'winddir', # 修正风向 → winddir
|
||
'fFixedWindSpeed': 'windspeed', # 修正风速 → windspeed
|
||
'fWindTemperature': 'temperature', # 风温 → temperature
|
||
'CO2': 'CO2', # CO2浓度 → co2
|
||
'pitch': 'course_elevation', # pitch → course_elevation
|
||
'yaw': 'course_azimuth' # yaw → course_azimuth
|
||
}
|
||
|
||
# 先复制字段,再对复制的字段重命名
|
||
columns_renamed = []
|
||
for old_name, new_name in column_mapping.items():
|
||
if old_name in df.columns:
|
||
# 复制字段到新名称
|
||
df[new_name] = df[old_name].copy()
|
||
columns_renamed.append((old_name, new_name))
|
||
print(f" 复制并重命名: {old_name} → {new_name}")
|
||
|
||
if columns_renamed:
|
||
print(f"共处理了 {len(columns_renamed)} 个字段")
|
||
|
||
return df
|
||
|
||
|
||
def ensure_float64_types(df, config=None):
|
||
"""
|
||
确保数值字段为float64类型,以满足GasFlux处理要求
|
||
|
||
Args:
|
||
df: 输入DataFrame
|
||
config: 配置字典,包含gases字段
|
||
|
||
Returns:
|
||
pd.DataFrame: 数据类型转换后的DataFrame
|
||
"""
|
||
print(" 确保数值字段类型为float64(GasFlux要求)...")
|
||
|
||
# 定义基础需要转换为float64的字段
|
||
float64_columns = [
|
||
'longitude', 'latitude', 'height_ato', # 位置和高度
|
||
'winddir', 'windspeed', 'temperature', # 风和温度
|
||
'pressure', # 气压
|
||
'course_elevation', 'course_azimuth' # 姿态角
|
||
]
|
||
|
||
# 如果提供了配置,添加gases中的气体列
|
||
if config and 'gases' in config:
|
||
gas_columns = list(config['gases'].keys())
|
||
float64_columns.extend(gas_columns)
|
||
print(f"从配置中添加气体列: {gas_columns}")
|
||
|
||
converted_count = 0
|
||
for col in float64_columns:
|
||
if col in df.columns:
|
||
try:
|
||
original_dtype = df[col].dtype
|
||
df[col] = df[col].astype('float64')
|
||
new_dtype = df[col].dtype
|
||
if original_dtype != new_dtype:
|
||
print(f" 转换: {col} ({original_dtype} → {new_dtype})")
|
||
converted_count += 1
|
||
except Exception as e:
|
||
print(f" 转换失败: {col} - {e}")
|
||
|
||
if converted_count > 0:
|
||
print(f"共转换了 {converted_count} 个字段的数据类型为float64")
|
||
else:
|
||
print("所有数值字段已经是float64类型")
|
||
|
||
return df
|
||
|
||
|
||
def process_excel_file(file_path, config_path=None):
|
||
"""
|
||
处理单个Excel文件的主函数
|
||
|
||
Args:
|
||
file_path: Excel文件路径
|
||
config_path: 配置文件路径,用于读取gases配置
|
||
"""
|
||
_safe_print(f" === 开始处理文件: {Path(file_path).name} ===\n")
|
||
|
||
# 1. 读取数据
|
||
df = load_excel_data(file_path)
|
||
|
||
# 2. 坐标转换
|
||
df = convert_coordinates(df)
|
||
|
||
# 4. 计算气压数据
|
||
df = calculate_pressure(df, max_samples=None, height_tolerance=10.0, height_bin_size=2.0) # 计算所有行,高度容差10米,分档2米
|
||
|
||
# 5. 高度调整
|
||
df = adjust_altitude(df)
|
||
|
||
# 6. 时间戳融合(保持原有时间格式)
|
||
df = merge_timestamp(df)
|
||
|
||
# 调试:检查当前列
|
||
print(f"时间戳融合后列名: {list(df.columns)}")
|
||
if 'timestamp' in df.columns:
|
||
print(f"timestamp列示例: {df['timestamp'].head(3).tolist()}")
|
||
|
||
# 7. 字段重命名
|
||
df = rename_columns(df)
|
||
|
||
# 8. 确保数值字段类型为float64
|
||
# 读取配置以获取gases字段
|
||
config = None
|
||
if config_path:
|
||
try:
|
||
with open(config_path, 'r', encoding='utf-8') as f:
|
||
config = yaml.safe_load(f)
|
||
except Exception as e:
|
||
print(f"读取配置文件失败: {e}")
|
||
|
||
df = ensure_float64_types(df, config)
|
||
|
||
# 保存处理结果
|
||
output_path = Path(file_path).with_suffix('.processed.csv')
|
||
df.to_csv(output_path, index=False, encoding='utf-8-sig')
|
||
|
||
print(f"\n 处理完成!")
|
||
_safe_print(f" 输出文件: {output_path}")
|
||
print(f" 最终数据: {df.shape[0]:,} 行 × {df.shape[1]} 列")
|
||
print(f" 最终字段: {', '.join(df.columns)}")
|
||
|
||
return df
|
||
|
||
|
||
def process_file(input_file, output_file=None, config_file=None):
|
||
"""
|
||
直接处理Excel文件的函数(不使用命令行参数)
|
||
|
||
Args:
|
||
input_file: 输入Excel文件路径(字符串或Path对象)
|
||
output_file: 输出CSV文件路径(可选,字符串或Path对象)
|
||
config_file: 配置文件路径(可选,用于读取gases配置)
|
||
|
||
Returns:
|
||
pd.DataFrame: 处理后的DataFrame
|
||
"""
|
||
# 转换为Path对象
|
||
input_path = Path(input_file)
|
||
|
||
# 检查输入文件
|
||
if not input_path.exists():
|
||
raise FileNotFoundError(f"输入文件不存在: {input_path}")
|
||
|
||
if input_path.suffix.lower() not in ['.xlsx', '.xls']:
|
||
raise ValueError(f"输入文件必须是Excel格式 (.xlsx 或 .xls),当前文件: {input_path}")
|
||
|
||
# 处理文件
|
||
df = process_excel_file(str(input_path), config_file)
|
||
|
||
# 如果指定了输出路径,额外保存一份
|
||
if output_file:
|
||
output_path = Path(output_file)
|
||
df.to_csv(output_path, index=False, encoding='utf-8-sig')
|
||
_safe_print(f"额外保存到: {output_path}")
|
||
|
||
return df
|
||
|
||
|
||
def interactive_input():
|
||
"""
|
||
交互式输入模式 - 手动输入参数
|
||
|
||
Returns:
|
||
tuple: (input_file, output_file) 文件路径元组
|
||
"""
|
||
print("=== 手动输入模式 ===")
|
||
print("请按照提示输入参数...\n")
|
||
|
||
# 输入文件路径
|
||
while True:
|
||
input_file = input("请输入Excel文件路径 (例如: data.xlsx): ").strip()
|
||
if not input_file:
|
||
print("文件路径不能为空,请重新输入")
|
||
continue
|
||
|
||
input_path = Path(input_file)
|
||
if not input_path.exists():
|
||
print(f"文件不存在: {input_path}")
|
||
print("提示: 请确保文件路径正确,或者将文件放在当前目录下")
|
||
continue
|
||
|
||
if input_path.suffix.lower() not in ['.xlsx', '.xls']:
|
||
print(f"文件格式错误: {input_path.suffix}")
|
||
print("只支持 .xlsx 和 .xls 格式的Excel文件")
|
||
continue
|
||
|
||
break
|
||
|
||
# 输出文件路径(可选)
|
||
output_file = input("请输入输出CSV文件路径 (可选,直接回车使用默认): ").strip()
|
||
if not output_file:
|
||
output_file = None
|
||
print("使用默认输出文件名")
|
||
|
||
print(f"\n输入确认:")
|
||
print(f" 输入文件: {input_file}")
|
||
print(f" 输出文件: {output_file or '自动生成'}")
|
||
|
||
confirm = input("\n确认开始处理? (y/N): ").strip().lower()
|
||
if confirm not in ['y', 'yes', '是', '确认']:
|
||
print("用户取消操作")
|
||
return None, None
|
||
|
||
return input_file, output_file
|
||
|
||
|
||
def main(input_file=None, output_file=None, interactive=False):
|
||
"""
|
||
主函数 - 支持多种输入方式
|
||
|
||
Args:
|
||
input_file: 输入文件路径(直接调用时使用)
|
||
output_file: 输出文件路径(直接调用时使用)
|
||
interactive: 是否启用交互式输入模式
|
||
"""
|
||
# 如果启用交互式输入
|
||
if interactive:
|
||
input_file, output_file = interactive_input()
|
||
if input_file is None:
|
||
return None
|
||
|
||
# 如果提供了直接参数,使用直接参数
|
||
if input_file is not None:
|
||
try:
|
||
return process_file(input_file, output_file)
|
||
except Exception as e:
|
||
print(f"处理失败: {e}")
|
||
raise
|
||
|
||
# 否则使用命令行参数
|
||
import argparse
|
||
|
||
parser = argparse.ArgumentParser(
|
||
description="无人机数据处理工具 - 将Excel数据转换为GasFlux格式",
|
||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||
epilog="""
|
||
使用方式:
|
||
|
||
1. 命令行模式:
|
||
python data_processor.py data.xlsx
|
||
python data_processor.py data.xlsx -o output.csv
|
||
|
||
2. 交互式模式:
|
||
python data_processor.py --interactive
|
||
|
||
3. 直接调用:
|
||
from data_processor import process_file
|
||
df = process_file('data.xlsx', 'output.csv')
|
||
|
||
4. Python脚本调用:
|
||
from data_processor import main
|
||
df = main(input_file='data.xlsx', output_file='output.csv')
|
||
|
||
处理步骤:
|
||
1. 读取Excel文件
|
||
2. 删除不需要的列
|
||
3. 根据文件名修正时间格式
|
||
4. 经纬度坐标转换 (除以10^7)
|
||
5. 计算气压数据
|
||
6. 高度调整 (减去最小值)
|
||
7. 时间戳融合
|
||
8. 字段重命名为GasFlux格式
|
||
"""
|
||
)
|
||
|
||
parser.add_argument('input_file', nargs='?', help='输入的Excel文件路径')
|
||
parser.add_argument('-o', '--output', help='输出CSV文件路径(可选,默认自动生成)')
|
||
parser.add_argument('-i', '--interactive', action='store_true', help='启用交互式输入模式')
|
||
|
||
args = parser.parse_args()
|
||
|
||
# 如果启用交互式模式
|
||
if args.interactive:
|
||
input_file, output_file = interactive_input()
|
||
if input_file is None:
|
||
return None
|
||
else:
|
||
input_file = args.input_file
|
||
output_file = args.output
|
||
|
||
# 如果没有提供输入文件,显示帮助和使用示例
|
||
if not input_file:
|
||
parser.print_help()
|
||
print("\n" + "="*60)
|
||
print("使用示例:")
|
||
print("="*60)
|
||
print("1. 命令行模式:")
|
||
print(" python data_processor.py your_file.xlsx")
|
||
print(" python data_processor.py data.xlsx -o output.csv")
|
||
print("")
|
||
print("2. Python脚本中直接调用:")
|
||
print(" from data_processor import process_file")
|
||
print(" df = process_file('input.xlsx')")
|
||
print(" df = process_file('input.xlsx', 'output.csv')")
|
||
print("")
|
||
print("3. 交互式模式:")
|
||
print(" python data_processor.py --interactive")
|
||
print("="*60)
|
||
return None
|
||
|
||
# 检查输入文件
|
||
input_path = Path(input_file)
|
||
if not input_path.exists():
|
||
print(f"错误:输入文件不存在: {input_path}")
|
||
sys.exit(1)
|
||
|
||
if input_path.suffix.lower() not in ['.xlsx', '.xls']:
|
||
print(f"错误:输入文件必须是Excel格式 (.xlsx 或 .xls)")
|
||
sys.exit(1)
|
||
|
||
# 处理文件
|
||
try:
|
||
df = process_excel_file(str(input_path))
|
||
|
||
# 如果指定了输出路径,额外保存一份
|
||
if output_file:
|
||
output_path = Path(output_file)
|
||
df.to_csv(output_path, index=False, encoding='utf-8-sig')
|
||
_safe_print(f"额外保存到: {output_path}")
|
||
|
||
return df
|
||
|
||
except KeyboardInterrupt:
|
||
print("\n用户中断处理")
|
||
sys.exit(1)
|
||
except Exception as e:
|
||
print(f"\n处理失败: {e}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
sys.exit(1)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
# 当直接运行脚本时,使用命令行参数模式
|
||
main() |