Files
UAV-CO2/src/gasflux/pre_processing.py
DXC 9bafc9ba0e revert(plotting): 恢复原始 plotting.py 并添加适配层
1. 从初始提交 (f085d7c) 恢复完整的原始 plotting.py
   - 原始配色: geyser 色阶, simple_white 模板
   - 风玫瑰: Beaufort 12级分桶 + turquoise 扇形
   - 时序图: 散点 + 滚动均线 + 多Y轴
   - 背景校正: 双Y轴 + timestamp + 虚线基线
   - 3D散点: px.scatter_3d + 自定义 hover
   - 克里金: 散点叠加等高线 + 地面线

2. 新增适配包装函数(原始签名与当前调用方式不兼容):
   - _scatter_3d_wrapper(df,gas) → scatter_3d(df,color=...)
   - _windrose_wrapper(df) → windrose(df)
   - _time_series_wrapper(df) → time_series(df,ys=['windspeed'])
   - _contour_krig_wrapper(krig_vars) → contour_krig(df,gas,xx,yy,field)
   - _heatmap_krig_wrapper(krig_vars) → heatmap_krig(xx,yy,field)
   - _semivariogram_plot(variogram,gas) → 保留 Plotly 实现
   - _outliers_wrapper(df,col,name) → outliers(series,high,low)

3. 更新调用方使用包装函数:
   - processing_pipelines.py: _scatter_3d_wrapper/_windrose_wrapper/_time_series_wrapper
   - interpolation.py: _contour_krig_wrapper/_heatmap_krig_wrapper/_semivariogram_plot
   - pre_processing.py: _outliers_wrapper
2026-07-10 16:34:49 +08:00

147 lines
5.4 KiB
Python

"""Functions that organise the data into standard columns in pandas dataframes. Conversion functions (e.g. WGS84 to UTM)
are here but transformations take place in processing.py"""
import numpy as np
import pandas as pd
from pyproj import Transformer
from .processing import circ_median
def data_tests(df: pd.DataFrame):
assert df["ch4"].min() > 1.6, "ch4 values are too low"
assert df.index.is_monotonic_increasing, "data is not sorted by time"
assert df.index.is_unique, "data has duplicate timestamps"
assert df["ch4"].isna().sum() == 0, "ch4 has missing values"
assert df["windspeed"].min() >= 0, "windspeed values are negative"
assert df["windspeed"].max() < 20, "windspeed values are too high"
if df["windspeed"].max() > 15:
print("Warning: windspeed is greater than 15 m/s, perhaps due to errors in the data.")
# make timestamp column from UTCs, Month, Day, Year
def timestamp_from_four_columns(df):
df["Year"] = df["Year"] + 2000
df["time"] = pd.to_datetime(df["UTCs"], unit="s")
df["date"] = pd.to_datetime(df[["Year", "Month", "Day"]])
df["timestamp"] = pd.to_datetime(df["date"].dt.date.astype(str) + " " + df["time"].dt.time.astype(str))
df.index = df["timestamp"]
df.drop(
["Year", "Month", "Day", "time", "date", "timestamp", "UTCs"],
axis=1,
inplace=True,
)
return df
# add UTM from latitudes and longitudes
def add_utm(df: pd.DataFrame) -> pd.DataFrame:
"""
Convert WGS84 coordinates to UTM using pyproj.
This function replaces the geopandas implementation with a lighter pyproj-based solution.
"""
# Make a copy to avoid modifying the original DataFrame
df = df.copy()
# Get the average longitude to determine the UTM zone
# For simplicity, we'll use the first valid longitude to determine the zone
# In production, you might want to use the centroid or handle multiple zones
valid_lons = df["longitude"].dropna()
if len(valid_lons) == 0:
raise ValueError("No valid longitude values found")
# Calculate UTM zone from longitude
# UTM zones are 6 degrees wide, starting from -180
lon = valid_lons.iloc[0] # Use first valid longitude
zone_number = int((lon + 180) / 6) + 1
# Determine if it's northern or southern hemisphere
# Use first valid latitude
valid_lats = df["latitude"].dropna()
if len(valid_lats) == 0:
raise ValueError("No valid latitude values found")
lat = valid_lats.iloc[0]
hemisphere = 'north' if lat >= 0 else 'south'
# Create UTM CRS string
utm_crs = f"EPSG:326{zone_number:02d}" if hemisphere == 'north' else f"EPSG:327{zone_number:02d}"
# Create transformer from WGS84 to UTM
transformer = Transformer.from_crs("EPSG:4326", utm_crs, always_xy=True)
# Transform coordinates
utm_easting, utm_northing = transformer.transform(
df["longitude"].values,
df["latitude"].values
)
# Add UTM coordinates to DataFrame
df["utm_easting"] = utm_easting
df["utm_northing"] = utm_northing
return df
# add columns for drone course azimuth and elevation
def add_course(df, rolling_window=1):
df["hor_distance"] = np.sqrt((df["utm_northing"].diff()) ** 2 + (df["utm_easting"].diff()) ** 2)
df["vert_distance"] = df["height_ato"].diff()
df["vert_distance"] = pd.to_numeric(df["vert_distance"], errors="coerce")
df["hor_distance"] = pd.to_numeric(df["hor_distance"], errors="coerce")
df["course_azimuth"] = (
(np.degrees(np.arctan2(df["utm_easting"].diff(), df["utm_northing"].diff())) % 360)
.rolling(rolling_window)
.apply(lambda x: circ_median(x), raw=True)
)
df["course_elevation"] = (
np.degrees(np.arctan2(df["vert_distance"], df["hor_distance"]))
.rolling(rolling_window)
.apply(lambda x: circ_median(x), raw=True)
)
return df
def manual_filtering(dict_dfs: dict, split_times: dict, mask_spans: dict) -> dict:
filtered_dfs = {}
for name, df in dict_dfs.items():
if name in mask_spans:
for i in range(len(mask_spans[name])):
df = df.drop(
df.between_time(mask_spans[name][i].split(" - ")[0], mask_spans[name][i].split(" - ")[1]).index,
).copy()
filtered_dfs[name] = df.copy()
if name in split_times:
split_times[name].append("23:59:59")
split_times[name].insert(0, "00:00:00")
for i in range(len(split_times[name]) - 1):
df2 = df.between_time(split_times[name][i], split_times[name][i + 1]).copy()
filtered_dfs[name + "_" + str(i)] = df2.copy()
elif name not in split_times:
filtered_dfs[name] = df.copy()
return filtered_dfs
def remove_outliers(df: pd.DataFrame, column: str, name: str):
q1 = df[column].quantile(0.25)
q3 = df[column].quantile(0.75)
iqr = q3 - q1
fence_low = q1 - 3 * iqr
fence_high = q3 + 3 * iqr
try:
from . import plotting
fig = plotting._outliers_wrapper(df, column, name)
except Exception:
fig = None
outliers = df.loc[(df[column] < fence_low) | (df[column] > fence_high)]
if len(outliers) > 0:
print(f"{len(outliers)} outliers removed from {name} {column} data")
# nan for outliers, not row removal
df.loc[(df[column] < fence_low) | (df[column] > fence_high), column] = float("nan")
elif len(outliers) == 0:
print(f"No outliers found in {name} {column} data")
return df, fig