py_flux_tracer
1from .campbell.eddy_data_preprocessor import EddyDataPreprocessor 2from .campbell.spectrum_calculator import SpectrumCalculator 3from .commons.hotspot_data import HotspotData, HotspotType 4from .footprint.flux_footprint_analyzer import FluxFootprintAnalyzer 5from .mobile.correcting_utils import CorrectingUtils, CORRECTION_TYPES_PATTERN 6from .mobile.mobile_spatial_analyzer import ( 7 EmissionData, 8 MobileSpatialAnalyzer, 9 MSAInputConfig, 10) 11from .monthly.monthly_converter import MonthlyConverter 12from .monthly.monthly_figures_generator import MonthlyFiguresGenerator 13from .transfer_function.fft_files_reorganizer import FftFileReorganizer 14from .transfer_function.transfer_function_calculator import TransferFunctionCalculator 15 16""" 17versionを動的に設定する。 18`./_version.py`がない場合はsetuptools_scmを用いてGitからバージョン取得を試行 19それも失敗した場合にデフォルトバージョン(0.0.0)を設定 20""" 21try: 22 from ._version import __version__ # type:ignore 23except ImportError: 24 try: 25 from setuptools_scm import get_version 26 27 __version__ = get_version(root="..", relative_to=__file__) 28 except Exception: 29 __version__ = "0.0.0" 30 31__version__ = __version__ 32""" 33@private 34このモジュールはバージョン情報の管理に使用され、ドキュメントには含めません。 35private属性を適用するために再宣言してdocstringを記述しています。 36""" 37 38# モジュールを __all__ にセット 39__all__ = [ 40 "__version__", 41 "EddyDataPreprocessor", 42 "SpectrumCalculator", 43 "HotspotData", 44 "HotspotType", 45 "FluxFootprintAnalyzer", 46 "CorrectingUtils", 47 "CORRECTION_TYPES_PATTERN", 48 "EmissionData", 49 "MobileSpatialAnalyzer", 50 "MSAInputConfig", 51 "MonthlyConverter", 52 "MonthlyFiguresGenerator", 53 "FftFileReorganizer", 54 "TransferFunctionCalculator", 55]
13class EddyDataPreprocessor: 14 def __init__( 15 self, 16 fs: float = 10, 17 logger: Logger | None = None, 18 logging_debug: bool = False, 19 ): 20 """ 21 渦相関法によって記録されたデータファイルを処理するクラス。 22 23 Parameters 24 ---------- 25 fs (float): サンプリング周波数。 26 logger (Logger | None): 使用するロガー。Noneの場合は新しいロガーを作成します。 27 logging_debug (bool): ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。 28 """ 29 self.fs: float = fs 30 31 # ロガー 32 log_level: int = INFO 33 if logging_debug: 34 log_level = DEBUG 35 self.logger: Logger = EddyDataPreprocessor.setup_logger(logger, log_level) 36 37 def add_uvw_columns(self, df: pd.DataFrame) -> pd.DataFrame: 38 """ 39 DataFrameに水平風速u、v、鉛直風速wの列を追加する関数。 40 各成分のキーは`wind_u`、`wind_v`、`wind_w`である。 41 42 Parameters 43 ----- 44 df : pd.DataFrame 45 風速データを含むDataFrame 46 47 Returns 48 ----- 49 pd.DataFrame 50 水平風速u、v、鉛直風速wの列を追加したDataFrame 51 """ 52 required_columns: list[str] = ["Ux", "Uy", "Uz"] 53 # 必要な列がDataFrameに存在するか確認 54 for column in required_columns: 55 if column not in df.columns: 56 raise ValueError(f"必要な列 '{column}' がDataFrameに存在しません。") 57 58 processed_df: pd.DataFrame = df.copy() 59 # pandasの.valuesを使用してnumpy配列を取得し、その型をnp.ndarrayに明示的にキャストする 60 wind_x_array: np.ndarray = np.array(processed_df["Ux"].values) 61 wind_y_array: np.ndarray = np.array(processed_df["Uy"].values) 62 wind_z_array: np.ndarray = np.array(processed_df["Uz"].values) 63 64 # 平均風向を計算 65 wind_direction: float = EddyDataPreprocessor._wind_direction( 66 wind_x_array, wind_y_array 67 ) 68 69 # 水平方向に座標回転を行u, v成分を求める 70 wind_u_array, wind_v_array = EddyDataPreprocessor._horizontal_wind_speed( 71 wind_x_array, wind_y_array, wind_direction 72 ) 73 wind_w_array: np.ndarray = wind_z_array # wはz成分そのまま 74 75 # u, wから風の迎角を計算 76 wind_inclination: float = EddyDataPreprocessor._wind_inclination( 77 wind_u_array, wind_w_array 78 ) 79 80 # 2回座標回転を行い、u, wを求める 81 wind_u_array_rotated, wind_w_array_rotated = ( 82 EddyDataPreprocessor._vertical_rotation( 83 wind_u_array, wind_w_array, wind_inclination 84 ) 85 ) 86 87 processed_df["wind_u"] = wind_u_array_rotated 88 processed_df["wind_v"] = wind_v_array 89 processed_df["wind_w"] = wind_w_array_rotated 90 processed_df["rad_wind_dir"] = wind_direction 91 processed_df["rad_wind_inc"] = wind_inclination 92 processed_df["degree_wind_dir"] = np.degrees(wind_direction) 93 processed_df["degree_wind_inc"] = np.degrees(wind_inclination) 94 95 return processed_df 96 97 def analyze_lag_times( 98 self, 99 input_dir: str, 100 figsize: tuple[float, float] = (10, 8), 101 input_files_pattern: str = r"Eddy_(\d+)", 102 input_files_suffix: str = ".dat", 103 col1: str = "wind_w", 104 col2_list: list[str] = ["Tv"], 105 median_range: float = 20, 106 metadata_rows: int = 4, 107 output_dir: str | None = None, 108 output_tag: str = "", 109 plot_range_tuple: tuple = (-50, 200), 110 print_results: bool = True, 111 skiprows: list[int] = [0, 2, 3], 112 use_resampling: bool = True, 113 ) -> dict[str, float]: 114 """ 115 遅れ時間(ラグ)の統計分析を行い、指定されたディレクトリ内のデータファイルを処理します。 116 解析結果とメタデータはCSVファイルとして出力されます。 117 118 Parameters: 119 ----- 120 input_dir : str 121 入力データファイルが格納されているディレクトリのパス。 122 figsize : tuple[float, float] 123 プロットのサイズ(幅、高さ)。 124 input_files_pattern : str 125 入力ファイル名のパターン(正規表現)。 126 input_files_suffix : str 127 入力ファイルの拡張子。 128 col1 : str 129 基準変数の列名。 130 col2_list : list[str] 131 比較変数の列名のリスト。 132 median_range : float 133 中央値を中心とした範囲。 134 metadata_rows : int 135 メタデータの行数。 136 output_dir : str | None 137 出力ディレクトリのパス。Noneの場合は保存しない。 138 output_tag : str 139 出力ファイルに付与するタグ。デフォルトは空文字で、何も付与されない。 140 plot_range_tuple : tuple 141 ヒストグラムの表示範囲。 142 print_results : bool 143 結果をコンソールに表示するかどうか。 144 skiprows : list[int] 145 スキップする行番号のリスト。 146 use_resampling : bool 147 データをリサンプリングするかどうか。 148 inputするファイルが既にリサンプリング済みの場合はFalseでよい。 149 デフォルトはTrue。 150 151 Returns: 152 ----- 153 dict[str, float] 154 各変数の遅れ時間(平均値を採用)を含む辞書。 155 """ 156 if output_dir is None: 157 self.logger.warn( 158 "output_dirが指定されていません。解析結果を保存する場合は、有効なディレクトリを指定してください。" 159 ) 160 all_lags_indices: list[list[int]] = [] 161 results: dict[str, float] = {} 162 163 # メイン処理 164 # ファイル名に含まれる数字に基づいてソート 165 csv_files = EddyDataPreprocessor._get_sorted_files( 166 input_dir, input_files_pattern, input_files_suffix 167 ) 168 if not csv_files: 169 raise FileNotFoundError( 170 f"There is no '{input_files_suffix}' file to process; input_dir: '{input_dir}', input_files_suffix: '{input_files_suffix}'" 171 ) 172 173 for file in tqdm(csv_files, desc="Calculating"): 174 path: str = os.path.join(input_dir, file) 175 if use_resampling: 176 df, _ = self.get_resampled_df( 177 filepath=path, metadata_rows=metadata_rows, skiprows=skiprows 178 ) 179 else: 180 df = pd.read_csv(path, skiprows=skiprows) 181 df = self.add_uvw_columns(df) 182 lags_list = EddyDataPreprocessor._calculate_lag_time( 183 df, 184 col1, 185 col2_list, 186 ) 187 all_lags_indices.append(lags_list) 188 self.logger.info("すべてのCSVファイルにおける遅れ時間が計算されました。") 189 190 # Convert all_lags_indices to a DataFrame 191 lags_indices_df: pd.DataFrame = pd.DataFrame( 192 all_lags_indices, columns=col2_list 193 ) 194 195 # フォーマット用のキーの最大の長さ 196 max_col_name_length: int = max(len(column) for column in lags_indices_df.columns) 197 198 if print_results: 199 self.logger.info(f"カラム`{col1}`に対する遅れ時間を表示します。") 200 201 # 結果を格納するためのリスト 202 output_data = [] 203 204 for column in lags_indices_df.columns: 205 data: pd.Series = lags_indices_df[column] 206 207 # ヒストグラムの作成 208 plt.figure(figsize=figsize) 209 plt.hist(data, bins=20, range=plot_range_tuple) 210 plt.title(f"Delays of {column}") 211 plt.xlabel("Seconds") 212 plt.ylabel("Frequency") 213 plt.xlim(plot_range_tuple) 214 215 # ファイルとして保存するか 216 if output_dir is not None: 217 os.makedirs(output_dir, exist_ok=True) 218 filename: str = f"lags_histogram-{column}{output_tag}.png" 219 filepath: str = os.path.join(output_dir, filename) 220 plt.savefig(filepath, dpi=300, bbox_inches="tight") 221 plt.close() 222 223 # 中央値を計算し、その周辺のデータのみを使用 224 median_value = np.median(data) 225 filtered_data: pd.Series = data[ 226 (data >= median_value - median_range) 227 & (data <= median_value + median_range) 228 ] 229 230 # 平均値を計算 231 mean_value = np.mean(filtered_data) 232 mean_seconds: float = float(mean_value / self.fs) # 統計値を秒に変換 233 results[column] = mean_seconds 234 235 # 結果とメタデータを出力データに追加 236 output_data.append( 237 { 238 "col1": col1, 239 "col2": column, 240 "col2_lag": round(mean_seconds, 2), # 数値として小数点2桁を保持 241 "lag_unit": "s", 242 "median_range": median_range, 243 } 244 ) 245 246 if print_results: 247 print(f"{column:<{max_col_name_length}} : {mean_seconds:.2f} s") 248 249 # 結果をCSVファイルとして出力 250 if output_dir is not None: 251 output_df: pd.DataFrame = pd.DataFrame(output_data) 252 csv_filepath: str = os.path.join( 253 output_dir, f"lags_results{output_tag}.csv" 254 ) 255 output_df.to_csv(csv_filepath, index=False, encoding="utf-8") 256 self.logger.info(f"解析結果をCSVファイルに保存しました: {csv_filepath}") 257 258 return results 259 260 def get_resampled_df( 261 self, 262 filepath: str, 263 index_column: str = "TIMESTAMP", 264 index_format: str = "%Y-%m-%d %H:%M:%S.%f", 265 interpolate: bool = True, 266 numeric_columns: list[str] = [ 267 "Ux", 268 "Uy", 269 "Uz", 270 "Tv", 271 "diag_sonic", 272 "CO2_new", 273 "H2O", 274 "diag_irga", 275 "cell_tmpr", 276 "cell_press", 277 "Ultra_CH4_ppm", 278 "Ultra_C2H6_ppb", 279 "Ultra_H2O_ppm", 280 "Ultra_CH4_ppm_C", 281 "Ultra_C2H6_ppb_C", 282 ], 283 metadata_rows: int = 4, 284 skiprows: list[int] = [0, 2, 3], 285 is_already_resampled: bool = False, 286 ) -> tuple[pd.DataFrame, list[str]]: 287 """ 288 CSVファイルを読み込み、前処理を行う 289 290 前処理の手順は以下の通りです: 291 1. 不要な行を削除する。デフォルト(`skiprows=[0, 2, 3]`)の場合は、2行目をヘッダーとして残し、1、3、4行目が削除される。 292 2. 数値データを float 型に変換する 293 3. TIMESTAMP列をDateTimeインデックスに設定する 294 4. エラー値をNaNに置き換える 295 5. 指定されたサンプリングレートでリサンプリングする 296 6. 欠損値(NaN)を前後の値から線形補間する 297 7. DateTimeインデックスを削除する 298 299 Parameters: 300 ----- 301 filepath : str 302 読み込むCSVファイルのパス 303 index_column : str, optional 304 インデックスに使用する列名。デフォルトは'TIMESTAMP'。 305 index_format : str, optional 306 インデックスの日付形式。デフォルトは'%Y-%m-%d %H:%M:%S.%f'。 307 interpolate : bool, optional 308 欠損値の補完を適用するフラグ。デフォルトはTrue。 309 numeric_columns : list[str], optional 310 数値型に変換する列名のリスト。 311 デフォルトは["Ux", "Uy", "Uz", "Tv", "diag_sonic", "CO2_new", "H2O", "diag_irga", "cell_tmpr", "cell_press", "Ultra_CH4_ppm", "Ultra_C2H6_ppb", "Ultra_H2O_ppm", "Ultra_CH4_ppm_C", "Ultra_C2H6_ppb_C"]。 312 metadata_rows : int, optional 313 メタデータとして読み込む行数。デフォルトは4。 314 skiprows : list[int], optional 315 スキップする行インデックスのリスト。デフォルトは[0, 2, 3]のため、1, 3, 4行目がスキップされる。 316 is_already_resampled : bool 317 既にリサンプリング&欠損補間されているか。Trueの場合はfloat変換などの処理のみ適用する。 318 319 Returns: 320 ----- 321 tuple[pd.DataFrame, list[str]] 322 前処理済みのデータフレームとメタデータのリスト。 323 """ 324 # メタデータを読み込む 325 metadata: list[str] = [] 326 with open(filepath, "r") as f: 327 for _ in range(metadata_rows): 328 line = f.readline().strip() 329 metadata.append(line.replace('"', "")) 330 331 # CSVファイルを読み込む 332 df: pd.DataFrame = pd.read_csv(filepath, skiprows=skiprows) 333 334 # 数値データをfloat型に変換する 335 for col in numeric_columns: 336 if col in df.columns: 337 df[col] = pd.to_numeric(df[col], errors="coerce") 338 339 if not is_already_resampled: 340 # μ秒がない場合は".0"を追加する 341 df[index_column] = df[index_column].apply( 342 lambda x: f"{x}.0" if "." not in x else x 343 ) 344 # TIMESTAMPをDateTimeインデックスに設定する 345 df[index_column] = pd.to_datetime(df[index_column], format=index_format) 346 df = df.set_index(index_column) 347 348 # リサンプリング前の有効数字を取得 349 decimal_places = {} 350 for col in numeric_columns: 351 if col in df.columns: 352 max_decimals = ( 353 df[col].astype(str).str.extract(r"\.(\d+)")[0].str.len().max() 354 ) 355 decimal_places[col] = ( 356 int(max_decimals) if pd.notna(max_decimals) else 0 357 ) 358 359 # リサンプリングを実行 360 resampling_period: int = int(1000 / self.fs) 361 df_resampled: pd.DataFrame = df.resample(f"{resampling_period}ms").mean( 362 numeric_only=True 363 ) 364 365 if interpolate: 366 # 補間を実行 367 df_resampled = df_resampled.interpolate() 368 # 有効数字を調整 369 for col, decimals in decimal_places.items(): 370 if col in df_resampled.columns: 371 df_resampled[col] = df_resampled[col].round(decimals) 372 373 # DateTimeインデックスを削除する 374 df = df_resampled.reset_index() 375 # ミリ秒を1桁にフォーマット 376 df[index_column] = ( 377 df[index_column].dt.strftime("%Y-%m-%d %H:%M:%S.%f").str[:-5] 378 ) 379 380 return df, metadata 381 382 def output_resampled_data( 383 self, 384 input_dir: str, 385 resampled_dir: str, 386 ratio_dir: str, 387 input_file_pattern: str = r"Eddy_(\d+)", 388 input_files_suffix: str = ".dat", 389 col_ch4_conc: str = "Ultra_CH4_ppm_C", 390 col_c2h6_conc: str = "Ultra_C2H6_ppb", 391 output_ratio: bool = True, 392 output_resampled: bool = True, 393 ratio_csv_prefix: str = "SAC.Ultra", 394 index_column: str = "TIMESTAMP", 395 index_format: str = "%Y-%m-%d %H:%M:%S.%f", 396 interpolate: bool = True, 397 numeric_columns: list[str] = [ 398 "Ux", 399 "Uy", 400 "Uz", 401 "Tv", 402 "diag_sonic", 403 "CO2_new", 404 "H2O", 405 "diag_irga", 406 "cell_tmpr", 407 "cell_press", 408 "Ultra_CH4_ppm", 409 "Ultra_C2H6_ppb", 410 "Ultra_H2O_ppm", 411 "Ultra_CH4_ppm_C", 412 "Ultra_C2H6_ppb_C", 413 ], 414 metadata_rows: int = 4, 415 skiprows: list[int] = [0, 2, 3], 416 ) -> None: 417 """ 418 指定されたディレクトリ内のCSVファイルを処理し、リサンプリングと欠損値補間を行います。 419 420 このメソッドは、指定されたディレクトリ内のCSVファイルを読み込み、リサンプリングを行い、 421 欠損値を補完します。処理結果として、リサンプリングされたCSVファイルを出力し、 422 相関係数やC2H6/CH4比を計算してDataFrameに保存します。 423 リサンプリングと欠損値補完は`get_resampled_df`と同様のロジックを使用します。 424 425 Parameters: 426 ----- 427 input_dir : str 428 入力CSVファイルが格納されているディレクトリのパス。 429 resampled_dir : str 430 リサンプリングされたCSVファイルを出力するディレクトリのパス。 431 ratio_dir : str 432 計算結果を保存するディレクトリのパス。 433 input_file_pattern : str 434 ファイル名からソートキーを抽出する正規表現パターン。デフォルトでは、最初の数字グループでソートします。 435 input_files_suffix : str 436 入力ファイルの拡張子(.datや.csvなど)。デフォルトは".dat"。 437 col_ch4_conc : str 438 CH4濃度を含む列名。デフォルトは'Ultra_CH4_ppm_C'。 439 col_c2h6_conc : str 440 C2H6濃度を含む列名。デフォルトは'Ultra_C2H6_ppb'。 441 output_ratio : bool, optional 442 線形回帰を行うかどうか。デフォルトはTrue。 443 output_resampled : bool, optional 444 リサンプリングされたCSVファイルを出力するかどうか。デフォルトはTrue。 445 ratio_csv_prefix : str 446 出力ファイルの接頭辞。デフォルトは'SAC.Ultra'で、出力時は'SAC.Ultra.2024.09.21.ratio.csv'のような形式となる。 447 index_column : str 448 日時情報を含む列名。デフォルトは'TIMESTAMP'。 449 index_format : str, optional 450 インデックスの日付形式。デフォルトは'%Y-%m-%d %H:%M:%S.%f'。 451 interpolate : bool 452 欠損値補間を行うかどうか。デフォルトはTrue。 453 numeric_columns : list[str] 454 数値データを含む列名のリスト。デフォルトは指定された列名のリスト。 455 metadata_rows : int 456 メタデータとして読み込む行数。デフォルトは4。 457 skiprows : list[int] 458 読み飛ばす行のインデックスリスト。デフォルトは[0, 2, 3]。 459 460 Raises: 461 ----- 462 OSError 463 ディレクトリの作成に失敗した場合。 464 FileNotFoundError 465 入力ファイルが見つからない場合。 466 ValueError 467 出力ディレクトリが指定されていない、またはデータの処理中にエラーが発生した場合。 468 """ 469 # 出力オプションとディレクトリの検証 470 if output_resampled and resampled_dir is None: 471 raise ValueError("output_resampled が True の場合、resampled_dir を指定する必要があります") 472 if output_ratio and ratio_dir is None: 473 raise ValueError("output_ratio が True の場合、ratio_dir を指定する必要があります") 474 475 # ディレクトリの作成(必要な場合のみ) 476 if output_resampled: 477 os.makedirs(resampled_dir, exist_ok=True) 478 if output_ratio: 479 os.makedirs(ratio_dir, exist_ok=True) 480 481 ratio_data: list[dict[str, str | float]] = [] 482 latest_date: datetime = datetime.min 483 484 # csvファイル名のリスト 485 csv_files: list[str] = EddyDataPreprocessor._get_sorted_files( 486 input_dir, input_file_pattern, input_files_suffix 487 ) 488 489 for filename in tqdm(csv_files, desc="Processing files"): 490 input_filepath: str = os.path.join(input_dir, filename) 491 # リサンプリング&欠損値補間 492 df, metadata = self.get_resampled_df( 493 filepath=input_filepath, 494 index_column=index_column, 495 index_format=index_format, 496 interpolate=interpolate, 497 numeric_columns=numeric_columns, 498 metadata_rows=metadata_rows, 499 skiprows=skiprows, 500 ) 501 502 # 開始時間を取得 503 start_time: datetime = pd.to_datetime(df[index_column].iloc[0]) 504 # 処理したファイルの中で最も最新の日付 505 latest_date = max(latest_date, start_time) 506 507 # リサンプリング&欠損値補間したCSVを出力 508 if output_resampled: 509 base_filename: str = re.sub(rf"\{input_files_suffix}$", "", filename) 510 output_csv_path: str = os.path.join( 511 resampled_dir, f"{base_filename}-resampled.csv" 512 ) 513 # メタデータを先に書き込む 514 with open(output_csv_path, "w") as f: 515 for line in metadata: 516 f.write(f"{line}\n") 517 # データフレームを追記モードで書き込む 518 df.to_csv( 519 output_csv_path, index=False, mode="a", quoting=3, header=False 520 ) 521 522 # 相関係数とC2H6/CH4比を計算 523 if output_ratio: 524 ch4_data: pd.Series = df[col_ch4_conc] 525 c2h6_data: pd.Series = df[col_c2h6_conc] 526 527 ratio_row: dict[str, str | float] = { 528 "Date": start_time.strftime("%Y-%m-%d %H:%M:%S.%f"), 529 "slope": f"{np.nan}", 530 "intercept": f"{np.nan}", 531 "r_value": f"{np.nan}", 532 "p_value": f"{np.nan}", 533 "stderr": f"{np.nan}", 534 } 535 # 近似直線の傾き、切片、相関係数を計算 536 try: 537 slope, intercept, r_value, p_value, stderr = stats.linregress( 538 ch4_data, c2h6_data 539 ) 540 ratio_row: dict[str, str | float] = { 541 "Date": start_time.strftime("%Y-%m-%d %H:%M:%S.%f"), 542 "slope": f"{slope:.6f}", 543 "intercept": f"{intercept:.6f}", 544 "r_value": f"{r_value:.6f}", 545 "p_value": f"{p_value:.6f}", 546 "stderr": f"{stderr:.6f}", 547 } 548 except Exception: 549 # 何もせず、デフォルトの ratio_row を使用する 550 pass 551 552 # 結果をリストに追加 553 ratio_data.append(ratio_row) 554 555 if output_ratio: 556 # DataFrameを作成し、Dateカラムで昇順ソート 557 ratio_df: pd.DataFrame = pd.DataFrame(ratio_data) 558 ratio_df["Date"] = pd.to_datetime( 559 ratio_df["Date"] 560 ) # Dateカラムをdatetime型に変換 561 ratio_df = ratio_df.sort_values("Date") # Dateカラムで昇順ソート 562 563 # CSVとして保存 564 ratio_filename: str = ( 565 f"{ratio_csv_prefix}.{latest_date.strftime('%Y.%m.%d')}.ratio.csv" 566 ) 567 ratio_path: str = os.path.join(ratio_dir, ratio_filename) 568 ratio_df.to_csv(ratio_path, index=False) 569 570 def resample_and_analyze_lag_times( 571 self, 572 input_dir: str, 573 input_file_pattern: str = r"Eddy_(\d+)", 574 input_files_suffix: str = ".dat", 575 col_ch4_conc: str = "Ultra_CH4_ppm_C", 576 col_c2h6_conc: str = "Ultra_C2H6_ppb", 577 output_ratio: bool = True, 578 ratio_dir: str | None = None, 579 output_resampled: bool = True, 580 resampled_dir: str | None = None, 581 output_lag_times: bool = True, # lag times解析の有効化フラグ 582 lag_times_dir: str | None = None, # lag timesの結果出力ディレクトリ 583 lag_times_col1: str = "wind_w", # 基準変数 584 lag_times_col2_list: list[str] = ["Tv"], # 比較変数のリスト 585 lag_times_median_range: float = 20, # 中央値を中心とした範囲 586 lag_times_plot_range: tuple[float, float] = ( 587 -50, 588 200, 589 ), # ヒストグラムの表示範囲 590 lag_times_figsize: tuple[float, float] = (10, 8), # プロットサイズ 591 ratio_csv_prefix: str = "SAC.Ultra", 592 index_column: str = "TIMESTAMP", 593 index_format: str = "%Y-%m-%d %H:%M:%S.%f", 594 interpolate: bool = True, 595 numeric_columns: list[str] = [ 596 "Ux", 597 "Uy", 598 "Uz", 599 "Tv", 600 "diag_sonic", 601 "CO2_new", 602 "H2O", 603 "diag_irga", 604 "cell_tmpr", 605 "cell_press", 606 "Ultra_CH4_ppm", 607 "Ultra_C2H6_ppb", 608 "Ultra_H2O_ppm", 609 "Ultra_CH4_ppm_C", 610 "Ultra_C2H6_ppb_C", 611 ], 612 metadata_rows: int = 4, 613 skiprows: list[int] = [0, 2, 3], 614 ) -> None: 615 """ 616 指定されたディレクトリ内のCSVファイルを処理し、リサンプリングと欠損値補間を行います。 617 618 このメソッドは、指定されたディレクトリ内のCSVファイルを読み込み、リサンプリングを行い、 619 欠損値を補完します。処理結果として以下の出力が可能です: 620 1. リサンプリングされたCSVファイル (output_resampled=True) 621 2. 相関係数やC2H6/CH4比を計算したDataFrame (output_ratio=True) 622 3. lag times解析結果 (output_lag_times=True) 623 624 Parameters: 625 ----- 626 input_dir : str 627 入力CSVファイルが格納されているディレクトリのパス。 628 resampled_dir : str | None 629 リサンプリングされたCSVファイルを出力するディレクトリのパス。 630 ratio_dir : str | None 631 C2H6/CH4比の計算結果を保存するディレクトリのパス。 632 input_file_pattern : str 633 ファイル名からソートキーを抽出する正規表現パターン。 634 input_files_suffix : str 635 入力ファイルの拡張子(.datや.csvなど)。デフォルトは".dat"。 636 col_ch4_conc : str 637 CH4濃度を含む列名。デフォルトは'Ultra_CH4_ppm_C'。 638 col_c2h6_conc : str 639 C2H6濃度を含む列名。デフォルトは'Ultra_C2H6_ppb'。 640 output_ratio : bool 641 線形回帰を行うかどうか。デフォルトはTrue。 642 output_resampled : bool 643 リサンプリングされたCSVファイルを出力するかどうか。デフォルトはTrue。 644 output_lag_times : bool 645 lag times解析を行うかどうか。デフォルトはFalse。 646 lag_times_dir : str | None 647 lag times解析結果の出力ディレクトリ。 648 lag_times_col1 : str 649 lag times解析の基準変数。デフォルトは"wind_w"。 650 lag_times_col2_list : list[str] 651 lag times解析の比較変数のリスト。デフォルトは["Tv"]。 652 lag_times_median_range : float 653 lag times解析の中央値を中心とした範囲。デフォルトは20。 654 lag_times_plot_range : tuple[float, float] 655 lag times解析のヒストグラム表示範囲。デフォルトは(-50, 200)。 656 lag_times_figsize : tuple[float, float] 657 lag times解析のプロットサイズ。デフォルトは(10, 8)。 658 ratio_csv_prefix : str 659 出力ファイルの接頭辞。 660 index_column : str 661 日時情報を含む列名。デフォルトは'TIMESTAMP'。 662 index_format : str 663 インデックスの日付形式。デフォルトは'%Y-%m-%d %H:%M:%S.%f'。 664 interpolate : bool 665 欠損値補間を行うかどうか。デフォルトはTrue。 666 numeric_columns : list[str] 667 数値データを含む列名のリスト。 668 metadata_rows : int 669 メタデータとして読み込む行数。デフォルトは4。 670 skiprows : list[int] 671 読み飛ばす行のインデックスリスト。デフォルトは[0, 2, 3]。 672 673 Raises: 674 ----- 675 ValueError 676 出力オプションが指定されているのにディレクトリが指定されていない場合。 677 FileNotFoundError 678 入力ファイルが見つからない場合。 679 OSError 680 ディレクトリの作成に失敗した場合。 681 """ 682 # 出力オプションとディレクトリの検証 683 if output_resampled and resampled_dir is None: 684 raise ValueError( 685 "output_resampled が True の場合、resampled_dir を指定する必要があります" 686 ) 687 if output_ratio and ratio_dir is None: 688 raise ValueError( 689 "output_ratio が True の場合、ratio_dir を指定する必要があります" 690 ) 691 if output_lag_times and lag_times_dir is None: 692 raise ValueError( 693 "output_lag_times が True の場合、lag_times_dir を指定する必要があります" 694 ) 695 696 # ディレクトリの作成(必要な場合のみ) 697 if output_resampled and resampled_dir is not None: 698 os.makedirs(resampled_dir, exist_ok=True) 699 if output_ratio and ratio_dir is not None: 700 os.makedirs(ratio_dir, exist_ok=True) 701 if output_lag_times and lag_times_dir is not None: 702 os.makedirs(lag_times_dir, exist_ok=True) 703 704 ratio_data: list[dict[str, str | float]] = [] 705 all_lags_indices: list[list[int]] = [] 706 latest_date: datetime = datetime.min 707 708 # csvファイル名のリスト 709 csv_files: list[str] = EddyDataPreprocessor._get_sorted_files( 710 input_dir, input_file_pattern, input_files_suffix 711 ) 712 713 if not csv_files: 714 raise FileNotFoundError( 715 f"There is no '{input_files_suffix}' file to process; input_dir: '{input_dir}'" 716 ) 717 718 for filename in tqdm(csv_files, desc="Processing files"): 719 input_filepath: str = os.path.join(input_dir, filename) 720 # リサンプリング&欠損値補間 721 df, metadata = self.get_resampled_df( 722 filepath=input_filepath, 723 index_column=index_column, 724 index_format=index_format, 725 interpolate=interpolate, 726 numeric_columns=numeric_columns, 727 metadata_rows=metadata_rows, 728 skiprows=skiprows, 729 ) 730 731 # 開始時間を取得 732 start_time: datetime = pd.to_datetime(df[index_column].iloc[0]) 733 # 処理したファイルの中で最も最新の日付を更新 734 latest_date = max(latest_date, start_time) 735 736 # リサンプリング&欠損値補間したCSVを出力 737 if output_resampled and resampled_dir is not None: 738 base_filename: str = re.sub(rf"\{input_files_suffix}$", "", filename) 739 output_csv_path: str = os.path.join( 740 resampled_dir, f"{base_filename}-resampled.csv" 741 ) 742 # メタデータを先に書き込む 743 with open(output_csv_path, "w") as f: 744 for line in metadata: 745 f.write(f"{line}\n") 746 # データフレームを追記モードで書き込む 747 df.to_csv( 748 output_csv_path, index=False, mode="a", quoting=3, header=False 749 ) 750 751 # 相関係数とC2H6/CH4比を計算 752 if output_ratio: 753 ch4_data: pd.Series = df[col_ch4_conc] 754 c2h6_data: pd.Series = df[col_c2h6_conc] 755 756 ratio_row: dict[str, str | float] = { 757 "Date": start_time.strftime("%Y-%m-%d %H:%M:%S.%f"), 758 "slope": f"{np.nan}", 759 "intercept": f"{np.nan}", 760 "r_value": f"{np.nan}", 761 "p_value": f"{np.nan}", 762 "stderr": f"{np.nan}", 763 } 764 765 # 近似直線の傾き、切片、相関係数を計算 766 try: 767 slope, intercept, r_value, p_value, stderr = stats.linregress( 768 ch4_data, c2h6_data 769 ) 770 ratio_row = { 771 "Date": start_time.strftime("%Y-%m-%d %H:%M:%S.%f"), 772 "slope": f"{slope:.6f}", 773 "intercept": f"{intercept:.6f}", 774 "r_value": f"{r_value:.6f}", 775 "p_value": f"{p_value:.6f}", 776 "stderr": f"{stderr:.6f}", 777 } 778 except Exception: 779 # 何もせず、デフォルトの ratio_row を使用する 780 pass 781 782 ratio_data.append(ratio_row) 783 784 # Lag times解析用のデータを収集 785 if output_lag_times: 786 df = self.add_uvw_columns(df) 787 lags_list = EddyDataPreprocessor._calculate_lag_time( 788 df, 789 lag_times_col1, 790 lag_times_col2_list, 791 ) 792 all_lags_indices.append(lags_list) 793 794 # Ratio解析結果の保存 795 if output_ratio and ratio_dir is not None: 796 # DataFrameを作成し、Dateカラムで昇順ソート 797 ratio_df: pd.DataFrame = pd.DataFrame(ratio_data) 798 ratio_df["Date"] = pd.to_datetime(ratio_df["Date"]) 799 ratio_df = ratio_df.sort_values("Date") 800 801 # CSVとして保存 802 ratio_filename: str = ( 803 f"{ratio_csv_prefix}.{latest_date.strftime('%Y.%m.%d')}.ratio.csv" 804 ) 805 ratio_path: str = os.path.join(ratio_dir, ratio_filename) 806 ratio_df.to_csv(ratio_path, index=False) 807 self.logger.info(f"Ratio解析結果を保存しました: {ratio_path}") 808 809 # Lag times解析結果の処理と保存 810 if output_lag_times and lag_times_dir is not None: 811 # lag timesの解析結果をDataFrameに変換 812 lags_indices_df = pd.DataFrame( 813 all_lags_indices, columns=lag_times_col2_list 814 ) 815 lag_times_output_data = [] 816 817 # 各変数に対する解析 818 for column in lags_indices_df.columns: 819 data = lags_indices_df[column] 820 821 # ヒストグラムの作成 822 plt.figure(figsize=lag_times_figsize) 823 plt.hist(data, bins=20, range=lag_times_plot_range) 824 plt.title(f"Delays of {column}") 825 plt.xlabel("Seconds") 826 plt.ylabel("Frequency") 827 plt.xlim(lag_times_plot_range) 828 829 # ヒストグラムの保存 830 filename = f"lags_histogram-{column}.png" 831 filepath = os.path.join(lag_times_dir, filename) 832 plt.savefig(filepath, dpi=300, bbox_inches="tight") 833 plt.close() 834 835 # 中央値を計算し、その周辺のデータのみを使用 836 median_value = np.median(data) 837 filtered_data = data[ 838 (data >= median_value - lag_times_median_range) 839 & (data <= median_value + lag_times_median_range) 840 ] 841 842 # 平均値を計算 843 mean_value = np.mean(filtered_data) 844 mean_seconds = float(mean_value / self.fs) 845 846 # 結果を格納 847 lag_times_output_data.append( 848 { 849 "col1": lag_times_col1, 850 "col2": column, 851 "col2_lag": round(mean_seconds, 2), 852 "lag_unit": "s", 853 "median_range": lag_times_median_range, 854 } 855 ) 856 857 # 結果をCSVとして保存 858 if lag_times_output_data: 859 lag_times_df = pd.DataFrame(lag_times_output_data) 860 lag_times_csv_path = os.path.join(lag_times_dir, "lags_results.csv") 861 lag_times_df.to_csv(lag_times_csv_path, index=False, encoding="utf-8") 862 self.logger.info( 863 f"Lag times解析結果を保存しました: {lag_times_csv_path}" 864 ) 865 866 # 遅れ時間を表示 867 self.logger.info(f"カラム`{lag_times_col1}`に対する遅れ時間:") 868 max_col_name_length = max(len(column) for column in lag_times_df["col2"]) 869 for _, row in lag_times_df.iterrows(): 870 print(f"{row['col2']:<{max_col_name_length}} : {row['col2_lag']:.2f} s") 871 872 @staticmethod 873 def _calculate_lag_time( 874 df: pd.DataFrame, 875 col1: str, 876 col2_list: list[str], 877 ) -> list[int]: 878 """ 879 指定された基準変数(col1)と比較変数のリスト(col2_list)の間の遅れ時間(ディレイ)を計算する。 880 周波数が10Hzでcol1がcol2より10.0秒遅れている場合は、+100がインデックスとして取得される 881 882 Parameters: 883 ----- 884 df : pd.DataFrame 885 遅れ時間の計算に使用するデータフレーム 886 col1 : str 887 基準変数の列名 888 col2_list : list[str] 889 比較変数の列名のリスト 890 891 Returns: 892 ----- 893 list[int] 894 各比較変数に対する遅れ時間(ディレイ)のリスト 895 """ 896 lags_list: list[int] = [] 897 for col2 in col2_list: 898 data1: np.ndarray = np.array(df[col1].values) 899 data2: np.ndarray = np.array(df[col2].values) 900 901 # 平均を0に調整 902 data1 = data1 - data1.mean() 903 data2 = data2 - data2.mean() 904 905 data_length: int = len(data1) 906 907 # 相互相関の計算 908 correlation: np.ndarray = np.correlate( 909 data1, data2, mode="full" 910 ) # data2とdata1の順序を入れ替え 911 912 # 相互相関のピークのインデックスを取得 913 lag: int = int((data_length - 1) - correlation.argmax()) # 符号を反転 914 915 lags_list.append(lag) 916 return lags_list 917 918 @staticmethod 919 def _get_sorted_files(directory: str, pattern: str, suffix: str) -> list[str]: 920 """ 921 指定されたディレクトリ内のファイルを、ファイル名に含まれる数字に基づいてソートして返す。 922 923 Parameters: 924 ----- 925 directory : str 926 ファイルが格納されているディレクトリのパス 927 pattern : str 928 ファイル名からソートキーを抽出する正規表現パターン 929 suffix : str 930 ファイルの拡張子 931 932 Returns: 933 ----- 934 list[str] 935 ソートされたファイル名のリスト 936 """ 937 files: list[str] = [f for f in os.listdir(directory) if f.endswith(suffix)] 938 files = [f for f in files if re.search(pattern, f)] 939 files.sort( 940 key=lambda x: int(re.search(pattern, x).group(1)) # type:ignore 941 if re.search(pattern, x) 942 else float("inf") 943 ) 944 return files 945 946 @staticmethod 947 def _horizontal_wind_speed( 948 x_array: np.ndarray, y_array: np.ndarray, wind_dir: float 949 ) -> tuple[np.ndarray, np.ndarray]: 950 """ 951 風速のu成分とv成分を計算する関数 952 953 Parameters: 954 ----- 955 x_array : numpy.ndarray 956 x方向の風速成分の配列 957 y_array : numpy.ndarray 958 y方向の風速成分の配列 959 wind_dir : float 960 水平成分の風向(ラジアン) 961 962 Returns: 963 ----- 964 tuple[numpy.ndarray, numpy.ndarray] 965 u成分とv成分のタプル 966 """ 967 # スカラー風速の計算 968 scalar_hypotenuse: np.ndarray = np.sqrt(x_array**2 + y_array**2) 969 # CSAT3では以下の補正が必要 970 instantaneous_wind_directions = EddyDataPreprocessor._wind_direction( 971 x_array=x_array, y_array=y_array 972 ) 973 # ベクトル風速の計算 974 vector_u: np.ndarray = scalar_hypotenuse * np.cos( 975 instantaneous_wind_directions - wind_dir 976 ) 977 vector_v: np.ndarray = scalar_hypotenuse * np.sin( 978 instantaneous_wind_directions - wind_dir 979 ) 980 return vector_u, vector_v 981 982 @staticmethod 983 def setup_logger(logger: Logger | None, log_level: int = INFO) -> Logger: 984 """ 985 ロガーを設定します。 986 987 このメソッドは、ロギングの設定を行い、ログメッセージのフォーマットを指定します。 988 ログメッセージには、日付、ログレベル、メッセージが含まれます。 989 990 渡されたロガーがNoneまたは不正な場合は、新たにロガーを作成し、標準出力に 991 ログメッセージが表示されるようにStreamHandlerを追加します。ロガーのレベルは 992 引数で指定されたlog_levelに基づいて設定されます。 993 994 Parameters: 995 ----- 996 logger : Logger | None 997 使用するロガー。Noneの場合は新しいロガーを作成します。 998 log_level : int 999 ロガーのログレベル。デフォルトはINFO。 1000 1001 Returns: 1002 ----- 1003 Logger 1004 設定されたロガーオブジェクト。 1005 """ 1006 if logger is not None and isinstance(logger, Logger): 1007 return logger 1008 # 渡されたロガーがNoneまたは正しいものでない場合は独自に設定 1009 new_logger: Logger = getLogger() 1010 # 既存のハンドラーをすべて削除 1011 for handler in new_logger.handlers[:]: 1012 new_logger.removeHandler(handler) 1013 new_logger.setLevel(log_level) # ロガーのレベルを設定 1014 ch = StreamHandler() 1015 ch_formatter = Formatter("%(asctime)s - %(levelname)s - %(message)s") 1016 ch.setFormatter(ch_formatter) # フォーマッターをハンドラーに設定 1017 new_logger.addHandler(ch) # StreamHandlerの追加 1018 return new_logger 1019 1020 @staticmethod 1021 def _vertical_rotation( 1022 u_array: np.ndarray, 1023 w_array: np.ndarray, 1024 wind_inc: float, 1025 ) -> tuple[np.ndarray, np.ndarray]: 1026 """ 1027 鉛直方向の座標回転を行い、u, wを求める関数 1028 1029 Parameters: 1030 ----- 1031 u_array (numpy.ndarray): u方向の風速 1032 w_array (numpy.ndarray): w方向の風速 1033 wind_inc (float): 平均風向に対する迎角(ラジアン) 1034 1035 Returns: 1036 ----- 1037 tuple[numpy.ndarray, numpy.ndarray]: 回転後のu, w 1038 """ 1039 # 迎角を用いて鉛直方向に座標回転 1040 u_rotated = u_array * np.cos(wind_inc) + w_array * np.sin(wind_inc) 1041 w_rotated = w_array * np.cos(wind_inc) - u_array * np.sin(wind_inc) 1042 return u_rotated, w_rotated 1043 1044 @staticmethod 1045 def _wind_direction( 1046 x_array: np.ndarray, y_array: np.ndarray, correction_angle: float = 0.0 1047 ) -> float: 1048 """ 1049 水平方向の平均風向を計算する関数 1050 1051 Parameters: 1052 ----- 1053 x_array (numpy.ndarray): 西方向の風速成分 1054 y_array (numpy.ndarray): 南北方向の風速成分 1055 correction_angle (float): 風向補正角度(ラジアン)。デフォルトは0.0。CSAT3の場合は0.0を指定。 1056 1057 Returns: 1058 ----- 1059 wind_direction (float): 風向 (radians) 1060 """ 1061 wind_direction: float = np.arctan2(np.mean(y_array), np.mean(x_array)) 1062 # 補正角度を適用 1063 wind_direction = correction_angle - wind_direction 1064 return wind_direction 1065 1066 @staticmethod 1067 def _wind_inclination(u_array: np.ndarray, w_array: np.ndarray) -> float: 1068 """ 1069 平均風向に対する迎角を計算する関数 1070 1071 Parameters: 1072 ----- 1073 u_array (numpy.ndarray): u方向の瞬間風速 1074 w_array (numpy.ndarray): w方向の瞬間風速 1075 1076 Returns: 1077 ----- 1078 wind_inc (float): 平均風向に対する迎角(ラジアン) 1079 """ 1080 wind_inc: float = np.arctan2(np.mean(w_array), np.mean(u_array)) 1081 return wind_inc
14 def __init__( 15 self, 16 fs: float = 10, 17 logger: Logger | None = None, 18 logging_debug: bool = False, 19 ): 20 """ 21 渦相関法によって記録されたデータファイルを処理するクラス。 22 23 Parameters 24 ---------- 25 fs (float): サンプリング周波数。 26 logger (Logger | None): 使用するロガー。Noneの場合は新しいロガーを作成します。 27 logging_debug (bool): ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。 28 """ 29 self.fs: float = fs 30 31 # ロガー 32 log_level: int = INFO 33 if logging_debug: 34 log_level = DEBUG 35 self.logger: Logger = EddyDataPreprocessor.setup_logger(logger, log_level)
渦相関法によって記録されたデータファイルを処理するクラス。
Parameters
fs (float): サンプリング周波数。
logger (Logger | None): 使用するロガー。Noneの場合は新しいロガーを作成します。
logging_debug (bool): ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。
37 def add_uvw_columns(self, df: pd.DataFrame) -> pd.DataFrame: 38 """ 39 DataFrameに水平風速u、v、鉛直風速wの列を追加する関数。 40 各成分のキーは`wind_u`、`wind_v`、`wind_w`である。 41 42 Parameters 43 ----- 44 df : pd.DataFrame 45 風速データを含むDataFrame 46 47 Returns 48 ----- 49 pd.DataFrame 50 水平風速u、v、鉛直風速wの列を追加したDataFrame 51 """ 52 required_columns: list[str] = ["Ux", "Uy", "Uz"] 53 # 必要な列がDataFrameに存在するか確認 54 for column in required_columns: 55 if column not in df.columns: 56 raise ValueError(f"必要な列 '{column}' がDataFrameに存在しません。") 57 58 processed_df: pd.DataFrame = df.copy() 59 # pandasの.valuesを使用してnumpy配列を取得し、その型をnp.ndarrayに明示的にキャストする 60 wind_x_array: np.ndarray = np.array(processed_df["Ux"].values) 61 wind_y_array: np.ndarray = np.array(processed_df["Uy"].values) 62 wind_z_array: np.ndarray = np.array(processed_df["Uz"].values) 63 64 # 平均風向を計算 65 wind_direction: float = EddyDataPreprocessor._wind_direction( 66 wind_x_array, wind_y_array 67 ) 68 69 # 水平方向に座標回転を行u, v成分を求める 70 wind_u_array, wind_v_array = EddyDataPreprocessor._horizontal_wind_speed( 71 wind_x_array, wind_y_array, wind_direction 72 ) 73 wind_w_array: np.ndarray = wind_z_array # wはz成分そのまま 74 75 # u, wから風の迎角を計算 76 wind_inclination: float = EddyDataPreprocessor._wind_inclination( 77 wind_u_array, wind_w_array 78 ) 79 80 # 2回座標回転を行い、u, wを求める 81 wind_u_array_rotated, wind_w_array_rotated = ( 82 EddyDataPreprocessor._vertical_rotation( 83 wind_u_array, wind_w_array, wind_inclination 84 ) 85 ) 86 87 processed_df["wind_u"] = wind_u_array_rotated 88 processed_df["wind_v"] = wind_v_array 89 processed_df["wind_w"] = wind_w_array_rotated 90 processed_df["rad_wind_dir"] = wind_direction 91 processed_df["rad_wind_inc"] = wind_inclination 92 processed_df["degree_wind_dir"] = np.degrees(wind_direction) 93 processed_df["degree_wind_inc"] = np.degrees(wind_inclination) 94 95 return processed_df
DataFrameに水平風速u、v、鉛直風速wの列を追加する関数。
各成分のキーはwind_u
、wind_v
、wind_w
である。
Parameters
df : pd.DataFrame
風速データを含むDataFrame
Returns
pd.DataFrame
水平風速u、v、鉛直風速wの列を追加したDataFrame
97 def analyze_lag_times( 98 self, 99 input_dir: str, 100 figsize: tuple[float, float] = (10, 8), 101 input_files_pattern: str = r"Eddy_(\d+)", 102 input_files_suffix: str = ".dat", 103 col1: str = "wind_w", 104 col2_list: list[str] = ["Tv"], 105 median_range: float = 20, 106 metadata_rows: int = 4, 107 output_dir: str | None = None, 108 output_tag: str = "", 109 plot_range_tuple: tuple = (-50, 200), 110 print_results: bool = True, 111 skiprows: list[int] = [0, 2, 3], 112 use_resampling: bool = True, 113 ) -> dict[str, float]: 114 """ 115 遅れ時間(ラグ)の統計分析を行い、指定されたディレクトリ内のデータファイルを処理します。 116 解析結果とメタデータはCSVファイルとして出力されます。 117 118 Parameters: 119 ----- 120 input_dir : str 121 入力データファイルが格納されているディレクトリのパス。 122 figsize : tuple[float, float] 123 プロットのサイズ(幅、高さ)。 124 input_files_pattern : str 125 入力ファイル名のパターン(正規表現)。 126 input_files_suffix : str 127 入力ファイルの拡張子。 128 col1 : str 129 基準変数の列名。 130 col2_list : list[str] 131 比較変数の列名のリスト。 132 median_range : float 133 中央値を中心とした範囲。 134 metadata_rows : int 135 メタデータの行数。 136 output_dir : str | None 137 出力ディレクトリのパス。Noneの場合は保存しない。 138 output_tag : str 139 出力ファイルに付与するタグ。デフォルトは空文字で、何も付与されない。 140 plot_range_tuple : tuple 141 ヒストグラムの表示範囲。 142 print_results : bool 143 結果をコンソールに表示するかどうか。 144 skiprows : list[int] 145 スキップする行番号のリスト。 146 use_resampling : bool 147 データをリサンプリングするかどうか。 148 inputするファイルが既にリサンプリング済みの場合はFalseでよい。 149 デフォルトはTrue。 150 151 Returns: 152 ----- 153 dict[str, float] 154 各変数の遅れ時間(平均値を採用)を含む辞書。 155 """ 156 if output_dir is None: 157 self.logger.warn( 158 "output_dirが指定されていません。解析結果を保存する場合は、有効なディレクトリを指定してください。" 159 ) 160 all_lags_indices: list[list[int]] = [] 161 results: dict[str, float] = {} 162 163 # メイン処理 164 # ファイル名に含まれる数字に基づいてソート 165 csv_files = EddyDataPreprocessor._get_sorted_files( 166 input_dir, input_files_pattern, input_files_suffix 167 ) 168 if not csv_files: 169 raise FileNotFoundError( 170 f"There is no '{input_files_suffix}' file to process; input_dir: '{input_dir}', input_files_suffix: '{input_files_suffix}'" 171 ) 172 173 for file in tqdm(csv_files, desc="Calculating"): 174 path: str = os.path.join(input_dir, file) 175 if use_resampling: 176 df, _ = self.get_resampled_df( 177 filepath=path, metadata_rows=metadata_rows, skiprows=skiprows 178 ) 179 else: 180 df = pd.read_csv(path, skiprows=skiprows) 181 df = self.add_uvw_columns(df) 182 lags_list = EddyDataPreprocessor._calculate_lag_time( 183 df, 184 col1, 185 col2_list, 186 ) 187 all_lags_indices.append(lags_list) 188 self.logger.info("すべてのCSVファイルにおける遅れ時間が計算されました。") 189 190 # Convert all_lags_indices to a DataFrame 191 lags_indices_df: pd.DataFrame = pd.DataFrame( 192 all_lags_indices, columns=col2_list 193 ) 194 195 # フォーマット用のキーの最大の長さ 196 max_col_name_length: int = max(len(column) for column in lags_indices_df.columns) 197 198 if print_results: 199 self.logger.info(f"カラム`{col1}`に対する遅れ時間を表示します。") 200 201 # 結果を格納するためのリスト 202 output_data = [] 203 204 for column in lags_indices_df.columns: 205 data: pd.Series = lags_indices_df[column] 206 207 # ヒストグラムの作成 208 plt.figure(figsize=figsize) 209 plt.hist(data, bins=20, range=plot_range_tuple) 210 plt.title(f"Delays of {column}") 211 plt.xlabel("Seconds") 212 plt.ylabel("Frequency") 213 plt.xlim(plot_range_tuple) 214 215 # ファイルとして保存するか 216 if output_dir is not None: 217 os.makedirs(output_dir, exist_ok=True) 218 filename: str = f"lags_histogram-{column}{output_tag}.png" 219 filepath: str = os.path.join(output_dir, filename) 220 plt.savefig(filepath, dpi=300, bbox_inches="tight") 221 plt.close() 222 223 # 中央値を計算し、その周辺のデータのみを使用 224 median_value = np.median(data) 225 filtered_data: pd.Series = data[ 226 (data >= median_value - median_range) 227 & (data <= median_value + median_range) 228 ] 229 230 # 平均値を計算 231 mean_value = np.mean(filtered_data) 232 mean_seconds: float = float(mean_value / self.fs) # 統計値を秒に変換 233 results[column] = mean_seconds 234 235 # 結果とメタデータを出力データに追加 236 output_data.append( 237 { 238 "col1": col1, 239 "col2": column, 240 "col2_lag": round(mean_seconds, 2), # 数値として小数点2桁を保持 241 "lag_unit": "s", 242 "median_range": median_range, 243 } 244 ) 245 246 if print_results: 247 print(f"{column:<{max_col_name_length}} : {mean_seconds:.2f} s") 248 249 # 結果をCSVファイルとして出力 250 if output_dir is not None: 251 output_df: pd.DataFrame = pd.DataFrame(output_data) 252 csv_filepath: str = os.path.join( 253 output_dir, f"lags_results{output_tag}.csv" 254 ) 255 output_df.to_csv(csv_filepath, index=False, encoding="utf-8") 256 self.logger.info(f"解析結果をCSVファイルに保存しました: {csv_filepath}") 257 258 return results
遅れ時間(ラグ)の統計分析を行い、指定されたディレクトリ内のデータファイルを処理します。 解析結果とメタデータはCSVファイルとして出力されます。
Parameters:
input_dir : str
入力データファイルが格納されているディレクトリのパス。
figsize : tuple[float, float]
プロットのサイズ(幅、高さ)。
input_files_pattern : str
入力ファイル名のパターン(正規表現)。
input_files_suffix : str
入力ファイルの拡張子。
col1 : str
基準変数の列名。
col2_list : list[str]
比較変数の列名のリスト。
median_range : float
中央値を中心とした範囲。
metadata_rows : int
メタデータの行数。
output_dir : str | None
出力ディレクトリのパス。Noneの場合は保存しない。
output_tag : str
出力ファイルに付与するタグ。デフォルトは空文字で、何も付与されない。
plot_range_tuple : tuple
ヒストグラムの表示範囲。
print_results : bool
結果をコンソールに表示するかどうか。
skiprows : list[int]
スキップする行番号のリスト。
use_resampling : bool
データをリサンプリングするかどうか。
inputするファイルが既にリサンプリング済みの場合はFalseでよい。
デフォルトはTrue。
Returns:
dict[str, float]
各変数の遅れ時間(平均値を採用)を含む辞書。
260 def get_resampled_df( 261 self, 262 filepath: str, 263 index_column: str = "TIMESTAMP", 264 index_format: str = "%Y-%m-%d %H:%M:%S.%f", 265 interpolate: bool = True, 266 numeric_columns: list[str] = [ 267 "Ux", 268 "Uy", 269 "Uz", 270 "Tv", 271 "diag_sonic", 272 "CO2_new", 273 "H2O", 274 "diag_irga", 275 "cell_tmpr", 276 "cell_press", 277 "Ultra_CH4_ppm", 278 "Ultra_C2H6_ppb", 279 "Ultra_H2O_ppm", 280 "Ultra_CH4_ppm_C", 281 "Ultra_C2H6_ppb_C", 282 ], 283 metadata_rows: int = 4, 284 skiprows: list[int] = [0, 2, 3], 285 is_already_resampled: bool = False, 286 ) -> tuple[pd.DataFrame, list[str]]: 287 """ 288 CSVファイルを読み込み、前処理を行う 289 290 前処理の手順は以下の通りです: 291 1. 不要な行を削除する。デフォルト(`skiprows=[0, 2, 3]`)の場合は、2行目をヘッダーとして残し、1、3、4行目が削除される。 292 2. 数値データを float 型に変換する 293 3. TIMESTAMP列をDateTimeインデックスに設定する 294 4. エラー値をNaNに置き換える 295 5. 指定されたサンプリングレートでリサンプリングする 296 6. 欠損値(NaN)を前後の値から線形補間する 297 7. DateTimeインデックスを削除する 298 299 Parameters: 300 ----- 301 filepath : str 302 読み込むCSVファイルのパス 303 index_column : str, optional 304 インデックスに使用する列名。デフォルトは'TIMESTAMP'。 305 index_format : str, optional 306 インデックスの日付形式。デフォルトは'%Y-%m-%d %H:%M:%S.%f'。 307 interpolate : bool, optional 308 欠損値の補完を適用するフラグ。デフォルトはTrue。 309 numeric_columns : list[str], optional 310 数値型に変換する列名のリスト。 311 デフォルトは["Ux", "Uy", "Uz", "Tv", "diag_sonic", "CO2_new", "H2O", "diag_irga", "cell_tmpr", "cell_press", "Ultra_CH4_ppm", "Ultra_C2H6_ppb", "Ultra_H2O_ppm", "Ultra_CH4_ppm_C", "Ultra_C2H6_ppb_C"]。 312 metadata_rows : int, optional 313 メタデータとして読み込む行数。デフォルトは4。 314 skiprows : list[int], optional 315 スキップする行インデックスのリスト。デフォルトは[0, 2, 3]のため、1, 3, 4行目がスキップされる。 316 is_already_resampled : bool 317 既にリサンプリング&欠損補間されているか。Trueの場合はfloat変換などの処理のみ適用する。 318 319 Returns: 320 ----- 321 tuple[pd.DataFrame, list[str]] 322 前処理済みのデータフレームとメタデータのリスト。 323 """ 324 # メタデータを読み込む 325 metadata: list[str] = [] 326 with open(filepath, "r") as f: 327 for _ in range(metadata_rows): 328 line = f.readline().strip() 329 metadata.append(line.replace('"', "")) 330 331 # CSVファイルを読み込む 332 df: pd.DataFrame = pd.read_csv(filepath, skiprows=skiprows) 333 334 # 数値データをfloat型に変換する 335 for col in numeric_columns: 336 if col in df.columns: 337 df[col] = pd.to_numeric(df[col], errors="coerce") 338 339 if not is_already_resampled: 340 # μ秒がない場合は".0"を追加する 341 df[index_column] = df[index_column].apply( 342 lambda x: f"{x}.0" if "." not in x else x 343 ) 344 # TIMESTAMPをDateTimeインデックスに設定する 345 df[index_column] = pd.to_datetime(df[index_column], format=index_format) 346 df = df.set_index(index_column) 347 348 # リサンプリング前の有効数字を取得 349 decimal_places = {} 350 for col in numeric_columns: 351 if col in df.columns: 352 max_decimals = ( 353 df[col].astype(str).str.extract(r"\.(\d+)")[0].str.len().max() 354 ) 355 decimal_places[col] = ( 356 int(max_decimals) if pd.notna(max_decimals) else 0 357 ) 358 359 # リサンプリングを実行 360 resampling_period: int = int(1000 / self.fs) 361 df_resampled: pd.DataFrame = df.resample(f"{resampling_period}ms").mean( 362 numeric_only=True 363 ) 364 365 if interpolate: 366 # 補間を実行 367 df_resampled = df_resampled.interpolate() 368 # 有効数字を調整 369 for col, decimals in decimal_places.items(): 370 if col in df_resampled.columns: 371 df_resampled[col] = df_resampled[col].round(decimals) 372 373 # DateTimeインデックスを削除する 374 df = df_resampled.reset_index() 375 # ミリ秒を1桁にフォーマット 376 df[index_column] = ( 377 df[index_column].dt.strftime("%Y-%m-%d %H:%M:%S.%f").str[:-5] 378 ) 379 380 return df, metadata
CSVファイルを読み込み、前処理を行う
前処理の手順は以下の通りです:
- 不要な行を削除する。デフォルト(
skiprows=[0, 2, 3]
)の場合は、2行目をヘッダーとして残し、1、3、4行目が削除される。 - 数値データを float 型に変換する
- TIMESTAMP列をDateTimeインデックスに設定する
- エラー値をNaNに置き換える
- 指定されたサンプリングレートでリサンプリングする
- 欠損値(NaN)を前後の値から線形補間する
- DateTimeインデックスを削除する
Parameters:
filepath : str
読み込むCSVファイルのパス
index_column : str, optional
インデックスに使用する列名。デフォルトは'TIMESTAMP'。
index_format : str, optional
インデックスの日付形式。デフォルトは'%Y-%m-%d %H:%M:%S.%f'。
interpolate : bool, optional
欠損値の補完を適用するフラグ。デフォルトはTrue。
numeric_columns : list[str], optional
数値型に変換する列名のリスト。
デフォルトは["Ux", "Uy", "Uz", "Tv", "diag_sonic", "CO2_new", "H2O", "diag_irga", "cell_tmpr", "cell_press", "Ultra_CH4_ppm", "Ultra_C2H6_ppb", "Ultra_H2O_ppm", "Ultra_CH4_ppm_C", "Ultra_C2H6_ppb_C"]。
metadata_rows : int, optional
メタデータとして読み込む行数。デフォルトは4。
skiprows : list[int], optional
スキップする行インデックスのリスト。デフォルトは[0, 2, 3]のため、1, 3, 4行目がスキップされる。
is_already_resampled : bool
既にリサンプリング&欠損補間されているか。Trueの場合はfloat変換などの処理のみ適用する。
Returns:
tuple[pd.DataFrame, list[str]]
前処理済みのデータフレームとメタデータのリスト。
382 def output_resampled_data( 383 self, 384 input_dir: str, 385 resampled_dir: str, 386 ratio_dir: str, 387 input_file_pattern: str = r"Eddy_(\d+)", 388 input_files_suffix: str = ".dat", 389 col_ch4_conc: str = "Ultra_CH4_ppm_C", 390 col_c2h6_conc: str = "Ultra_C2H6_ppb", 391 output_ratio: bool = True, 392 output_resampled: bool = True, 393 ratio_csv_prefix: str = "SAC.Ultra", 394 index_column: str = "TIMESTAMP", 395 index_format: str = "%Y-%m-%d %H:%M:%S.%f", 396 interpolate: bool = True, 397 numeric_columns: list[str] = [ 398 "Ux", 399 "Uy", 400 "Uz", 401 "Tv", 402 "diag_sonic", 403 "CO2_new", 404 "H2O", 405 "diag_irga", 406 "cell_tmpr", 407 "cell_press", 408 "Ultra_CH4_ppm", 409 "Ultra_C2H6_ppb", 410 "Ultra_H2O_ppm", 411 "Ultra_CH4_ppm_C", 412 "Ultra_C2H6_ppb_C", 413 ], 414 metadata_rows: int = 4, 415 skiprows: list[int] = [0, 2, 3], 416 ) -> None: 417 """ 418 指定されたディレクトリ内のCSVファイルを処理し、リサンプリングと欠損値補間を行います。 419 420 このメソッドは、指定されたディレクトリ内のCSVファイルを読み込み、リサンプリングを行い、 421 欠損値を補完します。処理結果として、リサンプリングされたCSVファイルを出力し、 422 相関係数やC2H6/CH4比を計算してDataFrameに保存します。 423 リサンプリングと欠損値補完は`get_resampled_df`と同様のロジックを使用します。 424 425 Parameters: 426 ----- 427 input_dir : str 428 入力CSVファイルが格納されているディレクトリのパス。 429 resampled_dir : str 430 リサンプリングされたCSVファイルを出力するディレクトリのパス。 431 ratio_dir : str 432 計算結果を保存するディレクトリのパス。 433 input_file_pattern : str 434 ファイル名からソートキーを抽出する正規表現パターン。デフォルトでは、最初の数字グループでソートします。 435 input_files_suffix : str 436 入力ファイルの拡張子(.datや.csvなど)。デフォルトは".dat"。 437 col_ch4_conc : str 438 CH4濃度を含む列名。デフォルトは'Ultra_CH4_ppm_C'。 439 col_c2h6_conc : str 440 C2H6濃度を含む列名。デフォルトは'Ultra_C2H6_ppb'。 441 output_ratio : bool, optional 442 線形回帰を行うかどうか。デフォルトはTrue。 443 output_resampled : bool, optional 444 リサンプリングされたCSVファイルを出力するかどうか。デフォルトはTrue。 445 ratio_csv_prefix : str 446 出力ファイルの接頭辞。デフォルトは'SAC.Ultra'で、出力時は'SAC.Ultra.2024.09.21.ratio.csv'のような形式となる。 447 index_column : str 448 日時情報を含む列名。デフォルトは'TIMESTAMP'。 449 index_format : str, optional 450 インデックスの日付形式。デフォルトは'%Y-%m-%d %H:%M:%S.%f'。 451 interpolate : bool 452 欠損値補間を行うかどうか。デフォルトはTrue。 453 numeric_columns : list[str] 454 数値データを含む列名のリスト。デフォルトは指定された列名のリスト。 455 metadata_rows : int 456 メタデータとして読み込む行数。デフォルトは4。 457 skiprows : list[int] 458 読み飛ばす行のインデックスリスト。デフォルトは[0, 2, 3]。 459 460 Raises: 461 ----- 462 OSError 463 ディレクトリの作成に失敗した場合。 464 FileNotFoundError 465 入力ファイルが見つからない場合。 466 ValueError 467 出力ディレクトリが指定されていない、またはデータの処理中にエラーが発生した場合。 468 """ 469 # 出力オプションとディレクトリの検証 470 if output_resampled and resampled_dir is None: 471 raise ValueError("output_resampled が True の場合、resampled_dir を指定する必要があります") 472 if output_ratio and ratio_dir is None: 473 raise ValueError("output_ratio が True の場合、ratio_dir を指定する必要があります") 474 475 # ディレクトリの作成(必要な場合のみ) 476 if output_resampled: 477 os.makedirs(resampled_dir, exist_ok=True) 478 if output_ratio: 479 os.makedirs(ratio_dir, exist_ok=True) 480 481 ratio_data: list[dict[str, str | float]] = [] 482 latest_date: datetime = datetime.min 483 484 # csvファイル名のリスト 485 csv_files: list[str] = EddyDataPreprocessor._get_sorted_files( 486 input_dir, input_file_pattern, input_files_suffix 487 ) 488 489 for filename in tqdm(csv_files, desc="Processing files"): 490 input_filepath: str = os.path.join(input_dir, filename) 491 # リサンプリング&欠損値補間 492 df, metadata = self.get_resampled_df( 493 filepath=input_filepath, 494 index_column=index_column, 495 index_format=index_format, 496 interpolate=interpolate, 497 numeric_columns=numeric_columns, 498 metadata_rows=metadata_rows, 499 skiprows=skiprows, 500 ) 501 502 # 開始時間を取得 503 start_time: datetime = pd.to_datetime(df[index_column].iloc[0]) 504 # 処理したファイルの中で最も最新の日付 505 latest_date = max(latest_date, start_time) 506 507 # リサンプリング&欠損値補間したCSVを出力 508 if output_resampled: 509 base_filename: str = re.sub(rf"\{input_files_suffix}$", "", filename) 510 output_csv_path: str = os.path.join( 511 resampled_dir, f"{base_filename}-resampled.csv" 512 ) 513 # メタデータを先に書き込む 514 with open(output_csv_path, "w") as f: 515 for line in metadata: 516 f.write(f"{line}\n") 517 # データフレームを追記モードで書き込む 518 df.to_csv( 519 output_csv_path, index=False, mode="a", quoting=3, header=False 520 ) 521 522 # 相関係数とC2H6/CH4比を計算 523 if output_ratio: 524 ch4_data: pd.Series = df[col_ch4_conc] 525 c2h6_data: pd.Series = df[col_c2h6_conc] 526 527 ratio_row: dict[str, str | float] = { 528 "Date": start_time.strftime("%Y-%m-%d %H:%M:%S.%f"), 529 "slope": f"{np.nan}", 530 "intercept": f"{np.nan}", 531 "r_value": f"{np.nan}", 532 "p_value": f"{np.nan}", 533 "stderr": f"{np.nan}", 534 } 535 # 近似直線の傾き、切片、相関係数を計算 536 try: 537 slope, intercept, r_value, p_value, stderr = stats.linregress( 538 ch4_data, c2h6_data 539 ) 540 ratio_row: dict[str, str | float] = { 541 "Date": start_time.strftime("%Y-%m-%d %H:%M:%S.%f"), 542 "slope": f"{slope:.6f}", 543 "intercept": f"{intercept:.6f}", 544 "r_value": f"{r_value:.6f}", 545 "p_value": f"{p_value:.6f}", 546 "stderr": f"{stderr:.6f}", 547 } 548 except Exception: 549 # 何もせず、デフォルトの ratio_row を使用する 550 pass 551 552 # 結果をリストに追加 553 ratio_data.append(ratio_row) 554 555 if output_ratio: 556 # DataFrameを作成し、Dateカラムで昇順ソート 557 ratio_df: pd.DataFrame = pd.DataFrame(ratio_data) 558 ratio_df["Date"] = pd.to_datetime( 559 ratio_df["Date"] 560 ) # Dateカラムをdatetime型に変換 561 ratio_df = ratio_df.sort_values("Date") # Dateカラムで昇順ソート 562 563 # CSVとして保存 564 ratio_filename: str = ( 565 f"{ratio_csv_prefix}.{latest_date.strftime('%Y.%m.%d')}.ratio.csv" 566 ) 567 ratio_path: str = os.path.join(ratio_dir, ratio_filename) 568 ratio_df.to_csv(ratio_path, index=False)
指定されたディレクトリ内のCSVファイルを処理し、リサンプリングと欠損値補間を行います。
このメソッドは、指定されたディレクトリ内のCSVファイルを読み込み、リサンプリングを行い、
欠損値を補完します。処理結果として、リサンプリングされたCSVファイルを出力し、
相関係数やC2H6/CH4比を計算してDataFrameに保存します。
リサンプリングと欠損値補完はget_resampled_df
と同様のロジックを使用します。
Parameters:
input_dir : str
入力CSVファイルが格納されているディレクトリのパス。
resampled_dir : str
リサンプリングされたCSVファイルを出力するディレクトリのパス。
ratio_dir : str
計算結果を保存するディレクトリのパス。
input_file_pattern : str
ファイル名からソートキーを抽出する正規表現パターン。デフォルトでは、最初の数字グループでソートします。
input_files_suffix : str
入力ファイルの拡張子(.datや.csvなど)。デフォルトは".dat"。
col_ch4_conc : str
CH4濃度を含む列名。デフォルトは'Ultra_CH4_ppm_C'。
col_c2h6_conc : str
C2H6濃度を含む列名。デフォルトは'Ultra_C2H6_ppb'。
output_ratio : bool, optional
線形回帰を行うかどうか。デフォルトはTrue。
output_resampled : bool, optional
リサンプリングされたCSVファイルを出力するかどうか。デフォルトはTrue。
ratio_csv_prefix : str
出力ファイルの接頭辞。デフォルトは'SAC.Ultra'で、出力時は'SAC.Ultra.2024.09.21.ratio.csv'のような形式となる。
index_column : str
日時情報を含む列名。デフォルトは'TIMESTAMP'。
index_format : str, optional
インデックスの日付形式。デフォルトは'%Y-%m-%d %H:%M:%S.%f'。
interpolate : bool
欠損値補間を行うかどうか。デフォルトはTrue。
numeric_columns : list[str]
数値データを含む列名のリスト。デフォルトは指定された列名のリスト。
metadata_rows : int
メタデータとして読み込む行数。デフォルトは4。
skiprows : list[int]
読み飛ばす行のインデックスリスト。デフォルトは[0, 2, 3]。
Raises:
OSError
ディレクトリの作成に失敗した場合。
FileNotFoundError
入力ファイルが見つからない場合。
ValueError
出力ディレクトリが指定されていない、またはデータの処理中にエラーが発生した場合。
570 def resample_and_analyze_lag_times( 571 self, 572 input_dir: str, 573 input_file_pattern: str = r"Eddy_(\d+)", 574 input_files_suffix: str = ".dat", 575 col_ch4_conc: str = "Ultra_CH4_ppm_C", 576 col_c2h6_conc: str = "Ultra_C2H6_ppb", 577 output_ratio: bool = True, 578 ratio_dir: str | None = None, 579 output_resampled: bool = True, 580 resampled_dir: str | None = None, 581 output_lag_times: bool = True, # lag times解析の有効化フラグ 582 lag_times_dir: str | None = None, # lag timesの結果出力ディレクトリ 583 lag_times_col1: str = "wind_w", # 基準変数 584 lag_times_col2_list: list[str] = ["Tv"], # 比較変数のリスト 585 lag_times_median_range: float = 20, # 中央値を中心とした範囲 586 lag_times_plot_range: tuple[float, float] = ( 587 -50, 588 200, 589 ), # ヒストグラムの表示範囲 590 lag_times_figsize: tuple[float, float] = (10, 8), # プロットサイズ 591 ratio_csv_prefix: str = "SAC.Ultra", 592 index_column: str = "TIMESTAMP", 593 index_format: str = "%Y-%m-%d %H:%M:%S.%f", 594 interpolate: bool = True, 595 numeric_columns: list[str] = [ 596 "Ux", 597 "Uy", 598 "Uz", 599 "Tv", 600 "diag_sonic", 601 "CO2_new", 602 "H2O", 603 "diag_irga", 604 "cell_tmpr", 605 "cell_press", 606 "Ultra_CH4_ppm", 607 "Ultra_C2H6_ppb", 608 "Ultra_H2O_ppm", 609 "Ultra_CH4_ppm_C", 610 "Ultra_C2H6_ppb_C", 611 ], 612 metadata_rows: int = 4, 613 skiprows: list[int] = [0, 2, 3], 614 ) -> None: 615 """ 616 指定されたディレクトリ内のCSVファイルを処理し、リサンプリングと欠損値補間を行います。 617 618 このメソッドは、指定されたディレクトリ内のCSVファイルを読み込み、リサンプリングを行い、 619 欠損値を補完します。処理結果として以下の出力が可能です: 620 1. リサンプリングされたCSVファイル (output_resampled=True) 621 2. 相関係数やC2H6/CH4比を計算したDataFrame (output_ratio=True) 622 3. lag times解析結果 (output_lag_times=True) 623 624 Parameters: 625 ----- 626 input_dir : str 627 入力CSVファイルが格納されているディレクトリのパス。 628 resampled_dir : str | None 629 リサンプリングされたCSVファイルを出力するディレクトリのパス。 630 ratio_dir : str | None 631 C2H6/CH4比の計算結果を保存するディレクトリのパス。 632 input_file_pattern : str 633 ファイル名からソートキーを抽出する正規表現パターン。 634 input_files_suffix : str 635 入力ファイルの拡張子(.datや.csvなど)。デフォルトは".dat"。 636 col_ch4_conc : str 637 CH4濃度を含む列名。デフォルトは'Ultra_CH4_ppm_C'。 638 col_c2h6_conc : str 639 C2H6濃度を含む列名。デフォルトは'Ultra_C2H6_ppb'。 640 output_ratio : bool 641 線形回帰を行うかどうか。デフォルトはTrue。 642 output_resampled : bool 643 リサンプリングされたCSVファイルを出力するかどうか。デフォルトはTrue。 644 output_lag_times : bool 645 lag times解析を行うかどうか。デフォルトはFalse。 646 lag_times_dir : str | None 647 lag times解析結果の出力ディレクトリ。 648 lag_times_col1 : str 649 lag times解析の基準変数。デフォルトは"wind_w"。 650 lag_times_col2_list : list[str] 651 lag times解析の比較変数のリスト。デフォルトは["Tv"]。 652 lag_times_median_range : float 653 lag times解析の中央値を中心とした範囲。デフォルトは20。 654 lag_times_plot_range : tuple[float, float] 655 lag times解析のヒストグラム表示範囲。デフォルトは(-50, 200)。 656 lag_times_figsize : tuple[float, float] 657 lag times解析のプロットサイズ。デフォルトは(10, 8)。 658 ratio_csv_prefix : str 659 出力ファイルの接頭辞。 660 index_column : str 661 日時情報を含む列名。デフォルトは'TIMESTAMP'。 662 index_format : str 663 インデックスの日付形式。デフォルトは'%Y-%m-%d %H:%M:%S.%f'。 664 interpolate : bool 665 欠損値補間を行うかどうか。デフォルトはTrue。 666 numeric_columns : list[str] 667 数値データを含む列名のリスト。 668 metadata_rows : int 669 メタデータとして読み込む行数。デフォルトは4。 670 skiprows : list[int] 671 読み飛ばす行のインデックスリスト。デフォルトは[0, 2, 3]。 672 673 Raises: 674 ----- 675 ValueError 676 出力オプションが指定されているのにディレクトリが指定されていない場合。 677 FileNotFoundError 678 入力ファイルが見つからない場合。 679 OSError 680 ディレクトリの作成に失敗した場合。 681 """ 682 # 出力オプションとディレクトリの検証 683 if output_resampled and resampled_dir is None: 684 raise ValueError( 685 "output_resampled が True の場合、resampled_dir を指定する必要があります" 686 ) 687 if output_ratio and ratio_dir is None: 688 raise ValueError( 689 "output_ratio が True の場合、ratio_dir を指定する必要があります" 690 ) 691 if output_lag_times and lag_times_dir is None: 692 raise ValueError( 693 "output_lag_times が True の場合、lag_times_dir を指定する必要があります" 694 ) 695 696 # ディレクトリの作成(必要な場合のみ) 697 if output_resampled and resampled_dir is not None: 698 os.makedirs(resampled_dir, exist_ok=True) 699 if output_ratio and ratio_dir is not None: 700 os.makedirs(ratio_dir, exist_ok=True) 701 if output_lag_times and lag_times_dir is not None: 702 os.makedirs(lag_times_dir, exist_ok=True) 703 704 ratio_data: list[dict[str, str | float]] = [] 705 all_lags_indices: list[list[int]] = [] 706 latest_date: datetime = datetime.min 707 708 # csvファイル名のリスト 709 csv_files: list[str] = EddyDataPreprocessor._get_sorted_files( 710 input_dir, input_file_pattern, input_files_suffix 711 ) 712 713 if not csv_files: 714 raise FileNotFoundError( 715 f"There is no '{input_files_suffix}' file to process; input_dir: '{input_dir}'" 716 ) 717 718 for filename in tqdm(csv_files, desc="Processing files"): 719 input_filepath: str = os.path.join(input_dir, filename) 720 # リサンプリング&欠損値補間 721 df, metadata = self.get_resampled_df( 722 filepath=input_filepath, 723 index_column=index_column, 724 index_format=index_format, 725 interpolate=interpolate, 726 numeric_columns=numeric_columns, 727 metadata_rows=metadata_rows, 728 skiprows=skiprows, 729 ) 730 731 # 開始時間を取得 732 start_time: datetime = pd.to_datetime(df[index_column].iloc[0]) 733 # 処理したファイルの中で最も最新の日付を更新 734 latest_date = max(latest_date, start_time) 735 736 # リサンプリング&欠損値補間したCSVを出力 737 if output_resampled and resampled_dir is not None: 738 base_filename: str = re.sub(rf"\{input_files_suffix}$", "", filename) 739 output_csv_path: str = os.path.join( 740 resampled_dir, f"{base_filename}-resampled.csv" 741 ) 742 # メタデータを先に書き込む 743 with open(output_csv_path, "w") as f: 744 for line in metadata: 745 f.write(f"{line}\n") 746 # データフレームを追記モードで書き込む 747 df.to_csv( 748 output_csv_path, index=False, mode="a", quoting=3, header=False 749 ) 750 751 # 相関係数とC2H6/CH4比を計算 752 if output_ratio: 753 ch4_data: pd.Series = df[col_ch4_conc] 754 c2h6_data: pd.Series = df[col_c2h6_conc] 755 756 ratio_row: dict[str, str | float] = { 757 "Date": start_time.strftime("%Y-%m-%d %H:%M:%S.%f"), 758 "slope": f"{np.nan}", 759 "intercept": f"{np.nan}", 760 "r_value": f"{np.nan}", 761 "p_value": f"{np.nan}", 762 "stderr": f"{np.nan}", 763 } 764 765 # 近似直線の傾き、切片、相関係数を計算 766 try: 767 slope, intercept, r_value, p_value, stderr = stats.linregress( 768 ch4_data, c2h6_data 769 ) 770 ratio_row = { 771 "Date": start_time.strftime("%Y-%m-%d %H:%M:%S.%f"), 772 "slope": f"{slope:.6f}", 773 "intercept": f"{intercept:.6f}", 774 "r_value": f"{r_value:.6f}", 775 "p_value": f"{p_value:.6f}", 776 "stderr": f"{stderr:.6f}", 777 } 778 except Exception: 779 # 何もせず、デフォルトの ratio_row を使用する 780 pass 781 782 ratio_data.append(ratio_row) 783 784 # Lag times解析用のデータを収集 785 if output_lag_times: 786 df = self.add_uvw_columns(df) 787 lags_list = EddyDataPreprocessor._calculate_lag_time( 788 df, 789 lag_times_col1, 790 lag_times_col2_list, 791 ) 792 all_lags_indices.append(lags_list) 793 794 # Ratio解析結果の保存 795 if output_ratio and ratio_dir is not None: 796 # DataFrameを作成し、Dateカラムで昇順ソート 797 ratio_df: pd.DataFrame = pd.DataFrame(ratio_data) 798 ratio_df["Date"] = pd.to_datetime(ratio_df["Date"]) 799 ratio_df = ratio_df.sort_values("Date") 800 801 # CSVとして保存 802 ratio_filename: str = ( 803 f"{ratio_csv_prefix}.{latest_date.strftime('%Y.%m.%d')}.ratio.csv" 804 ) 805 ratio_path: str = os.path.join(ratio_dir, ratio_filename) 806 ratio_df.to_csv(ratio_path, index=False) 807 self.logger.info(f"Ratio解析結果を保存しました: {ratio_path}") 808 809 # Lag times解析結果の処理と保存 810 if output_lag_times and lag_times_dir is not None: 811 # lag timesの解析結果をDataFrameに変換 812 lags_indices_df = pd.DataFrame( 813 all_lags_indices, columns=lag_times_col2_list 814 ) 815 lag_times_output_data = [] 816 817 # 各変数に対する解析 818 for column in lags_indices_df.columns: 819 data = lags_indices_df[column] 820 821 # ヒストグラムの作成 822 plt.figure(figsize=lag_times_figsize) 823 plt.hist(data, bins=20, range=lag_times_plot_range) 824 plt.title(f"Delays of {column}") 825 plt.xlabel("Seconds") 826 plt.ylabel("Frequency") 827 plt.xlim(lag_times_plot_range) 828 829 # ヒストグラムの保存 830 filename = f"lags_histogram-{column}.png" 831 filepath = os.path.join(lag_times_dir, filename) 832 plt.savefig(filepath, dpi=300, bbox_inches="tight") 833 plt.close() 834 835 # 中央値を計算し、その周辺のデータのみを使用 836 median_value = np.median(data) 837 filtered_data = data[ 838 (data >= median_value - lag_times_median_range) 839 & (data <= median_value + lag_times_median_range) 840 ] 841 842 # 平均値を計算 843 mean_value = np.mean(filtered_data) 844 mean_seconds = float(mean_value / self.fs) 845 846 # 結果を格納 847 lag_times_output_data.append( 848 { 849 "col1": lag_times_col1, 850 "col2": column, 851 "col2_lag": round(mean_seconds, 2), 852 "lag_unit": "s", 853 "median_range": lag_times_median_range, 854 } 855 ) 856 857 # 結果をCSVとして保存 858 if lag_times_output_data: 859 lag_times_df = pd.DataFrame(lag_times_output_data) 860 lag_times_csv_path = os.path.join(lag_times_dir, "lags_results.csv") 861 lag_times_df.to_csv(lag_times_csv_path, index=False, encoding="utf-8") 862 self.logger.info( 863 f"Lag times解析結果を保存しました: {lag_times_csv_path}" 864 ) 865 866 # 遅れ時間を表示 867 self.logger.info(f"カラム`{lag_times_col1}`に対する遅れ時間:") 868 max_col_name_length = max(len(column) for column in lag_times_df["col2"]) 869 for _, row in lag_times_df.iterrows(): 870 print(f"{row['col2']:<{max_col_name_length}} : {row['col2_lag']:.2f} s")
指定されたディレクトリ内のCSVファイルを処理し、リサンプリングと欠損値補間を行います。
このメソッドは、指定されたディレクトリ内のCSVファイルを読み込み、リサンプリングを行い、 欠損値を補完します。処理結果として以下の出力が可能です:
- リサンプリングされたCSVファイル (output_resampled=True)
- 相関係数やC2H6/CH4比を計算したDataFrame (output_ratio=True)
- lag times解析結果 (output_lag_times=True)
Parameters:
input_dir : str
入力CSVファイルが格納されているディレクトリのパス。
resampled_dir : str | None
リサンプリングされたCSVファイルを出力するディレクトリのパス。
ratio_dir : str | None
C2H6/CH4比の計算結果を保存するディレクトリのパス。
input_file_pattern : str
ファイル名からソートキーを抽出する正規表現パターン。
input_files_suffix : str
入力ファイルの拡張子(.datや.csvなど)。デフォルトは".dat"。
col_ch4_conc : str
CH4濃度を含む列名。デフォルトは'Ultra_CH4_ppm_C'。
col_c2h6_conc : str
C2H6濃度を含む列名。デフォルトは'Ultra_C2H6_ppb'。
output_ratio : bool
線形回帰を行うかどうか。デフォルトはTrue。
output_resampled : bool
リサンプリングされたCSVファイルを出力するかどうか。デフォルトはTrue。
output_lag_times : bool
lag times解析を行うかどうか。デフォルトはFalse。
lag_times_dir : str | None
lag times解析結果の出力ディレクトリ。
lag_times_col1 : str
lag times解析の基準変数。デフォルトは"wind_w"。
lag_times_col2_list : list[str]
lag times解析の比較変数のリスト。デフォルトは["Tv"]。
lag_times_median_range : float
lag times解析の中央値を中心とした範囲。デフォルトは20。
lag_times_plot_range : tuple[float, float]
lag times解析のヒストグラム表示範囲。デフォルトは(-50, 200)。
lag_times_figsize : tuple[float, float]
lag times解析のプロットサイズ。デフォルトは(10, 8)。
ratio_csv_prefix : str
出力ファイルの接頭辞。
index_column : str
日時情報を含む列名。デフォルトは'TIMESTAMP'。
index_format : str
インデックスの日付形式。デフォルトは'%Y-%m-%d %H:%M:%S.%f'。
interpolate : bool
欠損値補間を行うかどうか。デフォルトはTrue。
numeric_columns : list[str]
数値データを含む列名のリスト。
metadata_rows : int
メタデータとして読み込む行数。デフォルトは4。
skiprows : list[int]
読み飛ばす行のインデックスリスト。デフォルトは[0, 2, 3]。
Raises:
ValueError
出力オプションが指定されているのにディレクトリが指定されていない場合。
FileNotFoundError
入力ファイルが見つからない場合。
OSError
ディレクトリの作成に失敗した場合。
982 @staticmethod 983 def setup_logger(logger: Logger | None, log_level: int = INFO) -> Logger: 984 """ 985 ロガーを設定します。 986 987 このメソッドは、ロギングの設定を行い、ログメッセージのフォーマットを指定します。 988 ログメッセージには、日付、ログレベル、メッセージが含まれます。 989 990 渡されたロガーがNoneまたは不正な場合は、新たにロガーを作成し、標準出力に 991 ログメッセージが表示されるようにStreamHandlerを追加します。ロガーのレベルは 992 引数で指定されたlog_levelに基づいて設定されます。 993 994 Parameters: 995 ----- 996 logger : Logger | None 997 使用するロガー。Noneの場合は新しいロガーを作成します。 998 log_level : int 999 ロガーのログレベル。デフォルトはINFO。 1000 1001 Returns: 1002 ----- 1003 Logger 1004 設定されたロガーオブジェクト。 1005 """ 1006 if logger is not None and isinstance(logger, Logger): 1007 return logger 1008 # 渡されたロガーがNoneまたは正しいものでない場合は独自に設定 1009 new_logger: Logger = getLogger() 1010 # 既存のハンドラーをすべて削除 1011 for handler in new_logger.handlers[:]: 1012 new_logger.removeHandler(handler) 1013 new_logger.setLevel(log_level) # ロガーのレベルを設定 1014 ch = StreamHandler() 1015 ch_formatter = Formatter("%(asctime)s - %(levelname)s - %(message)s") 1016 ch.setFormatter(ch_formatter) # フォーマッターをハンドラーに設定 1017 new_logger.addHandler(ch) # StreamHandlerの追加 1018 return new_logger
ロガーを設定します。
このメソッドは、ロギングの設定を行い、ログメッセージのフォーマットを指定します。 ログメッセージには、日付、ログレベル、メッセージが含まれます。
渡されたロガーがNoneまたは不正な場合は、新たにロガーを作成し、標準出力に ログメッセージが表示されるようにStreamHandlerを追加します。ロガーのレベルは 引数で指定されたlog_levelに基づいて設定されます。
Parameters:
logger : Logger | None
使用するロガー。Noneの場合は新しいロガーを作成します。
log_level : int
ロガーのログレベル。デフォルトはINFO。
Returns:
Logger
設定されたロガーオブジェクト。
7class SpectrumCalculator: 8 def __init__( 9 self, 10 df: pd.DataFrame, 11 fs: float, 12 lag_second: float, 13 cols_apply_lag_time: list[str], 14 apply_window: bool = True, 15 plots: int = 30, 16 window_type: str = "hamming", 17 ): 18 """ 19 データロガーから取得したデータファイルを用いて計算を行うクラス。 20 21 Parameters: 22 ------ 23 df : pd.DataFrame 24 pandasのデータフレーム。解析対象のデータを含む。 25 cols_apply_lag_time : list[str] 26 コスペクトルの遅れ時間補正を適用するキーのリスト。 27 fs : float 28 サンプリング周波数(Hz)。データのサンプリングレートを指定。 29 lag_second : float 30 遅延時間(秒)。データの遅延を指定。 31 apply_window : bool, optional 32 窓関数を適用するフラグ。デフォルトはTrue。 33 plots : int 34 プロットする点の数。可視化のためのデータポイント数。 35 """ 36 self._df: pd.DataFrame = df 37 self._fs: float = fs 38 self._cols_apply_lag_time: list[str] = cols_apply_lag_time 39 self._apply_window: bool = apply_window 40 self._lag_second: float = lag_second 41 self._plots: int = plots 42 self._window_type: str = window_type 43 44 def calculate_co_spectrum( 45 self, 46 col1: str, 47 col2: str, 48 dimensionless: bool = True, 49 frequency_weighted: bool = True, 50 interpolate_points: bool = True, 51 scaling: str = "spectrum", 52 ) -> tuple: 53 """ 54 指定されたcol1とcol2のコスペクトルをDataFrameから計算するためのメソッド。 55 56 Parameters: 57 ------ 58 col1 : str 59 データの列名1。 60 col2 : str 61 データの列名2。 62 dimensionless : bool, optional 63 Trueの場合、分散で割って無次元化を行う。デフォルトはTrue。 64 frequency_weighted : bool, optional 65 周波数の重みづけを適用するかどうか。デフォルトはTrue。 66 interpolate_points : bool, optional 67 等間隔なデータ点を生成するかどうか(対数軸上で等間隔)。デフォルトはTrue。 68 scaling : str 69 "density"でスペクトル密度、"spectrum"でスペクトル。デフォルトは"spectrum"。 70 71 Returns: 72 ------ 73 tuple 74 (freqs, co_spectrum, corr_coef) 75 - freqs : np.ndarray 76 周波数軸(対数スケールの場合は対数変換済み)。 77 - co_spectrum : np.ndarray 78 コスペクトル(対数スケールの場合は対数変換済み)。 79 - corr_coef : float 80 変数の相関係数。 81 """ 82 freqs, co_spectrum, _, corr_coef = self.calculate_cross_spectrum( 83 col1=col1, 84 col2=col2, 85 dimensionless=dimensionless, 86 frequency_weighted=frequency_weighted, 87 interpolate_points=interpolate_points, 88 scaling=scaling, 89 ) 90 return freqs, co_spectrum, corr_coef 91 92 def calculate_cross_spectrum( 93 self, 94 col1: str, 95 col2: str, 96 dimensionless: bool = True, 97 frequency_weighted: bool = True, 98 interpolate_points: bool = True, 99 scaling: str = "spectrum", 100 ) -> tuple: 101 """ 102 指定されたcol1とcol2のコスペクトルとクアドラチャスペクトルをDataFrameから計算するためのメソッド。 103 104 Parameters: 105 ------ 106 col1 : str 107 データの列名1。 108 col2 : str 109 データの列名2。 110 dimensionless : bool, optional 111 Trueの場合、分散で割って無次元化を行う。デフォルトはTrue。 112 frequency_weighted : bool, optional 113 周波数の重みづけを適用するかどうか。デフォルトはTrue。 114 interpolate_points : bool, optional 115 等間隔なデータ点を生成するかどうか(対数軸上で等間隔)。デフォルトはTrue。 116 scaling : str 117 "density"でスペクトル密度、"spectrum"でスペクトル。デフォルトは"spectrum"。 118 119 Returns: 120 ------ 121 tuple 122 (freqs, co_spectrum, quadrature_spectrum, corr_coef) 123 - freqs : np.ndarray 124 周波数軸(対数スケールの場合は対数変換済み)。 125 - co_spectrum : np.ndarray 126 コスペクトル(対数スケールの場合は対数変換済み)。 127 - quadrature_spectrum : np.ndarray 128 クアドラチャスペクトル(対数スケールの場合は対数変換済み)。 129 - corr_coef : float 130 変数の相関係数。 131 """ 132 # バリデーション 133 valid_scaling_options = ["density", "spectrum"] 134 if scaling not in valid_scaling_options: 135 raise ValueError( 136 f"'scaling'は次のパラメータから選択してください: {valid_scaling_options}" 137 ) 138 139 fs: float = self._fs 140 df: pd.DataFrame = self._df.copy() 141 # col1とcol2に一致するデータを取得 142 data1: np.ndarray = np.array(df[col1].values) 143 data2: np.ndarray = np.array(df[col2].values) 144 145 # 遅れ時間の補正 146 if col2 in self._cols_apply_lag_time: 147 data1, data2 = SpectrumCalculator._correct_lag_time( 148 data1=data1, data2=data2, fs=fs, lag_second=self._lag_second 149 ) 150 151 # トレンド除去 152 data1 = SpectrumCalculator._detrend(data=data1, fs=fs, first=True) 153 data2 = SpectrumCalculator._detrend(data=data2, fs=fs, first=True) 154 155 # トレンド除去後のデータでパラメータを計算 156 data_length: int = len(data1) # データ長 157 corr_coef: float = np.corrcoef(data1, data2)[0, 1] # 相関係数の計算 158 159 # 窓関数の適用 160 window_scale = 1.0 161 if self._apply_window: 162 window = SpectrumCalculator._generate_window_function( 163 type=self._window_type, data_length=data_length 164 ) 165 data1 *= window 166 data2 *= window 167 window_scale = np.mean(window**2) 168 169 # FFTの計算 170 fft1 = np.fft.rfft(data1) 171 fft2 = np.fft.rfft(data2) 172 173 # 周波数軸の作成 174 freqs: np.ndarray = np.fft.rfftfreq(data_length, 1.0 / self._fs) 175 176 # fft.cと同様のコスペクトル計算ロジック 177 co_spectrum = np.zeros(len(freqs)) 178 quad_spectrum = np.zeros(len(freqs)) 179 180 for i in range(1, len(freqs)): # 0Hz成分を除外 181 z1 = fft1[i] 182 z2 = fft2[i] 183 z1_star = np.conj(z1) 184 z2_star = np.conj(z2) 185 186 # x1 = z1 + z1*, x2 = z2 + z2* 187 x1 = z1 + z1_star 188 x2 = z2 + z2_star 189 x1_re = x1.real 190 x1_im = x1.imag 191 x2_re = x2.real 192 x2_im = x2.imag 193 194 # y1 = z1 - z1*, y2 = z2 - z2* 195 y1 = z1 - z1_star 196 y2 = z2 - z2_star 197 # 虚部と実部を入れ替え 198 y1_re = y1.imag 199 y1_im = -y1.real 200 y2_re = y2.imag 201 y2_im = -y2.real 202 203 # コスペクトルとクァドラチャスペクトルの計算 204 conj_x1_x2 = complex( 205 x1_re * x2_re + x1_im * x2_im, x1_im * x2_re - x1_re * x2_im 206 ) 207 conj_y1_y2 = complex( 208 y1_re * y2_re + y1_im * y2_im, y1_im * y2_re - y1_re * y2_im 209 ) 210 211 # スケーリングパラメータを計算 212 scale_factor = 0.5 / (len(data1) * window_scale) # spectrumの場合 213 # スペクトル密度の場合、周波数間隔で正規化 214 if scaling == "density": 215 df = freqs[1] - freqs[0] # 周波数間隔 216 scale_factor = 0.5 / (len(data1) * window_scale * df) 217 218 # スケーリングを適用 219 co_spectrum[i] = conj_x1_x2.real * scale_factor 220 quad_spectrum[i] = conj_y1_y2.real * scale_factor 221 222 # 周波数の重みづけ 223 if frequency_weighted: 224 co_spectrum[1:] *= freqs[1:] 225 quad_spectrum[1:] *= freqs[1:] 226 227 # 無次元化 228 if dimensionless: 229 cov_matrix: np.ndarray = np.cov(data1, data2) 230 covariance: float = cov_matrix[0, 1] # 共分散 231 co_spectrum /= covariance 232 quad_spectrum /= covariance 233 234 if interpolate_points: 235 # 補間処理(0Hz除外の前に実施) 236 log_freq_min = np.log10(0.001) 237 log_freq_max = np.log10(freqs[-1]) 238 log_freq_resampled = np.logspace(log_freq_min, log_freq_max, self._plots) 239 240 # コスペクトルとクアドラチャスペクトルの補間 241 co_resampled = np.interp( 242 log_freq_resampled, freqs, co_spectrum, left=np.nan, right=np.nan 243 ) 244 quad_resampled = np.interp( 245 log_freq_resampled, freqs, quad_spectrum, left=np.nan, right=np.nan 246 ) 247 248 # NaNを除外 249 valid_mask = ~np.isnan(co_resampled) 250 freqs = log_freq_resampled[valid_mask] 251 co_spectrum = co_resampled[valid_mask] 252 quad_spectrum = quad_resampled[valid_mask] 253 254 # 0Hz成分を除外 255 nonzero_mask = freqs != 0 256 freqs = freqs[nonzero_mask] 257 co_spectrum = co_spectrum[nonzero_mask] 258 quad_spectrum = quad_spectrum[nonzero_mask] 259 260 return freqs, co_spectrum, quad_spectrum, corr_coef 261 262 def calculate_power_spectrum( 263 self, 264 col: str, 265 dimensionless: bool = True, 266 frequency_weighted: bool = True, 267 interpolate_points: bool = True, 268 scaling: str = "spectrum", 269 ) -> tuple: 270 """ 271 指定されたcolに基づいてDataFrameからパワースペクトルと周波数軸を計算します。 272 scipy.signal.welchを使用してパワースペクトルを計算します。 273 274 Parameters: 275 ------ 276 col : str 277 データの列名 278 dimensionless : bool, optional 279 Trueの場合、分散で割って無次元化を行います。デフォルトはTrueです。 280 frequency_weighted : bool, optional 281 周波数の重みづけを適用するかどうか。デフォルトはTrueです。 282 interpolate_points : bool, optional 283 等間隔なデータ点を生成するかどうか(対数軸上で等間隔)。 284 scaling : str, optional 285 "density"でスペクトル密度、"spectrum"でスペクトル。デフォルトは"spectrum"です。 286 287 Returns: 288 ------ 289 tuple 290 - freqs (np.ndarray): 周波数軸(対数スケールの場合は対数変換済み) 291 - power_spectrum (np.ndarray): パワースペクトル(対数スケールの場合は対数変換済み) 292 """ 293 # バリデーション 294 valid_scaling_options = ["density", "spectrum"] 295 if scaling not in valid_scaling_options: 296 raise ValueError( 297 f"'scaling'は次のパラメータから選択してください: {valid_scaling_options}" 298 ) 299 300 # データの取得とトレンド除去 301 data: np.ndarray = np.array(self._df[col].values) 302 data = SpectrumCalculator._detrend(data, self._fs) 303 304 # welchメソッドでパワースペクトル計算 305 freqs, power_spectrum = signal.welch( 306 data, fs=self._fs, window=self._window_type, nperseg=1024, scaling=scaling 307 ) 308 309 # 周波数の重みづけ(0Hz除外の前に実施) 310 if frequency_weighted: 311 power_spectrum = freqs * power_spectrum 312 313 # # 無次元化(0Hz除外の前に実施) 314 if dimensionless: 315 variance = np.var(data) 316 power_spectrum /= variance 317 318 if interpolate_points: 319 # 補間処理(0Hz除外の前に実施) 320 log_freq_min = np.log10(0.001) 321 log_freq_max = np.log10(freqs[-1]) 322 log_freq_resampled = np.logspace(log_freq_min, log_freq_max, self._plots) 323 324 power_spectrum_resampled = np.interp( 325 log_freq_resampled, freqs, power_spectrum, left=np.nan, right=np.nan 326 ) 327 328 # NaNを除外 329 valid_mask = ~np.isnan(power_spectrum_resampled) 330 freqs = log_freq_resampled[valid_mask] 331 power_spectrum = power_spectrum_resampled[valid_mask] 332 333 # 0Hz成分を最後に除外 334 nonzero_mask = freqs != 0 335 freqs = freqs[nonzero_mask] 336 power_spectrum = power_spectrum[nonzero_mask] 337 338 return freqs, power_spectrum 339 340 @staticmethod 341 def _correct_lag_time( 342 data1: np.ndarray, 343 data2: np.ndarray, 344 fs: float, 345 lag_second: float, 346 ) -> tuple: 347 """ 348 相互相関関数を用いて遅れ時間を補正する。クロススペクトルの計算に使用。 349 350 Parameters: 351 ------ 352 data1 : np.ndarray 353 基準データ 354 data2 : np.ndarray 355 遅れているデータ 356 fs : float 357 サンプリング周波数 358 lag_second : float 359 data1からdata2が遅れている時間(秒)。負の値は許可されない。 360 361 Returns: 362 ------ 363 tuple 364 - data1 : np.ndarray 365 補正された基準データ 366 - data2 : np.ndarray 367 補正された遅れているデータ 368 369 Raises: 370 ------ 371 ValueError 372 lag_secondが負の値の場合 373 """ 374 if lag_second < 0: 375 raise ValueError("lag_second must be non-negative.") 376 # lag_secondをサンプリング周波数でスケーリングしてインデックスに変換 377 lag_index: int = int(lag_second * fs) 378 # データ1とデータ2の共通部分を抽出 379 data1 = data1[lag_index:] 380 data2 = data2[:-lag_index] 381 return data1, data2 382 383 @staticmethod 384 def _detrend( 385 data: np.ndarray, fs: float, first: bool = True, second: bool = False 386 ) -> np.ndarray: 387 """ 388 データから一次トレンドおよび二次トレンドを除去します。 389 390 Parameters: 391 ------ 392 data : np.ndarray 393 入力データ 394 fs : float 395 サンプリング周波数 396 first : bool, optional 397 一次トレンドを除去するかどうか. デフォルトはTrue. 398 second : bool, optional 399 二次トレンドを除去するかどうか. デフォルトはFalse. 400 401 Returns: 402 ------ 403 np.ndarray 404 トレンド除去後のデータ 405 406 Raises: 407 ------ 408 ValueError 409 first と second の両方がFalseの場合 410 """ 411 if not (first or second): 412 raise ValueError("少なくとも一次または二次トレンドの除去を指定してください") 413 414 detrended_data: np.ndarray = data.copy() 415 416 # 一次トレンドの除去 417 if first: 418 detrended_data = signal.detrend(detrended_data) 419 420 # 二次トレンドの除去 421 if second: 422 # 二次トレンドを除去するために、まず一次トレンドを除去 423 detrended_data = signal.detrend(detrended_data, type="linear") 424 # 二次トレンドを除去するために、二次多項式フィッティングを行う 425 coeffs_second = np.polyfit( 426 np.arange(len(detrended_data)), detrended_data, 2 427 ) 428 trend_second = np.polyval(coeffs_second, np.arange(len(detrended_data))) 429 detrended_data = detrended_data - trend_second 430 431 return detrended_data 432 433 @staticmethod 434 def _generate_window_function(type: str, data_length: int) -> np.ndarray: 435 """ 436 指定された種類の窓関数を適用する 437 438 Parameters: 439 ------ 440 type : str 441 窓関数の種類 ('hanning', 'hamming', 'blackman') 442 data_length : int 443 データ長 444 445 Returns: 446 ------ 447 np.ndarray 448 適用された窓関数 449 450 Notes: 451 ------ 452 - 指定された種類の窓関数を適用し、numpy配列として返す 453 - 無効な種類が指定された場合、警告を表示しHann窓を適用する 454 """ 455 if type == "hanning": 456 return np.hanning(data_length) 457 elif type == "hamming": 458 return np.hamming(data_length) 459 elif type == "blackman": 460 return np.blackman(data_length) 461 else: 462 print('Warning: Invalid argument "type". Return hanning window.') 463 return np.hanning(data_length) 464 465 @staticmethod 466 def _smooth_spectrum( 467 spectrum: np.ndarray, frequencies: np.ndarray, freq_threshold: float = 0.1 468 ) -> np.ndarray: 469 """ 470 高周波数領域に対して3点移動平均を適用する処理を行う。 471 この処理により、高周波数成分のノイズを低減し、スペクトルの滑らかさを向上させる。 472 473 Parameters: 474 ------ 475 spectrum : np.ndarray 476 スペクトルデータ 477 frequencies : np.ndarray 478 対応する周波数データ 479 freq_threshold : float 480 高周波数の閾値 481 482 Returns: 483 ------ 484 np.ndarray 485 スムーズ化されたスペクトルデータ 486 """ 487 smoothed = spectrum.copy() # オリジナルデータのコピーを作成 488 489 # 周波数閾値以上の部分のインデックスを取得 490 high_freq_mask = frequencies >= freq_threshold 491 492 # 高周波数領域のみを処理 493 high_freq_indices = np.where(high_freq_mask)[0] 494 if len(high_freq_indices) > 2: # 最低3点必要 495 for i in high_freq_indices[1:-1]: # 端点を除く 496 smoothed[i] = ( 497 0.25 * spectrum[i - 1] + 0.5 * spectrum[i] + 0.25 * spectrum[i + 1] 498 ) 499 500 # 高周波領域の端点の処理 501 first_idx = high_freq_indices[0] 502 last_idx = high_freq_indices[-1] 503 smoothed[first_idx] = 0.5 * (spectrum[first_idx] + spectrum[first_idx + 1]) 504 smoothed[last_idx] = 0.5 * (spectrum[last_idx - 1] + spectrum[last_idx]) 505 506 return smoothed
8 def __init__( 9 self, 10 df: pd.DataFrame, 11 fs: float, 12 lag_second: float, 13 cols_apply_lag_time: list[str], 14 apply_window: bool = True, 15 plots: int = 30, 16 window_type: str = "hamming", 17 ): 18 """ 19 データロガーから取得したデータファイルを用いて計算を行うクラス。 20 21 Parameters: 22 ------ 23 df : pd.DataFrame 24 pandasのデータフレーム。解析対象のデータを含む。 25 cols_apply_lag_time : list[str] 26 コスペクトルの遅れ時間補正を適用するキーのリスト。 27 fs : float 28 サンプリング周波数(Hz)。データのサンプリングレートを指定。 29 lag_second : float 30 遅延時間(秒)。データの遅延を指定。 31 apply_window : bool, optional 32 窓関数を適用するフラグ。デフォルトはTrue。 33 plots : int 34 プロットする点の数。可視化のためのデータポイント数。 35 """ 36 self._df: pd.DataFrame = df 37 self._fs: float = fs 38 self._cols_apply_lag_time: list[str] = cols_apply_lag_time 39 self._apply_window: bool = apply_window 40 self._lag_second: float = lag_second 41 self._plots: int = plots 42 self._window_type: str = window_type
データロガーから取得したデータファイルを用いて計算を行うクラス。
Parameters:
df : pd.DataFrame
pandasのデータフレーム。解析対象のデータを含む。
cols_apply_lag_time : list[str]
コスペクトルの遅れ時間補正を適用するキーのリスト。
fs : float
サンプリング周波数(Hz)。データのサンプリングレートを指定。
lag_second : float
遅延時間(秒)。データの遅延を指定。
apply_window : bool, optional
窓関数を適用するフラグ。デフォルトはTrue。
plots : int
プロットする点の数。可視化のためのデータポイント数。
44 def calculate_co_spectrum( 45 self, 46 col1: str, 47 col2: str, 48 dimensionless: bool = True, 49 frequency_weighted: bool = True, 50 interpolate_points: bool = True, 51 scaling: str = "spectrum", 52 ) -> tuple: 53 """ 54 指定されたcol1とcol2のコスペクトルをDataFrameから計算するためのメソッド。 55 56 Parameters: 57 ------ 58 col1 : str 59 データの列名1。 60 col2 : str 61 データの列名2。 62 dimensionless : bool, optional 63 Trueの場合、分散で割って無次元化を行う。デフォルトはTrue。 64 frequency_weighted : bool, optional 65 周波数の重みづけを適用するかどうか。デフォルトはTrue。 66 interpolate_points : bool, optional 67 等間隔なデータ点を生成するかどうか(対数軸上で等間隔)。デフォルトはTrue。 68 scaling : str 69 "density"でスペクトル密度、"spectrum"でスペクトル。デフォルトは"spectrum"。 70 71 Returns: 72 ------ 73 tuple 74 (freqs, co_spectrum, corr_coef) 75 - freqs : np.ndarray 76 周波数軸(対数スケールの場合は対数変換済み)。 77 - co_spectrum : np.ndarray 78 コスペクトル(対数スケールの場合は対数変換済み)。 79 - corr_coef : float 80 変数の相関係数。 81 """ 82 freqs, co_spectrum, _, corr_coef = self.calculate_cross_spectrum( 83 col1=col1, 84 col2=col2, 85 dimensionless=dimensionless, 86 frequency_weighted=frequency_weighted, 87 interpolate_points=interpolate_points, 88 scaling=scaling, 89 ) 90 return freqs, co_spectrum, corr_coef
指定されたcol1とcol2のコスペクトルをDataFrameから計算するためのメソッド。
Parameters:
col1 : str
データの列名1。
col2 : str
データの列名2。
dimensionless : bool, optional
Trueの場合、分散で割って無次元化を行う。デフォルトはTrue。
frequency_weighted : bool, optional
周波数の重みづけを適用するかどうか。デフォルトはTrue。
interpolate_points : bool, optional
等間隔なデータ点を生成するかどうか(対数軸上で等間隔)。デフォルトはTrue。
scaling : str
"density"でスペクトル密度、"spectrum"でスペクトル。デフォルトは"spectrum"。
Returns:
tuple
(freqs, co_spectrum, corr_coef)
- freqs : np.ndarray
周波数軸(対数スケールの場合は対数変換済み)。
- co_spectrum : np.ndarray
コスペクトル(対数スケールの場合は対数変換済み)。
- corr_coef : float
変数の相関係数。
92 def calculate_cross_spectrum( 93 self, 94 col1: str, 95 col2: str, 96 dimensionless: bool = True, 97 frequency_weighted: bool = True, 98 interpolate_points: bool = True, 99 scaling: str = "spectrum", 100 ) -> tuple: 101 """ 102 指定されたcol1とcol2のコスペクトルとクアドラチャスペクトルをDataFrameから計算するためのメソッド。 103 104 Parameters: 105 ------ 106 col1 : str 107 データの列名1。 108 col2 : str 109 データの列名2。 110 dimensionless : bool, optional 111 Trueの場合、分散で割って無次元化を行う。デフォルトはTrue。 112 frequency_weighted : bool, optional 113 周波数の重みづけを適用するかどうか。デフォルトはTrue。 114 interpolate_points : bool, optional 115 等間隔なデータ点を生成するかどうか(対数軸上で等間隔)。デフォルトはTrue。 116 scaling : str 117 "density"でスペクトル密度、"spectrum"でスペクトル。デフォルトは"spectrum"。 118 119 Returns: 120 ------ 121 tuple 122 (freqs, co_spectrum, quadrature_spectrum, corr_coef) 123 - freqs : np.ndarray 124 周波数軸(対数スケールの場合は対数変換済み)。 125 - co_spectrum : np.ndarray 126 コスペクトル(対数スケールの場合は対数変換済み)。 127 - quadrature_spectrum : np.ndarray 128 クアドラチャスペクトル(対数スケールの場合は対数変換済み)。 129 - corr_coef : float 130 変数の相関係数。 131 """ 132 # バリデーション 133 valid_scaling_options = ["density", "spectrum"] 134 if scaling not in valid_scaling_options: 135 raise ValueError( 136 f"'scaling'は次のパラメータから選択してください: {valid_scaling_options}" 137 ) 138 139 fs: float = self._fs 140 df: pd.DataFrame = self._df.copy() 141 # col1とcol2に一致するデータを取得 142 data1: np.ndarray = np.array(df[col1].values) 143 data2: np.ndarray = np.array(df[col2].values) 144 145 # 遅れ時間の補正 146 if col2 in self._cols_apply_lag_time: 147 data1, data2 = SpectrumCalculator._correct_lag_time( 148 data1=data1, data2=data2, fs=fs, lag_second=self._lag_second 149 ) 150 151 # トレンド除去 152 data1 = SpectrumCalculator._detrend(data=data1, fs=fs, first=True) 153 data2 = SpectrumCalculator._detrend(data=data2, fs=fs, first=True) 154 155 # トレンド除去後のデータでパラメータを計算 156 data_length: int = len(data1) # データ長 157 corr_coef: float = np.corrcoef(data1, data2)[0, 1] # 相関係数の計算 158 159 # 窓関数の適用 160 window_scale = 1.0 161 if self._apply_window: 162 window = SpectrumCalculator._generate_window_function( 163 type=self._window_type, data_length=data_length 164 ) 165 data1 *= window 166 data2 *= window 167 window_scale = np.mean(window**2) 168 169 # FFTの計算 170 fft1 = np.fft.rfft(data1) 171 fft2 = np.fft.rfft(data2) 172 173 # 周波数軸の作成 174 freqs: np.ndarray = np.fft.rfftfreq(data_length, 1.0 / self._fs) 175 176 # fft.cと同様のコスペクトル計算ロジック 177 co_spectrum = np.zeros(len(freqs)) 178 quad_spectrum = np.zeros(len(freqs)) 179 180 for i in range(1, len(freqs)): # 0Hz成分を除外 181 z1 = fft1[i] 182 z2 = fft2[i] 183 z1_star = np.conj(z1) 184 z2_star = np.conj(z2) 185 186 # x1 = z1 + z1*, x2 = z2 + z2* 187 x1 = z1 + z1_star 188 x2 = z2 + z2_star 189 x1_re = x1.real 190 x1_im = x1.imag 191 x2_re = x2.real 192 x2_im = x2.imag 193 194 # y1 = z1 - z1*, y2 = z2 - z2* 195 y1 = z1 - z1_star 196 y2 = z2 - z2_star 197 # 虚部と実部を入れ替え 198 y1_re = y1.imag 199 y1_im = -y1.real 200 y2_re = y2.imag 201 y2_im = -y2.real 202 203 # コスペクトルとクァドラチャスペクトルの計算 204 conj_x1_x2 = complex( 205 x1_re * x2_re + x1_im * x2_im, x1_im * x2_re - x1_re * x2_im 206 ) 207 conj_y1_y2 = complex( 208 y1_re * y2_re + y1_im * y2_im, y1_im * y2_re - y1_re * y2_im 209 ) 210 211 # スケーリングパラメータを計算 212 scale_factor = 0.5 / (len(data1) * window_scale) # spectrumの場合 213 # スペクトル密度の場合、周波数間隔で正規化 214 if scaling == "density": 215 df = freqs[1] - freqs[0] # 周波数間隔 216 scale_factor = 0.5 / (len(data1) * window_scale * df) 217 218 # スケーリングを適用 219 co_spectrum[i] = conj_x1_x2.real * scale_factor 220 quad_spectrum[i] = conj_y1_y2.real * scale_factor 221 222 # 周波数の重みづけ 223 if frequency_weighted: 224 co_spectrum[1:] *= freqs[1:] 225 quad_spectrum[1:] *= freqs[1:] 226 227 # 無次元化 228 if dimensionless: 229 cov_matrix: np.ndarray = np.cov(data1, data2) 230 covariance: float = cov_matrix[0, 1] # 共分散 231 co_spectrum /= covariance 232 quad_spectrum /= covariance 233 234 if interpolate_points: 235 # 補間処理(0Hz除外の前に実施) 236 log_freq_min = np.log10(0.001) 237 log_freq_max = np.log10(freqs[-1]) 238 log_freq_resampled = np.logspace(log_freq_min, log_freq_max, self._plots) 239 240 # コスペクトルとクアドラチャスペクトルの補間 241 co_resampled = np.interp( 242 log_freq_resampled, freqs, co_spectrum, left=np.nan, right=np.nan 243 ) 244 quad_resampled = np.interp( 245 log_freq_resampled, freqs, quad_spectrum, left=np.nan, right=np.nan 246 ) 247 248 # NaNを除外 249 valid_mask = ~np.isnan(co_resampled) 250 freqs = log_freq_resampled[valid_mask] 251 co_spectrum = co_resampled[valid_mask] 252 quad_spectrum = quad_resampled[valid_mask] 253 254 # 0Hz成分を除外 255 nonzero_mask = freqs != 0 256 freqs = freqs[nonzero_mask] 257 co_spectrum = co_spectrum[nonzero_mask] 258 quad_spectrum = quad_spectrum[nonzero_mask] 259 260 return freqs, co_spectrum, quad_spectrum, corr_coef
指定されたcol1とcol2のコスペクトルとクアドラチャスペクトルをDataFrameから計算するためのメソッド。
Parameters:
col1 : str
データの列名1。
col2 : str
データの列名2。
dimensionless : bool, optional
Trueの場合、分散で割って無次元化を行う。デフォルトはTrue。
frequency_weighted : bool, optional
周波数の重みづけを適用するかどうか。デフォルトはTrue。
interpolate_points : bool, optional
等間隔なデータ点を生成するかどうか(対数軸上で等間隔)。デフォルトはTrue。
scaling : str
"density"でスペクトル密度、"spectrum"でスペクトル。デフォルトは"spectrum"。
Returns:
tuple
(freqs, co_spectrum, quadrature_spectrum, corr_coef)
- freqs : np.ndarray
周波数軸(対数スケールの場合は対数変換済み)。
- co_spectrum : np.ndarray
コスペクトル(対数スケールの場合は対数変換済み)。
- quadrature_spectrum : np.ndarray
クアドラチャスペクトル(対数スケールの場合は対数変換済み)。
- corr_coef : float
変数の相関係数。
262 def calculate_power_spectrum( 263 self, 264 col: str, 265 dimensionless: bool = True, 266 frequency_weighted: bool = True, 267 interpolate_points: bool = True, 268 scaling: str = "spectrum", 269 ) -> tuple: 270 """ 271 指定されたcolに基づいてDataFrameからパワースペクトルと周波数軸を計算します。 272 scipy.signal.welchを使用してパワースペクトルを計算します。 273 274 Parameters: 275 ------ 276 col : str 277 データの列名 278 dimensionless : bool, optional 279 Trueの場合、分散で割って無次元化を行います。デフォルトはTrueです。 280 frequency_weighted : bool, optional 281 周波数の重みづけを適用するかどうか。デフォルトはTrueです。 282 interpolate_points : bool, optional 283 等間隔なデータ点を生成するかどうか(対数軸上で等間隔)。 284 scaling : str, optional 285 "density"でスペクトル密度、"spectrum"でスペクトル。デフォルトは"spectrum"です。 286 287 Returns: 288 ------ 289 tuple 290 - freqs (np.ndarray): 周波数軸(対数スケールの場合は対数変換済み) 291 - power_spectrum (np.ndarray): パワースペクトル(対数スケールの場合は対数変換済み) 292 """ 293 # バリデーション 294 valid_scaling_options = ["density", "spectrum"] 295 if scaling not in valid_scaling_options: 296 raise ValueError( 297 f"'scaling'は次のパラメータから選択してください: {valid_scaling_options}" 298 ) 299 300 # データの取得とトレンド除去 301 data: np.ndarray = np.array(self._df[col].values) 302 data = SpectrumCalculator._detrend(data, self._fs) 303 304 # welchメソッドでパワースペクトル計算 305 freqs, power_spectrum = signal.welch( 306 data, fs=self._fs, window=self._window_type, nperseg=1024, scaling=scaling 307 ) 308 309 # 周波数の重みづけ(0Hz除外の前に実施) 310 if frequency_weighted: 311 power_spectrum = freqs * power_spectrum 312 313 # # 無次元化(0Hz除外の前に実施) 314 if dimensionless: 315 variance = np.var(data) 316 power_spectrum /= variance 317 318 if interpolate_points: 319 # 補間処理(0Hz除外の前に実施) 320 log_freq_min = np.log10(0.001) 321 log_freq_max = np.log10(freqs[-1]) 322 log_freq_resampled = np.logspace(log_freq_min, log_freq_max, self._plots) 323 324 power_spectrum_resampled = np.interp( 325 log_freq_resampled, freqs, power_spectrum, left=np.nan, right=np.nan 326 ) 327 328 # NaNを除外 329 valid_mask = ~np.isnan(power_spectrum_resampled) 330 freqs = log_freq_resampled[valid_mask] 331 power_spectrum = power_spectrum_resampled[valid_mask] 332 333 # 0Hz成分を最後に除外 334 nonzero_mask = freqs != 0 335 freqs = freqs[nonzero_mask] 336 power_spectrum = power_spectrum[nonzero_mask] 337 338 return freqs, power_spectrum
指定されたcolに基づいてDataFrameからパワースペクトルと周波数軸を計算します。 scipy.signal.welchを使用してパワースペクトルを計算します。
Parameters:
col : str
データの列名
dimensionless : bool, optional
Trueの場合、分散で割って無次元化を行います。デフォルトはTrueです。
frequency_weighted : bool, optional
周波数の重みづけを適用するかどうか。デフォルトはTrueです。
interpolate_points : bool, optional
等間隔なデータ点を生成するかどうか(対数軸上で等間隔)。
scaling : str, optional
"density"でスペクトル密度、"spectrum"でスペクトル。デフォルトは"spectrum"です。
Returns:
tuple
- freqs (np.ndarray): 周波数軸(対数スケールの場合は対数変換済み)
- power_spectrum (np.ndarray): パワースペクトル(対数スケールの場合は対数変換済み)
9@dataclass 10class HotspotData: 11 """ 12 ホットスポットの情報を保持するデータクラス 13 14 Parameters: 15 ------ 16 source : str 17 データソース 18 angle : float 19 中心からの角度 20 avg_lat : float 21 平均緯度 22 avg_lon : float 23 平均経度 24 delta_ch4 : float 25 CH4の増加量 26 delta_c2h6 : float 27 C2H6の増加量 28 correlation : float 29 ΔC2H6/ΔCH4相関係数 30 ratio : float 31 ΔC2H6/ΔCH4の比率 32 section : int 33 所属する区画番号 34 type : HotspotType 35 ホットスポットの種類 36 """ 37 38 source: str 39 angle: float 40 avg_lat: float 41 avg_lon: float 42 delta_ch4: float 43 delta_c2h6: float 44 correlation: float 45 ratio: float 46 section: int 47 type: HotspotType 48 49 def __post_init__(self): 50 """ 51 __post_init__で各プロパティをバリデーション 52 """ 53 # データソースが空でないことを確認 54 if not self.source.strip(): 55 raise ValueError(f"'source' must not be empty: {self.source}") 56 57 # 角度は-180~180度の範囲内であることを確認 58 if not -180 <= self.angle <= 180: 59 raise ValueError( 60 f"'angle' must be between -180 and 180 degrees: {self.angle}" 61 ) 62 63 # 緯度は-90から90度の範囲内であることを確認 64 if not -90 <= self.avg_lat <= 90: 65 raise ValueError( 66 f"'avg_lat' must be between -90 and 90 degrees: {self.avg_lat}" 67 ) 68 69 # 経度は-180から180度の範囲内であることを確認 70 if not -180 <= self.avg_lon <= 180: 71 raise ValueError( 72 f"'avg_lon' must be between -180 and 180 degrees: {self.avg_lon}" 73 ) 74 75 # ΔCH4はfloat型であり、0以上を許可 76 if not isinstance(self.delta_c2h6, float) or self.delta_ch4 < 0: 77 raise ValueError( 78 f"'delta_ch4' must be a non-negative value and at least 0: {self.delta_ch4}" 79 ) 80 81 # ΔC2H6はfloat型のみを許可 82 if not isinstance(self.delta_c2h6, float): 83 raise ValueError(f"'delta_c2h6' must be a float value: {self.delta_c2h6}") 84 85 # 相関係数は-1から1の範囲内であることを確認 86 if not -1 <= self.correlation <= 1 and str(self.correlation) != "nan": 87 raise ValueError( 88 f"'correlation' must be between -1 and 1: {self.correlation}" 89 ) 90 91 # 比率は0または正の値であることを確認 92 if self.ratio < 0: 93 raise ValueError(f"'ratio' must be 0 or a positive value: {self.ratio}") 94 95 # セクション番号は0または正の整数であることを確認 96 if not isinstance(self.section, int) or self.section < 0: 97 raise ValueError( 98 f"'section' must be a non-negative integer: {self.section}" 99 )
ホットスポットの情報を保持するデータクラス
Parameters:
source : str
データソース
angle : float
中心からの角度
avg_lat : float
平均緯度
avg_lon : float
平均経度
delta_ch4 : float
CH4の増加量
delta_c2h6 : float
C2H6の増加量
correlation : float
ΔC2H6/ΔCH4相関係数
ratio : float
ΔC2H6/ΔCH4の比率
section : int
所属する区画番号
type : HotspotType
ホットスポットの種類
18class FluxFootprintAnalyzer: 19 """ 20 フラックスフットプリントを解析および可視化するクラス。 21 22 このクラスは、フラックスデータの処理、フットプリントの計算、 23 および結果を衛星画像上に可視化するメソッドを提供します。 24 座標系と単位に関する重要な注意: 25 - すべての距離はメートル単位で計算されます 26 - 座標系の原点(0,0)は測定タワーの位置に対応します 27 - x軸は東西方向(正が東) 28 - y軸は南北方向(正が北) 29 - 風向は気象学的風向(北から時計回りに測定)を使用 30 31 この実装は、Kormann and Meixner (2001) および Takano et al. (2021)に基づいています。 32 """ 33 34 EARTH_RADIUS_METER: int = 6371000 # 地球の半径(メートル) 35 36 def __init__( 37 self, 38 z_m: float, 39 labelsize: float = 20, 40 ticksize: float = 16, 41 plot_params=None, 42 logger: Logger | None = None, 43 logging_debug: bool = False, 44 ): 45 """ 46 衛星画像を用いて FluxFootprintAnalyzer を初期化します。 47 48 Parameters: 49 ------ 50 z_m : float 51 測定の高さ(メートル単位)。 52 labelsize : float 53 軸ラベルのフォントサイズ。デフォルトは20。 54 ticksize : float 55 軸目盛りのフォントサイズ。デフォルトは16。 56 plot_params : Optional[Dict[str, any]] 57 matplotlibのプロットパラメータを指定する辞書。 58 logger : Logger | None 59 使用するロガー。Noneの場合は新しいロガーを生成します。 60 logging_debug : bool 61 ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。 62 """ 63 # 定数や共通の変数 64 self._required_columns: list[str] = [ 65 "Date", 66 "WS vector", 67 "u*", 68 "z/L", 69 "Wind direction", 70 "sigmaV", 71 ] # 必要なカラムの名前 72 self._col_weekday: str = "ffa_is_weekday" # クラスで生成するカラムのキー名 73 self._z_m: float = z_m # 測定高度 74 # 状態を管理するフラグ 75 self._got_satellite_image: bool = False 76 77 # 図表の初期設定 78 FluxFootprintAnalyzer.setup_plot_params(labelsize, ticksize, plot_params) 79 # ロガー 80 log_level: int = INFO 81 if logging_debug: 82 log_level = DEBUG 83 self.logger: Logger = FluxFootprintAnalyzer.setup_logger(logger, log_level) 84 85 @staticmethod 86 def setup_logger(logger: Logger | None, log_level: int = INFO) -> Logger: 87 """ 88 ロガーを設定します。 89 90 このメソッドは、ロギングの設定を行い、ログメッセージのフォーマットを指定します。 91 ログメッセージには、日付、ログレベル、メッセージが含まれます。 92 93 渡されたロガーがNoneまたは不正な場合は、新たにロガーを作成し、標準出力に 94 ログメッセージが表示されるようにStreamHandlerを追加します。ロガーのレベルは 95 引数で指定されたlog_levelに基づいて設定されます。 96 97 Parameters: 98 ------ 99 logger : Logger | None 100 使用するロガー。Noneの場合は新しいロガーを作成します。 101 log_level : int 102 ロガーのログレベル。デフォルトはINFO。 103 104 Returns: 105 ------ 106 Logger 107 設定されたロガーオブジェクト。 108 """ 109 if logger is not None and isinstance(logger, Logger): 110 return logger 111 # 渡されたロガーがNoneまたは正しいものでない場合は独自に設定 112 new_logger: Logger = getLogger() 113 # 既存のハンドラーをすべて削除 114 for handler in new_logger.handlers[:]: 115 new_logger.removeHandler(handler) 116 new_logger.setLevel(log_level) # ロガーのレベルを設定 117 ch = StreamHandler() 118 ch_formatter = Formatter("%(asctime)s - %(levelname)s - %(message)s") 119 ch.setFormatter(ch_formatter) # フォーマッターをハンドラーに設定 120 new_logger.addHandler(ch) # StreamHandlerの追加 121 return new_logger 122 123 @staticmethod 124 def setup_plot_params(labelsize: float, ticksize: float, plot_params=None) -> None: 125 """ 126 matplotlibのプロットパラメータを設定します。 127 128 Parameters: 129 ------ 130 labelsize : float 131 軸ラベルのフォントサイズ。 132 ticksize : float 133 軸目盛りのフォントサイズ。 134 plot_params : Optional[Dict[str, any]] 135 matplotlibのプロットパラメータの辞書。 136 137 Returns: 138 ------ 139 None 140 このメソッドは戻り値を持ちませんが、プロットパラメータを更新します。 141 """ 142 # デフォルトのプロットパラメータ 143 default_params = { 144 "font.family": ["Arial", "Dejavu Sans"], 145 "axes.edgecolor": "None", 146 "axes.labelcolor": "black", 147 "text.color": "black", 148 "xtick.color": "black", 149 "ytick.color": "black", 150 "grid.color": "gray", 151 "axes.grid": False, 152 "xtick.major.size": 0, 153 "ytick.major.size": 0, 154 "ytick.direction": "out", 155 "ytick.major.width": 1.0, 156 "axes.linewidth": 1.0, 157 "grid.linewidth": 1.0, 158 "font.size": labelsize, 159 "xtick.labelsize": ticksize, 160 "ytick.labelsize": ticksize, 161 } 162 163 # plot_paramsが定義されている場合、デフォルトに追記 164 if plot_params: 165 default_params.update(plot_params) 166 167 plt.rcParams.update(default_params) # プロットパラメータを更新 168 169 def calculate_flux_footprint( 170 self, 171 df: pd.DataFrame, 172 col_flux: str, 173 plot_count: int = 10000, 174 start_time: str = "10:00", 175 end_time: str = "16:00", 176 ) -> tuple[list[float], list[float], list[float]]: 177 """ 178 フラックスフットプリントを計算し、指定された時間帯のデータを基に可視化します。 179 180 Parameters: 181 ------ 182 df : pd.DataFrame 183 分析対象のデータフレーム。フラックスデータを含む。 184 col_flux : str 185 フラックスデータの列名。計算に使用される。 186 plot_count : int, optional 187 生成するプロットの数。デフォルトは10000。 188 start_time : str, optional 189 フットプリント計算に使用する開始時間。デフォルトは"10:00"。 190 end_time : str, optional 191 フットプリント計算に使用する終了時間。デフォルトは"16:00"。 192 193 Returns: 194 ------ 195 tuple[list[float], list[float], list[float]]: 196 x座標 (メートル): タワーを原点とした東西方向の距離 197 y座標 (メートル): タワーを原点とした南北方向の距離 198 対象スカラー量の値: 各地点でのフラックス値 199 200 Notes: 201 ------ 202 - 返却される座標は測定タワーを原点(0,0)とした相対位置です 203 - すべての距離はメートル単位で表されます 204 - 正のx値は東方向、正のy値は北方向を示します 205 """ 206 df: pd.DataFrame = df.copy() 207 208 # インデックスがdatetimeであることを確認し、必要に応じて変換 209 if not isinstance(df.index, pd.DatetimeIndex): 210 df.index = pd.to_datetime(df.index) 211 212 # DatetimeIndexから直接dateプロパティにアクセス 213 datelist: np.ndarray = np.array(df.index.date) 214 215 # 各日付が平日かどうかを判定し、リストに格納 216 numbers: list[int] = [ 217 FluxFootprintAnalyzer.is_weekday(date) for date in datelist 218 ] 219 220 # col_weekdayに基づいてデータフレームに平日情報を追加 221 df.loc[:, self._col_weekday] = numbers # .locを使用して値を設定 222 223 # 値が1のもの(平日)をコピーする 224 data_weekday: pd.DataFrame = df[df[self._col_weekday] == 1].copy() 225 # 特定の時間帯を抽出 226 data_weekday = data_weekday.between_time( 227 start_time, end_time 228 ) # 引数を使用して時間帯を抽出 229 data_weekday = data_weekday.dropna(subset=[col_flux]) 230 231 directions: list[float] = [ 232 wind_direction if wind_direction >= 0 else wind_direction + 360 233 for wind_direction in data_weekday["Wind direction"] 234 ] 235 236 data_weekday.loc[:, "Wind direction_360"] = directions 237 data_weekday.loc[:, "radian"] = data_weekday["Wind direction_360"] / 180 * np.pi 238 239 # 風向が欠測なら除去 240 data_weekday = data_weekday.dropna(subset=["Wind direction", col_flux]) 241 242 # 数値型への変換を確実に行う 243 numeric_columns: list[str] = ["u*", "WS vector", "sigmaV", "z/L"] 244 for col in numeric_columns: 245 data_weekday[col] = pd.to_numeric(data_weekday[col], errors="coerce") 246 247 # 地面修正量dの計算 248 z_m: float = self._z_m 249 Z_d: float = FluxFootprintAnalyzer._calculate_ground_correction( 250 z_m=z_m, 251 wind_speed=data_weekday["WS vector"].values, 252 friction_velocity=data_weekday["u*"].values, 253 stability_parameter=data_weekday["z/L"].values, 254 ) 255 256 x_list: list[float] = [] 257 y_list: list[float] = [] 258 c_list: list[float] | None = [] 259 260 # tqdmを使用してプログレスバーを表示 261 for i in tqdm(range(len(data_weekday)), desc="Calculating footprint"): 262 dUstar: float = data_weekday["u*"].iloc[i] 263 dU: float = data_weekday["WS vector"].iloc[i] 264 sigmaV: float = data_weekday["sigmaV"].iloc[i] 265 dzL: float = data_weekday["z/L"].iloc[i] 266 267 if pd.isna(dUstar) or pd.isna(dU) or pd.isna(sigmaV) or pd.isna(dzL): 268 self.logger.warning(f"#N/A fields are exist.: i = {i}") 269 continue 270 elif dUstar < 5.0 and dUstar != 0.0 and dU > 0.1: 271 phi_m, phi_c, n = FluxFootprintAnalyzer._calculate_stability_parameters( 272 dzL=dzL 273 ) 274 m, U, r, mu, ksi = ( 275 FluxFootprintAnalyzer._calculate_footprint_parameters( 276 dUstar=dUstar, dU=dU, Z_d=Z_d, phi_m=phi_m, phi_c=phi_c, n=n 277 ) 278 ) 279 280 # 80%ソースエリアの計算 281 x80: float = FluxFootprintAnalyzer._source_area_KM2001( 282 ksi=ksi, mu=mu, dU=dU, sigmaV=sigmaV, Z_d=Z_d, max_ratio=0.8 283 ) 284 285 if not np.isnan(x80): 286 x1, y1, flux1 = FluxFootprintAnalyzer._prepare_plot_data( 287 x80, 288 ksi, 289 mu, 290 r, 291 U, 292 m, 293 sigmaV, 294 data_weekday[col_flux].iloc[i], 295 plot_count=plot_count, 296 ) 297 x1_, y1_ = FluxFootprintAnalyzer._rotate_coordinates( 298 x=x1, y=y1, radian=data_weekday["radian"].iloc[i] 299 ) 300 301 x_list.extend(x1_) 302 y_list.extend(y1_) 303 c_list.extend(flux1) 304 305 return ( 306 x_list, 307 y_list, 308 c_list, 309 ) 310 311 def combine_all_data( 312 self, data_source: str | pd.DataFrame, source_type: str = "csv", **kwargs 313 ) -> pd.DataFrame: 314 """ 315 CSVファイルまたはMonthlyConverterからのデータを統合します 316 317 Parameters: 318 ------ 319 data_source : str | pd.DataFrame 320 CSVディレクトリパスまたはDataFrame 321 source_type : str 322 "csv" または "monthly" 323 **kwargs : 324 追加パラメータ 325 - sheet_names : list[str] 326 Monthlyの場合のシート名 327 - start_date : str 328 開始日 329 - end_date : str 330 終了日 331 332 Returns: 333 ------ 334 pd.DataFrame 335 処理済みのデータフレーム 336 """ 337 if source_type == "csv": 338 # 既存のCSV処理ロジック 339 return self._combine_all_csv(data_source) 340 elif source_type == "monthly": 341 # MonthlyConverterからのデータを処理 342 if not isinstance(data_source, pd.DataFrame): 343 raise ValueError("monthly形式の場合、DataFrameを直接渡す必要があります") 344 345 df = data_source.copy() 346 347 # required_columnsからDateを除外して欠損値チェックを行う 348 check_columns = [col for col in self._required_columns if col != "Date"] 349 350 # インデックスがdatetimeであることを確認 351 if not isinstance(df.index, pd.DatetimeIndex) and "Date" not in df.columns: 352 raise ValueError("DatetimeIndexまたはDateカラムが必要です") 353 354 if "Date" in df.columns: 355 df.set_index("Date", inplace=True) 356 357 # 必要なカラムの存在確認 358 missing_columns = [ 359 col for col in check_columns if col not in df.columns.tolist() 360 ] 361 if missing_columns: 362 missing_cols = "','".join(missing_columns) 363 current_cols = "','".join(df.columns.tolist()) 364 raise ValueError( 365 f"必要なカラムが不足しています: '{missing_cols}'\n" 366 f"現在のカラム: '{current_cols}'" 367 ) 368 369 # 平日/休日の判定用カラムを追加 370 df[self._col_weekday] = df.index.map(FluxFootprintAnalyzer.is_weekday) 371 372 # Dateを除外したカラムで欠損値の処理 373 df = df.dropna(subset=check_columns) 374 375 # インデックスの重複を除去 376 df = df.loc[~df.index.duplicated(), :] 377 378 return df 379 else: 380 raise ValueError("source_typeは'csv'または'monthly'である必要があります") 381 382 def get_satellite_image_from_api( 383 self, 384 api_key: str, 385 center_lat: float, 386 center_lon: float, 387 output_path: str, 388 scale: int = 1, 389 size: tuple[int, int] = (2160, 2160), 390 zoom: int = 13, 391 ) -> ImageFile: 392 """ 393 Google Maps Static APIを使用して衛星画像を取得します。 394 395 Parameters: 396 ------ 397 api_key : str 398 Google Maps Static APIのキー。 399 center_lat : float 400 中心の緯度。 401 center_lon : float 402 中心の経度。 403 output_path : str 404 画像の保存先パス。拡張子は'.png'のみ許可される。 405 scale : int, optional 406 画像の解像度スケール(1か2)。デフォルトは1。 407 size : tuple[int, int], optional 408 画像サイズ (幅, 高さ)。デフォルトは(2160, 2160)。 409 zoom : int, optional 410 ズームレベル(0-21)。デフォルトは13。 411 412 Returns: 413 ------ 414 ImageFile 415 取得した衛星画像 416 417 Raises: 418 ------ 419 requests.RequestException 420 API呼び出しに失敗した場合 421 """ 422 # バリデーション 423 if not output_path.endswith(".png"): 424 raise ValueError("出力ファイル名は'.png'で終わる必要があります。") 425 426 # HTTPリクエストの定義 427 base_url = "https://maps.googleapis.com/maps/api/staticmap" 428 params = { 429 "center": f"{center_lat},{center_lon}", 430 "zoom": zoom, 431 "size": f"{size[0]}x{size[1]}", 432 "maptype": "satellite", 433 "scale": scale, 434 "key": api_key, 435 } 436 437 try: 438 response = requests.get(base_url, params=params) 439 response.raise_for_status() 440 # 画像ファイルに変換 441 image = Image.open(io.BytesIO(response.content)) 442 image.save(output_path) 443 self._got_satellite_image = True 444 self.logger.info(f"リモート画像を取得し、保存しました: {output_path}") 445 return image 446 except requests.RequestException as e: 447 self.logger.error(f"衛星画像の取得に失敗しました: {str(e)}") 448 raise 449 450 def get_satellite_image_from_local( 451 self, 452 local_image_path: str, 453 ) -> ImageFile: 454 """ 455 ローカルファイルから衛星画像を読み込みます。 456 457 Parameters: 458 ------ 459 local_image_path : str 460 ローカル画像のパス 461 462 Returns: 463 ------ 464 ImageFile 465 読み込んだ衛星画像 466 467 Raises: 468 ------ 469 FileNotFoundError 470 指定されたパスにファイルが存在しない場合 471 """ 472 if not os.path.exists(local_image_path): 473 raise FileNotFoundError( 474 f"指定されたローカル画像が存在しません: {local_image_path}" 475 ) 476 image = Image.open(local_image_path) 477 self._got_satellite_image = True 478 self.logger.info(f"ローカル画像を使用しました: {local_image_path}") 479 return image 480 481 def plot_flux_footprint( 482 self, 483 x_list: list[float], 484 y_list: list[float], 485 c_list: list[float] | None, 486 center_lat: float, 487 center_lon: float, 488 vmin: float, 489 vmax: float, 490 add_cbar: bool = True, 491 add_legend: bool = True, 492 cbar_label: str | None = None, 493 cbar_labelpad: int = 20, 494 cmap: str = "jet", 495 reduce_c_function: callable = np.mean, 496 lat_correction: float = 1, 497 lon_correction: float = 1, 498 output_dir: str | None = None, 499 output_filename: str = "footprint.png", 500 save_fig: bool = True, 501 show_fig: bool = True, 502 satellite_image: ImageFile | None = None, 503 xy_max: float = 5000, 504 ) -> None: 505 """ 506 フットプリントデータをプロットします。 507 508 このメソッドは、指定されたフットプリントデータのみを可視化します。 509 510 Parameters: 511 ------ 512 x_list : list[float] 513 フットプリントのx座標リスト(メートル単位)。 514 y_list : list[float] 515 フットプリントのy座標リスト(メートル単位)。 516 c_list : list[float] | None 517 フットプリントの強度を示す値のリスト。 518 center_lat : float 519 プロットの中心となる緯度。 520 center_lon : float 521 プロットの中心となる経度。 522 cmap : str 523 使用するカラーマップの名前。 524 vmin : float 525 カラーバーの最小値。 526 vmax : float 527 カラーバーの最大値。 528 reduce_c_function : callable, optional 529 フットプリントの集約関数(デフォルトはnp.mean)。 530 cbar_label : str | None, optional 531 カラーバーのラベル。 532 cbar_labelpad : int, optional 533 カラーバーラベルのパディング。 534 lon_correction : float, optional 535 経度方向の補正係数(デフォルトは1)。 536 lat_correction : float, optional 537 緯度方向の補正係数(デフォルトは1)。 538 output_dir : str | None, optional 539 プロット画像の保存先パス。 540 output_filename : str 541 プロット画像の保存ファイル名(拡張子を含む)。デフォルトは'footprint.png'。 542 save_fig : bool 543 図の保存を許可するフラグ。デフォルトはTrue。 544 show_fig : bool 545 図の表示を許可するフラグ。デフォルトはTrue。 546 satellite_image : ImageFile | None, optional 547 使用する衛星画像。指定がない場合はデフォルトの画像が生成されます。 548 xy_max : float, optional 549 表示範囲の最大値(デフォルトは4000)。 550 """ 551 self.plot_flux_footprint_with_hotspots( 552 x_list=x_list, 553 y_list=y_list, 554 c_list=c_list, 555 center_lat=center_lat, 556 center_lon=center_lon, 557 vmin=vmin, 558 vmax=vmax, 559 add_cbar=add_cbar, 560 add_legend=add_legend, 561 cbar_label=cbar_label, 562 cbar_labelpad=cbar_labelpad, 563 cmap=cmap, 564 reduce_c_function=reduce_c_function, 565 hotspots=None, # hotspotsをNoneに設定 566 hotspot_colors=None, 567 lat_correction=lat_correction, 568 lon_correction=lon_correction, 569 output_dir=output_dir, 570 output_filename=output_filename, 571 save_fig=save_fig, 572 show_fig=show_fig, 573 satellite_image=satellite_image, 574 xy_max=xy_max, 575 ) 576 577 def plot_flux_footprint_with_hotspots( 578 self, 579 x_list: list[float], 580 y_list: list[float], 581 c_list: list[float] | None, 582 center_lat: float, 583 center_lon: float, 584 vmin: float, 585 vmax: float, 586 add_cbar: bool = True, 587 add_legend: bool = True, 588 cbar_label: str | None = None, 589 cbar_labelpad: int = 20, 590 cmap: str = "jet", 591 reduce_c_function: callable = np.mean, 592 hotspots: list[HotspotData] | None = None, 593 hotspot_colors: dict[HotspotType, str] | None = None, 594 hotspot_markers: dict[HotspotType, str] | None = None, 595 lat_correction: float = 1, 596 lon_correction: float = 1, 597 output_dir: str | None = None, 598 output_filename: str = "footprint.png", 599 save_fig: bool = True, 600 show_fig: bool = True, 601 satellite_image: ImageFile | None = None, 602 xy_max: float = 5000, 603 ) -> None: 604 """ 605 Staticな衛星画像上にフットプリントデータとホットスポットをプロットします。 606 607 このメソッドは、指定されたフットプリントデータとホットスポットを可視化します。 608 ホットスポットが指定されない場合は、フットプリントのみ作図します。 609 610 Parameters: 611 ------ 612 x_list : list[float] 613 フットプリントのx座標リスト(メートル単位)。 614 y_list : list[float] 615 フットプリントのy座標リスト(メートル単位)。 616 c_list : list[float] | None 617 フットプリントの強度を示す値のリスト。 618 center_lat : float 619 プロットの中心となる緯度。 620 center_lon : float 621 プロットの中心となる経度。 622 vmin : float 623 カラーバーの最小値。 624 vmax : float 625 カラーバーの最大値。 626 add_cbar : bool, optional 627 カラーバーを追加するかどうか(デフォルトはTrue)。 628 add_legend : bool, optional 629 凡例を追加するかどうか(デフォルトはTrue)。 630 cbar_label : str | None, optional 631 カラーバーのラベル。 632 cbar_labelpad : int, optional 633 カラーバーラベルのパディング。 634 cmap : str 635 使用するカラーマップの名前。 636 reduce_c_function : callable 637 フットプリントの集約関数(デフォルトはnp.mean)。 638 hotspots : list[HotspotData] | None, optional 639 ホットスポットデータのリスト。デフォルトはNone。 640 hotspot_colors : dict[HotspotType, str] | None, optional 641 ホットスポットの色を指定する辞書。 642 hotspot_markers : dict[HotspotType, str] | None, optional 643 ホットスポットの形状を指定する辞書。 644 指定の例は {'bio': '^', 'gas': 'o', 'comb': 's'} (三角、丸、四角)など。 645 lat_correction : float, optional 646 緯度方向の補正係数(デフォルトは1)。 647 lon_correction : float, optional 648 経度方向の補正係数(デフォルトは1)。 649 output_dir : str | None, optional 650 プロット画像の保存先パス。 651 output_filename : str 652 プロット画像の保存ファイル名(拡張子を含む)。デフォルトは'footprint.png'。 653 save_fig : bool 654 図の保存を許可するフラグ。デフォルトはTrue。 655 show_fig : bool 656 図の表示を許可するフラグ。デフォルトはTrue。 657 satellite_image : ImageFile | None, optional 658 使用する衛星画像。指定がない場合はデフォルトの画像が生成されます。 659 xy_max : float, optional 660 表示範囲の最大値(デフォルトは5000)。 661 """ 662 # 1. 引数のバリデーション 663 valid_extensions: list[str] = [".png", ".jpg", ".jpeg", ".pdf", ".svg"] 664 _, file_extension = os.path.splitext(output_filename) 665 if file_extension.lower() not in valid_extensions: 666 quoted_extensions: list[str] = [f'"{ext}"' for ext in valid_extensions] 667 self.logger.error( 668 f"`output_filename`は有効な拡張子ではありません。プロットを保存するには、次のいずれかを指定してください: {','.join(quoted_extensions)}" 669 ) 670 return 671 672 # 2. フラグチェック 673 if not self._got_satellite_image: 674 raise ValueError( 675 "`get_satellite_image_from_api`または`get_satellite_image_from_local`が実行されていません。" 676 ) 677 678 # 3. 衛星画像の取得 679 if satellite_image is None: 680 satellite_image = Image.new("RGB", (2160, 2160), "lightgray") 681 682 self.logger.info("プロットを作成中...") 683 684 # 4. 座標変換のための定数計算(1回だけ) 685 meters_per_lat: float = self.EARTH_RADIUS_METER * ( 686 math.pi / 180 687 ) # 緯度1度あたりのメートル 688 meters_per_lon: float = meters_per_lat * math.cos( 689 math.radians(center_lat) 690 ) # 経度1度あたりのメートル 691 692 # 5. フットプリントデータの座標変換(まとめて1回で実行) 693 x_deg = ( 694 np.array(x_list) / meters_per_lon * lon_correction 695 ) # 補正係数も同時に適用 696 y_deg = ( 697 np.array(y_list) / meters_per_lat * lat_correction 698 ) # 補正係数も同時に適用 699 700 # 6. 中心点からの相対座標を実際の緯度経度に変換 701 lons = center_lon + x_deg 702 lats = center_lat + y_deg 703 704 # 7. 表示範囲の計算(変更なし) 705 x_range: float = xy_max / meters_per_lon 706 y_range: float = xy_max / meters_per_lat 707 map_boundaries: tuple[float, float, float, float] = ( 708 center_lon - x_range, # left_lon 709 center_lon + x_range, # right_lon 710 center_lat - y_range, # bottom_lat 711 center_lat + y_range, # top_lat 712 ) 713 left_lon, right_lon, bottom_lat, top_lat = map_boundaries 714 715 # 8. プロットの作成 716 plt.rcParams["axes.edgecolor"] = "None" 717 fig: plt.Figure = plt.figure(figsize=(10, 8), dpi=300) 718 ax_data: plt.Axes = fig.add_axes([0.05, 0.1, 0.8, 0.8]) 719 720 # 9. フットプリントの描画 721 # フットプリントの描画とカラーバー用の2つのhexbinを作成 722 if c_list is not None: 723 ax_data.hexbin( 724 lons, 725 lats, 726 C=c_list, 727 cmap=cmap, 728 vmin=vmin, 729 vmax=vmax, 730 alpha=0.3, # 実際のプロット用 731 gridsize=100, 732 linewidths=0, 733 mincnt=100, 734 extent=[left_lon, right_lon, bottom_lat, top_lat], 735 reduce_C_function=reduce_c_function, 736 ) 737 738 # カラーバー用の非表示hexbin(alpha=1.0) 739 hidden_hexbin = ax_data.hexbin( 740 lons, 741 lats, 742 C=c_list, 743 cmap=cmap, 744 vmin=vmin, 745 vmax=vmax, 746 alpha=1.0, # カラーバー用 747 gridsize=100, 748 linewidths=0, 749 mincnt=100, 750 extent=[left_lon, right_lon, bottom_lat, top_lat], 751 reduce_C_function=reduce_c_function, 752 visible=False, # プロットには表示しない 753 ) 754 755 # 10. ホットスポットの描画 756 spot_handles = [] 757 if hotspots is not None: 758 default_colors: dict[HotspotType, str] = { 759 "bio": "blue", 760 "gas": "red", 761 "comb": "green", 762 } 763 764 # デフォルトのマーカー形状を定義 765 default_markers: dict[HotspotType, str] = { 766 "bio": "o", 767 "gas": "o", 768 "comb": "o", 769 } 770 771 # 座標変換のための定数 772 meters_per_lat: float = self.EARTH_RADIUS_METER * (math.pi / 180) 773 meters_per_lon: float = meters_per_lat * math.cos(math.radians(center_lat)) 774 775 for spot_type, color in (hotspot_colors or default_colors).items(): 776 spots_lon = [] 777 spots_lat = [] 778 779 # 使用するマーカーを決定 780 marker = (hotspot_markers or default_markers).get(spot_type, "o") 781 782 for spot in hotspots: 783 if spot.type == spot_type: 784 # 変換前の緯度経度をログ出力 785 self.logger.debug( 786 f"Before - Type: {spot_type}, Lat: {spot.avg_lat:.6f}, Lon: {spot.avg_lon:.6f}" 787 ) 788 789 # 中心からの相対距離を計算 790 dx: float = (spot.avg_lon - center_lon) * meters_per_lon 791 dy: float = (spot.avg_lat - center_lat) * meters_per_lat 792 793 # 補正前の相対座標をログ出力 794 self.logger.debug( 795 f"Relative - Type: {spot_type}, X: {dx:.2f}m, Y: {dy:.2f}m" 796 ) 797 798 # 補正を適用 799 corrected_dx: float = dx * lon_correction 800 corrected_dy: float = dy * lat_correction 801 802 # 補正後の緯度経度を計算 803 adjusted_lon: float = center_lon + corrected_dx / meters_per_lon 804 adjusted_lat: float = center_lat + corrected_dy / meters_per_lat 805 806 # 変換後の緯度経度をログ出力 807 self.logger.debug( 808 f"After - Type: {spot_type}, Lat: {adjusted_lat:.6f}, Lon: {adjusted_lon:.6f}\n" 809 ) 810 811 if ( 812 left_lon <= adjusted_lon <= right_lon 813 and bottom_lat <= adjusted_lat <= top_lat 814 ): 815 spots_lon.append(adjusted_lon) 816 spots_lat.append(adjusted_lat) 817 818 if spots_lon: 819 handle = ax_data.scatter( 820 spots_lon, 821 spots_lat, 822 c=color, 823 marker=marker, # マーカー形状を指定 824 s=100, 825 alpha=0.7, 826 label=spot_type, # "bio","gas","comb" 827 edgecolor="black", 828 linewidth=1, 829 ) 830 spot_handles.append(handle) 831 832 # 11. 背景画像の設定 833 ax_img = ax_data.twiny().twinx() 834 ax_img.imshow( 835 satellite_image, 836 extent=[left_lon, right_lon, bottom_lat, top_lat], 837 aspect="equal", 838 ) 839 840 # 12. 軸の設定 841 for ax in [ax_data, ax_img]: 842 ax.set_xlim(left_lon, right_lon) 843 ax.set_ylim(bottom_lat, top_lat) 844 ax.set_xticks([]) 845 ax.set_yticks([]) 846 847 ax_data.set_zorder(2) 848 ax_data.patch.set_alpha(0) 849 ax_img.set_zorder(1) 850 851 # 13. カラーバーの追加 852 if add_cbar: 853 cbar_ax: plt.Axes = fig.add_axes([0.88, 0.1, 0.03, 0.8]) 854 cbar = fig.colorbar(hidden_hexbin, cax=cbar_ax) # hidden_hexbinを使用 855 # cbar_labelが指定されている場合のみラベルを設定 856 if cbar_label: 857 cbar.set_label(cbar_label, rotation=270, labelpad=cbar_labelpad) 858 859 # 14. ホットスポットの凡例追加 860 if add_legend and hotspots and spot_handles: 861 ax_data.legend( 862 handles=spot_handles, 863 loc="upper center", # 位置を上部中央に 864 bbox_to_anchor=(0.55, -0.01), # 図の下に配置 865 ncol=len(spot_handles), # ハンドルの数に応じて列数を設定 866 ) 867 868 # 15. 画像の保存 869 if save_fig: 870 if output_dir is None: 871 raise ValueError( 872 "save_fig=Trueの場合、output_dirを指定する必要があります。有効なディレクトリパスを指定してください。" 873 ) 874 output_path: str = os.path.join(output_dir, output_filename) 875 self.logger.info("プロットを保存中...") 876 try: 877 fig.savefig(output_path, bbox_inches="tight") 878 self.logger.info(f"プロットが正常に保存されました: {output_path}") 879 except Exception as e: 880 self.logger.error(f"プロットの保存中にエラーが発生しました: {str(e)}") 881 # 16. 画像の表示 882 if show_fig: 883 plt.show() 884 else: 885 plt.close(fig=fig) 886 887 def plot_flux_footprint_with_scale_checker( 888 self, 889 x_list: list[float], 890 y_list: list[float], 891 c_list: list[float] | None, 892 center_lat: float, 893 center_lon: float, 894 check_points: list[tuple[float, float, str]] | None = None, 895 vmin: float = 0, 896 vmax: float = 100, 897 add_cbar: bool = True, 898 cbar_label: str | None = None, 899 cbar_labelpad: int = 20, 900 cmap: str = "jet", 901 reduce_c_function: callable = np.mean, 902 lat_correction: float = 1, 903 lon_correction: float = 1, 904 output_dir: str | None = None, 905 output_filename: str = "footprint-scale_checker.png", 906 save_fig: bool = True, 907 show_fig: bool = True, 908 satellite_image: ImageFile | None = None, 909 xy_max: float = 5000, 910 ) -> None: 911 """ 912 Staticな衛星画像上にフットプリントデータとホットスポットをプロットします。 913 914 このメソッドは、指定されたフットプリントデータとホットスポットを可視化します。 915 ホットスポットが指定されない場合は、フットプリントのみ作図します。 916 917 Parameters: 918 ------ 919 x_list : list[float] 920 フットプリントのx座標リスト(メートル単位)。 921 y_list : list[float] 922 フットプリントのy座標リスト(メートル単位)。 923 c_list : list[float] | None 924 フットプリントの強度を示す値のリスト。 925 center_lat : float 926 プロットの中心となる緯度。 927 center_lon : float 928 プロットの中心となる経度。 929 check_points : list[tuple[float, float, str]] | None 930 確認用の地点リスト。各要素は (緯度, 経度, ラベル) のタプル。 931 Noneの場合は中心から500m、1000m、2000m、3000mの位置に仮想的な点を配置。 932 cmap : str 933 使用するカラーマップの名前。 934 vmin : float 935 カラーバーの最小値。 936 vmax : float 937 カラーバーの最大値。 938 reduce_c_function : callable, optional 939 フットプリントの集約関数(デフォルトはnp.mean)。 940 cbar_label : str, optional 941 カラーバーのラベル。 942 cbar_labelpad : int, optional 943 カラーバーラベルのパディング。 944 hotspots : list[HotspotData] | None 945 ホットスポットデータのリスト。デフォルトはNone。 946 hotspot_colors : dict[str, str] | None, optional 947 ホットスポットの色を指定する辞書。 948 lon_correction : float, optional 949 経度方向の補正係数(デフォルトは1)。 950 lat_correction : float, optional 951 緯度方向の補正係数(デフォルトは1)。 952 output_dir : str | None, optional 953 プロット画像の保存先パス。 954 output_filename : str 955 プロット画像の保存ファイル名(拡張子を含む)。デフォルトは'footprint.png'。 956 save_fig : bool 957 図の保存を許可するフラグ。デフォルトはTrue。 958 show_fig : bool 959 図の表示を許可するフラグ。デフォルトはTrue。 960 satellite_image : ImageFile | None, optional 961 使用する衛星画像。指定がない場合はデフォルトの画像が生成されます。 962 xy_max : float, optional 963 表示範囲の最大値(デフォルトは5000)。 964 """ 965 if check_points is None: 966 # デフォルトの確認ポイントを生成(従来の方式) 967 default_points = [ 968 (500, "North", 90), # 北 500m 969 (1000, "East", 0), # 東 1000m 970 (2000, "South", 270), # 南 2000m 971 (3000, "West", 180), # 西 3000m 972 ] 973 974 dummy_hotspots = [] 975 for distance, direction, angle in default_points: 976 rad = math.radians(angle) 977 meters_per_lat = self.EARTH_RADIUS_METER * (math.pi / 180) 978 meters_per_lon = meters_per_lat * math.cos(math.radians(center_lat)) 979 980 dx = distance * math.cos(rad) 981 dy = distance * math.sin(rad) 982 983 delta_lon = dx / meters_per_lon 984 delta_lat = dy / meters_per_lat 985 986 hotspot = HotspotData( 987 avg_lat=center_lat + delta_lat, 988 avg_lon=center_lon + delta_lon, 989 delta_ch4=0.0, 990 delta_c2h6=0.0, 991 ratio=0.0, 992 type=f"{direction}_{distance}m", 993 section=0, 994 source="scale_check", 995 angle=0, 996 correlation=0, 997 ) 998 dummy_hotspots.append(hotspot) 999 else: 1000 # 指定された緯度経度を使用 1001 dummy_hotspots = [] 1002 for lat, lon, label in check_points: 1003 hotspot = HotspotData( 1004 avg_lat=lat, 1005 avg_lon=lon, 1006 delta_ch4=0.0, 1007 delta_c2h6=0.0, 1008 ratio=0.0, 1009 type=label, 1010 section=0, 1011 source="scale_check", 1012 angle=0, 1013 correlation=0, 1014 ) 1015 dummy_hotspots.append(hotspot) 1016 1017 # カスタムカラーマップの作成 1018 hotspot_colors = { 1019 spot.type: plt.cm.tab10(i % 10) for i, spot in enumerate(dummy_hotspots) 1020 } 1021 1022 # 既存のメソッドを呼び出してプロット 1023 self.plot_flux_footprint_with_hotspots( 1024 x_list=x_list, 1025 y_list=y_list, 1026 c_list=c_list, 1027 center_lat=center_lat, 1028 center_lon=center_lon, 1029 vmin=vmin, 1030 vmax=vmax, 1031 add_cbar=add_cbar, 1032 add_legend=True, 1033 cbar_label=cbar_label, 1034 cbar_labelpad=cbar_labelpad, 1035 cmap=cmap, 1036 reduce_c_function=reduce_c_function, 1037 hotspots=dummy_hotspots, 1038 hotspot_colors=hotspot_colors, 1039 lat_correction=lat_correction, 1040 lon_correction=lon_correction, 1041 output_dir=output_dir, 1042 output_filename=output_filename, 1043 save_fig=save_fig, 1044 show_fig=show_fig, 1045 satellite_image=satellite_image, 1046 xy_max=xy_max, 1047 ) 1048 1049 def _combine_all_csv(self, csv_dir_path: str, suffix: str = ".csv") -> pd.DataFrame: 1050 """ 1051 指定されたディレクトリ内の全CSVファイルを読み込み、処理し、結合します。 1052 Monthlyシートを結合することを想定しています。 1053 1054 Parameters: 1055 ------ 1056 csv_dir_path : str 1057 CSVファイルが格納されているディレクトリのパス。 1058 suffix : str, optional 1059 読み込むファイルの拡張子。デフォルトは".csv"。 1060 1061 Returns: 1062 ------ 1063 pandas.DataFrame 1064 結合および処理済みのデータフレーム。 1065 1066 Notes: 1067 ------ 1068 - ディレクトリ内に少なくとも1つのCSVファイルが必要です。 1069 """ 1070 csv_files = [f for f in os.listdir(csv_dir_path) if f.endswith(suffix)] 1071 if not csv_files: 1072 raise ValueError("指定されたディレクトリにCSVファイルが見つかりません。") 1073 1074 df_array: list[pd.DataFrame] = [] 1075 for csv_file in csv_files: 1076 file_path: str = os.path.join(csv_dir_path, csv_file) 1077 df: pd.DataFrame = self._prepare_csv(file_path) 1078 df_array.append(df) 1079 1080 # 結合 1081 df_combined: pd.DataFrame = pd.concat(df_array, join="outer") 1082 df_combined = df_combined.loc[~df_combined.index.duplicated(), :] 1083 1084 # 平日と休日の判定に使用するカラムを作成 1085 df_combined[self._col_weekday] = df_combined.index.map( 1086 FluxFootprintAnalyzer.is_weekday 1087 ) # 共通の関数を使用 1088 1089 return df_combined 1090 1091 def _prepare_csv(self, file_path: str) -> pd.DataFrame: 1092 """ 1093 フラックスデータを含むCSVファイルを読み込み、処理します。 1094 1095 Parameters: 1096 ------ 1097 file_path : str 1098 CSVファイルのパス。 1099 1100 Returns: 1101 ------ 1102 pandas.DataFrame 1103 処理済みのデータフレーム。 1104 """ 1105 # CSVファイルの最初の行を読み込み、ヘッダーを取得するための一時データフレームを作成 1106 temp: pd.DataFrame = pd.read_csv(file_path, header=None, nrows=1, skiprows=0) 1107 header = temp.loc[temp.index[0]] 1108 1109 # 実際のデータを読み込み、必要な行をスキップし、欠損値を指定 1110 df: pd.DataFrame = pd.read_csv( 1111 file_path, 1112 header=None, 1113 skiprows=2, 1114 na_values=["#DIV/0!", "#VALUE!", "#REF!", "#N/A", "#NAME?", "NAN"], 1115 low_memory=False, 1116 ) 1117 # 取得したヘッダーをデータフレームに設定 1118 df.columns = header 1119 1120 # self._required_columnsのカラムが存在するか確認 1121 missing_columns: list[str] = [ 1122 col for col in self._required_columns if col not in df.columns.tolist() 1123 ] 1124 if missing_columns: 1125 raise ValueError( 1126 f"必要なカラムが不足しています: {', '.join(missing_columns)}" 1127 ) 1128 1129 # "Date"カラムをインデックスに設定して返却 1130 df["Date"] = pd.to_datetime(df["Date"]) 1131 df = df.dropna(subset=["Date"]) 1132 df.set_index("Date", inplace=True) 1133 return df 1134 1135 @staticmethod 1136 def _calculate_footprint_parameters( 1137 dUstar: float, dU: float, Z_d: float, phi_m: float, phi_c: float, n: float 1138 ) -> tuple[float, float, float, float, float]: 1139 """ 1140 フットプリントパラメータを計算します。 1141 1142 Parameters: 1143 ------ 1144 dUstar : float 1145 摩擦速度 1146 dU : float 1147 風速 1148 Z_d : float 1149 地面修正後の測定高度 1150 phi_m : float 1151 運動量の安定度関数 1152 phi_c : float 1153 スカラーの安定度関数 1154 n : float 1155 安定度パラメータ 1156 1157 Returns: 1158 ------ 1159 tuple[float, float, float, float, float] 1160 m (べき指数), 1161 U (基準高度での風速), 1162 r (べき指数の補正項), 1163 mu (形状パラメータ), 1164 ksi (フラックス長さスケール) 1165 """ 1166 KARMAN: float = 0.4 # フォン・カルマン定数 1167 # パラメータの計算 1168 m: float = dUstar / KARMAN * phi_m / dU 1169 U: float = dU / pow(Z_d, m) 1170 r: float = 2.0 + m - n 1171 mu: float = (1.0 + m) / r 1172 kz: float = KARMAN * dUstar * Z_d / phi_c 1173 k: float = kz / pow(Z_d, n) 1174 ksi: float = U * pow(Z_d, r) / r / r / k 1175 return m, U, r, mu, ksi 1176 1177 @staticmethod 1178 def _calculate_ground_correction( 1179 z_m: float, 1180 wind_speed: np.ndarray, 1181 friction_velocity: np.ndarray, 1182 stability_parameter: np.ndarray, 1183 ) -> float: 1184 """ 1185 地面修正量を計算します(Pennypacker and Baldocchi, 2016)。 1186 1187 この関数は、与えられた気象データを使用して地面修正量を計算します。 1188 計算は以下のステップで行われます: 1189 1. 変位高さ(d)を計算 1190 2. 中立条件外のデータを除外 1191 3. 平均変位高さを計算 1192 4. 地面修正量を返す 1193 1194 Parameters: 1195 ------ 1196 z_m : float 1197 観測地点の高度 1198 wind_speed : np.ndarray 1199 風速データ配列 (WS vector) 1200 friction_velocity : np.ndarray 1201 摩擦速度データ配列 (u*) 1202 stability_parameter : np.ndarray 1203 安定度パラメータ配列 (z/L) 1204 1205 Returns: 1206 ------ 1207 float 1208 計算された地面修正量 1209 """ 1210 KARMAN: float = 0.4 # フォン・カルマン定数 1211 z: float = z_m 1212 1213 # 変位高さ(d)の計算 1214 displacement_height = 0.6 * ( 1215 z / (0.6 + 0.1 * (np.exp((KARMAN * wind_speed) / friction_velocity))) 1216 ) 1217 1218 # 中立条件外のデータをマスク(中立条件:-0.1 < z/L < 0.1) 1219 neutral_condition_mask = (stability_parameter < -0.1) | ( 1220 0.1 < stability_parameter 1221 ) 1222 displacement_height[neutral_condition_mask] = np.nan 1223 1224 # 平均変位高さを計算 1225 d: float = np.nanmean(displacement_height) 1226 1227 # 地面修正量を返す 1228 return z - d 1229 1230 @staticmethod 1231 def _calculate_stability_parameters(dzL: float) -> tuple[float, float, float]: 1232 """ 1233 安定性パラメータを計算します。 1234 大気安定度に基づいて、運動量とスカラーの安定度関数、および安定度パラメータを計算します。 1235 1236 Parameters: 1237 ------ 1238 dzL : float 1239 無次元高度 (z/L)、ここで z は測定高度、L はモニン・オブコフ長 1240 1241 Returns: 1242 ------ 1243 tuple[float, float, float] 1244 phi_m : float 1245 運動量の安定度関数 1246 phi_c : float 1247 スカラーの安定度関数 1248 n : float 1249 安定度パラメータ 1250 """ 1251 phi_m: float = 0 1252 phi_c: float = 0 1253 n: float = 0 1254 if dzL > 0.0: 1255 # 安定成層の場合 1256 dzL = min(dzL, 2.0) 1257 phi_m = 1.0 + 5.0 * dzL 1258 phi_c = 1.0 + 5.0 * dzL 1259 n = 1.0 / (1.0 + 5.0 * dzL) 1260 else: 1261 # 不安定成層の場合 1262 phi_m = pow(1.0 - 16.0 * dzL, -0.25) 1263 phi_c = pow(1.0 - 16.0 * dzL, -0.50) 1264 n = (1.0 - 24.0 * dzL) / (1.0 - 16.0 * dzL) 1265 return phi_m, phi_c, n 1266 1267 @staticmethod 1268 def filter_data( 1269 df: pd.DataFrame, 1270 start_date: str | None = None, 1271 end_date: str | None = None, 1272 months: list[int] | None = None, 1273 ) -> pd.DataFrame: 1274 """ 1275 指定された期間や月でデータをフィルタリングするメソッド。 1276 1277 Parameters: 1278 ------ 1279 df : pd.DataFrame 1280 フィルタリングするデータフレーム 1281 start_date : str | None 1282 フィルタリングの開始日('YYYY-MM-DD'形式)。デフォルトはNone。 1283 end_date : str | None 1284 フィルタリングの終了日('YYYY-MM-DD'形式)。デフォルトはNone。 1285 months : list[int] | None 1286 フィルタリングする月のリスト(例:[1, 2, 12])。デフォルトはNone。 1287 1288 Returns: 1289 ------ 1290 pd.DataFrame 1291 フィルタリングされたデータフレーム 1292 1293 Raises: 1294 ------ 1295 ValueError 1296 インデックスがDatetimeIndexでない場合、または日付の形式が不正な場合 1297 """ 1298 # インデックスの検証 1299 if not isinstance(df.index, pd.DatetimeIndex): 1300 raise ValueError( 1301 "DataFrameのインデックスはDatetimeIndexである必要があります" 1302 ) 1303 1304 filtered_df: pd.DataFrame = df.copy() 1305 1306 # 日付形式の検証と変換 1307 try: 1308 if start_date is not None: 1309 start_date = pd.to_datetime(start_date) 1310 if end_date is not None: 1311 end_date = pd.to_datetime(end_date) 1312 except ValueError as e: 1313 raise ValueError( 1314 "日付の形式が不正です。'YYYY-MM-DD'形式で指定してください" 1315 ) from e 1316 1317 # 期間でフィルタリング 1318 if start_date is not None or end_date is not None: 1319 filtered_df = filtered_df.loc[start_date:end_date] 1320 1321 # 月のバリデーション 1322 if months is not None: 1323 if not all(isinstance(m, int) and 1 <= m <= 12 for m in months): 1324 raise ValueError( 1325 "monthsは1から12までの整数のリストである必要があります" 1326 ) 1327 filtered_df = filtered_df[filtered_df.index.month.isin(months)] 1328 1329 # フィルタリング後のデータが空でないことを確認 1330 if filtered_df.empty: 1331 raise ValueError("フィルタリング後のデータが空になりました") 1332 1333 return filtered_df 1334 1335 @staticmethod 1336 def is_weekday(date: datetime) -> int: 1337 """ 1338 指定された日付が平日であるかどうかを判定します。 1339 1340 Parameters: 1341 ------ 1342 date : datetime 1343 判定する日付。 1344 1345 Returns: 1346 ------ 1347 int 1348 平日であれば1、そうでなければ0。 1349 """ 1350 return 1 if not jpholiday.is_holiday(date) and date.weekday() < 5 else 0 1351 1352 @staticmethod 1353 def _prepare_plot_data( 1354 x80: float, 1355 ksi: float, 1356 mu: float, 1357 r: float, 1358 U: float, 1359 m: float, 1360 sigmaV: float, 1361 flux_value: float, 1362 plot_count: int, 1363 ) -> tuple[np.ndarray, np.ndarray, np.ndarray]: 1364 """ 1365 フットプリントのプロットデータを準備します。 1366 1367 Parameters: 1368 ------ 1369 x80 : float 1370 80%寄与距離 1371 ksi : float 1372 フラックス長さスケール 1373 mu : float 1374 形状パラメータ 1375 r : float 1376 べき指数 1377 U : float 1378 風速 1379 m : float 1380 風速プロファイルのべき指数 1381 sigmaV : float 1382 風速の標準偏差 1383 flux_value : float 1384 フラックス値 1385 plot_count : int 1386 生成するプロット数 1387 1388 Returns: 1389 ------ 1390 tuple[np.ndarray, np.ndarray, np.ndarray] 1391 x座標、y座標、フラックス値の配列のタプル 1392 """ 1393 KARMAN: float = 0.4 # フォン・カルマン定数 (pp.210) 1394 x_lim: int = int(x80) 1395 1396 """ 1397 各ランで生成するプロット数 1398 多いほどメモリに付加がかかるため注意 1399 """ 1400 plot_num: int = plot_count # 各ランで生成するプロット数 1401 1402 # x方向の距離配列を生成 1403 x_list: np.ndarray = np.arange(1, x_lim + 1, dtype="float64") 1404 1405 # クロスウィンド積分フットプリント関数を計算 1406 f_list: np.ndarray = ( 1407 ksi**mu * np.exp(-ksi / x_list) / math.gamma(mu) / x_list ** (1.0 + mu) 1408 ) 1409 1410 # プロット数に基づいてx座標を生成 1411 num_list: np.ndarray = np.round(f_list * plot_num).astype("int64") 1412 x1: np.ndarray = np.repeat(x_list, num_list) 1413 1414 # 風速プロファイルを計算 1415 Ux: np.ndarray = ( 1416 (math.gamma(mu) / math.gamma(1 / r)) 1417 * ((r**2 * KARMAN) / U) ** (m / r) 1418 * U 1419 * x1 ** (m / r) 1420 ) 1421 1422 # y方向の分散を計算し、正規分布に従ってy座標を生成 1423 sigma_array: np.ndarray = sigmaV * x1 / Ux 1424 y1: np.ndarray = np.random.normal(0, sigma_array) 1425 1426 # フラックス値の配列を生成 1427 flux1 = np.full_like(x1, flux_value) 1428 1429 return x1, y1, flux1 1430 1431 @staticmethod 1432 def _rotate_coordinates( 1433 x: np.ndarray, y: np.ndarray, radian: float 1434 ) -> tuple[np.ndarray, np.ndarray]: 1435 """ 1436 座標を指定された角度で回転させます。 1437 1438 この関数は、与えられたx座標とy座標を、指定された角度(ラジアン)で回転させます。 1439 回転は原点を中心に反時計回りに行われます。 1440 1441 Parameters: 1442 ------ 1443 x : np.ndarray 1444 回転させるx座標の配列 1445 y : np.ndarray 1446 回転させるy座標の配列 1447 radian : float 1448 回転角度(ラジアン) 1449 1450 Returns: 1451 ------ 1452 tuple[np.ndarray, np.ndarray] 1453 回転後の(x_, y_)座標の組 1454 """ 1455 radian1: float = (radian - (np.pi / 2)) * (-1) 1456 x_: np.ndarray = x * np.cos(radian1) - y * np.sin(radian1) 1457 y_: np.ndarray = x * np.sin(radian1) + y * np.cos(radian1) 1458 return x_, y_ 1459 1460 @staticmethod 1461 def _source_area_KM2001( 1462 ksi: float, 1463 mu: float, 1464 dU: float, 1465 sigmaV: float, 1466 Z_d: float, 1467 max_ratio: float = 0.8, 1468 ) -> float: 1469 """ 1470 Kormann and Meixner (2001)のフットプリントモデルに基づいてソースエリアを計算します。 1471 1472 このメソッドは、与えられたパラメータを使用して、フラックスの寄与距離を計算します。 1473 計算は反復的に行われ、寄与率が'max_ratio'に達するまで、または最大反復回数に達するまで続けられます。 1474 1475 Parameters: 1476 ------ 1477 ksi : float 1478 フラックス長さスケール 1479 mu : float 1480 形状パラメータ 1481 dU : float 1482 風速の変化率 1483 sigmaV : float 1484 風速の標準偏差 1485 Z_d : float 1486 ゼロ面変位高度 1487 max_ratio : float, optional 1488 寄与率の最大値。デフォルトは0.8。 1489 1490 Returns: 1491 ------ 1492 float 1493 80%寄与距離(メートル単位)。計算が収束しない場合はnp.nan。 1494 1495 Notes: 1496 ------ 1497 - 計算が収束しない場合(最大反復回数に達した場合)、結果はnp.nanとなります。 1498 """ 1499 if max_ratio > 1: 1500 raise ValueError("max_ratio は0以上1以下である必要があります。") 1501 # 変数の初期値 1502 sum_f: float = 0.0 # 寄与率(0 < sum_f < 1.0) 1503 x1: float = 0.0 1504 dF_xd: float = 0.0 1505 1506 x_d: float = ksi / ( 1507 1.0 + mu 1508 ) # Eq. 22 (x_d : クロスウィンド積分フラックスフットプリント最大位置) 1509 1510 dx: float = x_d / 100.0 # 等値線の拡がりの最大距離の100分の1(m) 1511 1512 # 寄与率が80%に達するまでfを積算 1513 while sum_f < (max_ratio / 1): 1514 x1 += dx 1515 1516 # Equation 21 (dF : クロスウィンド積分フットプリント) 1517 dF: float = ( 1518 pow(ksi, mu) * math.exp(-ksi / x1) / math.gamma(mu) / pow(x1, 1.0 + mu) 1519 ) 1520 1521 sum_f += dF # Footprint を加えていく (0.0 < dF < 1.0) 1522 dx *= 2.0 # 距離は2倍ずつ増やしていく 1523 1524 if dx > 1.0: 1525 dx = 1.0 # 一気に、1 m 以上はインクリメントしない 1526 if x1 > Z_d * 1000.0: 1527 break # ソースエリアが測定高度の1000倍以上となった場合、エラーとして止める 1528 1529 x_dst: float = x1 # 寄与率が80%に達するまでの積算距離 1530 f_last: float = ( 1531 pow(ksi, mu) 1532 * math.exp(-ksi / x_dst) 1533 / math.gamma(mu) 1534 / pow(x_dst, 1.0 + mu) 1535 ) # Page 214 just below the Eq. 21. 1536 1537 # y方向の最大距離とその位置のxの距離 1538 dy: float = x_d / 100.0 # 等値線の拡がりの最大距離の100分の1 1539 y_dst: float = 0.0 1540 accumulated_y: float = 0.0 # y方向の積算距離を表す変数 1541 1542 # 最大反復回数を設定 1543 MAX_ITERATIONS: int = 100000 1544 for _ in range(MAX_ITERATIONS): 1545 accumulated_y += dy 1546 if accumulated_y >= x_dst: 1547 break 1548 1549 dF_xd = ( 1550 pow(ksi, mu) 1551 * math.exp(-ksi / accumulated_y) 1552 / math.gamma(mu) 1553 / pow(accumulated_y, 1.0 + mu) 1554 ) # 式21の直下(214ページ) 1555 1556 aa: float = math.log(x_dst * dF_xd / f_last / accumulated_y) 1557 sigma: float = sigmaV * accumulated_y / dU # 215ページ8行目 1558 1559 if 2.0 * aa >= 0: 1560 y_dst_new: float = sigma * math.sqrt(2.0 * aa) 1561 if y_dst_new <= y_dst: 1562 break # forループを抜ける 1563 y_dst = y_dst_new 1564 1565 dy = min(dy * 2.0, 1.0) 1566 1567 else: 1568 # ループが正常に終了しなかった場合(最大反復回数に達した場合) 1569 x_dst = np.nan 1570 1571 return x_dst
フラックスフットプリントを解析および可視化するクラス。
このクラスは、フラックスデータの処理、フットプリントの計算、 および結果を衛星画像上に可視化するメソッドを提供します。 座標系と単位に関する重要な注意:
- すべての距離はメートル単位で計算されます
- 座標系の原点(0,0)は測定タワーの位置に対応します
- x軸は東西方向(正が東)
- y軸は南北方向(正が北)
- 風向は気象学的風向(北から時計回りに測定)を使用
この実装は、Kormann and Meixner (2001) および Takano et al. (2021)に基づいています。
36 def __init__( 37 self, 38 z_m: float, 39 labelsize: float = 20, 40 ticksize: float = 16, 41 plot_params=None, 42 logger: Logger | None = None, 43 logging_debug: bool = False, 44 ): 45 """ 46 衛星画像を用いて FluxFootprintAnalyzer を初期化します。 47 48 Parameters: 49 ------ 50 z_m : float 51 測定の高さ(メートル単位)。 52 labelsize : float 53 軸ラベルのフォントサイズ。デフォルトは20。 54 ticksize : float 55 軸目盛りのフォントサイズ。デフォルトは16。 56 plot_params : Optional[Dict[str, any]] 57 matplotlibのプロットパラメータを指定する辞書。 58 logger : Logger | None 59 使用するロガー。Noneの場合は新しいロガーを生成します。 60 logging_debug : bool 61 ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。 62 """ 63 # 定数や共通の変数 64 self._required_columns: list[str] = [ 65 "Date", 66 "WS vector", 67 "u*", 68 "z/L", 69 "Wind direction", 70 "sigmaV", 71 ] # 必要なカラムの名前 72 self._col_weekday: str = "ffa_is_weekday" # クラスで生成するカラムのキー名 73 self._z_m: float = z_m # 測定高度 74 # 状態を管理するフラグ 75 self._got_satellite_image: bool = False 76 77 # 図表の初期設定 78 FluxFootprintAnalyzer.setup_plot_params(labelsize, ticksize, plot_params) 79 # ロガー 80 log_level: int = INFO 81 if logging_debug: 82 log_level = DEBUG 83 self.logger: Logger = FluxFootprintAnalyzer.setup_logger(logger, log_level)
衛星画像を用いて FluxFootprintAnalyzer を初期化します。
Parameters:
z_m : float
測定の高さ(メートル単位)。
labelsize : float
軸ラベルのフォントサイズ。デフォルトは20。
ticksize : float
軸目盛りのフォントサイズ。デフォルトは16。
plot_params : Optional[Dict[str, any]]
matplotlibのプロットパラメータを指定する辞書。
logger : Logger | None
使用するロガー。Noneの場合は新しいロガーを生成します。
logging_debug : bool
ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。
85 @staticmethod 86 def setup_logger(logger: Logger | None, log_level: int = INFO) -> Logger: 87 """ 88 ロガーを設定します。 89 90 このメソッドは、ロギングの設定を行い、ログメッセージのフォーマットを指定します。 91 ログメッセージには、日付、ログレベル、メッセージが含まれます。 92 93 渡されたロガーがNoneまたは不正な場合は、新たにロガーを作成し、標準出力に 94 ログメッセージが表示されるようにStreamHandlerを追加します。ロガーのレベルは 95 引数で指定されたlog_levelに基づいて設定されます。 96 97 Parameters: 98 ------ 99 logger : Logger | None 100 使用するロガー。Noneの場合は新しいロガーを作成します。 101 log_level : int 102 ロガーのログレベル。デフォルトはINFO。 103 104 Returns: 105 ------ 106 Logger 107 設定されたロガーオブジェクト。 108 """ 109 if logger is not None and isinstance(logger, Logger): 110 return logger 111 # 渡されたロガーがNoneまたは正しいものでない場合は独自に設定 112 new_logger: Logger = getLogger() 113 # 既存のハンドラーをすべて削除 114 for handler in new_logger.handlers[:]: 115 new_logger.removeHandler(handler) 116 new_logger.setLevel(log_level) # ロガーのレベルを設定 117 ch = StreamHandler() 118 ch_formatter = Formatter("%(asctime)s - %(levelname)s - %(message)s") 119 ch.setFormatter(ch_formatter) # フォーマッターをハンドラーに設定 120 new_logger.addHandler(ch) # StreamHandlerの追加 121 return new_logger
ロガーを設定します。
このメソッドは、ロギングの設定を行い、ログメッセージのフォーマットを指定します。 ログメッセージには、日付、ログレベル、メッセージが含まれます。
渡されたロガーがNoneまたは不正な場合は、新たにロガーを作成し、標準出力に ログメッセージが表示されるようにStreamHandlerを追加します。ロガーのレベルは 引数で指定されたlog_levelに基づいて設定されます。
Parameters:
logger : Logger | None
使用するロガー。Noneの場合は新しいロガーを作成します。
log_level : int
ロガーのログレベル。デフォルトはINFO。
Returns:
Logger
設定されたロガーオブジェクト。
123 @staticmethod 124 def setup_plot_params(labelsize: float, ticksize: float, plot_params=None) -> None: 125 """ 126 matplotlibのプロットパラメータを設定します。 127 128 Parameters: 129 ------ 130 labelsize : float 131 軸ラベルのフォントサイズ。 132 ticksize : float 133 軸目盛りのフォントサイズ。 134 plot_params : Optional[Dict[str, any]] 135 matplotlibのプロットパラメータの辞書。 136 137 Returns: 138 ------ 139 None 140 このメソッドは戻り値を持ちませんが、プロットパラメータを更新します。 141 """ 142 # デフォルトのプロットパラメータ 143 default_params = { 144 "font.family": ["Arial", "Dejavu Sans"], 145 "axes.edgecolor": "None", 146 "axes.labelcolor": "black", 147 "text.color": "black", 148 "xtick.color": "black", 149 "ytick.color": "black", 150 "grid.color": "gray", 151 "axes.grid": False, 152 "xtick.major.size": 0, 153 "ytick.major.size": 0, 154 "ytick.direction": "out", 155 "ytick.major.width": 1.0, 156 "axes.linewidth": 1.0, 157 "grid.linewidth": 1.0, 158 "font.size": labelsize, 159 "xtick.labelsize": ticksize, 160 "ytick.labelsize": ticksize, 161 } 162 163 # plot_paramsが定義されている場合、デフォルトに追記 164 if plot_params: 165 default_params.update(plot_params) 166 167 plt.rcParams.update(default_params) # プロットパラメータを更新
matplotlibのプロットパラメータを設定します。
Parameters:
labelsize : float
軸ラベルのフォントサイズ。
ticksize : float
軸目盛りのフォントサイズ。
plot_params : Optional[Dict[str, any]]
matplotlibのプロットパラメータの辞書。
Returns:
None
このメソッドは戻り値を持ちませんが、プロットパラメータを更新します。
169 def calculate_flux_footprint( 170 self, 171 df: pd.DataFrame, 172 col_flux: str, 173 plot_count: int = 10000, 174 start_time: str = "10:00", 175 end_time: str = "16:00", 176 ) -> tuple[list[float], list[float], list[float]]: 177 """ 178 フラックスフットプリントを計算し、指定された時間帯のデータを基に可視化します。 179 180 Parameters: 181 ------ 182 df : pd.DataFrame 183 分析対象のデータフレーム。フラックスデータを含む。 184 col_flux : str 185 フラックスデータの列名。計算に使用される。 186 plot_count : int, optional 187 生成するプロットの数。デフォルトは10000。 188 start_time : str, optional 189 フットプリント計算に使用する開始時間。デフォルトは"10:00"。 190 end_time : str, optional 191 フットプリント計算に使用する終了時間。デフォルトは"16:00"。 192 193 Returns: 194 ------ 195 tuple[list[float], list[float], list[float]]: 196 x座標 (メートル): タワーを原点とした東西方向の距離 197 y座標 (メートル): タワーを原点とした南北方向の距離 198 対象スカラー量の値: 各地点でのフラックス値 199 200 Notes: 201 ------ 202 - 返却される座標は測定タワーを原点(0,0)とした相対位置です 203 - すべての距離はメートル単位で表されます 204 - 正のx値は東方向、正のy値は北方向を示します 205 """ 206 df: pd.DataFrame = df.copy() 207 208 # インデックスがdatetimeであることを確認し、必要に応じて変換 209 if not isinstance(df.index, pd.DatetimeIndex): 210 df.index = pd.to_datetime(df.index) 211 212 # DatetimeIndexから直接dateプロパティにアクセス 213 datelist: np.ndarray = np.array(df.index.date) 214 215 # 各日付が平日かどうかを判定し、リストに格納 216 numbers: list[int] = [ 217 FluxFootprintAnalyzer.is_weekday(date) for date in datelist 218 ] 219 220 # col_weekdayに基づいてデータフレームに平日情報を追加 221 df.loc[:, self._col_weekday] = numbers # .locを使用して値を設定 222 223 # 値が1のもの(平日)をコピーする 224 data_weekday: pd.DataFrame = df[df[self._col_weekday] == 1].copy() 225 # 特定の時間帯を抽出 226 data_weekday = data_weekday.between_time( 227 start_time, end_time 228 ) # 引数を使用して時間帯を抽出 229 data_weekday = data_weekday.dropna(subset=[col_flux]) 230 231 directions: list[float] = [ 232 wind_direction if wind_direction >= 0 else wind_direction + 360 233 for wind_direction in data_weekday["Wind direction"] 234 ] 235 236 data_weekday.loc[:, "Wind direction_360"] = directions 237 data_weekday.loc[:, "radian"] = data_weekday["Wind direction_360"] / 180 * np.pi 238 239 # 風向が欠測なら除去 240 data_weekday = data_weekday.dropna(subset=["Wind direction", col_flux]) 241 242 # 数値型への変換を確実に行う 243 numeric_columns: list[str] = ["u*", "WS vector", "sigmaV", "z/L"] 244 for col in numeric_columns: 245 data_weekday[col] = pd.to_numeric(data_weekday[col], errors="coerce") 246 247 # 地面修正量dの計算 248 z_m: float = self._z_m 249 Z_d: float = FluxFootprintAnalyzer._calculate_ground_correction( 250 z_m=z_m, 251 wind_speed=data_weekday["WS vector"].values, 252 friction_velocity=data_weekday["u*"].values, 253 stability_parameter=data_weekday["z/L"].values, 254 ) 255 256 x_list: list[float] = [] 257 y_list: list[float] = [] 258 c_list: list[float] | None = [] 259 260 # tqdmを使用してプログレスバーを表示 261 for i in tqdm(range(len(data_weekday)), desc="Calculating footprint"): 262 dUstar: float = data_weekday["u*"].iloc[i] 263 dU: float = data_weekday["WS vector"].iloc[i] 264 sigmaV: float = data_weekday["sigmaV"].iloc[i] 265 dzL: float = data_weekday["z/L"].iloc[i] 266 267 if pd.isna(dUstar) or pd.isna(dU) or pd.isna(sigmaV) or pd.isna(dzL): 268 self.logger.warning(f"#N/A fields are exist.: i = {i}") 269 continue 270 elif dUstar < 5.0 and dUstar != 0.0 and dU > 0.1: 271 phi_m, phi_c, n = FluxFootprintAnalyzer._calculate_stability_parameters( 272 dzL=dzL 273 ) 274 m, U, r, mu, ksi = ( 275 FluxFootprintAnalyzer._calculate_footprint_parameters( 276 dUstar=dUstar, dU=dU, Z_d=Z_d, phi_m=phi_m, phi_c=phi_c, n=n 277 ) 278 ) 279 280 # 80%ソースエリアの計算 281 x80: float = FluxFootprintAnalyzer._source_area_KM2001( 282 ksi=ksi, mu=mu, dU=dU, sigmaV=sigmaV, Z_d=Z_d, max_ratio=0.8 283 ) 284 285 if not np.isnan(x80): 286 x1, y1, flux1 = FluxFootprintAnalyzer._prepare_plot_data( 287 x80, 288 ksi, 289 mu, 290 r, 291 U, 292 m, 293 sigmaV, 294 data_weekday[col_flux].iloc[i], 295 plot_count=plot_count, 296 ) 297 x1_, y1_ = FluxFootprintAnalyzer._rotate_coordinates( 298 x=x1, y=y1, radian=data_weekday["radian"].iloc[i] 299 ) 300 301 x_list.extend(x1_) 302 y_list.extend(y1_) 303 c_list.extend(flux1) 304 305 return ( 306 x_list, 307 y_list, 308 c_list, 309 )
フラックスフットプリントを計算し、指定された時間帯のデータを基に可視化します。
Parameters:
df : pd.DataFrame
分析対象のデータフレーム。フラックスデータを含む。
col_flux : str
フラックスデータの列名。計算に使用される。
plot_count : int, optional
生成するプロットの数。デフォルトは10000。
start_time : str, optional
フットプリント計算に使用する開始時間。デフォルトは"10:00"。
end_time : str, optional
フットプリント計算に使用する終了時間。デフォルトは"16:00"。
Returns:
tuple[list[float], list[float], list[float]]:
x座標 (メートル): タワーを原点とした東西方向の距離
y座標 (メートル): タワーを原点とした南北方向の距離
対象スカラー量の値: 各地点でのフラックス値
Notes:
- 返却される座標は測定タワーを原点(0,0)とした相対位置です
- すべての距離はメートル単位で表されます
- 正のx値は東方向、正のy値は北方向を示します
311 def combine_all_data( 312 self, data_source: str | pd.DataFrame, source_type: str = "csv", **kwargs 313 ) -> pd.DataFrame: 314 """ 315 CSVファイルまたはMonthlyConverterからのデータを統合します 316 317 Parameters: 318 ------ 319 data_source : str | pd.DataFrame 320 CSVディレクトリパスまたはDataFrame 321 source_type : str 322 "csv" または "monthly" 323 **kwargs : 324 追加パラメータ 325 - sheet_names : list[str] 326 Monthlyの場合のシート名 327 - start_date : str 328 開始日 329 - end_date : str 330 終了日 331 332 Returns: 333 ------ 334 pd.DataFrame 335 処理済みのデータフレーム 336 """ 337 if source_type == "csv": 338 # 既存のCSV処理ロジック 339 return self._combine_all_csv(data_source) 340 elif source_type == "monthly": 341 # MonthlyConverterからのデータを処理 342 if not isinstance(data_source, pd.DataFrame): 343 raise ValueError("monthly形式の場合、DataFrameを直接渡す必要があります") 344 345 df = data_source.copy() 346 347 # required_columnsからDateを除外して欠損値チェックを行う 348 check_columns = [col for col in self._required_columns if col != "Date"] 349 350 # インデックスがdatetimeであることを確認 351 if not isinstance(df.index, pd.DatetimeIndex) and "Date" not in df.columns: 352 raise ValueError("DatetimeIndexまたはDateカラムが必要です") 353 354 if "Date" in df.columns: 355 df.set_index("Date", inplace=True) 356 357 # 必要なカラムの存在確認 358 missing_columns = [ 359 col for col in check_columns if col not in df.columns.tolist() 360 ] 361 if missing_columns: 362 missing_cols = "','".join(missing_columns) 363 current_cols = "','".join(df.columns.tolist()) 364 raise ValueError( 365 f"必要なカラムが不足しています: '{missing_cols}'\n" 366 f"現在のカラム: '{current_cols}'" 367 ) 368 369 # 平日/休日の判定用カラムを追加 370 df[self._col_weekday] = df.index.map(FluxFootprintAnalyzer.is_weekday) 371 372 # Dateを除外したカラムで欠損値の処理 373 df = df.dropna(subset=check_columns) 374 375 # インデックスの重複を除去 376 df = df.loc[~df.index.duplicated(), :] 377 378 return df 379 else: 380 raise ValueError("source_typeは'csv'または'monthly'である必要があります")
CSVファイルまたはMonthlyConverterからのデータを統合します
Parameters:
data_source : str | pd.DataFrame
CSVディレクトリパスまたはDataFrame
source_type : str
"csv" または "monthly"
**kwargs :
追加パラメータ
- sheet_names : list[str]
Monthlyの場合のシート名
- start_date : str
開始日
- end_date : str
終了日
Returns:
pd.DataFrame
処理済みのデータフレーム
382 def get_satellite_image_from_api( 383 self, 384 api_key: str, 385 center_lat: float, 386 center_lon: float, 387 output_path: str, 388 scale: int = 1, 389 size: tuple[int, int] = (2160, 2160), 390 zoom: int = 13, 391 ) -> ImageFile: 392 """ 393 Google Maps Static APIを使用して衛星画像を取得します。 394 395 Parameters: 396 ------ 397 api_key : str 398 Google Maps Static APIのキー。 399 center_lat : float 400 中心の緯度。 401 center_lon : float 402 中心の経度。 403 output_path : str 404 画像の保存先パス。拡張子は'.png'のみ許可される。 405 scale : int, optional 406 画像の解像度スケール(1か2)。デフォルトは1。 407 size : tuple[int, int], optional 408 画像サイズ (幅, 高さ)。デフォルトは(2160, 2160)。 409 zoom : int, optional 410 ズームレベル(0-21)。デフォルトは13。 411 412 Returns: 413 ------ 414 ImageFile 415 取得した衛星画像 416 417 Raises: 418 ------ 419 requests.RequestException 420 API呼び出しに失敗した場合 421 """ 422 # バリデーション 423 if not output_path.endswith(".png"): 424 raise ValueError("出力ファイル名は'.png'で終わる必要があります。") 425 426 # HTTPリクエストの定義 427 base_url = "https://maps.googleapis.com/maps/api/staticmap" 428 params = { 429 "center": f"{center_lat},{center_lon}", 430 "zoom": zoom, 431 "size": f"{size[0]}x{size[1]}", 432 "maptype": "satellite", 433 "scale": scale, 434 "key": api_key, 435 } 436 437 try: 438 response = requests.get(base_url, params=params) 439 response.raise_for_status() 440 # 画像ファイルに変換 441 image = Image.open(io.BytesIO(response.content)) 442 image.save(output_path) 443 self._got_satellite_image = True 444 self.logger.info(f"リモート画像を取得し、保存しました: {output_path}") 445 return image 446 except requests.RequestException as e: 447 self.logger.error(f"衛星画像の取得に失敗しました: {str(e)}") 448 raise
Google Maps Static APIを使用して衛星画像を取得します。
Parameters:
api_key : str
Google Maps Static APIのキー。
center_lat : float
中心の緯度。
center_lon : float
中心の経度。
output_path : str
画像の保存先パス。拡張子は'.png'のみ許可される。
scale : int, optional
画像の解像度スケール(1か2)。デフォルトは1。
size : tuple[int, int], optional
画像サイズ (幅, 高さ)。デフォルトは(2160, 2160)。
zoom : int, optional
ズームレベル(0-21)。デフォルトは13。
Returns:
ImageFile
取得した衛星画像
Raises:
requests.RequestException
API呼び出しに失敗した場合
450 def get_satellite_image_from_local( 451 self, 452 local_image_path: str, 453 ) -> ImageFile: 454 """ 455 ローカルファイルから衛星画像を読み込みます。 456 457 Parameters: 458 ------ 459 local_image_path : str 460 ローカル画像のパス 461 462 Returns: 463 ------ 464 ImageFile 465 読み込んだ衛星画像 466 467 Raises: 468 ------ 469 FileNotFoundError 470 指定されたパスにファイルが存在しない場合 471 """ 472 if not os.path.exists(local_image_path): 473 raise FileNotFoundError( 474 f"指定されたローカル画像が存在しません: {local_image_path}" 475 ) 476 image = Image.open(local_image_path) 477 self._got_satellite_image = True 478 self.logger.info(f"ローカル画像を使用しました: {local_image_path}") 479 return image
ローカルファイルから衛星画像を読み込みます。
Parameters:
local_image_path : str
ローカル画像のパス
Returns:
ImageFile
読み込んだ衛星画像
Raises:
FileNotFoundError
指定されたパスにファイルが存在しない場合
481 def plot_flux_footprint( 482 self, 483 x_list: list[float], 484 y_list: list[float], 485 c_list: list[float] | None, 486 center_lat: float, 487 center_lon: float, 488 vmin: float, 489 vmax: float, 490 add_cbar: bool = True, 491 add_legend: bool = True, 492 cbar_label: str | None = None, 493 cbar_labelpad: int = 20, 494 cmap: str = "jet", 495 reduce_c_function: callable = np.mean, 496 lat_correction: float = 1, 497 lon_correction: float = 1, 498 output_dir: str | None = None, 499 output_filename: str = "footprint.png", 500 save_fig: bool = True, 501 show_fig: bool = True, 502 satellite_image: ImageFile | None = None, 503 xy_max: float = 5000, 504 ) -> None: 505 """ 506 フットプリントデータをプロットします。 507 508 このメソッドは、指定されたフットプリントデータのみを可視化します。 509 510 Parameters: 511 ------ 512 x_list : list[float] 513 フットプリントのx座標リスト(メートル単位)。 514 y_list : list[float] 515 フットプリントのy座標リスト(メートル単位)。 516 c_list : list[float] | None 517 フットプリントの強度を示す値のリスト。 518 center_lat : float 519 プロットの中心となる緯度。 520 center_lon : float 521 プロットの中心となる経度。 522 cmap : str 523 使用するカラーマップの名前。 524 vmin : float 525 カラーバーの最小値。 526 vmax : float 527 カラーバーの最大値。 528 reduce_c_function : callable, optional 529 フットプリントの集約関数(デフォルトはnp.mean)。 530 cbar_label : str | None, optional 531 カラーバーのラベル。 532 cbar_labelpad : int, optional 533 カラーバーラベルのパディング。 534 lon_correction : float, optional 535 経度方向の補正係数(デフォルトは1)。 536 lat_correction : float, optional 537 緯度方向の補正係数(デフォルトは1)。 538 output_dir : str | None, optional 539 プロット画像の保存先パス。 540 output_filename : str 541 プロット画像の保存ファイル名(拡張子を含む)。デフォルトは'footprint.png'。 542 save_fig : bool 543 図の保存を許可するフラグ。デフォルトはTrue。 544 show_fig : bool 545 図の表示を許可するフラグ。デフォルトはTrue。 546 satellite_image : ImageFile | None, optional 547 使用する衛星画像。指定がない場合はデフォルトの画像が生成されます。 548 xy_max : float, optional 549 表示範囲の最大値(デフォルトは4000)。 550 """ 551 self.plot_flux_footprint_with_hotspots( 552 x_list=x_list, 553 y_list=y_list, 554 c_list=c_list, 555 center_lat=center_lat, 556 center_lon=center_lon, 557 vmin=vmin, 558 vmax=vmax, 559 add_cbar=add_cbar, 560 add_legend=add_legend, 561 cbar_label=cbar_label, 562 cbar_labelpad=cbar_labelpad, 563 cmap=cmap, 564 reduce_c_function=reduce_c_function, 565 hotspots=None, # hotspotsをNoneに設定 566 hotspot_colors=None, 567 lat_correction=lat_correction, 568 lon_correction=lon_correction, 569 output_dir=output_dir, 570 output_filename=output_filename, 571 save_fig=save_fig, 572 show_fig=show_fig, 573 satellite_image=satellite_image, 574 xy_max=xy_max, 575 )
フットプリントデータをプロットします。
このメソッドは、指定されたフットプリントデータのみを可視化します。
Parameters:
x_list : list[float]
フットプリントのx座標リスト(メートル単位)。
y_list : list[float]
フットプリントのy座標リスト(メートル単位)。
c_list : list[float] | None
フットプリントの強度を示す値のリスト。
center_lat : float
プロットの中心となる緯度。
center_lon : float
プロットの中心となる経度。
cmap : str
使用するカラーマップの名前。
vmin : float
カラーバーの最小値。
vmax : float
カラーバーの最大値。
reduce_c_function : callable, optional
フットプリントの集約関数(デフォルトはnp.mean)。
cbar_label : str | None, optional
カラーバーのラベル。
cbar_labelpad : int, optional
カラーバーラベルのパディング。
lon_correction : float, optional
経度方向の補正係数(デフォルトは1)。
lat_correction : float, optional
緯度方向の補正係数(デフォルトは1)。
output_dir : str | None, optional
プロット画像の保存先パス。
output_filename : str
プロット画像の保存ファイル名(拡張子を含む)。デフォルトは'footprint.png'。
save_fig : bool
図の保存を許可するフラグ。デフォルトはTrue。
show_fig : bool
図の表示を許可するフラグ。デフォルトはTrue。
satellite_image : ImageFile | None, optional
使用する衛星画像。指定がない場合はデフォルトの画像が生成されます。
xy_max : float, optional
表示範囲の最大値(デフォルトは4000)。
577 def plot_flux_footprint_with_hotspots( 578 self, 579 x_list: list[float], 580 y_list: list[float], 581 c_list: list[float] | None, 582 center_lat: float, 583 center_lon: float, 584 vmin: float, 585 vmax: float, 586 add_cbar: bool = True, 587 add_legend: bool = True, 588 cbar_label: str | None = None, 589 cbar_labelpad: int = 20, 590 cmap: str = "jet", 591 reduce_c_function: callable = np.mean, 592 hotspots: list[HotspotData] | None = None, 593 hotspot_colors: dict[HotspotType, str] | None = None, 594 hotspot_markers: dict[HotspotType, str] | None = None, 595 lat_correction: float = 1, 596 lon_correction: float = 1, 597 output_dir: str | None = None, 598 output_filename: str = "footprint.png", 599 save_fig: bool = True, 600 show_fig: bool = True, 601 satellite_image: ImageFile | None = None, 602 xy_max: float = 5000, 603 ) -> None: 604 """ 605 Staticな衛星画像上にフットプリントデータとホットスポットをプロットします。 606 607 このメソッドは、指定されたフットプリントデータとホットスポットを可視化します。 608 ホットスポットが指定されない場合は、フットプリントのみ作図します。 609 610 Parameters: 611 ------ 612 x_list : list[float] 613 フットプリントのx座標リスト(メートル単位)。 614 y_list : list[float] 615 フットプリントのy座標リスト(メートル単位)。 616 c_list : list[float] | None 617 フットプリントの強度を示す値のリスト。 618 center_lat : float 619 プロットの中心となる緯度。 620 center_lon : float 621 プロットの中心となる経度。 622 vmin : float 623 カラーバーの最小値。 624 vmax : float 625 カラーバーの最大値。 626 add_cbar : bool, optional 627 カラーバーを追加するかどうか(デフォルトはTrue)。 628 add_legend : bool, optional 629 凡例を追加するかどうか(デフォルトはTrue)。 630 cbar_label : str | None, optional 631 カラーバーのラベル。 632 cbar_labelpad : int, optional 633 カラーバーラベルのパディング。 634 cmap : str 635 使用するカラーマップの名前。 636 reduce_c_function : callable 637 フットプリントの集約関数(デフォルトはnp.mean)。 638 hotspots : list[HotspotData] | None, optional 639 ホットスポットデータのリスト。デフォルトはNone。 640 hotspot_colors : dict[HotspotType, str] | None, optional 641 ホットスポットの色を指定する辞書。 642 hotspot_markers : dict[HotspotType, str] | None, optional 643 ホットスポットの形状を指定する辞書。 644 指定の例は {'bio': '^', 'gas': 'o', 'comb': 's'} (三角、丸、四角)など。 645 lat_correction : float, optional 646 緯度方向の補正係数(デフォルトは1)。 647 lon_correction : float, optional 648 経度方向の補正係数(デフォルトは1)。 649 output_dir : str | None, optional 650 プロット画像の保存先パス。 651 output_filename : str 652 プロット画像の保存ファイル名(拡張子を含む)。デフォルトは'footprint.png'。 653 save_fig : bool 654 図の保存を許可するフラグ。デフォルトはTrue。 655 show_fig : bool 656 図の表示を許可するフラグ。デフォルトはTrue。 657 satellite_image : ImageFile | None, optional 658 使用する衛星画像。指定がない場合はデフォルトの画像が生成されます。 659 xy_max : float, optional 660 表示範囲の最大値(デフォルトは5000)。 661 """ 662 # 1. 引数のバリデーション 663 valid_extensions: list[str] = [".png", ".jpg", ".jpeg", ".pdf", ".svg"] 664 _, file_extension = os.path.splitext(output_filename) 665 if file_extension.lower() not in valid_extensions: 666 quoted_extensions: list[str] = [f'"{ext}"' for ext in valid_extensions] 667 self.logger.error( 668 f"`output_filename`は有効な拡張子ではありません。プロットを保存するには、次のいずれかを指定してください: {','.join(quoted_extensions)}" 669 ) 670 return 671 672 # 2. フラグチェック 673 if not self._got_satellite_image: 674 raise ValueError( 675 "`get_satellite_image_from_api`または`get_satellite_image_from_local`が実行されていません。" 676 ) 677 678 # 3. 衛星画像の取得 679 if satellite_image is None: 680 satellite_image = Image.new("RGB", (2160, 2160), "lightgray") 681 682 self.logger.info("プロットを作成中...") 683 684 # 4. 座標変換のための定数計算(1回だけ) 685 meters_per_lat: float = self.EARTH_RADIUS_METER * ( 686 math.pi / 180 687 ) # 緯度1度あたりのメートル 688 meters_per_lon: float = meters_per_lat * math.cos( 689 math.radians(center_lat) 690 ) # 経度1度あたりのメートル 691 692 # 5. フットプリントデータの座標変換(まとめて1回で実行) 693 x_deg = ( 694 np.array(x_list) / meters_per_lon * lon_correction 695 ) # 補正係数も同時に適用 696 y_deg = ( 697 np.array(y_list) / meters_per_lat * lat_correction 698 ) # 補正係数も同時に適用 699 700 # 6. 中心点からの相対座標を実際の緯度経度に変換 701 lons = center_lon + x_deg 702 lats = center_lat + y_deg 703 704 # 7. 表示範囲の計算(変更なし) 705 x_range: float = xy_max / meters_per_lon 706 y_range: float = xy_max / meters_per_lat 707 map_boundaries: tuple[float, float, float, float] = ( 708 center_lon - x_range, # left_lon 709 center_lon + x_range, # right_lon 710 center_lat - y_range, # bottom_lat 711 center_lat + y_range, # top_lat 712 ) 713 left_lon, right_lon, bottom_lat, top_lat = map_boundaries 714 715 # 8. プロットの作成 716 plt.rcParams["axes.edgecolor"] = "None" 717 fig: plt.Figure = plt.figure(figsize=(10, 8), dpi=300) 718 ax_data: plt.Axes = fig.add_axes([0.05, 0.1, 0.8, 0.8]) 719 720 # 9. フットプリントの描画 721 # フットプリントの描画とカラーバー用の2つのhexbinを作成 722 if c_list is not None: 723 ax_data.hexbin( 724 lons, 725 lats, 726 C=c_list, 727 cmap=cmap, 728 vmin=vmin, 729 vmax=vmax, 730 alpha=0.3, # 実際のプロット用 731 gridsize=100, 732 linewidths=0, 733 mincnt=100, 734 extent=[left_lon, right_lon, bottom_lat, top_lat], 735 reduce_C_function=reduce_c_function, 736 ) 737 738 # カラーバー用の非表示hexbin(alpha=1.0) 739 hidden_hexbin = ax_data.hexbin( 740 lons, 741 lats, 742 C=c_list, 743 cmap=cmap, 744 vmin=vmin, 745 vmax=vmax, 746 alpha=1.0, # カラーバー用 747 gridsize=100, 748 linewidths=0, 749 mincnt=100, 750 extent=[left_lon, right_lon, bottom_lat, top_lat], 751 reduce_C_function=reduce_c_function, 752 visible=False, # プロットには表示しない 753 ) 754 755 # 10. ホットスポットの描画 756 spot_handles = [] 757 if hotspots is not None: 758 default_colors: dict[HotspotType, str] = { 759 "bio": "blue", 760 "gas": "red", 761 "comb": "green", 762 } 763 764 # デフォルトのマーカー形状を定義 765 default_markers: dict[HotspotType, str] = { 766 "bio": "o", 767 "gas": "o", 768 "comb": "o", 769 } 770 771 # 座標変換のための定数 772 meters_per_lat: float = self.EARTH_RADIUS_METER * (math.pi / 180) 773 meters_per_lon: float = meters_per_lat * math.cos(math.radians(center_lat)) 774 775 for spot_type, color in (hotspot_colors or default_colors).items(): 776 spots_lon = [] 777 spots_lat = [] 778 779 # 使用するマーカーを決定 780 marker = (hotspot_markers or default_markers).get(spot_type, "o") 781 782 for spot in hotspots: 783 if spot.type == spot_type: 784 # 変換前の緯度経度をログ出力 785 self.logger.debug( 786 f"Before - Type: {spot_type}, Lat: {spot.avg_lat:.6f}, Lon: {spot.avg_lon:.6f}" 787 ) 788 789 # 中心からの相対距離を計算 790 dx: float = (spot.avg_lon - center_lon) * meters_per_lon 791 dy: float = (spot.avg_lat - center_lat) * meters_per_lat 792 793 # 補正前の相対座標をログ出力 794 self.logger.debug( 795 f"Relative - Type: {spot_type}, X: {dx:.2f}m, Y: {dy:.2f}m" 796 ) 797 798 # 補正を適用 799 corrected_dx: float = dx * lon_correction 800 corrected_dy: float = dy * lat_correction 801 802 # 補正後の緯度経度を計算 803 adjusted_lon: float = center_lon + corrected_dx / meters_per_lon 804 adjusted_lat: float = center_lat + corrected_dy / meters_per_lat 805 806 # 変換後の緯度経度をログ出力 807 self.logger.debug( 808 f"After - Type: {spot_type}, Lat: {adjusted_lat:.6f}, Lon: {adjusted_lon:.6f}\n" 809 ) 810 811 if ( 812 left_lon <= adjusted_lon <= right_lon 813 and bottom_lat <= adjusted_lat <= top_lat 814 ): 815 spots_lon.append(adjusted_lon) 816 spots_lat.append(adjusted_lat) 817 818 if spots_lon: 819 handle = ax_data.scatter( 820 spots_lon, 821 spots_lat, 822 c=color, 823 marker=marker, # マーカー形状を指定 824 s=100, 825 alpha=0.7, 826 label=spot_type, # "bio","gas","comb" 827 edgecolor="black", 828 linewidth=1, 829 ) 830 spot_handles.append(handle) 831 832 # 11. 背景画像の設定 833 ax_img = ax_data.twiny().twinx() 834 ax_img.imshow( 835 satellite_image, 836 extent=[left_lon, right_lon, bottom_lat, top_lat], 837 aspect="equal", 838 ) 839 840 # 12. 軸の設定 841 for ax in [ax_data, ax_img]: 842 ax.set_xlim(left_lon, right_lon) 843 ax.set_ylim(bottom_lat, top_lat) 844 ax.set_xticks([]) 845 ax.set_yticks([]) 846 847 ax_data.set_zorder(2) 848 ax_data.patch.set_alpha(0) 849 ax_img.set_zorder(1) 850 851 # 13. カラーバーの追加 852 if add_cbar: 853 cbar_ax: plt.Axes = fig.add_axes([0.88, 0.1, 0.03, 0.8]) 854 cbar = fig.colorbar(hidden_hexbin, cax=cbar_ax) # hidden_hexbinを使用 855 # cbar_labelが指定されている場合のみラベルを設定 856 if cbar_label: 857 cbar.set_label(cbar_label, rotation=270, labelpad=cbar_labelpad) 858 859 # 14. ホットスポットの凡例追加 860 if add_legend and hotspots and spot_handles: 861 ax_data.legend( 862 handles=spot_handles, 863 loc="upper center", # 位置を上部中央に 864 bbox_to_anchor=(0.55, -0.01), # 図の下に配置 865 ncol=len(spot_handles), # ハンドルの数に応じて列数を設定 866 ) 867 868 # 15. 画像の保存 869 if save_fig: 870 if output_dir is None: 871 raise ValueError( 872 "save_fig=Trueの場合、output_dirを指定する必要があります。有効なディレクトリパスを指定してください。" 873 ) 874 output_path: str = os.path.join(output_dir, output_filename) 875 self.logger.info("プロットを保存中...") 876 try: 877 fig.savefig(output_path, bbox_inches="tight") 878 self.logger.info(f"プロットが正常に保存されました: {output_path}") 879 except Exception as e: 880 self.logger.error(f"プロットの保存中にエラーが発生しました: {str(e)}") 881 # 16. 画像の表示 882 if show_fig: 883 plt.show() 884 else: 885 plt.close(fig=fig)
Staticな衛星画像上にフットプリントデータとホットスポットをプロットします。
このメソッドは、指定されたフットプリントデータとホットスポットを可視化します。 ホットスポットが指定されない場合は、フットプリントのみ作図します。
Parameters:
x_list : list[float]
フットプリントのx座標リスト(メートル単位)。
y_list : list[float]
フットプリントのy座標リスト(メートル単位)。
c_list : list[float] | None
フットプリントの強度を示す値のリスト。
center_lat : float
プロットの中心となる緯度。
center_lon : float
プロットの中心となる経度。
vmin : float
カラーバーの最小値。
vmax : float
カラーバーの最大値。
add_cbar : bool, optional
カラーバーを追加するかどうか(デフォルトはTrue)。
add_legend : bool, optional
凡例を追加するかどうか(デフォルトはTrue)。
cbar_label : str | None, optional
カラーバーのラベル。
cbar_labelpad : int, optional
カラーバーラベルのパディング。
cmap : str
使用するカラーマップの名前。
reduce_c_function : callable
フットプリントの集約関数(デフォルトはnp.mean)。
hotspots : list[HotspotData] | None, optional
ホットスポットデータのリスト。デフォルトはNone。
hotspot_colors : dict[HotspotType, str] | None, optional
ホットスポットの色を指定する辞書。
hotspot_markers : dict[HotspotType, str] | None, optional
ホットスポットの形状を指定する辞書。
指定の例は {'bio': '^', 'gas': 'o', 'comb': 's'} (三角、丸、四角)など。
lat_correction : float, optional
緯度方向の補正係数(デフォルトは1)。
lon_correction : float, optional
経度方向の補正係数(デフォルトは1)。
output_dir : str | None, optional
プロット画像の保存先パス。
output_filename : str
プロット画像の保存ファイル名(拡張子を含む)。デフォルトは'footprint.png'。
save_fig : bool
図の保存を許可するフラグ。デフォルトはTrue。
show_fig : bool
図の表示を許可するフラグ。デフォルトはTrue。
satellite_image : ImageFile | None, optional
使用する衛星画像。指定がない場合はデフォルトの画像が生成されます。
xy_max : float, optional
表示範囲の最大値(デフォルトは5000)。
887 def plot_flux_footprint_with_scale_checker( 888 self, 889 x_list: list[float], 890 y_list: list[float], 891 c_list: list[float] | None, 892 center_lat: float, 893 center_lon: float, 894 check_points: list[tuple[float, float, str]] | None = None, 895 vmin: float = 0, 896 vmax: float = 100, 897 add_cbar: bool = True, 898 cbar_label: str | None = None, 899 cbar_labelpad: int = 20, 900 cmap: str = "jet", 901 reduce_c_function: callable = np.mean, 902 lat_correction: float = 1, 903 lon_correction: float = 1, 904 output_dir: str | None = None, 905 output_filename: str = "footprint-scale_checker.png", 906 save_fig: bool = True, 907 show_fig: bool = True, 908 satellite_image: ImageFile | None = None, 909 xy_max: float = 5000, 910 ) -> None: 911 """ 912 Staticな衛星画像上にフットプリントデータとホットスポットをプロットします。 913 914 このメソッドは、指定されたフットプリントデータとホットスポットを可視化します。 915 ホットスポットが指定されない場合は、フットプリントのみ作図します。 916 917 Parameters: 918 ------ 919 x_list : list[float] 920 フットプリントのx座標リスト(メートル単位)。 921 y_list : list[float] 922 フットプリントのy座標リスト(メートル単位)。 923 c_list : list[float] | None 924 フットプリントの強度を示す値のリスト。 925 center_lat : float 926 プロットの中心となる緯度。 927 center_lon : float 928 プロットの中心となる経度。 929 check_points : list[tuple[float, float, str]] | None 930 確認用の地点リスト。各要素は (緯度, 経度, ラベル) のタプル。 931 Noneの場合は中心から500m、1000m、2000m、3000mの位置に仮想的な点を配置。 932 cmap : str 933 使用するカラーマップの名前。 934 vmin : float 935 カラーバーの最小値。 936 vmax : float 937 カラーバーの最大値。 938 reduce_c_function : callable, optional 939 フットプリントの集約関数(デフォルトはnp.mean)。 940 cbar_label : str, optional 941 カラーバーのラベル。 942 cbar_labelpad : int, optional 943 カラーバーラベルのパディング。 944 hotspots : list[HotspotData] | None 945 ホットスポットデータのリスト。デフォルトはNone。 946 hotspot_colors : dict[str, str] | None, optional 947 ホットスポットの色を指定する辞書。 948 lon_correction : float, optional 949 経度方向の補正係数(デフォルトは1)。 950 lat_correction : float, optional 951 緯度方向の補正係数(デフォルトは1)。 952 output_dir : str | None, optional 953 プロット画像の保存先パス。 954 output_filename : str 955 プロット画像の保存ファイル名(拡張子を含む)。デフォルトは'footprint.png'。 956 save_fig : bool 957 図の保存を許可するフラグ。デフォルトはTrue。 958 show_fig : bool 959 図の表示を許可するフラグ。デフォルトはTrue。 960 satellite_image : ImageFile | None, optional 961 使用する衛星画像。指定がない場合はデフォルトの画像が生成されます。 962 xy_max : float, optional 963 表示範囲の最大値(デフォルトは5000)。 964 """ 965 if check_points is None: 966 # デフォルトの確認ポイントを生成(従来の方式) 967 default_points = [ 968 (500, "North", 90), # 北 500m 969 (1000, "East", 0), # 東 1000m 970 (2000, "South", 270), # 南 2000m 971 (3000, "West", 180), # 西 3000m 972 ] 973 974 dummy_hotspots = [] 975 for distance, direction, angle in default_points: 976 rad = math.radians(angle) 977 meters_per_lat = self.EARTH_RADIUS_METER * (math.pi / 180) 978 meters_per_lon = meters_per_lat * math.cos(math.radians(center_lat)) 979 980 dx = distance * math.cos(rad) 981 dy = distance * math.sin(rad) 982 983 delta_lon = dx / meters_per_lon 984 delta_lat = dy / meters_per_lat 985 986 hotspot = HotspotData( 987 avg_lat=center_lat + delta_lat, 988 avg_lon=center_lon + delta_lon, 989 delta_ch4=0.0, 990 delta_c2h6=0.0, 991 ratio=0.0, 992 type=f"{direction}_{distance}m", 993 section=0, 994 source="scale_check", 995 angle=0, 996 correlation=0, 997 ) 998 dummy_hotspots.append(hotspot) 999 else: 1000 # 指定された緯度経度を使用 1001 dummy_hotspots = [] 1002 for lat, lon, label in check_points: 1003 hotspot = HotspotData( 1004 avg_lat=lat, 1005 avg_lon=lon, 1006 delta_ch4=0.0, 1007 delta_c2h6=0.0, 1008 ratio=0.0, 1009 type=label, 1010 section=0, 1011 source="scale_check", 1012 angle=0, 1013 correlation=0, 1014 ) 1015 dummy_hotspots.append(hotspot) 1016 1017 # カスタムカラーマップの作成 1018 hotspot_colors = { 1019 spot.type: plt.cm.tab10(i % 10) for i, spot in enumerate(dummy_hotspots) 1020 } 1021 1022 # 既存のメソッドを呼び出してプロット 1023 self.plot_flux_footprint_with_hotspots( 1024 x_list=x_list, 1025 y_list=y_list, 1026 c_list=c_list, 1027 center_lat=center_lat, 1028 center_lon=center_lon, 1029 vmin=vmin, 1030 vmax=vmax, 1031 add_cbar=add_cbar, 1032 add_legend=True, 1033 cbar_label=cbar_label, 1034 cbar_labelpad=cbar_labelpad, 1035 cmap=cmap, 1036 reduce_c_function=reduce_c_function, 1037 hotspots=dummy_hotspots, 1038 hotspot_colors=hotspot_colors, 1039 lat_correction=lat_correction, 1040 lon_correction=lon_correction, 1041 output_dir=output_dir, 1042 output_filename=output_filename, 1043 save_fig=save_fig, 1044 show_fig=show_fig, 1045 satellite_image=satellite_image, 1046 xy_max=xy_max, 1047 )
Staticな衛星画像上にフットプリントデータとホットスポットをプロットします。
このメソッドは、指定されたフットプリントデータとホットスポットを可視化します。 ホットスポットが指定されない場合は、フットプリントのみ作図します。
Parameters:
x_list : list[float]
フットプリントのx座標リスト(メートル単位)。
y_list : list[float]
フットプリントのy座標リスト(メートル単位)。
c_list : list[float] | None
フットプリントの強度を示す値のリスト。
center_lat : float
プロットの中心となる緯度。
center_lon : float
プロットの中心となる経度。
check_points : list[tuple[float, float, str]] | None
確認用の地点リスト。各要素は (緯度, 経度, ラベル) のタプル。
Noneの場合は中心から500m、1000m、2000m、3000mの位置に仮想的な点を配置。
cmap : str
使用するカラーマップの名前。
vmin : float
カラーバーの最小値。
vmax : float
カラーバーの最大値。
reduce_c_function : callable, optional
フットプリントの集約関数(デフォルトはnp.mean)。
cbar_label : str, optional
カラーバーのラベル。
cbar_labelpad : int, optional
カラーバーラベルのパディング。
hotspots : list[HotspotData] | None
ホットスポットデータのリスト。デフォルトはNone。
hotspot_colors : dict[str, str] | None, optional
ホットスポットの色を指定する辞書。
lon_correction : float, optional
経度方向の補正係数(デフォルトは1)。
lat_correction : float, optional
緯度方向の補正係数(デフォルトは1)。
output_dir : str | None, optional
プロット画像の保存先パス。
output_filename : str
プロット画像の保存ファイル名(拡張子を含む)。デフォルトは'footprint.png'。
save_fig : bool
図の保存を許可するフラグ。デフォルトはTrue。
show_fig : bool
図の表示を許可するフラグ。デフォルトはTrue。
satellite_image : ImageFile | None, optional
使用する衛星画像。指定がない場合はデフォルトの画像が生成されます。
xy_max : float, optional
表示範囲の最大値(デフォルトは5000)。
1267 @staticmethod 1268 def filter_data( 1269 df: pd.DataFrame, 1270 start_date: str | None = None, 1271 end_date: str | None = None, 1272 months: list[int] | None = None, 1273 ) -> pd.DataFrame: 1274 """ 1275 指定された期間や月でデータをフィルタリングするメソッド。 1276 1277 Parameters: 1278 ------ 1279 df : pd.DataFrame 1280 フィルタリングするデータフレーム 1281 start_date : str | None 1282 フィルタリングの開始日('YYYY-MM-DD'形式)。デフォルトはNone。 1283 end_date : str | None 1284 フィルタリングの終了日('YYYY-MM-DD'形式)。デフォルトはNone。 1285 months : list[int] | None 1286 フィルタリングする月のリスト(例:[1, 2, 12])。デフォルトはNone。 1287 1288 Returns: 1289 ------ 1290 pd.DataFrame 1291 フィルタリングされたデータフレーム 1292 1293 Raises: 1294 ------ 1295 ValueError 1296 インデックスがDatetimeIndexでない場合、または日付の形式が不正な場合 1297 """ 1298 # インデックスの検証 1299 if not isinstance(df.index, pd.DatetimeIndex): 1300 raise ValueError( 1301 "DataFrameのインデックスはDatetimeIndexである必要があります" 1302 ) 1303 1304 filtered_df: pd.DataFrame = df.copy() 1305 1306 # 日付形式の検証と変換 1307 try: 1308 if start_date is not None: 1309 start_date = pd.to_datetime(start_date) 1310 if end_date is not None: 1311 end_date = pd.to_datetime(end_date) 1312 except ValueError as e: 1313 raise ValueError( 1314 "日付の形式が不正です。'YYYY-MM-DD'形式で指定してください" 1315 ) from e 1316 1317 # 期間でフィルタリング 1318 if start_date is not None or end_date is not None: 1319 filtered_df = filtered_df.loc[start_date:end_date] 1320 1321 # 月のバリデーション 1322 if months is not None: 1323 if not all(isinstance(m, int) and 1 <= m <= 12 for m in months): 1324 raise ValueError( 1325 "monthsは1から12までの整数のリストである必要があります" 1326 ) 1327 filtered_df = filtered_df[filtered_df.index.month.isin(months)] 1328 1329 # フィルタリング後のデータが空でないことを確認 1330 if filtered_df.empty: 1331 raise ValueError("フィルタリング後のデータが空になりました") 1332 1333 return filtered_df
指定された期間や月でデータをフィルタリングするメソッド。
Parameters:
df : pd.DataFrame
フィルタリングするデータフレーム
start_date : str | None
フィルタリングの開始日('YYYY-MM-DD'形式)。デフォルトはNone。
end_date : str | None
フィルタリングの終了日('YYYY-MM-DD'形式)。デフォルトはNone。
months : list[int] | None
フィルタリングする月のリスト(例:[1, 2, 12])。デフォルトはNone。
Returns:
pd.DataFrame
フィルタリングされたデータフレーム
Raises:
ValueError
インデックスがDatetimeIndexでない場合、または日付の形式が不正な場合
1335 @staticmethod 1336 def is_weekday(date: datetime) -> int: 1337 """ 1338 指定された日付が平日であるかどうかを判定します。 1339 1340 Parameters: 1341 ------ 1342 date : datetime 1343 判定する日付。 1344 1345 Returns: 1346 ------ 1347 int 1348 平日であれば1、そうでなければ0。 1349 """ 1350 return 1 if not jpholiday.is_holiday(date) and date.weekday() < 5 else 0
指定された日付が平日であるかどうかを判定します。
Parameters:
date : datetime
判定する日付。
Returns:
int
平日であれば1、そうでなければ0。
11class CorrectingUtils: 12 @staticmethod 13 def correct_df_by_type(df: pd.DataFrame, correction_type: str) -> pd.DataFrame: 14 """ 15 指定された補正式に基づいてデータフレームを補正します。 16 17 Parameters: 18 ------ 19 df : pd.DataFrame 20 補正対象のデータフレーム。 21 correction_type : str 22 適用する補正式の種類。CORRECTION_TYPES_PATTERNから選択する。 23 24 Returns: 25 ------ 26 pd.DataFrame 27 補正後のデータフレーム。 28 29 Raises: 30 ------ 31 ValueError 32 無効な補正式が指定された場合。 33 """ 34 if correction_type == "pico_1": 35 coef_a: float = 2.0631 # 切片 36 coef_b: float = 1.0111e-06 # 1次の係数 37 coef_c: float = -1.8683e-10 # 2次の係数 38 # 水蒸気補正 39 df_corrected: pd.DataFrame = CorrectingUtils._correct_h2o_interference( 40 df=df, 41 coef_a=coef_a, 42 coef_b=coef_b, 43 coef_c=coef_c, 44 col_ch4="ch4_ppm", 45 col_h2o="h2o_ppm", 46 h2o_threshold=2000, 47 ) 48 # 負の値のエタン濃度の補正など 49 df_corrected = CorrectingUtils._remove_bias( 50 df=df_corrected, col_ch4_ppm="ch4_ppm", col_c2h6_ppb="c2h6_ppb" 51 ) 52 return df_corrected 53 else: 54 raise ValueError(f"invalid correction_type: {correction_type}.") 55 56 @staticmethod 57 def _correct_h2o_interference( 58 df: pd.DataFrame, 59 coef_a: float, 60 coef_b: float, 61 coef_c: float, 62 col_ch4: str = "ch4_ppm", 63 col_h2o: str = "h2o_ppm", 64 h2o_threshold: float | None = 2000, 65 ) -> pd.DataFrame: 66 """ 67 水蒸気干渉を補正するためのメソッドです。 68 CH4濃度に対する水蒸気の影響を2次関数を用いて補正します。 69 70 References: 71 ------ 72 - Commane et al. (2023): Intercomparison of commercial analyzers for atmospheric ethane and methane observations 73 https://amt.copernicus.org/articles/16/1431/2023/, 74 https://amt.copernicus.org/articles/16/1431/2023/amt-16-1431-2023.pdf 75 76 Parameters: 77 ------ 78 df : pd.DataFrame 79 補正対象のデータフレーム 80 coef_a : float 81 補正曲線の切片 82 coef_b : float 83 補正曲線の1次係数 84 coef_c : float 85 補正曲線の2次係数 86 col_ch4 : str 87 CH4濃度を示すカラム名 88 col_h2o : str 89 水蒸気濃度を示すカラム名 90 h2o_threshold : float | None 91 水蒸気濃度の下限値(この値未満のデータは除外) 92 93 Returns: 94 ------ 95 pd.DataFrame 96 水蒸気干渉が補正されたデータフレーム 97 """ 98 # 元のデータを保護するためコピーを作成 99 df = df.copy() 100 # 水蒸気濃度の配列を取得 101 h2o = np.array(df[col_h2o]) 102 103 # 補正項の計算 104 correction_curve = coef_a + coef_b * h2o + coef_c * pow(h2o, 2) 105 max_correction = np.max(correction_curve) 106 correction_term = -(correction_curve - max_correction) 107 108 # CH4濃度の補正 109 df[col_ch4] = df[col_ch4] + correction_term 110 111 # 極端に低い水蒸気濃度のデータは信頼性が低いため除外 112 if h2o_threshold is not None: 113 df.loc[df[col_h2o] < h2o_threshold, col_ch4] = np.nan 114 df = df.dropna(subset=[col_ch4]) 115 116 return df 117 118 @staticmethod 119 def _remove_bias( 120 df: pd.DataFrame, 121 col_ch4_ppm: str = "ch4_ppm", 122 col_c2h6_ppb: str = "c2h6_ppb", 123 ) -> pd.DataFrame: 124 """ 125 データフレームからバイアスを除去します。 126 127 Parameters: 128 ------ 129 df : pd.DataFrame 130 バイアスを除去する対象のデータフレーム。 131 col_ch4_ppm : str 132 CH4濃度を示すカラム名。デフォルトは"ch4_ppm"。 133 col_c2h6_ppb : str 134 C2H6濃度を示すカラム名。デフォルトは"c2h6_ppb"。 135 136 Returns: 137 ------ 138 pd.DataFrame 139 バイアスが除去されたデータフレーム。 140 """ 141 df_processed: pd.DataFrame = df.copy() 142 c2h6_min = np.percentile(df_processed[col_c2h6_ppb], 5) 143 df_processed[col_c2h6_ppb] = df_processed[col_c2h6_ppb] - c2h6_min 144 ch4_min = np.percentile(df_processed[col_ch4_ppm], 5) 145 df_processed[col_ch4_ppm] = df_processed[col_ch4_ppm] - ch4_min + 2.0 146 return df_processed
12 @staticmethod 13 def correct_df_by_type(df: pd.DataFrame, correction_type: str) -> pd.DataFrame: 14 """ 15 指定された補正式に基づいてデータフレームを補正します。 16 17 Parameters: 18 ------ 19 df : pd.DataFrame 20 補正対象のデータフレーム。 21 correction_type : str 22 適用する補正式の種類。CORRECTION_TYPES_PATTERNから選択する。 23 24 Returns: 25 ------ 26 pd.DataFrame 27 補正後のデータフレーム。 28 29 Raises: 30 ------ 31 ValueError 32 無効な補正式が指定された場合。 33 """ 34 if correction_type == "pico_1": 35 coef_a: float = 2.0631 # 切片 36 coef_b: float = 1.0111e-06 # 1次の係数 37 coef_c: float = -1.8683e-10 # 2次の係数 38 # 水蒸気補正 39 df_corrected: pd.DataFrame = CorrectingUtils._correct_h2o_interference( 40 df=df, 41 coef_a=coef_a, 42 coef_b=coef_b, 43 coef_c=coef_c, 44 col_ch4="ch4_ppm", 45 col_h2o="h2o_ppm", 46 h2o_threshold=2000, 47 ) 48 # 負の値のエタン濃度の補正など 49 df_corrected = CorrectingUtils._remove_bias( 50 df=df_corrected, col_ch4_ppm="ch4_ppm", col_c2h6_ppb="c2h6_ppb" 51 ) 52 return df_corrected 53 else: 54 raise ValueError(f"invalid correction_type: {correction_type}.")
指定された補正式に基づいてデータフレームを補正します。
Parameters:
df : pd.DataFrame
補正対象のデータフレーム。
correction_type : str
適用する補正式の種類。CORRECTION_TYPES_PATTERNから選択する。
Returns:
pd.DataFrame
補正後のデータフレーム。
Raises:
ValueError
無効な補正式が指定された場合。
22@dataclass 23class EmissionData: 24 """ 25 ホットスポットの排出量データを格納するクラス。 26 27 Parameters: 28 ------ 29 source : str 30 データソース(日時) 31 type : HotspotType 32 ホットスポットの種類(`HotspotType`を参照) 33 section : str | int | float 34 セクション情報 35 latitude : float 36 緯度 37 longitude : float 38 経度 39 delta_ch4 : float 40 CH4の増加量 (ppm) 41 delta_c2h6 : float 42 C2H6の増加量 (ppb) 43 ratio : float 44 C2H6/CH4比 45 emission_rate : float 46 排出量 (L/min) 47 daily_emission : float 48 日排出量 (L/day) 49 annual_emission : float 50 年間排出量 (L/year) 51 """ 52 53 source: str 54 type: HotspotType 55 section: str | int | float 56 latitude: float 57 longitude: float 58 delta_ch4: float 59 delta_c2h6: float 60 ratio: float 61 emission_rate: float 62 daily_emission: float 63 annual_emission: float 64 65 def __post_init__(self) -> None: 66 """ 67 Initialize時のバリデーションを行います。 68 69 Raises: 70 ------ 71 ValueError: 入力値が不正な場合 72 """ 73 # sourceのバリデーション 74 if not isinstance(self.source, str) or not self.source.strip(): 75 raise ValueError("Source must be a non-empty string") 76 77 # typeのバリデーションは型システムによって保証されるため削除 78 # HotspotTypeはLiteral["bio", "gas", "comb"]として定義されているため、 79 # 不正な値は型チェック時に検出されます 80 81 # sectionのバリデーション(Noneは許可) 82 if self.section is not None and not isinstance(self.section, (str, int, float)): 83 raise ValueError("Section must be a string, int, float, or None") 84 85 # 緯度のバリデーション 86 if ( 87 not isinstance(self.latitude, (int, float)) 88 or not -90 <= self.latitude <= 90 89 ): 90 raise ValueError("Latitude must be a number between -90 and 90") 91 92 # 経度のバリデーション 93 if ( 94 not isinstance(self.longitude, (int, float)) 95 or not -180 <= self.longitude <= 180 96 ): 97 raise ValueError("Longitude must be a number between -180 and 180") 98 99 # delta_ch4のバリデーション 100 if not isinstance(self.delta_ch4, (int, float)) or self.delta_ch4 < 0: 101 raise ValueError("Delta CH4 must be a non-negative number") 102 103 # delta_c2h6のバリデーション 104 if not isinstance(self.delta_c2h6, (int, float)) or self.delta_c2h6 < 0: 105 raise ValueError("Delta C2H6 must be a non-negative number") 106 107 # ratioのバリデーション 108 if not isinstance(self.ratio, (int, float)) or self.ratio < 0: 109 raise ValueError("Ratio must be a non-negative number") 110 111 # emission_rateのバリデーション 112 if not isinstance(self.emission_rate, (int, float)) or self.emission_rate < 0: 113 raise ValueError("Emission rate must be a non-negative number") 114 115 # daily_emissionのバリデーション 116 expected_daily = self.emission_rate * 60 * 24 117 if not math.isclose(self.daily_emission, expected_daily, rel_tol=1e-10): 118 raise ValueError( 119 f"Daily emission ({self.daily_emission}) does not match " 120 f"calculated value from emission rate ({expected_daily})" 121 ) 122 123 # annual_emissionのバリデーション 124 expected_annual = self.daily_emission * 365 125 if not math.isclose(self.annual_emission, expected_annual, rel_tol=1e-10): 126 raise ValueError( 127 f"Annual emission ({self.annual_emission}) does not match " 128 f"calculated value from daily emission ({expected_annual})" 129 ) 130 131 # NaN値のチェック 132 numeric_fields = [ 133 self.latitude, 134 self.longitude, 135 self.delta_ch4, 136 self.delta_c2h6, 137 self.ratio, 138 self.emission_rate, 139 self.daily_emission, 140 self.annual_emission, 141 ] 142 if any(math.isnan(x) for x in numeric_fields): 143 raise ValueError("Numeric fields cannot contain NaN values") 144 145 def to_dict(self) -> dict: 146 """ 147 データクラスの内容を辞書形式に変換します。 148 149 Returns: 150 ------ 151 dict: データクラスの属性と値を含む辞書 152 """ 153 return { 154 "source": self.source, 155 "type": self.type, 156 "section": self.section, 157 "latitude": self.latitude, 158 "longitude": self.longitude, 159 "delta_ch4": self.delta_ch4, 160 "delta_c2h6": self.delta_c2h6, 161 "ratio": self.ratio, 162 "emission_rate": self.emission_rate, 163 "daily_emission": self.daily_emission, 164 "annual_emission": self.annual_emission, 165 }
ホットスポットの排出量データを格納するクラス。
Parameters:
source : str
データソース(日時)
type : HotspotType
ホットスポットの種類(`HotspotType`を参照)
section : str | int | float
セクション情報
latitude : float
緯度
longitude : float
経度
delta_ch4 : float
CH4の増加量 (ppm)
delta_c2h6 : float
C2H6の増加量 (ppb)
ratio : float
C2H6/CH4比
emission_rate : float
排出量 (L/min)
daily_emission : float
日排出量 (L/day)
annual_emission : float
年間排出量 (L/year)
145 def to_dict(self) -> dict: 146 """ 147 データクラスの内容を辞書形式に変換します。 148 149 Returns: 150 ------ 151 dict: データクラスの属性と値を含む辞書 152 """ 153 return { 154 "source": self.source, 155 "type": self.type, 156 "section": self.section, 157 "latitude": self.latitude, 158 "longitude": self.longitude, 159 "delta_ch4": self.delta_ch4, 160 "delta_c2h6": self.delta_c2h6, 161 "ratio": self.ratio, 162 "emission_rate": self.emission_rate, 163 "daily_emission": self.daily_emission, 164 "annual_emission": self.annual_emission, 165 }
データクラスの内容を辞書形式に変換します。
Returns:
dict: データクラスの属性と値を含む辞書
258class MobileSpatialAnalyzer: 259 """ 260 移動観測で得られた測定データを解析するクラス 261 """ 262 263 EARTH_RADIUS_METERS: float = 6371000 # 地球の半径(メートル) 264 265 def __init__( 266 self, 267 center_lat: float, 268 center_lon: float, 269 inputs: list[MSAInputConfig] | list[tuple[float, float, str | Path]], 270 num_sections: int = 4, 271 ch4_enhance_threshold: float = 0.1, 272 correlation_threshold: float = 0.7, 273 hotspot_area_meter: float = 50, 274 window_minutes: float = 5, 275 column_mapping: dict[str, str] = { 276 "Time Stamp": "timestamp", 277 "CH4 (ppm)": "ch4_ppm", 278 "C2H6 (ppb)": "c2h6_ppb", 279 "H2O (ppm)": "h2o_ppm", 280 "Latitude": "latitude", 281 "Longitude": "longitude", 282 }, 283 logger: Logger | None = None, 284 logging_debug: bool = False, 285 ): 286 """ 287 測定データ解析クラスの初期化 288 289 Parameters: 290 ------ 291 center_lat : float 292 中心緯度 293 center_lon : float 294 中心経度 295 inputs : list[MSAInputConfig] | list[tuple[float, float, str | Path]] 296 入力ファイルのリスト 297 num_sections : int 298 分割する区画数。デフォルトは4。 299 ch4_enhance_threshold : float 300 CH4増加の閾値(ppm)。デフォルトは0.1。 301 correlation_threshold : float 302 相関係数の閾値。デフォルトは0.7。 303 hotspot_area_meter : float 304 ホットスポットの検出に使用するエリアの半径(メートル)。デフォルトは50メートル。 305 window_minutes : float 306 移動窓の大きさ(分)。デフォルトは5分。 307 column_mapping : dict[str, str] 308 元のデータファイルのヘッダーを汎用的な単語に変換するための辞書型データ。 309 - timestamp,ch4_ppm,c2h6_ppm,h2o_ppm,latitude,longitudeをvalueに、それぞれに対応するカラム名をcolに指定してください。 310 logger : Logger | None 311 使用するロガー。Noneの場合は新しいロガーを作成します。 312 logging_debug : bool 313 ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。 314 315 Returns: 316 ------ 317 None 318 初期化処理が完了したことを示します。 319 """ 320 # ロガー 321 log_level: int = INFO 322 if logging_debug: 323 log_level = DEBUG 324 self.logger: Logger = MobileSpatialAnalyzer.setup_logger(logger, log_level) 325 # プライベートなプロパティ 326 self._center_lat: float = center_lat 327 self._center_lon: float = center_lon 328 self._ch4_enhance_threshold: float = ch4_enhance_threshold 329 self._correlation_threshold: float = correlation_threshold 330 self._hotspot_area_meter: float = hotspot_area_meter 331 self._column_mapping: dict[str, str] = column_mapping 332 self._num_sections: int = num_sections 333 # セクションの範囲 334 section_size: float = 360 / num_sections 335 self._section_size: float = section_size 336 self._sections = MobileSpatialAnalyzer._initialize_sections( 337 num_sections, section_size 338 ) 339 # window_sizeをデータポイント数に変換(分→秒→データポイント数) 340 self._window_size: int = MobileSpatialAnalyzer._calculate_window_size( 341 window_minutes 342 ) 343 # 入力設定の標準化 344 normalized_input_configs: list[MSAInputConfig] = ( 345 MobileSpatialAnalyzer._normalize_inputs(inputs) 346 ) 347 # 複数ファイルのデータを読み込み 348 self._data: dict[str, pd.DataFrame] = self._load_all_data( 349 normalized_input_configs 350 ) 351 352 def analyze_delta_ch4_stats(self, hotspots: list[HotspotData]) -> None: 353 """ 354 各タイプのホットスポットについてΔCH4の統計情報を計算し、結果を表示します。 355 356 Parameters: 357 ------ 358 hotspots : list[HotspotData] 359 分析対象のホットスポットリスト 360 361 Returns: 362 ------ 363 None 364 統計情報の表示が完了したことを示します。 365 """ 366 # タイプごとにホットスポットを分類 367 hotspots_by_type: dict[HotspotType, list[HotspotData]] = { 368 "bio": [h for h in hotspots if h.type == "bio"], 369 "gas": [h for h in hotspots if h.type == "gas"], 370 "comb": [h for h in hotspots if h.type == "comb"], 371 } 372 373 # 統計情報を計算し、表示 374 for spot_type, spots in hotspots_by_type.items(): 375 if spots: 376 delta_ch4_values = [spot.delta_ch4 for spot in spots] 377 max_value = max(delta_ch4_values) 378 mean_value = sum(delta_ch4_values) / len(delta_ch4_values) 379 median_value = sorted(delta_ch4_values)[len(delta_ch4_values) // 2] 380 print(f"{spot_type}タイプのホットスポットの統計情報:") 381 print(f" 最大値: {max_value}") 382 print(f" 平均値: {mean_value}") 383 print(f" 中央値: {median_value}") 384 else: 385 print(f"{spot_type}タイプのホットスポットは存在しません。") 386 387 def analyze_hotspots( 388 self, 389 duplicate_check_mode: str = "none", 390 min_time_threshold_seconds: float = 300, 391 max_time_threshold_hours: float = 12, 392 ) -> list[HotspotData]: 393 """ 394 ホットスポットを検出して分析します。 395 396 Parameters: 397 ------ 398 duplicate_check_mode : str 399 重複チェックのモード("none","time_window","time_all")。 400 - "none": 重複チェックを行わない。 401 - "time_window": 指定された時間窓内の重複のみを除外。 402 - "time_all": すべての時間範囲で重複チェックを行う。 403 min_time_threshold_seconds : float 404 重複とみなす最小時間の閾値(秒)。デフォルトは300秒。 405 max_time_threshold_hours : float 406 重複チェックを一時的に無視する最大時間の閾値(時間)。デフォルトは12時間。 407 408 Returns: 409 ------ 410 list[HotspotData] 411 検出されたホットスポットのリスト。 412 """ 413 # 不正な入力値に対するエラーチェック 414 valid_modes = {"none", "time_window", "time_all"} 415 if duplicate_check_mode not in valid_modes: 416 raise ValueError( 417 f"無効な重複チェックモード: {duplicate_check_mode}. 有効な値は {valid_modes} です。" 418 ) 419 420 all_hotspots: list[HotspotData] = [] 421 422 # 各データソースに対して解析を実行 423 for _, df in self._data.items(): 424 # パラメータの計算 425 df = MobileSpatialAnalyzer._calculate_hotspots_parameters( 426 df, self._window_size 427 ) 428 429 # ホットスポットの検出 430 hotspots: list[HotspotData] = self._detect_hotspots( 431 df, 432 ch4_enhance_threshold=self._ch4_enhance_threshold, 433 ) 434 all_hotspots.extend(hotspots) 435 436 # 重複チェックモードに応じて処理 437 if duplicate_check_mode != "none": 438 unique_hotspots = MobileSpatialAnalyzer.remove_hotspots_duplicates( 439 all_hotspots, 440 check_time_all=duplicate_check_mode == "time_all", 441 min_time_threshold_seconds=min_time_threshold_seconds, 442 max_time_threshold_hours=max_time_threshold_hours, 443 hotspot_area_meter=self._hotspot_area_meter, 444 ) 445 self.logger.info( 446 f"重複除外: {len(all_hotspots)} → {len(unique_hotspots)} ホットスポット" 447 ) 448 return unique_hotspots 449 450 return all_hotspots 451 452 def calculate_measurement_stats( 453 self, 454 print_individual_stats: bool = True, 455 print_total_stats: bool = True, 456 ) -> tuple[float, timedelta]: 457 """ 458 各ファイルの測定時間と走行距離を計算し、合計を返します。 459 460 Parameters: 461 ------ 462 print_individual_stats : bool 463 個別ファイルの統計を表示するかどうか。デフォルトはTrue。 464 print_total_stats : bool 465 合計統計を表示するかどうか。デフォルトはTrue。 466 467 Returns: 468 ------ 469 tuple[float, timedelta] 470 総距離(km)と総時間のタプル 471 """ 472 total_distance: float = 0.0 473 total_time: timedelta = timedelta() 474 individual_stats: list[dict] = [] # 個別の統計情報を保存するリスト 475 476 # プログレスバーを表示しながら計算 477 for source_name, df in tqdm( 478 self._data.items(), desc="Calculating", unit="file" 479 ): 480 # 時間の計算 481 time_spent = df.index[-1] - df.index[0] 482 483 # 距離の計算 484 distance_km = 0.0 485 for i in range(len(df) - 1): 486 lat1, lon1 = df.iloc[i][["latitude", "longitude"]] 487 lat2, lon2 = df.iloc[i + 1][["latitude", "longitude"]] 488 distance_km += ( 489 MobileSpatialAnalyzer._calculate_distance( 490 lat1=lat1, lon1=lon1, lat2=lat2, lon2=lon2 491 ) 492 / 1000 493 ) 494 495 # 合計に加算 496 total_distance += distance_km 497 total_time += time_spent 498 499 # 統計情報を保存 500 if print_individual_stats: 501 average_speed = distance_km / (time_spent.total_seconds() / 3600) 502 individual_stats.append( 503 { 504 "source": source_name, 505 "distance": distance_km, 506 "time": time_spent, 507 "speed": average_speed, 508 } 509 ) 510 511 # 計算完了後に統計情報を表示 512 if print_individual_stats: 513 self.logger.info("=== Individual Stats ===") 514 for stat in individual_stats: 515 print(f"File : {stat['source']}") 516 print(f" Distance : {stat['distance']:.2f} km") 517 print(f" Time : {stat['time']}") 518 print(f" Avg. Speed : {stat['speed']:.1f} km/h\n") 519 520 # 合計を表示 521 if print_total_stats: 522 average_speed_total: float = total_distance / ( 523 total_time.total_seconds() / 3600 524 ) 525 self.logger.info("=== Total Stats ===") 526 print(f" Distance : {total_distance:.2f} km") 527 print(f" Time : {total_time}") 528 print(f" Avg. Speed : {average_speed_total:.1f} km/h\n") 529 530 return total_distance, total_time 531 532 def create_hotspots_map( 533 self, 534 hotspots: list[HotspotData], 535 output_dir: str | Path | None = None, 536 output_filename: str = "hotspots_map.html", 537 center_marker_label: str = "Center", 538 plot_center_marker: bool = True, 539 radius_meters: float = 3000, 540 save_fig: bool = True, 541 ) -> None: 542 """ 543 ホットスポットの分布を地図上にプロットして保存 544 545 Parameters: 546 ------ 547 hotspots : list[HotspotData] 548 プロットするホットスポットのリスト 549 output_dir : str | Path 550 保存先のディレクトリパス 551 output_filename : str 552 保存するファイル名。デフォルトは"hotspots_map"。 553 center_marker_label : str 554 中心を示すマーカーのラベルテキスト。デフォルトは"Center"。 555 plot_center_marker : bool 556 中心を示すマーカーの有無。デフォルトはTrue。 557 radius_meters : float 558 区画分けを示す線の長さ。デフォルトは3000。 559 save_fig : bool 560 図の保存を許可するフラグ。デフォルトはTrue。 561 """ 562 # 地図の作成 563 m = folium.Map( 564 location=[self._center_lat, self._center_lon], 565 zoom_start=15, 566 tiles="OpenStreetMap", 567 ) 568 569 # ホットスポットの種類ごとに異なる色でプロット 570 for spot in hotspots: 571 # NaN値チェックを追加 572 if math.isnan(spot.avg_lat) or math.isnan(spot.avg_lon): 573 continue 574 575 # default type 576 color = "black" 577 # タイプに応じて色を設定 578 if spot.type == "comb": 579 color = "green" 580 elif spot.type == "gas": 581 color = "red" 582 elif spot.type == "bio": 583 color = "blue" 584 585 # CSSのgrid layoutを使用してHTMLタグを含むテキストをフォーマット 586 popup_html = f""" 587 <div style='font-family: Arial; font-size: 12px; display: grid; grid-template-columns: auto auto auto; gap: 5px;'> 588 <b>Date</b> <span>:</span> <span>{spot.source}</span> 589 <b>Lat</b> <span>:</span> <span>{spot.avg_lat:.3f}</span> 590 <b>Lon</b> <span>:</span> <span>{spot.avg_lon:.3f}</span> 591 <b>ΔCH<sub>4</sub></b> <span>:</span> <span>{spot.delta_ch4:.3f}</span> 592 <b>ΔC<sub>2</sub>H<sub>6</sub></b> <span>:</span> <span>{spot.delta_c2h6:.3f}</span> 593 <b>Ratio</b> <span>:</span> <span>{spot.ratio:.3f}</span> 594 <b>Type</b> <span>:</span> <span>{spot.type}</span> 595 <b>Section</b> <span>:</span> <span>{spot.section}</span> 596 </div> 597 """ 598 599 # ポップアップのサイズを指定 600 popup = folium.Popup( 601 folium.Html(popup_html, script=True), 602 max_width=200, # 最大幅(ピクセル) 603 ) 604 605 folium.CircleMarker( 606 location=[spot.avg_lat, spot.avg_lon], 607 radius=8, 608 color=color, 609 fill=True, 610 popup=popup, 611 ).add_to(m) 612 613 # 中心点のマーカー 614 if plot_center_marker: 615 folium.Marker( 616 [self._center_lat, self._center_lon], 617 popup=center_marker_label, 618 icon=folium.Icon(color="green", icon="info-sign"), 619 ).add_to(m) 620 621 # 区画の境界線を描画 622 for section in range(self._num_sections): 623 start_angle = math.radians(-180 + section * self._section_size) 624 625 R = self.EARTH_RADIUS_METERS 626 627 # 境界線の座標を計算 628 lat1 = self._center_lat 629 lon1 = self._center_lon 630 lat2 = math.degrees( 631 math.asin( 632 math.sin(math.radians(lat1)) * math.cos(radius_meters / R) 633 + math.cos(math.radians(lat1)) 634 * math.sin(radius_meters / R) 635 * math.cos(start_angle) 636 ) 637 ) 638 lon2 = self._center_lon + math.degrees( 639 math.atan2( 640 math.sin(start_angle) 641 * math.sin(radius_meters / R) 642 * math.cos(math.radians(lat1)), 643 math.cos(radius_meters / R) 644 - math.sin(math.radians(lat1)) * math.sin(math.radians(lat2)), 645 ) 646 ) 647 648 # 境界線を描画 649 folium.PolyLine( 650 locations=[[lat1, lon1], [lat2, lon2]], 651 color="black", 652 weight=1, 653 opacity=0.5, 654 ).add_to(m) 655 656 # 地図を保存 657 if save_fig and output_dir is None: 658 raise ValueError( 659 "save_fig=Trueの場合、output_dirを指定する必要があります。有効なディレクトリパスを指定してください。" 660 ) 661 output_path: str = os.path.join(output_dir, output_filename) 662 m.save(str(output_path)) 663 self.logger.info(f"地図を保存しました: {output_path}") 664 665 def export_hotspots_to_csv( 666 self, 667 hotspots: list[HotspotData], 668 output_dir: str | Path | None = None, 669 output_filename: str = "hotspots.csv", 670 ) -> None: 671 """ 672 ホットスポットの情報をCSVファイルに出力します。 673 674 Parameters: 675 ------ 676 hotspots : list[HotspotData] 677 出力するホットスポットのリスト 678 output_dir : str | Path | None 679 出力先ディレクトリ 680 output_filename : str 681 出力ファイル名 682 """ 683 # 日時の昇順でソート 684 sorted_hotspots = sorted(hotspots, key=lambda x: x.source) 685 686 # 出力用のデータを作成 687 records = [] 688 for spot in sorted_hotspots: 689 record = { 690 "source": spot.source, 691 "type": spot.type, 692 "delta_ch4": spot.delta_ch4, 693 "delta_c2h6": spot.delta_c2h6, 694 "ratio": spot.ratio, 695 "correlation": spot.correlation, 696 "angle": spot.angle, 697 "section": spot.section, 698 "latitude": spot.avg_lat, 699 "longitude": spot.avg_lon, 700 } 701 records.append(record) 702 703 # DataFrameに変換してCSVに出力 704 if output_dir is None: 705 raise ValueError( 706 "output_dirが指定されていません。有効なディレクトリパスを指定してください。" 707 ) 708 output_path: str = os.path.join(output_dir, output_filename) 709 df = pd.DataFrame(records) 710 df.to_csv(output_path, index=False) 711 self.logger.info( 712 f"ホットスポット情報をCSVファイルに出力しました: {output_path}" 713 ) 714 715 def get_preprocessed_data( 716 self, 717 ) -> pd.DataFrame: 718 """ 719 データ前処理を行い、CH4とC2H6の相関解析に必要な形式に整えます。 720 コンストラクタで読み込んだすべてのデータを前処理し、結合したDataFrameを返します。 721 722 Returns: 723 ------ 724 pd.DataFrame 725 前処理済みの結合されたDataFrame 726 """ 727 processed_dfs: list[pd.DataFrame] = [] 728 729 # 各データソースに対して解析を実行 730 for source_name, df in self._data.items(): 731 # パラメータの計算 732 processed_df = MobileSpatialAnalyzer._calculate_hotspots_parameters( 733 df, self._window_size 734 ) 735 # ソース名を列として追加 736 processed_df["source"] = source_name 737 processed_dfs.append(processed_df) 738 739 # すべてのDataFrameを結合 740 if not processed_dfs: 741 raise ValueError("処理対象のデータが存在しません。") 742 743 combined_df = pd.concat(processed_dfs, axis=0) 744 return combined_df 745 746 def get_section_size(self) -> float: 747 """ 748 セクションのサイズを取得するメソッド。 749 このメソッドは、解析対象のデータを区画に分割する際の 750 各区画の角度範囲を示すサイズを返します。 751 752 Returns: 753 ------ 754 float 755 1セクションのサイズ(度単位) 756 """ 757 return self._section_size 758 759 def plot_ch4_delta_histogram( 760 self, 761 hotspots: list[HotspotData], 762 output_dir: str | Path | None, 763 output_filename: str = "ch4_delta_histogram.png", 764 dpi: int = 200, 765 figsize: tuple[int, int] = (8, 6), 766 fontsize: float = 20, 767 xlim: tuple[float, float] | None = None, 768 ylim: tuple[float, float] | None = None, 769 save_fig: bool = True, 770 show_fig: bool = True, 771 yscale_log: bool = True, 772 print_bins_analysis: bool = False, 773 ) -> None: 774 """ 775 CH4の増加量(ΔCH4)の積み上げヒストグラムをプロットします。 776 777 Parameters: 778 ------ 779 hotspots : list[HotspotData] 780 プロットするホットスポットのリスト 781 output_dir : str | Path | None 782 保存先のディレクトリパス 783 output_filename : str 784 保存するファイル名。デフォルトは"ch4_delta_histogram.png"。 785 dpi : int 786 解像度。デフォルトは200。 787 figsize : tuple[int, int] 788 図のサイズ。デフォルトは(8, 6)。 789 fontsize : float 790 フォントサイズ。デフォルトは20。 791 xlim : tuple[float, float] | None 792 x軸の範囲。Noneの場合は自動設定。 793 ylim : tuple[float, float] | None 794 y軸の範囲。Noneの場合は自動設定。 795 save_fig : bool 796 図の保存を許可するフラグ。デフォルトはTrue。 797 show_fig : bool 798 図の表示を許可するフラグ。デフォルトはTrue。 799 yscale_log : bool 800 y軸をlogにするかどうか。デフォルトはTrue。 801 print_bins_analysis : bool 802 ビンごとの内訳を表示するオプション。 803 """ 804 plt.rcParams["font.size"] = fontsize 805 fig = plt.figure(figsize=figsize, dpi=dpi) 806 807 # ホットスポットからデータを抽出 808 all_ch4_deltas = [] 809 all_types = [] 810 for spot in hotspots: 811 all_ch4_deltas.append(spot.delta_ch4) 812 all_types.append(spot.type) 813 814 # データをNumPy配列に変換 815 all_ch4_deltas = np.array(all_ch4_deltas) 816 all_types = np.array(all_types) 817 818 # 0.1刻みのビンを作成 819 if xlim is not None: 820 bins = np.arange(xlim[0], xlim[1] + 0.1, 0.1) 821 else: 822 max_val = np.ceil(np.max(all_ch4_deltas) * 10) / 10 823 bins = np.arange(0, max_val + 0.1, 0.1) 824 825 # タイプごとのヒストグラムデータを計算 826 hist_data = {} 827 # HotspotTypeのリテラル値を使用してイテレーション 828 for type_name in get_args(HotspotType): # typing.get_argsをインポート 829 mask = all_types == type_name 830 if np.any(mask): 831 counts, _ = np.histogram(all_ch4_deltas[mask], bins=bins) 832 hist_data[type_name] = counts 833 834 # ビンごとの内訳を表示 835 if print_bins_analysis: 836 self.logger.info("各ビンの内訳:") 837 print(f"{'Bin Range':15} {'bio':>8} {'gas':>8} {'comb':>8} {'Total':>8}") 838 print("-" * 50) 839 840 for i in range(len(bins) - 1): 841 bin_start = bins[i] 842 bin_end = bins[i + 1] 843 bio_count = hist_data.get("bio", np.zeros(len(bins) - 1))[i] 844 gas_count = hist_data.get("gas", np.zeros(len(bins) - 1))[i] 845 comb_count = hist_data.get("comb", np.zeros(len(bins) - 1))[i] 846 total = bio_count + gas_count + comb_count 847 848 if total > 0: # 合計が0のビンは表示しない 849 print( 850 f"{bin_start:4.1f}-{bin_end:<8.1f}" 851 f"{int(bio_count):8d}" 852 f"{int(gas_count):8d}" 853 f"{int(comb_count):8d}" 854 f"{int(total):8d}" 855 ) 856 857 # 積み上げヒストグラムを作成 858 bottom = np.zeros_like(hist_data.get("bio", np.zeros(len(bins) - 1))) 859 860 # 色の定義をHotspotTypeを使用して型安全に定義 861 colors: dict[HotspotType, str] = {"bio": "blue", "gas": "red", "comb": "green"} 862 863 # HotspotTypeのリテラル値を使用してイテレーション 864 for type_name in get_args(HotspotType): 865 if type_name in hist_data: 866 plt.bar( 867 bins[:-1], 868 hist_data[type_name], 869 width=np.diff(bins)[0], 870 bottom=bottom, 871 color=colors[type_name], 872 label=type_name, 873 alpha=0.6, 874 align="edge", 875 ) 876 bottom += hist_data[type_name] 877 878 if yscale_log: 879 plt.yscale("log") 880 plt.xlabel("Δ$\\mathregular{CH_{4}}$ (ppm)") 881 plt.ylabel("Frequency") 882 plt.legend() 883 plt.grid(True, which="both", ls="-", alpha=0.2) 884 885 # 軸の範囲を設定 886 if xlim is not None: 887 plt.xlim(xlim) 888 if ylim is not None: 889 plt.ylim(ylim) 890 891 # グラフの保存または表示 892 if save_fig: 893 if output_dir is None: 894 raise ValueError( 895 "save_fig=Trueの場合、output_dirを指定する必要があります。有効なディレクトリパスを指定してください。" 896 ) 897 os.makedirs(output_dir, exist_ok=True) 898 output_path: str = os.path.join(output_dir, output_filename) 899 plt.savefig(output_path, bbox_inches="tight") 900 self.logger.info(f"ヒストグラムを保存しました: {output_path}") 901 if show_fig: 902 plt.show() 903 else: 904 plt.close(fig=fig) 905 906 def plot_mapbox( 907 self, 908 df: pd.DataFrame, 909 col: str, 910 mapbox_access_token: str, 911 sort_value_column: bool = True, 912 output_dir: str | Path | None = None, 913 output_filename: str = "mapbox_plot.html", 914 lat_column: str = "latitude", 915 lon_column: str = "longitude", 916 colorscale: str = "Jet", 917 center_lat: float | None = None, 918 center_lon: float | None = None, 919 zoom: float = 12, 920 width: int = 700, 921 height: int = 700, 922 tick_font_family: str = "Arial", 923 title_font_family: str = "Arial", 924 tick_font_size: int = 12, 925 title_font_size: int = 14, 926 marker_size: int = 4, 927 colorbar_title: str | None = None, 928 value_range: tuple[float, float] | None = None, 929 save_fig: bool = True, 930 show_fig: bool = True, 931 ) -> None: 932 """ 933 Plotlyを使用してMapbox上にデータをプロットします。 934 935 Parameters: 936 ------ 937 df : pd.DataFrame 938 プロットするデータを含むDataFrame 939 col : str 940 カラーマッピングに使用する列名 941 mapbox_access_token : str 942 Mapboxのアクセストークン 943 sort_value_column : bool 944 value_columnをソートするか否か。デフォルトはTrue。 945 output_dir : str | Path | None 946 出力ディレクトリのパス 947 output_filename : str 948 出力ファイル名。デフォルトは"mapbox_plot.html" 949 lat_column : str 950 緯度の列名。デフォルトは"latitude" 951 lon_column : str 952 経度の列名。デフォルトは"longitude" 953 colorscale : str 954 使用するカラースケール。デフォルトは"Jet" 955 center_lat : float | None 956 中心緯度。デフォルトはNoneで、self._center_latを使用 957 center_lon : float | None 958 中心経度。デフォルトはNoneで、self._center_lonを使用 959 zoom : float 960 マップの初期ズームレベル。デフォルトは12 961 width : int 962 プロットの幅(ピクセル)。デフォルトは700 963 height : int 964 プロットの高さ(ピクセル)。デフォルトは700 965 tick_font_family : str 966 カラーバーの目盛りフォントファミリー。デフォルトは"Arial" 967 title_font_family : str 968 カラーバーのラベルフォントファミリー。デフォルトは"Arial" 969 tick_font_size : int 970 カラーバーの目盛りフォントサイズ。デフォルトは12 971 title_font_size : int 972 カラーバーのラベルフォントサイズ。デフォルトは14 973 marker_size : int 974 マーカーのサイズ。デフォルトは4 975 colorbar_title : str | None 976 カラーバーのラベル 977 value_range : tuple[float, float] | None 978 カラーマッピングの範囲。デフォルトはNoneで、データの最小値と最大値を使用 979 save_fig : bool 980 図を保存するかどうか。デフォルトはTrue 981 show_fig : bool 982 図を表示するかどうか。デフォルトはTrue 983 """ 984 df_mapping: pd.DataFrame = df.copy().dropna(subset=[col]) 985 if sort_value_column: 986 df_mapping = df_mapping.sort_values(col) 987 # 中心座標の設定 988 center_lat = center_lat if center_lat is not None else self._center_lat 989 center_lon = center_lon if center_lon is not None else self._center_lon 990 991 # カラーマッピングの範囲を設定 992 cmin, cmax = 0, 0 993 if value_range is None: 994 cmin = df_mapping[col].min() 995 cmax = df_mapping[col].max() 996 else: 997 cmin, cmax = value_range 998 999 # カラーバーのタイトルを設定 1000 title_text = colorbar_title if colorbar_title is not None else col 1001 1002 # Scattermapboxのデータを作成 1003 scatter_data = go.Scattermapbox( 1004 lat=df_mapping[lat_column], 1005 lon=df_mapping[lon_column], 1006 text=df_mapping[col].astype(str), 1007 hoverinfo="text", 1008 mode="markers", 1009 marker=dict( 1010 color=df_mapping[col], 1011 size=marker_size, 1012 reversescale=False, 1013 autocolorscale=False, 1014 colorscale=colorscale, 1015 cmin=cmin, 1016 cmax=cmax, 1017 colorbar=dict( 1018 tickformat="3.2f", 1019 outlinecolor="black", 1020 outlinewidth=1.5, 1021 ticks="outside", 1022 ticklen=7, 1023 tickwidth=1.5, 1024 tickcolor="black", 1025 tickfont=dict( 1026 family=tick_font_family, color="black", size=tick_font_size 1027 ), 1028 title=dict( 1029 text=title_text, side="top" 1030 ), # カラーバーのタイトルを設定 1031 titlefont=dict( 1032 family=title_font_family, 1033 color="black", 1034 size=title_font_size, 1035 ), 1036 ), 1037 ), 1038 ) 1039 1040 # レイアウトの設定 1041 layout = go.Layout( 1042 width=width, 1043 height=height, 1044 showlegend=False, 1045 mapbox=dict( 1046 accesstoken=mapbox_access_token, 1047 center=dict(lat=center_lat, lon=center_lon), 1048 zoom=zoom, 1049 ), 1050 ) 1051 1052 # 図の作成 1053 fig = go.Figure(data=[scatter_data], layout=layout) 1054 1055 # 図の保存 1056 if save_fig: 1057 # 保存時の出力ディレクトリチェック 1058 if output_dir is None: 1059 raise ValueError( 1060 "save_fig=Trueの場合、output_dirを指定する必要があります。" 1061 ) 1062 os.makedirs(output_dir, exist_ok=True) 1063 output_path = os.path.join(output_dir, output_filename) 1064 pyo.plot(fig, filename=output_path, auto_open=False) 1065 self.logger.info(f"Mapboxプロットを保存しました: {output_path}") 1066 # 図の表示 1067 if show_fig: 1068 pyo.iplot(fig) 1069 1070 def plot_scatter_c2c1( 1071 self, 1072 hotspots: list[HotspotData], 1073 output_dir: str | Path | None = None, 1074 output_filename: str = "scatter_c2c1.png", 1075 dpi: int = 200, 1076 figsize: tuple[int, int] = (4, 4), 1077 fontsize: float = 12, 1078 save_fig: bool = True, 1079 show_fig: bool = True, 1080 ratio_labels: dict[float, tuple[float, float, str]] | None = None, 1081 ) -> None: 1082 """ 1083 検出されたホットスポットのΔC2H6とΔCH4の散布図をプロットします。 1084 1085 Parameters: 1086 ------ 1087 hotspots : list[HotspotData] 1088 プロットするホットスポットのリスト 1089 output_dir : str | Path | None 1090 保存先のディレクトリパス 1091 output_filename : str 1092 保存するファイル名。デフォルトは"scatter_c2c1.png"。 1093 dpi : int 1094 解像度。デフォルトは200。 1095 figsize : tuple[int, int] 1096 図のサイズ。デフォルトは(4, 4)。 1097 fontsize : float 1098 フォントサイズ。デフォルトは12。 1099 save_fig : bool 1100 図の保存を許可するフラグ。デフォルトはTrue。 1101 show_fig : bool 1102 図の表示を許可するフラグ。デフォルトはTrue。 1103 ratio_labels : dict[float, tuple[float, float, str]] | None 1104 比率線とラベルの設定。 1105 キーは比率値、値は (x位置, y位置, ラベルテキスト) のタプル。 1106 Noneの場合はデフォルト設定を使用。デフォルト値: 1107 { 1108 0.001: (1.25, 2, "0.001"), 1109 0.005: (1.25, 8, "0.005"), 1110 0.010: (1.25, 15, "0.01"), 1111 0.020: (1.25, 30, "0.02"), 1112 0.030: (1.0, 40, "0.03"), 1113 0.076: (0.20, 42, "0.076 (Osaka)") 1114 } 1115 """ 1116 plt.rcParams["font.size"] = fontsize 1117 fig = plt.figure(figsize=figsize, dpi=dpi) 1118 1119 # タイプごとのデータを収集 1120 type_data: dict[HotspotType, list[tuple[float, float]]] = { 1121 "bio": [], 1122 "gas": [], 1123 "comb": [], 1124 } 1125 for spot in hotspots: 1126 type_data[spot.type].append((spot.delta_ch4, spot.delta_c2h6)) 1127 1128 # 色とラベルの定義 1129 colors: dict[HotspotType, str] = {"bio": "blue", "gas": "red", "comb": "green"} 1130 labels: dict[HotspotType, str] = {"bio": "bio", "gas": "gas", "comb": "comb"} 1131 1132 # タイプごとにプロット(データが存在する場合のみ) 1133 for spot_type, data in type_data.items(): 1134 if data: # データが存在する場合のみプロット 1135 ch4_values, c2h6_values = zip(*data) 1136 plt.plot( 1137 ch4_values, 1138 c2h6_values, 1139 "o", 1140 c=colors[spot_type], 1141 alpha=0.5, 1142 ms=2, 1143 label=labels[spot_type], 1144 ) 1145 1146 # デフォルトの比率とラベル設定 1147 default_ratio_labels = { 1148 0.001: (1.25, 2, "0.001"), 1149 0.005: (1.25, 8, "0.005"), 1150 0.010: (1.25, 15, "0.01"), 1151 0.020: (1.25, 30, "0.02"), 1152 0.030: (1.0, 40, "0.03"), 1153 0.076: (0.20, 42, "0.076 (Osaka)"), 1154 } 1155 1156 ratio_labels = ratio_labels or default_ratio_labels 1157 1158 # プロット後、軸の設定前に比率の線を追加 1159 x = np.array([0, 5]) 1160 base_ch4 = 0.0 1161 base = 0.0 1162 1163 # 各比率に対して線を引く 1164 for ratio, (x_pos, y_pos, label) in ratio_labels.items(): 1165 y = (x - base_ch4) * 1000 * ratio + base 1166 plt.plot(x, y, "-", c="black", alpha=0.5) 1167 plt.text(x_pos, y_pos, label) 1168 1169 plt.ylim(0, 50) 1170 plt.xlim(0, 2.0) 1171 plt.ylabel("Δ$\\mathregular{C_{2}H_{6}}$ (ppb)") 1172 plt.xlabel("Δ$\\mathregular{CH_{4}}$ (ppm)") 1173 plt.legend() 1174 1175 # グラフの保存または表示 1176 if save_fig: 1177 if output_dir is None: 1178 raise ValueError( 1179 "save_fig=Trueの場合、output_dirを指定する必要があります。有効なディレクトリパスを指定してください。" 1180 ) 1181 output_path: str = os.path.join(output_dir, output_filename) 1182 plt.savefig(output_path, bbox_inches="tight") 1183 self.logger.info(f"散布図を保存しました: {output_path}") 1184 if show_fig: 1185 plt.show() 1186 else: 1187 plt.close(fig=fig) 1188 1189 def plot_timeseries( 1190 self, 1191 dpi: int = 200, 1192 source_name: str | None = None, 1193 figsize: tuple[float, float] = (8, 4), 1194 output_dir: str | Path | None = None, 1195 output_filename: str = "timeseries.png", 1196 save_fig: bool = False, 1197 show_fig: bool = True, 1198 col_ch4: str = "ch4_ppm", 1199 col_c2h6: str = "c2h6_ppb", 1200 col_h2o: str = "h2o_ppm", 1201 ylim_ch4: tuple[float, float] | None = None, 1202 ylim_c2h6: tuple[float, float] | None = None, 1203 ylim_h2o: tuple[float, float] | None = None, 1204 ) -> None: 1205 """ 1206 時系列データをプロットします。 1207 1208 Parameters: 1209 ------ 1210 dpi : int 1211 図の解像度を指定します。デフォルトは200です。 1212 source_name : str | None 1213 プロットするデータソースの名前。Noneの場合は最初のデータソースを使用します。 1214 figsize : tuple[float, float] 1215 図のサイズを指定します。デフォルトは(8, 4)です。 1216 output_dir : str | Path | None 1217 保存先のディレクトリを指定します。save_fig=Trueの場合は必須です。 1218 output_filename : str 1219 保存するファイル名を指定します。デフォルトは"time_series.png"です。 1220 save_fig : bool 1221 図を保存するかどうかを指定します。デフォルトはFalseです。 1222 show_fig : bool 1223 図を表示するかどうかを指定します。デフォルトはTrueです。 1224 col_ch4 : str 1225 CH4データのキーを指定します。デフォルトは"ch4_ppm"です。 1226 col_c2h6 : str 1227 C2H6データのキーを指定します。デフォルトは"c2h6_ppb"です。 1228 col_h2o : str 1229 H2Oデータのキーを指定します。デフォルトは"h2o_ppm"です。 1230 ylim_ch4 : tuple[float, float] | None 1231 CH4プロットのy軸範囲を指定します。デフォルトはNoneです。 1232 ylim_c2h6 : tuple[float, float] | None 1233 C2H6プロットのy軸範囲を指定します。デフォルトはNoneです。 1234 ylim_h2o : tuple[float, float] | None 1235 H2Oプロットのy軸範囲を指定します。デフォルトはNoneです。 1236 """ 1237 dfs_dict: dict[str, pd.DataFrame] = self._data.copy() 1238 # データソースの選択 1239 if not dfs_dict: 1240 raise ValueError("データが読み込まれていません。") 1241 1242 if source_name is None: 1243 source_name = list(dfs_dict.keys())[0] 1244 elif source_name not in dfs_dict: 1245 raise ValueError( 1246 f"指定されたデータソース '{source_name}' が見つかりません。" 1247 ) 1248 1249 df = dfs_dict[source_name] 1250 1251 # プロットの作成 1252 fig = plt.figure(figsize=figsize, dpi=dpi) 1253 1254 # CH4プロット 1255 ax1 = fig.add_subplot(3, 1, 1) 1256 ax1.plot(df.index, df[col_ch4], c="red") 1257 if ylim_ch4: 1258 ax1.set_ylim(ylim_ch4) 1259 ax1.set_ylabel("$\\mathregular{CH_{4}}$ (ppm)") 1260 ax1.grid(True, alpha=0.3) 1261 1262 # C2H6プロット 1263 ax2 = fig.add_subplot(3, 1, 2) 1264 ax2.plot(df.index, df[col_c2h6], c="red") 1265 if ylim_c2h6: 1266 ax2.set_ylim(ylim_c2h6) 1267 ax2.set_ylabel("$\\mathregular{C_{2}H_{6}}$ (ppb)") 1268 ax2.grid(True, alpha=0.3) 1269 1270 # H2Oプロット 1271 ax3 = fig.add_subplot(3, 1, 3) 1272 ax3.plot(df.index, df[col_h2o], c="red") 1273 if ylim_h2o: 1274 ax3.set_ylim(ylim_h2o) 1275 ax3.set_ylabel("$\\mathregular{H_{2}O}$ (ppm)") 1276 ax3.grid(True, alpha=0.3) 1277 1278 # x軸のフォーマット調整 1279 for ax in [ax1, ax2, ax3]: 1280 ax.xaxis.set_major_formatter(mdates.DateFormatter("%H:%M")) 1281 1282 plt.subplots_adjust(wspace=0.38, hspace=0.38) 1283 1284 # 図の保存 1285 if save_fig: 1286 if output_dir is None: 1287 raise ValueError( 1288 "save_fig=Trueの場合、output_dirを指定する必要があります。有効なディレクトリパスを指定してください。" 1289 ) 1290 output_path = os.path.join(output_dir, output_filename) 1291 plt.savefig(output_path, bbox_inches="tight") 1292 self.logger.info(f"時系列プロットを保存しました: {output_path}") 1293 1294 if show_fig: 1295 plt.show() 1296 else: 1297 plt.close(fig=fig) 1298 1299 def _detect_hotspots( 1300 self, 1301 df: pd.DataFrame, 1302 ch4_enhance_threshold: float, 1303 ) -> list[HotspotData]: 1304 """ 1305 シンプル化したホットスポット検出 1306 1307 Parameters: 1308 ------ 1309 df : pd.DataFrame 1310 入力データフレーム 1311 ch4_enhance_threshold : float 1312 CH4増加の閾値 1313 1314 Returns: 1315 ------ 1316 list[HotspotData] 1317 検出されたホットスポットのリスト 1318 """ 1319 hotspots: list[HotspotData] = [] 1320 1321 # CH4増加量が閾値を超えるデータポイントを抽出 1322 enhanced_mask = df["ch4_ppm_delta"] >= ch4_enhance_threshold 1323 1324 if enhanced_mask.any(): 1325 lat = df["latitude"][enhanced_mask] 1326 lon = df["longitude"][enhanced_mask] 1327 ratios = df["c2c1_ratio_delta"][enhanced_mask] 1328 delta_ch4 = df["ch4_ppm_delta"][enhanced_mask] 1329 delta_c2h6 = df["c2h6_ppb_delta"][enhanced_mask] 1330 1331 # 各ポイントに対してホットスポットを作成 1332 for i in range(len(lat)): 1333 if pd.notna(ratios.iloc[i]): 1334 current_lat = lat.iloc[i] 1335 current_lon = lon.iloc[i] 1336 correlation = df["ch4_c2h6_correlation"].iloc[i] 1337 1338 # 比率に基づいてタイプを決定 1339 spot_type: HotspotType = "bio" 1340 if ratios.iloc[i] >= 100: 1341 spot_type = "comb" 1342 elif ratios.iloc[i] >= 5: 1343 spot_type = "gas" 1344 1345 angle: float = MobileSpatialAnalyzer._calculate_angle( 1346 lat=current_lat, 1347 lon=current_lon, 1348 center_lat=self._center_lat, 1349 center_lon=self._center_lon, 1350 ) 1351 section: int = self._determine_section(angle) 1352 1353 hotspots.append( 1354 HotspotData( 1355 source=ratios.index[i].strftime("%Y-%m-%d %H:%M:%S"), 1356 angle=angle, 1357 avg_lat=current_lat, 1358 avg_lon=current_lon, 1359 delta_ch4=delta_ch4.iloc[i], 1360 delta_c2h6=delta_c2h6.iloc[i], 1361 correlation=max(-1, min(1, correlation)), 1362 ratio=ratios.iloc[i], 1363 section=section, 1364 type=spot_type, 1365 ) 1366 ) 1367 1368 return hotspots 1369 1370 def _determine_section(self, angle: float) -> int: 1371 """ 1372 角度に基づいて所属する区画を特定します。 1373 1374 Parameters: 1375 ------ 1376 angle : float 1377 計算された角度 1378 1379 Returns: 1380 ------ 1381 int 1382 区画番号(0-based-index) 1383 """ 1384 for section_num, (start, end) in self._sections.items(): 1385 if start <= angle < end: 1386 return section_num 1387 # -180度の場合は最後の区画に含める 1388 return self._num_sections - 1 1389 1390 def _load_all_data( 1391 self, input_configs: list[MSAInputConfig] 1392 ) -> dict[str, pd.DataFrame]: 1393 """ 1394 全入力ファイルのデータを読み込み、データフレームの辞書を返します。 1395 1396 このメソッドは、指定された入力設定に基づいてすべてのデータファイルを読み込み、 1397 各ファイルのデータをデータフレームとして格納した辞書を生成します。 1398 1399 Parameters: 1400 ------ 1401 input_configs : list[MSAInputConfig] 1402 読み込むファイルの設定リスト。 1403 1404 Returns: 1405 ------ 1406 dict[str, pd.DataFrame] 1407 読み込まれたデータフレームの辞書。キーはファイル名、値はデータフレーム。 1408 """ 1409 all_data: dict[str, pd.DataFrame] = {} 1410 for config in input_configs: 1411 df, source_name = self._load_data(config) 1412 all_data[source_name] = df 1413 return all_data 1414 1415 def _load_data(self, config: MSAInputConfig) -> tuple[pd.DataFrame, str]: 1416 """ 1417 測定データを読み込み、前処理を行うメソッド。 1418 1419 Parameters: 1420 ------ 1421 config : MSAInputConfig 1422 入力ファイルの設定を含むオブジェクト。 1423 1424 Returns: 1425 ------ 1426 tuple[pd.DataFrame, str] 1427 読み込まれたデータフレームとそのソース名を含むタプル。 1428 """ 1429 source_name: str = Path(config.path).stem 1430 df: pd.DataFrame = pd.read_csv(config.path, na_values=["No Data", "nan"]) 1431 1432 # カラム名の標準化(測器に依存しない汎用的な名前に変更) 1433 df = df.rename(columns=self._column_mapping) 1434 df["timestamp"] = pd.to_datetime(df["timestamp"]) 1435 # インデックスを設定(元のtimestampカラムは保持) 1436 df = df.set_index("timestamp", drop=False) 1437 1438 # 緯度経度のnanを削除 1439 df = df.dropna(subset=["latitude", "longitude"]) 1440 1441 if config.lag < 0: 1442 raise ValueError( 1443 f"Invalid lag value: {config.lag}. Must be a non-negative float." 1444 ) 1445 1446 # 遅れ時間の補正 1447 columns_to_shift: list[str] = ["ch4_ppm", "c2h6_ppb", "h2o_ppm"] 1448 # サンプリング周波数に応じてシフト量を調整 1449 shift_periods: float = -config.lag * config.fs # fsを掛けて補正 1450 1451 for col in columns_to_shift: 1452 df[col] = df[col].shift(shift_periods) 1453 1454 df = df.dropna(subset=columns_to_shift) 1455 1456 # 水蒸気干渉などの補正式を適用 1457 if config.correction_type is not None: 1458 df = CorrectingUtils.correct_df_by_type(df, config.correction_type) 1459 else: 1460 self.logger.warn( 1461 f"'correction_type' is None, so no correction functions will be applied. Source: {source_name}" 1462 ) 1463 1464 return df, source_name 1465 1466 @staticmethod 1467 def _calculate_angle( 1468 lat: float, lon: float, center_lat: float, center_lon: float 1469 ) -> float: 1470 """ 1471 中心からの角度を計算 1472 1473 Parameters: 1474 ------ 1475 lat : float 1476 対象地点の緯度 1477 lon : float 1478 対象地点の経度 1479 center_lat : float 1480 中心の緯度 1481 center_lon : float 1482 中心の経度 1483 1484 Returns: 1485 ------ 1486 float 1487 真北を0°として時計回りの角度(-180°から180°) 1488 """ 1489 d_lat: float = lat - center_lat 1490 d_lon: float = lon - center_lon 1491 # arctanを使用して角度を計算(ラジアン) 1492 angle_rad: float = math.atan2(d_lon, d_lat) 1493 # ラジアンから度に変換(-180から180の範囲) 1494 angle_deg: float = math.degrees(angle_rad) 1495 return angle_deg 1496 1497 @classmethod 1498 def _calculate_distance( 1499 cls, lat1: float, lon1: float, lat2: float, lon2: float 1500 ) -> float: 1501 """ 1502 2点間の距離をメートル単位で計算(Haversine formula) 1503 1504 Parameters: 1505 ------ 1506 lat1 : float 1507 地点1の緯度 1508 lon1 : float 1509 地点1の経度 1510 lat2 : float 1511 地点2の緯度 1512 lon2 : float 1513 地点2の経度 1514 1515 Returns: 1516 ------ 1517 float 1518 2地点間の距離(メートル) 1519 """ 1520 R = cls.EARTH_RADIUS_METERS 1521 1522 # 緯度経度をラジアンに変換 1523 lat1_rad: float = math.radians(lat1) 1524 lon1_rad: float = math.radians(lon1) 1525 lat2_rad: float = math.radians(lat2) 1526 lon2_rad: float = math.radians(lon2) 1527 1528 # 緯度と経度の差分 1529 dlat: float = lat2_rad - lat1_rad 1530 dlon: float = lon2_rad - lon1_rad 1531 1532 # Haversine formula 1533 a: float = ( 1534 math.sin(dlat / 2) ** 2 1535 + math.cos(lat1_rad) * math.cos(lat2_rad) * math.sin(dlon / 2) ** 2 1536 ) 1537 c: float = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a)) 1538 1539 return R * c # メートル単位での距離 1540 1541 @staticmethod 1542 def _calculate_hotspots_parameters( 1543 df: pd.DataFrame, 1544 window_size: int, 1545 col_ch4_ppm: str = "ch4_ppm", 1546 col_c2h6_ppb: str = "c2h6_ppb", 1547 ch4_threshold: float = 0.05, 1548 c2h6_threshold: float = 0.0, 1549 ) -> pd.DataFrame: 1550 """ 1551 ホットスポットのパラメータを計算します。 1552 このメソッドは、指定されたデータフレームに対して移動平均や相関を計算し、 1553 各種のデルタ値や比率を追加します。これにより、ホットスポットの分析に必要な 1554 パラメータを整形します。 1555 1556 Parameters: 1557 ------ 1558 df : pd.DataFrame 1559 入力データフレーム 1560 window_size : int 1561 移動窓のサイズ 1562 col_ch4_ppm : str 1563 CH4濃度を示すカラム名 1564 col_c2h6_ppb : str 1565 C2H6濃度を示すカラム名 1566 ch4_threshold : float 1567 CH4の閾値 1568 c2h6_threshold : float 1569 C2H6の閾値 1570 1571 Returns: 1572 ------ 1573 pd.DataFrame 1574 計算されたパラメータを含むデータフレーム 1575 """ 1576 # 移動平均の計算 1577 df["ch4_ppm_mv"] = ( 1578 df[col_ch4_ppm] 1579 .rolling(window=window_size, center=True, min_periods=1) 1580 .mean() 1581 ) 1582 df["c2h6_ppb_mv"] = ( 1583 df[col_c2h6_ppb] 1584 .rolling(window=window_size, center=True, min_periods=1) 1585 .mean() 1586 ) 1587 1588 # 移動相関の計算 1589 df["ch4_c2h6_correlation"] = ( 1590 df[col_ch4_ppm] 1591 .rolling(window=window_size, min_periods=1) 1592 .corr(df[col_c2h6_ppb]) 1593 ) 1594 1595 # 移動平均からの偏差 1596 df["ch4_ppm_delta"] = df[col_ch4_ppm] - df["ch4_ppm_mv"] 1597 df["c2h6_ppb_delta"] = df[col_c2h6_ppb] - df["c2h6_ppb_mv"] 1598 1599 # C2H6/CH4の比率計算 1600 df["c2c1_ratio"] = df[col_c2h6_ppb] / df[col_ch4_ppm] 1601 1602 # デルタ値に基づく比の計算 1603 df["c2c1_ratio_delta"] = np.where( 1604 (df["ch4_ppm_delta"].abs() >= ch4_threshold) 1605 & (df["c2h6_ppb_delta"] >= c2h6_threshold), 1606 df["c2h6_ppb_delta"] / df["ch4_ppm_delta"], 1607 np.nan, 1608 ) 1609 1610 return df 1611 1612 @staticmethod 1613 def _calculate_window_size(window_minutes: float) -> int: 1614 """ 1615 時間窓からデータポイント数を計算 1616 1617 Parameters: 1618 ------ 1619 window_minutes : float 1620 時間窓の大きさ(分) 1621 1622 Returns: 1623 ------ 1624 int 1625 データポイント数 1626 """ 1627 return int(60 * window_minutes) 1628 1629 @staticmethod 1630 def _initialize_sections( 1631 num_sections: int, section_size: float 1632 ) -> dict[int, tuple[float, float]]: 1633 """ 1634 指定された区画数と区画サイズに基づいて、区画の範囲を初期化します。 1635 1636 Parameters: 1637 ------ 1638 num_sections : int 1639 初期化する区画の数。 1640 section_size : float 1641 各区画の角度範囲のサイズ。 1642 1643 Returns: 1644 ------ 1645 dict[int, tuple[float, float]] 1646 区画番号(0-based-index)とその範囲の辞書。各区画は-180度から180度の範囲に分割されます。 1647 """ 1648 sections: dict[int, tuple[float, float]] = {} 1649 for i in range(num_sections): 1650 # -180から180の範囲で区画を設定 1651 start_angle = -180 + i * section_size 1652 end_angle = -180 + (i + 1) * section_size 1653 sections[i] = (start_angle, end_angle) 1654 return sections 1655 1656 @staticmethod 1657 def _is_duplicate_spot( 1658 current_lat: float, 1659 current_lon: float, 1660 current_time: str, 1661 used_positions: list[tuple[float, float, str, float]], 1662 check_time_all: bool, 1663 min_time_threshold_seconds: float, 1664 max_time_threshold_hours: float, 1665 hotspot_area_meter: float, 1666 ) -> bool: 1667 """ 1668 与えられた地点が既存の地点と重複しているかを判定します。 1669 1670 Parameters: 1671 ------ 1672 current_lat : float 1673 判定する地点の緯度 1674 current_lon : float 1675 判定する地点の経度 1676 current_time : str 1677 判定する地点の時刻 1678 used_positions : list[tuple[float, float, str, float]] 1679 既存の地点情報のリスト (lat, lon, time, value) 1680 check_time_all : bool 1681 時間に関係なく重複チェックを行うかどうか 1682 min_time_threshold_seconds : float 1683 重複とみなす最小時間の閾値(秒) 1684 max_time_threshold_hours : float 1685 重複チェックを一時的に無視する最大時間の閾値(時間) 1686 hotspot_area_meter : float 1687 重複とみなす距離の閾値(m) 1688 1689 Returns: 1690 ------ 1691 bool 1692 重複している場合はTrue、そうでない場合はFalse 1693 """ 1694 for used_lat, used_lon, used_time, _ in used_positions: 1695 # 距離チェック 1696 distance = MobileSpatialAnalyzer._calculate_distance( 1697 lat1=current_lat, lon1=current_lon, lat2=used_lat, lon2=used_lon 1698 ) 1699 1700 if distance < hotspot_area_meter: 1701 # 時間差の計算(秒単位) 1702 time_diff = pd.Timedelta( 1703 pd.to_datetime(current_time) - pd.to_datetime(used_time) 1704 ).total_seconds() 1705 time_diff_abs = abs(time_diff) 1706 1707 if check_time_all: 1708 # 時間に関係なく、距離が近ければ重複とみなす 1709 return True 1710 else: 1711 # 時間窓による判定を行う 1712 if time_diff_abs <= min_time_threshold_seconds: 1713 # Case 1: 最小時間閾値以内は重複とみなす 1714 return True 1715 elif time_diff_abs > max_time_threshold_hours * 3600: 1716 # Case 2: 最大時間閾値を超えた場合は重複チェックをスキップ 1717 continue 1718 # Case 3: その間の時間差の場合は、距離が近ければ重複とみなす 1719 return True 1720 1721 return False 1722 1723 @staticmethod 1724 def _normalize_inputs( 1725 inputs: list[MSAInputConfig] | list[tuple[float, float, str | Path]], 1726 ) -> list[MSAInputConfig]: 1727 """ 1728 入力設定を標準化 1729 1730 Parameters: 1731 ------ 1732 inputs : list[MSAInputConfig] | list[tuple[float, float, str | Path]] 1733 入力設定のリスト 1734 1735 Returns: 1736 ------ 1737 list[MSAInputConfig] 1738 標準化された入力設定のリスト 1739 """ 1740 normalized: list[MSAInputConfig] = [] 1741 for inp in inputs: 1742 if isinstance(inp, MSAInputConfig): 1743 normalized.append(inp) # すでに検証済みのため、そのまま追加 1744 else: 1745 fs, lag, path = inp 1746 normalized.append( 1747 MSAInputConfig.validate_and_create(fs=fs, lag=lag, path=path) 1748 ) 1749 return normalized 1750 1751 def remove_c2c1_ratio_duplicates( 1752 self, 1753 df: pd.DataFrame, 1754 min_time_threshold_seconds: float = 300, # 5分以内は重複とみなす 1755 max_time_threshold_hours: float = 12.0, # 12時間以上離れている場合は別のポイントとして扱う 1756 check_time_all: bool = True, # 時間閾値を超えた場合の重複チェックを継続するかどうか 1757 hotspot_area_meter: float = 50.0, # 重複とみなす距離の閾値(メートル) 1758 col_ch4_ppm: str = "ch4_ppm", 1759 col_ch4_ppm_mv: str = "ch4_ppm_mv", 1760 col_ch4_ppm_delta: str = "ch4_ppm_delta", 1761 ): 1762 """ 1763 メタン濃度の増加が閾値を超えた地点から、重複を除外してユニークなホットスポットを抽出する関数。 1764 1765 Parameters: 1766 ------ 1767 df : pandas.DataFrame 1768 入力データフレーム。必須カラム: 1769 - ch4_ppm: メタン濃度(ppm) 1770 - ch4_ppm_mv: メタン濃度の移動平均(ppm) 1771 - ch4_ppm_delta: メタン濃度の増加量(ppm) 1772 - latitude: 緯度 1773 - longitude: 経度 1774 min_time_threshold_seconds : float, optional 1775 重複とみなす最小時間差(秒)。デフォルトは300秒(5分)。 1776 max_time_threshold_hours : float, optional 1777 別ポイントとして扱う最大時間差(時間)。デフォルトは12時間。 1778 check_time_all : bool, optional 1779 時間閾値を超えた場合の重複チェックを継続するかどうか。デフォルトはTrue。 1780 hotspot_area_meter : float, optional 1781 重複とみなす距離の閾値(メートル)。デフォルトは50メートル。 1782 1783 Returns: 1784 ------ 1785 pandas.DataFrame 1786 ユニークなホットスポットのデータフレーム。 1787 """ 1788 df_data: pd.DataFrame = df.copy() 1789 # メタン濃度の増加が閾値を超えた点を抽出 1790 mask = ( 1791 df_data[col_ch4_ppm] - df_data[col_ch4_ppm_mv] > self._ch4_enhance_threshold 1792 ) 1793 hotspot_candidates = df_data[mask].copy() 1794 1795 # ΔCH4の降順でソート 1796 sorted_hotspots = hotspot_candidates.sort_values( 1797 by=col_ch4_ppm_delta, ascending=False 1798 ) 1799 used_positions = [] 1800 unique_hotspots = pd.DataFrame() 1801 1802 for _, spot in sorted_hotspots.iterrows(): 1803 should_add = True 1804 for used_lat, used_lon, used_time in used_positions: 1805 # 距離チェック 1806 distance = geodesic( 1807 (spot.latitude, spot.longitude), (used_lat, used_lon) 1808 ).meters 1809 1810 if distance < hotspot_area_meter: 1811 # 時間差の計算(秒単位) 1812 time_diff = pd.Timedelta( 1813 spot.name - pd.to_datetime(used_time) 1814 ).total_seconds() 1815 time_diff_abs = abs(time_diff) 1816 1817 # 時間差に基づく判定 1818 if check_time_all: 1819 # 時間に関係なく、距離が近ければ重複とみなす 1820 # ΔCH4が大きい方を残す(現在のスポットは必ず小さい) 1821 should_add = False 1822 break 1823 else: 1824 # 時間窓による判定を行う 1825 if time_diff_abs <= min_time_threshold_seconds: 1826 # Case 1: 最小時間閾値以内は重複とみなす 1827 should_add = False 1828 break 1829 elif time_diff_abs > max_time_threshold_hours * 3600: 1830 # Case 2: 最大時間閾値を超えた場合は重複チェックをスキップ 1831 continue 1832 # Case 3: その間の時間差の場合は、距離が近ければ重複とみなす 1833 should_add = False 1834 break 1835 1836 if should_add: 1837 unique_hotspots = pd.concat([unique_hotspots, pd.DataFrame([spot])]) 1838 used_positions.append((spot.latitude, spot.longitude, spot.name)) 1839 1840 return unique_hotspots 1841 1842 @staticmethod 1843 def remove_hotspots_duplicates( 1844 hotspots: list[HotspotData], 1845 check_time_all: bool, 1846 min_time_threshold_seconds: float = 300, 1847 max_time_threshold_hours: float = 12, 1848 hotspot_area_meter: float = 50, 1849 ) -> list[HotspotData]: 1850 """ 1851 重複するホットスポットを除外します。 1852 1853 このメソッドは、与えられたホットスポットのリストから重複を検出し、 1854 一意のホットスポットのみを返します。重複の判定は、指定された 1855 時間および距離の閾値に基づいて行われます。 1856 1857 Parameters: 1858 ------ 1859 hotspots : list[HotspotData] 1860 重複を除外する対象のホットスポットのリスト。 1861 check_time_all : bool 1862 時間に関係なく重複チェックを行うかどうか。 1863 min_time_threshold_seconds : float 1864 重複とみなす最小時間の閾値(秒)。 1865 max_time_threshold_hours : float 1866 重複チェックを一時的に無視する最大時間の閾値(時間)。 1867 hotspot_area_meter : float 1868 重複とみなす距離の閾値(メートル)。 1869 1870 Returns: 1871 ------ 1872 list[HotspotData] 1873 重複を除去したホットスポットのリスト。 1874 """ 1875 # ΔCH4の降順でソート 1876 sorted_hotspots: list[HotspotData] = sorted( 1877 hotspots, key=lambda x: x.delta_ch4, reverse=True 1878 ) 1879 used_positions_by_type: dict[ 1880 HotspotType, list[tuple[float, float, str, float]] 1881 ] = { 1882 "bio": [], 1883 "gas": [], 1884 "comb": [], 1885 } 1886 unique_hotspots: list[HotspotData] = [] 1887 1888 for spot in sorted_hotspots: 1889 is_duplicate = MobileSpatialAnalyzer._is_duplicate_spot( 1890 current_lat=spot.avg_lat, 1891 current_lon=spot.avg_lon, 1892 current_time=spot.source, 1893 used_positions=used_positions_by_type[spot.type], 1894 check_time_all=check_time_all, 1895 min_time_threshold_seconds=min_time_threshold_seconds, 1896 max_time_threshold_hours=max_time_threshold_hours, 1897 hotspot_area_meter=hotspot_area_meter, 1898 ) 1899 1900 if not is_duplicate: 1901 unique_hotspots.append(spot) 1902 used_positions_by_type[spot.type].append( 1903 (spot.avg_lat, spot.avg_lon, spot.source, spot.delta_ch4) 1904 ) 1905 1906 return unique_hotspots 1907 1908 @staticmethod 1909 def setup_logger(logger: Logger | None, log_level: int = INFO) -> Logger: 1910 """ 1911 ロガーを設定します。 1912 1913 このメソッドは、ロギングの設定を行い、ログメッセージのフォーマットを指定します。 1914 ログメッセージには、日付、ログレベル、メッセージが含まれます。 1915 1916 渡されたロガーがNoneまたは不正な場合は、新たにロガーを作成し、標準出力に 1917 ログメッセージが表示されるようにStreamHandlerを追加します。ロガーのレベルは 1918 引数で指定されたlog_levelに基づいて設定されます。 1919 1920 Parameters: 1921 ------ 1922 logger : Logger | None 1923 使用するロガー。Noneの場合は新しいロガーを作成します。 1924 log_level : int 1925 ロガーのログレベル。デフォルトはINFO。 1926 1927 Returns: 1928 ------ 1929 Logger 1930 設定されたロガーオブジェクト。 1931 """ 1932 if logger is not None and isinstance(logger, Logger): 1933 return logger 1934 # 渡されたロガーがNoneまたは正しいものでない場合は独自に設定 1935 new_logger: Logger = getLogger() 1936 # 既存のハンドラーをすべて削除 1937 for handler in new_logger.handlers[:]: 1938 new_logger.removeHandler(handler) 1939 new_logger.setLevel(log_level) # ロガーのレベルを設定 1940 ch = StreamHandler() 1941 ch_formatter = Formatter("%(asctime)s - %(levelname)s - %(message)s") 1942 ch.setFormatter(ch_formatter) # フォーマッターをハンドラーに設定 1943 new_logger.addHandler(ch) # StreamHandlerの追加 1944 return new_logger 1945 1946 @staticmethod 1947 def calculate_emission_rates( 1948 hotspots: list[HotspotData], 1949 method: Literal["weller", "weitzel", "joo", "umezawa"] = "weller", 1950 print_summary: bool = True, 1951 custom_formulas: dict[str, dict[str, float]] | None = None, 1952 ) -> tuple[list[EmissionData], dict[str, dict[str, float]]]: 1953 """ 1954 検出されたホットスポットのCH4漏出量を計算・解析し、統計情報を生成します。 1955 1956 Parameters: 1957 ------ 1958 hotspots : list[HotspotData] 1959 分析対象のホットスポットのリスト 1960 method : Literal["weller", "weitzel", "joo", "umezawa"] 1961 使用する計算式。デフォルトは"weller"。 1962 print_summary : bool 1963 統計情報を表示するかどうか。デフォルトはTrue。 1964 custom_formulas : dict[str, dict[str, float]] | None 1965 カスタム計算式の係数。 1966 例: {"custom_method": {"a": 1.0, "b": 1.0}} 1967 Noneの場合はデフォルトの計算式を使用。 1968 1969 Returns: 1970 ------ 1971 tuple[list[EmissionData], dict[str, dict[str, float]]] 1972 - 各ホットスポットの排出量データを含むリスト 1973 - タイプ別の統計情報を含む辞書 1974 """ 1975 # デフォルトの経験式係数 1976 default_formulas = { 1977 "weller": {"a": 0.988, "b": 0.817}, 1978 "weitzel": {"a": 0.521, "b": 0.795}, 1979 "joo": {"a": 2.738, "b": 1.329}, 1980 "umezawa": {"a": 2.716, "b": 0.741}, 1981 } 1982 1983 # カスタム計算式がある場合は追加 1984 emission_formulas = default_formulas.copy() 1985 if custom_formulas: 1986 emission_formulas.update(custom_formulas) 1987 1988 if method not in emission_formulas: 1989 raise ValueError(f"Unknown method: {method}") 1990 1991 # 係数の取得 1992 a = emission_formulas[method]["a"] 1993 b = emission_formulas[method]["b"] 1994 1995 # 排出量の計算 1996 emission_data_list = [] 1997 for spot in hotspots: 1998 # 漏出量の計算 (L/min) 1999 emission_rate = np.exp((np.log(spot.delta_ch4) + a) / b) 2000 # 日排出量 (L/day) 2001 daily_emission = emission_rate * 60 * 24 2002 # 年間排出量 (L/year) 2003 annual_emission = daily_emission * 365 2004 2005 emission_data = EmissionData( 2006 source=spot.source, 2007 type=spot.type, 2008 section=spot.section, 2009 latitude=spot.avg_lat, 2010 longitude=spot.avg_lon, 2011 delta_ch4=spot.delta_ch4, 2012 delta_c2h6=spot.delta_c2h6, 2013 ratio=spot.ratio, 2014 emission_rate=emission_rate, 2015 daily_emission=daily_emission, 2016 annual_emission=annual_emission, 2017 ) 2018 emission_data_list.append(emission_data) 2019 2020 # 統計計算用にDataFrameを作成 2021 emission_df = pd.DataFrame([e.to_dict() for e in emission_data_list]) 2022 2023 # タイプ別の統計情報を計算 2024 stats = {} 2025 # emission_formulas の定義の後に、排出量カテゴリーの閾値を定義 2026 emission_categories = { 2027 "low": {"min": 0, "max": 6}, # < 6 L/min 2028 "medium": {"min": 6, "max": 40}, # 6-40 L/min 2029 "high": {"min": 40, "max": float("inf")}, # > 40 L/min 2030 } 2031 # get_args(HotspotType)を使用して型安全なリストを作成 2032 types = list(get_args(HotspotType)) 2033 for spot_type in types: 2034 df_type = emission_df[emission_df["type"] == spot_type] 2035 if len(df_type) > 0: 2036 # 既存の統計情報を計算 2037 type_stats = { 2038 "count": len(df_type), 2039 "emission_rate_min": df_type["emission_rate"].min(), 2040 "emission_rate_max": df_type["emission_rate"].max(), 2041 "emission_rate_mean": df_type["emission_rate"].mean(), 2042 "emission_rate_median": df_type["emission_rate"].median(), 2043 "total_annual_emission": df_type["annual_emission"].sum(), 2044 "mean_annual_emission": df_type["annual_emission"].mean(), 2045 } 2046 2047 # 排出量カテゴリー別の統計を追加 2048 category_counts = { 2049 "low": len( 2050 df_type[ 2051 df_type["emission_rate"] < emission_categories["low"]["max"] 2052 ] 2053 ), 2054 "medium": len( 2055 df_type[ 2056 ( 2057 df_type["emission_rate"] 2058 >= emission_categories["medium"]["min"] 2059 ) 2060 & ( 2061 df_type["emission_rate"] 2062 < emission_categories["medium"]["max"] 2063 ) 2064 ] 2065 ), 2066 "high": len( 2067 df_type[ 2068 df_type["emission_rate"] 2069 >= emission_categories["high"]["min"] 2070 ] 2071 ), 2072 } 2073 type_stats["emission_categories"] = category_counts 2074 2075 stats[spot_type] = type_stats 2076 2077 if print_summary: 2078 print(f"\n{spot_type}タイプの統計情報:") 2079 print(f" 検出数: {type_stats['count']}") 2080 print(" 排出量 (L/min):") 2081 print(f" 最小値: {type_stats['emission_rate_min']:.2f}") 2082 print(f" 最大値: {type_stats['emission_rate_max']:.2f}") 2083 print(f" 平均値: {type_stats['emission_rate_mean']:.2f}") 2084 print(f" 中央値: {type_stats['emission_rate_median']:.2f}") 2085 print(" 排出量カテゴリー別の検出数:") 2086 print(f" 低放出 (< 6 L/min): {category_counts['low']}") 2087 print(f" 中放出 (6-40 L/min): {category_counts['medium']}") 2088 print(f" 高放出 (> 40 L/min): {category_counts['high']}") 2089 print(" 年間排出量 (L/year):") 2090 print(f" 合計: {type_stats['total_annual_emission']:.2f}") 2091 print(f" 平均: {type_stats['mean_annual_emission']:.2f}") 2092 2093 return emission_data_list, stats 2094 2095 @staticmethod 2096 def plot_emission_analysis( 2097 emission_data_list: list[EmissionData], 2098 dpi: int = 300, 2099 output_dir: str | Path | None = None, 2100 output_filename: str = "emission_analysis.png", 2101 figsize: tuple[float, float] = (12, 5), 2102 add_legend: bool = True, 2103 hist_log_y: bool = False, 2104 hist_xlim: tuple[float, float] | None = None, 2105 hist_ylim: tuple[float, float] | None = None, 2106 scatter_xlim: tuple[float, float] | None = None, 2107 scatter_ylim: tuple[float, float] | None = None, 2108 hist_bin_width: float = 0.5, 2109 print_summary: bool = True, 2110 save_fig: bool = False, 2111 show_fig: bool = True, 2112 show_scatter: bool = True, # 散布図の表示を制御するオプションを追加 2113 ) -> None: 2114 """ 2115 排出量分析のプロットを作成する静的メソッド。 2116 2117 Parameters: 2118 ------ 2119 emission_data_list : list[EmissionData] 2120 EmissionDataオブジェクトのリスト。 2121 output_dir : str | Path | None 2122 出力先ディレクトリのパス。 2123 output_filename : str 2124 保存するファイル名。デフォルトは"emission_analysis.png"。 2125 dpi : int 2126 プロットの解像度。デフォルトは300。 2127 figsize : tuple[float, float] 2128 プロットのサイズ。デフォルトは(12, 5)。 2129 add_legend : bool 2130 凡例を追加するかどうか。デフォルトはTrue。 2131 hist_log_y : bool 2132 ヒストグラムのy軸を対数スケールにするかどうか。デフォルトはFalse。 2133 hist_xlim : tuple[float, float] | None 2134 ヒストグラムのx軸の範囲。デフォルトはNone。 2135 hist_ylim : tuple[float, float] | None 2136 ヒストグラムのy軸の範囲。デフォルトはNone。 2137 scatter_xlim : tuple[float, float] | None 2138 散布図のx軸の範囲。デフォルトはNone。 2139 scatter_ylim : tuple[float, float] | None 2140 散布図のy軸の範囲。デフォルトはNone。 2141 hist_bin_width : float 2142 ヒストグラムのビンの幅。デフォルトは0.5。 2143 print_summary : bool 2144 集計結果を表示するかどうか。デフォルトはFalse。 2145 save_fig : bool 2146 図をファイルに保存するかどうか。デフォルトはFalse。 2147 show_fig : bool 2148 図を表示するかどうか。デフォルトはTrue。 2149 show_scatter : bool 2150 散布図(右図)を表示するかどうか。デフォルトはTrue。 2151 """ 2152 # データをDataFrameに変換 2153 df = pd.DataFrame([e.to_dict() for e in emission_data_list]) 2154 2155 # プロットの作成(散布図の有無に応じてサブプロット数を調整) 2156 if show_scatter: 2157 fig, (ax1, ax2) = plt.subplots(1, 2, figsize=figsize) 2158 axes = [ax1, ax2] 2159 else: 2160 fig, ax1 = plt.subplots(1, 1, figsize=(figsize[0] // 2, figsize[1])) 2161 axes = [ax1] 2162 2163 # カラーマップの定義 2164 colors: dict[HotspotType, str] = {"bio": "blue", "gas": "red", "comb": "green"} 2165 2166 # 存在するタイプを確認 2167 # HotspotTypeの定義順を基準にソート 2168 hotspot_types = list(get_args(HotspotType)) 2169 existing_types = sorted( 2170 df["type"].unique(), key=lambda x: hotspot_types.index(x) 2171 ) 2172 2173 # 左側: ヒストグラム 2174 # ビンの範囲を設定 2175 start = 0 # 必ず0から開始 2176 if hist_xlim is not None: 2177 end = hist_xlim[1] 2178 else: 2179 end = np.ceil(df["emission_rate"].max() * 1.05) 2180 2181 # ビン数を計算(end値をbin_widthで割り切れるように調整) 2182 n_bins = int(np.ceil(end / hist_bin_width)) 2183 end = n_bins * hist_bin_width 2184 2185 # ビンの生成(0から開始し、bin_widthの倍数で区切る) 2186 bins = np.linspace(start, end, n_bins + 1) 2187 2188 # タイプごとにヒストグラムを積み上げ 2189 bottom = np.zeros(len(bins) - 1) 2190 for spot_type in existing_types: 2191 data = df[df["type"] == spot_type]["emission_rate"] 2192 if len(data) > 0: 2193 counts, _ = np.histogram(data, bins=bins) 2194 ax1.bar( 2195 bins[:-1], 2196 counts, 2197 width=hist_bin_width, 2198 bottom=bottom, 2199 alpha=0.6, 2200 label=spot_type, 2201 color=colors[spot_type], 2202 ) 2203 bottom += counts 2204 2205 ax1.set_xlabel("CH$_4$ Emission (L min$^{-1}$)") 2206 ax1.set_ylabel("Frequency") 2207 if hist_log_y: 2208 # ax1.set_yscale("log") 2209 # 非線形スケールを設定(linthreshで線形から対数への遷移点を指定) 2210 ax1.set_yscale("symlog", linthresh=1.0) 2211 if hist_xlim is not None: 2212 ax1.set_xlim(hist_xlim) 2213 else: 2214 ax1.set_xlim(0, np.ceil(df["emission_rate"].max() * 1.05)) 2215 2216 if hist_ylim is not None: 2217 ax1.set_ylim(hist_ylim) 2218 else: 2219 ax1.set_ylim(0, ax1.get_ylim()[1]) # 下限を0に設定 2220 2221 if show_scatter: 2222 # 右側: 散布図 2223 for spot_type in existing_types: 2224 mask = df["type"] == spot_type 2225 ax2.scatter( 2226 df[mask]["emission_rate"], 2227 df[mask]["delta_ch4"], 2228 alpha=0.6, 2229 label=spot_type, 2230 color=colors[spot_type], 2231 ) 2232 2233 ax2.set_xlabel("Emission Rate (L min$^{-1}$)") 2234 ax2.set_ylabel("ΔCH$_4$ (ppm)") 2235 if scatter_xlim is not None: 2236 ax2.set_xlim(scatter_xlim) 2237 else: 2238 ax2.set_xlim(0, np.ceil(df["emission_rate"].max() * 1.05)) 2239 2240 if scatter_ylim is not None: 2241 ax2.set_ylim(scatter_ylim) 2242 else: 2243 ax2.set_ylim(0, np.ceil(df["delta_ch4"].max() * 1.05)) 2244 2245 # 凡例の表示 2246 if add_legend: 2247 for ax in axes: 2248 ax.legend( 2249 bbox_to_anchor=(0.5, -0.30), 2250 loc="upper center", 2251 ncol=len(existing_types), 2252 ) 2253 2254 plt.tight_layout() 2255 2256 # 図の保存 2257 if save_fig: 2258 if output_dir is None: 2259 raise ValueError( 2260 "save_fig=Trueの場合、output_dirを指定する必要があります。有効なディレクトリパスを指定してください。" 2261 ) 2262 os.makedirs(output_dir, exist_ok=True) 2263 output_path = os.path.join(output_dir, output_filename) 2264 plt.savefig(output_path, bbox_inches="tight", dpi=dpi) 2265 # 図の表示 2266 if show_fig: 2267 plt.show() 2268 else: 2269 plt.close(fig=fig) 2270 2271 if print_summary: 2272 # デバッグ用の出力 2273 print("\nビンごとの集計:") 2274 print(f"{'Range':>12} | {'bio':>8} | {'gas':>8} | {'total':>8}") 2275 print("-" * 50) 2276 2277 for i in range(len(bins) - 1): 2278 bin_start = bins[i] 2279 bin_end = bins[i + 1] 2280 2281 # 各タイプのカウントを計算 2282 counts_by_type: dict[HotspotType, int] = {"bio": 0, "gas": 0, "comb": 0} 2283 total = 0 2284 for spot_type in existing_types: 2285 mask = ( 2286 (df["type"] == spot_type) 2287 & (df["emission_rate"] >= bin_start) 2288 & (df["emission_rate"] < bin_end) 2289 ) 2290 count = len(df[mask]) 2291 counts_by_type[spot_type] = count 2292 total += count 2293 2294 # カウントが0の場合はスキップ 2295 if total > 0: 2296 range_str = f"{bin_start:5.1f}-{bin_end:<5.1f}" 2297 bio_count = counts_by_type.get("bio", 0) 2298 gas_count = counts_by_type.get("gas", 0) 2299 print( 2300 f"{range_str:>12} | {bio_count:8d} | {gas_count:8d} | {total:8d}" 2301 )
移動観測で得られた測定データを解析するクラス
265 def __init__( 266 self, 267 center_lat: float, 268 center_lon: float, 269 inputs: list[MSAInputConfig] | list[tuple[float, float, str | Path]], 270 num_sections: int = 4, 271 ch4_enhance_threshold: float = 0.1, 272 correlation_threshold: float = 0.7, 273 hotspot_area_meter: float = 50, 274 window_minutes: float = 5, 275 column_mapping: dict[str, str] = { 276 "Time Stamp": "timestamp", 277 "CH4 (ppm)": "ch4_ppm", 278 "C2H6 (ppb)": "c2h6_ppb", 279 "H2O (ppm)": "h2o_ppm", 280 "Latitude": "latitude", 281 "Longitude": "longitude", 282 }, 283 logger: Logger | None = None, 284 logging_debug: bool = False, 285 ): 286 """ 287 測定データ解析クラスの初期化 288 289 Parameters: 290 ------ 291 center_lat : float 292 中心緯度 293 center_lon : float 294 中心経度 295 inputs : list[MSAInputConfig] | list[tuple[float, float, str | Path]] 296 入力ファイルのリスト 297 num_sections : int 298 分割する区画数。デフォルトは4。 299 ch4_enhance_threshold : float 300 CH4増加の閾値(ppm)。デフォルトは0.1。 301 correlation_threshold : float 302 相関係数の閾値。デフォルトは0.7。 303 hotspot_area_meter : float 304 ホットスポットの検出に使用するエリアの半径(メートル)。デフォルトは50メートル。 305 window_minutes : float 306 移動窓の大きさ(分)。デフォルトは5分。 307 column_mapping : dict[str, str] 308 元のデータファイルのヘッダーを汎用的な単語に変換するための辞書型データ。 309 - timestamp,ch4_ppm,c2h6_ppm,h2o_ppm,latitude,longitudeをvalueに、それぞれに対応するカラム名をcolに指定してください。 310 logger : Logger | None 311 使用するロガー。Noneの場合は新しいロガーを作成します。 312 logging_debug : bool 313 ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。 314 315 Returns: 316 ------ 317 None 318 初期化処理が完了したことを示します。 319 """ 320 # ロガー 321 log_level: int = INFO 322 if logging_debug: 323 log_level = DEBUG 324 self.logger: Logger = MobileSpatialAnalyzer.setup_logger(logger, log_level) 325 # プライベートなプロパティ 326 self._center_lat: float = center_lat 327 self._center_lon: float = center_lon 328 self._ch4_enhance_threshold: float = ch4_enhance_threshold 329 self._correlation_threshold: float = correlation_threshold 330 self._hotspot_area_meter: float = hotspot_area_meter 331 self._column_mapping: dict[str, str] = column_mapping 332 self._num_sections: int = num_sections 333 # セクションの範囲 334 section_size: float = 360 / num_sections 335 self._section_size: float = section_size 336 self._sections = MobileSpatialAnalyzer._initialize_sections( 337 num_sections, section_size 338 ) 339 # window_sizeをデータポイント数に変換(分→秒→データポイント数) 340 self._window_size: int = MobileSpatialAnalyzer._calculate_window_size( 341 window_minutes 342 ) 343 # 入力設定の標準化 344 normalized_input_configs: list[MSAInputConfig] = ( 345 MobileSpatialAnalyzer._normalize_inputs(inputs) 346 ) 347 # 複数ファイルのデータを読み込み 348 self._data: dict[str, pd.DataFrame] = self._load_all_data( 349 normalized_input_configs 350 )
測定データ解析クラスの初期化
Parameters:
center_lat : float
中心緯度
center_lon : float
中心経度
inputs : list[MSAInputConfig] | list[tuple[float, float, str | Path]]
入力ファイルのリスト
num_sections : int
分割する区画数。デフォルトは4。
ch4_enhance_threshold : float
CH4増加の閾値(ppm)。デフォルトは0.1。
correlation_threshold : float
相関係数の閾値。デフォルトは0.7。
hotspot_area_meter : float
ホットスポットの検出に使用するエリアの半径(メートル)。デフォルトは50メートル。
window_minutes : float
移動窓の大きさ(分)。デフォルトは5分。
column_mapping : dict[str, str]
元のデータファイルのヘッダーを汎用的な単語に変換するための辞書型データ。
- timestamp,ch4_ppm,c2h6_ppm,h2o_ppm,latitude,longitudeをvalueに、それぞれに対応するカラム名をcolに指定してください。
logger : Logger | None
使用するロガー。Noneの場合は新しいロガーを作成します。
logging_debug : bool
ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。
Returns:
None
初期化処理が完了したことを示します。
352 def analyze_delta_ch4_stats(self, hotspots: list[HotspotData]) -> None: 353 """ 354 各タイプのホットスポットについてΔCH4の統計情報を計算し、結果を表示します。 355 356 Parameters: 357 ------ 358 hotspots : list[HotspotData] 359 分析対象のホットスポットリスト 360 361 Returns: 362 ------ 363 None 364 統計情報の表示が完了したことを示します。 365 """ 366 # タイプごとにホットスポットを分類 367 hotspots_by_type: dict[HotspotType, list[HotspotData]] = { 368 "bio": [h for h in hotspots if h.type == "bio"], 369 "gas": [h for h in hotspots if h.type == "gas"], 370 "comb": [h for h in hotspots if h.type == "comb"], 371 } 372 373 # 統計情報を計算し、表示 374 for spot_type, spots in hotspots_by_type.items(): 375 if spots: 376 delta_ch4_values = [spot.delta_ch4 for spot in spots] 377 max_value = max(delta_ch4_values) 378 mean_value = sum(delta_ch4_values) / len(delta_ch4_values) 379 median_value = sorted(delta_ch4_values)[len(delta_ch4_values) // 2] 380 print(f"{spot_type}タイプのホットスポットの統計情報:") 381 print(f" 最大値: {max_value}") 382 print(f" 平均値: {mean_value}") 383 print(f" 中央値: {median_value}") 384 else: 385 print(f"{spot_type}タイプのホットスポットは存在しません。")
各タイプのホットスポットについてΔCH4の統計情報を計算し、結果を表示します。
Parameters:
hotspots : list[HotspotData]
分析対象のホットスポットリスト
Returns:
None
統計情報の表示が完了したことを示します。
387 def analyze_hotspots( 388 self, 389 duplicate_check_mode: str = "none", 390 min_time_threshold_seconds: float = 300, 391 max_time_threshold_hours: float = 12, 392 ) -> list[HotspotData]: 393 """ 394 ホットスポットを検出して分析します。 395 396 Parameters: 397 ------ 398 duplicate_check_mode : str 399 重複チェックのモード("none","time_window","time_all")。 400 - "none": 重複チェックを行わない。 401 - "time_window": 指定された時間窓内の重複のみを除外。 402 - "time_all": すべての時間範囲で重複チェックを行う。 403 min_time_threshold_seconds : float 404 重複とみなす最小時間の閾値(秒)。デフォルトは300秒。 405 max_time_threshold_hours : float 406 重複チェックを一時的に無視する最大時間の閾値(時間)。デフォルトは12時間。 407 408 Returns: 409 ------ 410 list[HotspotData] 411 検出されたホットスポットのリスト。 412 """ 413 # 不正な入力値に対するエラーチェック 414 valid_modes = {"none", "time_window", "time_all"} 415 if duplicate_check_mode not in valid_modes: 416 raise ValueError( 417 f"無効な重複チェックモード: {duplicate_check_mode}. 有効な値は {valid_modes} です。" 418 ) 419 420 all_hotspots: list[HotspotData] = [] 421 422 # 各データソースに対して解析を実行 423 for _, df in self._data.items(): 424 # パラメータの計算 425 df = MobileSpatialAnalyzer._calculate_hotspots_parameters( 426 df, self._window_size 427 ) 428 429 # ホットスポットの検出 430 hotspots: list[HotspotData] = self._detect_hotspots( 431 df, 432 ch4_enhance_threshold=self._ch4_enhance_threshold, 433 ) 434 all_hotspots.extend(hotspots) 435 436 # 重複チェックモードに応じて処理 437 if duplicate_check_mode != "none": 438 unique_hotspots = MobileSpatialAnalyzer.remove_hotspots_duplicates( 439 all_hotspots, 440 check_time_all=duplicate_check_mode == "time_all", 441 min_time_threshold_seconds=min_time_threshold_seconds, 442 max_time_threshold_hours=max_time_threshold_hours, 443 hotspot_area_meter=self._hotspot_area_meter, 444 ) 445 self.logger.info( 446 f"重複除外: {len(all_hotspots)} → {len(unique_hotspots)} ホットスポット" 447 ) 448 return unique_hotspots 449 450 return all_hotspots
ホットスポットを検出して分析します。
Parameters:
duplicate_check_mode : str
重複チェックのモード("none","time_window","time_all")。
- "none": 重複チェックを行わない。
- "time_window": 指定された時間窓内の重複のみを除外。
- "time_all": すべての時間範囲で重複チェックを行う。
min_time_threshold_seconds : float
重複とみなす最小時間の閾値(秒)。デフォルトは300秒。
max_time_threshold_hours : float
重複チェックを一時的に無視する最大時間の閾値(時間)。デフォルトは12時間。
Returns:
list[HotspotData]
検出されたホットスポットのリスト。
452 def calculate_measurement_stats( 453 self, 454 print_individual_stats: bool = True, 455 print_total_stats: bool = True, 456 ) -> tuple[float, timedelta]: 457 """ 458 各ファイルの測定時間と走行距離を計算し、合計を返します。 459 460 Parameters: 461 ------ 462 print_individual_stats : bool 463 個別ファイルの統計を表示するかどうか。デフォルトはTrue。 464 print_total_stats : bool 465 合計統計を表示するかどうか。デフォルトはTrue。 466 467 Returns: 468 ------ 469 tuple[float, timedelta] 470 総距離(km)と総時間のタプル 471 """ 472 total_distance: float = 0.0 473 total_time: timedelta = timedelta() 474 individual_stats: list[dict] = [] # 個別の統計情報を保存するリスト 475 476 # プログレスバーを表示しながら計算 477 for source_name, df in tqdm( 478 self._data.items(), desc="Calculating", unit="file" 479 ): 480 # 時間の計算 481 time_spent = df.index[-1] - df.index[0] 482 483 # 距離の計算 484 distance_km = 0.0 485 for i in range(len(df) - 1): 486 lat1, lon1 = df.iloc[i][["latitude", "longitude"]] 487 lat2, lon2 = df.iloc[i + 1][["latitude", "longitude"]] 488 distance_km += ( 489 MobileSpatialAnalyzer._calculate_distance( 490 lat1=lat1, lon1=lon1, lat2=lat2, lon2=lon2 491 ) 492 / 1000 493 ) 494 495 # 合計に加算 496 total_distance += distance_km 497 total_time += time_spent 498 499 # 統計情報を保存 500 if print_individual_stats: 501 average_speed = distance_km / (time_spent.total_seconds() / 3600) 502 individual_stats.append( 503 { 504 "source": source_name, 505 "distance": distance_km, 506 "time": time_spent, 507 "speed": average_speed, 508 } 509 ) 510 511 # 計算完了後に統計情報を表示 512 if print_individual_stats: 513 self.logger.info("=== Individual Stats ===") 514 for stat in individual_stats: 515 print(f"File : {stat['source']}") 516 print(f" Distance : {stat['distance']:.2f} km") 517 print(f" Time : {stat['time']}") 518 print(f" Avg. Speed : {stat['speed']:.1f} km/h\n") 519 520 # 合計を表示 521 if print_total_stats: 522 average_speed_total: float = total_distance / ( 523 total_time.total_seconds() / 3600 524 ) 525 self.logger.info("=== Total Stats ===") 526 print(f" Distance : {total_distance:.2f} km") 527 print(f" Time : {total_time}") 528 print(f" Avg. Speed : {average_speed_total:.1f} km/h\n") 529 530 return total_distance, total_time
各ファイルの測定時間と走行距離を計算し、合計を返します。
Parameters:
print_individual_stats : bool
個別ファイルの統計を表示するかどうか。デフォルトはTrue。
print_total_stats : bool
合計統計を表示するかどうか。デフォルトはTrue。
Returns:
tuple[float, timedelta]
総距離(km)と総時間のタプル
532 def create_hotspots_map( 533 self, 534 hotspots: list[HotspotData], 535 output_dir: str | Path | None = None, 536 output_filename: str = "hotspots_map.html", 537 center_marker_label: str = "Center", 538 plot_center_marker: bool = True, 539 radius_meters: float = 3000, 540 save_fig: bool = True, 541 ) -> None: 542 """ 543 ホットスポットの分布を地図上にプロットして保存 544 545 Parameters: 546 ------ 547 hotspots : list[HotspotData] 548 プロットするホットスポットのリスト 549 output_dir : str | Path 550 保存先のディレクトリパス 551 output_filename : str 552 保存するファイル名。デフォルトは"hotspots_map"。 553 center_marker_label : str 554 中心を示すマーカーのラベルテキスト。デフォルトは"Center"。 555 plot_center_marker : bool 556 中心を示すマーカーの有無。デフォルトはTrue。 557 radius_meters : float 558 区画分けを示す線の長さ。デフォルトは3000。 559 save_fig : bool 560 図の保存を許可するフラグ。デフォルトはTrue。 561 """ 562 # 地図の作成 563 m = folium.Map( 564 location=[self._center_lat, self._center_lon], 565 zoom_start=15, 566 tiles="OpenStreetMap", 567 ) 568 569 # ホットスポットの種類ごとに異なる色でプロット 570 for spot in hotspots: 571 # NaN値チェックを追加 572 if math.isnan(spot.avg_lat) or math.isnan(spot.avg_lon): 573 continue 574 575 # default type 576 color = "black" 577 # タイプに応じて色を設定 578 if spot.type == "comb": 579 color = "green" 580 elif spot.type == "gas": 581 color = "red" 582 elif spot.type == "bio": 583 color = "blue" 584 585 # CSSのgrid layoutを使用してHTMLタグを含むテキストをフォーマット 586 popup_html = f""" 587 <div style='font-family: Arial; font-size: 12px; display: grid; grid-template-columns: auto auto auto; gap: 5px;'> 588 <b>Date</b> <span>:</span> <span>{spot.source}</span> 589 <b>Lat</b> <span>:</span> <span>{spot.avg_lat:.3f}</span> 590 <b>Lon</b> <span>:</span> <span>{spot.avg_lon:.3f}</span> 591 <b>ΔCH<sub>4</sub></b> <span>:</span> <span>{spot.delta_ch4:.3f}</span> 592 <b>ΔC<sub>2</sub>H<sub>6</sub></b> <span>:</span> <span>{spot.delta_c2h6:.3f}</span> 593 <b>Ratio</b> <span>:</span> <span>{spot.ratio:.3f}</span> 594 <b>Type</b> <span>:</span> <span>{spot.type}</span> 595 <b>Section</b> <span>:</span> <span>{spot.section}</span> 596 </div> 597 """ 598 599 # ポップアップのサイズを指定 600 popup = folium.Popup( 601 folium.Html(popup_html, script=True), 602 max_width=200, # 最大幅(ピクセル) 603 ) 604 605 folium.CircleMarker( 606 location=[spot.avg_lat, spot.avg_lon], 607 radius=8, 608 color=color, 609 fill=True, 610 popup=popup, 611 ).add_to(m) 612 613 # 中心点のマーカー 614 if plot_center_marker: 615 folium.Marker( 616 [self._center_lat, self._center_lon], 617 popup=center_marker_label, 618 icon=folium.Icon(color="green", icon="info-sign"), 619 ).add_to(m) 620 621 # 区画の境界線を描画 622 for section in range(self._num_sections): 623 start_angle = math.radians(-180 + section * self._section_size) 624 625 R = self.EARTH_RADIUS_METERS 626 627 # 境界線の座標を計算 628 lat1 = self._center_lat 629 lon1 = self._center_lon 630 lat2 = math.degrees( 631 math.asin( 632 math.sin(math.radians(lat1)) * math.cos(radius_meters / R) 633 + math.cos(math.radians(lat1)) 634 * math.sin(radius_meters / R) 635 * math.cos(start_angle) 636 ) 637 ) 638 lon2 = self._center_lon + math.degrees( 639 math.atan2( 640 math.sin(start_angle) 641 * math.sin(radius_meters / R) 642 * math.cos(math.radians(lat1)), 643 math.cos(radius_meters / R) 644 - math.sin(math.radians(lat1)) * math.sin(math.radians(lat2)), 645 ) 646 ) 647 648 # 境界線を描画 649 folium.PolyLine( 650 locations=[[lat1, lon1], [lat2, lon2]], 651 color="black", 652 weight=1, 653 opacity=0.5, 654 ).add_to(m) 655 656 # 地図を保存 657 if save_fig and output_dir is None: 658 raise ValueError( 659 "save_fig=Trueの場合、output_dirを指定する必要があります。有効なディレクトリパスを指定してください。" 660 ) 661 output_path: str = os.path.join(output_dir, output_filename) 662 m.save(str(output_path)) 663 self.logger.info(f"地図を保存しました: {output_path}")
ホットスポットの分布を地図上にプロットして保存
Parameters:
hotspots : list[HotspotData]
プロットするホットスポットのリスト
output_dir : str | Path
保存先のディレクトリパス
output_filename : str
保存するファイル名。デフォルトは"hotspots_map"。
center_marker_label : str
中心を示すマーカーのラベルテキスト。デフォルトは"Center"。
plot_center_marker : bool
中心を示すマーカーの有無。デフォルトはTrue。
radius_meters : float
区画分けを示す線の長さ。デフォルトは3000。
save_fig : bool
図の保存を許可するフラグ。デフォルトはTrue。
665 def export_hotspots_to_csv( 666 self, 667 hotspots: list[HotspotData], 668 output_dir: str | Path | None = None, 669 output_filename: str = "hotspots.csv", 670 ) -> None: 671 """ 672 ホットスポットの情報をCSVファイルに出力します。 673 674 Parameters: 675 ------ 676 hotspots : list[HotspotData] 677 出力するホットスポットのリスト 678 output_dir : str | Path | None 679 出力先ディレクトリ 680 output_filename : str 681 出力ファイル名 682 """ 683 # 日時の昇順でソート 684 sorted_hotspots = sorted(hotspots, key=lambda x: x.source) 685 686 # 出力用のデータを作成 687 records = [] 688 for spot in sorted_hotspots: 689 record = { 690 "source": spot.source, 691 "type": spot.type, 692 "delta_ch4": spot.delta_ch4, 693 "delta_c2h6": spot.delta_c2h6, 694 "ratio": spot.ratio, 695 "correlation": spot.correlation, 696 "angle": spot.angle, 697 "section": spot.section, 698 "latitude": spot.avg_lat, 699 "longitude": spot.avg_lon, 700 } 701 records.append(record) 702 703 # DataFrameに変換してCSVに出力 704 if output_dir is None: 705 raise ValueError( 706 "output_dirが指定されていません。有効なディレクトリパスを指定してください。" 707 ) 708 output_path: str = os.path.join(output_dir, output_filename) 709 df = pd.DataFrame(records) 710 df.to_csv(output_path, index=False) 711 self.logger.info( 712 f"ホットスポット情報をCSVファイルに出力しました: {output_path}" 713 )
ホットスポットの情報をCSVファイルに出力します。
Parameters:
hotspots : list[HotspotData]
出力するホットスポットのリスト
output_dir : str | Path | None
出力先ディレクトリ
output_filename : str
出力ファイル名
715 def get_preprocessed_data( 716 self, 717 ) -> pd.DataFrame: 718 """ 719 データ前処理を行い、CH4とC2H6の相関解析に必要な形式に整えます。 720 コンストラクタで読み込んだすべてのデータを前処理し、結合したDataFrameを返します。 721 722 Returns: 723 ------ 724 pd.DataFrame 725 前処理済みの結合されたDataFrame 726 """ 727 processed_dfs: list[pd.DataFrame] = [] 728 729 # 各データソースに対して解析を実行 730 for source_name, df in self._data.items(): 731 # パラメータの計算 732 processed_df = MobileSpatialAnalyzer._calculate_hotspots_parameters( 733 df, self._window_size 734 ) 735 # ソース名を列として追加 736 processed_df["source"] = source_name 737 processed_dfs.append(processed_df) 738 739 # すべてのDataFrameを結合 740 if not processed_dfs: 741 raise ValueError("処理対象のデータが存在しません。") 742 743 combined_df = pd.concat(processed_dfs, axis=0) 744 return combined_df
データ前処理を行い、CH4とC2H6の相関解析に必要な形式に整えます。 コンストラクタで読み込んだすべてのデータを前処理し、結合したDataFrameを返します。
Returns:
pd.DataFrame
前処理済みの結合されたDataFrame
746 def get_section_size(self) -> float: 747 """ 748 セクションのサイズを取得するメソッド。 749 このメソッドは、解析対象のデータを区画に分割する際の 750 各区画の角度範囲を示すサイズを返します。 751 752 Returns: 753 ------ 754 float 755 1セクションのサイズ(度単位) 756 """ 757 return self._section_size
セクションのサイズを取得するメソッド。 このメソッドは、解析対象のデータを区画に分割する際の 各区画の角度範囲を示すサイズを返します。
Returns:
float
1セクションのサイズ(度単位)
759 def plot_ch4_delta_histogram( 760 self, 761 hotspots: list[HotspotData], 762 output_dir: str | Path | None, 763 output_filename: str = "ch4_delta_histogram.png", 764 dpi: int = 200, 765 figsize: tuple[int, int] = (8, 6), 766 fontsize: float = 20, 767 xlim: tuple[float, float] | None = None, 768 ylim: tuple[float, float] | None = None, 769 save_fig: bool = True, 770 show_fig: bool = True, 771 yscale_log: bool = True, 772 print_bins_analysis: bool = False, 773 ) -> None: 774 """ 775 CH4の増加量(ΔCH4)の積み上げヒストグラムをプロットします。 776 777 Parameters: 778 ------ 779 hotspots : list[HotspotData] 780 プロットするホットスポットのリスト 781 output_dir : str | Path | None 782 保存先のディレクトリパス 783 output_filename : str 784 保存するファイル名。デフォルトは"ch4_delta_histogram.png"。 785 dpi : int 786 解像度。デフォルトは200。 787 figsize : tuple[int, int] 788 図のサイズ。デフォルトは(8, 6)。 789 fontsize : float 790 フォントサイズ。デフォルトは20。 791 xlim : tuple[float, float] | None 792 x軸の範囲。Noneの場合は自動設定。 793 ylim : tuple[float, float] | None 794 y軸の範囲。Noneの場合は自動設定。 795 save_fig : bool 796 図の保存を許可するフラグ。デフォルトはTrue。 797 show_fig : bool 798 図の表示を許可するフラグ。デフォルトはTrue。 799 yscale_log : bool 800 y軸をlogにするかどうか。デフォルトはTrue。 801 print_bins_analysis : bool 802 ビンごとの内訳を表示するオプション。 803 """ 804 plt.rcParams["font.size"] = fontsize 805 fig = plt.figure(figsize=figsize, dpi=dpi) 806 807 # ホットスポットからデータを抽出 808 all_ch4_deltas = [] 809 all_types = [] 810 for spot in hotspots: 811 all_ch4_deltas.append(spot.delta_ch4) 812 all_types.append(spot.type) 813 814 # データをNumPy配列に変換 815 all_ch4_deltas = np.array(all_ch4_deltas) 816 all_types = np.array(all_types) 817 818 # 0.1刻みのビンを作成 819 if xlim is not None: 820 bins = np.arange(xlim[0], xlim[1] + 0.1, 0.1) 821 else: 822 max_val = np.ceil(np.max(all_ch4_deltas) * 10) / 10 823 bins = np.arange(0, max_val + 0.1, 0.1) 824 825 # タイプごとのヒストグラムデータを計算 826 hist_data = {} 827 # HotspotTypeのリテラル値を使用してイテレーション 828 for type_name in get_args(HotspotType): # typing.get_argsをインポート 829 mask = all_types == type_name 830 if np.any(mask): 831 counts, _ = np.histogram(all_ch4_deltas[mask], bins=bins) 832 hist_data[type_name] = counts 833 834 # ビンごとの内訳を表示 835 if print_bins_analysis: 836 self.logger.info("各ビンの内訳:") 837 print(f"{'Bin Range':15} {'bio':>8} {'gas':>8} {'comb':>8} {'Total':>8}") 838 print("-" * 50) 839 840 for i in range(len(bins) - 1): 841 bin_start = bins[i] 842 bin_end = bins[i + 1] 843 bio_count = hist_data.get("bio", np.zeros(len(bins) - 1))[i] 844 gas_count = hist_data.get("gas", np.zeros(len(bins) - 1))[i] 845 comb_count = hist_data.get("comb", np.zeros(len(bins) - 1))[i] 846 total = bio_count + gas_count + comb_count 847 848 if total > 0: # 合計が0のビンは表示しない 849 print( 850 f"{bin_start:4.1f}-{bin_end:<8.1f}" 851 f"{int(bio_count):8d}" 852 f"{int(gas_count):8d}" 853 f"{int(comb_count):8d}" 854 f"{int(total):8d}" 855 ) 856 857 # 積み上げヒストグラムを作成 858 bottom = np.zeros_like(hist_data.get("bio", np.zeros(len(bins) - 1))) 859 860 # 色の定義をHotspotTypeを使用して型安全に定義 861 colors: dict[HotspotType, str] = {"bio": "blue", "gas": "red", "comb": "green"} 862 863 # HotspotTypeのリテラル値を使用してイテレーション 864 for type_name in get_args(HotspotType): 865 if type_name in hist_data: 866 plt.bar( 867 bins[:-1], 868 hist_data[type_name], 869 width=np.diff(bins)[0], 870 bottom=bottom, 871 color=colors[type_name], 872 label=type_name, 873 alpha=0.6, 874 align="edge", 875 ) 876 bottom += hist_data[type_name] 877 878 if yscale_log: 879 plt.yscale("log") 880 plt.xlabel("Δ$\\mathregular{CH_{4}}$ (ppm)") 881 plt.ylabel("Frequency") 882 plt.legend() 883 plt.grid(True, which="both", ls="-", alpha=0.2) 884 885 # 軸の範囲を設定 886 if xlim is not None: 887 plt.xlim(xlim) 888 if ylim is not None: 889 plt.ylim(ylim) 890 891 # グラフの保存または表示 892 if save_fig: 893 if output_dir is None: 894 raise ValueError( 895 "save_fig=Trueの場合、output_dirを指定する必要があります。有効なディレクトリパスを指定してください。" 896 ) 897 os.makedirs(output_dir, exist_ok=True) 898 output_path: str = os.path.join(output_dir, output_filename) 899 plt.savefig(output_path, bbox_inches="tight") 900 self.logger.info(f"ヒストグラムを保存しました: {output_path}") 901 if show_fig: 902 plt.show() 903 else: 904 plt.close(fig=fig)
CH4の増加量(ΔCH4)の積み上げヒストグラムをプロットします。
Parameters:
hotspots : list[HotspotData]
プロットするホットスポットのリスト
output_dir : str | Path | None
保存先のディレクトリパス
output_filename : str
保存するファイル名。デフォルトは"ch4_delta_histogram.png"。
dpi : int
解像度。デフォルトは200。
figsize : tuple[int, int]
図のサイズ。デフォルトは(8, 6)。
fontsize : float
フォントサイズ。デフォルトは20。
xlim : tuple[float, float] | None
x軸の範囲。Noneの場合は自動設定。
ylim : tuple[float, float] | None
y軸の範囲。Noneの場合は自動設定。
save_fig : bool
図の保存を許可するフラグ。デフォルトはTrue。
show_fig : bool
図の表示を許可するフラグ。デフォルトはTrue。
yscale_log : bool
y軸をlogにするかどうか。デフォルトはTrue。
print_bins_analysis : bool
ビンごとの内訳を表示するオプション。
906 def plot_mapbox( 907 self, 908 df: pd.DataFrame, 909 col: str, 910 mapbox_access_token: str, 911 sort_value_column: bool = True, 912 output_dir: str | Path | None = None, 913 output_filename: str = "mapbox_plot.html", 914 lat_column: str = "latitude", 915 lon_column: str = "longitude", 916 colorscale: str = "Jet", 917 center_lat: float | None = None, 918 center_lon: float | None = None, 919 zoom: float = 12, 920 width: int = 700, 921 height: int = 700, 922 tick_font_family: str = "Arial", 923 title_font_family: str = "Arial", 924 tick_font_size: int = 12, 925 title_font_size: int = 14, 926 marker_size: int = 4, 927 colorbar_title: str | None = None, 928 value_range: tuple[float, float] | None = None, 929 save_fig: bool = True, 930 show_fig: bool = True, 931 ) -> None: 932 """ 933 Plotlyを使用してMapbox上にデータをプロットします。 934 935 Parameters: 936 ------ 937 df : pd.DataFrame 938 プロットするデータを含むDataFrame 939 col : str 940 カラーマッピングに使用する列名 941 mapbox_access_token : str 942 Mapboxのアクセストークン 943 sort_value_column : bool 944 value_columnをソートするか否か。デフォルトはTrue。 945 output_dir : str | Path | None 946 出力ディレクトリのパス 947 output_filename : str 948 出力ファイル名。デフォルトは"mapbox_plot.html" 949 lat_column : str 950 緯度の列名。デフォルトは"latitude" 951 lon_column : str 952 経度の列名。デフォルトは"longitude" 953 colorscale : str 954 使用するカラースケール。デフォルトは"Jet" 955 center_lat : float | None 956 中心緯度。デフォルトはNoneで、self._center_latを使用 957 center_lon : float | None 958 中心経度。デフォルトはNoneで、self._center_lonを使用 959 zoom : float 960 マップの初期ズームレベル。デフォルトは12 961 width : int 962 プロットの幅(ピクセル)。デフォルトは700 963 height : int 964 プロットの高さ(ピクセル)。デフォルトは700 965 tick_font_family : str 966 カラーバーの目盛りフォントファミリー。デフォルトは"Arial" 967 title_font_family : str 968 カラーバーのラベルフォントファミリー。デフォルトは"Arial" 969 tick_font_size : int 970 カラーバーの目盛りフォントサイズ。デフォルトは12 971 title_font_size : int 972 カラーバーのラベルフォントサイズ。デフォルトは14 973 marker_size : int 974 マーカーのサイズ。デフォルトは4 975 colorbar_title : str | None 976 カラーバーのラベル 977 value_range : tuple[float, float] | None 978 カラーマッピングの範囲。デフォルトはNoneで、データの最小値と最大値を使用 979 save_fig : bool 980 図を保存するかどうか。デフォルトはTrue 981 show_fig : bool 982 図を表示するかどうか。デフォルトはTrue 983 """ 984 df_mapping: pd.DataFrame = df.copy().dropna(subset=[col]) 985 if sort_value_column: 986 df_mapping = df_mapping.sort_values(col) 987 # 中心座標の設定 988 center_lat = center_lat if center_lat is not None else self._center_lat 989 center_lon = center_lon if center_lon is not None else self._center_lon 990 991 # カラーマッピングの範囲を設定 992 cmin, cmax = 0, 0 993 if value_range is None: 994 cmin = df_mapping[col].min() 995 cmax = df_mapping[col].max() 996 else: 997 cmin, cmax = value_range 998 999 # カラーバーのタイトルを設定 1000 title_text = colorbar_title if colorbar_title is not None else col 1001 1002 # Scattermapboxのデータを作成 1003 scatter_data = go.Scattermapbox( 1004 lat=df_mapping[lat_column], 1005 lon=df_mapping[lon_column], 1006 text=df_mapping[col].astype(str), 1007 hoverinfo="text", 1008 mode="markers", 1009 marker=dict( 1010 color=df_mapping[col], 1011 size=marker_size, 1012 reversescale=False, 1013 autocolorscale=False, 1014 colorscale=colorscale, 1015 cmin=cmin, 1016 cmax=cmax, 1017 colorbar=dict( 1018 tickformat="3.2f", 1019 outlinecolor="black", 1020 outlinewidth=1.5, 1021 ticks="outside", 1022 ticklen=7, 1023 tickwidth=1.5, 1024 tickcolor="black", 1025 tickfont=dict( 1026 family=tick_font_family, color="black", size=tick_font_size 1027 ), 1028 title=dict( 1029 text=title_text, side="top" 1030 ), # カラーバーのタイトルを設定 1031 titlefont=dict( 1032 family=title_font_family, 1033 color="black", 1034 size=title_font_size, 1035 ), 1036 ), 1037 ), 1038 ) 1039 1040 # レイアウトの設定 1041 layout = go.Layout( 1042 width=width, 1043 height=height, 1044 showlegend=False, 1045 mapbox=dict( 1046 accesstoken=mapbox_access_token, 1047 center=dict(lat=center_lat, lon=center_lon), 1048 zoom=zoom, 1049 ), 1050 ) 1051 1052 # 図の作成 1053 fig = go.Figure(data=[scatter_data], layout=layout) 1054 1055 # 図の保存 1056 if save_fig: 1057 # 保存時の出力ディレクトリチェック 1058 if output_dir is None: 1059 raise ValueError( 1060 "save_fig=Trueの場合、output_dirを指定する必要があります。" 1061 ) 1062 os.makedirs(output_dir, exist_ok=True) 1063 output_path = os.path.join(output_dir, output_filename) 1064 pyo.plot(fig, filename=output_path, auto_open=False) 1065 self.logger.info(f"Mapboxプロットを保存しました: {output_path}") 1066 # 図の表示 1067 if show_fig: 1068 pyo.iplot(fig)
Plotlyを使用してMapbox上にデータをプロットします。
Parameters:
df : pd.DataFrame
プロットするデータを含むDataFrame
col : str
カラーマッピングに使用する列名
mapbox_access_token : str
Mapboxのアクセストークン
sort_value_column : bool
value_columnをソートするか否か。デフォルトはTrue。
output_dir : str | Path | None
出力ディレクトリのパス
output_filename : str
出力ファイル名。デフォルトは"mapbox_plot.html"
lat_column : str
緯度の列名。デフォルトは"latitude"
lon_column : str
経度の列名。デフォルトは"longitude"
colorscale : str
使用するカラースケール。デフォルトは"Jet"
center_lat : float | None
中心緯度。デフォルトはNoneで、self._center_latを使用
center_lon : float | None
中心経度。デフォルトはNoneで、self._center_lonを使用
zoom : float
マップの初期ズームレベル。デフォルトは12
width : int
プロットの幅(ピクセル)。デフォルトは700
height : int
プロットの高さ(ピクセル)。デフォルトは700
tick_font_family : str
カラーバーの目盛りフォントファミリー。デフォルトは"Arial"
title_font_family : str
カラーバーのラベルフォントファミリー。デフォルトは"Arial"
tick_font_size : int
カラーバーの目盛りフォントサイズ。デフォルトは12
title_font_size : int
カラーバーのラベルフォントサイズ。デフォルトは14
marker_size : int
マーカーのサイズ。デフォルトは4
colorbar_title : str | None
カラーバーのラベル
value_range : tuple[float, float] | None
カラーマッピングの範囲。デフォルトはNoneで、データの最小値と最大値を使用
save_fig : bool
図を保存するかどうか。デフォルトはTrue
show_fig : bool
図を表示するかどうか。デフォルトはTrue
1070 def plot_scatter_c2c1( 1071 self, 1072 hotspots: list[HotspotData], 1073 output_dir: str | Path | None = None, 1074 output_filename: str = "scatter_c2c1.png", 1075 dpi: int = 200, 1076 figsize: tuple[int, int] = (4, 4), 1077 fontsize: float = 12, 1078 save_fig: bool = True, 1079 show_fig: bool = True, 1080 ratio_labels: dict[float, tuple[float, float, str]] | None = None, 1081 ) -> None: 1082 """ 1083 検出されたホットスポットのΔC2H6とΔCH4の散布図をプロットします。 1084 1085 Parameters: 1086 ------ 1087 hotspots : list[HotspotData] 1088 プロットするホットスポットのリスト 1089 output_dir : str | Path | None 1090 保存先のディレクトリパス 1091 output_filename : str 1092 保存するファイル名。デフォルトは"scatter_c2c1.png"。 1093 dpi : int 1094 解像度。デフォルトは200。 1095 figsize : tuple[int, int] 1096 図のサイズ。デフォルトは(4, 4)。 1097 fontsize : float 1098 フォントサイズ。デフォルトは12。 1099 save_fig : bool 1100 図の保存を許可するフラグ。デフォルトはTrue。 1101 show_fig : bool 1102 図の表示を許可するフラグ。デフォルトはTrue。 1103 ratio_labels : dict[float, tuple[float, float, str]] | None 1104 比率線とラベルの設定。 1105 キーは比率値、値は (x位置, y位置, ラベルテキスト) のタプル。 1106 Noneの場合はデフォルト設定を使用。デフォルト値: 1107 { 1108 0.001: (1.25, 2, "0.001"), 1109 0.005: (1.25, 8, "0.005"), 1110 0.010: (1.25, 15, "0.01"), 1111 0.020: (1.25, 30, "0.02"), 1112 0.030: (1.0, 40, "0.03"), 1113 0.076: (0.20, 42, "0.076 (Osaka)") 1114 } 1115 """ 1116 plt.rcParams["font.size"] = fontsize 1117 fig = plt.figure(figsize=figsize, dpi=dpi) 1118 1119 # タイプごとのデータを収集 1120 type_data: dict[HotspotType, list[tuple[float, float]]] = { 1121 "bio": [], 1122 "gas": [], 1123 "comb": [], 1124 } 1125 for spot in hotspots: 1126 type_data[spot.type].append((spot.delta_ch4, spot.delta_c2h6)) 1127 1128 # 色とラベルの定義 1129 colors: dict[HotspotType, str] = {"bio": "blue", "gas": "red", "comb": "green"} 1130 labels: dict[HotspotType, str] = {"bio": "bio", "gas": "gas", "comb": "comb"} 1131 1132 # タイプごとにプロット(データが存在する場合のみ) 1133 for spot_type, data in type_data.items(): 1134 if data: # データが存在する場合のみプロット 1135 ch4_values, c2h6_values = zip(*data) 1136 plt.plot( 1137 ch4_values, 1138 c2h6_values, 1139 "o", 1140 c=colors[spot_type], 1141 alpha=0.5, 1142 ms=2, 1143 label=labels[spot_type], 1144 ) 1145 1146 # デフォルトの比率とラベル設定 1147 default_ratio_labels = { 1148 0.001: (1.25, 2, "0.001"), 1149 0.005: (1.25, 8, "0.005"), 1150 0.010: (1.25, 15, "0.01"), 1151 0.020: (1.25, 30, "0.02"), 1152 0.030: (1.0, 40, "0.03"), 1153 0.076: (0.20, 42, "0.076 (Osaka)"), 1154 } 1155 1156 ratio_labels = ratio_labels or default_ratio_labels 1157 1158 # プロット後、軸の設定前に比率の線を追加 1159 x = np.array([0, 5]) 1160 base_ch4 = 0.0 1161 base = 0.0 1162 1163 # 各比率に対して線を引く 1164 for ratio, (x_pos, y_pos, label) in ratio_labels.items(): 1165 y = (x - base_ch4) * 1000 * ratio + base 1166 plt.plot(x, y, "-", c="black", alpha=0.5) 1167 plt.text(x_pos, y_pos, label) 1168 1169 plt.ylim(0, 50) 1170 plt.xlim(0, 2.0) 1171 plt.ylabel("Δ$\\mathregular{C_{2}H_{6}}$ (ppb)") 1172 plt.xlabel("Δ$\\mathregular{CH_{4}}$ (ppm)") 1173 plt.legend() 1174 1175 # グラフの保存または表示 1176 if save_fig: 1177 if output_dir is None: 1178 raise ValueError( 1179 "save_fig=Trueの場合、output_dirを指定する必要があります。有効なディレクトリパスを指定してください。" 1180 ) 1181 output_path: str = os.path.join(output_dir, output_filename) 1182 plt.savefig(output_path, bbox_inches="tight") 1183 self.logger.info(f"散布図を保存しました: {output_path}") 1184 if show_fig: 1185 plt.show() 1186 else: 1187 plt.close(fig=fig)
検出されたホットスポットのΔC2H6とΔCH4の散布図をプロットします。
Parameters:
hotspots : list[HotspotData]
プロットするホットスポットのリスト
output_dir : str | Path | None
保存先のディレクトリパス
output_filename : str
保存するファイル名。デフォルトは"scatter_c2c1.png"。
dpi : int
解像度。デフォルトは200。
figsize : tuple[int, int]
図のサイズ。デフォルトは(4, 4)。
fontsize : float
フォントサイズ。デフォルトは12。
save_fig : bool
図の保存を許可するフラグ。デフォルトはTrue。
show_fig : bool
図の表示を許可するフラグ。デフォルトはTrue。
ratio_labels : dict[float, tuple[float, float, str]] | None
比率線とラベルの設定。
キーは比率値、値は (x位置, y位置, ラベルテキスト) のタプル。
Noneの場合はデフォルト設定を使用。デフォルト値:
{
0.001: (1.25, 2, "0.001"),
0.005: (1.25, 8, "0.005"),
0.010: (1.25, 15, "0.01"),
0.020: (1.25, 30, "0.02"),
0.030: (1.0, 40, "0.03"),
0.076: (0.20, 42, "0.076 (Osaka)")
}
1189 def plot_timeseries( 1190 self, 1191 dpi: int = 200, 1192 source_name: str | None = None, 1193 figsize: tuple[float, float] = (8, 4), 1194 output_dir: str | Path | None = None, 1195 output_filename: str = "timeseries.png", 1196 save_fig: bool = False, 1197 show_fig: bool = True, 1198 col_ch4: str = "ch4_ppm", 1199 col_c2h6: str = "c2h6_ppb", 1200 col_h2o: str = "h2o_ppm", 1201 ylim_ch4: tuple[float, float] | None = None, 1202 ylim_c2h6: tuple[float, float] | None = None, 1203 ylim_h2o: tuple[float, float] | None = None, 1204 ) -> None: 1205 """ 1206 時系列データをプロットします。 1207 1208 Parameters: 1209 ------ 1210 dpi : int 1211 図の解像度を指定します。デフォルトは200です。 1212 source_name : str | None 1213 プロットするデータソースの名前。Noneの場合は最初のデータソースを使用します。 1214 figsize : tuple[float, float] 1215 図のサイズを指定します。デフォルトは(8, 4)です。 1216 output_dir : str | Path | None 1217 保存先のディレクトリを指定します。save_fig=Trueの場合は必須です。 1218 output_filename : str 1219 保存するファイル名を指定します。デフォルトは"time_series.png"です。 1220 save_fig : bool 1221 図を保存するかどうかを指定します。デフォルトはFalseです。 1222 show_fig : bool 1223 図を表示するかどうかを指定します。デフォルトはTrueです。 1224 col_ch4 : str 1225 CH4データのキーを指定します。デフォルトは"ch4_ppm"です。 1226 col_c2h6 : str 1227 C2H6データのキーを指定します。デフォルトは"c2h6_ppb"です。 1228 col_h2o : str 1229 H2Oデータのキーを指定します。デフォルトは"h2o_ppm"です。 1230 ylim_ch4 : tuple[float, float] | None 1231 CH4プロットのy軸範囲を指定します。デフォルトはNoneです。 1232 ylim_c2h6 : tuple[float, float] | None 1233 C2H6プロットのy軸範囲を指定します。デフォルトはNoneです。 1234 ylim_h2o : tuple[float, float] | None 1235 H2Oプロットのy軸範囲を指定します。デフォルトはNoneです。 1236 """ 1237 dfs_dict: dict[str, pd.DataFrame] = self._data.copy() 1238 # データソースの選択 1239 if not dfs_dict: 1240 raise ValueError("データが読み込まれていません。") 1241 1242 if source_name is None: 1243 source_name = list(dfs_dict.keys())[0] 1244 elif source_name not in dfs_dict: 1245 raise ValueError( 1246 f"指定されたデータソース '{source_name}' が見つかりません。" 1247 ) 1248 1249 df = dfs_dict[source_name] 1250 1251 # プロットの作成 1252 fig = plt.figure(figsize=figsize, dpi=dpi) 1253 1254 # CH4プロット 1255 ax1 = fig.add_subplot(3, 1, 1) 1256 ax1.plot(df.index, df[col_ch4], c="red") 1257 if ylim_ch4: 1258 ax1.set_ylim(ylim_ch4) 1259 ax1.set_ylabel("$\\mathregular{CH_{4}}$ (ppm)") 1260 ax1.grid(True, alpha=0.3) 1261 1262 # C2H6プロット 1263 ax2 = fig.add_subplot(3, 1, 2) 1264 ax2.plot(df.index, df[col_c2h6], c="red") 1265 if ylim_c2h6: 1266 ax2.set_ylim(ylim_c2h6) 1267 ax2.set_ylabel("$\\mathregular{C_{2}H_{6}}$ (ppb)") 1268 ax2.grid(True, alpha=0.3) 1269 1270 # H2Oプロット 1271 ax3 = fig.add_subplot(3, 1, 3) 1272 ax3.plot(df.index, df[col_h2o], c="red") 1273 if ylim_h2o: 1274 ax3.set_ylim(ylim_h2o) 1275 ax3.set_ylabel("$\\mathregular{H_{2}O}$ (ppm)") 1276 ax3.grid(True, alpha=0.3) 1277 1278 # x軸のフォーマット調整 1279 for ax in [ax1, ax2, ax3]: 1280 ax.xaxis.set_major_formatter(mdates.DateFormatter("%H:%M")) 1281 1282 plt.subplots_adjust(wspace=0.38, hspace=0.38) 1283 1284 # 図の保存 1285 if save_fig: 1286 if output_dir is None: 1287 raise ValueError( 1288 "save_fig=Trueの場合、output_dirを指定する必要があります。有効なディレクトリパスを指定してください。" 1289 ) 1290 output_path = os.path.join(output_dir, output_filename) 1291 plt.savefig(output_path, bbox_inches="tight") 1292 self.logger.info(f"時系列プロットを保存しました: {output_path}") 1293 1294 if show_fig: 1295 plt.show() 1296 else: 1297 plt.close(fig=fig)
時系列データをプロットします。
Parameters:
dpi : int
図の解像度を指定します。デフォルトは200です。
source_name : str | None
プロットするデータソースの名前。Noneの場合は最初のデータソースを使用します。
figsize : tuple[float, float]
図のサイズを指定します。デフォルトは(8, 4)です。
output_dir : str | Path | None
保存先のディレクトリを指定します。save_fig=Trueの場合は必須です。
output_filename : str
保存するファイル名を指定します。デフォルトは"time_series.png"です。
save_fig : bool
図を保存するかどうかを指定します。デフォルトはFalseです。
show_fig : bool
図を表示するかどうかを指定します。デフォルトはTrueです。
col_ch4 : str
CH4データのキーを指定します。デフォルトは"ch4_ppm"です。
col_c2h6 : str
C2H6データのキーを指定します。デフォルトは"c2h6_ppb"です。
col_h2o : str
H2Oデータのキーを指定します。デフォルトは"h2o_ppm"です。
ylim_ch4 : tuple[float, float] | None
CH4プロットのy軸範囲を指定します。デフォルトはNoneです。
ylim_c2h6 : tuple[float, float] | None
C2H6プロットのy軸範囲を指定します。デフォルトはNoneです。
ylim_h2o : tuple[float, float] | None
H2Oプロットのy軸範囲を指定します。デフォルトはNoneです。
1751 def remove_c2c1_ratio_duplicates( 1752 self, 1753 df: pd.DataFrame, 1754 min_time_threshold_seconds: float = 300, # 5分以内は重複とみなす 1755 max_time_threshold_hours: float = 12.0, # 12時間以上離れている場合は別のポイントとして扱う 1756 check_time_all: bool = True, # 時間閾値を超えた場合の重複チェックを継続するかどうか 1757 hotspot_area_meter: float = 50.0, # 重複とみなす距離の閾値(メートル) 1758 col_ch4_ppm: str = "ch4_ppm", 1759 col_ch4_ppm_mv: str = "ch4_ppm_mv", 1760 col_ch4_ppm_delta: str = "ch4_ppm_delta", 1761 ): 1762 """ 1763 メタン濃度の増加が閾値を超えた地点から、重複を除外してユニークなホットスポットを抽出する関数。 1764 1765 Parameters: 1766 ------ 1767 df : pandas.DataFrame 1768 入力データフレーム。必須カラム: 1769 - ch4_ppm: メタン濃度(ppm) 1770 - ch4_ppm_mv: メタン濃度の移動平均(ppm) 1771 - ch4_ppm_delta: メタン濃度の増加量(ppm) 1772 - latitude: 緯度 1773 - longitude: 経度 1774 min_time_threshold_seconds : float, optional 1775 重複とみなす最小時間差(秒)。デフォルトは300秒(5分)。 1776 max_time_threshold_hours : float, optional 1777 別ポイントとして扱う最大時間差(時間)。デフォルトは12時間。 1778 check_time_all : bool, optional 1779 時間閾値を超えた場合の重複チェックを継続するかどうか。デフォルトはTrue。 1780 hotspot_area_meter : float, optional 1781 重複とみなす距離の閾値(メートル)。デフォルトは50メートル。 1782 1783 Returns: 1784 ------ 1785 pandas.DataFrame 1786 ユニークなホットスポットのデータフレーム。 1787 """ 1788 df_data: pd.DataFrame = df.copy() 1789 # メタン濃度の増加が閾値を超えた点を抽出 1790 mask = ( 1791 df_data[col_ch4_ppm] - df_data[col_ch4_ppm_mv] > self._ch4_enhance_threshold 1792 ) 1793 hotspot_candidates = df_data[mask].copy() 1794 1795 # ΔCH4の降順でソート 1796 sorted_hotspots = hotspot_candidates.sort_values( 1797 by=col_ch4_ppm_delta, ascending=False 1798 ) 1799 used_positions = [] 1800 unique_hotspots = pd.DataFrame() 1801 1802 for _, spot in sorted_hotspots.iterrows(): 1803 should_add = True 1804 for used_lat, used_lon, used_time in used_positions: 1805 # 距離チェック 1806 distance = geodesic( 1807 (spot.latitude, spot.longitude), (used_lat, used_lon) 1808 ).meters 1809 1810 if distance < hotspot_area_meter: 1811 # 時間差の計算(秒単位) 1812 time_diff = pd.Timedelta( 1813 spot.name - pd.to_datetime(used_time) 1814 ).total_seconds() 1815 time_diff_abs = abs(time_diff) 1816 1817 # 時間差に基づく判定 1818 if check_time_all: 1819 # 時間に関係なく、距離が近ければ重複とみなす 1820 # ΔCH4が大きい方を残す(現在のスポットは必ず小さい) 1821 should_add = False 1822 break 1823 else: 1824 # 時間窓による判定を行う 1825 if time_diff_abs <= min_time_threshold_seconds: 1826 # Case 1: 最小時間閾値以内は重複とみなす 1827 should_add = False 1828 break 1829 elif time_diff_abs > max_time_threshold_hours * 3600: 1830 # Case 2: 最大時間閾値を超えた場合は重複チェックをスキップ 1831 continue 1832 # Case 3: その間の時間差の場合は、距離が近ければ重複とみなす 1833 should_add = False 1834 break 1835 1836 if should_add: 1837 unique_hotspots = pd.concat([unique_hotspots, pd.DataFrame([spot])]) 1838 used_positions.append((spot.latitude, spot.longitude, spot.name)) 1839 1840 return unique_hotspots
メタン濃度の増加が閾値を超えた地点から、重複を除外してユニークなホットスポットを抽出する関数。
Parameters:
df : pandas.DataFrame
入力データフレーム。必須カラム:
- ch4_ppm: メタン濃度(ppm)
- ch4_ppm_mv: メタン濃度の移動平均(ppm)
- ch4_ppm_delta: メタン濃度の増加量(ppm)
- latitude: 緯度
- longitude: 経度
min_time_threshold_seconds : float, optional
重複とみなす最小時間差(秒)。デフォルトは300秒(5分)。
max_time_threshold_hours : float, optional
別ポイントとして扱う最大時間差(時間)。デフォルトは12時間。
check_time_all : bool, optional
時間閾値を超えた場合の重複チェックを継続するかどうか。デフォルトはTrue。
hotspot_area_meter : float, optional
重複とみなす距離の閾値(メートル)。デフォルトは50メートル。
Returns:
pandas.DataFrame
ユニークなホットスポットのデータフレーム。
1842 @staticmethod 1843 def remove_hotspots_duplicates( 1844 hotspots: list[HotspotData], 1845 check_time_all: bool, 1846 min_time_threshold_seconds: float = 300, 1847 max_time_threshold_hours: float = 12, 1848 hotspot_area_meter: float = 50, 1849 ) -> list[HotspotData]: 1850 """ 1851 重複するホットスポットを除外します。 1852 1853 このメソッドは、与えられたホットスポットのリストから重複を検出し、 1854 一意のホットスポットのみを返します。重複の判定は、指定された 1855 時間および距離の閾値に基づいて行われます。 1856 1857 Parameters: 1858 ------ 1859 hotspots : list[HotspotData] 1860 重複を除外する対象のホットスポットのリスト。 1861 check_time_all : bool 1862 時間に関係なく重複チェックを行うかどうか。 1863 min_time_threshold_seconds : float 1864 重複とみなす最小時間の閾値(秒)。 1865 max_time_threshold_hours : float 1866 重複チェックを一時的に無視する最大時間の閾値(時間)。 1867 hotspot_area_meter : float 1868 重複とみなす距離の閾値(メートル)。 1869 1870 Returns: 1871 ------ 1872 list[HotspotData] 1873 重複を除去したホットスポットのリスト。 1874 """ 1875 # ΔCH4の降順でソート 1876 sorted_hotspots: list[HotspotData] = sorted( 1877 hotspots, key=lambda x: x.delta_ch4, reverse=True 1878 ) 1879 used_positions_by_type: dict[ 1880 HotspotType, list[tuple[float, float, str, float]] 1881 ] = { 1882 "bio": [], 1883 "gas": [], 1884 "comb": [], 1885 } 1886 unique_hotspots: list[HotspotData] = [] 1887 1888 for spot in sorted_hotspots: 1889 is_duplicate = MobileSpatialAnalyzer._is_duplicate_spot( 1890 current_lat=spot.avg_lat, 1891 current_lon=spot.avg_lon, 1892 current_time=spot.source, 1893 used_positions=used_positions_by_type[spot.type], 1894 check_time_all=check_time_all, 1895 min_time_threshold_seconds=min_time_threshold_seconds, 1896 max_time_threshold_hours=max_time_threshold_hours, 1897 hotspot_area_meter=hotspot_area_meter, 1898 ) 1899 1900 if not is_duplicate: 1901 unique_hotspots.append(spot) 1902 used_positions_by_type[spot.type].append( 1903 (spot.avg_lat, spot.avg_lon, spot.source, spot.delta_ch4) 1904 ) 1905 1906 return unique_hotspots
重複するホットスポットを除外します。
このメソッドは、与えられたホットスポットのリストから重複を検出し、 一意のホットスポットのみを返します。重複の判定は、指定された 時間および距離の閾値に基づいて行われます。
Parameters:
hotspots : list[HotspotData]
重複を除外する対象のホットスポットのリスト。
check_time_all : bool
時間に関係なく重複チェックを行うかどうか。
min_time_threshold_seconds : float
重複とみなす最小時間の閾値(秒)。
max_time_threshold_hours : float
重複チェックを一時的に無視する最大時間の閾値(時間)。
hotspot_area_meter : float
重複とみなす距離の閾値(メートル)。
Returns:
list[HotspotData]
重複を除去したホットスポットのリスト。
1908 @staticmethod 1909 def setup_logger(logger: Logger | None, log_level: int = INFO) -> Logger: 1910 """ 1911 ロガーを設定します。 1912 1913 このメソッドは、ロギングの設定を行い、ログメッセージのフォーマットを指定します。 1914 ログメッセージには、日付、ログレベル、メッセージが含まれます。 1915 1916 渡されたロガーがNoneまたは不正な場合は、新たにロガーを作成し、標準出力に 1917 ログメッセージが表示されるようにStreamHandlerを追加します。ロガーのレベルは 1918 引数で指定されたlog_levelに基づいて設定されます。 1919 1920 Parameters: 1921 ------ 1922 logger : Logger | None 1923 使用するロガー。Noneの場合は新しいロガーを作成します。 1924 log_level : int 1925 ロガーのログレベル。デフォルトはINFO。 1926 1927 Returns: 1928 ------ 1929 Logger 1930 設定されたロガーオブジェクト。 1931 """ 1932 if logger is not None and isinstance(logger, Logger): 1933 return logger 1934 # 渡されたロガーがNoneまたは正しいものでない場合は独自に設定 1935 new_logger: Logger = getLogger() 1936 # 既存のハンドラーをすべて削除 1937 for handler in new_logger.handlers[:]: 1938 new_logger.removeHandler(handler) 1939 new_logger.setLevel(log_level) # ロガーのレベルを設定 1940 ch = StreamHandler() 1941 ch_formatter = Formatter("%(asctime)s - %(levelname)s - %(message)s") 1942 ch.setFormatter(ch_formatter) # フォーマッターをハンドラーに設定 1943 new_logger.addHandler(ch) # StreamHandlerの追加 1944 return new_logger
ロガーを設定します。
このメソッドは、ロギングの設定を行い、ログメッセージのフォーマットを指定します。 ログメッセージには、日付、ログレベル、メッセージが含まれます。
渡されたロガーがNoneまたは不正な場合は、新たにロガーを作成し、標準出力に ログメッセージが表示されるようにStreamHandlerを追加します。ロガーのレベルは 引数で指定されたlog_levelに基づいて設定されます。
Parameters:
logger : Logger | None
使用するロガー。Noneの場合は新しいロガーを作成します。
log_level : int
ロガーのログレベル。デフォルトはINFO。
Returns:
Logger
設定されたロガーオブジェクト。
1946 @staticmethod 1947 def calculate_emission_rates( 1948 hotspots: list[HotspotData], 1949 method: Literal["weller", "weitzel", "joo", "umezawa"] = "weller", 1950 print_summary: bool = True, 1951 custom_formulas: dict[str, dict[str, float]] | None = None, 1952 ) -> tuple[list[EmissionData], dict[str, dict[str, float]]]: 1953 """ 1954 検出されたホットスポットのCH4漏出量を計算・解析し、統計情報を生成します。 1955 1956 Parameters: 1957 ------ 1958 hotspots : list[HotspotData] 1959 分析対象のホットスポットのリスト 1960 method : Literal["weller", "weitzel", "joo", "umezawa"] 1961 使用する計算式。デフォルトは"weller"。 1962 print_summary : bool 1963 統計情報を表示するかどうか。デフォルトはTrue。 1964 custom_formulas : dict[str, dict[str, float]] | None 1965 カスタム計算式の係数。 1966 例: {"custom_method": {"a": 1.0, "b": 1.0}} 1967 Noneの場合はデフォルトの計算式を使用。 1968 1969 Returns: 1970 ------ 1971 tuple[list[EmissionData], dict[str, dict[str, float]]] 1972 - 各ホットスポットの排出量データを含むリスト 1973 - タイプ別の統計情報を含む辞書 1974 """ 1975 # デフォルトの経験式係数 1976 default_formulas = { 1977 "weller": {"a": 0.988, "b": 0.817}, 1978 "weitzel": {"a": 0.521, "b": 0.795}, 1979 "joo": {"a": 2.738, "b": 1.329}, 1980 "umezawa": {"a": 2.716, "b": 0.741}, 1981 } 1982 1983 # カスタム計算式がある場合は追加 1984 emission_formulas = default_formulas.copy() 1985 if custom_formulas: 1986 emission_formulas.update(custom_formulas) 1987 1988 if method not in emission_formulas: 1989 raise ValueError(f"Unknown method: {method}") 1990 1991 # 係数の取得 1992 a = emission_formulas[method]["a"] 1993 b = emission_formulas[method]["b"] 1994 1995 # 排出量の計算 1996 emission_data_list = [] 1997 for spot in hotspots: 1998 # 漏出量の計算 (L/min) 1999 emission_rate = np.exp((np.log(spot.delta_ch4) + a) / b) 2000 # 日排出量 (L/day) 2001 daily_emission = emission_rate * 60 * 24 2002 # 年間排出量 (L/year) 2003 annual_emission = daily_emission * 365 2004 2005 emission_data = EmissionData( 2006 source=spot.source, 2007 type=spot.type, 2008 section=spot.section, 2009 latitude=spot.avg_lat, 2010 longitude=spot.avg_lon, 2011 delta_ch4=spot.delta_ch4, 2012 delta_c2h6=spot.delta_c2h6, 2013 ratio=spot.ratio, 2014 emission_rate=emission_rate, 2015 daily_emission=daily_emission, 2016 annual_emission=annual_emission, 2017 ) 2018 emission_data_list.append(emission_data) 2019 2020 # 統計計算用にDataFrameを作成 2021 emission_df = pd.DataFrame([e.to_dict() for e in emission_data_list]) 2022 2023 # タイプ別の統計情報を計算 2024 stats = {} 2025 # emission_formulas の定義の後に、排出量カテゴリーの閾値を定義 2026 emission_categories = { 2027 "low": {"min": 0, "max": 6}, # < 6 L/min 2028 "medium": {"min": 6, "max": 40}, # 6-40 L/min 2029 "high": {"min": 40, "max": float("inf")}, # > 40 L/min 2030 } 2031 # get_args(HotspotType)を使用して型安全なリストを作成 2032 types = list(get_args(HotspotType)) 2033 for spot_type in types: 2034 df_type = emission_df[emission_df["type"] == spot_type] 2035 if len(df_type) > 0: 2036 # 既存の統計情報を計算 2037 type_stats = { 2038 "count": len(df_type), 2039 "emission_rate_min": df_type["emission_rate"].min(), 2040 "emission_rate_max": df_type["emission_rate"].max(), 2041 "emission_rate_mean": df_type["emission_rate"].mean(), 2042 "emission_rate_median": df_type["emission_rate"].median(), 2043 "total_annual_emission": df_type["annual_emission"].sum(), 2044 "mean_annual_emission": df_type["annual_emission"].mean(), 2045 } 2046 2047 # 排出量カテゴリー別の統計を追加 2048 category_counts = { 2049 "low": len( 2050 df_type[ 2051 df_type["emission_rate"] < emission_categories["low"]["max"] 2052 ] 2053 ), 2054 "medium": len( 2055 df_type[ 2056 ( 2057 df_type["emission_rate"] 2058 >= emission_categories["medium"]["min"] 2059 ) 2060 & ( 2061 df_type["emission_rate"] 2062 < emission_categories["medium"]["max"] 2063 ) 2064 ] 2065 ), 2066 "high": len( 2067 df_type[ 2068 df_type["emission_rate"] 2069 >= emission_categories["high"]["min"] 2070 ] 2071 ), 2072 } 2073 type_stats["emission_categories"] = category_counts 2074 2075 stats[spot_type] = type_stats 2076 2077 if print_summary: 2078 print(f"\n{spot_type}タイプの統計情報:") 2079 print(f" 検出数: {type_stats['count']}") 2080 print(" 排出量 (L/min):") 2081 print(f" 最小値: {type_stats['emission_rate_min']:.2f}") 2082 print(f" 最大値: {type_stats['emission_rate_max']:.2f}") 2083 print(f" 平均値: {type_stats['emission_rate_mean']:.2f}") 2084 print(f" 中央値: {type_stats['emission_rate_median']:.2f}") 2085 print(" 排出量カテゴリー別の検出数:") 2086 print(f" 低放出 (< 6 L/min): {category_counts['low']}") 2087 print(f" 中放出 (6-40 L/min): {category_counts['medium']}") 2088 print(f" 高放出 (> 40 L/min): {category_counts['high']}") 2089 print(" 年間排出量 (L/year):") 2090 print(f" 合計: {type_stats['total_annual_emission']:.2f}") 2091 print(f" 平均: {type_stats['mean_annual_emission']:.2f}") 2092 2093 return emission_data_list, stats
検出されたホットスポットのCH4漏出量を計算・解析し、統計情報を生成します。
Parameters:
hotspots : list[HotspotData]
分析対象のホットスポットのリスト
method : Literal["weller", "weitzel", "joo", "umezawa"]
使用する計算式。デフォルトは"weller"。
print_summary : bool
統計情報を表示するかどうか。デフォルトはTrue。
custom_formulas : dict[str, dict[str, float]] | None
カスタム計算式の係数。
例: {"custom_method": {"a": 1.0, "b": 1.0}}
Noneの場合はデフォルトの計算式を使用。
Returns:
tuple[list[EmissionData], dict[str, dict[str, float]]]
- 各ホットスポットの排出量データを含むリスト
- タイプ別の統計情報を含む辞書
2095 @staticmethod 2096 def plot_emission_analysis( 2097 emission_data_list: list[EmissionData], 2098 dpi: int = 300, 2099 output_dir: str | Path | None = None, 2100 output_filename: str = "emission_analysis.png", 2101 figsize: tuple[float, float] = (12, 5), 2102 add_legend: bool = True, 2103 hist_log_y: bool = False, 2104 hist_xlim: tuple[float, float] | None = None, 2105 hist_ylim: tuple[float, float] | None = None, 2106 scatter_xlim: tuple[float, float] | None = None, 2107 scatter_ylim: tuple[float, float] | None = None, 2108 hist_bin_width: float = 0.5, 2109 print_summary: bool = True, 2110 save_fig: bool = False, 2111 show_fig: bool = True, 2112 show_scatter: bool = True, # 散布図の表示を制御するオプションを追加 2113 ) -> None: 2114 """ 2115 排出量分析のプロットを作成する静的メソッド。 2116 2117 Parameters: 2118 ------ 2119 emission_data_list : list[EmissionData] 2120 EmissionDataオブジェクトのリスト。 2121 output_dir : str | Path | None 2122 出力先ディレクトリのパス。 2123 output_filename : str 2124 保存するファイル名。デフォルトは"emission_analysis.png"。 2125 dpi : int 2126 プロットの解像度。デフォルトは300。 2127 figsize : tuple[float, float] 2128 プロットのサイズ。デフォルトは(12, 5)。 2129 add_legend : bool 2130 凡例を追加するかどうか。デフォルトはTrue。 2131 hist_log_y : bool 2132 ヒストグラムのy軸を対数スケールにするかどうか。デフォルトはFalse。 2133 hist_xlim : tuple[float, float] | None 2134 ヒストグラムのx軸の範囲。デフォルトはNone。 2135 hist_ylim : tuple[float, float] | None 2136 ヒストグラムのy軸の範囲。デフォルトはNone。 2137 scatter_xlim : tuple[float, float] | None 2138 散布図のx軸の範囲。デフォルトはNone。 2139 scatter_ylim : tuple[float, float] | None 2140 散布図のy軸の範囲。デフォルトはNone。 2141 hist_bin_width : float 2142 ヒストグラムのビンの幅。デフォルトは0.5。 2143 print_summary : bool 2144 集計結果を表示するかどうか。デフォルトはFalse。 2145 save_fig : bool 2146 図をファイルに保存するかどうか。デフォルトはFalse。 2147 show_fig : bool 2148 図を表示するかどうか。デフォルトはTrue。 2149 show_scatter : bool 2150 散布図(右図)を表示するかどうか。デフォルトはTrue。 2151 """ 2152 # データをDataFrameに変換 2153 df = pd.DataFrame([e.to_dict() for e in emission_data_list]) 2154 2155 # プロットの作成(散布図の有無に応じてサブプロット数を調整) 2156 if show_scatter: 2157 fig, (ax1, ax2) = plt.subplots(1, 2, figsize=figsize) 2158 axes = [ax1, ax2] 2159 else: 2160 fig, ax1 = plt.subplots(1, 1, figsize=(figsize[0] // 2, figsize[1])) 2161 axes = [ax1] 2162 2163 # カラーマップの定義 2164 colors: dict[HotspotType, str] = {"bio": "blue", "gas": "red", "comb": "green"} 2165 2166 # 存在するタイプを確認 2167 # HotspotTypeの定義順を基準にソート 2168 hotspot_types = list(get_args(HotspotType)) 2169 existing_types = sorted( 2170 df["type"].unique(), key=lambda x: hotspot_types.index(x) 2171 ) 2172 2173 # 左側: ヒストグラム 2174 # ビンの範囲を設定 2175 start = 0 # 必ず0から開始 2176 if hist_xlim is not None: 2177 end = hist_xlim[1] 2178 else: 2179 end = np.ceil(df["emission_rate"].max() * 1.05) 2180 2181 # ビン数を計算(end値をbin_widthで割り切れるように調整) 2182 n_bins = int(np.ceil(end / hist_bin_width)) 2183 end = n_bins * hist_bin_width 2184 2185 # ビンの生成(0から開始し、bin_widthの倍数で区切る) 2186 bins = np.linspace(start, end, n_bins + 1) 2187 2188 # タイプごとにヒストグラムを積み上げ 2189 bottom = np.zeros(len(bins) - 1) 2190 for spot_type in existing_types: 2191 data = df[df["type"] == spot_type]["emission_rate"] 2192 if len(data) > 0: 2193 counts, _ = np.histogram(data, bins=bins) 2194 ax1.bar( 2195 bins[:-1], 2196 counts, 2197 width=hist_bin_width, 2198 bottom=bottom, 2199 alpha=0.6, 2200 label=spot_type, 2201 color=colors[spot_type], 2202 ) 2203 bottom += counts 2204 2205 ax1.set_xlabel("CH$_4$ Emission (L min$^{-1}$)") 2206 ax1.set_ylabel("Frequency") 2207 if hist_log_y: 2208 # ax1.set_yscale("log") 2209 # 非線形スケールを設定(linthreshで線形から対数への遷移点を指定) 2210 ax1.set_yscale("symlog", linthresh=1.0) 2211 if hist_xlim is not None: 2212 ax1.set_xlim(hist_xlim) 2213 else: 2214 ax1.set_xlim(0, np.ceil(df["emission_rate"].max() * 1.05)) 2215 2216 if hist_ylim is not None: 2217 ax1.set_ylim(hist_ylim) 2218 else: 2219 ax1.set_ylim(0, ax1.get_ylim()[1]) # 下限を0に設定 2220 2221 if show_scatter: 2222 # 右側: 散布図 2223 for spot_type in existing_types: 2224 mask = df["type"] == spot_type 2225 ax2.scatter( 2226 df[mask]["emission_rate"], 2227 df[mask]["delta_ch4"], 2228 alpha=0.6, 2229 label=spot_type, 2230 color=colors[spot_type], 2231 ) 2232 2233 ax2.set_xlabel("Emission Rate (L min$^{-1}$)") 2234 ax2.set_ylabel("ΔCH$_4$ (ppm)") 2235 if scatter_xlim is not None: 2236 ax2.set_xlim(scatter_xlim) 2237 else: 2238 ax2.set_xlim(0, np.ceil(df["emission_rate"].max() * 1.05)) 2239 2240 if scatter_ylim is not None: 2241 ax2.set_ylim(scatter_ylim) 2242 else: 2243 ax2.set_ylim(0, np.ceil(df["delta_ch4"].max() * 1.05)) 2244 2245 # 凡例の表示 2246 if add_legend: 2247 for ax in axes: 2248 ax.legend( 2249 bbox_to_anchor=(0.5, -0.30), 2250 loc="upper center", 2251 ncol=len(existing_types), 2252 ) 2253 2254 plt.tight_layout() 2255 2256 # 図の保存 2257 if save_fig: 2258 if output_dir is None: 2259 raise ValueError( 2260 "save_fig=Trueの場合、output_dirを指定する必要があります。有効なディレクトリパスを指定してください。" 2261 ) 2262 os.makedirs(output_dir, exist_ok=True) 2263 output_path = os.path.join(output_dir, output_filename) 2264 plt.savefig(output_path, bbox_inches="tight", dpi=dpi) 2265 # 図の表示 2266 if show_fig: 2267 plt.show() 2268 else: 2269 plt.close(fig=fig) 2270 2271 if print_summary: 2272 # デバッグ用の出力 2273 print("\nビンごとの集計:") 2274 print(f"{'Range':>12} | {'bio':>8} | {'gas':>8} | {'total':>8}") 2275 print("-" * 50) 2276 2277 for i in range(len(bins) - 1): 2278 bin_start = bins[i] 2279 bin_end = bins[i + 1] 2280 2281 # 各タイプのカウントを計算 2282 counts_by_type: dict[HotspotType, int] = {"bio": 0, "gas": 0, "comb": 0} 2283 total = 0 2284 for spot_type in existing_types: 2285 mask = ( 2286 (df["type"] == spot_type) 2287 & (df["emission_rate"] >= bin_start) 2288 & (df["emission_rate"] < bin_end) 2289 ) 2290 count = len(df[mask]) 2291 counts_by_type[spot_type] = count 2292 total += count 2293 2294 # カウントが0の場合はスキップ 2295 if total > 0: 2296 range_str = f"{bin_start:5.1f}-{bin_end:<5.1f}" 2297 bio_count = counts_by_type.get("bio", 0) 2298 gas_count = counts_by_type.get("gas", 0) 2299 print( 2300 f"{range_str:>12} | {bio_count:8d} | {gas_count:8d} | {total:8d}" 2301 )
排出量分析のプロットを作成する静的メソッド。
Parameters:
emission_data_list : list[EmissionData]
EmissionDataオブジェクトのリスト。
output_dir : str | Path | None
出力先ディレクトリのパス。
output_filename : str
保存するファイル名。デフォルトは"emission_analysis.png"。
dpi : int
プロットの解像度。デフォルトは300。
figsize : tuple[float, float]
プロットのサイズ。デフォルトは(12, 5)。
add_legend : bool
凡例を追加するかどうか。デフォルトはTrue。
hist_log_y : bool
ヒストグラムのy軸を対数スケールにするかどうか。デフォルトはFalse。
hist_xlim : tuple[float, float] | None
ヒストグラムのx軸の範囲。デフォルトはNone。
hist_ylim : tuple[float, float] | None
ヒストグラムのy軸の範囲。デフォルトはNone。
scatter_xlim : tuple[float, float] | None
散布図のx軸の範囲。デフォルトはNone。
scatter_ylim : tuple[float, float] | None
散布図のy軸の範囲。デフォルトはNone。
hist_bin_width : float
ヒストグラムのビンの幅。デフォルトは0.5。
print_summary : bool
集計結果を表示するかどうか。デフォルトはFalse。
save_fig : bool
図をファイルに保存するかどうか。デフォルトはFalse。
show_fig : bool
図を表示するかどうか。デフォルトはTrue。
show_scatter : bool
散布図(右図)を表示するかどうか。デフォルトはTrue。
168@dataclass 169class MSAInputConfig: 170 """入力ファイルの設定を保持するデータクラス 171 172 Parameters: 173 ------ 174 fs : float 175 サンプリング周波数(Hz) 176 lag : float 177 測器の遅れ時間(秒) 178 path : Path | str 179 ファイルパス 180 correction_type : str | None 181 適用する補正式の種類を表す文字列 182 """ 183 184 fs: float # サンプリング周波数(Hz) 185 lag: float # 測器の遅れ時間(秒) 186 path: Path | str # ファイルパス 187 correction_type: str | None = None # 適用する補正式の種類を表す文字列 188 189 def __post_init__(self) -> None: 190 """ 191 インスタンス生成後に入力値の検証を行います。 192 193 Raises: 194 ------ 195 ValueError: 遅延時間が負の値である場合、またはサポートされていないファイル拡張子の場合。 196 """ 197 # fsが有効かを確認 198 if not isinstance(self.fs, (int, float)) or self.fs <= 0: 199 raise ValueError( 200 f"Invalid sampling frequency: {self.fs}. Must be a positive float." 201 ) 202 # lagが0以上のfloatかを確認 203 if not isinstance(self.lag, (int, float)) or self.lag < 0: 204 raise ValueError( 205 f"Invalid lag value: {self.lag}. Must be a non-negative float." 206 ) 207 # 拡張子の確認 208 supported_extensions: list[str] = [".txt", ".csv"] 209 extension = Path(self.path).suffix 210 if extension not in supported_extensions: 211 raise ValueError( 212 f"Unsupported file extension: '{extension}'. Supported: {supported_extensions}" 213 ) 214 # 与えられたcorrection_typeがNoneでない場合、CORRECTION_TYPES_PATTERNに含まれているかを検証します 215 if self.correction_type is not None: 216 if not isinstance(self.correction_type, str): 217 raise ValueError( 218 f"Invalid correction_type: {self.correction_type}. Must be a str instance." 219 ) 220 if self.correction_type not in CORRECTION_TYPES_PATTERN: 221 raise ValueError( 222 f"Invalid correction_type: {self.correction_type}. Must be one of {CORRECTION_TYPES_PATTERN}." 223 ) 224 225 @classmethod 226 def validate_and_create( 227 cls, 228 fs: float, 229 lag: float, 230 path: Path | str, 231 correction_type: str | None, 232 ) -> "MSAInputConfig": 233 """ 234 入力値を検証し、MSAInputConfigインスタンスを生成するファクトリメソッドです。 235 236 指定された遅延時間、サンプリング周波数、およびファイルパスが有効であることを確認し、 237 有効な場合に新しいMSAInputConfigオブジェクトを返します。 238 239 Parameters: 240 ------ 241 fs : float 242 サンプリング周波数。正のfloatである必要があります。 243 lag : float 244 遅延時間。0以上のfloatである必要があります。 245 path : Path | str 246 入力ファイルのパス。サポートされている拡張子は.txtと.csvです。 247 correction_type : str | None 248 適用する補正式の種類を表す文字列。 249 250 Returns: 251 ------ 252 MSAInputConfig 253 検証された入力設定を持つMSAInputConfigオブジェクト。 254 """ 255 return cls(fs=fs, lag=lag, path=path, correction_type=correction_type)
入力ファイルの設定を保持するデータクラス
Parameters:
fs : float
サンプリング周波数(Hz)
lag : float
測器の遅れ時間(秒)
path : Path | str
ファイルパス
correction_type : str | None
適用する補正式の種類を表す文字列
225 @classmethod 226 def validate_and_create( 227 cls, 228 fs: float, 229 lag: float, 230 path: Path | str, 231 correction_type: str | None, 232 ) -> "MSAInputConfig": 233 """ 234 入力値を検証し、MSAInputConfigインスタンスを生成するファクトリメソッドです。 235 236 指定された遅延時間、サンプリング周波数、およびファイルパスが有効であることを確認し、 237 有効な場合に新しいMSAInputConfigオブジェクトを返します。 238 239 Parameters: 240 ------ 241 fs : float 242 サンプリング周波数。正のfloatである必要があります。 243 lag : float 244 遅延時間。0以上のfloatである必要があります。 245 path : Path | str 246 入力ファイルのパス。サポートされている拡張子は.txtと.csvです。 247 correction_type : str | None 248 適用する補正式の種類を表す文字列。 249 250 Returns: 251 ------ 252 MSAInputConfig 253 検証された入力設定を持つMSAInputConfigオブジェクト。 254 """ 255 return cls(fs=fs, lag=lag, path=path, correction_type=correction_type)
入力値を検証し、MSAInputConfigインスタンスを生成するファクトリメソッドです。
指定された遅延時間、サンプリング周波数、およびファイルパスが有効であることを確認し、 有効な場合に新しいMSAInputConfigオブジェクトを返します。
Parameters:
fs : float
サンプリング周波数。正のfloatである必要があります。
lag : float
遅延時間。0以上のfloatである必要があります。
path : Path | str
入力ファイルのパス。サポートされている拡張子は.txtと.csvです。
correction_type : str | None
適用する補正式の種類を表す文字列。
Returns:
MSAInputConfig
検証された入力設定を持つMSAInputConfigオブジェクト。
8class MonthlyConverter: 9 """ 10 Monthlyシート(Excel)を一括で読み込み、DataFrameに変換するクラス。 11 デフォルトは'SA.Ultra.*.xlsx'に対応していますが、コンストラクタのfile_patternを 12 変更すると別のシートにも対応可能です(例: 'SA.Picaro.*.xlsx')。 13 """ 14 15 FILE_DATE_FORMAT = "%Y.%m" # ファイル名用 16 PERIOD_DATE_FORMAT = "%Y-%m-%d" # 期間指定用 17 18 def __init__( 19 self, 20 directory: str | Path, 21 file_pattern: str = "SA.Ultra.*.xlsx", 22 logger: Logger | None = None, 23 logging_debug: bool = False, 24 ): 25 """ 26 MonthlyConverterクラスのコンストラクタ 27 28 Parameters: 29 ------ 30 directory : str | Path 31 Excelファイルが格納されているディレクトリのパス 32 file_pattern : str 33 ファイル名のパターン。デフォルトは'SA.Ultra.*.xlsx'。 34 logger : Logger | None 35 使用するロガー。Noneの場合は新しいロガーを作成します。 36 logging_debug : bool 37 ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。 38 """ 39 # ロガー 40 log_level: int = INFO 41 if logging_debug: 42 log_level = DEBUG 43 self.logger: Logger = MonthlyConverter.setup_logger(logger, log_level) 44 45 self._directory = Path(directory) 46 if not self._directory.exists(): 47 raise NotADirectoryError(f"Directory not found: {self._directory}") 48 49 # Excelファイルのパスを保持 50 self._excel_files: dict[str, pd.ExcelFile] = {} 51 self._file_pattern: str = file_pattern 52 53 @staticmethod 54 def setup_logger(logger: Logger | None, log_level: int = INFO) -> Logger: 55 """ 56 ロガーを設定します。 57 58 このメソッドは、ロギングの設定を行い、ログメッセージのフォーマットを指定します。 59 ログメッセージには、日付、ログレベル、メッセージが含まれます。 60 61 渡されたロガーがNoneまたは不正な場合は、新たにロガーを作成し、標準出力に 62 ログメッセージが表示されるようにStreamHandlerを追加します。ロガーのレベルは 63 引数で指定されたlog_levelに基づいて設定されます。 64 65 Parameters: 66 ------ 67 logger : Logger | None 68 使用するロガー。Noneの場合は新しいロガーを作成します。 69 log_level : int 70 ロガーのログレベル。デフォルトはINFO。 71 72 Returns: 73 ------ 74 Logger 75 設定されたロガーオブジェクト。 76 """ 77 if logger is not None and isinstance(logger, Logger): 78 return logger 79 # 渡されたロガーがNoneまたは正しいものでない場合は独自に設定 80 new_logger: Logger = getLogger() 81 # 既存のハンドラーをすべて削除 82 for handler in new_logger.handlers[:]: 83 new_logger.removeHandler(handler) 84 new_logger.setLevel(log_level) # ロガーのレベルを設定 85 ch = StreamHandler() 86 ch_formatter = Formatter("%(asctime)s - %(levelname)s - %(message)s") 87 ch.setFormatter(ch_formatter) # フォーマッターをハンドラーに設定 88 new_logger.addHandler(ch) # StreamHandlerの追加 89 return new_logger 90 91 def close(self) -> None: 92 """ 93 すべてのExcelファイルをクローズする 94 """ 95 for excel_file in self._excel_files.values(): 96 excel_file.close() 97 self._excel_files.clear() 98 99 def get_available_dates(self) -> list[str]: 100 """ 101 利用可能なファイルの日付一覧を返却します。 102 103 Returns: 104 ------ 105 list[str] 106 'yyyy.MM'形式の日付リスト 107 """ 108 dates = [] 109 for file_name in self._directory.glob(self._file_pattern): 110 try: 111 date = self._extract_date(file_name.name) 112 dates.append(date.strftime(self.FILE_DATE_FORMAT)) 113 except ValueError: 114 continue 115 return sorted(dates) 116 117 def get_sheet_names(self, file_name: str) -> list[str]: 118 """ 119 指定されたファイルで利用可能なシート名の一覧を返却する 120 121 Parameters: 122 ------ 123 file_name : str 124 Excelファイル名 125 126 Returns: 127 ------ 128 list[str] 129 シート名のリスト 130 """ 131 if file_name not in self._excel_files: 132 file_path = self._directory / file_name 133 if not file_path.exists(): 134 raise FileNotFoundError(f"File not found: {file_path}") 135 self._excel_files[file_name] = pd.ExcelFile(file_path) 136 return self._excel_files[file_name].sheet_names 137 138 def read_sheets( 139 self, 140 sheet_names: str | list[str], 141 columns: list[str] | None = None, # 新しいパラメータを追加 142 col_datetime: str = "Date", 143 header: int = 0, 144 skiprows: int | list[int] = [1], 145 start_date: str | None = None, 146 end_date: str | None = None, 147 include_end_date: bool = True, 148 sort_by_date: bool = True, 149 ) -> pd.DataFrame: 150 """ 151 指定されたシートを読み込み、DataFrameとして返却します。 152 デフォルトでは2行目(単位の行)はスキップされます。 153 重複するカラム名がある場合は、より先に指定されたシートに存在するカラムの値を保持します。 154 155 Parameters: 156 ------ 157 sheet_names : str | list[str] 158 読み込むシート名。文字列または文字列のリストを指定できます。 159 columns : list[str] | None 160 残すカラム名のリスト。Noneの場合は全てのカラムを保持します。 161 col_datetime : str 162 日付と時刻の情報が含まれるカラム名。デフォルトは'Date'。 163 header : int 164 データのヘッダー行を指定します。デフォルトは0。 165 skiprows : int | list[int] 166 スキップする行数。デフォルトでは1行目をスキップします。 167 start_date : str | None 168 開始日 ('yyyy-MM-dd')。この日付の'00:00:00'のデータが開始行となります。 169 end_date : str | None 170 終了日 ('yyyy-MM-dd')。この日付をデータに含めるかはinclude_end_dateフラグによって変わります。 171 include_end_date : bool 172 終了日を含めるかどうか。デフォルトはTrueです。 173 sort_by_date : bool 174 ファイルの日付でソートするかどうか。デフォルトはTrueです。 175 176 Returns: 177 ------ 178 pd.DataFrame 179 読み込まれたデータを結合したDataFrameを返します。 180 """ 181 if isinstance(sheet_names, str): 182 sheet_names = [sheet_names] 183 184 self._load_excel_files(start_date, end_date) 185 186 if not self._excel_files: 187 raise ValueError("No Excel files found matching the criteria") 188 189 # ファイルを日付順にソート 190 sorted_files = ( 191 sorted(self._excel_files.items(), key=lambda x: self._extract_date(x[0])) 192 if sort_by_date 193 else self._excel_files.items() 194 ) 195 196 # 各シートのデータを格納するリスト 197 sheet_dfs = {sheet_name: [] for sheet_name in sheet_names} 198 199 # 各ファイルからデータを読み込む 200 for file_name, excel_file in sorted_files: 201 file_date = self._extract_date(file_name) 202 203 for sheet_name in sheet_names: 204 if sheet_name in excel_file.sheet_names: 205 df = pd.read_excel( 206 excel_file, 207 sheet_name=sheet_name, 208 header=header, 209 skiprows=skiprows, 210 na_values=[ 211 "#DIV/0!", 212 "#VALUE!", 213 "#REF!", 214 "#N/A", 215 "#NAME?", 216 "NAN", 217 ], 218 ) 219 # 年と月を追加 220 df["year"] = file_date.year 221 df["month"] = file_date.month 222 sheet_dfs[sheet_name].append(df) 223 224 if not any(sheet_dfs.values()): 225 raise ValueError(f"No sheets found matching: {sheet_names}") 226 227 # 各シートのデータを結合 228 combined_sheets = {} 229 for sheet_name, dfs in sheet_dfs.items(): 230 if dfs: # シートにデータがある場合のみ結合 231 combined_sheets[sheet_name] = pd.concat(dfs, ignore_index=True) 232 233 # 最初のシートをベースにする 234 base_df = combined_sheets[sheet_names[0]] 235 236 # 2つ目以降のシートを結合 237 for sheet_name in sheet_names[1:]: 238 if sheet_name in combined_sheets: 239 base_df = self.merge_dataframes( 240 base_df, combined_sheets[sheet_name], date_column=col_datetime 241 ) 242 243 # 日付でフィルタリング 244 if start_date: 245 start_dt = pd.to_datetime(start_date) 246 base_df = base_df[base_df[col_datetime] >= start_dt] 247 248 if end_date: 249 end_dt = pd.to_datetime(end_date) 250 if include_end_date: 251 end_dt += pd.Timedelta(days=1) 252 base_df = base_df[base_df[col_datetime] < end_dt] 253 254 # カラムの選択 255 if columns is not None: 256 required_columns = [col_datetime, "year", "month"] 257 available_columns = base_df.columns.tolist() # 利用可能なカラムを取得 258 if not all(col in available_columns for col in columns): 259 raise ValueError( 260 f"指定されたカラムが見つかりません: {columns}. 利用可能なカラム: {available_columns}" 261 ) 262 selected_columns = list(set(columns + required_columns)) 263 base_df = base_df[selected_columns] 264 265 return base_df 266 267 def __enter__(self) -> "MonthlyConverter": 268 return self 269 270 def __exit__(self, exc_type, exc_val, exc_tb) -> None: 271 self.close() 272 273 def _extract_date(self, file_name: str) -> datetime: 274 """ 275 ファイル名から日付を抽出する 276 277 Parameters: 278 ------ 279 file_name : str 280 "SA.Ultra.yyyy.MM.xlsx"または"SA.Picaro.yyyy.MM.xlsx"形式のファイル名 281 282 Returns: 283 ------ 284 datetime 285 抽出された日付 286 """ 287 # ファイル名から日付部分を抽出 288 date_str = ".".join(file_name.split(".")[-3:-1]) # "yyyy.MM"の部分を取得 289 return datetime.strptime(date_str, self.FILE_DATE_FORMAT) 290 291 def _load_excel_files( 292 self, start_date: str | None = None, end_date: str | None = None 293 ) -> None: 294 """ 295 指定された日付範囲のExcelファイルを読み込む 296 297 Parameters: 298 ------ 299 start_date : str | None 300 開始日 ('yyyy-MM-dd'形式) 301 end_date : str | None 302 終了日 ('yyyy-MM-dd'形式) 303 """ 304 # 期間指定がある場合は、yyyy-MM-dd形式から年月のみを抽出 305 start_dt = None 306 end_dt = None 307 if start_date: 308 temp_dt = datetime.strptime(start_date, self.PERIOD_DATE_FORMAT) 309 start_dt = datetime(temp_dt.year, temp_dt.month, 1) 310 if end_date: 311 temp_dt = datetime.strptime(end_date, self.PERIOD_DATE_FORMAT) 312 end_dt = datetime(temp_dt.year, temp_dt.month, 1) 313 314 # 既存のファイルをクリア 315 self.close() 316 317 for excel_path in self._directory.glob(self._file_pattern): 318 try: 319 file_date = self._extract_date(excel_path.name) 320 321 # 日付範囲チェック 322 if start_dt and file_date < start_dt: 323 continue 324 if end_dt and file_date > end_dt: 325 continue 326 327 if excel_path.name not in self._excel_files: 328 self._excel_files[excel_path.name] = pd.ExcelFile(excel_path) 329 330 except ValueError as e: 331 self.logger.warning( 332 f"Could not parse date from file {excel_path.name}: {e}" 333 ) 334 335 @staticmethod 336 def extract_monthly_data( 337 df: pd.DataFrame, 338 target_months: list[int], 339 start_day: int | None = None, 340 end_day: int | None = None, 341 datetime_column: str = "Date", 342 ) -> pd.DataFrame: 343 """ 344 指定された月と期間のデータを抽出します。 345 346 Parameters: 347 ------ 348 df : pd.DataFrame 349 入力データフレーム。 350 target_months : list[int] 351 抽出したい月のリスト(1から12の整数)。 352 start_day : int | None 353 開始日(1から31の整数)。Noneの場合は月初め。 354 end_day : int | None 355 終了日(1から31の整数)。Noneの場合は月末。 356 datetime_column : str, optional 357 日付を含む列の名前。デフォルトは"Date"。 358 359 Returns: 360 ------ 361 pd.DataFrame 362 指定された期間のデータのみを含むデータフレーム。 363 """ 364 # 入力チェック 365 if not all(1 <= month <= 12 for month in target_months): 366 raise ValueError("target_monthsは1から12の間である必要があります") 367 368 if start_day is not None and not 1 <= start_day <= 31: 369 raise ValueError("start_dayは1から31の間である必要があります") 370 371 if end_day is not None and not 1 <= end_day <= 31: 372 raise ValueError("end_dayは1から31の間である必要があります") 373 374 if start_day is not None and end_day is not None and start_day > end_day: 375 raise ValueError("start_dayはend_day以下である必要があります") 376 377 # datetime_column をDatetime型に変換 378 df = df.copy() 379 df[datetime_column] = pd.to_datetime(df[datetime_column]) 380 381 # 月でフィルタリング 382 monthly_data = df[df[datetime_column].dt.month.isin(target_months)] 383 384 # 日付範囲でフィルタリング 385 if start_day is not None: 386 monthly_data = monthly_data[ 387 monthly_data[datetime_column].dt.day >= start_day 388 ] 389 if end_day is not None: 390 monthly_data = monthly_data[monthly_data[datetime_column].dt.day <= end_day] 391 392 return monthly_data 393 394 @staticmethod 395 def merge_dataframes( 396 df1: pd.DataFrame, df2: pd.DataFrame, date_column: str = "Date" 397 ) -> pd.DataFrame: 398 """ 399 2つのDataFrameを結合します。重複するカラムは元の名前とサフィックス付きの両方を保持します。 400 401 Parameters: 402 ------ 403 df1 : pd.DataFrame 404 ベースとなるDataFrame 405 df2 : pd.DataFrame 406 結合するDataFrame 407 date_column : str 408 日付カラムの名前。デフォルトは"Date" 409 410 Returns: 411 ------ 412 pd.DataFrame 413 結合されたDataFrame 414 """ 415 # インデックスをリセット 416 df1 = df1.reset_index(drop=True) 417 df2 = df2.reset_index(drop=True) 418 419 # 日付カラムを統一 420 df2[date_column] = df1[date_column] 421 422 # 重複しないカラムと重複するカラムを分離 423 duplicate_cols = [date_column, "year", "month"] # 常に除外するカラム 424 overlapping_cols = [ 425 col 426 for col in df2.columns 427 if col in df1.columns and col not in duplicate_cols 428 ] 429 unique_cols = [ 430 col 431 for col in df2.columns 432 if col not in df1.columns and col not in duplicate_cols 433 ] 434 435 # 結果のDataFrameを作成 436 result = df1.copy() 437 438 # 重複しないカラムを追加 439 for col in unique_cols: 440 result[col] = df2[col] 441 442 # 重複するカラムを処理 443 for col in overlapping_cols: 444 # 元のカラムはdf1の値を保持(既に result に含まれている) 445 # _x サフィックスでdf1の値を追加 446 result[f"{col}_x"] = df1[col] 447 # _y サフィックスでdf2の値を追加 448 result[f"{col}_y"] = df2[col] 449 450 return result
Monthlyシート(Excel)を一括で読み込み、DataFrameに変換するクラス。 デフォルトは'SA.Ultra..xlsx'に対応していますが、コンストラクタのfile_patternを 変更すると別のシートにも対応可能です(例: 'SA.Picaro..xlsx')。
18 def __init__( 19 self, 20 directory: str | Path, 21 file_pattern: str = "SA.Ultra.*.xlsx", 22 logger: Logger | None = None, 23 logging_debug: bool = False, 24 ): 25 """ 26 MonthlyConverterクラスのコンストラクタ 27 28 Parameters: 29 ------ 30 directory : str | Path 31 Excelファイルが格納されているディレクトリのパス 32 file_pattern : str 33 ファイル名のパターン。デフォルトは'SA.Ultra.*.xlsx'。 34 logger : Logger | None 35 使用するロガー。Noneの場合は新しいロガーを作成します。 36 logging_debug : bool 37 ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。 38 """ 39 # ロガー 40 log_level: int = INFO 41 if logging_debug: 42 log_level = DEBUG 43 self.logger: Logger = MonthlyConverter.setup_logger(logger, log_level) 44 45 self._directory = Path(directory) 46 if not self._directory.exists(): 47 raise NotADirectoryError(f"Directory not found: {self._directory}") 48 49 # Excelファイルのパスを保持 50 self._excel_files: dict[str, pd.ExcelFile] = {} 51 self._file_pattern: str = file_pattern
MonthlyConverterクラスのコンストラクタ
Parameters:
directory : str | Path
Excelファイルが格納されているディレクトリのパス
file_pattern : str
ファイル名のパターン。デフォルトは'SA.Ultra.*.xlsx'。
logger : Logger | None
使用するロガー。Noneの場合は新しいロガーを作成します。
logging_debug : bool
ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。
53 @staticmethod 54 def setup_logger(logger: Logger | None, log_level: int = INFO) -> Logger: 55 """ 56 ロガーを設定します。 57 58 このメソッドは、ロギングの設定を行い、ログメッセージのフォーマットを指定します。 59 ログメッセージには、日付、ログレベル、メッセージが含まれます。 60 61 渡されたロガーがNoneまたは不正な場合は、新たにロガーを作成し、標準出力に 62 ログメッセージが表示されるようにStreamHandlerを追加します。ロガーのレベルは 63 引数で指定されたlog_levelに基づいて設定されます。 64 65 Parameters: 66 ------ 67 logger : Logger | None 68 使用するロガー。Noneの場合は新しいロガーを作成します。 69 log_level : int 70 ロガーのログレベル。デフォルトはINFO。 71 72 Returns: 73 ------ 74 Logger 75 設定されたロガーオブジェクト。 76 """ 77 if logger is not None and isinstance(logger, Logger): 78 return logger 79 # 渡されたロガーがNoneまたは正しいものでない場合は独自に設定 80 new_logger: Logger = getLogger() 81 # 既存のハンドラーをすべて削除 82 for handler in new_logger.handlers[:]: 83 new_logger.removeHandler(handler) 84 new_logger.setLevel(log_level) # ロガーのレベルを設定 85 ch = StreamHandler() 86 ch_formatter = Formatter("%(asctime)s - %(levelname)s - %(message)s") 87 ch.setFormatter(ch_formatter) # フォーマッターをハンドラーに設定 88 new_logger.addHandler(ch) # StreamHandlerの追加 89 return new_logger
ロガーを設定します。
このメソッドは、ロギングの設定を行い、ログメッセージのフォーマットを指定します。 ログメッセージには、日付、ログレベル、メッセージが含まれます。
渡されたロガーがNoneまたは不正な場合は、新たにロガーを作成し、標準出力に ログメッセージが表示されるようにStreamHandlerを追加します。ロガーのレベルは 引数で指定されたlog_levelに基づいて設定されます。
Parameters:
logger : Logger | None
使用するロガー。Noneの場合は新しいロガーを作成します。
log_level : int
ロガーのログレベル。デフォルトはINFO。
Returns:
Logger
設定されたロガーオブジェクト。
91 def close(self) -> None: 92 """ 93 すべてのExcelファイルをクローズする 94 """ 95 for excel_file in self._excel_files.values(): 96 excel_file.close() 97 self._excel_files.clear()
すべてのExcelファイルをクローズする
99 def get_available_dates(self) -> list[str]: 100 """ 101 利用可能なファイルの日付一覧を返却します。 102 103 Returns: 104 ------ 105 list[str] 106 'yyyy.MM'形式の日付リスト 107 """ 108 dates = [] 109 for file_name in self._directory.glob(self._file_pattern): 110 try: 111 date = self._extract_date(file_name.name) 112 dates.append(date.strftime(self.FILE_DATE_FORMAT)) 113 except ValueError: 114 continue 115 return sorted(dates)
利用可能なファイルの日付一覧を返却します。
Returns:
list[str]
'yyyy.MM'形式の日付リスト
117 def get_sheet_names(self, file_name: str) -> list[str]: 118 """ 119 指定されたファイルで利用可能なシート名の一覧を返却する 120 121 Parameters: 122 ------ 123 file_name : str 124 Excelファイル名 125 126 Returns: 127 ------ 128 list[str] 129 シート名のリスト 130 """ 131 if file_name not in self._excel_files: 132 file_path = self._directory / file_name 133 if not file_path.exists(): 134 raise FileNotFoundError(f"File not found: {file_path}") 135 self._excel_files[file_name] = pd.ExcelFile(file_path) 136 return self._excel_files[file_name].sheet_names
指定されたファイルで利用可能なシート名の一覧を返却する
Parameters:
file_name : str
Excelファイル名
Returns:
list[str]
シート名のリスト
138 def read_sheets( 139 self, 140 sheet_names: str | list[str], 141 columns: list[str] | None = None, # 新しいパラメータを追加 142 col_datetime: str = "Date", 143 header: int = 0, 144 skiprows: int | list[int] = [1], 145 start_date: str | None = None, 146 end_date: str | None = None, 147 include_end_date: bool = True, 148 sort_by_date: bool = True, 149 ) -> pd.DataFrame: 150 """ 151 指定されたシートを読み込み、DataFrameとして返却します。 152 デフォルトでは2行目(単位の行)はスキップされます。 153 重複するカラム名がある場合は、より先に指定されたシートに存在するカラムの値を保持します。 154 155 Parameters: 156 ------ 157 sheet_names : str | list[str] 158 読み込むシート名。文字列または文字列のリストを指定できます。 159 columns : list[str] | None 160 残すカラム名のリスト。Noneの場合は全てのカラムを保持します。 161 col_datetime : str 162 日付と時刻の情報が含まれるカラム名。デフォルトは'Date'。 163 header : int 164 データのヘッダー行を指定します。デフォルトは0。 165 skiprows : int | list[int] 166 スキップする行数。デフォルトでは1行目をスキップします。 167 start_date : str | None 168 開始日 ('yyyy-MM-dd')。この日付の'00:00:00'のデータが開始行となります。 169 end_date : str | None 170 終了日 ('yyyy-MM-dd')。この日付をデータに含めるかはinclude_end_dateフラグによって変わります。 171 include_end_date : bool 172 終了日を含めるかどうか。デフォルトはTrueです。 173 sort_by_date : bool 174 ファイルの日付でソートするかどうか。デフォルトはTrueです。 175 176 Returns: 177 ------ 178 pd.DataFrame 179 読み込まれたデータを結合したDataFrameを返します。 180 """ 181 if isinstance(sheet_names, str): 182 sheet_names = [sheet_names] 183 184 self._load_excel_files(start_date, end_date) 185 186 if not self._excel_files: 187 raise ValueError("No Excel files found matching the criteria") 188 189 # ファイルを日付順にソート 190 sorted_files = ( 191 sorted(self._excel_files.items(), key=lambda x: self._extract_date(x[0])) 192 if sort_by_date 193 else self._excel_files.items() 194 ) 195 196 # 各シートのデータを格納するリスト 197 sheet_dfs = {sheet_name: [] for sheet_name in sheet_names} 198 199 # 各ファイルからデータを読み込む 200 for file_name, excel_file in sorted_files: 201 file_date = self._extract_date(file_name) 202 203 for sheet_name in sheet_names: 204 if sheet_name in excel_file.sheet_names: 205 df = pd.read_excel( 206 excel_file, 207 sheet_name=sheet_name, 208 header=header, 209 skiprows=skiprows, 210 na_values=[ 211 "#DIV/0!", 212 "#VALUE!", 213 "#REF!", 214 "#N/A", 215 "#NAME?", 216 "NAN", 217 ], 218 ) 219 # 年と月を追加 220 df["year"] = file_date.year 221 df["month"] = file_date.month 222 sheet_dfs[sheet_name].append(df) 223 224 if not any(sheet_dfs.values()): 225 raise ValueError(f"No sheets found matching: {sheet_names}") 226 227 # 各シートのデータを結合 228 combined_sheets = {} 229 for sheet_name, dfs in sheet_dfs.items(): 230 if dfs: # シートにデータがある場合のみ結合 231 combined_sheets[sheet_name] = pd.concat(dfs, ignore_index=True) 232 233 # 最初のシートをベースにする 234 base_df = combined_sheets[sheet_names[0]] 235 236 # 2つ目以降のシートを結合 237 for sheet_name in sheet_names[1:]: 238 if sheet_name in combined_sheets: 239 base_df = self.merge_dataframes( 240 base_df, combined_sheets[sheet_name], date_column=col_datetime 241 ) 242 243 # 日付でフィルタリング 244 if start_date: 245 start_dt = pd.to_datetime(start_date) 246 base_df = base_df[base_df[col_datetime] >= start_dt] 247 248 if end_date: 249 end_dt = pd.to_datetime(end_date) 250 if include_end_date: 251 end_dt += pd.Timedelta(days=1) 252 base_df = base_df[base_df[col_datetime] < end_dt] 253 254 # カラムの選択 255 if columns is not None: 256 required_columns = [col_datetime, "year", "month"] 257 available_columns = base_df.columns.tolist() # 利用可能なカラムを取得 258 if not all(col in available_columns for col in columns): 259 raise ValueError( 260 f"指定されたカラムが見つかりません: {columns}. 利用可能なカラム: {available_columns}" 261 ) 262 selected_columns = list(set(columns + required_columns)) 263 base_df = base_df[selected_columns] 264 265 return base_df
指定されたシートを読み込み、DataFrameとして返却します。 デフォルトでは2行目(単位の行)はスキップされます。 重複するカラム名がある場合は、より先に指定されたシートに存在するカラムの値を保持します。
Parameters:
sheet_names : str | list[str]
読み込むシート名。文字列または文字列のリストを指定できます。
columns : list[str] | None
残すカラム名のリスト。Noneの場合は全てのカラムを保持します。
col_datetime : str
日付と時刻の情報が含まれるカラム名。デフォルトは'Date'。
header : int
データのヘッダー行を指定します。デフォルトは0。
skiprows : int | list[int]
スキップする行数。デフォルトでは1行目をスキップします。
start_date : str | None
開始日 ('yyyy-MM-dd')。この日付の'00:00:00'のデータが開始行となります。
end_date : str | None
終了日 ('yyyy-MM-dd')。この日付をデータに含めるかはinclude_end_dateフラグによって変わります。
include_end_date : bool
終了日を含めるかどうか。デフォルトはTrueです。
sort_by_date : bool
ファイルの日付でソートするかどうか。デフォルトはTrueです。
Returns:
pd.DataFrame
読み込まれたデータを結合したDataFrameを返します。
335 @staticmethod 336 def extract_monthly_data( 337 df: pd.DataFrame, 338 target_months: list[int], 339 start_day: int | None = None, 340 end_day: int | None = None, 341 datetime_column: str = "Date", 342 ) -> pd.DataFrame: 343 """ 344 指定された月と期間のデータを抽出します。 345 346 Parameters: 347 ------ 348 df : pd.DataFrame 349 入力データフレーム。 350 target_months : list[int] 351 抽出したい月のリスト(1から12の整数)。 352 start_day : int | None 353 開始日(1から31の整数)。Noneの場合は月初め。 354 end_day : int | None 355 終了日(1から31の整数)。Noneの場合は月末。 356 datetime_column : str, optional 357 日付を含む列の名前。デフォルトは"Date"。 358 359 Returns: 360 ------ 361 pd.DataFrame 362 指定された期間のデータのみを含むデータフレーム。 363 """ 364 # 入力チェック 365 if not all(1 <= month <= 12 for month in target_months): 366 raise ValueError("target_monthsは1から12の間である必要があります") 367 368 if start_day is not None and not 1 <= start_day <= 31: 369 raise ValueError("start_dayは1から31の間である必要があります") 370 371 if end_day is not None and not 1 <= end_day <= 31: 372 raise ValueError("end_dayは1から31の間である必要があります") 373 374 if start_day is not None and end_day is not None and start_day > end_day: 375 raise ValueError("start_dayはend_day以下である必要があります") 376 377 # datetime_column をDatetime型に変換 378 df = df.copy() 379 df[datetime_column] = pd.to_datetime(df[datetime_column]) 380 381 # 月でフィルタリング 382 monthly_data = df[df[datetime_column].dt.month.isin(target_months)] 383 384 # 日付範囲でフィルタリング 385 if start_day is not None: 386 monthly_data = monthly_data[ 387 monthly_data[datetime_column].dt.day >= start_day 388 ] 389 if end_day is not None: 390 monthly_data = monthly_data[monthly_data[datetime_column].dt.day <= end_day] 391 392 return monthly_data
指定された月と期間のデータを抽出します。
Parameters:
df : pd.DataFrame
入力データフレーム。
target_months : list[int]
抽出したい月のリスト(1から12の整数)。
start_day : int | None
開始日(1から31の整数)。Noneの場合は月初め。
end_day : int | None
終了日(1から31の整数)。Noneの場合は月末。
datetime_column : str, optional
日付を含む列の名前。デフォルトは"Date"。
Returns:
pd.DataFrame
指定された期間のデータのみを含むデータフレーム。
394 @staticmethod 395 def merge_dataframes( 396 df1: pd.DataFrame, df2: pd.DataFrame, date_column: str = "Date" 397 ) -> pd.DataFrame: 398 """ 399 2つのDataFrameを結合します。重複するカラムは元の名前とサフィックス付きの両方を保持します。 400 401 Parameters: 402 ------ 403 df1 : pd.DataFrame 404 ベースとなるDataFrame 405 df2 : pd.DataFrame 406 結合するDataFrame 407 date_column : str 408 日付カラムの名前。デフォルトは"Date" 409 410 Returns: 411 ------ 412 pd.DataFrame 413 結合されたDataFrame 414 """ 415 # インデックスをリセット 416 df1 = df1.reset_index(drop=True) 417 df2 = df2.reset_index(drop=True) 418 419 # 日付カラムを統一 420 df2[date_column] = df1[date_column] 421 422 # 重複しないカラムと重複するカラムを分離 423 duplicate_cols = [date_column, "year", "month"] # 常に除外するカラム 424 overlapping_cols = [ 425 col 426 for col in df2.columns 427 if col in df1.columns and col not in duplicate_cols 428 ] 429 unique_cols = [ 430 col 431 for col in df2.columns 432 if col not in df1.columns and col not in duplicate_cols 433 ] 434 435 # 結果のDataFrameを作成 436 result = df1.copy() 437 438 # 重複しないカラムを追加 439 for col in unique_cols: 440 result[col] = df2[col] 441 442 # 重複するカラムを処理 443 for col in overlapping_cols: 444 # 元のカラムはdf1の値を保持(既に result に含まれている) 445 # _x サフィックスでdf1の値を追加 446 result[f"{col}_x"] = df1[col] 447 # _y サフィックスでdf2の値を追加 448 result[f"{col}_y"] = df2[col] 449 450 return result
2つのDataFrameを結合します。重複するカラムは元の名前とサフィックス付きの両方を保持します。
Parameters:
df1 : pd.DataFrame
ベースとなるDataFrame
df2 : pd.DataFrame
結合するDataFrame
date_column : str
日付カラムの名前。デフォルトは"Date"
Returns:
pd.DataFrame
結合されたDataFrame
63class MonthlyFiguresGenerator: 64 def __init__( 65 self, 66 logger: Logger | None = None, 67 logging_debug: bool = False, 68 ) -> None: 69 """ 70 クラスのコンストラクタ 71 72 Parameters: 73 ------ 74 logger : Logger | None 75 使用するロガー。Noneの場合は新しいロガーを作成します。 76 logging_debug : bool 77 ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。 78 """ 79 # ロガー 80 log_level: int = INFO 81 if logging_debug: 82 log_level = DEBUG 83 self.logger: Logger = MonthlyFiguresGenerator.setup_logger(logger, log_level) 84 85 def plot_c1c2_fluxes_timeseries( 86 self, 87 df, 88 output_dir: str, 89 output_filename: str = "timeseries.png", 90 col_datetime: str = "Date", 91 col_c1_flux: str = "Fch4_ultra", 92 col_c2_flux: str = "Fc2h6_ultra", 93 ): 94 """ 95 月別のフラックスデータを時系列プロットとして出力する 96 97 Parameters: 98 ------ 99 df : pd.DataFrame 100 月別データを含むDataFrame 101 output_dir : str 102 出力ファイルを保存するディレクトリのパス 103 output_filename : str 104 出力ファイルの名前 105 col_datetime : str 106 日付を含む列の名前。デフォルトは"Date"。 107 col_c1_flux : str 108 CH4フラックスを含む列の名前。デフォルトは"Fch4_ultra"。 109 col_c2_flux : str 110 C2H6フラックスを含む列の名前。デフォルトは"Fc2h6_ultra"。 111 """ 112 os.makedirs(output_dir, exist_ok=True) 113 output_path: str = os.path.join(output_dir, output_filename) 114 115 # 図の作成 116 _, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 8), sharex=True) 117 118 # CH4フラックスのプロット 119 ax1.scatter(df[col_datetime], df[col_c1_flux], color="red", alpha=0.5, s=20) 120 ax1.set_ylabel(r"CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)") 121 ax1.set_ylim(-100, 600) 122 ax1.text(0.02, 0.98, "(a)", transform=ax1.transAxes, va="top", fontsize=20) 123 ax1.grid(True, alpha=0.3) 124 125 # C2H6フラックスのプロット 126 ax2.scatter( 127 df[col_datetime], 128 df[col_c2_flux], 129 color="orange", 130 alpha=0.5, 131 s=20, 132 ) 133 ax2.set_ylabel(r"C$_2$H$_6$ flux (nmol m$^{-2}$ s$^{-1}$)") 134 ax2.set_ylim(-20, 60) 135 ax2.text(0.02, 0.98, "(b)", transform=ax2.transAxes, va="top", fontsize=20) 136 ax2.grid(True, alpha=0.3) 137 138 # x軸の設定 139 ax2.xaxis.set_major_locator(mdates.MonthLocator(interval=1)) 140 ax2.xaxis.set_major_formatter(mdates.DateFormatter("%m")) 141 plt.setp(ax2.get_xticklabels(), rotation=0, ha="right") 142 ax2.set_xlabel("Month") 143 144 # 図の保存 145 plt.savefig(output_path, dpi=300, bbox_inches="tight") 146 plt.close() 147 148 def plot_c1c2_concentrations_and_fluxes_timeseries( 149 self, 150 df: pd.DataFrame, 151 output_dir: str, 152 output_filename: str = "conc_flux_timeseries.png", 153 col_datetime: str = "Date", 154 col_ch4_conc: str = "CH4_ultra", 155 col_ch4_flux: str = "Fch4_ultra", 156 col_c2h6_conc: str = "C2H6_ultra", 157 col_c2h6_flux: str = "Fc2h6_ultra", 158 print_summary: bool = True, 159 ) -> None: 160 """ 161 CH4とC2H6の濃度とフラックスの時系列プロットを作成する 162 163 Parameters: 164 ------ 165 df : pd.DataFrame 166 月別データを含むDataFrame 167 output_dir : str 168 出力ディレクトリのパス 169 output_filename : str 170 出力ファイル名 171 col_datetime : str 172 日付列の名前 173 col_ch4_conc : str 174 CH4濃度列の名前 175 col_ch4_flux : str 176 CH4フラックス列の名前 177 col_c2h6_conc : str 178 C2H6濃度列の名前 179 col_c2h6_flux : str 180 C2H6フラックス列の名前 181 print_summary : bool 182 解析情報をprintするかどうか 183 """ 184 # 出力ディレクトリの作成 185 os.makedirs(output_dir, exist_ok=True) 186 output_path: str = os.path.join(output_dir, output_filename) 187 188 if print_summary: 189 # 統計情報の計算と表示 190 for name, col in [ 191 ("CH4 concentration", col_ch4_conc), 192 ("CH4 flux", col_ch4_flux), 193 ("C2H6 concentration", col_c2h6_conc), 194 ("C2H6 flux", col_c2h6_flux), 195 ]: 196 # NaNを除外してから統計量を計算 197 valid_data = df[col].dropna() 198 199 if len(valid_data) > 0: 200 percentile_5 = np.nanpercentile(valid_data, 5) 201 percentile_95 = np.nanpercentile(valid_data, 95) 202 mean_value = np.nanmean(valid_data) 203 positive_ratio = (valid_data > 0).mean() * 100 204 205 print(f"\n{name}:") 206 print( 207 f"90パーセンタイルレンジ: {percentile_5:.2f} - {percentile_95:.2f}" 208 ) 209 print(f"平均値: {mean_value:.2f}") 210 print(f"正の値の割合: {positive_ratio:.1f}%") 211 else: 212 print(f"\n{name}: データが存在しません") 213 214 # プロットの作成 215 _, (ax1, ax2, ax3, ax4) = plt.subplots(4, 1, figsize=(12, 16), sharex=True) 216 217 # CH4濃度のプロット 218 ax1.scatter(df[col_datetime], df[col_ch4_conc], color="red", alpha=0.5, s=20) 219 ax1.set_ylabel("CH$_4$ Concentration\n(ppm)") 220 ax1.set_ylim(1.8, 2.6) 221 ax1.text(0.02, 0.98, "(a)", transform=ax1.transAxes, va="top", fontsize=20) 222 ax1.grid(True, alpha=0.3) 223 224 # CH4フラックスのプロット 225 ax2.scatter(df[col_datetime], df[col_ch4_flux], color="red", alpha=0.5, s=20) 226 ax2.set_ylabel("CH$_4$ flux\n(nmol m$^{-2}$ s$^{-1}$)") 227 ax2.set_ylim(-100, 600) 228 # ax2.set_yticks([-100, 0, 200, 400, 600]) 229 ax2.text(0.02, 0.98, "(b)", transform=ax2.transAxes, va="top", fontsize=20) 230 ax2.grid(True, alpha=0.3) 231 232 # C2H6濃度のプロット 233 ax3.scatter( 234 df[col_datetime], df[col_c2h6_conc], color="orange", alpha=0.5, s=20 235 ) 236 ax3.set_ylabel("C$_2$H$_6$ Concentration\n(ppb)") 237 ax3.text(0.02, 0.98, "(c)", transform=ax3.transAxes, va="top", fontsize=20) 238 ax3.grid(True, alpha=0.3) 239 240 # C2H6フラックスのプロット 241 ax4.scatter( 242 df[col_datetime], df[col_c2h6_flux], color="orange", alpha=0.5, s=20 243 ) 244 ax4.set_ylabel("C$_2$H$_6$ flux\n(nmol m$^{-2}$ s$^{-1}$)") 245 ax4.set_ylim(-20, 40) 246 ax4.text(0.02, 0.98, "(d)", transform=ax4.transAxes, va="top", fontsize=20) 247 ax4.grid(True, alpha=0.3) 248 249 # x軸の設定 250 ax4.xaxis.set_major_locator(mdates.MonthLocator(interval=1)) 251 ax4.xaxis.set_major_formatter(mdates.DateFormatter("%m")) 252 plt.setp(ax4.get_xticklabels(), rotation=0, ha="right") 253 ax4.set_xlabel("Month") 254 255 # レイアウトの調整と保存 256 plt.tight_layout() 257 plt.savefig(output_path, dpi=300, bbox_inches="tight") 258 plt.close() 259 260 if print_summary: 261 262 def analyze_top_values(df, column_name, top_percent=20): 263 print(f"\n{column_name}の上位{top_percent}%の分析:") 264 265 # DataFrameのコピーを作成し、日時関連の列を追加 266 df_analysis = df.copy() 267 df_analysis["hour"] = pd.to_datetime(df_analysis[col_datetime]).dt.hour 268 df_analysis["month"] = pd.to_datetime( 269 df_analysis[col_datetime] 270 ).dt.month 271 df_analysis["weekday"] = pd.to_datetime( 272 df_analysis[col_datetime] 273 ).dt.dayofweek 274 275 # 上位20%のしきい値を計算 276 threshold = df[column_name].quantile(1 - top_percent / 100) 277 high_values = df_analysis[df_analysis[column_name] > threshold] 278 279 # 月ごとの分析 280 print("\n月別分布:") 281 monthly_counts = high_values.groupby("month").size() 282 total_counts = df_analysis.groupby("month").size() 283 monthly_percentages = (monthly_counts / total_counts * 100).round(1) 284 285 # 月ごとのデータを安全に表示 286 available_months = set(monthly_counts.index) & set(total_counts.index) 287 for month in sorted(available_months): 288 print( 289 f"月{month}: {monthly_percentages[month]}% ({monthly_counts[month]}件/{total_counts[month]}件)" 290 ) 291 292 # 時間帯ごとの分析(3時間区切り) 293 print("\n時間帯別分布:") 294 # copyを作成して新しい列を追加 295 high_values = high_values.copy() 296 high_values["time_block"] = high_values["hour"] // 3 * 3 297 time_blocks = high_values.groupby("time_block").size() 298 total_time_blocks = df_analysis.groupby( 299 df_analysis["hour"] // 3 * 3 300 ).size() 301 time_percentages = (time_blocks / total_time_blocks * 100).round(1) 302 303 # 時間帯ごとのデータを安全に表示 304 available_blocks = set(time_blocks.index) & set(total_time_blocks.index) 305 for block in sorted(available_blocks): 306 print( 307 f"{block:02d}:00-{block + 3:02d}:00: {time_percentages[block]}% ({time_blocks[block]}件/{total_time_blocks[block]}件)" 308 ) 309 310 # 曜日ごとの分析 311 print("\n曜日別分布:") 312 weekday_names = ["月曜", "火曜", "水曜", "木曜", "金曜", "土曜", "日曜"] 313 weekday_counts = high_values.groupby("weekday").size() 314 total_weekdays = df_analysis.groupby("weekday").size() 315 weekday_percentages = (weekday_counts / total_weekdays * 100).round(1) 316 317 # 曜日ごとのデータを安全に表示 318 available_days = set(weekday_counts.index) & set(total_weekdays.index) 319 for day in sorted(available_days): 320 if 0 <= day <= 6: # 有効な曜日インデックスのチェック 321 print( 322 f"{weekday_names[day]}: {weekday_percentages[day]}% ({weekday_counts[day]}件/{total_weekdays[day]}件)" 323 ) 324 325 # 濃度とフラックスそれぞれの分析を実行 326 print("\n=== 上位値の時間帯・曜日分析 ===") 327 analyze_top_values(df, col_ch4_conc) 328 analyze_top_values(df, col_ch4_flux) 329 analyze_top_values(df, col_c2h6_conc) 330 analyze_top_values(df, col_c2h6_flux) 331 332 def plot_ch4c2h6_timeseries( 333 self, 334 df: pd.DataFrame, 335 output_dir: str, 336 col_ch4_flux: str, 337 col_c2h6_flux: str, 338 output_filename: str = "timeseries_year.png", 339 col_datetime: str = "Date", 340 window_size: int = 24 * 7, # 1週間の移動平均のデフォルト値 341 confidence_interval: float = 0.95, # 95%信頼区間 342 subplot_label_ch4: str | None = "(a)", 343 subplot_label_c2h6: str | None = "(b)", 344 subplot_fontsize: int = 20, 345 show_ci: bool = True, 346 ch4_ylim: tuple[float, float] | None = None, 347 c2h6_ylim: tuple[float, float] | None = None, 348 start_date: str | None = None, # 追加:"YYYY-MM-DD"形式 349 end_date: str | None = None, # 追加:"YYYY-MM-DD"形式 350 figsize: tuple[float, float] = (16, 6), 351 ) -> None: 352 """CH4とC2H6フラックスの時系列変動をプロット 353 354 Parameters: 355 ------ 356 df : pd.DataFrame 357 データフレーム 358 output_dir : str 359 出力ディレクトリのパス 360 col_ch4_flux : str 361 CH4フラックスのカラム名 362 col_c2h6_flux : str 363 C2H6フラックスのカラム名 364 output_filename : str 365 出力ファイル名 366 col_datetime : str 367 日時カラムの名前 368 window_size : int 369 移動平均の窓サイズ 370 confidence_interval : float 371 信頼区間(0-1) 372 subplot_label_ch4 : str | None 373 CH4プロットのラベル 374 subplot_label_c2h6 : str | None 375 C2H6プロットのラベル 376 subplot_fontsize : int 377 サブプロットのフォントサイズ 378 show_ci : bool 379 信頼区間を表示するか 380 ch4_ylim : tuple[float, float] | None 381 CH4のy軸範囲 382 c2h6_ylim : tuple[float, float] | None 383 C2H6のy軸範囲 384 start_date : str | None 385 開始日(YYYY-MM-DD形式) 386 end_date : str | None 387 終了日(YYYY-MM-DD形式) 388 """ 389 # 出力ディレクトリの作成 390 os.makedirs(output_dir, exist_ok=True) 391 output_path: str = os.path.join(output_dir, output_filename) 392 393 # データの準備 394 df = df.copy() 395 if not isinstance(df.index, pd.DatetimeIndex): 396 df[col_datetime] = pd.to_datetime(df[col_datetime]) 397 df.set_index(col_datetime, inplace=True) 398 399 # 日付範囲の処理 400 if start_date is not None: 401 start_dt = pd.to_datetime(start_date) 402 if start_dt < df.index.min(): 403 self.logger.warning( 404 f"指定された開始日{start_date}がデータの開始日{df.index.min():%Y-%m-%d}より前です。" 405 f"データの開始日を使用します。" 406 ) 407 start_dt = df.index.min() 408 else: 409 start_dt = df.index.min() 410 411 if end_date is not None: 412 end_dt = pd.to_datetime(end_date) 413 if end_dt > df.index.max(): 414 self.logger.warning( 415 f"指定された終了日{end_date}がデータの終了日{df.index.max():%Y-%m-%d}より後です。" 416 f"データの終了日を使用します。" 417 ) 418 end_dt = df.index.max() 419 else: 420 end_dt = df.index.max() 421 422 # 指定された期間のデータを抽出 423 mask = (df.index >= start_dt) & (df.index <= end_dt) 424 df = df[mask] 425 426 # CH4とC2H6の移動平均と信頼区間を計算 427 ch4_mean, ch4_lower, ch4_upper = calculate_rolling_stats( 428 df[col_ch4_flux], window_size, confidence_interval 429 ) 430 c2h6_mean, c2h6_lower, c2h6_upper = calculate_rolling_stats( 431 df[col_c2h6_flux], window_size, confidence_interval 432 ) 433 434 # プロットの作成 435 fig, (ax1, ax2) = plt.subplots(1, 2, figsize=figsize) 436 437 # CH4プロット 438 ax1.plot(df.index, ch4_mean, "red", label="CH$_4$") 439 if show_ci: 440 ax1.fill_between(df.index, ch4_lower, ch4_upper, color="red", alpha=0.2) 441 if subplot_label_ch4: 442 ax1.text( 443 0.02, 444 0.98, 445 subplot_label_ch4, 446 transform=ax1.transAxes, 447 va="top", 448 fontsize=subplot_fontsize, 449 ) 450 ax1.set_ylabel("CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)") 451 if ch4_ylim is not None: 452 ax1.set_ylim(ch4_ylim) 453 ax1.grid(True, alpha=0.3) 454 455 # C2H6プロット 456 ax2.plot(df.index, c2h6_mean, "orange", label="C$_2$H$_6$") 457 if show_ci: 458 ax2.fill_between( 459 df.index, c2h6_lower, c2h6_upper, color="orange", alpha=0.2 460 ) 461 if subplot_label_c2h6: 462 ax2.text( 463 0.02, 464 0.98, 465 subplot_label_c2h6, 466 transform=ax2.transAxes, 467 va="top", 468 fontsize=subplot_fontsize, 469 ) 470 ax2.set_ylabel("C$_2$H$_6$ flux (nmol m$^{-2}$ s$^{-1}$)") 471 if c2h6_ylim is not None: 472 ax2.set_ylim(c2h6_ylim) 473 ax2.grid(True, alpha=0.3) 474 475 # x軸の設定 476 for ax in [ax1, ax2]: 477 ax.set_xlabel("Month") 478 # x軸の範囲を設定 479 ax.set_xlim(start_dt, end_dt) 480 481 # 1ヶ月ごとの主目盛り 482 ax.xaxis.set_major_locator(mdates.MonthLocator(interval=1)) 483 484 # カスタムフォーマッタの作成(数字を通常フォントで表示) 485 def date_formatter(x, p): 486 date = mdates.num2date(x) 487 return f"{date.strftime('%m')}" 488 489 ax.xaxis.set_major_formatter(plt.FuncFormatter(date_formatter)) 490 491 # 補助目盛りの設定 492 ax.xaxis.set_minor_locator(mdates.MonthLocator()) 493 # ティックラベルの回転と位置調整 494 plt.setp(ax.xaxis.get_majorticklabels(), ha="right") 495 496 plt.tight_layout() 497 plt.savefig(output_path, dpi=300, bbox_inches="tight") 498 plt.close(fig) 499 500 def plot_ch4_flux_comparison( 501 self, 502 df: pd.DataFrame, 503 output_dir: str, 504 col_g2401_flux: str, 505 col_ultra_flux: str, 506 output_filename: str = "ch4_flux_comparison.png", 507 col_datetime: str = "Date", 508 window_size: int = 24 * 7, # 1週間の移動平均のデフォルト値 509 confidence_interval: float = 0.95, # 95%信頼区間 510 subplot_label: str | None = None, 511 subplot_fontsize: int = 20, 512 show_ci: bool = True, 513 y_lim: tuple[float, float] | None = None, 514 start_date: str | None = None, 515 end_date: str | None = None, 516 figsize: tuple[float, float] = (12, 6), 517 legend_loc: str = "upper right", 518 ) -> None: 519 """G2401とUltraによるCH4フラックスの時系列比較プロット 520 521 Parameters: 522 ------ 523 df : pd.DataFrame 524 データフレーム 525 output_dir : str 526 出力ディレクトリのパス 527 col_g2401_flux : str 528 G2401のCH4フラックスのカラム名 529 col_ultra_flux : str 530 UltraのCH4フラックスのカラム名 531 output_filename : str 532 出力ファイル名 533 col_datetime : str 534 日時カラムの名前 535 window_size : int 536 移動平均の窓サイズ 537 confidence_interval : float 538 信頼区間(0-1) 539 subplot_label : str | None 540 プロットのラベル 541 subplot_fontsize : int 542 サブプロットのフォントサイズ 543 show_ci : bool 544 信頼区間を表示するか 545 y_lim : tuple[float, float] | None 546 y軸の範囲 547 start_date : str | None 548 開始日(YYYY-MM-DD形式) 549 end_date : str | None 550 終了日(YYYY-MM-DD形式) 551 figsize : tuple[float, float] 552 図のサイズ 553 legend_loc : str 554 凡例の位置 555 """ 556 # 出力ディレクトリの作成 557 os.makedirs(output_dir, exist_ok=True) 558 output_path: str = os.path.join(output_dir, output_filename) 559 560 # データの準備 561 df = df.copy() 562 if not isinstance(df.index, pd.DatetimeIndex): 563 df[col_datetime] = pd.to_datetime(df[col_datetime]) 564 df.set_index(col_datetime, inplace=True) 565 566 # 日付範囲の処理(既存のコードと同様) 567 if start_date is not None: 568 start_dt = pd.to_datetime(start_date) 569 if start_dt < df.index.min(): 570 self.logger.warning( 571 f"指定された開始日{start_date}がデータの開始日{df.index.min():%Y-%m-%d}より前です。" 572 f"データの開始日を使用します。" 573 ) 574 start_dt = df.index.min() 575 else: 576 start_dt = df.index.min() 577 578 if end_date is not None: 579 end_dt = pd.to_datetime(end_date) 580 if end_dt > df.index.max(): 581 self.logger.warning( 582 f"指定された終了日{end_date}がデータの終了日{df.index.max():%Y-%m-%d}より後です。" 583 f"データの終了日を使用します。" 584 ) 585 end_dt = df.index.max() 586 else: 587 end_dt = df.index.max() 588 589 # 指定された期間のデータを抽出 590 mask = (df.index >= start_dt) & (df.index <= end_dt) 591 df = df[mask] 592 593 # 移動平均の計算(既存の関数を使用) 594 g2401_mean, g2401_lower, g2401_upper = calculate_rolling_stats( 595 df[col_g2401_flux], window_size, confidence_interval 596 ) 597 ultra_mean, ultra_lower, ultra_upper = calculate_rolling_stats( 598 df[col_ultra_flux], window_size, confidence_interval 599 ) 600 601 # プロットの作成 602 fig, ax = plt.subplots(figsize=figsize) 603 604 # G2401データのプロット 605 ax.plot(df.index, g2401_mean, "blue", label="G2401", alpha=0.7) 606 if show_ci: 607 ax.fill_between(df.index, g2401_lower, g2401_upper, color="blue", alpha=0.2) 608 609 # Ultraデータのプロット 610 ax.plot(df.index, ultra_mean, "red", label="Ultra", alpha=0.7) 611 if show_ci: 612 ax.fill_between(df.index, ultra_lower, ultra_upper, color="red", alpha=0.2) 613 614 # プロットの設定 615 if subplot_label: 616 ax.text( 617 0.02, 618 0.98, 619 subplot_label, 620 transform=ax.transAxes, 621 va="top", 622 fontsize=subplot_fontsize, 623 ) 624 625 ax.set_ylabel("CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)") 626 ax.set_xlabel("Month") 627 628 if y_lim is not None: 629 ax.set_ylim(y_lim) 630 631 ax.grid(True, alpha=0.3) 632 ax.legend(loc=legend_loc) 633 634 # x軸の設定 635 ax.set_xlim(start_dt, end_dt) 636 ax.xaxis.set_major_locator(mdates.MonthLocator(interval=1)) 637 638 # カスタムフォーマッタの作成(数字を通常フォントで表示) 639 def date_formatter(x, p): 640 date = mdates.num2date(x) 641 return f"{date.strftime('%m')}" 642 643 ax.xaxis.set_major_formatter(plt.FuncFormatter(date_formatter)) 644 ax.xaxis.set_minor_locator(mdates.MonthLocator()) 645 plt.setp(ax.xaxis.get_majorticklabels(), ha="right") 646 647 plt.tight_layout() 648 plt.savefig(output_path, dpi=300, bbox_inches="tight") 649 plt.close(fig) 650 651 def plot_c1c2_fluxes_diurnal_patterns( 652 self, 653 df: pd.DataFrame, 654 y_cols_ch4: list[str], 655 y_cols_c2h6: list[str], 656 labels_ch4: list[str], 657 labels_c2h6: list[str], 658 colors_ch4: list[str], 659 colors_c2h6: list[str], 660 output_dir: str, 661 output_filename: str = "diurnal.png", 662 legend_only_ch4: bool = False, 663 show_label: bool = True, 664 show_legend: bool = True, 665 show_std: bool = False, # 標準偏差表示のオプションを追加 666 std_alpha: float = 0.2, # 標準偏差の透明度 667 subplot_fontsize: int = 20, 668 subplot_label_ch4: str | None = "(a)", 669 subplot_label_c2h6: str | None = "(b)", 670 ax1_ylim: tuple[float, float] | None = None, 671 ax2_ylim: tuple[float, float] | None = None, 672 ) -> None: 673 """CH4とC2H6の日変化パターンを1つの図に並べてプロットする 674 675 Parameters: 676 ------ 677 df : pd.DataFrame 678 入力データフレーム。 679 y_cols_ch4 : list[str] 680 CH4のプロットに使用するカラム名のリスト。 681 y_cols_c2h6 : list[str] 682 C2H6のプロットに使用するカラム名のリスト。 683 labels_ch4 : list[str] 684 CH4の各ラインに対応するラベルのリスト。 685 labels_c2h6 : list[str] 686 C2H6の各ラインに対応するラベルのリスト。 687 colors_ch4 : list[str] 688 CH4の各ラインに使用する色のリスト。 689 colors_c2h6 : list[str] 690 C2H6の各ラインに使用する色のリスト。 691 output_dir : str 692 出力先ディレクトリのパス。 693 output_filename : str, optional 694 出力ファイル名。デフォルトは"diurnal.png"。 695 legend_only_ch4 : bool, optional 696 CH4の凡例のみを表示するかどうか。デフォルトはFalse。 697 show_label : bool, optional 698 サブプロットラベルを表示するかどうか。デフォルトはTrue。 699 show_legend : bool, optional 700 凡例を表示するかどうか。デフォルトはTrue。 701 show_std : bool, optional 702 標準偏差を表示するかどうか。デフォルトはFalse。 703 std_alpha : float, optional 704 標準偏差の透明度。デフォルトは0.2。 705 subplot_fontsize : int, optional 706 サブプロットのフォントサイズ。デフォルトは20。 707 subplot_label_ch4 : str | None, optional 708 CH4プロットのラベル。デフォルトは"(a)"。 709 subplot_label_c2h6 : str | None, optional 710 C2H6プロットのラベル。デフォルトは"(b)"。 711 ax1_ylim : tuple[float, float] | None, optional 712 CH4プロットのy軸の範囲。デフォルトはNone。 713 ax2_ylim : tuple[float, float] | None, optional 714 C2H6プロットのy軸の範囲。デフォルトはNone。 715 """ 716 os.makedirs(output_dir, exist_ok=True) 717 output_path: str = os.path.join(output_dir, output_filename) 718 719 # データの準備 720 target_columns = y_cols_ch4 + y_cols_c2h6 721 hourly_means, time_points = self._prepare_diurnal_data(df, target_columns) 722 723 # 標準偏差の計算を追加 724 hourly_stds = {} 725 if show_std: 726 hourly_stds = df.groupby(df.index.hour)[target_columns].std() 727 # 24時間目のデータ点を追加 728 last_hour = hourly_stds.iloc[0:1].copy() 729 last_hour.index = [24] 730 hourly_stds = pd.concat([hourly_stds, last_hour]) 731 732 # プロットの作成 733 fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5)) 734 735 # CH4のプロット (左側) 736 ch4_lines = [] 737 for y_col, label, color in zip(y_cols_ch4, labels_ch4, colors_ch4): 738 mean_values = hourly_means["all"][y_col] 739 line = ax1.plot( 740 time_points, 741 mean_values, 742 "-o", 743 label=label, 744 color=color, 745 ) 746 ch4_lines.extend(line) 747 748 # 標準偏差の表示 749 if show_std: 750 std_values = hourly_stds[y_col] 751 ax1.fill_between( 752 time_points, 753 mean_values - std_values, 754 mean_values + std_values, 755 color=color, 756 alpha=std_alpha, 757 ) 758 759 # C2H6のプロット (右側) 760 c2h6_lines = [] 761 for y_col, label, color in zip(y_cols_c2h6, labels_c2h6, colors_c2h6): 762 mean_values = hourly_means["all"][y_col] 763 line = ax2.plot( 764 time_points, 765 mean_values, 766 "o-", 767 label=label, 768 color=color, 769 ) 770 c2h6_lines.extend(line) 771 772 # 標準偏差の表示 773 if show_std: 774 std_values = hourly_stds[y_col] 775 ax2.fill_between( 776 time_points, 777 mean_values - std_values, 778 mean_values + std_values, 779 color=color, 780 alpha=std_alpha, 781 ) 782 783 # 軸の設定 784 for ax, ylabel, subplot_label in [ 785 (ax1, r"CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)", subplot_label_ch4), 786 (ax2, r"C$_2$H$_6$ flux (nmol m$^{-2}$ s$^{-1}$)", subplot_label_c2h6), 787 ]: 788 self._setup_diurnal_axes( 789 ax=ax, 790 time_points=time_points, 791 ylabel=ylabel, 792 subplot_label=subplot_label, 793 show_label=show_label, 794 show_legend=False, # 個別の凡例は表示しない 795 subplot_fontsize=subplot_fontsize, 796 ) 797 798 if ax1_ylim is not None: 799 ax1.set_ylim(ax1_ylim) 800 ax1.yaxis.set_major_locator(MultipleLocator(20)) 801 ax1.yaxis.set_major_formatter(FuncFormatter(lambda x, p: f"{x:.0f}")) 802 803 if ax2_ylim is not None: 804 ax2.set_ylim(ax2_ylim) 805 ax2.yaxis.set_major_locator(MultipleLocator(1)) 806 ax2.yaxis.set_major_formatter(FuncFormatter(lambda x, p: f"{x:.1f}")) 807 808 plt.tight_layout() 809 810 # 共通の凡例 811 if show_legend: 812 all_lines = ch4_lines 813 all_labels = [line.get_label() for line in ch4_lines] 814 if not legend_only_ch4: 815 all_lines += c2h6_lines 816 all_labels += [line.get_label() for line in c2h6_lines] 817 fig.legend( 818 all_lines, 819 all_labels, 820 loc="center", 821 bbox_to_anchor=(0.5, 0.02), 822 ncol=len(all_lines), 823 ) 824 plt.subplots_adjust(bottom=0.25) # 下部に凡例用のスペースを確保 825 826 fig.savefig(output_path, dpi=300, bbox_inches="tight") 827 plt.close(fig) 828 829 def plot_c1c2_fluxes_diurnal_patterns_by_date( 830 self, 831 df: pd.DataFrame, 832 y_col_ch4: str, 833 y_col_c2h6: str, 834 output_dir: str, 835 output_filename: str = "diurnal_by_date.png", 836 plot_all: bool = True, 837 plot_weekday: bool = True, 838 plot_weekend: bool = True, 839 plot_holiday: bool = True, 840 show_label: bool = True, 841 show_legend: bool = True, 842 show_std: bool = False, # 標準偏差表示のオプションを追加 843 std_alpha: float = 0.2, # 標準偏差の透明度 844 legend_only_ch4: bool = False, 845 subplot_fontsize: int = 20, 846 subplot_label_ch4: str | None = "(a)", 847 subplot_label_c2h6: str | None = "(b)", 848 ax1_ylim: tuple[float, float] | None = None, 849 ax2_ylim: tuple[float, float] | None = None, 850 print_summary: bool = True, # 追加: 統計情報を表示するかどうか 851 ) -> None: 852 """CH4とC2H6の日変化パターンを日付分類して1つの図に並べてプロットする 853 854 Parameters: 855 ------ 856 df : pd.DataFrame 857 入力データフレーム。 858 y_col_ch4 : str 859 CH4フラックスを含むカラム名。 860 y_col_c2h6 : str 861 C2H6フラックスを含むカラム名。 862 output_dir : str 863 出力先ディレクトリのパス。 864 output_filename : str, optional 865 出力ファイル名。デフォルトは"diurnal_by_date.png"。 866 plot_all : bool, optional 867 すべての日をプロットするかどうか。デフォルトはTrue。 868 plot_weekday : bool, optional 869 平日をプロットするかどうか。デフォルトはTrue。 870 plot_weekend : bool, optional 871 週末をプロットするかどうか。デフォルトはTrue。 872 plot_holiday : bool, optional 873 祝日をプロットするかどうか。デフォルトはTrue。 874 show_label : bool, optional 875 サブプロットラベルを表示するかどうか。デフォルトはTrue。 876 show_legend : bool, optional 877 凡例を表示するかどうか。デフォルトはTrue。 878 show_std : bool, optional 879 標準偏差を表示するかどうか。デフォルトはFalse。 880 std_alpha : float, optional 881 標準偏差の透明度。デフォルトは0.2。 882 legend_only_ch4 : bool, optional 883 CH4の凡例のみを表示するかどうか。デフォルトはFalse。 884 subplot_fontsize : int, optional 885 サブプロットのフォントサイズ。デフォルトは20。 886 subplot_label_ch4 : str | None, optional 887 CH4プロットのラベル。デフォルトは"(a)"。 888 subplot_label_c2h6 : str | None, optional 889 C2H6プロットのラベル。デフォルトは"(b)"。 890 ax1_ylim : tuple[float, float] | None, optional 891 CH4プロットのy軸の範囲。デフォルトはNone。 892 ax2_ylim : tuple[float, float] | None, optional 893 C2H6プロットのy軸の範囲。デフォルトはNone。 894 print_summary : bool, optional 895 統計情報を表示するかどうか。デフォルトはTrue。 896 """ 897 os.makedirs(output_dir, exist_ok=True) 898 output_path: str = os.path.join(output_dir, output_filename) 899 900 # データの準備 901 target_columns = [y_col_ch4, y_col_c2h6] 902 hourly_means, time_points = self._prepare_diurnal_data( 903 df, target_columns, include_date_types=True 904 ) 905 906 # 標準偏差の計算を追加 907 hourly_stds = {} 908 if show_std: 909 for condition in ["all", "weekday", "weekend", "holiday"]: 910 if condition == "all": 911 condition_data = df 912 elif condition == "weekday": 913 condition_data = df[ 914 ~( 915 df.index.dayofweek.isin([5, 6]) 916 | df.index.map(lambda x: jpholiday.is_holiday(x.date())) 917 ) 918 ] 919 elif condition == "weekend": 920 condition_data = df[df.index.dayofweek.isin([5, 6])] 921 else: # holiday 922 condition_data = df[ 923 df.index.map(lambda x: jpholiday.is_holiday(x.date())) 924 ] 925 926 hourly_stds[condition] = condition_data.groupby( 927 condition_data.index.hour 928 )[target_columns].std() 929 # 24時間目のデータ点を追加 930 last_hour = hourly_stds[condition].iloc[0:1].copy() 931 last_hour.index = [24] 932 hourly_stds[condition] = pd.concat([hourly_stds[condition], last_hour]) 933 934 # プロットスタイルの設定 935 styles = { 936 "all": { 937 "color": "black", 938 "linestyle": "-", 939 "alpha": 1.0, 940 "label": "All days", 941 }, 942 "weekday": { 943 "color": "blue", 944 "linestyle": "-", 945 "alpha": 0.8, 946 "label": "Weekdays", 947 }, 948 "weekend": { 949 "color": "red", 950 "linestyle": "-", 951 "alpha": 0.8, 952 "label": "Weekends", 953 }, 954 "holiday": { 955 "color": "green", 956 "linestyle": "-", 957 "alpha": 0.8, 958 "label": "Weekends & Holidays", 959 }, 960 } 961 962 # プロット対象の条件を選択 963 plot_conditions = { 964 "all": plot_all, 965 "weekday": plot_weekday, 966 "weekend": plot_weekend, 967 "holiday": plot_holiday, 968 } 969 selected_conditions = { 970 col: means 971 for col, means in hourly_means.items() 972 if col in plot_conditions and plot_conditions[col] 973 } 974 975 # プロットの作成 976 fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5)) 977 978 # CH4とC2H6のプロット用のラインオブジェクトを保存 979 ch4_lines = [] 980 c2h6_lines = [] 981 982 # CH4とC2H6のプロット 983 for condition, means in selected_conditions.items(): 984 style = styles[condition].copy() 985 986 # CH4プロット 987 mean_values_ch4 = means[y_col_ch4] 988 line_ch4 = ax1.plot(time_points, mean_values_ch4, marker="o", **style) 989 ch4_lines.extend(line_ch4) 990 991 if show_std and condition in hourly_stds: 992 std_values = hourly_stds[condition][y_col_ch4] 993 ax1.fill_between( 994 time_points, 995 mean_values_ch4 - std_values, 996 mean_values_ch4 + std_values, 997 color=style["color"], 998 alpha=std_alpha, 999 ) 1000 1001 # C2H6プロット 1002 style["linestyle"] = "--" 1003 mean_values_c2h6 = means[y_col_c2h6] 1004 line_c2h6 = ax2.plot(time_points, mean_values_c2h6, marker="o", **style) 1005 c2h6_lines.extend(line_c2h6) 1006 1007 if show_std and condition in hourly_stds: 1008 std_values = hourly_stds[condition][y_col_c2h6] 1009 ax2.fill_between( 1010 time_points, 1011 mean_values_c2h6 - std_values, 1012 mean_values_c2h6 + std_values, 1013 color=style["color"], 1014 alpha=std_alpha, 1015 ) 1016 1017 # 軸の設定 1018 for ax, ylabel, subplot_label in [ 1019 (ax1, r"CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)", subplot_label_ch4), 1020 (ax2, r"C$_2$H$_6$ flux (nmol m$^{-2}$ s$^{-1}$)", subplot_label_c2h6), 1021 ]: 1022 self._setup_diurnal_axes( 1023 ax=ax, 1024 time_points=time_points, 1025 ylabel=ylabel, 1026 subplot_label=subplot_label, 1027 show_label=show_label, 1028 show_legend=False, 1029 subplot_fontsize=subplot_fontsize, 1030 ) 1031 1032 if ax1_ylim is not None: 1033 ax1.set_ylim(ax1_ylim) 1034 ax1.yaxis.set_major_locator(MultipleLocator(20)) 1035 ax1.yaxis.set_major_formatter(FuncFormatter(lambda x, p: f"{x:.0f}")) 1036 1037 if ax2_ylim is not None: 1038 ax2.set_ylim(ax2_ylim) 1039 ax2.yaxis.set_major_locator(MultipleLocator(1)) 1040 ax2.yaxis.set_major_formatter(FuncFormatter(lambda x, p: f"{x:.1f}")) 1041 1042 plt.tight_layout() 1043 1044 # 共通の凡例を図の下部に配置 1045 if show_legend: 1046 lines_to_show = ( 1047 ch4_lines if legend_only_ch4 else ch4_lines[: len(selected_conditions)] 1048 ) 1049 fig.legend( 1050 lines_to_show, 1051 [ 1052 style["label"] 1053 for style in list(styles.values())[: len(lines_to_show)] 1054 ], 1055 loc="center", 1056 bbox_to_anchor=(0.5, 0.02), 1057 ncol=len(lines_to_show), 1058 ) 1059 plt.subplots_adjust(bottom=0.25) # 下部に凡例用のスペースを確保 1060 1061 fig.savefig(output_path, dpi=300, bbox_inches="tight") 1062 plt.close(fig) 1063 1064 # 日変化パターンの統計分析を追加 1065 if print_summary: 1066 # 平日と休日のデータを準備 1067 dates = pd.to_datetime(df.index) 1068 is_weekend = dates.dayofweek.isin([5, 6]) 1069 is_holiday = dates.map(lambda x: jpholiday.is_holiday(x.date())) 1070 is_weekday = ~(is_weekend | is_holiday) 1071 1072 weekday_data = df[is_weekday] 1073 holiday_data = df[is_weekend | is_holiday] 1074 1075 def get_diurnal_stats(data, column): 1076 # 時間ごとの平均値を計算 1077 hourly_means = data.groupby(data.index.hour)[column].mean() 1078 1079 # 8-16時の時間帯の統計 1080 daytime_means = hourly_means[ 1081 (hourly_means.index >= 8) & (hourly_means.index <= 16) 1082 ] 1083 1084 if len(daytime_means) == 0: 1085 return None 1086 1087 return { 1088 "mean": daytime_means.mean(), 1089 "max": daytime_means.max(), 1090 "max_hour": daytime_means.idxmax(), 1091 "min": daytime_means.min(), 1092 "min_hour": daytime_means.idxmin(), 1093 "hours_count": len(daytime_means), 1094 } 1095 1096 # CH4とC2H6それぞれの統計を計算 1097 for col, gas_name in [(y_col_ch4, "CH4"), (y_col_c2h6, "C2H6")]: 1098 print(f"\n=== {gas_name} フラックス 8-16時の統計分析 ===") 1099 1100 weekday_stats = get_diurnal_stats(weekday_data, col) 1101 holiday_stats = get_diurnal_stats(holiday_data, col) 1102 1103 if weekday_stats and holiday_stats: 1104 print("\n平日:") 1105 print(f" 平均値: {weekday_stats['mean']:.2f}") 1106 print( 1107 f" 最大値: {weekday_stats['max']:.2f} ({weekday_stats['max_hour']}時)" 1108 ) 1109 print( 1110 f" 最小値: {weekday_stats['min']:.2f} ({weekday_stats['min_hour']}時)" 1111 ) 1112 print(f" 集計時間数: {weekday_stats['hours_count']}") 1113 1114 print("\n休日:") 1115 print(f" 平均値: {holiday_stats['mean']:.2f}") 1116 print( 1117 f" 最大値: {holiday_stats['max']:.2f} ({holiday_stats['max_hour']}時)" 1118 ) 1119 print( 1120 f" 最小値: {holiday_stats['min']:.2f} ({holiday_stats['min_hour']}時)" 1121 ) 1122 print(f" 集計時間数: {holiday_stats['hours_count']}") 1123 1124 # 平日/休日の比率を計算 1125 print("\n平日/休日の比率:") 1126 print( 1127 f" 平均値比: {weekday_stats['mean'] / holiday_stats['mean']:.2f}" 1128 ) 1129 print( 1130 f" 最大値比: {weekday_stats['max'] / holiday_stats['max']:.2f}" 1131 ) 1132 print( 1133 f" 最小値比: {weekday_stats['min'] / holiday_stats['min']:.2f}" 1134 ) 1135 else: 1136 print("十分なデータがありません") 1137 1138 def plot_diurnal_concentrations( 1139 self, 1140 df: pd.DataFrame, 1141 output_dir: str, 1142 col_ch4_conc: str = "CH4_ultra_cal", 1143 col_c2h6_conc: str = "C2H6_ultra_cal", 1144 col_datetime: str = "Date", 1145 output_filename: str = "diurnal_concentrations.png", 1146 show_std: bool = True, 1147 alpha_std: float = 0.2, 1148 add_legend: bool = True, # 凡例表示のオプションを追加 1149 print_summary: bool = True, 1150 subplot_label_ch4: str | None = None, 1151 subplot_label_c2h6: str | None = None, 1152 subplot_fontsize: int = 24, 1153 ch4_ylim: tuple[float, float] | None = None, 1154 c2h6_ylim: tuple[float, float] | None = None, 1155 interval: str = "1H", # "30min" または "1H" を指定 1156 ) -> None: 1157 """CH4とC2H6の濃度の日内変動を描画する 1158 1159 Parameters: 1160 ------ 1161 df : pd.DataFrame 1162 濃度データを含むDataFrame 1163 output_dir : str 1164 出力ディレクトリのパス 1165 col_ch4_conc : str 1166 CH4濃度のカラム名 1167 col_c2h6_conc : str 1168 C2H6濃度のカラム名 1169 col_datetime : str 1170 日時カラム名 1171 output_filename : str 1172 出力ファイル名 1173 show_std : bool 1174 標準偏差を表示するかどうか 1175 alpha_std : float 1176 標準偏差の透明度 1177 add_legend : bool 1178 凡例を追加するかどうか 1179 print_summary : bool 1180 統計情報を表示するかどうか 1181 subplot_label_ch4 : str | None 1182 CH4プロットのラベル 1183 subplot_label_c2h6 : str | None 1184 C2H6プロットのラベル 1185 subplot_fontsize : int 1186 サブプロットのフォントサイズ 1187 ch4_ylim : tuple[float, float] | None 1188 CH4のy軸範囲 1189 c2h6_ylim : tuple[float, float] | None 1190 C2H6のy軸範囲 1191 interval : str 1192 時間間隔。"30min"または"1H"を指定 1193 """ 1194 # 出力ディレクトリの作成 1195 os.makedirs(output_dir, exist_ok=True) 1196 output_path: str = os.path.join(output_dir, output_filename) 1197 1198 # データの準備 1199 df = df.copy() 1200 if interval == "30min": 1201 # 30分間隔の場合、時間と30分を別々に取得 1202 df["hour"] = pd.to_datetime(df[col_datetime]).dt.hour 1203 df["minute"] = pd.to_datetime(df[col_datetime]).dt.minute 1204 df["time_bin"] = df["hour"] + df["minute"].map({0: 0, 30: 0.5}) 1205 else: 1206 # 1時間間隔の場合 1207 df["time_bin"] = pd.to_datetime(df[col_datetime]).dt.hour 1208 1209 # 時間ごとの平均値と標準偏差を計算 1210 hourly_stats = df.groupby("time_bin")[[col_ch4_conc, col_c2h6_conc]].agg( 1211 ["mean", "std"] 1212 ) 1213 1214 # 最後のデータポイントを追加(最初のデータを使用) 1215 last_point = hourly_stats.iloc[0:1].copy() 1216 last_point.index = [ 1217 hourly_stats.index[-1] + (0.5 if interval == "30min" else 1) 1218 ] 1219 hourly_stats = pd.concat([hourly_stats, last_point]) 1220 1221 # 時間軸の作成 1222 if interval == "30min": 1223 time_points = pd.date_range("2024-01-01", periods=49, freq="30min") 1224 x_ticks = [0, 6, 12, 18, 24] # 主要な時間のティック 1225 else: 1226 time_points = pd.date_range("2024-01-01", periods=25, freq="1H") 1227 x_ticks = [0, 6, 12, 18, 24] 1228 1229 # プロットの作成 1230 fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5)) 1231 1232 # CH4濃度プロット 1233 mean_ch4 = hourly_stats[col_ch4_conc]["mean"] 1234 if show_std: 1235 std_ch4 = hourly_stats[col_ch4_conc]["std"] 1236 ax1.fill_between( 1237 time_points, 1238 mean_ch4 - std_ch4, 1239 mean_ch4 + std_ch4, 1240 color="red", 1241 alpha=alpha_std, 1242 ) 1243 ch4_line = ax1.plot(time_points, mean_ch4, "red", label="CH$_4$")[0] 1244 1245 ax1.set_ylabel("CH$_4$ (ppm)") 1246 if ch4_ylim is not None: 1247 ax1.set_ylim(ch4_ylim) 1248 if subplot_label_ch4: 1249 ax1.text( 1250 0.02, 1251 0.98, 1252 subplot_label_ch4, 1253 transform=ax1.transAxes, 1254 va="top", 1255 fontsize=subplot_fontsize, 1256 ) 1257 1258 # C2H6濃度プロット 1259 mean_c2h6 = hourly_stats[col_c2h6_conc]["mean"] 1260 if show_std: 1261 std_c2h6 = hourly_stats[col_c2h6_conc]["std"] 1262 ax2.fill_between( 1263 time_points, 1264 mean_c2h6 - std_c2h6, 1265 mean_c2h6 + std_c2h6, 1266 color="orange", 1267 alpha=alpha_std, 1268 ) 1269 c2h6_line = ax2.plot(time_points, mean_c2h6, "orange", label="C$_2$H$_6$")[0] 1270 1271 ax2.set_ylabel("C$_2$H$_6$ (ppb)") 1272 if c2h6_ylim is not None: 1273 ax2.set_ylim(c2h6_ylim) 1274 if subplot_label_c2h6: 1275 ax2.text( 1276 0.02, 1277 0.98, 1278 subplot_label_c2h6, 1279 transform=ax2.transAxes, 1280 va="top", 1281 fontsize=subplot_fontsize, 1282 ) 1283 1284 # 両プロットの共通設定 1285 for ax in [ax1, ax2]: 1286 ax.set_xlabel("Time (hour)") 1287 ax.xaxis.set_major_formatter(mdates.DateFormatter("%-H")) 1288 ax.xaxis.set_major_locator(mdates.HourLocator(byhour=x_ticks)) 1289 ax.set_xlim(time_points[0], time_points[-1]) 1290 # 1時間ごとの縦線を表示 1291 ax.grid(True, which="major", alpha=0.3) 1292 # 補助目盛りは表示するが、グリッド線は表示しない 1293 # if interval == "30min": 1294 # ax.xaxis.set_minor_locator(mdates.MinuteLocator(byminute=[30])) 1295 # ax.tick_params(which='minor', length=4) 1296 1297 # 共通の凡例を図の下部に配置 1298 if add_legend: 1299 fig.legend( 1300 [ch4_line, c2h6_line], 1301 ["CH$_4$", "C$_2$H$_6$"], 1302 loc="center", 1303 bbox_to_anchor=(0.5, 0.02), 1304 ncol=2, 1305 ) 1306 plt.subplots_adjust(bottom=0.2) 1307 1308 plt.tight_layout() 1309 plt.savefig(output_path, dpi=300, bbox_inches="tight") 1310 plt.close(fig) 1311 1312 if print_summary: 1313 # 統計情報の表示 1314 for name, col in [("CH4", col_ch4_conc), ("C2H6", col_c2h6_conc)]: 1315 stats = hourly_stats[col] 1316 mean_vals = stats["mean"] 1317 1318 print(f"\n{name}濃度の日内変動統計:") 1319 print(f"最小値: {mean_vals.min():.3f} (Hour: {mean_vals.idxmin()})") 1320 print(f"最大値: {mean_vals.max():.3f} (Hour: {mean_vals.idxmax()})") 1321 print(f"平均値: {mean_vals.mean():.3f}") 1322 print(f"日内変動幅: {mean_vals.max() - mean_vals.min():.3f}") 1323 print(f"最大/最小比: {mean_vals.max() / mean_vals.min():.3f}") 1324 1325 def plot_flux_diurnal_patterns_with_std( 1326 self, 1327 df: pd.DataFrame, 1328 output_dir: str, 1329 col_ch4_flux: str = "Fch4", 1330 col_c2h6_flux: str = "Fc2h6", 1331 ch4_label: str = r"$\mathregular{CH_{4}}$フラックス", 1332 c2h6_label: str = r"$\mathregular{C_{2}H_{6}}$フラックス", 1333 col_datetime: str = "Date", 1334 output_filename: str = "diurnal_patterns.png", 1335 window_size: int = 6, # 移動平均の窓サイズ 1336 show_std: bool = True, # 標準偏差の表示有無 1337 alpha_std: float = 0.1, # 標準偏差の透明度 1338 ) -> None: 1339 """CH4とC2H6フラックスの日変化パターンをプロットする 1340 1341 Parameters: 1342 ------ 1343 df : pd.DataFrame 1344 データフレーム 1345 output_dir : str 1346 出力ディレクトリのパス 1347 col_ch4_flux : str 1348 CH4フラックスのカラム名 1349 col_c2h6_flux : str 1350 C2H6フラックスのカラム名 1351 ch4_label : str 1352 CH4フラックスのラベル 1353 c2h6_label : str 1354 C2H6フラックスのラベル 1355 col_datetime : str 1356 日時カラムの名前 1357 output_filename : str 1358 出力ファイル名 1359 window_size : int 1360 移動平均の窓サイズ(デフォルト6) 1361 show_std : bool 1362 標準偏差を表示するかどうか 1363 alpha_std : float 1364 標準偏差の透明度(0-1) 1365 """ 1366 # 出力ディレクトリの作成 1367 os.makedirs(output_dir, exist_ok=True) 1368 output_path: str = os.path.join(output_dir, output_filename) 1369 1370 # # プロットのスタイル設定 1371 # plt.rcParams.update({ 1372 # 'font.size': 20, 1373 # 'axes.labelsize': 20, 1374 # 'axes.titlesize': 20, 1375 # 'xtick.labelsize': 20, 1376 # 'ytick.labelsize': 20, 1377 # 'legend.fontsize': 20, 1378 # }) 1379 1380 # 日時インデックスの処理 1381 df = df.copy() 1382 if not isinstance(df.index, pd.DatetimeIndex): 1383 df[col_datetime] = pd.to_datetime(df[col_datetime]) 1384 df.set_index(col_datetime, inplace=True) 1385 1386 # 時刻データの抽出とグループ化 1387 df["hour"] = df.index.hour 1388 hourly_means = df.groupby("hour")[[col_ch4_flux, col_c2h6_flux]].agg( 1389 ["mean", "std"] 1390 ) 1391 1392 # 24時間目のデータ点を追加(0時のデータを使用) 1393 last_hour = hourly_means.iloc[0:1].copy() 1394 last_hour.index = [24] 1395 hourly_means = pd.concat([hourly_means, last_hour]) 1396 1397 # 24時間分のデータポイントを作成 1398 time_points = pd.date_range("2024-01-01", periods=25, freq="h") 1399 1400 # プロットの作成 1401 fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5)) 1402 1403 # 移動平均の計算と描画 1404 ch4_mean = ( 1405 hourly_means[(col_ch4_flux, "mean")] 1406 .rolling(window=window_size, center=True, min_periods=1) 1407 .mean() 1408 ) 1409 c2h6_mean = ( 1410 hourly_means[(col_c2h6_flux, "mean")] 1411 .rolling(window=window_size, center=True, min_periods=1) 1412 .mean() 1413 ) 1414 1415 if show_std: 1416 ch4_std = ( 1417 hourly_means[(col_ch4_flux, "std")] 1418 .rolling(window=window_size, center=True, min_periods=1) 1419 .mean() 1420 ) 1421 c2h6_std = ( 1422 hourly_means[(col_c2h6_flux, "std")] 1423 .rolling(window=window_size, center=True, min_periods=1) 1424 .mean() 1425 ) 1426 1427 ax1.fill_between( 1428 time_points, 1429 ch4_mean - ch4_std, 1430 ch4_mean + ch4_std, 1431 color="blue", 1432 alpha=alpha_std, 1433 ) 1434 ax2.fill_between( 1435 time_points, 1436 c2h6_mean - c2h6_std, 1437 c2h6_mean + c2h6_std, 1438 color="red", 1439 alpha=alpha_std, 1440 ) 1441 1442 # メインのラインプロット 1443 ax1.plot(time_points, ch4_mean, "blue", label=ch4_label) 1444 ax2.plot(time_points, c2h6_mean, "red", label=c2h6_label) 1445 1446 # 軸の設定 1447 for ax, ylabel in [ 1448 (ax1, r"CH$_4$ (nmol m$^{-2}$ s$^{-1}$)"), 1449 (ax2, r"C$_2$H$_6$ (nmol m$^{-2}$ s$^{-1}$)"), 1450 ]: 1451 ax.set_xlabel("Time") 1452 ax.set_ylabel(ylabel) 1453 ax.xaxis.set_major_formatter(mdates.DateFormatter("%-H")) 1454 ax.xaxis.set_major_locator(mdates.HourLocator(byhour=[0, 6, 12, 18, 24])) 1455 ax.set_xlim(time_points[0], time_points[-1]) 1456 ax.grid(True, alpha=0.3) 1457 ax.legend() 1458 1459 # グラフの保存 1460 plt.tight_layout() 1461 plt.savefig(output_path, dpi=300, bbox_inches="tight") 1462 plt.close() 1463 1464 # 統計情報の表示(オプション) 1465 for col, name in [(col_ch4_flux, "CH4"), (col_c2h6_flux, "C2H6")]: 1466 mean_val = hourly_means[(col, "mean")].mean() 1467 min_val = hourly_means[(col, "mean")].min() 1468 max_val = hourly_means[(col, "mean")].max() 1469 min_time = hourly_means[(col, "mean")].idxmin() 1470 max_time = hourly_means[(col, "mean")].idxmax() 1471 1472 self.logger.info(f"{name} Statistics:") 1473 self.logger.info(f"Mean: {mean_val:.2f}") 1474 self.logger.info(f"Min: {min_val:.2f} (Hour: {min_time})") 1475 self.logger.info(f"Max: {max_val:.2f} (Hour: {max_time})") 1476 self.logger.info(f"Max/Min ratio: {max_val / min_val:.2f}\n") 1477 1478 def plot_scatter( 1479 self, 1480 df: pd.DataFrame, 1481 x_col: str, 1482 y_col: str, 1483 output_dir: str, 1484 output_filename: str = "scatter.png", 1485 xlabel: str | None = None, 1486 ylabel: str | None = None, 1487 show_label: bool = True, 1488 x_axis_range: tuple | None = None, 1489 y_axis_range: tuple | None = None, 1490 fixed_slope: float = 0.076, 1491 show_fixed_slope: bool = False, 1492 x_scientific: bool = False, # 追加:x軸を指数表記にするかどうか 1493 y_scientific: bool = False, # 追加:y軸を指数表記にするかどうか 1494 ) -> None: 1495 """散布図を作成し、TLS回帰直線を描画します。 1496 1497 Parameters: 1498 ------ 1499 df : pd.DataFrame 1500 プロットに使用するデータフレーム 1501 x_col : str 1502 x軸に使用する列名 1503 y_col : str 1504 y軸に使用する列名 1505 xlabel : str 1506 x軸のラベル 1507 ylabel : str 1508 y軸のラベル 1509 output_dir : str 1510 出力先ディレクトリ 1511 output_filename : str, optional 1512 出力ファイル名。デフォルトは"scatter.png" 1513 show_label : bool, optional 1514 軸ラベルを表示するかどうか。デフォルトはTrue 1515 x_axis_range : tuple, optional 1516 x軸の範囲。デフォルトはNone。 1517 y_axis_range : tuple, optional 1518 y軸の範囲。デフォルトはNone。 1519 fixed_slope : float, optional 1520 固定傾きを指定するための値。デフォルトは0.076 1521 show_fixed_slope : bool, optional 1522 固定傾きの線を表示するかどうか。デフォルトはFalse 1523 """ 1524 os.makedirs(output_dir, exist_ok=True) 1525 output_path: str = os.path.join(output_dir, output_filename) 1526 1527 # 有効なデータの抽出 1528 df = MonthlyFiguresGenerator.get_valid_data(df, x_col, y_col) 1529 1530 # データの準備 1531 x = df[x_col].values 1532 y = df[y_col].values 1533 1534 # データの中心化 1535 x_mean = np.mean(x) 1536 y_mean = np.mean(y) 1537 x_c = x - x_mean 1538 y_c = y - y_mean 1539 1540 # TLS回帰の計算 1541 data_matrix = np.vstack((x_c, y_c)) 1542 cov_matrix = np.cov(data_matrix) 1543 _, eigenvecs = linalg.eigh(cov_matrix) 1544 largest_eigenvec = eigenvecs[:, -1] 1545 1546 slope = largest_eigenvec[1] / largest_eigenvec[0] 1547 intercept = y_mean - slope * x_mean 1548 1549 # R²とRMSEの計算 1550 y_pred = slope * x + intercept 1551 r_squared = 1 - np.sum((y - y_pred) ** 2) / np.sum((y - np.mean(y)) ** 2) 1552 rmse = np.sqrt(np.mean((y - y_pred) ** 2)) 1553 1554 # プロットの作成 1555 fig, ax = plt.subplots(figsize=(6, 6)) 1556 1557 # データ点のプロット 1558 ax.scatter(x, y, color="black") 1559 1560 # データの範囲を取得 1561 if x_axis_range is None: 1562 x_axis_range = (df[x_col].min(), df[x_col].max()) 1563 if y_axis_range is None: 1564 y_axis_range = (df[y_col].min(), df[y_col].max()) 1565 1566 # 回帰直線のプロット 1567 x_range = np.linspace(x_axis_range[0], x_axis_range[1], 150) 1568 y_range = slope * x_range + intercept 1569 ax.plot(x_range, y_range, "r", label="TLS regression") 1570 1571 # 傾き固定の線を追加(フラグがTrueの場合) 1572 if show_fixed_slope: 1573 fixed_intercept = ( 1574 y_mean - fixed_slope * x_mean 1575 ) # 中心点を通るように切片を計算 1576 y_fixed = fixed_slope * x_range + fixed_intercept 1577 ax.plot(x_range, y_fixed, "b--", label=f"Slope = {fixed_slope}", alpha=0.7) 1578 1579 # 軸の設定 1580 ax.set_xlim(x_axis_range) 1581 ax.set_ylim(y_axis_range) 1582 1583 # 指数表記の設定 1584 if x_scientific: 1585 ax.ticklabel_format(style="sci", axis="x", scilimits=(0, 0)) 1586 ax.xaxis.get_offset_text().set_position((1.1, 0)) # 指数の位置調整 1587 if y_scientific: 1588 ax.ticklabel_format(style="sci", axis="y", scilimits=(0, 0)) 1589 ax.yaxis.get_offset_text().set_position((0, 1.1)) # 指数の位置調整 1590 1591 if show_label: 1592 if xlabel is not None: 1593 ax.set_xlabel(xlabel) 1594 if ylabel is not None: 1595 ax.set_ylabel(ylabel) 1596 1597 # 1:1の関係を示す点線(軸の範囲が同じ場合のみ表示) 1598 if ( 1599 x_axis_range is not None 1600 and y_axis_range is not None 1601 and x_axis_range == y_axis_range 1602 ): 1603 ax.plot( 1604 [x_axis_range[0], x_axis_range[1]], 1605 [x_axis_range[0], x_axis_range[1]], 1606 "k--", 1607 alpha=0.5, 1608 ) 1609 1610 # 回帰情報の表示 1611 equation = ( 1612 f"y = {slope:.2f}x {'+' if intercept >= 0 else '-'} {abs(intercept):.2f}" 1613 ) 1614 position_x = 0.05 1615 fig_ha: str = "left" 1616 ax.text( 1617 position_x, 1618 0.95, 1619 equation, 1620 transform=ax.transAxes, 1621 va="top", 1622 ha=fig_ha, 1623 color="red", 1624 ) 1625 ax.text( 1626 position_x, 1627 0.88, 1628 f"R² = {r_squared:.2f}", 1629 transform=ax.transAxes, 1630 va="top", 1631 ha=fig_ha, 1632 color="red", 1633 ) 1634 ax.text( 1635 position_x, 1636 0.81, # RMSEのための新しい位置 1637 f"RMSE = {rmse:.2f}", 1638 transform=ax.transAxes, 1639 va="top", 1640 ha=fig_ha, 1641 color="red", 1642 ) 1643 # 目盛り線の設定 1644 ax.grid(True, alpha=0.3) 1645 1646 fig.savefig(output_path, dpi=300, bbox_inches="tight") 1647 plt.close(fig) 1648 1649 def plot_source_contributions_diurnal( 1650 self, 1651 df: pd.DataFrame, 1652 output_dir: str, 1653 col_ch4_flux: str, 1654 col_c2h6_flux: str, 1655 label_gas: str = "gas", 1656 label_bio: str = "bio", 1657 col_datetime: str = "Date", 1658 output_filename: str = "source_contributions.png", 1659 window_size: int = 6, # 移動平均の窓サイズ 1660 print_summary: bool = True, # 統計情報を表示するかどうか, 1661 show_legend: bool = False, 1662 smooth: bool = False, 1663 y_max: float = 100, # y軸の上限値を追加 1664 subplot_label: str | None = None, 1665 subplot_fontsize: int = 20, 1666 ) -> None: 1667 """CH4フラックスの都市ガス起源と生物起源の日変化を積み上げグラフとして表示 1668 1669 Parameters: 1670 ------ 1671 df : pd.DataFrame 1672 データフレーム 1673 output_dir : str 1674 出力ディレクトリのパス 1675 col_ch4_flux : str 1676 CH4フラックスのカラム名 1677 col_c2h6_flux : str 1678 C2H6フラックスのカラム名 1679 label_gas : str 1680 都市ガス起源のラベル 1681 label_bio : str 1682 生物起源のラベル 1683 col_datetime : str 1684 日時カラムの名前 1685 output_filename : str 1686 出力ファイル名 1687 window_size : int 1688 移動平均の窓サイズ 1689 print_summary : bool 1690 統計情報を表示するかどうか 1691 smooth : bool 1692 移動平均を適用するかどうか 1693 y_max : float 1694 y軸の上限値(デフォルト: 100) 1695 """ 1696 # 出力ディレクトリの作成 1697 os.makedirs(output_dir, exist_ok=True) 1698 output_path: str = os.path.join(output_dir, output_filename) 1699 1700 # 起源の計算 1701 df_with_sources = self._calculate_source_contributions( 1702 df=df, 1703 col_ch4_flux=col_ch4_flux, 1704 col_c2h6_flux=col_c2h6_flux, 1705 col_datetime=col_datetime, 1706 ) 1707 1708 # 時刻データの抽出とグループ化 1709 df_with_sources["hour"] = df_with_sources.index.hour 1710 hourly_means = df_with_sources.groupby("hour")[["ch4_gas", "ch4_bio"]].mean() 1711 1712 # 24時間目のデータ点を追加(0時のデータを使用) 1713 last_hour = hourly_means.iloc[0:1].copy() 1714 last_hour.index = [24] 1715 hourly_means = pd.concat([hourly_means, last_hour]) 1716 1717 # 移動平均の適用 1718 hourly_means_smoothed = hourly_means 1719 if smooth: 1720 hourly_means_smoothed = hourly_means.rolling( 1721 window=window_size, center=True, min_periods=1 1722 ).mean() 1723 1724 # 24時間分のデータポイントを作成 1725 time_points = pd.date_range("2024-01-01", periods=25, freq="h") 1726 1727 # プロットの作成 1728 plt.figure(figsize=(10, 6)) 1729 ax = plt.gca() 1730 1731 # サブプロットラベルの追加(subplot_labelが指定されている場合) 1732 if subplot_label: 1733 ax.text( 1734 0.02, # x位置 1735 0.98, # y位置 1736 subplot_label, 1737 transform=ax.transAxes, 1738 va="top", 1739 fontsize=subplot_fontsize, 1740 ) 1741 1742 # 積み上げプロット 1743 ax.fill_between( 1744 time_points, 1745 0, 1746 hourly_means_smoothed["ch4_bio"], 1747 color="blue", 1748 alpha=0.6, 1749 label=label_bio, 1750 ) 1751 ax.fill_between( 1752 time_points, 1753 hourly_means_smoothed["ch4_bio"], 1754 hourly_means_smoothed["ch4_bio"] + hourly_means_smoothed["ch4_gas"], 1755 color="red", 1756 alpha=0.6, 1757 label=label_gas, 1758 ) 1759 1760 # 合計値のライン 1761 total_flux = hourly_means_smoothed["ch4_bio"] + hourly_means_smoothed["ch4_gas"] 1762 ax.plot(time_points, total_flux, "-", color="black", alpha=0.5) 1763 1764 # 軸の設定 1765 ax.set_xlabel("Time (hour)") 1766 ax.set_ylabel(r"CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)") 1767 ax.xaxis.set_major_formatter(mdates.DateFormatter("%-H")) 1768 ax.xaxis.set_major_locator(mdates.HourLocator(byhour=[0, 6, 12, 18, 24])) 1769 ax.set_xlim(time_points[0], time_points[-1]) 1770 ax.set_ylim(0, y_max) # y軸の範囲を設定 1771 ax.grid(True, alpha=0.3) 1772 1773 # 凡例を図の下部に配置 1774 if show_legend: 1775 handles, labels = ax.get_legend_handles_labels() 1776 fig = plt.gcf() # 現在の図を取得 1777 fig.legend( 1778 handles, 1779 labels, 1780 loc="center", 1781 bbox_to_anchor=(0.5, 0.01), 1782 ncol=len(handles), 1783 ) 1784 plt.subplots_adjust(bottom=0.2) # 下部に凡例用のスペースを確保 1785 1786 # グラフの保存 1787 plt.tight_layout() 1788 plt.savefig(output_path, dpi=300, bbox_inches="tight") 1789 plt.close() 1790 1791 # 統計情報の表示 1792 if print_summary: 1793 stats = { 1794 "都市ガス起源": hourly_means["ch4_gas"], 1795 "生物起源": hourly_means["ch4_bio"], 1796 "合計": hourly_means["ch4_gas"] + hourly_means["ch4_bio"], 1797 } 1798 1799 for source, data in stats.items(): 1800 mean_val = data.mean() 1801 min_val = data.min() 1802 max_val = data.max() 1803 min_time = data.idxmin() 1804 max_time = data.idxmax() 1805 1806 self.logger.info(f"{source}の統計:") 1807 print(f" 平均値: {mean_val:.2f}") 1808 print(f" 最小値: {min_val:.2f} (Hour: {min_time})") 1809 print(f" 最大値: {max_val:.2f} (Hour: {max_time})") 1810 if min_val != 0: 1811 print(f" 最大/最小比: {max_val / min_val:.2f}") 1812 1813 def plot_source_contributions_diurnal_by_date( 1814 self, 1815 df: pd.DataFrame, 1816 output_dir: str, 1817 col_ch4_flux: str, 1818 col_c2h6_flux: str, 1819 label_gas: str = "gas", 1820 label_bio: str = "bio", 1821 col_datetime: str = "Date", 1822 output_filename: str = "source_contributions_by_date.png", 1823 show_label: bool = True, 1824 show_legend: bool = False, 1825 print_summary: bool = False, # 統計情報を表示するかどうか, 1826 subplot_fontsize: int = 20, 1827 subplot_label_weekday: str | None = None, 1828 subplot_label_weekend: str | None = None, 1829 y_max: float | None = None, # y軸の上限値 1830 ) -> None: 1831 """CH4フラックスの都市ガス起源と生物起源の日変化を平日・休日別に表示 1832 1833 Parameters: 1834 ------ 1835 df : pd.DataFrame 1836 データフレーム 1837 output_dir : str 1838 出力ディレクトリのパス 1839 col_ch4_flux : str 1840 CH4フラックスのカラム名 1841 col_c2h6_flux : str 1842 C2H6フラックスのカラム名 1843 label_gas : str 1844 都市ガス起源のラベル 1845 label_bio : str 1846 生物起源のラベル 1847 col_datetime : str 1848 日時カラムの名前 1849 output_filename : str 1850 出力ファイル名 1851 show_label : bool 1852 ラベルを表示するか 1853 show_legend : bool 1854 凡例を表示するか 1855 subplot_fontsize : int 1856 サブプロットのフォントサイズ 1857 subplot_label_weekday : str | None 1858 平日グラフのラベル 1859 subplot_label_weekend : str | None 1860 休日グラフのラベル 1861 y_max : float | None 1862 y軸の上限値 1863 """ 1864 # 出力ディレクトリの作成 1865 os.makedirs(output_dir, exist_ok=True) 1866 output_path: str = os.path.join(output_dir, output_filename) 1867 1868 # 起源の計算 1869 df_with_sources = self._calculate_source_contributions( 1870 df=df, 1871 col_ch4_flux=col_ch4_flux, 1872 col_c2h6_flux=col_c2h6_flux, 1873 col_datetime=col_datetime, 1874 ) 1875 1876 # 日付タイプの分類 1877 dates = pd.to_datetime(df_with_sources.index) 1878 is_weekend = dates.dayofweek.isin([5, 6]) 1879 is_holiday = dates.map(lambda x: jpholiday.is_holiday(x.date())) 1880 is_weekday = ~(is_weekend | is_holiday) 1881 1882 # データの分類 1883 data_weekday = df_with_sources[is_weekday] 1884 data_holiday = df_with_sources[is_weekend | is_holiday] 1885 1886 # プロットの作成 1887 fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5)) 1888 1889 # 平日と休日それぞれのプロット 1890 for ax, data, label in [ 1891 (ax1, data_weekday, "Weekdays"), 1892 (ax2, data_holiday, "Weekends & Holidays"), 1893 ]: 1894 # 時間ごとの平均値を計算 1895 hourly_means = data.groupby(data.index.hour)[["ch4_gas", "ch4_bio"]].mean() 1896 1897 # 24時間目のデータ点を追加 1898 last_hour = hourly_means.iloc[0:1].copy() 1899 last_hour.index = [24] 1900 hourly_means = pd.concat([hourly_means, last_hour]) 1901 1902 # 24時間分のデータポイントを作成 1903 time_points = pd.date_range("2024-01-01", periods=25, freq="h") 1904 1905 # 積み上げプロット 1906 ax.fill_between( 1907 time_points, 1908 0, 1909 hourly_means["ch4_bio"], 1910 color="blue", 1911 alpha=0.6, 1912 label=label_bio, 1913 ) 1914 ax.fill_between( 1915 time_points, 1916 hourly_means["ch4_bio"], 1917 hourly_means["ch4_bio"] + hourly_means["ch4_gas"], 1918 color="red", 1919 alpha=0.6, 1920 label=label_gas, 1921 ) 1922 1923 # 合計値のライン 1924 total_flux = hourly_means["ch4_bio"] + hourly_means["ch4_gas"] 1925 ax.plot(time_points, total_flux, "-", color="black", alpha=0.5) 1926 1927 # 軸の設定 1928 if show_label: 1929 ax.set_xlabel("Time (hour)") 1930 if ax == ax1: # 左側のプロットのラベル 1931 ax.set_ylabel("Weekdays CH$_4$ flux\n" r"(nmol m$^{-2}$ s$^{-1}$)") 1932 else: # 右側のプロットのラベル 1933 ax.set_ylabel("Weekends CH$_4$ flux\n" r"(nmol m$^{-2}$ s$^{-1}$)") 1934 1935 ax.xaxis.set_major_formatter(mdates.DateFormatter("%-H")) 1936 ax.xaxis.set_major_locator(mdates.HourLocator(byhour=[0, 6, 12, 18, 24])) 1937 ax.set_xlim(time_points[0], time_points[-1]) 1938 if y_max is not None: 1939 ax.set_ylim(0, y_max) 1940 ax.grid(True, alpha=0.3) 1941 1942 # サブプロットラベルの追加 1943 if subplot_label_weekday: 1944 ax1.text( 1945 0.02, 1946 0.98, 1947 subplot_label_weekday, 1948 transform=ax1.transAxes, 1949 va="top", 1950 fontsize=subplot_fontsize, 1951 ) 1952 if subplot_label_weekend: 1953 ax2.text( 1954 0.02, 1955 0.98, 1956 subplot_label_weekend, 1957 transform=ax2.transAxes, 1958 va="top", 1959 fontsize=subplot_fontsize, 1960 ) 1961 1962 # 凡例を図の下部に配置 1963 if show_legend: 1964 # 最初のプロットから凡例のハンドルとラベルを取得 1965 handles, labels = ax1.get_legend_handles_labels() 1966 # 図の下部に凡例を配置 1967 fig.legend( 1968 handles, 1969 labels, 1970 loc="center", 1971 bbox_to_anchor=(0.5, 0.01), # x=0.5で中央、y=0.01で下部に配置 1972 ncol=len(handles), # ハンドルの数だけ列を作成(一行に表示) 1973 ) 1974 # 凡例用のスペースを確保 1975 plt.subplots_adjust(bottom=0.2) # 下部に30%のスペースを確保 1976 1977 plt.tight_layout() 1978 plt.savefig(output_path, dpi=300, bbox_inches="tight") 1979 plt.close() 1980 1981 # 統計情報の表示 1982 if print_summary: 1983 for data, label in [ 1984 (data_weekday, "Weekdays"), 1985 (data_holiday, "Weekends & Holidays"), 1986 ]: 1987 hourly_means = data.groupby(data.index.hour)[ 1988 ["ch4_gas", "ch4_bio"] 1989 ].mean() 1990 total_flux = hourly_means["ch4_gas"] + hourly_means["ch4_bio"] 1991 1992 print(f"\n{label}の統計:") 1993 print(f" 平均値: {total_flux.mean():.2f}") 1994 print(f" 最小値: {total_flux.min():.2f} (Hour: {total_flux.idxmin()})") 1995 print(f" 最大値: {total_flux.max():.2f} (Hour: {total_flux.idxmax()})") 1996 if total_flux.min() != 0: 1997 print(f" 最大/最小比: {total_flux.max() / total_flux.min():.2f}") 1998 1999 def plot_spectra( 2000 self, 2001 input_dir: str, 2002 output_dir: str, 2003 fs: float, 2004 lag_second: float, 2005 col_ch4: str = "Ultra_CH4_ppm_C", 2006 col_c2h6: str = "Ultra_C2H6_ppb", 2007 label_ch4: str | None = None, 2008 label_c2h6: str | None = None, 2009 are_inputs_resampled: bool = True, 2010 file_pattern: str = "*.csv", 2011 output_basename: str = "spectrum", 2012 plot_power: bool = True, 2013 plot_co: bool = True, 2014 markersize: float = 14, 2015 ) -> None: 2016 """ 2017 月間の平均パワースペクトル密度を計算してプロットする。 2018 2019 データファイルを指定されたディレクトリから読み込み、パワースペクトル密度を計算し、 2020 結果を指定された出力ディレクトリにプロットして保存します。 2021 2022 Parameters: 2023 ------ 2024 input_dir : str 2025 データファイルが格納されているディレクトリ。 2026 output_dir : str 2027 出力先ディレクトリ。 2028 fs : float 2029 サンプリング周波数。 2030 lag_second : float 2031 ラグ時間(秒)。 2032 col_ch4 : str, optional 2033 CH4の濃度データが入ったカラムのキー。デフォルトは"Ultra_CH4_ppm_C"。 2034 col_c2h6 : str, optional 2035 C2H6の濃度データが入ったカラムのキー。デフォルトは"Ultra_C2H6_ppb"。 2036 are_inputs_resampled : bool, optional 2037 入力データが再サンプリングされているかどうか。デフォルトはTrue。 2038 file_pattern : str, optional 2039 処理対象のファイルパターン。デフォルトは"*.csv"。 2040 output_basename : str, optional 2041 出力ファイル名。デフォルトは"spectrum"。 2042 """ 2043 # データの読み込みと結合 2044 edp = EddyDataPreprocessor() 2045 2046 # 各変数のパワースペクトルを格納する辞書 2047 power_spectra = {col_ch4: [], col_c2h6: []} 2048 co_spectra = {col_ch4: [], col_c2h6: []} 2049 freqs = None 2050 2051 # プログレスバーを表示しながらファイルを処理 2052 file_list = glob.glob(os.path.join(input_dir, file_pattern)) 2053 for filepath in tqdm(file_list, desc="Processing files"): 2054 df, _ = edp.get_resampled_df( 2055 filepath=filepath, is_already_resampled=are_inputs_resampled 2056 ) 2057 2058 # 風速成分の計算を追加 2059 df = edp.add_uvw_columns(df) 2060 2061 # NaNや無限大を含む行を削除 2062 df = df.replace([np.inf, -np.inf], np.nan).dropna( 2063 subset=[col_ch4, col_c2h6, "wind_w"] 2064 ) 2065 2066 # データが十分な行数を持っているか確認 2067 if len(df) < 100: 2068 continue 2069 2070 # 各ファイルごとにスペクトル計算 2071 calculator = SpectrumCalculator( 2072 df=df, 2073 fs=fs, 2074 cols_apply_lag_time=[col_ch4, col_c2h6], 2075 lag_second=lag_second, 2076 ) 2077 2078 # 各変数のパワースペクトルを計算して保存 2079 for col in power_spectra.keys(): 2080 f, ps = calculator.calculate_power_spectrum( 2081 col=col, 2082 dimensionless=True, 2083 frequency_weighted=True, 2084 interpolate_points=True, 2085 scaling="density", 2086 ) 2087 # 最初のファイル処理時にfreqsを初期化 2088 if freqs is None: 2089 freqs = f 2090 power_spectra[col].append(ps) 2091 # 以降は周波数配列の長さが一致する場合のみ追加 2092 elif len(f) == len(freqs): 2093 power_spectra[col].append(ps) 2094 2095 # コスペクトル 2096 _, cs, _ = calculator.calculate_co_spectrum( 2097 col1="wind_w", 2098 col2=col, 2099 dimensionless=True, 2100 frequency_weighted=True, 2101 interpolate_points=True, 2102 # scaling="density", 2103 scaling="spectrum", 2104 ) 2105 if freqs is not None and len(cs) == len(freqs): 2106 co_spectra[col].append(cs) 2107 2108 # 各変数のスペクトルを平均化 2109 averaged_power_spectra = { 2110 col: np.mean(spectra, axis=0) for col, spectra in power_spectra.items() 2111 } 2112 averaged_co_spectra = { 2113 col: np.mean(spectra, axis=0) for col, spectra in co_spectra.items() 2114 } 2115 2116 # # プロット設定 2117 # plt.rcParams.update( 2118 # { 2119 # "font.size": 20, 2120 # "axes.labelsize": 20, 2121 # "axes.titlesize": 20, 2122 # "xtick.labelsize": 20, 2123 # "ytick.labelsize": 20, 2124 # "legend.fontsize": 20, 2125 # } 2126 # ) 2127 2128 # プロット設定を修正 2129 plot_configs = [ 2130 { 2131 "col": col_ch4, 2132 "psd_ylabel": r"$fS_{\mathrm{CH_4}} / s_{\mathrm{CH_4}}^2$", 2133 "co_ylabel": r"$fCo_{w\mathrm{CH_4}} / (\sigma_w \sigma_{\mathrm{CH_4}})$", 2134 "color": "red", 2135 "label": label_ch4, 2136 }, 2137 { 2138 "col": col_c2h6, 2139 "psd_ylabel": r"$fS_{\mathrm{C_2H_6}} / s_{\mathrm{C_2H_6}}^2$", 2140 "co_ylabel": r"$fCo_{w\mathrm{C_2H_6}} / (\sigma_w \sigma_{\mathrm{C_2H_6}})$", 2141 "color": "orange", 2142 "label": label_c2h6, 2143 }, 2144 ] 2145 2146 # # パワースペクトルの図を作成 2147 # if plot_power: 2148 # _, axes_psd = plt.subplots(1, 2, figsize=(12, 5), sharex=True) 2149 # for ax, config in zip(axes_psd, plot_configs): 2150 # ax.scatter( 2151 # freqs, 2152 # averaged_power_spectra[config["col"]], 2153 # c=config["color"], 2154 # s=100, 2155 # ) 2156 # ax.set_xscale("log") 2157 # ax.set_yscale("log") 2158 # ax.set_xlim(0.001, 10) 2159 # ax.plot([0.01, 10], [1, 0.01], "-", color="black", alpha=0.5) 2160 # ax.text(0.1, 0.06, "-2/3", fontsize=18) 2161 # ax.set_ylabel(config["psd_ylabel"]) 2162 # if config["label"] is not None: 2163 # ax.text( 2164 # 0.02, 0.98, config["label"], transform=ax.transAxes, va="top" 2165 # ) 2166 # ax.grid(True, alpha=0.3) 2167 # ax.set_xlabel("f (Hz)") 2168 2169 # パワースペクトルの図を作成 2170 if plot_power: 2171 _, axes_psd = plt.subplots(1, 2, figsize=(12, 5), sharex=True) 2172 for ax, config in zip(axes_psd, plot_configs): 2173 ax.plot( 2174 freqs, 2175 averaged_power_spectra[config["col"]], 2176 "o", # マーカーを丸に設定 2177 color=config["color"], 2178 markersize=markersize, 2179 ) 2180 ax.set_xscale("log") 2181 ax.set_yscale("log") 2182 ax.set_xlim(0.001, 10) 2183 ax.plot([0.01, 10], [1, 0.01], "-", color="black", alpha=0.5) 2184 ax.text(0.1, 0.06, "-2/3", fontsize=18) 2185 ax.set_ylabel(config["psd_ylabel"]) 2186 if config["label"] is not None: 2187 ax.text( 2188 0.02, 0.98, config["label"], transform=ax.transAxes, va="top" 2189 ) 2190 ax.grid(True, alpha=0.3) 2191 ax.set_xlabel("f (Hz)") 2192 2193 plt.tight_layout() 2194 os.makedirs(output_dir, exist_ok=True) 2195 output_path_psd: str = os.path.join( 2196 output_dir, f"power_{output_basename}.png" 2197 ) 2198 plt.savefig( 2199 output_path_psd, 2200 dpi=300, 2201 bbox_inches="tight", 2202 ) 2203 plt.close() 2204 2205 # # コスペクトルの図を作成 2206 # if plot_co: 2207 # _, axes_cosp = plt.subplots(1, 2, figsize=(12, 5), sharex=True) 2208 # for ax, config in zip(axes_cosp, plot_configs): 2209 # ax.scatter( 2210 # freqs, 2211 # averaged_co_spectra[config["col"]], 2212 # c=config["color"], 2213 # s=100, 2214 # ) 2215 # ax.set_xscale("log") 2216 # ax.set_yscale("log") 2217 # ax.set_xlim(0.001, 10) 2218 # ax.plot([0.01, 10], [1, 0.01], "-", color="black", alpha=0.5) 2219 # ax.text(0.1, 0.1, "-4/3", fontsize=18) 2220 # ax.set_ylabel(config["co_ylabel"]) 2221 # if config["label"] is not None: 2222 # ax.text( 2223 # 0.02, 0.98, config["label"], transform=ax.transAxes, va="top" 2224 # ) 2225 # ax.grid(True, alpha=0.3) 2226 # ax.set_xlabel("f (Hz)") 2227 2228 # コスペクトルの図を作成 2229 if plot_co: 2230 _, axes_cosp = plt.subplots(1, 2, figsize=(12, 5), sharex=True) 2231 for ax, config in zip(axes_cosp, plot_configs): 2232 ax.plot( 2233 freqs, 2234 averaged_co_spectra[config["col"]], 2235 "o", # マーカーを丸に設定 2236 color=config["color"], 2237 markersize=markersize, 2238 ) 2239 ax.set_xscale("log") 2240 ax.set_yscale("log") 2241 ax.set_xlim(0.001, 10) 2242 ax.plot([0.01, 10], [1, 0.01], "-", color="black", alpha=0.5) 2243 ax.text(0.1, 0.1, "-4/3", fontsize=18) 2244 ax.set_ylabel(config["co_ylabel"]) 2245 if config["label"] is not None: 2246 ax.text( 2247 0.02, 0.98, config["label"], transform=ax.transAxes, va="top" 2248 ) 2249 ax.grid(True, alpha=0.3) 2250 ax.set_xlabel("f (Hz)") 2251 2252 plt.tight_layout() 2253 output_path_csd: str = os.path.join(output_dir, f"co_{output_basename}.png") 2254 plt.savefig( 2255 output_path_csd, 2256 dpi=300, 2257 bbox_inches="tight", 2258 ) 2259 plt.close() 2260 2261 def plot_turbulence( 2262 self, 2263 df: pd.DataFrame, 2264 output_dir: str, 2265 output_filename: str = "turbulence.png", 2266 col_uz: str = "Uz", 2267 col_ch4: str = "Ultra_CH4_ppm_C", 2268 col_c2h6: str = "Ultra_C2H6_ppb", 2269 col_timestamp: str = "TIMESTAMP", 2270 add_serial_labels: bool = True, 2271 ) -> None: 2272 """時系列データのプロットを作成する 2273 2274 Parameters: 2275 ------ 2276 df : pd.DataFrame 2277 プロットするデータを含むDataFrame 2278 output_dir : str 2279 出力ディレクトリのパス 2280 output_filename : str 2281 出力ファイル名 2282 col_uz : str 2283 鉛直風速データのカラム名 2284 col_ch4 : str 2285 メタンデータのカラム名 2286 col_c2h6 : str 2287 エタンデータのカラム名 2288 col_timestamp : str 2289 タイムスタンプのカラム名 2290 """ 2291 # 出力ディレクトリの作成 2292 os.makedirs(output_dir, exist_ok=True) 2293 output_path: str = os.path.join(output_dir, output_filename) 2294 2295 # データの前処理 2296 df = df.copy() 2297 2298 # タイムスタンプをインデックスに設定(まだ設定されていない場合) 2299 if not isinstance(df.index, pd.DatetimeIndex): 2300 df[col_timestamp] = pd.to_datetime(df[col_timestamp]) 2301 df.set_index(col_timestamp, inplace=True) 2302 2303 # 開始時刻と終了時刻を取得 2304 start_time = df.index[0] 2305 end_time = df.index[-1] 2306 2307 # 開始時刻の分を取得 2308 start_minute = start_time.minute 2309 2310 # 時間軸の作成(実際の開始時刻からの経過分数) 2311 minutes_elapsed = (df.index - start_time).total_seconds() / 60 2312 2313 # プロットの作成 2314 _, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=(12, 10), sharex=True) 2315 2316 # 鉛直風速 2317 ax1.plot(minutes_elapsed, df[col_uz], "k-", linewidth=0.5) 2318 ax1.set_ylabel(r"$w$ (m s$^{-1}$)") 2319 if add_serial_labels: 2320 ax1.text(0.02, 0.98, "(a)", transform=ax1.transAxes, va="top") 2321 ax1.grid(True, alpha=0.3) 2322 2323 # CH4濃度 2324 ax2.plot(minutes_elapsed, df[col_ch4], "r-", linewidth=0.5) 2325 ax2.set_ylabel(r"$\mathrm{CH_4}$ (ppm)") 2326 if add_serial_labels: 2327 ax2.text(0.02, 0.98, "(b)", transform=ax2.transAxes, va="top") 2328 ax2.grid(True, alpha=0.3) 2329 2330 # C2H6濃度 2331 ax3.plot(minutes_elapsed, df[col_c2h6], "orange", linewidth=0.5) 2332 ax3.set_ylabel(r"$\mathrm{C_2H_6}$ (ppb)") 2333 if add_serial_labels: 2334 ax3.text(0.02, 0.98, "(c)", transform=ax3.transAxes, va="top") 2335 ax3.grid(True, alpha=0.3) 2336 ax3.set_xlabel("Time (minutes)") 2337 2338 # x軸の範囲を実際の開始時刻から30分後までに設定 2339 total_minutes = (end_time - start_time).total_seconds() / 60 2340 ax3.set_xlim(0, min(30, total_minutes)) 2341 2342 # x軸の目盛りを5分間隔で設定 2343 np.arange(start_minute, start_minute + 35, 5) 2344 ax3.xaxis.set_major_locator(MultipleLocator(5)) 2345 2346 # レイアウトの調整 2347 plt.tight_layout() 2348 2349 # 図の保存 2350 plt.savefig(output_path, dpi=300, bbox_inches="tight") 2351 plt.close() 2352 2353 def plot_wind_rose_sources( 2354 self, 2355 df: pd.DataFrame, 2356 output_dir: str | Path | None = None, 2357 output_filename: str = "wind_rose.png", 2358 col_datetime: str = "Date", 2359 col_ch4_flux: str = "Fch4", 2360 col_c2h6_flux: str = "Fc2h6", 2361 col_wind_dir: str = "Wind direction", 2362 flux_unit: str = r"(nmol m$^{-2}$ s$^{-1}$)", 2363 ymax: float | None = None, # フラックスの上限値 2364 label_gas: str = "都市ガス起源", 2365 label_bio: str = "生物起源", 2366 figsize: tuple[float, float] = (8, 8), 2367 flux_alpha: float = 0.4, 2368 num_directions: int = 8, # 方位の数(8方位) 2369 center_on_angles: bool = True, # 追加:45度刻みの線を境界にするかどうか 2370 subplot_label: str | None = None, 2371 add_legend: bool = True, 2372 print_summary: bool = True, # 統計情報を表示するかどうか 2373 save_fig: bool = True, 2374 show_fig: bool = True, 2375 ) -> None: 2376 """CH4フラックスの都市ガス起源と生物起源の風配図を作成する関数 2377 2378 Parameters: 2379 ------ 2380 df : pd.DataFrame 2381 風配図を作成するためのデータフレーム 2382 output_dir : str | Path | None 2383 生成された図を保存するディレクトリのパス 2384 output_filename : str 2385 保存するファイル名(デフォルトは"wind_rose.png") 2386 col_ch4_flux : str 2387 CH4フラックスを示すカラム名 2388 col_c2h6_flux : str 2389 C2H6フラックスを示すカラム名 2390 col_wind_dir : str 2391 風向を示すカラム名 2392 label_gas : str 2393 都市ガス起源のフラックスに対するラベル 2394 label_bio : str 2395 生物起源のフラックスに対するラベル 2396 col_datetime : str 2397 日時を示すカラム名 2398 num_directions : int 2399 風向の数(デフォルトは8) 2400 center_on_angles: bool 2401 Trueの場合、45度刻みの線を境界として扇形を描画します。 2402 Falseの場合、45度の中間(22.5度)を中心として扇形を描画します。 2403 subplot_label : str 2404 サブプロットに表示するラベル 2405 print_summary : bool 2406 統計情報を表示するかどうかのフラグ 2407 flux_unit : str 2408 フラックスの単位 2409 ymax : float | None 2410 y軸の上限値(指定しない場合はデータの最大値に基づいて自動設定) 2411 figsize : tuple[float, float] 2412 図のサイズ 2413 flux_alpha : float 2414 フラックスの透明度 2415 save_fig : bool 2416 図を保存するかどうかのフラグ 2417 show_fig : bool 2418 図を表示するかどうかのフラグ 2419 """ 2420 # 起源の計算 2421 df_with_sources = self._calculate_source_contributions( 2422 df=df, 2423 col_ch4_flux=col_ch4_flux, 2424 col_c2h6_flux=col_c2h6_flux, 2425 col_datetime=col_datetime, 2426 ) 2427 2428 # 方位の定義 2429 direction_ranges = self._define_direction_ranges( 2430 num_directions, center_on_angles 2431 ) 2432 2433 # 方位ごとのデータを集計 2434 direction_data = self._aggregate_direction_data( 2435 df_with_sources, col_wind_dir, direction_ranges 2436 ) 2437 2438 # プロットの作成 2439 fig = plt.figure(figsize=figsize) 2440 ax = fig.add_subplot(111, projection="polar") 2441 2442 # 方位の角度(ラジアン)を計算 2443 theta = np.array( 2444 [np.radians(angle) for angle in direction_data["center_angle"]] 2445 ) 2446 2447 # 生物起源と都市ガス起源を独立してプロット 2448 ax.bar( 2449 theta, 2450 direction_data["bio_flux"], 2451 width=np.radians(360 / num_directions), 2452 bottom=0.0, 2453 color="blue", 2454 alpha=flux_alpha, 2455 label=label_bio, 2456 ) 2457 2458 ax.bar( 2459 theta, 2460 direction_data["gas_flux"], 2461 width=np.radians(360 / num_directions), 2462 bottom=0.0, 2463 color="red", 2464 alpha=flux_alpha, 2465 label=label_gas, 2466 ) 2467 2468 # y軸の範囲を設定 2469 if ymax is not None: 2470 ax.set_ylim(0, ymax) 2471 else: 2472 # データの最大値に基づいて自動設定 2473 max_value = max( 2474 direction_data["bio_flux"].max(), direction_data["gas_flux"].max() 2475 ) 2476 ax.set_ylim(0, max_value * 1.1) # 最大値の1.1倍を上限に設定 2477 2478 # 方位ラベルの設定 2479 ax.set_theta_zero_location("N") # 北を上に設定 2480 ax.set_theta_direction(-1) # 時計回りに設定 2481 2482 # 方位ラベルの表示 2483 labels = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"] 2484 angles = np.radians(np.linspace(0, 360, len(labels), endpoint=False)) 2485 ax.set_xticks(angles) 2486 ax.set_xticklabels(labels) 2487 2488 # プロット領域の調整(上部と下部にスペースを確保) 2489 plt.subplots_adjust( 2490 top=0.8, # 上部に20%のスペースを確保 2491 bottom=0.2, # 下部に20%のスペースを確保(凡例用) 2492 ) 2493 2494 # サブプロットラベルの追加(デフォルトは左上) 2495 if subplot_label: 2496 ax.text( 2497 0.01, 2498 0.99, 2499 subplot_label, 2500 transform=ax.transAxes, 2501 ) 2502 2503 # 単位の追加(図の下部中央に配置) 2504 plt.figtext( 2505 0.5, # x位置(中央) 2506 0.1, # y位置(下部) 2507 flux_unit, 2508 ha="center", # 水平方向の位置揃え 2509 va="bottom", # 垂直方向の位置揃え 2510 ) 2511 2512 # 凡例の追加(単位の下に配置) 2513 if add_legend: 2514 # 最初のプロットから凡例のハンドルとラベルを取得 2515 handles, labels = ax.get_legend_handles_labels() 2516 # 図の下部に凡例を配置 2517 fig.legend( 2518 handles, 2519 labels, 2520 loc="center", 2521 bbox_to_anchor=(0.5, 0.05), # x=0.5で中央、y=0.05で下部に配置 2522 ncol=len(handles), # ハンドルの数だけ列を作成(一行に表示) 2523 ) 2524 2525 # グラフの保存 2526 if save_fig: 2527 if output_dir is None: 2528 raise ValueError( 2529 "save_fig=Trueのとき、output_dirに有効なパスを指定する必要があります。" 2530 ) 2531 # 出力ディレクトリの作成 2532 os.makedirs(output_dir, exist_ok=True) 2533 output_path: str = os.path.join(output_dir, output_filename) 2534 plt.savefig(output_path, dpi=300, bbox_inches="tight") 2535 2536 # グラフの表示 2537 if show_fig: 2538 plt.show() 2539 else: 2540 plt.close(fig=fig) 2541 2542 # 統計情報の表示 2543 if print_summary: 2544 for source in ["gas", "bio"]: 2545 flux_data = direction_data[f"{source}_flux"] 2546 mean_val = flux_data.mean() 2547 max_val = flux_data.max() 2548 max_dir = direction_data.loc[flux_data.idxmax(), "name"] 2549 2550 self.logger.info( 2551 f"{label_gas if source == 'gas' else label_bio}の統計:" 2552 ) 2553 print(f" 平均フラックス: {mean_val:.2f}") 2554 print(f" 最大フラックス: {max_val:.2f}") 2555 print(f" 最大フラックスの方位: {max_dir}") 2556 2557 def _define_direction_ranges( 2558 self, 2559 num_directions: int = 8, 2560 center_on_angles: bool = False, 2561 ) -> pd.DataFrame: 2562 """方位の範囲を定義 2563 2564 Parameters: 2565 ------ 2566 num_directions : int 2567 方位の数(デフォルトは8) 2568 center_on_angles : bool 2569 Trueの場合、45度刻みの線を境界として扇形を描画します。 2570 Falseの場合、45度の中間(22.5度)を中心として扇形を描画します。 2571 2572 Returns: 2573 ------ 2574 pd.DataFrame 2575 方位の定義を含むDataFrame 2576 """ 2577 if num_directions == 8: 2578 if center_on_angles: 2579 # 45度刻みの線を境界とする場合 2580 directions = pd.DataFrame( 2581 { 2582 "name": ["N", "NE", "E", "SE", "S", "SW", "W", "NW"], 2583 "center_angle": [ 2584 22.5, 2585 67.5, 2586 112.5, 2587 157.5, 2588 202.5, 2589 247.5, 2590 292.5, 2591 337.5, 2592 ], 2593 } 2594 ) 2595 else: 2596 # 従来通り45度を中心とする場合 2597 directions = pd.DataFrame( 2598 { 2599 "name": ["N", "NE", "E", "SE", "S", "SW", "W", "NW"], 2600 "center_angle": [0, 45, 90, 135, 180, 225, 270, 315], 2601 } 2602 ) 2603 else: 2604 raise ValueError(f"現在{num_directions}方位はサポートされていません") 2605 2606 # 各方位の範囲を計算 2607 angle_range = 360 / num_directions 2608 directions["start_angle"] = directions["center_angle"] - angle_range / 2 2609 directions["end_angle"] = directions["center_angle"] + angle_range / 2 2610 2611 # -180度から180度の範囲に正規化 2612 directions["start_angle"] = np.where( 2613 directions["start_angle"] > 180, 2614 directions["start_angle"] - 360, 2615 directions["start_angle"], 2616 ) 2617 directions["end_angle"] = np.where( 2618 directions["end_angle"] > 180, 2619 directions["end_angle"] - 360, 2620 directions["end_angle"], 2621 ) 2622 2623 return directions 2624 2625 def _aggregate_direction_data( 2626 self, 2627 df: pd.DataFrame, 2628 col_wind_dir: str, 2629 direction_ranges: pd.DataFrame, 2630 ) -> pd.DataFrame: 2631 """方位ごとのフラックスデータを集計 2632 2633 Parameters: 2634 ------ 2635 df : pd.DataFrame 2636 ソース分離済みのデータフレーム 2637 col_wind_dir : str 2638 風向のカラム名 2639 direction_ranges : pd.DataFrame 2640 方位の定義 2641 2642 Returns: 2643 ------ 2644 pd.DataFrame 2645 方位ごとの集計データ 2646 """ 2647 result_data = direction_ranges.copy() 2648 result_data["gas_flux"] = 0.0 2649 result_data["bio_flux"] = 0.0 2650 2651 for idx, row in direction_ranges.iterrows(): 2652 if row["start_angle"] < row["end_angle"]: 2653 mask = (df[col_wind_dir] > row["start_angle"]) & ( 2654 df[col_wind_dir] <= row["end_angle"] 2655 ) 2656 else: # 北方向など、-180度と180度をまたぐ場合 2657 mask = (df[col_wind_dir] > row["start_angle"]) | ( 2658 df[col_wind_dir] <= row["end_angle"] 2659 ) 2660 2661 result_data.loc[idx, "gas_flux"] = df.loc[mask, "ch4_gas"].mean() 2662 result_data.loc[idx, "bio_flux"] = df.loc[mask, "ch4_bio"].mean() 2663 2664 # NaNを0に置換 2665 result_data = result_data.fillna(0) 2666 2667 return result_data 2668 2669 def _calculate_source_contributions( 2670 self, 2671 df: pd.DataFrame, 2672 col_ch4_flux: str, 2673 col_c2h6_flux: str, 2674 gas_ratio_c1c2: float = 0.076, 2675 col_datetime: str = "Date", 2676 ) -> pd.DataFrame: 2677 """ 2678 CH4フラックスの都市ガス起源と生物起源の寄与を計算する。 2679 このロジックでは、燃焼起源のCH4フラックスは考慮せず計算している。 2680 2681 Parameters: 2682 ------ 2683 df : pd.DataFrame 2684 入力データフレーム 2685 col_ch4_flux : str 2686 CH4フラックスのカラム名 2687 col_c2h6_flux : str 2688 C2H6フラックスのカラム名 2689 gas_ratio_c1c2 : float 2690 ガスのC2H6/CH4比(ppb/ppb) 2691 col_datetime : str 2692 日時カラムの名前 2693 2694 Returns: 2695 ------ 2696 pd.DataFrame 2697 起源別のフラックス値を含むデータフレーム 2698 """ 2699 df_processed = df.copy() 2700 2701 # 日時インデックスの処理 2702 if not isinstance(df_processed.index, pd.DatetimeIndex): 2703 df_processed[col_datetime] = pd.to_datetime(df_processed[col_datetime]) 2704 df_processed.set_index(col_datetime, inplace=True) 2705 2706 # C2H6/CH4比の計算 2707 df_processed["c2c1_ratio"] = ( 2708 df_processed[col_c2h6_flux] / df_processed[col_ch4_flux] 2709 ) 2710 2711 # 都市ガスの標準組成に基づく都市ガス比率の計算 2712 df_processed["gas_ratio"] = df_processed["c2c1_ratio"] / gas_ratio_c1c2 * 100 2713 2714 # gas_ratioに基づいて都市ガス起源と生物起源の寄与を比例配分 2715 df_processed["ch4_gas"] = df_processed[col_ch4_flux] * np.clip( 2716 df_processed["gas_ratio"] / 100, 0, 1 2717 ) 2718 df_processed["ch4_bio"] = df_processed[col_ch4_flux] * ( 2719 1 - np.clip(df_processed["gas_ratio"] / 100, 0, 1) 2720 ) 2721 2722 return df_processed 2723 2724 def _prepare_diurnal_data( 2725 self, 2726 df: pd.DataFrame, 2727 target_columns: list[str], 2728 include_date_types: bool = False, 2729 ) -> tuple[dict[str, pd.DataFrame], pd.DatetimeIndex]: 2730 """ 2731 日変化パターンの計算に必要なデータを準備する。 2732 2733 Parameters: 2734 ------ 2735 df : pd.DataFrame 2736 入力データフレーム 2737 target_columns : list[str] 2738 計算対象の列名のリスト 2739 include_date_types : bool 2740 日付タイプ(平日/休日など)の分類を含めるかどうか 2741 2742 Returns: 2743 ------ 2744 tuple[dict[str, pd.DataFrame], pd.DatetimeIndex] 2745 - 時間帯ごとの平均値を含むDataFrameの辞書 2746 - 24時間分の時間点 2747 """ 2748 df = df.copy() 2749 df["hour"] = pd.to_datetime(df["Date"]).dt.hour 2750 2751 # 時間ごとの平均値を計算する関数 2752 def calculate_hourly_means(data_df, condition=None): 2753 if condition is not None: 2754 data_df = data_df[condition] 2755 return data_df.groupby("hour")[target_columns].mean().reset_index() 2756 2757 # 基本の全日データを計算 2758 hourly_means = {"all": calculate_hourly_means(df)} 2759 2760 # 日付タイプによる分類が必要な場合 2761 if include_date_types: 2762 dates = pd.to_datetime(df["Date"]) 2763 is_weekend = dates.dt.dayofweek.isin([5, 6]) 2764 is_holiday = dates.map(lambda x: jpholiday.is_holiday(x.date())) 2765 is_weekday = ~(is_weekend | is_holiday) 2766 2767 hourly_means.update( 2768 { 2769 "weekday": calculate_hourly_means(df, is_weekday), 2770 "weekend": calculate_hourly_means(df, is_weekend), 2771 "holiday": calculate_hourly_means(df, is_weekend | is_holiday), 2772 } 2773 ) 2774 2775 # 24時目のデータを追加 2776 for col in hourly_means: 2777 last_row = hourly_means[col].iloc[0:1].copy() 2778 last_row["hour"] = 24 2779 hourly_means[col] = pd.concat( 2780 [hourly_means[col], last_row], ignore_index=True 2781 ) 2782 2783 # 24時間分のデータポイントを作成 2784 time_points = pd.date_range("2024-01-01", periods=25, freq="h") 2785 2786 return hourly_means, time_points 2787 2788 def _setup_diurnal_axes( 2789 self, 2790 ax: plt.Axes, 2791 time_points: pd.DatetimeIndex, 2792 ylabel: str, 2793 subplot_label: str | None = None, 2794 show_label: bool = True, 2795 show_legend: bool = True, 2796 subplot_fontsize: int = 20, 2797 ) -> None: 2798 """日変化プロットの軸の設定を行う 2799 2800 Parameters: 2801 ------ 2802 ax : plt.Axes 2803 設定対象の軸 2804 time_points : pd.DatetimeIndex 2805 時間軸のポイント 2806 ylabel : str 2807 y軸のラベル 2808 subplot_label : str | None 2809 サブプロットのラベル 2810 show_label : bool 2811 軸ラベルを表示するかどうか 2812 show_legend : bool 2813 凡例を表示するかどうか 2814 subplot_fontsize : int 2815 サブプロットのフォントサイズ 2816 """ 2817 if show_label: 2818 ax.set_xlabel("Time (hour)") 2819 ax.set_ylabel(ylabel) 2820 2821 ax.xaxis.set_major_formatter(mdates.DateFormatter("%-H")) 2822 ax.xaxis.set_major_locator(mdates.HourLocator(byhour=[0, 6, 12, 18, 24])) 2823 ax.set_xlim(time_points[0], time_points[-1]) 2824 ax.set_xticks(time_points[::6]) 2825 ax.set_xticklabels(["0", "6", "12", "18", "24"]) 2826 2827 if subplot_label: 2828 ax.text( 2829 0.02, 2830 0.98, 2831 subplot_label, 2832 transform=ax.transAxes, 2833 va="top", 2834 fontsize=subplot_fontsize, 2835 ) 2836 2837 if show_legend: 2838 ax.legend() 2839 2840 @staticmethod 2841 def get_valid_data(df: pd.DataFrame, x_col: str, y_col: str) -> pd.DataFrame: 2842 """ 2843 指定された列の有効なデータ(NaNを除いた)を取得します。 2844 2845 Parameters: 2846 ------ 2847 df : pd.DataFrame 2848 データフレーム 2849 x_col : str 2850 X軸の列名 2851 y_col : str 2852 Y軸の列名 2853 2854 Returns: 2855 ------ 2856 pd.DataFrame 2857 有効なデータのみを含むDataFrame 2858 """ 2859 return df.copy().dropna(subset=[x_col, y_col]) 2860 2861 @staticmethod 2862 def setup_logger(logger: Logger | None, log_level: int = INFO) -> Logger: 2863 """ 2864 ロガーを設定します。 2865 2866 このメソッドは、ロギングの設定を行い、ログメッセージのフォーマットを指定します。 2867 ログメッセージには、日付、ログレベル、メッセージが含まれます。 2868 2869 渡されたロガーがNoneまたは不正な場合は、新たにロガーを作成し、標準出力に 2870 ログメッセージが表示されるようにStreamHandlerを追加します。ロガーのレベルは 2871 引数で指定されたlog_levelに基づいて設定されます。 2872 2873 Parameters: 2874 ------ 2875 logger : Logger | None 2876 使用するロガー。Noneの場合は新しいロガーを作成します。 2877 log_level : int 2878 ロガーのログレベル。デフォルトはINFO。 2879 2880 Returns: 2881 ------ 2882 Logger 2883 設定されたロガーオブジェクト。 2884 """ 2885 if logger is not None and isinstance(logger, Logger): 2886 return logger 2887 # 渡されたロガーがNoneまたは正しいものでない場合は独自に設定 2888 new_logger: Logger = getLogger() 2889 # 既存のハンドラーをすべて削除 2890 for handler in new_logger.handlers[:]: 2891 new_logger.removeHandler(handler) 2892 new_logger.setLevel(log_level) # ロガーのレベルを設定 2893 ch = StreamHandler() 2894 ch_formatter = Formatter("%(asctime)s - %(levelname)s - %(message)s") 2895 ch.setFormatter(ch_formatter) # フォーマッターをハンドラーに設定 2896 new_logger.addHandler(ch) # StreamHandlerの追加 2897 return new_logger 2898 2899 @staticmethod 2900 def setup_plot_params( 2901 font_family: list[str] = ["Arial", "Dejavu Sans"], 2902 font_size: float = 20, 2903 legend_size: float = 20, 2904 tick_size: float = 20, 2905 title_size: float = 20, 2906 plot_params=None, 2907 ) -> None: 2908 """ 2909 matplotlibのプロットパラメータを設定します。 2910 2911 Parameters: 2912 ------ 2913 font_family : list[str] 2914 使用するフォントファミリーのリスト。 2915 font_size : float 2916 軸ラベルのフォントサイズ。 2917 legend_size : float 2918 凡例のフォントサイズ。 2919 tick_size : float 2920 軸目盛りのフォントサイズ。 2921 title_size : float 2922 タイトルのフォントサイズ。 2923 plot_params : Optional[Dict[str, any]] 2924 matplotlibのプロットパラメータの辞書。 2925 """ 2926 # デフォルトのプロットパラメータ 2927 default_params = { 2928 "axes.linewidth": 1.0, 2929 "axes.titlesize": title_size, # タイトル 2930 "grid.color": "gray", 2931 "grid.linewidth": 1.0, 2932 "font.family": font_family, 2933 "font.size": font_size, # 軸ラベル 2934 "legend.fontsize": legend_size, # 凡例 2935 "text.color": "black", 2936 "xtick.color": "black", 2937 "ytick.color": "black", 2938 "xtick.labelsize": tick_size, # 軸目盛 2939 "ytick.labelsize": tick_size, # 軸目盛 2940 "xtick.major.size": 0, 2941 "ytick.major.size": 0, 2942 "ytick.direction": "out", 2943 "ytick.major.width": 1.0, 2944 } 2945 2946 # plot_paramsが定義されている場合、デフォルトに追記 2947 if plot_params: 2948 default_params.update(plot_params) 2949 2950 plt.rcParams.update(default_params) # プロットパラメータを更新 2951 2952 @staticmethod 2953 def plot_flux_distributions( 2954 g2401_flux: pd.Series, 2955 ultra_flux: pd.Series, 2956 month: int, 2957 output_dir: str, 2958 xlim: tuple[float, float] = (-50, 200), 2959 bandwidth: float = 1.0, # デフォルト値を1.0に設定 2960 ) -> None: 2961 """ 2962 両測器のCH4フラックス分布を可視化 2963 2964 Parameters: 2965 ------ 2966 g2401_flux : pd.Series 2967 G2401で測定されたフラックス値の配列 2968 ultra_flux : pd.Series 2969 Ultraで測定されたフラックス値の配列 2970 month : int 2971 測定月 2972 output_dir : str 2973 出力ディレクトリ 2974 xlim : tuple[float, float] 2975 x軸の範囲(タプル) 2976 bandwidth : float 2977 カーネル密度推定のバンド幅調整係数(デフォルト: 1.0) 2978 """ 2979 # nanを除去 2980 g2401_flux = g2401_flux.dropna() 2981 ultra_flux = ultra_flux.dropna() 2982 2983 plt.figure(figsize=(10, 6)) 2984 2985 # KDEプロット(確率密度推定) 2986 sns.kdeplot( 2987 data=g2401_flux, label="G2401", color="blue", alpha=0.5, bw_adjust=bandwidth 2988 ) 2989 sns.kdeplot( 2990 data=ultra_flux, label="Ultra", color="red", alpha=0.5, bw_adjust=bandwidth 2991 ) 2992 2993 # 平均値と中央値のマーカー 2994 plt.axvline( 2995 g2401_flux.mean(), 2996 color="blue", 2997 linestyle="--", 2998 alpha=0.5, 2999 label="G2401 mean", 3000 ) 3001 plt.axvline( 3002 ultra_flux.mean(), 3003 color="red", 3004 linestyle="--", 3005 alpha=0.5, 3006 label="Ultra mean", 3007 ) 3008 plt.axvline( 3009 np.median(g2401_flux), 3010 color="blue", 3011 linestyle=":", 3012 alpha=0.5, 3013 label="G2401 median", 3014 ) 3015 plt.axvline( 3016 np.median(ultra_flux), 3017 color="red", 3018 linestyle=":", 3019 alpha=0.5, 3020 label="Ultra median", 3021 ) 3022 3023 # 軸ラベルとタイトル 3024 plt.xlabel(r"CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)") 3025 plt.ylabel("Probability Density") 3026 plt.title(f"Distribution of CH$_4$ fluxes - Month {month}") 3027 3028 # x軸の範囲設定 3029 plt.xlim(xlim) 3030 3031 # グリッド表示 3032 plt.grid(True, alpha=0.3) 3033 3034 # 統計情報 3035 stats_text = ( 3036 f"G2401:\n" 3037 f" Mean: {g2401_flux.mean():.2f}\n" 3038 f" Median: {np.median(g2401_flux):.2f}\n" 3039 f" Std: {g2401_flux.std():.2f}\n" 3040 f"Ultra:\n" 3041 f" Mean: {ultra_flux.mean():.2f}\n" 3042 f" Median: {np.median(ultra_flux):.2f}\n" 3043 f" Std: {ultra_flux.std():.2f}" 3044 ) 3045 plt.text( 3046 0.02, 3047 0.98, 3048 stats_text, 3049 transform=plt.gca().transAxes, 3050 verticalalignment="top", 3051 fontsize=10, 3052 bbox=dict(boxstyle="round", facecolor="white", alpha=0.8), 3053 ) 3054 3055 # 凡例の表示 3056 plt.legend(loc="upper right") 3057 3058 # グラフの保存 3059 os.makedirs(output_dir, exist_ok=True) 3060 plt.tight_layout() 3061 plt.savefig( 3062 os.path.join(output_dir, f"flux_distribution_month_{month}.png"), 3063 dpi=300, 3064 bbox_inches="tight", 3065 ) 3066 plt.close()
64 def __init__( 65 self, 66 logger: Logger | None = None, 67 logging_debug: bool = False, 68 ) -> None: 69 """ 70 クラスのコンストラクタ 71 72 Parameters: 73 ------ 74 logger : Logger | None 75 使用するロガー。Noneの場合は新しいロガーを作成します。 76 logging_debug : bool 77 ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。 78 """ 79 # ロガー 80 log_level: int = INFO 81 if logging_debug: 82 log_level = DEBUG 83 self.logger: Logger = MonthlyFiguresGenerator.setup_logger(logger, log_level)
クラスのコンストラクタ
Parameters:
logger : Logger | None
使用するロガー。Noneの場合は新しいロガーを作成します。
logging_debug : bool
ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。
85 def plot_c1c2_fluxes_timeseries( 86 self, 87 df, 88 output_dir: str, 89 output_filename: str = "timeseries.png", 90 col_datetime: str = "Date", 91 col_c1_flux: str = "Fch4_ultra", 92 col_c2_flux: str = "Fc2h6_ultra", 93 ): 94 """ 95 月別のフラックスデータを時系列プロットとして出力する 96 97 Parameters: 98 ------ 99 df : pd.DataFrame 100 月別データを含むDataFrame 101 output_dir : str 102 出力ファイルを保存するディレクトリのパス 103 output_filename : str 104 出力ファイルの名前 105 col_datetime : str 106 日付を含む列の名前。デフォルトは"Date"。 107 col_c1_flux : str 108 CH4フラックスを含む列の名前。デフォルトは"Fch4_ultra"。 109 col_c2_flux : str 110 C2H6フラックスを含む列の名前。デフォルトは"Fc2h6_ultra"。 111 """ 112 os.makedirs(output_dir, exist_ok=True) 113 output_path: str = os.path.join(output_dir, output_filename) 114 115 # 図の作成 116 _, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 8), sharex=True) 117 118 # CH4フラックスのプロット 119 ax1.scatter(df[col_datetime], df[col_c1_flux], color="red", alpha=0.5, s=20) 120 ax1.set_ylabel(r"CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)") 121 ax1.set_ylim(-100, 600) 122 ax1.text(0.02, 0.98, "(a)", transform=ax1.transAxes, va="top", fontsize=20) 123 ax1.grid(True, alpha=0.3) 124 125 # C2H6フラックスのプロット 126 ax2.scatter( 127 df[col_datetime], 128 df[col_c2_flux], 129 color="orange", 130 alpha=0.5, 131 s=20, 132 ) 133 ax2.set_ylabel(r"C$_2$H$_6$ flux (nmol m$^{-2}$ s$^{-1}$)") 134 ax2.set_ylim(-20, 60) 135 ax2.text(0.02, 0.98, "(b)", transform=ax2.transAxes, va="top", fontsize=20) 136 ax2.grid(True, alpha=0.3) 137 138 # x軸の設定 139 ax2.xaxis.set_major_locator(mdates.MonthLocator(interval=1)) 140 ax2.xaxis.set_major_formatter(mdates.DateFormatter("%m")) 141 plt.setp(ax2.get_xticklabels(), rotation=0, ha="right") 142 ax2.set_xlabel("Month") 143 144 # 図の保存 145 plt.savefig(output_path, dpi=300, bbox_inches="tight") 146 plt.close()
月別のフラックスデータを時系列プロットとして出力する
Parameters:
df : pd.DataFrame
月別データを含むDataFrame
output_dir : str
出力ファイルを保存するディレクトリのパス
output_filename : str
出力ファイルの名前
col_datetime : str
日付を含む列の名前。デフォルトは"Date"。
col_c1_flux : str
CH4フラックスを含む列の名前。デフォルトは"Fch4_ultra"。
col_c2_flux : str
C2H6フラックスを含む列の名前。デフォルトは"Fc2h6_ultra"。
148 def plot_c1c2_concentrations_and_fluxes_timeseries( 149 self, 150 df: pd.DataFrame, 151 output_dir: str, 152 output_filename: str = "conc_flux_timeseries.png", 153 col_datetime: str = "Date", 154 col_ch4_conc: str = "CH4_ultra", 155 col_ch4_flux: str = "Fch4_ultra", 156 col_c2h6_conc: str = "C2H6_ultra", 157 col_c2h6_flux: str = "Fc2h6_ultra", 158 print_summary: bool = True, 159 ) -> None: 160 """ 161 CH4とC2H6の濃度とフラックスの時系列プロットを作成する 162 163 Parameters: 164 ------ 165 df : pd.DataFrame 166 月別データを含むDataFrame 167 output_dir : str 168 出力ディレクトリのパス 169 output_filename : str 170 出力ファイル名 171 col_datetime : str 172 日付列の名前 173 col_ch4_conc : str 174 CH4濃度列の名前 175 col_ch4_flux : str 176 CH4フラックス列の名前 177 col_c2h6_conc : str 178 C2H6濃度列の名前 179 col_c2h6_flux : str 180 C2H6フラックス列の名前 181 print_summary : bool 182 解析情報をprintするかどうか 183 """ 184 # 出力ディレクトリの作成 185 os.makedirs(output_dir, exist_ok=True) 186 output_path: str = os.path.join(output_dir, output_filename) 187 188 if print_summary: 189 # 統計情報の計算と表示 190 for name, col in [ 191 ("CH4 concentration", col_ch4_conc), 192 ("CH4 flux", col_ch4_flux), 193 ("C2H6 concentration", col_c2h6_conc), 194 ("C2H6 flux", col_c2h6_flux), 195 ]: 196 # NaNを除外してから統計量を計算 197 valid_data = df[col].dropna() 198 199 if len(valid_data) > 0: 200 percentile_5 = np.nanpercentile(valid_data, 5) 201 percentile_95 = np.nanpercentile(valid_data, 95) 202 mean_value = np.nanmean(valid_data) 203 positive_ratio = (valid_data > 0).mean() * 100 204 205 print(f"\n{name}:") 206 print( 207 f"90パーセンタイルレンジ: {percentile_5:.2f} - {percentile_95:.2f}" 208 ) 209 print(f"平均値: {mean_value:.2f}") 210 print(f"正の値の割合: {positive_ratio:.1f}%") 211 else: 212 print(f"\n{name}: データが存在しません") 213 214 # プロットの作成 215 _, (ax1, ax2, ax3, ax4) = plt.subplots(4, 1, figsize=(12, 16), sharex=True) 216 217 # CH4濃度のプロット 218 ax1.scatter(df[col_datetime], df[col_ch4_conc], color="red", alpha=0.5, s=20) 219 ax1.set_ylabel("CH$_4$ Concentration\n(ppm)") 220 ax1.set_ylim(1.8, 2.6) 221 ax1.text(0.02, 0.98, "(a)", transform=ax1.transAxes, va="top", fontsize=20) 222 ax1.grid(True, alpha=0.3) 223 224 # CH4フラックスのプロット 225 ax2.scatter(df[col_datetime], df[col_ch4_flux], color="red", alpha=0.5, s=20) 226 ax2.set_ylabel("CH$_4$ flux\n(nmol m$^{-2}$ s$^{-1}$)") 227 ax2.set_ylim(-100, 600) 228 # ax2.set_yticks([-100, 0, 200, 400, 600]) 229 ax2.text(0.02, 0.98, "(b)", transform=ax2.transAxes, va="top", fontsize=20) 230 ax2.grid(True, alpha=0.3) 231 232 # C2H6濃度のプロット 233 ax3.scatter( 234 df[col_datetime], df[col_c2h6_conc], color="orange", alpha=0.5, s=20 235 ) 236 ax3.set_ylabel("C$_2$H$_6$ Concentration\n(ppb)") 237 ax3.text(0.02, 0.98, "(c)", transform=ax3.transAxes, va="top", fontsize=20) 238 ax3.grid(True, alpha=0.3) 239 240 # C2H6フラックスのプロット 241 ax4.scatter( 242 df[col_datetime], df[col_c2h6_flux], color="orange", alpha=0.5, s=20 243 ) 244 ax4.set_ylabel("C$_2$H$_6$ flux\n(nmol m$^{-2}$ s$^{-1}$)") 245 ax4.set_ylim(-20, 40) 246 ax4.text(0.02, 0.98, "(d)", transform=ax4.transAxes, va="top", fontsize=20) 247 ax4.grid(True, alpha=0.3) 248 249 # x軸の設定 250 ax4.xaxis.set_major_locator(mdates.MonthLocator(interval=1)) 251 ax4.xaxis.set_major_formatter(mdates.DateFormatter("%m")) 252 plt.setp(ax4.get_xticklabels(), rotation=0, ha="right") 253 ax4.set_xlabel("Month") 254 255 # レイアウトの調整と保存 256 plt.tight_layout() 257 plt.savefig(output_path, dpi=300, bbox_inches="tight") 258 plt.close() 259 260 if print_summary: 261 262 def analyze_top_values(df, column_name, top_percent=20): 263 print(f"\n{column_name}の上位{top_percent}%の分析:") 264 265 # DataFrameのコピーを作成し、日時関連の列を追加 266 df_analysis = df.copy() 267 df_analysis["hour"] = pd.to_datetime(df_analysis[col_datetime]).dt.hour 268 df_analysis["month"] = pd.to_datetime( 269 df_analysis[col_datetime] 270 ).dt.month 271 df_analysis["weekday"] = pd.to_datetime( 272 df_analysis[col_datetime] 273 ).dt.dayofweek 274 275 # 上位20%のしきい値を計算 276 threshold = df[column_name].quantile(1 - top_percent / 100) 277 high_values = df_analysis[df_analysis[column_name] > threshold] 278 279 # 月ごとの分析 280 print("\n月別分布:") 281 monthly_counts = high_values.groupby("month").size() 282 total_counts = df_analysis.groupby("month").size() 283 monthly_percentages = (monthly_counts / total_counts * 100).round(1) 284 285 # 月ごとのデータを安全に表示 286 available_months = set(monthly_counts.index) & set(total_counts.index) 287 for month in sorted(available_months): 288 print( 289 f"月{month}: {monthly_percentages[month]}% ({monthly_counts[month]}件/{total_counts[month]}件)" 290 ) 291 292 # 時間帯ごとの分析(3時間区切り) 293 print("\n時間帯別分布:") 294 # copyを作成して新しい列を追加 295 high_values = high_values.copy() 296 high_values["time_block"] = high_values["hour"] // 3 * 3 297 time_blocks = high_values.groupby("time_block").size() 298 total_time_blocks = df_analysis.groupby( 299 df_analysis["hour"] // 3 * 3 300 ).size() 301 time_percentages = (time_blocks / total_time_blocks * 100).round(1) 302 303 # 時間帯ごとのデータを安全に表示 304 available_blocks = set(time_blocks.index) & set(total_time_blocks.index) 305 for block in sorted(available_blocks): 306 print( 307 f"{block:02d}:00-{block + 3:02d}:00: {time_percentages[block]}% ({time_blocks[block]}件/{total_time_blocks[block]}件)" 308 ) 309 310 # 曜日ごとの分析 311 print("\n曜日別分布:") 312 weekday_names = ["月曜", "火曜", "水曜", "木曜", "金曜", "土曜", "日曜"] 313 weekday_counts = high_values.groupby("weekday").size() 314 total_weekdays = df_analysis.groupby("weekday").size() 315 weekday_percentages = (weekday_counts / total_weekdays * 100).round(1) 316 317 # 曜日ごとのデータを安全に表示 318 available_days = set(weekday_counts.index) & set(total_weekdays.index) 319 for day in sorted(available_days): 320 if 0 <= day <= 6: # 有効な曜日インデックスのチェック 321 print( 322 f"{weekday_names[day]}: {weekday_percentages[day]}% ({weekday_counts[day]}件/{total_weekdays[day]}件)" 323 ) 324 325 # 濃度とフラックスそれぞれの分析を実行 326 print("\n=== 上位値の時間帯・曜日分析 ===") 327 analyze_top_values(df, col_ch4_conc) 328 analyze_top_values(df, col_ch4_flux) 329 analyze_top_values(df, col_c2h6_conc) 330 analyze_top_values(df, col_c2h6_flux)
CH4とC2H6の濃度とフラックスの時系列プロットを作成する
Parameters:
df : pd.DataFrame
月別データを含むDataFrame
output_dir : str
出力ディレクトリのパス
output_filename : str
出力ファイル名
col_datetime : str
日付列の名前
col_ch4_conc : str
CH4濃度列の名前
col_ch4_flux : str
CH4フラックス列の名前
col_c2h6_conc : str
C2H6濃度列の名前
col_c2h6_flux : str
C2H6フラックス列の名前
print_summary : bool
解析情報をprintするかどうか
332 def plot_ch4c2h6_timeseries( 333 self, 334 df: pd.DataFrame, 335 output_dir: str, 336 col_ch4_flux: str, 337 col_c2h6_flux: str, 338 output_filename: str = "timeseries_year.png", 339 col_datetime: str = "Date", 340 window_size: int = 24 * 7, # 1週間の移動平均のデフォルト値 341 confidence_interval: float = 0.95, # 95%信頼区間 342 subplot_label_ch4: str | None = "(a)", 343 subplot_label_c2h6: str | None = "(b)", 344 subplot_fontsize: int = 20, 345 show_ci: bool = True, 346 ch4_ylim: tuple[float, float] | None = None, 347 c2h6_ylim: tuple[float, float] | None = None, 348 start_date: str | None = None, # 追加:"YYYY-MM-DD"形式 349 end_date: str | None = None, # 追加:"YYYY-MM-DD"形式 350 figsize: tuple[float, float] = (16, 6), 351 ) -> None: 352 """CH4とC2H6フラックスの時系列変動をプロット 353 354 Parameters: 355 ------ 356 df : pd.DataFrame 357 データフレーム 358 output_dir : str 359 出力ディレクトリのパス 360 col_ch4_flux : str 361 CH4フラックスのカラム名 362 col_c2h6_flux : str 363 C2H6フラックスのカラム名 364 output_filename : str 365 出力ファイル名 366 col_datetime : str 367 日時カラムの名前 368 window_size : int 369 移動平均の窓サイズ 370 confidence_interval : float 371 信頼区間(0-1) 372 subplot_label_ch4 : str | None 373 CH4プロットのラベル 374 subplot_label_c2h6 : str | None 375 C2H6プロットのラベル 376 subplot_fontsize : int 377 サブプロットのフォントサイズ 378 show_ci : bool 379 信頼区間を表示するか 380 ch4_ylim : tuple[float, float] | None 381 CH4のy軸範囲 382 c2h6_ylim : tuple[float, float] | None 383 C2H6のy軸範囲 384 start_date : str | None 385 開始日(YYYY-MM-DD形式) 386 end_date : str | None 387 終了日(YYYY-MM-DD形式) 388 """ 389 # 出力ディレクトリの作成 390 os.makedirs(output_dir, exist_ok=True) 391 output_path: str = os.path.join(output_dir, output_filename) 392 393 # データの準備 394 df = df.copy() 395 if not isinstance(df.index, pd.DatetimeIndex): 396 df[col_datetime] = pd.to_datetime(df[col_datetime]) 397 df.set_index(col_datetime, inplace=True) 398 399 # 日付範囲の処理 400 if start_date is not None: 401 start_dt = pd.to_datetime(start_date) 402 if start_dt < df.index.min(): 403 self.logger.warning( 404 f"指定された開始日{start_date}がデータの開始日{df.index.min():%Y-%m-%d}より前です。" 405 f"データの開始日を使用します。" 406 ) 407 start_dt = df.index.min() 408 else: 409 start_dt = df.index.min() 410 411 if end_date is not None: 412 end_dt = pd.to_datetime(end_date) 413 if end_dt > df.index.max(): 414 self.logger.warning( 415 f"指定された終了日{end_date}がデータの終了日{df.index.max():%Y-%m-%d}より後です。" 416 f"データの終了日を使用します。" 417 ) 418 end_dt = df.index.max() 419 else: 420 end_dt = df.index.max() 421 422 # 指定された期間のデータを抽出 423 mask = (df.index >= start_dt) & (df.index <= end_dt) 424 df = df[mask] 425 426 # CH4とC2H6の移動平均と信頼区間を計算 427 ch4_mean, ch4_lower, ch4_upper = calculate_rolling_stats( 428 df[col_ch4_flux], window_size, confidence_interval 429 ) 430 c2h6_mean, c2h6_lower, c2h6_upper = calculate_rolling_stats( 431 df[col_c2h6_flux], window_size, confidence_interval 432 ) 433 434 # プロットの作成 435 fig, (ax1, ax2) = plt.subplots(1, 2, figsize=figsize) 436 437 # CH4プロット 438 ax1.plot(df.index, ch4_mean, "red", label="CH$_4$") 439 if show_ci: 440 ax1.fill_between(df.index, ch4_lower, ch4_upper, color="red", alpha=0.2) 441 if subplot_label_ch4: 442 ax1.text( 443 0.02, 444 0.98, 445 subplot_label_ch4, 446 transform=ax1.transAxes, 447 va="top", 448 fontsize=subplot_fontsize, 449 ) 450 ax1.set_ylabel("CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)") 451 if ch4_ylim is not None: 452 ax1.set_ylim(ch4_ylim) 453 ax1.grid(True, alpha=0.3) 454 455 # C2H6プロット 456 ax2.plot(df.index, c2h6_mean, "orange", label="C$_2$H$_6$") 457 if show_ci: 458 ax2.fill_between( 459 df.index, c2h6_lower, c2h6_upper, color="orange", alpha=0.2 460 ) 461 if subplot_label_c2h6: 462 ax2.text( 463 0.02, 464 0.98, 465 subplot_label_c2h6, 466 transform=ax2.transAxes, 467 va="top", 468 fontsize=subplot_fontsize, 469 ) 470 ax2.set_ylabel("C$_2$H$_6$ flux (nmol m$^{-2}$ s$^{-1}$)") 471 if c2h6_ylim is not None: 472 ax2.set_ylim(c2h6_ylim) 473 ax2.grid(True, alpha=0.3) 474 475 # x軸の設定 476 for ax in [ax1, ax2]: 477 ax.set_xlabel("Month") 478 # x軸の範囲を設定 479 ax.set_xlim(start_dt, end_dt) 480 481 # 1ヶ月ごとの主目盛り 482 ax.xaxis.set_major_locator(mdates.MonthLocator(interval=1)) 483 484 # カスタムフォーマッタの作成(数字を通常フォントで表示) 485 def date_formatter(x, p): 486 date = mdates.num2date(x) 487 return f"{date.strftime('%m')}" 488 489 ax.xaxis.set_major_formatter(plt.FuncFormatter(date_formatter)) 490 491 # 補助目盛りの設定 492 ax.xaxis.set_minor_locator(mdates.MonthLocator()) 493 # ティックラベルの回転と位置調整 494 plt.setp(ax.xaxis.get_majorticklabels(), ha="right") 495 496 plt.tight_layout() 497 plt.savefig(output_path, dpi=300, bbox_inches="tight") 498 plt.close(fig)
CH4とC2H6フラックスの時系列変動をプロット
Parameters:
df : pd.DataFrame
データフレーム
output_dir : str
出力ディレクトリのパス
col_ch4_flux : str
CH4フラックスのカラム名
col_c2h6_flux : str
C2H6フラックスのカラム名
output_filename : str
出力ファイル名
col_datetime : str
日時カラムの名前
window_size : int
移動平均の窓サイズ
confidence_interval : float
信頼区間(0-1)
subplot_label_ch4 : str | None
CH4プロットのラベル
subplot_label_c2h6 : str | None
C2H6プロットのラベル
subplot_fontsize : int
サブプロットのフォントサイズ
show_ci : bool
信頼区間を表示するか
ch4_ylim : tuple[float, float] | None
CH4のy軸範囲
c2h6_ylim : tuple[float, float] | None
C2H6のy軸範囲
start_date : str | None
開始日(YYYY-MM-DD形式)
end_date : str | None
終了日(YYYY-MM-DD形式)
500 def plot_ch4_flux_comparison( 501 self, 502 df: pd.DataFrame, 503 output_dir: str, 504 col_g2401_flux: str, 505 col_ultra_flux: str, 506 output_filename: str = "ch4_flux_comparison.png", 507 col_datetime: str = "Date", 508 window_size: int = 24 * 7, # 1週間の移動平均のデフォルト値 509 confidence_interval: float = 0.95, # 95%信頼区間 510 subplot_label: str | None = None, 511 subplot_fontsize: int = 20, 512 show_ci: bool = True, 513 y_lim: tuple[float, float] | None = None, 514 start_date: str | None = None, 515 end_date: str | None = None, 516 figsize: tuple[float, float] = (12, 6), 517 legend_loc: str = "upper right", 518 ) -> None: 519 """G2401とUltraによるCH4フラックスの時系列比較プロット 520 521 Parameters: 522 ------ 523 df : pd.DataFrame 524 データフレーム 525 output_dir : str 526 出力ディレクトリのパス 527 col_g2401_flux : str 528 G2401のCH4フラックスのカラム名 529 col_ultra_flux : str 530 UltraのCH4フラックスのカラム名 531 output_filename : str 532 出力ファイル名 533 col_datetime : str 534 日時カラムの名前 535 window_size : int 536 移動平均の窓サイズ 537 confidence_interval : float 538 信頼区間(0-1) 539 subplot_label : str | None 540 プロットのラベル 541 subplot_fontsize : int 542 サブプロットのフォントサイズ 543 show_ci : bool 544 信頼区間を表示するか 545 y_lim : tuple[float, float] | None 546 y軸の範囲 547 start_date : str | None 548 開始日(YYYY-MM-DD形式) 549 end_date : str | None 550 終了日(YYYY-MM-DD形式) 551 figsize : tuple[float, float] 552 図のサイズ 553 legend_loc : str 554 凡例の位置 555 """ 556 # 出力ディレクトリの作成 557 os.makedirs(output_dir, exist_ok=True) 558 output_path: str = os.path.join(output_dir, output_filename) 559 560 # データの準備 561 df = df.copy() 562 if not isinstance(df.index, pd.DatetimeIndex): 563 df[col_datetime] = pd.to_datetime(df[col_datetime]) 564 df.set_index(col_datetime, inplace=True) 565 566 # 日付範囲の処理(既存のコードと同様) 567 if start_date is not None: 568 start_dt = pd.to_datetime(start_date) 569 if start_dt < df.index.min(): 570 self.logger.warning( 571 f"指定された開始日{start_date}がデータの開始日{df.index.min():%Y-%m-%d}より前です。" 572 f"データの開始日を使用します。" 573 ) 574 start_dt = df.index.min() 575 else: 576 start_dt = df.index.min() 577 578 if end_date is not None: 579 end_dt = pd.to_datetime(end_date) 580 if end_dt > df.index.max(): 581 self.logger.warning( 582 f"指定された終了日{end_date}がデータの終了日{df.index.max():%Y-%m-%d}より後です。" 583 f"データの終了日を使用します。" 584 ) 585 end_dt = df.index.max() 586 else: 587 end_dt = df.index.max() 588 589 # 指定された期間のデータを抽出 590 mask = (df.index >= start_dt) & (df.index <= end_dt) 591 df = df[mask] 592 593 # 移動平均の計算(既存の関数を使用) 594 g2401_mean, g2401_lower, g2401_upper = calculate_rolling_stats( 595 df[col_g2401_flux], window_size, confidence_interval 596 ) 597 ultra_mean, ultra_lower, ultra_upper = calculate_rolling_stats( 598 df[col_ultra_flux], window_size, confidence_interval 599 ) 600 601 # プロットの作成 602 fig, ax = plt.subplots(figsize=figsize) 603 604 # G2401データのプロット 605 ax.plot(df.index, g2401_mean, "blue", label="G2401", alpha=0.7) 606 if show_ci: 607 ax.fill_between(df.index, g2401_lower, g2401_upper, color="blue", alpha=0.2) 608 609 # Ultraデータのプロット 610 ax.plot(df.index, ultra_mean, "red", label="Ultra", alpha=0.7) 611 if show_ci: 612 ax.fill_between(df.index, ultra_lower, ultra_upper, color="red", alpha=0.2) 613 614 # プロットの設定 615 if subplot_label: 616 ax.text( 617 0.02, 618 0.98, 619 subplot_label, 620 transform=ax.transAxes, 621 va="top", 622 fontsize=subplot_fontsize, 623 ) 624 625 ax.set_ylabel("CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)") 626 ax.set_xlabel("Month") 627 628 if y_lim is not None: 629 ax.set_ylim(y_lim) 630 631 ax.grid(True, alpha=0.3) 632 ax.legend(loc=legend_loc) 633 634 # x軸の設定 635 ax.set_xlim(start_dt, end_dt) 636 ax.xaxis.set_major_locator(mdates.MonthLocator(interval=1)) 637 638 # カスタムフォーマッタの作成(数字を通常フォントで表示) 639 def date_formatter(x, p): 640 date = mdates.num2date(x) 641 return f"{date.strftime('%m')}" 642 643 ax.xaxis.set_major_formatter(plt.FuncFormatter(date_formatter)) 644 ax.xaxis.set_minor_locator(mdates.MonthLocator()) 645 plt.setp(ax.xaxis.get_majorticklabels(), ha="right") 646 647 plt.tight_layout() 648 plt.savefig(output_path, dpi=300, bbox_inches="tight") 649 plt.close(fig)
G2401とUltraによるCH4フラックスの時系列比較プロット
Parameters:
df : pd.DataFrame
データフレーム
output_dir : str
出力ディレクトリのパス
col_g2401_flux : str
G2401のCH4フラックスのカラム名
col_ultra_flux : str
UltraのCH4フラックスのカラム名
output_filename : str
出力ファイル名
col_datetime : str
日時カラムの名前
window_size : int
移動平均の窓サイズ
confidence_interval : float
信頼区間(0-1)
subplot_label : str | None
プロットのラベル
subplot_fontsize : int
サブプロットのフォントサイズ
show_ci : bool
信頼区間を表示するか
y_lim : tuple[float, float] | None
y軸の範囲
start_date : str | None
開始日(YYYY-MM-DD形式)
end_date : str | None
終了日(YYYY-MM-DD形式)
figsize : tuple[float, float]
図のサイズ
legend_loc : str
凡例の位置
651 def plot_c1c2_fluxes_diurnal_patterns( 652 self, 653 df: pd.DataFrame, 654 y_cols_ch4: list[str], 655 y_cols_c2h6: list[str], 656 labels_ch4: list[str], 657 labels_c2h6: list[str], 658 colors_ch4: list[str], 659 colors_c2h6: list[str], 660 output_dir: str, 661 output_filename: str = "diurnal.png", 662 legend_only_ch4: bool = False, 663 show_label: bool = True, 664 show_legend: bool = True, 665 show_std: bool = False, # 標準偏差表示のオプションを追加 666 std_alpha: float = 0.2, # 標準偏差の透明度 667 subplot_fontsize: int = 20, 668 subplot_label_ch4: str | None = "(a)", 669 subplot_label_c2h6: str | None = "(b)", 670 ax1_ylim: tuple[float, float] | None = None, 671 ax2_ylim: tuple[float, float] | None = None, 672 ) -> None: 673 """CH4とC2H6の日変化パターンを1つの図に並べてプロットする 674 675 Parameters: 676 ------ 677 df : pd.DataFrame 678 入力データフレーム。 679 y_cols_ch4 : list[str] 680 CH4のプロットに使用するカラム名のリスト。 681 y_cols_c2h6 : list[str] 682 C2H6のプロットに使用するカラム名のリスト。 683 labels_ch4 : list[str] 684 CH4の各ラインに対応するラベルのリスト。 685 labels_c2h6 : list[str] 686 C2H6の各ラインに対応するラベルのリスト。 687 colors_ch4 : list[str] 688 CH4の各ラインに使用する色のリスト。 689 colors_c2h6 : list[str] 690 C2H6の各ラインに使用する色のリスト。 691 output_dir : str 692 出力先ディレクトリのパス。 693 output_filename : str, optional 694 出力ファイル名。デフォルトは"diurnal.png"。 695 legend_only_ch4 : bool, optional 696 CH4の凡例のみを表示するかどうか。デフォルトはFalse。 697 show_label : bool, optional 698 サブプロットラベルを表示するかどうか。デフォルトはTrue。 699 show_legend : bool, optional 700 凡例を表示するかどうか。デフォルトはTrue。 701 show_std : bool, optional 702 標準偏差を表示するかどうか。デフォルトはFalse。 703 std_alpha : float, optional 704 標準偏差の透明度。デフォルトは0.2。 705 subplot_fontsize : int, optional 706 サブプロットのフォントサイズ。デフォルトは20。 707 subplot_label_ch4 : str | None, optional 708 CH4プロットのラベル。デフォルトは"(a)"。 709 subplot_label_c2h6 : str | None, optional 710 C2H6プロットのラベル。デフォルトは"(b)"。 711 ax1_ylim : tuple[float, float] | None, optional 712 CH4プロットのy軸の範囲。デフォルトはNone。 713 ax2_ylim : tuple[float, float] | None, optional 714 C2H6プロットのy軸の範囲。デフォルトはNone。 715 """ 716 os.makedirs(output_dir, exist_ok=True) 717 output_path: str = os.path.join(output_dir, output_filename) 718 719 # データの準備 720 target_columns = y_cols_ch4 + y_cols_c2h6 721 hourly_means, time_points = self._prepare_diurnal_data(df, target_columns) 722 723 # 標準偏差の計算を追加 724 hourly_stds = {} 725 if show_std: 726 hourly_stds = df.groupby(df.index.hour)[target_columns].std() 727 # 24時間目のデータ点を追加 728 last_hour = hourly_stds.iloc[0:1].copy() 729 last_hour.index = [24] 730 hourly_stds = pd.concat([hourly_stds, last_hour]) 731 732 # プロットの作成 733 fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5)) 734 735 # CH4のプロット (左側) 736 ch4_lines = [] 737 for y_col, label, color in zip(y_cols_ch4, labels_ch4, colors_ch4): 738 mean_values = hourly_means["all"][y_col] 739 line = ax1.plot( 740 time_points, 741 mean_values, 742 "-o", 743 label=label, 744 color=color, 745 ) 746 ch4_lines.extend(line) 747 748 # 標準偏差の表示 749 if show_std: 750 std_values = hourly_stds[y_col] 751 ax1.fill_between( 752 time_points, 753 mean_values - std_values, 754 mean_values + std_values, 755 color=color, 756 alpha=std_alpha, 757 ) 758 759 # C2H6のプロット (右側) 760 c2h6_lines = [] 761 for y_col, label, color in zip(y_cols_c2h6, labels_c2h6, colors_c2h6): 762 mean_values = hourly_means["all"][y_col] 763 line = ax2.plot( 764 time_points, 765 mean_values, 766 "o-", 767 label=label, 768 color=color, 769 ) 770 c2h6_lines.extend(line) 771 772 # 標準偏差の表示 773 if show_std: 774 std_values = hourly_stds[y_col] 775 ax2.fill_between( 776 time_points, 777 mean_values - std_values, 778 mean_values + std_values, 779 color=color, 780 alpha=std_alpha, 781 ) 782 783 # 軸の設定 784 for ax, ylabel, subplot_label in [ 785 (ax1, r"CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)", subplot_label_ch4), 786 (ax2, r"C$_2$H$_6$ flux (nmol m$^{-2}$ s$^{-1}$)", subplot_label_c2h6), 787 ]: 788 self._setup_diurnal_axes( 789 ax=ax, 790 time_points=time_points, 791 ylabel=ylabel, 792 subplot_label=subplot_label, 793 show_label=show_label, 794 show_legend=False, # 個別の凡例は表示しない 795 subplot_fontsize=subplot_fontsize, 796 ) 797 798 if ax1_ylim is not None: 799 ax1.set_ylim(ax1_ylim) 800 ax1.yaxis.set_major_locator(MultipleLocator(20)) 801 ax1.yaxis.set_major_formatter(FuncFormatter(lambda x, p: f"{x:.0f}")) 802 803 if ax2_ylim is not None: 804 ax2.set_ylim(ax2_ylim) 805 ax2.yaxis.set_major_locator(MultipleLocator(1)) 806 ax2.yaxis.set_major_formatter(FuncFormatter(lambda x, p: f"{x:.1f}")) 807 808 plt.tight_layout() 809 810 # 共通の凡例 811 if show_legend: 812 all_lines = ch4_lines 813 all_labels = [line.get_label() for line in ch4_lines] 814 if not legend_only_ch4: 815 all_lines += c2h6_lines 816 all_labels += [line.get_label() for line in c2h6_lines] 817 fig.legend( 818 all_lines, 819 all_labels, 820 loc="center", 821 bbox_to_anchor=(0.5, 0.02), 822 ncol=len(all_lines), 823 ) 824 plt.subplots_adjust(bottom=0.25) # 下部に凡例用のスペースを確保 825 826 fig.savefig(output_path, dpi=300, bbox_inches="tight") 827 plt.close(fig)
CH4とC2H6の日変化パターンを1つの図に並べてプロットする
Parameters:
df : pd.DataFrame
入力データフレーム。
y_cols_ch4 : list[str]
CH4のプロットに使用するカラム名のリスト。
y_cols_c2h6 : list[str]
C2H6のプロットに使用するカラム名のリスト。
labels_ch4 : list[str]
CH4の各ラインに対応するラベルのリスト。
labels_c2h6 : list[str]
C2H6の各ラインに対応するラベルのリスト。
colors_ch4 : list[str]
CH4の各ラインに使用する色のリスト。
colors_c2h6 : list[str]
C2H6の各ラインに使用する色のリスト。
output_dir : str
出力先ディレクトリのパス。
output_filename : str, optional
出力ファイル名。デフォルトは"diurnal.png"。
legend_only_ch4 : bool, optional
CH4の凡例のみを表示するかどうか。デフォルトはFalse。
show_label : bool, optional
サブプロットラベルを表示するかどうか。デフォルトはTrue。
show_legend : bool, optional
凡例を表示するかどうか。デフォルトはTrue。
show_std : bool, optional
標準偏差を表示するかどうか。デフォルトはFalse。
std_alpha : float, optional
標準偏差の透明度。デフォルトは0.2。
subplot_fontsize : int, optional
サブプロットのフォントサイズ。デフォルトは20。
subplot_label_ch4 : str | None, optional
CH4プロットのラベル。デフォルトは"(a)"。
subplot_label_c2h6 : str | None, optional
C2H6プロットのラベル。デフォルトは"(b)"。
ax1_ylim : tuple[float, float] | None, optional
CH4プロットのy軸の範囲。デフォルトはNone。
ax2_ylim : tuple[float, float] | None, optional
C2H6プロットのy軸の範囲。デフォルトはNone。
829 def plot_c1c2_fluxes_diurnal_patterns_by_date( 830 self, 831 df: pd.DataFrame, 832 y_col_ch4: str, 833 y_col_c2h6: str, 834 output_dir: str, 835 output_filename: str = "diurnal_by_date.png", 836 plot_all: bool = True, 837 plot_weekday: bool = True, 838 plot_weekend: bool = True, 839 plot_holiday: bool = True, 840 show_label: bool = True, 841 show_legend: bool = True, 842 show_std: bool = False, # 標準偏差表示のオプションを追加 843 std_alpha: float = 0.2, # 標準偏差の透明度 844 legend_only_ch4: bool = False, 845 subplot_fontsize: int = 20, 846 subplot_label_ch4: str | None = "(a)", 847 subplot_label_c2h6: str | None = "(b)", 848 ax1_ylim: tuple[float, float] | None = None, 849 ax2_ylim: tuple[float, float] | None = None, 850 print_summary: bool = True, # 追加: 統計情報を表示するかどうか 851 ) -> None: 852 """CH4とC2H6の日変化パターンを日付分類して1つの図に並べてプロットする 853 854 Parameters: 855 ------ 856 df : pd.DataFrame 857 入力データフレーム。 858 y_col_ch4 : str 859 CH4フラックスを含むカラム名。 860 y_col_c2h6 : str 861 C2H6フラックスを含むカラム名。 862 output_dir : str 863 出力先ディレクトリのパス。 864 output_filename : str, optional 865 出力ファイル名。デフォルトは"diurnal_by_date.png"。 866 plot_all : bool, optional 867 すべての日をプロットするかどうか。デフォルトはTrue。 868 plot_weekday : bool, optional 869 平日をプロットするかどうか。デフォルトはTrue。 870 plot_weekend : bool, optional 871 週末をプロットするかどうか。デフォルトはTrue。 872 plot_holiday : bool, optional 873 祝日をプロットするかどうか。デフォルトはTrue。 874 show_label : bool, optional 875 サブプロットラベルを表示するかどうか。デフォルトはTrue。 876 show_legend : bool, optional 877 凡例を表示するかどうか。デフォルトはTrue。 878 show_std : bool, optional 879 標準偏差を表示するかどうか。デフォルトはFalse。 880 std_alpha : float, optional 881 標準偏差の透明度。デフォルトは0.2。 882 legend_only_ch4 : bool, optional 883 CH4の凡例のみを表示するかどうか。デフォルトはFalse。 884 subplot_fontsize : int, optional 885 サブプロットのフォントサイズ。デフォルトは20。 886 subplot_label_ch4 : str | None, optional 887 CH4プロットのラベル。デフォルトは"(a)"。 888 subplot_label_c2h6 : str | None, optional 889 C2H6プロットのラベル。デフォルトは"(b)"。 890 ax1_ylim : tuple[float, float] | None, optional 891 CH4プロットのy軸の範囲。デフォルトはNone。 892 ax2_ylim : tuple[float, float] | None, optional 893 C2H6プロットのy軸の範囲。デフォルトはNone。 894 print_summary : bool, optional 895 統計情報を表示するかどうか。デフォルトはTrue。 896 """ 897 os.makedirs(output_dir, exist_ok=True) 898 output_path: str = os.path.join(output_dir, output_filename) 899 900 # データの準備 901 target_columns = [y_col_ch4, y_col_c2h6] 902 hourly_means, time_points = self._prepare_diurnal_data( 903 df, target_columns, include_date_types=True 904 ) 905 906 # 標準偏差の計算を追加 907 hourly_stds = {} 908 if show_std: 909 for condition in ["all", "weekday", "weekend", "holiday"]: 910 if condition == "all": 911 condition_data = df 912 elif condition == "weekday": 913 condition_data = df[ 914 ~( 915 df.index.dayofweek.isin([5, 6]) 916 | df.index.map(lambda x: jpholiday.is_holiday(x.date())) 917 ) 918 ] 919 elif condition == "weekend": 920 condition_data = df[df.index.dayofweek.isin([5, 6])] 921 else: # holiday 922 condition_data = df[ 923 df.index.map(lambda x: jpholiday.is_holiday(x.date())) 924 ] 925 926 hourly_stds[condition] = condition_data.groupby( 927 condition_data.index.hour 928 )[target_columns].std() 929 # 24時間目のデータ点を追加 930 last_hour = hourly_stds[condition].iloc[0:1].copy() 931 last_hour.index = [24] 932 hourly_stds[condition] = pd.concat([hourly_stds[condition], last_hour]) 933 934 # プロットスタイルの設定 935 styles = { 936 "all": { 937 "color": "black", 938 "linestyle": "-", 939 "alpha": 1.0, 940 "label": "All days", 941 }, 942 "weekday": { 943 "color": "blue", 944 "linestyle": "-", 945 "alpha": 0.8, 946 "label": "Weekdays", 947 }, 948 "weekend": { 949 "color": "red", 950 "linestyle": "-", 951 "alpha": 0.8, 952 "label": "Weekends", 953 }, 954 "holiday": { 955 "color": "green", 956 "linestyle": "-", 957 "alpha": 0.8, 958 "label": "Weekends & Holidays", 959 }, 960 } 961 962 # プロット対象の条件を選択 963 plot_conditions = { 964 "all": plot_all, 965 "weekday": plot_weekday, 966 "weekend": plot_weekend, 967 "holiday": plot_holiday, 968 } 969 selected_conditions = { 970 col: means 971 for col, means in hourly_means.items() 972 if col in plot_conditions and plot_conditions[col] 973 } 974 975 # プロットの作成 976 fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5)) 977 978 # CH4とC2H6のプロット用のラインオブジェクトを保存 979 ch4_lines = [] 980 c2h6_lines = [] 981 982 # CH4とC2H6のプロット 983 for condition, means in selected_conditions.items(): 984 style = styles[condition].copy() 985 986 # CH4プロット 987 mean_values_ch4 = means[y_col_ch4] 988 line_ch4 = ax1.plot(time_points, mean_values_ch4, marker="o", **style) 989 ch4_lines.extend(line_ch4) 990 991 if show_std and condition in hourly_stds: 992 std_values = hourly_stds[condition][y_col_ch4] 993 ax1.fill_between( 994 time_points, 995 mean_values_ch4 - std_values, 996 mean_values_ch4 + std_values, 997 color=style["color"], 998 alpha=std_alpha, 999 ) 1000 1001 # C2H6プロット 1002 style["linestyle"] = "--" 1003 mean_values_c2h6 = means[y_col_c2h6] 1004 line_c2h6 = ax2.plot(time_points, mean_values_c2h6, marker="o", **style) 1005 c2h6_lines.extend(line_c2h6) 1006 1007 if show_std and condition in hourly_stds: 1008 std_values = hourly_stds[condition][y_col_c2h6] 1009 ax2.fill_between( 1010 time_points, 1011 mean_values_c2h6 - std_values, 1012 mean_values_c2h6 + std_values, 1013 color=style["color"], 1014 alpha=std_alpha, 1015 ) 1016 1017 # 軸の設定 1018 for ax, ylabel, subplot_label in [ 1019 (ax1, r"CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)", subplot_label_ch4), 1020 (ax2, r"C$_2$H$_6$ flux (nmol m$^{-2}$ s$^{-1}$)", subplot_label_c2h6), 1021 ]: 1022 self._setup_diurnal_axes( 1023 ax=ax, 1024 time_points=time_points, 1025 ylabel=ylabel, 1026 subplot_label=subplot_label, 1027 show_label=show_label, 1028 show_legend=False, 1029 subplot_fontsize=subplot_fontsize, 1030 ) 1031 1032 if ax1_ylim is not None: 1033 ax1.set_ylim(ax1_ylim) 1034 ax1.yaxis.set_major_locator(MultipleLocator(20)) 1035 ax1.yaxis.set_major_formatter(FuncFormatter(lambda x, p: f"{x:.0f}")) 1036 1037 if ax2_ylim is not None: 1038 ax2.set_ylim(ax2_ylim) 1039 ax2.yaxis.set_major_locator(MultipleLocator(1)) 1040 ax2.yaxis.set_major_formatter(FuncFormatter(lambda x, p: f"{x:.1f}")) 1041 1042 plt.tight_layout() 1043 1044 # 共通の凡例を図の下部に配置 1045 if show_legend: 1046 lines_to_show = ( 1047 ch4_lines if legend_only_ch4 else ch4_lines[: len(selected_conditions)] 1048 ) 1049 fig.legend( 1050 lines_to_show, 1051 [ 1052 style["label"] 1053 for style in list(styles.values())[: len(lines_to_show)] 1054 ], 1055 loc="center", 1056 bbox_to_anchor=(0.5, 0.02), 1057 ncol=len(lines_to_show), 1058 ) 1059 plt.subplots_adjust(bottom=0.25) # 下部に凡例用のスペースを確保 1060 1061 fig.savefig(output_path, dpi=300, bbox_inches="tight") 1062 plt.close(fig) 1063 1064 # 日変化パターンの統計分析を追加 1065 if print_summary: 1066 # 平日と休日のデータを準備 1067 dates = pd.to_datetime(df.index) 1068 is_weekend = dates.dayofweek.isin([5, 6]) 1069 is_holiday = dates.map(lambda x: jpholiday.is_holiday(x.date())) 1070 is_weekday = ~(is_weekend | is_holiday) 1071 1072 weekday_data = df[is_weekday] 1073 holiday_data = df[is_weekend | is_holiday] 1074 1075 def get_diurnal_stats(data, column): 1076 # 時間ごとの平均値を計算 1077 hourly_means = data.groupby(data.index.hour)[column].mean() 1078 1079 # 8-16時の時間帯の統計 1080 daytime_means = hourly_means[ 1081 (hourly_means.index >= 8) & (hourly_means.index <= 16) 1082 ] 1083 1084 if len(daytime_means) == 0: 1085 return None 1086 1087 return { 1088 "mean": daytime_means.mean(), 1089 "max": daytime_means.max(), 1090 "max_hour": daytime_means.idxmax(), 1091 "min": daytime_means.min(), 1092 "min_hour": daytime_means.idxmin(), 1093 "hours_count": len(daytime_means), 1094 } 1095 1096 # CH4とC2H6それぞれの統計を計算 1097 for col, gas_name in [(y_col_ch4, "CH4"), (y_col_c2h6, "C2H6")]: 1098 print(f"\n=== {gas_name} フラックス 8-16時の統計分析 ===") 1099 1100 weekday_stats = get_diurnal_stats(weekday_data, col) 1101 holiday_stats = get_diurnal_stats(holiday_data, col) 1102 1103 if weekday_stats and holiday_stats: 1104 print("\n平日:") 1105 print(f" 平均値: {weekday_stats['mean']:.2f}") 1106 print( 1107 f" 最大値: {weekday_stats['max']:.2f} ({weekday_stats['max_hour']}時)" 1108 ) 1109 print( 1110 f" 最小値: {weekday_stats['min']:.2f} ({weekday_stats['min_hour']}時)" 1111 ) 1112 print(f" 集計時間数: {weekday_stats['hours_count']}") 1113 1114 print("\n休日:") 1115 print(f" 平均値: {holiday_stats['mean']:.2f}") 1116 print( 1117 f" 最大値: {holiday_stats['max']:.2f} ({holiday_stats['max_hour']}時)" 1118 ) 1119 print( 1120 f" 最小値: {holiday_stats['min']:.2f} ({holiday_stats['min_hour']}時)" 1121 ) 1122 print(f" 集計時間数: {holiday_stats['hours_count']}") 1123 1124 # 平日/休日の比率を計算 1125 print("\n平日/休日の比率:") 1126 print( 1127 f" 平均値比: {weekday_stats['mean'] / holiday_stats['mean']:.2f}" 1128 ) 1129 print( 1130 f" 最大値比: {weekday_stats['max'] / holiday_stats['max']:.2f}" 1131 ) 1132 print( 1133 f" 最小値比: {weekday_stats['min'] / holiday_stats['min']:.2f}" 1134 ) 1135 else: 1136 print("十分なデータがありません")
CH4とC2H6の日変化パターンを日付分類して1つの図に並べてプロットする
Parameters:
df : pd.DataFrame
入力データフレーム。
y_col_ch4 : str
CH4フラックスを含むカラム名。
y_col_c2h6 : str
C2H6フラックスを含むカラム名。
output_dir : str
出力先ディレクトリのパス。
output_filename : str, optional
出力ファイル名。デフォルトは"diurnal_by_date.png"。
plot_all : bool, optional
すべての日をプロットするかどうか。デフォルトはTrue。
plot_weekday : bool, optional
平日をプロットするかどうか。デフォルトはTrue。
plot_weekend : bool, optional
週末をプロットするかどうか。デフォルトはTrue。
plot_holiday : bool, optional
祝日をプロットするかどうか。デフォルトはTrue。
show_label : bool, optional
サブプロットラベルを表示するかどうか。デフォルトはTrue。
show_legend : bool, optional
凡例を表示するかどうか。デフォルトはTrue。
show_std : bool, optional
標準偏差を表示するかどうか。デフォルトはFalse。
std_alpha : float, optional
標準偏差の透明度。デフォルトは0.2。
legend_only_ch4 : bool, optional
CH4の凡例のみを表示するかどうか。デフォルトはFalse。
subplot_fontsize : int, optional
サブプロットのフォントサイズ。デフォルトは20。
subplot_label_ch4 : str | None, optional
CH4プロットのラベル。デフォルトは"(a)"。
subplot_label_c2h6 : str | None, optional
C2H6プロットのラベル。デフォルトは"(b)"。
ax1_ylim : tuple[float, float] | None, optional
CH4プロットのy軸の範囲。デフォルトはNone。
ax2_ylim : tuple[float, float] | None, optional
C2H6プロットのy軸の範囲。デフォルトはNone。
print_summary : bool, optional
統計情報を表示するかどうか。デフォルトはTrue。
1138 def plot_diurnal_concentrations( 1139 self, 1140 df: pd.DataFrame, 1141 output_dir: str, 1142 col_ch4_conc: str = "CH4_ultra_cal", 1143 col_c2h6_conc: str = "C2H6_ultra_cal", 1144 col_datetime: str = "Date", 1145 output_filename: str = "diurnal_concentrations.png", 1146 show_std: bool = True, 1147 alpha_std: float = 0.2, 1148 add_legend: bool = True, # 凡例表示のオプションを追加 1149 print_summary: bool = True, 1150 subplot_label_ch4: str | None = None, 1151 subplot_label_c2h6: str | None = None, 1152 subplot_fontsize: int = 24, 1153 ch4_ylim: tuple[float, float] | None = None, 1154 c2h6_ylim: tuple[float, float] | None = None, 1155 interval: str = "1H", # "30min" または "1H" を指定 1156 ) -> None: 1157 """CH4とC2H6の濃度の日内変動を描画する 1158 1159 Parameters: 1160 ------ 1161 df : pd.DataFrame 1162 濃度データを含むDataFrame 1163 output_dir : str 1164 出力ディレクトリのパス 1165 col_ch4_conc : str 1166 CH4濃度のカラム名 1167 col_c2h6_conc : str 1168 C2H6濃度のカラム名 1169 col_datetime : str 1170 日時カラム名 1171 output_filename : str 1172 出力ファイル名 1173 show_std : bool 1174 標準偏差を表示するかどうか 1175 alpha_std : float 1176 標準偏差の透明度 1177 add_legend : bool 1178 凡例を追加するかどうか 1179 print_summary : bool 1180 統計情報を表示するかどうか 1181 subplot_label_ch4 : str | None 1182 CH4プロットのラベル 1183 subplot_label_c2h6 : str | None 1184 C2H6プロットのラベル 1185 subplot_fontsize : int 1186 サブプロットのフォントサイズ 1187 ch4_ylim : tuple[float, float] | None 1188 CH4のy軸範囲 1189 c2h6_ylim : tuple[float, float] | None 1190 C2H6のy軸範囲 1191 interval : str 1192 時間間隔。"30min"または"1H"を指定 1193 """ 1194 # 出力ディレクトリの作成 1195 os.makedirs(output_dir, exist_ok=True) 1196 output_path: str = os.path.join(output_dir, output_filename) 1197 1198 # データの準備 1199 df = df.copy() 1200 if interval == "30min": 1201 # 30分間隔の場合、時間と30分を別々に取得 1202 df["hour"] = pd.to_datetime(df[col_datetime]).dt.hour 1203 df["minute"] = pd.to_datetime(df[col_datetime]).dt.minute 1204 df["time_bin"] = df["hour"] + df["minute"].map({0: 0, 30: 0.5}) 1205 else: 1206 # 1時間間隔の場合 1207 df["time_bin"] = pd.to_datetime(df[col_datetime]).dt.hour 1208 1209 # 時間ごとの平均値と標準偏差を計算 1210 hourly_stats = df.groupby("time_bin")[[col_ch4_conc, col_c2h6_conc]].agg( 1211 ["mean", "std"] 1212 ) 1213 1214 # 最後のデータポイントを追加(最初のデータを使用) 1215 last_point = hourly_stats.iloc[0:1].copy() 1216 last_point.index = [ 1217 hourly_stats.index[-1] + (0.5 if interval == "30min" else 1) 1218 ] 1219 hourly_stats = pd.concat([hourly_stats, last_point]) 1220 1221 # 時間軸の作成 1222 if interval == "30min": 1223 time_points = pd.date_range("2024-01-01", periods=49, freq="30min") 1224 x_ticks = [0, 6, 12, 18, 24] # 主要な時間のティック 1225 else: 1226 time_points = pd.date_range("2024-01-01", periods=25, freq="1H") 1227 x_ticks = [0, 6, 12, 18, 24] 1228 1229 # プロットの作成 1230 fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5)) 1231 1232 # CH4濃度プロット 1233 mean_ch4 = hourly_stats[col_ch4_conc]["mean"] 1234 if show_std: 1235 std_ch4 = hourly_stats[col_ch4_conc]["std"] 1236 ax1.fill_between( 1237 time_points, 1238 mean_ch4 - std_ch4, 1239 mean_ch4 + std_ch4, 1240 color="red", 1241 alpha=alpha_std, 1242 ) 1243 ch4_line = ax1.plot(time_points, mean_ch4, "red", label="CH$_4$")[0] 1244 1245 ax1.set_ylabel("CH$_4$ (ppm)") 1246 if ch4_ylim is not None: 1247 ax1.set_ylim(ch4_ylim) 1248 if subplot_label_ch4: 1249 ax1.text( 1250 0.02, 1251 0.98, 1252 subplot_label_ch4, 1253 transform=ax1.transAxes, 1254 va="top", 1255 fontsize=subplot_fontsize, 1256 ) 1257 1258 # C2H6濃度プロット 1259 mean_c2h6 = hourly_stats[col_c2h6_conc]["mean"] 1260 if show_std: 1261 std_c2h6 = hourly_stats[col_c2h6_conc]["std"] 1262 ax2.fill_between( 1263 time_points, 1264 mean_c2h6 - std_c2h6, 1265 mean_c2h6 + std_c2h6, 1266 color="orange", 1267 alpha=alpha_std, 1268 ) 1269 c2h6_line = ax2.plot(time_points, mean_c2h6, "orange", label="C$_2$H$_6$")[0] 1270 1271 ax2.set_ylabel("C$_2$H$_6$ (ppb)") 1272 if c2h6_ylim is not None: 1273 ax2.set_ylim(c2h6_ylim) 1274 if subplot_label_c2h6: 1275 ax2.text( 1276 0.02, 1277 0.98, 1278 subplot_label_c2h6, 1279 transform=ax2.transAxes, 1280 va="top", 1281 fontsize=subplot_fontsize, 1282 ) 1283 1284 # 両プロットの共通設定 1285 for ax in [ax1, ax2]: 1286 ax.set_xlabel("Time (hour)") 1287 ax.xaxis.set_major_formatter(mdates.DateFormatter("%-H")) 1288 ax.xaxis.set_major_locator(mdates.HourLocator(byhour=x_ticks)) 1289 ax.set_xlim(time_points[0], time_points[-1]) 1290 # 1時間ごとの縦線を表示 1291 ax.grid(True, which="major", alpha=0.3) 1292 # 補助目盛りは表示するが、グリッド線は表示しない 1293 # if interval == "30min": 1294 # ax.xaxis.set_minor_locator(mdates.MinuteLocator(byminute=[30])) 1295 # ax.tick_params(which='minor', length=4) 1296 1297 # 共通の凡例を図の下部に配置 1298 if add_legend: 1299 fig.legend( 1300 [ch4_line, c2h6_line], 1301 ["CH$_4$", "C$_2$H$_6$"], 1302 loc="center", 1303 bbox_to_anchor=(0.5, 0.02), 1304 ncol=2, 1305 ) 1306 plt.subplots_adjust(bottom=0.2) 1307 1308 plt.tight_layout() 1309 plt.savefig(output_path, dpi=300, bbox_inches="tight") 1310 plt.close(fig) 1311 1312 if print_summary: 1313 # 統計情報の表示 1314 for name, col in [("CH4", col_ch4_conc), ("C2H6", col_c2h6_conc)]: 1315 stats = hourly_stats[col] 1316 mean_vals = stats["mean"] 1317 1318 print(f"\n{name}濃度の日内変動統計:") 1319 print(f"最小値: {mean_vals.min():.3f} (Hour: {mean_vals.idxmin()})") 1320 print(f"最大値: {mean_vals.max():.3f} (Hour: {mean_vals.idxmax()})") 1321 print(f"平均値: {mean_vals.mean():.3f}") 1322 print(f"日内変動幅: {mean_vals.max() - mean_vals.min():.3f}") 1323 print(f"最大/最小比: {mean_vals.max() / mean_vals.min():.3f}")
CH4とC2H6の濃度の日内変動を描画する
Parameters:
df : pd.DataFrame
濃度データを含むDataFrame
output_dir : str
出力ディレクトリのパス
col_ch4_conc : str
CH4濃度のカラム名
col_c2h6_conc : str
C2H6濃度のカラム名
col_datetime : str
日時カラム名
output_filename : str
出力ファイル名
show_std : bool
標準偏差を表示するかどうか
alpha_std : float
標準偏差の透明度
add_legend : bool
凡例を追加するかどうか
print_summary : bool
統計情報を表示するかどうか
subplot_label_ch4 : str | None
CH4プロットのラベル
subplot_label_c2h6 : str | None
C2H6プロットのラベル
subplot_fontsize : int
サブプロットのフォントサイズ
ch4_ylim : tuple[float, float] | None
CH4のy軸範囲
c2h6_ylim : tuple[float, float] | None
C2H6のy軸範囲
interval : str
時間間隔。"30min"または"1H"を指定
1325 def plot_flux_diurnal_patterns_with_std( 1326 self, 1327 df: pd.DataFrame, 1328 output_dir: str, 1329 col_ch4_flux: str = "Fch4", 1330 col_c2h6_flux: str = "Fc2h6", 1331 ch4_label: str = r"$\mathregular{CH_{4}}$フラックス", 1332 c2h6_label: str = r"$\mathregular{C_{2}H_{6}}$フラックス", 1333 col_datetime: str = "Date", 1334 output_filename: str = "diurnal_patterns.png", 1335 window_size: int = 6, # 移動平均の窓サイズ 1336 show_std: bool = True, # 標準偏差の表示有無 1337 alpha_std: float = 0.1, # 標準偏差の透明度 1338 ) -> None: 1339 """CH4とC2H6フラックスの日変化パターンをプロットする 1340 1341 Parameters: 1342 ------ 1343 df : pd.DataFrame 1344 データフレーム 1345 output_dir : str 1346 出力ディレクトリのパス 1347 col_ch4_flux : str 1348 CH4フラックスのカラム名 1349 col_c2h6_flux : str 1350 C2H6フラックスのカラム名 1351 ch4_label : str 1352 CH4フラックスのラベル 1353 c2h6_label : str 1354 C2H6フラックスのラベル 1355 col_datetime : str 1356 日時カラムの名前 1357 output_filename : str 1358 出力ファイル名 1359 window_size : int 1360 移動平均の窓サイズ(デフォルト6) 1361 show_std : bool 1362 標準偏差を表示するかどうか 1363 alpha_std : float 1364 標準偏差の透明度(0-1) 1365 """ 1366 # 出力ディレクトリの作成 1367 os.makedirs(output_dir, exist_ok=True) 1368 output_path: str = os.path.join(output_dir, output_filename) 1369 1370 # # プロットのスタイル設定 1371 # plt.rcParams.update({ 1372 # 'font.size': 20, 1373 # 'axes.labelsize': 20, 1374 # 'axes.titlesize': 20, 1375 # 'xtick.labelsize': 20, 1376 # 'ytick.labelsize': 20, 1377 # 'legend.fontsize': 20, 1378 # }) 1379 1380 # 日時インデックスの処理 1381 df = df.copy() 1382 if not isinstance(df.index, pd.DatetimeIndex): 1383 df[col_datetime] = pd.to_datetime(df[col_datetime]) 1384 df.set_index(col_datetime, inplace=True) 1385 1386 # 時刻データの抽出とグループ化 1387 df["hour"] = df.index.hour 1388 hourly_means = df.groupby("hour")[[col_ch4_flux, col_c2h6_flux]].agg( 1389 ["mean", "std"] 1390 ) 1391 1392 # 24時間目のデータ点を追加(0時のデータを使用) 1393 last_hour = hourly_means.iloc[0:1].copy() 1394 last_hour.index = [24] 1395 hourly_means = pd.concat([hourly_means, last_hour]) 1396 1397 # 24時間分のデータポイントを作成 1398 time_points = pd.date_range("2024-01-01", periods=25, freq="h") 1399 1400 # プロットの作成 1401 fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5)) 1402 1403 # 移動平均の計算と描画 1404 ch4_mean = ( 1405 hourly_means[(col_ch4_flux, "mean")] 1406 .rolling(window=window_size, center=True, min_periods=1) 1407 .mean() 1408 ) 1409 c2h6_mean = ( 1410 hourly_means[(col_c2h6_flux, "mean")] 1411 .rolling(window=window_size, center=True, min_periods=1) 1412 .mean() 1413 ) 1414 1415 if show_std: 1416 ch4_std = ( 1417 hourly_means[(col_ch4_flux, "std")] 1418 .rolling(window=window_size, center=True, min_periods=1) 1419 .mean() 1420 ) 1421 c2h6_std = ( 1422 hourly_means[(col_c2h6_flux, "std")] 1423 .rolling(window=window_size, center=True, min_periods=1) 1424 .mean() 1425 ) 1426 1427 ax1.fill_between( 1428 time_points, 1429 ch4_mean - ch4_std, 1430 ch4_mean + ch4_std, 1431 color="blue", 1432 alpha=alpha_std, 1433 ) 1434 ax2.fill_between( 1435 time_points, 1436 c2h6_mean - c2h6_std, 1437 c2h6_mean + c2h6_std, 1438 color="red", 1439 alpha=alpha_std, 1440 ) 1441 1442 # メインのラインプロット 1443 ax1.plot(time_points, ch4_mean, "blue", label=ch4_label) 1444 ax2.plot(time_points, c2h6_mean, "red", label=c2h6_label) 1445 1446 # 軸の設定 1447 for ax, ylabel in [ 1448 (ax1, r"CH$_4$ (nmol m$^{-2}$ s$^{-1}$)"), 1449 (ax2, r"C$_2$H$_6$ (nmol m$^{-2}$ s$^{-1}$)"), 1450 ]: 1451 ax.set_xlabel("Time") 1452 ax.set_ylabel(ylabel) 1453 ax.xaxis.set_major_formatter(mdates.DateFormatter("%-H")) 1454 ax.xaxis.set_major_locator(mdates.HourLocator(byhour=[0, 6, 12, 18, 24])) 1455 ax.set_xlim(time_points[0], time_points[-1]) 1456 ax.grid(True, alpha=0.3) 1457 ax.legend() 1458 1459 # グラフの保存 1460 plt.tight_layout() 1461 plt.savefig(output_path, dpi=300, bbox_inches="tight") 1462 plt.close() 1463 1464 # 統計情報の表示(オプション) 1465 for col, name in [(col_ch4_flux, "CH4"), (col_c2h6_flux, "C2H6")]: 1466 mean_val = hourly_means[(col, "mean")].mean() 1467 min_val = hourly_means[(col, "mean")].min() 1468 max_val = hourly_means[(col, "mean")].max() 1469 min_time = hourly_means[(col, "mean")].idxmin() 1470 max_time = hourly_means[(col, "mean")].idxmax() 1471 1472 self.logger.info(f"{name} Statistics:") 1473 self.logger.info(f"Mean: {mean_val:.2f}") 1474 self.logger.info(f"Min: {min_val:.2f} (Hour: {min_time})") 1475 self.logger.info(f"Max: {max_val:.2f} (Hour: {max_time})") 1476 self.logger.info(f"Max/Min ratio: {max_val / min_val:.2f}\n")
CH4とC2H6フラックスの日変化パターンをプロットする
Parameters:
df : pd.DataFrame
データフレーム
output_dir : str
出力ディレクトリのパス
col_ch4_flux : str
CH4フラックスのカラム名
col_c2h6_flux : str
C2H6フラックスのカラム名
ch4_label : str
CH4フラックスのラベル
c2h6_label : str
C2H6フラックスのラベル
col_datetime : str
日時カラムの名前
output_filename : str
出力ファイル名
window_size : int
移動平均の窓サイズ(デフォルト6)
show_std : bool
標準偏差を表示するかどうか
alpha_std : float
標準偏差の透明度(0-1)
1478 def plot_scatter( 1479 self, 1480 df: pd.DataFrame, 1481 x_col: str, 1482 y_col: str, 1483 output_dir: str, 1484 output_filename: str = "scatter.png", 1485 xlabel: str | None = None, 1486 ylabel: str | None = None, 1487 show_label: bool = True, 1488 x_axis_range: tuple | None = None, 1489 y_axis_range: tuple | None = None, 1490 fixed_slope: float = 0.076, 1491 show_fixed_slope: bool = False, 1492 x_scientific: bool = False, # 追加:x軸を指数表記にするかどうか 1493 y_scientific: bool = False, # 追加:y軸を指数表記にするかどうか 1494 ) -> None: 1495 """散布図を作成し、TLS回帰直線を描画します。 1496 1497 Parameters: 1498 ------ 1499 df : pd.DataFrame 1500 プロットに使用するデータフレーム 1501 x_col : str 1502 x軸に使用する列名 1503 y_col : str 1504 y軸に使用する列名 1505 xlabel : str 1506 x軸のラベル 1507 ylabel : str 1508 y軸のラベル 1509 output_dir : str 1510 出力先ディレクトリ 1511 output_filename : str, optional 1512 出力ファイル名。デフォルトは"scatter.png" 1513 show_label : bool, optional 1514 軸ラベルを表示するかどうか。デフォルトはTrue 1515 x_axis_range : tuple, optional 1516 x軸の範囲。デフォルトはNone。 1517 y_axis_range : tuple, optional 1518 y軸の範囲。デフォルトはNone。 1519 fixed_slope : float, optional 1520 固定傾きを指定するための値。デフォルトは0.076 1521 show_fixed_slope : bool, optional 1522 固定傾きの線を表示するかどうか。デフォルトはFalse 1523 """ 1524 os.makedirs(output_dir, exist_ok=True) 1525 output_path: str = os.path.join(output_dir, output_filename) 1526 1527 # 有効なデータの抽出 1528 df = MonthlyFiguresGenerator.get_valid_data(df, x_col, y_col) 1529 1530 # データの準備 1531 x = df[x_col].values 1532 y = df[y_col].values 1533 1534 # データの中心化 1535 x_mean = np.mean(x) 1536 y_mean = np.mean(y) 1537 x_c = x - x_mean 1538 y_c = y - y_mean 1539 1540 # TLS回帰の計算 1541 data_matrix = np.vstack((x_c, y_c)) 1542 cov_matrix = np.cov(data_matrix) 1543 _, eigenvecs = linalg.eigh(cov_matrix) 1544 largest_eigenvec = eigenvecs[:, -1] 1545 1546 slope = largest_eigenvec[1] / largest_eigenvec[0] 1547 intercept = y_mean - slope * x_mean 1548 1549 # R²とRMSEの計算 1550 y_pred = slope * x + intercept 1551 r_squared = 1 - np.sum((y - y_pred) ** 2) / np.sum((y - np.mean(y)) ** 2) 1552 rmse = np.sqrt(np.mean((y - y_pred) ** 2)) 1553 1554 # プロットの作成 1555 fig, ax = plt.subplots(figsize=(6, 6)) 1556 1557 # データ点のプロット 1558 ax.scatter(x, y, color="black") 1559 1560 # データの範囲を取得 1561 if x_axis_range is None: 1562 x_axis_range = (df[x_col].min(), df[x_col].max()) 1563 if y_axis_range is None: 1564 y_axis_range = (df[y_col].min(), df[y_col].max()) 1565 1566 # 回帰直線のプロット 1567 x_range = np.linspace(x_axis_range[0], x_axis_range[1], 150) 1568 y_range = slope * x_range + intercept 1569 ax.plot(x_range, y_range, "r", label="TLS regression") 1570 1571 # 傾き固定の線を追加(フラグがTrueの場合) 1572 if show_fixed_slope: 1573 fixed_intercept = ( 1574 y_mean - fixed_slope * x_mean 1575 ) # 中心点を通るように切片を計算 1576 y_fixed = fixed_slope * x_range + fixed_intercept 1577 ax.plot(x_range, y_fixed, "b--", label=f"Slope = {fixed_slope}", alpha=0.7) 1578 1579 # 軸の設定 1580 ax.set_xlim(x_axis_range) 1581 ax.set_ylim(y_axis_range) 1582 1583 # 指数表記の設定 1584 if x_scientific: 1585 ax.ticklabel_format(style="sci", axis="x", scilimits=(0, 0)) 1586 ax.xaxis.get_offset_text().set_position((1.1, 0)) # 指数の位置調整 1587 if y_scientific: 1588 ax.ticklabel_format(style="sci", axis="y", scilimits=(0, 0)) 1589 ax.yaxis.get_offset_text().set_position((0, 1.1)) # 指数の位置調整 1590 1591 if show_label: 1592 if xlabel is not None: 1593 ax.set_xlabel(xlabel) 1594 if ylabel is not None: 1595 ax.set_ylabel(ylabel) 1596 1597 # 1:1の関係を示す点線(軸の範囲が同じ場合のみ表示) 1598 if ( 1599 x_axis_range is not None 1600 and y_axis_range is not None 1601 and x_axis_range == y_axis_range 1602 ): 1603 ax.plot( 1604 [x_axis_range[0], x_axis_range[1]], 1605 [x_axis_range[0], x_axis_range[1]], 1606 "k--", 1607 alpha=0.5, 1608 ) 1609 1610 # 回帰情報の表示 1611 equation = ( 1612 f"y = {slope:.2f}x {'+' if intercept >= 0 else '-'} {abs(intercept):.2f}" 1613 ) 1614 position_x = 0.05 1615 fig_ha: str = "left" 1616 ax.text( 1617 position_x, 1618 0.95, 1619 equation, 1620 transform=ax.transAxes, 1621 va="top", 1622 ha=fig_ha, 1623 color="red", 1624 ) 1625 ax.text( 1626 position_x, 1627 0.88, 1628 f"R² = {r_squared:.2f}", 1629 transform=ax.transAxes, 1630 va="top", 1631 ha=fig_ha, 1632 color="red", 1633 ) 1634 ax.text( 1635 position_x, 1636 0.81, # RMSEのための新しい位置 1637 f"RMSE = {rmse:.2f}", 1638 transform=ax.transAxes, 1639 va="top", 1640 ha=fig_ha, 1641 color="red", 1642 ) 1643 # 目盛り線の設定 1644 ax.grid(True, alpha=0.3) 1645 1646 fig.savefig(output_path, dpi=300, bbox_inches="tight") 1647 plt.close(fig)
散布図を作成し、TLS回帰直線を描画します。
Parameters:
df : pd.DataFrame
プロットに使用するデータフレーム
x_col : str
x軸に使用する列名
y_col : str
y軸に使用する列名
xlabel : str
x軸のラベル
ylabel : str
y軸のラベル
output_dir : str
出力先ディレクトリ
output_filename : str, optional
出力ファイル名。デフォルトは"scatter.png"
show_label : bool, optional
軸ラベルを表示するかどうか。デフォルトはTrue
x_axis_range : tuple, optional
x軸の範囲。デフォルトはNone。
y_axis_range : tuple, optional
y軸の範囲。デフォルトはNone。
fixed_slope : float, optional
固定傾きを指定するための値。デフォルトは0.076
show_fixed_slope : bool, optional
固定傾きの線を表示するかどうか。デフォルトはFalse
1649 def plot_source_contributions_diurnal( 1650 self, 1651 df: pd.DataFrame, 1652 output_dir: str, 1653 col_ch4_flux: str, 1654 col_c2h6_flux: str, 1655 label_gas: str = "gas", 1656 label_bio: str = "bio", 1657 col_datetime: str = "Date", 1658 output_filename: str = "source_contributions.png", 1659 window_size: int = 6, # 移動平均の窓サイズ 1660 print_summary: bool = True, # 統計情報を表示するかどうか, 1661 show_legend: bool = False, 1662 smooth: bool = False, 1663 y_max: float = 100, # y軸の上限値を追加 1664 subplot_label: str | None = None, 1665 subplot_fontsize: int = 20, 1666 ) -> None: 1667 """CH4フラックスの都市ガス起源と生物起源の日変化を積み上げグラフとして表示 1668 1669 Parameters: 1670 ------ 1671 df : pd.DataFrame 1672 データフレーム 1673 output_dir : str 1674 出力ディレクトリのパス 1675 col_ch4_flux : str 1676 CH4フラックスのカラム名 1677 col_c2h6_flux : str 1678 C2H6フラックスのカラム名 1679 label_gas : str 1680 都市ガス起源のラベル 1681 label_bio : str 1682 生物起源のラベル 1683 col_datetime : str 1684 日時カラムの名前 1685 output_filename : str 1686 出力ファイル名 1687 window_size : int 1688 移動平均の窓サイズ 1689 print_summary : bool 1690 統計情報を表示するかどうか 1691 smooth : bool 1692 移動平均を適用するかどうか 1693 y_max : float 1694 y軸の上限値(デフォルト: 100) 1695 """ 1696 # 出力ディレクトリの作成 1697 os.makedirs(output_dir, exist_ok=True) 1698 output_path: str = os.path.join(output_dir, output_filename) 1699 1700 # 起源の計算 1701 df_with_sources = self._calculate_source_contributions( 1702 df=df, 1703 col_ch4_flux=col_ch4_flux, 1704 col_c2h6_flux=col_c2h6_flux, 1705 col_datetime=col_datetime, 1706 ) 1707 1708 # 時刻データの抽出とグループ化 1709 df_with_sources["hour"] = df_with_sources.index.hour 1710 hourly_means = df_with_sources.groupby("hour")[["ch4_gas", "ch4_bio"]].mean() 1711 1712 # 24時間目のデータ点を追加(0時のデータを使用) 1713 last_hour = hourly_means.iloc[0:1].copy() 1714 last_hour.index = [24] 1715 hourly_means = pd.concat([hourly_means, last_hour]) 1716 1717 # 移動平均の適用 1718 hourly_means_smoothed = hourly_means 1719 if smooth: 1720 hourly_means_smoothed = hourly_means.rolling( 1721 window=window_size, center=True, min_periods=1 1722 ).mean() 1723 1724 # 24時間分のデータポイントを作成 1725 time_points = pd.date_range("2024-01-01", periods=25, freq="h") 1726 1727 # プロットの作成 1728 plt.figure(figsize=(10, 6)) 1729 ax = plt.gca() 1730 1731 # サブプロットラベルの追加(subplot_labelが指定されている場合) 1732 if subplot_label: 1733 ax.text( 1734 0.02, # x位置 1735 0.98, # y位置 1736 subplot_label, 1737 transform=ax.transAxes, 1738 va="top", 1739 fontsize=subplot_fontsize, 1740 ) 1741 1742 # 積み上げプロット 1743 ax.fill_between( 1744 time_points, 1745 0, 1746 hourly_means_smoothed["ch4_bio"], 1747 color="blue", 1748 alpha=0.6, 1749 label=label_bio, 1750 ) 1751 ax.fill_between( 1752 time_points, 1753 hourly_means_smoothed["ch4_bio"], 1754 hourly_means_smoothed["ch4_bio"] + hourly_means_smoothed["ch4_gas"], 1755 color="red", 1756 alpha=0.6, 1757 label=label_gas, 1758 ) 1759 1760 # 合計値のライン 1761 total_flux = hourly_means_smoothed["ch4_bio"] + hourly_means_smoothed["ch4_gas"] 1762 ax.plot(time_points, total_flux, "-", color="black", alpha=0.5) 1763 1764 # 軸の設定 1765 ax.set_xlabel("Time (hour)") 1766 ax.set_ylabel(r"CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)") 1767 ax.xaxis.set_major_formatter(mdates.DateFormatter("%-H")) 1768 ax.xaxis.set_major_locator(mdates.HourLocator(byhour=[0, 6, 12, 18, 24])) 1769 ax.set_xlim(time_points[0], time_points[-1]) 1770 ax.set_ylim(0, y_max) # y軸の範囲を設定 1771 ax.grid(True, alpha=0.3) 1772 1773 # 凡例を図の下部に配置 1774 if show_legend: 1775 handles, labels = ax.get_legend_handles_labels() 1776 fig = plt.gcf() # 現在の図を取得 1777 fig.legend( 1778 handles, 1779 labels, 1780 loc="center", 1781 bbox_to_anchor=(0.5, 0.01), 1782 ncol=len(handles), 1783 ) 1784 plt.subplots_adjust(bottom=0.2) # 下部に凡例用のスペースを確保 1785 1786 # グラフの保存 1787 plt.tight_layout() 1788 plt.savefig(output_path, dpi=300, bbox_inches="tight") 1789 plt.close() 1790 1791 # 統計情報の表示 1792 if print_summary: 1793 stats = { 1794 "都市ガス起源": hourly_means["ch4_gas"], 1795 "生物起源": hourly_means["ch4_bio"], 1796 "合計": hourly_means["ch4_gas"] + hourly_means["ch4_bio"], 1797 } 1798 1799 for source, data in stats.items(): 1800 mean_val = data.mean() 1801 min_val = data.min() 1802 max_val = data.max() 1803 min_time = data.idxmin() 1804 max_time = data.idxmax() 1805 1806 self.logger.info(f"{source}の統計:") 1807 print(f" 平均値: {mean_val:.2f}") 1808 print(f" 最小値: {min_val:.2f} (Hour: {min_time})") 1809 print(f" 最大値: {max_val:.2f} (Hour: {max_time})") 1810 if min_val != 0: 1811 print(f" 最大/最小比: {max_val / min_val:.2f}")
CH4フラックスの都市ガス起源と生物起源の日変化を積み上げグラフとして表示
Parameters:
df : pd.DataFrame
データフレーム
output_dir : str
出力ディレクトリのパス
col_ch4_flux : str
CH4フラックスのカラム名
col_c2h6_flux : str
C2H6フラックスのカラム名
label_gas : str
都市ガス起源のラベル
label_bio : str
生物起源のラベル
col_datetime : str
日時カラムの名前
output_filename : str
出力ファイル名
window_size : int
移動平均の窓サイズ
print_summary : bool
統計情報を表示するかどうか
smooth : bool
移動平均を適用するかどうか
y_max : float
y軸の上限値(デフォルト: 100)
1813 def plot_source_contributions_diurnal_by_date( 1814 self, 1815 df: pd.DataFrame, 1816 output_dir: str, 1817 col_ch4_flux: str, 1818 col_c2h6_flux: str, 1819 label_gas: str = "gas", 1820 label_bio: str = "bio", 1821 col_datetime: str = "Date", 1822 output_filename: str = "source_contributions_by_date.png", 1823 show_label: bool = True, 1824 show_legend: bool = False, 1825 print_summary: bool = False, # 統計情報を表示するかどうか, 1826 subplot_fontsize: int = 20, 1827 subplot_label_weekday: str | None = None, 1828 subplot_label_weekend: str | None = None, 1829 y_max: float | None = None, # y軸の上限値 1830 ) -> None: 1831 """CH4フラックスの都市ガス起源と生物起源の日変化を平日・休日別に表示 1832 1833 Parameters: 1834 ------ 1835 df : pd.DataFrame 1836 データフレーム 1837 output_dir : str 1838 出力ディレクトリのパス 1839 col_ch4_flux : str 1840 CH4フラックスのカラム名 1841 col_c2h6_flux : str 1842 C2H6フラックスのカラム名 1843 label_gas : str 1844 都市ガス起源のラベル 1845 label_bio : str 1846 生物起源のラベル 1847 col_datetime : str 1848 日時カラムの名前 1849 output_filename : str 1850 出力ファイル名 1851 show_label : bool 1852 ラベルを表示するか 1853 show_legend : bool 1854 凡例を表示するか 1855 subplot_fontsize : int 1856 サブプロットのフォントサイズ 1857 subplot_label_weekday : str | None 1858 平日グラフのラベル 1859 subplot_label_weekend : str | None 1860 休日グラフのラベル 1861 y_max : float | None 1862 y軸の上限値 1863 """ 1864 # 出力ディレクトリの作成 1865 os.makedirs(output_dir, exist_ok=True) 1866 output_path: str = os.path.join(output_dir, output_filename) 1867 1868 # 起源の計算 1869 df_with_sources = self._calculate_source_contributions( 1870 df=df, 1871 col_ch4_flux=col_ch4_flux, 1872 col_c2h6_flux=col_c2h6_flux, 1873 col_datetime=col_datetime, 1874 ) 1875 1876 # 日付タイプの分類 1877 dates = pd.to_datetime(df_with_sources.index) 1878 is_weekend = dates.dayofweek.isin([5, 6]) 1879 is_holiday = dates.map(lambda x: jpholiday.is_holiday(x.date())) 1880 is_weekday = ~(is_weekend | is_holiday) 1881 1882 # データの分類 1883 data_weekday = df_with_sources[is_weekday] 1884 data_holiday = df_with_sources[is_weekend | is_holiday] 1885 1886 # プロットの作成 1887 fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5)) 1888 1889 # 平日と休日それぞれのプロット 1890 for ax, data, label in [ 1891 (ax1, data_weekday, "Weekdays"), 1892 (ax2, data_holiday, "Weekends & Holidays"), 1893 ]: 1894 # 時間ごとの平均値を計算 1895 hourly_means = data.groupby(data.index.hour)[["ch4_gas", "ch4_bio"]].mean() 1896 1897 # 24時間目のデータ点を追加 1898 last_hour = hourly_means.iloc[0:1].copy() 1899 last_hour.index = [24] 1900 hourly_means = pd.concat([hourly_means, last_hour]) 1901 1902 # 24時間分のデータポイントを作成 1903 time_points = pd.date_range("2024-01-01", periods=25, freq="h") 1904 1905 # 積み上げプロット 1906 ax.fill_between( 1907 time_points, 1908 0, 1909 hourly_means["ch4_bio"], 1910 color="blue", 1911 alpha=0.6, 1912 label=label_bio, 1913 ) 1914 ax.fill_between( 1915 time_points, 1916 hourly_means["ch4_bio"], 1917 hourly_means["ch4_bio"] + hourly_means["ch4_gas"], 1918 color="red", 1919 alpha=0.6, 1920 label=label_gas, 1921 ) 1922 1923 # 合計値のライン 1924 total_flux = hourly_means["ch4_bio"] + hourly_means["ch4_gas"] 1925 ax.plot(time_points, total_flux, "-", color="black", alpha=0.5) 1926 1927 # 軸の設定 1928 if show_label: 1929 ax.set_xlabel("Time (hour)") 1930 if ax == ax1: # 左側のプロットのラベル 1931 ax.set_ylabel("Weekdays CH$_4$ flux\n" r"(nmol m$^{-2}$ s$^{-1}$)") 1932 else: # 右側のプロットのラベル 1933 ax.set_ylabel("Weekends CH$_4$ flux\n" r"(nmol m$^{-2}$ s$^{-1}$)") 1934 1935 ax.xaxis.set_major_formatter(mdates.DateFormatter("%-H")) 1936 ax.xaxis.set_major_locator(mdates.HourLocator(byhour=[0, 6, 12, 18, 24])) 1937 ax.set_xlim(time_points[0], time_points[-1]) 1938 if y_max is not None: 1939 ax.set_ylim(0, y_max) 1940 ax.grid(True, alpha=0.3) 1941 1942 # サブプロットラベルの追加 1943 if subplot_label_weekday: 1944 ax1.text( 1945 0.02, 1946 0.98, 1947 subplot_label_weekday, 1948 transform=ax1.transAxes, 1949 va="top", 1950 fontsize=subplot_fontsize, 1951 ) 1952 if subplot_label_weekend: 1953 ax2.text( 1954 0.02, 1955 0.98, 1956 subplot_label_weekend, 1957 transform=ax2.transAxes, 1958 va="top", 1959 fontsize=subplot_fontsize, 1960 ) 1961 1962 # 凡例を図の下部に配置 1963 if show_legend: 1964 # 最初のプロットから凡例のハンドルとラベルを取得 1965 handles, labels = ax1.get_legend_handles_labels() 1966 # 図の下部に凡例を配置 1967 fig.legend( 1968 handles, 1969 labels, 1970 loc="center", 1971 bbox_to_anchor=(0.5, 0.01), # x=0.5で中央、y=0.01で下部に配置 1972 ncol=len(handles), # ハンドルの数だけ列を作成(一行に表示) 1973 ) 1974 # 凡例用のスペースを確保 1975 plt.subplots_adjust(bottom=0.2) # 下部に30%のスペースを確保 1976 1977 plt.tight_layout() 1978 plt.savefig(output_path, dpi=300, bbox_inches="tight") 1979 plt.close() 1980 1981 # 統計情報の表示 1982 if print_summary: 1983 for data, label in [ 1984 (data_weekday, "Weekdays"), 1985 (data_holiday, "Weekends & Holidays"), 1986 ]: 1987 hourly_means = data.groupby(data.index.hour)[ 1988 ["ch4_gas", "ch4_bio"] 1989 ].mean() 1990 total_flux = hourly_means["ch4_gas"] + hourly_means["ch4_bio"] 1991 1992 print(f"\n{label}の統計:") 1993 print(f" 平均値: {total_flux.mean():.2f}") 1994 print(f" 最小値: {total_flux.min():.2f} (Hour: {total_flux.idxmin()})") 1995 print(f" 最大値: {total_flux.max():.2f} (Hour: {total_flux.idxmax()})") 1996 if total_flux.min() != 0: 1997 print(f" 最大/最小比: {total_flux.max() / total_flux.min():.2f}")
CH4フラックスの都市ガス起源と生物起源の日変化を平日・休日別に表示
Parameters:
df : pd.DataFrame
データフレーム
output_dir : str
出力ディレクトリのパス
col_ch4_flux : str
CH4フラックスのカラム名
col_c2h6_flux : str
C2H6フラックスのカラム名
label_gas : str
都市ガス起源のラベル
label_bio : str
生物起源のラベル
col_datetime : str
日時カラムの名前
output_filename : str
出力ファイル名
show_label : bool
ラベルを表示するか
show_legend : bool
凡例を表示するか
subplot_fontsize : int
サブプロットのフォントサイズ
subplot_label_weekday : str | None
平日グラフのラベル
subplot_label_weekend : str | None
休日グラフのラベル
y_max : float | None
y軸の上限値
1999 def plot_spectra( 2000 self, 2001 input_dir: str, 2002 output_dir: str, 2003 fs: float, 2004 lag_second: float, 2005 col_ch4: str = "Ultra_CH4_ppm_C", 2006 col_c2h6: str = "Ultra_C2H6_ppb", 2007 label_ch4: str | None = None, 2008 label_c2h6: str | None = None, 2009 are_inputs_resampled: bool = True, 2010 file_pattern: str = "*.csv", 2011 output_basename: str = "spectrum", 2012 plot_power: bool = True, 2013 plot_co: bool = True, 2014 markersize: float = 14, 2015 ) -> None: 2016 """ 2017 月間の平均パワースペクトル密度を計算してプロットする。 2018 2019 データファイルを指定されたディレクトリから読み込み、パワースペクトル密度を計算し、 2020 結果を指定された出力ディレクトリにプロットして保存します。 2021 2022 Parameters: 2023 ------ 2024 input_dir : str 2025 データファイルが格納されているディレクトリ。 2026 output_dir : str 2027 出力先ディレクトリ。 2028 fs : float 2029 サンプリング周波数。 2030 lag_second : float 2031 ラグ時間(秒)。 2032 col_ch4 : str, optional 2033 CH4の濃度データが入ったカラムのキー。デフォルトは"Ultra_CH4_ppm_C"。 2034 col_c2h6 : str, optional 2035 C2H6の濃度データが入ったカラムのキー。デフォルトは"Ultra_C2H6_ppb"。 2036 are_inputs_resampled : bool, optional 2037 入力データが再サンプリングされているかどうか。デフォルトはTrue。 2038 file_pattern : str, optional 2039 処理対象のファイルパターン。デフォルトは"*.csv"。 2040 output_basename : str, optional 2041 出力ファイル名。デフォルトは"spectrum"。 2042 """ 2043 # データの読み込みと結合 2044 edp = EddyDataPreprocessor() 2045 2046 # 各変数のパワースペクトルを格納する辞書 2047 power_spectra = {col_ch4: [], col_c2h6: []} 2048 co_spectra = {col_ch4: [], col_c2h6: []} 2049 freqs = None 2050 2051 # プログレスバーを表示しながらファイルを処理 2052 file_list = glob.glob(os.path.join(input_dir, file_pattern)) 2053 for filepath in tqdm(file_list, desc="Processing files"): 2054 df, _ = edp.get_resampled_df( 2055 filepath=filepath, is_already_resampled=are_inputs_resampled 2056 ) 2057 2058 # 風速成分の計算を追加 2059 df = edp.add_uvw_columns(df) 2060 2061 # NaNや無限大を含む行を削除 2062 df = df.replace([np.inf, -np.inf], np.nan).dropna( 2063 subset=[col_ch4, col_c2h6, "wind_w"] 2064 ) 2065 2066 # データが十分な行数を持っているか確認 2067 if len(df) < 100: 2068 continue 2069 2070 # 各ファイルごとにスペクトル計算 2071 calculator = SpectrumCalculator( 2072 df=df, 2073 fs=fs, 2074 cols_apply_lag_time=[col_ch4, col_c2h6], 2075 lag_second=lag_second, 2076 ) 2077 2078 # 各変数のパワースペクトルを計算して保存 2079 for col in power_spectra.keys(): 2080 f, ps = calculator.calculate_power_spectrum( 2081 col=col, 2082 dimensionless=True, 2083 frequency_weighted=True, 2084 interpolate_points=True, 2085 scaling="density", 2086 ) 2087 # 最初のファイル処理時にfreqsを初期化 2088 if freqs is None: 2089 freqs = f 2090 power_spectra[col].append(ps) 2091 # 以降は周波数配列の長さが一致する場合のみ追加 2092 elif len(f) == len(freqs): 2093 power_spectra[col].append(ps) 2094 2095 # コスペクトル 2096 _, cs, _ = calculator.calculate_co_spectrum( 2097 col1="wind_w", 2098 col2=col, 2099 dimensionless=True, 2100 frequency_weighted=True, 2101 interpolate_points=True, 2102 # scaling="density", 2103 scaling="spectrum", 2104 ) 2105 if freqs is not None and len(cs) == len(freqs): 2106 co_spectra[col].append(cs) 2107 2108 # 各変数のスペクトルを平均化 2109 averaged_power_spectra = { 2110 col: np.mean(spectra, axis=0) for col, spectra in power_spectra.items() 2111 } 2112 averaged_co_spectra = { 2113 col: np.mean(spectra, axis=0) for col, spectra in co_spectra.items() 2114 } 2115 2116 # # プロット設定 2117 # plt.rcParams.update( 2118 # { 2119 # "font.size": 20, 2120 # "axes.labelsize": 20, 2121 # "axes.titlesize": 20, 2122 # "xtick.labelsize": 20, 2123 # "ytick.labelsize": 20, 2124 # "legend.fontsize": 20, 2125 # } 2126 # ) 2127 2128 # プロット設定を修正 2129 plot_configs = [ 2130 { 2131 "col": col_ch4, 2132 "psd_ylabel": r"$fS_{\mathrm{CH_4}} / s_{\mathrm{CH_4}}^2$", 2133 "co_ylabel": r"$fCo_{w\mathrm{CH_4}} / (\sigma_w \sigma_{\mathrm{CH_4}})$", 2134 "color": "red", 2135 "label": label_ch4, 2136 }, 2137 { 2138 "col": col_c2h6, 2139 "psd_ylabel": r"$fS_{\mathrm{C_2H_6}} / s_{\mathrm{C_2H_6}}^2$", 2140 "co_ylabel": r"$fCo_{w\mathrm{C_2H_6}} / (\sigma_w \sigma_{\mathrm{C_2H_6}})$", 2141 "color": "orange", 2142 "label": label_c2h6, 2143 }, 2144 ] 2145 2146 # # パワースペクトルの図を作成 2147 # if plot_power: 2148 # _, axes_psd = plt.subplots(1, 2, figsize=(12, 5), sharex=True) 2149 # for ax, config in zip(axes_psd, plot_configs): 2150 # ax.scatter( 2151 # freqs, 2152 # averaged_power_spectra[config["col"]], 2153 # c=config["color"], 2154 # s=100, 2155 # ) 2156 # ax.set_xscale("log") 2157 # ax.set_yscale("log") 2158 # ax.set_xlim(0.001, 10) 2159 # ax.plot([0.01, 10], [1, 0.01], "-", color="black", alpha=0.5) 2160 # ax.text(0.1, 0.06, "-2/3", fontsize=18) 2161 # ax.set_ylabel(config["psd_ylabel"]) 2162 # if config["label"] is not None: 2163 # ax.text( 2164 # 0.02, 0.98, config["label"], transform=ax.transAxes, va="top" 2165 # ) 2166 # ax.grid(True, alpha=0.3) 2167 # ax.set_xlabel("f (Hz)") 2168 2169 # パワースペクトルの図を作成 2170 if plot_power: 2171 _, axes_psd = plt.subplots(1, 2, figsize=(12, 5), sharex=True) 2172 for ax, config in zip(axes_psd, plot_configs): 2173 ax.plot( 2174 freqs, 2175 averaged_power_spectra[config["col"]], 2176 "o", # マーカーを丸に設定 2177 color=config["color"], 2178 markersize=markersize, 2179 ) 2180 ax.set_xscale("log") 2181 ax.set_yscale("log") 2182 ax.set_xlim(0.001, 10) 2183 ax.plot([0.01, 10], [1, 0.01], "-", color="black", alpha=0.5) 2184 ax.text(0.1, 0.06, "-2/3", fontsize=18) 2185 ax.set_ylabel(config["psd_ylabel"]) 2186 if config["label"] is not None: 2187 ax.text( 2188 0.02, 0.98, config["label"], transform=ax.transAxes, va="top" 2189 ) 2190 ax.grid(True, alpha=0.3) 2191 ax.set_xlabel("f (Hz)") 2192 2193 plt.tight_layout() 2194 os.makedirs(output_dir, exist_ok=True) 2195 output_path_psd: str = os.path.join( 2196 output_dir, f"power_{output_basename}.png" 2197 ) 2198 plt.savefig( 2199 output_path_psd, 2200 dpi=300, 2201 bbox_inches="tight", 2202 ) 2203 plt.close() 2204 2205 # # コスペクトルの図を作成 2206 # if plot_co: 2207 # _, axes_cosp = plt.subplots(1, 2, figsize=(12, 5), sharex=True) 2208 # for ax, config in zip(axes_cosp, plot_configs): 2209 # ax.scatter( 2210 # freqs, 2211 # averaged_co_spectra[config["col"]], 2212 # c=config["color"], 2213 # s=100, 2214 # ) 2215 # ax.set_xscale("log") 2216 # ax.set_yscale("log") 2217 # ax.set_xlim(0.001, 10) 2218 # ax.plot([0.01, 10], [1, 0.01], "-", color="black", alpha=0.5) 2219 # ax.text(0.1, 0.1, "-4/3", fontsize=18) 2220 # ax.set_ylabel(config["co_ylabel"]) 2221 # if config["label"] is not None: 2222 # ax.text( 2223 # 0.02, 0.98, config["label"], transform=ax.transAxes, va="top" 2224 # ) 2225 # ax.grid(True, alpha=0.3) 2226 # ax.set_xlabel("f (Hz)") 2227 2228 # コスペクトルの図を作成 2229 if plot_co: 2230 _, axes_cosp = plt.subplots(1, 2, figsize=(12, 5), sharex=True) 2231 for ax, config in zip(axes_cosp, plot_configs): 2232 ax.plot( 2233 freqs, 2234 averaged_co_spectra[config["col"]], 2235 "o", # マーカーを丸に設定 2236 color=config["color"], 2237 markersize=markersize, 2238 ) 2239 ax.set_xscale("log") 2240 ax.set_yscale("log") 2241 ax.set_xlim(0.001, 10) 2242 ax.plot([0.01, 10], [1, 0.01], "-", color="black", alpha=0.5) 2243 ax.text(0.1, 0.1, "-4/3", fontsize=18) 2244 ax.set_ylabel(config["co_ylabel"]) 2245 if config["label"] is not None: 2246 ax.text( 2247 0.02, 0.98, config["label"], transform=ax.transAxes, va="top" 2248 ) 2249 ax.grid(True, alpha=0.3) 2250 ax.set_xlabel("f (Hz)") 2251 2252 plt.tight_layout() 2253 output_path_csd: str = os.path.join(output_dir, f"co_{output_basename}.png") 2254 plt.savefig( 2255 output_path_csd, 2256 dpi=300, 2257 bbox_inches="tight", 2258 ) 2259 plt.close()
月間の平均パワースペクトル密度を計算してプロットする。
データファイルを指定されたディレクトリから読み込み、パワースペクトル密度を計算し、 結果を指定された出力ディレクトリにプロットして保存します。
Parameters:
input_dir : str
データファイルが格納されているディレクトリ。
output_dir : str
出力先ディレクトリ。
fs : float
サンプリング周波数。
lag_second : float
ラグ時間(秒)。
col_ch4 : str, optional
CH4の濃度データが入ったカラムのキー。デフォルトは"Ultra_CH4_ppm_C"。
col_c2h6 : str, optional
C2H6の濃度データが入ったカラムのキー。デフォルトは"Ultra_C2H6_ppb"。
are_inputs_resampled : bool, optional
入力データが再サンプリングされているかどうか。デフォルトはTrue。
file_pattern : str, optional
処理対象のファイルパターン。デフォルトは"*.csv"。
output_basename : str, optional
出力ファイル名。デフォルトは"spectrum"。
2261 def plot_turbulence( 2262 self, 2263 df: pd.DataFrame, 2264 output_dir: str, 2265 output_filename: str = "turbulence.png", 2266 col_uz: str = "Uz", 2267 col_ch4: str = "Ultra_CH4_ppm_C", 2268 col_c2h6: str = "Ultra_C2H6_ppb", 2269 col_timestamp: str = "TIMESTAMP", 2270 add_serial_labels: bool = True, 2271 ) -> None: 2272 """時系列データのプロットを作成する 2273 2274 Parameters: 2275 ------ 2276 df : pd.DataFrame 2277 プロットするデータを含むDataFrame 2278 output_dir : str 2279 出力ディレクトリのパス 2280 output_filename : str 2281 出力ファイル名 2282 col_uz : str 2283 鉛直風速データのカラム名 2284 col_ch4 : str 2285 メタンデータのカラム名 2286 col_c2h6 : str 2287 エタンデータのカラム名 2288 col_timestamp : str 2289 タイムスタンプのカラム名 2290 """ 2291 # 出力ディレクトリの作成 2292 os.makedirs(output_dir, exist_ok=True) 2293 output_path: str = os.path.join(output_dir, output_filename) 2294 2295 # データの前処理 2296 df = df.copy() 2297 2298 # タイムスタンプをインデックスに設定(まだ設定されていない場合) 2299 if not isinstance(df.index, pd.DatetimeIndex): 2300 df[col_timestamp] = pd.to_datetime(df[col_timestamp]) 2301 df.set_index(col_timestamp, inplace=True) 2302 2303 # 開始時刻と終了時刻を取得 2304 start_time = df.index[0] 2305 end_time = df.index[-1] 2306 2307 # 開始時刻の分を取得 2308 start_minute = start_time.minute 2309 2310 # 時間軸の作成(実際の開始時刻からの経過分数) 2311 minutes_elapsed = (df.index - start_time).total_seconds() / 60 2312 2313 # プロットの作成 2314 _, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=(12, 10), sharex=True) 2315 2316 # 鉛直風速 2317 ax1.plot(minutes_elapsed, df[col_uz], "k-", linewidth=0.5) 2318 ax1.set_ylabel(r"$w$ (m s$^{-1}$)") 2319 if add_serial_labels: 2320 ax1.text(0.02, 0.98, "(a)", transform=ax1.transAxes, va="top") 2321 ax1.grid(True, alpha=0.3) 2322 2323 # CH4濃度 2324 ax2.plot(minutes_elapsed, df[col_ch4], "r-", linewidth=0.5) 2325 ax2.set_ylabel(r"$\mathrm{CH_4}$ (ppm)") 2326 if add_serial_labels: 2327 ax2.text(0.02, 0.98, "(b)", transform=ax2.transAxes, va="top") 2328 ax2.grid(True, alpha=0.3) 2329 2330 # C2H6濃度 2331 ax3.plot(minutes_elapsed, df[col_c2h6], "orange", linewidth=0.5) 2332 ax3.set_ylabel(r"$\mathrm{C_2H_6}$ (ppb)") 2333 if add_serial_labels: 2334 ax3.text(0.02, 0.98, "(c)", transform=ax3.transAxes, va="top") 2335 ax3.grid(True, alpha=0.3) 2336 ax3.set_xlabel("Time (minutes)") 2337 2338 # x軸の範囲を実際の開始時刻から30分後までに設定 2339 total_minutes = (end_time - start_time).total_seconds() / 60 2340 ax3.set_xlim(0, min(30, total_minutes)) 2341 2342 # x軸の目盛りを5分間隔で設定 2343 np.arange(start_minute, start_minute + 35, 5) 2344 ax3.xaxis.set_major_locator(MultipleLocator(5)) 2345 2346 # レイアウトの調整 2347 plt.tight_layout() 2348 2349 # 図の保存 2350 plt.savefig(output_path, dpi=300, bbox_inches="tight") 2351 plt.close()
時系列データのプロットを作成する
Parameters:
df : pd.DataFrame
プロットするデータを含むDataFrame
output_dir : str
出力ディレクトリのパス
output_filename : str
出力ファイル名
col_uz : str
鉛直風速データのカラム名
col_ch4 : str
メタンデータのカラム名
col_c2h6 : str
エタンデータのカラム名
col_timestamp : str
タイムスタンプのカラム名
2353 def plot_wind_rose_sources( 2354 self, 2355 df: pd.DataFrame, 2356 output_dir: str | Path | None = None, 2357 output_filename: str = "wind_rose.png", 2358 col_datetime: str = "Date", 2359 col_ch4_flux: str = "Fch4", 2360 col_c2h6_flux: str = "Fc2h6", 2361 col_wind_dir: str = "Wind direction", 2362 flux_unit: str = r"(nmol m$^{-2}$ s$^{-1}$)", 2363 ymax: float | None = None, # フラックスの上限値 2364 label_gas: str = "都市ガス起源", 2365 label_bio: str = "生物起源", 2366 figsize: tuple[float, float] = (8, 8), 2367 flux_alpha: float = 0.4, 2368 num_directions: int = 8, # 方位の数(8方位) 2369 center_on_angles: bool = True, # 追加:45度刻みの線を境界にするかどうか 2370 subplot_label: str | None = None, 2371 add_legend: bool = True, 2372 print_summary: bool = True, # 統計情報を表示するかどうか 2373 save_fig: bool = True, 2374 show_fig: bool = True, 2375 ) -> None: 2376 """CH4フラックスの都市ガス起源と生物起源の風配図を作成する関数 2377 2378 Parameters: 2379 ------ 2380 df : pd.DataFrame 2381 風配図を作成するためのデータフレーム 2382 output_dir : str | Path | None 2383 生成された図を保存するディレクトリのパス 2384 output_filename : str 2385 保存するファイル名(デフォルトは"wind_rose.png") 2386 col_ch4_flux : str 2387 CH4フラックスを示すカラム名 2388 col_c2h6_flux : str 2389 C2H6フラックスを示すカラム名 2390 col_wind_dir : str 2391 風向を示すカラム名 2392 label_gas : str 2393 都市ガス起源のフラックスに対するラベル 2394 label_bio : str 2395 生物起源のフラックスに対するラベル 2396 col_datetime : str 2397 日時を示すカラム名 2398 num_directions : int 2399 風向の数(デフォルトは8) 2400 center_on_angles: bool 2401 Trueの場合、45度刻みの線を境界として扇形を描画します。 2402 Falseの場合、45度の中間(22.5度)を中心として扇形を描画します。 2403 subplot_label : str 2404 サブプロットに表示するラベル 2405 print_summary : bool 2406 統計情報を表示するかどうかのフラグ 2407 flux_unit : str 2408 フラックスの単位 2409 ymax : float | None 2410 y軸の上限値(指定しない場合はデータの最大値に基づいて自動設定) 2411 figsize : tuple[float, float] 2412 図のサイズ 2413 flux_alpha : float 2414 フラックスの透明度 2415 save_fig : bool 2416 図を保存するかどうかのフラグ 2417 show_fig : bool 2418 図を表示するかどうかのフラグ 2419 """ 2420 # 起源の計算 2421 df_with_sources = self._calculate_source_contributions( 2422 df=df, 2423 col_ch4_flux=col_ch4_flux, 2424 col_c2h6_flux=col_c2h6_flux, 2425 col_datetime=col_datetime, 2426 ) 2427 2428 # 方位の定義 2429 direction_ranges = self._define_direction_ranges( 2430 num_directions, center_on_angles 2431 ) 2432 2433 # 方位ごとのデータを集計 2434 direction_data = self._aggregate_direction_data( 2435 df_with_sources, col_wind_dir, direction_ranges 2436 ) 2437 2438 # プロットの作成 2439 fig = plt.figure(figsize=figsize) 2440 ax = fig.add_subplot(111, projection="polar") 2441 2442 # 方位の角度(ラジアン)を計算 2443 theta = np.array( 2444 [np.radians(angle) for angle in direction_data["center_angle"]] 2445 ) 2446 2447 # 生物起源と都市ガス起源を独立してプロット 2448 ax.bar( 2449 theta, 2450 direction_data["bio_flux"], 2451 width=np.radians(360 / num_directions), 2452 bottom=0.0, 2453 color="blue", 2454 alpha=flux_alpha, 2455 label=label_bio, 2456 ) 2457 2458 ax.bar( 2459 theta, 2460 direction_data["gas_flux"], 2461 width=np.radians(360 / num_directions), 2462 bottom=0.0, 2463 color="red", 2464 alpha=flux_alpha, 2465 label=label_gas, 2466 ) 2467 2468 # y軸の範囲を設定 2469 if ymax is not None: 2470 ax.set_ylim(0, ymax) 2471 else: 2472 # データの最大値に基づいて自動設定 2473 max_value = max( 2474 direction_data["bio_flux"].max(), direction_data["gas_flux"].max() 2475 ) 2476 ax.set_ylim(0, max_value * 1.1) # 最大値の1.1倍を上限に設定 2477 2478 # 方位ラベルの設定 2479 ax.set_theta_zero_location("N") # 北を上に設定 2480 ax.set_theta_direction(-1) # 時計回りに設定 2481 2482 # 方位ラベルの表示 2483 labels = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"] 2484 angles = np.radians(np.linspace(0, 360, len(labels), endpoint=False)) 2485 ax.set_xticks(angles) 2486 ax.set_xticklabels(labels) 2487 2488 # プロット領域の調整(上部と下部にスペースを確保) 2489 plt.subplots_adjust( 2490 top=0.8, # 上部に20%のスペースを確保 2491 bottom=0.2, # 下部に20%のスペースを確保(凡例用) 2492 ) 2493 2494 # サブプロットラベルの追加(デフォルトは左上) 2495 if subplot_label: 2496 ax.text( 2497 0.01, 2498 0.99, 2499 subplot_label, 2500 transform=ax.transAxes, 2501 ) 2502 2503 # 単位の追加(図の下部中央に配置) 2504 plt.figtext( 2505 0.5, # x位置(中央) 2506 0.1, # y位置(下部) 2507 flux_unit, 2508 ha="center", # 水平方向の位置揃え 2509 va="bottom", # 垂直方向の位置揃え 2510 ) 2511 2512 # 凡例の追加(単位の下に配置) 2513 if add_legend: 2514 # 最初のプロットから凡例のハンドルとラベルを取得 2515 handles, labels = ax.get_legend_handles_labels() 2516 # 図の下部に凡例を配置 2517 fig.legend( 2518 handles, 2519 labels, 2520 loc="center", 2521 bbox_to_anchor=(0.5, 0.05), # x=0.5で中央、y=0.05で下部に配置 2522 ncol=len(handles), # ハンドルの数だけ列を作成(一行に表示) 2523 ) 2524 2525 # グラフの保存 2526 if save_fig: 2527 if output_dir is None: 2528 raise ValueError( 2529 "save_fig=Trueのとき、output_dirに有効なパスを指定する必要があります。" 2530 ) 2531 # 出力ディレクトリの作成 2532 os.makedirs(output_dir, exist_ok=True) 2533 output_path: str = os.path.join(output_dir, output_filename) 2534 plt.savefig(output_path, dpi=300, bbox_inches="tight") 2535 2536 # グラフの表示 2537 if show_fig: 2538 plt.show() 2539 else: 2540 plt.close(fig=fig) 2541 2542 # 統計情報の表示 2543 if print_summary: 2544 for source in ["gas", "bio"]: 2545 flux_data = direction_data[f"{source}_flux"] 2546 mean_val = flux_data.mean() 2547 max_val = flux_data.max() 2548 max_dir = direction_data.loc[flux_data.idxmax(), "name"] 2549 2550 self.logger.info( 2551 f"{label_gas if source == 'gas' else label_bio}の統計:" 2552 ) 2553 print(f" 平均フラックス: {mean_val:.2f}") 2554 print(f" 最大フラックス: {max_val:.2f}") 2555 print(f" 最大フラックスの方位: {max_dir}")
CH4フラックスの都市ガス起源と生物起源の風配図を作成する関数
Parameters:
df : pd.DataFrame
風配図を作成するためのデータフレーム
output_dir : str | Path | None
生成された図を保存するディレクトリのパス
output_filename : str
保存するファイル名(デフォルトは"wind_rose.png")
col_ch4_flux : str
CH4フラックスを示すカラム名
col_c2h6_flux : str
C2H6フラックスを示すカラム名
col_wind_dir : str
風向を示すカラム名
label_gas : str
都市ガス起源のフラックスに対するラベル
label_bio : str
生物起源のフラックスに対するラベル
col_datetime : str
日時を示すカラム名
num_directions : int
風向の数(デフォルトは8)
center_on_angles: bool
Trueの場合、45度刻みの線を境界として扇形を描画します。
Falseの場合、45度の中間(22.5度)を中心として扇形を描画します。
subplot_label : str
サブプロットに表示するラベル
print_summary : bool
統計情報を表示するかどうかのフラグ
flux_unit : str
フラックスの単位
ymax : float | None
y軸の上限値(指定しない場合はデータの最大値に基づいて自動設定)
figsize : tuple[float, float]
図のサイズ
flux_alpha : float
フラックスの透明度
save_fig : bool
図を保存するかどうかのフラグ
show_fig : bool
図を表示するかどうかのフラグ
2840 @staticmethod 2841 def get_valid_data(df: pd.DataFrame, x_col: str, y_col: str) -> pd.DataFrame: 2842 """ 2843 指定された列の有効なデータ(NaNを除いた)を取得します。 2844 2845 Parameters: 2846 ------ 2847 df : pd.DataFrame 2848 データフレーム 2849 x_col : str 2850 X軸の列名 2851 y_col : str 2852 Y軸の列名 2853 2854 Returns: 2855 ------ 2856 pd.DataFrame 2857 有効なデータのみを含むDataFrame 2858 """ 2859 return df.copy().dropna(subset=[x_col, y_col])
指定された列の有効なデータ(NaNを除いた)を取得します。
Parameters:
df : pd.DataFrame
データフレーム
x_col : str
X軸の列名
y_col : str
Y軸の列名
Returns:
pd.DataFrame
有効なデータのみを含むDataFrame
2861 @staticmethod 2862 def setup_logger(logger: Logger | None, log_level: int = INFO) -> Logger: 2863 """ 2864 ロガーを設定します。 2865 2866 このメソッドは、ロギングの設定を行い、ログメッセージのフォーマットを指定します。 2867 ログメッセージには、日付、ログレベル、メッセージが含まれます。 2868 2869 渡されたロガーがNoneまたは不正な場合は、新たにロガーを作成し、標準出力に 2870 ログメッセージが表示されるようにStreamHandlerを追加します。ロガーのレベルは 2871 引数で指定されたlog_levelに基づいて設定されます。 2872 2873 Parameters: 2874 ------ 2875 logger : Logger | None 2876 使用するロガー。Noneの場合は新しいロガーを作成します。 2877 log_level : int 2878 ロガーのログレベル。デフォルトはINFO。 2879 2880 Returns: 2881 ------ 2882 Logger 2883 設定されたロガーオブジェクト。 2884 """ 2885 if logger is not None and isinstance(logger, Logger): 2886 return logger 2887 # 渡されたロガーがNoneまたは正しいものでない場合は独自に設定 2888 new_logger: Logger = getLogger() 2889 # 既存のハンドラーをすべて削除 2890 for handler in new_logger.handlers[:]: 2891 new_logger.removeHandler(handler) 2892 new_logger.setLevel(log_level) # ロガーのレベルを設定 2893 ch = StreamHandler() 2894 ch_formatter = Formatter("%(asctime)s - %(levelname)s - %(message)s") 2895 ch.setFormatter(ch_formatter) # フォーマッターをハンドラーに設定 2896 new_logger.addHandler(ch) # StreamHandlerの追加 2897 return new_logger
ロガーを設定します。
このメソッドは、ロギングの設定を行い、ログメッセージのフォーマットを指定します。 ログメッセージには、日付、ログレベル、メッセージが含まれます。
渡されたロガーがNoneまたは不正な場合は、新たにロガーを作成し、標準出力に ログメッセージが表示されるようにStreamHandlerを追加します。ロガーのレベルは 引数で指定されたlog_levelに基づいて設定されます。
Parameters:
logger : Logger | None
使用するロガー。Noneの場合は新しいロガーを作成します。
log_level : int
ロガーのログレベル。デフォルトはINFO。
Returns:
Logger
設定されたロガーオブジェクト。
2899 @staticmethod 2900 def setup_plot_params( 2901 font_family: list[str] = ["Arial", "Dejavu Sans"], 2902 font_size: float = 20, 2903 legend_size: float = 20, 2904 tick_size: float = 20, 2905 title_size: float = 20, 2906 plot_params=None, 2907 ) -> None: 2908 """ 2909 matplotlibのプロットパラメータを設定します。 2910 2911 Parameters: 2912 ------ 2913 font_family : list[str] 2914 使用するフォントファミリーのリスト。 2915 font_size : float 2916 軸ラベルのフォントサイズ。 2917 legend_size : float 2918 凡例のフォントサイズ。 2919 tick_size : float 2920 軸目盛りのフォントサイズ。 2921 title_size : float 2922 タイトルのフォントサイズ。 2923 plot_params : Optional[Dict[str, any]] 2924 matplotlibのプロットパラメータの辞書。 2925 """ 2926 # デフォルトのプロットパラメータ 2927 default_params = { 2928 "axes.linewidth": 1.0, 2929 "axes.titlesize": title_size, # タイトル 2930 "grid.color": "gray", 2931 "grid.linewidth": 1.0, 2932 "font.family": font_family, 2933 "font.size": font_size, # 軸ラベル 2934 "legend.fontsize": legend_size, # 凡例 2935 "text.color": "black", 2936 "xtick.color": "black", 2937 "ytick.color": "black", 2938 "xtick.labelsize": tick_size, # 軸目盛 2939 "ytick.labelsize": tick_size, # 軸目盛 2940 "xtick.major.size": 0, 2941 "ytick.major.size": 0, 2942 "ytick.direction": "out", 2943 "ytick.major.width": 1.0, 2944 } 2945 2946 # plot_paramsが定義されている場合、デフォルトに追記 2947 if plot_params: 2948 default_params.update(plot_params) 2949 2950 plt.rcParams.update(default_params) # プロットパラメータを更新
matplotlibのプロットパラメータを設定します。
Parameters:
font_family : list[str]
使用するフォントファミリーのリスト。
font_size : float
軸ラベルのフォントサイズ。
legend_size : float
凡例のフォントサイズ。
tick_size : float
軸目盛りのフォントサイズ。
title_size : float
タイトルのフォントサイズ。
plot_params : Optional[Dict[str, any]]
matplotlibのプロットパラメータの辞書。
2952 @staticmethod 2953 def plot_flux_distributions( 2954 g2401_flux: pd.Series, 2955 ultra_flux: pd.Series, 2956 month: int, 2957 output_dir: str, 2958 xlim: tuple[float, float] = (-50, 200), 2959 bandwidth: float = 1.0, # デフォルト値を1.0に設定 2960 ) -> None: 2961 """ 2962 両測器のCH4フラックス分布を可視化 2963 2964 Parameters: 2965 ------ 2966 g2401_flux : pd.Series 2967 G2401で測定されたフラックス値の配列 2968 ultra_flux : pd.Series 2969 Ultraで測定されたフラックス値の配列 2970 month : int 2971 測定月 2972 output_dir : str 2973 出力ディレクトリ 2974 xlim : tuple[float, float] 2975 x軸の範囲(タプル) 2976 bandwidth : float 2977 カーネル密度推定のバンド幅調整係数(デフォルト: 1.0) 2978 """ 2979 # nanを除去 2980 g2401_flux = g2401_flux.dropna() 2981 ultra_flux = ultra_flux.dropna() 2982 2983 plt.figure(figsize=(10, 6)) 2984 2985 # KDEプロット(確率密度推定) 2986 sns.kdeplot( 2987 data=g2401_flux, label="G2401", color="blue", alpha=0.5, bw_adjust=bandwidth 2988 ) 2989 sns.kdeplot( 2990 data=ultra_flux, label="Ultra", color="red", alpha=0.5, bw_adjust=bandwidth 2991 ) 2992 2993 # 平均値と中央値のマーカー 2994 plt.axvline( 2995 g2401_flux.mean(), 2996 color="blue", 2997 linestyle="--", 2998 alpha=0.5, 2999 label="G2401 mean", 3000 ) 3001 plt.axvline( 3002 ultra_flux.mean(), 3003 color="red", 3004 linestyle="--", 3005 alpha=0.5, 3006 label="Ultra mean", 3007 ) 3008 plt.axvline( 3009 np.median(g2401_flux), 3010 color="blue", 3011 linestyle=":", 3012 alpha=0.5, 3013 label="G2401 median", 3014 ) 3015 plt.axvline( 3016 np.median(ultra_flux), 3017 color="red", 3018 linestyle=":", 3019 alpha=0.5, 3020 label="Ultra median", 3021 ) 3022 3023 # 軸ラベルとタイトル 3024 plt.xlabel(r"CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)") 3025 plt.ylabel("Probability Density") 3026 plt.title(f"Distribution of CH$_4$ fluxes - Month {month}") 3027 3028 # x軸の範囲設定 3029 plt.xlim(xlim) 3030 3031 # グリッド表示 3032 plt.grid(True, alpha=0.3) 3033 3034 # 統計情報 3035 stats_text = ( 3036 f"G2401:\n" 3037 f" Mean: {g2401_flux.mean():.2f}\n" 3038 f" Median: {np.median(g2401_flux):.2f}\n" 3039 f" Std: {g2401_flux.std():.2f}\n" 3040 f"Ultra:\n" 3041 f" Mean: {ultra_flux.mean():.2f}\n" 3042 f" Median: {np.median(ultra_flux):.2f}\n" 3043 f" Std: {ultra_flux.std():.2f}" 3044 ) 3045 plt.text( 3046 0.02, 3047 0.98, 3048 stats_text, 3049 transform=plt.gca().transAxes, 3050 verticalalignment="top", 3051 fontsize=10, 3052 bbox=dict(boxstyle="round", facecolor="white", alpha=0.8), 3053 ) 3054 3055 # 凡例の表示 3056 plt.legend(loc="upper right") 3057 3058 # グラフの保存 3059 os.makedirs(output_dir, exist_ok=True) 3060 plt.tight_layout() 3061 plt.savefig( 3062 os.path.join(output_dir, f"flux_distribution_month_{month}.png"), 3063 dpi=300, 3064 bbox_inches="tight", 3065 ) 3066 plt.close()
両測器のCH4フラックス分布を可視化
Parameters:
g2401_flux : pd.Series
G2401で測定されたフラックス値の配列
ultra_flux : pd.Series
Ultraで測定されたフラックス値の配列
month : int
測定月
output_dir : str
出力ディレクトリ
xlim : tuple[float, float]
x軸の範囲(タプル)
bandwidth : float
カーネル密度推定のバンド幅調整係数(デフォルト: 1.0)
11class FftFileReorganizer: 12 """ 13 FFTファイルを再編成するためのクラス。 14 15 入力ディレクトリからファイルを読み取り、フラグファイルに基づいて 16 出力ディレクトリに再編成します。時間の完全一致を要求し、 17 一致しないファイルはスキップして警告を出します。 18 オプションで相対湿度(RH)に基づいたサブディレクトリへの分類も可能です。 19 """ 20 21 # クラス定数の定義 22 DEFAULT_FILENAME_PATTERNS: list[str] = [ 23 r"FFT_TOA5_\d+\.SAC_Eddy_\d+_(\d{4})_(\d{2})_(\d{2})_(\d{4})(?:\+)?\.csv", 24 r"FFT_TOA5_\d+\.SAC_Ultra\.Eddy_\d+_(\d{4})_(\d{2})_(\d{2})_(\d{4})(?:\+)?(?:-resampled)?\.csv", 25 ] # デフォルトのファイル名のパターン(正規表現) 26 DEFAULT_OUTPUT_DIRS = { 27 "GOOD_DATA": "good_data_all", 28 "BAD_DATA": "bad_data", 29 } # 出力ディレクトリの構造に関する定数 30 31 def __init__( 32 self, 33 input_dir: str, 34 output_dir: str, 35 flag_csv_path: str, 36 filename_patterns: list[str] | None = None, 37 output_dirs: dict[str, str] | None = None, 38 sort_by_rh: bool = True, 39 logger: Logger | None = None, 40 logging_debug: bool = False, 41 ): 42 """ 43 FftFileReorganizerクラスを初期化します。 44 45 Parameters: 46 ------ 47 input_dir : str 48 入力ファイルが格納されているディレクトリのパス 49 output_dir : str 50 出力ファイルを格納するディレクトリのパス 51 flag_csv_path : str 52 フラグ情報が記載されているCSVファイルのパス 53 filename_patterns : list[str] | None 54 ファイル名のパターン(正規表現)のリスト 55 output_dirs : dict[str, str] | None 56 出力ディレクトリの構造を定義する辞書 57 sort_by_rh : bool 58 RHに基づいてサブディレクトリにファイルを分類するかどうか 59 logger : Logger | None 60 使用するロガー 61 logging_debug : bool 62 ログレベルをDEBUGに設定するかどうか 63 """ 64 self._fft_path: str = input_dir 65 self._sorted_path: str = output_dir 66 self._output_dirs = output_dirs or self.DEFAULT_OUTPUT_DIRS 67 self._good_data_path: str = os.path.join( 68 output_dir, self._output_dirs["GOOD_DATA"] 69 ) 70 self._bad_data_path: str = os.path.join( 71 output_dir, self._output_dirs["BAD_DATA"] 72 ) 73 self._filename_patterns: list[str] = ( 74 self.DEFAULT_FILENAME_PATTERNS.copy() 75 if filename_patterns is None 76 else filename_patterns 77 ) 78 self._flag_file_path: str = flag_csv_path 79 self._sort_by_rh: bool = sort_by_rh 80 self._flags = {} 81 self._warnings = [] 82 # ロガー 83 log_level: int = INFO 84 if logging_debug: 85 log_level = DEBUG 86 self.logger: Logger = FftFileReorganizer.setup_logger(logger, log_level) 87 88 def reorganize(self): 89 """ 90 ファイルの再編成プロセス全体を実行します。 91 ディレクトリの準備、フラグファイルの読み込み、 92 有効なファイルの取得、ファイルのコピーを順に行います。 93 処理後、警告メッセージがあれば出力します。 94 """ 95 self._prepare_directories() 96 self._read_flag_file() 97 valid_files = self._get_valid_files() 98 self._copy_files(valid_files) 99 self.logger.info("ファイルのコピーが完了しました。") 100 101 if self._warnings: 102 self.logger.warning("Warnings:") 103 for warning in self._warnings: 104 self.logger.warning(warning) 105 106 def _copy_files(self, valid_files): 107 """ 108 有効なファイルを適切な出力ディレクトリにコピーします。 109 フラグファイルの時間と完全に一致するファイルのみを処理します。 110 111 Parameters: 112 ------ 113 valid_files : list 114 コピーする有効なファイル名のリスト 115 """ 116 with tqdm(total=len(valid_files)) as pbar: 117 for filename in valid_files: 118 src_file = os.path.join(self._fft_path, filename) 119 file_time = self._parse_datetime(filename) 120 121 if file_time in self._flags: 122 flag = self._flags[file_time]["Flg"] 123 rh = self._flags[file_time]["RH"] 124 if flag == 0: 125 # Copy to self._good_data_path 126 dst_file_good = os.path.join(self._good_data_path, filename) 127 shutil.copy2(src_file, dst_file_good) 128 129 if self._sort_by_rh: 130 # Copy to RH directory 131 rh_dir = FftFileReorganizer.get_rh_directory(rh) 132 dst_file_rh = os.path.join( 133 self._sorted_path, rh_dir, filename 134 ) 135 shutil.copy2(src_file, dst_file_rh) 136 else: 137 dst_file = os.path.join(self._bad_data_path, filename) 138 shutil.copy2(src_file, dst_file) 139 else: 140 self._warnings.append( 141 f"{filename} に対応するフラグが見つかりません。スキップします。" 142 ) 143 144 pbar.update(1) 145 146 def _get_valid_files(self): 147 """ 148 入力ディレクトリから有効なファイルのリストを取得します。 149 150 Parameters: 151 ------ 152 なし 153 154 Returns: 155 ------ 156 valid_files : list 157 日時でソートされた有効なファイル名のリスト 158 """ 159 fft_files = os.listdir(self._fft_path) 160 valid_files = [] 161 for file in fft_files: 162 try: 163 self._parse_datetime(file) 164 valid_files.append(file) 165 except ValueError as e: 166 self._warnings.append(f"{file} をスキップします: {str(e)}") 167 return sorted(valid_files, key=self._parse_datetime) 168 169 def _parse_datetime(self, filename: str) -> datetime: 170 """ 171 ファイル名から日時情報を抽出します。 172 173 Parameters: 174 ------ 175 filename : str 176 解析対象のファイル名 177 178 Returns: 179 ------ 180 datetime : datetime 181 抽出された日時情報 182 183 Raises: 184 ------ 185 ValueError 186 ファイル名から日時情報を抽出できない場合 187 """ 188 for pattern in self._filename_patterns: 189 match = re.match(pattern, filename) 190 if match: 191 year, month, day, time = match.groups() 192 datetime_str: str = f"{year}{month}{day}{time}" 193 return datetime.strptime(datetime_str, "%Y%m%d%H%M") 194 195 raise ValueError(f"Could not parse datetime from filename: {filename}") 196 197 def _prepare_directories(self): 198 """ 199 出力ディレクトリを準備します。 200 既存のディレクトリがある場合は削除し、新しく作成します。 201 """ 202 for path in [self._sorted_path, self._good_data_path, self._bad_data_path]: 203 if os.path.exists(path): 204 shutil.rmtree(path) 205 os.makedirs(path, exist_ok=True) 206 207 if self._sort_by_rh: 208 for i in range(10, 101, 10): 209 rh_path = os.path.join(self._sorted_path, f"RH{i}") 210 os.makedirs(rh_path, exist_ok=True) 211 212 def _read_flag_file(self): 213 """ 214 フラグファイルを読み込み、self._flagsディクショナリに格納します。 215 """ 216 with open(self._flag_file_path, "r") as f: 217 reader = csv.DictReader(f) 218 for row in reader: 219 time = datetime.strptime(row["time"], "%Y/%m/%d %H:%M") 220 try: 221 rh = float(row["RH"]) 222 except ValueError: # RHが#N/Aなどの数値に変換できない値の場合 223 self.logger.debug(f"Invalid RH value at {time}: {row['RH']}") 224 rh = -1 # 不正な値として扱うため、負の値を設定 225 226 self._flags[time] = {"Flg": int(row["Flg"]), "RH": rh} 227 228 @staticmethod 229 def get_rh_directory(rh: float): 230 """ 231 すべての値を10刻みで切り上げる(例: 80.1 → RH90, 86.0 → RH90, 91.2 → RH100) 232 """ 233 if rh < 0 or rh > 100: # 相対湿度として不正な値を除外 234 return "bad_data" 235 elif rh == 0: # 0の場合はRH0に入れる 236 return "RH0" 237 else: # 10刻みで切り上げ 238 return f"RH{min(int((rh + 9.99) // 10 * 10), 100)}" 239 240 @staticmethod 241 def setup_logger(logger: Logger | None, log_level: int = INFO) -> Logger: 242 """ 243 ロガーを設定します。 244 245 ロギングの設定を行い、ログメッセージのフォーマットを指定します。 246 ログメッセージには、日付、ログレベル、メッセージが含まれます。 247 248 渡されたロガーがNoneまたは不正な場合は、新たにロガーを作成し、標準出力に 249 ログメッセージが表示されるようにStreamHandlerを追加します。ロガーのレベルは 250 引数で指定されたlog_levelに基づいて設定されます。 251 252 Parameters: 253 ------ 254 logger : Logger | None 255 使用するロガー。Noneの場合は新しいロガーを作成します。 256 log_level : int 257 ロガーのログレベル。デフォルトはINFO。 258 259 Returns: 260 ------ 261 Logger 262 設定されたロガーオブジェクト。 263 """ 264 if logger is not None and isinstance(logger, Logger): 265 return logger 266 # 渡されたロガーがNoneまたは正しいものでない場合は独自に設定 267 new_logger: Logger = getLogger() 268 # 既存のハンドラーをすべて削除 269 for handler in new_logger.handlers[:]: 270 new_logger.removeHandler(handler) 271 new_logger.setLevel(log_level) # ロガーのレベルを設定 272 ch = StreamHandler() 273 ch_formatter = Formatter("%(asctime)s - %(levelname)s - %(message)s") 274 ch.setFormatter(ch_formatter) # フォーマッターをハンドラーに設定 275 new_logger.addHandler(ch) # StreamHandlerの追加 276 return new_logger
FFTファイルを再編成するためのクラス。
入力ディレクトリからファイルを読み取り、フラグファイルに基づいて 出力ディレクトリに再編成します。時間の完全一致を要求し、 一致しないファイルはスキップして警告を出します。 オプションで相対湿度(RH)に基づいたサブディレクトリへの分類も可能です。
31 def __init__( 32 self, 33 input_dir: str, 34 output_dir: str, 35 flag_csv_path: str, 36 filename_patterns: list[str] | None = None, 37 output_dirs: dict[str, str] | None = None, 38 sort_by_rh: bool = True, 39 logger: Logger | None = None, 40 logging_debug: bool = False, 41 ): 42 """ 43 FftFileReorganizerクラスを初期化します。 44 45 Parameters: 46 ------ 47 input_dir : str 48 入力ファイルが格納されているディレクトリのパス 49 output_dir : str 50 出力ファイルを格納するディレクトリのパス 51 flag_csv_path : str 52 フラグ情報が記載されているCSVファイルのパス 53 filename_patterns : list[str] | None 54 ファイル名のパターン(正規表現)のリスト 55 output_dirs : dict[str, str] | None 56 出力ディレクトリの構造を定義する辞書 57 sort_by_rh : bool 58 RHに基づいてサブディレクトリにファイルを分類するかどうか 59 logger : Logger | None 60 使用するロガー 61 logging_debug : bool 62 ログレベルをDEBUGに設定するかどうか 63 """ 64 self._fft_path: str = input_dir 65 self._sorted_path: str = output_dir 66 self._output_dirs = output_dirs or self.DEFAULT_OUTPUT_DIRS 67 self._good_data_path: str = os.path.join( 68 output_dir, self._output_dirs["GOOD_DATA"] 69 ) 70 self._bad_data_path: str = os.path.join( 71 output_dir, self._output_dirs["BAD_DATA"] 72 ) 73 self._filename_patterns: list[str] = ( 74 self.DEFAULT_FILENAME_PATTERNS.copy() 75 if filename_patterns is None 76 else filename_patterns 77 ) 78 self._flag_file_path: str = flag_csv_path 79 self._sort_by_rh: bool = sort_by_rh 80 self._flags = {} 81 self._warnings = [] 82 # ロガー 83 log_level: int = INFO 84 if logging_debug: 85 log_level = DEBUG 86 self.logger: Logger = FftFileReorganizer.setup_logger(logger, log_level)
FftFileReorganizerクラスを初期化します。
Parameters:
input_dir : str
入力ファイルが格納されているディレクトリのパス
output_dir : str
出力ファイルを格納するディレクトリのパス
flag_csv_path : str
フラグ情報が記載されているCSVファイルのパス
filename_patterns : list[str] | None
ファイル名のパターン(正規表現)のリスト
output_dirs : dict[str, str] | None
出力ディレクトリの構造を定義する辞書
sort_by_rh : bool
RHに基づいてサブディレクトリにファイルを分類するかどうか
logger : Logger | None
使用するロガー
logging_debug : bool
ログレベルをDEBUGに設定するかどうか
88 def reorganize(self): 89 """ 90 ファイルの再編成プロセス全体を実行します。 91 ディレクトリの準備、フラグファイルの読み込み、 92 有効なファイルの取得、ファイルのコピーを順に行います。 93 処理後、警告メッセージがあれば出力します。 94 """ 95 self._prepare_directories() 96 self._read_flag_file() 97 valid_files = self._get_valid_files() 98 self._copy_files(valid_files) 99 self.logger.info("ファイルのコピーが完了しました。") 100 101 if self._warnings: 102 self.logger.warning("Warnings:") 103 for warning in self._warnings: 104 self.logger.warning(warning)
ファイルの再編成プロセス全体を実行します。 ディレクトリの準備、フラグファイルの読み込み、 有効なファイルの取得、ファイルのコピーを順に行います。 処理後、警告メッセージがあれば出力します。
228 @staticmethod 229 def get_rh_directory(rh: float): 230 """ 231 すべての値を10刻みで切り上げる(例: 80.1 → RH90, 86.0 → RH90, 91.2 → RH100) 232 """ 233 if rh < 0 or rh > 100: # 相対湿度として不正な値を除外 234 return "bad_data" 235 elif rh == 0: # 0の場合はRH0に入れる 236 return "RH0" 237 else: # 10刻みで切り上げ 238 return f"RH{min(int((rh + 9.99) // 10 * 10), 100)}"
すべての値を10刻みで切り上げる(例: 80.1 → RH90, 86.0 → RH90, 91.2 → RH100)
240 @staticmethod 241 def setup_logger(logger: Logger | None, log_level: int = INFO) -> Logger: 242 """ 243 ロガーを設定します。 244 245 ロギングの設定を行い、ログメッセージのフォーマットを指定します。 246 ログメッセージには、日付、ログレベル、メッセージが含まれます。 247 248 渡されたロガーがNoneまたは不正な場合は、新たにロガーを作成し、標準出力に 249 ログメッセージが表示されるようにStreamHandlerを追加します。ロガーのレベルは 250 引数で指定されたlog_levelに基づいて設定されます。 251 252 Parameters: 253 ------ 254 logger : Logger | None 255 使用するロガー。Noneの場合は新しいロガーを作成します。 256 log_level : int 257 ロガーのログレベル。デフォルトはINFO。 258 259 Returns: 260 ------ 261 Logger 262 設定されたロガーオブジェクト。 263 """ 264 if logger is not None and isinstance(logger, Logger): 265 return logger 266 # 渡されたロガーがNoneまたは正しいものでない場合は独自に設定 267 new_logger: Logger = getLogger() 268 # 既存のハンドラーをすべて削除 269 for handler in new_logger.handlers[:]: 270 new_logger.removeHandler(handler) 271 new_logger.setLevel(log_level) # ロガーのレベルを設定 272 ch = StreamHandler() 273 ch_formatter = Formatter("%(asctime)s - %(levelname)s - %(message)s") 274 ch.setFormatter(ch_formatter) # フォーマッターをハンドラーに設定 275 new_logger.addHandler(ch) # StreamHandlerの追加 276 return new_logger
ロガーを設定します。
ロギングの設定を行い、ログメッセージのフォーマットを指定します。 ログメッセージには、日付、ログレベル、メッセージが含まれます。
渡されたロガーがNoneまたは不正な場合は、新たにロガーを作成し、標準出力に ログメッセージが表示されるようにStreamHandlerを追加します。ロガーのレベルは 引数で指定されたlog_levelに基づいて設定されます。
Parameters:
logger : Logger | None
使用するロガー。Noneの場合は新しいロガーを作成します。
log_level : int
ロガーのログレベル。デフォルトはINFO。
Returns:
Logger
設定されたロガーオブジェクト。
9class TransferFunctionCalculator: 10 """ 11 このクラスは、CSVファイルからデータを読み込み、処理し、 12 伝達関数を計算してプロットするための機能を提供します。 13 14 この実装は Moore (1986) の論文に基づいています。 15 """ 16 17 def __init__( 18 self, 19 file_path: str, 20 col_freq: str, 21 cutoff_freq_low: float = 0.01, 22 cutoff_freq_high: float = 1, 23 ): 24 """ 25 TransferFunctionCalculatorクラスのコンストラクタ。 26 27 Parameters: 28 ------ 29 file_path : str 30 分析対象のCSVファイルのパス。 31 col_freq : str 32 周波数のキー。 33 cutoff_freq_low : float 34 カットオフ周波数の最低値。 35 cutoff_freq_high : float 36 カットオフ周波数の最高値。 37 """ 38 self._col_freq: str = col_freq 39 self._cutoff_freq_low: float = cutoff_freq_low 40 self._cutoff_freq_high: float = cutoff_freq_high 41 self._df: pd.DataFrame = TransferFunctionCalculator._load_data(file_path) 42 43 def calculate_transfer_function( 44 self, col_reference: str, col_target: str 45 ) -> tuple[float, float, pd.DataFrame]: 46 """ 47 伝達関数の係数を計算する。 48 49 Parameters: 50 ------ 51 col_reference : str 52 参照データのカラム名。 53 col_target : str 54 ターゲットデータのカラム名。 55 56 Returns: 57 ------ 58 tuple[float, float, pandas.DataFrame] 59 伝達関数の係数aとその標準誤差、および計算に用いたDataFrame。 60 """ 61 df_processed: pd.DataFrame = self.process_data( 62 col_reference=col_reference, col_target=col_target 63 ) 64 df_cutoff: pd.DataFrame = self._cutoff_df(df_processed) 65 66 array_x = np.array(df_cutoff.index) 67 array_y = np.array(df_cutoff["target"] / df_cutoff["reference"]) 68 69 # フィッティングパラメータと共分散行列を取得 70 popt, pcov = curve_fit( 71 TransferFunctionCalculator.transfer_function, array_x, array_y 72 ) 73 74 # 標準誤差を計算(共分散行列の対角成分の平方根) 75 perr = np.sqrt(np.diag(pcov)) 76 77 # 係数aとその標準誤差、および計算に用いたDataFrameを返す 78 return popt[0], perr[0], df_processed 79 80 def create_plot_co_spectra( 81 self, 82 col1: str, 83 col2: str, 84 color1: str = "gray", 85 color2: str = "red", 86 figsize: tuple[int, int] = (10, 8), 87 label1: str | None = None, 88 label2: str | None = None, 89 output_dir: str | None = None, 90 output_basename: str = "co", 91 add_legend: bool = True, 92 add_xy_labels: bool = True, 93 show_fig: bool = True, 94 subplot_label: str | None = "(a)", 95 window_size: int = 5, # 移動平均の窓サイズ 96 markersize: float = 14, 97 ) -> None: 98 """ 99 2種類のコスペクトルをプロットする。 100 101 Parameters: 102 ------ 103 col1 : str 104 1つ目のコスペクトルデータのカラム名。 105 col2 : str 106 2つ目のコスペクトルデータのカラム名。 107 color1 : str, optional 108 1つ目のデータの色。デフォルトは'gray'。 109 color2 : str, optional 110 2つ目のデータの色。デフォルトは'red'。 111 figsize : tuple[int, int], optional 112 プロットのサイズ。デフォルトは(10, 8)。 113 label1 : str, optional 114 1つ目のデータのラベル名。デフォルトはNone。 115 label2 : str, optional 116 2つ目のデータのラベル名。デフォルトはNone。 117 output_dir : str | None, optional 118 プロットを保存するディレクトリ。デフォルトはNoneで、保存しない。 119 output_basename : str, optional 120 保存するファイル名のベース。デフォルトは"co"。 121 show_fig : bool, optional 122 プロットを表示するかどうか。デフォルトはTrue。 123 subplot_label : str | None, optional 124 左上に表示するサブプロットラベル。デフォルトは"(a)"。 125 window_size : int, optional 126 移動平均の窓サイズ。デフォルトは5。 127 """ 128 df: pd.DataFrame = self._df.copy() 129 # データの取得と移動平均の適用 130 data1 = df[df[col1] > 0].groupby(self._col_freq)[col1].median() 131 data2 = df[df[col2] > 0].groupby(self._col_freq)[col2].median() 132 133 data1 = data1.rolling(window=window_size, center=True, min_periods=1).mean() 134 data2 = data2.rolling(window=window_size, center=True, min_periods=1).mean() 135 136 fig = plt.figure(figsize=figsize) 137 ax = fig.add_subplot(111) 138 139 # マーカーサイズを設定して見やすくする 140 ax.plot( 141 data1.index, data1, "o", color=color1, label=label1, markersize=markersize 142 ) 143 ax.plot( 144 data2.index, data2, "o", color=color2, label=label2, markersize=markersize 145 ) 146 ax.plot([0.01, 10], [10, 0.001], "-", color="black") 147 ax.text(0.25, 0.4, "-4/3") 148 149 ax.grid(True, alpha=0.3) 150 ax.set_xscale("log") 151 ax.set_yscale("log") 152 ax.set_xlim(0.0001, 10) 153 ax.set_ylim(0.0001, 10) 154 if add_xy_labels: 155 ax.set_xlabel("f (Hz)") 156 ax.set_ylabel("無次元コスペクトル") 157 158 if add_legend: 159 ax.legend( 160 bbox_to_anchor=(0.05, 1), 161 loc="lower left", 162 fontsize=16, 163 ncol=3, 164 frameon=False, 165 ) 166 if subplot_label is not None: 167 ax.text(0.00015, 3, subplot_label) 168 fig.tight_layout() 169 170 if output_dir is not None: 171 os.makedirs(output_dir, exist_ok=True) 172 # プロットをPNG形式で保存 173 filename: str = f"{output_basename}.png" 174 fig.savefig(os.path.join(output_dir, filename), dpi=300) 175 if show_fig: 176 plt.show() 177 else: 178 plt.close(fig=fig) 179 180 def create_plot_ratio( 181 self, 182 df_processed: pd.DataFrame, 183 reference_name: str, 184 target_name: str, 185 figsize: tuple[int, int] = (10, 6), 186 output_dir: str | None = None, 187 output_basename: str = "ratio", 188 show_fig: bool = True, 189 ) -> None: 190 """ 191 ターゲットと参照の比率をプロットする。 192 193 Parameters: 194 ------ 195 df_processed : pd.DataFrame 196 処理されたデータフレーム。 197 reference_name : str 198 参照の名前。 199 target_name : str 200 ターゲットの名前。 201 figsize : tuple[int, int], optional 202 プロットのサイズ。デフォルトは(10, 6)。 203 output_dir : str | None, optional 204 プロットを保存するディレクトリ。デフォルトはNoneで、保存しない。 205 output_basename : str, optional 206 保存するファイル名のベース。デフォルトは"ratio"。 207 show_fig : bool, optional 208 プロットを表示するかどうか。デフォルトはTrue。 209 """ 210 fig = plt.figure(figsize=figsize) 211 ax = fig.add_subplot(111) 212 213 ax.plot( 214 df_processed.index, df_processed["target"] / df_processed["reference"], "o" 215 ) 216 ax.set_xscale("log") 217 ax.set_yscale("log") 218 ax.set_xlabel("f (Hz)") 219 ax.set_ylabel(f"{target_name} / {reference_name}") 220 ax.set_title(f"{target_name}と{reference_name}の比") 221 222 if output_dir is not None: 223 # プロットをPNG形式で保存 224 filename: str = f"{output_basename}-{reference_name}_{target_name}.png" 225 fig.savefig(os.path.join(output_dir, filename), dpi=300) 226 if show_fig: 227 plt.show() 228 else: 229 plt.close(fig=fig) 230 231 @classmethod 232 def create_plot_tf_curves_from_csv( 233 cls, 234 file_path: str, 235 gas_configs: list[tuple[str, str, str, str]], 236 output_dir: str | None = None, 237 output_basename: str = "all_tf_curves", 238 col_datetime: str = "Date", 239 add_xlabel: bool = True, 240 label_x: str = "f (Hz)", 241 label_y: str = "無次元コスペクトル比", 242 label_avg: str = "Avg.", 243 label_co_ref: str = "Tv", 244 line_colors: list[str] | None = None, 245 font_family: list[str] = ["Arial", "MS Gothic"], 246 font_size: float = 20, 247 save_fig: bool = True, 248 show_fig: bool = True, 249 ) -> None: 250 """ 251 複数の伝達関数の係数をプロットし、各ガスの平均値を表示します。 252 各ガスのデータをCSVファイルから読み込み、指定された設定に基づいてプロットを生成します。 253 プロットはオプションで保存することも可能です。 254 255 Parameters: 256 ------ 257 file_path : str 258 伝達関数の係数が格納されたCSVファイルのパス。 259 gas_configs : list[tuple[str, str, str, str]] 260 ガスごとの設定のリスト。各タプルは以下の要素を含む: 261 (係数のカラム名, ガスの表示ラベル, 平均線の色, 出力ファイル用のガス名) 262 例: [("a_ch4-used", "CH$_4$", "red", "ch4")] 263 output_dir : str | None, optional 264 出力ディレクトリ。Noneの場合は保存しない。 265 output_basename : str, optional 266 出力ファイル名のベース。デフォルトは"all_tf_curves"。 267 col_datetime : str, optional 268 日付情報が格納されているカラム名。デフォルトは"Date"。 269 add_xlabel : bool, optional 270 x軸ラベルを追加するかどうか。デフォルトはTrue。 271 label_x : str, optional 272 x軸のラベル。デフォルトは"f (Hz)"。 273 label_y : str, optional 274 y軸のラベル。デフォルトは"無次元コスペクトル比"。 275 label_avg : str, optional 276 平均値のラベル。デフォルトは"Avg."。 277 line_colors : list[str] | None, optional 278 各日付のデータに使用する色のリスト。 279 font_family : list[str], optional 280 使用するフォントファミリーのリスト。 281 font_size : float, optional 282 フォントサイズ。 283 save_fig : bool, optional 284 プロットを保存するかどうか。デフォルトはTrue。 285 show_fig : bool, optional 286 プロットを表示するかどうか。デフォルトはTrue。 287 """ 288 # プロットパラメータの設定 289 plt.rcParams.update( 290 { 291 "font.family": font_family, 292 "font.size": font_size, 293 "axes.labelsize": font_size, 294 "axes.titlesize": font_size, 295 "xtick.labelsize": font_size, 296 "ytick.labelsize": font_size, 297 "legend.fontsize": font_size, 298 } 299 ) 300 301 # CSVファイルを読み込む 302 df = pd.read_csv(file_path) 303 304 # 各ガスについてプロット 305 for col_coef_a, label_gas, base_color, gas_name in gas_configs: 306 fig = plt.figure(figsize=(10, 6)) 307 308 # データ数に応じたデフォルトの色リストを作成 309 if line_colors is None: 310 default_colors = [ 311 "#1f77b4", 312 "#ff7f0e", 313 "#2ca02c", 314 "#d62728", 315 "#9467bd", 316 "#8c564b", 317 "#e377c2", 318 "#7f7f7f", 319 "#bcbd22", 320 "#17becf", 321 ] 322 n_dates = len(df) 323 plot_colors = (default_colors * (n_dates // len(default_colors) + 1))[ 324 :n_dates 325 ] 326 else: 327 plot_colors = line_colors 328 329 # 全てのa値を用いて伝達関数をプロット 330 for i, row in enumerate(df.iterrows()): 331 a = row[1][col_coef_a] 332 date = row[1][col_datetime] 333 x_fit = np.logspace(-3, 1, 1000) 334 y_fit = cls.transfer_function(x_fit, a) 335 plt.plot( 336 x_fit, 337 y_fit, 338 "-", 339 color=plot_colors[i], 340 alpha=0.7, 341 label=f"{date} (a = {a:.3f})", 342 ) 343 344 # 平均のa値を用いた伝達関数をプロット 345 a_mean = df[col_coef_a].mean() 346 x_fit = np.logspace(-3, 1, 1000) 347 y_fit = cls.transfer_function(x_fit, a_mean) 348 plt.plot( 349 x_fit, 350 y_fit, 351 "-", 352 color=base_color, 353 linewidth=3, 354 label=f"{label_avg} (a = {a_mean:.3f})", 355 ) 356 357 # グラフの設定 358 label_y_formatted: str = f"{label_y}\n({label_gas} / {label_co_ref})" 359 plt.xscale("log") 360 if add_xlabel: 361 plt.xlabel(label_x) 362 plt.ylabel(label_y_formatted) 363 plt.legend(loc="lower left", fontsize=font_size - 6) 364 plt.grid(True, which="both", ls="-", alpha=0.2) 365 plt.tight_layout() 366 367 if save_fig: 368 if output_dir is None: 369 raise ValueError( 370 "save_fig=Trueのとき、output_dirに有効なディレクトリパスを指定する必要があります。" 371 ) 372 os.makedirs(output_dir, exist_ok=True) 373 output_path: str = os.path.join( 374 output_dir, f"{output_basename}-{gas_name}.png" 375 ) 376 plt.savefig(output_path, dpi=300, bbox_inches="tight") 377 if show_fig: 378 plt.show() 379 else: 380 plt.close(fig=fig) 381 382 def create_plot_transfer_function( 383 self, 384 a: float, 385 df_processed: pd.DataFrame, 386 reference_name: str, 387 target_name: str, 388 figsize: tuple[int, int] = (10, 6), 389 output_dir: str | None = None, 390 output_basename: str = "tf", 391 show_fig: bool = True, 392 add_xlabel: bool = True, 393 label_x: str = "f (Hz)", 394 label_y: str = "コスペクトル比", 395 label_gas: str | None = None, 396 ) -> None: 397 """ 398 伝達関数とそのフィットをプロットする。 399 400 Parameters: 401 ------ 402 a : float 403 伝達関数の係数。 404 df_processed : pd.DataFrame 405 処理されたデータフレーム。 406 reference_name : str 407 参照の名前。 408 target_name : str 409 ターゲットの名前。 410 figsize : tuple[int, int], optional 411 プロットのサイズ。デフォルトは(10, 6)。 412 output_dir : str | None, optional 413 プロットを保存するディレクトリ。デフォルトはNoneで、保存しない。 414 output_basename : str, optional 415 保存するファイル名のベース。デフォルトは"tf"。 416 show_fig : bool, optional 417 プロットを表示するかどうか。デフォルトはTrue。 418 """ 419 df_cutoff: pd.DataFrame = self._cutoff_df(df_processed) 420 421 fig = plt.figure(figsize=figsize) 422 ax = fig.add_subplot(111) 423 424 ax.plot( 425 df_cutoff.index, 426 df_cutoff["target"] / df_cutoff["reference"], 427 "o", 428 label=f"{target_name} / {reference_name}", 429 ) 430 431 x_fit = np.logspace( 432 np.log10(self._cutoff_freq_low), np.log10(self._cutoff_freq_high), 1000 433 ) 434 y_fit = self.transfer_function(x_fit, a) 435 ax.plot(x_fit, y_fit, "-", label=f"フィット (a = {a:.4f})") 436 437 ax.set_xscale("log") 438 # グラフの設定 439 label_y_formatted: str = f"{label_y}\n({label_gas} / 顕熱)" 440 plt.xscale("log") 441 if add_xlabel: 442 plt.xlabel(label_x) 443 plt.ylabel(label_y_formatted) 444 ax.legend() 445 446 if output_dir is not None: 447 # プロットをPNG形式で保存 448 filename: str = f"{output_basename}-{reference_name}_{target_name}.png" 449 fig.savefig(os.path.join(output_dir, filename), dpi=300) 450 if show_fig: 451 plt.show() 452 else: 453 plt.close(fig=fig) 454 455 def process_data(self, col_reference: str, col_target: str) -> pd.DataFrame: 456 """ 457 指定されたキーに基づいてデータを処理する。 458 459 Parameters: 460 ------ 461 col_reference : str 462 参照データのカラム名。 463 col_target : str 464 ターゲットデータのカラム名。 465 466 Returns: 467 ------ 468 pd.DataFrame 469 処理されたデータフレーム。 470 """ 471 df: pd.DataFrame = self._df.copy() 472 col_freq: str = self._col_freq 473 474 # データ型の確認と変換 475 df[col_freq] = pd.to_numeric(df[col_freq], errors="coerce") 476 df[col_reference] = pd.to_numeric(df[col_reference], errors="coerce") 477 df[col_target] = pd.to_numeric(df[col_target], errors="coerce") 478 479 # NaNを含む行を削除 480 df = df.dropna(subset=[col_freq, col_reference, col_target]) 481 482 # グループ化と中央値の計算 483 grouped = df.groupby(col_freq) 484 reference_data = grouped[col_reference].median() 485 target_data = grouped[col_target].median() 486 487 df_processed = pd.DataFrame( 488 {"reference": reference_data, "target": target_data} 489 ) 490 491 # 異常な比率を除去 492 df_processed.loc[ 493 ( 494 (df_processed["target"] / df_processed["reference"] > 1) 495 | (df_processed["target"] / df_processed["reference"] < 0) 496 ) 497 ] = np.nan 498 df_processed = df_processed.dropna() 499 500 return df_processed 501 502 def _cutoff_df(self, df: pd.DataFrame) -> pd.DataFrame: 503 """ 504 カットオフ周波数に基づいてDataFrameを加工するメソッド 505 506 Parameters: 507 ------ 508 df : pd.DataFrame 509 加工対象のデータフレーム。 510 511 Returns: 512 ------ 513 pd.DataFrame 514 カットオフ周波数に基づいて加工されたデータフレーム。 515 """ 516 df_cutoff: pd.DataFrame = df.loc[ 517 (self._cutoff_freq_low <= df.index) & (df.index <= self._cutoff_freq_high) 518 ] 519 return df_cutoff 520 521 @classmethod 522 def transfer_function(cls, x: np.ndarray, a: float) -> np.ndarray: 523 """ 524 伝達関数を計算する。 525 526 Parameters: 527 ------ 528 x : np.ndarray 529 周波数の配列。 530 a : float 531 伝達関数の係数。 532 533 Returns: 534 ------ 535 np.ndarray 536 伝達関数の値。 537 """ 538 return np.exp(-np.log(np.sqrt(2)) * np.power(x / a, 2)) 539 540 @staticmethod 541 def _load_data(file_path: str) -> pd.DataFrame: 542 """ 543 CSVファイルからデータを読み込む。 544 545 Parameters: 546 ------ 547 file_path : str 548 csvファイルのパス。 549 550 Returns: 551 ------ 552 pd.DataFrame 553 読み込まれたデータフレーム。 554 """ 555 tmp = pd.read_csv(file_path, header=None, nrows=1, skiprows=0) 556 header = tmp.loc[tmp.index[0]] 557 df = pd.read_csv(file_path, header=None, skiprows=1) 558 df.columns = header 559 return df 560 561 @staticmethod 562 def setup_plot_params( 563 font_family: list[str] = ["Arial", "Dejavu Sans"], 564 font_size: float = 20, 565 legend_size: float = 20, 566 tick_size: float = 20, 567 title_size: float = 20, 568 plot_params=None, 569 ) -> None: 570 """ 571 matplotlibのプロットパラメータを設定します。 572 573 Parameters: 574 ------ 575 font_family : list[str] 576 使用するフォントファミリーのリスト。 577 font_size : float 578 軸ラベルのフォントサイズ。 579 legend_size : float 580 凡例のフォントサイズ。 581 tick_size : float 582 軸目盛りのフォントサイズ。 583 title_size : float 584 タイトルのフォントサイズ。 585 plot_params : Optional[Dict[str, any]] 586 matplotlibのプロットパラメータの辞書。 587 """ 588 # デフォルトのプロットパラメータ 589 default_params = { 590 "axes.linewidth": 1.0, 591 "axes.titlesize": title_size, # タイトル 592 "grid.color": "gray", 593 "grid.linewidth": 1.0, 594 "font.family": font_family, 595 "font.size": font_size, # 軸ラベル 596 "legend.fontsize": legend_size, # 凡例 597 "text.color": "black", 598 "xtick.color": "black", 599 "ytick.color": "black", 600 "xtick.labelsize": tick_size, # 軸目盛 601 "ytick.labelsize": tick_size, # 軸目盛 602 "xtick.major.size": 0, 603 "ytick.major.size": 0, 604 "ytick.direction": "out", 605 "ytick.major.width": 1.0, 606 } 607 608 # plot_paramsが定義されている場合、デフォルトに追記 609 if plot_params: 610 default_params.update(plot_params) 611 612 plt.rcParams.update(default_params) # プロットパラメータを更新
このクラスは、CSVファイルからデータを読み込み、処理し、 伝達関数を計算してプロットするための機能を提供します。
この実装は Moore (1986) の論文に基づいています。
17 def __init__( 18 self, 19 file_path: str, 20 col_freq: str, 21 cutoff_freq_low: float = 0.01, 22 cutoff_freq_high: float = 1, 23 ): 24 """ 25 TransferFunctionCalculatorクラスのコンストラクタ。 26 27 Parameters: 28 ------ 29 file_path : str 30 分析対象のCSVファイルのパス。 31 col_freq : str 32 周波数のキー。 33 cutoff_freq_low : float 34 カットオフ周波数の最低値。 35 cutoff_freq_high : float 36 カットオフ周波数の最高値。 37 """ 38 self._col_freq: str = col_freq 39 self._cutoff_freq_low: float = cutoff_freq_low 40 self._cutoff_freq_high: float = cutoff_freq_high 41 self._df: pd.DataFrame = TransferFunctionCalculator._load_data(file_path)
TransferFunctionCalculatorクラスのコンストラクタ。
Parameters:
file_path : str
分析対象のCSVファイルのパス。
col_freq : str
周波数のキー。
cutoff_freq_low : float
カットオフ周波数の最低値。
cutoff_freq_high : float
カットオフ周波数の最高値。
43 def calculate_transfer_function( 44 self, col_reference: str, col_target: str 45 ) -> tuple[float, float, pd.DataFrame]: 46 """ 47 伝達関数の係数を計算する。 48 49 Parameters: 50 ------ 51 col_reference : str 52 参照データのカラム名。 53 col_target : str 54 ターゲットデータのカラム名。 55 56 Returns: 57 ------ 58 tuple[float, float, pandas.DataFrame] 59 伝達関数の係数aとその標準誤差、および計算に用いたDataFrame。 60 """ 61 df_processed: pd.DataFrame = self.process_data( 62 col_reference=col_reference, col_target=col_target 63 ) 64 df_cutoff: pd.DataFrame = self._cutoff_df(df_processed) 65 66 array_x = np.array(df_cutoff.index) 67 array_y = np.array(df_cutoff["target"] / df_cutoff["reference"]) 68 69 # フィッティングパラメータと共分散行列を取得 70 popt, pcov = curve_fit( 71 TransferFunctionCalculator.transfer_function, array_x, array_y 72 ) 73 74 # 標準誤差を計算(共分散行列の対角成分の平方根) 75 perr = np.sqrt(np.diag(pcov)) 76 77 # 係数aとその標準誤差、および計算に用いたDataFrameを返す 78 return popt[0], perr[0], df_processed
伝達関数の係数を計算する。
Parameters:
col_reference : str
参照データのカラム名。
col_target : str
ターゲットデータのカラム名。
Returns:
tuple[float, float, pandas.DataFrame]
伝達関数の係数aとその標準誤差、および計算に用いたDataFrame。
80 def create_plot_co_spectra( 81 self, 82 col1: str, 83 col2: str, 84 color1: str = "gray", 85 color2: str = "red", 86 figsize: tuple[int, int] = (10, 8), 87 label1: str | None = None, 88 label2: str | None = None, 89 output_dir: str | None = None, 90 output_basename: str = "co", 91 add_legend: bool = True, 92 add_xy_labels: bool = True, 93 show_fig: bool = True, 94 subplot_label: str | None = "(a)", 95 window_size: int = 5, # 移動平均の窓サイズ 96 markersize: float = 14, 97 ) -> None: 98 """ 99 2種類のコスペクトルをプロットする。 100 101 Parameters: 102 ------ 103 col1 : str 104 1つ目のコスペクトルデータのカラム名。 105 col2 : str 106 2つ目のコスペクトルデータのカラム名。 107 color1 : str, optional 108 1つ目のデータの色。デフォルトは'gray'。 109 color2 : str, optional 110 2つ目のデータの色。デフォルトは'red'。 111 figsize : tuple[int, int], optional 112 プロットのサイズ。デフォルトは(10, 8)。 113 label1 : str, optional 114 1つ目のデータのラベル名。デフォルトはNone。 115 label2 : str, optional 116 2つ目のデータのラベル名。デフォルトはNone。 117 output_dir : str | None, optional 118 プロットを保存するディレクトリ。デフォルトはNoneで、保存しない。 119 output_basename : str, optional 120 保存するファイル名のベース。デフォルトは"co"。 121 show_fig : bool, optional 122 プロットを表示するかどうか。デフォルトはTrue。 123 subplot_label : str | None, optional 124 左上に表示するサブプロットラベル。デフォルトは"(a)"。 125 window_size : int, optional 126 移動平均の窓サイズ。デフォルトは5。 127 """ 128 df: pd.DataFrame = self._df.copy() 129 # データの取得と移動平均の適用 130 data1 = df[df[col1] > 0].groupby(self._col_freq)[col1].median() 131 data2 = df[df[col2] > 0].groupby(self._col_freq)[col2].median() 132 133 data1 = data1.rolling(window=window_size, center=True, min_periods=1).mean() 134 data2 = data2.rolling(window=window_size, center=True, min_periods=1).mean() 135 136 fig = plt.figure(figsize=figsize) 137 ax = fig.add_subplot(111) 138 139 # マーカーサイズを設定して見やすくする 140 ax.plot( 141 data1.index, data1, "o", color=color1, label=label1, markersize=markersize 142 ) 143 ax.plot( 144 data2.index, data2, "o", color=color2, label=label2, markersize=markersize 145 ) 146 ax.plot([0.01, 10], [10, 0.001], "-", color="black") 147 ax.text(0.25, 0.4, "-4/3") 148 149 ax.grid(True, alpha=0.3) 150 ax.set_xscale("log") 151 ax.set_yscale("log") 152 ax.set_xlim(0.0001, 10) 153 ax.set_ylim(0.0001, 10) 154 if add_xy_labels: 155 ax.set_xlabel("f (Hz)") 156 ax.set_ylabel("無次元コスペクトル") 157 158 if add_legend: 159 ax.legend( 160 bbox_to_anchor=(0.05, 1), 161 loc="lower left", 162 fontsize=16, 163 ncol=3, 164 frameon=False, 165 ) 166 if subplot_label is not None: 167 ax.text(0.00015, 3, subplot_label) 168 fig.tight_layout() 169 170 if output_dir is not None: 171 os.makedirs(output_dir, exist_ok=True) 172 # プロットをPNG形式で保存 173 filename: str = f"{output_basename}.png" 174 fig.savefig(os.path.join(output_dir, filename), dpi=300) 175 if show_fig: 176 plt.show() 177 else: 178 plt.close(fig=fig)
2種類のコスペクトルをプロットする。
Parameters:
col1 : str
1つ目のコスペクトルデータのカラム名。
col2 : str
2つ目のコスペクトルデータのカラム名。
color1 : str, optional
1つ目のデータの色。デフォルトは'gray'。
color2 : str, optional
2つ目のデータの色。デフォルトは'red'。
figsize : tuple[int, int], optional
プロットのサイズ。デフォルトは(10, 8)。
label1 : str, optional
1つ目のデータのラベル名。デフォルトはNone。
label2 : str, optional
2つ目のデータのラベル名。デフォルトはNone。
output_dir : str | None, optional
プロットを保存するディレクトリ。デフォルトはNoneで、保存しない。
output_basename : str, optional
保存するファイル名のベース。デフォルトは"co"。
show_fig : bool, optional
プロットを表示するかどうか。デフォルトはTrue。
subplot_label : str | None, optional
左上に表示するサブプロットラベル。デフォルトは"(a)"。
window_size : int, optional
移動平均の窓サイズ。デフォルトは5。
180 def create_plot_ratio( 181 self, 182 df_processed: pd.DataFrame, 183 reference_name: str, 184 target_name: str, 185 figsize: tuple[int, int] = (10, 6), 186 output_dir: str | None = None, 187 output_basename: str = "ratio", 188 show_fig: bool = True, 189 ) -> None: 190 """ 191 ターゲットと参照の比率をプロットする。 192 193 Parameters: 194 ------ 195 df_processed : pd.DataFrame 196 処理されたデータフレーム。 197 reference_name : str 198 参照の名前。 199 target_name : str 200 ターゲットの名前。 201 figsize : tuple[int, int], optional 202 プロットのサイズ。デフォルトは(10, 6)。 203 output_dir : str | None, optional 204 プロットを保存するディレクトリ。デフォルトはNoneで、保存しない。 205 output_basename : str, optional 206 保存するファイル名のベース。デフォルトは"ratio"。 207 show_fig : bool, optional 208 プロットを表示するかどうか。デフォルトはTrue。 209 """ 210 fig = plt.figure(figsize=figsize) 211 ax = fig.add_subplot(111) 212 213 ax.plot( 214 df_processed.index, df_processed["target"] / df_processed["reference"], "o" 215 ) 216 ax.set_xscale("log") 217 ax.set_yscale("log") 218 ax.set_xlabel("f (Hz)") 219 ax.set_ylabel(f"{target_name} / {reference_name}") 220 ax.set_title(f"{target_name}と{reference_name}の比") 221 222 if output_dir is not None: 223 # プロットをPNG形式で保存 224 filename: str = f"{output_basename}-{reference_name}_{target_name}.png" 225 fig.savefig(os.path.join(output_dir, filename), dpi=300) 226 if show_fig: 227 plt.show() 228 else: 229 plt.close(fig=fig)
ターゲットと参照の比率をプロットする。
Parameters:
df_processed : pd.DataFrame
処理されたデータフレーム。
reference_name : str
参照の名前。
target_name : str
ターゲットの名前。
figsize : tuple[int, int], optional
プロットのサイズ。デフォルトは(10, 6)。
output_dir : str | None, optional
プロットを保存するディレクトリ。デフォルトはNoneで、保存しない。
output_basename : str, optional
保存するファイル名のベース。デフォルトは"ratio"。
show_fig : bool, optional
プロットを表示するかどうか。デフォルトはTrue。
231 @classmethod 232 def create_plot_tf_curves_from_csv( 233 cls, 234 file_path: str, 235 gas_configs: list[tuple[str, str, str, str]], 236 output_dir: str | None = None, 237 output_basename: str = "all_tf_curves", 238 col_datetime: str = "Date", 239 add_xlabel: bool = True, 240 label_x: str = "f (Hz)", 241 label_y: str = "無次元コスペクトル比", 242 label_avg: str = "Avg.", 243 label_co_ref: str = "Tv", 244 line_colors: list[str] | None = None, 245 font_family: list[str] = ["Arial", "MS Gothic"], 246 font_size: float = 20, 247 save_fig: bool = True, 248 show_fig: bool = True, 249 ) -> None: 250 """ 251 複数の伝達関数の係数をプロットし、各ガスの平均値を表示します。 252 各ガスのデータをCSVファイルから読み込み、指定された設定に基づいてプロットを生成します。 253 プロットはオプションで保存することも可能です。 254 255 Parameters: 256 ------ 257 file_path : str 258 伝達関数の係数が格納されたCSVファイルのパス。 259 gas_configs : list[tuple[str, str, str, str]] 260 ガスごとの設定のリスト。各タプルは以下の要素を含む: 261 (係数のカラム名, ガスの表示ラベル, 平均線の色, 出力ファイル用のガス名) 262 例: [("a_ch4-used", "CH$_4$", "red", "ch4")] 263 output_dir : str | None, optional 264 出力ディレクトリ。Noneの場合は保存しない。 265 output_basename : str, optional 266 出力ファイル名のベース。デフォルトは"all_tf_curves"。 267 col_datetime : str, optional 268 日付情報が格納されているカラム名。デフォルトは"Date"。 269 add_xlabel : bool, optional 270 x軸ラベルを追加するかどうか。デフォルトはTrue。 271 label_x : str, optional 272 x軸のラベル。デフォルトは"f (Hz)"。 273 label_y : str, optional 274 y軸のラベル。デフォルトは"無次元コスペクトル比"。 275 label_avg : str, optional 276 平均値のラベル。デフォルトは"Avg."。 277 line_colors : list[str] | None, optional 278 各日付のデータに使用する色のリスト。 279 font_family : list[str], optional 280 使用するフォントファミリーのリスト。 281 font_size : float, optional 282 フォントサイズ。 283 save_fig : bool, optional 284 プロットを保存するかどうか。デフォルトはTrue。 285 show_fig : bool, optional 286 プロットを表示するかどうか。デフォルトはTrue。 287 """ 288 # プロットパラメータの設定 289 plt.rcParams.update( 290 { 291 "font.family": font_family, 292 "font.size": font_size, 293 "axes.labelsize": font_size, 294 "axes.titlesize": font_size, 295 "xtick.labelsize": font_size, 296 "ytick.labelsize": font_size, 297 "legend.fontsize": font_size, 298 } 299 ) 300 301 # CSVファイルを読み込む 302 df = pd.read_csv(file_path) 303 304 # 各ガスについてプロット 305 for col_coef_a, label_gas, base_color, gas_name in gas_configs: 306 fig = plt.figure(figsize=(10, 6)) 307 308 # データ数に応じたデフォルトの色リストを作成 309 if line_colors is None: 310 default_colors = [ 311 "#1f77b4", 312 "#ff7f0e", 313 "#2ca02c", 314 "#d62728", 315 "#9467bd", 316 "#8c564b", 317 "#e377c2", 318 "#7f7f7f", 319 "#bcbd22", 320 "#17becf", 321 ] 322 n_dates = len(df) 323 plot_colors = (default_colors * (n_dates // len(default_colors) + 1))[ 324 :n_dates 325 ] 326 else: 327 plot_colors = line_colors 328 329 # 全てのa値を用いて伝達関数をプロット 330 for i, row in enumerate(df.iterrows()): 331 a = row[1][col_coef_a] 332 date = row[1][col_datetime] 333 x_fit = np.logspace(-3, 1, 1000) 334 y_fit = cls.transfer_function(x_fit, a) 335 plt.plot( 336 x_fit, 337 y_fit, 338 "-", 339 color=plot_colors[i], 340 alpha=0.7, 341 label=f"{date} (a = {a:.3f})", 342 ) 343 344 # 平均のa値を用いた伝達関数をプロット 345 a_mean = df[col_coef_a].mean() 346 x_fit = np.logspace(-3, 1, 1000) 347 y_fit = cls.transfer_function(x_fit, a_mean) 348 plt.plot( 349 x_fit, 350 y_fit, 351 "-", 352 color=base_color, 353 linewidth=3, 354 label=f"{label_avg} (a = {a_mean:.3f})", 355 ) 356 357 # グラフの設定 358 label_y_formatted: str = f"{label_y}\n({label_gas} / {label_co_ref})" 359 plt.xscale("log") 360 if add_xlabel: 361 plt.xlabel(label_x) 362 plt.ylabel(label_y_formatted) 363 plt.legend(loc="lower left", fontsize=font_size - 6) 364 plt.grid(True, which="both", ls="-", alpha=0.2) 365 plt.tight_layout() 366 367 if save_fig: 368 if output_dir is None: 369 raise ValueError( 370 "save_fig=Trueのとき、output_dirに有効なディレクトリパスを指定する必要があります。" 371 ) 372 os.makedirs(output_dir, exist_ok=True) 373 output_path: str = os.path.join( 374 output_dir, f"{output_basename}-{gas_name}.png" 375 ) 376 plt.savefig(output_path, dpi=300, bbox_inches="tight") 377 if show_fig: 378 plt.show() 379 else: 380 plt.close(fig=fig)
複数の伝達関数の係数をプロットし、各ガスの平均値を表示します。 各ガスのデータをCSVファイルから読み込み、指定された設定に基づいてプロットを生成します。 プロットはオプションで保存することも可能です。
Parameters:
file_path : str
伝達関数の係数が格納されたCSVファイルのパス。
gas_configs : list[tuple[str, str, str, str]]
ガスごとの設定のリスト。各タプルは以下の要素を含む:
(係数のカラム名, ガスの表示ラベル, 平均線の色, 出力ファイル用のガス名)
例: [("a_ch4-used", "CH$_4$", "red", "ch4")]
output_dir : str | None, optional
出力ディレクトリ。Noneの場合は保存しない。
output_basename : str, optional
出力ファイル名のベース。デフォルトは"all_tf_curves"。
col_datetime : str, optional
日付情報が格納されているカラム名。デフォルトは"Date"。
add_xlabel : bool, optional
x軸ラベルを追加するかどうか。デフォルトはTrue。
label_x : str, optional
x軸のラベル。デフォルトは"f (Hz)"。
label_y : str, optional
y軸のラベル。デフォルトは"無次元コスペクトル比"。
label_avg : str, optional
平均値のラベル。デフォルトは"Avg."。
line_colors : list[str] | None, optional
各日付のデータに使用する色のリスト。
font_family : list[str], optional
使用するフォントファミリーのリスト。
font_size : float, optional
フォントサイズ。
save_fig : bool, optional
プロットを保存するかどうか。デフォルトはTrue。
show_fig : bool, optional
プロットを表示するかどうか。デフォルトはTrue。
382 def create_plot_transfer_function( 383 self, 384 a: float, 385 df_processed: pd.DataFrame, 386 reference_name: str, 387 target_name: str, 388 figsize: tuple[int, int] = (10, 6), 389 output_dir: str | None = None, 390 output_basename: str = "tf", 391 show_fig: bool = True, 392 add_xlabel: bool = True, 393 label_x: str = "f (Hz)", 394 label_y: str = "コスペクトル比", 395 label_gas: str | None = None, 396 ) -> None: 397 """ 398 伝達関数とそのフィットをプロットする。 399 400 Parameters: 401 ------ 402 a : float 403 伝達関数の係数。 404 df_processed : pd.DataFrame 405 処理されたデータフレーム。 406 reference_name : str 407 参照の名前。 408 target_name : str 409 ターゲットの名前。 410 figsize : tuple[int, int], optional 411 プロットのサイズ。デフォルトは(10, 6)。 412 output_dir : str | None, optional 413 プロットを保存するディレクトリ。デフォルトはNoneで、保存しない。 414 output_basename : str, optional 415 保存するファイル名のベース。デフォルトは"tf"。 416 show_fig : bool, optional 417 プロットを表示するかどうか。デフォルトはTrue。 418 """ 419 df_cutoff: pd.DataFrame = self._cutoff_df(df_processed) 420 421 fig = plt.figure(figsize=figsize) 422 ax = fig.add_subplot(111) 423 424 ax.plot( 425 df_cutoff.index, 426 df_cutoff["target"] / df_cutoff["reference"], 427 "o", 428 label=f"{target_name} / {reference_name}", 429 ) 430 431 x_fit = np.logspace( 432 np.log10(self._cutoff_freq_low), np.log10(self._cutoff_freq_high), 1000 433 ) 434 y_fit = self.transfer_function(x_fit, a) 435 ax.plot(x_fit, y_fit, "-", label=f"フィット (a = {a:.4f})") 436 437 ax.set_xscale("log") 438 # グラフの設定 439 label_y_formatted: str = f"{label_y}\n({label_gas} / 顕熱)" 440 plt.xscale("log") 441 if add_xlabel: 442 plt.xlabel(label_x) 443 plt.ylabel(label_y_formatted) 444 ax.legend() 445 446 if output_dir is not None: 447 # プロットをPNG形式で保存 448 filename: str = f"{output_basename}-{reference_name}_{target_name}.png" 449 fig.savefig(os.path.join(output_dir, filename), dpi=300) 450 if show_fig: 451 plt.show() 452 else: 453 plt.close(fig=fig)
伝達関数とそのフィットをプロットする。
Parameters:
a : float
伝達関数の係数。
df_processed : pd.DataFrame
処理されたデータフレーム。
reference_name : str
参照の名前。
target_name : str
ターゲットの名前。
figsize : tuple[int, int], optional
プロットのサイズ。デフォルトは(10, 6)。
output_dir : str | None, optional
プロットを保存するディレクトリ。デフォルトはNoneで、保存しない。
output_basename : str, optional
保存するファイル名のベース。デフォルトは"tf"。
show_fig : bool, optional
プロットを表示するかどうか。デフォルトはTrue。
455 def process_data(self, col_reference: str, col_target: str) -> pd.DataFrame: 456 """ 457 指定されたキーに基づいてデータを処理する。 458 459 Parameters: 460 ------ 461 col_reference : str 462 参照データのカラム名。 463 col_target : str 464 ターゲットデータのカラム名。 465 466 Returns: 467 ------ 468 pd.DataFrame 469 処理されたデータフレーム。 470 """ 471 df: pd.DataFrame = self._df.copy() 472 col_freq: str = self._col_freq 473 474 # データ型の確認と変換 475 df[col_freq] = pd.to_numeric(df[col_freq], errors="coerce") 476 df[col_reference] = pd.to_numeric(df[col_reference], errors="coerce") 477 df[col_target] = pd.to_numeric(df[col_target], errors="coerce") 478 479 # NaNを含む行を削除 480 df = df.dropna(subset=[col_freq, col_reference, col_target]) 481 482 # グループ化と中央値の計算 483 grouped = df.groupby(col_freq) 484 reference_data = grouped[col_reference].median() 485 target_data = grouped[col_target].median() 486 487 df_processed = pd.DataFrame( 488 {"reference": reference_data, "target": target_data} 489 ) 490 491 # 異常な比率を除去 492 df_processed.loc[ 493 ( 494 (df_processed["target"] / df_processed["reference"] > 1) 495 | (df_processed["target"] / df_processed["reference"] < 0) 496 ) 497 ] = np.nan 498 df_processed = df_processed.dropna() 499 500 return df_processed
指定されたキーに基づいてデータを処理する。
Parameters:
col_reference : str
参照データのカラム名。
col_target : str
ターゲットデータのカラム名。
Returns:
pd.DataFrame
処理されたデータフレーム。
521 @classmethod 522 def transfer_function(cls, x: np.ndarray, a: float) -> np.ndarray: 523 """ 524 伝達関数を計算する。 525 526 Parameters: 527 ------ 528 x : np.ndarray 529 周波数の配列。 530 a : float 531 伝達関数の係数。 532 533 Returns: 534 ------ 535 np.ndarray 536 伝達関数の値。 537 """ 538 return np.exp(-np.log(np.sqrt(2)) * np.power(x / a, 2))
伝達関数を計算する。
Parameters:
x : np.ndarray
周波数の配列。
a : float
伝達関数の係数。
Returns:
np.ndarray
伝達関数の値。
561 @staticmethod 562 def setup_plot_params( 563 font_family: list[str] = ["Arial", "Dejavu Sans"], 564 font_size: float = 20, 565 legend_size: float = 20, 566 tick_size: float = 20, 567 title_size: float = 20, 568 plot_params=None, 569 ) -> None: 570 """ 571 matplotlibのプロットパラメータを設定します。 572 573 Parameters: 574 ------ 575 font_family : list[str] 576 使用するフォントファミリーのリスト。 577 font_size : float 578 軸ラベルのフォントサイズ。 579 legend_size : float 580 凡例のフォントサイズ。 581 tick_size : float 582 軸目盛りのフォントサイズ。 583 title_size : float 584 タイトルのフォントサイズ。 585 plot_params : Optional[Dict[str, any]] 586 matplotlibのプロットパラメータの辞書。 587 """ 588 # デフォルトのプロットパラメータ 589 default_params = { 590 "axes.linewidth": 1.0, 591 "axes.titlesize": title_size, # タイトル 592 "grid.color": "gray", 593 "grid.linewidth": 1.0, 594 "font.family": font_family, 595 "font.size": font_size, # 軸ラベル 596 "legend.fontsize": legend_size, # 凡例 597 "text.color": "black", 598 "xtick.color": "black", 599 "ytick.color": "black", 600 "xtick.labelsize": tick_size, # 軸目盛 601 "ytick.labelsize": tick_size, # 軸目盛 602 "xtick.major.size": 0, 603 "ytick.major.size": 0, 604 "ytick.direction": "out", 605 "ytick.major.width": 1.0, 606 } 607 608 # plot_paramsが定義されている場合、デフォルトに追記 609 if plot_params: 610 default_params.update(plot_params) 611 612 plt.rcParams.update(default_params) # プロットパラメータを更新
matplotlibのプロットパラメータを設定します。
Parameters:
font_family : list[str]
使用するフォントファミリーのリスト。
font_size : float
軸ラベルのフォントサイズ。
legend_size : float
凡例のフォントサイズ。
tick_size : float
軸目盛りのフォントサイズ。
title_size : float
タイトルのフォントサイズ。
plot_params : Optional[Dict[str, any]]
matplotlibのプロットパラメータの辞書。