py_flux_tracer
1from .campbell.eddy_data_preprocessor import EddyDataPreprocessor 2from .campbell.spectrum_calculator import SpectrumCalculator 3from .commons.figure_utils import FigureUtils 4from .commons.hotspot_data import HotspotData, HotspotType 5from .footprint.flux_footprint_analyzer import FluxFootprintAnalyzer 6from .mobile.correcting_utils import ( 7 CorrectingUtils, 8 H2OCorrectionConfig, 9 BiasRemovalConfig, 10) 11from .mobile.mobile_spatial_analyzer import ( 12 EmissionData, 13 HotspotParams, 14 MobileSpatialAnalyzer, 15 MSAInputConfig, 16) 17from .monthly.monthly_converter import MonthlyConverter 18from .monthly.monthly_figures_generator import MonthlyFiguresGenerator 19from .transfer_function.fft_files_reorganizer import FftFileReorganizer 20from .transfer_function.transfer_function_calculator import TransferFunctionCalculator 21 22""" 23versionを動的に設定する。 24`./_version.py`がない場合はsetuptools_scmを用いてGitからバージョン取得を試行 25それも失敗した場合にデフォルトバージョン(0.0.0)を設定 26""" 27try: 28 from ._version import __version__ # type:ignore 29except ImportError: 30 try: 31 from setuptools_scm import get_version 32 33 __version__ = get_version(root="..", relative_to=__file__) 34 except Exception: 35 __version__ = "0.0.0" 36 37__version__ = __version__ 38""" 39@private 40このモジュールはバージョン情報の管理に使用され、ドキュメントには含めません。 41private属性を適用するために再宣言してdocstringを記述しています。 42""" 43 44# モジュールを __all__ にセット 45__all__ = [ 46 "__version__", 47 "EddyDataPreprocessor", 48 "SpectrumCalculator", 49 "FigureUtils", 50 "HotspotData", 51 "HotspotType", 52 "FluxFootprintAnalyzer", 53 "CorrectingUtils", 54 "H2OCorrectionConfig", 55 "BiasRemovalConfig", 56 "EmissionData", 57 "HotspotParams", 58 "MobileSpatialAnalyzer", 59 "MSAInputConfig", 60 "MonthlyConverter", 61 "MonthlyFiguresGenerator", 62 "FftFileReorganizer", 63 "TransferFunctionCalculator", 64]
14class EddyDataPreprocessor: 15 # カラム名を定数として定義 16 WIND_U = "edp_wind_u" 17 WIND_V = "edp_wind_v" 18 WIND_W = "edp_wind_w" 19 RAD_WIND_DIR = "edp_rad_wind_dir" 20 RAD_WIND_INC = "edp_rad_wind_inc" 21 DEGREE_WIND_DIR = "edp_degree_wind_dir" 22 DEGREE_WIND_INC = "edp_degree_wind_inc" 23 24 def __init__( 25 self, 26 fs: float = 10, 27 logger: Logger | None = None, 28 logging_debug: bool = False, 29 ): 30 """ 31 渦相関法によって記録されたデータファイルを処理するクラス。 32 33 Parameters 34 ---------- 35 fs (float): サンプリング周波数。 36 logger (Logger | None): 使用するロガー。Noneの場合は新しいロガーを作成します。 37 logging_debug (bool): ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。 38 """ 39 self.fs: float = fs 40 41 # ロガー 42 log_level: int = INFO 43 if logging_debug: 44 log_level = DEBUG 45 self.logger: Logger = EddyDataPreprocessor.setup_logger(logger, log_level) 46 47 def add_uvw_columns( 48 self, 49 df: pd.DataFrame, 50 column_mapping: dict[str, str] = { 51 "u_m": "Ux", 52 "v_m": "Uy", 53 "w_m": "Uz" 54 }, 55 ) -> pd.DataFrame: 56 """ 57 DataFrameに水平風速u、v、鉛直風速wの列を追加する関数。 58 各成分のキーは`edp_wind_u`、`edp_wind_v`、`edp_wind_w`である。 59 60 Parameters 61 ---------- 62 df : pd.DataFrame 63 風速データを含むDataFrame 64 column_mapping : dict[str, str] 65 入力データのカラム名マッピング。 66 キーは"u_m", "v_m", "w_m"で、値は対応する入力データのカラム名。 67 デフォルトは{"u_m": "Ux", "v_m": "Uy", "w_m": "Uz"}。 68 69 Returns 70 ---------- 71 pd.DataFrame 72 水平風速u、v、鉛直風速wの列を追加したDataFrame 73 74 Raises 75 ---------- 76 ValueError 77 必要なカラムが存在しない場合、またはマッピングに必要なキーが不足している場合 78 """ 79 required_keys = ["u_m", "v_m", "w_m"] 80 # マッピングに必要なキーが存在するか確認 81 for key in required_keys: 82 if key not in column_mapping: 83 raise ValueError(f"column_mappingに必要なキー '{key}' が存在しません。") 84 85 # 必要な列がDataFrameに存在するか確認 86 for key, column in column_mapping.items(): 87 if column not in df.columns: 88 raise ValueError(f"必要な列 '{column}' (mapped from '{key}') がDataFrameに存在しません。") 89 90 df_copied: pd.DataFrame = df.copy() 91 # pandasの.valuesを使用してnumpy配列を取得し、その型をnp.ndarrayに明示的にキャストする 92 wind_x_array: np.ndarray = np.array(df_copied[column_mapping["u_m"]].values) 93 wind_y_array: np.ndarray = np.array(df_copied[column_mapping["v_m"]].values) 94 wind_z_array: np.ndarray = np.array(df_copied[column_mapping["w_m"]].values) 95 96 # 平均風向を計算 97 wind_direction: float = EddyDataPreprocessor._wind_direction( 98 wind_x_array, wind_y_array 99 ) 100 101 # 水平方向に座標回転を行u, v成分を求める 102 wind_u_array, wind_v_array = EddyDataPreprocessor._horizontal_wind_speed( 103 wind_x_array, wind_y_array, wind_direction 104 ) 105 wind_w_array: np.ndarray = wind_z_array # wはz成分そのまま 106 107 # u, wから風の迎角を計算 108 wind_inclination: float = EddyDataPreprocessor._wind_inclination( 109 wind_u_array, wind_w_array 110 ) 111 112 # 2回座標回転を行い、u, wを求める 113 wind_u_array_rotated, wind_w_array_rotated = ( 114 EddyDataPreprocessor._vertical_rotation( 115 wind_u_array, wind_w_array, wind_inclination 116 ) 117 ) 118 119 df_copied[self.WIND_U] = wind_u_array_rotated 120 df_copied[self.WIND_V] = wind_v_array 121 df_copied[self.WIND_W] = wind_w_array_rotated 122 df_copied[self.RAD_WIND_DIR] = wind_direction 123 df_copied[self.RAD_WIND_INC] = wind_inclination 124 df_copied[self.DEGREE_WIND_DIR] = np.degrees(wind_direction) 125 df_copied[self.DEGREE_WIND_INC] = np.degrees(wind_inclination) 126 127 return df_copied 128 129 def analyze_lag_times( 130 self, 131 input_dir: str | Path, 132 figsize: tuple[float, float] = (10, 8), 133 input_files_pattern: str = r"Eddy_(\d+)", 134 input_files_suffix: str = ".dat", 135 col1: str = "edp_wind_w", 136 col2_list: list[str] = ["Tv"], 137 median_range: float = 20, 138 output_dir: str | Path | None = None, 139 output_tag: str = "", 140 plot_range_tuple: tuple = (-50, 200), 141 add_title: bool = True, 142 xlabel: str | None = "Seconds", 143 ylabel: str | None = "Frequency", 144 print_results: bool = True, 145 index_column: str = "TIMESTAMP", 146 index_format: str = "%Y-%m-%d %H:%M:%S.%f", 147 resample_in_processing: bool = False, 148 interpolate: bool = True, 149 numeric_columns: list[str] = [ 150 "Ux", 151 "Uy", 152 "Uz", 153 "Tv", 154 "diag_sonic", 155 "CO2_new", 156 "H2O", 157 "diag_irga", 158 "cell_tmpr", 159 "cell_press", 160 "Ultra_CH4_ppm", 161 "Ultra_C2H6_ppb", 162 "Ultra_H2O_ppm", 163 "Ultra_CH4_ppm_C", 164 "Ultra_C2H6_ppb_C", 165 ], 166 metadata_rows: int = 4, 167 skiprows: list[int] = [0, 2, 3], 168 add_uvw_columns: bool = True, 169 uvw_column_mapping: dict[str, str] = { 170 "u_m": "Ux", 171 "v_m": "Uy", 172 "w_m": "Uz" 173 }, 174 ) -> dict[str, float]: 175 """ 176 遅れ時間(ラグ)の統計分析を行い、指定されたディレクトリ内のデータファイルを処理します。 177 解析結果とメタデータはCSVファイルとして出力されます。 178 179 Parameters 180 ---------- 181 input_dir : str | Path 182 入力データファイルが格納されているディレクトリのパス。 183 figsize : tuple[float, float] 184 プロットのサイズ(幅、高さ)。 185 input_files_pattern : str 186 入力ファイル名のパターン(正規表現)。 187 input_files_suffix : str 188 入力ファイルの拡張子。 189 col1 : str 190 基準変数の列名。 191 col2_list : list[str] 192 比較変数の列名のリスト。 193 median_range : float 194 中央値を中心とした範囲。 195 output_dir : str | Path | None 196 出力ディレクトリのパス。Noneの場合は保存しない。 197 output_tag : str 198 出力ファイルに付与するタグ。デフォルトは空文字で、何も付与されない。 199 plot_range_tuple : tuple 200 ヒストグラムの表示範囲。 201 add_title : bool 202 プロットにタイトルを追加するかどうか。デフォルトはTrue。 203 xlabel : str | None 204 x軸のラベル。デフォルトは"Seconds"。 205 ylabel : str | None 206 y軸のラベル。デフォルトは"Frequency"。 207 print_results : bool 208 結果をコンソールに表示するかどうか。 209 resample_in_processing : bool 210 データを遅れ時間の計算中にリサンプリングするかどうか。 211 inputするファイルが既にリサンプリング済みの場合はFalseでよい。 212 デフォルトはFalse。 213 interpolate : bool 214 欠損値の補完を適用するフラグ。デフォルトはTrue。 215 numeric_columns : list[str] 216 数値型に変換する列名のリスト。デフォルトは特定の列名のリスト。 217 metadata_rows : int 218 メタデータの行数。 219 skiprows : list[int] 220 スキップする行番号のリスト。 221 add_uvw_columns : bool 222 u, v, wの列を追加するかどうか。デフォルトはTrue。 223 uvw_column_mapping : dict[str, str] 224 u, v, wの列名をマッピングする辞書。デフォルトは以下の通り。 225 { 226 "u_m": "Ux", 227 "v_m": "Uy", 228 "w_m": "Uz" 229 } 230 231 Returns 232 ---------- 233 dict[str, float] 234 各変数の遅れ時間(平均値を採用)を含む辞書。 235 """ 236 if output_dir is None: 237 self.logger.warn( 238 "output_dirが指定されていません。解析結果を保存する場合は、有効なディレクトリを指定してください。" 239 ) 240 all_lags_indices: list[list[int]] = [] 241 results: dict[str, float] = {} 242 243 # メイン処理 244 # ファイル名に含まれる数字に基づいてソート 245 csv_files = EddyDataPreprocessor._get_sorted_files( 246 input_dir, input_files_pattern, input_files_suffix 247 ) 248 if not csv_files: 249 raise FileNotFoundError( 250 f"There is no '{input_files_suffix}' file to process; input_dir: '{input_dir}', input_files_suffix: '{input_files_suffix}'" 251 ) 252 253 for file in tqdm(csv_files, desc="Calculating"): 254 path: str = os.path.join(input_dir, file) 255 df: pd.DataFrame = {} # 未定義エラーを防止 256 if resample_in_processing: 257 df, _ = self.get_resampled_df( 258 filepath=path, 259 metadata_rows=metadata_rows, 260 skiprows=skiprows, 261 index_column=index_column, 262 index_format=index_format, 263 numeric_columns=numeric_columns, 264 interpolate=interpolate, 265 resample=resample_in_processing, 266 ) 267 else: 268 df = pd.read_csv(path, skiprows=skiprows) 269 if add_uvw_columns: 270 df = self.add_uvw_columns(df=df, column_mapping=uvw_column_mapping) 271 lags_list = EddyDataPreprocessor._calculate_lag_time( 272 df=df, 273 col1=col1, 274 col2_list=col2_list, 275 ) 276 all_lags_indices.append(lags_list) 277 self.logger.info("すべてのCSVファイルにおける遅れ時間が計算されました。") 278 279 # Convert all_lags_indices to a DataFrame 280 lags_indices_df: pd.DataFrame = pd.DataFrame( 281 all_lags_indices, columns=col2_list 282 ) 283 284 # フォーマット用のキーの最大の長さ 285 max_col_name_length: int = max( 286 len(column) for column in lags_indices_df.columns 287 ) 288 289 if print_results: 290 self.logger.info(f"カラム`{col1}`に対する遅れ時間を表示します。") 291 292 # 結果を格納するためのリスト 293 output_data = [] 294 295 for column in lags_indices_df.columns: 296 data: pd.Series = lags_indices_df[column] 297 298 # ヒストグラムの作成 299 fig = plt.figure(figsize=figsize) 300 plt.hist(data, bins=20, range=plot_range_tuple) 301 if add_title: 302 plt.title(f"Delays of {column}") 303 plt.xlabel(xlabel) 304 plt.ylabel(ylabel) 305 plt.xlim(plot_range_tuple) 306 307 # ファイルとして保存するか 308 if output_dir is not None: 309 os.makedirs(output_dir, exist_ok=True) 310 filename: str = f"lags_histogram-{column}{output_tag}.png" 311 filepath: str = os.path.join(output_dir, filename) 312 plt.savefig(filepath, dpi=300, bbox_inches="tight") 313 plt.close(fig=fig) 314 315 # 中央値を計算し、その周辺のデータのみを使用 316 median_value = np.median(data) 317 filtered_data: pd.Series = data[ 318 (data >= median_value - median_range) 319 & (data <= median_value + median_range) 320 ] 321 322 # 平均値を計算 323 mean_value = np.mean(filtered_data) 324 mean_seconds: float = float(mean_value / self.fs) # 統計値を秒に変換 325 results[column] = mean_seconds 326 327 # 結果とメタデータを出力データに追加 328 output_data.append( 329 { 330 "col1": col1, 331 "col2": column, 332 "col2_lag": round(mean_seconds, 2), # 数値として小数点2桁を保持 333 "lag_unit": "s", 334 "median_range": median_range, 335 } 336 ) 337 338 if print_results: 339 print(f"{column:<{max_col_name_length}} : {mean_seconds:.2f} s") 340 341 # 結果をCSVファイルとして出力 342 if output_dir is not None: 343 output_df: pd.DataFrame = pd.DataFrame(output_data) 344 csv_filepath: str = os.path.join( 345 output_dir, f"lags_results{output_tag}.csv" 346 ) 347 output_df.to_csv(csv_filepath, index=False, encoding="utf-8") 348 self.logger.info(f"解析結果をCSVファイルに保存しました: {csv_filepath}") 349 350 return results 351 352 def get_generated_columns_names(self, print: bool = True) -> list[str]: 353 """ 354 クラス内部で生成されるカラム名を取得する。 355 356 Parameters 357 ---------- 358 print : bool 359 print()で表示するか。デフォルトはTrue。 360 361 Returns 362 ---------- 363 list[str] 364 生成されるカラム名のリスト。 365 """ 366 list_cols: list[str] = [ 367 self.WIND_U, 368 self.WIND_V, 369 self.WIND_W, 370 self.RAD_WIND_DIR, 371 self.RAD_WIND_INC, 372 self.DEGREE_WIND_DIR, 373 self.DEGREE_WIND_INC, 374 ] 375 if print: 376 print(list_cols) 377 return list_cols 378 379 def get_resampled_df( 380 self, 381 filepath: str, 382 index_column: str = "TIMESTAMP", 383 index_format: str = "%Y-%m-%d %H:%M:%S.%f", 384 numeric_columns: list[str] = [ 385 "Ux", 386 "Uy", 387 "Uz", 388 "Tv", 389 "diag_sonic", 390 "CO2_new", 391 "H2O", 392 "diag_irga", 393 "cell_tmpr", 394 "cell_press", 395 "Ultra_CH4_ppm", 396 "Ultra_C2H6_ppb", 397 "Ultra_H2O_ppm", 398 "Ultra_CH4_ppm_C", 399 "Ultra_C2H6_ppb_C", 400 ], 401 metadata_rows: int = 4, 402 skiprows: list[int] = [0, 2, 3], 403 resample: bool = True, 404 interpolate: bool = True, 405 ) -> tuple[pd.DataFrame, list[str]]: 406 """ 407 CSVファイルを読み込み、前処理を行う 408 409 前処理の手順は以下の通りです: 410 1. 不要な行を削除する。デフォルト(`skiprows=[0, 2, 3]`)の場合は、2行目をヘッダーとして残し、1、3、4行目が削除される。 411 2. 数値データを float 型に変換する 412 3. TIMESTAMP列をDateTimeインデックスに設定する 413 4. エラー値をNaNに置き換える 414 5. 指定されたサンプリングレートでリサンプリングする 415 6. 欠損値(NaN)を前後の値から線形補間する 416 7. DateTimeインデックスを削除する 417 418 Parameters 419 ---------- 420 filepath : str 421 読み込むCSVファイルのパス 422 index_column : str, optional 423 インデックスに使用する列名。デフォルトは'TIMESTAMP'。 424 index_format : str, optional 425 インデックスの日付形式。デフォルトは'%Y-%m-%d %H:%M:%S.%f'。 426 numeric_columns : list[str], optional 427 数値型に変換する列名のリスト。 428 デフォルトは["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"]。 429 metadata_rows : int, optional 430 メタデータとして読み込む行数。デフォルトは4。 431 skiprows : list[int], optional 432 スキップする行インデックスのリスト。デフォルトは[0, 2, 3]のため、1, 3, 4行目がスキップされる。 433 resample : bool 434 メソッド内でリサンプリング&欠損補間をするか。Falseの場合はfloat変換などの処理のみ適用する。 435 interpolate : bool, optional 436 欠損値の補完を適用するフラグ。デフォルトはTrue。 437 438 Returns 439 ---------- 440 tuple[pd.DataFrame, list[str]] 441 前処理済みのデータフレームとメタデータのリスト。 442 """ 443 # メタデータを読み込む 444 metadata: list[str] = [] 445 with open(filepath, "r") as f: 446 for _ in range(metadata_rows): 447 line = f.readline().strip() 448 metadata.append(line.replace('"', "")) 449 450 # CSVファイルを読み込む 451 df: pd.DataFrame = pd.read_csv(filepath, skiprows=skiprows) 452 453 # 数値データをfloat型に変換する 454 for col in numeric_columns: 455 if col in df.columns: 456 df[col] = pd.to_numeric(df[col], errors="coerce") 457 458 if not resample: 459 # μ秒がない場合は".0"を追加する 460 df[index_column] = df[index_column].apply( 461 lambda x: f"{x}.0" if "." not in x else x 462 ) 463 # TIMESTAMPをDateTimeインデックスに設定する 464 df[index_column] = pd.to_datetime(df[index_column], format=index_format) 465 df = df.set_index(index_column) 466 467 # リサンプリング前の有効数字を取得 468 decimal_places = {} 469 for col in numeric_columns: 470 if col in df.columns: 471 max_decimals = ( 472 df[col].astype(str).str.extract(r"\.(\d+)")[0].str.len().max() 473 ) 474 decimal_places[col] = ( 475 int(max_decimals) if pd.notna(max_decimals) else 0 476 ) 477 478 # リサンプリングを実行 479 resampling_period: int = int(1000 / self.fs) 480 df_resampled: pd.DataFrame = df.resample(f"{resampling_period}ms").mean( 481 numeric_only=True 482 ) 483 484 if interpolate: 485 # 補間を実行 486 df_resampled = df_resampled.interpolate() 487 # 有効数字を調整 488 for col, decimals in decimal_places.items(): 489 if col in df_resampled.columns: 490 df_resampled[col] = df_resampled[col].round(decimals) 491 492 # DateTimeインデックスを削除する 493 df = df_resampled.reset_index() 494 # ミリ秒を1桁にフォーマット 495 df[index_column] = ( 496 df[index_column].dt.strftime("%Y-%m-%d %H:%M:%S.%f").str[:-5] 497 ) 498 499 return df, metadata 500 501 def output_resampled_data( 502 self, 503 input_dir: str, 504 resampled_dir: str, 505 c2c1_ratio_dir: str, 506 input_file_pattern: str = r"Eddy_(\d+)", 507 input_files_suffix: str = ".dat", 508 col_c1: str = "Ultra_CH4_ppm_C", 509 col_c2: str = "Ultra_C2H6_ppb", 510 output_c2c1_ratio: bool = True, 511 output_resampled: bool = True, 512 c2c1_ratio_csv_prefix: str = "SAC.Ultra", 513 index_column: str = "TIMESTAMP", 514 index_format: str = "%Y-%m-%d %H:%M:%S.%f", 515 resample: bool = True, 516 interpolate: bool = True, 517 numeric_columns: list[str] = [ 518 "Ux", 519 "Uy", 520 "Uz", 521 "Tv", 522 "diag_sonic", 523 "CO2_new", 524 "H2O", 525 "diag_irga", 526 "cell_tmpr", 527 "cell_press", 528 "Ultra_CH4_ppm", 529 "Ultra_C2H6_ppb", 530 "Ultra_H2O_ppm", 531 "Ultra_CH4_ppm_C", 532 "Ultra_C2H6_ppb_C", 533 ], 534 metadata_rows: int = 4, 535 skiprows: list[int] = [0, 2, 3], 536 ) -> None: 537 """ 538 指定されたディレクトリ内のCSVファイルを処理し、リサンプリングと欠損値補間を行います。 539 540 このメソッドは、指定されたディレクトリ内のCSVファイルを読み込み、リサンプリングを行い、 541 欠損値を補完します。処理結果として、リサンプリングされたCSVファイルを出力し、 542 相関係数やC2H6/CH4比を計算してDataFrameに保存します。 543 リサンプリングと欠損値補完は`get_resampled_df`と同様のロジックを使用します。 544 545 Parameters 546 ---------- 547 input_dir : str 548 入力CSVファイルが格納されているディレクトリのパス。 549 resampled_dir : str 550 リサンプリングされたCSVファイルを出力するディレクトリのパス。 551 c2c1_ratio_dir : str 552 計算結果を保存するディレクトリのパス。 553 input_file_pattern : str 554 ファイル名からソートキーを抽出する正規表現パターン。デフォルトでは、最初の数字グループでソートします。 555 input_files_suffix : str 556 入力ファイルの拡張子(.datや.csvなど)。デフォルトは".dat"。 557 col_c1 : str 558 CH4濃度を含む列名。デフォルトは'Ultra_CH4_ppm_C'。 559 col_c2 : str 560 C2H6濃度を含む列名。デフォルトは'Ultra_C2H6_ppb'。 561 output_c2c1_ratio : bool, optional 562 線形回帰を行うかどうか。デフォルトはTrue。 563 output_resampled : bool, optional 564 リサンプリングされたCSVファイルを出力するかどうか。デフォルトはTrue。 565 c2c1_ratio_csv_prefix : str 566 出力ファイルの接頭辞。デフォルトは'SAC.Ultra'で、出力時は'SAC.Ultra.2024.09.21.ratio.csv'のような形式となる。 567 index_column : str 568 日時情報を含む列名。デフォルトは'TIMESTAMP'。 569 index_format : str, optional 570 インデックスの日付形式。デフォルトは'%Y-%m-%d %H:%M:%S.%f'。 571 resample : bool 572 リサンプリングを行うかどうか。デフォルトはTrue。 573 interpolate : bool 574 欠損値補間を行うかどうか。デフォルトはTrue。 575 numeric_columns : list[str] 576 数値データを含む列名のリスト。デフォルトは指定された列名のリスト。 577 metadata_rows : int 578 メタデータとして読み込む行数。デフォルトは4。 579 skiprows : list[int] 580 読み飛ばす行のインデックスリスト。デフォルトは[0, 2, 3]。 581 582 Raises 583 ---------- 584 OSError 585 ディレクトリの作成に失敗した場合。 586 FileNotFoundError 587 入力ファイルが見つからない場合。 588 ValueError 589 出力ディレクトリが指定されていない、またはデータの処理中にエラーが発生した場合。 590 """ 591 # 出力オプションとディレクトリの検証 592 if output_resampled and resampled_dir is None: 593 raise ValueError( 594 "output_resampled が True の場合、resampled_dir を指定する必要があります" 595 ) 596 if output_c2c1_ratio and c2c1_ratio_dir is None: 597 raise ValueError( 598 "output_c2c1_ratio が True の場合、c2c1_ratio_dir を指定する必要があります" 599 ) 600 601 # ディレクトリの作成(必要な場合のみ) 602 if output_resampled: 603 os.makedirs(resampled_dir, exist_ok=True) 604 if output_c2c1_ratio: 605 os.makedirs(c2c1_ratio_dir, exist_ok=True) 606 607 ratio_data: list[dict[str, str | float]] = [] 608 latest_date: datetime = datetime.min 609 610 # csvファイル名のリスト 611 csv_files: list[str] = EddyDataPreprocessor._get_sorted_files( 612 input_dir, input_file_pattern, input_files_suffix 613 ) 614 615 for filename in tqdm(csv_files, desc="Processing files"): 616 input_filepath: str = os.path.join(input_dir, filename) 617 # リサンプリング&欠損値補間 618 df, metadata = self.get_resampled_df( 619 filepath=input_filepath, 620 index_column=index_column, 621 index_format=index_format, 622 interpolate=interpolate, 623 resample=resample, 624 numeric_columns=numeric_columns, 625 metadata_rows=metadata_rows, 626 skiprows=skiprows, 627 ) 628 629 # 開始時間を取得 630 start_time: datetime = pd.to_datetime(df[index_column].iloc[0]) 631 # 処理したファイルの中で最も最新の日付 632 latest_date = max(latest_date, start_time) 633 634 # リサンプリング&欠損値補間したCSVを出力 635 if output_resampled: 636 base_filename: str = re.sub(rf"\{input_files_suffix}$", "", filename) 637 output_csv_path: str = os.path.join( 638 resampled_dir, f"{base_filename}-resampled.csv" 639 ) 640 # メタデータを先に書き込む 641 with open(output_csv_path, "w") as f: 642 for line in metadata: 643 f.write(f"{line}\n") 644 # データフレームを追記モードで書き込む 645 df.to_csv( 646 output_csv_path, index=False, mode="a", quoting=3, header=False 647 ) 648 649 # 相関係数とC2H6/CH4比を計算 650 if output_c2c1_ratio: 651 ch4_data: pd.Series = df[col_c1] 652 c2h6_data: pd.Series = df[col_c2] 653 654 ratio_row: dict[str, str | float] = { 655 "Date": start_time.strftime("%Y-%m-%d %H:%M:%S.%f"), 656 "slope": f"{np.nan}", 657 "intercept": f"{np.nan}", 658 "r_value": f"{np.nan}", 659 "p_value": f"{np.nan}", 660 "stderr": f"{np.nan}", 661 } 662 # 近似直線の傾き、切片、相関係数を計算 663 try: 664 slope, intercept, r_value, p_value, stderr = stats.linregress( 665 ch4_data, c2h6_data 666 ) 667 ratio_row: dict[str, str | float] = { 668 "Date": start_time.strftime("%Y-%m-%d %H:%M:%S.%f"), 669 "slope": f"{slope:.6f}", 670 "intercept": f"{intercept:.6f}", 671 "r_value": f"{r_value:.6f}", 672 "p_value": f"{p_value:.6f}", 673 "stderr": f"{stderr:.6f}", 674 } 675 except Exception: 676 # 何もせず、デフォルトの ratio_row を使用する 677 pass 678 679 # 結果をリストに追加 680 ratio_data.append(ratio_row) 681 682 if output_c2c1_ratio: 683 # DataFrameを作成し、Dateカラムで昇順ソート 684 ratio_df: pd.DataFrame = pd.DataFrame(ratio_data) 685 ratio_df["Date"] = pd.to_datetime( 686 ratio_df["Date"] 687 ) # Dateカラムをdatetime型に変換 688 ratio_df = ratio_df.sort_values("Date") # Dateカラムで昇順ソート 689 690 # CSVとして保存 691 ratio_filename: str = ( 692 f"{c2c1_ratio_csv_prefix}.{latest_date.strftime('%Y.%m.%d')}.ratio.csv" 693 ) 694 ratio_path: str = os.path.join(c2c1_ratio_dir, ratio_filename) 695 ratio_df.to_csv(ratio_path, index=False) 696 697 @staticmethod 698 def _calculate_lag_time( 699 df: pd.DataFrame, 700 col1: str, 701 col2_list: list[str], 702 ) -> list[int]: 703 """ 704 指定された基準変数(col1)と比較変数のリスト(col2_list)の間の遅れ時間(ディレイ)を計算する。 705 周波数が10Hzでcol1がcol2より10.0秒遅れている場合は、+100がインデックスとして取得される 706 707 Parameters 708 ---------- 709 df : pd.DataFrame 710 遅れ時間の計算に使用するデータフレーム 711 col1 : str 712 基準変数の列名 713 col2_list : list[str] 714 比較変数の列名のリスト 715 716 Returns 717 ---------- 718 list[int] 719 各比較変数に対する遅れ時間(ディレイ)のリスト 720 """ 721 lags_list: list[int] = [] 722 for col2 in col2_list: 723 data1: np.ndarray = np.array(df[col1].values) 724 data2: np.ndarray = np.array(df[col2].values) 725 726 # 平均を0に調整 727 data1 = data1 - data1.mean() 728 data2 = data2 - data2.mean() 729 730 data_length: int = len(data1) 731 732 # 相互相関の計算 733 correlation: np.ndarray = np.correlate( 734 data1, data2, mode="full" 735 ) # data2とdata1の順序を入れ替え 736 737 # 相互相関のピークのインデックスを取得 738 lag: int = int((data_length - 1) - correlation.argmax()) # 符号を反転 739 740 lags_list.append(lag) 741 return lags_list 742 743 @staticmethod 744 def _get_sorted_files(directory: str, pattern: str, suffix: str) -> list[str]: 745 """ 746 指定されたディレクトリ内のファイルを、ファイル名に含まれる数字に基づいてソートして返す。 747 748 Parameters 749 ---------- 750 directory : str 751 ファイルが格納されているディレクトリのパス 752 pattern : str 753 ファイル名からソートキーを抽出する正規表現パターン 754 suffix : str 755 ファイルの拡張子 756 757 Returns 758 ---------- 759 list[str] 760 ソートされたファイル名のリスト 761 """ 762 files: list[str] = [f for f in os.listdir(directory) if f.endswith(suffix)] 763 files = [f for f in files if re.search(pattern, f)] 764 files.sort( 765 key=lambda x: int(re.search(pattern, x).group(1)) # type:ignore 766 if re.search(pattern, x) 767 else float("inf") 768 ) 769 return files 770 771 @staticmethod 772 def _horizontal_wind_speed( 773 x_array: np.ndarray, y_array: np.ndarray, wind_dir: float 774 ) -> tuple[np.ndarray, np.ndarray]: 775 """ 776 風速のu成分とv成分を計算する関数 777 778 Parameters 779 ---------- 780 x_array : numpy.ndarray 781 x方向の風速成分の配列 782 y_array : numpy.ndarray 783 y方向の風速成分の配列 784 wind_dir : float 785 水平成分の風向(ラジアン) 786 787 Returns 788 ---------- 789 tuple[numpy.ndarray, numpy.ndarray] 790 u成分とv成分のタプル 791 """ 792 # スカラー風速の計算 793 scalar_hypotenuse: np.ndarray = np.sqrt(x_array**2 + y_array**2) 794 # CSAT3では以下の補正が必要 795 instantaneous_wind_directions = EddyDataPreprocessor._wind_direction( 796 x_array=x_array, y_array=y_array 797 ) 798 # ベクトル風速の計算 799 vector_u: np.ndarray = scalar_hypotenuse * np.cos( 800 instantaneous_wind_directions - wind_dir 801 ) 802 vector_v: np.ndarray = scalar_hypotenuse * np.sin( 803 instantaneous_wind_directions - wind_dir 804 ) 805 return vector_u, vector_v 806 807 @staticmethod 808 def setup_logger(logger: Logger | None, log_level: int = INFO) -> Logger: 809 """ 810 ロガーを設定します。 811 812 このメソッドは、ロギングの設定を行い、ログメッセージのフォーマットを指定します。 813 ログメッセージには、日付、ログレベル、メッセージが含まれます。 814 815 渡されたロガーがNoneまたは不正な場合は、新たにロガーを作成し、標準出力に 816 ログメッセージが表示されるようにStreamHandlerを追加します。ロガーのレベルは 817 引数で指定されたlog_levelに基づいて設定されます。 818 819 Parameters 820 ---------- 821 logger : Logger | None 822 使用するロガー。Noneの場合は新しいロガーを作成します。 823 log_level : int 824 ロガーのログレベル。デフォルトはINFO。 825 826 Returns 827 ---------- 828 Logger 829 設定されたロガーオブジェクト。 830 """ 831 if logger is not None and isinstance(logger, Logger): 832 return logger 833 # 渡されたロガーがNoneまたは正しいものでない場合は独自に設定 834 new_logger: Logger = getLogger() 835 # 既存のハンドラーをすべて削除 836 for handler in new_logger.handlers[:]: 837 new_logger.removeHandler(handler) 838 new_logger.setLevel(log_level) # ロガーのレベルを設定 839 ch = StreamHandler() 840 ch_formatter = Formatter("%(asctime)s - %(levelname)s - %(message)s") 841 ch.setFormatter(ch_formatter) # フォーマッターをハンドラーに設定 842 new_logger.addHandler(ch) # StreamHandlerの追加 843 return new_logger 844 845 @staticmethod 846 def _vertical_rotation( 847 u_array: np.ndarray, 848 w_array: np.ndarray, 849 wind_inc: float, 850 ) -> tuple[np.ndarray, np.ndarray]: 851 """ 852 鉛直方向の座標回転を行い、u, wを求める関数 853 854 Parameters 855 ---------- 856 u_array (numpy.ndarray): u方向の風速 857 w_array (numpy.ndarray): w方向の風速 858 wind_inc (float): 平均風向に対する迎角(ラジアン) 859 860 Returns 861 ---------- 862 tuple[numpy.ndarray, numpy.ndarray]: 回転後のu, w 863 """ 864 # 迎角を用いて鉛直方向に座標回転 865 u_rotated = u_array * np.cos(wind_inc) + w_array * np.sin(wind_inc) 866 w_rotated = w_array * np.cos(wind_inc) - u_array * np.sin(wind_inc) 867 return u_rotated, w_rotated 868 869 @staticmethod 870 def _wind_direction( 871 x_array: np.ndarray, y_array: np.ndarray, correction_angle: float = 0.0 872 ) -> float: 873 """ 874 水平方向の平均風向を計算する関数 875 876 Parameters 877 ---------- 878 x_array (numpy.ndarray): 西方向の風速成分 879 y_array (numpy.ndarray): 南北方向の風速成分 880 correction_angle (float): 風向補正角度(ラジアン)。デフォルトは0.0。CSAT3の場合は0.0を指定。 881 882 Returns 883 ---------- 884 wind_direction (float): 風向 (radians) 885 """ 886 wind_direction: float = np.arctan2(np.mean(y_array), np.mean(x_array)) 887 # 補正角度を適用 888 wind_direction = correction_angle - wind_direction 889 return wind_direction 890 891 @staticmethod 892 def _wind_inclination(u_array: np.ndarray, w_array: np.ndarray) -> float: 893 """ 894 平均風向に対する迎角を計算する関数 895 896 Parameters 897 ---------- 898 u_array (numpy.ndarray): u方向の瞬間風速 899 w_array (numpy.ndarray): w方向の瞬間風速 900 901 Returns 902 ---------- 903 wind_inc (float): 平均風向に対する迎角(ラジアン) 904 """ 905 wind_inc: float = np.arctan2(np.mean(w_array), np.mean(u_array)) 906 return wind_inc
24 def __init__( 25 self, 26 fs: float = 10, 27 logger: Logger | None = None, 28 logging_debug: bool = False, 29 ): 30 """ 31 渦相関法によって記録されたデータファイルを処理するクラス。 32 33 Parameters 34 ---------- 35 fs (float): サンプリング周波数。 36 logger (Logger | None): 使用するロガー。Noneの場合は新しいロガーを作成します。 37 logging_debug (bool): ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。 38 """ 39 self.fs: float = fs 40 41 # ロガー 42 log_level: int = INFO 43 if logging_debug: 44 log_level = DEBUG 45 self.logger: Logger = EddyDataPreprocessor.setup_logger(logger, log_level)
渦相関法によって記録されたデータファイルを処理するクラス。
Parameters
fs (float): サンプリング周波数。
logger (Logger | None): 使用するロガー。Noneの場合は新しいロガーを作成します。
logging_debug (bool): ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。
47 def add_uvw_columns( 48 self, 49 df: pd.DataFrame, 50 column_mapping: dict[str, str] = { 51 "u_m": "Ux", 52 "v_m": "Uy", 53 "w_m": "Uz" 54 }, 55 ) -> pd.DataFrame: 56 """ 57 DataFrameに水平風速u、v、鉛直風速wの列を追加する関数。 58 各成分のキーは`edp_wind_u`、`edp_wind_v`、`edp_wind_w`である。 59 60 Parameters 61 ---------- 62 df : pd.DataFrame 63 風速データを含むDataFrame 64 column_mapping : dict[str, str] 65 入力データのカラム名マッピング。 66 キーは"u_m", "v_m", "w_m"で、値は対応する入力データのカラム名。 67 デフォルトは{"u_m": "Ux", "v_m": "Uy", "w_m": "Uz"}。 68 69 Returns 70 ---------- 71 pd.DataFrame 72 水平風速u、v、鉛直風速wの列を追加したDataFrame 73 74 Raises 75 ---------- 76 ValueError 77 必要なカラムが存在しない場合、またはマッピングに必要なキーが不足している場合 78 """ 79 required_keys = ["u_m", "v_m", "w_m"] 80 # マッピングに必要なキーが存在するか確認 81 for key in required_keys: 82 if key not in column_mapping: 83 raise ValueError(f"column_mappingに必要なキー '{key}' が存在しません。") 84 85 # 必要な列がDataFrameに存在するか確認 86 for key, column in column_mapping.items(): 87 if column not in df.columns: 88 raise ValueError(f"必要な列 '{column}' (mapped from '{key}') がDataFrameに存在しません。") 89 90 df_copied: pd.DataFrame = df.copy() 91 # pandasの.valuesを使用してnumpy配列を取得し、その型をnp.ndarrayに明示的にキャストする 92 wind_x_array: np.ndarray = np.array(df_copied[column_mapping["u_m"]].values) 93 wind_y_array: np.ndarray = np.array(df_copied[column_mapping["v_m"]].values) 94 wind_z_array: np.ndarray = np.array(df_copied[column_mapping["w_m"]].values) 95 96 # 平均風向を計算 97 wind_direction: float = EddyDataPreprocessor._wind_direction( 98 wind_x_array, wind_y_array 99 ) 100 101 # 水平方向に座標回転を行u, v成分を求める 102 wind_u_array, wind_v_array = EddyDataPreprocessor._horizontal_wind_speed( 103 wind_x_array, wind_y_array, wind_direction 104 ) 105 wind_w_array: np.ndarray = wind_z_array # wはz成分そのまま 106 107 # u, wから風の迎角を計算 108 wind_inclination: float = EddyDataPreprocessor._wind_inclination( 109 wind_u_array, wind_w_array 110 ) 111 112 # 2回座標回転を行い、u, wを求める 113 wind_u_array_rotated, wind_w_array_rotated = ( 114 EddyDataPreprocessor._vertical_rotation( 115 wind_u_array, wind_w_array, wind_inclination 116 ) 117 ) 118 119 df_copied[self.WIND_U] = wind_u_array_rotated 120 df_copied[self.WIND_V] = wind_v_array 121 df_copied[self.WIND_W] = wind_w_array_rotated 122 df_copied[self.RAD_WIND_DIR] = wind_direction 123 df_copied[self.RAD_WIND_INC] = wind_inclination 124 df_copied[self.DEGREE_WIND_DIR] = np.degrees(wind_direction) 125 df_copied[self.DEGREE_WIND_INC] = np.degrees(wind_inclination) 126 127 return df_copied
DataFrameに水平風速u、v、鉛直風速wの列を追加する関数。
各成分のキーはedp_wind_u
、edp_wind_v
、edp_wind_w
である。
Parameters
df : pd.DataFrame
風速データを含むDataFrame
column_mapping : dict[str, str]
入力データのカラム名マッピング。
キーは"u_m", "v_m", "w_m"で、値は対応する入力データのカラム名。
デフォルトは{"u_m": "Ux", "v_m": "Uy", "w_m": "Uz"}。
Returns
pd.DataFrame
水平風速u、v、鉛直風速wの列を追加したDataFrame
Raises
ValueError
必要なカラムが存在しない場合、またはマッピングに必要なキーが不足している場合
129 def analyze_lag_times( 130 self, 131 input_dir: str | Path, 132 figsize: tuple[float, float] = (10, 8), 133 input_files_pattern: str = r"Eddy_(\d+)", 134 input_files_suffix: str = ".dat", 135 col1: str = "edp_wind_w", 136 col2_list: list[str] = ["Tv"], 137 median_range: float = 20, 138 output_dir: str | Path | None = None, 139 output_tag: str = "", 140 plot_range_tuple: tuple = (-50, 200), 141 add_title: bool = True, 142 xlabel: str | None = "Seconds", 143 ylabel: str | None = "Frequency", 144 print_results: bool = True, 145 index_column: str = "TIMESTAMP", 146 index_format: str = "%Y-%m-%d %H:%M:%S.%f", 147 resample_in_processing: bool = False, 148 interpolate: bool = True, 149 numeric_columns: list[str] = [ 150 "Ux", 151 "Uy", 152 "Uz", 153 "Tv", 154 "diag_sonic", 155 "CO2_new", 156 "H2O", 157 "diag_irga", 158 "cell_tmpr", 159 "cell_press", 160 "Ultra_CH4_ppm", 161 "Ultra_C2H6_ppb", 162 "Ultra_H2O_ppm", 163 "Ultra_CH4_ppm_C", 164 "Ultra_C2H6_ppb_C", 165 ], 166 metadata_rows: int = 4, 167 skiprows: list[int] = [0, 2, 3], 168 add_uvw_columns: bool = True, 169 uvw_column_mapping: dict[str, str] = { 170 "u_m": "Ux", 171 "v_m": "Uy", 172 "w_m": "Uz" 173 }, 174 ) -> dict[str, float]: 175 """ 176 遅れ時間(ラグ)の統計分析を行い、指定されたディレクトリ内のデータファイルを処理します。 177 解析結果とメタデータはCSVファイルとして出力されます。 178 179 Parameters 180 ---------- 181 input_dir : str | Path 182 入力データファイルが格納されているディレクトリのパス。 183 figsize : tuple[float, float] 184 プロットのサイズ(幅、高さ)。 185 input_files_pattern : str 186 入力ファイル名のパターン(正規表現)。 187 input_files_suffix : str 188 入力ファイルの拡張子。 189 col1 : str 190 基準変数の列名。 191 col2_list : list[str] 192 比較変数の列名のリスト。 193 median_range : float 194 中央値を中心とした範囲。 195 output_dir : str | Path | None 196 出力ディレクトリのパス。Noneの場合は保存しない。 197 output_tag : str 198 出力ファイルに付与するタグ。デフォルトは空文字で、何も付与されない。 199 plot_range_tuple : tuple 200 ヒストグラムの表示範囲。 201 add_title : bool 202 プロットにタイトルを追加するかどうか。デフォルトはTrue。 203 xlabel : str | None 204 x軸のラベル。デフォルトは"Seconds"。 205 ylabel : str | None 206 y軸のラベル。デフォルトは"Frequency"。 207 print_results : bool 208 結果をコンソールに表示するかどうか。 209 resample_in_processing : bool 210 データを遅れ時間の計算中にリサンプリングするかどうか。 211 inputするファイルが既にリサンプリング済みの場合はFalseでよい。 212 デフォルトはFalse。 213 interpolate : bool 214 欠損値の補完を適用するフラグ。デフォルトはTrue。 215 numeric_columns : list[str] 216 数値型に変換する列名のリスト。デフォルトは特定の列名のリスト。 217 metadata_rows : int 218 メタデータの行数。 219 skiprows : list[int] 220 スキップする行番号のリスト。 221 add_uvw_columns : bool 222 u, v, wの列を追加するかどうか。デフォルトはTrue。 223 uvw_column_mapping : dict[str, str] 224 u, v, wの列名をマッピングする辞書。デフォルトは以下の通り。 225 { 226 "u_m": "Ux", 227 "v_m": "Uy", 228 "w_m": "Uz" 229 } 230 231 Returns 232 ---------- 233 dict[str, float] 234 各変数の遅れ時間(平均値を採用)を含む辞書。 235 """ 236 if output_dir is None: 237 self.logger.warn( 238 "output_dirが指定されていません。解析結果を保存する場合は、有効なディレクトリを指定してください。" 239 ) 240 all_lags_indices: list[list[int]] = [] 241 results: dict[str, float] = {} 242 243 # メイン処理 244 # ファイル名に含まれる数字に基づいてソート 245 csv_files = EddyDataPreprocessor._get_sorted_files( 246 input_dir, input_files_pattern, input_files_suffix 247 ) 248 if not csv_files: 249 raise FileNotFoundError( 250 f"There is no '{input_files_suffix}' file to process; input_dir: '{input_dir}', input_files_suffix: '{input_files_suffix}'" 251 ) 252 253 for file in tqdm(csv_files, desc="Calculating"): 254 path: str = os.path.join(input_dir, file) 255 df: pd.DataFrame = {} # 未定義エラーを防止 256 if resample_in_processing: 257 df, _ = self.get_resampled_df( 258 filepath=path, 259 metadata_rows=metadata_rows, 260 skiprows=skiprows, 261 index_column=index_column, 262 index_format=index_format, 263 numeric_columns=numeric_columns, 264 interpolate=interpolate, 265 resample=resample_in_processing, 266 ) 267 else: 268 df = pd.read_csv(path, skiprows=skiprows) 269 if add_uvw_columns: 270 df = self.add_uvw_columns(df=df, column_mapping=uvw_column_mapping) 271 lags_list = EddyDataPreprocessor._calculate_lag_time( 272 df=df, 273 col1=col1, 274 col2_list=col2_list, 275 ) 276 all_lags_indices.append(lags_list) 277 self.logger.info("すべてのCSVファイルにおける遅れ時間が計算されました。") 278 279 # Convert all_lags_indices to a DataFrame 280 lags_indices_df: pd.DataFrame = pd.DataFrame( 281 all_lags_indices, columns=col2_list 282 ) 283 284 # フォーマット用のキーの最大の長さ 285 max_col_name_length: int = max( 286 len(column) for column in lags_indices_df.columns 287 ) 288 289 if print_results: 290 self.logger.info(f"カラム`{col1}`に対する遅れ時間を表示します。") 291 292 # 結果を格納するためのリスト 293 output_data = [] 294 295 for column in lags_indices_df.columns: 296 data: pd.Series = lags_indices_df[column] 297 298 # ヒストグラムの作成 299 fig = plt.figure(figsize=figsize) 300 plt.hist(data, bins=20, range=plot_range_tuple) 301 if add_title: 302 plt.title(f"Delays of {column}") 303 plt.xlabel(xlabel) 304 plt.ylabel(ylabel) 305 plt.xlim(plot_range_tuple) 306 307 # ファイルとして保存するか 308 if output_dir is not None: 309 os.makedirs(output_dir, exist_ok=True) 310 filename: str = f"lags_histogram-{column}{output_tag}.png" 311 filepath: str = os.path.join(output_dir, filename) 312 plt.savefig(filepath, dpi=300, bbox_inches="tight") 313 plt.close(fig=fig) 314 315 # 中央値を計算し、その周辺のデータのみを使用 316 median_value = np.median(data) 317 filtered_data: pd.Series = data[ 318 (data >= median_value - median_range) 319 & (data <= median_value + median_range) 320 ] 321 322 # 平均値を計算 323 mean_value = np.mean(filtered_data) 324 mean_seconds: float = float(mean_value / self.fs) # 統計値を秒に変換 325 results[column] = mean_seconds 326 327 # 結果とメタデータを出力データに追加 328 output_data.append( 329 { 330 "col1": col1, 331 "col2": column, 332 "col2_lag": round(mean_seconds, 2), # 数値として小数点2桁を保持 333 "lag_unit": "s", 334 "median_range": median_range, 335 } 336 ) 337 338 if print_results: 339 print(f"{column:<{max_col_name_length}} : {mean_seconds:.2f} s") 340 341 # 結果をCSVファイルとして出力 342 if output_dir is not None: 343 output_df: pd.DataFrame = pd.DataFrame(output_data) 344 csv_filepath: str = os.path.join( 345 output_dir, f"lags_results{output_tag}.csv" 346 ) 347 output_df.to_csv(csv_filepath, index=False, encoding="utf-8") 348 self.logger.info(f"解析結果をCSVファイルに保存しました: {csv_filepath}") 349 350 return results
遅れ時間(ラグ)の統計分析を行い、指定されたディレクトリ内のデータファイルを処理します。 解析結果とメタデータはCSVファイルとして出力されます。
Parameters
input_dir : str | Path
入力データファイルが格納されているディレクトリのパス。
figsize : tuple[float, float]
プロットのサイズ(幅、高さ)。
input_files_pattern : str
入力ファイル名のパターン(正規表現)。
input_files_suffix : str
入力ファイルの拡張子。
col1 : str
基準変数の列名。
col2_list : list[str]
比較変数の列名のリスト。
median_range : float
中央値を中心とした範囲。
output_dir : str | Path | None
出力ディレクトリのパス。Noneの場合は保存しない。
output_tag : str
出力ファイルに付与するタグ。デフォルトは空文字で、何も付与されない。
plot_range_tuple : tuple
ヒストグラムの表示範囲。
add_title : bool
プロットにタイトルを追加するかどうか。デフォルトはTrue。
xlabel : str | None
x軸のラベル。デフォルトは"Seconds"。
ylabel : str | None
y軸のラベル。デフォルトは"Frequency"。
print_results : bool
結果をコンソールに表示するかどうか。
resample_in_processing : bool
データを遅れ時間の計算中にリサンプリングするかどうか。
inputするファイルが既にリサンプリング済みの場合はFalseでよい。
デフォルトはFalse。
interpolate : bool
欠損値の補完を適用するフラグ。デフォルトはTrue。
numeric_columns : list[str]
数値型に変換する列名のリスト。デフォルトは特定の列名のリスト。
metadata_rows : int
メタデータの行数。
skiprows : list[int]
スキップする行番号のリスト。
add_uvw_columns : bool
u, v, wの列を追加するかどうか。デフォルトはTrue。
uvw_column_mapping : dict[str, str]
u, v, wの列名をマッピングする辞書。デフォルトは以下の通り。
{
"u_m": "Ux",
"v_m": "Uy",
"w_m": "Uz"
}
Returns
dict[str, float]
各変数の遅れ時間(平均値を採用)を含む辞書。
352 def get_generated_columns_names(self, print: bool = True) -> list[str]: 353 """ 354 クラス内部で生成されるカラム名を取得する。 355 356 Parameters 357 ---------- 358 print : bool 359 print()で表示するか。デフォルトはTrue。 360 361 Returns 362 ---------- 363 list[str] 364 生成されるカラム名のリスト。 365 """ 366 list_cols: list[str] = [ 367 self.WIND_U, 368 self.WIND_V, 369 self.WIND_W, 370 self.RAD_WIND_DIR, 371 self.RAD_WIND_INC, 372 self.DEGREE_WIND_DIR, 373 self.DEGREE_WIND_INC, 374 ] 375 if print: 376 print(list_cols) 377 return list_cols
クラス内部で生成されるカラム名を取得する。
Parameters
print : bool
print()で表示するか。デフォルトはTrue。
Returns
list[str]
生成されるカラム名のリスト。
379 def get_resampled_df( 380 self, 381 filepath: str, 382 index_column: str = "TIMESTAMP", 383 index_format: str = "%Y-%m-%d %H:%M:%S.%f", 384 numeric_columns: list[str] = [ 385 "Ux", 386 "Uy", 387 "Uz", 388 "Tv", 389 "diag_sonic", 390 "CO2_new", 391 "H2O", 392 "diag_irga", 393 "cell_tmpr", 394 "cell_press", 395 "Ultra_CH4_ppm", 396 "Ultra_C2H6_ppb", 397 "Ultra_H2O_ppm", 398 "Ultra_CH4_ppm_C", 399 "Ultra_C2H6_ppb_C", 400 ], 401 metadata_rows: int = 4, 402 skiprows: list[int] = [0, 2, 3], 403 resample: bool = True, 404 interpolate: bool = True, 405 ) -> tuple[pd.DataFrame, list[str]]: 406 """ 407 CSVファイルを読み込み、前処理を行う 408 409 前処理の手順は以下の通りです: 410 1. 不要な行を削除する。デフォルト(`skiprows=[0, 2, 3]`)の場合は、2行目をヘッダーとして残し、1、3、4行目が削除される。 411 2. 数値データを float 型に変換する 412 3. TIMESTAMP列をDateTimeインデックスに設定する 413 4. エラー値をNaNに置き換える 414 5. 指定されたサンプリングレートでリサンプリングする 415 6. 欠損値(NaN)を前後の値から線形補間する 416 7. DateTimeインデックスを削除する 417 418 Parameters 419 ---------- 420 filepath : str 421 読み込むCSVファイルのパス 422 index_column : str, optional 423 インデックスに使用する列名。デフォルトは'TIMESTAMP'。 424 index_format : str, optional 425 インデックスの日付形式。デフォルトは'%Y-%m-%d %H:%M:%S.%f'。 426 numeric_columns : list[str], optional 427 数値型に変換する列名のリスト。 428 デフォルトは["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"]。 429 metadata_rows : int, optional 430 メタデータとして読み込む行数。デフォルトは4。 431 skiprows : list[int], optional 432 スキップする行インデックスのリスト。デフォルトは[0, 2, 3]のため、1, 3, 4行目がスキップされる。 433 resample : bool 434 メソッド内でリサンプリング&欠損補間をするか。Falseの場合はfloat変換などの処理のみ適用する。 435 interpolate : bool, optional 436 欠損値の補完を適用するフラグ。デフォルトはTrue。 437 438 Returns 439 ---------- 440 tuple[pd.DataFrame, list[str]] 441 前処理済みのデータフレームとメタデータのリスト。 442 """ 443 # メタデータを読み込む 444 metadata: list[str] = [] 445 with open(filepath, "r") as f: 446 for _ in range(metadata_rows): 447 line = f.readline().strip() 448 metadata.append(line.replace('"', "")) 449 450 # CSVファイルを読み込む 451 df: pd.DataFrame = pd.read_csv(filepath, skiprows=skiprows) 452 453 # 数値データをfloat型に変換する 454 for col in numeric_columns: 455 if col in df.columns: 456 df[col] = pd.to_numeric(df[col], errors="coerce") 457 458 if not resample: 459 # μ秒がない場合は".0"を追加する 460 df[index_column] = df[index_column].apply( 461 lambda x: f"{x}.0" if "." not in x else x 462 ) 463 # TIMESTAMPをDateTimeインデックスに設定する 464 df[index_column] = pd.to_datetime(df[index_column], format=index_format) 465 df = df.set_index(index_column) 466 467 # リサンプリング前の有効数字を取得 468 decimal_places = {} 469 for col in numeric_columns: 470 if col in df.columns: 471 max_decimals = ( 472 df[col].astype(str).str.extract(r"\.(\d+)")[0].str.len().max() 473 ) 474 decimal_places[col] = ( 475 int(max_decimals) if pd.notna(max_decimals) else 0 476 ) 477 478 # リサンプリングを実行 479 resampling_period: int = int(1000 / self.fs) 480 df_resampled: pd.DataFrame = df.resample(f"{resampling_period}ms").mean( 481 numeric_only=True 482 ) 483 484 if interpolate: 485 # 補間を実行 486 df_resampled = df_resampled.interpolate() 487 # 有効数字を調整 488 for col, decimals in decimal_places.items(): 489 if col in df_resampled.columns: 490 df_resampled[col] = df_resampled[col].round(decimals) 491 492 # DateTimeインデックスを削除する 493 df = df_resampled.reset_index() 494 # ミリ秒を1桁にフォーマット 495 df[index_column] = ( 496 df[index_column].dt.strftime("%Y-%m-%d %H:%M:%S.%f").str[:-5] 497 ) 498 499 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'。
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行目がスキップされる。
resample : bool
メソッド内でリサンプリング&欠損補間をするか。Falseの場合はfloat変換などの処理のみ適用する。
interpolate : bool, optional
欠損値の補完を適用するフラグ。デフォルトはTrue。
Returns
tuple[pd.DataFrame, list[str]]
前処理済みのデータフレームとメタデータのリスト。
501 def output_resampled_data( 502 self, 503 input_dir: str, 504 resampled_dir: str, 505 c2c1_ratio_dir: str, 506 input_file_pattern: str = r"Eddy_(\d+)", 507 input_files_suffix: str = ".dat", 508 col_c1: str = "Ultra_CH4_ppm_C", 509 col_c2: str = "Ultra_C2H6_ppb", 510 output_c2c1_ratio: bool = True, 511 output_resampled: bool = True, 512 c2c1_ratio_csv_prefix: str = "SAC.Ultra", 513 index_column: str = "TIMESTAMP", 514 index_format: str = "%Y-%m-%d %H:%M:%S.%f", 515 resample: bool = True, 516 interpolate: bool = True, 517 numeric_columns: list[str] = [ 518 "Ux", 519 "Uy", 520 "Uz", 521 "Tv", 522 "diag_sonic", 523 "CO2_new", 524 "H2O", 525 "diag_irga", 526 "cell_tmpr", 527 "cell_press", 528 "Ultra_CH4_ppm", 529 "Ultra_C2H6_ppb", 530 "Ultra_H2O_ppm", 531 "Ultra_CH4_ppm_C", 532 "Ultra_C2H6_ppb_C", 533 ], 534 metadata_rows: int = 4, 535 skiprows: list[int] = [0, 2, 3], 536 ) -> None: 537 """ 538 指定されたディレクトリ内のCSVファイルを処理し、リサンプリングと欠損値補間を行います。 539 540 このメソッドは、指定されたディレクトリ内のCSVファイルを読み込み、リサンプリングを行い、 541 欠損値を補完します。処理結果として、リサンプリングされたCSVファイルを出力し、 542 相関係数やC2H6/CH4比を計算してDataFrameに保存します。 543 リサンプリングと欠損値補完は`get_resampled_df`と同様のロジックを使用します。 544 545 Parameters 546 ---------- 547 input_dir : str 548 入力CSVファイルが格納されているディレクトリのパス。 549 resampled_dir : str 550 リサンプリングされたCSVファイルを出力するディレクトリのパス。 551 c2c1_ratio_dir : str 552 計算結果を保存するディレクトリのパス。 553 input_file_pattern : str 554 ファイル名からソートキーを抽出する正規表現パターン。デフォルトでは、最初の数字グループでソートします。 555 input_files_suffix : str 556 入力ファイルの拡張子(.datや.csvなど)。デフォルトは".dat"。 557 col_c1 : str 558 CH4濃度を含む列名。デフォルトは'Ultra_CH4_ppm_C'。 559 col_c2 : str 560 C2H6濃度を含む列名。デフォルトは'Ultra_C2H6_ppb'。 561 output_c2c1_ratio : bool, optional 562 線形回帰を行うかどうか。デフォルトはTrue。 563 output_resampled : bool, optional 564 リサンプリングされたCSVファイルを出力するかどうか。デフォルトはTrue。 565 c2c1_ratio_csv_prefix : str 566 出力ファイルの接頭辞。デフォルトは'SAC.Ultra'で、出力時は'SAC.Ultra.2024.09.21.ratio.csv'のような形式となる。 567 index_column : str 568 日時情報を含む列名。デフォルトは'TIMESTAMP'。 569 index_format : str, optional 570 インデックスの日付形式。デフォルトは'%Y-%m-%d %H:%M:%S.%f'。 571 resample : bool 572 リサンプリングを行うかどうか。デフォルトはTrue。 573 interpolate : bool 574 欠損値補間を行うかどうか。デフォルトはTrue。 575 numeric_columns : list[str] 576 数値データを含む列名のリスト。デフォルトは指定された列名のリスト。 577 metadata_rows : int 578 メタデータとして読み込む行数。デフォルトは4。 579 skiprows : list[int] 580 読み飛ばす行のインデックスリスト。デフォルトは[0, 2, 3]。 581 582 Raises 583 ---------- 584 OSError 585 ディレクトリの作成に失敗した場合。 586 FileNotFoundError 587 入力ファイルが見つからない場合。 588 ValueError 589 出力ディレクトリが指定されていない、またはデータの処理中にエラーが発生した場合。 590 """ 591 # 出力オプションとディレクトリの検証 592 if output_resampled and resampled_dir is None: 593 raise ValueError( 594 "output_resampled が True の場合、resampled_dir を指定する必要があります" 595 ) 596 if output_c2c1_ratio and c2c1_ratio_dir is None: 597 raise ValueError( 598 "output_c2c1_ratio が True の場合、c2c1_ratio_dir を指定する必要があります" 599 ) 600 601 # ディレクトリの作成(必要な場合のみ) 602 if output_resampled: 603 os.makedirs(resampled_dir, exist_ok=True) 604 if output_c2c1_ratio: 605 os.makedirs(c2c1_ratio_dir, exist_ok=True) 606 607 ratio_data: list[dict[str, str | float]] = [] 608 latest_date: datetime = datetime.min 609 610 # csvファイル名のリスト 611 csv_files: list[str] = EddyDataPreprocessor._get_sorted_files( 612 input_dir, input_file_pattern, input_files_suffix 613 ) 614 615 for filename in tqdm(csv_files, desc="Processing files"): 616 input_filepath: str = os.path.join(input_dir, filename) 617 # リサンプリング&欠損値補間 618 df, metadata = self.get_resampled_df( 619 filepath=input_filepath, 620 index_column=index_column, 621 index_format=index_format, 622 interpolate=interpolate, 623 resample=resample, 624 numeric_columns=numeric_columns, 625 metadata_rows=metadata_rows, 626 skiprows=skiprows, 627 ) 628 629 # 開始時間を取得 630 start_time: datetime = pd.to_datetime(df[index_column].iloc[0]) 631 # 処理したファイルの中で最も最新の日付 632 latest_date = max(latest_date, start_time) 633 634 # リサンプリング&欠損値補間したCSVを出力 635 if output_resampled: 636 base_filename: str = re.sub(rf"\{input_files_suffix}$", "", filename) 637 output_csv_path: str = os.path.join( 638 resampled_dir, f"{base_filename}-resampled.csv" 639 ) 640 # メタデータを先に書き込む 641 with open(output_csv_path, "w") as f: 642 for line in metadata: 643 f.write(f"{line}\n") 644 # データフレームを追記モードで書き込む 645 df.to_csv( 646 output_csv_path, index=False, mode="a", quoting=3, header=False 647 ) 648 649 # 相関係数とC2H6/CH4比を計算 650 if output_c2c1_ratio: 651 ch4_data: pd.Series = df[col_c1] 652 c2h6_data: pd.Series = df[col_c2] 653 654 ratio_row: dict[str, str | float] = { 655 "Date": start_time.strftime("%Y-%m-%d %H:%M:%S.%f"), 656 "slope": f"{np.nan}", 657 "intercept": f"{np.nan}", 658 "r_value": f"{np.nan}", 659 "p_value": f"{np.nan}", 660 "stderr": f"{np.nan}", 661 } 662 # 近似直線の傾き、切片、相関係数を計算 663 try: 664 slope, intercept, r_value, p_value, stderr = stats.linregress( 665 ch4_data, c2h6_data 666 ) 667 ratio_row: dict[str, str | float] = { 668 "Date": start_time.strftime("%Y-%m-%d %H:%M:%S.%f"), 669 "slope": f"{slope:.6f}", 670 "intercept": f"{intercept:.6f}", 671 "r_value": f"{r_value:.6f}", 672 "p_value": f"{p_value:.6f}", 673 "stderr": f"{stderr:.6f}", 674 } 675 except Exception: 676 # 何もせず、デフォルトの ratio_row を使用する 677 pass 678 679 # 結果をリストに追加 680 ratio_data.append(ratio_row) 681 682 if output_c2c1_ratio: 683 # DataFrameを作成し、Dateカラムで昇順ソート 684 ratio_df: pd.DataFrame = pd.DataFrame(ratio_data) 685 ratio_df["Date"] = pd.to_datetime( 686 ratio_df["Date"] 687 ) # Dateカラムをdatetime型に変換 688 ratio_df = ratio_df.sort_values("Date") # Dateカラムで昇順ソート 689 690 # CSVとして保存 691 ratio_filename: str = ( 692 f"{c2c1_ratio_csv_prefix}.{latest_date.strftime('%Y.%m.%d')}.ratio.csv" 693 ) 694 ratio_path: str = os.path.join(c2c1_ratio_dir, ratio_filename) 695 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ファイルを出力するディレクトリのパス。
c2c1_ratio_dir : str
計算結果を保存するディレクトリのパス。
input_file_pattern : str
ファイル名からソートキーを抽出する正規表現パターン。デフォルトでは、最初の数字グループでソートします。
input_files_suffix : str
入力ファイルの拡張子(.datや.csvなど)。デフォルトは".dat"。
col_c1 : str
CH4濃度を含む列名。デフォルトは'Ultra_CH4_ppm_C'。
col_c2 : str
C2H6濃度を含む列名。デフォルトは'Ultra_C2H6_ppb'。
output_c2c1_ratio : bool, optional
線形回帰を行うかどうか。デフォルトはTrue。
output_resampled : bool, optional
リサンプリングされたCSVファイルを出力するかどうか。デフォルトはTrue。
c2c1_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'。
resample : bool
リサンプリングを行うかどうか。デフォルトはTrue。
interpolate : bool
欠損値補間を行うかどうか。デフォルトはTrue。
numeric_columns : list[str]
数値データを含む列名のリスト。デフォルトは指定された列名のリスト。
metadata_rows : int
メタデータとして読み込む行数。デフォルトは4。
skiprows : list[int]
読み飛ばす行のインデックスリスト。デフォルトは[0, 2, 3]。
Raises
OSError
ディレクトリの作成に失敗した場合。
FileNotFoundError
入力ファイルが見つからない場合。
ValueError
出力ディレクトリが指定されていない、またはデータの処理中にエラーが発生した場合。
807 @staticmethod 808 def setup_logger(logger: Logger | None, log_level: int = INFO) -> Logger: 809 """ 810 ロガーを設定します。 811 812 このメソッドは、ロギングの設定を行い、ログメッセージのフォーマットを指定します。 813 ログメッセージには、日付、ログレベル、メッセージが含まれます。 814 815 渡されたロガーがNoneまたは不正な場合は、新たにロガーを作成し、標準出力に 816 ログメッセージが表示されるようにStreamHandlerを追加します。ロガーのレベルは 817 引数で指定されたlog_levelに基づいて設定されます。 818 819 Parameters 820 ---------- 821 logger : Logger | None 822 使用するロガー。Noneの場合は新しいロガーを作成します。 823 log_level : int 824 ロガーのログレベル。デフォルトはINFO。 825 826 Returns 827 ---------- 828 Logger 829 設定されたロガーオブジェクト。 830 """ 831 if logger is not None and isinstance(logger, Logger): 832 return logger 833 # 渡されたロガーがNoneまたは正しいものでない場合は独自に設定 834 new_logger: Logger = getLogger() 835 # 既存のハンドラーをすべて削除 836 for handler in new_logger.handlers[:]: 837 new_logger.removeHandler(handler) 838 new_logger.setLevel(log_level) # ロガーのレベルを設定 839 ch = StreamHandler() 840 ch_formatter = Formatter("%(asctime)s - %(levelname)s - %(message)s") 841 ch.setFormatter(ch_formatter) # フォーマッターをハンドラーに設定 842 new_logger.addHandler(ch) # StreamHandlerの追加 843 return new_logger
ロガーを設定します。
このメソッドは、ロギングの設定を行い、ログメッセージのフォーマットを指定します。 ログメッセージには、日付、ログレベル、メッセージが含まれます。
渡されたロガーがNoneまたは不正な場合は、新たにロガーを作成し、標準出力に ログメッセージが表示されるようにStreamHandlerを追加します。ロガーのレベルは 引数で指定されたlog_levelに基づいて設定されます。
Parameters
logger : Logger | None
使用するロガー。Noneの場合は新しいロガーを作成します。
log_level : int
ロガーのログレベル。デフォルトはINFO。
Returns
Logger
設定されたロガーオブジェクト。
8class SpectrumCalculator: 9 def __init__( 10 self, 11 df: pd.DataFrame, 12 fs: float, 13 apply_window: bool = True, 14 plots: int = 30, 15 window_type: str = "hamming", 16 ): 17 """ 18 データロガーから取得したデータファイルを用いて計算を行うクラス。 19 20 Parameters 21 ---------- 22 df : pd.DataFrame 23 pandasのデータフレーム。解析対象のデータを含む。 24 fs : float 25 サンプリング周波数(Hz)。データのサンプリングレートを指定。 26 apply_window : bool, optional 27 窓関数を適用するフラグ。デフォルトはTrue。 28 plots : int 29 プロットする点の数。可視化のためのデータポイント数。 30 window_type : str 31 窓関数の種類。デフォルトは'hamming'。 32 """ 33 self._df: pd.DataFrame = df 34 self._fs: float = fs 35 self._apply_window: bool = apply_window 36 self._plots: int = plots 37 self._window_type: str = window_type 38 39 def calculate_co_spectrum( 40 self, 41 col1: str, 42 col2: str, 43 dimensionless: bool = True, 44 frequency_weighted: bool = True, 45 interpolate_points: bool = True, 46 scaling: str = "spectrum", 47 detrend_1st: bool = True, 48 detrend_2nd: bool = False, 49 apply_lag_correction_to_col2: bool = True, 50 lag_second: float | None = None, 51 ) -> tuple: 52 """ 53 指定されたcol1とcol2のコスペクトルをDataFrameから計算するためのメソッド。 54 55 Parameters 56 ---------- 57 col1 : str 58 データの列名1。 59 col2 : str 60 データの列名2。 61 dimensionless : bool, optional 62 Trueの場合、分散で割って無次元化を行う。デフォルトはTrue。 63 frequency_weighted : bool, optional 64 周波数の重みづけを適用するかどうか。デフォルトはTrue。 65 interpolate_points : bool, optional 66 等間隔なデータ点を生成するかどうか(対数軸上で等間隔)。デフォルトはTrue。 67 scaling : str 68 "density"でスペクトル密度、"spectrum"でスペクトル。デフォルトは"spectrum"。 69 detrend_1st : bool, optional 70 1次トレンドを除去するかどうか。デフォルトはTrue。 71 detrend_2nd : bool, optional 72 2次トレンドを除去するかどうか。デフォルトはFalse。 73 apply_lag_correction_to_col2 : bool, optional 74 col2に遅れ時間補正を適用するかどうか。デフォルトはTrue。 75 lag_second : float | None, optional 76 col1からcol2が遅れている時間(秒)。apply_lag_correction_to_col2がTrueの場合に必要。デフォルトはNone。 77 78 Returns 79 ---------- 80 tuple 81 (freqs, co_spectrum, corr_coef) 82 - freqs : np.ndarray 83 周波数軸(対数スケールの場合は対数変換済み)。 84 - co_spectrum : np.ndarray 85 コスペクトル(対数スケールの場合は対数変換済み)。 86 - corr_coef : float 87 変数の相関係数。 88 """ 89 freqs, co_spectrum, _, corr_coef = self.calculate_cross_spectrum( 90 col1=col1, 91 col2=col2, 92 dimensionless=dimensionless, 93 frequency_weighted=frequency_weighted, 94 interpolate_points=interpolate_points, 95 scaling=scaling, 96 detrend_1st=detrend_1st, 97 detrend_2nd=detrend_2nd, 98 apply_lag_correction_to_col2=apply_lag_correction_to_col2, 99 lag_second=lag_second, 100 ) 101 return freqs, co_spectrum, corr_coef 102 103 def calculate_cross_spectrum( 104 self, 105 col1: str, 106 col2: str, 107 dimensionless: bool = True, 108 frequency_weighted: bool = True, 109 interpolate_points: bool = True, 110 scaling: str = "spectrum", 111 detrend_1st: bool = True, 112 detrend_2nd: bool = False, 113 apply_lag_correction_to_col2: bool = True, 114 lag_second: float | None = None, 115 ) -> tuple: 116 """ 117 指定されたcol1とcol2のクロススペクトルをDataFrameから計算するためのメソッド。 118 119 Parameters 120 ---------- 121 col1 : str 122 データの列名1。 123 col2 : str 124 データの列名2。 125 dimensionless : bool, optional 126 Trueの場合、分散で割って無次元化を行う。デフォルトはTrue。 127 frequency_weighted : bool, optional 128 周波数の重みづけを適用するかどうか。デフォルトはTrue。 129 interpolate_points : bool, optional 130 等間隔なデータ点を生成するかどうか(対数軸上で等間隔)。デフォルトはTrue。 131 scaling : str 132 "density"でスペクトル密度、"spectrum"でスペクトル。デフォルトは"spectrum"。 133 detrend_1st : bool, optional 134 1次トレンドを除去するかどうか。デフォルトはTrue。 135 detrend_2nd : bool, optional 136 2次トレンドを除去するかどうか。デフォルトはFalse。 137 apply_lag_correction_to_col2 : bool, optional 138 col2に遅れ時間補正を適用するかどうか。デフォルトはTrue。 139 lag_second : float | None, optional 140 col1からcol2が遅れている時間(秒)。apply_lag_correction_to_col2がTrueの場合に必要。デフォルトはNone。 141 142 Returns 143 ---------- 144 tuple 145 (freqs, co_spectrum, corr_coef) 146 - freqs : np.ndarray 147 周波数軸(対数スケールの場合は対数変換済み)。 148 - co_spectrum : np.ndarray 149 クロススペクトル(対数スケールの場合は対数変換済み)。 150 - corr_coef : float 151 変数の相関係数。 152 """ 153 # バリデーション 154 valid_scaling_options = ["density", "spectrum"] 155 if scaling not in valid_scaling_options: 156 raise ValueError( 157 f"'scaling'は次のパラメータから選択してください: {valid_scaling_options}" 158 ) 159 160 fs: float = self._fs 161 df_copied: pd.DataFrame = self._df.copy() 162 # データ取得と前処理 163 data1: np.ndarray = np.array(df_copied[col1].values) 164 data2: np.ndarray = np.array(df_copied[col2].values) 165 166 # 遅れ時間の補正 167 if apply_lag_correction_to_col2: 168 if lag_second is None: 169 raise ValueError( 170 "apply_lag_correction_to_col2=True の場合は lag_second に有効な遅れ時間(秒)を指定してください。" 171 ) 172 data1, data2 = SpectrumCalculator._correct_lag_time( 173 data1=data1, data2=data2, fs=fs, lag_second=lag_second 174 ) 175 176 # トレンド除去 177 if detrend_1st or detrend_2nd: 178 data1 = SpectrumCalculator._detrend( 179 data=data1, first=detrend_1st, second=detrend_2nd 180 ) 181 data2 = SpectrumCalculator._detrend( 182 data=data2, first=detrend_1st, second=detrend_2nd 183 ) 184 185 # 相関係数の計算 186 corr_coef: float = np.corrcoef(data1, data2)[0, 1] 187 188 # クロススペクトル計算 189 freqs, Pxy = signal.csd( 190 data1, 191 data2, 192 fs=self._fs, 193 window=self._window_type, 194 nperseg=1024, 195 scaling=scaling, 196 ) 197 198 # コスペクトルとクアドラチャスペクトルの抽出 199 co_spectrum = np.real(Pxy) 200 quad_spectrum = np.imag(Pxy) 201 202 # 周波数の重みづけ 203 if frequency_weighted: 204 co_spectrum[1:] *= freqs[1:] 205 quad_spectrum[1:] *= freqs[1:] 206 207 # 無次元化 208 if dimensionless: 209 cov_matrix: np.ndarray = np.cov(data1, data2) 210 covariance: float = cov_matrix[0, 1] 211 co_spectrum /= covariance 212 quad_spectrum /= covariance 213 214 if interpolate_points: 215 # 補間処理 216 log_freq_min = np.log10(0.001) 217 log_freq_max = np.log10(freqs[-1]) 218 log_freq_resampled = np.logspace(log_freq_min, log_freq_max, self._plots) 219 220 # スペクトルの補間 221 co_resampled = np.interp( 222 log_freq_resampled, freqs, co_spectrum, left=np.nan, right=np.nan 223 ) 224 quad_resampled = np.interp( 225 log_freq_resampled, freqs, quad_spectrum, left=np.nan, right=np.nan 226 ) 227 228 # NaNを除外 229 valid_mask = ~np.isnan(co_resampled) 230 freqs = log_freq_resampled[valid_mask] 231 co_spectrum = co_resampled[valid_mask] 232 quad_spectrum = quad_resampled[valid_mask] 233 234 # 0Hz成分を除外 235 nonzero_mask = freqs != 0 236 freqs = freqs[nonzero_mask] 237 co_spectrum = co_spectrum[nonzero_mask] 238 quad_spectrum = quad_spectrum[nonzero_mask] 239 240 return freqs, co_spectrum, quad_spectrum, corr_coef 241 242 def calculate_power_spectrum( 243 self, 244 col: str, 245 dimensionless: bool = True, 246 frequency_weighted: bool = True, 247 interpolate_points: bool = True, 248 scaling: str = "spectrum", 249 detrend_1st: bool = True, 250 detrend_2nd: bool = False, 251 ) -> tuple: 252 """ 253 指定されたcolに基づいてDataFrameからパワースペクトルと周波数軸を計算します。 254 scipy.signal.welchを使用してパワースペクトルを計算します。 255 256 Parameters 257 ---------- 258 col : str 259 データの列名 260 dimensionless : bool, optional 261 Trueの場合、分散で割って無次元化を行います。デフォルトはTrueです。 262 frequency_weighted : bool, optional 263 周波数の重みづけを適用するかどうか。デフォルトはTrueです。 264 interpolate_points : bool, optional 265 等間隔なデータ点を生成するかどうか(対数軸上で等間隔)。 266 scaling : str, optional 267 "density"でスペクトル密度、"spectrum"でスペクトル。デフォルトは"spectrum"です。 268 detrend_1st : bool, optional 269 1次トレンドを除去するかどうか。デフォルトはTrue。 270 detrend_2nd : bool, optional 271 2次トレンドを除去するかどうか。デフォルトはFalse。 272 273 Returns 274 ---------- 275 tuple 276 - freqs (np.ndarray): 周波数軸(対数スケールの場合は対数変換済み) 277 - power_spectrum (np.ndarray): パワースペクトル(対数スケールの場合は対数変換済み) 278 """ 279 # バリデーション 280 valid_scaling_options = ["density", "spectrum"] 281 if scaling not in valid_scaling_options: 282 raise ValueError( 283 f"'scaling'は次のパラメータから選択してください: {valid_scaling_options}" 284 ) 285 286 # データの取得とトレンド除去 287 df_copied: pd.DataFrame = self._df.copy() 288 data: np.ndarray = np.array(df_copied[col].values) 289 # どちらか一方でもTrueの場合は適用 290 if detrend_1st or detrend_2nd: 291 data = SpectrumCalculator._detrend( 292 data=data, first=detrend_1st, second=detrend_2nd 293 ) 294 295 # welchメソッドでパワースペクトル計算 296 freqs, power_spectrum = signal.welch( 297 data, fs=self._fs, window=self._window_type, nperseg=1024, scaling=scaling 298 ) 299 300 # 周波数の重みづけ(0Hz除外の前に実施) 301 if frequency_weighted: 302 power_spectrum = freqs * power_spectrum 303 304 # 無次元化(0Hz除外の前に実施) 305 if dimensionless: 306 variance = np.var(data) 307 power_spectrum /= variance 308 309 if interpolate_points: 310 # 補間処理(0Hz除外の前に実施) 311 log_freq_min = np.log10(0.001) 312 log_freq_max = np.log10(freqs[-1]) 313 log_freq_resampled = np.logspace(log_freq_min, log_freq_max, self._plots) 314 315 power_spectrum_resampled = np.interp( 316 log_freq_resampled, freqs, power_spectrum, left=np.nan, right=np.nan 317 ) 318 319 # NaNを除外 320 valid_mask = ~np.isnan(power_spectrum_resampled) 321 freqs = log_freq_resampled[valid_mask] 322 power_spectrum = power_spectrum_resampled[valid_mask] 323 324 # 0Hz成分を最後に除外 325 nonzero_mask = freqs != 0 326 freqs = freqs[nonzero_mask] 327 power_spectrum = power_spectrum[nonzero_mask] 328 329 return freqs, power_spectrum 330 331 @staticmethod 332 def _correct_lag_time( 333 data1: np.ndarray, 334 data2: np.ndarray, 335 fs: float, 336 lag_second: float, 337 ) -> tuple: 338 """ 339 相互相関関数を用いて遅れ時間を補正する。クロススペクトルの計算に使用。 340 341 Parameters 342 ---------- 343 data1 : np.ndarray 344 基準データ 345 data2 : np.ndarray 346 遅れているデータ 347 fs : float 348 サンプリング周波数 349 lag_second : float 350 data1からdata2が遅れている時間(秒)。負の値は許可されない。 351 352 Returns 353 ---------- 354 tuple 355 - data1 : np.ndarray 356 基準データ(シフトなし) 357 - data2 : np.ndarray 358 補正された遅れているデータ 359 """ 360 if lag_second < 0: 361 raise ValueError("lag_second must be non-negative.") 362 363 # lag_secondをサンプリング周波数でスケーリングしてインデックスに変換 364 lag_index: int = int(lag_second * fs) 365 366 # データの長さを取得 367 data_length = len(data1) 368 369 # data2のみをシフト(NaNで初期化) 370 shifted_data2 = np.full(data_length, np.nan) 371 shifted_data2[:-lag_index] = data2[lag_index:] if lag_index > 0 else data2 372 373 # NaNを含まない部分のみを抽出 374 valid_mask = ~np.isnan(shifted_data2) 375 data1 = data1[valid_mask] 376 data2 = shifted_data2[valid_mask] 377 378 return data1, data2 379 380 @staticmethod 381 def _detrend( 382 data: np.ndarray, first: bool = True, second: bool = False 383 ) -> np.ndarray: 384 """ 385 データから一次トレンドおよび二次トレンドを除去します。 386 387 Parameters 388 ---------- 389 data : np.ndarray 390 入力データ 391 first : bool, optional 392 一次トレンドを除去するかどうか. デフォルトはTrue. 393 second : bool, optional 394 二次トレンドを除去するかどうか. デフォルトはFalse. 395 396 Returns 397 ---------- 398 np.ndarray 399 トレンド除去後のデータ 400 401 Raises 402 ---------- 403 ValueError 404 first と second の両方がFalseの場合 405 """ 406 if not (first or second): 407 raise ValueError("少なくとも一次または二次トレンドの除去を指定してください") 408 409 detrended_data: np.ndarray = data.copy() 410 411 # 一次トレンドの除去 412 if first: 413 detrended_data = signal.detrend(detrended_data) 414 415 # 二次トレンドの除去 416 if second: 417 # 二次トレンドを除去するために、まず一次トレンドを除去 418 detrended_data = signal.detrend(detrended_data, type="linear") 419 # 二次トレンドを除去するために、二次多項式フィッティングを行う 420 coeffs_second = np.polyfit( 421 np.arange(len(detrended_data)), detrended_data, 2 422 ) 423 trend_second = np.polyval(coeffs_second, np.arange(len(detrended_data))) 424 detrended_data = detrended_data - trend_second 425 426 return detrended_data 427 428 @staticmethod 429 def _generate_window_function( 430 type: Literal["hanning", "hamming", "blackman"], data_length: int 431 ) -> np.ndarray: 432 """ 433 指定された種類の窓関数を適用する 434 435 Parameters 436 ---------- 437 type : Literal['hanning', 'hamming', 'blackman'] 438 窓関数の種類 ('hanning', 'hamming', 'blackman') 439 data_length : int 440 データ長 441 442 Returns 443 ---------- 444 np.ndarray 445 適用された窓関数 446 447 Notes 448 ---------- 449 - 指定された種類の窓関数を適用し、numpy配列として返す 450 - 無効な種類が指定された場合、警告を表示しHann窓を適用する 451 """ 452 if type == "hamming": 453 return np.hamming(data_length) 454 elif type == "blackman": 455 return np.blackman(data_length) 456 return np.hanning(data_length) 457 458 @staticmethod 459 def _smooth_spectrum( 460 spectrum: np.ndarray, frequencies: np.ndarray, freq_threshold: float = 0.1 461 ) -> np.ndarray: 462 """ 463 高周波数領域に対して3点移動平均を適用する処理を行う。 464 この処理により、高周波数成分のノイズを低減し、スペクトルの滑らかさを向上させる。 465 466 Parameters 467 ---------- 468 spectrum : np.ndarray 469 スペクトルデータ 470 frequencies : np.ndarray 471 対応する周波数データ 472 freq_threshold : float 473 高周波数の閾値 474 475 Returns 476 ---------- 477 np.ndarray 478 スムーズ化されたスペクトルデータ 479 """ 480 smoothed = spectrum.copy() # オリジナルデータのコピーを作成 481 482 # 周波数閾値以上の部分のインデックスを取得 483 high_freq_mask = frequencies >= freq_threshold 484 485 # 高周波数領域のみを処理 486 high_freq_indices = np.where(high_freq_mask)[0] 487 if len(high_freq_indices) > 2: # 最低3点必要 488 for i in high_freq_indices[1:-1]: # 端点を除く 489 smoothed[i] = ( 490 0.25 * spectrum[i - 1] + 0.5 * spectrum[i] + 0.25 * spectrum[i + 1] 491 ) 492 493 # 高周波領域の端点の処理 494 first_idx = high_freq_indices[0] 495 last_idx = high_freq_indices[-1] 496 smoothed[first_idx] = 0.5 * (spectrum[first_idx] + spectrum[first_idx + 1]) 497 smoothed[last_idx] = 0.5 * (spectrum[last_idx - 1] + spectrum[last_idx]) 498 499 return smoothed
9 def __init__( 10 self, 11 df: pd.DataFrame, 12 fs: float, 13 apply_window: bool = True, 14 plots: int = 30, 15 window_type: str = "hamming", 16 ): 17 """ 18 データロガーから取得したデータファイルを用いて計算を行うクラス。 19 20 Parameters 21 ---------- 22 df : pd.DataFrame 23 pandasのデータフレーム。解析対象のデータを含む。 24 fs : float 25 サンプリング周波数(Hz)。データのサンプリングレートを指定。 26 apply_window : bool, optional 27 窓関数を適用するフラグ。デフォルトはTrue。 28 plots : int 29 プロットする点の数。可視化のためのデータポイント数。 30 window_type : str 31 窓関数の種類。デフォルトは'hamming'。 32 """ 33 self._df: pd.DataFrame = df 34 self._fs: float = fs 35 self._apply_window: bool = apply_window 36 self._plots: int = plots 37 self._window_type: str = window_type
データロガーから取得したデータファイルを用いて計算を行うクラス。
Parameters
df : pd.DataFrame
pandasのデータフレーム。解析対象のデータを含む。
fs : float
サンプリング周波数(Hz)。データのサンプリングレートを指定。
apply_window : bool, optional
窓関数を適用するフラグ。デフォルトはTrue。
plots : int
プロットする点の数。可視化のためのデータポイント数。
window_type : str
窓関数の種類。デフォルトは'hamming'。
39 def calculate_co_spectrum( 40 self, 41 col1: str, 42 col2: str, 43 dimensionless: bool = True, 44 frequency_weighted: bool = True, 45 interpolate_points: bool = True, 46 scaling: str = "spectrum", 47 detrend_1st: bool = True, 48 detrend_2nd: bool = False, 49 apply_lag_correction_to_col2: bool = True, 50 lag_second: float | None = None, 51 ) -> tuple: 52 """ 53 指定されたcol1とcol2のコスペクトルをDataFrameから計算するためのメソッド。 54 55 Parameters 56 ---------- 57 col1 : str 58 データの列名1。 59 col2 : str 60 データの列名2。 61 dimensionless : bool, optional 62 Trueの場合、分散で割って無次元化を行う。デフォルトはTrue。 63 frequency_weighted : bool, optional 64 周波数の重みづけを適用するかどうか。デフォルトはTrue。 65 interpolate_points : bool, optional 66 等間隔なデータ点を生成するかどうか(対数軸上で等間隔)。デフォルトはTrue。 67 scaling : str 68 "density"でスペクトル密度、"spectrum"でスペクトル。デフォルトは"spectrum"。 69 detrend_1st : bool, optional 70 1次トレンドを除去するかどうか。デフォルトはTrue。 71 detrend_2nd : bool, optional 72 2次トレンドを除去するかどうか。デフォルトはFalse。 73 apply_lag_correction_to_col2 : bool, optional 74 col2に遅れ時間補正を適用するかどうか。デフォルトはTrue。 75 lag_second : float | None, optional 76 col1からcol2が遅れている時間(秒)。apply_lag_correction_to_col2がTrueの場合に必要。デフォルトはNone。 77 78 Returns 79 ---------- 80 tuple 81 (freqs, co_spectrum, corr_coef) 82 - freqs : np.ndarray 83 周波数軸(対数スケールの場合は対数変換済み)。 84 - co_spectrum : np.ndarray 85 コスペクトル(対数スケールの場合は対数変換済み)。 86 - corr_coef : float 87 変数の相関係数。 88 """ 89 freqs, co_spectrum, _, corr_coef = self.calculate_cross_spectrum( 90 col1=col1, 91 col2=col2, 92 dimensionless=dimensionless, 93 frequency_weighted=frequency_weighted, 94 interpolate_points=interpolate_points, 95 scaling=scaling, 96 detrend_1st=detrend_1st, 97 detrend_2nd=detrend_2nd, 98 apply_lag_correction_to_col2=apply_lag_correction_to_col2, 99 lag_second=lag_second, 100 ) 101 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"。
detrend_1st : bool, optional
1次トレンドを除去するかどうか。デフォルトはTrue。
detrend_2nd : bool, optional
2次トレンドを除去するかどうか。デフォルトはFalse。
apply_lag_correction_to_col2 : bool, optional
col2に遅れ時間補正を適用するかどうか。デフォルトはTrue。
lag_second : float | None, optional
col1からcol2が遅れている時間(秒)。apply_lag_correction_to_col2がTrueの場合に必要。デフォルトはNone。
Returns
tuple
(freqs, co_spectrum, corr_coef)
- freqs : np.ndarray
周波数軸(対数スケールの場合は対数変換済み)。
- co_spectrum : np.ndarray
コスペクトル(対数スケールの場合は対数変換済み)。
- corr_coef : float
変数の相関係数。
103 def calculate_cross_spectrum( 104 self, 105 col1: str, 106 col2: str, 107 dimensionless: bool = True, 108 frequency_weighted: bool = True, 109 interpolate_points: bool = True, 110 scaling: str = "spectrum", 111 detrend_1st: bool = True, 112 detrend_2nd: bool = False, 113 apply_lag_correction_to_col2: bool = True, 114 lag_second: float | None = None, 115 ) -> tuple: 116 """ 117 指定されたcol1とcol2のクロススペクトルをDataFrameから計算するためのメソッド。 118 119 Parameters 120 ---------- 121 col1 : str 122 データの列名1。 123 col2 : str 124 データの列名2。 125 dimensionless : bool, optional 126 Trueの場合、分散で割って無次元化を行う。デフォルトはTrue。 127 frequency_weighted : bool, optional 128 周波数の重みづけを適用するかどうか。デフォルトはTrue。 129 interpolate_points : bool, optional 130 等間隔なデータ点を生成するかどうか(対数軸上で等間隔)。デフォルトはTrue。 131 scaling : str 132 "density"でスペクトル密度、"spectrum"でスペクトル。デフォルトは"spectrum"。 133 detrend_1st : bool, optional 134 1次トレンドを除去するかどうか。デフォルトはTrue。 135 detrend_2nd : bool, optional 136 2次トレンドを除去するかどうか。デフォルトはFalse。 137 apply_lag_correction_to_col2 : bool, optional 138 col2に遅れ時間補正を適用するかどうか。デフォルトはTrue。 139 lag_second : float | None, optional 140 col1からcol2が遅れている時間(秒)。apply_lag_correction_to_col2がTrueの場合に必要。デフォルトはNone。 141 142 Returns 143 ---------- 144 tuple 145 (freqs, co_spectrum, corr_coef) 146 - freqs : np.ndarray 147 周波数軸(対数スケールの場合は対数変換済み)。 148 - co_spectrum : np.ndarray 149 クロススペクトル(対数スケールの場合は対数変換済み)。 150 - corr_coef : float 151 変数の相関係数。 152 """ 153 # バリデーション 154 valid_scaling_options = ["density", "spectrum"] 155 if scaling not in valid_scaling_options: 156 raise ValueError( 157 f"'scaling'は次のパラメータから選択してください: {valid_scaling_options}" 158 ) 159 160 fs: float = self._fs 161 df_copied: pd.DataFrame = self._df.copy() 162 # データ取得と前処理 163 data1: np.ndarray = np.array(df_copied[col1].values) 164 data2: np.ndarray = np.array(df_copied[col2].values) 165 166 # 遅れ時間の補正 167 if apply_lag_correction_to_col2: 168 if lag_second is None: 169 raise ValueError( 170 "apply_lag_correction_to_col2=True の場合は lag_second に有効な遅れ時間(秒)を指定してください。" 171 ) 172 data1, data2 = SpectrumCalculator._correct_lag_time( 173 data1=data1, data2=data2, fs=fs, lag_second=lag_second 174 ) 175 176 # トレンド除去 177 if detrend_1st or detrend_2nd: 178 data1 = SpectrumCalculator._detrend( 179 data=data1, first=detrend_1st, second=detrend_2nd 180 ) 181 data2 = SpectrumCalculator._detrend( 182 data=data2, first=detrend_1st, second=detrend_2nd 183 ) 184 185 # 相関係数の計算 186 corr_coef: float = np.corrcoef(data1, data2)[0, 1] 187 188 # クロススペクトル計算 189 freqs, Pxy = signal.csd( 190 data1, 191 data2, 192 fs=self._fs, 193 window=self._window_type, 194 nperseg=1024, 195 scaling=scaling, 196 ) 197 198 # コスペクトルとクアドラチャスペクトルの抽出 199 co_spectrum = np.real(Pxy) 200 quad_spectrum = np.imag(Pxy) 201 202 # 周波数の重みづけ 203 if frequency_weighted: 204 co_spectrum[1:] *= freqs[1:] 205 quad_spectrum[1:] *= freqs[1:] 206 207 # 無次元化 208 if dimensionless: 209 cov_matrix: np.ndarray = np.cov(data1, data2) 210 covariance: float = cov_matrix[0, 1] 211 co_spectrum /= covariance 212 quad_spectrum /= covariance 213 214 if interpolate_points: 215 # 補間処理 216 log_freq_min = np.log10(0.001) 217 log_freq_max = np.log10(freqs[-1]) 218 log_freq_resampled = np.logspace(log_freq_min, log_freq_max, self._plots) 219 220 # スペクトルの補間 221 co_resampled = np.interp( 222 log_freq_resampled, freqs, co_spectrum, left=np.nan, right=np.nan 223 ) 224 quad_resampled = np.interp( 225 log_freq_resampled, freqs, quad_spectrum, left=np.nan, right=np.nan 226 ) 227 228 # NaNを除外 229 valid_mask = ~np.isnan(co_resampled) 230 freqs = log_freq_resampled[valid_mask] 231 co_spectrum = co_resampled[valid_mask] 232 quad_spectrum = quad_resampled[valid_mask] 233 234 # 0Hz成分を除外 235 nonzero_mask = freqs != 0 236 freqs = freqs[nonzero_mask] 237 co_spectrum = co_spectrum[nonzero_mask] 238 quad_spectrum = quad_spectrum[nonzero_mask] 239 240 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"。
detrend_1st : bool, optional
1次トレンドを除去するかどうか。デフォルトはTrue。
detrend_2nd : bool, optional
2次トレンドを除去するかどうか。デフォルトはFalse。
apply_lag_correction_to_col2 : bool, optional
col2に遅れ時間補正を適用するかどうか。デフォルトはTrue。
lag_second : float | None, optional
col1からcol2が遅れている時間(秒)。apply_lag_correction_to_col2がTrueの場合に必要。デフォルトはNone。
Returns
tuple
(freqs, co_spectrum, corr_coef)
- freqs : np.ndarray
周波数軸(対数スケールの場合は対数変換済み)。
- co_spectrum : np.ndarray
クロススペクトル(対数スケールの場合は対数変換済み)。
- corr_coef : float
変数の相関係数。
242 def calculate_power_spectrum( 243 self, 244 col: str, 245 dimensionless: bool = True, 246 frequency_weighted: bool = True, 247 interpolate_points: bool = True, 248 scaling: str = "spectrum", 249 detrend_1st: bool = True, 250 detrend_2nd: bool = False, 251 ) -> tuple: 252 """ 253 指定されたcolに基づいてDataFrameからパワースペクトルと周波数軸を計算します。 254 scipy.signal.welchを使用してパワースペクトルを計算します。 255 256 Parameters 257 ---------- 258 col : str 259 データの列名 260 dimensionless : bool, optional 261 Trueの場合、分散で割って無次元化を行います。デフォルトはTrueです。 262 frequency_weighted : bool, optional 263 周波数の重みづけを適用するかどうか。デフォルトはTrueです。 264 interpolate_points : bool, optional 265 等間隔なデータ点を生成するかどうか(対数軸上で等間隔)。 266 scaling : str, optional 267 "density"でスペクトル密度、"spectrum"でスペクトル。デフォルトは"spectrum"です。 268 detrend_1st : bool, optional 269 1次トレンドを除去するかどうか。デフォルトはTrue。 270 detrend_2nd : bool, optional 271 2次トレンドを除去するかどうか。デフォルトはFalse。 272 273 Returns 274 ---------- 275 tuple 276 - freqs (np.ndarray): 周波数軸(対数スケールの場合は対数変換済み) 277 - power_spectrum (np.ndarray): パワースペクトル(対数スケールの場合は対数変換済み) 278 """ 279 # バリデーション 280 valid_scaling_options = ["density", "spectrum"] 281 if scaling not in valid_scaling_options: 282 raise ValueError( 283 f"'scaling'は次のパラメータから選択してください: {valid_scaling_options}" 284 ) 285 286 # データの取得とトレンド除去 287 df_copied: pd.DataFrame = self._df.copy() 288 data: np.ndarray = np.array(df_copied[col].values) 289 # どちらか一方でもTrueの場合は適用 290 if detrend_1st or detrend_2nd: 291 data = SpectrumCalculator._detrend( 292 data=data, first=detrend_1st, second=detrend_2nd 293 ) 294 295 # welchメソッドでパワースペクトル計算 296 freqs, power_spectrum = signal.welch( 297 data, fs=self._fs, window=self._window_type, nperseg=1024, scaling=scaling 298 ) 299 300 # 周波数の重みづけ(0Hz除外の前に実施) 301 if frequency_weighted: 302 power_spectrum = freqs * power_spectrum 303 304 # 無次元化(0Hz除外の前に実施) 305 if dimensionless: 306 variance = np.var(data) 307 power_spectrum /= variance 308 309 if interpolate_points: 310 # 補間処理(0Hz除外の前に実施) 311 log_freq_min = np.log10(0.001) 312 log_freq_max = np.log10(freqs[-1]) 313 log_freq_resampled = np.logspace(log_freq_min, log_freq_max, self._plots) 314 315 power_spectrum_resampled = np.interp( 316 log_freq_resampled, freqs, power_spectrum, left=np.nan, right=np.nan 317 ) 318 319 # NaNを除外 320 valid_mask = ~np.isnan(power_spectrum_resampled) 321 freqs = log_freq_resampled[valid_mask] 322 power_spectrum = power_spectrum_resampled[valid_mask] 323 324 # 0Hz成分を最後に除外 325 nonzero_mask = freqs != 0 326 freqs = freqs[nonzero_mask] 327 power_spectrum = power_spectrum[nonzero_mask] 328 329 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"です。
detrend_1st : bool, optional
1次トレンドを除去するかどうか。デフォルトはTrue。
detrend_2nd : bool, optional
2次トレンドを除去するかどうか。デフォルトはFalse。
Returns
tuple
- freqs (np.ndarray): 周波数軸(対数スケールの場合は対数変換済み)
- power_spectrum (np.ndarray): パワースペクトル(対数スケールの場合は対数変換済み)
5class FigureUtils: 6 @staticmethod 7 def setup_plot_params( 8 font_family: list[str] = ["Arial", "MS Gothic", "Dejavu Sans"], 9 font_size: float = 20, 10 legend_size: float = 20, 11 tick_size: float = 20, 12 title_size: float = 20, 13 plot_params: dict[str, any] | None = None, 14 ) -> None: 15 """ 16 matplotlibのプロットパラメータを設定します。 17 18 Parameters 19 ---------- 20 font_family : list[str] 21 使用するフォントファミリーのリスト。 22 font_size : float 23 軸ラベルのフォントサイズ。 24 legend_size : float 25 凡例のフォントサイズ。 26 tick_size : float 27 軸目盛りのフォントサイズ。 28 title_size : float 29 タイトルのフォントサイズ。 30 plot_params : dict[str, any] | None 31 matplotlibのプロットパラメータの辞書。 32 """ 33 # デフォルトのプロットパラメータ 34 default_params = { 35 "axes.linewidth": 1.0, 36 "axes.titlesize": title_size, # タイトル 37 "grid.color": "gray", 38 "grid.linewidth": 1.0, 39 "font.family": font_family, 40 "font.size": font_size, # 軸ラベル 41 "legend.fontsize": legend_size, # 凡例 42 "text.color": "black", 43 "xtick.color": "black", 44 "ytick.color": "black", 45 "xtick.labelsize": tick_size, # 軸目盛 46 "ytick.labelsize": tick_size, # 軸目盛 47 "xtick.major.size": 0, 48 "ytick.major.size": 0, 49 "ytick.direction": "out", 50 "ytick.major.width": 1.0, 51 } 52 53 # plot_paramsが定義されている場合、デフォルトに追記 54 if plot_params: 55 default_params.update(plot_params) 56 57 plt.rcParams.update(default_params) # プロットパラメータを更新
6 @staticmethod 7 def setup_plot_params( 8 font_family: list[str] = ["Arial", "MS Gothic", "Dejavu Sans"], 9 font_size: float = 20, 10 legend_size: float = 20, 11 tick_size: float = 20, 12 title_size: float = 20, 13 plot_params: dict[str, any] | None = None, 14 ) -> None: 15 """ 16 matplotlibのプロットパラメータを設定します。 17 18 Parameters 19 ---------- 20 font_family : list[str] 21 使用するフォントファミリーのリスト。 22 font_size : float 23 軸ラベルのフォントサイズ。 24 legend_size : float 25 凡例のフォントサイズ。 26 tick_size : float 27 軸目盛りのフォントサイズ。 28 title_size : float 29 タイトルのフォントサイズ。 30 plot_params : dict[str, any] | None 31 matplotlibのプロットパラメータの辞書。 32 """ 33 # デフォルトのプロットパラメータ 34 default_params = { 35 "axes.linewidth": 1.0, 36 "axes.titlesize": title_size, # タイトル 37 "grid.color": "gray", 38 "grid.linewidth": 1.0, 39 "font.family": font_family, 40 "font.size": font_size, # 軸ラベル 41 "legend.fontsize": legend_size, # 凡例 42 "text.color": "black", 43 "xtick.color": "black", 44 "ytick.color": "black", 45 "xtick.labelsize": tick_size, # 軸目盛 46 "ytick.labelsize": tick_size, # 軸目盛 47 "xtick.major.size": 0, 48 "ytick.major.size": 0, 49 "ytick.direction": "out", 50 "ytick.major.width": 1.0, 51 } 52 53 # plot_paramsが定義されている場合、デフォルトに追記 54 if plot_params: 55 default_params.update(plot_params) 56 57 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 : dict[str, any] | None
matplotlibのプロットパラメータの辞書。
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
ホットスポットの種類
34class FluxFootprintAnalyzer: 35 """ 36 フラックスフットプリントを解析および可視化するクラス。 37 38 このクラスは、フラックスデータの処理、フットプリントの計算、 39 および結果を衛星画像上に可視化するメソッドを提供します。 40 座標系と単位に関する重要な注意: 41 - すべての距離はメートル単位で計算されます 42 - 座標系の原点(0,0)は測定タワーの位置に対応します 43 - x軸は東西方向(正が東) 44 - y軸は南北方向(正が北) 45 - 風向は気象学的風向(北から時計回りに測定)を使用 46 47 この実装は、Kormann and Meixner (2001) および Takano et al. (2021)に基づいています。 48 """ 49 50 EARTH_RADIUS_METER: int = 6371000 # 地球の半径(メートル) 51 # クラス内部で生成するカラム名 52 COL_FFA_IS_WEEKDAY = "ffa_is_weekday" 53 COL_FFA_RADIAN = "ffa_radian" 54 COL_FFA_WIND_DIR_360 = "ffa_wind_direction_360" 55 56 def __init__( 57 self, 58 z_m: float, 59 na_values: list[str] = [ 60 "#DIV/0!", 61 "#VALUE!", 62 "#REF!", 63 "#N/A", 64 "#NAME?", 65 "NAN", 66 "nan", 67 ], 68 column_mapping: Mapping[str, str] | None = None, 69 labelsize: float = 20, 70 ticksize: float = 16, 71 plot_params: dict[str, any] | None = None, 72 logger: Logger | None = None, 73 logging_debug: bool = False, 74 ): 75 """ 76 衛星画像を用いて FluxFootprintAnalyzer を初期化します。 77 78 Parameters 79 ---------- 80 z_m : float 81 測定の高さ(メートル単位)。 82 na_values : list[str] 83 NaNと判定する値のパターン。 84 column_mapping : Mapping[str, str] | None, optional 85 入力データのカラム名とデフォルトカラム名のマッピング 86 例: { 87 "wind_dir": "WIND_DIRECTION", 88 "ws": "WIND_SPEED", 89 "ustar": "FRICTION_VELOCITY", 90 "sigma_v": "SIGMA_V", 91 "stability": "STABILITY", 92 "timestamp": "DATETIME", 93 } 94 labelsize : float 95 軸ラベルのフォントサイズ。デフォルトは20。 96 ticksize : float 97 軸目盛りのフォントサイズ。デフォルトは16。 98 plot_params : dict[str, any] | None 99 matplotlibのプロットパラメータを指定する辞書。 100 logger : Logger | None 101 使用するロガー。Noneの場合は新しいロガーを生成します。 102 logging_debug : bool 103 ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。 104 """ 105 # デフォルトのカラム名を設定 106 self._default_cols = DefaultColumnNames() 107 # カラム名マッピングの作成 108 self._cols = self._create_column_mapping(column_mapping) 109 # 必須カラムのリストを作成 110 self._required_columns = [ 111 self._cols[self._default_cols.WIND_DIRECTION], 112 self._cols[self._default_cols.WIND_SPEED], 113 self._cols[self._default_cols.FRICTION_VELOCITY], 114 self._cols[self._default_cols.SIGMA_V], 115 self._cols[self._default_cols.STABILITY], 116 ] 117 self._z_m: float = z_m # 測定高度 118 self._na_values: list[str] = na_values 119 # 状態を管理するフラグ 120 self._got_satellite_image: bool = False 121 122 # 図表の初期設定 123 FigureUtils.setup_plot_params( 124 font_size=labelsize, tick_size=ticksize, plot_params=plot_params 125 ) 126 # ロガー 127 log_level: int = INFO 128 if logging_debug: 129 log_level = DEBUG 130 self.logger: Logger = FluxFootprintAnalyzer.setup_logger(logger, log_level) 131 132 def _create_column_mapping( 133 self, mapping: Mapping[str, str] | None 134 ) -> Mapping[str, str]: 135 """カラム名マッピングを作成""" 136 if mapping is None: 137 # マッピングが指定されていない場合はデフォルト値をそのまま使用 138 return { 139 self._default_cols.DATETIME: self._default_cols.DATETIME, 140 self._default_cols.WIND_DIRECTION: self._default_cols.WIND_DIRECTION, 141 self._default_cols.WIND_SPEED: self._default_cols.WIND_SPEED, 142 self._default_cols.FRICTION_VELOCITY: self._default_cols.FRICTION_VELOCITY, 143 self._default_cols.SIGMA_V: self._default_cols.SIGMA_V, 144 self._default_cols.STABILITY: self._default_cols.STABILITY, 145 } 146 147 # デフォルトのマッピングを作成 148 result = { 149 self._default_cols.DATETIME: self._default_cols.DATETIME, 150 self._default_cols.WIND_DIRECTION: self._default_cols.WIND_DIRECTION, 151 self._default_cols.WIND_SPEED: self._default_cols.WIND_SPEED, 152 self._default_cols.FRICTION_VELOCITY: self._default_cols.FRICTION_VELOCITY, 153 self._default_cols.SIGMA_V: self._default_cols.SIGMA_V, 154 self._default_cols.STABILITY: self._default_cols.STABILITY, 155 } 156 157 # 指定されたマッピングで上書き 158 for input_col, default_col in mapping.items(): 159 if hasattr(self._default_cols, default_col): 160 result[getattr(self._default_cols, default_col)] = input_col 161 else: 162 self.logger.warning(f"Unknown default column name: {default_col}") 163 164 return result 165 166 def check_required_columns( 167 self, 168 df: pd.DataFrame, 169 col_datetime: str | None = None, 170 ) -> bool: 171 """ 172 必須カラムの存在チェック 173 174 Parameters 175 ---------- 176 df : pd.DataFrame 177 チェック対象のデータフレーム 178 col_datetime : str | None 179 日時カラム名(指定された場合はチェックから除外) 180 181 Returns 182 ---------- 183 bool 184 すべての必須カラムが存在する場合True 185 """ 186 check_columns: list[str] = [ 187 col for col in self._required_columns if col != col_datetime 188 ] 189 190 missing_columns = [col for col in check_columns if col not in df.columns] 191 192 if missing_columns: 193 self.logger.error( 194 f"Required columns are missing: {missing_columns}" 195 f"\nAvailable columns: {df.columns.tolist()}" 196 ) 197 return False 198 199 return True 200 201 @staticmethod 202 def setup_logger(logger: Logger | None, log_level: int = INFO) -> Logger: 203 """ 204 ロガーを設定します。 205 206 このメソッドは、ロギングの設定を行い、ログメッセージのフォーマットを指定します。 207 ログメッセージには、日付、ログレベル、メッセージが含まれます。 208 209 渡されたロガーがNoneまたは不正な場合は、新たにロガーを作成し、標準出力に 210 ログメッセージが表示されるようにStreamHandlerを追加します。ロガーのレベルは 211 引数で指定されたlog_levelに基づいて設定されます。 212 213 Parameters 214 ---------- 215 logger : Logger | None 216 使用するロガー。Noneの場合は新しいロガーを作成します。 217 log_level : int 218 ロガーのログレベル。デフォルトはINFO。 219 220 Returns 221 ---------- 222 Logger 223 設定されたロガーオブジェクト。 224 """ 225 if logger is not None and isinstance(logger, Logger): 226 return logger 227 # 渡されたロガーがNoneまたは正しいものでない場合は独自に設定 228 new_logger: Logger = getLogger() 229 # 既存のハンドラーをすべて削除 230 for handler in new_logger.handlers[:]: 231 new_logger.removeHandler(handler) 232 new_logger.setLevel(log_level) # ロガーのレベルを設定 233 ch = StreamHandler() 234 ch_formatter = Formatter("%(asctime)s - %(levelname)s - %(message)s") 235 ch.setFormatter(ch_formatter) # フォーマッターをハンドラーに設定 236 new_logger.addHandler(ch) # StreamHandlerの追加 237 return new_logger 238 239 def calculate_flux_footprint( 240 self, 241 df: pd.DataFrame, 242 col_flux: str, 243 plot_count: int = 10000, 244 start_time: str = "10:00", 245 end_time: str = "16:00", 246 ) -> tuple[list[float], list[float], list[float]]: 247 """ 248 フラックスフットプリントを計算し、指定された時間帯のデータを基に可視化します。 249 250 Parameters 251 ---------- 252 df : pd.DataFrame 253 分析対象のデータフレーム。フラックスデータを含む。 254 col_flux : str 255 フラックスデータの列名。計算に使用される。 256 plot_count : int, optional 257 生成するプロットの数。デフォルトは10000。 258 start_time : str, optional 259 フットプリント計算に使用する開始時間。デフォルトは"10:00"。 260 end_time : str, optional 261 フットプリント計算に使用する終了時間。デフォルトは"16:00"。 262 263 Returns 264 ---------- 265 tuple[list[float], list[float], list[float]]: 266 x座標 (メートル): タワーを原点とした東西方向の距離 267 y座標 (メートル): タワーを原点とした南北方向の距離 268 対象スカラー量の値: 各地点でのフラックス値 269 270 Notes 271 ---------- 272 - 返却される座標は測定タワーを原点(0,0)とした相対位置です 273 - すべての距離はメートル単位で表されます 274 - 正のx値は東方向、正のy値は北方向を示します 275 Required columns (default names): 276 - Wind direction: 風向 (度) 277 - WS vector: 風速 (m/s) 278 - u*: 摩擦速度 (m/s) 279 - sigmaV: 風速の標準偏差 (m/s) 280 - z/L: 安定度パラメータ (無次元) 281 """ 282 col_weekday: str = self.COL_FFA_IS_WEEKDAY 283 df_copied: pd.DataFrame = df.copy() 284 285 # インデックスがdatetimeであることを確認し、必要に応じて変換 286 if not isinstance(df_copied.index, pd.DatetimeIndex): 287 df_copied.index = pd.to_datetime(df_copied.index) 288 289 # DatetimeIndexから直接dateプロパティにアクセス 290 datelist: np.ndarray = np.array(df_copied.index.date) 291 292 # 各日付が平日かどうかを判定し、リストに格納 293 numbers: list[int] = [ 294 FluxFootprintAnalyzer.is_weekday(date) for date in datelist 295 ] 296 297 # col_weekdayに基づいてデータフレームに平日情報を追加 298 df_copied.loc[:, col_weekday] = numbers # .locを使用して値を設定 299 300 # 値が1のもの(平日)をコピーする 301 data_weekday: pd.DataFrame = df_copied[df_copied[col_weekday] == 1].copy() 302 # 特定の時間帯を抽出 303 data_weekday = data_weekday.between_time( 304 start_time, end_time 305 ) # 引数を使用して時間帯を抽出 306 data_weekday = data_weekday.dropna(subset=[col_flux]) 307 308 directions: list[float] = [ 309 wind_direction if wind_direction >= 0 else wind_direction + 360 310 for wind_direction in data_weekday[ 311 self._cols[self._default_cols.WIND_DIRECTION] 312 ] 313 ] 314 315 data_weekday.loc[:, self.COL_FFA_WIND_DIR_360] = directions 316 data_weekday.loc[:, self.COL_FFA_RADIAN] = ( 317 data_weekday[self.COL_FFA_WIND_DIR_360] / 180 * np.pi 318 ) 319 320 # 風向が欠測なら除去 321 data_weekday = data_weekday.dropna( 322 subset=[self._cols[self._default_cols.WIND_DIRECTION], col_flux] 323 ) 324 325 # 数値型への変換を確実に行う 326 numeric_columns: list[str] = [ 327 self._cols[self._default_cols.FRICTION_VELOCITY], 328 self._cols[self._default_cols.WIND_SPEED], 329 self._cols[self._default_cols.SIGMA_V], 330 self._cols[self._default_cols.STABILITY], 331 ] 332 for col in numeric_columns: 333 data_weekday[col] = pd.to_numeric(data_weekday[col], errors="coerce") 334 335 # 地面修正量dの計算 336 z_m: float = self._z_m 337 z_d: float = FluxFootprintAnalyzer._calculate_ground_correction( 338 z_m=z_m, 339 wind_speed=data_weekday[self._cols[self._default_cols.WIND_SPEED]].values, 340 friction_velocity=data_weekday[ 341 self._cols[self._default_cols.FRICTION_VELOCITY] 342 ].values, 343 stability_parameter=data_weekday[ 344 self._cols[self._default_cols.STABILITY] 345 ].values, 346 ) 347 348 x_list: list[float] = [] 349 y_list: list[float] = [] 350 c_list: list[float] | None = [] 351 352 # tqdmを使用してプログレスバーを表示 353 for i in tqdm(range(len(data_weekday)), desc="Calculating footprint"): 354 dUstar: float = data_weekday[self._cols[self._default_cols.FRICTION_VELOCITY]].iloc[i] 355 dU: float = data_weekday[self._cols[self._default_cols.WIND_SPEED]].iloc[i] 356 sigmaV: float = data_weekday[self._cols[self._default_cols.SIGMA_V]].iloc[i] 357 dzL: float = data_weekday[self._cols[self._default_cols.STABILITY]].iloc[i] 358 359 360 if pd.isna(dUstar) or pd.isna(dU) or pd.isna(sigmaV) or pd.isna(dzL): 361 self.logger.warning(f"NaN fields are exist.: i = {i}") 362 continue 363 elif dUstar < 5.0 and dUstar != 0.0 and dU > 0.1: 364 phi_m, phi_c, n = FluxFootprintAnalyzer._calculate_stability_parameters( 365 dzL=dzL 366 ) 367 m, U, r, mu, ksi = ( 368 FluxFootprintAnalyzer._calculate_footprint_parameters( 369 dUstar=dUstar, dU=dU, z_d=z_d, phi_m=phi_m, phi_c=phi_c, n=n 370 ) 371 ) 372 373 # 80%ソースエリアの計算 374 x80: float = FluxFootprintAnalyzer._source_area_KM2001( 375 ksi=ksi, mu=mu, dU=dU, sigmaV=sigmaV, z_d=z_d, max_ratio=0.8 376 ) 377 378 if not np.isnan(x80): 379 x1, y1, flux1 = FluxFootprintAnalyzer._prepare_plot_data( 380 x80, 381 ksi, 382 mu, 383 r, 384 U, 385 m, 386 sigmaV, 387 data_weekday[col_flux].iloc[i], 388 plot_count=plot_count, 389 ) 390 x1_, y1_ = FluxFootprintAnalyzer._rotate_coordinates( 391 x=x1, y=y1, radian=data_weekday[self.COL_FFA_RADIAN].iloc[i] 392 ) 393 394 x_list.extend(x1_) 395 y_list.extend(y1_) 396 c_list.extend(flux1) 397 398 return ( 399 x_list, 400 y_list, 401 c_list, 402 ) 403 404 def combine_all_data( 405 self, 406 data_source: str | pd.DataFrame, 407 col_datetime: str = "Date", 408 source_type: Literal["csv", "monthly"] = "csv", 409 ) -> pd.DataFrame: 410 """ 411 CSVファイルまたはMonthlyConverterからのデータを統合します 412 413 Parameters 414 ---------- 415 data_source : str | pd.DataFrame 416 CSVディレクトリパスまたはDataFrame 417 col_datetime :str 418 datetimeカラムのカラム名。デフォルトは"Date"。 419 source_type : str 420 "csv" または "monthly" 421 422 Returns 423 ---------- 424 pd.DataFrame 425 処理済みのデータフレーム 426 """ 427 col_weekday: str = self.COL_FFA_IS_WEEKDAY 428 if source_type == "csv": 429 # 既存のCSV処理ロジック 430 return self._combine_all_csv( 431 csv_dir_path=data_source, col_datetime=col_datetime 432 ) 433 elif source_type == "monthly": 434 # MonthlyConverterからのデータを処理 435 if not isinstance(data_source, pd.DataFrame): 436 raise ValueError("monthly形式の場合、DataFrameを直接渡す必要があります") 437 438 df: pd.DataFrame = data_source.copy() 439 440 # required_columnsからDateを除外して欠損値チェックを行う 441 check_columns: list[str] = [ 442 col for col in self._required_columns if col != col_datetime 443 ] 444 445 # インデックスがdatetimeであることを確認 446 if ( 447 not isinstance(df.index, pd.DatetimeIndex) 448 and col_datetime not in df.columns 449 ): 450 raise ValueError(f"DatetimeIndexまたは{col_datetime}カラムが必要です") 451 452 if col_datetime in df.columns: 453 df.set_index(col_datetime, inplace=True) 454 455 # 必要なカラムの存在確認 456 missing_columns = [ 457 col for col in check_columns if col not in df.columns.tolist() 458 ] 459 if missing_columns: 460 missing_cols = "','".join(missing_columns) 461 current_cols = "','".join(df.columns.tolist()) 462 raise ValueError( 463 f"必要なカラムが不足しています: '{missing_cols}'\n" 464 f"現在のカラム: '{current_cols}'" 465 ) 466 467 # 平日/休日の判定用カラムを追加 468 df[col_weekday] = df.index.map(FluxFootprintAnalyzer.is_weekday) 469 470 # Dateを除外したカラムで欠損値の処理 471 df = df.dropna(subset=check_columns) 472 473 # インデックスの重複を除去 474 df = df.loc[~df.index.duplicated(), :] 475 476 return df 477 478 def get_satellite_image_from_api( 479 self, 480 api_key: str, 481 center_lat: float, 482 center_lon: float, 483 output_path: str, 484 scale: int = 1, 485 size: tuple[int, int] = (2160, 2160), 486 zoom: int = 13, 487 ) -> ImageFile: 488 """ 489 Google Maps Static APIを使用して衛星画像を取得します。 490 491 Parameters 492 ---------- 493 api_key : str 494 Google Maps Static APIのキー。 495 center_lat : float 496 中心の緯度。 497 center_lon : float 498 中心の経度。 499 output_path : str 500 画像の保存先パス。拡張子は'.png'のみ許可される。 501 scale : int, optional 502 画像の解像度スケール(1か2)。デフォルトは1。 503 size : tuple[int, int], optional 504 画像サイズ (幅, 高さ)。デフォルトは(2160, 2160)。 505 zoom : int, optional 506 ズームレベル(0-21)。デフォルトは13。 507 508 Returns 509 ---------- 510 ImageFile 511 取得した衛星画像 512 513 Raises 514 ---------- 515 requests.RequestException 516 API呼び出しに失敗した場合 517 """ 518 # バリデーション 519 if not output_path.endswith(".png"): 520 raise ValueError("出力ファイル名は'.png'で終わる必要があります。") 521 522 # HTTPリクエストの定義 523 base_url = "https://maps.googleapis.com/maps/api/staticmap" 524 params = { 525 "center": f"{center_lat},{center_lon}", 526 "zoom": zoom, 527 "size": f"{size[0]}x{size[1]}", 528 "maptype": "satellite", 529 "scale": scale, 530 "key": api_key, 531 } 532 533 try: 534 response = requests.get(base_url, params=params) 535 response.raise_for_status() 536 # 画像ファイルに変換 537 image = Image.open(io.BytesIO(response.content)) 538 image.save(output_path) 539 self._got_satellite_image = True 540 self.logger.info(f"リモート画像を取得し、保存しました: {output_path}") 541 return image 542 except requests.RequestException as e: 543 self.logger.error(f"衛星画像の取得に失敗しました: {str(e)}") 544 raise e 545 546 def get_satellite_image_from_local( 547 self, 548 local_image_path: str, 549 alpha: float = 1.0, 550 grayscale: bool = False, 551 ) -> ImageFile: 552 """ 553 ローカルファイルから衛星画像を読み込みます。 554 555 Parameters 556 ---------- 557 local_image_path : str 558 ローカル画像のパス 559 alpha : float, optional 560 画像の透過率(0.0~1.0)。デフォルトは1.0。 561 grayscale : bool, optional 562 Trueの場合、画像を白黒に変換します。デフォルトはFalse。 563 564 Returns 565 ---------- 566 ImageFile 567 読み込んだ衛星画像(透過設定済み) 568 569 Raises 570 ---------- 571 FileNotFoundError 572 指定されたパスにファイルが存在しない場合 573 """ 574 if not os.path.exists(local_image_path): 575 raise FileNotFoundError( 576 f"指定されたローカル画像が存在しません: {local_image_path}" 577 ) 578 579 # 画像を読み込む 580 image: ImageFile = Image.open(local_image_path) 581 582 # 白黒変換が指定されている場合 583 if grayscale: 584 image = image.convert("L") # グレースケールに変換 585 586 # RGBAモードに変換 587 image = image.convert("RGBA") 588 589 # 透過率を設定 590 data = image.getdata() 591 new_data = [(r, g, b, int(255 * alpha)) for r, g, b, a in data] 592 image.putdata(new_data) 593 594 self._got_satellite_image = True 595 self.logger.info( 596 f"ローカル画像を使用しました(透過率: {alpha}, 白黒: {grayscale}): {local_image_path}" 597 ) 598 return image 599 600 def plot_flux_footprint( 601 self, 602 x_list: list[float], 603 y_list: list[float], 604 c_list: list[float] | None, 605 center_lat: float, 606 center_lon: float, 607 vmin: float, 608 vmax: float, 609 add_cbar: bool = True, 610 add_legend: bool = True, 611 cbar_label: str | None = None, 612 cbar_labelpad: int = 20, 613 cmap: str = "jet", 614 reduce_c_function: callable = np.mean, 615 lat_correction: float = 1, 616 lon_correction: float = 1, 617 output_dir: str | Path | None = None, 618 output_filename: str = "footprint.png", 619 save_fig: bool = True, 620 show_fig: bool = True, 621 satellite_image: ImageFile | None = None, 622 xy_max: float = 5000, 623 ) -> None: 624 """ 625 フットプリントデータをプロットします。 626 627 このメソッドは、指定されたフットプリントデータのみを可視化します。 628 629 Parameters 630 ---------- 631 x_list : list[float] 632 フットプリントのx座標リスト(メートル単位)。 633 y_list : list[float] 634 フットプリントのy座標リスト(メートル単位)。 635 c_list : list[float] | None 636 フットプリントの強度を示す値のリスト。 637 center_lat : float 638 プロットの中心となる緯度。 639 center_lon : float 640 プロットの中心となる経度。 641 cmap : str 642 使用するカラーマップの名前。 643 vmin : float 644 カラーバーの最小値。 645 vmax : float 646 カラーバーの最大値。 647 reduce_c_function : callable, optional 648 フットプリントの集約関数(デフォルトはnp.mean)。 649 cbar_label : str | None, optional 650 カラーバーのラベル。 651 cbar_labelpad : int, optional 652 カラーバーラベルのパディング。 653 lon_correction : float, optional 654 経度方向の補正係数(デフォルトは1)。 655 lat_correction : float, optional 656 緯度方向の補正係数(デフォルトは1)。 657 output_dir : str | Path | None, optional 658 プロット画像の保存先パス。 659 output_filename : str 660 プロット画像の保存ファイル名(拡張子を含む)。デフォルトは'footprint.png'。 661 save_fig : bool 662 図の保存を許可するフラグ。デフォルトはTrue。 663 show_fig : bool 664 図の表示を許可するフラグ。デフォルトはTrue。 665 satellite_image : ImageFile | None, optional 666 使用する衛星画像。指定がない場合はデフォルトの画像が生成されます。 667 xy_max : float, optional 668 表示範囲の最大値(デフォルトは4000)。 669 """ 670 self.plot_flux_footprint_with_hotspots( 671 x_list=x_list, 672 y_list=y_list, 673 c_list=c_list, 674 center_lat=center_lat, 675 center_lon=center_lon, 676 vmin=vmin, 677 vmax=vmax, 678 add_cbar=add_cbar, 679 add_legend=add_legend, 680 cbar_label=cbar_label, 681 cbar_labelpad=cbar_labelpad, 682 cmap=cmap, 683 reduce_c_function=reduce_c_function, 684 hotspots=None, # hotspotsをNoneに設定 685 hotspot_colors=None, 686 lat_correction=lat_correction, 687 lon_correction=lon_correction, 688 output_dir=output_dir, 689 output_filename=output_filename, 690 save_fig=save_fig, 691 show_fig=show_fig, 692 satellite_image=satellite_image, 693 xy_max=xy_max, 694 ) 695 696 def plot_flux_footprint_with_hotspots( 697 self, 698 x_list: list[float], 699 y_list: list[float], 700 c_list: list[float] | None, 701 center_lat: float, 702 center_lon: float, 703 vmin: float, 704 vmax: float, 705 add_cbar: bool = True, 706 add_legend: bool = True, 707 cbar_label: str | None = None, 708 cbar_labelpad: int = 20, 709 cmap: str = "jet", 710 reduce_c_function: callable = np.mean, 711 hotspots: list[HotspotData] | None = None, 712 hotspots_alpha: float = 0.7, 713 hotspot_colors: dict[HotspotType, str] | None = None, 714 hotspot_labels: dict[HotspotType, str] | None = None, 715 hotspot_markers: dict[HotspotType, str] | None = None, 716 legend_bbox_to_anchor: tuple[float, float] = (0.55, -0.01), 717 lat_correction: float = 1, 718 lon_correction: float = 1, 719 output_dir: str | Path | None = None, 720 output_filename: str = "footprint.png", 721 save_fig: bool = True, 722 show_fig: bool = True, 723 satellite_image: ImageFile | None = None, 724 xy_max: float = 5000, 725 ) -> None: 726 """ 727 Staticな衛星画像上にフットプリントデータとホットスポットをプロットします。 728 729 このメソッドは、指定されたフットプリントデータとホットスポットを可視化します。 730 ホットスポットが指定されない場合は、フットプリントのみ作図します。 731 732 Parameters 733 ---------- 734 x_list : list[float] 735 フットプリントのx座標リスト(メートル単位)。 736 y_list : list[float] 737 フットプリントのy座標リスト(メートル単位)。 738 c_list : list[float] | None 739 フットプリントの強度を示す値のリスト。 740 center_lat : float 741 プロットの中心となる緯度。 742 center_lon : float 743 プロットの中心となる経度。 744 vmin : float 745 カラーバーの最小値。 746 vmax : float 747 カラーバーの最大値。 748 add_cbar : bool, optional 749 カラーバーを追加するかどうか(デフォルトはTrue)。 750 add_legend : bool, optional 751 凡例を追加するかどうか(デフォルトはTrue)。 752 cbar_label : str | None, optional 753 カラーバーのラベル。 754 cbar_labelpad : int, optional 755 カラーバーラベルのパディング。 756 cmap : str 757 使用するカラーマップの名前。 758 reduce_c_function : callable 759 フットプリントの集約関数(デフォルトはnp.mean)。 760 hotspots : list[HotspotData] | None, optional 761 ホットスポットデータのリスト。デフォルトはNone。 762 hotspots_alpha : float, optional 763 ホットスポットの透明度。デフォルトは0.7。 764 hotspot_colors : dict[HotspotType, str] | None, optional 765 ホットスポットの色を指定する辞書。 766 例: {'bio': 'blue', 'gas': 'red', 'comb': 'green'} 767 hotspot_labels : dict[HotspotType, str] | None, optional 768 ホットスポットの表示ラベルを指定する辞書。 769 例: {'bio': '生物', 'gas': 'ガス', 'comb': '燃焼'} 770 hotspot_markers : dict[HotspotType, str] | None, optional 771 ホットスポットの形状を指定する辞書。 772 例: {'bio': '^', 'gas': 'o', 'comb': 's'} 773 legend_bbox_to_anchor : tuple[float, flaot], optional 774 ホットスポットの凡例の位置。デフォルトは (0.55, -0.01) 。 775 lat_correction : float, optional 776 緯度方向の補正係数(デフォルトは1)。 777 lon_correction : float, optional 778 経度方向の補正係数(デフォルトは1)。 779 output_dir : str | Path | None, optional 780 プロット画像の保存先パス。 781 output_filename : str 782 プロット画像の保存ファイル名(拡張子を含む)。デフォルトは'footprint.png'。 783 save_fig : bool 784 図の保存を許可するフラグ。デフォルトはTrue。 785 show_fig : bool 786 図の表示を許可するフラグ。デフォルトはTrue。 787 satellite_image : ImageFile | None, optional 788 使用する衛星画像。指定がない場合はデフォルトの画像が生成されます。 789 xy_max : float, optional 790 表示範囲の最大値(デフォルトは5000)。 791 """ 792 # 1. 引数のバリデーション 793 valid_extensions: list[str] = [".png", ".jpg", ".jpeg", ".pdf", ".svg"] 794 _, file_extension = os.path.splitext(output_filename) 795 if file_extension.lower() not in valid_extensions: 796 quoted_extensions: list[str] = [f'"{ext}"' for ext in valid_extensions] 797 self.logger.error( 798 f"`output_filename`は有効な拡張子ではありません。プロットを保存するには、次のいずれかを指定してください: {','.join(quoted_extensions)}" 799 ) 800 return 801 802 # 2. フラグチェック 803 if not self._got_satellite_image: 804 raise ValueError( 805 "`get_satellite_image_from_api`または`get_satellite_image_from_local`が実行されていません。" 806 ) 807 808 # 3. 衛星画像の取得 809 if satellite_image is None: 810 satellite_image = Image.new("RGB", (2160, 2160), "lightgray") 811 812 self.logger.info("プロットを作成中...") 813 814 # 4. 座標変換のための定数計算(1回だけ) 815 meters_per_lat: float = self.EARTH_RADIUS_METER * ( 816 math.pi / 180 817 ) # 緯度1度あたりのメートル 818 meters_per_lon: float = meters_per_lat * math.cos( 819 math.radians(center_lat) 820 ) # 経度1度あたりのメートル 821 822 # 5. フットプリントデータの座標変換(まとめて1回で実行) 823 x_deg = ( 824 np.array(x_list) / meters_per_lon * lon_correction 825 ) # 補正係数も同時に適用 826 y_deg = ( 827 np.array(y_list) / meters_per_lat * lat_correction 828 ) # 補正係数も同時に適用 829 830 # 6. 中心点からの相対座標を実際の緯度経度に変換 831 lons = center_lon + x_deg 832 lats = center_lat + y_deg 833 834 # 7. 表示範囲の計算(変更なし) 835 x_range: float = xy_max / meters_per_lon 836 y_range: float = xy_max / meters_per_lat 837 map_boundaries: tuple[float, float, float, float] = ( 838 center_lon - x_range, # left_lon 839 center_lon + x_range, # right_lon 840 center_lat - y_range, # bottom_lat 841 center_lat + y_range, # top_lat 842 ) 843 left_lon, right_lon, bottom_lat, top_lat = map_boundaries 844 845 # 8. プロットの作成 846 plt.rcParams["axes.edgecolor"] = "None" 847 fig: plt.Figure = plt.figure(figsize=(10, 8), dpi=300) 848 ax_data: plt.Axes = fig.add_axes([0.05, 0.1, 0.8, 0.8]) 849 850 # 9. フットプリントの描画 851 # フットプリントの描画とカラーバー用の2つのhexbinを作成 852 if c_list is not None: 853 ax_data.hexbin( 854 lons, 855 lats, 856 C=c_list, 857 cmap=cmap, 858 vmin=vmin, 859 vmax=vmax, 860 alpha=0.3, # 実際のプロット用 861 gridsize=100, 862 linewidths=0, 863 mincnt=100, 864 extent=[left_lon, right_lon, bottom_lat, top_lat], 865 reduce_C_function=reduce_c_function, 866 ) 867 868 # カラーバー用の非表示hexbin(alpha=1.0) 869 hidden_hexbin = ax_data.hexbin( 870 lons, 871 lats, 872 C=c_list, 873 cmap=cmap, 874 vmin=vmin, 875 vmax=vmax, 876 alpha=1.0, # カラーバー用 877 gridsize=100, 878 linewidths=0, 879 mincnt=100, 880 extent=[left_lon, right_lon, bottom_lat, top_lat], 881 reduce_C_function=reduce_c_function, 882 visible=False, # プロットには表示しない 883 ) 884 885 # 10. ホットスポットの描画 886 spot_handles = [] 887 if hotspots is not None: 888 default_colors: dict[HotspotType, str] = { 889 "bio": "blue", 890 "gas": "red", 891 "comb": "green", 892 } 893 894 # デフォルトのマーカー形状を定義 895 default_markers: dict[HotspotType, str] = { 896 "bio": "^", # 三角 897 "gas": "o", # 丸 898 "comb": "s", # 四角 899 } 900 901 # デフォルトのラベルを定義 902 default_labels: dict[HotspotType, str] = { 903 "bio": "bio", 904 "gas": "gas", 905 "comb": "comb", 906 } 907 908 # 座標変換のための定数 909 meters_per_lat: float = self.EARTH_RADIUS_METER * (math.pi / 180) 910 meters_per_lon: float = meters_per_lat * math.cos(math.radians(center_lat)) 911 912 for spot_type, color in (hotspot_colors or default_colors).items(): 913 spots_lon = [] 914 spots_lat = [] 915 916 # 使用するマーカーを決定 917 marker = (hotspot_markers or default_markers).get(spot_type, "o") 918 919 for spot in hotspots: 920 if spot.type == spot_type: 921 # 変換前の緯度経度をログ出力 922 self.logger.debug( 923 f"Before - Type: {spot_type}, Lat: {spot.avg_lat:.6f}, Lon: {spot.avg_lon:.6f}" 924 ) 925 926 # 中心からの相対距離を計算 927 dx: float = (spot.avg_lon - center_lon) * meters_per_lon 928 dy: float = (spot.avg_lat - center_lat) * meters_per_lat 929 930 # 補正前の相対座標をログ出力 931 self.logger.debug( 932 f"Relative - Type: {spot_type}, X: {dx:.2f}m, Y: {dy:.2f}m" 933 ) 934 935 # 補正を適用 936 corrected_dx: float = dx * lon_correction 937 corrected_dy: float = dy * lat_correction 938 939 # 補正後の緯度経度を計算 940 adjusted_lon: float = center_lon + corrected_dx / meters_per_lon 941 adjusted_lat: float = center_lat + corrected_dy / meters_per_lat 942 943 # 変換後の緯度経度をログ出力 944 self.logger.debug( 945 f"After - Type: {spot_type}, Lat: {adjusted_lat:.6f}, Lon: {adjusted_lon:.6f}\n" 946 ) 947 948 if ( 949 left_lon <= adjusted_lon <= right_lon 950 and bottom_lat <= adjusted_lat <= top_lat 951 ): 952 spots_lon.append(adjusted_lon) 953 spots_lat.append(adjusted_lat) 954 955 if spots_lon: 956 # 使用するラベルを決定 957 label = (hotspot_labels or default_labels).get(spot_type, spot_type) 958 959 handle = ax_data.scatter( 960 spots_lon, 961 spots_lat, 962 c=color, 963 marker=marker, # マーカー形状を指定 964 s=100, 965 alpha=hotspots_alpha, 966 label=label, 967 edgecolor="black", 968 linewidth=1, 969 ) 970 spot_handles.append(handle) 971 972 # 11. 背景画像の設定 973 ax_img = ax_data.twiny().twinx() 974 ax_img.imshow( 975 satellite_image, 976 extent=[left_lon, right_lon, bottom_lat, top_lat], 977 aspect="equal", 978 ) 979 980 # 12. 軸の設定 981 for ax in [ax_data, ax_img]: 982 ax.set_xlim(left_lon, right_lon) 983 ax.set_ylim(bottom_lat, top_lat) 984 ax.set_xticks([]) 985 ax.set_yticks([]) 986 987 ax_data.set_zorder(2) 988 ax_data.patch.set_alpha(0) 989 ax_img.set_zorder(1) 990 991 # 13. カラーバーの追加 992 if add_cbar: 993 cbar_ax: plt.Axes = fig.add_axes([0.88, 0.1, 0.03, 0.8]) 994 cbar = fig.colorbar(hidden_hexbin, cax=cbar_ax) # hidden_hexbinを使用 995 # cbar_labelが指定されている場合のみラベルを設定 996 if cbar_label: 997 cbar.set_label(cbar_label, rotation=270, labelpad=cbar_labelpad) 998 999 # 14. ホットスポットの凡例追加 1000 if add_legend and hotspots and spot_handles: 1001 ax_data.legend( 1002 handles=spot_handles, 1003 loc="upper center", 1004 bbox_to_anchor=legend_bbox_to_anchor, # 図の下に配置 1005 ncol=len(spot_handles), # ハンドルの数に応じて列数を設定 1006 ) 1007 1008 # 15. 画像の保存 1009 if save_fig: 1010 if output_dir is None: 1011 raise ValueError( 1012 "save_fig=Trueの場合、output_dirを指定する必要があります。有効なディレクトリパスを指定してください。" 1013 ) 1014 output_path: str = os.path.join(output_dir, output_filename) 1015 self.logger.info("プロットを保存中...") 1016 try: 1017 fig.savefig(output_path, bbox_inches="tight") 1018 self.logger.info(f"プロットが正常に保存されました: {output_path}") 1019 except Exception as e: 1020 self.logger.error(f"プロットの保存中にエラーが発生しました: {str(e)}") 1021 # 16. 画像の表示 1022 if show_fig: 1023 plt.show() 1024 else: 1025 plt.close(fig=fig) 1026 1027 def plot_flux_footprint_with_scale_checker( 1028 self, 1029 x_list: list[float], 1030 y_list: list[float], 1031 c_list: list[float] | None, 1032 center_lat: float, 1033 center_lon: float, 1034 check_points: list[tuple[float, float, str]] | None = None, 1035 vmin: float = 0, 1036 vmax: float = 100, 1037 add_cbar: bool = True, 1038 cbar_label: str | None = None, 1039 cbar_labelpad: int = 20, 1040 cmap: str = "jet", 1041 reduce_c_function: callable = np.mean, 1042 lat_correction: float = 1, 1043 lon_correction: float = 1, 1044 output_dir: str | Path | None = None, 1045 output_filename: str = "footprint-scale_checker.png", 1046 save_fig: bool = True, 1047 show_fig: bool = True, 1048 satellite_image: ImageFile | None = None, 1049 xy_max: float = 5000, 1050 ) -> None: 1051 """ 1052 Staticな衛星画像上にフットプリントデータとホットスポットをプロットします。 1053 1054 このメソッドは、指定されたフットプリントデータとホットスポットを可視化します。 1055 ホットスポットが指定されない場合は、フットプリントのみ作図します。 1056 1057 Parameters 1058 ---------- 1059 x_list : list[float] 1060 フットプリントのx座標リスト(メートル単位)。 1061 y_list : list[float] 1062 フットプリントのy座標リスト(メートル単位)。 1063 c_list : list[float] | None 1064 フットプリントの強度を示す値のリスト。 1065 center_lat : float 1066 プロットの中心となる緯度。 1067 center_lon : float 1068 プロットの中心となる経度。 1069 check_points : list[tuple[float, float, str]] | None 1070 確認用の地点リスト。各要素は (緯度, 経度, ラベル) のタプル。 1071 Noneの場合は中心から500m、1000m、2000m、3000mの位置に仮想的な点を配置。 1072 cmap : str 1073 使用するカラーマップの名前。 1074 vmin : float 1075 カラーバーの最小値。 1076 vmax : float 1077 カラーバーの最大値。 1078 reduce_c_function : callable, optional 1079 フットプリントの集約関数(デフォルトはnp.mean)。 1080 cbar_label : str, optional 1081 カラーバーのラベル。 1082 cbar_labelpad : int, optional 1083 カラーバーラベルのパディング。 1084 hotspots : list[HotspotData] | None 1085 ホットスポットデータのリスト。デフォルトはNone。 1086 hotspot_colors : dict[str, str] | None, optional 1087 ホットスポットの色を指定する辞書。 1088 lon_correction : float, optional 1089 経度方向の補正係数(デフォルトは1)。 1090 lat_correction : float, optional 1091 緯度方向の補正係数(デフォルトは1)。 1092 output_dir : str | Path | None, optional 1093 プロット画像の保存先パス。 1094 output_filename : str 1095 プロット画像の保存ファイル名(拡張子を含む)。デフォルトは'footprint.png'。 1096 save_fig : bool 1097 図の保存を許可するフラグ。デフォルトはTrue。 1098 show_fig : bool 1099 図の表示を許可するフラグ。デフォルトはTrue。 1100 satellite_image : ImageFile | None, optional 1101 使用する衛星画像。指定がない場合はデフォルトの画像が生成されます。 1102 xy_max : float, optional 1103 表示範囲の最大値(デフォルトは5000)。 1104 """ 1105 if check_points is None: 1106 # デフォルトの確認ポイントを生成(従来の方式) 1107 default_points = [ 1108 (500, "North", 90), # 北 500m 1109 (1000, "East", 0), # 東 1000m 1110 (2000, "South", 270), # 南 2000m 1111 (3000, "West", 180), # 西 3000m 1112 ] 1113 1114 dummy_hotspots = [] 1115 for distance, direction, angle in default_points: 1116 rad = math.radians(angle) 1117 meters_per_lat = self.EARTH_RADIUS_METER * (math.pi / 180) 1118 meters_per_lon = meters_per_lat * math.cos(math.radians(center_lat)) 1119 1120 dx = distance * math.cos(rad) 1121 dy = distance * math.sin(rad) 1122 1123 delta_lon = dx / meters_per_lon 1124 delta_lat = dy / meters_per_lat 1125 1126 hotspot = HotspotData( 1127 avg_lat=center_lat + delta_lat, 1128 avg_lon=center_lon + delta_lon, 1129 delta_ch4=0.0, 1130 delta_c2h6=0.0, 1131 ratio=0.0, 1132 type=f"{direction}_{distance}m", 1133 section=0, 1134 source="scale_check", 1135 angle=0, 1136 correlation=0, 1137 ) 1138 dummy_hotspots.append(hotspot) 1139 else: 1140 # 指定された緯度経度を使用 1141 dummy_hotspots = [] 1142 for lat, lon, label in check_points: 1143 hotspot = HotspotData( 1144 avg_lat=lat, 1145 avg_lon=lon, 1146 delta_ch4=0.0, 1147 delta_c2h6=0.0, 1148 ratio=0.0, 1149 type=label, 1150 section=0, 1151 source="scale_check", 1152 angle=0, 1153 correlation=0, 1154 ) 1155 dummy_hotspots.append(hotspot) 1156 1157 # カスタムカラーマップの作成 1158 hotspot_colors = { 1159 spot.type: plt.cm.tab10(i % 10) for i, spot in enumerate(dummy_hotspots) 1160 } 1161 1162 # 既存のメソッドを呼び出してプロット 1163 self.plot_flux_footprint_with_hotspots( 1164 x_list=x_list, 1165 y_list=y_list, 1166 c_list=c_list, 1167 center_lat=center_lat, 1168 center_lon=center_lon, 1169 vmin=vmin, 1170 vmax=vmax, 1171 add_cbar=add_cbar, 1172 add_legend=True, 1173 cbar_label=cbar_label, 1174 cbar_labelpad=cbar_labelpad, 1175 cmap=cmap, 1176 reduce_c_function=reduce_c_function, 1177 hotspots=dummy_hotspots, 1178 hotspot_colors=hotspot_colors, 1179 lat_correction=lat_correction, 1180 lon_correction=lon_correction, 1181 output_dir=output_dir, 1182 output_filename=output_filename, 1183 save_fig=save_fig, 1184 show_fig=show_fig, 1185 satellite_image=satellite_image, 1186 xy_max=xy_max, 1187 ) 1188 1189 def _combine_all_csv( 1190 self, csv_dir_path: str, col_datetime: str, suffix: str = ".csv" 1191 ) -> pd.DataFrame: 1192 """ 1193 指定されたディレクトリ内の全CSVファイルを読み込み、処理し、結合します。 1194 Monthlyシートを結合することを想定しています。 1195 1196 Parameters 1197 ---------- 1198 csv_dir_path : str 1199 CSVファイルが格納されているディレクトリのパス。 1200 col_datetime : str 1201 datetimeカラムのカラム名。 1202 suffix : str, optional 1203 読み込むファイルの拡張子。デフォルトは".csv"。 1204 1205 Returns 1206 ---------- 1207 pandas.DataFrame 1208 結合および処理済みのデータフレーム。 1209 1210 Notes 1211 ---------- 1212 - ディレクトリ内に少なくとも1つのCSVファイルが必要です。 1213 """ 1214 col_weekday: str = self.COL_FFA_IS_WEEKDAY 1215 csv_files = [f for f in os.listdir(csv_dir_path) if f.endswith(suffix)] 1216 if not csv_files: 1217 raise ValueError("指定されたディレクトリにCSVファイルが見つかりません。") 1218 1219 df_array: list[pd.DataFrame] = [] 1220 for csv_file in csv_files: 1221 file_path: str = os.path.join(csv_dir_path, csv_file) 1222 df: pd.DataFrame = self._prepare_csv( 1223 file_path=file_path, col_datetime=col_datetime 1224 ) 1225 df_array.append(df) 1226 1227 # 結合 1228 df_combined: pd.DataFrame = pd.concat(df_array, join="outer") 1229 df_combined = df_combined.loc[~df_combined.index.duplicated(), :] 1230 1231 # 平日と休日の判定に使用するカラムを作成 1232 df_combined[col_weekday] = df_combined.index.map( 1233 FluxFootprintAnalyzer.is_weekday 1234 ) # 共通の関数を使用 1235 1236 return df_combined 1237 1238 def _prepare_csv(self, file_path: str,col_datetime:str) -> pd.DataFrame: 1239 """ 1240 フラックスデータを含むCSVファイルを読み込み、処理します。 1241 1242 Parameters 1243 ---------- 1244 file_path : str 1245 CSVファイルのパス。 1246 col_datetime : str 1247 datetimeカラムのカラム名。 1248 1249 Returns 1250 ---------- 1251 pandas.DataFrame 1252 処理済みのデータフレーム。 1253 """ 1254 # CSVファイルの最初の行を読み込み、ヘッダーを取得するための一時データフレームを作成 1255 temp: pd.DataFrame = pd.read_csv(file_path, header=None, nrows=1, skiprows=0) 1256 header = temp.loc[temp.index[0]] 1257 1258 # 実際のデータを読み込み、必要な行をスキップし、欠損値を指定 1259 df: pd.DataFrame = pd.read_csv( 1260 file_path, 1261 header=None, 1262 skiprows=2, 1263 na_values=self._na_values, 1264 low_memory=False, 1265 ) 1266 # 取得したヘッダーをデータフレームに設定 1267 df.columns = header 1268 1269 # self._required_columnsのカラムが存在するか確認 1270 missing_columns: list[str] = [ 1271 col for col in self._required_columns if col not in df.columns.tolist() 1272 ] 1273 if missing_columns: 1274 raise ValueError( 1275 f"必要なカラムが不足しています: {', '.join(missing_columns)}" 1276 ) 1277 1278 # {col_datetime}カラムをインデックスに設定して返却 1279 df[col_datetime] = pd.to_datetime(df[col_datetime]) 1280 df = df.dropna(subset=[col_datetime]) 1281 df.set_index(col_datetime, inplace=True) 1282 return df 1283 1284 @staticmethod 1285 def _calculate_footprint_parameters( 1286 dUstar: float, dU: float, z_d: float, phi_m: float, phi_c: float, n: float 1287 ) -> tuple[float, float, float, float, float]: 1288 """ 1289 フットプリントパラメータを計算します。 1290 1291 Parameters 1292 ---------- 1293 dUstar : float 1294 摩擦速度 1295 dU : float 1296 風速 1297 z_d : float 1298 地面修正後の測定高度 1299 phi_m : float 1300 運動量の安定度関数 1301 phi_c : float 1302 スカラーの安定度関数 1303 n : float 1304 安定度パラメータ 1305 1306 Returns 1307 ---------- 1308 tuple[float, float, float, float, float] 1309 m (べき指数), 1310 U (基準高度での風速), 1311 r (べき指数の補正項), 1312 mu (形状パラメータ), 1313 ksi (フラックス長さスケール) 1314 """ 1315 KARMAN: float = 0.4 # フォン・カルマン定数 1316 # パラメータの計算 1317 m: float = dUstar / KARMAN * phi_m / dU 1318 U: float = dU / pow(z_d, m) 1319 r: float = 2.0 + m - n 1320 mu: float = (1.0 + m) / r 1321 kz: float = KARMAN * dUstar * z_d / phi_c 1322 k: float = kz / pow(z_d, n) 1323 ksi: float = U * pow(z_d, r) / r / r / k 1324 return m, U, r, mu, ksi 1325 1326 @staticmethod 1327 def _calculate_ground_correction( 1328 z_m: float, 1329 wind_speed: np.ndarray, 1330 friction_velocity: np.ndarray, 1331 stability_parameter: np.ndarray, 1332 ) -> float: 1333 """ 1334 地面修正量を計算します(Pennypacker and Baldocchi, 2016)。 1335 1336 この関数は、与えられた気象データを使用して地面修正量を計算します。 1337 計算は以下のステップで行われます: 1338 1. 変位高さ(d)を計算 1339 2. 中立条件外のデータを除外 1340 3. 平均変位高さを計算 1341 4. 地面修正量を返す 1342 1343 Parameters 1344 ---------- 1345 z_m : float 1346 観測地点の高度 1347 wind_speed : np.ndarray 1348 風速データ配列 (WS vector) 1349 friction_velocity : np.ndarray 1350 摩擦速度データ配列 (u*) 1351 stability_parameter : np.ndarray 1352 安定度パラメータ配列 (z/L) 1353 1354 Returns 1355 ---------- 1356 float 1357 計算された地面修正量 1358 """ 1359 KARMAN: float = 0.4 # フォン・カルマン定数 1360 z: float = z_m 1361 1362 # 変位高さ(d)の計算 1363 displacement_height = 0.6 * ( 1364 z / (0.6 + 0.1 * (np.exp((KARMAN * wind_speed) / friction_velocity))) 1365 ) 1366 1367 # 中立条件外のデータをマスク(中立条件:-0.1 < z/L < 0.1) 1368 neutral_condition_mask = (stability_parameter < -0.1) | ( 1369 0.1 < stability_parameter 1370 ) 1371 displacement_height[neutral_condition_mask] = np.nan 1372 1373 # 平均変位高さを計算 1374 d: float = np.nanmean(displacement_height) 1375 1376 # 地面修正量を返す 1377 return z - d 1378 1379 @staticmethod 1380 def _calculate_stability_parameters(dzL: float) -> tuple[float, float, float]: 1381 """ 1382 安定性パラメータを計算します。 1383 大気安定度に基づいて、運動量とスカラーの安定度関数、および安定度パラメータを計算します。 1384 1385 Parameters 1386 ---------- 1387 dzL : float 1388 無次元高度 (z/L)、ここで z は測定高度、L はモニン・オブコフ長 1389 1390 Returns 1391 ---------- 1392 tuple[float, float, float] 1393 phi_m : float 1394 運動量の安定度関数 1395 phi_c : float 1396 スカラーの安定度関数 1397 n : float 1398 安定度パラメータ 1399 """ 1400 phi_m: float = 0 1401 phi_c: float = 0 1402 n: float = 0 1403 if dzL > 0.0: 1404 # 安定成層の場合 1405 dzL = min(dzL, 2.0) 1406 phi_m = 1.0 + 5.0 * dzL 1407 phi_c = 1.0 + 5.0 * dzL 1408 n = 1.0 / (1.0 + 5.0 * dzL) 1409 else: 1410 # 不安定成層の場合 1411 phi_m = pow(1.0 - 16.0 * dzL, -0.25) 1412 phi_c = pow(1.0 - 16.0 * dzL, -0.50) 1413 n = (1.0 - 24.0 * dzL) / (1.0 - 16.0 * dzL) 1414 return phi_m, phi_c, n 1415 1416 @staticmethod 1417 def filter_data( 1418 df: pd.DataFrame, 1419 start_date: str | None = None, 1420 end_date: str | None = None, 1421 months: list[int] | None = None, 1422 ) -> pd.DataFrame: 1423 """ 1424 指定された期間や月でデータをフィルタリングするメソッド。 1425 1426 Parameters 1427 ---------- 1428 df : pd.DataFrame 1429 フィルタリングするデータフレーム 1430 start_date : str | None 1431 フィルタリングの開始日('YYYY-MM-DD'形式)。デフォルトはNone。 1432 end_date : str | None 1433 フィルタリングの終了日('YYYY-MM-DD'形式)。デフォルトはNone。 1434 months : list[int] | None 1435 フィルタリングする月のリスト(例:[1, 2, 12])。デフォルトはNone。 1436 1437 Returns 1438 ---------- 1439 pd.DataFrame 1440 フィルタリングされたデータフレーム 1441 1442 Raises 1443 ---------- 1444 ValueError 1445 インデックスがDatetimeIndexでない場合、または日付の形式が不正な場合 1446 """ 1447 # インデックスの検証 1448 if not isinstance(df.index, pd.DatetimeIndex): 1449 raise ValueError( 1450 "DataFrameのインデックスはDatetimeIndexである必要があります" 1451 ) 1452 1453 df_copied: pd.DataFrame = df.copy() 1454 1455 # 日付形式の検証と変換 1456 try: 1457 if start_date is not None: 1458 start_date = pd.to_datetime(start_date) 1459 if end_date is not None: 1460 end_date = pd.to_datetime(end_date) 1461 except ValueError as e: 1462 raise ValueError( 1463 "日付の形式が不正です。'YYYY-MM-DD'形式で指定してください" 1464 ) from e 1465 1466 # 期間でフィルタリング 1467 if start_date is not None or end_date is not None: 1468 df_copied = df_copied.loc[start_date:end_date] 1469 1470 # 月のバリデーション 1471 if months is not None: 1472 if not all(isinstance(m, int) and 1 <= m <= 12 for m in months): 1473 raise ValueError( 1474 "monthsは1から12までの整数のリストである必要があります" 1475 ) 1476 df_copied = df_copied[df_copied.index.month.isin(months)] 1477 1478 # フィルタリング後のデータが空でないことを確認 1479 if df_copied.empty: 1480 raise ValueError("フィルタリング後のデータが空になりました") 1481 1482 return df_copied 1483 1484 @staticmethod 1485 def is_weekday(date: datetime) -> int: 1486 """ 1487 指定された日付が平日であるかどうかを判定します。 1488 1489 Parameters 1490 ---------- 1491 date : datetime 1492 判定する日付。 1493 1494 Returns 1495 ---------- 1496 int 1497 平日であれば1、そうでなければ0。 1498 """ 1499 return 1 if not jpholiday.is_holiday(date) and date.weekday() < 5 else 0 1500 1501 @staticmethod 1502 def _prepare_plot_data( 1503 x80: float, 1504 ksi: float, 1505 mu: float, 1506 r: float, 1507 U: float, 1508 m: float, 1509 sigmaV: float, 1510 flux_value: float, 1511 plot_count: int, 1512 ) -> tuple[np.ndarray, np.ndarray, np.ndarray]: 1513 """ 1514 フットプリントのプロットデータを準備します。 1515 1516 Parameters 1517 ---------- 1518 x80 : float 1519 80%寄与距離 1520 ksi : float 1521 フラックス長さスケール 1522 mu : float 1523 形状パラメータ 1524 r : float 1525 べき指数 1526 U : float 1527 風速 1528 m : float 1529 風速プロファイルのべき指数 1530 sigmaV : float 1531 風速の標準偏差 1532 flux_value : float 1533 フラックス値 1534 plot_count : int 1535 生成するプロット数 1536 1537 Returns 1538 ---------- 1539 tuple[np.ndarray, np.ndarray, np.ndarray] 1540 x座標、y座標、フラックス値の配列のタプル 1541 """ 1542 KARMAN: float = 0.4 # フォン・カルマン定数 (pp.210) 1543 x_lim: int = int(x80) 1544 1545 """ 1546 各ランで生成するプロット数 1547 多いほどメモリに付加がかかるため注意 1548 """ 1549 plot_num: int = plot_count # 各ランで生成するプロット数 1550 1551 # x方向の距離配列を生成 1552 x_list: np.ndarray = np.arange(1, x_lim + 1, dtype="float64") 1553 1554 # クロスウィンド積分フットプリント関数を計算 1555 f_list: np.ndarray = ( 1556 ksi**mu * np.exp(-ksi / x_list) / math.gamma(mu) / x_list ** (1.0 + mu) 1557 ) 1558 1559 # プロット数に基づいてx座標を生成 1560 num_list: np.ndarray = np.round(f_list * plot_num).astype("int64") 1561 x1: np.ndarray = np.repeat(x_list, num_list) 1562 1563 # 風速プロファイルを計算 1564 Ux: np.ndarray = ( 1565 (math.gamma(mu) / math.gamma(1 / r)) 1566 * ((r**2 * KARMAN) / U) ** (m / r) 1567 * U 1568 * x1 ** (m / r) 1569 ) 1570 1571 # y方向の分散を計算し、正規分布に従ってy座標を生成 1572 sigma_array: np.ndarray = sigmaV * x1 / Ux 1573 y1: np.ndarray = np.random.normal(0, sigma_array) 1574 1575 # フラックス値の配列を生成 1576 flux1 = np.full_like(x1, flux_value) 1577 1578 return x1, y1, flux1 1579 1580 @staticmethod 1581 def _rotate_coordinates( 1582 x: np.ndarray, y: np.ndarray, radian: float 1583 ) -> tuple[np.ndarray, np.ndarray]: 1584 """ 1585 座標を指定された角度で回転させます。 1586 1587 この関数は、与えられたx座標とy座標を、指定された角度(ラジアン)で回転させます。 1588 回転は原点を中心に反時計回りに行われます。 1589 1590 Parameters 1591 ---------- 1592 x : np.ndarray 1593 回転させるx座標の配列 1594 y : np.ndarray 1595 回転させるy座標の配列 1596 radian : float 1597 回転角度(ラジアン) 1598 1599 Returns 1600 ---------- 1601 tuple[np.ndarray, np.ndarray] 1602 回転後の(x_, y_)座標の組 1603 """ 1604 radian1: float = (radian - (np.pi / 2)) * (-1) 1605 x_: np.ndarray = x * np.cos(radian1) - y * np.sin(radian1) 1606 y_: np.ndarray = x * np.sin(radian1) + y * np.cos(radian1) 1607 return x_, y_ 1608 1609 @staticmethod 1610 def _source_area_KM2001( 1611 ksi: float, 1612 mu: float, 1613 dU: float, 1614 sigmaV: float, 1615 z_d: float, 1616 max_ratio: float = 0.8, 1617 ) -> float: 1618 """ 1619 Kormann and Meixner (2001)のフットプリントモデルに基づいてソースエリアを計算します。 1620 1621 このメソッドは、与えられたパラメータを使用して、フラックスの寄与距離を計算します。 1622 計算は反復的に行われ、寄与率が'max_ratio'に達するまで、または最大反復回数に達するまで続けられます。 1623 1624 Parameters 1625 ---------- 1626 ksi : float 1627 フラックス長さスケール 1628 mu : float 1629 形状パラメータ 1630 dU : float 1631 風速の変化率 1632 sigmaV : float 1633 風速の標準偏差 1634 z_d : float 1635 ゼロ面変位高度 1636 max_ratio : float, optional 1637 寄与率の最大値。デフォルトは0.8。 1638 1639 Returns 1640 ---------- 1641 float 1642 80%寄与距離(メートル単位)。計算が収束しない場合はnp.nan。 1643 1644 Notes 1645 ---------- 1646 - 計算が収束しない場合(最大反復回数に達した場合)、結果はnp.nanとなります。 1647 """ 1648 if max_ratio > 1: 1649 raise ValueError("max_ratio は0以上1以下である必要があります。") 1650 # 変数の初期値 1651 sum_f: float = 0.0 # 寄与率(0 < sum_f < 1.0) 1652 x1: float = 0.0 1653 dF_xd: float = 0.0 1654 1655 x_d: float = ksi / ( 1656 1.0 + mu 1657 ) # Eq. 22 (x_d : クロスウィンド積分フラックスフットプリント最大位置) 1658 1659 dx: float = x_d / 100.0 # 等値線の拡がりの最大距離の100分の1(m) 1660 1661 # 寄与率が80%に達するまでfを積算 1662 while sum_f < (max_ratio / 1): 1663 x1 += dx 1664 1665 # Equation 21 (dF : クロスウィンド積分フットプリント) 1666 dF: float = ( 1667 pow(ksi, mu) * math.exp(-ksi / x1) / math.gamma(mu) / pow(x1, 1.0 + mu) 1668 ) 1669 1670 sum_f += dF # Footprint を加えていく (0.0 < dF < 1.0) 1671 dx *= 2.0 # 距離は2倍ずつ増やしていく 1672 1673 if dx > 1.0: 1674 dx = 1.0 # 一気に、1 m 以上はインクリメントしない 1675 if x1 > z_d * 1000.0: 1676 break # ソースエリアが測定高度の1000倍以上となった場合、エラーとして止める 1677 1678 x_dst: float = x1 # 寄与率が80%に達するまでの積算距離 1679 f_last: float = ( 1680 pow(ksi, mu) 1681 * math.exp(-ksi / x_dst) 1682 / math.gamma(mu) 1683 / pow(x_dst, 1.0 + mu) 1684 ) # Page 214 just below the Eq. 21. 1685 1686 # y方向の最大距離とその位置のxの距離 1687 dy: float = x_d / 100.0 # 等値線の拡がりの最大距離の100分の1 1688 y_dst: float = 0.0 1689 accumulated_y: float = 0.0 # y方向の積算距離を表す変数 1690 1691 # 最大反復回数を設定 1692 MAX_ITERATIONS: int = 100000 1693 for _ in range(MAX_ITERATIONS): 1694 accumulated_y += dy 1695 if accumulated_y >= x_dst: 1696 break 1697 1698 dF_xd = ( 1699 pow(ksi, mu) 1700 * math.exp(-ksi / accumulated_y) 1701 / math.gamma(mu) 1702 / pow(accumulated_y, 1.0 + mu) 1703 ) # 式21の直下(214ページ) 1704 1705 aa: float = math.log(x_dst * dF_xd / f_last / accumulated_y) 1706 sigma: float = sigmaV * accumulated_y / dU # 215ページ8行目 1707 1708 if 2.0 * aa >= 0: 1709 y_dst_new: float = sigma * math.sqrt(2.0 * aa) 1710 if y_dst_new <= y_dst: 1711 break # forループを抜ける 1712 y_dst = y_dst_new 1713 1714 dy = min(dy * 2.0, 1.0) 1715 1716 else: 1717 # ループが正常に終了しなかった場合(最大反復回数に達した場合) 1718 x_dst = np.nan 1719 1720 return x_dst
フラックスフットプリントを解析および可視化するクラス。
このクラスは、フラックスデータの処理、フットプリントの計算、 および結果を衛星画像上に可視化するメソッドを提供します。 座標系と単位に関する重要な注意:
- すべての距離はメートル単位で計算されます
- 座標系の原点(0,0)は測定タワーの位置に対応します
- x軸は東西方向(正が東)
- y軸は南北方向(正が北)
- 風向は気象学的風向(北から時計回りに測定)を使用
この実装は、Kormann and Meixner (2001) および Takano et al. (2021)に基づいています。
56 def __init__( 57 self, 58 z_m: float, 59 na_values: list[str] = [ 60 "#DIV/0!", 61 "#VALUE!", 62 "#REF!", 63 "#N/A", 64 "#NAME?", 65 "NAN", 66 "nan", 67 ], 68 column_mapping: Mapping[str, str] | None = None, 69 labelsize: float = 20, 70 ticksize: float = 16, 71 plot_params: dict[str, any] | None = None, 72 logger: Logger | None = None, 73 logging_debug: bool = False, 74 ): 75 """ 76 衛星画像を用いて FluxFootprintAnalyzer を初期化します。 77 78 Parameters 79 ---------- 80 z_m : float 81 測定の高さ(メートル単位)。 82 na_values : list[str] 83 NaNと判定する値のパターン。 84 column_mapping : Mapping[str, str] | None, optional 85 入力データのカラム名とデフォルトカラム名のマッピング 86 例: { 87 "wind_dir": "WIND_DIRECTION", 88 "ws": "WIND_SPEED", 89 "ustar": "FRICTION_VELOCITY", 90 "sigma_v": "SIGMA_V", 91 "stability": "STABILITY", 92 "timestamp": "DATETIME", 93 } 94 labelsize : float 95 軸ラベルのフォントサイズ。デフォルトは20。 96 ticksize : float 97 軸目盛りのフォントサイズ。デフォルトは16。 98 plot_params : dict[str, any] | None 99 matplotlibのプロットパラメータを指定する辞書。 100 logger : Logger | None 101 使用するロガー。Noneの場合は新しいロガーを生成します。 102 logging_debug : bool 103 ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。 104 """ 105 # デフォルトのカラム名を設定 106 self._default_cols = DefaultColumnNames() 107 # カラム名マッピングの作成 108 self._cols = self._create_column_mapping(column_mapping) 109 # 必須カラムのリストを作成 110 self._required_columns = [ 111 self._cols[self._default_cols.WIND_DIRECTION], 112 self._cols[self._default_cols.WIND_SPEED], 113 self._cols[self._default_cols.FRICTION_VELOCITY], 114 self._cols[self._default_cols.SIGMA_V], 115 self._cols[self._default_cols.STABILITY], 116 ] 117 self._z_m: float = z_m # 測定高度 118 self._na_values: list[str] = na_values 119 # 状態を管理するフラグ 120 self._got_satellite_image: bool = False 121 122 # 図表の初期設定 123 FigureUtils.setup_plot_params( 124 font_size=labelsize, tick_size=ticksize, plot_params=plot_params 125 ) 126 # ロガー 127 log_level: int = INFO 128 if logging_debug: 129 log_level = DEBUG 130 self.logger: Logger = FluxFootprintAnalyzer.setup_logger(logger, log_level)
衛星画像を用いて FluxFootprintAnalyzer を初期化します。
Parameters
z_m : float
測定の高さ(メートル単位)。
na_values : list[str]
NaNと判定する値のパターン。
column_mapping : Mapping[str, str] | None, optional
入力データのカラム名とデフォルトカラム名のマッピング
例: {
"wind_dir": "WIND_DIRECTION",
"ws": "WIND_SPEED",
"ustar": "FRICTION_VELOCITY",
"sigma_v": "SIGMA_V",
"stability": "STABILITY",
"timestamp": "DATETIME",
}
labelsize : float
軸ラベルのフォントサイズ。デフォルトは20。
ticksize : float
軸目盛りのフォントサイズ。デフォルトは16。
plot_params : dict[str, any] | None
matplotlibのプロットパラメータを指定する辞書。
logger : Logger | None
使用するロガー。Noneの場合は新しいロガーを生成します。
logging_debug : bool
ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。
166 def check_required_columns( 167 self, 168 df: pd.DataFrame, 169 col_datetime: str | None = None, 170 ) -> bool: 171 """ 172 必須カラムの存在チェック 173 174 Parameters 175 ---------- 176 df : pd.DataFrame 177 チェック対象のデータフレーム 178 col_datetime : str | None 179 日時カラム名(指定された場合はチェックから除外) 180 181 Returns 182 ---------- 183 bool 184 すべての必須カラムが存在する場合True 185 """ 186 check_columns: list[str] = [ 187 col for col in self._required_columns if col != col_datetime 188 ] 189 190 missing_columns = [col for col in check_columns if col not in df.columns] 191 192 if missing_columns: 193 self.logger.error( 194 f"Required columns are missing: {missing_columns}" 195 f"\nAvailable columns: {df.columns.tolist()}" 196 ) 197 return False 198 199 return True
必須カラムの存在チェック
Parameters
df : pd.DataFrame
チェック対象のデータフレーム
col_datetime : str | None
日時カラム名(指定された場合はチェックから除外)
Returns
bool
すべての必須カラムが存在する場合True
201 @staticmethod 202 def setup_logger(logger: Logger | None, log_level: int = INFO) -> Logger: 203 """ 204 ロガーを設定します。 205 206 このメソッドは、ロギングの設定を行い、ログメッセージのフォーマットを指定します。 207 ログメッセージには、日付、ログレベル、メッセージが含まれます。 208 209 渡されたロガーがNoneまたは不正な場合は、新たにロガーを作成し、標準出力に 210 ログメッセージが表示されるようにStreamHandlerを追加します。ロガーのレベルは 211 引数で指定されたlog_levelに基づいて設定されます。 212 213 Parameters 214 ---------- 215 logger : Logger | None 216 使用するロガー。Noneの場合は新しいロガーを作成します。 217 log_level : int 218 ロガーのログレベル。デフォルトはINFO。 219 220 Returns 221 ---------- 222 Logger 223 設定されたロガーオブジェクト。 224 """ 225 if logger is not None and isinstance(logger, Logger): 226 return logger 227 # 渡されたロガーがNoneまたは正しいものでない場合は独自に設定 228 new_logger: Logger = getLogger() 229 # 既存のハンドラーをすべて削除 230 for handler in new_logger.handlers[:]: 231 new_logger.removeHandler(handler) 232 new_logger.setLevel(log_level) # ロガーのレベルを設定 233 ch = StreamHandler() 234 ch_formatter = Formatter("%(asctime)s - %(levelname)s - %(message)s") 235 ch.setFormatter(ch_formatter) # フォーマッターをハンドラーに設定 236 new_logger.addHandler(ch) # StreamHandlerの追加 237 return new_logger
ロガーを設定します。
このメソッドは、ロギングの設定を行い、ログメッセージのフォーマットを指定します。 ログメッセージには、日付、ログレベル、メッセージが含まれます。
渡されたロガーがNoneまたは不正な場合は、新たにロガーを作成し、標準出力に ログメッセージが表示されるようにStreamHandlerを追加します。ロガーのレベルは 引数で指定されたlog_levelに基づいて設定されます。
Parameters
logger : Logger | None
使用するロガー。Noneの場合は新しいロガーを作成します。
log_level : int
ロガーのログレベル。デフォルトはINFO。
Returns
Logger
設定されたロガーオブジェクト。
239 def calculate_flux_footprint( 240 self, 241 df: pd.DataFrame, 242 col_flux: str, 243 plot_count: int = 10000, 244 start_time: str = "10:00", 245 end_time: str = "16:00", 246 ) -> tuple[list[float], list[float], list[float]]: 247 """ 248 フラックスフットプリントを計算し、指定された時間帯のデータを基に可視化します。 249 250 Parameters 251 ---------- 252 df : pd.DataFrame 253 分析対象のデータフレーム。フラックスデータを含む。 254 col_flux : str 255 フラックスデータの列名。計算に使用される。 256 plot_count : int, optional 257 生成するプロットの数。デフォルトは10000。 258 start_time : str, optional 259 フットプリント計算に使用する開始時間。デフォルトは"10:00"。 260 end_time : str, optional 261 フットプリント計算に使用する終了時間。デフォルトは"16:00"。 262 263 Returns 264 ---------- 265 tuple[list[float], list[float], list[float]]: 266 x座標 (メートル): タワーを原点とした東西方向の距離 267 y座標 (メートル): タワーを原点とした南北方向の距離 268 対象スカラー量の値: 各地点でのフラックス値 269 270 Notes 271 ---------- 272 - 返却される座標は測定タワーを原点(0,0)とした相対位置です 273 - すべての距離はメートル単位で表されます 274 - 正のx値は東方向、正のy値は北方向を示します 275 Required columns (default names): 276 - Wind direction: 風向 (度) 277 - WS vector: 風速 (m/s) 278 - u*: 摩擦速度 (m/s) 279 - sigmaV: 風速の標準偏差 (m/s) 280 - z/L: 安定度パラメータ (無次元) 281 """ 282 col_weekday: str = self.COL_FFA_IS_WEEKDAY 283 df_copied: pd.DataFrame = df.copy() 284 285 # インデックスがdatetimeであることを確認し、必要に応じて変換 286 if not isinstance(df_copied.index, pd.DatetimeIndex): 287 df_copied.index = pd.to_datetime(df_copied.index) 288 289 # DatetimeIndexから直接dateプロパティにアクセス 290 datelist: np.ndarray = np.array(df_copied.index.date) 291 292 # 各日付が平日かどうかを判定し、リストに格納 293 numbers: list[int] = [ 294 FluxFootprintAnalyzer.is_weekday(date) for date in datelist 295 ] 296 297 # col_weekdayに基づいてデータフレームに平日情報を追加 298 df_copied.loc[:, col_weekday] = numbers # .locを使用して値を設定 299 300 # 値が1のもの(平日)をコピーする 301 data_weekday: pd.DataFrame = df_copied[df_copied[col_weekday] == 1].copy() 302 # 特定の時間帯を抽出 303 data_weekday = data_weekday.between_time( 304 start_time, end_time 305 ) # 引数を使用して時間帯を抽出 306 data_weekday = data_weekday.dropna(subset=[col_flux]) 307 308 directions: list[float] = [ 309 wind_direction if wind_direction >= 0 else wind_direction + 360 310 for wind_direction in data_weekday[ 311 self._cols[self._default_cols.WIND_DIRECTION] 312 ] 313 ] 314 315 data_weekday.loc[:, self.COL_FFA_WIND_DIR_360] = directions 316 data_weekday.loc[:, self.COL_FFA_RADIAN] = ( 317 data_weekday[self.COL_FFA_WIND_DIR_360] / 180 * np.pi 318 ) 319 320 # 風向が欠測なら除去 321 data_weekday = data_weekday.dropna( 322 subset=[self._cols[self._default_cols.WIND_DIRECTION], col_flux] 323 ) 324 325 # 数値型への変換を確実に行う 326 numeric_columns: list[str] = [ 327 self._cols[self._default_cols.FRICTION_VELOCITY], 328 self._cols[self._default_cols.WIND_SPEED], 329 self._cols[self._default_cols.SIGMA_V], 330 self._cols[self._default_cols.STABILITY], 331 ] 332 for col in numeric_columns: 333 data_weekday[col] = pd.to_numeric(data_weekday[col], errors="coerce") 334 335 # 地面修正量dの計算 336 z_m: float = self._z_m 337 z_d: float = FluxFootprintAnalyzer._calculate_ground_correction( 338 z_m=z_m, 339 wind_speed=data_weekday[self._cols[self._default_cols.WIND_SPEED]].values, 340 friction_velocity=data_weekday[ 341 self._cols[self._default_cols.FRICTION_VELOCITY] 342 ].values, 343 stability_parameter=data_weekday[ 344 self._cols[self._default_cols.STABILITY] 345 ].values, 346 ) 347 348 x_list: list[float] = [] 349 y_list: list[float] = [] 350 c_list: list[float] | None = [] 351 352 # tqdmを使用してプログレスバーを表示 353 for i in tqdm(range(len(data_weekday)), desc="Calculating footprint"): 354 dUstar: float = data_weekday[self._cols[self._default_cols.FRICTION_VELOCITY]].iloc[i] 355 dU: float = data_weekday[self._cols[self._default_cols.WIND_SPEED]].iloc[i] 356 sigmaV: float = data_weekday[self._cols[self._default_cols.SIGMA_V]].iloc[i] 357 dzL: float = data_weekday[self._cols[self._default_cols.STABILITY]].iloc[i] 358 359 360 if pd.isna(dUstar) or pd.isna(dU) or pd.isna(sigmaV) or pd.isna(dzL): 361 self.logger.warning(f"NaN fields are exist.: i = {i}") 362 continue 363 elif dUstar < 5.0 and dUstar != 0.0 and dU > 0.1: 364 phi_m, phi_c, n = FluxFootprintAnalyzer._calculate_stability_parameters( 365 dzL=dzL 366 ) 367 m, U, r, mu, ksi = ( 368 FluxFootprintAnalyzer._calculate_footprint_parameters( 369 dUstar=dUstar, dU=dU, z_d=z_d, phi_m=phi_m, phi_c=phi_c, n=n 370 ) 371 ) 372 373 # 80%ソースエリアの計算 374 x80: float = FluxFootprintAnalyzer._source_area_KM2001( 375 ksi=ksi, mu=mu, dU=dU, sigmaV=sigmaV, z_d=z_d, max_ratio=0.8 376 ) 377 378 if not np.isnan(x80): 379 x1, y1, flux1 = FluxFootprintAnalyzer._prepare_plot_data( 380 x80, 381 ksi, 382 mu, 383 r, 384 U, 385 m, 386 sigmaV, 387 data_weekday[col_flux].iloc[i], 388 plot_count=plot_count, 389 ) 390 x1_, y1_ = FluxFootprintAnalyzer._rotate_coordinates( 391 x=x1, y=y1, radian=data_weekday[self.COL_FFA_RADIAN].iloc[i] 392 ) 393 394 x_list.extend(x1_) 395 y_list.extend(y1_) 396 c_list.extend(flux1) 397 398 return ( 399 x_list, 400 y_list, 401 c_list, 402 )
フラックスフットプリントを計算し、指定された時間帯のデータを基に可視化します。
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値は北方向を示します
Required columns (default names):
- Wind direction: 風向 (度)
- WS vector: 風速 (m/s)
- u*: 摩擦速度 (m/s)
- sigmaV: 風速の標準偏差 (m/s)
- z/L: 安定度パラメータ (無次元)
404 def combine_all_data( 405 self, 406 data_source: str | pd.DataFrame, 407 col_datetime: str = "Date", 408 source_type: Literal["csv", "monthly"] = "csv", 409 ) -> pd.DataFrame: 410 """ 411 CSVファイルまたはMonthlyConverterからのデータを統合します 412 413 Parameters 414 ---------- 415 data_source : str | pd.DataFrame 416 CSVディレクトリパスまたはDataFrame 417 col_datetime :str 418 datetimeカラムのカラム名。デフォルトは"Date"。 419 source_type : str 420 "csv" または "monthly" 421 422 Returns 423 ---------- 424 pd.DataFrame 425 処理済みのデータフレーム 426 """ 427 col_weekday: str = self.COL_FFA_IS_WEEKDAY 428 if source_type == "csv": 429 # 既存のCSV処理ロジック 430 return self._combine_all_csv( 431 csv_dir_path=data_source, col_datetime=col_datetime 432 ) 433 elif source_type == "monthly": 434 # MonthlyConverterからのデータを処理 435 if not isinstance(data_source, pd.DataFrame): 436 raise ValueError("monthly形式の場合、DataFrameを直接渡す必要があります") 437 438 df: pd.DataFrame = data_source.copy() 439 440 # required_columnsからDateを除外して欠損値チェックを行う 441 check_columns: list[str] = [ 442 col for col in self._required_columns if col != col_datetime 443 ] 444 445 # インデックスがdatetimeであることを確認 446 if ( 447 not isinstance(df.index, pd.DatetimeIndex) 448 and col_datetime not in df.columns 449 ): 450 raise ValueError(f"DatetimeIndexまたは{col_datetime}カラムが必要です") 451 452 if col_datetime in df.columns: 453 df.set_index(col_datetime, inplace=True) 454 455 # 必要なカラムの存在確認 456 missing_columns = [ 457 col for col in check_columns if col not in df.columns.tolist() 458 ] 459 if missing_columns: 460 missing_cols = "','".join(missing_columns) 461 current_cols = "','".join(df.columns.tolist()) 462 raise ValueError( 463 f"必要なカラムが不足しています: '{missing_cols}'\n" 464 f"現在のカラム: '{current_cols}'" 465 ) 466 467 # 平日/休日の判定用カラムを追加 468 df[col_weekday] = df.index.map(FluxFootprintAnalyzer.is_weekday) 469 470 # Dateを除外したカラムで欠損値の処理 471 df = df.dropna(subset=check_columns) 472 473 # インデックスの重複を除去 474 df = df.loc[~df.index.duplicated(), :] 475 476 return df
CSVファイルまたはMonthlyConverterからのデータを統合します
Parameters
data_source : str | pd.DataFrame
CSVディレクトリパスまたはDataFrame
col_datetime :str
datetimeカラムのカラム名。デフォルトは"Date"。
source_type : str
"csv" または "monthly"
Returns
pd.DataFrame
処理済みのデータフレーム
478 def get_satellite_image_from_api( 479 self, 480 api_key: str, 481 center_lat: float, 482 center_lon: float, 483 output_path: str, 484 scale: int = 1, 485 size: tuple[int, int] = (2160, 2160), 486 zoom: int = 13, 487 ) -> ImageFile: 488 """ 489 Google Maps Static APIを使用して衛星画像を取得します。 490 491 Parameters 492 ---------- 493 api_key : str 494 Google Maps Static APIのキー。 495 center_lat : float 496 中心の緯度。 497 center_lon : float 498 中心の経度。 499 output_path : str 500 画像の保存先パス。拡張子は'.png'のみ許可される。 501 scale : int, optional 502 画像の解像度スケール(1か2)。デフォルトは1。 503 size : tuple[int, int], optional 504 画像サイズ (幅, 高さ)。デフォルトは(2160, 2160)。 505 zoom : int, optional 506 ズームレベル(0-21)。デフォルトは13。 507 508 Returns 509 ---------- 510 ImageFile 511 取得した衛星画像 512 513 Raises 514 ---------- 515 requests.RequestException 516 API呼び出しに失敗した場合 517 """ 518 # バリデーション 519 if not output_path.endswith(".png"): 520 raise ValueError("出力ファイル名は'.png'で終わる必要があります。") 521 522 # HTTPリクエストの定義 523 base_url = "https://maps.googleapis.com/maps/api/staticmap" 524 params = { 525 "center": f"{center_lat},{center_lon}", 526 "zoom": zoom, 527 "size": f"{size[0]}x{size[1]}", 528 "maptype": "satellite", 529 "scale": scale, 530 "key": api_key, 531 } 532 533 try: 534 response = requests.get(base_url, params=params) 535 response.raise_for_status() 536 # 画像ファイルに変換 537 image = Image.open(io.BytesIO(response.content)) 538 image.save(output_path) 539 self._got_satellite_image = True 540 self.logger.info(f"リモート画像を取得し、保存しました: {output_path}") 541 return image 542 except requests.RequestException as e: 543 self.logger.error(f"衛星画像の取得に失敗しました: {str(e)}") 544 raise e
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呼び出しに失敗した場合
546 def get_satellite_image_from_local( 547 self, 548 local_image_path: str, 549 alpha: float = 1.0, 550 grayscale: bool = False, 551 ) -> ImageFile: 552 """ 553 ローカルファイルから衛星画像を読み込みます。 554 555 Parameters 556 ---------- 557 local_image_path : str 558 ローカル画像のパス 559 alpha : float, optional 560 画像の透過率(0.0~1.0)。デフォルトは1.0。 561 grayscale : bool, optional 562 Trueの場合、画像を白黒に変換します。デフォルトはFalse。 563 564 Returns 565 ---------- 566 ImageFile 567 読み込んだ衛星画像(透過設定済み) 568 569 Raises 570 ---------- 571 FileNotFoundError 572 指定されたパスにファイルが存在しない場合 573 """ 574 if not os.path.exists(local_image_path): 575 raise FileNotFoundError( 576 f"指定されたローカル画像が存在しません: {local_image_path}" 577 ) 578 579 # 画像を読み込む 580 image: ImageFile = Image.open(local_image_path) 581 582 # 白黒変換が指定されている場合 583 if grayscale: 584 image = image.convert("L") # グレースケールに変換 585 586 # RGBAモードに変換 587 image = image.convert("RGBA") 588 589 # 透過率を設定 590 data = image.getdata() 591 new_data = [(r, g, b, int(255 * alpha)) for r, g, b, a in data] 592 image.putdata(new_data) 593 594 self._got_satellite_image = True 595 self.logger.info( 596 f"ローカル画像を使用しました(透過率: {alpha}, 白黒: {grayscale}): {local_image_path}" 597 ) 598 return image
ローカルファイルから衛星画像を読み込みます。
Parameters
local_image_path : str
ローカル画像のパス
alpha : float, optional
画像の透過率(0.0~1.0)。デフォルトは1.0。
grayscale : bool, optional
Trueの場合、画像を白黒に変換します。デフォルトはFalse。
Returns
ImageFile
読み込んだ衛星画像(透過設定済み)
Raises
FileNotFoundError
指定されたパスにファイルが存在しない場合
600 def plot_flux_footprint( 601 self, 602 x_list: list[float], 603 y_list: list[float], 604 c_list: list[float] | None, 605 center_lat: float, 606 center_lon: float, 607 vmin: float, 608 vmax: float, 609 add_cbar: bool = True, 610 add_legend: bool = True, 611 cbar_label: str | None = None, 612 cbar_labelpad: int = 20, 613 cmap: str = "jet", 614 reduce_c_function: callable = np.mean, 615 lat_correction: float = 1, 616 lon_correction: float = 1, 617 output_dir: str | Path | None = None, 618 output_filename: str = "footprint.png", 619 save_fig: bool = True, 620 show_fig: bool = True, 621 satellite_image: ImageFile | None = None, 622 xy_max: float = 5000, 623 ) -> None: 624 """ 625 フットプリントデータをプロットします。 626 627 このメソッドは、指定されたフットプリントデータのみを可視化します。 628 629 Parameters 630 ---------- 631 x_list : list[float] 632 フットプリントのx座標リスト(メートル単位)。 633 y_list : list[float] 634 フットプリントのy座標リスト(メートル単位)。 635 c_list : list[float] | None 636 フットプリントの強度を示す値のリスト。 637 center_lat : float 638 プロットの中心となる緯度。 639 center_lon : float 640 プロットの中心となる経度。 641 cmap : str 642 使用するカラーマップの名前。 643 vmin : float 644 カラーバーの最小値。 645 vmax : float 646 カラーバーの最大値。 647 reduce_c_function : callable, optional 648 フットプリントの集約関数(デフォルトはnp.mean)。 649 cbar_label : str | None, optional 650 カラーバーのラベル。 651 cbar_labelpad : int, optional 652 カラーバーラベルのパディング。 653 lon_correction : float, optional 654 経度方向の補正係数(デフォルトは1)。 655 lat_correction : float, optional 656 緯度方向の補正係数(デフォルトは1)。 657 output_dir : str | Path | None, optional 658 プロット画像の保存先パス。 659 output_filename : str 660 プロット画像の保存ファイル名(拡張子を含む)。デフォルトは'footprint.png'。 661 save_fig : bool 662 図の保存を許可するフラグ。デフォルトはTrue。 663 show_fig : bool 664 図の表示を許可するフラグ。デフォルトはTrue。 665 satellite_image : ImageFile | None, optional 666 使用する衛星画像。指定がない場合はデフォルトの画像が生成されます。 667 xy_max : float, optional 668 表示範囲の最大値(デフォルトは4000)。 669 """ 670 self.plot_flux_footprint_with_hotspots( 671 x_list=x_list, 672 y_list=y_list, 673 c_list=c_list, 674 center_lat=center_lat, 675 center_lon=center_lon, 676 vmin=vmin, 677 vmax=vmax, 678 add_cbar=add_cbar, 679 add_legend=add_legend, 680 cbar_label=cbar_label, 681 cbar_labelpad=cbar_labelpad, 682 cmap=cmap, 683 reduce_c_function=reduce_c_function, 684 hotspots=None, # hotspotsをNoneに設定 685 hotspot_colors=None, 686 lat_correction=lat_correction, 687 lon_correction=lon_correction, 688 output_dir=output_dir, 689 output_filename=output_filename, 690 save_fig=save_fig, 691 show_fig=show_fig, 692 satellite_image=satellite_image, 693 xy_max=xy_max, 694 )
フットプリントデータをプロットします。
このメソッドは、指定されたフットプリントデータのみを可視化します。
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 | Path | 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)。
696 def plot_flux_footprint_with_hotspots( 697 self, 698 x_list: list[float], 699 y_list: list[float], 700 c_list: list[float] | None, 701 center_lat: float, 702 center_lon: float, 703 vmin: float, 704 vmax: float, 705 add_cbar: bool = True, 706 add_legend: bool = True, 707 cbar_label: str | None = None, 708 cbar_labelpad: int = 20, 709 cmap: str = "jet", 710 reduce_c_function: callable = np.mean, 711 hotspots: list[HotspotData] | None = None, 712 hotspots_alpha: float = 0.7, 713 hotspot_colors: dict[HotspotType, str] | None = None, 714 hotspot_labels: dict[HotspotType, str] | None = None, 715 hotspot_markers: dict[HotspotType, str] | None = None, 716 legend_bbox_to_anchor: tuple[float, float] = (0.55, -0.01), 717 lat_correction: float = 1, 718 lon_correction: float = 1, 719 output_dir: str | Path | None = None, 720 output_filename: str = "footprint.png", 721 save_fig: bool = True, 722 show_fig: bool = True, 723 satellite_image: ImageFile | None = None, 724 xy_max: float = 5000, 725 ) -> None: 726 """ 727 Staticな衛星画像上にフットプリントデータとホットスポットをプロットします。 728 729 このメソッドは、指定されたフットプリントデータとホットスポットを可視化します。 730 ホットスポットが指定されない場合は、フットプリントのみ作図します。 731 732 Parameters 733 ---------- 734 x_list : list[float] 735 フットプリントのx座標リスト(メートル単位)。 736 y_list : list[float] 737 フットプリントのy座標リスト(メートル単位)。 738 c_list : list[float] | None 739 フットプリントの強度を示す値のリスト。 740 center_lat : float 741 プロットの中心となる緯度。 742 center_lon : float 743 プロットの中心となる経度。 744 vmin : float 745 カラーバーの最小値。 746 vmax : float 747 カラーバーの最大値。 748 add_cbar : bool, optional 749 カラーバーを追加するかどうか(デフォルトはTrue)。 750 add_legend : bool, optional 751 凡例を追加するかどうか(デフォルトはTrue)。 752 cbar_label : str | None, optional 753 カラーバーのラベル。 754 cbar_labelpad : int, optional 755 カラーバーラベルのパディング。 756 cmap : str 757 使用するカラーマップの名前。 758 reduce_c_function : callable 759 フットプリントの集約関数(デフォルトはnp.mean)。 760 hotspots : list[HotspotData] | None, optional 761 ホットスポットデータのリスト。デフォルトはNone。 762 hotspots_alpha : float, optional 763 ホットスポットの透明度。デフォルトは0.7。 764 hotspot_colors : dict[HotspotType, str] | None, optional 765 ホットスポットの色を指定する辞書。 766 例: {'bio': 'blue', 'gas': 'red', 'comb': 'green'} 767 hotspot_labels : dict[HotspotType, str] | None, optional 768 ホットスポットの表示ラベルを指定する辞書。 769 例: {'bio': '生物', 'gas': 'ガス', 'comb': '燃焼'} 770 hotspot_markers : dict[HotspotType, str] | None, optional 771 ホットスポットの形状を指定する辞書。 772 例: {'bio': '^', 'gas': 'o', 'comb': 's'} 773 legend_bbox_to_anchor : tuple[float, flaot], optional 774 ホットスポットの凡例の位置。デフォルトは (0.55, -0.01) 。 775 lat_correction : float, optional 776 緯度方向の補正係数(デフォルトは1)。 777 lon_correction : float, optional 778 経度方向の補正係数(デフォルトは1)。 779 output_dir : str | Path | None, optional 780 プロット画像の保存先パス。 781 output_filename : str 782 プロット画像の保存ファイル名(拡張子を含む)。デフォルトは'footprint.png'。 783 save_fig : bool 784 図の保存を許可するフラグ。デフォルトはTrue。 785 show_fig : bool 786 図の表示を許可するフラグ。デフォルトはTrue。 787 satellite_image : ImageFile | None, optional 788 使用する衛星画像。指定がない場合はデフォルトの画像が生成されます。 789 xy_max : float, optional 790 表示範囲の最大値(デフォルトは5000)。 791 """ 792 # 1. 引数のバリデーション 793 valid_extensions: list[str] = [".png", ".jpg", ".jpeg", ".pdf", ".svg"] 794 _, file_extension = os.path.splitext(output_filename) 795 if file_extension.lower() not in valid_extensions: 796 quoted_extensions: list[str] = [f'"{ext}"' for ext in valid_extensions] 797 self.logger.error( 798 f"`output_filename`は有効な拡張子ではありません。プロットを保存するには、次のいずれかを指定してください: {','.join(quoted_extensions)}" 799 ) 800 return 801 802 # 2. フラグチェック 803 if not self._got_satellite_image: 804 raise ValueError( 805 "`get_satellite_image_from_api`または`get_satellite_image_from_local`が実行されていません。" 806 ) 807 808 # 3. 衛星画像の取得 809 if satellite_image is None: 810 satellite_image = Image.new("RGB", (2160, 2160), "lightgray") 811 812 self.logger.info("プロットを作成中...") 813 814 # 4. 座標変換のための定数計算(1回だけ) 815 meters_per_lat: float = self.EARTH_RADIUS_METER * ( 816 math.pi / 180 817 ) # 緯度1度あたりのメートル 818 meters_per_lon: float = meters_per_lat * math.cos( 819 math.radians(center_lat) 820 ) # 経度1度あたりのメートル 821 822 # 5. フットプリントデータの座標変換(まとめて1回で実行) 823 x_deg = ( 824 np.array(x_list) / meters_per_lon * lon_correction 825 ) # 補正係数も同時に適用 826 y_deg = ( 827 np.array(y_list) / meters_per_lat * lat_correction 828 ) # 補正係数も同時に適用 829 830 # 6. 中心点からの相対座標を実際の緯度経度に変換 831 lons = center_lon + x_deg 832 lats = center_lat + y_deg 833 834 # 7. 表示範囲の計算(変更なし) 835 x_range: float = xy_max / meters_per_lon 836 y_range: float = xy_max / meters_per_lat 837 map_boundaries: tuple[float, float, float, float] = ( 838 center_lon - x_range, # left_lon 839 center_lon + x_range, # right_lon 840 center_lat - y_range, # bottom_lat 841 center_lat + y_range, # top_lat 842 ) 843 left_lon, right_lon, bottom_lat, top_lat = map_boundaries 844 845 # 8. プロットの作成 846 plt.rcParams["axes.edgecolor"] = "None" 847 fig: plt.Figure = plt.figure(figsize=(10, 8), dpi=300) 848 ax_data: plt.Axes = fig.add_axes([0.05, 0.1, 0.8, 0.8]) 849 850 # 9. フットプリントの描画 851 # フットプリントの描画とカラーバー用の2つのhexbinを作成 852 if c_list is not None: 853 ax_data.hexbin( 854 lons, 855 lats, 856 C=c_list, 857 cmap=cmap, 858 vmin=vmin, 859 vmax=vmax, 860 alpha=0.3, # 実際のプロット用 861 gridsize=100, 862 linewidths=0, 863 mincnt=100, 864 extent=[left_lon, right_lon, bottom_lat, top_lat], 865 reduce_C_function=reduce_c_function, 866 ) 867 868 # カラーバー用の非表示hexbin(alpha=1.0) 869 hidden_hexbin = ax_data.hexbin( 870 lons, 871 lats, 872 C=c_list, 873 cmap=cmap, 874 vmin=vmin, 875 vmax=vmax, 876 alpha=1.0, # カラーバー用 877 gridsize=100, 878 linewidths=0, 879 mincnt=100, 880 extent=[left_lon, right_lon, bottom_lat, top_lat], 881 reduce_C_function=reduce_c_function, 882 visible=False, # プロットには表示しない 883 ) 884 885 # 10. ホットスポットの描画 886 spot_handles = [] 887 if hotspots is not None: 888 default_colors: dict[HotspotType, str] = { 889 "bio": "blue", 890 "gas": "red", 891 "comb": "green", 892 } 893 894 # デフォルトのマーカー形状を定義 895 default_markers: dict[HotspotType, str] = { 896 "bio": "^", # 三角 897 "gas": "o", # 丸 898 "comb": "s", # 四角 899 } 900 901 # デフォルトのラベルを定義 902 default_labels: dict[HotspotType, str] = { 903 "bio": "bio", 904 "gas": "gas", 905 "comb": "comb", 906 } 907 908 # 座標変換のための定数 909 meters_per_lat: float = self.EARTH_RADIUS_METER * (math.pi / 180) 910 meters_per_lon: float = meters_per_lat * math.cos(math.radians(center_lat)) 911 912 for spot_type, color in (hotspot_colors or default_colors).items(): 913 spots_lon = [] 914 spots_lat = [] 915 916 # 使用するマーカーを決定 917 marker = (hotspot_markers or default_markers).get(spot_type, "o") 918 919 for spot in hotspots: 920 if spot.type == spot_type: 921 # 変換前の緯度経度をログ出力 922 self.logger.debug( 923 f"Before - Type: {spot_type}, Lat: {spot.avg_lat:.6f}, Lon: {spot.avg_lon:.6f}" 924 ) 925 926 # 中心からの相対距離を計算 927 dx: float = (spot.avg_lon - center_lon) * meters_per_lon 928 dy: float = (spot.avg_lat - center_lat) * meters_per_lat 929 930 # 補正前の相対座標をログ出力 931 self.logger.debug( 932 f"Relative - Type: {spot_type}, X: {dx:.2f}m, Y: {dy:.2f}m" 933 ) 934 935 # 補正を適用 936 corrected_dx: float = dx * lon_correction 937 corrected_dy: float = dy * lat_correction 938 939 # 補正後の緯度経度を計算 940 adjusted_lon: float = center_lon + corrected_dx / meters_per_lon 941 adjusted_lat: float = center_lat + corrected_dy / meters_per_lat 942 943 # 変換後の緯度経度をログ出力 944 self.logger.debug( 945 f"After - Type: {spot_type}, Lat: {adjusted_lat:.6f}, Lon: {adjusted_lon:.6f}\n" 946 ) 947 948 if ( 949 left_lon <= adjusted_lon <= right_lon 950 and bottom_lat <= adjusted_lat <= top_lat 951 ): 952 spots_lon.append(adjusted_lon) 953 spots_lat.append(adjusted_lat) 954 955 if spots_lon: 956 # 使用するラベルを決定 957 label = (hotspot_labels or default_labels).get(spot_type, spot_type) 958 959 handle = ax_data.scatter( 960 spots_lon, 961 spots_lat, 962 c=color, 963 marker=marker, # マーカー形状を指定 964 s=100, 965 alpha=hotspots_alpha, 966 label=label, 967 edgecolor="black", 968 linewidth=1, 969 ) 970 spot_handles.append(handle) 971 972 # 11. 背景画像の設定 973 ax_img = ax_data.twiny().twinx() 974 ax_img.imshow( 975 satellite_image, 976 extent=[left_lon, right_lon, bottom_lat, top_lat], 977 aspect="equal", 978 ) 979 980 # 12. 軸の設定 981 for ax in [ax_data, ax_img]: 982 ax.set_xlim(left_lon, right_lon) 983 ax.set_ylim(bottom_lat, top_lat) 984 ax.set_xticks([]) 985 ax.set_yticks([]) 986 987 ax_data.set_zorder(2) 988 ax_data.patch.set_alpha(0) 989 ax_img.set_zorder(1) 990 991 # 13. カラーバーの追加 992 if add_cbar: 993 cbar_ax: plt.Axes = fig.add_axes([0.88, 0.1, 0.03, 0.8]) 994 cbar = fig.colorbar(hidden_hexbin, cax=cbar_ax) # hidden_hexbinを使用 995 # cbar_labelが指定されている場合のみラベルを設定 996 if cbar_label: 997 cbar.set_label(cbar_label, rotation=270, labelpad=cbar_labelpad) 998 999 # 14. ホットスポットの凡例追加 1000 if add_legend and hotspots and spot_handles: 1001 ax_data.legend( 1002 handles=spot_handles, 1003 loc="upper center", 1004 bbox_to_anchor=legend_bbox_to_anchor, # 図の下に配置 1005 ncol=len(spot_handles), # ハンドルの数に応じて列数を設定 1006 ) 1007 1008 # 15. 画像の保存 1009 if save_fig: 1010 if output_dir is None: 1011 raise ValueError( 1012 "save_fig=Trueの場合、output_dirを指定する必要があります。有効なディレクトリパスを指定してください。" 1013 ) 1014 output_path: str = os.path.join(output_dir, output_filename) 1015 self.logger.info("プロットを保存中...") 1016 try: 1017 fig.savefig(output_path, bbox_inches="tight") 1018 self.logger.info(f"プロットが正常に保存されました: {output_path}") 1019 except Exception as e: 1020 self.logger.error(f"プロットの保存中にエラーが発生しました: {str(e)}") 1021 # 16. 画像の表示 1022 if show_fig: 1023 plt.show() 1024 else: 1025 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。
hotspots_alpha : float, optional
ホットスポットの透明度。デフォルトは0.7。
hotspot_colors : dict[HotspotType, str] | None, optional
ホットスポットの色を指定する辞書。
例: {'bio': 'blue', 'gas': 'red', 'comb': 'green'}
hotspot_labels : dict[HotspotType, str] | None, optional
ホットスポットの表示ラベルを指定する辞書。
例: {'bio': '生物', 'gas': 'ガス', 'comb': '燃焼'}
hotspot_markers : dict[HotspotType, str] | None, optional
ホットスポットの形状を指定する辞書。
例: {'bio': '^', 'gas': 'o', 'comb': 's'}
legend_bbox_to_anchor : tuple[float, flaot], optional
ホットスポットの凡例の位置。デフォルトは (0.55, -0.01) 。
lat_correction : float, optional
緯度方向の補正係数(デフォルトは1)。
lon_correction : float, optional
経度方向の補正係数(デフォルトは1)。
output_dir : str | Path | 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)。
1027 def plot_flux_footprint_with_scale_checker( 1028 self, 1029 x_list: list[float], 1030 y_list: list[float], 1031 c_list: list[float] | None, 1032 center_lat: float, 1033 center_lon: float, 1034 check_points: list[tuple[float, float, str]] | None = None, 1035 vmin: float = 0, 1036 vmax: float = 100, 1037 add_cbar: bool = True, 1038 cbar_label: str | None = None, 1039 cbar_labelpad: int = 20, 1040 cmap: str = "jet", 1041 reduce_c_function: callable = np.mean, 1042 lat_correction: float = 1, 1043 lon_correction: float = 1, 1044 output_dir: str | Path | None = None, 1045 output_filename: str = "footprint-scale_checker.png", 1046 save_fig: bool = True, 1047 show_fig: bool = True, 1048 satellite_image: ImageFile | None = None, 1049 xy_max: float = 5000, 1050 ) -> None: 1051 """ 1052 Staticな衛星画像上にフットプリントデータとホットスポットをプロットします。 1053 1054 このメソッドは、指定されたフットプリントデータとホットスポットを可視化します。 1055 ホットスポットが指定されない場合は、フットプリントのみ作図します。 1056 1057 Parameters 1058 ---------- 1059 x_list : list[float] 1060 フットプリントのx座標リスト(メートル単位)。 1061 y_list : list[float] 1062 フットプリントのy座標リスト(メートル単位)。 1063 c_list : list[float] | None 1064 フットプリントの強度を示す値のリスト。 1065 center_lat : float 1066 プロットの中心となる緯度。 1067 center_lon : float 1068 プロットの中心となる経度。 1069 check_points : list[tuple[float, float, str]] | None 1070 確認用の地点リスト。各要素は (緯度, 経度, ラベル) のタプル。 1071 Noneの場合は中心から500m、1000m、2000m、3000mの位置に仮想的な点を配置。 1072 cmap : str 1073 使用するカラーマップの名前。 1074 vmin : float 1075 カラーバーの最小値。 1076 vmax : float 1077 カラーバーの最大値。 1078 reduce_c_function : callable, optional 1079 フットプリントの集約関数(デフォルトはnp.mean)。 1080 cbar_label : str, optional 1081 カラーバーのラベル。 1082 cbar_labelpad : int, optional 1083 カラーバーラベルのパディング。 1084 hotspots : list[HotspotData] | None 1085 ホットスポットデータのリスト。デフォルトはNone。 1086 hotspot_colors : dict[str, str] | None, optional 1087 ホットスポットの色を指定する辞書。 1088 lon_correction : float, optional 1089 経度方向の補正係数(デフォルトは1)。 1090 lat_correction : float, optional 1091 緯度方向の補正係数(デフォルトは1)。 1092 output_dir : str | Path | None, optional 1093 プロット画像の保存先パス。 1094 output_filename : str 1095 プロット画像の保存ファイル名(拡張子を含む)。デフォルトは'footprint.png'。 1096 save_fig : bool 1097 図の保存を許可するフラグ。デフォルトはTrue。 1098 show_fig : bool 1099 図の表示を許可するフラグ。デフォルトはTrue。 1100 satellite_image : ImageFile | None, optional 1101 使用する衛星画像。指定がない場合はデフォルトの画像が生成されます。 1102 xy_max : float, optional 1103 表示範囲の最大値(デフォルトは5000)。 1104 """ 1105 if check_points is None: 1106 # デフォルトの確認ポイントを生成(従来の方式) 1107 default_points = [ 1108 (500, "North", 90), # 北 500m 1109 (1000, "East", 0), # 東 1000m 1110 (2000, "South", 270), # 南 2000m 1111 (3000, "West", 180), # 西 3000m 1112 ] 1113 1114 dummy_hotspots = [] 1115 for distance, direction, angle in default_points: 1116 rad = math.radians(angle) 1117 meters_per_lat = self.EARTH_RADIUS_METER * (math.pi / 180) 1118 meters_per_lon = meters_per_lat * math.cos(math.radians(center_lat)) 1119 1120 dx = distance * math.cos(rad) 1121 dy = distance * math.sin(rad) 1122 1123 delta_lon = dx / meters_per_lon 1124 delta_lat = dy / meters_per_lat 1125 1126 hotspot = HotspotData( 1127 avg_lat=center_lat + delta_lat, 1128 avg_lon=center_lon + delta_lon, 1129 delta_ch4=0.0, 1130 delta_c2h6=0.0, 1131 ratio=0.0, 1132 type=f"{direction}_{distance}m", 1133 section=0, 1134 source="scale_check", 1135 angle=0, 1136 correlation=0, 1137 ) 1138 dummy_hotspots.append(hotspot) 1139 else: 1140 # 指定された緯度経度を使用 1141 dummy_hotspots = [] 1142 for lat, lon, label in check_points: 1143 hotspot = HotspotData( 1144 avg_lat=lat, 1145 avg_lon=lon, 1146 delta_ch4=0.0, 1147 delta_c2h6=0.0, 1148 ratio=0.0, 1149 type=label, 1150 section=0, 1151 source="scale_check", 1152 angle=0, 1153 correlation=0, 1154 ) 1155 dummy_hotspots.append(hotspot) 1156 1157 # カスタムカラーマップの作成 1158 hotspot_colors = { 1159 spot.type: plt.cm.tab10(i % 10) for i, spot in enumerate(dummy_hotspots) 1160 } 1161 1162 # 既存のメソッドを呼び出してプロット 1163 self.plot_flux_footprint_with_hotspots( 1164 x_list=x_list, 1165 y_list=y_list, 1166 c_list=c_list, 1167 center_lat=center_lat, 1168 center_lon=center_lon, 1169 vmin=vmin, 1170 vmax=vmax, 1171 add_cbar=add_cbar, 1172 add_legend=True, 1173 cbar_label=cbar_label, 1174 cbar_labelpad=cbar_labelpad, 1175 cmap=cmap, 1176 reduce_c_function=reduce_c_function, 1177 hotspots=dummy_hotspots, 1178 hotspot_colors=hotspot_colors, 1179 lat_correction=lat_correction, 1180 lon_correction=lon_correction, 1181 output_dir=output_dir, 1182 output_filename=output_filename, 1183 save_fig=save_fig, 1184 show_fig=show_fig, 1185 satellite_image=satellite_image, 1186 xy_max=xy_max, 1187 )
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 | Path | 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)。
1416 @staticmethod 1417 def filter_data( 1418 df: pd.DataFrame, 1419 start_date: str | None = None, 1420 end_date: str | None = None, 1421 months: list[int] | None = None, 1422 ) -> pd.DataFrame: 1423 """ 1424 指定された期間や月でデータをフィルタリングするメソッド。 1425 1426 Parameters 1427 ---------- 1428 df : pd.DataFrame 1429 フィルタリングするデータフレーム 1430 start_date : str | None 1431 フィルタリングの開始日('YYYY-MM-DD'形式)。デフォルトはNone。 1432 end_date : str | None 1433 フィルタリングの終了日('YYYY-MM-DD'形式)。デフォルトはNone。 1434 months : list[int] | None 1435 フィルタリングする月のリスト(例:[1, 2, 12])。デフォルトはNone。 1436 1437 Returns 1438 ---------- 1439 pd.DataFrame 1440 フィルタリングされたデータフレーム 1441 1442 Raises 1443 ---------- 1444 ValueError 1445 インデックスがDatetimeIndexでない場合、または日付の形式が不正な場合 1446 """ 1447 # インデックスの検証 1448 if not isinstance(df.index, pd.DatetimeIndex): 1449 raise ValueError( 1450 "DataFrameのインデックスはDatetimeIndexである必要があります" 1451 ) 1452 1453 df_copied: pd.DataFrame = df.copy() 1454 1455 # 日付形式の検証と変換 1456 try: 1457 if start_date is not None: 1458 start_date = pd.to_datetime(start_date) 1459 if end_date is not None: 1460 end_date = pd.to_datetime(end_date) 1461 except ValueError as e: 1462 raise ValueError( 1463 "日付の形式が不正です。'YYYY-MM-DD'形式で指定してください" 1464 ) from e 1465 1466 # 期間でフィルタリング 1467 if start_date is not None or end_date is not None: 1468 df_copied = df_copied.loc[start_date:end_date] 1469 1470 # 月のバリデーション 1471 if months is not None: 1472 if not all(isinstance(m, int) and 1 <= m <= 12 for m in months): 1473 raise ValueError( 1474 "monthsは1から12までの整数のリストである必要があります" 1475 ) 1476 df_copied = df_copied[df_copied.index.month.isin(months)] 1477 1478 # フィルタリング後のデータが空でないことを確認 1479 if df_copied.empty: 1480 raise ValueError("フィルタリング後のデータが空になりました") 1481 1482 return df_copied
指定された期間や月でデータをフィルタリングするメソッド。
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でない場合、または日付の形式が不正な場合
1484 @staticmethod 1485 def is_weekday(date: datetime) -> int: 1486 """ 1487 指定された日付が平日であるかどうかを判定します。 1488 1489 Parameters 1490 ---------- 1491 date : datetime 1492 判定する日付。 1493 1494 Returns 1495 ---------- 1496 int 1497 平日であれば1、そうでなければ0。 1498 """ 1499 return 1 if not jpholiday.is_holiday(date) and date.weekday() < 5 else 0
指定された日付が平日であるかどうかを判定します。
Parameters
date : datetime
判定する日付。
Returns
int
平日であれば1、そうでなければ0。
47class CorrectingUtils: 48 @staticmethod 49 def correct_h2o_interference( 50 df: pd.DataFrame, 51 coef_a: float, 52 coef_b: float, 53 coef_c: float, 54 col_ch4_ppm: str = "ch4_ppm", 55 col_h2o_ppm: str = "h2o_ppm", 56 h2o_ppm_threshold: float | None = 2000, 57 ) -> pd.DataFrame: 58 """ 59 水蒸気干渉を補正するためのメソッドです。 60 CH4濃度に対する水蒸気の影響を2次関数を用いて補正します。 61 62 References 63 ---------- 64 - Commane et al. (2023): Intercomparison of commercial analyzers for atmospheric ethane and methane observations 65 https://amt.copernicus.org/articles/16/1431/2023/, 66 https://amt.copernicus.org/articles/16/1431/2023/amt-16-1431-2023.pdf 67 68 Parameters 69 ---------- 70 df : pd.DataFrame 71 補正対象のデータフレーム 72 coef_a : float 73 補正曲線の切片 74 coef_b : float 75 補正曲線の1次係数 76 coef_c : float 77 補正曲線の2次係数 78 col_ch4_ppm : str 79 CH4濃度を示すカラム名 80 col_h2o_ppm : str 81 水蒸気濃度を示すカラム名 82 h2o_ppm_threshold : float | None 83 水蒸気濃度の下限値(この値未満のデータは除外) 84 85 Returns 86 ---------- 87 pd.DataFrame 88 水蒸気干渉が補正されたデータフレーム 89 """ 90 # 元のデータを保護するためコピーを作成 91 df_h2o_corrected = df.copy() 92 # 水蒸気濃度の配列を取得 93 h2o = np.array(df_h2o_corrected[col_h2o_ppm]) 94 95 # 補正項の計算 96 correction_curve = coef_a + coef_b * h2o + coef_c * pow(h2o, 2) 97 max_correction = np.max(correction_curve) 98 correction_term = -(correction_curve - max_correction) 99 100 # CH4濃度の補正 101 df_h2o_corrected[col_ch4_ppm] = df_h2o_corrected[col_ch4_ppm] + correction_term 102 103 # 極端に低い水蒸気濃度のデータは信頼性が低いため除外 104 if h2o_ppm_threshold is not None: 105 df_h2o_corrected.loc[df[col_h2o_ppm] < h2o_ppm_threshold, col_ch4_ppm] = np.nan 106 df_h2o_corrected = df_h2o_corrected.dropna(subset=[col_ch4_ppm]) 107 108 return df_h2o_corrected 109 110 @staticmethod 111 def remove_bias( 112 df: pd.DataFrame, 113 col_ch4_ppm: str = "ch4_ppm", 114 col_c2h6_ppb: str = "c2h6_ppb", 115 base_ch4_ppm: float = 2.0, 116 base_c2h6_ppb: float = 0, 117 percentile: float = 5, 118 ) -> pd.DataFrame: 119 """ 120 データフレームからバイアスを除去します。 121 122 Parameters 123 ---------- 124 df : pd.DataFrame 125 バイアスを除去する対象のデータフレーム。 126 col_ch4_ppm : str 127 CH4濃度を示すカラム名。デフォルトは"ch4_ppm"。 128 col_c2h6_ppb : str 129 C2H6濃度を示すカラム名。デフォルトは"c2h6_ppb"。 130 base_ch4_ppm : float 131 補正前の値から最小値を引いた後に足すCH4濃度。 132 base_c2h6_ppb : float 133 補正前の値から最小値を引いた後に足すC2H6濃度。 134 percentile : float 135 下位何パーセンタイルの値を最小値として補正を行うか。 136 137 Returns 138 ---------- 139 pd.DataFrame 140 バイアスが除去されたデータフレーム。 141 """ 142 df_copied: pd.DataFrame = df.copy() 143 c2h6_min = np.percentile(df_copied[col_c2h6_ppb], percentile) 144 df_copied[col_c2h6_ppb] = df_copied[col_c2h6_ppb] - c2h6_min + base_c2h6_ppb 145 ch4_min = np.percentile(df_copied[col_ch4_ppm], percentile) 146 df_copied[col_ch4_ppm] = df_copied[col_ch4_ppm] - ch4_min + base_ch4_ppm 147 return df_copied
48 @staticmethod 49 def correct_h2o_interference( 50 df: pd.DataFrame, 51 coef_a: float, 52 coef_b: float, 53 coef_c: float, 54 col_ch4_ppm: str = "ch4_ppm", 55 col_h2o_ppm: str = "h2o_ppm", 56 h2o_ppm_threshold: float | None = 2000, 57 ) -> pd.DataFrame: 58 """ 59 水蒸気干渉を補正するためのメソッドです。 60 CH4濃度に対する水蒸気の影響を2次関数を用いて補正します。 61 62 References 63 ---------- 64 - Commane et al. (2023): Intercomparison of commercial analyzers for atmospheric ethane and methane observations 65 https://amt.copernicus.org/articles/16/1431/2023/, 66 https://amt.copernicus.org/articles/16/1431/2023/amt-16-1431-2023.pdf 67 68 Parameters 69 ---------- 70 df : pd.DataFrame 71 補正対象のデータフレーム 72 coef_a : float 73 補正曲線の切片 74 coef_b : float 75 補正曲線の1次係数 76 coef_c : float 77 補正曲線の2次係数 78 col_ch4_ppm : str 79 CH4濃度を示すカラム名 80 col_h2o_ppm : str 81 水蒸気濃度を示すカラム名 82 h2o_ppm_threshold : float | None 83 水蒸気濃度の下限値(この値未満のデータは除外) 84 85 Returns 86 ---------- 87 pd.DataFrame 88 水蒸気干渉が補正されたデータフレーム 89 """ 90 # 元のデータを保護するためコピーを作成 91 df_h2o_corrected = df.copy() 92 # 水蒸気濃度の配列を取得 93 h2o = np.array(df_h2o_corrected[col_h2o_ppm]) 94 95 # 補正項の計算 96 correction_curve = coef_a + coef_b * h2o + coef_c * pow(h2o, 2) 97 max_correction = np.max(correction_curve) 98 correction_term = -(correction_curve - max_correction) 99 100 # CH4濃度の補正 101 df_h2o_corrected[col_ch4_ppm] = df_h2o_corrected[col_ch4_ppm] + correction_term 102 103 # 極端に低い水蒸気濃度のデータは信頼性が低いため除外 104 if h2o_ppm_threshold is not None: 105 df_h2o_corrected.loc[df[col_h2o_ppm] < h2o_ppm_threshold, col_ch4_ppm] = np.nan 106 df_h2o_corrected = df_h2o_corrected.dropna(subset=[col_ch4_ppm]) 107 108 return df_h2o_corrected
水蒸気干渉を補正するためのメソッドです。 CH4濃度に対する水蒸気の影響を2次関数を用いて補正します。
References
- Commane et al. (2023): Intercomparison of commercial analyzers for atmospheric ethane and methane observations
https://amt.copernicus.org/articles/16/1431/2023/,
https://amt.copernicus.org/articles/16/1431/2023/amt-16-1431-2023.pdf
Parameters
df : pd.DataFrame
補正対象のデータフレーム
coef_a : float
補正曲線の切片
coef_b : float
補正曲線の1次係数
coef_c : float
補正曲線の2次係数
col_ch4_ppm : str
CH4濃度を示すカラム名
col_h2o_ppm : str
水蒸気濃度を示すカラム名
h2o_ppm_threshold : float | None
水蒸気濃度の下限値(この値未満のデータは除外)
Returns
pd.DataFrame
水蒸気干渉が補正されたデータフレーム
110 @staticmethod 111 def remove_bias( 112 df: pd.DataFrame, 113 col_ch4_ppm: str = "ch4_ppm", 114 col_c2h6_ppb: str = "c2h6_ppb", 115 base_ch4_ppm: float = 2.0, 116 base_c2h6_ppb: float = 0, 117 percentile: float = 5, 118 ) -> pd.DataFrame: 119 """ 120 データフレームからバイアスを除去します。 121 122 Parameters 123 ---------- 124 df : pd.DataFrame 125 バイアスを除去する対象のデータフレーム。 126 col_ch4_ppm : str 127 CH4濃度を示すカラム名。デフォルトは"ch4_ppm"。 128 col_c2h6_ppb : str 129 C2H6濃度を示すカラム名。デフォルトは"c2h6_ppb"。 130 base_ch4_ppm : float 131 補正前の値から最小値を引いた後に足すCH4濃度。 132 base_c2h6_ppb : float 133 補正前の値から最小値を引いた後に足すC2H6濃度。 134 percentile : float 135 下位何パーセンタイルの値を最小値として補正を行うか。 136 137 Returns 138 ---------- 139 pd.DataFrame 140 バイアスが除去されたデータフレーム。 141 """ 142 df_copied: pd.DataFrame = df.copy() 143 c2h6_min = np.percentile(df_copied[col_c2h6_ppb], percentile) 144 df_copied[col_c2h6_ppb] = df_copied[col_c2h6_ppb] - c2h6_min + base_c2h6_ppb 145 ch4_min = np.percentile(df_copied[col_ch4_ppm], percentile) 146 df_copied[col_ch4_ppm] = df_copied[col_ch4_ppm] - ch4_min + base_ch4_ppm 147 return df_copied
データフレームからバイアスを除去します。
Parameters
df : pd.DataFrame
バイアスを除去する対象のデータフレーム。
col_ch4_ppm : str
CH4濃度を示すカラム名。デフォルトは"ch4_ppm"。
col_c2h6_ppb : str
C2H6濃度を示すカラム名。デフォルトは"c2h6_ppb"。
base_ch4_ppm : float
補正前の値から最小値を引いた後に足すCH4濃度。
base_c2h6_ppb : float
補正前の値から最小値を引いた後に足すC2H6濃度。
percentile : float
下位何パーセンタイルの値を最小値として補正を行うか。
Returns
pd.DataFrame
バイアスが除去されたデータフレーム。
7@dataclass 8class H2OCorrectionConfig: 9 """水蒸気補正の設定を保持するデータクラス 10 11 Parameters 12 ---------- 13 coef_a : float | None 14 補正曲線の切片 15 coef_b : float | None 16 補正曲線の1次係数 17 coef_c : float | None 18 補正曲線の2次係数 19 h2o_ppm_threshold : float | None 20 水蒸気濃度の下限値(この値未満のデータは除外) 21 """ 22 23 coef_a: float | None = None 24 coef_b: float | None = None 25 coef_c: float | None = None 26 h2o_ppm_threshold: float | None = 2000
水蒸気補正の設定を保持するデータクラス
Parameters
coef_a : float | None 補正曲線の切片 coef_b : float | None 補正曲線の1次係数 coef_c : float | None 補正曲線の2次係数 h2o_ppm_threshold : float | None 水蒸気濃度の下限値(この値未満のデータは除外)
28@dataclass 29class BiasRemovalConfig: 30 """バイアス除去の設定を保持するデータクラス 31 32 Parameters 33 ---------- 34 percentile : float 35 バイアス除去に使用するパーセンタイル値 36 base_ch4_ppm : float 37 補正前の値から最小値を引いた後に足すCH4濃度の基準値。 38 base_c2h6_ppb : float 39 補正前の値から最小値を引いた後に足すC2H6濃度の基準値。 40 """ 41 42 percentile: float = 5.0 43 base_ch4_ppm: float = 2.0 44 base_c2h6_ppb: float = 0
バイアス除去の設定を保持するデータクラス
Parameters
percentile : float バイアス除去に使用するパーセンタイル値 base_ch4_ppm : float 補正前の値から最小値を引いた後に足すCH4濃度の基準値。 base_c2h6_ppb : float 補正前の値から最小値を引いた後に足すC2H6濃度の基準値。
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)): 105 raise ValueError("Delta C2H6 must be a int or float") 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: データクラスの属性と値を含む辞書
169@dataclass 170class HotspotParams: 171 """ホットスポット解析のパラメータ設定 172 173 Parameters 174 ---------- 175 CH4_PPM : str 176 CH4濃度を示すカラム名 177 C2H6_PPB : str 178 C2H6濃度を示すカラム名 179 H2O_PPM : str 180 H2O濃度を示すカラム名 181 CH4_PPM_DELTA_THRESHOLD : float 182 CH4の閾値 183 C2H6_PPB_DELTA_THRESHOLD : float 184 C2H6の閾値 185 USE_QUANTILE : bool 186 5パーセンタイルを使用するかどうかのフラグ 187 ROLLING_METHOD : RollingMethod 188 移動計算の方法 189 - "quantile"は下位{QUANTILE_VALUE}%の値を使用する。 190 - "mean"は移動平均を行う。 191 QUANTILE_VALUE : float 192 下位何パーセントの値を使用するか。デフォルトは5。 193 """ 194 195 CH4_PPM: str = "ch4_ppm" 196 C2H6_PPB: str = "c2h6_ppb" 197 H2O_PPM: str = "h2o_ppm" 198 CH4_PPM_DELTA_THRESHOLD: float = 0.05 199 C2H6_PPB_DELTA_THRESHOLD: float = 0.0 200 H2O_PPM_THRESHOLD: float = 2000 201 ROLLING_METHOD: RollingMethod = "quantile" 202 QUANTILE_VALUE: float = 5 203 204 def __post_init__(self) -> None: 205 """パラメータの検証を行います。 206 207 Raises 208 ---------- 209 ValueError: QUANTILE_VALUEが0以上100以下でない場合 210 """ 211 # QUANTILE_VALUEの値域チェック 212 if not 0 <= self.QUANTILE_VALUE <= 100: 213 raise ValueError( 214 f"QUANTILE_VALUE must be between 0 and 100, got {self.QUANTILE_VALUE}" 215 )
ホットスポット解析のパラメータ設定
Parameters
CH4_PPM : str CH4濃度を示すカラム名 C2H6_PPB : str C2H6濃度を示すカラム名 H2O_PPM : str H2O濃度を示すカラム名 CH4_PPM_DELTA_THRESHOLD : float CH4の閾値 C2H6_PPB_DELTA_THRESHOLD : float C2H6の閾値 USE_QUANTILE : bool 5パーセンタイルを使用するかどうかのフラグ ROLLING_METHOD : RollingMethod 移動計算の方法 - "quantile"は下位{QUANTILE_VALUE}%の値を使用する。 - "mean"は移動平均を行う。 QUANTILE_VALUE : float 下位何パーセントの値を使用するか。デフォルトは5。
305class MobileSpatialAnalyzer: 306 """ 307 移動観測で得られた測定データを解析するクラス 308 """ 309 310 EARTH_RADIUS_METERS: float = 6371000 # 地球の半径(メートル) 311 312 def __init__( 313 self, 314 center_lat: float, 315 center_lon: float, 316 inputs: list[MSAInputConfig] | list[tuple[float, float, str | Path]], 317 num_sections: int = 4, 318 ch4_enhance_threshold: float = 0.1, 319 correlation_threshold: float = 0.7, 320 hotspot_area_meter: float = 50, 321 hotspot_params: HotspotParams | None = None, 322 window_minutes: float = 5, 323 column_mapping: dict[str, str] = { 324 "Time Stamp": "timestamp", 325 "CH4 (ppm)": "ch4_ppm", 326 "C2H6 (ppb)": "c2h6_ppb", 327 "H2O (ppm)": "h2o_ppm", 328 "Latitude": "latitude", 329 "Longitude": "longitude", 330 }, 331 na_values: list[str] = ["No Data", "nan"], 332 logger: Logger | None = None, 333 logging_debug: bool = False, 334 ): 335 """ 336 測定データ解析クラスの初期化 337 338 Parameters 339 ---------- 340 center_lat : float 341 中心緯度 342 center_lon : float 343 中心経度 344 inputs : list[MSAInputConfig] | list[tuple[float, float, str | Path]] 345 入力ファイルのリスト 346 num_sections : int 347 分割する区画数。デフォルトは4。 348 ch4_enhance_threshold : float 349 CH4増加の閾値(ppm)。デフォルトは0.1。 350 correlation_threshold : float 351 相関係数の閾値。デフォルトは0.7。 352 hotspot_area_meter : float 353 ホットスポットの検出に使用するエリアの半径(メートル)。デフォルトは50メートル。 354 hotspot_params : HotspotParams | None, optional 355 ホットスポット解析のパラメータ設定 356 window_minutes : float 357 移動窓の大きさ(分)。デフォルトは5分。 358 column_mapping : dict[str, str] 359 元のデータファイルのヘッダーを汎用的な単語に変換するための辞書型データ。 360 - timestamp,ch4_ppm,c2h6_ppm,h2o_ppm,latitude,longitudeをvalueに、それぞれに対応するカラム名をcolに指定してください。 361 - デフォルト: { 362 "Time Stamp": "timestamp", 363 "CH4 (ppm)": "ch4_ppm", 364 "C2H6 (ppb)": "c2h6_ppb", 365 "H2O (ppm)": "h2o_ppm", 366 "Latitude": "latitude", 367 "Longitude": "longitude", 368 } 369 na_values : list[str] 370 NaNと判定する値のパターン。 371 logger : Logger | None 372 使用するロガー。Noneの場合は新しいロガーを作成します。 373 logging_debug : bool 374 ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。 375 376 Returns 377 ---------- 378 None 379 初期化処理が完了したことを示します。 380 """ 381 # ロガー 382 log_level: int = INFO 383 if logging_debug: 384 log_level = DEBUG 385 self.logger: Logger = MobileSpatialAnalyzer.setup_logger(logger, log_level) 386 # プライベートなプロパティ 387 self._center_lat: float = center_lat 388 self._center_lon: float = center_lon 389 self._ch4_enhance_threshold: float = ch4_enhance_threshold 390 self._correlation_threshold: float = correlation_threshold 391 self._hotspot_area_meter: float = hotspot_area_meter 392 self._column_mapping: dict[str, str] = column_mapping 393 self._na_values: list[str] = na_values 394 self._hotspot_params = hotspot_params or HotspotParams() 395 self._num_sections: int = num_sections 396 # セクションの範囲 397 section_size: float = 360 / num_sections 398 self._section_size: float = section_size 399 self._sections = MobileSpatialAnalyzer._initialize_sections( 400 num_sections, section_size 401 ) 402 # window_sizeをデータポイント数に変換(分→秒→データポイント数) 403 self._window_size: int = MobileSpatialAnalyzer._calculate_window_size( 404 window_minutes 405 ) 406 # 入力設定の標準化 407 normalized_input_configs: list[MSAInputConfig] = ( 408 MobileSpatialAnalyzer._normalize_inputs(inputs) 409 ) 410 # 複数ファイルのデータを読み込み 411 self._data: dict[str, pd.DataFrame] = self._load_all_data( 412 normalized_input_configs 413 ) 414 415 @property 416 def hotspot_params(self) -> HotspotParams: 417 """ホットスポット解析のパラメータ設定を取得""" 418 return self._hotspot_params 419 420 @hotspot_params.setter 421 def hotspot_params(self, params: HotspotParams) -> None: 422 """ホットスポット解析のパラメータ設定を更新""" 423 self._hotspot_params = params 424 425 def analyze_delta_ch4_stats(self, hotspots: list[HotspotData]) -> None: 426 """ 427 各タイプのホットスポットについてΔCH4の統計情報を計算し、結果を表示します。 428 429 Parameters 430 ---------- 431 hotspots : list[HotspotData] 432 分析対象のホットスポットリスト 433 434 Returns 435 ---------- 436 None 437 統計情報の表示が完了したことを示します。 438 """ 439 # タイプごとにホットスポットを分類 440 hotspots_by_type: dict[HotspotType, list[HotspotData]] = { 441 "bio": [h for h in hotspots if h.type == "bio"], 442 "gas": [h for h in hotspots if h.type == "gas"], 443 "comb": [h for h in hotspots if h.type == "comb"], 444 } 445 446 # 統計情報を計算し、表示 447 for spot_type, spots in hotspots_by_type.items(): 448 if spots: 449 delta_ch4_values = [spot.delta_ch4 for spot in spots] 450 max_value = max(delta_ch4_values) 451 mean_value = sum(delta_ch4_values) / len(delta_ch4_values) 452 median_value = sorted(delta_ch4_values)[len(delta_ch4_values) // 2] 453 print(f"{spot_type}タイプのホットスポットの統計情報:") 454 print(f" 最大値: {max_value}") 455 print(f" 平均値: {mean_value}") 456 print(f" 中央値: {median_value}") 457 else: 458 print(f"{spot_type}タイプのホットスポットは存在しません。") 459 460 def analyze_hotspots( 461 self, 462 duplicate_check_mode: Literal["none", "time_window", "time_all"] = "none", 463 min_time_threshold_seconds: float = 300, 464 max_time_threshold_hours: float = 12, 465 ) -> list[HotspotData]: 466 """ 467 ホットスポットを検出して分析します。 468 469 Parameters 470 ---------- 471 duplicate_check_mode : Literal["none", "time_window", "time_all"] 472 重複チェックのモード。 473 - "none": 重複チェックを行わない。 474 - "time_window": 指定された時間窓内の重複のみを除外。 475 - "time_all": すべての時間範囲で重複チェックを行う。 476 min_time_threshold_seconds : float 477 重複とみなす最小時間の閾値(秒)。デフォルトは300秒。 478 max_time_threshold_hours : float 479 重複チェックを一時的に無視する最大時間の閾値(時間)。デフォルトは12時間。 480 481 Returns 482 ---------- 483 list[HotspotData] 484 検出されたホットスポットのリスト。 485 """ 486 all_hotspots: list[HotspotData] = [] 487 params: HotspotParams = self._hotspot_params 488 489 # 各データソースに対して解析を実行 490 for _, df in self._data.items(): 491 # パラメータの計算 492 df = MobileSpatialAnalyzer._calculate_hotspots_parameters( 493 df=df, 494 window_size=self._window_size, 495 col_ch4_ppm=params.CH4_PPM, 496 col_c2h6_ppb=params.C2H6_PPB, 497 col_h2o_ppm=params.H2O_PPM, 498 ch4_ppm_delta_threshold=params.CH4_PPM_DELTA_THRESHOLD, 499 c2h6_ppb_delta_threshold=params.C2H6_PPB_DELTA_THRESHOLD, 500 h2o_ppm_threshold=params.H2O_PPM_THRESHOLD, 501 rolling_method=params.ROLLING_METHOD, 502 quantile_value=params.QUANTILE_VALUE, 503 ) 504 505 # ホットスポットの検出 506 hotspots: list[HotspotData] = self._detect_hotspots( 507 df=df, 508 ch4_enhance_threshold=self._ch4_enhance_threshold, 509 ) 510 all_hotspots.extend(hotspots) 511 512 # 重複チェックモードに応じて処理 513 if duplicate_check_mode != "none": 514 unique_hotspots = MobileSpatialAnalyzer.remove_hotspots_duplicates( 515 all_hotspots, 516 check_time_all=(duplicate_check_mode == "time_all"), 517 min_time_threshold_seconds=min_time_threshold_seconds, 518 max_time_threshold_hours=max_time_threshold_hours, 519 hotspot_area_meter=self._hotspot_area_meter, 520 ) 521 self.logger.info( 522 f"重複除外: {len(all_hotspots)} → {len(unique_hotspots)} ホットスポット" 523 ) 524 return unique_hotspots 525 526 return all_hotspots 527 528 def calculate_measurement_stats( 529 self, 530 print_individual_stats: bool = True, 531 print_total_stats: bool = True, 532 col_latitude: str = "latitude", 533 col_longitude: str = "longitude", 534 ) -> tuple[float, timedelta]: 535 """ 536 各ファイルの測定時間と走行距離を計算し、合計を返します。 537 538 Parameters 539 ---------- 540 print_individual_stats : bool 541 個別ファイルの統計を表示するかどうか。デフォルトはTrue。 542 print_total_stats : bool 543 合計統計を表示するかどうか。デフォルトはTrue。 544 col_latitude : str 545 緯度情報が格納されているカラム名。デフォルトは"latitude"。 546 col_longitude : str 547 経度情報が格納されているカラム名。デフォルトは"longitude"。 548 549 Returns 550 ---------- 551 tuple[float, timedelta] 552 総距離(km)と総時間のタプル 553 """ 554 total_distance: float = 0.0 555 total_time: timedelta = timedelta() 556 individual_stats: list[dict] = [] # 個別の統計情報を保存するリスト 557 558 # プログレスバーを表示しながら計算 559 for source_name, df in tqdm( 560 self._data.items(), desc="Calculating", unit="file" 561 ): 562 # 時間の計算 563 time_spent = df.index[-1] - df.index[0] 564 565 # 距離の計算 566 distance_km = 0.0 567 for i in range(len(df) - 1): 568 lat1, lon1 = df.iloc[i][[col_latitude, col_longitude]] 569 lat2, lon2 = df.iloc[i + 1][[col_latitude, col_longitude]] 570 distance_km += ( 571 MobileSpatialAnalyzer._calculate_distance( 572 lat1=lat1, lon1=lon1, lat2=lat2, lon2=lon2 573 ) 574 / 1000 575 ) 576 577 # 合計に加算 578 total_distance += distance_km 579 total_time += time_spent 580 581 # 統計情報を保存 582 if print_individual_stats: 583 average_speed = distance_km / (time_spent.total_seconds() / 3600) 584 individual_stats.append( 585 { 586 "source": source_name, 587 "distance": distance_km, 588 "time": time_spent, 589 "speed": average_speed, 590 } 591 ) 592 593 # 計算完了後に統計情報を表示 594 if print_individual_stats: 595 self.logger.info("=== Individual Stats ===") 596 for stat in individual_stats: 597 print(f"File : {stat['source']}") 598 print(f" Distance : {stat['distance']:.2f} km") 599 print(f" Time : {stat['time']}") 600 print(f" Avg. Speed : {stat['speed']:.1f} km/h\n") 601 602 # 合計を表示 603 if print_total_stats: 604 average_speed_total: float = total_distance / ( 605 total_time.total_seconds() / 3600 606 ) 607 self.logger.info("=== Total Stats ===") 608 print(f" Distance : {total_distance:.2f} km") 609 print(f" Time : {total_time}") 610 print(f" Avg. Speed : {average_speed_total:.1f} km/h\n") 611 612 return total_distance, total_time 613 614 def create_hotspots_map( 615 self, 616 hotspots: list[HotspotData], 617 output_dir: str | Path | None = None, 618 output_filename: str = "hotspots_map.html", 619 center_marker_color: str = "green", 620 center_marker_label: str = "Center", 621 plot_center_marker: bool = True, 622 radius_meters: float = 3000, 623 save_fig: bool = True, 624 ) -> None: 625 """ 626 ホットスポットの分布を地図上にプロットして保存 627 628 Parameters 629 ---------- 630 hotspots : list[HotspotData] 631 プロットするホットスポットのリスト 632 output_dir : str | Path 633 保存先のディレクトリパス 634 output_filename : str 635 保存するファイル名。デフォルトは"hotspots_map"。 636 center_marker_color : str 637 中心を示すマーカーのラベルカラー。デフォルトは"green"。 638 center_marker_label : str 639 中心を示すマーカーのラベルテキスト。デフォルトは"Center"。 640 plot_center_marker : bool 641 中心を示すマーカーの有無。デフォルトはTrue。 642 radius_meters : float 643 区画分けを示す線の長さ。デフォルトは3000。 644 save_fig : bool 645 図の保存を許可するフラグ。デフォルトはTrue。 646 """ 647 # 地図の作成 648 m = folium.Map( 649 location=[self._center_lat, self._center_lon], 650 zoom_start=15, 651 tiles="OpenStreetMap", 652 ) 653 654 # ホットスポットの種類ごとに異なる色でプロット 655 for spot in hotspots: 656 # NaN値チェックを追加 657 if math.isnan(spot.avg_lat) or math.isnan(spot.avg_lon): 658 continue 659 660 # default type 661 color = "black" 662 # タイプに応じて色を設定 663 if spot.type == "comb": 664 color = "green" 665 elif spot.type == "gas": 666 color = "red" 667 elif spot.type == "bio": 668 color = "blue" 669 670 # CSSのgrid layoutを使用してHTMLタグを含むテキストをフォーマット 671 popup_html = f""" 672 <div style='font-family: Arial; font-size: 12px; display: grid; grid-template-columns: auto auto auto; gap: 5px;'> 673 <b>Date</b> <span>:</span> <span>{spot.source}</span> 674 <b>Lat</b> <span>:</span> <span>{spot.avg_lat:.3f}</span> 675 <b>Lon</b> <span>:</span> <span>{spot.avg_lon:.3f}</span> 676 <b>ΔCH<sub>4</sub></b> <span>:</span> <span>{spot.delta_ch4:.3f}</span> 677 <b>ΔC<sub>2</sub>H<sub>6</sub></b> <span>:</span> <span>{spot.delta_c2h6:.3f}</span> 678 <b>Ratio</b> <span>:</span> <span>{spot.ratio:.3f}</span> 679 <b>Type</b> <span>:</span> <span>{spot.type}</span> 680 <b>Section</b> <span>:</span> <span>{spot.section}</span> 681 </div> 682 """ 683 684 # ポップアップのサイズを指定 685 popup = folium.Popup( 686 folium.Html(popup_html, script=True), 687 max_width=200, # 最大幅(ピクセル) 688 ) 689 690 folium.CircleMarker( 691 location=[spot.avg_lat, spot.avg_lon], 692 radius=8, 693 color=color, 694 fill=True, 695 popup=popup, 696 ).add_to(m) 697 698 # 中心点のマーカー 699 if plot_center_marker: 700 folium.Marker( 701 [self._center_lat, self._center_lon], 702 popup=center_marker_label, 703 icon=folium.Icon(color=center_marker_color, icon="info-sign"), 704 ).add_to(m) 705 706 # 区画の境界線を描画 707 for section in range(self._num_sections): 708 start_angle = math.radians(-180 + section * self._section_size) 709 710 R = self.EARTH_RADIUS_METERS 711 712 # 境界線の座標を計算 713 lat1 = self._center_lat 714 lon1 = self._center_lon 715 lat2 = math.degrees( 716 math.asin( 717 math.sin(math.radians(lat1)) * math.cos(radius_meters / R) 718 + math.cos(math.radians(lat1)) 719 * math.sin(radius_meters / R) 720 * math.cos(start_angle) 721 ) 722 ) 723 lon2 = self._center_lon + math.degrees( 724 math.atan2( 725 math.sin(start_angle) 726 * math.sin(radius_meters / R) 727 * math.cos(math.radians(lat1)), 728 math.cos(radius_meters / R) 729 - math.sin(math.radians(lat1)) * math.sin(math.radians(lat2)), 730 ) 731 ) 732 733 # 境界線を描画 734 folium.PolyLine( 735 locations=[[lat1, lon1], [lat2, lon2]], 736 color="black", 737 weight=1, 738 opacity=0.5, 739 ).add_to(m) 740 741 # 地図を保存 742 if save_fig and output_dir is None: 743 raise ValueError( 744 "save_fig=Trueの場合、output_dirを指定する必要があります。有効なディレクトリパスを指定してください。" 745 ) 746 output_path: str = os.path.join(output_dir, output_filename) 747 m.save(str(output_path)) 748 self.logger.info(f"地図を保存しました: {output_path}") 749 750 def export_hotspots_to_csv( 751 self, 752 hotspots: list[HotspotData], 753 output_dir: str | Path | None = None, 754 output_filename: str = "hotspots.csv", 755 ) -> None: 756 """ 757 ホットスポットの情報をCSVファイルに出力します。 758 759 Parameters 760 ---------- 761 hotspots : list[HotspotData] 762 出力するホットスポットのリスト 763 output_dir : str | Path | None 764 出力先ディレクトリ 765 output_filename : str 766 出力ファイル名 767 """ 768 # 日時の昇順でソート 769 sorted_hotspots = sorted(hotspots, key=lambda x: x.source) 770 771 # 出力用のデータを作成 772 records = [] 773 for spot in sorted_hotspots: 774 record = { 775 "source": spot.source, 776 "type": spot.type, 777 "delta_ch4": spot.delta_ch4, 778 "delta_c2h6": spot.delta_c2h6, 779 "ratio": spot.ratio, 780 "correlation": spot.correlation, 781 "angle": spot.angle, 782 "section": spot.section, 783 "latitude": spot.avg_lat, 784 "longitude": spot.avg_lon, 785 } 786 records.append(record) 787 788 # DataFrameに変換してCSVに出力 789 if output_dir is None: 790 raise ValueError( 791 "output_dirが指定されていません。有効なディレクトリパスを指定してください。" 792 ) 793 os.makedirs(output_dir, exist_ok=True) 794 output_path: str = os.path.join(output_dir, output_filename) 795 df: pd.DataFrame = pd.DataFrame(records) 796 df.to_csv(output_path, index=False) 797 self.logger.info( 798 f"ホットスポット情報をCSVファイルに出力しました: {output_path}" 799 ) 800 801 @staticmethod 802 def extract_source_name_from_path(path: str | Path) -> str: 803 """ 804 ファイルパスからソース名(拡張子なしのファイル名)を抽出します。 805 806 Parameters 807 ---------- 808 path : str | Path 809 ソース名を抽出するファイルパス 810 例: "/path/to/Pico100121_241017_092120+.txt" 811 812 Returns 813 ---------- 814 str 815 抽出されたソース名 816 例: "Pico100121_241017_092120+" 817 818 Examples: 819 ---------- 820 >>> path = "/path/to/data/Pico100121_241017_092120+.txt" 821 >>> MobileSpatialAnalyzer.extract_source_from_path(path) 822 'Pico100121_241017_092120+' 823 """ 824 # Pathオブジェクトに変換 825 path_obj: Path = Path(path) 826 # stem属性で拡張子なしのファイル名を取得 827 source_name: str = path_obj.stem 828 return source_name 829 830 def get_preprocessed_data( 831 self, 832 ) -> pd.DataFrame: 833 """ 834 データ前処理を行い、CH4とC2H6の相関解析に必要な形式に整えます。 835 コンストラクタで読み込んだすべてのデータを前処理し、結合したDataFrameを返します。 836 837 Returns 838 ---------- 839 pd.DataFrame 840 前処理済みの結合されたDataFrame 841 """ 842 processed_dfs: list[pd.DataFrame] = [] 843 params: HotspotParams = self._hotspot_params 844 845 # 各データソースに対して解析を実行 846 for source_name, df in self._data.items(): 847 # パラメータの計算 848 processed_df = MobileSpatialAnalyzer._calculate_hotspots_parameters( 849 df=df, 850 window_size=self._window_size, 851 col_ch4_ppm=params.CH4_PPM, 852 col_c2h6_ppb=params.C2H6_PPB, 853 col_h2o_ppm=params.H2O_PPM, 854 ch4_ppm_delta_threshold=params.CH4_PPM_DELTA_THRESHOLD, 855 c2h6_ppb_delta_threshold=params.C2H6_PPB_DELTA_THRESHOLD, 856 h2o_ppm_threshold=params.H2O_PPM_THRESHOLD, 857 rolling_method=params.ROLLING_METHOD, 858 quantile_value=params.QUANTILE_VALUE, 859 ) 860 # ソース名を列として追加 861 processed_df["source"] = source_name 862 processed_dfs.append(processed_df) 863 864 # すべてのDataFrameを結合 865 if not processed_dfs: 866 raise ValueError("処理対象のデータが存在しません。") 867 868 combined_df: pd.DataFrame = pd.concat(processed_dfs, axis=0) 869 return combined_df 870 871 def get_section_size(self) -> float: 872 """ 873 セクションのサイズを取得するメソッド。 874 このメソッドは、解析対象のデータを区画に分割する際の 875 各区画の角度範囲を示すサイズを返します。 876 877 Returns 878 ---------- 879 float 880 1セクションのサイズ(度単位) 881 """ 882 return self._section_size 883 884 def get_source_names(self, print_all: bool = False) -> list[str]: 885 """ 886 データソースの名前を取得します。 887 888 Parameters 889 ---------- 890 print_all : bool, optional 891 すべてのデータソース名を表示するかどうかを指定します。デフォルトはFalseです。 892 893 Returns 894 ---------- 895 list[str] 896 データソース名のリスト 897 898 Raises 899 ---------- 900 ValueError 901 データが読み込まれていない場合に発生します。 902 """ 903 dfs_dict: dict[str, pd.DataFrame] = self._data 904 # データソースの選択 905 if not dfs_dict: 906 raise ValueError("データが読み込まれていません。") 907 source_name_list: list[str] = list(dfs_dict.keys()) 908 if print_all: 909 print(source_name_list) 910 return source_name_list 911 912 def plot_ch4_delta_histogram( 913 self, 914 hotspots: list[HotspotData], 915 output_dir: str | Path | None, 916 output_filename: str = "ch4_delta_histogram.png", 917 dpi: int = 200, 918 figsize: tuple[int, int] = (8, 6), 919 fontsize: float = 20, 920 hotspot_colors: dict[HotspotType, str] = { 921 "bio": "blue", 922 "gas": "red", 923 "comb": "green", 924 }, 925 xlabel: str = "Δ$\\mathregular{CH_{4}}$ (ppm)", 926 ylabel: str = "Frequency", 927 xlim: tuple[float, float] | None = None, 928 ylim: tuple[float, float] | None = None, 929 save_fig: bool = True, 930 show_fig: bool = True, 931 yscale_log: bool = True, 932 print_bins_analysis: bool = False, 933 ) -> None: 934 """ 935 CH4の増加量(ΔCH4)の積み上げヒストグラムをプロットします。 936 937 Parameters 938 ---------- 939 hotspots : list[HotspotData] 940 プロットするホットスポットのリスト 941 output_dir : str | Path | None 942 保存先のディレクトリパス 943 output_filename : str 944 保存するファイル名。デフォルトは"ch4_delta_histogram.png"。 945 dpi : int 946 解像度。デフォルトは200。 947 figsize : tuple[int, int] 948 図のサイズ。デフォルトは(8, 6)。 949 fontsize : float 950 フォントサイズ。デフォルトは20。 951 hotspot_colors : dict[HotspotType, str] 952 ホットスポットの色を定義する辞書。 953 xlabel : str 954 x軸のラベル。 955 ylabel : str 956 y軸のラベル。 957 xlim : tuple[float, float] | None 958 x軸の範囲。Noneの場合は自動設定。 959 ylim : tuple[float, float] | None 960 y軸の範囲。Noneの場合は自動設定。 961 save_fig : bool 962 図の保存を許可するフラグ。デフォルトはTrue。 963 show_fig : bool 964 図の表示を許可するフラグ。デフォルトはTrue。 965 yscale_log : bool 966 y軸をlogにするかどうか。デフォルトはTrue。 967 print_bins_analysis : bool 968 ビンごとの内訳を表示するオプション。 969 """ 970 plt.rcParams["font.size"] = fontsize 971 fig = plt.figure(figsize=figsize, dpi=dpi) 972 973 # ホットスポットからデータを抽出 974 all_ch4_deltas = [] 975 all_types = [] 976 for spot in hotspots: 977 all_ch4_deltas.append(spot.delta_ch4) 978 all_types.append(spot.type) 979 980 # データをNumPy配列に変換 981 all_ch4_deltas = np.array(all_ch4_deltas) 982 all_types = np.array(all_types) 983 984 # 0.1刻みのビンを作成 985 if xlim is not None: 986 bins = np.arange(xlim[0], xlim[1] + 0.1, 0.1) 987 else: 988 max_val = np.ceil(np.max(all_ch4_deltas) * 10) / 10 989 bins = np.arange(0, max_val + 0.1, 0.1) 990 991 # タイプごとのヒストグラムデータを計算 992 hist_data = {} 993 # HotspotTypeのリテラル値を使用してイテレーション 994 for type_name in get_args(HotspotType): # typing.get_argsをインポート 995 mask = all_types == type_name 996 if np.any(mask): 997 counts, _ = np.histogram(all_ch4_deltas[mask], bins=bins) 998 hist_data[type_name] = counts 999 1000 # ビンごとの内訳を表示 1001 if print_bins_analysis: 1002 self.logger.info("各ビンの内訳:") 1003 print(f"{'Bin Range':15} {'bio':>8} {'gas':>8} {'comb':>8} {'Total':>8}") 1004 print("-" * 50) 1005 1006 for i in range(len(bins) - 1): 1007 bin_start = bins[i] 1008 bin_end = bins[i + 1] 1009 bio_count = hist_data.get("bio", np.zeros(len(bins) - 1))[i] 1010 gas_count = hist_data.get("gas", np.zeros(len(bins) - 1))[i] 1011 comb_count = hist_data.get("comb", np.zeros(len(bins) - 1))[i] 1012 total = bio_count + gas_count + comb_count 1013 1014 if total > 0: # 合計が0のビンは表示しない 1015 print( 1016 f"{bin_start:4.1f}-{bin_end:<8.1f}" 1017 f"{int(bio_count):8d}" 1018 f"{int(gas_count):8d}" 1019 f"{int(comb_count):8d}" 1020 f"{int(total):8d}" 1021 ) 1022 1023 # 積み上げヒストグラムを作成 1024 bottom = np.zeros_like(hist_data.get("bio", np.zeros(len(bins) - 1))) 1025 1026 # HotspotTypeのリテラル値を使用してイテレーション 1027 for type_name in get_args(HotspotType): 1028 if type_name in hist_data: 1029 plt.bar( 1030 bins[:-1], 1031 hist_data[type_name], 1032 width=np.diff(bins)[0], 1033 bottom=bottom, 1034 color=hotspot_colors[type_name], 1035 label=type_name, 1036 alpha=0.6, 1037 align="edge", 1038 ) 1039 bottom += hist_data[type_name] 1040 1041 if yscale_log: 1042 plt.yscale("log") 1043 plt.xlabel(xlabel) 1044 plt.ylabel(ylabel) 1045 plt.legend() 1046 plt.grid(True, which="both", ls="-", alpha=0.2) 1047 1048 # 軸の範囲を設定 1049 if xlim is not None: 1050 plt.xlim(xlim) 1051 if ylim is not None: 1052 plt.ylim(ylim) 1053 1054 # グラフの保存または表示 1055 if save_fig: 1056 if output_dir is None: 1057 raise ValueError( 1058 "save_fig=Trueの場合、output_dirを指定する必要があります。有効なディレクトリパスを指定してください。" 1059 ) 1060 os.makedirs(output_dir, exist_ok=True) 1061 output_path: str = os.path.join(output_dir, output_filename) 1062 plt.savefig(output_path, bbox_inches="tight") 1063 self.logger.info(f"ヒストグラムを保存しました: {output_path}") 1064 if show_fig: 1065 plt.show() 1066 else: 1067 plt.close(fig=fig) 1068 1069 def plot_mapbox( 1070 self, 1071 df: pd.DataFrame, 1072 col_conc: str, 1073 mapbox_access_token: str, 1074 sort_conc_column: bool = True, 1075 output_dir: str | Path | None = None, 1076 output_filename: str = "mapbox_plot.html", 1077 col_lat: str = "latitude", 1078 col_lon: str = "longitude", 1079 colorscale: str = "Jet", 1080 center_lat: float | None = None, 1081 center_lon: float | None = None, 1082 zoom: float = 12, 1083 width: int = 700, 1084 height: int = 700, 1085 tick_font_family: str = "Arial", 1086 title_font_family: str = "Arial", 1087 tick_font_size: int = 12, 1088 title_font_size: int = 14, 1089 marker_size: int = 4, 1090 colorbar_title: str | None = None, 1091 value_range: tuple[float, float] | None = None, 1092 save_fig: bool = True, 1093 show_fig: bool = True, 1094 ) -> None: 1095 """ 1096 Plotlyを使用してMapbox上にデータをプロットします。 1097 1098 Parameters 1099 ---------- 1100 df : pd.DataFrame 1101 プロットするデータを含むDataFrame 1102 col_conc : str 1103 カラーマッピングに使用する列名 1104 mapbox_access_token : str 1105 Mapboxのアクセストークン 1106 sort_conc_column : bool 1107 value_columnをソートするか否か。デフォルトはTrue。 1108 output_dir : str | Path | None 1109 出力ディレクトリのパス 1110 output_filename : str 1111 出力ファイル名。デフォルトは"mapbox_plot.html" 1112 col_lat : str 1113 緯度の列名。デフォルトは"latitude" 1114 col_lon : str 1115 経度の列名。デフォルトは"longitude" 1116 colorscale : str 1117 使用するカラースケール。デフォルトは"Jet" 1118 center_lat : float | None 1119 中心緯度。デフォルトはNoneで、self._center_latを使用 1120 center_lon : float | None 1121 中心経度。デフォルトはNoneで、self._center_lonを使用 1122 zoom : float 1123 マップの初期ズームレベル。デフォルトは12 1124 width : int 1125 プロットの幅(ピクセル)。デフォルトは700 1126 height : int 1127 プロットの高さ(ピクセル)。デフォルトは700 1128 tick_font_family : str 1129 カラーバーの目盛りフォントファミリー。デフォルトは"Arial" 1130 title_font_family : str 1131 カラーバーのラベルフォントファミリー。デフォルトは"Arial" 1132 tick_font_size : int 1133 カラーバーの目盛りフォントサイズ。デフォルトは12 1134 title_font_size : int 1135 カラーバーのラベルフォントサイズ。デフォルトは14 1136 marker_size : int 1137 マーカーのサイズ。デフォルトは4 1138 colorbar_title : str | None 1139 カラーバーのラベル 1140 value_range : tuple[float, float] | None 1141 カラーマッピングの範囲。デフォルトはNoneで、データの最小値と最大値を使用 1142 save_fig : bool 1143 図を保存するかどうか。デフォルトはTrue 1144 show_fig : bool 1145 図を表示するかどうか。デフォルトはTrue 1146 """ 1147 df_mapping: pd.DataFrame = df.copy().dropna(subset=[col_conc]) 1148 if sort_conc_column: 1149 df_mapping = df_mapping.sort_values(col_conc) 1150 # 中心座標の設定 1151 center_lat = center_lat if center_lat is not None else self._center_lat 1152 center_lon = center_lon if center_lon is not None else self._center_lon 1153 1154 # カラーマッピングの範囲を設定 1155 cmin, cmax = 0, 0 1156 if value_range is None: 1157 cmin = df_mapping[col_conc].min() 1158 cmax = df_mapping[col_conc].max() 1159 else: 1160 cmin, cmax = value_range 1161 1162 # カラーバーのタイトルを設定 1163 title_text = colorbar_title if colorbar_title is not None else col_conc 1164 1165 # Scattermapboxのデータを作成 1166 scatter_data = go.Scattermapbox( 1167 lat=df_mapping[col_lat], 1168 lon=df_mapping[col_lon], 1169 text=df_mapping[col_conc].astype(str), 1170 hoverinfo="text", 1171 mode="markers", 1172 marker=dict( 1173 color=df_mapping[col_conc], 1174 size=marker_size, 1175 reversescale=False, 1176 autocolorscale=False, 1177 colorscale=colorscale, 1178 cmin=cmin, 1179 cmax=cmax, 1180 colorbar=dict( 1181 tickformat="3.2f", 1182 outlinecolor="black", 1183 outlinewidth=1.5, 1184 ticks="outside", 1185 ticklen=7, 1186 tickwidth=1.5, 1187 tickcolor="black", 1188 tickfont=dict( 1189 family=tick_font_family, color="black", size=tick_font_size 1190 ), 1191 title=dict( 1192 text=title_text, side="top" 1193 ), # カラーバーのタイトルを設定 1194 titlefont=dict( 1195 family=title_font_family, 1196 color="black", 1197 size=title_font_size, 1198 ), 1199 ), 1200 ), 1201 ) 1202 1203 # レイアウトの設定 1204 layout = go.Layout( 1205 width=width, 1206 height=height, 1207 showlegend=False, 1208 mapbox=dict( 1209 accesstoken=mapbox_access_token, 1210 center=dict(lat=center_lat, lon=center_lon), 1211 zoom=zoom, 1212 ), 1213 ) 1214 1215 # 図の作成 1216 fig = go.Figure(data=[scatter_data], layout=layout) 1217 1218 # 図の保存 1219 if save_fig: 1220 # 保存時の出力ディレクトリチェック 1221 if output_dir is None: 1222 raise ValueError( 1223 "save_fig=Trueの場合、output_dirを指定する必要があります。" 1224 ) 1225 os.makedirs(output_dir, exist_ok=True) 1226 output_path = os.path.join(output_dir, output_filename) 1227 pyo.plot(fig, filename=output_path, auto_open=False) 1228 self.logger.info(f"Mapboxプロットを保存しました: {output_path}") 1229 # 図の表示 1230 if show_fig: 1231 pyo.iplot(fig) 1232 1233 def plot_scatter_c2c1( 1234 self, 1235 hotspots: list[HotspotData], 1236 output_dir: str | Path | None = None, 1237 output_filename: str = "scatter_c2c1.png", 1238 dpi: int = 200, 1239 figsize: tuple[int, int] = (4, 4), 1240 hotspot_colors: dict[HotspotType, str] = { 1241 "bio": "blue", 1242 "gas": "red", 1243 "comb": "green", 1244 }, 1245 hotspot_labels: dict[HotspotType, str] = { 1246 "bio": "bio", 1247 "gas": "gas", 1248 "comb": "comb", 1249 }, 1250 fontsize: float = 12, 1251 xlim: tuple[float, float] = (0, 2.0), 1252 ylim: tuple[float, float] = (0, 50), 1253 xlabel: str = "Δ$\\mathregular{CH_{4}}$ (ppm)", 1254 ylabel: str = "Δ$\\mathregular{C_{2}H_{6}}$ (ppb)", 1255 add_legend: bool = True, 1256 save_fig: bool = True, 1257 show_fig: bool = True, 1258 ratio_labels: dict[float, tuple[float, float, str]] | None = None, 1259 ) -> None: 1260 """ 1261 検出されたホットスポットのΔC2H6とΔCH4の散布図をプロットします。 1262 1263 Parameters 1264 ---------- 1265 hotspots : list[HotspotData] 1266 プロットするホットスポットのリスト 1267 output_dir : str | Path | None 1268 保存先のディレクトリパス 1269 output_filename : str 1270 保存するファイル名。デフォルトは"scatter_c2c1.png"。 1271 dpi : int 1272 解像度。デフォルトは200。 1273 figsize : tuple[int, int] 1274 図のサイズ。デフォルトは(4, 4)。 1275 fontsize : float 1276 フォントサイズ。デフォルトは12。 1277 hotspot_colors : dict[HotspotType, str] 1278 ホットスポットの色を定義する辞書。 1279 hotspot_labels : dict[HotspotType, str] 1280 ホットスポットのラベルを定義する辞書。 1281 save_fig : bool 1282 図の保存を許可するフラグ。デフォルトはTrue。 1283 show_fig : bool 1284 図の表示を許可するフラグ。デフォルトはTrue。 1285 ratio_labels : dict[float, tuple[float, float, str]] | None 1286 比率線とラベルの設定。 1287 キーは比率値、値は (x位置, y位置, ラベルテキスト) のタプル。 1288 Noneの場合はデフォルト設定を使用。デフォルト値: 1289 { 1290 0.001: (1.25, 2, "0.001"), 1291 0.005: (1.25, 8, "0.005"), 1292 0.010: (1.25, 15, "0.01"), 1293 0.020: (1.25, 30, "0.02"), 1294 0.030: (1.0, 40, "0.03"), 1295 0.076: (0.20, 42, "0.076 (Osaka)") 1296 } 1297 xlim : tuple[float, float] 1298 x軸の範囲を指定します。デフォルトは(0, 2.0)です。 1299 ylim : tuple[float, float] 1300 y軸の範囲を指定します。デフォルトは(0, 50)です。 1301 xlabel : str 1302 x軸のラベルを指定します。デフォルトは"Δ$\\mathregular{CH_{4}}$ (ppm)"です。 1303 ylabel : str 1304 y軸のラベルを指定します。デフォルトは"Δ$\\mathregular{C_{2}H_{6}}$ (ppb)"です。 1305 add_legend : bool 1306 凡例を追加するかどうか。 1307 """ 1308 plt.rcParams["font.size"] = fontsize 1309 fig = plt.figure(figsize=figsize, dpi=dpi) 1310 1311 # タイプごとのデータを収集 1312 type_data: dict[HotspotType, list[tuple[float, float]]] = { 1313 "bio": [], 1314 "gas": [], 1315 "comb": [], 1316 } 1317 for spot in hotspots: 1318 type_data[spot.type].append((spot.delta_ch4, spot.delta_c2h6)) 1319 1320 # タイプごとにプロット(データが存在する場合のみ) 1321 for spot_type, data in type_data.items(): 1322 if data: # データが存在する場合のみプロット 1323 ch4_values, c2h6_values = zip(*data) 1324 plt.plot( 1325 ch4_values, 1326 c2h6_values, 1327 "o", 1328 c=hotspot_colors[spot_type], 1329 alpha=0.5, 1330 ms=2, 1331 label=hotspot_labels[spot_type], 1332 ) 1333 1334 # デフォルトの比率とラベル設定 1335 default_ratio_labels = { 1336 0.001: (1.25, 2, "0.001"), 1337 0.005: (1.25, 8, "0.005"), 1338 0.010: (1.25, 15, "0.01"), 1339 0.020: (1.25, 30, "0.02"), 1340 0.030: (1.0, 40, "0.03"), 1341 0.076: (0.20, 42, "0.076 (Osaka)"), 1342 } 1343 1344 ratio_labels = ratio_labels or default_ratio_labels 1345 1346 # プロット後、軸の設定前に比率の線を追加 1347 x = np.array([0, 5]) 1348 base_ch4 = 0.0 1349 base = 0.0 1350 1351 # 各比率に対して線を引く 1352 for ratio, (x_pos, y_pos, label) in ratio_labels.items(): 1353 y = (x - base_ch4) * 1000 * ratio + base 1354 plt.plot(x, y, "-", c="black", alpha=0.5) 1355 plt.text(x_pos, y_pos, label) 1356 1357 plt.xlim(xlim) 1358 plt.ylim(ylim) 1359 plt.xlabel(xlabel) 1360 plt.ylabel(ylabel) 1361 if add_legend: 1362 plt.legend() 1363 1364 # グラフの保存または表示 1365 if save_fig: 1366 if output_dir is None: 1367 raise ValueError( 1368 "save_fig=Trueの場合、output_dirを指定する必要があります。有効なディレクトリパスを指定してください。" 1369 ) 1370 output_path: str = os.path.join(output_dir, output_filename) 1371 plt.savefig(output_path, bbox_inches="tight") 1372 self.logger.info(f"散布図を保存しました: {output_path}") 1373 if show_fig: 1374 plt.show() 1375 else: 1376 plt.close(fig=fig) 1377 1378 def plot_conc_timeseries( 1379 self, 1380 source_name: str | None = None, 1381 output_dir: str | Path | None = None, 1382 output_filename: str = "timeseries.png", 1383 dpi: int = 200, 1384 figsize: tuple[float, float] = (8, 4), 1385 save_fig: bool = True, 1386 show_fig: bool = True, 1387 col_ch4: str = "ch4_ppm", 1388 col_c2h6: str = "c2h6_ppb", 1389 col_h2o: str = "h2o_ppm", 1390 ylim_ch4: tuple[float, float] | None = None, 1391 ylim_c2h6: tuple[float, float] | None = None, 1392 ylim_h2o: tuple[float, float] | None = None, 1393 font_size: float = 12, 1394 label_pad: float = 10, 1395 ) -> None: 1396 """ 1397 時系列データをプロットします。 1398 1399 Parameters 1400 ---------- 1401 dpi : int 1402 図の解像度を指定します。デフォルトは200です。 1403 source_name : str | None 1404 プロットするデータソースの名前。Noneの場合は最初のデータソースを使用します。 1405 figsize : tuple[float, float] 1406 図のサイズを指定します。デフォルトは(8, 4)です。 1407 output_dir : str | Path | None 1408 保存先のディレクトリを指定します。save_fig=Trueの場合は必須です。 1409 output_filename : str 1410 保存するファイル名を指定します。デフォルトは"time_series.png"です。 1411 save_fig : bool 1412 図を保存するかどうかを指定します。デフォルトはFalseです。 1413 show_fig : bool 1414 図を表示するかどうかを指定します。デフォルトはTrueです。 1415 col_ch4 : str 1416 CH4データのキーを指定します。デフォルトは"ch4_ppm"です。 1417 col_c2h6 : str 1418 C2H6データのキーを指定します。デフォルトは"c2h6_ppb"です。 1419 col_h2o : str 1420 H2Oデータのキーを指定します。デフォルトは"h2o_ppm"です。 1421 ylim_ch4 : tuple[float, float] | None 1422 CH4プロットのy軸範囲を指定します。デフォルトはNoneです。 1423 ylim_c2h6 : tuple[float, float] | None 1424 C2H6プロットのy軸範囲を指定します。デフォルトはNoneです。 1425 ylim_h2o : tuple[float, float] | None 1426 H2Oプロットのy軸範囲を指定します。デフォルトはNoneです。 1427 font_size : float 1428 基本フォントサイズ。デフォルトは12。 1429 label_pad : float 1430 y軸ラベルのパディング。デフォルトは10。 1431 """ 1432 # プロットパラメータの設定 1433 plt.rcParams.update( 1434 { 1435 "font.size": font_size, 1436 "axes.labelsize": font_size, 1437 "axes.titlesize": font_size, 1438 "xtick.labelsize": font_size, 1439 "ytick.labelsize": font_size, 1440 } 1441 ) 1442 dfs_dict: dict[str, pd.DataFrame] = self._data.copy() 1443 # データソースの選択 1444 if not dfs_dict: 1445 raise ValueError("データが読み込まれていません。") 1446 1447 if source_name not in dfs_dict: 1448 raise ValueError( 1449 f"指定されたデータソース '{source_name}' が見つかりません。" 1450 ) 1451 1452 df = dfs_dict[source_name] 1453 1454 # プロットの作成 1455 fig = plt.figure(figsize=figsize, dpi=dpi) 1456 1457 # CH4プロット 1458 ax1 = fig.add_subplot(3, 1, 1) 1459 ax1.plot(df.index, df[col_ch4], c="red") 1460 if ylim_ch4: 1461 ax1.set_ylim(ylim_ch4) 1462 ax1.set_ylabel("$\\mathregular{CH_{4}}$ (ppm)", labelpad=label_pad) 1463 ax1.grid(True, alpha=0.3) 1464 1465 # C2H6プロット 1466 ax2 = fig.add_subplot(3, 1, 2) 1467 ax2.plot(df.index, df[col_c2h6], c="red") 1468 if ylim_c2h6: 1469 ax2.set_ylim(ylim_c2h6) 1470 ax2.set_ylabel("$\\mathregular{C_{2}H_{6}}$ (ppb)", labelpad=label_pad) 1471 ax2.grid(True, alpha=0.3) 1472 1473 # H2Oプロット 1474 ax3 = fig.add_subplot(3, 1, 3) 1475 ax3.plot(df.index, df[col_h2o], c="red") 1476 if ylim_h2o: 1477 ax3.set_ylim(ylim_h2o) 1478 ax3.set_ylabel("$\\mathregular{H_{2}O}$ (ppm)", labelpad=label_pad) 1479 ax3.grid(True, alpha=0.3) 1480 1481 # x軸のフォーマット調整 1482 for ax in [ax1, ax2, ax3]: 1483 ax.xaxis.set_major_formatter(mdates.DateFormatter("%H:%M")) 1484 # 軸のラベルとグリッド線の調整 1485 ax.tick_params(axis="both", which="major", labelsize=font_size) 1486 ax.grid(True, alpha=0.3) 1487 1488 # サブプロット間の間隔調整 1489 plt.subplots_adjust(wspace=0.38, hspace=0.38) 1490 1491 # 図の保存 1492 if save_fig: 1493 if output_dir is None: 1494 raise ValueError( 1495 "save_fig=Trueの場合、output_dirを指定する必要があります。有効なディレクトリパスを指定してください。" 1496 ) 1497 os.makedirs(output_dir, exist_ok=True) 1498 output_path = os.path.join(output_dir, output_filename) 1499 plt.savefig(output_path, bbox_inches="tight") 1500 1501 if show_fig: 1502 plt.show() 1503 else: 1504 plt.close(fig=fig) 1505 1506 def _detect_hotspots( 1507 self, 1508 df: pd.DataFrame, 1509 ch4_enhance_threshold: float, 1510 ) -> list[HotspotData]: 1511 """ 1512 シンプル化したホットスポット検出 1513 1514 Parameters 1515 ---------- 1516 df : pd.DataFrame 1517 入力データフレーム 1518 ch4_enhance_threshold : float 1519 CH4増加の閾値 1520 1521 Returns 1522 ---------- 1523 list[HotspotData] 1524 検出されたホットスポットのリスト 1525 """ 1526 hotspots: list[HotspotData] = [] 1527 1528 # CH4増加量が閾値を超えるデータポイントを抽出 1529 enhanced_mask = df["ch4_ppm_delta"] >= ch4_enhance_threshold 1530 1531 if enhanced_mask.any(): 1532 lat = df["latitude"][enhanced_mask] 1533 lon = df["longitude"][enhanced_mask] 1534 ratios = df["c2c1_ratio_delta"][enhanced_mask] 1535 delta_ch4 = df["ch4_ppm_delta"][enhanced_mask] 1536 delta_c2h6 = df["c2h6_ppb_delta"][enhanced_mask] 1537 1538 # 各ポイントに対してホットスポットを作成 1539 for i in range(len(lat)): 1540 if pd.notna(ratios.iloc[i]): 1541 current_lat = lat.iloc[i] 1542 current_lon = lon.iloc[i] 1543 correlation = df["c1c2_correlation"].iloc[i] 1544 1545 # 比率に基づいてタイプを決定 1546 spot_type: HotspotType = "bio" 1547 if ratios.iloc[i] >= 100: 1548 spot_type = "comb" 1549 elif ratios.iloc[i] >= 5: 1550 spot_type = "gas" 1551 1552 angle: float = MobileSpatialAnalyzer._calculate_angle( 1553 lat=current_lat, 1554 lon=current_lon, 1555 center_lat=self._center_lat, 1556 center_lon=self._center_lon, 1557 ) 1558 section: int = self._determine_section(angle) 1559 1560 hotspots.append( 1561 HotspotData( 1562 source=ratios.index[i].strftime("%Y-%m-%d %H:%M:%S"), 1563 angle=angle, 1564 avg_lat=current_lat, 1565 avg_lon=current_lon, 1566 delta_ch4=delta_ch4.iloc[i], 1567 delta_c2h6=delta_c2h6.iloc[i], 1568 correlation=max(-1, min(1, correlation)), 1569 ratio=ratios.iloc[i], 1570 section=section, 1571 type=spot_type, 1572 ) 1573 ) 1574 1575 return hotspots 1576 1577 def _determine_section(self, angle: float) -> int: 1578 """ 1579 角度に基づいて所属する区画を特定します。 1580 1581 Parameters 1582 ---------- 1583 angle : float 1584 計算された角度 1585 1586 Returns 1587 ---------- 1588 int 1589 区画番号(0-based-index) 1590 """ 1591 for section_num, (start, end) in self._sections.items(): 1592 if start <= angle < end: 1593 return section_num 1594 # -180度の場合は最後の区画に含める 1595 return self._num_sections - 1 1596 1597 def _load_all_data( 1598 self, input_configs: list[MSAInputConfig] 1599 ) -> dict[str, pd.DataFrame]: 1600 """ 1601 全入力ファイルのデータを読み込み、データフレームの辞書を返します。 1602 1603 このメソッドは、指定された入力設定に基づいてすべてのデータファイルを読み込み、 1604 各ファイルのデータをデータフレームとして格納した辞書を生成します。 1605 1606 Parameters 1607 ---------- 1608 input_configs : list[MSAInputConfig] 1609 読み込むファイルの設定リスト。 1610 1611 Returns 1612 ---------- 1613 dict[str, pd.DataFrame] 1614 読み込まれたデータフレームの辞書。キーはファイル名、値はデータフレーム。 1615 """ 1616 all_data: dict[str, pd.DataFrame] = {} 1617 for config in input_configs: 1618 df, source_name = self._load_data(config) 1619 all_data[source_name] = df 1620 return all_data 1621 1622 def _load_data( 1623 self, 1624 config: MSAInputConfig, 1625 columns_to_shift: list[str] = ["ch4_ppm", "c2h6_ppb", "h2o_ppm"], 1626 col_timestamp: str = "timestamp", 1627 col_latitude: str = "latitude", 1628 col_longitude: str = "longitude", 1629 ) -> tuple[pd.DataFrame, str]: 1630 """ 1631 測定データを読み込み、前処理を行うメソッド。 1632 1633 Parameters 1634 ---------- 1635 config : MSAInputConfig 1636 入力ファイルの設定を含むオブジェクト。ファイルパス、遅れ時間、サンプリング周波数、補正タイプなどの情報を持つ。 1637 columns_to_shift : list[str], optional 1638 シフトを適用するカラム名のリスト。デフォルトは["ch4_ppm", "c2h6_ppb", "h2o_ppm"]で、これらのカラムに対して遅れ時間の補正が行われる。 1639 col_timestamp : str, optional 1640 タイムスタンプのカラム名。デフォルトは"timestamp"。 1641 col_latitude : str, optional 1642 緯度のカラム名。デフォルトは"latitude"。 1643 col_longitude : str, optional 1644 経度のカラム名。デフォルトは"longitude"。 1645 1646 Returns 1647 ---------- 1648 tuple[pd.DataFrame, str] 1649 読み込まれたデータフレームとそのソース名を含むタプル。データフレームは前処理が施されており、ソース名はファイル名から抽出されたもの。 1650 """ 1651 source_name: str = MobileSpatialAnalyzer.extract_source_name_from_path( 1652 config.path 1653 ) 1654 df: pd.DataFrame = pd.read_csv(config.path, na_values=self._na_values) 1655 1656 # カラム名の標準化(測器に依存しない汎用的な名前に変更) 1657 df = df.rename(columns=self._column_mapping) 1658 df[col_timestamp] = pd.to_datetime(df[col_timestamp]) 1659 # インデックスを設定(元のtimestampカラムは保持) 1660 df = df.set_index(col_timestamp, drop=False) 1661 1662 if config.lag < 0: 1663 raise ValueError( 1664 f"Invalid lag value: {config.lag}. Must be a non-negative float." 1665 ) 1666 1667 # サンプリング周波数に応じてシフト量を調整 1668 shift_periods: float = -config.lag * config.fs # fsを掛けて補正 1669 1670 # 遅れ時間の補正 1671 for col in columns_to_shift: 1672 df[col] = df[col].shift(shift_periods) 1673 1674 # 緯度経度とシフト対象カラムのnanを一度に削除 1675 df = df.dropna(subset=[col_latitude, col_longitude] + columns_to_shift) 1676 1677 # 水蒸気補正の適用 1678 h2o_correction: H2OCorrectionConfig = config.h2o_correction 1679 if config.h2o_correction is not None and all( 1680 x is not None 1681 for x in [ 1682 h2o_correction.coef_a, 1683 h2o_correction.coef_b, 1684 h2o_correction.coef_c, 1685 ] 1686 ): 1687 df = CorrectingUtils.correct_h2o_interference( 1688 df=df, 1689 coef_a=h2o_correction.coef_a, 1690 coef_b=h2o_correction.coef_b, 1691 coef_c=h2o_correction.coef_c, 1692 h2o_ppm_threshold=h2o_correction.h2o_ppm_threshold, 1693 ) 1694 1695 # バイアス除去の適用 1696 bias_removal: BiasRemovalConfig = config.bias_removal 1697 if bias_removal is not None: 1698 df = CorrectingUtils.remove_bias( 1699 df=df, 1700 percentile=bias_removal.percentile, 1701 base_ch4_ppm=bias_removal.base_ch4_ppm, 1702 base_c2h6_ppb=bias_removal.base_c2h6_ppb, 1703 ) 1704 1705 return df, source_name 1706 1707 @staticmethod 1708 def _calculate_angle( 1709 lat: float, lon: float, center_lat: float, center_lon: float 1710 ) -> float: 1711 """ 1712 中心からの角度を計算 1713 1714 Parameters 1715 ---------- 1716 lat : float 1717 対象地点の緯度 1718 lon : float 1719 対象地点の経度 1720 center_lat : float 1721 中心の緯度 1722 center_lon : float 1723 中心の経度 1724 1725 Returns 1726 ---------- 1727 float 1728 真北を0°として時計回りの角度(-180°から180°) 1729 """ 1730 d_lat: float = lat - center_lat 1731 d_lon: float = lon - center_lon 1732 # arctanを使用して角度を計算(ラジアン) 1733 angle_rad: float = math.atan2(d_lon, d_lat) 1734 # ラジアンから度に変換(-180から180の範囲) 1735 angle_deg: float = math.degrees(angle_rad) 1736 return angle_deg 1737 1738 @classmethod 1739 def _calculate_distance( 1740 cls, lat1: float, lon1: float, lat2: float, lon2: float 1741 ) -> float: 1742 """ 1743 2点間の距離をメートル単位で計算(Haversine formula) 1744 1745 Parameters 1746 ---------- 1747 lat1 : float 1748 地点1の緯度 1749 lon1 : float 1750 地点1の経度 1751 lat2 : float 1752 地点2の緯度 1753 lon2 : float 1754 地点2の経度 1755 1756 Returns 1757 ---------- 1758 float 1759 2地点間の距離(メートル) 1760 """ 1761 R = cls.EARTH_RADIUS_METERS 1762 1763 # 緯度経度をラジアンに変換 1764 lat1_rad: float = math.radians(lat1) 1765 lon1_rad: float = math.radians(lon1) 1766 lat2_rad: float = math.radians(lat2) 1767 lon2_rad: float = math.radians(lon2) 1768 1769 # 緯度と経度の差分 1770 dlat: float = lat2_rad - lat1_rad 1771 dlon: float = lon2_rad - lon1_rad 1772 1773 # Haversine formula 1774 a: float = ( 1775 math.sin(dlat / 2) ** 2 1776 + math.cos(lat1_rad) * math.cos(lat2_rad) * math.sin(dlon / 2) ** 2 1777 ) 1778 c: float = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a)) 1779 1780 return R * c # メートル単位での距離 1781 1782 @staticmethod 1783 def _calculate_hotspots_parameters( 1784 df: pd.DataFrame, 1785 window_size: int, 1786 col_ch4_ppm: str, 1787 col_c2h6_ppb: str, 1788 col_h2o_ppm: str, 1789 ch4_ppm_delta_threshold: float = 0.05, 1790 c2h6_ppb_delta_threshold: float = 0.0, 1791 h2o_ppm_threshold: float = 2000, 1792 rolling_method: RollingMethod = "quantile", 1793 quantile_value: float = 5, 1794 ) -> pd.DataFrame: 1795 """ 1796 ホットスポットのパラメータを計算します。 1797 このメソッドは、指定されたデータフレームに対して移動平均(または指定されたパーセンタイル)や相関を計算し、 1798 各種のデルタ値や比率を追加します。 1799 1800 Parameters 1801 ---------- 1802 df : pd.DataFrame 1803 入力データフレーム 1804 window_size : int 1805 移動窓のサイズ 1806 col_ch4_ppm : str 1807 CH4濃度を示すカラム名 1808 col_c2h6_ppb : str 1809 C2H6濃度を示すカラム名 1810 col_h2o_ppm : str 1811 H2O濃度を示すカラム名 1812 ch4_ppm_delta_threshold : float 1813 CH4の閾値 1814 c2h6_ppb_delta_threshold : float 1815 C2H6の閾値 1816 h2o_ppm_threshold : float 1817 H2Oの閾値 1818 rolling_method : RollingMethod 1819 バックグラウンド値の移動計算に使用する方法を指定します。 1820 - 'quantile'はパーセンタイルを使用します。 1821 - 'mean'は平均を使用します。 1822 quantile_value : float 1823 使用するパーセンタイルの値(デフォルトは5) 1824 1825 Returns 1826 ---------- 1827 pd.DataFrame 1828 計算されたパラメータを含むデータフレーム 1829 1830 Raises 1831 ---------- 1832 ValueError 1833 quantile_value が0未満または100を超える場合に発生します。 1834 """ 1835 # 引数のバリデーション 1836 if quantile_value < 0 or quantile_value > 100: 1837 raise ValueError("quantile_value は0以上100以下の float で指定する必要があります。") 1838 quantile_value_decimal: float = quantile_value / 100 # パーセントから小数に変換 1839 1840 # データのコピーを作成 1841 df_copied: pd.DataFrame = df.copy() 1842 1843 # 移動相関の計算 1844 df_copied["c1c2_correlation"] = ( 1845 df_copied[col_ch4_ppm].rolling(window=window_size).corr(df_copied[col_c2h6_ppb]) 1846 ) 1847 1848 # バックグラウンド値の計算(指定されたパーセンタイルまたは移動平均) 1849 if rolling_method == "quantile": 1850 df_copied["ch4_ppm_bg"] = ( 1851 df_copied[col_ch4_ppm] 1852 .rolling(window=window_size, center=True, min_periods=1) 1853 .quantile(quantile_value_decimal) 1854 ) 1855 df_copied["c2h6_ppb_bg"] = ( 1856 df_copied[col_c2h6_ppb] 1857 .rolling(window=window_size, center=True, min_periods=1) 1858 .quantile(quantile_value_decimal) 1859 ) 1860 elif rolling_method == "mean": 1861 df_copied["ch4_ppm_bg"] = ( 1862 df_copied[col_ch4_ppm] 1863 .rolling(window=window_size, center=True, min_periods=1) 1864 .mean() 1865 ) 1866 df_copied["c2h6_ppb_bg"] = ( 1867 df_copied[col_c2h6_ppb] 1868 .rolling(window=window_size, center=True, min_periods=1) 1869 .mean() 1870 ) 1871 1872 # デルタ値の計算 1873 df_copied["ch4_ppm_delta"] = df_copied[col_ch4_ppm] - df_copied["ch4_ppm_bg"] 1874 df_copied["c2h6_ppb_delta"] = df_copied[col_c2h6_ppb] - df_copied["c2h6_ppb_bg"] 1875 1876 # C2H6/CH4の比率計算 1877 df_copied["c2c1_ratio"] = df_copied[col_c2h6_ppb] / df_copied[col_ch4_ppm] 1878 # デルタ値に基づく比の計算とフィルタリング 1879 df_copied["c2c1_ratio_delta"] = df_copied["c2h6_ppb_delta"] / df_copied["ch4_ppm_delta"] 1880 1881 # フィルタリング条件の適用 1882 df_copied.loc[df_copied["ch4_ppm_delta"] < ch4_ppm_delta_threshold, "c2c1_ratio_delta"] = np.nan 1883 df_copied.loc[df_copied["c2h6_ppb_delta"] < -10.0, "c2h6_ppb_delta"] = np.nan 1884 df_copied.loc[df_copied["c2h6_ppb_delta"] > 1000.0, "c2h6_ppb_delta"] = np.nan 1885 # ホットスポットの定義上0未満の値もカウントされるので0未満は一律0とする 1886 df_copied.loc[df_copied["c2h6_ppb_delta"] < c2h6_ppb_delta_threshold, "c2c1_ratio_delta"] = 0.0 1887 1888 # 水蒸気濃度によるフィルタリング 1889 df_copied.loc[df_copied[col_h2o_ppm] < h2o_ppm_threshold, [col_ch4_ppm, col_c2h6_ppb]] = np.nan 1890 1891 # 欠損値の除去 1892 df_copied = df_copied.dropna(subset=[col_ch4_ppm, col_c2h6_ppb]) 1893 1894 return df_copied 1895 1896 @staticmethod 1897 def _calculate_window_size(window_minutes: float) -> int: 1898 """ 1899 時間窓からデータポイント数を計算 1900 1901 Parameters 1902 ---------- 1903 window_minutes : float 1904 時間窓の大きさ(分) 1905 1906 Returns 1907 ---------- 1908 int 1909 データポイント数 1910 """ 1911 return int(60 * window_minutes) 1912 1913 @staticmethod 1914 def _initialize_sections( 1915 num_sections: int, section_size: float 1916 ) -> dict[int, tuple[float, float]]: 1917 """ 1918 指定された区画数と区画サイズに基づいて、区画の範囲を初期化します。 1919 1920 Parameters 1921 ---------- 1922 num_sections : int 1923 初期化する区画の数。 1924 section_size : float 1925 各区画の角度範囲のサイズ。 1926 1927 Returns 1928 ---------- 1929 dict[int, tuple[float, float]] 1930 区画番号(0-based-index)とその範囲の辞書。各区画は-180度から180度の範囲に分割されます。 1931 """ 1932 sections: dict[int, tuple[float, float]] = {} 1933 for i in range(num_sections): 1934 # -180から180の範囲で区画を設定 1935 start_angle = -180 + i * section_size 1936 end_angle = -180 + (i + 1) * section_size 1937 sections[i] = (start_angle, end_angle) 1938 return sections 1939 1940 @staticmethod 1941 def _is_duplicate_spot( 1942 current_lat: float, 1943 current_lon: float, 1944 current_time: str, 1945 used_positions: list[tuple[float, float, str, float]], 1946 check_time_all: bool, 1947 min_time_threshold_seconds: float, 1948 max_time_threshold_hours: float, 1949 hotspot_area_meter: float, 1950 ) -> bool: 1951 """ 1952 与えられた地点が既存の地点と重複しているかを判定します。 1953 1954 Parameters 1955 ---------- 1956 current_lat : float 1957 判定する地点の緯度 1958 current_lon : float 1959 判定する地点の経度 1960 current_time : str 1961 判定する地点の時刻 1962 used_positions : list[tuple[float, float, str, float]] 1963 既存の地点情報のリスト (lat, lon, time, value) 1964 check_time_all : bool 1965 時間に関係なく重複チェックを行うかどうか 1966 min_time_threshold_seconds : float 1967 重複とみなす最小時間の閾値(秒) 1968 max_time_threshold_hours : float 1969 重複チェックを一時的に無視する最大時間の閾値(時間) 1970 hotspot_area_meter : float 1971 重複とみなす距離の閾値(m) 1972 1973 Returns 1974 ---------- 1975 bool 1976 重複している場合はTrue、そうでない場合はFalse 1977 """ 1978 for used_lat, used_lon, used_time, _ in used_positions: 1979 # 距離チェック 1980 distance = MobileSpatialAnalyzer._calculate_distance( 1981 lat1=current_lat, lon1=current_lon, lat2=used_lat, lon2=used_lon 1982 ) 1983 1984 if distance < hotspot_area_meter: 1985 # 時間差の計算(秒単位) 1986 time_diff = pd.Timedelta( 1987 pd.to_datetime(current_time) - pd.to_datetime(used_time) 1988 ).total_seconds() 1989 time_diff_abs = abs(time_diff) 1990 1991 if check_time_all: 1992 # 時間に関係なく、距離が近ければ重複とみなす 1993 return True 1994 else: 1995 # 時間窓による判定を行う 1996 if time_diff_abs <= min_time_threshold_seconds: 1997 # Case 1: 最小時間閾値以内は重複とみなす 1998 return True 1999 elif time_diff_abs > max_time_threshold_hours * 3600: 2000 # Case 2: 最大時間閾値を超えた場合は重複チェックをスキップ 2001 continue 2002 # Case 3: その間の時間差の場合は、距離が近ければ重複とみなす 2003 return True 2004 2005 return False 2006 2007 @staticmethod 2008 def _normalize_inputs( 2009 inputs: list[MSAInputConfig] | list[tuple[float, float, str | Path]], 2010 ) -> list[MSAInputConfig]: 2011 """ 2012 入力設定を標準化 2013 2014 Parameters 2015 ---------- 2016 inputs : list[MSAInputConfig] | list[tuple[float, float, str | Path]] 2017 入力設定のリスト 2018 2019 Returns 2020 ---------- 2021 list[MSAInputConfig] 2022 標準化された入力設定のリスト 2023 """ 2024 normalized: list[MSAInputConfig] = [] 2025 for inp in inputs: 2026 if isinstance(inp, MSAInputConfig): 2027 normalized.append(inp) # すでに検証済みのため、そのまま追加 2028 else: 2029 fs, lag, path = inp 2030 normalized.append( 2031 MSAInputConfig.validate_and_create(fs=fs, lag=lag, path=path) 2032 ) 2033 return normalized 2034 2035 def remove_c2c1_ratio_duplicates( 2036 self, 2037 df: pd.DataFrame, 2038 min_time_threshold_seconds: float = 300, # 5分以内は重複とみなす 2039 max_time_threshold_hours: float = 12.0, # 12時間以上離れている場合は別のポイントとして扱う 2040 check_time_all: bool = True, # 時間閾値を超えた場合の重複チェックを継続するかどうか 2041 hotspot_area_meter: float = 50.0, # 重複とみなす距離の閾値(メートル) 2042 col_ch4_ppm: str = "ch4_ppm", 2043 col_ch4_ppm_bg: str = "ch4_ppm_bg", 2044 col_ch4_ppm_delta: str = "ch4_ppm_delta", 2045 ): 2046 """ 2047 メタン濃度の増加が閾値を超えた地点から、重複を除外してユニークなホットスポットを抽出する関数。 2048 2049 Parameters 2050 ---------- 2051 df : pandas.DataFrame 2052 入力データフレーム。必須カラム: 2053 - ch4_ppm: メタン濃度(ppm) 2054 - ch4_ppm_bg: メタン濃度の移動平均(ppm) 2055 - ch4_ppm_delta: メタン濃度の増加量(ppm) 2056 - latitude: 緯度 2057 - longitude: 経度 2058 min_time_threshold_seconds : float, optional 2059 重複とみなす最小時間差(秒)。デフォルトは300秒(5分)。 2060 max_time_threshold_hours : float, optional 2061 別ポイントとして扱う最大時間差(時間)。デフォルトは12時間。 2062 check_time_all : bool, optional 2063 時間閾値を超えた場合の重複チェックを継続するかどうか。デフォルトはTrue。 2064 hotspot_area_meter : float, optional 2065 重複とみなす距離の閾値(メートル)。デフォルトは50メートル。 2066 2067 Returns 2068 ---------- 2069 pandas.DataFrame 2070 ユニークなホットスポットのデータフレーム。 2071 """ 2072 df_data: pd.DataFrame = df.copy() 2073 # メタン濃度の増加が閾値を超えた点を抽出 2074 mask = ( 2075 df_data[col_ch4_ppm] - df_data[col_ch4_ppm_bg] > self._ch4_enhance_threshold 2076 ) 2077 hotspot_candidates = df_data[mask].copy() 2078 2079 # ΔCH4の降順でソート 2080 sorted_hotspots = hotspot_candidates.sort_values( 2081 by=col_ch4_ppm_delta, ascending=False 2082 ) 2083 used_positions = [] 2084 unique_hotspots = pd.DataFrame() 2085 2086 for _, spot in sorted_hotspots.iterrows(): 2087 should_add = True 2088 for used_lat, used_lon, used_time in used_positions: 2089 # 距離チェック 2090 distance = geodesic( 2091 (spot.latitude, spot.longitude), (used_lat, used_lon) 2092 ).meters 2093 2094 if distance < hotspot_area_meter: 2095 # 時間差の計算(秒単位) 2096 time_diff = pd.Timedelta( 2097 spot.name - pd.to_datetime(used_time) 2098 ).total_seconds() 2099 time_diff_abs = abs(time_diff) 2100 2101 # 時間差に基づく判定 2102 if check_time_all: 2103 # 時間に関係なく、距離が近ければ重複とみなす 2104 # ΔCH4が大きい方を残す(現在のスポットは必ず小さい) 2105 should_add = False 2106 break 2107 else: 2108 # 時間窓による判定を行う 2109 if time_diff_abs <= min_time_threshold_seconds: 2110 # Case 1: 最小時間閾値以内は重複とみなす 2111 should_add = False 2112 break 2113 elif time_diff_abs > max_time_threshold_hours * 3600: 2114 # Case 2: 最大時間閾値を超えた場合は重複チェックをスキップ 2115 continue 2116 # Case 3: その間の時間差の場合は、距離が近ければ重複とみなす 2117 should_add = False 2118 break 2119 2120 if should_add: 2121 unique_hotspots = pd.concat([unique_hotspots, pd.DataFrame([spot])]) 2122 used_positions.append((spot.latitude, spot.longitude, spot.name)) 2123 2124 return unique_hotspots 2125 2126 @staticmethod 2127 def remove_hotspots_duplicates( 2128 hotspots: list[HotspotData], 2129 check_time_all: bool, 2130 min_time_threshold_seconds: float = 300, 2131 max_time_threshold_hours: float = 12, 2132 hotspot_area_meter: float = 50, 2133 ) -> list[HotspotData]: 2134 """ 2135 重複するホットスポットを除外します。 2136 2137 このメソッドは、与えられたホットスポットのリストから重複を検出し、 2138 一意のホットスポットのみを返します。重複の判定は、指定された 2139 時間および距離の閾値に基づいて行われます。 2140 2141 Parameters 2142 ---------- 2143 hotspots : list[HotspotData] 2144 重複を除外する対象のホットスポットのリスト。 2145 check_time_all : bool 2146 時間に関係なく重複チェックを行うかどうか。 2147 min_time_threshold_seconds : float 2148 重複とみなす最小時間の閾値(秒)。 2149 max_time_threshold_hours : float 2150 重複チェックを一時的に無視する最大時間の閾値(時間)。 2151 hotspot_area_meter : float 2152 重複とみなす距離の閾値(メートル)。 2153 2154 Returns 2155 ---------- 2156 list[HotspotData] 2157 重複を除去したホットスポットのリスト。 2158 """ 2159 # ΔCH4の降順でソート 2160 sorted_hotspots: list[HotspotData] = sorted( 2161 hotspots, key=lambda x: x.delta_ch4, reverse=True 2162 ) 2163 used_positions_by_type: dict[ 2164 HotspotType, list[tuple[float, float, str, float]] 2165 ] = { 2166 "bio": [], 2167 "gas": [], 2168 "comb": [], 2169 } 2170 unique_hotspots: list[HotspotData] = [] 2171 2172 for spot in sorted_hotspots: 2173 is_duplicate = MobileSpatialAnalyzer._is_duplicate_spot( 2174 current_lat=spot.avg_lat, 2175 current_lon=spot.avg_lon, 2176 current_time=spot.source, 2177 used_positions=used_positions_by_type[spot.type], 2178 check_time_all=check_time_all, 2179 min_time_threshold_seconds=min_time_threshold_seconds, 2180 max_time_threshold_hours=max_time_threshold_hours, 2181 hotspot_area_meter=hotspot_area_meter, 2182 ) 2183 2184 if not is_duplicate: 2185 unique_hotspots.append(spot) 2186 used_positions_by_type[spot.type].append( 2187 (spot.avg_lat, spot.avg_lon, spot.source, spot.delta_ch4) 2188 ) 2189 2190 return unique_hotspots 2191 2192 @staticmethod 2193 def setup_logger(logger: Logger | None, log_level: int = INFO) -> Logger: 2194 """ 2195 ロガーを設定します。 2196 2197 このメソッドは、ロギングの設定を行い、ログメッセージのフォーマットを指定します。 2198 ログメッセージには、日付、ログレベル、メッセージが含まれます。 2199 2200 渡されたロガーがNoneまたは不正な場合は、新たにロガーを作成し、標準出力に 2201 ログメッセージが表示されるようにStreamHandlerを追加します。ロガーのレベルは 2202 引数で指定されたlog_levelに基づいて設定されます。 2203 2204 Parameters 2205 ---------- 2206 logger : Logger | None 2207 使用するロガー。Noneの場合は新しいロガーを作成します。 2208 log_level : int 2209 ロガーのログレベル。デフォルトはINFO。 2210 2211 Returns 2212 ---------- 2213 Logger 2214 設定されたロガーオブジェクト。 2215 """ 2216 if logger is not None and isinstance(logger, Logger): 2217 return logger 2218 # 渡されたロガーがNoneまたは正しいものでない場合は独自に設定 2219 new_logger: Logger = getLogger() 2220 # 既存のハンドラーをすべて削除 2221 for handler in new_logger.handlers[:]: 2222 new_logger.removeHandler(handler) 2223 new_logger.setLevel(log_level) # ロガーのレベルを設定 2224 ch = StreamHandler() 2225 ch_formatter = Formatter("%(asctime)s - %(levelname)s - %(message)s") 2226 ch.setFormatter(ch_formatter) # フォーマッターをハンドラーに設定 2227 new_logger.addHandler(ch) # StreamHandlerの追加 2228 return new_logger 2229 2230 @staticmethod 2231 def calculate_emission_rates( 2232 hotspots: list[HotspotData], 2233 method: Literal["weller", "weitzel", "joo", "umezawa"] = "weller", 2234 print_summary: bool = True, 2235 custom_formulas: dict[str, dict[str, float]] | None = None, 2236 ) -> tuple[list[EmissionData], dict[str, dict[str, float]]]: 2237 """ 2238 検出されたホットスポットのCH4漏出量を計算・解析し、統計情報を生成します。 2239 2240 Parameters 2241 ---------- 2242 hotspots : list[HotspotData] 2243 分析対象のホットスポットのリスト 2244 method : Literal["weller", "weitzel", "joo", "umezawa"] 2245 使用する計算式。デフォルトは"weller"。 2246 print_summary : bool 2247 統計情報を表示するかどうか。デフォルトはTrue。 2248 custom_formulas : dict[str, dict[str, float]] | None 2249 カスタム計算式の係数。 2250 例: {"custom_method": {"a": 1.0, "b": 1.0}} 2251 Noneの場合はデフォルトの計算式を使用。 2252 2253 Returns 2254 ---------- 2255 tuple[list[EmissionData], dict[str, dict[str, float]]] 2256 - 各ホットスポットの排出量データを含むリスト 2257 - タイプ別の統計情報を含む辞書 2258 """ 2259 # デフォルトの経験式係数 2260 default_formulas = { 2261 "weller": {"a": 0.988, "b": 0.817}, 2262 "weitzel": {"a": 0.521, "b": 0.795}, 2263 "joo": {"a": 2.738, "b": 1.329}, 2264 "umezawa": {"a": 2.716, "b": 0.741}, 2265 } 2266 2267 # カスタム計算式がある場合は追加 2268 emission_formulas = default_formulas.copy() 2269 if custom_formulas: 2270 emission_formulas.update(custom_formulas) 2271 2272 if method not in emission_formulas: 2273 raise ValueError(f"Unknown method: {method}") 2274 2275 # 係数の取得 2276 a = emission_formulas[method]["a"] 2277 b = emission_formulas[method]["b"] 2278 2279 # 排出量の計算 2280 emission_data_list = [] 2281 for spot in hotspots: 2282 # 漏出量の計算 (L/min) 2283 emission_rate = np.exp((np.log(spot.delta_ch4) + a) / b) 2284 # 日排出量 (L/day) 2285 daily_emission = emission_rate * 60 * 24 2286 # 年間排出量 (L/year) 2287 annual_emission = daily_emission * 365 2288 2289 emission_data = EmissionData( 2290 source=spot.source, 2291 type=spot.type, 2292 section=spot.section, 2293 latitude=spot.avg_lat, 2294 longitude=spot.avg_lon, 2295 delta_ch4=spot.delta_ch4, 2296 delta_c2h6=spot.delta_c2h6, 2297 ratio=spot.ratio, 2298 emission_rate=emission_rate, 2299 daily_emission=daily_emission, 2300 annual_emission=annual_emission, 2301 ) 2302 emission_data_list.append(emission_data) 2303 2304 # 統計計算用にDataFrameを作成 2305 emission_df = pd.DataFrame([e.to_dict() for e in emission_data_list]) 2306 2307 # タイプ別の統計情報を計算 2308 stats = {} 2309 # emission_formulas の定義の後に、排出量カテゴリーの閾値を定義 2310 emission_categories = { 2311 "low": {"min": 0, "max": 6}, # < 6 L/min 2312 "medium": {"min": 6, "max": 40}, # 6-40 L/min 2313 "high": {"min": 40, "max": float("inf")}, # > 40 L/min 2314 } 2315 # get_args(HotspotType)を使用して型安全なリストを作成 2316 types = list(get_args(HotspotType)) 2317 for spot_type in types: 2318 df_type = emission_df[emission_df["type"] == spot_type] 2319 if len(df_type) > 0: 2320 # 既存の統計情報を計算 2321 type_stats = { 2322 "count": len(df_type), 2323 "emission_rate_min": df_type["emission_rate"].min(), 2324 "emission_rate_max": df_type["emission_rate"].max(), 2325 "emission_rate_mean": df_type["emission_rate"].mean(), 2326 "emission_rate_median": df_type["emission_rate"].median(), 2327 "total_annual_emission": df_type["annual_emission"].sum(), 2328 "mean_annual_emission": df_type["annual_emission"].mean(), 2329 } 2330 2331 # 排出量カテゴリー別の統計を追加 2332 category_counts = { 2333 "low": len( 2334 df_type[ 2335 df_type["emission_rate"] < emission_categories["low"]["max"] 2336 ] 2337 ), 2338 "medium": len( 2339 df_type[ 2340 ( 2341 df_type["emission_rate"] 2342 >= emission_categories["medium"]["min"] 2343 ) 2344 & ( 2345 df_type["emission_rate"] 2346 < emission_categories["medium"]["max"] 2347 ) 2348 ] 2349 ), 2350 "high": len( 2351 df_type[ 2352 df_type["emission_rate"] 2353 >= emission_categories["high"]["min"] 2354 ] 2355 ), 2356 } 2357 type_stats["emission_categories"] = category_counts 2358 2359 stats[spot_type] = type_stats 2360 2361 if print_summary: 2362 print(f"\n{spot_type}タイプの統計情報:") 2363 print(f" 検出数: {type_stats['count']}") 2364 print(" 排出量 (L/min):") 2365 print(f" 最小値: {type_stats['emission_rate_min']:.2f}") 2366 print(f" 最大値: {type_stats['emission_rate_max']:.2f}") 2367 print(f" 平均値: {type_stats['emission_rate_mean']:.2f}") 2368 print(f" 中央値: {type_stats['emission_rate_median']:.2f}") 2369 print(" 排出量カテゴリー別の検出数:") 2370 print(f" 低放出 (< 6 L/min): {category_counts['low']}") 2371 print(f" 中放出 (6-40 L/min): {category_counts['medium']}") 2372 print(f" 高放出 (> 40 L/min): {category_counts['high']}") 2373 print(" 年間排出量 (L/year):") 2374 print(f" 合計: {type_stats['total_annual_emission']:.2f}") 2375 print(f" 平均: {type_stats['mean_annual_emission']:.2f}") 2376 2377 return emission_data_list, stats 2378 2379 @staticmethod 2380 def plot_emission_analysis( 2381 emission_data_list: list[EmissionData], 2382 dpi: int = 300, 2383 output_dir: str | Path | None = None, 2384 output_filename: str = "emission_analysis.png", 2385 figsize: tuple[float, float] = (12, 5), 2386 hotspot_colors: dict[HotspotType, str] = { 2387 "bio": "blue", 2388 "gas": "red", 2389 "comb": "green", 2390 }, 2391 add_legend: bool = True, 2392 hist_log_y: bool = False, 2393 hist_xlim: tuple[float, float] | None = None, 2394 hist_ylim: tuple[float, float] | None = None, 2395 scatter_xlim: tuple[float, float] | None = None, 2396 scatter_ylim: tuple[float, float] | None = None, 2397 hist_bin_width: float = 0.5, 2398 print_summary: bool = True, 2399 save_fig: bool = False, 2400 show_fig: bool = True, 2401 show_scatter: bool = True, # 散布図の表示を制御するオプションを追加 2402 ) -> None: 2403 """ 2404 排出量分析のプロットを作成する静的メソッド。 2405 2406 Parameters 2407 ---------- 2408 emission_data_list : list[EmissionData] 2409 EmissionDataオブジェクトのリスト。 2410 output_dir : str | Path | None 2411 出力先ディレクトリのパス。 2412 output_filename : str 2413 保存するファイル名。デフォルトは"emission_analysis.png"。 2414 dpi : int 2415 プロットの解像度。デフォルトは300。 2416 figsize : tuple[float, float] 2417 プロットのサイズ。デフォルトは(12, 5)。 2418 hotspot_colors : dict[HotspotType, str] 2419 ホットスポットの色を定義する辞書。 2420 add_legend : bool 2421 凡例を追加するかどうか。デフォルトはTrue。 2422 hist_log_y : bool 2423 ヒストグラムのy軸を対数スケールにするかどうか。デフォルトはFalse。 2424 hist_xlim : tuple[float, float] | None 2425 ヒストグラムのx軸の範囲。デフォルトはNone。 2426 hist_ylim : tuple[float, float] | None 2427 ヒストグラムのy軸の範囲。デフォルトはNone。 2428 scatter_xlim : tuple[float, float] | None 2429 散布図のx軸の範囲。デフォルトはNone。 2430 scatter_ylim : tuple[float, float] | None 2431 散布図のy軸の範囲。デフォルトはNone。 2432 hist_bin_width : float 2433 ヒストグラムのビンの幅。デフォルトは0.5。 2434 print_summary : bool 2435 集計結果を表示するかどうか。デフォルトはFalse。 2436 save_fig : bool 2437 図をファイルに保存するかどうか。デフォルトはFalse。 2438 show_fig : bool 2439 図を表示するかどうか。デフォルトはTrue。 2440 show_scatter : bool 2441 散布図(右図)を表示するかどうか。デフォルトはTrue。 2442 """ 2443 # データをDataFrameに変換 2444 df = pd.DataFrame([e.to_dict() for e in emission_data_list]) 2445 2446 # プロットの作成(散布図の有無に応じてサブプロット数を調整) 2447 if show_scatter: 2448 fig, (ax1, ax2) = plt.subplots(1, 2, figsize=figsize) 2449 axes = [ax1, ax2] 2450 else: 2451 fig, ax1 = plt.subplots(1, 1, figsize=(figsize[0] // 2, figsize[1])) 2452 axes = [ax1] 2453 2454 # 存在するタイプを確認 2455 # HotspotTypeの定義順を基準にソート 2456 hotspot_types = list(get_args(HotspotType)) 2457 existing_types = sorted( 2458 df["type"].unique(), key=lambda x: hotspot_types.index(x) 2459 ) 2460 2461 # 左側: ヒストグラム 2462 # ビンの範囲を設定 2463 start = 0 # 必ず0から開始 2464 if hist_xlim is not None: 2465 end = hist_xlim[1] 2466 else: 2467 end = np.ceil(df["emission_rate"].max() * 1.05) 2468 2469 # ビン数を計算(end値をbin_widthで割り切れるように調整) 2470 n_bins = int(np.ceil(end / hist_bin_width)) 2471 end = n_bins * hist_bin_width 2472 2473 # ビンの生成(0から開始し、bin_widthの倍数で区切る) 2474 bins = np.linspace(start, end, n_bins + 1) 2475 2476 # タイプごとにヒストグラムを積み上げ 2477 bottom = np.zeros(len(bins) - 1) 2478 for spot_type in existing_types: 2479 data = df[df["type"] == spot_type]["emission_rate"] 2480 if len(data) > 0: 2481 counts, _ = np.histogram(data, bins=bins) 2482 ax1.bar( 2483 bins[:-1], 2484 counts, 2485 width=hist_bin_width, 2486 bottom=bottom, 2487 alpha=0.6, 2488 label=spot_type, 2489 color=hotspot_colors[spot_type], 2490 ) 2491 bottom += counts 2492 2493 ax1.set_xlabel("CH$_4$ Emission (L min$^{-1}$)") 2494 ax1.set_ylabel("Frequency") 2495 if hist_log_y: 2496 # ax1.set_yscale("log") 2497 # 非線形スケールを設定(linthreshで線形から対数への遷移点を指定) 2498 ax1.set_yscale("symlog", linthresh=1.0) 2499 if hist_xlim is not None: 2500 ax1.set_xlim(hist_xlim) 2501 else: 2502 ax1.set_xlim(0, np.ceil(df["emission_rate"].max() * 1.05)) 2503 2504 if hist_ylim is not None: 2505 ax1.set_ylim(hist_ylim) 2506 else: 2507 ax1.set_ylim(0, ax1.get_ylim()[1]) # 下限を0に設定 2508 2509 if show_scatter: 2510 # 右側: 散布図 2511 for spot_type in existing_types: 2512 mask = df["type"] == spot_type 2513 ax2.scatter( 2514 df[mask]["emission_rate"], 2515 df[mask]["delta_ch4"], 2516 alpha=0.6, 2517 label=spot_type, 2518 color=hotspot_colors[spot_type], 2519 ) 2520 2521 ax2.set_xlabel("Emission Rate (L min$^{-1}$)") 2522 ax2.set_ylabel("ΔCH$_4$ (ppm)") 2523 if scatter_xlim is not None: 2524 ax2.set_xlim(scatter_xlim) 2525 else: 2526 ax2.set_xlim(0, np.ceil(df["emission_rate"].max() * 1.05)) 2527 2528 if scatter_ylim is not None: 2529 ax2.set_ylim(scatter_ylim) 2530 else: 2531 ax2.set_ylim(0, np.ceil(df["delta_ch4"].max() * 1.05)) 2532 2533 # 凡例の表示 2534 if add_legend: 2535 for ax in axes: 2536 ax.legend( 2537 bbox_to_anchor=(0.5, -0.30), 2538 loc="upper center", 2539 ncol=len(existing_types), 2540 ) 2541 2542 plt.tight_layout() 2543 2544 # 図の保存 2545 if save_fig: 2546 if output_dir is None: 2547 raise ValueError( 2548 "save_fig=Trueの場合、output_dirを指定する必要があります。有効なディレクトリパスを指定してください。" 2549 ) 2550 os.makedirs(output_dir, exist_ok=True) 2551 output_path = os.path.join(output_dir, output_filename) 2552 plt.savefig(output_path, bbox_inches="tight", dpi=dpi) 2553 # 図の表示 2554 if show_fig: 2555 plt.show() 2556 else: 2557 plt.close(fig=fig) 2558 2559 if print_summary: 2560 # デバッグ用の出力 2561 print("\nビンごとの集計:") 2562 print(f"{'Range':>12} | {'bio':>8} | {'gas':>8} | {'total':>8}") 2563 print("-" * 50) 2564 2565 for i in range(len(bins) - 1): 2566 bin_start = bins[i] 2567 bin_end = bins[i + 1] 2568 2569 # 各タイプのカウントを計算 2570 counts_by_type: dict[HotspotType, int] = {"bio": 0, "gas": 0, "comb": 0} 2571 total = 0 2572 for spot_type in existing_types: 2573 mask = ( 2574 (df["type"] == spot_type) 2575 & (df["emission_rate"] >= bin_start) 2576 & (df["emission_rate"] < bin_end) 2577 ) 2578 count = len(df[mask]) 2579 counts_by_type[spot_type] = count 2580 total += count 2581 2582 # カウントが0の場合はスキップ 2583 if total > 0: 2584 range_str = f"{bin_start:5.1f}-{bin_end:<5.1f}" 2585 bio_count = counts_by_type.get("bio", 0) 2586 gas_count = counts_by_type.get("gas", 0) 2587 print( 2588 f"{range_str:>12} | {bio_count:8d} | {gas_count:8d} | {total:8d}" 2589 )
移動観測で得られた測定データを解析するクラス
312 def __init__( 313 self, 314 center_lat: float, 315 center_lon: float, 316 inputs: list[MSAInputConfig] | list[tuple[float, float, str | Path]], 317 num_sections: int = 4, 318 ch4_enhance_threshold: float = 0.1, 319 correlation_threshold: float = 0.7, 320 hotspot_area_meter: float = 50, 321 hotspot_params: HotspotParams | None = None, 322 window_minutes: float = 5, 323 column_mapping: dict[str, str] = { 324 "Time Stamp": "timestamp", 325 "CH4 (ppm)": "ch4_ppm", 326 "C2H6 (ppb)": "c2h6_ppb", 327 "H2O (ppm)": "h2o_ppm", 328 "Latitude": "latitude", 329 "Longitude": "longitude", 330 }, 331 na_values: list[str] = ["No Data", "nan"], 332 logger: Logger | None = None, 333 logging_debug: bool = False, 334 ): 335 """ 336 測定データ解析クラスの初期化 337 338 Parameters 339 ---------- 340 center_lat : float 341 中心緯度 342 center_lon : float 343 中心経度 344 inputs : list[MSAInputConfig] | list[tuple[float, float, str | Path]] 345 入力ファイルのリスト 346 num_sections : int 347 分割する区画数。デフォルトは4。 348 ch4_enhance_threshold : float 349 CH4増加の閾値(ppm)。デフォルトは0.1。 350 correlation_threshold : float 351 相関係数の閾値。デフォルトは0.7。 352 hotspot_area_meter : float 353 ホットスポットの検出に使用するエリアの半径(メートル)。デフォルトは50メートル。 354 hotspot_params : HotspotParams | None, optional 355 ホットスポット解析のパラメータ設定 356 window_minutes : float 357 移動窓の大きさ(分)。デフォルトは5分。 358 column_mapping : dict[str, str] 359 元のデータファイルのヘッダーを汎用的な単語に変換するための辞書型データ。 360 - timestamp,ch4_ppm,c2h6_ppm,h2o_ppm,latitude,longitudeをvalueに、それぞれに対応するカラム名をcolに指定してください。 361 - デフォルト: { 362 "Time Stamp": "timestamp", 363 "CH4 (ppm)": "ch4_ppm", 364 "C2H6 (ppb)": "c2h6_ppb", 365 "H2O (ppm)": "h2o_ppm", 366 "Latitude": "latitude", 367 "Longitude": "longitude", 368 } 369 na_values : list[str] 370 NaNと判定する値のパターン。 371 logger : Logger | None 372 使用するロガー。Noneの場合は新しいロガーを作成します。 373 logging_debug : bool 374 ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。 375 376 Returns 377 ---------- 378 None 379 初期化処理が完了したことを示します。 380 """ 381 # ロガー 382 log_level: int = INFO 383 if logging_debug: 384 log_level = DEBUG 385 self.logger: Logger = MobileSpatialAnalyzer.setup_logger(logger, log_level) 386 # プライベートなプロパティ 387 self._center_lat: float = center_lat 388 self._center_lon: float = center_lon 389 self._ch4_enhance_threshold: float = ch4_enhance_threshold 390 self._correlation_threshold: float = correlation_threshold 391 self._hotspot_area_meter: float = hotspot_area_meter 392 self._column_mapping: dict[str, str] = column_mapping 393 self._na_values: list[str] = na_values 394 self._hotspot_params = hotspot_params or HotspotParams() 395 self._num_sections: int = num_sections 396 # セクションの範囲 397 section_size: float = 360 / num_sections 398 self._section_size: float = section_size 399 self._sections = MobileSpatialAnalyzer._initialize_sections( 400 num_sections, section_size 401 ) 402 # window_sizeをデータポイント数に変換(分→秒→データポイント数) 403 self._window_size: int = MobileSpatialAnalyzer._calculate_window_size( 404 window_minutes 405 ) 406 # 入力設定の標準化 407 normalized_input_configs: list[MSAInputConfig] = ( 408 MobileSpatialAnalyzer._normalize_inputs(inputs) 409 ) 410 # 複数ファイルのデータを読み込み 411 self._data: dict[str, pd.DataFrame] = self._load_all_data( 412 normalized_input_configs 413 )
測定データ解析クラスの初期化
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メートル。
hotspot_params : HotspotParams | None, optional
ホットスポット解析のパラメータ設定
window_minutes : float
移動窓の大きさ(分)。デフォルトは5分。
column_mapping : dict[str, str]
元のデータファイルのヘッダーを汎用的な単語に変換するための辞書型データ。
- timestamp,ch4_ppm,c2h6_ppm,h2o_ppm,latitude,longitudeをvalueに、それぞれに対応するカラム名をcolに指定してください。
- デフォルト: {
"Time Stamp": "timestamp",
"CH4 (ppm)": "ch4_ppm",
"C2H6 (ppb)": "c2h6_ppb",
"H2O (ppm)": "h2o_ppm",
"Latitude": "latitude",
"Longitude": "longitude",
}
na_values : list[str]
NaNと判定する値のパターン。
logger : Logger | None
使用するロガー。Noneの場合は新しいロガーを作成します。
logging_debug : bool
ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。
Returns
None
初期化処理が完了したことを示します。
415 @property 416 def hotspot_params(self) -> HotspotParams: 417 """ホットスポット解析のパラメータ設定を取得""" 418 return self._hotspot_params
ホットスポット解析のパラメータ設定を取得
425 def analyze_delta_ch4_stats(self, hotspots: list[HotspotData]) -> None: 426 """ 427 各タイプのホットスポットについてΔCH4の統計情報を計算し、結果を表示します。 428 429 Parameters 430 ---------- 431 hotspots : list[HotspotData] 432 分析対象のホットスポットリスト 433 434 Returns 435 ---------- 436 None 437 統計情報の表示が完了したことを示します。 438 """ 439 # タイプごとにホットスポットを分類 440 hotspots_by_type: dict[HotspotType, list[HotspotData]] = { 441 "bio": [h for h in hotspots if h.type == "bio"], 442 "gas": [h for h in hotspots if h.type == "gas"], 443 "comb": [h for h in hotspots if h.type == "comb"], 444 } 445 446 # 統計情報を計算し、表示 447 for spot_type, spots in hotspots_by_type.items(): 448 if spots: 449 delta_ch4_values = [spot.delta_ch4 for spot in spots] 450 max_value = max(delta_ch4_values) 451 mean_value = sum(delta_ch4_values) / len(delta_ch4_values) 452 median_value = sorted(delta_ch4_values)[len(delta_ch4_values) // 2] 453 print(f"{spot_type}タイプのホットスポットの統計情報:") 454 print(f" 最大値: {max_value}") 455 print(f" 平均値: {mean_value}") 456 print(f" 中央値: {median_value}") 457 else: 458 print(f"{spot_type}タイプのホットスポットは存在しません。")
各タイプのホットスポットについてΔCH4の統計情報を計算し、結果を表示します。
Parameters
hotspots : list[HotspotData]
分析対象のホットスポットリスト
Returns
None
統計情報の表示が完了したことを示します。
460 def analyze_hotspots( 461 self, 462 duplicate_check_mode: Literal["none", "time_window", "time_all"] = "none", 463 min_time_threshold_seconds: float = 300, 464 max_time_threshold_hours: float = 12, 465 ) -> list[HotspotData]: 466 """ 467 ホットスポットを検出して分析します。 468 469 Parameters 470 ---------- 471 duplicate_check_mode : Literal["none", "time_window", "time_all"] 472 重複チェックのモード。 473 - "none": 重複チェックを行わない。 474 - "time_window": 指定された時間窓内の重複のみを除外。 475 - "time_all": すべての時間範囲で重複チェックを行う。 476 min_time_threshold_seconds : float 477 重複とみなす最小時間の閾値(秒)。デフォルトは300秒。 478 max_time_threshold_hours : float 479 重複チェックを一時的に無視する最大時間の閾値(時間)。デフォルトは12時間。 480 481 Returns 482 ---------- 483 list[HotspotData] 484 検出されたホットスポットのリスト。 485 """ 486 all_hotspots: list[HotspotData] = [] 487 params: HotspotParams = self._hotspot_params 488 489 # 各データソースに対して解析を実行 490 for _, df in self._data.items(): 491 # パラメータの計算 492 df = MobileSpatialAnalyzer._calculate_hotspots_parameters( 493 df=df, 494 window_size=self._window_size, 495 col_ch4_ppm=params.CH4_PPM, 496 col_c2h6_ppb=params.C2H6_PPB, 497 col_h2o_ppm=params.H2O_PPM, 498 ch4_ppm_delta_threshold=params.CH4_PPM_DELTA_THRESHOLD, 499 c2h6_ppb_delta_threshold=params.C2H6_PPB_DELTA_THRESHOLD, 500 h2o_ppm_threshold=params.H2O_PPM_THRESHOLD, 501 rolling_method=params.ROLLING_METHOD, 502 quantile_value=params.QUANTILE_VALUE, 503 ) 504 505 # ホットスポットの検出 506 hotspots: list[HotspotData] = self._detect_hotspots( 507 df=df, 508 ch4_enhance_threshold=self._ch4_enhance_threshold, 509 ) 510 all_hotspots.extend(hotspots) 511 512 # 重複チェックモードに応じて処理 513 if duplicate_check_mode != "none": 514 unique_hotspots = MobileSpatialAnalyzer.remove_hotspots_duplicates( 515 all_hotspots, 516 check_time_all=(duplicate_check_mode == "time_all"), 517 min_time_threshold_seconds=min_time_threshold_seconds, 518 max_time_threshold_hours=max_time_threshold_hours, 519 hotspot_area_meter=self._hotspot_area_meter, 520 ) 521 self.logger.info( 522 f"重複除外: {len(all_hotspots)} → {len(unique_hotspots)} ホットスポット" 523 ) 524 return unique_hotspots 525 526 return all_hotspots
ホットスポットを検出して分析します。
Parameters
duplicate_check_mode : Literal["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]
検出されたホットスポットのリスト。
528 def calculate_measurement_stats( 529 self, 530 print_individual_stats: bool = True, 531 print_total_stats: bool = True, 532 col_latitude: str = "latitude", 533 col_longitude: str = "longitude", 534 ) -> tuple[float, timedelta]: 535 """ 536 各ファイルの測定時間と走行距離を計算し、合計を返します。 537 538 Parameters 539 ---------- 540 print_individual_stats : bool 541 個別ファイルの統計を表示するかどうか。デフォルトはTrue。 542 print_total_stats : bool 543 合計統計を表示するかどうか。デフォルトはTrue。 544 col_latitude : str 545 緯度情報が格納されているカラム名。デフォルトは"latitude"。 546 col_longitude : str 547 経度情報が格納されているカラム名。デフォルトは"longitude"。 548 549 Returns 550 ---------- 551 tuple[float, timedelta] 552 総距離(km)と総時間のタプル 553 """ 554 total_distance: float = 0.0 555 total_time: timedelta = timedelta() 556 individual_stats: list[dict] = [] # 個別の統計情報を保存するリスト 557 558 # プログレスバーを表示しながら計算 559 for source_name, df in tqdm( 560 self._data.items(), desc="Calculating", unit="file" 561 ): 562 # 時間の計算 563 time_spent = df.index[-1] - df.index[0] 564 565 # 距離の計算 566 distance_km = 0.0 567 for i in range(len(df) - 1): 568 lat1, lon1 = df.iloc[i][[col_latitude, col_longitude]] 569 lat2, lon2 = df.iloc[i + 1][[col_latitude, col_longitude]] 570 distance_km += ( 571 MobileSpatialAnalyzer._calculate_distance( 572 lat1=lat1, lon1=lon1, lat2=lat2, lon2=lon2 573 ) 574 / 1000 575 ) 576 577 # 合計に加算 578 total_distance += distance_km 579 total_time += time_spent 580 581 # 統計情報を保存 582 if print_individual_stats: 583 average_speed = distance_km / (time_spent.total_seconds() / 3600) 584 individual_stats.append( 585 { 586 "source": source_name, 587 "distance": distance_km, 588 "time": time_spent, 589 "speed": average_speed, 590 } 591 ) 592 593 # 計算完了後に統計情報を表示 594 if print_individual_stats: 595 self.logger.info("=== Individual Stats ===") 596 for stat in individual_stats: 597 print(f"File : {stat['source']}") 598 print(f" Distance : {stat['distance']:.2f} km") 599 print(f" Time : {stat['time']}") 600 print(f" Avg. Speed : {stat['speed']:.1f} km/h\n") 601 602 # 合計を表示 603 if print_total_stats: 604 average_speed_total: float = total_distance / ( 605 total_time.total_seconds() / 3600 606 ) 607 self.logger.info("=== Total Stats ===") 608 print(f" Distance : {total_distance:.2f} km") 609 print(f" Time : {total_time}") 610 print(f" Avg. Speed : {average_speed_total:.1f} km/h\n") 611 612 return total_distance, total_time
各ファイルの測定時間と走行距離を計算し、合計を返します。
Parameters
print_individual_stats : bool
個別ファイルの統計を表示するかどうか。デフォルトはTrue。
print_total_stats : bool
合計統計を表示するかどうか。デフォルトはTrue。
col_latitude : str
緯度情報が格納されているカラム名。デフォルトは"latitude"。
col_longitude : str
経度情報が格納されているカラム名。デフォルトは"longitude"。
Returns
tuple[float, timedelta]
総距離(km)と総時間のタプル
614 def create_hotspots_map( 615 self, 616 hotspots: list[HotspotData], 617 output_dir: str | Path | None = None, 618 output_filename: str = "hotspots_map.html", 619 center_marker_color: str = "green", 620 center_marker_label: str = "Center", 621 plot_center_marker: bool = True, 622 radius_meters: float = 3000, 623 save_fig: bool = True, 624 ) -> None: 625 """ 626 ホットスポットの分布を地図上にプロットして保存 627 628 Parameters 629 ---------- 630 hotspots : list[HotspotData] 631 プロットするホットスポットのリスト 632 output_dir : str | Path 633 保存先のディレクトリパス 634 output_filename : str 635 保存するファイル名。デフォルトは"hotspots_map"。 636 center_marker_color : str 637 中心を示すマーカーのラベルカラー。デフォルトは"green"。 638 center_marker_label : str 639 中心を示すマーカーのラベルテキスト。デフォルトは"Center"。 640 plot_center_marker : bool 641 中心を示すマーカーの有無。デフォルトはTrue。 642 radius_meters : float 643 区画分けを示す線の長さ。デフォルトは3000。 644 save_fig : bool 645 図の保存を許可するフラグ。デフォルトはTrue。 646 """ 647 # 地図の作成 648 m = folium.Map( 649 location=[self._center_lat, self._center_lon], 650 zoom_start=15, 651 tiles="OpenStreetMap", 652 ) 653 654 # ホットスポットの種類ごとに異なる色でプロット 655 for spot in hotspots: 656 # NaN値チェックを追加 657 if math.isnan(spot.avg_lat) or math.isnan(spot.avg_lon): 658 continue 659 660 # default type 661 color = "black" 662 # タイプに応じて色を設定 663 if spot.type == "comb": 664 color = "green" 665 elif spot.type == "gas": 666 color = "red" 667 elif spot.type == "bio": 668 color = "blue" 669 670 # CSSのgrid layoutを使用してHTMLタグを含むテキストをフォーマット 671 popup_html = f""" 672 <div style='font-family: Arial; font-size: 12px; display: grid; grid-template-columns: auto auto auto; gap: 5px;'> 673 <b>Date</b> <span>:</span> <span>{spot.source}</span> 674 <b>Lat</b> <span>:</span> <span>{spot.avg_lat:.3f}</span> 675 <b>Lon</b> <span>:</span> <span>{spot.avg_lon:.3f}</span> 676 <b>ΔCH<sub>4</sub></b> <span>:</span> <span>{spot.delta_ch4:.3f}</span> 677 <b>ΔC<sub>2</sub>H<sub>6</sub></b> <span>:</span> <span>{spot.delta_c2h6:.3f}</span> 678 <b>Ratio</b> <span>:</span> <span>{spot.ratio:.3f}</span> 679 <b>Type</b> <span>:</span> <span>{spot.type}</span> 680 <b>Section</b> <span>:</span> <span>{spot.section}</span> 681 </div> 682 """ 683 684 # ポップアップのサイズを指定 685 popup = folium.Popup( 686 folium.Html(popup_html, script=True), 687 max_width=200, # 最大幅(ピクセル) 688 ) 689 690 folium.CircleMarker( 691 location=[spot.avg_lat, spot.avg_lon], 692 radius=8, 693 color=color, 694 fill=True, 695 popup=popup, 696 ).add_to(m) 697 698 # 中心点のマーカー 699 if plot_center_marker: 700 folium.Marker( 701 [self._center_lat, self._center_lon], 702 popup=center_marker_label, 703 icon=folium.Icon(color=center_marker_color, icon="info-sign"), 704 ).add_to(m) 705 706 # 区画の境界線を描画 707 for section in range(self._num_sections): 708 start_angle = math.radians(-180 + section * self._section_size) 709 710 R = self.EARTH_RADIUS_METERS 711 712 # 境界線の座標を計算 713 lat1 = self._center_lat 714 lon1 = self._center_lon 715 lat2 = math.degrees( 716 math.asin( 717 math.sin(math.radians(lat1)) * math.cos(radius_meters / R) 718 + math.cos(math.radians(lat1)) 719 * math.sin(radius_meters / R) 720 * math.cos(start_angle) 721 ) 722 ) 723 lon2 = self._center_lon + math.degrees( 724 math.atan2( 725 math.sin(start_angle) 726 * math.sin(radius_meters / R) 727 * math.cos(math.radians(lat1)), 728 math.cos(radius_meters / R) 729 - math.sin(math.radians(lat1)) * math.sin(math.radians(lat2)), 730 ) 731 ) 732 733 # 境界線を描画 734 folium.PolyLine( 735 locations=[[lat1, lon1], [lat2, lon2]], 736 color="black", 737 weight=1, 738 opacity=0.5, 739 ).add_to(m) 740 741 # 地図を保存 742 if save_fig and output_dir is None: 743 raise ValueError( 744 "save_fig=Trueの場合、output_dirを指定する必要があります。有効なディレクトリパスを指定してください。" 745 ) 746 output_path: str = os.path.join(output_dir, output_filename) 747 m.save(str(output_path)) 748 self.logger.info(f"地図を保存しました: {output_path}")
ホットスポットの分布を地図上にプロットして保存
Parameters
hotspots : list[HotspotData]
プロットするホットスポットのリスト
output_dir : str | Path
保存先のディレクトリパス
output_filename : str
保存するファイル名。デフォルトは"hotspots_map"。
center_marker_color : str
中心を示すマーカーのラベルカラー。デフォルトは"green"。
center_marker_label : str
中心を示すマーカーのラベルテキスト。デフォルトは"Center"。
plot_center_marker : bool
中心を示すマーカーの有無。デフォルトはTrue。
radius_meters : float
区画分けを示す線の長さ。デフォルトは3000。
save_fig : bool
図の保存を許可するフラグ。デフォルトはTrue。
750 def export_hotspots_to_csv( 751 self, 752 hotspots: list[HotspotData], 753 output_dir: str | Path | None = None, 754 output_filename: str = "hotspots.csv", 755 ) -> None: 756 """ 757 ホットスポットの情報をCSVファイルに出力します。 758 759 Parameters 760 ---------- 761 hotspots : list[HotspotData] 762 出力するホットスポットのリスト 763 output_dir : str | Path | None 764 出力先ディレクトリ 765 output_filename : str 766 出力ファイル名 767 """ 768 # 日時の昇順でソート 769 sorted_hotspots = sorted(hotspots, key=lambda x: x.source) 770 771 # 出力用のデータを作成 772 records = [] 773 for spot in sorted_hotspots: 774 record = { 775 "source": spot.source, 776 "type": spot.type, 777 "delta_ch4": spot.delta_ch4, 778 "delta_c2h6": spot.delta_c2h6, 779 "ratio": spot.ratio, 780 "correlation": spot.correlation, 781 "angle": spot.angle, 782 "section": spot.section, 783 "latitude": spot.avg_lat, 784 "longitude": spot.avg_lon, 785 } 786 records.append(record) 787 788 # DataFrameに変換してCSVに出力 789 if output_dir is None: 790 raise ValueError( 791 "output_dirが指定されていません。有効なディレクトリパスを指定してください。" 792 ) 793 os.makedirs(output_dir, exist_ok=True) 794 output_path: str = os.path.join(output_dir, output_filename) 795 df: pd.DataFrame = pd.DataFrame(records) 796 df.to_csv(output_path, index=False) 797 self.logger.info( 798 f"ホットスポット情報をCSVファイルに出力しました: {output_path}" 799 )
ホットスポットの情報をCSVファイルに出力します。
Parameters
hotspots : list[HotspotData]
出力するホットスポットのリスト
output_dir : str | Path | None
出力先ディレクトリ
output_filename : str
出力ファイル名
801 @staticmethod 802 def extract_source_name_from_path(path: str | Path) -> str: 803 """ 804 ファイルパスからソース名(拡張子なしのファイル名)を抽出します。 805 806 Parameters 807 ---------- 808 path : str | Path 809 ソース名を抽出するファイルパス 810 例: "/path/to/Pico100121_241017_092120+.txt" 811 812 Returns 813 ---------- 814 str 815 抽出されたソース名 816 例: "Pico100121_241017_092120+" 817 818 Examples: 819 ---------- 820 >>> path = "/path/to/data/Pico100121_241017_092120+.txt" 821 >>> MobileSpatialAnalyzer.extract_source_from_path(path) 822 'Pico100121_241017_092120+' 823 """ 824 # Pathオブジェクトに変換 825 path_obj: Path = Path(path) 826 # stem属性で拡張子なしのファイル名を取得 827 source_name: str = path_obj.stem 828 return source_name
ファイルパスからソース名(拡張子なしのファイル名)を抽出します。
Parameters
path : str | Path
ソース名を抽出するファイルパス
例: "/path/to/Pico100121_241017_092120+.txt"
Returns
str
抽出されたソース名
例: "Pico100121_241017_092120+"
Examples:
>>> path = "/path/to/data/Pico100121_241017_092120+.txt"
>>> MobileSpatialAnalyzer.extract_source_from_path(path)
'Pico100121_241017_092120+'
830 def get_preprocessed_data( 831 self, 832 ) -> pd.DataFrame: 833 """ 834 データ前処理を行い、CH4とC2H6の相関解析に必要な形式に整えます。 835 コンストラクタで読み込んだすべてのデータを前処理し、結合したDataFrameを返します。 836 837 Returns 838 ---------- 839 pd.DataFrame 840 前処理済みの結合されたDataFrame 841 """ 842 processed_dfs: list[pd.DataFrame] = [] 843 params: HotspotParams = self._hotspot_params 844 845 # 各データソースに対して解析を実行 846 for source_name, df in self._data.items(): 847 # パラメータの計算 848 processed_df = MobileSpatialAnalyzer._calculate_hotspots_parameters( 849 df=df, 850 window_size=self._window_size, 851 col_ch4_ppm=params.CH4_PPM, 852 col_c2h6_ppb=params.C2H6_PPB, 853 col_h2o_ppm=params.H2O_PPM, 854 ch4_ppm_delta_threshold=params.CH4_PPM_DELTA_THRESHOLD, 855 c2h6_ppb_delta_threshold=params.C2H6_PPB_DELTA_THRESHOLD, 856 h2o_ppm_threshold=params.H2O_PPM_THRESHOLD, 857 rolling_method=params.ROLLING_METHOD, 858 quantile_value=params.QUANTILE_VALUE, 859 ) 860 # ソース名を列として追加 861 processed_df["source"] = source_name 862 processed_dfs.append(processed_df) 863 864 # すべてのDataFrameを結合 865 if not processed_dfs: 866 raise ValueError("処理対象のデータが存在しません。") 867 868 combined_df: pd.DataFrame = pd.concat(processed_dfs, axis=0) 869 return combined_df
データ前処理を行い、CH4とC2H6の相関解析に必要な形式に整えます。 コンストラクタで読み込んだすべてのデータを前処理し、結合したDataFrameを返します。
Returns
pd.DataFrame
前処理済みの結合されたDataFrame
871 def get_section_size(self) -> float: 872 """ 873 セクションのサイズを取得するメソッド。 874 このメソッドは、解析対象のデータを区画に分割する際の 875 各区画の角度範囲を示すサイズを返します。 876 877 Returns 878 ---------- 879 float 880 1セクションのサイズ(度単位) 881 """ 882 return self._section_size
セクションのサイズを取得するメソッド。 このメソッドは、解析対象のデータを区画に分割する際の 各区画の角度範囲を示すサイズを返します。
Returns
float
1セクションのサイズ(度単位)
884 def get_source_names(self, print_all: bool = False) -> list[str]: 885 """ 886 データソースの名前を取得します。 887 888 Parameters 889 ---------- 890 print_all : bool, optional 891 すべてのデータソース名を表示するかどうかを指定します。デフォルトはFalseです。 892 893 Returns 894 ---------- 895 list[str] 896 データソース名のリスト 897 898 Raises 899 ---------- 900 ValueError 901 データが読み込まれていない場合に発生します。 902 """ 903 dfs_dict: dict[str, pd.DataFrame] = self._data 904 # データソースの選択 905 if not dfs_dict: 906 raise ValueError("データが読み込まれていません。") 907 source_name_list: list[str] = list(dfs_dict.keys()) 908 if print_all: 909 print(source_name_list) 910 return source_name_list
データソースの名前を取得します。
Parameters
print_all : bool, optional すべてのデータソース名を表示するかどうかを指定します。デフォルトはFalseです。
Returns
list[str] データソース名のリスト
Raises
ValueError データが読み込まれていない場合に発生します。
912 def plot_ch4_delta_histogram( 913 self, 914 hotspots: list[HotspotData], 915 output_dir: str | Path | None, 916 output_filename: str = "ch4_delta_histogram.png", 917 dpi: int = 200, 918 figsize: tuple[int, int] = (8, 6), 919 fontsize: float = 20, 920 hotspot_colors: dict[HotspotType, str] = { 921 "bio": "blue", 922 "gas": "red", 923 "comb": "green", 924 }, 925 xlabel: str = "Δ$\\mathregular{CH_{4}}$ (ppm)", 926 ylabel: str = "Frequency", 927 xlim: tuple[float, float] | None = None, 928 ylim: tuple[float, float] | None = None, 929 save_fig: bool = True, 930 show_fig: bool = True, 931 yscale_log: bool = True, 932 print_bins_analysis: bool = False, 933 ) -> None: 934 """ 935 CH4の増加量(ΔCH4)の積み上げヒストグラムをプロットします。 936 937 Parameters 938 ---------- 939 hotspots : list[HotspotData] 940 プロットするホットスポットのリスト 941 output_dir : str | Path | None 942 保存先のディレクトリパス 943 output_filename : str 944 保存するファイル名。デフォルトは"ch4_delta_histogram.png"。 945 dpi : int 946 解像度。デフォルトは200。 947 figsize : tuple[int, int] 948 図のサイズ。デフォルトは(8, 6)。 949 fontsize : float 950 フォントサイズ。デフォルトは20。 951 hotspot_colors : dict[HotspotType, str] 952 ホットスポットの色を定義する辞書。 953 xlabel : str 954 x軸のラベル。 955 ylabel : str 956 y軸のラベル。 957 xlim : tuple[float, float] | None 958 x軸の範囲。Noneの場合は自動設定。 959 ylim : tuple[float, float] | None 960 y軸の範囲。Noneの場合は自動設定。 961 save_fig : bool 962 図の保存を許可するフラグ。デフォルトはTrue。 963 show_fig : bool 964 図の表示を許可するフラグ。デフォルトはTrue。 965 yscale_log : bool 966 y軸をlogにするかどうか。デフォルトはTrue。 967 print_bins_analysis : bool 968 ビンごとの内訳を表示するオプション。 969 """ 970 plt.rcParams["font.size"] = fontsize 971 fig = plt.figure(figsize=figsize, dpi=dpi) 972 973 # ホットスポットからデータを抽出 974 all_ch4_deltas = [] 975 all_types = [] 976 for spot in hotspots: 977 all_ch4_deltas.append(spot.delta_ch4) 978 all_types.append(spot.type) 979 980 # データをNumPy配列に変換 981 all_ch4_deltas = np.array(all_ch4_deltas) 982 all_types = np.array(all_types) 983 984 # 0.1刻みのビンを作成 985 if xlim is not None: 986 bins = np.arange(xlim[0], xlim[1] + 0.1, 0.1) 987 else: 988 max_val = np.ceil(np.max(all_ch4_deltas) * 10) / 10 989 bins = np.arange(0, max_val + 0.1, 0.1) 990 991 # タイプごとのヒストグラムデータを計算 992 hist_data = {} 993 # HotspotTypeのリテラル値を使用してイテレーション 994 for type_name in get_args(HotspotType): # typing.get_argsをインポート 995 mask = all_types == type_name 996 if np.any(mask): 997 counts, _ = np.histogram(all_ch4_deltas[mask], bins=bins) 998 hist_data[type_name] = counts 999 1000 # ビンごとの内訳を表示 1001 if print_bins_analysis: 1002 self.logger.info("各ビンの内訳:") 1003 print(f"{'Bin Range':15} {'bio':>8} {'gas':>8} {'comb':>8} {'Total':>8}") 1004 print("-" * 50) 1005 1006 for i in range(len(bins) - 1): 1007 bin_start = bins[i] 1008 bin_end = bins[i + 1] 1009 bio_count = hist_data.get("bio", np.zeros(len(bins) - 1))[i] 1010 gas_count = hist_data.get("gas", np.zeros(len(bins) - 1))[i] 1011 comb_count = hist_data.get("comb", np.zeros(len(bins) - 1))[i] 1012 total = bio_count + gas_count + comb_count 1013 1014 if total > 0: # 合計が0のビンは表示しない 1015 print( 1016 f"{bin_start:4.1f}-{bin_end:<8.1f}" 1017 f"{int(bio_count):8d}" 1018 f"{int(gas_count):8d}" 1019 f"{int(comb_count):8d}" 1020 f"{int(total):8d}" 1021 ) 1022 1023 # 積み上げヒストグラムを作成 1024 bottom = np.zeros_like(hist_data.get("bio", np.zeros(len(bins) - 1))) 1025 1026 # HotspotTypeのリテラル値を使用してイテレーション 1027 for type_name in get_args(HotspotType): 1028 if type_name in hist_data: 1029 plt.bar( 1030 bins[:-1], 1031 hist_data[type_name], 1032 width=np.diff(bins)[0], 1033 bottom=bottom, 1034 color=hotspot_colors[type_name], 1035 label=type_name, 1036 alpha=0.6, 1037 align="edge", 1038 ) 1039 bottom += hist_data[type_name] 1040 1041 if yscale_log: 1042 plt.yscale("log") 1043 plt.xlabel(xlabel) 1044 plt.ylabel(ylabel) 1045 plt.legend() 1046 plt.grid(True, which="both", ls="-", alpha=0.2) 1047 1048 # 軸の範囲を設定 1049 if xlim is not None: 1050 plt.xlim(xlim) 1051 if ylim is not None: 1052 plt.ylim(ylim) 1053 1054 # グラフの保存または表示 1055 if save_fig: 1056 if output_dir is None: 1057 raise ValueError( 1058 "save_fig=Trueの場合、output_dirを指定する必要があります。有効なディレクトリパスを指定してください。" 1059 ) 1060 os.makedirs(output_dir, exist_ok=True) 1061 output_path: str = os.path.join(output_dir, output_filename) 1062 plt.savefig(output_path, bbox_inches="tight") 1063 self.logger.info(f"ヒストグラムを保存しました: {output_path}") 1064 if show_fig: 1065 plt.show() 1066 else: 1067 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。
hotspot_colors : dict[HotspotType, str]
ホットスポットの色を定義する辞書。
xlabel : str
x軸のラベル。
ylabel : str
y軸のラベル。
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
ビンごとの内訳を表示するオプション。
1069 def plot_mapbox( 1070 self, 1071 df: pd.DataFrame, 1072 col_conc: str, 1073 mapbox_access_token: str, 1074 sort_conc_column: bool = True, 1075 output_dir: str | Path | None = None, 1076 output_filename: str = "mapbox_plot.html", 1077 col_lat: str = "latitude", 1078 col_lon: str = "longitude", 1079 colorscale: str = "Jet", 1080 center_lat: float | None = None, 1081 center_lon: float | None = None, 1082 zoom: float = 12, 1083 width: int = 700, 1084 height: int = 700, 1085 tick_font_family: str = "Arial", 1086 title_font_family: str = "Arial", 1087 tick_font_size: int = 12, 1088 title_font_size: int = 14, 1089 marker_size: int = 4, 1090 colorbar_title: str | None = None, 1091 value_range: tuple[float, float] | None = None, 1092 save_fig: bool = True, 1093 show_fig: bool = True, 1094 ) -> None: 1095 """ 1096 Plotlyを使用してMapbox上にデータをプロットします。 1097 1098 Parameters 1099 ---------- 1100 df : pd.DataFrame 1101 プロットするデータを含むDataFrame 1102 col_conc : str 1103 カラーマッピングに使用する列名 1104 mapbox_access_token : str 1105 Mapboxのアクセストークン 1106 sort_conc_column : bool 1107 value_columnをソートするか否か。デフォルトはTrue。 1108 output_dir : str | Path | None 1109 出力ディレクトリのパス 1110 output_filename : str 1111 出力ファイル名。デフォルトは"mapbox_plot.html" 1112 col_lat : str 1113 緯度の列名。デフォルトは"latitude" 1114 col_lon : str 1115 経度の列名。デフォルトは"longitude" 1116 colorscale : str 1117 使用するカラースケール。デフォルトは"Jet" 1118 center_lat : float | None 1119 中心緯度。デフォルトはNoneで、self._center_latを使用 1120 center_lon : float | None 1121 中心経度。デフォルトはNoneで、self._center_lonを使用 1122 zoom : float 1123 マップの初期ズームレベル。デフォルトは12 1124 width : int 1125 プロットの幅(ピクセル)。デフォルトは700 1126 height : int 1127 プロットの高さ(ピクセル)。デフォルトは700 1128 tick_font_family : str 1129 カラーバーの目盛りフォントファミリー。デフォルトは"Arial" 1130 title_font_family : str 1131 カラーバーのラベルフォントファミリー。デフォルトは"Arial" 1132 tick_font_size : int 1133 カラーバーの目盛りフォントサイズ。デフォルトは12 1134 title_font_size : int 1135 カラーバーのラベルフォントサイズ。デフォルトは14 1136 marker_size : int 1137 マーカーのサイズ。デフォルトは4 1138 colorbar_title : str | None 1139 カラーバーのラベル 1140 value_range : tuple[float, float] | None 1141 カラーマッピングの範囲。デフォルトはNoneで、データの最小値と最大値を使用 1142 save_fig : bool 1143 図を保存するかどうか。デフォルトはTrue 1144 show_fig : bool 1145 図を表示するかどうか。デフォルトはTrue 1146 """ 1147 df_mapping: pd.DataFrame = df.copy().dropna(subset=[col_conc]) 1148 if sort_conc_column: 1149 df_mapping = df_mapping.sort_values(col_conc) 1150 # 中心座標の設定 1151 center_lat = center_lat if center_lat is not None else self._center_lat 1152 center_lon = center_lon if center_lon is not None else self._center_lon 1153 1154 # カラーマッピングの範囲を設定 1155 cmin, cmax = 0, 0 1156 if value_range is None: 1157 cmin = df_mapping[col_conc].min() 1158 cmax = df_mapping[col_conc].max() 1159 else: 1160 cmin, cmax = value_range 1161 1162 # カラーバーのタイトルを設定 1163 title_text = colorbar_title if colorbar_title is not None else col_conc 1164 1165 # Scattermapboxのデータを作成 1166 scatter_data = go.Scattermapbox( 1167 lat=df_mapping[col_lat], 1168 lon=df_mapping[col_lon], 1169 text=df_mapping[col_conc].astype(str), 1170 hoverinfo="text", 1171 mode="markers", 1172 marker=dict( 1173 color=df_mapping[col_conc], 1174 size=marker_size, 1175 reversescale=False, 1176 autocolorscale=False, 1177 colorscale=colorscale, 1178 cmin=cmin, 1179 cmax=cmax, 1180 colorbar=dict( 1181 tickformat="3.2f", 1182 outlinecolor="black", 1183 outlinewidth=1.5, 1184 ticks="outside", 1185 ticklen=7, 1186 tickwidth=1.5, 1187 tickcolor="black", 1188 tickfont=dict( 1189 family=tick_font_family, color="black", size=tick_font_size 1190 ), 1191 title=dict( 1192 text=title_text, side="top" 1193 ), # カラーバーのタイトルを設定 1194 titlefont=dict( 1195 family=title_font_family, 1196 color="black", 1197 size=title_font_size, 1198 ), 1199 ), 1200 ), 1201 ) 1202 1203 # レイアウトの設定 1204 layout = go.Layout( 1205 width=width, 1206 height=height, 1207 showlegend=False, 1208 mapbox=dict( 1209 accesstoken=mapbox_access_token, 1210 center=dict(lat=center_lat, lon=center_lon), 1211 zoom=zoom, 1212 ), 1213 ) 1214 1215 # 図の作成 1216 fig = go.Figure(data=[scatter_data], layout=layout) 1217 1218 # 図の保存 1219 if save_fig: 1220 # 保存時の出力ディレクトリチェック 1221 if output_dir is None: 1222 raise ValueError( 1223 "save_fig=Trueの場合、output_dirを指定する必要があります。" 1224 ) 1225 os.makedirs(output_dir, exist_ok=True) 1226 output_path = os.path.join(output_dir, output_filename) 1227 pyo.plot(fig, filename=output_path, auto_open=False) 1228 self.logger.info(f"Mapboxプロットを保存しました: {output_path}") 1229 # 図の表示 1230 if show_fig: 1231 pyo.iplot(fig)
Plotlyを使用してMapbox上にデータをプロットします。
Parameters
df : pd.DataFrame
プロットするデータを含むDataFrame
col_conc : str
カラーマッピングに使用する列名
mapbox_access_token : str
Mapboxのアクセストークン
sort_conc_column : bool
value_columnをソートするか否か。デフォルトはTrue。
output_dir : str | Path | None
出力ディレクトリのパス
output_filename : str
出力ファイル名。デフォルトは"mapbox_plot.html"
col_lat : str
緯度の列名。デフォルトは"latitude"
col_lon : 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
1233 def plot_scatter_c2c1( 1234 self, 1235 hotspots: list[HotspotData], 1236 output_dir: str | Path | None = None, 1237 output_filename: str = "scatter_c2c1.png", 1238 dpi: int = 200, 1239 figsize: tuple[int, int] = (4, 4), 1240 hotspot_colors: dict[HotspotType, str] = { 1241 "bio": "blue", 1242 "gas": "red", 1243 "comb": "green", 1244 }, 1245 hotspot_labels: dict[HotspotType, str] = { 1246 "bio": "bio", 1247 "gas": "gas", 1248 "comb": "comb", 1249 }, 1250 fontsize: float = 12, 1251 xlim: tuple[float, float] = (0, 2.0), 1252 ylim: tuple[float, float] = (0, 50), 1253 xlabel: str = "Δ$\\mathregular{CH_{4}}$ (ppm)", 1254 ylabel: str = "Δ$\\mathregular{C_{2}H_{6}}$ (ppb)", 1255 add_legend: bool = True, 1256 save_fig: bool = True, 1257 show_fig: bool = True, 1258 ratio_labels: dict[float, tuple[float, float, str]] | None = None, 1259 ) -> None: 1260 """ 1261 検出されたホットスポットのΔC2H6とΔCH4の散布図をプロットします。 1262 1263 Parameters 1264 ---------- 1265 hotspots : list[HotspotData] 1266 プロットするホットスポットのリスト 1267 output_dir : str | Path | None 1268 保存先のディレクトリパス 1269 output_filename : str 1270 保存するファイル名。デフォルトは"scatter_c2c1.png"。 1271 dpi : int 1272 解像度。デフォルトは200。 1273 figsize : tuple[int, int] 1274 図のサイズ。デフォルトは(4, 4)。 1275 fontsize : float 1276 フォントサイズ。デフォルトは12。 1277 hotspot_colors : dict[HotspotType, str] 1278 ホットスポットの色を定義する辞書。 1279 hotspot_labels : dict[HotspotType, str] 1280 ホットスポットのラベルを定義する辞書。 1281 save_fig : bool 1282 図の保存を許可するフラグ。デフォルトはTrue。 1283 show_fig : bool 1284 図の表示を許可するフラグ。デフォルトはTrue。 1285 ratio_labels : dict[float, tuple[float, float, str]] | None 1286 比率線とラベルの設定。 1287 キーは比率値、値は (x位置, y位置, ラベルテキスト) のタプル。 1288 Noneの場合はデフォルト設定を使用。デフォルト値: 1289 { 1290 0.001: (1.25, 2, "0.001"), 1291 0.005: (1.25, 8, "0.005"), 1292 0.010: (1.25, 15, "0.01"), 1293 0.020: (1.25, 30, "0.02"), 1294 0.030: (1.0, 40, "0.03"), 1295 0.076: (0.20, 42, "0.076 (Osaka)") 1296 } 1297 xlim : tuple[float, float] 1298 x軸の範囲を指定します。デフォルトは(0, 2.0)です。 1299 ylim : tuple[float, float] 1300 y軸の範囲を指定します。デフォルトは(0, 50)です。 1301 xlabel : str 1302 x軸のラベルを指定します。デフォルトは"Δ$\\mathregular{CH_{4}}$ (ppm)"です。 1303 ylabel : str 1304 y軸のラベルを指定します。デフォルトは"Δ$\\mathregular{C_{2}H_{6}}$ (ppb)"です。 1305 add_legend : bool 1306 凡例を追加するかどうか。 1307 """ 1308 plt.rcParams["font.size"] = fontsize 1309 fig = plt.figure(figsize=figsize, dpi=dpi) 1310 1311 # タイプごとのデータを収集 1312 type_data: dict[HotspotType, list[tuple[float, float]]] = { 1313 "bio": [], 1314 "gas": [], 1315 "comb": [], 1316 } 1317 for spot in hotspots: 1318 type_data[spot.type].append((spot.delta_ch4, spot.delta_c2h6)) 1319 1320 # タイプごとにプロット(データが存在する場合のみ) 1321 for spot_type, data in type_data.items(): 1322 if data: # データが存在する場合のみプロット 1323 ch4_values, c2h6_values = zip(*data) 1324 plt.plot( 1325 ch4_values, 1326 c2h6_values, 1327 "o", 1328 c=hotspot_colors[spot_type], 1329 alpha=0.5, 1330 ms=2, 1331 label=hotspot_labels[spot_type], 1332 ) 1333 1334 # デフォルトの比率とラベル設定 1335 default_ratio_labels = { 1336 0.001: (1.25, 2, "0.001"), 1337 0.005: (1.25, 8, "0.005"), 1338 0.010: (1.25, 15, "0.01"), 1339 0.020: (1.25, 30, "0.02"), 1340 0.030: (1.0, 40, "0.03"), 1341 0.076: (0.20, 42, "0.076 (Osaka)"), 1342 } 1343 1344 ratio_labels = ratio_labels or default_ratio_labels 1345 1346 # プロット後、軸の設定前に比率の線を追加 1347 x = np.array([0, 5]) 1348 base_ch4 = 0.0 1349 base = 0.0 1350 1351 # 各比率に対して線を引く 1352 for ratio, (x_pos, y_pos, label) in ratio_labels.items(): 1353 y = (x - base_ch4) * 1000 * ratio + base 1354 plt.plot(x, y, "-", c="black", alpha=0.5) 1355 plt.text(x_pos, y_pos, label) 1356 1357 plt.xlim(xlim) 1358 plt.ylim(ylim) 1359 plt.xlabel(xlabel) 1360 plt.ylabel(ylabel) 1361 if add_legend: 1362 plt.legend() 1363 1364 # グラフの保存または表示 1365 if save_fig: 1366 if output_dir is None: 1367 raise ValueError( 1368 "save_fig=Trueの場合、output_dirを指定する必要があります。有効なディレクトリパスを指定してください。" 1369 ) 1370 output_path: str = os.path.join(output_dir, output_filename) 1371 plt.savefig(output_path, bbox_inches="tight") 1372 self.logger.info(f"散布図を保存しました: {output_path}") 1373 if show_fig: 1374 plt.show() 1375 else: 1376 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。
hotspot_colors : dict[HotspotType, str]
ホットスポットの色を定義する辞書。
hotspot_labels : dict[HotspotType, str]
ホットスポットのラベルを定義する辞書。
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)")
}
xlim : tuple[float, float]
x軸の範囲を指定します。デフォルトは(0, 2.0)です。
ylim : tuple[float, float]
y軸の範囲を指定します。デフォルトは(0, 50)です。
xlabel : str
x軸のラベルを指定します。デフォルトは"Δ$\mathregular{CH_{4}}$ (ppm)"です。
ylabel : str
y軸のラベルを指定します。デフォルトは"Δ$\mathregular{C_{2}H_{6}}$ (ppb)"です。
add_legend : bool
凡例を追加するかどうか。
1378 def plot_conc_timeseries( 1379 self, 1380 source_name: str | None = None, 1381 output_dir: str | Path | None = None, 1382 output_filename: str = "timeseries.png", 1383 dpi: int = 200, 1384 figsize: tuple[float, float] = (8, 4), 1385 save_fig: bool = True, 1386 show_fig: bool = True, 1387 col_ch4: str = "ch4_ppm", 1388 col_c2h6: str = "c2h6_ppb", 1389 col_h2o: str = "h2o_ppm", 1390 ylim_ch4: tuple[float, float] | None = None, 1391 ylim_c2h6: tuple[float, float] | None = None, 1392 ylim_h2o: tuple[float, float] | None = None, 1393 font_size: float = 12, 1394 label_pad: float = 10, 1395 ) -> None: 1396 """ 1397 時系列データをプロットします。 1398 1399 Parameters 1400 ---------- 1401 dpi : int 1402 図の解像度を指定します。デフォルトは200です。 1403 source_name : str | None 1404 プロットするデータソースの名前。Noneの場合は最初のデータソースを使用します。 1405 figsize : tuple[float, float] 1406 図のサイズを指定します。デフォルトは(8, 4)です。 1407 output_dir : str | Path | None 1408 保存先のディレクトリを指定します。save_fig=Trueの場合は必須です。 1409 output_filename : str 1410 保存するファイル名を指定します。デフォルトは"time_series.png"です。 1411 save_fig : bool 1412 図を保存するかどうかを指定します。デフォルトはFalseです。 1413 show_fig : bool 1414 図を表示するかどうかを指定します。デフォルトはTrueです。 1415 col_ch4 : str 1416 CH4データのキーを指定します。デフォルトは"ch4_ppm"です。 1417 col_c2h6 : str 1418 C2H6データのキーを指定します。デフォルトは"c2h6_ppb"です。 1419 col_h2o : str 1420 H2Oデータのキーを指定します。デフォルトは"h2o_ppm"です。 1421 ylim_ch4 : tuple[float, float] | None 1422 CH4プロットのy軸範囲を指定します。デフォルトはNoneです。 1423 ylim_c2h6 : tuple[float, float] | None 1424 C2H6プロットのy軸範囲を指定します。デフォルトはNoneです。 1425 ylim_h2o : tuple[float, float] | None 1426 H2Oプロットのy軸範囲を指定します。デフォルトはNoneです。 1427 font_size : float 1428 基本フォントサイズ。デフォルトは12。 1429 label_pad : float 1430 y軸ラベルのパディング。デフォルトは10。 1431 """ 1432 # プロットパラメータの設定 1433 plt.rcParams.update( 1434 { 1435 "font.size": font_size, 1436 "axes.labelsize": font_size, 1437 "axes.titlesize": font_size, 1438 "xtick.labelsize": font_size, 1439 "ytick.labelsize": font_size, 1440 } 1441 ) 1442 dfs_dict: dict[str, pd.DataFrame] = self._data.copy() 1443 # データソースの選択 1444 if not dfs_dict: 1445 raise ValueError("データが読み込まれていません。") 1446 1447 if source_name not in dfs_dict: 1448 raise ValueError( 1449 f"指定されたデータソース '{source_name}' が見つかりません。" 1450 ) 1451 1452 df = dfs_dict[source_name] 1453 1454 # プロットの作成 1455 fig = plt.figure(figsize=figsize, dpi=dpi) 1456 1457 # CH4プロット 1458 ax1 = fig.add_subplot(3, 1, 1) 1459 ax1.plot(df.index, df[col_ch4], c="red") 1460 if ylim_ch4: 1461 ax1.set_ylim(ylim_ch4) 1462 ax1.set_ylabel("$\\mathregular{CH_{4}}$ (ppm)", labelpad=label_pad) 1463 ax1.grid(True, alpha=0.3) 1464 1465 # C2H6プロット 1466 ax2 = fig.add_subplot(3, 1, 2) 1467 ax2.plot(df.index, df[col_c2h6], c="red") 1468 if ylim_c2h6: 1469 ax2.set_ylim(ylim_c2h6) 1470 ax2.set_ylabel("$\\mathregular{C_{2}H_{6}}$ (ppb)", labelpad=label_pad) 1471 ax2.grid(True, alpha=0.3) 1472 1473 # H2Oプロット 1474 ax3 = fig.add_subplot(3, 1, 3) 1475 ax3.plot(df.index, df[col_h2o], c="red") 1476 if ylim_h2o: 1477 ax3.set_ylim(ylim_h2o) 1478 ax3.set_ylabel("$\\mathregular{H_{2}O}$ (ppm)", labelpad=label_pad) 1479 ax3.grid(True, alpha=0.3) 1480 1481 # x軸のフォーマット調整 1482 for ax in [ax1, ax2, ax3]: 1483 ax.xaxis.set_major_formatter(mdates.DateFormatter("%H:%M")) 1484 # 軸のラベルとグリッド線の調整 1485 ax.tick_params(axis="both", which="major", labelsize=font_size) 1486 ax.grid(True, alpha=0.3) 1487 1488 # サブプロット間の間隔調整 1489 plt.subplots_adjust(wspace=0.38, hspace=0.38) 1490 1491 # 図の保存 1492 if save_fig: 1493 if output_dir is None: 1494 raise ValueError( 1495 "save_fig=Trueの場合、output_dirを指定する必要があります。有効なディレクトリパスを指定してください。" 1496 ) 1497 os.makedirs(output_dir, exist_ok=True) 1498 output_path = os.path.join(output_dir, output_filename) 1499 plt.savefig(output_path, bbox_inches="tight") 1500 1501 if show_fig: 1502 plt.show() 1503 else: 1504 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です。
font_size : float
基本フォントサイズ。デフォルトは12。
label_pad : float
y軸ラベルのパディング。デフォルトは10。
2035 def remove_c2c1_ratio_duplicates( 2036 self, 2037 df: pd.DataFrame, 2038 min_time_threshold_seconds: float = 300, # 5分以内は重複とみなす 2039 max_time_threshold_hours: float = 12.0, # 12時間以上離れている場合は別のポイントとして扱う 2040 check_time_all: bool = True, # 時間閾値を超えた場合の重複チェックを継続するかどうか 2041 hotspot_area_meter: float = 50.0, # 重複とみなす距離の閾値(メートル) 2042 col_ch4_ppm: str = "ch4_ppm", 2043 col_ch4_ppm_bg: str = "ch4_ppm_bg", 2044 col_ch4_ppm_delta: str = "ch4_ppm_delta", 2045 ): 2046 """ 2047 メタン濃度の増加が閾値を超えた地点から、重複を除外してユニークなホットスポットを抽出する関数。 2048 2049 Parameters 2050 ---------- 2051 df : pandas.DataFrame 2052 入力データフレーム。必須カラム: 2053 - ch4_ppm: メタン濃度(ppm) 2054 - ch4_ppm_bg: メタン濃度の移動平均(ppm) 2055 - ch4_ppm_delta: メタン濃度の増加量(ppm) 2056 - latitude: 緯度 2057 - longitude: 経度 2058 min_time_threshold_seconds : float, optional 2059 重複とみなす最小時間差(秒)。デフォルトは300秒(5分)。 2060 max_time_threshold_hours : float, optional 2061 別ポイントとして扱う最大時間差(時間)。デフォルトは12時間。 2062 check_time_all : bool, optional 2063 時間閾値を超えた場合の重複チェックを継続するかどうか。デフォルトはTrue。 2064 hotspot_area_meter : float, optional 2065 重複とみなす距離の閾値(メートル)。デフォルトは50メートル。 2066 2067 Returns 2068 ---------- 2069 pandas.DataFrame 2070 ユニークなホットスポットのデータフレーム。 2071 """ 2072 df_data: pd.DataFrame = df.copy() 2073 # メタン濃度の増加が閾値を超えた点を抽出 2074 mask = ( 2075 df_data[col_ch4_ppm] - df_data[col_ch4_ppm_bg] > self._ch4_enhance_threshold 2076 ) 2077 hotspot_candidates = df_data[mask].copy() 2078 2079 # ΔCH4の降順でソート 2080 sorted_hotspots = hotspot_candidates.sort_values( 2081 by=col_ch4_ppm_delta, ascending=False 2082 ) 2083 used_positions = [] 2084 unique_hotspots = pd.DataFrame() 2085 2086 for _, spot in sorted_hotspots.iterrows(): 2087 should_add = True 2088 for used_lat, used_lon, used_time in used_positions: 2089 # 距離チェック 2090 distance = geodesic( 2091 (spot.latitude, spot.longitude), (used_lat, used_lon) 2092 ).meters 2093 2094 if distance < hotspot_area_meter: 2095 # 時間差の計算(秒単位) 2096 time_diff = pd.Timedelta( 2097 spot.name - pd.to_datetime(used_time) 2098 ).total_seconds() 2099 time_diff_abs = abs(time_diff) 2100 2101 # 時間差に基づく判定 2102 if check_time_all: 2103 # 時間に関係なく、距離が近ければ重複とみなす 2104 # ΔCH4が大きい方を残す(現在のスポットは必ず小さい) 2105 should_add = False 2106 break 2107 else: 2108 # 時間窓による判定を行う 2109 if time_diff_abs <= min_time_threshold_seconds: 2110 # Case 1: 最小時間閾値以内は重複とみなす 2111 should_add = False 2112 break 2113 elif time_diff_abs > max_time_threshold_hours * 3600: 2114 # Case 2: 最大時間閾値を超えた場合は重複チェックをスキップ 2115 continue 2116 # Case 3: その間の時間差の場合は、距離が近ければ重複とみなす 2117 should_add = False 2118 break 2119 2120 if should_add: 2121 unique_hotspots = pd.concat([unique_hotspots, pd.DataFrame([spot])]) 2122 used_positions.append((spot.latitude, spot.longitude, spot.name)) 2123 2124 return unique_hotspots
メタン濃度の増加が閾値を超えた地点から、重複を除外してユニークなホットスポットを抽出する関数。
Parameters
df : pandas.DataFrame
入力データフレーム。必須カラム:
- ch4_ppm: メタン濃度(ppm)
- ch4_ppm_bg: メタン濃度の移動平均(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
ユニークなホットスポットのデータフレーム。
2126 @staticmethod 2127 def remove_hotspots_duplicates( 2128 hotspots: list[HotspotData], 2129 check_time_all: bool, 2130 min_time_threshold_seconds: float = 300, 2131 max_time_threshold_hours: float = 12, 2132 hotspot_area_meter: float = 50, 2133 ) -> list[HotspotData]: 2134 """ 2135 重複するホットスポットを除外します。 2136 2137 このメソッドは、与えられたホットスポットのリストから重複を検出し、 2138 一意のホットスポットのみを返します。重複の判定は、指定された 2139 時間および距離の閾値に基づいて行われます。 2140 2141 Parameters 2142 ---------- 2143 hotspots : list[HotspotData] 2144 重複を除外する対象のホットスポットのリスト。 2145 check_time_all : bool 2146 時間に関係なく重複チェックを行うかどうか。 2147 min_time_threshold_seconds : float 2148 重複とみなす最小時間の閾値(秒)。 2149 max_time_threshold_hours : float 2150 重複チェックを一時的に無視する最大時間の閾値(時間)。 2151 hotspot_area_meter : float 2152 重複とみなす距離の閾値(メートル)。 2153 2154 Returns 2155 ---------- 2156 list[HotspotData] 2157 重複を除去したホットスポットのリスト。 2158 """ 2159 # ΔCH4の降順でソート 2160 sorted_hotspots: list[HotspotData] = sorted( 2161 hotspots, key=lambda x: x.delta_ch4, reverse=True 2162 ) 2163 used_positions_by_type: dict[ 2164 HotspotType, list[tuple[float, float, str, float]] 2165 ] = { 2166 "bio": [], 2167 "gas": [], 2168 "comb": [], 2169 } 2170 unique_hotspots: list[HotspotData] = [] 2171 2172 for spot in sorted_hotspots: 2173 is_duplicate = MobileSpatialAnalyzer._is_duplicate_spot( 2174 current_lat=spot.avg_lat, 2175 current_lon=spot.avg_lon, 2176 current_time=spot.source, 2177 used_positions=used_positions_by_type[spot.type], 2178 check_time_all=check_time_all, 2179 min_time_threshold_seconds=min_time_threshold_seconds, 2180 max_time_threshold_hours=max_time_threshold_hours, 2181 hotspot_area_meter=hotspot_area_meter, 2182 ) 2183 2184 if not is_duplicate: 2185 unique_hotspots.append(spot) 2186 used_positions_by_type[spot.type].append( 2187 (spot.avg_lat, spot.avg_lon, spot.source, spot.delta_ch4) 2188 ) 2189 2190 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]
重複を除去したホットスポットのリスト。
2192 @staticmethod 2193 def setup_logger(logger: Logger | None, log_level: int = INFO) -> Logger: 2194 """ 2195 ロガーを設定します。 2196 2197 このメソッドは、ロギングの設定を行い、ログメッセージのフォーマットを指定します。 2198 ログメッセージには、日付、ログレベル、メッセージが含まれます。 2199 2200 渡されたロガーがNoneまたは不正な場合は、新たにロガーを作成し、標準出力に 2201 ログメッセージが表示されるようにStreamHandlerを追加します。ロガーのレベルは 2202 引数で指定されたlog_levelに基づいて設定されます。 2203 2204 Parameters 2205 ---------- 2206 logger : Logger | None 2207 使用するロガー。Noneの場合は新しいロガーを作成します。 2208 log_level : int 2209 ロガーのログレベル。デフォルトはINFO。 2210 2211 Returns 2212 ---------- 2213 Logger 2214 設定されたロガーオブジェクト。 2215 """ 2216 if logger is not None and isinstance(logger, Logger): 2217 return logger 2218 # 渡されたロガーがNoneまたは正しいものでない場合は独自に設定 2219 new_logger: Logger = getLogger() 2220 # 既存のハンドラーをすべて削除 2221 for handler in new_logger.handlers[:]: 2222 new_logger.removeHandler(handler) 2223 new_logger.setLevel(log_level) # ロガーのレベルを設定 2224 ch = StreamHandler() 2225 ch_formatter = Formatter("%(asctime)s - %(levelname)s - %(message)s") 2226 ch.setFormatter(ch_formatter) # フォーマッターをハンドラーに設定 2227 new_logger.addHandler(ch) # StreamHandlerの追加 2228 return new_logger
ロガーを設定します。
このメソッドは、ロギングの設定を行い、ログメッセージのフォーマットを指定します。 ログメッセージには、日付、ログレベル、メッセージが含まれます。
渡されたロガーがNoneまたは不正な場合は、新たにロガーを作成し、標準出力に ログメッセージが表示されるようにStreamHandlerを追加します。ロガーのレベルは 引数で指定されたlog_levelに基づいて設定されます。
Parameters
logger : Logger | None
使用するロガー。Noneの場合は新しいロガーを作成します。
log_level : int
ロガーのログレベル。デフォルトはINFO。
Returns
Logger
設定されたロガーオブジェクト。
2230 @staticmethod 2231 def calculate_emission_rates( 2232 hotspots: list[HotspotData], 2233 method: Literal["weller", "weitzel", "joo", "umezawa"] = "weller", 2234 print_summary: bool = True, 2235 custom_formulas: dict[str, dict[str, float]] | None = None, 2236 ) -> tuple[list[EmissionData], dict[str, dict[str, float]]]: 2237 """ 2238 検出されたホットスポットのCH4漏出量を計算・解析し、統計情報を生成します。 2239 2240 Parameters 2241 ---------- 2242 hotspots : list[HotspotData] 2243 分析対象のホットスポットのリスト 2244 method : Literal["weller", "weitzel", "joo", "umezawa"] 2245 使用する計算式。デフォルトは"weller"。 2246 print_summary : bool 2247 統計情報を表示するかどうか。デフォルトはTrue。 2248 custom_formulas : dict[str, dict[str, float]] | None 2249 カスタム計算式の係数。 2250 例: {"custom_method": {"a": 1.0, "b": 1.0}} 2251 Noneの場合はデフォルトの計算式を使用。 2252 2253 Returns 2254 ---------- 2255 tuple[list[EmissionData], dict[str, dict[str, float]]] 2256 - 各ホットスポットの排出量データを含むリスト 2257 - タイプ別の統計情報を含む辞書 2258 """ 2259 # デフォルトの経験式係数 2260 default_formulas = { 2261 "weller": {"a": 0.988, "b": 0.817}, 2262 "weitzel": {"a": 0.521, "b": 0.795}, 2263 "joo": {"a": 2.738, "b": 1.329}, 2264 "umezawa": {"a": 2.716, "b": 0.741}, 2265 } 2266 2267 # カスタム計算式がある場合は追加 2268 emission_formulas = default_formulas.copy() 2269 if custom_formulas: 2270 emission_formulas.update(custom_formulas) 2271 2272 if method not in emission_formulas: 2273 raise ValueError(f"Unknown method: {method}") 2274 2275 # 係数の取得 2276 a = emission_formulas[method]["a"] 2277 b = emission_formulas[method]["b"] 2278 2279 # 排出量の計算 2280 emission_data_list = [] 2281 for spot in hotspots: 2282 # 漏出量の計算 (L/min) 2283 emission_rate = np.exp((np.log(spot.delta_ch4) + a) / b) 2284 # 日排出量 (L/day) 2285 daily_emission = emission_rate * 60 * 24 2286 # 年間排出量 (L/year) 2287 annual_emission = daily_emission * 365 2288 2289 emission_data = EmissionData( 2290 source=spot.source, 2291 type=spot.type, 2292 section=spot.section, 2293 latitude=spot.avg_lat, 2294 longitude=spot.avg_lon, 2295 delta_ch4=spot.delta_ch4, 2296 delta_c2h6=spot.delta_c2h6, 2297 ratio=spot.ratio, 2298 emission_rate=emission_rate, 2299 daily_emission=daily_emission, 2300 annual_emission=annual_emission, 2301 ) 2302 emission_data_list.append(emission_data) 2303 2304 # 統計計算用にDataFrameを作成 2305 emission_df = pd.DataFrame([e.to_dict() for e in emission_data_list]) 2306 2307 # タイプ別の統計情報を計算 2308 stats = {} 2309 # emission_formulas の定義の後に、排出量カテゴリーの閾値を定義 2310 emission_categories = { 2311 "low": {"min": 0, "max": 6}, # < 6 L/min 2312 "medium": {"min": 6, "max": 40}, # 6-40 L/min 2313 "high": {"min": 40, "max": float("inf")}, # > 40 L/min 2314 } 2315 # get_args(HotspotType)を使用して型安全なリストを作成 2316 types = list(get_args(HotspotType)) 2317 for spot_type in types: 2318 df_type = emission_df[emission_df["type"] == spot_type] 2319 if len(df_type) > 0: 2320 # 既存の統計情報を計算 2321 type_stats = { 2322 "count": len(df_type), 2323 "emission_rate_min": df_type["emission_rate"].min(), 2324 "emission_rate_max": df_type["emission_rate"].max(), 2325 "emission_rate_mean": df_type["emission_rate"].mean(), 2326 "emission_rate_median": df_type["emission_rate"].median(), 2327 "total_annual_emission": df_type["annual_emission"].sum(), 2328 "mean_annual_emission": df_type["annual_emission"].mean(), 2329 } 2330 2331 # 排出量カテゴリー別の統計を追加 2332 category_counts = { 2333 "low": len( 2334 df_type[ 2335 df_type["emission_rate"] < emission_categories["low"]["max"] 2336 ] 2337 ), 2338 "medium": len( 2339 df_type[ 2340 ( 2341 df_type["emission_rate"] 2342 >= emission_categories["medium"]["min"] 2343 ) 2344 & ( 2345 df_type["emission_rate"] 2346 < emission_categories["medium"]["max"] 2347 ) 2348 ] 2349 ), 2350 "high": len( 2351 df_type[ 2352 df_type["emission_rate"] 2353 >= emission_categories["high"]["min"] 2354 ] 2355 ), 2356 } 2357 type_stats["emission_categories"] = category_counts 2358 2359 stats[spot_type] = type_stats 2360 2361 if print_summary: 2362 print(f"\n{spot_type}タイプの統計情報:") 2363 print(f" 検出数: {type_stats['count']}") 2364 print(" 排出量 (L/min):") 2365 print(f" 最小値: {type_stats['emission_rate_min']:.2f}") 2366 print(f" 最大値: {type_stats['emission_rate_max']:.2f}") 2367 print(f" 平均値: {type_stats['emission_rate_mean']:.2f}") 2368 print(f" 中央値: {type_stats['emission_rate_median']:.2f}") 2369 print(" 排出量カテゴリー別の検出数:") 2370 print(f" 低放出 (< 6 L/min): {category_counts['low']}") 2371 print(f" 中放出 (6-40 L/min): {category_counts['medium']}") 2372 print(f" 高放出 (> 40 L/min): {category_counts['high']}") 2373 print(" 年間排出量 (L/year):") 2374 print(f" 合計: {type_stats['total_annual_emission']:.2f}") 2375 print(f" 平均: {type_stats['mean_annual_emission']:.2f}") 2376 2377 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]]]
- 各ホットスポットの排出量データを含むリスト
- タイプ別の統計情報を含む辞書
2379 @staticmethod 2380 def plot_emission_analysis( 2381 emission_data_list: list[EmissionData], 2382 dpi: int = 300, 2383 output_dir: str | Path | None = None, 2384 output_filename: str = "emission_analysis.png", 2385 figsize: tuple[float, float] = (12, 5), 2386 hotspot_colors: dict[HotspotType, str] = { 2387 "bio": "blue", 2388 "gas": "red", 2389 "comb": "green", 2390 }, 2391 add_legend: bool = True, 2392 hist_log_y: bool = False, 2393 hist_xlim: tuple[float, float] | None = None, 2394 hist_ylim: tuple[float, float] | None = None, 2395 scatter_xlim: tuple[float, float] | None = None, 2396 scatter_ylim: tuple[float, float] | None = None, 2397 hist_bin_width: float = 0.5, 2398 print_summary: bool = True, 2399 save_fig: bool = False, 2400 show_fig: bool = True, 2401 show_scatter: bool = True, # 散布図の表示を制御するオプションを追加 2402 ) -> None: 2403 """ 2404 排出量分析のプロットを作成する静的メソッド。 2405 2406 Parameters 2407 ---------- 2408 emission_data_list : list[EmissionData] 2409 EmissionDataオブジェクトのリスト。 2410 output_dir : str | Path | None 2411 出力先ディレクトリのパス。 2412 output_filename : str 2413 保存するファイル名。デフォルトは"emission_analysis.png"。 2414 dpi : int 2415 プロットの解像度。デフォルトは300。 2416 figsize : tuple[float, float] 2417 プロットのサイズ。デフォルトは(12, 5)。 2418 hotspot_colors : dict[HotspotType, str] 2419 ホットスポットの色を定義する辞書。 2420 add_legend : bool 2421 凡例を追加するかどうか。デフォルトはTrue。 2422 hist_log_y : bool 2423 ヒストグラムのy軸を対数スケールにするかどうか。デフォルトはFalse。 2424 hist_xlim : tuple[float, float] | None 2425 ヒストグラムのx軸の範囲。デフォルトはNone。 2426 hist_ylim : tuple[float, float] | None 2427 ヒストグラムのy軸の範囲。デフォルトはNone。 2428 scatter_xlim : tuple[float, float] | None 2429 散布図のx軸の範囲。デフォルトはNone。 2430 scatter_ylim : tuple[float, float] | None 2431 散布図のy軸の範囲。デフォルトはNone。 2432 hist_bin_width : float 2433 ヒストグラムのビンの幅。デフォルトは0.5。 2434 print_summary : bool 2435 集計結果を表示するかどうか。デフォルトはFalse。 2436 save_fig : bool 2437 図をファイルに保存するかどうか。デフォルトはFalse。 2438 show_fig : bool 2439 図を表示するかどうか。デフォルトはTrue。 2440 show_scatter : bool 2441 散布図(右図)を表示するかどうか。デフォルトはTrue。 2442 """ 2443 # データをDataFrameに変換 2444 df = pd.DataFrame([e.to_dict() for e in emission_data_list]) 2445 2446 # プロットの作成(散布図の有無に応じてサブプロット数を調整) 2447 if show_scatter: 2448 fig, (ax1, ax2) = plt.subplots(1, 2, figsize=figsize) 2449 axes = [ax1, ax2] 2450 else: 2451 fig, ax1 = plt.subplots(1, 1, figsize=(figsize[0] // 2, figsize[1])) 2452 axes = [ax1] 2453 2454 # 存在するタイプを確認 2455 # HotspotTypeの定義順を基準にソート 2456 hotspot_types = list(get_args(HotspotType)) 2457 existing_types = sorted( 2458 df["type"].unique(), key=lambda x: hotspot_types.index(x) 2459 ) 2460 2461 # 左側: ヒストグラム 2462 # ビンの範囲を設定 2463 start = 0 # 必ず0から開始 2464 if hist_xlim is not None: 2465 end = hist_xlim[1] 2466 else: 2467 end = np.ceil(df["emission_rate"].max() * 1.05) 2468 2469 # ビン数を計算(end値をbin_widthで割り切れるように調整) 2470 n_bins = int(np.ceil(end / hist_bin_width)) 2471 end = n_bins * hist_bin_width 2472 2473 # ビンの生成(0から開始し、bin_widthの倍数で区切る) 2474 bins = np.linspace(start, end, n_bins + 1) 2475 2476 # タイプごとにヒストグラムを積み上げ 2477 bottom = np.zeros(len(bins) - 1) 2478 for spot_type in existing_types: 2479 data = df[df["type"] == spot_type]["emission_rate"] 2480 if len(data) > 0: 2481 counts, _ = np.histogram(data, bins=bins) 2482 ax1.bar( 2483 bins[:-1], 2484 counts, 2485 width=hist_bin_width, 2486 bottom=bottom, 2487 alpha=0.6, 2488 label=spot_type, 2489 color=hotspot_colors[spot_type], 2490 ) 2491 bottom += counts 2492 2493 ax1.set_xlabel("CH$_4$ Emission (L min$^{-1}$)") 2494 ax1.set_ylabel("Frequency") 2495 if hist_log_y: 2496 # ax1.set_yscale("log") 2497 # 非線形スケールを設定(linthreshで線形から対数への遷移点を指定) 2498 ax1.set_yscale("symlog", linthresh=1.0) 2499 if hist_xlim is not None: 2500 ax1.set_xlim(hist_xlim) 2501 else: 2502 ax1.set_xlim(0, np.ceil(df["emission_rate"].max() * 1.05)) 2503 2504 if hist_ylim is not None: 2505 ax1.set_ylim(hist_ylim) 2506 else: 2507 ax1.set_ylim(0, ax1.get_ylim()[1]) # 下限を0に設定 2508 2509 if show_scatter: 2510 # 右側: 散布図 2511 for spot_type in existing_types: 2512 mask = df["type"] == spot_type 2513 ax2.scatter( 2514 df[mask]["emission_rate"], 2515 df[mask]["delta_ch4"], 2516 alpha=0.6, 2517 label=spot_type, 2518 color=hotspot_colors[spot_type], 2519 ) 2520 2521 ax2.set_xlabel("Emission Rate (L min$^{-1}$)") 2522 ax2.set_ylabel("ΔCH$_4$ (ppm)") 2523 if scatter_xlim is not None: 2524 ax2.set_xlim(scatter_xlim) 2525 else: 2526 ax2.set_xlim(0, np.ceil(df["emission_rate"].max() * 1.05)) 2527 2528 if scatter_ylim is not None: 2529 ax2.set_ylim(scatter_ylim) 2530 else: 2531 ax2.set_ylim(0, np.ceil(df["delta_ch4"].max() * 1.05)) 2532 2533 # 凡例の表示 2534 if add_legend: 2535 for ax in axes: 2536 ax.legend( 2537 bbox_to_anchor=(0.5, -0.30), 2538 loc="upper center", 2539 ncol=len(existing_types), 2540 ) 2541 2542 plt.tight_layout() 2543 2544 # 図の保存 2545 if save_fig: 2546 if output_dir is None: 2547 raise ValueError( 2548 "save_fig=Trueの場合、output_dirを指定する必要があります。有効なディレクトリパスを指定してください。" 2549 ) 2550 os.makedirs(output_dir, exist_ok=True) 2551 output_path = os.path.join(output_dir, output_filename) 2552 plt.savefig(output_path, bbox_inches="tight", dpi=dpi) 2553 # 図の表示 2554 if show_fig: 2555 plt.show() 2556 else: 2557 plt.close(fig=fig) 2558 2559 if print_summary: 2560 # デバッグ用の出力 2561 print("\nビンごとの集計:") 2562 print(f"{'Range':>12} | {'bio':>8} | {'gas':>8} | {'total':>8}") 2563 print("-" * 50) 2564 2565 for i in range(len(bins) - 1): 2566 bin_start = bins[i] 2567 bin_end = bins[i + 1] 2568 2569 # 各タイプのカウントを計算 2570 counts_by_type: dict[HotspotType, int] = {"bio": 0, "gas": 0, "comb": 0} 2571 total = 0 2572 for spot_type in existing_types: 2573 mask = ( 2574 (df["type"] == spot_type) 2575 & (df["emission_rate"] >= bin_start) 2576 & (df["emission_rate"] < bin_end) 2577 ) 2578 count = len(df[mask]) 2579 counts_by_type[spot_type] = count 2580 total += count 2581 2582 # カウントが0の場合はスキップ 2583 if total > 0: 2584 range_str = f"{bin_start:5.1f}-{bin_end:<5.1f}" 2585 bio_count = counts_by_type.get("bio", 0) 2586 gas_count = counts_by_type.get("gas", 0) 2587 print( 2588 f"{range_str:>12} | {bio_count:8d} | {gas_count:8d} | {total:8d}" 2589 )
排出量分析のプロットを作成する静的メソッド。
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)。
hotspot_colors : dict[HotspotType, str]
ホットスポットの色を定義する辞書。
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。
217@dataclass 218class MSAInputConfig: 219 """入力ファイルの設定を保持するデータクラス 220 221 Parameters 222 ---------- 223 fs : float 224 サンプリング周波数(Hz) 225 lag : float 226 測器の遅れ時間(秒) 227 path : Path | str 228 ファイルパス 229 bias_removal : BiasRemovalConfig | None 230 バイアス除去の設定。None(または未定義)の場合は補正を実施しない。 231 h2o_correction : H2OCorrectionConfig | None 232 水蒸気補正の設定。None(または未定義)の場合は補正を実施しない。 233 """ 234 235 fs: float 236 lag: float 237 path: Path | str 238 bias_removal: BiasRemovalConfig | None = None 239 h2o_correction: H2OCorrectionConfig | None = None 240 241 def __post_init__(self) -> None: 242 """ 243 インスタンス生成後に入力値の検証を行います。 244 """ 245 # fsが有効かを確認 246 if not isinstance(self.fs, (int, float)) or self.fs <= 0: 247 raise ValueError( 248 f"Invalid sampling frequency: {self.fs}. Must be a positive float." 249 ) 250 # lagが0以上のfloatかを確認 251 if not isinstance(self.lag, (int, float)) or self.lag < 0: 252 raise ValueError( 253 f"Invalid lag value: {self.lag}. Must be a non-negative float." 254 ) 255 # 拡張子の確認 256 supported_extensions: list[str] = [".txt", ".csv"] 257 extension = Path(self.path).suffix 258 if extension not in supported_extensions: 259 raise ValueError( 260 f"Unsupported file extension: '{extension}'. Supported: {supported_extensions}" 261 ) 262 263 @classmethod 264 def validate_and_create( 265 cls, 266 fs: float, 267 lag: float, 268 path: Path | str, 269 h2o_correction: H2OCorrectionConfig | None = None, 270 bias_removal: BiasRemovalConfig | None = None, 271 ) -> "MSAInputConfig": 272 """ 273 入力値を検証し、MSAInputConfigインスタンスを生成するファクトリメソッドです。 274 275 指定された遅延時間、サンプリング周波数、およびファイルパスが有効であることを確認し、 276 有効な場合に新しいMSAInputConfigオブジェクトを返します。 277 278 Parameters 279 ---------- 280 fs : float 281 サンプリング周波数。正のfloatである必要があります。 282 lag : float 283 遅延時間。0以上のfloatである必要があります。 284 path : Path | str 285 入力ファイルのパス。サポートされている拡張子は.txtと.csvです。 286 bias_removal : BiasRemovalConfig | None 287 バイアス除去の設定。None(または未定義)の場合は補正を実施しない。 288 h2o_correction : H2OCorrectionConfig | None 289 水蒸気補正の設定。None(または未定義)の場合は補正を実施しない。 290 291 Returns 292 ---------- 293 MSAInputConfig 294 検証された入力設定を持つMSAInputConfigオブジェクト。 295 """ 296 return cls( 297 fs=fs, 298 lag=lag, 299 path=path, 300 bias_removal=bias_removal, 301 h2o_correction=h2o_correction, 302 )
入力ファイルの設定を保持するデータクラス
Parameters
fs : float
サンプリング周波数(Hz)
lag : float
測器の遅れ時間(秒)
path : Path | str
ファイルパス
bias_removal : BiasRemovalConfig | None
バイアス除去の設定。None(または未定義)の場合は補正を実施しない。
h2o_correction : H2OCorrectionConfig | None
水蒸気補正の設定。None(または未定義)の場合は補正を実施しない。
263 @classmethod 264 def validate_and_create( 265 cls, 266 fs: float, 267 lag: float, 268 path: Path | str, 269 h2o_correction: H2OCorrectionConfig | None = None, 270 bias_removal: BiasRemovalConfig | None = None, 271 ) -> "MSAInputConfig": 272 """ 273 入力値を検証し、MSAInputConfigインスタンスを生成するファクトリメソッドです。 274 275 指定された遅延時間、サンプリング周波数、およびファイルパスが有効であることを確認し、 276 有効な場合に新しいMSAInputConfigオブジェクトを返します。 277 278 Parameters 279 ---------- 280 fs : float 281 サンプリング周波数。正のfloatである必要があります。 282 lag : float 283 遅延時間。0以上のfloatである必要があります。 284 path : Path | str 285 入力ファイルのパス。サポートされている拡張子は.txtと.csvです。 286 bias_removal : BiasRemovalConfig | None 287 バイアス除去の設定。None(または未定義)の場合は補正を実施しない。 288 h2o_correction : H2OCorrectionConfig | None 289 水蒸気補正の設定。None(または未定義)の場合は補正を実施しない。 290 291 Returns 292 ---------- 293 MSAInputConfig 294 検証された入力設定を持つMSAInputConfigオブジェクト。 295 """ 296 return cls( 297 fs=fs, 298 lag=lag, 299 path=path, 300 bias_removal=bias_removal, 301 h2o_correction=h2o_correction, 302 )
入力値を検証し、MSAInputConfigインスタンスを生成するファクトリメソッドです。
指定された遅延時間、サンプリング周波数、およびファイルパスが有効であることを確認し、 有効な場合に新しいMSAInputConfigオブジェクトを返します。
Parameters
fs : float
サンプリング周波数。正のfloatである必要があります。
lag : float
遅延時間。0以上のfloatである必要があります。
path : Path | str
入力ファイルのパス。サポートされている拡張子は.txtと.csvです。
bias_removal : BiasRemovalConfig | None
バイアス除去の設定。None(または未定義)の場合は補正を実施しない。
h2o_correction : H2OCorrectionConfig | None
水蒸気補正の設定。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 na_values: list[str] = [ 23 "#DIV/0!", 24 "#VALUE!", 25 "#REF!", 26 "#N/A", 27 "#NAME?", 28 "NAN", 29 "nan", 30 ], 31 logger: Logger | None = None, 32 logging_debug: bool = False, 33 ): 34 """ 35 MonthlyConverterクラスのコンストラクタ 36 37 Parameters 38 ---------- 39 directory : str | Path 40 Excelファイルが格納されているディレクトリのパス 41 file_pattern : str 42 ファイル名のパターン。デフォルトは'SA.Ultra.*.xlsx'。 43 na_values : list[str] 44 NaNと判定する値のパターン。 45 logger : Logger | None 46 使用するロガー。Noneの場合は新しいロガーを作成します。 47 logging_debug : bool 48 ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。 49 """ 50 # ロガー 51 log_level: int = INFO 52 if logging_debug: 53 log_level = DEBUG 54 self.logger: Logger = MonthlyConverter.setup_logger(logger, log_level) 55 56 self._na_values: list[str] = na_values 57 self._directory = Path(directory) 58 if not self._directory.exists(): 59 raise NotADirectoryError(f"Directory not found: {self._directory}") 60 61 # Excelファイルのパスを保持 62 self._excel_files: dict[str, pd.ExcelFile] = {} 63 self._file_pattern: str = file_pattern 64 65 @staticmethod 66 def setup_logger(logger: Logger | None, log_level: int = INFO) -> Logger: 67 """ 68 ロガーを設定します。 69 70 このメソッドは、ロギングの設定を行い、ログメッセージのフォーマットを指定します。 71 ログメッセージには、日付、ログレベル、メッセージが含まれます。 72 73 渡されたロガーがNoneまたは不正な場合は、新たにロガーを作成し、標準出力に 74 ログメッセージが表示されるようにStreamHandlerを追加します。ロガーのレベルは 75 引数で指定されたlog_levelに基づいて設定されます。 76 77 Parameters 78 ---------- 79 logger : Logger | None 80 使用するロガー。Noneの場合は新しいロガーを作成します。 81 log_level : int 82 ロガーのログレベル。デフォルトはINFO。 83 84 Returns 85 ---------- 86 Logger 87 設定されたロガーオブジェクト。 88 """ 89 if logger is not None and isinstance(logger, Logger): 90 return logger 91 # 渡されたロガーがNoneまたは正しいものでない場合は独自に設定 92 new_logger: Logger = getLogger() 93 # 既存のハンドラーをすべて削除 94 for handler in new_logger.handlers[:]: 95 new_logger.removeHandler(handler) 96 new_logger.setLevel(log_level) # ロガーのレベルを設定 97 ch = StreamHandler() 98 ch_formatter = Formatter("%(asctime)s - %(levelname)s - %(message)s") 99 ch.setFormatter(ch_formatter) # フォーマッターをハンドラーに設定 100 new_logger.addHandler(ch) # StreamHandlerの追加 101 return new_logger 102 103 def close(self) -> None: 104 """ 105 すべてのExcelファイルをクローズする 106 """ 107 for excel_file in self._excel_files.values(): 108 excel_file.close() 109 self._excel_files.clear() 110 111 def get_available_dates(self) -> list[str]: 112 """ 113 利用可能なファイルの日付一覧を返却します。 114 115 Returns 116 ---------- 117 list[str] 118 'yyyy.MM'形式の日付リスト 119 """ 120 dates = [] 121 for file_name in self._directory.glob(self._file_pattern): 122 try: 123 date = self._extract_date(file_name.name) 124 dates.append(date.strftime(self.FILE_DATE_FORMAT)) 125 except ValueError: 126 continue 127 return sorted(dates) 128 129 def get_sheet_names(self, file_name: str) -> list[str]: 130 """ 131 指定されたファイルで利用可能なシート名の一覧を返却する 132 133 Parameters 134 ---------- 135 file_name : str 136 Excelファイル名 137 138 Returns 139 ---------- 140 list[str] 141 シート名のリスト 142 """ 143 if file_name not in self._excel_files: 144 file_path = self._directory / file_name 145 if not file_path.exists(): 146 raise FileNotFoundError(f"File not found: {file_path}") 147 self._excel_files[file_name] = pd.ExcelFile(file_path) 148 return self._excel_files[file_name].sheet_names 149 150 def read_sheets( 151 self, 152 sheet_names: str | list[str], 153 columns: list[str] | None = None, # 新しいパラメータを追加 154 col_datetime: str = "Date", 155 header: int = 0, 156 skiprows: int | list[int] = [1], 157 start_date: str | None = None, 158 end_date: str | None = None, 159 include_end_date: bool = True, 160 sort_by_date: bool = True, 161 ) -> pd.DataFrame: 162 """ 163 指定されたシートを読み込み、DataFrameとして返却します。 164 デフォルトでは2行目(単位の行)はスキップされます。 165 重複するカラム名がある場合は、より先に指定されたシートに存在するカラムの値を保持します。 166 167 Parameters 168 ---------- 169 sheet_names : str | list[str] 170 読み込むシート名。文字列または文字列のリストを指定できます。 171 columns : list[str] | None 172 残すカラム名のリスト。Noneの場合は全てのカラムを保持します。 173 col_datetime : str 174 日付と時刻の情報が含まれるカラム名。デフォルトは'Date'。 175 header : int 176 データのヘッダー行を指定します。デフォルトは0。 177 skiprows : int | list[int] 178 スキップする行数。デフォルトでは1行目をスキップします。 179 start_date : str | None 180 開始日 ('yyyy-MM-dd')。この日付の'00:00:00'のデータが開始行となります。 181 end_date : str | None 182 終了日 ('yyyy-MM-dd')。この日付をデータに含めるかはinclude_end_dateフラグによって変わります。 183 include_end_date : bool 184 終了日を含めるかどうか。デフォルトはTrueです。 185 sort_by_date : bool 186 ファイルの日付でソートするかどうか。デフォルトはTrueです。 187 188 Returns 189 ---------- 190 pd.DataFrame 191 読み込まれたデータを結合したDataFrameを返します。 192 """ 193 if isinstance(sheet_names, str): 194 sheet_names = [sheet_names] 195 196 self._load_excel_files(start_date, end_date) 197 198 if not self._excel_files: 199 raise ValueError("No Excel files found matching the criteria") 200 201 # ファイルを日付順にソート 202 sorted_files = ( 203 sorted(self._excel_files.items(), key=lambda x: self._extract_date(x[0])) 204 if sort_by_date 205 else self._excel_files.items() 206 ) 207 208 # 各シートのデータを格納するリスト 209 sheet_dfs = {sheet_name: [] for sheet_name in sheet_names} 210 211 # 各ファイルからデータを読み込む 212 for file_name, excel_file in sorted_files: 213 file_date = self._extract_date(file_name) 214 215 for sheet_name in sheet_names: 216 if sheet_name in excel_file.sheet_names: 217 df = pd.read_excel( 218 excel_file, 219 sheet_name=sheet_name, 220 header=header, 221 skiprows=skiprows, 222 na_values=self._na_values, 223 ) 224 # 年と月を追加 225 df["year"] = file_date.year 226 df["month"] = file_date.month 227 sheet_dfs[sheet_name].append(df) 228 229 if not any(sheet_dfs.values()): 230 raise ValueError(f"No sheets found matching: {sheet_names}") 231 232 # 各シートのデータを結合 233 combined_sheets = {} 234 for sheet_name, dfs in sheet_dfs.items(): 235 if dfs: # シートにデータがある場合のみ結合 236 combined_sheets[sheet_name] = pd.concat(dfs, ignore_index=True) 237 238 # 最初のシートをベースにする 239 base_df = combined_sheets[sheet_names[0]] 240 241 # 2つ目以降のシートを結合 242 for sheet_name in sheet_names[1:]: 243 if sheet_name in combined_sheets: 244 base_df = self.merge_dataframes( 245 base_df, combined_sheets[sheet_name], date_column=col_datetime 246 ) 247 248 # 日付でフィルタリング 249 if start_date: 250 start_dt = pd.to_datetime(start_date) 251 base_df = base_df[base_df[col_datetime] >= start_dt] 252 253 if end_date: 254 end_dt = pd.to_datetime(end_date) 255 if include_end_date: 256 end_dt += pd.Timedelta(days=1) 257 base_df = base_df[base_df[col_datetime] < end_dt] 258 259 # カラムの選択 260 if columns is not None: 261 required_columns = [col_datetime, "year", "month"] 262 available_columns = base_df.columns.tolist() # 利用可能なカラムを取得 263 if not all(col in available_columns for col in columns): 264 raise ValueError( 265 f"指定されたカラムが見つかりません: {columns}. 利用可能なカラム: {available_columns}" 266 ) 267 selected_columns = list(set(columns + required_columns)) 268 base_df = base_df[selected_columns] 269 270 return base_df 271 272 def __enter__(self) -> "MonthlyConverter": 273 return self 274 275 def __exit__(self, exc_type, exc_val, exc_tb) -> None: 276 self.close() 277 278 def _extract_date(self, file_name: str) -> datetime: 279 """ 280 ファイル名から日付を抽出する 281 282 Parameters 283 ---------- 284 file_name : str 285 "SA.Ultra.yyyy.MM.xlsx"または"SA.Picaro.yyyy.MM.xlsx"形式のファイル名 286 287 Returns 288 ---------- 289 datetime 290 抽出された日付 291 """ 292 # ファイル名から日付部分を抽出 293 date_str = ".".join(file_name.split(".")[-3:-1]) # "yyyy.MM"の部分を取得 294 return datetime.strptime(date_str, self.FILE_DATE_FORMAT) 295 296 def _load_excel_files( 297 self, start_date: str | None = None, end_date: str | None = None 298 ) -> None: 299 """ 300 指定された日付範囲のExcelファイルを読み込む 301 302 Parameters 303 ---------- 304 start_date : str | None 305 開始日 ('yyyy-MM-dd'形式) 306 end_date : str | None 307 終了日 ('yyyy-MM-dd'形式) 308 """ 309 # 期間指定がある場合は、yyyy-MM-dd形式から年月のみを抽出 310 start_dt = None 311 end_dt = None 312 if start_date: 313 temp_dt = datetime.strptime(start_date, self.PERIOD_DATE_FORMAT) 314 start_dt = datetime(temp_dt.year, temp_dt.month, 1) 315 if end_date: 316 temp_dt = datetime.strptime(end_date, self.PERIOD_DATE_FORMAT) 317 end_dt = datetime(temp_dt.year, temp_dt.month, 1) 318 319 # 既存のファイルをクリア 320 self.close() 321 322 for excel_path in self._directory.glob(self._file_pattern): 323 try: 324 file_date = self._extract_date(excel_path.name) 325 326 # 日付範囲チェック 327 if start_dt and file_date < start_dt: 328 continue 329 if end_dt and file_date > end_dt: 330 continue 331 332 if excel_path.name not in self._excel_files: 333 self._excel_files[excel_path.name] = pd.ExcelFile(excel_path) 334 335 except ValueError as e: 336 self.logger.warning( 337 f"Could not parse date from file {excel_path.name}: {e}" 338 ) 339 340 @staticmethod 341 def extract_monthly_data( 342 df: pd.DataFrame, 343 target_months: list[int], 344 start_day: int | None = None, 345 end_day: int | None = None, 346 datetime_column: str = "Date", 347 ) -> pd.DataFrame: 348 """ 349 指定された月と期間のデータを抽出します。 350 351 Parameters 352 ---------- 353 df : pd.DataFrame 354 入力データフレーム。 355 target_months : list[int] 356 抽出したい月のリスト(1から12の整数)。 357 start_day : int | None 358 開始日(1から31の整数)。Noneの場合は月初め。 359 end_day : int | None 360 終了日(1から31の整数)。Noneの場合は月末。 361 datetime_column : str, optional 362 日付を含む列の名前。デフォルトは"Date"。 363 364 Returns 365 ---------- 366 pd.DataFrame 367 指定された期間のデータのみを含むデータフレーム。 368 """ 369 # 入力チェック 370 if not all(1 <= month <= 12 for month in target_months): 371 raise ValueError("target_monthsは1から12の間である必要があります") 372 373 if start_day is not None and not 1 <= start_day <= 31: 374 raise ValueError("start_dayは1から31の間である必要があります") 375 376 if end_day is not None and not 1 <= end_day <= 31: 377 raise ValueError("end_dayは1から31の間である必要があります") 378 379 if start_day is not None and end_day is not None and start_day > end_day: 380 raise ValueError("start_dayはend_day以下である必要があります") 381 382 # datetime_column をDatetime型に変換 383 df_copied = df.copy() 384 df_copied[datetime_column] = pd.to_datetime(df_copied[datetime_column]) 385 386 # 月でフィルタリング 387 monthly_data = df_copied[df_copied[datetime_column].dt.month.isin(target_months)] 388 389 # 日付範囲でフィルタリング 390 if start_day is not None: 391 monthly_data = monthly_data[ 392 monthly_data[datetime_column].dt.day >= start_day 393 ] 394 if end_day is not None: 395 monthly_data = monthly_data[monthly_data[datetime_column].dt.day <= end_day] 396 397 return monthly_data 398 399 @staticmethod 400 def merge_dataframes( 401 df1: pd.DataFrame, df2: pd.DataFrame, date_column: str = "Date" 402 ) -> pd.DataFrame: 403 """ 404 2つのDataFrameを結合します。重複するカラムは元の名前とサフィックス付きの両方を保持します。 405 406 Parameters 407 ---------- 408 df1 : pd.DataFrame 409 ベースとなるDataFrame 410 df2 : pd.DataFrame 411 結合するDataFrame 412 date_column : str 413 日付カラムの名前。デフォルトは"Date" 414 415 Returns 416 ---------- 417 pd.DataFrame 418 結合されたDataFrame 419 """ 420 # インデックスをリセット 421 df1 = df1.reset_index(drop=True) 422 df2 = df2.reset_index(drop=True) 423 424 # 日付カラムを統一 425 df2[date_column] = df1[date_column] 426 427 # 重複しないカラムと重複するカラムを分離 428 duplicate_cols = [date_column, "year", "month"] # 常に除外するカラム 429 overlapping_cols = [ 430 col 431 for col in df2.columns 432 if col in df1.columns and col not in duplicate_cols 433 ] 434 unique_cols = [ 435 col 436 for col in df2.columns 437 if col not in df1.columns and col not in duplicate_cols 438 ] 439 440 # 結果のDataFrameを作成 441 result = df1.copy() 442 443 # 重複しないカラムを追加 444 for col in unique_cols: 445 result[col] = df2[col] 446 447 # 重複するカラムを処理 448 for col in overlapping_cols: 449 # 元のカラムはdf1の値を保持(既に result に含まれている) 450 # _x サフィックスでdf1の値を追加 451 result[f"{col}_x"] = df1[col] 452 # _y サフィックスでdf2の値を追加 453 result[f"{col}_y"] = df2[col] 454 455 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 na_values: list[str] = [ 23 "#DIV/0!", 24 "#VALUE!", 25 "#REF!", 26 "#N/A", 27 "#NAME?", 28 "NAN", 29 "nan", 30 ], 31 logger: Logger | None = None, 32 logging_debug: bool = False, 33 ): 34 """ 35 MonthlyConverterクラスのコンストラクタ 36 37 Parameters 38 ---------- 39 directory : str | Path 40 Excelファイルが格納されているディレクトリのパス 41 file_pattern : str 42 ファイル名のパターン。デフォルトは'SA.Ultra.*.xlsx'。 43 na_values : list[str] 44 NaNと判定する値のパターン。 45 logger : Logger | None 46 使用するロガー。Noneの場合は新しいロガーを作成します。 47 logging_debug : bool 48 ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。 49 """ 50 # ロガー 51 log_level: int = INFO 52 if logging_debug: 53 log_level = DEBUG 54 self.logger: Logger = MonthlyConverter.setup_logger(logger, log_level) 55 56 self._na_values: list[str] = na_values 57 self._directory = Path(directory) 58 if not self._directory.exists(): 59 raise NotADirectoryError(f"Directory not found: {self._directory}") 60 61 # Excelファイルのパスを保持 62 self._excel_files: dict[str, pd.ExcelFile] = {} 63 self._file_pattern: str = file_pattern
MonthlyConverterクラスのコンストラクタ
Parameters
directory : str | Path
Excelファイルが格納されているディレクトリのパス
file_pattern : str
ファイル名のパターン。デフォルトは'SA.Ultra.*.xlsx'。
na_values : list[str]
NaNと判定する値のパターン。
logger : Logger | None
使用するロガー。Noneの場合は新しいロガーを作成します。
logging_debug : bool
ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。
65 @staticmethod 66 def setup_logger(logger: Logger | None, log_level: int = INFO) -> Logger: 67 """ 68 ロガーを設定します。 69 70 このメソッドは、ロギングの設定を行い、ログメッセージのフォーマットを指定します。 71 ログメッセージには、日付、ログレベル、メッセージが含まれます。 72 73 渡されたロガーがNoneまたは不正な場合は、新たにロガーを作成し、標準出力に 74 ログメッセージが表示されるようにStreamHandlerを追加します。ロガーのレベルは 75 引数で指定されたlog_levelに基づいて設定されます。 76 77 Parameters 78 ---------- 79 logger : Logger | None 80 使用するロガー。Noneの場合は新しいロガーを作成します。 81 log_level : int 82 ロガーのログレベル。デフォルトはINFO。 83 84 Returns 85 ---------- 86 Logger 87 設定されたロガーオブジェクト。 88 """ 89 if logger is not None and isinstance(logger, Logger): 90 return logger 91 # 渡されたロガーがNoneまたは正しいものでない場合は独自に設定 92 new_logger: Logger = getLogger() 93 # 既存のハンドラーをすべて削除 94 for handler in new_logger.handlers[:]: 95 new_logger.removeHandler(handler) 96 new_logger.setLevel(log_level) # ロガーのレベルを設定 97 ch = StreamHandler() 98 ch_formatter = Formatter("%(asctime)s - %(levelname)s - %(message)s") 99 ch.setFormatter(ch_formatter) # フォーマッターをハンドラーに設定 100 new_logger.addHandler(ch) # StreamHandlerの追加 101 return new_logger
ロガーを設定します。
このメソッドは、ロギングの設定を行い、ログメッセージのフォーマットを指定します。 ログメッセージには、日付、ログレベル、メッセージが含まれます。
渡されたロガーがNoneまたは不正な場合は、新たにロガーを作成し、標準出力に ログメッセージが表示されるようにStreamHandlerを追加します。ロガーのレベルは 引数で指定されたlog_levelに基づいて設定されます。
Parameters
logger : Logger | None
使用するロガー。Noneの場合は新しいロガーを作成します。
log_level : int
ロガーのログレベル。デフォルトはINFO。
Returns
Logger
設定されたロガーオブジェクト。
103 def close(self) -> None: 104 """ 105 すべてのExcelファイルをクローズする 106 """ 107 for excel_file in self._excel_files.values(): 108 excel_file.close() 109 self._excel_files.clear()
すべてのExcelファイルをクローズする
111 def get_available_dates(self) -> list[str]: 112 """ 113 利用可能なファイルの日付一覧を返却します。 114 115 Returns 116 ---------- 117 list[str] 118 'yyyy.MM'形式の日付リスト 119 """ 120 dates = [] 121 for file_name in self._directory.glob(self._file_pattern): 122 try: 123 date = self._extract_date(file_name.name) 124 dates.append(date.strftime(self.FILE_DATE_FORMAT)) 125 except ValueError: 126 continue 127 return sorted(dates)
利用可能なファイルの日付一覧を返却します。
Returns
list[str]
'yyyy.MM'形式の日付リスト
129 def get_sheet_names(self, file_name: str) -> list[str]: 130 """ 131 指定されたファイルで利用可能なシート名の一覧を返却する 132 133 Parameters 134 ---------- 135 file_name : str 136 Excelファイル名 137 138 Returns 139 ---------- 140 list[str] 141 シート名のリスト 142 """ 143 if file_name not in self._excel_files: 144 file_path = self._directory / file_name 145 if not file_path.exists(): 146 raise FileNotFoundError(f"File not found: {file_path}") 147 self._excel_files[file_name] = pd.ExcelFile(file_path) 148 return self._excel_files[file_name].sheet_names
指定されたファイルで利用可能なシート名の一覧を返却する
Parameters
file_name : str
Excelファイル名
Returns
list[str]
シート名のリスト
150 def read_sheets( 151 self, 152 sheet_names: str | list[str], 153 columns: list[str] | None = None, # 新しいパラメータを追加 154 col_datetime: str = "Date", 155 header: int = 0, 156 skiprows: int | list[int] = [1], 157 start_date: str | None = None, 158 end_date: str | None = None, 159 include_end_date: bool = True, 160 sort_by_date: bool = True, 161 ) -> pd.DataFrame: 162 """ 163 指定されたシートを読み込み、DataFrameとして返却します。 164 デフォルトでは2行目(単位の行)はスキップされます。 165 重複するカラム名がある場合は、より先に指定されたシートに存在するカラムの値を保持します。 166 167 Parameters 168 ---------- 169 sheet_names : str | list[str] 170 読み込むシート名。文字列または文字列のリストを指定できます。 171 columns : list[str] | None 172 残すカラム名のリスト。Noneの場合は全てのカラムを保持します。 173 col_datetime : str 174 日付と時刻の情報が含まれるカラム名。デフォルトは'Date'。 175 header : int 176 データのヘッダー行を指定します。デフォルトは0。 177 skiprows : int | list[int] 178 スキップする行数。デフォルトでは1行目をスキップします。 179 start_date : str | None 180 開始日 ('yyyy-MM-dd')。この日付の'00:00:00'のデータが開始行となります。 181 end_date : str | None 182 終了日 ('yyyy-MM-dd')。この日付をデータに含めるかはinclude_end_dateフラグによって変わります。 183 include_end_date : bool 184 終了日を含めるかどうか。デフォルトはTrueです。 185 sort_by_date : bool 186 ファイルの日付でソートするかどうか。デフォルトはTrueです。 187 188 Returns 189 ---------- 190 pd.DataFrame 191 読み込まれたデータを結合したDataFrameを返します。 192 """ 193 if isinstance(sheet_names, str): 194 sheet_names = [sheet_names] 195 196 self._load_excel_files(start_date, end_date) 197 198 if not self._excel_files: 199 raise ValueError("No Excel files found matching the criteria") 200 201 # ファイルを日付順にソート 202 sorted_files = ( 203 sorted(self._excel_files.items(), key=lambda x: self._extract_date(x[0])) 204 if sort_by_date 205 else self._excel_files.items() 206 ) 207 208 # 各シートのデータを格納するリスト 209 sheet_dfs = {sheet_name: [] for sheet_name in sheet_names} 210 211 # 各ファイルからデータを読み込む 212 for file_name, excel_file in sorted_files: 213 file_date = self._extract_date(file_name) 214 215 for sheet_name in sheet_names: 216 if sheet_name in excel_file.sheet_names: 217 df = pd.read_excel( 218 excel_file, 219 sheet_name=sheet_name, 220 header=header, 221 skiprows=skiprows, 222 na_values=self._na_values, 223 ) 224 # 年と月を追加 225 df["year"] = file_date.year 226 df["month"] = file_date.month 227 sheet_dfs[sheet_name].append(df) 228 229 if not any(sheet_dfs.values()): 230 raise ValueError(f"No sheets found matching: {sheet_names}") 231 232 # 各シートのデータを結合 233 combined_sheets = {} 234 for sheet_name, dfs in sheet_dfs.items(): 235 if dfs: # シートにデータがある場合のみ結合 236 combined_sheets[sheet_name] = pd.concat(dfs, ignore_index=True) 237 238 # 最初のシートをベースにする 239 base_df = combined_sheets[sheet_names[0]] 240 241 # 2つ目以降のシートを結合 242 for sheet_name in sheet_names[1:]: 243 if sheet_name in combined_sheets: 244 base_df = self.merge_dataframes( 245 base_df, combined_sheets[sheet_name], date_column=col_datetime 246 ) 247 248 # 日付でフィルタリング 249 if start_date: 250 start_dt = pd.to_datetime(start_date) 251 base_df = base_df[base_df[col_datetime] >= start_dt] 252 253 if end_date: 254 end_dt = pd.to_datetime(end_date) 255 if include_end_date: 256 end_dt += pd.Timedelta(days=1) 257 base_df = base_df[base_df[col_datetime] < end_dt] 258 259 # カラムの選択 260 if columns is not None: 261 required_columns = [col_datetime, "year", "month"] 262 available_columns = base_df.columns.tolist() # 利用可能なカラムを取得 263 if not all(col in available_columns for col in columns): 264 raise ValueError( 265 f"指定されたカラムが見つかりません: {columns}. 利用可能なカラム: {available_columns}" 266 ) 267 selected_columns = list(set(columns + required_columns)) 268 base_df = base_df[selected_columns] 269 270 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を返します。
340 @staticmethod 341 def extract_monthly_data( 342 df: pd.DataFrame, 343 target_months: list[int], 344 start_day: int | None = None, 345 end_day: int | None = None, 346 datetime_column: str = "Date", 347 ) -> pd.DataFrame: 348 """ 349 指定された月と期間のデータを抽出します。 350 351 Parameters 352 ---------- 353 df : pd.DataFrame 354 入力データフレーム。 355 target_months : list[int] 356 抽出したい月のリスト(1から12の整数)。 357 start_day : int | None 358 開始日(1から31の整数)。Noneの場合は月初め。 359 end_day : int | None 360 終了日(1から31の整数)。Noneの場合は月末。 361 datetime_column : str, optional 362 日付を含む列の名前。デフォルトは"Date"。 363 364 Returns 365 ---------- 366 pd.DataFrame 367 指定された期間のデータのみを含むデータフレーム。 368 """ 369 # 入力チェック 370 if not all(1 <= month <= 12 for month in target_months): 371 raise ValueError("target_monthsは1から12の間である必要があります") 372 373 if start_day is not None and not 1 <= start_day <= 31: 374 raise ValueError("start_dayは1から31の間である必要があります") 375 376 if end_day is not None and not 1 <= end_day <= 31: 377 raise ValueError("end_dayは1から31の間である必要があります") 378 379 if start_day is not None and end_day is not None and start_day > end_day: 380 raise ValueError("start_dayはend_day以下である必要があります") 381 382 # datetime_column をDatetime型に変換 383 df_copied = df.copy() 384 df_copied[datetime_column] = pd.to_datetime(df_copied[datetime_column]) 385 386 # 月でフィルタリング 387 monthly_data = df_copied[df_copied[datetime_column].dt.month.isin(target_months)] 388 389 # 日付範囲でフィルタリング 390 if start_day is not None: 391 monthly_data = monthly_data[ 392 monthly_data[datetime_column].dt.day >= start_day 393 ] 394 if end_day is not None: 395 monthly_data = monthly_data[monthly_data[datetime_column].dt.day <= end_day] 396 397 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
指定された期間のデータのみを含むデータフレーム。
399 @staticmethod 400 def merge_dataframes( 401 df1: pd.DataFrame, df2: pd.DataFrame, date_column: str = "Date" 402 ) -> pd.DataFrame: 403 """ 404 2つのDataFrameを結合します。重複するカラムは元の名前とサフィックス付きの両方を保持します。 405 406 Parameters 407 ---------- 408 df1 : pd.DataFrame 409 ベースとなるDataFrame 410 df2 : pd.DataFrame 411 結合するDataFrame 412 date_column : str 413 日付カラムの名前。デフォルトは"Date" 414 415 Returns 416 ---------- 417 pd.DataFrame 418 結合されたDataFrame 419 """ 420 # インデックスをリセット 421 df1 = df1.reset_index(drop=True) 422 df2 = df2.reset_index(drop=True) 423 424 # 日付カラムを統一 425 df2[date_column] = df1[date_column] 426 427 # 重複しないカラムと重複するカラムを分離 428 duplicate_cols = [date_column, "year", "month"] # 常に除外するカラム 429 overlapping_cols = [ 430 col 431 for col in df2.columns 432 if col in df1.columns and col not in duplicate_cols 433 ] 434 unique_cols = [ 435 col 436 for col in df2.columns 437 if col not in df1.columns and col not in duplicate_cols 438 ] 439 440 # 結果のDataFrameを作成 441 result = df1.copy() 442 443 # 重複しないカラムを追加 444 for col in unique_cols: 445 result[col] = df2[col] 446 447 # 重複するカラムを処理 448 for col in overlapping_cols: 449 # 元のカラムはdf1の値を保持(既に result に含まれている) 450 # _x サフィックスでdf1の値を追加 451 result[f"{col}_x"] = df1[col] 452 # _y サフィックスでdf2の値を追加 453 result[f"{col}_y"] = df2[col] 454 455 return result
2つのDataFrameを結合します。重複するカラムは元の名前とサフィックス付きの両方を保持します。
Parameters
df1 : pd.DataFrame
ベースとなるDataFrame
df2 : pd.DataFrame
結合するDataFrame
date_column : str
日付カラムの名前。デフォルトは"Date"
Returns
pd.DataFrame
結合されたDataFrame
64class MonthlyFiguresGenerator: 65 def __init__( 66 self, 67 logger: Logger | None = None, 68 logging_debug: bool = False, 69 ) -> None: 70 """ 71 クラスのコンストラクタ 72 73 Parameters 74 ------ 75 logger : Logger | None 76 使用するロガー。Noneの場合は新しいロガーを作成します。 77 logging_debug : bool 78 ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。 79 """ 80 # ロガー 81 log_level: int = INFO 82 if logging_debug: 83 log_level = DEBUG 84 self.logger: Logger = MonthlyFiguresGenerator.setup_logger(logger, log_level) 85 86 def plot_c1c2_fluxes_timeseries( 87 self, 88 df, 89 output_dir: str, 90 output_filename: str = "timeseries.png", 91 col_datetime: str = "Date", 92 col_c1_flux: str = "Fch4_ultra", 93 col_c2_flux: str = "Fc2h6_ultra", 94 ): 95 """ 96 月別のフラックスデータを時系列プロットとして出力する 97 98 Parameters 99 ------ 100 df : pd.DataFrame 101 月別データを含むDataFrame 102 output_dir : str 103 出力ファイルを保存するディレクトリのパス 104 output_filename : str 105 出力ファイルの名前 106 col_datetime : str 107 日付を含む列の名前。デフォルトは"Date"。 108 col_c1_flux : str 109 CH4フラックスを含む列の名前。デフォルトは"Fch4_ultra"。 110 col_c2_flux : str 111 C2H6フラックスを含む列の名前。デフォルトは"Fc2h6_ultra"。 112 """ 113 os.makedirs(output_dir, exist_ok=True) 114 output_path: str = os.path.join(output_dir, output_filename) 115 116 # 図の作成 117 _, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 8), sharex=True) 118 119 # CH4フラックスのプロット 120 ax1.scatter(df[col_datetime], df[col_c1_flux], color="red", alpha=0.5, s=20) 121 ax1.set_ylabel(r"CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)") 122 ax1.set_ylim(-100, 600) 123 ax1.text(0.02, 0.98, "(a)", transform=ax1.transAxes, va="top", fontsize=20) 124 ax1.grid(True, alpha=0.3) 125 126 # C2H6フラックスのプロット 127 ax2.scatter( 128 df[col_datetime], 129 df[col_c2_flux], 130 color="orange", 131 alpha=0.5, 132 s=20, 133 ) 134 ax2.set_ylabel(r"C$_2$H$_6$ flux (nmol m$^{-2}$ s$^{-1}$)") 135 ax2.set_ylim(-20, 60) 136 ax2.text(0.02, 0.98, "(b)", transform=ax2.transAxes, va="top", fontsize=20) 137 ax2.grid(True, alpha=0.3) 138 139 # x軸の設定 140 ax2.xaxis.set_major_locator(mdates.MonthLocator(interval=1)) 141 ax2.xaxis.set_major_formatter(mdates.DateFormatter("%m")) 142 plt.setp(ax2.get_xticklabels(), rotation=0, ha="right") 143 ax2.set_xlabel("Month") 144 145 # 図の保存 146 plt.savefig(output_path, dpi=300, bbox_inches="tight") 147 plt.close() 148 149 def plot_c1c2_concentrations_and_fluxes_timeseries( 150 self, 151 df: pd.DataFrame, 152 output_dir: str, 153 output_filename: str = "conc_flux_timeseries.png", 154 col_datetime: str = "Date", 155 col_ch4_conc: str = "CH4_ultra", 156 col_ch4_flux: str = "Fch4_ultra", 157 col_c2h6_conc: str = "C2H6_ultra", 158 col_c2h6_flux: str = "Fc2h6_ultra", 159 print_summary: bool = True, 160 ) -> None: 161 """ 162 CH4とC2H6の濃度とフラックスの時系列プロットを作成する 163 164 Parameters 165 ------ 166 df : pd.DataFrame 167 月別データを含むDataFrame 168 output_dir : str 169 出力ディレクトリのパス 170 output_filename : str 171 出力ファイル名 172 col_datetime : str 173 日付列の名前 174 col_ch4_conc : str 175 CH4濃度列の名前 176 col_ch4_flux : str 177 CH4フラックス列の名前 178 col_c2h6_conc : str 179 C2H6濃度列の名前 180 col_c2h6_flux : str 181 C2H6フラックス列の名前 182 print_summary : bool 183 解析情報をprintするかどうか 184 """ 185 # 出力ディレクトリの作成 186 os.makedirs(output_dir, exist_ok=True) 187 output_path: str = os.path.join(output_dir, output_filename) 188 189 if print_summary: 190 # 統計情報の計算と表示 191 for name, col in [ 192 ("CH4 concentration", col_ch4_conc), 193 ("CH4 flux", col_ch4_flux), 194 ("C2H6 concentration", col_c2h6_conc), 195 ("C2H6 flux", col_c2h6_flux), 196 ]: 197 # NaNを除外してから統計量を計算 198 valid_data = df[col].dropna() 199 200 if len(valid_data) > 0: 201 percentile_5 = np.nanpercentile(valid_data, 5) 202 percentile_95 = np.nanpercentile(valid_data, 95) 203 mean_value = np.nanmean(valid_data) 204 positive_ratio = (valid_data > 0).mean() * 100 205 206 print(f"\n{name}:") 207 print( 208 f"90パーセンタイルレンジ: {percentile_5:.2f} - {percentile_95:.2f}" 209 ) 210 print(f"平均値: {mean_value:.2f}") 211 print(f"正の値の割合: {positive_ratio:.1f}%") 212 else: 213 print(f"\n{name}: データが存在しません") 214 215 # プロットの作成 216 _, (ax1, ax2, ax3, ax4) = plt.subplots(4, 1, figsize=(12, 16), sharex=True) 217 218 # CH4濃度のプロット 219 ax1.scatter(df[col_datetime], df[col_ch4_conc], color="red", alpha=0.5, s=20) 220 ax1.set_ylabel("CH$_4$ Concentration\n(ppm)") 221 ax1.set_ylim(1.8, 2.6) 222 ax1.text(0.02, 0.98, "(a)", transform=ax1.transAxes, va="top", fontsize=20) 223 ax1.grid(True, alpha=0.3) 224 225 # CH4フラックスのプロット 226 ax2.scatter(df[col_datetime], df[col_ch4_flux], color="red", alpha=0.5, s=20) 227 ax2.set_ylabel("CH$_4$ flux\n(nmol m$^{-2}$ s$^{-1}$)") 228 ax2.set_ylim(-100, 600) 229 # ax2.set_yticks([-100, 0, 200, 400, 600]) 230 ax2.text(0.02, 0.98, "(b)", transform=ax2.transAxes, va="top", fontsize=20) 231 ax2.grid(True, alpha=0.3) 232 233 # C2H6濃度のプロット 234 ax3.scatter( 235 df[col_datetime], df[col_c2h6_conc], color="orange", alpha=0.5, s=20 236 ) 237 ax3.set_ylabel("C$_2$H$_6$ Concentration\n(ppb)") 238 ax3.text(0.02, 0.98, "(c)", transform=ax3.transAxes, va="top", fontsize=20) 239 ax3.grid(True, alpha=0.3) 240 241 # C2H6フラックスのプロット 242 ax4.scatter( 243 df[col_datetime], df[col_c2h6_flux], color="orange", alpha=0.5, s=20 244 ) 245 ax4.set_ylabel("C$_2$H$_6$ flux\n(nmol m$^{-2}$ s$^{-1}$)") 246 ax4.set_ylim(-20, 40) 247 ax4.text(0.02, 0.98, "(d)", transform=ax4.transAxes, va="top", fontsize=20) 248 ax4.grid(True, alpha=0.3) 249 250 # x軸の設定 251 ax4.xaxis.set_major_locator(mdates.MonthLocator(interval=1)) 252 ax4.xaxis.set_major_formatter(mdates.DateFormatter("%m")) 253 plt.setp(ax4.get_xticklabels(), rotation=0, ha="right") 254 ax4.set_xlabel("Month") 255 256 # レイアウトの調整と保存 257 plt.tight_layout() 258 plt.savefig(output_path, dpi=300, bbox_inches="tight") 259 plt.close() 260 261 if print_summary: 262 263 def analyze_top_values(df, column_name, top_percent=20): 264 print(f"\n{column_name}の上位{top_percent}%の分析:") 265 266 # DataFrameのコピーを作成し、日時関連の列を追加 267 df_analysis = df.copy() 268 df_analysis["hour"] = pd.to_datetime(df_analysis[col_datetime]).dt.hour 269 df_analysis["month"] = pd.to_datetime( 270 df_analysis[col_datetime] 271 ).dt.month 272 df_analysis["weekday"] = pd.to_datetime( 273 df_analysis[col_datetime] 274 ).dt.dayofweek 275 276 # 上位20%のしきい値を計算 277 threshold = df[column_name].quantile(1 - top_percent / 100) 278 high_values = df_analysis[df_analysis[column_name] > threshold] 279 280 # 月ごとの分析 281 print("\n月別分布:") 282 monthly_counts = high_values.groupby("month").size() 283 total_counts = df_analysis.groupby("month").size() 284 monthly_percentages = (monthly_counts / total_counts * 100).round(1) 285 286 # 月ごとのデータを安全に表示 287 available_months = set(monthly_counts.index) & set(total_counts.index) 288 for month in sorted(available_months): 289 print( 290 f"月{month}: {monthly_percentages[month]}% ({monthly_counts[month]}件/{total_counts[month]}件)" 291 ) 292 293 # 時間帯ごとの分析(3時間区切り) 294 print("\n時間帯別分布:") 295 # copyを作成して新しい列を追加 296 high_values = high_values.copy() 297 high_values["time_block"] = high_values["hour"] // 3 * 3 298 time_blocks = high_values.groupby("time_block").size() 299 total_time_blocks = df_analysis.groupby( 300 df_analysis["hour"] // 3 * 3 301 ).size() 302 time_percentages = (time_blocks / total_time_blocks * 100).round(1) 303 304 # 時間帯ごとのデータを安全に表示 305 available_blocks = set(time_blocks.index) & set(total_time_blocks.index) 306 for block in sorted(available_blocks): 307 print( 308 f"{block:02d}:00-{block + 3:02d}:00: {time_percentages[block]}% ({time_blocks[block]}件/{total_time_blocks[block]}件)" 309 ) 310 311 # 曜日ごとの分析 312 print("\n曜日別分布:") 313 weekday_names = ["月曜", "火曜", "水曜", "木曜", "金曜", "土曜", "日曜"] 314 weekday_counts = high_values.groupby("weekday").size() 315 total_weekdays = df_analysis.groupby("weekday").size() 316 weekday_percentages = (weekday_counts / total_weekdays * 100).round(1) 317 318 # 曜日ごとのデータを安全に表示 319 available_days = set(weekday_counts.index) & set(total_weekdays.index) 320 for day in sorted(available_days): 321 if 0 <= day <= 6: # 有効な曜日インデックスのチェック 322 print( 323 f"{weekday_names[day]}: {weekday_percentages[day]}% ({weekday_counts[day]}件/{total_weekdays[day]}件)" 324 ) 325 326 # 濃度とフラックスそれぞれの分析を実行 327 print("\n=== 上位値の時間帯・曜日分析 ===") 328 analyze_top_values(df, col_ch4_conc) 329 analyze_top_values(df, col_ch4_flux) 330 analyze_top_values(df, col_c2h6_conc) 331 analyze_top_values(df, col_c2h6_flux) 332 333 def plot_c1c2_timeseries( 334 self, 335 df: pd.DataFrame, 336 output_dir: str, 337 col_ch4_flux: str, 338 col_c2h6_flux: str, 339 output_filename: str = "timeseries_year.png", 340 col_datetime: str = "Date", 341 window_size: int = 24 * 7, # 1週間の移動平均のデフォルト値 342 confidence_interval: float = 0.95, # 95%信頼区間 343 subplot_label_ch4: str | None = "(a)", 344 subplot_label_c2h6: str | None = "(b)", 345 subplot_fontsize: int = 20, 346 show_ci: bool = True, 347 ch4_ylim: tuple[float, float] | None = None, 348 c2h6_ylim: tuple[float, float] | None = None, 349 start_date: str | None = None, # 追加:"YYYY-MM-DD"形式 350 end_date: str | None = None, # 追加:"YYYY-MM-DD"形式 351 figsize: tuple[float, float] = (16, 6), 352 ) -> None: 353 """CH4とC2H6フラックスの時系列変動をプロット 354 355 Parameters 356 ------ 357 df : pd.DataFrame 358 データフレーム 359 output_dir : str 360 出力ディレクトリのパス 361 col_ch4_flux : str 362 CH4フラックスのカラム名 363 col_c2h6_flux : str 364 C2H6フラックスのカラム名 365 output_filename : str 366 出力ファイル名 367 col_datetime : str 368 日時カラムの名前 369 window_size : int 370 移動平均の窓サイズ 371 confidence_interval : float 372 信頼区間(0-1) 373 subplot_label_ch4 : str | None 374 CH4プロットのラベル 375 subplot_label_c2h6 : str | None 376 C2H6プロットのラベル 377 subplot_fontsize : int 378 サブプロットのフォントサイズ 379 show_ci : bool 380 信頼区間を表示するか 381 ch4_ylim : tuple[float, float] | None 382 CH4のy軸範囲 383 c2h6_ylim : tuple[float, float] | None 384 C2H6のy軸範囲 385 start_date : str | None 386 開始日(YYYY-MM-DD形式) 387 end_date : str | None 388 終了日(YYYY-MM-DD形式) 389 figsize : tuple[float, float] 390 図のサイズ。デフォルトは(16, 6)。 391 """ 392 # 出力ディレクトリの作成 393 os.makedirs(output_dir, exist_ok=True) 394 output_path: str = os.path.join(output_dir, output_filename) 395 396 # データの準備 397 df_copied = df.copy() 398 if not isinstance(df_copied.index, pd.DatetimeIndex): 399 df_copied[col_datetime] = pd.to_datetime(df_copied[col_datetime]) 400 df_copied.set_index(col_datetime, inplace=True) 401 402 # 日付範囲の処理 403 if start_date is not None: 404 start_dt = pd.to_datetime(start_date).normalize() # 時刻を00:00:00に設定 405 df_min_date = ( 406 df_copied.index.normalize().min().normalize() 407 ) # 日付のみの比較のため正規化 408 409 # データの最小日付が指定開始日より後の場合にのみ警告 410 if df_min_date.date() > start_dt.date(): 411 self.logger.warning( 412 f"指定された開始日{start_date}がデータの開始日{df_min_date.strftime('%Y-%m-%d')}より前です。" 413 f"データの開始日を使用します。" 414 ) 415 start_dt = df_min_date 416 else: 417 start_dt = df_copied.index.normalize().min() 418 419 if end_date is not None: 420 end_dt = ( 421 pd.to_datetime(end_date).normalize() 422 + pd.Timedelta(days=1) 423 - pd.Timedelta(seconds=1) 424 ) 425 df_max_date = ( 426 df_copied.index.normalize().max().normalize() 427 ) # 日付のみの比較のため正規化 428 429 # データの最大日付が指定終了日より前の場合にのみ警告 430 if df_max_date.date() < pd.to_datetime(end_date).date(): 431 self.logger.warning( 432 f"指定された終了日{end_date}がデータの終了日{df_max_date.strftime('%Y-%m-%d')}より後です。" 433 f"データの終了日を使用します。" 434 ) 435 end_dt = df_copied.index.max() 436 else: 437 end_dt = df_copied.index.max() 438 439 # 指定された期間のデータを抽出 440 mask = (df_copied.index >= start_dt) & (df_copied.index <= end_dt) 441 df_copied = df_copied[mask] 442 443 # CH4とC2H6の移動平均と信頼区間を計算 444 ch4_mean, ch4_lower, ch4_upper = calculate_rolling_stats( 445 df_copied[col_ch4_flux], window_size, confidence_interval 446 ) 447 c2h6_mean, c2h6_lower, c2h6_upper = calculate_rolling_stats( 448 df_copied[col_c2h6_flux], window_size, confidence_interval 449 ) 450 451 # プロットの作成 452 fig, (ax1, ax2) = plt.subplots(1, 2, figsize=figsize) 453 454 # CH4プロット 455 ax1.plot(df_copied.index, ch4_mean, "red", label="CH$_4$") 456 if show_ci: 457 ax1.fill_between(df_copied.index, ch4_lower, ch4_upper, color="red", alpha=0.2) 458 if subplot_label_ch4: 459 ax1.text( 460 0.02, 461 0.98, 462 subplot_label_ch4, 463 transform=ax1.transAxes, 464 va="top", 465 fontsize=subplot_fontsize, 466 ) 467 ax1.set_ylabel("CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)") 468 if ch4_ylim is not None: 469 ax1.set_ylim(ch4_ylim) 470 ax1.grid(True, alpha=0.3) 471 472 # C2H6プロット 473 ax2.plot(df_copied.index, c2h6_mean, "orange", label="C$_2$H$_6$") 474 if show_ci: 475 ax2.fill_between( 476 df_copied.index, c2h6_lower, c2h6_upper, color="orange", alpha=0.2 477 ) 478 if subplot_label_c2h6: 479 ax2.text( 480 0.02, 481 0.98, 482 subplot_label_c2h6, 483 transform=ax2.transAxes, 484 va="top", 485 fontsize=subplot_fontsize, 486 ) 487 ax2.set_ylabel("C$_2$H$_6$ flux (nmol m$^{-2}$ s$^{-1}$)") 488 if c2h6_ylim is not None: 489 ax2.set_ylim(c2h6_ylim) 490 ax2.grid(True, alpha=0.3) 491 492 # x軸の設定 493 for ax in [ax1, ax2]: 494 ax.set_xlabel("Month") 495 # x軸の範囲を設定 496 ax.set_xlim(start_dt, end_dt) 497 498 # 1ヶ月ごとの主目盛り 499 ax.xaxis.set_major_locator(mdates.MonthLocator(interval=1)) 500 501 # カスタムフォーマッタの作成(数字を通常フォントで表示) 502 def date_formatter(x, p): 503 date = mdates.num2date(x) 504 return f"{date.strftime('%m')}" 505 506 ax.xaxis.set_major_formatter(plt.FuncFormatter(date_formatter)) 507 508 # 補助目盛りの設定 509 ax.xaxis.set_minor_locator(mdates.MonthLocator()) 510 # ティックラベルの回転と位置調整 511 plt.setp(ax.xaxis.get_majorticklabels(), ha="right") 512 513 plt.tight_layout() 514 plt.savefig(output_path, dpi=300, bbox_inches="tight") 515 plt.close(fig) 516 517 def plot_fluxes_comparison( 518 self, 519 df: pd.DataFrame, 520 output_dir: str, 521 cols_flux: list[str], 522 labels: list[str], 523 colors: list[str], 524 output_filename: str = "ch4_flux_comparison.png", 525 col_datetime: str = "Date", 526 window_size: int = 24 * 7, # 1週間の移動平均のデフォルト値 527 confidence_interval: float = 0.95, # 95%信頼区間 528 subplot_label: str | None = None, 529 subplot_fontsize: int = 20, 530 show_ci: bool = True, 531 y_lim: tuple[float, float] | None = None, 532 start_date: str | None = None, 533 end_date: str | None = None, 534 include_end_date: bool = True, 535 figsize: tuple[float, float] = (12, 6), 536 legend_loc: str = "upper right", 537 apply_ma: bool = True, # 移動平均を適用するかどうか 538 hourly_mean: bool = False, # 1時間平均を適用するかどうか 539 x_interval: Literal["month", "10days"] = "month", # "month" または "10days" 540 xlabel: str = "Month", 541 ylabel: str = "CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)", 542 save_fig: bool = True, 543 show_fig: bool = False, 544 ) -> None: 545 """複数のCH4フラックスの時系列比較プロット 546 547 Parameters 548 ------ 549 df : pd.DataFrame 550 データフレーム 551 output_dir : str 552 出力ディレクトリのパス 553 cols_flux : list[str] 554 比較するフラックスのカラム名リスト 555 labels : list[str] 556 凡例に表示する各フラックスのラベルリスト 557 colors : list[str] 558 各フラックスの色リスト 559 output_filename : str 560 出力ファイル名 561 col_datetime : str 562 日時カラムの名前 563 window_size : int 564 移動平均の窓サイズ 565 confidence_interval : float 566 信頼区間(0-1) 567 subplot_label : str | None 568 プロットのラベル 569 subplot_fontsize : int 570 サブプロットのフォントサイズ 571 show_ci : bool 572 信頼区間を表示するか 573 y_lim : tuple[float, float] | None 574 y軸の範囲 575 start_date : str | None 576 開始日(YYYY-MM-DD形式) 577 end_date : str | None 578 終了日(YYYY-MM-DD形式) 579 include_end_date : bool 580 終了日を含めるかどうか。Falseの場合、終了日の前日までを表示 581 figsize : tuple[float, float] 582 図のサイズ 583 legend_loc : str 584 凡例の位置 585 apply_ma : bool 586 移動平均を適用するかどうか 587 hourly_mean : bool 588 1時間平均を適用するかどうか 589 x_interval : Literal['month', '10days'] 590 x軸の目盛り間隔。"month"(月初めのみ)または"10days"(10日刻み) 591 xlabel : str 592 x軸のラベル(通常は"Month") 593 ylabel : str 594 y軸のラベル(通常は"CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)") 595 save_fig : bool 596 図を保存するかどうか 597 show_fig : bool 598 図を表示するかどうか 599 """ 600 # 出力ディレクトリの作成 601 os.makedirs(output_dir, exist_ok=True) 602 output_path: str = os.path.join(output_dir, output_filename) 603 604 # データの準備 605 df = df.copy() 606 if not isinstance(df.index, pd.DatetimeIndex): 607 df[col_datetime] = pd.to_datetime(df[col_datetime]) 608 df.set_index(col_datetime, inplace=True) 609 610 # 1時間平均の適用 611 if hourly_mean: 612 # 時間情報のみを使用してグループ化 613 df = df.groupby([df.index.date, df.index.hour]).mean() 614 # マルチインデックスを日時インデックスに変換 615 df.index = pd.to_datetime( 616 [f"{date} {hour:02d}:00:00" for date, hour in df.index] 617 ) 618 619 # 日付範囲の処理 620 if start_date is not None: 621 start_dt = pd.to_datetime(start_date).normalize() # 時刻を00:00:00に設定 622 df_min_date = ( 623 df.index.normalize().min().normalize() 624 ) # 日付のみの比較のため正規化 625 626 # データの最小日付が指定開始日より後の場合にのみ警告 627 if df_min_date.date() > start_dt.date(): 628 self.logger.warning( 629 f"指定された開始日{start_date}がデータの開始日{df_min_date.strftime('%Y-%m-%d')}より前です。" 630 f"データの開始日を使用します。" 631 ) 632 start_dt = df_min_date 633 else: 634 start_dt = df.index.normalize().min() 635 636 if end_date is not None: 637 if include_end_date: 638 end_dt = ( 639 pd.to_datetime(end_date).normalize() 640 + pd.Timedelta(days=1) 641 - pd.Timedelta(seconds=1) 642 ) 643 else: 644 # 終了日を含まない場合、終了日の前日の23:59:59まで 645 end_dt = pd.to_datetime(end_date).normalize() - pd.Timedelta(seconds=1) 646 647 df_max_date = ( 648 df.index.normalize().max().normalize() 649 ) # 日付のみの比較のため正規化 650 651 # データの最大日付が指定終了日より前の場合にのみ警告 652 compare_date = pd.to_datetime(end_date).date() 653 if not include_end_date: 654 compare_date = compare_date - pd.Timedelta(days=1) 655 656 if df_max_date.date() < compare_date: 657 self.logger.warning( 658 f"指定された終了日{end_date}がデータの終了日{df_max_date.strftime('%Y-%m-%d')}より後です。" 659 f"データの終了日を使用します。" 660 ) 661 end_dt = df.index.max() 662 else: 663 end_dt = df.index.max() 664 665 # 指定された期間のデータを抽出 666 mask = (df.index >= start_dt) & (df.index <= end_dt) 667 df = df[mask] 668 669 # プロットの作成 670 fig, ax = plt.subplots(figsize=figsize) 671 672 # 各フラックスのプロット 673 for flux_col, label, color in zip(cols_flux, labels, colors): 674 if apply_ma: 675 # 移動平均の計算 676 mean, lower, upper = calculate_rolling_stats( 677 df[flux_col], window_size, confidence_interval 678 ) 679 ax.plot(df.index, mean, color, label=label, alpha=0.7) 680 if show_ci: 681 ax.fill_between(df.index, lower, upper, color=color, alpha=0.2) 682 else: 683 # 生データのプロット 684 ax.plot(df.index, df[flux_col], color, label=label, alpha=0.7) 685 686 # プロットの設定 687 if subplot_label: 688 ax.text( 689 0.02, 690 0.98, 691 subplot_label, 692 transform=ax.transAxes, 693 va="top", 694 fontsize=subplot_fontsize, 695 ) 696 697 ax.set_xlabel(xlabel) 698 ax.set_ylabel(ylabel) 699 700 if y_lim is not None: 701 ax.set_ylim(y_lim) 702 703 ax.grid(True, alpha=0.3) 704 ax.legend(loc=legend_loc) 705 706 # x軸の設定 707 ax.set_xlim(start_dt, end_dt) 708 709 if x_interval == "month": 710 # 月初めにメジャー線のみ表示 711 ax.xaxis.set_major_locator(mdates.MonthLocator()) 712 ax.xaxis.set_minor_locator(plt.NullLocator()) # マイナー線を非表示 713 elif x_interval == "10days": 714 # 10日刻みでメジャー線、日毎にマイナー線を表示 715 ax.xaxis.set_major_locator(mdates.DayLocator(bymonthday=[1, 11, 21])) 716 ax.xaxis.set_minor_locator(mdates.DayLocator()) 717 ax.grid(True, which="minor", alpha=0.1) # マイナー線の表示設定 718 719 # カスタムフォーマッタの作成(月初めの1日のみMMを表示) 720 def date_formatter(x, p): 721 date = mdates.num2date(x) 722 # 月初めの1日の場合のみ月を表示 723 if date.day == 1: 724 return f"{date.strftime('%m')}" 725 return "" 726 727 ax.xaxis.set_major_formatter(plt.FuncFormatter(date_formatter)) 728 plt.setp(ax.xaxis.get_majorticklabels(), ha="right", rotation=0) 729 730 plt.tight_layout() 731 732 if save_fig: 733 plt.savefig(output_path, dpi=300, bbox_inches="tight") 734 if show_fig: 735 plt.show() 736 plt.close(fig) 737 738 def plot_c1c2_fluxes_diurnal_patterns( 739 self, 740 df: pd.DataFrame, 741 y_cols_ch4: list[str], 742 y_cols_c2h6: list[str], 743 labels_ch4: list[str], 744 labels_c2h6: list[str], 745 colors_ch4: list[str], 746 colors_c2h6: list[str], 747 output_dir: str, 748 output_filename: str = "diurnal.png", 749 legend_only_ch4: bool = False, 750 add_label: bool = True, 751 add_legend: bool = True, 752 show_std: bool = False, # 標準偏差表示のオプションを追加 753 std_alpha: float = 0.2, # 標準偏差の透明度 754 subplot_fontsize: int = 20, 755 subplot_label_ch4: str | None = "(a)", 756 subplot_label_c2h6: str | None = "(b)", 757 ax1_ylim: tuple[float, float] | None = None, 758 ax2_ylim: tuple[float, float] | None = None, 759 ) -> None: 760 """CH4とC2H6の日変化パターンを1つの図に並べてプロットする 761 762 Parameters 763 ------ 764 df : pd.DataFrame 765 入力データフレーム。 766 y_cols_ch4 : list[str] 767 CH4のプロットに使用するカラム名のリスト。 768 y_cols_c2h6 : list[str] 769 C2H6のプロットに使用するカラム名のリスト。 770 labels_ch4 : list[str] 771 CH4の各ラインに対応するラベルのリスト。 772 labels_c2h6 : list[str] 773 C2H6の各ラインに対応するラベルのリスト。 774 colors_ch4 : list[str] 775 CH4の各ラインに使用する色のリスト。 776 colors_c2h6 : list[str] 777 C2H6の各ラインに使用する色のリスト。 778 output_dir : str 779 出力先ディレクトリのパス。 780 output_filename : str, optional 781 出力ファイル名。デフォルトは"diurnal.png"。 782 legend_only_ch4 : bool, optional 783 CH4の凡例のみを表示するかどうか。デフォルトはFalse。 784 add_label : bool, optional 785 サブプロットラベルを表示するかどうか。デフォルトはTrue。 786 add_legend : bool, optional 787 凡例を表示するかどうか。デフォルトはTrue。 788 show_std : bool, optional 789 標準偏差を表示するかどうか。デフォルトはFalse。 790 std_alpha : float, optional 791 標準偏差の透明度。デフォルトは0.2。 792 subplot_fontsize : int, optional 793 サブプロットのフォントサイズ。デフォルトは20。 794 subplot_label_ch4 : str | None, optional 795 CH4プロットのラベル。デフォルトは"(a)"。 796 subplot_label_c2h6 : str | None, optional 797 C2H6プロットのラベル。デフォルトは"(b)"。 798 ax1_ylim : tuple[float, float] | None, optional 799 CH4プロットのy軸の範囲。デフォルトはNone。 800 ax2_ylim : tuple[float, float] | None, optional 801 C2H6プロットのy軸の範囲。デフォルトはNone。 802 """ 803 os.makedirs(output_dir, exist_ok=True) 804 output_path: str = os.path.join(output_dir, output_filename) 805 806 # データの準備 807 target_columns = y_cols_ch4 + y_cols_c2h6 808 hourly_means, time_points = self._prepare_diurnal_data(df, target_columns) 809 810 # 標準偏差の計算を追加 811 hourly_stds = {} 812 if show_std: 813 hourly_stds = df.groupby(df.index.hour)[target_columns].std() 814 # 24時間目のデータ点を追加 815 last_hour = hourly_stds.iloc[0:1].copy() 816 last_hour.index = [24] 817 hourly_stds = pd.concat([hourly_stds, last_hour]) 818 819 # プロットの作成 820 fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5)) 821 822 # CH4のプロット (左側) 823 ch4_lines = [] 824 for y_col, label, color in zip(y_cols_ch4, labels_ch4, colors_ch4): 825 mean_values = hourly_means["all"][y_col] 826 line = ax1.plot( 827 time_points, 828 mean_values, 829 "-o", 830 label=label, 831 color=color, 832 ) 833 ch4_lines.extend(line) 834 835 # 標準偏差の表示 836 if show_std: 837 std_values = hourly_stds[y_col] 838 ax1.fill_between( 839 time_points, 840 mean_values - std_values, 841 mean_values + std_values, 842 color=color, 843 alpha=std_alpha, 844 ) 845 846 # C2H6のプロット (右側) 847 c2h6_lines = [] 848 for y_col, label, color in zip(y_cols_c2h6, labels_c2h6, colors_c2h6): 849 mean_values = hourly_means["all"][y_col] 850 line = ax2.plot( 851 time_points, 852 mean_values, 853 "o-", 854 label=label, 855 color=color, 856 ) 857 c2h6_lines.extend(line) 858 859 # 標準偏差の表示 860 if show_std: 861 std_values = hourly_stds[y_col] 862 ax2.fill_between( 863 time_points, 864 mean_values - std_values, 865 mean_values + std_values, 866 color=color, 867 alpha=std_alpha, 868 ) 869 870 # 軸の設定 871 for ax, ylabel, subplot_label in [ 872 (ax1, r"CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)", subplot_label_ch4), 873 (ax2, r"C$_2$H$_6$ flux (nmol m$^{-2}$ s$^{-1}$)", subplot_label_c2h6), 874 ]: 875 self._setup_diurnal_axes( 876 ax=ax, 877 time_points=time_points, 878 ylabel=ylabel, 879 subplot_label=subplot_label, 880 add_label=add_label, 881 add_legend=False, # 個別の凡例は表示しない 882 subplot_fontsize=subplot_fontsize, 883 ) 884 885 if ax1_ylim is not None: 886 ax1.set_ylim(ax1_ylim) 887 ax1.yaxis.set_major_locator(MultipleLocator(20)) 888 ax1.yaxis.set_major_formatter(FuncFormatter(lambda x, p: f"{x:.0f}")) 889 890 if ax2_ylim is not None: 891 ax2.set_ylim(ax2_ylim) 892 ax2.yaxis.set_major_locator(MultipleLocator(1)) 893 ax2.yaxis.set_major_formatter(FuncFormatter(lambda x, p: f"{x:.1f}")) 894 895 plt.tight_layout() 896 897 # 共通の凡例 898 if add_legend: 899 all_lines = ch4_lines 900 all_labels = [line.get_label() for line in ch4_lines] 901 if not legend_only_ch4: 902 all_lines += c2h6_lines 903 all_labels += [line.get_label() for line in c2h6_lines] 904 fig.legend( 905 all_lines, 906 all_labels, 907 loc="center", 908 bbox_to_anchor=(0.5, 0.02), 909 ncol=len(all_lines), 910 ) 911 plt.subplots_adjust(bottom=0.25) # 下部に凡例用のスペースを確保 912 913 fig.savefig(output_path, dpi=300, bbox_inches="tight") 914 plt.close(fig) 915 916 def plot_c1c2_fluxes_diurnal_patterns_by_date( 917 self, 918 df: pd.DataFrame, 919 y_col_ch4: str, 920 y_col_c2h6: str, 921 output_dir: str, 922 output_filename: str = "diurnal_by_date.png", 923 plot_all: bool = True, 924 plot_weekday: bool = True, 925 plot_weekend: bool = True, 926 plot_holiday: bool = True, 927 add_label: bool = True, 928 add_legend: bool = True, 929 show_std: bool = False, # 標準偏差表示のオプションを追加 930 std_alpha: float = 0.2, # 標準偏差の透明度 931 legend_only_ch4: bool = False, 932 subplot_fontsize: int = 20, 933 subplot_label_ch4: str | None = "(a)", 934 subplot_label_c2h6: str | None = "(b)", 935 ax1_ylim: tuple[float, float] | None = None, 936 ax2_ylim: tuple[float, float] | None = None, 937 print_summary: bool = True, # 追加: 統計情報を表示するかどうか 938 ) -> None: 939 """CH4とC2H6の日変化パターンを日付分類して1つの図に並べてプロットする 940 941 Parameters 942 ------ 943 df : pd.DataFrame 944 入力データフレーム。 945 y_col_ch4 : str 946 CH4フラックスを含むカラム名。 947 y_col_c2h6 : str 948 C2H6フラックスを含むカラム名。 949 output_dir : str 950 出力先ディレクトリのパス。 951 output_filename : str, optional 952 出力ファイル名。デフォルトは"diurnal_by_date.png"。 953 plot_all : bool, optional 954 すべての日をプロットするかどうか。デフォルトはTrue。 955 plot_weekday : bool, optional 956 平日をプロットするかどうか。デフォルトはTrue。 957 plot_weekend : bool, optional 958 週末をプロットするかどうか。デフォルトはTrue。 959 plot_holiday : bool, optional 960 祝日をプロットするかどうか。デフォルトはTrue。 961 add_label : bool, optional 962 サブプロットラベルを表示するかどうか。デフォルトはTrue。 963 add_legend : bool, optional 964 凡例を表示するかどうか。デフォルトはTrue。 965 show_std : bool, optional 966 標準偏差を表示するかどうか。デフォルトはFalse。 967 std_alpha : float, optional 968 標準偏差の透明度。デフォルトは0.2。 969 legend_only_ch4 : bool, optional 970 CH4の凡例のみを表示するかどうか。デフォルトはFalse。 971 subplot_fontsize : int, optional 972 サブプロットのフォントサイズ。デフォルトは20。 973 subplot_label_ch4 : str | None, optional 974 CH4プロットのラベル。デフォルトは"(a)"。 975 subplot_label_c2h6 : str | None, optional 976 C2H6プロットのラベル。デフォルトは"(b)"。 977 ax1_ylim : tuple[float, float] | None, optional 978 CH4プロットのy軸の範囲。デフォルトはNone。 979 ax2_ylim : tuple[float, float] | None, optional 980 C2H6プロットのy軸の範囲。デフォルトはNone。 981 print_summary : bool, optional 982 統計情報を表示するかどうか。デフォルトはTrue。 983 """ 984 os.makedirs(output_dir, exist_ok=True) 985 output_path: str = os.path.join(output_dir, output_filename) 986 987 # データの準備 988 target_columns = [y_col_ch4, y_col_c2h6] 989 hourly_means, time_points = self._prepare_diurnal_data( 990 df, target_columns, include_date_types=True 991 ) 992 993 # 標準偏差の計算を追加 994 hourly_stds = {} 995 if show_std: 996 for condition in ["all", "weekday", "weekend", "holiday"]: 997 if condition == "all": 998 condition_data = df 999 elif condition == "weekday": 1000 condition_data = df[ 1001 ~( 1002 df.index.dayofweek.isin([5, 6]) 1003 | df.index.map(lambda x: jpholiday.is_holiday(x.date())) 1004 ) 1005 ] 1006 elif condition == "weekend": 1007 condition_data = df[df.index.dayofweek.isin([5, 6])] 1008 else: # holiday 1009 condition_data = df[ 1010 df.index.map(lambda x: jpholiday.is_holiday(x.date())) 1011 ] 1012 1013 hourly_stds[condition] = condition_data.groupby( 1014 condition_data.index.hour 1015 )[target_columns].std() 1016 # 24時間目のデータ点を追加 1017 last_hour = hourly_stds[condition].iloc[0:1].copy() 1018 last_hour.index = [24] 1019 hourly_stds[condition] = pd.concat([hourly_stds[condition], last_hour]) 1020 1021 # プロットスタイルの設定 1022 styles = { 1023 "all": { 1024 "color": "black", 1025 "linestyle": "-", 1026 "alpha": 1.0, 1027 "label": "All days", 1028 }, 1029 "weekday": { 1030 "color": "blue", 1031 "linestyle": "-", 1032 "alpha": 0.8, 1033 "label": "Weekdays", 1034 }, 1035 "weekend": { 1036 "color": "red", 1037 "linestyle": "-", 1038 "alpha": 0.8, 1039 "label": "Weekends", 1040 }, 1041 "holiday": { 1042 "color": "green", 1043 "linestyle": "-", 1044 "alpha": 0.8, 1045 "label": "Weekends & Holidays", 1046 }, 1047 } 1048 1049 # プロット対象の条件を選択 1050 plot_conditions = { 1051 "all": plot_all, 1052 "weekday": plot_weekday, 1053 "weekend": plot_weekend, 1054 "holiday": plot_holiday, 1055 } 1056 selected_conditions = { 1057 col: means 1058 for col, means in hourly_means.items() 1059 if col in plot_conditions and plot_conditions[col] 1060 } 1061 1062 # プロットの作成 1063 fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5)) 1064 1065 # CH4とC2H6のプロット用のラインオブジェクトを保存 1066 ch4_lines = [] 1067 c2h6_lines = [] 1068 1069 # CH4とC2H6のプロット 1070 for condition, means in selected_conditions.items(): 1071 style = styles[condition].copy() 1072 1073 # CH4プロット 1074 mean_values_ch4 = means[y_col_ch4] 1075 line_ch4 = ax1.plot(time_points, mean_values_ch4, marker="o", **style) 1076 ch4_lines.extend(line_ch4) 1077 1078 if show_std and condition in hourly_stds: 1079 std_values = hourly_stds[condition][y_col_ch4] 1080 ax1.fill_between( 1081 time_points, 1082 mean_values_ch4 - std_values, 1083 mean_values_ch4 + std_values, 1084 color=style["color"], 1085 alpha=std_alpha, 1086 ) 1087 1088 # C2H6プロット 1089 style["linestyle"] = "--" 1090 mean_values_c2h6 = means[y_col_c2h6] 1091 line_c2h6 = ax2.plot(time_points, mean_values_c2h6, marker="o", **style) 1092 c2h6_lines.extend(line_c2h6) 1093 1094 if show_std and condition in hourly_stds: 1095 std_values = hourly_stds[condition][y_col_c2h6] 1096 ax2.fill_between( 1097 time_points, 1098 mean_values_c2h6 - std_values, 1099 mean_values_c2h6 + std_values, 1100 color=style["color"], 1101 alpha=std_alpha, 1102 ) 1103 1104 # 軸の設定 1105 for ax, ylabel, subplot_label in [ 1106 (ax1, r"CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)", subplot_label_ch4), 1107 (ax2, r"C$_2$H$_6$ flux (nmol m$^{-2}$ s$^{-1}$)", subplot_label_c2h6), 1108 ]: 1109 self._setup_diurnal_axes( 1110 ax=ax, 1111 time_points=time_points, 1112 ylabel=ylabel, 1113 subplot_label=subplot_label, 1114 add_label=add_label, 1115 add_legend=False, 1116 subplot_fontsize=subplot_fontsize, 1117 ) 1118 1119 if ax1_ylim is not None: 1120 ax1.set_ylim(ax1_ylim) 1121 ax1.yaxis.set_major_locator(MultipleLocator(20)) 1122 ax1.yaxis.set_major_formatter(FuncFormatter(lambda x, p: f"{x:.0f}")) 1123 1124 if ax2_ylim is not None: 1125 ax2.set_ylim(ax2_ylim) 1126 ax2.yaxis.set_major_locator(MultipleLocator(1)) 1127 ax2.yaxis.set_major_formatter(FuncFormatter(lambda x, p: f"{x:.1f}")) 1128 1129 plt.tight_layout() 1130 1131 # 共通の凡例を図の下部に配置 1132 if add_legend: 1133 lines_to_show = ( 1134 ch4_lines if legend_only_ch4 else ch4_lines[: len(selected_conditions)] 1135 ) 1136 fig.legend( 1137 lines_to_show, 1138 [ 1139 style["label"] 1140 for style in list(styles.values())[: len(lines_to_show)] 1141 ], 1142 loc="center", 1143 bbox_to_anchor=(0.5, 0.02), 1144 ncol=len(lines_to_show), 1145 ) 1146 plt.subplots_adjust(bottom=0.25) # 下部に凡例用のスペースを確保 1147 1148 fig.savefig(output_path, dpi=300, bbox_inches="tight") 1149 plt.close(fig) 1150 1151 # 日変化パターンの統計分析を追加 1152 if print_summary: 1153 # 平日と休日のデータを準備 1154 dates = pd.to_datetime(df.index) 1155 is_weekend = dates.dayofweek.isin([5, 6]) 1156 is_holiday = dates.map(lambda x: jpholiday.is_holiday(x.date())) 1157 is_weekday = ~(is_weekend | is_holiday) 1158 1159 weekday_data = df[is_weekday] 1160 holiday_data = df[is_weekend | is_holiday] 1161 1162 def get_diurnal_stats(data, column): 1163 # 時間ごとの平均値を計算 1164 hourly_means = data.groupby(data.index.hour)[column].mean() 1165 1166 # 8-16時の時間帯の統計 1167 daytime_means = hourly_means[ 1168 (hourly_means.index >= 8) & (hourly_means.index <= 16) 1169 ] 1170 1171 if len(daytime_means) == 0: 1172 return None 1173 1174 return { 1175 "mean": daytime_means.mean(), 1176 "max": daytime_means.max(), 1177 "max_hour": daytime_means.idxmax(), 1178 "min": daytime_means.min(), 1179 "min_hour": daytime_means.idxmin(), 1180 "hours_count": len(daytime_means), 1181 } 1182 1183 # CH4とC2H6それぞれの統計を計算 1184 for col, gas_name in [(y_col_ch4, "CH4"), (y_col_c2h6, "C2H6")]: 1185 print(f"\n=== {gas_name} フラックス 8-16時の統計分析 ===") 1186 1187 weekday_stats = get_diurnal_stats(weekday_data, col) 1188 holiday_stats = get_diurnal_stats(holiday_data, col) 1189 1190 if weekday_stats and holiday_stats: 1191 print("\n平日:") 1192 print(f" 平均値: {weekday_stats['mean']:.2f}") 1193 print( 1194 f" 最大値: {weekday_stats['max']:.2f} ({weekday_stats['max_hour']}時)" 1195 ) 1196 print( 1197 f" 最小値: {weekday_stats['min']:.2f} ({weekday_stats['min_hour']}時)" 1198 ) 1199 print(f" 集計時間数: {weekday_stats['hours_count']}") 1200 1201 print("\n休日:") 1202 print(f" 平均値: {holiday_stats['mean']:.2f}") 1203 print( 1204 f" 最大値: {holiday_stats['max']:.2f} ({holiday_stats['max_hour']}時)" 1205 ) 1206 print( 1207 f" 最小値: {holiday_stats['min']:.2f} ({holiday_stats['min_hour']}時)" 1208 ) 1209 print(f" 集計時間数: {holiday_stats['hours_count']}") 1210 1211 # 平日/休日の比率を計算 1212 print("\n平日/休日の比率:") 1213 print( 1214 f" 平均値比: {weekday_stats['mean'] / holiday_stats['mean']:.2f}" 1215 ) 1216 print( 1217 f" 最大値比: {weekday_stats['max'] / holiday_stats['max']:.2f}" 1218 ) 1219 print( 1220 f" 最小値比: {weekday_stats['min'] / holiday_stats['min']:.2f}" 1221 ) 1222 else: 1223 print("十分なデータがありません") 1224 1225 def plot_diurnal_concentrations( 1226 self, 1227 df: pd.DataFrame, 1228 output_dir: str, 1229 col_ch4_conc: str = "CH4_ultra_cal", 1230 col_c2h6_conc: str = "C2H6_ultra_cal", 1231 col_datetime: str = "Date", 1232 output_filename: str = "diurnal_concentrations.png", 1233 show_std: bool = True, 1234 alpha_std: float = 0.2, 1235 add_legend: bool = True, # 凡例表示のオプションを追加 1236 print_summary: bool = True, 1237 subplot_label_ch4: str | None = None, 1238 subplot_label_c2h6: str | None = None, 1239 subplot_fontsize: int = 24, 1240 ch4_ylim: tuple[float, float] | None = None, 1241 c2h6_ylim: tuple[float, float] | None = None, 1242 interval: str = "1H", # "30min" または "1H" を指定 1243 ) -> None: 1244 """CH4とC2H6の濃度の日内変動を描画する 1245 1246 Parameters 1247 ------ 1248 df : pd.DataFrame 1249 濃度データを含むDataFrame 1250 output_dir : str 1251 出力ディレクトリのパス 1252 col_ch4_conc : str 1253 CH4濃度のカラム名 1254 col_c2h6_conc : str 1255 C2H6濃度のカラム名 1256 col_datetime : str 1257 日時カラム名 1258 output_filename : str 1259 出力ファイル名 1260 show_std : bool 1261 標準偏差を表示するかどうか 1262 alpha_std : float 1263 標準偏差の透明度 1264 add_legend : bool 1265 凡例を追加するかどうか 1266 print_summary : bool 1267 統計情報を表示するかどうか 1268 subplot_label_ch4 : str | None 1269 CH4プロットのラベル 1270 subplot_label_c2h6 : str | None 1271 C2H6プロットのラベル 1272 subplot_fontsize : int 1273 サブプロットのフォントサイズ 1274 ch4_ylim : tuple[float, float] | None 1275 CH4のy軸範囲 1276 c2h6_ylim : tuple[float, float] | None 1277 C2H6のy軸範囲 1278 interval : str 1279 時間間隔。"30min"または"1H"を指定 1280 """ 1281 # 出力ディレクトリの作成 1282 os.makedirs(output_dir, exist_ok=True) 1283 output_path: str = os.path.join(output_dir, output_filename) 1284 1285 # データの準備 1286 df = df.copy() 1287 if interval == "30min": 1288 # 30分間隔の場合、時間と30分を別々に取得 1289 df["hour"] = pd.to_datetime(df[col_datetime]).dt.hour 1290 df["minute"] = pd.to_datetime(df[col_datetime]).dt.minute 1291 df["time_bin"] = df["hour"] + df["minute"].map({0: 0, 30: 0.5}) 1292 else: 1293 # 1時間間隔の場合 1294 df["time_bin"] = pd.to_datetime(df[col_datetime]).dt.hour 1295 1296 # 時間ごとの平均値と標準偏差を計算 1297 hourly_stats = df.groupby("time_bin")[[col_ch4_conc, col_c2h6_conc]].agg( 1298 ["mean", "std"] 1299 ) 1300 1301 # 最後のデータポイントを追加(最初のデータを使用) 1302 last_point = hourly_stats.iloc[0:1].copy() 1303 last_point.index = [ 1304 hourly_stats.index[-1] + (0.5 if interval == "30min" else 1) 1305 ] 1306 hourly_stats = pd.concat([hourly_stats, last_point]) 1307 1308 # 時間軸の作成 1309 if interval == "30min": 1310 time_points = pd.date_range("2024-01-01", periods=49, freq="30min") 1311 x_ticks = [0, 6, 12, 18, 24] # 主要な時間のティック 1312 else: 1313 time_points = pd.date_range("2024-01-01", periods=25, freq="1H") 1314 x_ticks = [0, 6, 12, 18, 24] 1315 1316 # プロットの作成 1317 fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5)) 1318 1319 # CH4濃度プロット 1320 mean_ch4 = hourly_stats[col_ch4_conc]["mean"] 1321 if show_std: 1322 std_ch4 = hourly_stats[col_ch4_conc]["std"] 1323 ax1.fill_between( 1324 time_points, 1325 mean_ch4 - std_ch4, 1326 mean_ch4 + std_ch4, 1327 color="red", 1328 alpha=alpha_std, 1329 ) 1330 ch4_line = ax1.plot(time_points, mean_ch4, "red", label="CH$_4$")[0] 1331 1332 ax1.set_ylabel("CH$_4$ (ppm)") 1333 if ch4_ylim is not None: 1334 ax1.set_ylim(ch4_ylim) 1335 if subplot_label_ch4: 1336 ax1.text( 1337 0.02, 1338 0.98, 1339 subplot_label_ch4, 1340 transform=ax1.transAxes, 1341 va="top", 1342 fontsize=subplot_fontsize, 1343 ) 1344 1345 # C2H6濃度プロット 1346 mean_c2h6 = hourly_stats[col_c2h6_conc]["mean"] 1347 if show_std: 1348 std_c2h6 = hourly_stats[col_c2h6_conc]["std"] 1349 ax2.fill_between( 1350 time_points, 1351 mean_c2h6 - std_c2h6, 1352 mean_c2h6 + std_c2h6, 1353 color="orange", 1354 alpha=alpha_std, 1355 ) 1356 c2h6_line = ax2.plot(time_points, mean_c2h6, "orange", label="C$_2$H$_6$")[0] 1357 1358 ax2.set_ylabel("C$_2$H$_6$ (ppb)") 1359 if c2h6_ylim is not None: 1360 ax2.set_ylim(c2h6_ylim) 1361 if subplot_label_c2h6: 1362 ax2.text( 1363 0.02, 1364 0.98, 1365 subplot_label_c2h6, 1366 transform=ax2.transAxes, 1367 va="top", 1368 fontsize=subplot_fontsize, 1369 ) 1370 1371 # 両プロットの共通設定 1372 for ax in [ax1, ax2]: 1373 ax.set_xlabel("Time (hour)") 1374 ax.xaxis.set_major_formatter(mdates.DateFormatter("%-H")) 1375 ax.xaxis.set_major_locator(mdates.HourLocator(byhour=x_ticks)) 1376 ax.set_xlim(time_points[0], time_points[-1]) 1377 # 1時間ごとの縦線を表示 1378 ax.grid(True, which="major", alpha=0.3) 1379 # 補助目盛りは表示するが、グリッド線は表示しない 1380 # if interval == "30min": 1381 # ax.xaxis.set_minor_locator(mdates.MinuteLocator(byminute=[30])) 1382 # ax.tick_params(which='minor', length=4) 1383 1384 # 共通の凡例を図の下部に配置 1385 if add_legend: 1386 fig.legend( 1387 [ch4_line, c2h6_line], 1388 ["CH$_4$", "C$_2$H$_6$"], 1389 loc="center", 1390 bbox_to_anchor=(0.5, 0.02), 1391 ncol=2, 1392 ) 1393 plt.subplots_adjust(bottom=0.2) 1394 1395 plt.tight_layout() 1396 plt.savefig(output_path, dpi=300, bbox_inches="tight") 1397 plt.close(fig) 1398 1399 if print_summary: 1400 # 統計情報の表示 1401 for name, col in [("CH4", col_ch4_conc), ("C2H6", col_c2h6_conc)]: 1402 stats = hourly_stats[col] 1403 mean_vals = stats["mean"] 1404 1405 print(f"\n{name}濃度の日内変動統計:") 1406 print(f"最小値: {mean_vals.min():.3f} (Hour: {mean_vals.idxmin()})") 1407 print(f"最大値: {mean_vals.max():.3f} (Hour: {mean_vals.idxmax()})") 1408 print(f"平均値: {mean_vals.mean():.3f}") 1409 print(f"日内変動幅: {mean_vals.max() - mean_vals.min():.3f}") 1410 print(f"最大/最小比: {mean_vals.max() / mean_vals.min():.3f}") 1411 1412 def plot_flux_diurnal_patterns_with_std( 1413 self, 1414 df: pd.DataFrame, 1415 output_dir: str, 1416 col_ch4_flux: str = "Fch4", 1417 col_c2h6_flux: str = "Fc2h6", 1418 ch4_label: str = r"$\mathregular{CH_{4}}$フラックス", 1419 c2h6_label: str = r"$\mathregular{C_{2}H_{6}}$フラックス", 1420 col_datetime: str = "Date", 1421 output_filename: str = "diurnal_patterns.png", 1422 window_size: int = 6, # 移動平均の窓サイズ 1423 show_std: bool = True, # 標準偏差の表示有無 1424 alpha_std: float = 0.1, # 標準偏差の透明度 1425 ) -> None: 1426 """CH4とC2H6フラックスの日変化パターンをプロットする 1427 1428 Parameters 1429 ------ 1430 df : pd.DataFrame 1431 データフレーム 1432 output_dir : str 1433 出力ディレクトリのパス 1434 col_ch4_flux : str 1435 CH4フラックスのカラム名 1436 col_c2h6_flux : str 1437 C2H6フラックスのカラム名 1438 ch4_label : str 1439 CH4フラックスのラベル 1440 c2h6_label : str 1441 C2H6フラックスのラベル 1442 col_datetime : str 1443 日時カラムの名前 1444 output_filename : str 1445 出力ファイル名 1446 window_size : int 1447 移動平均の窓サイズ(デフォルト6) 1448 show_std : bool 1449 標準偏差を表示するかどうか 1450 alpha_std : float 1451 標準偏差の透明度(0-1) 1452 """ 1453 # 出力ディレクトリの作成 1454 os.makedirs(output_dir, exist_ok=True) 1455 output_path: str = os.path.join(output_dir, output_filename) 1456 1457 # # プロットのスタイル設定 1458 # plt.rcParams.update({ 1459 # 'font.size': 20, 1460 # 'axes.labelsize': 20, 1461 # 'axes.titlesize': 20, 1462 # 'xtick.labelsize': 20, 1463 # 'ytick.labelsize': 20, 1464 # 'legend.fontsize': 20, 1465 # }) 1466 1467 # 日時インデックスの処理 1468 df = df.copy() 1469 if not isinstance(df.index, pd.DatetimeIndex): 1470 df[col_datetime] = pd.to_datetime(df[col_datetime]) 1471 df.set_index(col_datetime, inplace=True) 1472 1473 # 時刻データの抽出とグループ化 1474 df["hour"] = df.index.hour 1475 hourly_means = df.groupby("hour")[[col_ch4_flux, col_c2h6_flux]].agg( 1476 ["mean", "std"] 1477 ) 1478 1479 # 24時間目のデータ点を追加(0時のデータを使用) 1480 last_hour = hourly_means.iloc[0:1].copy() 1481 last_hour.index = [24] 1482 hourly_means = pd.concat([hourly_means, last_hour]) 1483 1484 # 24時間分のデータポイントを作成 1485 time_points = pd.date_range("2024-01-01", periods=25, freq="h") 1486 1487 # プロットの作成 1488 fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5)) 1489 1490 # 移動平均の計算と描画 1491 ch4_mean = ( 1492 hourly_means[(col_ch4_flux, "mean")] 1493 .rolling(window=window_size, center=True, min_periods=1) 1494 .mean() 1495 ) 1496 c2h6_mean = ( 1497 hourly_means[(col_c2h6_flux, "mean")] 1498 .rolling(window=window_size, center=True, min_periods=1) 1499 .mean() 1500 ) 1501 1502 if show_std: 1503 ch4_std = ( 1504 hourly_means[(col_ch4_flux, "std")] 1505 .rolling(window=window_size, center=True, min_periods=1) 1506 .mean() 1507 ) 1508 c2h6_std = ( 1509 hourly_means[(col_c2h6_flux, "std")] 1510 .rolling(window=window_size, center=True, min_periods=1) 1511 .mean() 1512 ) 1513 1514 ax1.fill_between( 1515 time_points, 1516 ch4_mean - ch4_std, 1517 ch4_mean + ch4_std, 1518 color="blue", 1519 alpha=alpha_std, 1520 ) 1521 ax2.fill_between( 1522 time_points, 1523 c2h6_mean - c2h6_std, 1524 c2h6_mean + c2h6_std, 1525 color="red", 1526 alpha=alpha_std, 1527 ) 1528 1529 # メインのラインプロット 1530 ax1.plot(time_points, ch4_mean, "blue", label=ch4_label) 1531 ax2.plot(time_points, c2h6_mean, "red", label=c2h6_label) 1532 1533 # 軸の設定 1534 for ax, ylabel in [ 1535 (ax1, r"CH$_4$ (nmol m$^{-2}$ s$^{-1}$)"), 1536 (ax2, r"C$_2$H$_6$ (nmol m$^{-2}$ s$^{-1}$)"), 1537 ]: 1538 ax.set_xlabel("Time") 1539 ax.set_ylabel(ylabel) 1540 ax.xaxis.set_major_formatter(mdates.DateFormatter("%-H")) 1541 ax.xaxis.set_major_locator(mdates.HourLocator(byhour=[0, 6, 12, 18, 24])) 1542 ax.set_xlim(time_points[0], time_points[-1]) 1543 ax.grid(True, alpha=0.3) 1544 ax.legend() 1545 1546 # グラフの保存 1547 plt.tight_layout() 1548 plt.savefig(output_path, dpi=300, bbox_inches="tight") 1549 plt.close() 1550 1551 # 統計情報の表示(オプション) 1552 for col, name in [(col_ch4_flux, "CH4"), (col_c2h6_flux, "C2H6")]: 1553 mean_val = hourly_means[(col, "mean")].mean() 1554 min_val = hourly_means[(col, "mean")].min() 1555 max_val = hourly_means[(col, "mean")].max() 1556 min_time = hourly_means[(col, "mean")].idxmin() 1557 max_time = hourly_means[(col, "mean")].idxmax() 1558 1559 self.logger.info(f"{name} Statistics:") 1560 self.logger.info(f"Mean: {mean_val:.2f}") 1561 self.logger.info(f"Min: {min_val:.2f} (Hour: {min_time})") 1562 self.logger.info(f"Max: {max_val:.2f} (Hour: {max_time})") 1563 self.logger.info(f"Max/Min ratio: {max_val / min_val:.2f}\n") 1564 1565 def plot_scatter( 1566 self, 1567 df: pd.DataFrame, 1568 x_col: str, 1569 y_col: str, 1570 output_dir: str, 1571 output_filename: str = "scatter.png", 1572 xlabel: str | None = None, 1573 ylabel: str | None = None, 1574 add_label: bool = True, 1575 x_axis_range: tuple | None = None, 1576 y_axis_range: tuple | None = None, 1577 fixed_slope: float = 0.076, 1578 show_fixed_slope: bool = False, 1579 x_scientific: bool = False, # 追加:x軸を指数表記にするかどうか 1580 y_scientific: bool = False, # 追加:y軸を指数表記にするかどうか 1581 ) -> None: 1582 """散布図を作成し、TLS回帰直線を描画します。 1583 1584 Parameters 1585 ------ 1586 df : pd.DataFrame 1587 プロットに使用するデータフレーム 1588 x_col : str 1589 x軸に使用する列名 1590 y_col : str 1591 y軸に使用する列名 1592 xlabel : str 1593 x軸のラベル 1594 ylabel : str 1595 y軸のラベル 1596 output_dir : str 1597 出力先ディレクトリ 1598 output_filename : str, optional 1599 出力ファイル名。デフォルトは"scatter.png" 1600 add_label : bool, optional 1601 軸ラベルを表示するかどうか。デフォルトはTrue 1602 x_axis_range : tuple, optional 1603 x軸の範囲。デフォルトはNone。 1604 y_axis_range : tuple, optional 1605 y軸の範囲。デフォルトはNone。 1606 fixed_slope : float, optional 1607 固定傾きを指定するための値。デフォルトは0.076 1608 show_fixed_slope : bool, optional 1609 固定傾きの線を表示するかどうか。デフォルトはFalse 1610 """ 1611 os.makedirs(output_dir, exist_ok=True) 1612 output_path: str = os.path.join(output_dir, output_filename) 1613 1614 # 有効なデータの抽出 1615 df = MonthlyFiguresGenerator.get_valid_data(df, x_col, y_col) 1616 1617 # データの準備 1618 x = df[x_col].values 1619 y = df[y_col].values 1620 1621 # データの中心化 1622 x_mean = np.mean(x) 1623 y_mean = np.mean(y) 1624 x_c = x - x_mean 1625 y_c = y - y_mean 1626 1627 # TLS回帰の計算 1628 data_matrix = np.vstack((x_c, y_c)) 1629 cov_matrix = np.cov(data_matrix) 1630 _, eigenvecs = linalg.eigh(cov_matrix) 1631 largest_eigenvec = eigenvecs[:, -1] 1632 1633 slope = largest_eigenvec[1] / largest_eigenvec[0] 1634 intercept = y_mean - slope * x_mean 1635 1636 # R²とRMSEの計算 1637 y_pred = slope * x + intercept 1638 r_squared = 1 - np.sum((y - y_pred) ** 2) / np.sum((y - np.mean(y)) ** 2) 1639 rmse = np.sqrt(np.mean((y - y_pred) ** 2)) 1640 1641 # プロットの作成 1642 fig, ax = plt.subplots(figsize=(6, 6)) 1643 1644 # データ点のプロット 1645 ax.scatter(x, y, color="black") 1646 1647 # データの範囲を取得 1648 if x_axis_range is None: 1649 x_axis_range = (df[x_col].min(), df[x_col].max()) 1650 if y_axis_range is None: 1651 y_axis_range = (df[y_col].min(), df[y_col].max()) 1652 1653 # 回帰直線のプロット 1654 x_range = np.linspace(x_axis_range[0], x_axis_range[1], 150) 1655 y_range = slope * x_range + intercept 1656 ax.plot(x_range, y_range, "r", label="TLS regression") 1657 1658 # 傾き固定の線を追加(フラグがTrueの場合) 1659 if show_fixed_slope: 1660 fixed_intercept = ( 1661 y_mean - fixed_slope * x_mean 1662 ) # 中心点を通るように切片を計算 1663 y_fixed = fixed_slope * x_range + fixed_intercept 1664 ax.plot(x_range, y_fixed, "b--", label=f"Slope = {fixed_slope}", alpha=0.7) 1665 1666 # 軸の設定 1667 ax.set_xlim(x_axis_range) 1668 ax.set_ylim(y_axis_range) 1669 1670 # 指数表記の設定 1671 if x_scientific: 1672 ax.ticklabel_format(style="sci", axis="x", scilimits=(0, 0)) 1673 ax.xaxis.get_offset_text().set_position((1.1, 0)) # 指数の位置調整 1674 if y_scientific: 1675 ax.ticklabel_format(style="sci", axis="y", scilimits=(0, 0)) 1676 ax.yaxis.get_offset_text().set_position((0, 1.1)) # 指数の位置調整 1677 1678 if add_label: 1679 if xlabel is not None: 1680 ax.set_xlabel(xlabel) 1681 if ylabel is not None: 1682 ax.set_ylabel(ylabel) 1683 1684 # 1:1の関係を示す点線(軸の範囲が同じ場合のみ表示) 1685 if ( 1686 x_axis_range is not None 1687 and y_axis_range is not None 1688 and x_axis_range == y_axis_range 1689 ): 1690 ax.plot( 1691 [x_axis_range[0], x_axis_range[1]], 1692 [x_axis_range[0], x_axis_range[1]], 1693 "k--", 1694 alpha=0.5, 1695 ) 1696 1697 # 回帰情報の表示 1698 equation = ( 1699 f"y = {slope:.2f}x {'+' if intercept >= 0 else '-'} {abs(intercept):.2f}" 1700 ) 1701 position_x = 0.05 1702 fig_ha: str = "left" 1703 ax.text( 1704 position_x, 1705 0.95, 1706 equation, 1707 transform=ax.transAxes, 1708 va="top", 1709 ha=fig_ha, 1710 color="red", 1711 ) 1712 ax.text( 1713 position_x, 1714 0.88, 1715 f"R² = {r_squared:.2f}", 1716 transform=ax.transAxes, 1717 va="top", 1718 ha=fig_ha, 1719 color="red", 1720 ) 1721 ax.text( 1722 position_x, 1723 0.81, # RMSEのための新しい位置 1724 f"RMSE = {rmse:.2f}", 1725 transform=ax.transAxes, 1726 va="top", 1727 ha=fig_ha, 1728 color="red", 1729 ) 1730 # 目盛り線の設定 1731 ax.grid(True, alpha=0.3) 1732 1733 fig.savefig(output_path, dpi=300, bbox_inches="tight") 1734 plt.close(fig) 1735 1736 def plot_source_contributions_diurnal( 1737 self, 1738 df: pd.DataFrame, 1739 output_dir: str, 1740 col_ch4_flux: str, 1741 col_c2h6_flux: str, 1742 color_bio: str = "blue", 1743 color_gas: str = "red", 1744 label_gas: str = "gas", 1745 label_bio: str = "bio", 1746 flux_alpha: float = 0.6, 1747 col_datetime: str = "Date", 1748 output_filename: str = "source_contributions.png", 1749 window_size: int = 6, # 移動平均の窓サイズ 1750 print_summary: bool = True, # 統計情報を表示するかどうか, 1751 add_legend: bool = True, 1752 smooth: bool = False, 1753 y_max: float = 100, # y軸の上限値を追加 1754 subplot_label: str | None = None, 1755 subplot_fontsize: int = 20, 1756 ) -> None: 1757 """CH4フラックスの都市ガス起源と生物起源の日変化を積み上げグラフとして表示 1758 1759 Parameters 1760 ------ 1761 df : pd.DataFrame 1762 データフレーム 1763 output_dir : str 1764 出力ディレクトリのパス 1765 col_ch4_flux : str 1766 CH4フラックスのカラム名 1767 col_c2h6_flux : str 1768 C2H6フラックスのカラム名 1769 label_gas : str 1770 都市ガス起源のラベル 1771 label_bio : str 1772 生物起源のラベル 1773 col_datetime : str 1774 日時カラムの名前 1775 output_filename : str 1776 出力ファイル名 1777 window_size : int 1778 移動平均の窓サイズ 1779 print_summary : bool 1780 統計情報を表示するかどうか 1781 smooth : bool 1782 移動平均を適用するかどうか 1783 y_max : float 1784 y軸の上限値(デフォルト: 100) 1785 """ 1786 # 出力ディレクトリの作成 1787 os.makedirs(output_dir, exist_ok=True) 1788 output_path: str = os.path.join(output_dir, output_filename) 1789 1790 # 起源の計算 1791 df_with_sources = self._calculate_source_contributions( 1792 df=df, 1793 col_ch4_flux=col_ch4_flux, 1794 col_c2h6_flux=col_c2h6_flux, 1795 col_datetime=col_datetime, 1796 ) 1797 1798 # 時刻データの抽出とグループ化 1799 df_with_sources["hour"] = df_with_sources.index.hour 1800 hourly_means = df_with_sources.groupby("hour")[["ch4_gas", "ch4_bio"]].mean() 1801 1802 # 24時間目のデータ点を追加(0時のデータを使用) 1803 last_hour = hourly_means.iloc[0:1].copy() 1804 last_hour.index = [24] 1805 hourly_means = pd.concat([hourly_means, last_hour]) 1806 1807 # 移動平均の適用 1808 hourly_means_smoothed = hourly_means 1809 if smooth: 1810 hourly_means_smoothed = hourly_means.rolling( 1811 window=window_size, center=True, min_periods=1 1812 ).mean() 1813 1814 # 24時間分のデータポイントを作成 1815 time_points = pd.date_range("2024-01-01", periods=25, freq="h") 1816 1817 # プロットの作成 1818 plt.figure(figsize=(10, 6)) 1819 ax = plt.gca() 1820 1821 # サブプロットラベルの追加(subplot_labelが指定されている場合) 1822 if subplot_label: 1823 ax.text( 1824 0.02, # x位置 1825 0.98, # y位置 1826 subplot_label, 1827 transform=ax.transAxes, 1828 va="top", 1829 fontsize=subplot_fontsize, 1830 ) 1831 1832 # 積み上げプロット 1833 ax.fill_between( 1834 time_points, 1835 0, 1836 hourly_means_smoothed["ch4_bio"], 1837 color=color_bio, 1838 alpha=flux_alpha, 1839 label=label_bio, 1840 ) 1841 ax.fill_between( 1842 time_points, 1843 hourly_means_smoothed["ch4_bio"], 1844 hourly_means_smoothed["ch4_bio"] + hourly_means_smoothed["ch4_gas"], 1845 color=color_gas, 1846 alpha=flux_alpha, 1847 label=label_gas, 1848 ) 1849 1850 # 合計値のライン 1851 total_flux = hourly_means_smoothed["ch4_bio"] + hourly_means_smoothed["ch4_gas"] 1852 ax.plot(time_points, total_flux, "-", color="black", alpha=0.5) 1853 1854 # 軸の設定 1855 ax.set_xlabel("Time (hour)") 1856 ax.set_ylabel(r"CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)") 1857 ax.xaxis.set_major_formatter(mdates.DateFormatter("%-H")) 1858 ax.xaxis.set_major_locator(mdates.HourLocator(byhour=[0, 6, 12, 18, 24])) 1859 ax.set_xlim(time_points[0], time_points[-1]) 1860 ax.set_ylim(0, y_max) # y軸の範囲を設定 1861 ax.grid(True, alpha=0.3) 1862 1863 # 凡例を図の下部に配置 1864 if add_legend: 1865 handles, labels = ax.get_legend_handles_labels() 1866 fig = plt.gcf() # 現在の図を取得 1867 fig.legend( 1868 handles, 1869 labels, 1870 loc="center", 1871 bbox_to_anchor=(0.5, 0.01), 1872 ncol=len(handles), 1873 ) 1874 plt.subplots_adjust(bottom=0.2) # 下部に凡例用のスペースを確保 1875 1876 # グラフの保存 1877 plt.tight_layout() 1878 plt.savefig(output_path, dpi=300, bbox_inches="tight") 1879 plt.close() 1880 1881 # 統計情報の表示 1882 if print_summary: 1883 stats = { 1884 "都市ガス起源": hourly_means["ch4_gas"], 1885 "生物起源": hourly_means["ch4_bio"], 1886 "合計": hourly_means["ch4_gas"] + hourly_means["ch4_bio"], 1887 } 1888 1889 for source, data in stats.items(): 1890 mean_val = data.mean() 1891 min_val = data.min() 1892 max_val = data.max() 1893 min_time = data.idxmin() 1894 max_time = data.idxmax() 1895 1896 self.logger.info(f"{source}の統計:") 1897 print(f" 平均値: {mean_val:.2f}") 1898 print(f" 最小値: {min_val:.2f} (Hour: {min_time})") 1899 print(f" 最大値: {max_val:.2f} (Hour: {max_time})") 1900 if min_val != 0: 1901 print(f" 最大/最小比: {max_val / min_val:.2f}") 1902 1903 def plot_source_contributions_diurnal_by_date( 1904 self, 1905 df: pd.DataFrame, 1906 output_dir: str, 1907 col_ch4_flux: str, 1908 col_c2h6_flux: str, 1909 color_bio: str = "blue", 1910 color_gas: str = "red", 1911 label_bio: str = "bio", 1912 label_gas: str = "gas", 1913 flux_alpha: float = 0.6, 1914 col_datetime: str = "Date", 1915 output_filename: str = "source_contributions_by_date.png", 1916 add_label: bool = True, 1917 add_legend: bool = True, 1918 print_summary: bool = True, # 統計情報を表示するかどうか, 1919 subplot_fontsize: int = 20, 1920 subplot_label_weekday: str | None = None, 1921 subplot_label_weekend: str | None = None, 1922 y_max: float | None = None, # y軸の上限値 1923 ) -> None: 1924 """CH4フラックスの都市ガス起源と生物起源の日変化を平日・休日別に表示 1925 1926 Parameters 1927 ------ 1928 df : pd.DataFrame 1929 データフレーム 1930 output_dir : str 1931 出力ディレクトリのパス 1932 col_ch4_flux : str 1933 CH4フラックスのカラム名 1934 col_c2h6_flux : str 1935 C2H6フラックスのカラム名 1936 label_bio : str 1937 生物起源のラベル 1938 label_gas : str 1939 都市ガス起源のラベル 1940 col_datetime : str 1941 日時カラムの名前 1942 output_filename : str 1943 出力ファイル名 1944 add_label : bool 1945 ラベルを表示するか 1946 add_legend : bool 1947 凡例を表示するか 1948 subplot_fontsize : int 1949 サブプロットのフォントサイズ 1950 subplot_label_weekday : str | None 1951 平日グラフのラベル 1952 subplot_label_weekend : str | None 1953 休日グラフのラベル 1954 y_max : float | None 1955 y軸の上限値 1956 """ 1957 # 出力ディレクトリの作成 1958 os.makedirs(output_dir, exist_ok=True) 1959 output_path: str = os.path.join(output_dir, output_filename) 1960 1961 # 起源の計算 1962 df_with_sources = self._calculate_source_contributions( 1963 df=df, 1964 col_ch4_flux=col_ch4_flux, 1965 col_c2h6_flux=col_c2h6_flux, 1966 col_datetime=col_datetime, 1967 ) 1968 1969 # 日付タイプの分類 1970 dates = pd.to_datetime(df_with_sources.index) 1971 is_weekend = dates.dayofweek.isin([5, 6]) 1972 is_holiday = dates.map(lambda x: jpholiday.is_holiday(x.date())) 1973 is_weekday = ~(is_weekend | is_holiday) 1974 1975 # データの分類 1976 data_weekday = df_with_sources[is_weekday] 1977 data_holiday = df_with_sources[is_weekend | is_holiday] 1978 1979 # プロットの作成 1980 fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5)) 1981 1982 # 平日と休日それぞれのプロット 1983 for ax, data, label in [ 1984 (ax1, data_weekday, "Weekdays"), 1985 (ax2, data_holiday, "Weekends & Holidays"), 1986 ]: 1987 # 時間ごとの平均値を計算 1988 hourly_means = data.groupby(data.index.hour)[["ch4_gas", "ch4_bio"]].mean() 1989 1990 # 24時間目のデータ点を追加 1991 last_hour = hourly_means.iloc[0:1].copy() 1992 last_hour.index = [24] 1993 hourly_means = pd.concat([hourly_means, last_hour]) 1994 1995 # 24時間分のデータポイントを作成 1996 time_points = pd.date_range("2024-01-01", periods=25, freq="h") 1997 1998 # 積み上げプロット 1999 ax.fill_between( 2000 time_points, 2001 0, 2002 hourly_means["ch4_bio"], 2003 color=color_bio, 2004 alpha=flux_alpha, 2005 label=label_bio, 2006 ) 2007 ax.fill_between( 2008 time_points, 2009 hourly_means["ch4_bio"], 2010 hourly_means["ch4_bio"] + hourly_means["ch4_gas"], 2011 color=color_gas, 2012 alpha=flux_alpha, 2013 label=label_gas, 2014 ) 2015 2016 # 合計値のライン 2017 total_flux = hourly_means["ch4_bio"] + hourly_means["ch4_gas"] 2018 ax.plot(time_points, total_flux, "-", color="black", alpha=0.5) 2019 2020 # 軸の設定 2021 if add_label: 2022 ax.set_xlabel("Time (hour)") 2023 if ax == ax1: # 左側のプロットのラベル 2024 ax.set_ylabel("Weekdays CH$_4$ flux\n" r"(nmol m$^{-2}$ s$^{-1}$)") 2025 else: # 右側のプロットのラベル 2026 ax.set_ylabel("Weekends CH$_4$ flux\n" r"(nmol m$^{-2}$ s$^{-1}$)") 2027 2028 ax.xaxis.set_major_formatter(mdates.DateFormatter("%-H")) 2029 ax.xaxis.set_major_locator(mdates.HourLocator(byhour=[0, 6, 12, 18, 24])) 2030 ax.set_xlim(time_points[0], time_points[-1]) 2031 if y_max is not None: 2032 ax.set_ylim(0, y_max) 2033 ax.grid(True, alpha=0.3) 2034 2035 # サブプロットラベルの追加 2036 if subplot_label_weekday: 2037 ax1.text( 2038 0.02, 2039 0.98, 2040 subplot_label_weekday, 2041 transform=ax1.transAxes, 2042 va="top", 2043 fontsize=subplot_fontsize, 2044 ) 2045 if subplot_label_weekend: 2046 ax2.text( 2047 0.02, 2048 0.98, 2049 subplot_label_weekend, 2050 transform=ax2.transAxes, 2051 va="top", 2052 fontsize=subplot_fontsize, 2053 ) 2054 2055 # 凡例を図の下部に配置 2056 if add_legend: 2057 # 最初のプロットから凡例のハンドルとラベルを取得 2058 handles, labels = ax1.get_legend_handles_labels() 2059 # 図の下部に凡例を配置 2060 fig.legend( 2061 handles, 2062 labels, 2063 loc="center", 2064 bbox_to_anchor=(0.5, 0.01), # x=0.5で中央、y=0.01で下部に配置 2065 ncol=len(handles), # ハンドルの数だけ列を作成(一行に表示) 2066 ) 2067 # 凡例用のスペースを確保 2068 plt.subplots_adjust(bottom=0.2) # 下部に30%のスペースを確保 2069 2070 plt.tight_layout() 2071 plt.savefig(output_path, dpi=300, bbox_inches="tight") 2072 plt.close(fig=fig) 2073 2074 # 統計情報の表示 2075 if print_summary: 2076 for data, label in [ 2077 (data_weekday, "Weekdays"), 2078 (data_holiday, "Weekends & Holidays"), 2079 ]: 2080 hourly_means = data.groupby(data.index.hour)[ 2081 ["ch4_gas", "ch4_bio"] 2082 ].mean() 2083 2084 print(f"\n{label}の統計:") 2085 2086 # 都市ガス起源の統計 2087 gas_flux = hourly_means["ch4_gas"] 2088 bio_flux = hourly_means["ch4_bio"] 2089 2090 # 昼夜の時間帯を定義 2091 daytime_range: list[int] = [6, 19] # m~n時の場合、[m ,(n+1)]と定義 2092 daytime_hours = range(daytime_range[0], daytime_range[1]) 2093 nighttime_hours = list(range(0, daytime_range[0])) + list( 2094 range(daytime_range[1], 24) 2095 ) 2096 2097 # 昼間の統計 2098 daytime_gas = gas_flux[daytime_hours] 2099 daytime_bio = bio_flux[daytime_hours] 2100 daytime_total = daytime_gas + daytime_bio 2101 daytime_ratio = (daytime_gas.sum() / daytime_total.sum()) * 100 2102 2103 # 夜間の統計 2104 nighttime_gas = gas_flux[nighttime_hours] 2105 nighttime_bio = bio_flux[nighttime_hours] 2106 nighttime_total = nighttime_gas + nighttime_bio 2107 nighttime_ratio = (nighttime_gas.sum() / nighttime_total.sum()) * 100 2108 2109 print("\n都市ガス起源:") 2110 print(f" 平均値: {gas_flux.mean():.2f}") 2111 print(f" 最小値: {gas_flux.min():.2f} (Hour: {gas_flux.idxmin()})") 2112 print(f" 最大値: {gas_flux.max():.2f} (Hour: {gas_flux.idxmax()})") 2113 if gas_flux.min() != 0: 2114 print(f" 最大/最小比: {gas_flux.max() / gas_flux.min():.2f}") 2115 print( 2116 f" 全体に占める割合: {(gas_flux.sum() / (gas_flux.sum() + hourly_means['ch4_bio'].sum()) * 100):.1f}%" 2117 ) 2118 print( 2119 f" 昼間({daytime_range[0]}~{daytime_range[1] - 1}時)の割合: {daytime_ratio:.1f}%" 2120 ) 2121 print( 2122 f" 夜間({daytime_range[1] - 1}~{daytime_range[0]}時)の割合: {nighttime_ratio:.1f}%" 2123 ) 2124 2125 # 生物起源の統計 2126 bio_flux = hourly_means["ch4_bio"] 2127 print("\n生物起源:") 2128 print(f" 平均値: {bio_flux.mean():.2f}") 2129 print(f" 最小値: {bio_flux.min():.2f} (Hour: {bio_flux.idxmin()})") 2130 print(f" 最大値: {bio_flux.max():.2f} (Hour: {bio_flux.idxmax()})") 2131 if bio_flux.min() != 0: 2132 print(f" 最大/最小比: {bio_flux.max() / bio_flux.min():.2f}") 2133 print( 2134 f" 全体に占める割合: {(bio_flux.sum() / (gas_flux.sum() + bio_flux.sum()) * 100):.1f}%" 2135 ) 2136 2137 # 合計フラックスの統計 2138 total_flux = gas_flux + bio_flux 2139 print("\n合計:") 2140 print(f" 平均値: {total_flux.mean():.2f}") 2141 print(f" 最小値: {total_flux.min():.2f} (Hour: {total_flux.idxmin()})") 2142 print(f" 最大値: {total_flux.max():.2f} (Hour: {total_flux.idxmax()})") 2143 if total_flux.min() != 0: 2144 print(f" 最大/最小比: {total_flux.max() / total_flux.min():.2f}") 2145 2146 def plot_spectra( 2147 self, 2148 fs: float, 2149 lag_second: float, 2150 input_dir: str | Path | None, 2151 output_dir: str | Path | None, 2152 output_basename: str = "spectrum", 2153 col_ch4: str = "Ultra_CH4_ppm_C", 2154 col_c2h6: str = "Ultra_C2H6_ppb", 2155 col_tv: str = "Tv", 2156 label_ch4: str | None = None, 2157 label_c2h6: str | None = None, 2158 label_tv: str | None = None, 2159 file_pattern: str = "*.csv", 2160 markersize: float = 14, 2161 are_inputs_resampled: bool = True, 2162 save_fig: bool = True, 2163 show_fig: bool = True, 2164 plot_power: bool = True, 2165 plot_co: bool = True, 2166 add_tv_in_co: bool = True, 2167 ) -> None: 2168 """ 2169 月間の平均パワースペクトル密度を計算してプロットする。 2170 2171 データファイルを指定されたディレクトリから読み込み、パワースペクトル密度を計算し、 2172 結果を指定された出力ディレクトリにプロットして保存します。 2173 2174 Parameters 2175 ------ 2176 fs : float 2177 サンプリング周波数。 2178 lag_second : float 2179 ラグ時間(秒)。 2180 input_dir : str | Path | None 2181 データファイルが格納されているディレクトリ。 2182 output_dir : str | Path | None 2183 出力先ディレクトリ。 2184 col_ch4 : str, optional 2185 CH4の濃度データが入ったカラムのキー。デフォルトは"Ultra_CH4_ppm_C"。 2186 col_c2h6 : str, optional 2187 C2H6の濃度データが入ったカラムのキー。デフォルトは"Ultra_C2H6_ppb"。 2188 col_tv : str, optional 2189 気温データが入ったカラムのキー。デフォルトは"Tv"。 2190 label_ch4 : str | None, optional 2191 CH4のラベル。デフォルトはNone。 2192 label_c2h6 : str | None, optional 2193 C2H6のラベル。デフォルトはNone。 2194 label_tv : str | None, optional 2195 気温のラベル。デフォルトはNone。 2196 file_pattern : str, optional 2197 処理対象のファイルパターン。デフォルトは"*.csv"。 2198 markersize : float, optional 2199 プロットマーカーのサイズ。デフォルトは14。 2200 are_inputs_resampled : bool, optional 2201 入力データが再サンプリングされているかどうか。デフォルトはTrue。 2202 save_fig : bool, optional 2203 図を保存するかどうか。デフォルトはTrue。 2204 show_fig : bool, optional 2205 図を表示するかどうか。デフォルトはTrue。 2206 plot_power : bool, optional 2207 パワースペクトルをプロットするかどうか。デフォルトはTrue。 2208 plot_co : bool, optional 2209 COのスペクトルをプロットするかどうか。デフォルトはTrue。 2210 add_tv_in_co : bool, optional 2211 顕熱フラックスのコスペクトルを表示するかどうか。デフォルトはTrue。 2212 """ 2213 # 出力ディレクトリの作成 2214 if save_fig: 2215 if output_dir is None: 2216 raise ValueError( 2217 "save_fig=Trueのとき、output_dirに有効なディレクトリパスを指定する必要があります。" 2218 ) 2219 os.makedirs(output_dir, exist_ok=True) 2220 2221 # データの読み込みと結合 2222 edp = EddyDataPreprocessor(fs=fs) 2223 col_wind_w: str = EddyDataPreprocessor.WIND_W 2224 2225 # 各変数のパワースペクトルを格納する辞書 2226 power_spectra = {col_ch4: [], col_c2h6: []} 2227 co_spectra = {col_ch4: [], col_c2h6: [], col_tv: []} 2228 freqs = None 2229 2230 # プログレスバーを表示しながらファイルを処理 2231 file_list = glob.glob(os.path.join(input_dir, file_pattern)) 2232 for filepath in tqdm(file_list, desc="Processing files"): 2233 df, _ = edp.get_resampled_df( 2234 filepath=filepath, resample_in_processing=are_inputs_resampled 2235 ) 2236 2237 # 風速成分の計算を追加 2238 df = edp.add_uvw_columns(df) 2239 2240 # NaNや無限大を含む行を削除 2241 df = df.replace([np.inf, -np.inf], np.nan).dropna( 2242 subset=[col_ch4, col_c2h6, col_tv, col_wind_w] 2243 ) 2244 2245 # データが十分な行数を持っているか確認 2246 if len(df) < 100: 2247 continue 2248 2249 # 各ファイルごとにスペクトル計算 2250 calculator = SpectrumCalculator( 2251 df=df, 2252 fs=fs, 2253 ) 2254 2255 for col in power_spectra.keys(): 2256 # 各変数のパワースペクトルを計算して保存 2257 if plot_power: 2258 f, ps = calculator.calculate_power_spectrum( 2259 col=col, 2260 dimensionless=True, 2261 frequency_weighted=True, 2262 interpolate_points=True, 2263 scaling="density", 2264 ) 2265 # 最初のファイル処理時にfreqsを初期化 2266 if freqs is None: 2267 freqs = f 2268 power_spectra[col].append(ps) 2269 # 以降は周波数配列の長さが一致する場合のみ追加 2270 elif len(f) == len(freqs): 2271 power_spectra[col].append(ps) 2272 2273 # コスペクトル 2274 if plot_co: 2275 _, cs, _ = calculator.calculate_co_spectrum( 2276 col1=col_wind_w, 2277 col2=col, 2278 dimensionless=True, 2279 frequency_weighted=True, 2280 interpolate_points=True, 2281 scaling="spectrum", 2282 apply_lag_correction_to_col2=True, 2283 lag_second=lag_second, 2284 ) 2285 if freqs is not None and len(cs) == len(freqs): 2286 co_spectra[col].append(cs) 2287 2288 # 顕熱フラックスのコスペクトル計算を追加 2289 if plot_co and add_tv_in_co: 2290 _, cs_heat, _ = calculator.calculate_co_spectrum( 2291 col1=col_wind_w, 2292 col2=col_tv, 2293 dimensionless=True, 2294 frequency_weighted=True, 2295 interpolate_points=True, 2296 scaling="spectrum", 2297 ) 2298 if freqs is not None and len(cs_heat) == len(freqs): 2299 co_spectra[col_tv].append(cs_heat) 2300 2301 # 各変数のスペクトルを平均化 2302 if plot_power: 2303 averaged_power_spectra = { 2304 col: np.mean(spectra, axis=0) for col, spectra in power_spectra.items() 2305 } 2306 if plot_co: 2307 averaged_co_spectra = { 2308 col: np.mean(spectra, axis=0) for col, spectra in co_spectra.items() 2309 } 2310 # 顕熱フラックスの平均コスペクトル計算 2311 if plot_co and add_tv_in_co and co_spectra[col_tv]: 2312 averaged_heat_co_spectra = np.mean(co_spectra[col_tv], axis=0) 2313 2314 # プロット設定を修正 2315 plot_configs = [ 2316 { 2317 "col": col_ch4, 2318 "psd_ylabel": r"$fS_{\mathrm{CH_4}} / s_{\mathrm{CH_4}}^2$", 2319 "co_ylabel": r"$fC_{w\mathrm{CH_4}} / \overline{w'\mathrm{CH_4}'}$", 2320 "color": "red", 2321 "label": label_ch4, 2322 }, 2323 { 2324 "col": col_c2h6, 2325 "psd_ylabel": r"$fS_{\mathrm{C_2H_6}} / s_{\mathrm{C_2H_6}}^2$", 2326 "co_ylabel": r"$fC_{w\mathrm{C_2H_6}} / \overline{w'\mathrm{C_2H_6}'}$", 2327 "color": "orange", 2328 "label": label_c2h6, 2329 }, 2330 ] 2331 plot_tv_config = { 2332 "col": col_tv, 2333 "psd_ylabel": r"$fS_{T_v} / s_{T_v}^2$", 2334 "co_ylabel": r"$fC_{wT_v} / \overline{w'T_v'}$", 2335 "color": "blue", 2336 "label": label_tv, 2337 } 2338 2339 # パワースペクトルの図を作成 2340 if plot_power: 2341 fig_power, axes_psd = plt.subplots(1, 2, figsize=(12, 5), sharex=True) 2342 for ax, config in zip(axes_psd, plot_configs): 2343 ax.plot( 2344 freqs, 2345 averaged_power_spectra[config["col"]], 2346 "o", # マーカーを丸に設定 2347 color=config["color"], 2348 markersize=markersize, 2349 ) 2350 ax.set_xscale("log") 2351 ax.set_yscale("log") 2352 ax.set_xlim(0.001, 10) 2353 ax.plot([0.01, 10], [1, 0.01], "-", color="black", alpha=0.5) 2354 ax.text(0.1, 0.06, "-2/3", fontsize=18) 2355 ax.set_ylabel(config["psd_ylabel"]) 2356 if config["label"] is not None: 2357 ax.text( 2358 0.02, 0.98, config["label"], transform=ax.transAxes, va="top" 2359 ) 2360 ax.grid(True, alpha=0.3) 2361 ax.set_xlabel("f (Hz)") 2362 2363 plt.tight_layout() 2364 2365 if save_fig: 2366 output_path_psd: str = os.path.join( 2367 output_dir, f"power_{output_basename}.png" 2368 ) 2369 plt.savefig( 2370 output_path_psd, 2371 dpi=300, 2372 bbox_inches="tight", 2373 ) 2374 if show_fig: 2375 plt.show() 2376 else: 2377 plt.close(fig=fig_power) 2378 2379 # コスペクトルの図を作成 2380 if plot_co: 2381 fig_co, axes_cosp = plt.subplots(1, 2, figsize=(12, 5), sharex=True) 2382 for ax, config in zip(axes_cosp, plot_configs): 2383 # 顕熱フラックスのコスペクトルを先に描画(背景として) 2384 if add_tv_in_co and len(co_spectra[col_tv]) > 0: 2385 ax.plot( 2386 freqs, 2387 averaged_heat_co_spectra, 2388 "o", 2389 color="gray", 2390 alpha=0.3, 2391 markersize=markersize, 2392 label=plot_tv_config["label"] 2393 if plot_tv_config["label"] 2394 else None, 2395 ) 2396 2397 # CH4またはC2H6のコスペクトルを描画 2398 ax.plot( 2399 freqs, 2400 averaged_co_spectra[config["col"]], 2401 "o", 2402 color=config["color"], 2403 markersize=markersize, 2404 label=config["label"] if config["label"] else None, 2405 ) 2406 ax.set_xscale("log") 2407 ax.set_yscale("log") 2408 ax.set_xlim(0.001, 10) 2409 # ax.plot([0.01, 10], [1, 0.01], "-", color="black", alpha=0.5) 2410 # ax.text(0.1, 0.1, "-4/3", fontsize=18) 2411 ax.set_ylabel(config["co_ylabel"]) 2412 if config["label"] is not None: 2413 ax.text( 2414 0.02, 0.98, config["label"], transform=ax.transAxes, va="top" 2415 ) 2416 ax.grid(True, alpha=0.3) 2417 ax.set_xlabel("f (Hz)") 2418 # 凡例を追加(顕熱フラックスが含まれる場合) 2419 if add_tv_in_co and label_tv: 2420 ax.legend(loc="lower left") 2421 2422 plt.tight_layout() 2423 if save_fig: 2424 output_path_csd: str = os.path.join( 2425 output_dir, f"co_{output_basename}.png" 2426 ) 2427 plt.savefig( 2428 output_path_csd, 2429 dpi=300, 2430 bbox_inches="tight", 2431 ) 2432 if show_fig: 2433 plt.show() 2434 else: 2435 plt.close(fig=fig_co) 2436 2437 def plot_turbulence( 2438 self, 2439 df: pd.DataFrame, 2440 output_dir: str, 2441 output_filename: str = "turbulence.png", 2442 col_uz: str = "Uz", 2443 col_ch4: str = "Ultra_CH4_ppm_C", 2444 col_c2h6: str = "Ultra_C2H6_ppb", 2445 col_timestamp: str = "TIMESTAMP", 2446 add_serial_labels: bool = True, 2447 ) -> None: 2448 """時系列データのプロットを作成する 2449 2450 Parameters 2451 ------ 2452 df : pd.DataFrame 2453 プロットするデータを含むDataFrame 2454 output_dir : str 2455 出力ディレクトリのパス 2456 output_filename : str 2457 出力ファイル名 2458 col_uz : str 2459 鉛直風速データのカラム名 2460 col_ch4 : str 2461 メタンデータのカラム名 2462 col_c2h6 : str 2463 エタンデータのカラム名 2464 col_timestamp : str 2465 タイムスタンプのカラム名 2466 """ 2467 # 出力ディレクトリの作成 2468 os.makedirs(output_dir, exist_ok=True) 2469 output_path: str = os.path.join(output_dir, output_filename) 2470 2471 # データの前処理 2472 df = df.copy() 2473 2474 # タイムスタンプをインデックスに設定(まだ設定されていない場合) 2475 if not isinstance(df.index, pd.DatetimeIndex): 2476 df[col_timestamp] = pd.to_datetime(df[col_timestamp]) 2477 df.set_index(col_timestamp, inplace=True) 2478 2479 # 開始時刻と終了時刻を取得 2480 start_time = df.index[0] 2481 end_time = df.index[-1] 2482 2483 # 開始時刻の分を取得 2484 start_minute = start_time.minute 2485 2486 # 時間軸の作成(実際の開始時刻からの経過分数) 2487 minutes_elapsed = (df.index - start_time).total_seconds() / 60 2488 2489 # プロットの作成 2490 _, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=(12, 10), sharex=True) 2491 2492 # 鉛直風速 2493 ax1.plot(minutes_elapsed, df[col_uz], "k-", linewidth=0.5) 2494 ax1.set_ylabel(r"$w$ (m s$^{-1}$)") 2495 if add_serial_labels: 2496 ax1.text(0.02, 0.98, "(a)", transform=ax1.transAxes, va="top") 2497 ax1.grid(True, alpha=0.3) 2498 2499 # CH4濃度 2500 ax2.plot(minutes_elapsed, df[col_ch4], "r-", linewidth=0.5) 2501 ax2.set_ylabel(r"$\mathrm{CH_4}$ (ppm)") 2502 if add_serial_labels: 2503 ax2.text(0.02, 0.98, "(b)", transform=ax2.transAxes, va="top") 2504 ax2.grid(True, alpha=0.3) 2505 2506 # C2H6濃度 2507 ax3.plot(minutes_elapsed, df[col_c2h6], "orange", linewidth=0.5) 2508 ax3.set_ylabel(r"$\mathrm{C_2H_6}$ (ppb)") 2509 if add_serial_labels: 2510 ax3.text(0.02, 0.98, "(c)", transform=ax3.transAxes, va="top") 2511 ax3.grid(True, alpha=0.3) 2512 ax3.set_xlabel("Time (minutes)") 2513 2514 # x軸の範囲を実際の開始時刻から30分後までに設定 2515 total_minutes = (end_time - start_time).total_seconds() / 60 2516 ax3.set_xlim(0, min(30, total_minutes)) 2517 2518 # x軸の目盛りを5分間隔で設定 2519 np.arange(start_minute, start_minute + 35, 5) 2520 ax3.xaxis.set_major_locator(MultipleLocator(5)) 2521 2522 # レイアウトの調整 2523 plt.tight_layout() 2524 2525 # 図の保存 2526 plt.savefig(output_path, dpi=300, bbox_inches="tight") 2527 plt.close() 2528 2529 def plot_wind_rose_sources( 2530 self, 2531 df: pd.DataFrame, 2532 output_dir: str | Path | None = None, 2533 output_filename: str = "edp_wind_rose.png", 2534 col_datetime: str = "Date", 2535 col_ch4_flux: str = "Fch4", 2536 col_c2h6_flux: str = "Fc2h6", 2537 col_wind_dir: str = "Wind direction", 2538 flux_unit: str = r"(nmol m$^{-2}$ s$^{-1}$)", 2539 ymax: float | None = None, # フラックスの上限値 2540 color_bio: str = "blue", 2541 color_gas: str = "red", 2542 label_bio: str = "生物起源", 2543 label_gas: str = "都市ガス起源", 2544 figsize: tuple[float, float] = (8, 8), 2545 flux_alpha: float = 0.4, 2546 num_directions: int = 8, # 方位の数(8方位) 2547 gap_degrees: float = 0.0, # セクター間の隙間(度数) 2548 center_on_angles: bool = True, # 追加:45度刻みの線を境界にするかどうか 2549 subplot_label: str | None = None, 2550 add_legend: bool = True, 2551 stack_bars: bool = True, # 追加:積み上げ方式を選択するパラメータ 2552 print_summary: bool = True, # 統計情報を表示するかどうか 2553 save_fig: bool = True, 2554 show_fig: bool = True, 2555 ) -> None: 2556 """CH4フラックスの都市ガス起源と生物起源の風配図を作成する関数 2557 2558 Parameters 2559 ------ 2560 df : pd.DataFrame 2561 風配図を作成するためのデータフレーム 2562 output_dir : str | Path | None 2563 生成された図を保存するディレクトリのパス 2564 output_filename : str 2565 保存するファイル名(デフォルトは"edp_wind_rose.png") 2566 col_ch4_flux : str 2567 CH4フラックスを示すカラム名 2568 col_c2h6_flux : str 2569 C2H6フラックスを示すカラム名 2570 col_wind_dir : str 2571 風向を示すカラム名 2572 color_bio : str 2573 生物起源のフラックスに対する色 2574 color_gas : str 2575 都市ガス起源のフラックスに対する色 2576 風向を示すカラム名 2577 label_bio : str 2578 生物起源のフラックスに対するラベル 2579 label_gas : str 2580 都市ガス起源のフラックスに対するラベル 2581 col_datetime : str 2582 日時を示すカラム名 2583 num_directions : int 2584 風向の数(デフォルトは8) 2585 gap_degrees : float 2586 セクター間の隙間の大きさ(度数)。0の場合は隙間なし。 2587 center_on_angles: bool 2588 Trueの場合、45度刻みの線を境界として扇形を描画します。 2589 Falseの場合、45度の中間(22.5度)を中心として扇形を描画します。 2590 subplot_label : str 2591 サブプロットに表示するラベル 2592 print_summary : bool 2593 統計情報を表示するかどうかのフラグ 2594 flux_unit : str 2595 フラックスの単位 2596 ymax : float | None 2597 y軸の上限値(指定しない場合はデータの最大値に基づいて自動設定) 2598 figsize : tuple[float, float] 2599 図のサイズ 2600 flux_alpha : float 2601 フラックスの透明度 2602 stack_bars : bool, optional 2603 Trueの場合、生物起源の上に都市ガス起源を積み上げます(デフォルト)。 2604 Falseの場合、両方を0から積み上げます。 2605 save_fig : bool 2606 図を保存するかどうかのフラグ 2607 show_fig : bool 2608 図を表示するかどうかのフラグ 2609 """ 2610 # 起源の計算 2611 df_with_sources = self._calculate_source_contributions( 2612 df=df, 2613 col_ch4_flux=col_ch4_flux, 2614 col_c2h6_flux=col_c2h6_flux, 2615 col_datetime=col_datetime, 2616 ) 2617 2618 # 方位の定義 2619 direction_ranges = self._define_direction_ranges( 2620 num_directions, center_on_angles 2621 ) 2622 2623 # 方位ごとのデータを集計 2624 direction_data = self._aggregate_direction_data( 2625 df_with_sources, col_wind_dir, direction_ranges 2626 ) 2627 2628 # プロットの作成 2629 fig = plt.figure(figsize=figsize) 2630 ax = fig.add_subplot(111, projection="polar") 2631 2632 # 方位の角度(ラジアン)を計算 2633 theta = np.array( 2634 [np.radians(angle) for angle in direction_data["center_angle"]] 2635 ) 2636 2637 # セクターの幅を計算(隙間を考慮) 2638 sector_width = np.radians((360.0 / num_directions) - gap_degrees) 2639 2640 # 積み上げ方式に応じてプロット 2641 if stack_bars: 2642 # 生物起源を基準として描画 2643 ax.bar( 2644 theta, 2645 direction_data["bio_flux"], 2646 width=sector_width, # 隙間を考慮した幅 2647 bottom=0.0, 2648 color=color_bio, 2649 alpha=flux_alpha, 2650 label=label_bio, 2651 ) 2652 # 都市ガス起源を生物起源の上に積み上げ 2653 ax.bar( 2654 theta, 2655 direction_data["gas_flux"], 2656 width=sector_width, # 隙間を考慮した幅 2657 bottom=direction_data["bio_flux"], 2658 color=color_gas, 2659 alpha=flux_alpha, 2660 label=label_gas, 2661 ) 2662 else: 2663 # 両方を0から積み上げ 2664 ax.bar( 2665 theta, 2666 direction_data["bio_flux"], 2667 width=sector_width, # 隙間を考慮した幅 2668 bottom=0.0, 2669 color=color_bio, 2670 alpha=flux_alpha, 2671 label=label_bio, 2672 ) 2673 ax.bar( 2674 theta, 2675 direction_data["gas_flux"], 2676 width=sector_width, # 隙間を考慮した幅 2677 bottom=0.0, 2678 color=color_gas, 2679 alpha=flux_alpha, 2680 label=label_gas, 2681 ) 2682 2683 # y軸の範囲を設定 2684 if ymax is not None: 2685 ax.set_ylim(0, ymax) 2686 else: 2687 # データの最大値に基づいて自動設定 2688 max_value = max( 2689 direction_data["bio_flux"].max(), direction_data["gas_flux"].max() 2690 ) 2691 ax.set_ylim(0, max_value * 1.1) # 最大値の1.1倍を上限に設定 2692 2693 # 方位ラベルの設定 2694 ax.set_theta_zero_location("N") # 北を上に設定 2695 ax.set_theta_direction(-1) # 時計回りに設定 2696 2697 # 方位ラベルの表示 2698 labels = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"] 2699 angles = np.radians(np.linspace(0, 360, len(labels), endpoint=False)) 2700 ax.set_xticks(angles) 2701 ax.set_xticklabels(labels) 2702 2703 # プロット領域の調整(上部と下部にスペースを確保) 2704 plt.subplots_adjust( 2705 top=0.8, # 上部に20%のスペースを確保 2706 bottom=0.2, # 下部に20%のスペースを確保(凡例用) 2707 ) 2708 2709 # サブプロットラベルの追加(デフォルトは左上) 2710 if subplot_label: 2711 ax.text( 2712 0.01, 2713 0.99, 2714 subplot_label, 2715 transform=ax.transAxes, 2716 ) 2717 2718 # 単位の追加(図の下部中央に配置) 2719 plt.figtext( 2720 0.5, # x位置(中央) 2721 0.1, # y位置(下部) 2722 flux_unit, 2723 ha="center", # 水平方向の位置揃え 2724 va="bottom", # 垂直方向の位置揃え 2725 ) 2726 2727 # 凡例の追加(単位の下に配置) 2728 if add_legend: 2729 # 最初のプロットから凡例のハンドルとラベルを取得 2730 handles, labels = ax.get_legend_handles_labels() 2731 # 図の下部に凡例を配置 2732 fig.legend( 2733 handles, 2734 labels, 2735 loc="center", 2736 bbox_to_anchor=(0.5, 0.05), # x=0.5で中央、y=0.05で下部に配置 2737 ncol=len(handles), # ハンドルの数だけ列を作成(一行に表示) 2738 ) 2739 2740 # グラフの保存 2741 if save_fig: 2742 if output_dir is None: 2743 raise ValueError( 2744 "save_fig=Trueのとき、output_dirに有効なパスを指定する必要があります。" 2745 ) 2746 # 出力ディレクトリの作成 2747 os.makedirs(output_dir, exist_ok=True) 2748 output_path: str = os.path.join(output_dir, output_filename) 2749 plt.savefig(output_path, dpi=300, bbox_inches="tight") 2750 2751 # グラフの表示 2752 if show_fig: 2753 plt.show() 2754 else: 2755 plt.close(fig=fig) 2756 2757 # 統計情報の表示 2758 if print_summary: 2759 for source in ["gas", "bio"]: 2760 flux_data = direction_data[f"{source}_flux"] 2761 mean_val = flux_data.mean() 2762 max_val = flux_data.max() 2763 max_dir = direction_data.loc[flux_data.idxmax(), "name"] 2764 2765 self.logger.info( 2766 f"{label_gas if source == 'gas' else label_bio}の統計:" 2767 ) 2768 print(f" 平均フラックス: {mean_val:.2f}") 2769 print(f" 最大フラックス: {max_val:.2f}") 2770 print(f" 最大フラックスの方位: {max_dir}") 2771 2772 def _define_direction_ranges( 2773 self, 2774 num_directions: int = 8, 2775 center_on_angles: bool = False, 2776 ) -> pd.DataFrame: 2777 """方位の範囲を定義 2778 2779 Parameters 2780 ------ 2781 num_directions : int 2782 方位の数(デフォルトは8) 2783 center_on_angles : bool 2784 Trueの場合、45度刻みの線を境界として扇形を描画します。 2785 Falseの場合、45度の中間(22.5度)を中心として扇形を描画します。 2786 2787 Returns 2788 ------ 2789 pd.DataFrame 2790 方位の定義を含むDataFrame 2791 """ 2792 if num_directions == 8: 2793 if center_on_angles: 2794 # 45度刻みの線を境界とする場合 2795 directions = pd.DataFrame( 2796 { 2797 "name": ["N", "NE", "E", "SE", "S", "SW", "W", "NW"], 2798 "center_angle": [ 2799 22.5, 2800 67.5, 2801 112.5, 2802 157.5, 2803 202.5, 2804 247.5, 2805 292.5, 2806 337.5, 2807 ], 2808 } 2809 ) 2810 else: 2811 # 従来通り45度を中心とする場合 2812 directions = pd.DataFrame( 2813 { 2814 "name": ["N", "NE", "E", "SE", "S", "SW", "W", "NW"], 2815 "center_angle": [0, 45, 90, 135, 180, 225, 270, 315], 2816 } 2817 ) 2818 else: 2819 raise ValueError(f"現在{num_directions}方位はサポートされていません") 2820 2821 # 各方位の範囲を計算 2822 angle_range = 360 / num_directions 2823 directions["start_angle"] = directions["center_angle"] - angle_range / 2 2824 directions["end_angle"] = directions["center_angle"] + angle_range / 2 2825 2826 # -180度から180度の範囲に正規化 2827 directions["start_angle"] = np.where( 2828 directions["start_angle"] > 180, 2829 directions["start_angle"] - 360, 2830 directions["start_angle"], 2831 ) 2832 directions["end_angle"] = np.where( 2833 directions["end_angle"] > 180, 2834 directions["end_angle"] - 360, 2835 directions["end_angle"], 2836 ) 2837 2838 return directions 2839 2840 def _aggregate_direction_data( 2841 self, 2842 df: pd.DataFrame, 2843 col_wind_dir: str, 2844 direction_ranges: pd.DataFrame, 2845 ) -> pd.DataFrame: 2846 """方位ごとのフラックスデータを集計 2847 2848 Parameters 2849 ------ 2850 df : pd.DataFrame 2851 ソース分離済みのデータフレーム 2852 col_wind_dir : str 2853 風向のカラム名 2854 direction_ranges : pd.DataFrame 2855 方位の定義 2856 2857 Returns 2858 ------ 2859 pd.DataFrame 2860 方位ごとの集計データ 2861 """ 2862 result_data = direction_ranges.copy() 2863 result_data["gas_flux"] = 0.0 2864 result_data["bio_flux"] = 0.0 2865 2866 for idx, row in direction_ranges.iterrows(): 2867 if row["start_angle"] < row["end_angle"]: 2868 mask = (df[col_wind_dir] > row["start_angle"]) & ( 2869 df[col_wind_dir] <= row["end_angle"] 2870 ) 2871 else: # 北方向など、-180度と180度をまたぐ場合 2872 mask = (df[col_wind_dir] > row["start_angle"]) | ( 2873 df[col_wind_dir] <= row["end_angle"] 2874 ) 2875 2876 result_data.loc[idx, "gas_flux"] = df.loc[mask, "ch4_gas"].mean() 2877 result_data.loc[idx, "bio_flux"] = df.loc[mask, "ch4_bio"].mean() 2878 2879 # NaNを0に置換 2880 result_data = result_data.fillna(0) 2881 2882 return result_data 2883 2884 def _calculate_source_contributions( 2885 self, 2886 df: pd.DataFrame, 2887 col_ch4_flux: str, 2888 col_c2h6_flux: str, 2889 gas_ratio_c1c2: float = 0.076, 2890 col_datetime: str = "Date", 2891 ) -> pd.DataFrame: 2892 """ 2893 CH4フラックスの都市ガス起源と生物起源の寄与を計算する。 2894 このロジックでは、燃焼起源のCH4フラックスは考慮せず計算している。 2895 2896 Parameters 2897 ------ 2898 df : pd.DataFrame 2899 入力データフレーム 2900 col_ch4_flux : str 2901 CH4フラックスのカラム名 2902 col_c2h6_flux : str 2903 C2H6フラックスのカラム名 2904 gas_ratio_c1c2 : float 2905 ガスのC2H6/CH4比(ppb/ppb) 2906 col_datetime : str 2907 日時カラムの名前 2908 2909 Returns 2910 ------ 2911 pd.DataFrame 2912 起源別のフラックス値を含むデータフレーム 2913 """ 2914 df_copied = df.copy() 2915 2916 # 日時インデックスの処理 2917 if not isinstance(df_copied.index, pd.DatetimeIndex): 2918 df_copied[col_datetime] = pd.to_datetime(df_copied[col_datetime]) 2919 df_copied.set_index(col_datetime, inplace=True) 2920 2921 # C2H6/CH4比の計算 2922 df_copied["c2c1_ratio"] = ( 2923 df_copied[col_c2h6_flux] / df_copied[col_ch4_flux] 2924 ) 2925 2926 # 都市ガスの標準組成に基づく都市ガス比率の計算 2927 df_copied["gas_ratio"] = df_copied["c2c1_ratio"] / gas_ratio_c1c2 * 100 2928 2929 # gas_ratioに基づいて都市ガス起源と生物起源の寄与を比例配分 2930 df_copied["ch4_gas"] = df_copied[col_ch4_flux] * np.clip( 2931 df_copied["gas_ratio"] / 100, 0, 1 2932 ) 2933 df_copied["ch4_bio"] = df_copied[col_ch4_flux] * ( 2934 1 - np.clip(df_copied["gas_ratio"] / 100, 0, 1) 2935 ) 2936 2937 return df_copied 2938 2939 def _prepare_diurnal_data( 2940 self, 2941 df: pd.DataFrame, 2942 target_columns: list[str], 2943 include_date_types: bool = False, 2944 ) -> tuple[dict[str, pd.DataFrame], pd.DatetimeIndex]: 2945 """ 2946 日変化パターンの計算に必要なデータを準備する。 2947 2948 Parameters 2949 ------ 2950 df : pd.DataFrame 2951 入力データフレーム 2952 target_columns : list[str] 2953 計算対象の列名のリスト 2954 include_date_types : bool 2955 日付タイプ(平日/休日など)の分類を含めるかどうか 2956 2957 Returns 2958 ------ 2959 tuple[dict[str, pd.DataFrame], pd.DatetimeIndex] 2960 - 時間帯ごとの平均値を含むDataFrameの辞書 2961 - 24時間分の時間点 2962 """ 2963 df = df.copy() 2964 df["hour"] = pd.to_datetime(df["Date"]).dt.hour 2965 2966 # 時間ごとの平均値を計算する関数 2967 def calculate_hourly_means(data_df, condition=None): 2968 if condition is not None: 2969 data_df = data_df[condition] 2970 return data_df.groupby("hour")[target_columns].mean().reset_index() 2971 2972 # 基本の全日データを計算 2973 hourly_means = {"all": calculate_hourly_means(df)} 2974 2975 # 日付タイプによる分類が必要な場合 2976 if include_date_types: 2977 dates = pd.to_datetime(df["Date"]) 2978 is_weekend = dates.dt.dayofweek.isin([5, 6]) 2979 is_holiday = dates.map(lambda x: jpholiday.is_holiday(x.date())) 2980 is_weekday = ~(is_weekend | is_holiday) 2981 2982 hourly_means.update( 2983 { 2984 "weekday": calculate_hourly_means(df, is_weekday), 2985 "weekend": calculate_hourly_means(df, is_weekend), 2986 "holiday": calculate_hourly_means(df, is_weekend | is_holiday), 2987 } 2988 ) 2989 2990 # 24時目のデータを追加 2991 for col in hourly_means: 2992 last_row = hourly_means[col].iloc[0:1].copy() 2993 last_row["hour"] = 24 2994 hourly_means[col] = pd.concat( 2995 [hourly_means[col], last_row], ignore_index=True 2996 ) 2997 2998 # 24時間分のデータポイントを作成 2999 time_points = pd.date_range("2024-01-01", periods=25, freq="h") 3000 3001 return hourly_means, time_points 3002 3003 def _setup_diurnal_axes( 3004 self, 3005 ax: plt.Axes, 3006 time_points: pd.DatetimeIndex, 3007 ylabel: str, 3008 subplot_label: str | None = None, 3009 add_label: bool = True, 3010 add_legend: bool = True, 3011 subplot_fontsize: int = 20, 3012 ) -> None: 3013 """日変化プロットの軸の設定を行う 3014 3015 Parameters 3016 ------ 3017 ax : plt.Axes 3018 設定対象の軸 3019 time_points : pd.DatetimeIndex 3020 時間軸のポイント 3021 ylabel : str 3022 y軸のラベル 3023 subplot_label : str | None 3024 サブプロットのラベル 3025 add_label : bool 3026 軸ラベルを表示するかどうか 3027 add_legend : bool 3028 凡例を表示するかどうか 3029 subplot_fontsize : int 3030 サブプロットのフォントサイズ 3031 """ 3032 if add_label: 3033 ax.set_xlabel("Time (hour)") 3034 ax.set_ylabel(ylabel) 3035 3036 ax.xaxis.set_major_formatter(mdates.DateFormatter("%-H")) 3037 ax.xaxis.set_major_locator(mdates.HourLocator(byhour=[0, 6, 12, 18, 24])) 3038 ax.set_xlim(time_points[0], time_points[-1]) 3039 ax.set_xticks(time_points[::6]) 3040 ax.set_xticklabels(["0", "6", "12", "18", "24"]) 3041 3042 if subplot_label: 3043 ax.text( 3044 0.02, 3045 0.98, 3046 subplot_label, 3047 transform=ax.transAxes, 3048 va="top", 3049 fontsize=subplot_fontsize, 3050 ) 3051 3052 if add_legend: 3053 ax.legend() 3054 3055 @staticmethod 3056 def get_valid_data(df: pd.DataFrame, x_col: str, y_col: str) -> pd.DataFrame: 3057 """ 3058 指定された列の有効なデータ(NaNを除いた)を取得します。 3059 3060 Parameters 3061 ------ 3062 df : pd.DataFrame 3063 データフレーム 3064 x_col : str 3065 X軸の列名 3066 y_col : str 3067 Y軸の列名 3068 3069 Returns 3070 ------ 3071 pd.DataFrame 3072 有効なデータのみを含むDataFrame 3073 """ 3074 return df.copy().dropna(subset=[x_col, y_col]) 3075 3076 @staticmethod 3077 def setup_logger(logger: Logger | None, log_level: int = INFO) -> Logger: 3078 """ 3079 ロガーを設定します。 3080 3081 このメソッドは、ロギングの設定を行い、ログメッセージのフォーマットを指定します。 3082 ログメッセージには、日付、ログレベル、メッセージが含まれます。 3083 3084 渡されたロガーがNoneまたは不正な場合は、新たにロガーを作成し、標準出力に 3085 ログメッセージが表示されるようにStreamHandlerを追加します。ロガーのレベルは 3086 引数で指定されたlog_levelに基づいて設定されます。 3087 3088 Parameters 3089 ------ 3090 logger : Logger | None 3091 使用するロガー。Noneの場合は新しいロガーを作成します。 3092 log_level : int 3093 ロガーのログレベル。デフォルトはINFO。 3094 3095 Returns 3096 ------ 3097 Logger 3098 設定されたロガーオブジェクト。 3099 """ 3100 if logger is not None and isinstance(logger, Logger): 3101 return logger 3102 # 渡されたロガーがNoneまたは正しいものでない場合は独自に設定 3103 new_logger: Logger = getLogger() 3104 # 既存のハンドラーをすべて削除 3105 for handler in new_logger.handlers[:]: 3106 new_logger.removeHandler(handler) 3107 new_logger.setLevel(log_level) # ロガーのレベルを設定 3108 ch = StreamHandler() 3109 ch_formatter = Formatter("%(asctime)s - %(levelname)s - %(message)s") 3110 ch.setFormatter(ch_formatter) # フォーマッターをハンドラーに設定 3111 new_logger.addHandler(ch) # StreamHandlerの追加 3112 return new_logger 3113 3114 @staticmethod 3115 def plot_fluxes_distributions( 3116 flux_data: dict[str, pd.Series], 3117 month: int, 3118 output_dir: str | Path | None = None, 3119 output_filename: str = "flux_distribution.png", 3120 colors: dict[str, str] | None = None, 3121 xlim: tuple[float, float] = (-50, 200), 3122 bandwidth: float = 1.0, 3123 save_fig: bool = True, 3124 show_fig: bool = True, 3125 ) -> None: 3126 """複数のフラックスデータの分布を可視化 3127 3128 Parameters 3129 ------ 3130 flux_data : dict[str, pd.Series] 3131 各測器のフラックスデータを格納した辞書 3132 キー: 測器名, 値: フラックスデータ 3133 month : int 3134 測定月 3135 output_dir : str | Path | None 3136 出力ディレクトリ。指定しない場合はデフォルトのディレクトリに保存されます。 3137 output_filename : str 3138 出力ファイル名。デフォルトは"flux_distribution.png"です。 3139 colors : dict[str, str] | None 3140 各測器の色を指定する辞書。指定がない場合は自動で色を割り当てます。 3141 xlim : tuple[float, float] 3142 x軸の範囲。デフォルトは(-50, 200)です。 3143 bandwidth : float 3144 カーネル密度推定のバンド幅調整係数。デフォルトは1.0です。 3145 save_fig : bool 3146 図を保存するかどうか。デフォルトはTrueです。 3147 show_fig : bool 3148 図を表示するかどうか。デフォルトはTrueです。 3149 """ 3150 # デフォルトの色を設定 3151 default_colors = plt.rcParams["axes.prop_cycle"].by_key()["color"] 3152 if colors is None: 3153 colors = { 3154 name: default_colors[i % len(default_colors)] 3155 for i, name in enumerate(flux_data.keys()) 3156 } 3157 3158 fig = plt.figure(figsize=(10, 6)) 3159 3160 # 統計情報を格納する辞書 3161 stats_info = {} 3162 3163 # 各測器のデータをプロット 3164 for i, (name, flux) in enumerate(flux_data.items()): 3165 # nanを除去 3166 flux = flux.dropna() 3167 color = colors.get(name, default_colors[i % len(default_colors)]) 3168 3169 # KDEプロット 3170 sns.kdeplot( 3171 data=flux, 3172 label=name, 3173 color=color, 3174 alpha=0.5, 3175 bw_adjust=bandwidth, 3176 ) 3177 3178 # 平均値と中央値のマーカー 3179 mean_val = flux.mean() 3180 median_val = np.median(flux) 3181 plt.axvline( 3182 mean_val, 3183 color=color, 3184 linestyle="--", 3185 alpha=0.5, 3186 label=f"{name} mean", 3187 ) 3188 plt.axvline( 3189 median_val, 3190 color=color, 3191 linestyle=":", 3192 alpha=0.5, 3193 label=f"{name} median", 3194 ) 3195 3196 # 統計情報を保存 3197 stats_info[name] = { 3198 "mean": mean_val, 3199 "median": median_val, 3200 "std": flux.std(), 3201 } 3202 3203 # 軸ラベルとタイトル 3204 plt.xlabel(r"CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)") 3205 plt.ylabel("Probability Density") 3206 plt.title(f"Distribution of CH$_4$ fluxes - Month {month}") 3207 3208 # x軸の範囲設定 3209 plt.xlim(xlim) 3210 3211 # グリッド表示 3212 plt.grid(True, alpha=0.3) 3213 3214 # 統計情報のテキスト作成 3215 stats_text = "" 3216 for name, stats_item in stats_info.items(): 3217 stats_text += ( 3218 f"{name}:\n" 3219 f" Mean: {stats_item['mean']:.2f}\n" 3220 f" Median: {stats_item['median']:.2f}\n" 3221 f" Std: {stats_item['std']:.2f}\n" 3222 ) 3223 3224 # 統計情報の表示 3225 plt.text( 3226 0.02, 3227 0.98, 3228 stats_text.rstrip(), # 最後の改行を削除 3229 transform=plt.gca().transAxes, 3230 verticalalignment="top", 3231 fontsize=10, 3232 bbox=dict(boxstyle="round", facecolor="white", alpha=0.8), 3233 ) 3234 3235 # 凡例の表示 3236 plt.legend(loc="upper right") 3237 plt.tight_layout() 3238 3239 # グラフの保存 3240 if save_fig: 3241 if output_dir is None: 3242 raise ValueError( 3243 "save_fig=Trueのとき、output_dirに有効なディレクトリパスを指定する必要があります。" 3244 ) 3245 os.makedirs(output_dir, exist_ok=True) 3246 plt.savefig( 3247 os.path.join(output_dir, f"{output_filename.format(month=month)}"), 3248 dpi=300, 3249 bbox_inches="tight", 3250 ) 3251 if show_fig: 3252 plt.show() 3253 else: 3254 plt.close(fig=fig)
65 def __init__( 66 self, 67 logger: Logger | None = None, 68 logging_debug: bool = False, 69 ) -> None: 70 """ 71 クラスのコンストラクタ 72 73 Parameters 74 ------ 75 logger : Logger | None 76 使用するロガー。Noneの場合は新しいロガーを作成します。 77 logging_debug : bool 78 ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。 79 """ 80 # ロガー 81 log_level: int = INFO 82 if logging_debug: 83 log_level = DEBUG 84 self.logger: Logger = MonthlyFiguresGenerator.setup_logger(logger, log_level)
クラスのコンストラクタ
Parameters
logger : Logger | None
使用するロガー。Noneの場合は新しいロガーを作成します。
logging_debug : bool
ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。
86 def plot_c1c2_fluxes_timeseries( 87 self, 88 df, 89 output_dir: str, 90 output_filename: str = "timeseries.png", 91 col_datetime: str = "Date", 92 col_c1_flux: str = "Fch4_ultra", 93 col_c2_flux: str = "Fc2h6_ultra", 94 ): 95 """ 96 月別のフラックスデータを時系列プロットとして出力する 97 98 Parameters 99 ------ 100 df : pd.DataFrame 101 月別データを含むDataFrame 102 output_dir : str 103 出力ファイルを保存するディレクトリのパス 104 output_filename : str 105 出力ファイルの名前 106 col_datetime : str 107 日付を含む列の名前。デフォルトは"Date"。 108 col_c1_flux : str 109 CH4フラックスを含む列の名前。デフォルトは"Fch4_ultra"。 110 col_c2_flux : str 111 C2H6フラックスを含む列の名前。デフォルトは"Fc2h6_ultra"。 112 """ 113 os.makedirs(output_dir, exist_ok=True) 114 output_path: str = os.path.join(output_dir, output_filename) 115 116 # 図の作成 117 _, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 8), sharex=True) 118 119 # CH4フラックスのプロット 120 ax1.scatter(df[col_datetime], df[col_c1_flux], color="red", alpha=0.5, s=20) 121 ax1.set_ylabel(r"CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)") 122 ax1.set_ylim(-100, 600) 123 ax1.text(0.02, 0.98, "(a)", transform=ax1.transAxes, va="top", fontsize=20) 124 ax1.grid(True, alpha=0.3) 125 126 # C2H6フラックスのプロット 127 ax2.scatter( 128 df[col_datetime], 129 df[col_c2_flux], 130 color="orange", 131 alpha=0.5, 132 s=20, 133 ) 134 ax2.set_ylabel(r"C$_2$H$_6$ flux (nmol m$^{-2}$ s$^{-1}$)") 135 ax2.set_ylim(-20, 60) 136 ax2.text(0.02, 0.98, "(b)", transform=ax2.transAxes, va="top", fontsize=20) 137 ax2.grid(True, alpha=0.3) 138 139 # x軸の設定 140 ax2.xaxis.set_major_locator(mdates.MonthLocator(interval=1)) 141 ax2.xaxis.set_major_formatter(mdates.DateFormatter("%m")) 142 plt.setp(ax2.get_xticklabels(), rotation=0, ha="right") 143 ax2.set_xlabel("Month") 144 145 # 図の保存 146 plt.savefig(output_path, dpi=300, bbox_inches="tight") 147 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"。
149 def plot_c1c2_concentrations_and_fluxes_timeseries( 150 self, 151 df: pd.DataFrame, 152 output_dir: str, 153 output_filename: str = "conc_flux_timeseries.png", 154 col_datetime: str = "Date", 155 col_ch4_conc: str = "CH4_ultra", 156 col_ch4_flux: str = "Fch4_ultra", 157 col_c2h6_conc: str = "C2H6_ultra", 158 col_c2h6_flux: str = "Fc2h6_ultra", 159 print_summary: bool = True, 160 ) -> None: 161 """ 162 CH4とC2H6の濃度とフラックスの時系列プロットを作成する 163 164 Parameters 165 ------ 166 df : pd.DataFrame 167 月別データを含むDataFrame 168 output_dir : str 169 出力ディレクトリのパス 170 output_filename : str 171 出力ファイル名 172 col_datetime : str 173 日付列の名前 174 col_ch4_conc : str 175 CH4濃度列の名前 176 col_ch4_flux : str 177 CH4フラックス列の名前 178 col_c2h6_conc : str 179 C2H6濃度列の名前 180 col_c2h6_flux : str 181 C2H6フラックス列の名前 182 print_summary : bool 183 解析情報をprintするかどうか 184 """ 185 # 出力ディレクトリの作成 186 os.makedirs(output_dir, exist_ok=True) 187 output_path: str = os.path.join(output_dir, output_filename) 188 189 if print_summary: 190 # 統計情報の計算と表示 191 for name, col in [ 192 ("CH4 concentration", col_ch4_conc), 193 ("CH4 flux", col_ch4_flux), 194 ("C2H6 concentration", col_c2h6_conc), 195 ("C2H6 flux", col_c2h6_flux), 196 ]: 197 # NaNを除外してから統計量を計算 198 valid_data = df[col].dropna() 199 200 if len(valid_data) > 0: 201 percentile_5 = np.nanpercentile(valid_data, 5) 202 percentile_95 = np.nanpercentile(valid_data, 95) 203 mean_value = np.nanmean(valid_data) 204 positive_ratio = (valid_data > 0).mean() * 100 205 206 print(f"\n{name}:") 207 print( 208 f"90パーセンタイルレンジ: {percentile_5:.2f} - {percentile_95:.2f}" 209 ) 210 print(f"平均値: {mean_value:.2f}") 211 print(f"正の値の割合: {positive_ratio:.1f}%") 212 else: 213 print(f"\n{name}: データが存在しません") 214 215 # プロットの作成 216 _, (ax1, ax2, ax3, ax4) = plt.subplots(4, 1, figsize=(12, 16), sharex=True) 217 218 # CH4濃度のプロット 219 ax1.scatter(df[col_datetime], df[col_ch4_conc], color="red", alpha=0.5, s=20) 220 ax1.set_ylabel("CH$_4$ Concentration\n(ppm)") 221 ax1.set_ylim(1.8, 2.6) 222 ax1.text(0.02, 0.98, "(a)", transform=ax1.transAxes, va="top", fontsize=20) 223 ax1.grid(True, alpha=0.3) 224 225 # CH4フラックスのプロット 226 ax2.scatter(df[col_datetime], df[col_ch4_flux], color="red", alpha=0.5, s=20) 227 ax2.set_ylabel("CH$_4$ flux\n(nmol m$^{-2}$ s$^{-1}$)") 228 ax2.set_ylim(-100, 600) 229 # ax2.set_yticks([-100, 0, 200, 400, 600]) 230 ax2.text(0.02, 0.98, "(b)", transform=ax2.transAxes, va="top", fontsize=20) 231 ax2.grid(True, alpha=0.3) 232 233 # C2H6濃度のプロット 234 ax3.scatter( 235 df[col_datetime], df[col_c2h6_conc], color="orange", alpha=0.5, s=20 236 ) 237 ax3.set_ylabel("C$_2$H$_6$ Concentration\n(ppb)") 238 ax3.text(0.02, 0.98, "(c)", transform=ax3.transAxes, va="top", fontsize=20) 239 ax3.grid(True, alpha=0.3) 240 241 # C2H6フラックスのプロット 242 ax4.scatter( 243 df[col_datetime], df[col_c2h6_flux], color="orange", alpha=0.5, s=20 244 ) 245 ax4.set_ylabel("C$_2$H$_6$ flux\n(nmol m$^{-2}$ s$^{-1}$)") 246 ax4.set_ylim(-20, 40) 247 ax4.text(0.02, 0.98, "(d)", transform=ax4.transAxes, va="top", fontsize=20) 248 ax4.grid(True, alpha=0.3) 249 250 # x軸の設定 251 ax4.xaxis.set_major_locator(mdates.MonthLocator(interval=1)) 252 ax4.xaxis.set_major_formatter(mdates.DateFormatter("%m")) 253 plt.setp(ax4.get_xticklabels(), rotation=0, ha="right") 254 ax4.set_xlabel("Month") 255 256 # レイアウトの調整と保存 257 plt.tight_layout() 258 plt.savefig(output_path, dpi=300, bbox_inches="tight") 259 plt.close() 260 261 if print_summary: 262 263 def analyze_top_values(df, column_name, top_percent=20): 264 print(f"\n{column_name}の上位{top_percent}%の分析:") 265 266 # DataFrameのコピーを作成し、日時関連の列を追加 267 df_analysis = df.copy() 268 df_analysis["hour"] = pd.to_datetime(df_analysis[col_datetime]).dt.hour 269 df_analysis["month"] = pd.to_datetime( 270 df_analysis[col_datetime] 271 ).dt.month 272 df_analysis["weekday"] = pd.to_datetime( 273 df_analysis[col_datetime] 274 ).dt.dayofweek 275 276 # 上位20%のしきい値を計算 277 threshold = df[column_name].quantile(1 - top_percent / 100) 278 high_values = df_analysis[df_analysis[column_name] > threshold] 279 280 # 月ごとの分析 281 print("\n月別分布:") 282 monthly_counts = high_values.groupby("month").size() 283 total_counts = df_analysis.groupby("month").size() 284 monthly_percentages = (monthly_counts / total_counts * 100).round(1) 285 286 # 月ごとのデータを安全に表示 287 available_months = set(monthly_counts.index) & set(total_counts.index) 288 for month in sorted(available_months): 289 print( 290 f"月{month}: {monthly_percentages[month]}% ({monthly_counts[month]}件/{total_counts[month]}件)" 291 ) 292 293 # 時間帯ごとの分析(3時間区切り) 294 print("\n時間帯別分布:") 295 # copyを作成して新しい列を追加 296 high_values = high_values.copy() 297 high_values["time_block"] = high_values["hour"] // 3 * 3 298 time_blocks = high_values.groupby("time_block").size() 299 total_time_blocks = df_analysis.groupby( 300 df_analysis["hour"] // 3 * 3 301 ).size() 302 time_percentages = (time_blocks / total_time_blocks * 100).round(1) 303 304 # 時間帯ごとのデータを安全に表示 305 available_blocks = set(time_blocks.index) & set(total_time_blocks.index) 306 for block in sorted(available_blocks): 307 print( 308 f"{block:02d}:00-{block + 3:02d}:00: {time_percentages[block]}% ({time_blocks[block]}件/{total_time_blocks[block]}件)" 309 ) 310 311 # 曜日ごとの分析 312 print("\n曜日別分布:") 313 weekday_names = ["月曜", "火曜", "水曜", "木曜", "金曜", "土曜", "日曜"] 314 weekday_counts = high_values.groupby("weekday").size() 315 total_weekdays = df_analysis.groupby("weekday").size() 316 weekday_percentages = (weekday_counts / total_weekdays * 100).round(1) 317 318 # 曜日ごとのデータを安全に表示 319 available_days = set(weekday_counts.index) & set(total_weekdays.index) 320 for day in sorted(available_days): 321 if 0 <= day <= 6: # 有効な曜日インデックスのチェック 322 print( 323 f"{weekday_names[day]}: {weekday_percentages[day]}% ({weekday_counts[day]}件/{total_weekdays[day]}件)" 324 ) 325 326 # 濃度とフラックスそれぞれの分析を実行 327 print("\n=== 上位値の時間帯・曜日分析 ===") 328 analyze_top_values(df, col_ch4_conc) 329 analyze_top_values(df, col_ch4_flux) 330 analyze_top_values(df, col_c2h6_conc) 331 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するかどうか
333 def plot_c1c2_timeseries( 334 self, 335 df: pd.DataFrame, 336 output_dir: str, 337 col_ch4_flux: str, 338 col_c2h6_flux: str, 339 output_filename: str = "timeseries_year.png", 340 col_datetime: str = "Date", 341 window_size: int = 24 * 7, # 1週間の移動平均のデフォルト値 342 confidence_interval: float = 0.95, # 95%信頼区間 343 subplot_label_ch4: str | None = "(a)", 344 subplot_label_c2h6: str | None = "(b)", 345 subplot_fontsize: int = 20, 346 show_ci: bool = True, 347 ch4_ylim: tuple[float, float] | None = None, 348 c2h6_ylim: tuple[float, float] | None = None, 349 start_date: str | None = None, # 追加:"YYYY-MM-DD"形式 350 end_date: str | None = None, # 追加:"YYYY-MM-DD"形式 351 figsize: tuple[float, float] = (16, 6), 352 ) -> None: 353 """CH4とC2H6フラックスの時系列変動をプロット 354 355 Parameters 356 ------ 357 df : pd.DataFrame 358 データフレーム 359 output_dir : str 360 出力ディレクトリのパス 361 col_ch4_flux : str 362 CH4フラックスのカラム名 363 col_c2h6_flux : str 364 C2H6フラックスのカラム名 365 output_filename : str 366 出力ファイル名 367 col_datetime : str 368 日時カラムの名前 369 window_size : int 370 移動平均の窓サイズ 371 confidence_interval : float 372 信頼区間(0-1) 373 subplot_label_ch4 : str | None 374 CH4プロットのラベル 375 subplot_label_c2h6 : str | None 376 C2H6プロットのラベル 377 subplot_fontsize : int 378 サブプロットのフォントサイズ 379 show_ci : bool 380 信頼区間を表示するか 381 ch4_ylim : tuple[float, float] | None 382 CH4のy軸範囲 383 c2h6_ylim : tuple[float, float] | None 384 C2H6のy軸範囲 385 start_date : str | None 386 開始日(YYYY-MM-DD形式) 387 end_date : str | None 388 終了日(YYYY-MM-DD形式) 389 figsize : tuple[float, float] 390 図のサイズ。デフォルトは(16, 6)。 391 """ 392 # 出力ディレクトリの作成 393 os.makedirs(output_dir, exist_ok=True) 394 output_path: str = os.path.join(output_dir, output_filename) 395 396 # データの準備 397 df_copied = df.copy() 398 if not isinstance(df_copied.index, pd.DatetimeIndex): 399 df_copied[col_datetime] = pd.to_datetime(df_copied[col_datetime]) 400 df_copied.set_index(col_datetime, inplace=True) 401 402 # 日付範囲の処理 403 if start_date is not None: 404 start_dt = pd.to_datetime(start_date).normalize() # 時刻を00:00:00に設定 405 df_min_date = ( 406 df_copied.index.normalize().min().normalize() 407 ) # 日付のみの比較のため正規化 408 409 # データの最小日付が指定開始日より後の場合にのみ警告 410 if df_min_date.date() > start_dt.date(): 411 self.logger.warning( 412 f"指定された開始日{start_date}がデータの開始日{df_min_date.strftime('%Y-%m-%d')}より前です。" 413 f"データの開始日を使用します。" 414 ) 415 start_dt = df_min_date 416 else: 417 start_dt = df_copied.index.normalize().min() 418 419 if end_date is not None: 420 end_dt = ( 421 pd.to_datetime(end_date).normalize() 422 + pd.Timedelta(days=1) 423 - pd.Timedelta(seconds=1) 424 ) 425 df_max_date = ( 426 df_copied.index.normalize().max().normalize() 427 ) # 日付のみの比較のため正規化 428 429 # データの最大日付が指定終了日より前の場合にのみ警告 430 if df_max_date.date() < pd.to_datetime(end_date).date(): 431 self.logger.warning( 432 f"指定された終了日{end_date}がデータの終了日{df_max_date.strftime('%Y-%m-%d')}より後です。" 433 f"データの終了日を使用します。" 434 ) 435 end_dt = df_copied.index.max() 436 else: 437 end_dt = df_copied.index.max() 438 439 # 指定された期間のデータを抽出 440 mask = (df_copied.index >= start_dt) & (df_copied.index <= end_dt) 441 df_copied = df_copied[mask] 442 443 # CH4とC2H6の移動平均と信頼区間を計算 444 ch4_mean, ch4_lower, ch4_upper = calculate_rolling_stats( 445 df_copied[col_ch4_flux], window_size, confidence_interval 446 ) 447 c2h6_mean, c2h6_lower, c2h6_upper = calculate_rolling_stats( 448 df_copied[col_c2h6_flux], window_size, confidence_interval 449 ) 450 451 # プロットの作成 452 fig, (ax1, ax2) = plt.subplots(1, 2, figsize=figsize) 453 454 # CH4プロット 455 ax1.plot(df_copied.index, ch4_mean, "red", label="CH$_4$") 456 if show_ci: 457 ax1.fill_between(df_copied.index, ch4_lower, ch4_upper, color="red", alpha=0.2) 458 if subplot_label_ch4: 459 ax1.text( 460 0.02, 461 0.98, 462 subplot_label_ch4, 463 transform=ax1.transAxes, 464 va="top", 465 fontsize=subplot_fontsize, 466 ) 467 ax1.set_ylabel("CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)") 468 if ch4_ylim is not None: 469 ax1.set_ylim(ch4_ylim) 470 ax1.grid(True, alpha=0.3) 471 472 # C2H6プロット 473 ax2.plot(df_copied.index, c2h6_mean, "orange", label="C$_2$H$_6$") 474 if show_ci: 475 ax2.fill_between( 476 df_copied.index, c2h6_lower, c2h6_upper, color="orange", alpha=0.2 477 ) 478 if subplot_label_c2h6: 479 ax2.text( 480 0.02, 481 0.98, 482 subplot_label_c2h6, 483 transform=ax2.transAxes, 484 va="top", 485 fontsize=subplot_fontsize, 486 ) 487 ax2.set_ylabel("C$_2$H$_6$ flux (nmol m$^{-2}$ s$^{-1}$)") 488 if c2h6_ylim is not None: 489 ax2.set_ylim(c2h6_ylim) 490 ax2.grid(True, alpha=0.3) 491 492 # x軸の設定 493 for ax in [ax1, ax2]: 494 ax.set_xlabel("Month") 495 # x軸の範囲を設定 496 ax.set_xlim(start_dt, end_dt) 497 498 # 1ヶ月ごとの主目盛り 499 ax.xaxis.set_major_locator(mdates.MonthLocator(interval=1)) 500 501 # カスタムフォーマッタの作成(数字を通常フォントで表示) 502 def date_formatter(x, p): 503 date = mdates.num2date(x) 504 return f"{date.strftime('%m')}" 505 506 ax.xaxis.set_major_formatter(plt.FuncFormatter(date_formatter)) 507 508 # 補助目盛りの設定 509 ax.xaxis.set_minor_locator(mdates.MonthLocator()) 510 # ティックラベルの回転と位置調整 511 plt.setp(ax.xaxis.get_majorticklabels(), ha="right") 512 513 plt.tight_layout() 514 plt.savefig(output_path, dpi=300, bbox_inches="tight") 515 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形式)
figsize : tuple[float, float]
図のサイズ。デフォルトは(16, 6)。
517 def plot_fluxes_comparison( 518 self, 519 df: pd.DataFrame, 520 output_dir: str, 521 cols_flux: list[str], 522 labels: list[str], 523 colors: list[str], 524 output_filename: str = "ch4_flux_comparison.png", 525 col_datetime: str = "Date", 526 window_size: int = 24 * 7, # 1週間の移動平均のデフォルト値 527 confidence_interval: float = 0.95, # 95%信頼区間 528 subplot_label: str | None = None, 529 subplot_fontsize: int = 20, 530 show_ci: bool = True, 531 y_lim: tuple[float, float] | None = None, 532 start_date: str | None = None, 533 end_date: str | None = None, 534 include_end_date: bool = True, 535 figsize: tuple[float, float] = (12, 6), 536 legend_loc: str = "upper right", 537 apply_ma: bool = True, # 移動平均を適用するかどうか 538 hourly_mean: bool = False, # 1時間平均を適用するかどうか 539 x_interval: Literal["month", "10days"] = "month", # "month" または "10days" 540 xlabel: str = "Month", 541 ylabel: str = "CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)", 542 save_fig: bool = True, 543 show_fig: bool = False, 544 ) -> None: 545 """複数のCH4フラックスの時系列比較プロット 546 547 Parameters 548 ------ 549 df : pd.DataFrame 550 データフレーム 551 output_dir : str 552 出力ディレクトリのパス 553 cols_flux : list[str] 554 比較するフラックスのカラム名リスト 555 labels : list[str] 556 凡例に表示する各フラックスのラベルリスト 557 colors : list[str] 558 各フラックスの色リスト 559 output_filename : str 560 出力ファイル名 561 col_datetime : str 562 日時カラムの名前 563 window_size : int 564 移動平均の窓サイズ 565 confidence_interval : float 566 信頼区間(0-1) 567 subplot_label : str | None 568 プロットのラベル 569 subplot_fontsize : int 570 サブプロットのフォントサイズ 571 show_ci : bool 572 信頼区間を表示するか 573 y_lim : tuple[float, float] | None 574 y軸の範囲 575 start_date : str | None 576 開始日(YYYY-MM-DD形式) 577 end_date : str | None 578 終了日(YYYY-MM-DD形式) 579 include_end_date : bool 580 終了日を含めるかどうか。Falseの場合、終了日の前日までを表示 581 figsize : tuple[float, float] 582 図のサイズ 583 legend_loc : str 584 凡例の位置 585 apply_ma : bool 586 移動平均を適用するかどうか 587 hourly_mean : bool 588 1時間平均を適用するかどうか 589 x_interval : Literal['month', '10days'] 590 x軸の目盛り間隔。"month"(月初めのみ)または"10days"(10日刻み) 591 xlabel : str 592 x軸のラベル(通常は"Month") 593 ylabel : str 594 y軸のラベル(通常は"CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)") 595 save_fig : bool 596 図を保存するかどうか 597 show_fig : bool 598 図を表示するかどうか 599 """ 600 # 出力ディレクトリの作成 601 os.makedirs(output_dir, exist_ok=True) 602 output_path: str = os.path.join(output_dir, output_filename) 603 604 # データの準備 605 df = df.copy() 606 if not isinstance(df.index, pd.DatetimeIndex): 607 df[col_datetime] = pd.to_datetime(df[col_datetime]) 608 df.set_index(col_datetime, inplace=True) 609 610 # 1時間平均の適用 611 if hourly_mean: 612 # 時間情報のみを使用してグループ化 613 df = df.groupby([df.index.date, df.index.hour]).mean() 614 # マルチインデックスを日時インデックスに変換 615 df.index = pd.to_datetime( 616 [f"{date} {hour:02d}:00:00" for date, hour in df.index] 617 ) 618 619 # 日付範囲の処理 620 if start_date is not None: 621 start_dt = pd.to_datetime(start_date).normalize() # 時刻を00:00:00に設定 622 df_min_date = ( 623 df.index.normalize().min().normalize() 624 ) # 日付のみの比較のため正規化 625 626 # データの最小日付が指定開始日より後の場合にのみ警告 627 if df_min_date.date() > start_dt.date(): 628 self.logger.warning( 629 f"指定された開始日{start_date}がデータの開始日{df_min_date.strftime('%Y-%m-%d')}より前です。" 630 f"データの開始日を使用します。" 631 ) 632 start_dt = df_min_date 633 else: 634 start_dt = df.index.normalize().min() 635 636 if end_date is not None: 637 if include_end_date: 638 end_dt = ( 639 pd.to_datetime(end_date).normalize() 640 + pd.Timedelta(days=1) 641 - pd.Timedelta(seconds=1) 642 ) 643 else: 644 # 終了日を含まない場合、終了日の前日の23:59:59まで 645 end_dt = pd.to_datetime(end_date).normalize() - pd.Timedelta(seconds=1) 646 647 df_max_date = ( 648 df.index.normalize().max().normalize() 649 ) # 日付のみの比較のため正規化 650 651 # データの最大日付が指定終了日より前の場合にのみ警告 652 compare_date = pd.to_datetime(end_date).date() 653 if not include_end_date: 654 compare_date = compare_date - pd.Timedelta(days=1) 655 656 if df_max_date.date() < compare_date: 657 self.logger.warning( 658 f"指定された終了日{end_date}がデータの終了日{df_max_date.strftime('%Y-%m-%d')}より後です。" 659 f"データの終了日を使用します。" 660 ) 661 end_dt = df.index.max() 662 else: 663 end_dt = df.index.max() 664 665 # 指定された期間のデータを抽出 666 mask = (df.index >= start_dt) & (df.index <= end_dt) 667 df = df[mask] 668 669 # プロットの作成 670 fig, ax = plt.subplots(figsize=figsize) 671 672 # 各フラックスのプロット 673 for flux_col, label, color in zip(cols_flux, labels, colors): 674 if apply_ma: 675 # 移動平均の計算 676 mean, lower, upper = calculate_rolling_stats( 677 df[flux_col], window_size, confidence_interval 678 ) 679 ax.plot(df.index, mean, color, label=label, alpha=0.7) 680 if show_ci: 681 ax.fill_between(df.index, lower, upper, color=color, alpha=0.2) 682 else: 683 # 生データのプロット 684 ax.plot(df.index, df[flux_col], color, label=label, alpha=0.7) 685 686 # プロットの設定 687 if subplot_label: 688 ax.text( 689 0.02, 690 0.98, 691 subplot_label, 692 transform=ax.transAxes, 693 va="top", 694 fontsize=subplot_fontsize, 695 ) 696 697 ax.set_xlabel(xlabel) 698 ax.set_ylabel(ylabel) 699 700 if y_lim is not None: 701 ax.set_ylim(y_lim) 702 703 ax.grid(True, alpha=0.3) 704 ax.legend(loc=legend_loc) 705 706 # x軸の設定 707 ax.set_xlim(start_dt, end_dt) 708 709 if x_interval == "month": 710 # 月初めにメジャー線のみ表示 711 ax.xaxis.set_major_locator(mdates.MonthLocator()) 712 ax.xaxis.set_minor_locator(plt.NullLocator()) # マイナー線を非表示 713 elif x_interval == "10days": 714 # 10日刻みでメジャー線、日毎にマイナー線を表示 715 ax.xaxis.set_major_locator(mdates.DayLocator(bymonthday=[1, 11, 21])) 716 ax.xaxis.set_minor_locator(mdates.DayLocator()) 717 ax.grid(True, which="minor", alpha=0.1) # マイナー線の表示設定 718 719 # カスタムフォーマッタの作成(月初めの1日のみMMを表示) 720 def date_formatter(x, p): 721 date = mdates.num2date(x) 722 # 月初めの1日の場合のみ月を表示 723 if date.day == 1: 724 return f"{date.strftime('%m')}" 725 return "" 726 727 ax.xaxis.set_major_formatter(plt.FuncFormatter(date_formatter)) 728 plt.setp(ax.xaxis.get_majorticklabels(), ha="right", rotation=0) 729 730 plt.tight_layout() 731 732 if save_fig: 733 plt.savefig(output_path, dpi=300, bbox_inches="tight") 734 if show_fig: 735 plt.show() 736 plt.close(fig)
複数のCH4フラックスの時系列比較プロット
Parameters
df : pd.DataFrame
データフレーム
output_dir : str
出力ディレクトリのパス
cols_flux : list[str]
比較するフラックスのカラム名リスト
labels : list[str]
凡例に表示する各フラックスのラベルリスト
colors : list[str]
各フラックスの色リスト
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形式)
include_end_date : bool
終了日を含めるかどうか。Falseの場合、終了日の前日までを表示
figsize : tuple[float, float]
図のサイズ
legend_loc : str
凡例の位置
apply_ma : bool
移動平均を適用するかどうか
hourly_mean : bool
1時間平均を適用するかどうか
x_interval : Literal['month', '10days']
x軸の目盛り間隔。"month"(月初めのみ)または"10days"(10日刻み)
xlabel : str
x軸のラベル(通常は"Month")
ylabel : str
y軸のラベル(通常は"CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)")
save_fig : bool
図を保存するかどうか
show_fig : bool
図を表示するかどうか
738 def plot_c1c2_fluxes_diurnal_patterns( 739 self, 740 df: pd.DataFrame, 741 y_cols_ch4: list[str], 742 y_cols_c2h6: list[str], 743 labels_ch4: list[str], 744 labels_c2h6: list[str], 745 colors_ch4: list[str], 746 colors_c2h6: list[str], 747 output_dir: str, 748 output_filename: str = "diurnal.png", 749 legend_only_ch4: bool = False, 750 add_label: bool = True, 751 add_legend: bool = True, 752 show_std: bool = False, # 標準偏差表示のオプションを追加 753 std_alpha: float = 0.2, # 標準偏差の透明度 754 subplot_fontsize: int = 20, 755 subplot_label_ch4: str | None = "(a)", 756 subplot_label_c2h6: str | None = "(b)", 757 ax1_ylim: tuple[float, float] | None = None, 758 ax2_ylim: tuple[float, float] | None = None, 759 ) -> None: 760 """CH4とC2H6の日変化パターンを1つの図に並べてプロットする 761 762 Parameters 763 ------ 764 df : pd.DataFrame 765 入力データフレーム。 766 y_cols_ch4 : list[str] 767 CH4のプロットに使用するカラム名のリスト。 768 y_cols_c2h6 : list[str] 769 C2H6のプロットに使用するカラム名のリスト。 770 labels_ch4 : list[str] 771 CH4の各ラインに対応するラベルのリスト。 772 labels_c2h6 : list[str] 773 C2H6の各ラインに対応するラベルのリスト。 774 colors_ch4 : list[str] 775 CH4の各ラインに使用する色のリスト。 776 colors_c2h6 : list[str] 777 C2H6の各ラインに使用する色のリスト。 778 output_dir : str 779 出力先ディレクトリのパス。 780 output_filename : str, optional 781 出力ファイル名。デフォルトは"diurnal.png"。 782 legend_only_ch4 : bool, optional 783 CH4の凡例のみを表示するかどうか。デフォルトはFalse。 784 add_label : bool, optional 785 サブプロットラベルを表示するかどうか。デフォルトはTrue。 786 add_legend : bool, optional 787 凡例を表示するかどうか。デフォルトはTrue。 788 show_std : bool, optional 789 標準偏差を表示するかどうか。デフォルトはFalse。 790 std_alpha : float, optional 791 標準偏差の透明度。デフォルトは0.2。 792 subplot_fontsize : int, optional 793 サブプロットのフォントサイズ。デフォルトは20。 794 subplot_label_ch4 : str | None, optional 795 CH4プロットのラベル。デフォルトは"(a)"。 796 subplot_label_c2h6 : str | None, optional 797 C2H6プロットのラベル。デフォルトは"(b)"。 798 ax1_ylim : tuple[float, float] | None, optional 799 CH4プロットのy軸の範囲。デフォルトはNone。 800 ax2_ylim : tuple[float, float] | None, optional 801 C2H6プロットのy軸の範囲。デフォルトはNone。 802 """ 803 os.makedirs(output_dir, exist_ok=True) 804 output_path: str = os.path.join(output_dir, output_filename) 805 806 # データの準備 807 target_columns = y_cols_ch4 + y_cols_c2h6 808 hourly_means, time_points = self._prepare_diurnal_data(df, target_columns) 809 810 # 標準偏差の計算を追加 811 hourly_stds = {} 812 if show_std: 813 hourly_stds = df.groupby(df.index.hour)[target_columns].std() 814 # 24時間目のデータ点を追加 815 last_hour = hourly_stds.iloc[0:1].copy() 816 last_hour.index = [24] 817 hourly_stds = pd.concat([hourly_stds, last_hour]) 818 819 # プロットの作成 820 fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5)) 821 822 # CH4のプロット (左側) 823 ch4_lines = [] 824 for y_col, label, color in zip(y_cols_ch4, labels_ch4, colors_ch4): 825 mean_values = hourly_means["all"][y_col] 826 line = ax1.plot( 827 time_points, 828 mean_values, 829 "-o", 830 label=label, 831 color=color, 832 ) 833 ch4_lines.extend(line) 834 835 # 標準偏差の表示 836 if show_std: 837 std_values = hourly_stds[y_col] 838 ax1.fill_between( 839 time_points, 840 mean_values - std_values, 841 mean_values + std_values, 842 color=color, 843 alpha=std_alpha, 844 ) 845 846 # C2H6のプロット (右側) 847 c2h6_lines = [] 848 for y_col, label, color in zip(y_cols_c2h6, labels_c2h6, colors_c2h6): 849 mean_values = hourly_means["all"][y_col] 850 line = ax2.plot( 851 time_points, 852 mean_values, 853 "o-", 854 label=label, 855 color=color, 856 ) 857 c2h6_lines.extend(line) 858 859 # 標準偏差の表示 860 if show_std: 861 std_values = hourly_stds[y_col] 862 ax2.fill_between( 863 time_points, 864 mean_values - std_values, 865 mean_values + std_values, 866 color=color, 867 alpha=std_alpha, 868 ) 869 870 # 軸の設定 871 for ax, ylabel, subplot_label in [ 872 (ax1, r"CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)", subplot_label_ch4), 873 (ax2, r"C$_2$H$_6$ flux (nmol m$^{-2}$ s$^{-1}$)", subplot_label_c2h6), 874 ]: 875 self._setup_diurnal_axes( 876 ax=ax, 877 time_points=time_points, 878 ylabel=ylabel, 879 subplot_label=subplot_label, 880 add_label=add_label, 881 add_legend=False, # 個別の凡例は表示しない 882 subplot_fontsize=subplot_fontsize, 883 ) 884 885 if ax1_ylim is not None: 886 ax1.set_ylim(ax1_ylim) 887 ax1.yaxis.set_major_locator(MultipleLocator(20)) 888 ax1.yaxis.set_major_formatter(FuncFormatter(lambda x, p: f"{x:.0f}")) 889 890 if ax2_ylim is not None: 891 ax2.set_ylim(ax2_ylim) 892 ax2.yaxis.set_major_locator(MultipleLocator(1)) 893 ax2.yaxis.set_major_formatter(FuncFormatter(lambda x, p: f"{x:.1f}")) 894 895 plt.tight_layout() 896 897 # 共通の凡例 898 if add_legend: 899 all_lines = ch4_lines 900 all_labels = [line.get_label() for line in ch4_lines] 901 if not legend_only_ch4: 902 all_lines += c2h6_lines 903 all_labels += [line.get_label() for line in c2h6_lines] 904 fig.legend( 905 all_lines, 906 all_labels, 907 loc="center", 908 bbox_to_anchor=(0.5, 0.02), 909 ncol=len(all_lines), 910 ) 911 plt.subplots_adjust(bottom=0.25) # 下部に凡例用のスペースを確保 912 913 fig.savefig(output_path, dpi=300, bbox_inches="tight") 914 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。
add_label : bool, optional
サブプロットラベルを表示するかどうか。デフォルトはTrue。
add_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。
916 def plot_c1c2_fluxes_diurnal_patterns_by_date( 917 self, 918 df: pd.DataFrame, 919 y_col_ch4: str, 920 y_col_c2h6: str, 921 output_dir: str, 922 output_filename: str = "diurnal_by_date.png", 923 plot_all: bool = True, 924 plot_weekday: bool = True, 925 plot_weekend: bool = True, 926 plot_holiday: bool = True, 927 add_label: bool = True, 928 add_legend: bool = True, 929 show_std: bool = False, # 標準偏差表示のオプションを追加 930 std_alpha: float = 0.2, # 標準偏差の透明度 931 legend_only_ch4: bool = False, 932 subplot_fontsize: int = 20, 933 subplot_label_ch4: str | None = "(a)", 934 subplot_label_c2h6: str | None = "(b)", 935 ax1_ylim: tuple[float, float] | None = None, 936 ax2_ylim: tuple[float, float] | None = None, 937 print_summary: bool = True, # 追加: 統計情報を表示するかどうか 938 ) -> None: 939 """CH4とC2H6の日変化パターンを日付分類して1つの図に並べてプロットする 940 941 Parameters 942 ------ 943 df : pd.DataFrame 944 入力データフレーム。 945 y_col_ch4 : str 946 CH4フラックスを含むカラム名。 947 y_col_c2h6 : str 948 C2H6フラックスを含むカラム名。 949 output_dir : str 950 出力先ディレクトリのパス。 951 output_filename : str, optional 952 出力ファイル名。デフォルトは"diurnal_by_date.png"。 953 plot_all : bool, optional 954 すべての日をプロットするかどうか。デフォルトはTrue。 955 plot_weekday : bool, optional 956 平日をプロットするかどうか。デフォルトはTrue。 957 plot_weekend : bool, optional 958 週末をプロットするかどうか。デフォルトはTrue。 959 plot_holiday : bool, optional 960 祝日をプロットするかどうか。デフォルトはTrue。 961 add_label : bool, optional 962 サブプロットラベルを表示するかどうか。デフォルトはTrue。 963 add_legend : bool, optional 964 凡例を表示するかどうか。デフォルトはTrue。 965 show_std : bool, optional 966 標準偏差を表示するかどうか。デフォルトはFalse。 967 std_alpha : float, optional 968 標準偏差の透明度。デフォルトは0.2。 969 legend_only_ch4 : bool, optional 970 CH4の凡例のみを表示するかどうか。デフォルトはFalse。 971 subplot_fontsize : int, optional 972 サブプロットのフォントサイズ。デフォルトは20。 973 subplot_label_ch4 : str | None, optional 974 CH4プロットのラベル。デフォルトは"(a)"。 975 subplot_label_c2h6 : str | None, optional 976 C2H6プロットのラベル。デフォルトは"(b)"。 977 ax1_ylim : tuple[float, float] | None, optional 978 CH4プロットのy軸の範囲。デフォルトはNone。 979 ax2_ylim : tuple[float, float] | None, optional 980 C2H6プロットのy軸の範囲。デフォルトはNone。 981 print_summary : bool, optional 982 統計情報を表示するかどうか。デフォルトはTrue。 983 """ 984 os.makedirs(output_dir, exist_ok=True) 985 output_path: str = os.path.join(output_dir, output_filename) 986 987 # データの準備 988 target_columns = [y_col_ch4, y_col_c2h6] 989 hourly_means, time_points = self._prepare_diurnal_data( 990 df, target_columns, include_date_types=True 991 ) 992 993 # 標準偏差の計算を追加 994 hourly_stds = {} 995 if show_std: 996 for condition in ["all", "weekday", "weekend", "holiday"]: 997 if condition == "all": 998 condition_data = df 999 elif condition == "weekday": 1000 condition_data = df[ 1001 ~( 1002 df.index.dayofweek.isin([5, 6]) 1003 | df.index.map(lambda x: jpholiday.is_holiday(x.date())) 1004 ) 1005 ] 1006 elif condition == "weekend": 1007 condition_data = df[df.index.dayofweek.isin([5, 6])] 1008 else: # holiday 1009 condition_data = df[ 1010 df.index.map(lambda x: jpholiday.is_holiday(x.date())) 1011 ] 1012 1013 hourly_stds[condition] = condition_data.groupby( 1014 condition_data.index.hour 1015 )[target_columns].std() 1016 # 24時間目のデータ点を追加 1017 last_hour = hourly_stds[condition].iloc[0:1].copy() 1018 last_hour.index = [24] 1019 hourly_stds[condition] = pd.concat([hourly_stds[condition], last_hour]) 1020 1021 # プロットスタイルの設定 1022 styles = { 1023 "all": { 1024 "color": "black", 1025 "linestyle": "-", 1026 "alpha": 1.0, 1027 "label": "All days", 1028 }, 1029 "weekday": { 1030 "color": "blue", 1031 "linestyle": "-", 1032 "alpha": 0.8, 1033 "label": "Weekdays", 1034 }, 1035 "weekend": { 1036 "color": "red", 1037 "linestyle": "-", 1038 "alpha": 0.8, 1039 "label": "Weekends", 1040 }, 1041 "holiday": { 1042 "color": "green", 1043 "linestyle": "-", 1044 "alpha": 0.8, 1045 "label": "Weekends & Holidays", 1046 }, 1047 } 1048 1049 # プロット対象の条件を選択 1050 plot_conditions = { 1051 "all": plot_all, 1052 "weekday": plot_weekday, 1053 "weekend": plot_weekend, 1054 "holiday": plot_holiday, 1055 } 1056 selected_conditions = { 1057 col: means 1058 for col, means in hourly_means.items() 1059 if col in plot_conditions and plot_conditions[col] 1060 } 1061 1062 # プロットの作成 1063 fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5)) 1064 1065 # CH4とC2H6のプロット用のラインオブジェクトを保存 1066 ch4_lines = [] 1067 c2h6_lines = [] 1068 1069 # CH4とC2H6のプロット 1070 for condition, means in selected_conditions.items(): 1071 style = styles[condition].copy() 1072 1073 # CH4プロット 1074 mean_values_ch4 = means[y_col_ch4] 1075 line_ch4 = ax1.plot(time_points, mean_values_ch4, marker="o", **style) 1076 ch4_lines.extend(line_ch4) 1077 1078 if show_std and condition in hourly_stds: 1079 std_values = hourly_stds[condition][y_col_ch4] 1080 ax1.fill_between( 1081 time_points, 1082 mean_values_ch4 - std_values, 1083 mean_values_ch4 + std_values, 1084 color=style["color"], 1085 alpha=std_alpha, 1086 ) 1087 1088 # C2H6プロット 1089 style["linestyle"] = "--" 1090 mean_values_c2h6 = means[y_col_c2h6] 1091 line_c2h6 = ax2.plot(time_points, mean_values_c2h6, marker="o", **style) 1092 c2h6_lines.extend(line_c2h6) 1093 1094 if show_std and condition in hourly_stds: 1095 std_values = hourly_stds[condition][y_col_c2h6] 1096 ax2.fill_between( 1097 time_points, 1098 mean_values_c2h6 - std_values, 1099 mean_values_c2h6 + std_values, 1100 color=style["color"], 1101 alpha=std_alpha, 1102 ) 1103 1104 # 軸の設定 1105 for ax, ylabel, subplot_label in [ 1106 (ax1, r"CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)", subplot_label_ch4), 1107 (ax2, r"C$_2$H$_6$ flux (nmol m$^{-2}$ s$^{-1}$)", subplot_label_c2h6), 1108 ]: 1109 self._setup_diurnal_axes( 1110 ax=ax, 1111 time_points=time_points, 1112 ylabel=ylabel, 1113 subplot_label=subplot_label, 1114 add_label=add_label, 1115 add_legend=False, 1116 subplot_fontsize=subplot_fontsize, 1117 ) 1118 1119 if ax1_ylim is not None: 1120 ax1.set_ylim(ax1_ylim) 1121 ax1.yaxis.set_major_locator(MultipleLocator(20)) 1122 ax1.yaxis.set_major_formatter(FuncFormatter(lambda x, p: f"{x:.0f}")) 1123 1124 if ax2_ylim is not None: 1125 ax2.set_ylim(ax2_ylim) 1126 ax2.yaxis.set_major_locator(MultipleLocator(1)) 1127 ax2.yaxis.set_major_formatter(FuncFormatter(lambda x, p: f"{x:.1f}")) 1128 1129 plt.tight_layout() 1130 1131 # 共通の凡例を図の下部に配置 1132 if add_legend: 1133 lines_to_show = ( 1134 ch4_lines if legend_only_ch4 else ch4_lines[: len(selected_conditions)] 1135 ) 1136 fig.legend( 1137 lines_to_show, 1138 [ 1139 style["label"] 1140 for style in list(styles.values())[: len(lines_to_show)] 1141 ], 1142 loc="center", 1143 bbox_to_anchor=(0.5, 0.02), 1144 ncol=len(lines_to_show), 1145 ) 1146 plt.subplots_adjust(bottom=0.25) # 下部に凡例用のスペースを確保 1147 1148 fig.savefig(output_path, dpi=300, bbox_inches="tight") 1149 plt.close(fig) 1150 1151 # 日変化パターンの統計分析を追加 1152 if print_summary: 1153 # 平日と休日のデータを準備 1154 dates = pd.to_datetime(df.index) 1155 is_weekend = dates.dayofweek.isin([5, 6]) 1156 is_holiday = dates.map(lambda x: jpholiday.is_holiday(x.date())) 1157 is_weekday = ~(is_weekend | is_holiday) 1158 1159 weekday_data = df[is_weekday] 1160 holiday_data = df[is_weekend | is_holiday] 1161 1162 def get_diurnal_stats(data, column): 1163 # 時間ごとの平均値を計算 1164 hourly_means = data.groupby(data.index.hour)[column].mean() 1165 1166 # 8-16時の時間帯の統計 1167 daytime_means = hourly_means[ 1168 (hourly_means.index >= 8) & (hourly_means.index <= 16) 1169 ] 1170 1171 if len(daytime_means) == 0: 1172 return None 1173 1174 return { 1175 "mean": daytime_means.mean(), 1176 "max": daytime_means.max(), 1177 "max_hour": daytime_means.idxmax(), 1178 "min": daytime_means.min(), 1179 "min_hour": daytime_means.idxmin(), 1180 "hours_count": len(daytime_means), 1181 } 1182 1183 # CH4とC2H6それぞれの統計を計算 1184 for col, gas_name in [(y_col_ch4, "CH4"), (y_col_c2h6, "C2H6")]: 1185 print(f"\n=== {gas_name} フラックス 8-16時の統計分析 ===") 1186 1187 weekday_stats = get_diurnal_stats(weekday_data, col) 1188 holiday_stats = get_diurnal_stats(holiday_data, col) 1189 1190 if weekday_stats and holiday_stats: 1191 print("\n平日:") 1192 print(f" 平均値: {weekday_stats['mean']:.2f}") 1193 print( 1194 f" 最大値: {weekday_stats['max']:.2f} ({weekday_stats['max_hour']}時)" 1195 ) 1196 print( 1197 f" 最小値: {weekday_stats['min']:.2f} ({weekday_stats['min_hour']}時)" 1198 ) 1199 print(f" 集計時間数: {weekday_stats['hours_count']}") 1200 1201 print("\n休日:") 1202 print(f" 平均値: {holiday_stats['mean']:.2f}") 1203 print( 1204 f" 最大値: {holiday_stats['max']:.2f} ({holiday_stats['max_hour']}時)" 1205 ) 1206 print( 1207 f" 最小値: {holiday_stats['min']:.2f} ({holiday_stats['min_hour']}時)" 1208 ) 1209 print(f" 集計時間数: {holiday_stats['hours_count']}") 1210 1211 # 平日/休日の比率を計算 1212 print("\n平日/休日の比率:") 1213 print( 1214 f" 平均値比: {weekday_stats['mean'] / holiday_stats['mean']:.2f}" 1215 ) 1216 print( 1217 f" 最大値比: {weekday_stats['max'] / holiday_stats['max']:.2f}" 1218 ) 1219 print( 1220 f" 最小値比: {weekday_stats['min'] / holiday_stats['min']:.2f}" 1221 ) 1222 else: 1223 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。
add_label : bool, optional
サブプロットラベルを表示するかどうか。デフォルトはTrue。
add_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。
1225 def plot_diurnal_concentrations( 1226 self, 1227 df: pd.DataFrame, 1228 output_dir: str, 1229 col_ch4_conc: str = "CH4_ultra_cal", 1230 col_c2h6_conc: str = "C2H6_ultra_cal", 1231 col_datetime: str = "Date", 1232 output_filename: str = "diurnal_concentrations.png", 1233 show_std: bool = True, 1234 alpha_std: float = 0.2, 1235 add_legend: bool = True, # 凡例表示のオプションを追加 1236 print_summary: bool = True, 1237 subplot_label_ch4: str | None = None, 1238 subplot_label_c2h6: str | None = None, 1239 subplot_fontsize: int = 24, 1240 ch4_ylim: tuple[float, float] | None = None, 1241 c2h6_ylim: tuple[float, float] | None = None, 1242 interval: str = "1H", # "30min" または "1H" を指定 1243 ) -> None: 1244 """CH4とC2H6の濃度の日内変動を描画する 1245 1246 Parameters 1247 ------ 1248 df : pd.DataFrame 1249 濃度データを含むDataFrame 1250 output_dir : str 1251 出力ディレクトリのパス 1252 col_ch4_conc : str 1253 CH4濃度のカラム名 1254 col_c2h6_conc : str 1255 C2H6濃度のカラム名 1256 col_datetime : str 1257 日時カラム名 1258 output_filename : str 1259 出力ファイル名 1260 show_std : bool 1261 標準偏差を表示するかどうか 1262 alpha_std : float 1263 標準偏差の透明度 1264 add_legend : bool 1265 凡例を追加するかどうか 1266 print_summary : bool 1267 統計情報を表示するかどうか 1268 subplot_label_ch4 : str | None 1269 CH4プロットのラベル 1270 subplot_label_c2h6 : str | None 1271 C2H6プロットのラベル 1272 subplot_fontsize : int 1273 サブプロットのフォントサイズ 1274 ch4_ylim : tuple[float, float] | None 1275 CH4のy軸範囲 1276 c2h6_ylim : tuple[float, float] | None 1277 C2H6のy軸範囲 1278 interval : str 1279 時間間隔。"30min"または"1H"を指定 1280 """ 1281 # 出力ディレクトリの作成 1282 os.makedirs(output_dir, exist_ok=True) 1283 output_path: str = os.path.join(output_dir, output_filename) 1284 1285 # データの準備 1286 df = df.copy() 1287 if interval == "30min": 1288 # 30分間隔の場合、時間と30分を別々に取得 1289 df["hour"] = pd.to_datetime(df[col_datetime]).dt.hour 1290 df["minute"] = pd.to_datetime(df[col_datetime]).dt.minute 1291 df["time_bin"] = df["hour"] + df["minute"].map({0: 0, 30: 0.5}) 1292 else: 1293 # 1時間間隔の場合 1294 df["time_bin"] = pd.to_datetime(df[col_datetime]).dt.hour 1295 1296 # 時間ごとの平均値と標準偏差を計算 1297 hourly_stats = df.groupby("time_bin")[[col_ch4_conc, col_c2h6_conc]].agg( 1298 ["mean", "std"] 1299 ) 1300 1301 # 最後のデータポイントを追加(最初のデータを使用) 1302 last_point = hourly_stats.iloc[0:1].copy() 1303 last_point.index = [ 1304 hourly_stats.index[-1] + (0.5 if interval == "30min" else 1) 1305 ] 1306 hourly_stats = pd.concat([hourly_stats, last_point]) 1307 1308 # 時間軸の作成 1309 if interval == "30min": 1310 time_points = pd.date_range("2024-01-01", periods=49, freq="30min") 1311 x_ticks = [0, 6, 12, 18, 24] # 主要な時間のティック 1312 else: 1313 time_points = pd.date_range("2024-01-01", periods=25, freq="1H") 1314 x_ticks = [0, 6, 12, 18, 24] 1315 1316 # プロットの作成 1317 fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5)) 1318 1319 # CH4濃度プロット 1320 mean_ch4 = hourly_stats[col_ch4_conc]["mean"] 1321 if show_std: 1322 std_ch4 = hourly_stats[col_ch4_conc]["std"] 1323 ax1.fill_between( 1324 time_points, 1325 mean_ch4 - std_ch4, 1326 mean_ch4 + std_ch4, 1327 color="red", 1328 alpha=alpha_std, 1329 ) 1330 ch4_line = ax1.plot(time_points, mean_ch4, "red", label="CH$_4$")[0] 1331 1332 ax1.set_ylabel("CH$_4$ (ppm)") 1333 if ch4_ylim is not None: 1334 ax1.set_ylim(ch4_ylim) 1335 if subplot_label_ch4: 1336 ax1.text( 1337 0.02, 1338 0.98, 1339 subplot_label_ch4, 1340 transform=ax1.transAxes, 1341 va="top", 1342 fontsize=subplot_fontsize, 1343 ) 1344 1345 # C2H6濃度プロット 1346 mean_c2h6 = hourly_stats[col_c2h6_conc]["mean"] 1347 if show_std: 1348 std_c2h6 = hourly_stats[col_c2h6_conc]["std"] 1349 ax2.fill_between( 1350 time_points, 1351 mean_c2h6 - std_c2h6, 1352 mean_c2h6 + std_c2h6, 1353 color="orange", 1354 alpha=alpha_std, 1355 ) 1356 c2h6_line = ax2.plot(time_points, mean_c2h6, "orange", label="C$_2$H$_6$")[0] 1357 1358 ax2.set_ylabel("C$_2$H$_6$ (ppb)") 1359 if c2h6_ylim is not None: 1360 ax2.set_ylim(c2h6_ylim) 1361 if subplot_label_c2h6: 1362 ax2.text( 1363 0.02, 1364 0.98, 1365 subplot_label_c2h6, 1366 transform=ax2.transAxes, 1367 va="top", 1368 fontsize=subplot_fontsize, 1369 ) 1370 1371 # 両プロットの共通設定 1372 for ax in [ax1, ax2]: 1373 ax.set_xlabel("Time (hour)") 1374 ax.xaxis.set_major_formatter(mdates.DateFormatter("%-H")) 1375 ax.xaxis.set_major_locator(mdates.HourLocator(byhour=x_ticks)) 1376 ax.set_xlim(time_points[0], time_points[-1]) 1377 # 1時間ごとの縦線を表示 1378 ax.grid(True, which="major", alpha=0.3) 1379 # 補助目盛りは表示するが、グリッド線は表示しない 1380 # if interval == "30min": 1381 # ax.xaxis.set_minor_locator(mdates.MinuteLocator(byminute=[30])) 1382 # ax.tick_params(which='minor', length=4) 1383 1384 # 共通の凡例を図の下部に配置 1385 if add_legend: 1386 fig.legend( 1387 [ch4_line, c2h6_line], 1388 ["CH$_4$", "C$_2$H$_6$"], 1389 loc="center", 1390 bbox_to_anchor=(0.5, 0.02), 1391 ncol=2, 1392 ) 1393 plt.subplots_adjust(bottom=0.2) 1394 1395 plt.tight_layout() 1396 plt.savefig(output_path, dpi=300, bbox_inches="tight") 1397 plt.close(fig) 1398 1399 if print_summary: 1400 # 統計情報の表示 1401 for name, col in [("CH4", col_ch4_conc), ("C2H6", col_c2h6_conc)]: 1402 stats = hourly_stats[col] 1403 mean_vals = stats["mean"] 1404 1405 print(f"\n{name}濃度の日内変動統計:") 1406 print(f"最小値: {mean_vals.min():.3f} (Hour: {mean_vals.idxmin()})") 1407 print(f"最大値: {mean_vals.max():.3f} (Hour: {mean_vals.idxmax()})") 1408 print(f"平均値: {mean_vals.mean():.3f}") 1409 print(f"日内変動幅: {mean_vals.max() - mean_vals.min():.3f}") 1410 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"を指定
1412 def plot_flux_diurnal_patterns_with_std( 1413 self, 1414 df: pd.DataFrame, 1415 output_dir: str, 1416 col_ch4_flux: str = "Fch4", 1417 col_c2h6_flux: str = "Fc2h6", 1418 ch4_label: str = r"$\mathregular{CH_{4}}$フラックス", 1419 c2h6_label: str = r"$\mathregular{C_{2}H_{6}}$フラックス", 1420 col_datetime: str = "Date", 1421 output_filename: str = "diurnal_patterns.png", 1422 window_size: int = 6, # 移動平均の窓サイズ 1423 show_std: bool = True, # 標準偏差の表示有無 1424 alpha_std: float = 0.1, # 標準偏差の透明度 1425 ) -> None: 1426 """CH4とC2H6フラックスの日変化パターンをプロットする 1427 1428 Parameters 1429 ------ 1430 df : pd.DataFrame 1431 データフレーム 1432 output_dir : str 1433 出力ディレクトリのパス 1434 col_ch4_flux : str 1435 CH4フラックスのカラム名 1436 col_c2h6_flux : str 1437 C2H6フラックスのカラム名 1438 ch4_label : str 1439 CH4フラックスのラベル 1440 c2h6_label : str 1441 C2H6フラックスのラベル 1442 col_datetime : str 1443 日時カラムの名前 1444 output_filename : str 1445 出力ファイル名 1446 window_size : int 1447 移動平均の窓サイズ(デフォルト6) 1448 show_std : bool 1449 標準偏差を表示するかどうか 1450 alpha_std : float 1451 標準偏差の透明度(0-1) 1452 """ 1453 # 出力ディレクトリの作成 1454 os.makedirs(output_dir, exist_ok=True) 1455 output_path: str = os.path.join(output_dir, output_filename) 1456 1457 # # プロットのスタイル設定 1458 # plt.rcParams.update({ 1459 # 'font.size': 20, 1460 # 'axes.labelsize': 20, 1461 # 'axes.titlesize': 20, 1462 # 'xtick.labelsize': 20, 1463 # 'ytick.labelsize': 20, 1464 # 'legend.fontsize': 20, 1465 # }) 1466 1467 # 日時インデックスの処理 1468 df = df.copy() 1469 if not isinstance(df.index, pd.DatetimeIndex): 1470 df[col_datetime] = pd.to_datetime(df[col_datetime]) 1471 df.set_index(col_datetime, inplace=True) 1472 1473 # 時刻データの抽出とグループ化 1474 df["hour"] = df.index.hour 1475 hourly_means = df.groupby("hour")[[col_ch4_flux, col_c2h6_flux]].agg( 1476 ["mean", "std"] 1477 ) 1478 1479 # 24時間目のデータ点を追加(0時のデータを使用) 1480 last_hour = hourly_means.iloc[0:1].copy() 1481 last_hour.index = [24] 1482 hourly_means = pd.concat([hourly_means, last_hour]) 1483 1484 # 24時間分のデータポイントを作成 1485 time_points = pd.date_range("2024-01-01", periods=25, freq="h") 1486 1487 # プロットの作成 1488 fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5)) 1489 1490 # 移動平均の計算と描画 1491 ch4_mean = ( 1492 hourly_means[(col_ch4_flux, "mean")] 1493 .rolling(window=window_size, center=True, min_periods=1) 1494 .mean() 1495 ) 1496 c2h6_mean = ( 1497 hourly_means[(col_c2h6_flux, "mean")] 1498 .rolling(window=window_size, center=True, min_periods=1) 1499 .mean() 1500 ) 1501 1502 if show_std: 1503 ch4_std = ( 1504 hourly_means[(col_ch4_flux, "std")] 1505 .rolling(window=window_size, center=True, min_periods=1) 1506 .mean() 1507 ) 1508 c2h6_std = ( 1509 hourly_means[(col_c2h6_flux, "std")] 1510 .rolling(window=window_size, center=True, min_periods=1) 1511 .mean() 1512 ) 1513 1514 ax1.fill_between( 1515 time_points, 1516 ch4_mean - ch4_std, 1517 ch4_mean + ch4_std, 1518 color="blue", 1519 alpha=alpha_std, 1520 ) 1521 ax2.fill_between( 1522 time_points, 1523 c2h6_mean - c2h6_std, 1524 c2h6_mean + c2h6_std, 1525 color="red", 1526 alpha=alpha_std, 1527 ) 1528 1529 # メインのラインプロット 1530 ax1.plot(time_points, ch4_mean, "blue", label=ch4_label) 1531 ax2.plot(time_points, c2h6_mean, "red", label=c2h6_label) 1532 1533 # 軸の設定 1534 for ax, ylabel in [ 1535 (ax1, r"CH$_4$ (nmol m$^{-2}$ s$^{-1}$)"), 1536 (ax2, r"C$_2$H$_6$ (nmol m$^{-2}$ s$^{-1}$)"), 1537 ]: 1538 ax.set_xlabel("Time") 1539 ax.set_ylabel(ylabel) 1540 ax.xaxis.set_major_formatter(mdates.DateFormatter("%-H")) 1541 ax.xaxis.set_major_locator(mdates.HourLocator(byhour=[0, 6, 12, 18, 24])) 1542 ax.set_xlim(time_points[0], time_points[-1]) 1543 ax.grid(True, alpha=0.3) 1544 ax.legend() 1545 1546 # グラフの保存 1547 plt.tight_layout() 1548 plt.savefig(output_path, dpi=300, bbox_inches="tight") 1549 plt.close() 1550 1551 # 統計情報の表示(オプション) 1552 for col, name in [(col_ch4_flux, "CH4"), (col_c2h6_flux, "C2H6")]: 1553 mean_val = hourly_means[(col, "mean")].mean() 1554 min_val = hourly_means[(col, "mean")].min() 1555 max_val = hourly_means[(col, "mean")].max() 1556 min_time = hourly_means[(col, "mean")].idxmin() 1557 max_time = hourly_means[(col, "mean")].idxmax() 1558 1559 self.logger.info(f"{name} Statistics:") 1560 self.logger.info(f"Mean: {mean_val:.2f}") 1561 self.logger.info(f"Min: {min_val:.2f} (Hour: {min_time})") 1562 self.logger.info(f"Max: {max_val:.2f} (Hour: {max_time})") 1563 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)
1565 def plot_scatter( 1566 self, 1567 df: pd.DataFrame, 1568 x_col: str, 1569 y_col: str, 1570 output_dir: str, 1571 output_filename: str = "scatter.png", 1572 xlabel: str | None = None, 1573 ylabel: str | None = None, 1574 add_label: bool = True, 1575 x_axis_range: tuple | None = None, 1576 y_axis_range: tuple | None = None, 1577 fixed_slope: float = 0.076, 1578 show_fixed_slope: bool = False, 1579 x_scientific: bool = False, # 追加:x軸を指数表記にするかどうか 1580 y_scientific: bool = False, # 追加:y軸を指数表記にするかどうか 1581 ) -> None: 1582 """散布図を作成し、TLS回帰直線を描画します。 1583 1584 Parameters 1585 ------ 1586 df : pd.DataFrame 1587 プロットに使用するデータフレーム 1588 x_col : str 1589 x軸に使用する列名 1590 y_col : str 1591 y軸に使用する列名 1592 xlabel : str 1593 x軸のラベル 1594 ylabel : str 1595 y軸のラベル 1596 output_dir : str 1597 出力先ディレクトリ 1598 output_filename : str, optional 1599 出力ファイル名。デフォルトは"scatter.png" 1600 add_label : bool, optional 1601 軸ラベルを表示するかどうか。デフォルトはTrue 1602 x_axis_range : tuple, optional 1603 x軸の範囲。デフォルトはNone。 1604 y_axis_range : tuple, optional 1605 y軸の範囲。デフォルトはNone。 1606 fixed_slope : float, optional 1607 固定傾きを指定するための値。デフォルトは0.076 1608 show_fixed_slope : bool, optional 1609 固定傾きの線を表示するかどうか。デフォルトはFalse 1610 """ 1611 os.makedirs(output_dir, exist_ok=True) 1612 output_path: str = os.path.join(output_dir, output_filename) 1613 1614 # 有効なデータの抽出 1615 df = MonthlyFiguresGenerator.get_valid_data(df, x_col, y_col) 1616 1617 # データの準備 1618 x = df[x_col].values 1619 y = df[y_col].values 1620 1621 # データの中心化 1622 x_mean = np.mean(x) 1623 y_mean = np.mean(y) 1624 x_c = x - x_mean 1625 y_c = y - y_mean 1626 1627 # TLS回帰の計算 1628 data_matrix = np.vstack((x_c, y_c)) 1629 cov_matrix = np.cov(data_matrix) 1630 _, eigenvecs = linalg.eigh(cov_matrix) 1631 largest_eigenvec = eigenvecs[:, -1] 1632 1633 slope = largest_eigenvec[1] / largest_eigenvec[0] 1634 intercept = y_mean - slope * x_mean 1635 1636 # R²とRMSEの計算 1637 y_pred = slope * x + intercept 1638 r_squared = 1 - np.sum((y - y_pred) ** 2) / np.sum((y - np.mean(y)) ** 2) 1639 rmse = np.sqrt(np.mean((y - y_pred) ** 2)) 1640 1641 # プロットの作成 1642 fig, ax = plt.subplots(figsize=(6, 6)) 1643 1644 # データ点のプロット 1645 ax.scatter(x, y, color="black") 1646 1647 # データの範囲を取得 1648 if x_axis_range is None: 1649 x_axis_range = (df[x_col].min(), df[x_col].max()) 1650 if y_axis_range is None: 1651 y_axis_range = (df[y_col].min(), df[y_col].max()) 1652 1653 # 回帰直線のプロット 1654 x_range = np.linspace(x_axis_range[0], x_axis_range[1], 150) 1655 y_range = slope * x_range + intercept 1656 ax.plot(x_range, y_range, "r", label="TLS regression") 1657 1658 # 傾き固定の線を追加(フラグがTrueの場合) 1659 if show_fixed_slope: 1660 fixed_intercept = ( 1661 y_mean - fixed_slope * x_mean 1662 ) # 中心点を通るように切片を計算 1663 y_fixed = fixed_slope * x_range + fixed_intercept 1664 ax.plot(x_range, y_fixed, "b--", label=f"Slope = {fixed_slope}", alpha=0.7) 1665 1666 # 軸の設定 1667 ax.set_xlim(x_axis_range) 1668 ax.set_ylim(y_axis_range) 1669 1670 # 指数表記の設定 1671 if x_scientific: 1672 ax.ticklabel_format(style="sci", axis="x", scilimits=(0, 0)) 1673 ax.xaxis.get_offset_text().set_position((1.1, 0)) # 指数の位置調整 1674 if y_scientific: 1675 ax.ticklabel_format(style="sci", axis="y", scilimits=(0, 0)) 1676 ax.yaxis.get_offset_text().set_position((0, 1.1)) # 指数の位置調整 1677 1678 if add_label: 1679 if xlabel is not None: 1680 ax.set_xlabel(xlabel) 1681 if ylabel is not None: 1682 ax.set_ylabel(ylabel) 1683 1684 # 1:1の関係を示す点線(軸の範囲が同じ場合のみ表示) 1685 if ( 1686 x_axis_range is not None 1687 and y_axis_range is not None 1688 and x_axis_range == y_axis_range 1689 ): 1690 ax.plot( 1691 [x_axis_range[0], x_axis_range[1]], 1692 [x_axis_range[0], x_axis_range[1]], 1693 "k--", 1694 alpha=0.5, 1695 ) 1696 1697 # 回帰情報の表示 1698 equation = ( 1699 f"y = {slope:.2f}x {'+' if intercept >= 0 else '-'} {abs(intercept):.2f}" 1700 ) 1701 position_x = 0.05 1702 fig_ha: str = "left" 1703 ax.text( 1704 position_x, 1705 0.95, 1706 equation, 1707 transform=ax.transAxes, 1708 va="top", 1709 ha=fig_ha, 1710 color="red", 1711 ) 1712 ax.text( 1713 position_x, 1714 0.88, 1715 f"R² = {r_squared:.2f}", 1716 transform=ax.transAxes, 1717 va="top", 1718 ha=fig_ha, 1719 color="red", 1720 ) 1721 ax.text( 1722 position_x, 1723 0.81, # RMSEのための新しい位置 1724 f"RMSE = {rmse:.2f}", 1725 transform=ax.transAxes, 1726 va="top", 1727 ha=fig_ha, 1728 color="red", 1729 ) 1730 # 目盛り線の設定 1731 ax.grid(True, alpha=0.3) 1732 1733 fig.savefig(output_path, dpi=300, bbox_inches="tight") 1734 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"
add_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
1736 def plot_source_contributions_diurnal( 1737 self, 1738 df: pd.DataFrame, 1739 output_dir: str, 1740 col_ch4_flux: str, 1741 col_c2h6_flux: str, 1742 color_bio: str = "blue", 1743 color_gas: str = "red", 1744 label_gas: str = "gas", 1745 label_bio: str = "bio", 1746 flux_alpha: float = 0.6, 1747 col_datetime: str = "Date", 1748 output_filename: str = "source_contributions.png", 1749 window_size: int = 6, # 移動平均の窓サイズ 1750 print_summary: bool = True, # 統計情報を表示するかどうか, 1751 add_legend: bool = True, 1752 smooth: bool = False, 1753 y_max: float = 100, # y軸の上限値を追加 1754 subplot_label: str | None = None, 1755 subplot_fontsize: int = 20, 1756 ) -> None: 1757 """CH4フラックスの都市ガス起源と生物起源の日変化を積み上げグラフとして表示 1758 1759 Parameters 1760 ------ 1761 df : pd.DataFrame 1762 データフレーム 1763 output_dir : str 1764 出力ディレクトリのパス 1765 col_ch4_flux : str 1766 CH4フラックスのカラム名 1767 col_c2h6_flux : str 1768 C2H6フラックスのカラム名 1769 label_gas : str 1770 都市ガス起源のラベル 1771 label_bio : str 1772 生物起源のラベル 1773 col_datetime : str 1774 日時カラムの名前 1775 output_filename : str 1776 出力ファイル名 1777 window_size : int 1778 移動平均の窓サイズ 1779 print_summary : bool 1780 統計情報を表示するかどうか 1781 smooth : bool 1782 移動平均を適用するかどうか 1783 y_max : float 1784 y軸の上限値(デフォルト: 100) 1785 """ 1786 # 出力ディレクトリの作成 1787 os.makedirs(output_dir, exist_ok=True) 1788 output_path: str = os.path.join(output_dir, output_filename) 1789 1790 # 起源の計算 1791 df_with_sources = self._calculate_source_contributions( 1792 df=df, 1793 col_ch4_flux=col_ch4_flux, 1794 col_c2h6_flux=col_c2h6_flux, 1795 col_datetime=col_datetime, 1796 ) 1797 1798 # 時刻データの抽出とグループ化 1799 df_with_sources["hour"] = df_with_sources.index.hour 1800 hourly_means = df_with_sources.groupby("hour")[["ch4_gas", "ch4_bio"]].mean() 1801 1802 # 24時間目のデータ点を追加(0時のデータを使用) 1803 last_hour = hourly_means.iloc[0:1].copy() 1804 last_hour.index = [24] 1805 hourly_means = pd.concat([hourly_means, last_hour]) 1806 1807 # 移動平均の適用 1808 hourly_means_smoothed = hourly_means 1809 if smooth: 1810 hourly_means_smoothed = hourly_means.rolling( 1811 window=window_size, center=True, min_periods=1 1812 ).mean() 1813 1814 # 24時間分のデータポイントを作成 1815 time_points = pd.date_range("2024-01-01", periods=25, freq="h") 1816 1817 # プロットの作成 1818 plt.figure(figsize=(10, 6)) 1819 ax = plt.gca() 1820 1821 # サブプロットラベルの追加(subplot_labelが指定されている場合) 1822 if subplot_label: 1823 ax.text( 1824 0.02, # x位置 1825 0.98, # y位置 1826 subplot_label, 1827 transform=ax.transAxes, 1828 va="top", 1829 fontsize=subplot_fontsize, 1830 ) 1831 1832 # 積み上げプロット 1833 ax.fill_between( 1834 time_points, 1835 0, 1836 hourly_means_smoothed["ch4_bio"], 1837 color=color_bio, 1838 alpha=flux_alpha, 1839 label=label_bio, 1840 ) 1841 ax.fill_between( 1842 time_points, 1843 hourly_means_smoothed["ch4_bio"], 1844 hourly_means_smoothed["ch4_bio"] + hourly_means_smoothed["ch4_gas"], 1845 color=color_gas, 1846 alpha=flux_alpha, 1847 label=label_gas, 1848 ) 1849 1850 # 合計値のライン 1851 total_flux = hourly_means_smoothed["ch4_bio"] + hourly_means_smoothed["ch4_gas"] 1852 ax.plot(time_points, total_flux, "-", color="black", alpha=0.5) 1853 1854 # 軸の設定 1855 ax.set_xlabel("Time (hour)") 1856 ax.set_ylabel(r"CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)") 1857 ax.xaxis.set_major_formatter(mdates.DateFormatter("%-H")) 1858 ax.xaxis.set_major_locator(mdates.HourLocator(byhour=[0, 6, 12, 18, 24])) 1859 ax.set_xlim(time_points[0], time_points[-1]) 1860 ax.set_ylim(0, y_max) # y軸の範囲を設定 1861 ax.grid(True, alpha=0.3) 1862 1863 # 凡例を図の下部に配置 1864 if add_legend: 1865 handles, labels = ax.get_legend_handles_labels() 1866 fig = plt.gcf() # 現在の図を取得 1867 fig.legend( 1868 handles, 1869 labels, 1870 loc="center", 1871 bbox_to_anchor=(0.5, 0.01), 1872 ncol=len(handles), 1873 ) 1874 plt.subplots_adjust(bottom=0.2) # 下部に凡例用のスペースを確保 1875 1876 # グラフの保存 1877 plt.tight_layout() 1878 plt.savefig(output_path, dpi=300, bbox_inches="tight") 1879 plt.close() 1880 1881 # 統計情報の表示 1882 if print_summary: 1883 stats = { 1884 "都市ガス起源": hourly_means["ch4_gas"], 1885 "生物起源": hourly_means["ch4_bio"], 1886 "合計": hourly_means["ch4_gas"] + hourly_means["ch4_bio"], 1887 } 1888 1889 for source, data in stats.items(): 1890 mean_val = data.mean() 1891 min_val = data.min() 1892 max_val = data.max() 1893 min_time = data.idxmin() 1894 max_time = data.idxmax() 1895 1896 self.logger.info(f"{source}の統計:") 1897 print(f" 平均値: {mean_val:.2f}") 1898 print(f" 最小値: {min_val:.2f} (Hour: {min_time})") 1899 print(f" 最大値: {max_val:.2f} (Hour: {max_time})") 1900 if min_val != 0: 1901 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)
1903 def plot_source_contributions_diurnal_by_date( 1904 self, 1905 df: pd.DataFrame, 1906 output_dir: str, 1907 col_ch4_flux: str, 1908 col_c2h6_flux: str, 1909 color_bio: str = "blue", 1910 color_gas: str = "red", 1911 label_bio: str = "bio", 1912 label_gas: str = "gas", 1913 flux_alpha: float = 0.6, 1914 col_datetime: str = "Date", 1915 output_filename: str = "source_contributions_by_date.png", 1916 add_label: bool = True, 1917 add_legend: bool = True, 1918 print_summary: bool = True, # 統計情報を表示するかどうか, 1919 subplot_fontsize: int = 20, 1920 subplot_label_weekday: str | None = None, 1921 subplot_label_weekend: str | None = None, 1922 y_max: float | None = None, # y軸の上限値 1923 ) -> None: 1924 """CH4フラックスの都市ガス起源と生物起源の日変化を平日・休日別に表示 1925 1926 Parameters 1927 ------ 1928 df : pd.DataFrame 1929 データフレーム 1930 output_dir : str 1931 出力ディレクトリのパス 1932 col_ch4_flux : str 1933 CH4フラックスのカラム名 1934 col_c2h6_flux : str 1935 C2H6フラックスのカラム名 1936 label_bio : str 1937 生物起源のラベル 1938 label_gas : str 1939 都市ガス起源のラベル 1940 col_datetime : str 1941 日時カラムの名前 1942 output_filename : str 1943 出力ファイル名 1944 add_label : bool 1945 ラベルを表示するか 1946 add_legend : bool 1947 凡例を表示するか 1948 subplot_fontsize : int 1949 サブプロットのフォントサイズ 1950 subplot_label_weekday : str | None 1951 平日グラフのラベル 1952 subplot_label_weekend : str | None 1953 休日グラフのラベル 1954 y_max : float | None 1955 y軸の上限値 1956 """ 1957 # 出力ディレクトリの作成 1958 os.makedirs(output_dir, exist_ok=True) 1959 output_path: str = os.path.join(output_dir, output_filename) 1960 1961 # 起源の計算 1962 df_with_sources = self._calculate_source_contributions( 1963 df=df, 1964 col_ch4_flux=col_ch4_flux, 1965 col_c2h6_flux=col_c2h6_flux, 1966 col_datetime=col_datetime, 1967 ) 1968 1969 # 日付タイプの分類 1970 dates = pd.to_datetime(df_with_sources.index) 1971 is_weekend = dates.dayofweek.isin([5, 6]) 1972 is_holiday = dates.map(lambda x: jpholiday.is_holiday(x.date())) 1973 is_weekday = ~(is_weekend | is_holiday) 1974 1975 # データの分類 1976 data_weekday = df_with_sources[is_weekday] 1977 data_holiday = df_with_sources[is_weekend | is_holiday] 1978 1979 # プロットの作成 1980 fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5)) 1981 1982 # 平日と休日それぞれのプロット 1983 for ax, data, label in [ 1984 (ax1, data_weekday, "Weekdays"), 1985 (ax2, data_holiday, "Weekends & Holidays"), 1986 ]: 1987 # 時間ごとの平均値を計算 1988 hourly_means = data.groupby(data.index.hour)[["ch4_gas", "ch4_bio"]].mean() 1989 1990 # 24時間目のデータ点を追加 1991 last_hour = hourly_means.iloc[0:1].copy() 1992 last_hour.index = [24] 1993 hourly_means = pd.concat([hourly_means, last_hour]) 1994 1995 # 24時間分のデータポイントを作成 1996 time_points = pd.date_range("2024-01-01", periods=25, freq="h") 1997 1998 # 積み上げプロット 1999 ax.fill_between( 2000 time_points, 2001 0, 2002 hourly_means["ch4_bio"], 2003 color=color_bio, 2004 alpha=flux_alpha, 2005 label=label_bio, 2006 ) 2007 ax.fill_between( 2008 time_points, 2009 hourly_means["ch4_bio"], 2010 hourly_means["ch4_bio"] + hourly_means["ch4_gas"], 2011 color=color_gas, 2012 alpha=flux_alpha, 2013 label=label_gas, 2014 ) 2015 2016 # 合計値のライン 2017 total_flux = hourly_means["ch4_bio"] + hourly_means["ch4_gas"] 2018 ax.plot(time_points, total_flux, "-", color="black", alpha=0.5) 2019 2020 # 軸の設定 2021 if add_label: 2022 ax.set_xlabel("Time (hour)") 2023 if ax == ax1: # 左側のプロットのラベル 2024 ax.set_ylabel("Weekdays CH$_4$ flux\n" r"(nmol m$^{-2}$ s$^{-1}$)") 2025 else: # 右側のプロットのラベル 2026 ax.set_ylabel("Weekends CH$_4$ flux\n" r"(nmol m$^{-2}$ s$^{-1}$)") 2027 2028 ax.xaxis.set_major_formatter(mdates.DateFormatter("%-H")) 2029 ax.xaxis.set_major_locator(mdates.HourLocator(byhour=[0, 6, 12, 18, 24])) 2030 ax.set_xlim(time_points[0], time_points[-1]) 2031 if y_max is not None: 2032 ax.set_ylim(0, y_max) 2033 ax.grid(True, alpha=0.3) 2034 2035 # サブプロットラベルの追加 2036 if subplot_label_weekday: 2037 ax1.text( 2038 0.02, 2039 0.98, 2040 subplot_label_weekday, 2041 transform=ax1.transAxes, 2042 va="top", 2043 fontsize=subplot_fontsize, 2044 ) 2045 if subplot_label_weekend: 2046 ax2.text( 2047 0.02, 2048 0.98, 2049 subplot_label_weekend, 2050 transform=ax2.transAxes, 2051 va="top", 2052 fontsize=subplot_fontsize, 2053 ) 2054 2055 # 凡例を図の下部に配置 2056 if add_legend: 2057 # 最初のプロットから凡例のハンドルとラベルを取得 2058 handles, labels = ax1.get_legend_handles_labels() 2059 # 図の下部に凡例を配置 2060 fig.legend( 2061 handles, 2062 labels, 2063 loc="center", 2064 bbox_to_anchor=(0.5, 0.01), # x=0.5で中央、y=0.01で下部に配置 2065 ncol=len(handles), # ハンドルの数だけ列を作成(一行に表示) 2066 ) 2067 # 凡例用のスペースを確保 2068 plt.subplots_adjust(bottom=0.2) # 下部に30%のスペースを確保 2069 2070 plt.tight_layout() 2071 plt.savefig(output_path, dpi=300, bbox_inches="tight") 2072 plt.close(fig=fig) 2073 2074 # 統計情報の表示 2075 if print_summary: 2076 for data, label in [ 2077 (data_weekday, "Weekdays"), 2078 (data_holiday, "Weekends & Holidays"), 2079 ]: 2080 hourly_means = data.groupby(data.index.hour)[ 2081 ["ch4_gas", "ch4_bio"] 2082 ].mean() 2083 2084 print(f"\n{label}の統計:") 2085 2086 # 都市ガス起源の統計 2087 gas_flux = hourly_means["ch4_gas"] 2088 bio_flux = hourly_means["ch4_bio"] 2089 2090 # 昼夜の時間帯を定義 2091 daytime_range: list[int] = [6, 19] # m~n時の場合、[m ,(n+1)]と定義 2092 daytime_hours = range(daytime_range[0], daytime_range[1]) 2093 nighttime_hours = list(range(0, daytime_range[0])) + list( 2094 range(daytime_range[1], 24) 2095 ) 2096 2097 # 昼間の統計 2098 daytime_gas = gas_flux[daytime_hours] 2099 daytime_bio = bio_flux[daytime_hours] 2100 daytime_total = daytime_gas + daytime_bio 2101 daytime_ratio = (daytime_gas.sum() / daytime_total.sum()) * 100 2102 2103 # 夜間の統計 2104 nighttime_gas = gas_flux[nighttime_hours] 2105 nighttime_bio = bio_flux[nighttime_hours] 2106 nighttime_total = nighttime_gas + nighttime_bio 2107 nighttime_ratio = (nighttime_gas.sum() / nighttime_total.sum()) * 100 2108 2109 print("\n都市ガス起源:") 2110 print(f" 平均値: {gas_flux.mean():.2f}") 2111 print(f" 最小値: {gas_flux.min():.2f} (Hour: {gas_flux.idxmin()})") 2112 print(f" 最大値: {gas_flux.max():.2f} (Hour: {gas_flux.idxmax()})") 2113 if gas_flux.min() != 0: 2114 print(f" 最大/最小比: {gas_flux.max() / gas_flux.min():.2f}") 2115 print( 2116 f" 全体に占める割合: {(gas_flux.sum() / (gas_flux.sum() + hourly_means['ch4_bio'].sum()) * 100):.1f}%" 2117 ) 2118 print( 2119 f" 昼間({daytime_range[0]}~{daytime_range[1] - 1}時)の割合: {daytime_ratio:.1f}%" 2120 ) 2121 print( 2122 f" 夜間({daytime_range[1] - 1}~{daytime_range[0]}時)の割合: {nighttime_ratio:.1f}%" 2123 ) 2124 2125 # 生物起源の統計 2126 bio_flux = hourly_means["ch4_bio"] 2127 print("\n生物起源:") 2128 print(f" 平均値: {bio_flux.mean():.2f}") 2129 print(f" 最小値: {bio_flux.min():.2f} (Hour: {bio_flux.idxmin()})") 2130 print(f" 最大値: {bio_flux.max():.2f} (Hour: {bio_flux.idxmax()})") 2131 if bio_flux.min() != 0: 2132 print(f" 最大/最小比: {bio_flux.max() / bio_flux.min():.2f}") 2133 print( 2134 f" 全体に占める割合: {(bio_flux.sum() / (gas_flux.sum() + bio_flux.sum()) * 100):.1f}%" 2135 ) 2136 2137 # 合計フラックスの統計 2138 total_flux = gas_flux + bio_flux 2139 print("\n合計:") 2140 print(f" 平均値: {total_flux.mean():.2f}") 2141 print(f" 最小値: {total_flux.min():.2f} (Hour: {total_flux.idxmin()})") 2142 print(f" 最大値: {total_flux.max():.2f} (Hour: {total_flux.idxmax()})") 2143 if total_flux.min() != 0: 2144 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_bio : str
生物起源のラベル
label_gas : str
都市ガス起源のラベル
col_datetime : str
日時カラムの名前
output_filename : str
出力ファイル名
add_label : bool
ラベルを表示するか
add_legend : bool
凡例を表示するか
subplot_fontsize : int
サブプロットのフォントサイズ
subplot_label_weekday : str | None
平日グラフのラベル
subplot_label_weekend : str | None
休日グラフのラベル
y_max : float | None
y軸の上限値
2146 def plot_spectra( 2147 self, 2148 fs: float, 2149 lag_second: float, 2150 input_dir: str | Path | None, 2151 output_dir: str | Path | None, 2152 output_basename: str = "spectrum", 2153 col_ch4: str = "Ultra_CH4_ppm_C", 2154 col_c2h6: str = "Ultra_C2H6_ppb", 2155 col_tv: str = "Tv", 2156 label_ch4: str | None = None, 2157 label_c2h6: str | None = None, 2158 label_tv: str | None = None, 2159 file_pattern: str = "*.csv", 2160 markersize: float = 14, 2161 are_inputs_resampled: bool = True, 2162 save_fig: bool = True, 2163 show_fig: bool = True, 2164 plot_power: bool = True, 2165 plot_co: bool = True, 2166 add_tv_in_co: bool = True, 2167 ) -> None: 2168 """ 2169 月間の平均パワースペクトル密度を計算してプロットする。 2170 2171 データファイルを指定されたディレクトリから読み込み、パワースペクトル密度を計算し、 2172 結果を指定された出力ディレクトリにプロットして保存します。 2173 2174 Parameters 2175 ------ 2176 fs : float 2177 サンプリング周波数。 2178 lag_second : float 2179 ラグ時間(秒)。 2180 input_dir : str | Path | None 2181 データファイルが格納されているディレクトリ。 2182 output_dir : str | Path | None 2183 出力先ディレクトリ。 2184 col_ch4 : str, optional 2185 CH4の濃度データが入ったカラムのキー。デフォルトは"Ultra_CH4_ppm_C"。 2186 col_c2h6 : str, optional 2187 C2H6の濃度データが入ったカラムのキー。デフォルトは"Ultra_C2H6_ppb"。 2188 col_tv : str, optional 2189 気温データが入ったカラムのキー。デフォルトは"Tv"。 2190 label_ch4 : str | None, optional 2191 CH4のラベル。デフォルトはNone。 2192 label_c2h6 : str | None, optional 2193 C2H6のラベル。デフォルトはNone。 2194 label_tv : str | None, optional 2195 気温のラベル。デフォルトはNone。 2196 file_pattern : str, optional 2197 処理対象のファイルパターン。デフォルトは"*.csv"。 2198 markersize : float, optional 2199 プロットマーカーのサイズ。デフォルトは14。 2200 are_inputs_resampled : bool, optional 2201 入力データが再サンプリングされているかどうか。デフォルトはTrue。 2202 save_fig : bool, optional 2203 図を保存するかどうか。デフォルトはTrue。 2204 show_fig : bool, optional 2205 図を表示するかどうか。デフォルトはTrue。 2206 plot_power : bool, optional 2207 パワースペクトルをプロットするかどうか。デフォルトはTrue。 2208 plot_co : bool, optional 2209 COのスペクトルをプロットするかどうか。デフォルトはTrue。 2210 add_tv_in_co : bool, optional 2211 顕熱フラックスのコスペクトルを表示するかどうか。デフォルトはTrue。 2212 """ 2213 # 出力ディレクトリの作成 2214 if save_fig: 2215 if output_dir is None: 2216 raise ValueError( 2217 "save_fig=Trueのとき、output_dirに有効なディレクトリパスを指定する必要があります。" 2218 ) 2219 os.makedirs(output_dir, exist_ok=True) 2220 2221 # データの読み込みと結合 2222 edp = EddyDataPreprocessor(fs=fs) 2223 col_wind_w: str = EddyDataPreprocessor.WIND_W 2224 2225 # 各変数のパワースペクトルを格納する辞書 2226 power_spectra = {col_ch4: [], col_c2h6: []} 2227 co_spectra = {col_ch4: [], col_c2h6: [], col_tv: []} 2228 freqs = None 2229 2230 # プログレスバーを表示しながらファイルを処理 2231 file_list = glob.glob(os.path.join(input_dir, file_pattern)) 2232 for filepath in tqdm(file_list, desc="Processing files"): 2233 df, _ = edp.get_resampled_df( 2234 filepath=filepath, resample_in_processing=are_inputs_resampled 2235 ) 2236 2237 # 風速成分の計算を追加 2238 df = edp.add_uvw_columns(df) 2239 2240 # NaNや無限大を含む行を削除 2241 df = df.replace([np.inf, -np.inf], np.nan).dropna( 2242 subset=[col_ch4, col_c2h6, col_tv, col_wind_w] 2243 ) 2244 2245 # データが十分な行数を持っているか確認 2246 if len(df) < 100: 2247 continue 2248 2249 # 各ファイルごとにスペクトル計算 2250 calculator = SpectrumCalculator( 2251 df=df, 2252 fs=fs, 2253 ) 2254 2255 for col in power_spectra.keys(): 2256 # 各変数のパワースペクトルを計算して保存 2257 if plot_power: 2258 f, ps = calculator.calculate_power_spectrum( 2259 col=col, 2260 dimensionless=True, 2261 frequency_weighted=True, 2262 interpolate_points=True, 2263 scaling="density", 2264 ) 2265 # 最初のファイル処理時にfreqsを初期化 2266 if freqs is None: 2267 freqs = f 2268 power_spectra[col].append(ps) 2269 # 以降は周波数配列の長さが一致する場合のみ追加 2270 elif len(f) == len(freqs): 2271 power_spectra[col].append(ps) 2272 2273 # コスペクトル 2274 if plot_co: 2275 _, cs, _ = calculator.calculate_co_spectrum( 2276 col1=col_wind_w, 2277 col2=col, 2278 dimensionless=True, 2279 frequency_weighted=True, 2280 interpolate_points=True, 2281 scaling="spectrum", 2282 apply_lag_correction_to_col2=True, 2283 lag_second=lag_second, 2284 ) 2285 if freqs is not None and len(cs) == len(freqs): 2286 co_spectra[col].append(cs) 2287 2288 # 顕熱フラックスのコスペクトル計算を追加 2289 if plot_co and add_tv_in_co: 2290 _, cs_heat, _ = calculator.calculate_co_spectrum( 2291 col1=col_wind_w, 2292 col2=col_tv, 2293 dimensionless=True, 2294 frequency_weighted=True, 2295 interpolate_points=True, 2296 scaling="spectrum", 2297 ) 2298 if freqs is not None and len(cs_heat) == len(freqs): 2299 co_spectra[col_tv].append(cs_heat) 2300 2301 # 各変数のスペクトルを平均化 2302 if plot_power: 2303 averaged_power_spectra = { 2304 col: np.mean(spectra, axis=0) for col, spectra in power_spectra.items() 2305 } 2306 if plot_co: 2307 averaged_co_spectra = { 2308 col: np.mean(spectra, axis=0) for col, spectra in co_spectra.items() 2309 } 2310 # 顕熱フラックスの平均コスペクトル計算 2311 if plot_co and add_tv_in_co and co_spectra[col_tv]: 2312 averaged_heat_co_spectra = np.mean(co_spectra[col_tv], axis=0) 2313 2314 # プロット設定を修正 2315 plot_configs = [ 2316 { 2317 "col": col_ch4, 2318 "psd_ylabel": r"$fS_{\mathrm{CH_4}} / s_{\mathrm{CH_4}}^2$", 2319 "co_ylabel": r"$fC_{w\mathrm{CH_4}} / \overline{w'\mathrm{CH_4}'}$", 2320 "color": "red", 2321 "label": label_ch4, 2322 }, 2323 { 2324 "col": col_c2h6, 2325 "psd_ylabel": r"$fS_{\mathrm{C_2H_6}} / s_{\mathrm{C_2H_6}}^2$", 2326 "co_ylabel": r"$fC_{w\mathrm{C_2H_6}} / \overline{w'\mathrm{C_2H_6}'}$", 2327 "color": "orange", 2328 "label": label_c2h6, 2329 }, 2330 ] 2331 plot_tv_config = { 2332 "col": col_tv, 2333 "psd_ylabel": r"$fS_{T_v} / s_{T_v}^2$", 2334 "co_ylabel": r"$fC_{wT_v} / \overline{w'T_v'}$", 2335 "color": "blue", 2336 "label": label_tv, 2337 } 2338 2339 # パワースペクトルの図を作成 2340 if plot_power: 2341 fig_power, axes_psd = plt.subplots(1, 2, figsize=(12, 5), sharex=True) 2342 for ax, config in zip(axes_psd, plot_configs): 2343 ax.plot( 2344 freqs, 2345 averaged_power_spectra[config["col"]], 2346 "o", # マーカーを丸に設定 2347 color=config["color"], 2348 markersize=markersize, 2349 ) 2350 ax.set_xscale("log") 2351 ax.set_yscale("log") 2352 ax.set_xlim(0.001, 10) 2353 ax.plot([0.01, 10], [1, 0.01], "-", color="black", alpha=0.5) 2354 ax.text(0.1, 0.06, "-2/3", fontsize=18) 2355 ax.set_ylabel(config["psd_ylabel"]) 2356 if config["label"] is not None: 2357 ax.text( 2358 0.02, 0.98, config["label"], transform=ax.transAxes, va="top" 2359 ) 2360 ax.grid(True, alpha=0.3) 2361 ax.set_xlabel("f (Hz)") 2362 2363 plt.tight_layout() 2364 2365 if save_fig: 2366 output_path_psd: str = os.path.join( 2367 output_dir, f"power_{output_basename}.png" 2368 ) 2369 plt.savefig( 2370 output_path_psd, 2371 dpi=300, 2372 bbox_inches="tight", 2373 ) 2374 if show_fig: 2375 plt.show() 2376 else: 2377 plt.close(fig=fig_power) 2378 2379 # コスペクトルの図を作成 2380 if plot_co: 2381 fig_co, axes_cosp = plt.subplots(1, 2, figsize=(12, 5), sharex=True) 2382 for ax, config in zip(axes_cosp, plot_configs): 2383 # 顕熱フラックスのコスペクトルを先に描画(背景として) 2384 if add_tv_in_co and len(co_spectra[col_tv]) > 0: 2385 ax.plot( 2386 freqs, 2387 averaged_heat_co_spectra, 2388 "o", 2389 color="gray", 2390 alpha=0.3, 2391 markersize=markersize, 2392 label=plot_tv_config["label"] 2393 if plot_tv_config["label"] 2394 else None, 2395 ) 2396 2397 # CH4またはC2H6のコスペクトルを描画 2398 ax.plot( 2399 freqs, 2400 averaged_co_spectra[config["col"]], 2401 "o", 2402 color=config["color"], 2403 markersize=markersize, 2404 label=config["label"] if config["label"] else None, 2405 ) 2406 ax.set_xscale("log") 2407 ax.set_yscale("log") 2408 ax.set_xlim(0.001, 10) 2409 # ax.plot([0.01, 10], [1, 0.01], "-", color="black", alpha=0.5) 2410 # ax.text(0.1, 0.1, "-4/3", fontsize=18) 2411 ax.set_ylabel(config["co_ylabel"]) 2412 if config["label"] is not None: 2413 ax.text( 2414 0.02, 0.98, config["label"], transform=ax.transAxes, va="top" 2415 ) 2416 ax.grid(True, alpha=0.3) 2417 ax.set_xlabel("f (Hz)") 2418 # 凡例を追加(顕熱フラックスが含まれる場合) 2419 if add_tv_in_co and label_tv: 2420 ax.legend(loc="lower left") 2421 2422 plt.tight_layout() 2423 if save_fig: 2424 output_path_csd: str = os.path.join( 2425 output_dir, f"co_{output_basename}.png" 2426 ) 2427 plt.savefig( 2428 output_path_csd, 2429 dpi=300, 2430 bbox_inches="tight", 2431 ) 2432 if show_fig: 2433 plt.show() 2434 else: 2435 plt.close(fig=fig_co)
月間の平均パワースペクトル密度を計算してプロットする。
データファイルを指定されたディレクトリから読み込み、パワースペクトル密度を計算し、 結果を指定された出力ディレクトリにプロットして保存します。
Parameters
fs : float
サンプリング周波数。
lag_second : float
ラグ時間(秒)。
input_dir : str | Path | None
データファイルが格納されているディレクトリ。
output_dir : str | Path | None
出力先ディレクトリ。
col_ch4 : str, optional
CH4の濃度データが入ったカラムのキー。デフォルトは"Ultra_CH4_ppm_C"。
col_c2h6 : str, optional
C2H6の濃度データが入ったカラムのキー。デフォルトは"Ultra_C2H6_ppb"。
col_tv : str, optional
気温データが入ったカラムのキー。デフォルトは"Tv"。
label_ch4 : str | None, optional
CH4のラベル。デフォルトはNone。
label_c2h6 : str | None, optional
C2H6のラベル。デフォルトはNone。
label_tv : str | None, optional
気温のラベル。デフォルトはNone。
file_pattern : str, optional
処理対象のファイルパターン。デフォルトは"*.csv"。
markersize : float, optional
プロットマーカーのサイズ。デフォルトは14。
are_inputs_resampled : bool, optional
入力データが再サンプリングされているかどうか。デフォルトはTrue。
save_fig : bool, optional
図を保存するかどうか。デフォルトはTrue。
show_fig : bool, optional
図を表示するかどうか。デフォルトはTrue。
plot_power : bool, optional
パワースペクトルをプロットするかどうか。デフォルトはTrue。
plot_co : bool, optional
COのスペクトルをプロットするかどうか。デフォルトはTrue。
add_tv_in_co : bool, optional
顕熱フラックスのコスペクトルを表示するかどうか。デフォルトはTrue。
2437 def plot_turbulence( 2438 self, 2439 df: pd.DataFrame, 2440 output_dir: str, 2441 output_filename: str = "turbulence.png", 2442 col_uz: str = "Uz", 2443 col_ch4: str = "Ultra_CH4_ppm_C", 2444 col_c2h6: str = "Ultra_C2H6_ppb", 2445 col_timestamp: str = "TIMESTAMP", 2446 add_serial_labels: bool = True, 2447 ) -> None: 2448 """時系列データのプロットを作成する 2449 2450 Parameters 2451 ------ 2452 df : pd.DataFrame 2453 プロットするデータを含むDataFrame 2454 output_dir : str 2455 出力ディレクトリのパス 2456 output_filename : str 2457 出力ファイル名 2458 col_uz : str 2459 鉛直風速データのカラム名 2460 col_ch4 : str 2461 メタンデータのカラム名 2462 col_c2h6 : str 2463 エタンデータのカラム名 2464 col_timestamp : str 2465 タイムスタンプのカラム名 2466 """ 2467 # 出力ディレクトリの作成 2468 os.makedirs(output_dir, exist_ok=True) 2469 output_path: str = os.path.join(output_dir, output_filename) 2470 2471 # データの前処理 2472 df = df.copy() 2473 2474 # タイムスタンプをインデックスに設定(まだ設定されていない場合) 2475 if not isinstance(df.index, pd.DatetimeIndex): 2476 df[col_timestamp] = pd.to_datetime(df[col_timestamp]) 2477 df.set_index(col_timestamp, inplace=True) 2478 2479 # 開始時刻と終了時刻を取得 2480 start_time = df.index[0] 2481 end_time = df.index[-1] 2482 2483 # 開始時刻の分を取得 2484 start_minute = start_time.minute 2485 2486 # 時間軸の作成(実際の開始時刻からの経過分数) 2487 minutes_elapsed = (df.index - start_time).total_seconds() / 60 2488 2489 # プロットの作成 2490 _, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=(12, 10), sharex=True) 2491 2492 # 鉛直風速 2493 ax1.plot(minutes_elapsed, df[col_uz], "k-", linewidth=0.5) 2494 ax1.set_ylabel(r"$w$ (m s$^{-1}$)") 2495 if add_serial_labels: 2496 ax1.text(0.02, 0.98, "(a)", transform=ax1.transAxes, va="top") 2497 ax1.grid(True, alpha=0.3) 2498 2499 # CH4濃度 2500 ax2.plot(minutes_elapsed, df[col_ch4], "r-", linewidth=0.5) 2501 ax2.set_ylabel(r"$\mathrm{CH_4}$ (ppm)") 2502 if add_serial_labels: 2503 ax2.text(0.02, 0.98, "(b)", transform=ax2.transAxes, va="top") 2504 ax2.grid(True, alpha=0.3) 2505 2506 # C2H6濃度 2507 ax3.plot(minutes_elapsed, df[col_c2h6], "orange", linewidth=0.5) 2508 ax3.set_ylabel(r"$\mathrm{C_2H_6}$ (ppb)") 2509 if add_serial_labels: 2510 ax3.text(0.02, 0.98, "(c)", transform=ax3.transAxes, va="top") 2511 ax3.grid(True, alpha=0.3) 2512 ax3.set_xlabel("Time (minutes)") 2513 2514 # x軸の範囲を実際の開始時刻から30分後までに設定 2515 total_minutes = (end_time - start_time).total_seconds() / 60 2516 ax3.set_xlim(0, min(30, total_minutes)) 2517 2518 # x軸の目盛りを5分間隔で設定 2519 np.arange(start_minute, start_minute + 35, 5) 2520 ax3.xaxis.set_major_locator(MultipleLocator(5)) 2521 2522 # レイアウトの調整 2523 plt.tight_layout() 2524 2525 # 図の保存 2526 plt.savefig(output_path, dpi=300, bbox_inches="tight") 2527 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
タイムスタンプのカラム名
2529 def plot_wind_rose_sources( 2530 self, 2531 df: pd.DataFrame, 2532 output_dir: str | Path | None = None, 2533 output_filename: str = "edp_wind_rose.png", 2534 col_datetime: str = "Date", 2535 col_ch4_flux: str = "Fch4", 2536 col_c2h6_flux: str = "Fc2h6", 2537 col_wind_dir: str = "Wind direction", 2538 flux_unit: str = r"(nmol m$^{-2}$ s$^{-1}$)", 2539 ymax: float | None = None, # フラックスの上限値 2540 color_bio: str = "blue", 2541 color_gas: str = "red", 2542 label_bio: str = "生物起源", 2543 label_gas: str = "都市ガス起源", 2544 figsize: tuple[float, float] = (8, 8), 2545 flux_alpha: float = 0.4, 2546 num_directions: int = 8, # 方位の数(8方位) 2547 gap_degrees: float = 0.0, # セクター間の隙間(度数) 2548 center_on_angles: bool = True, # 追加:45度刻みの線を境界にするかどうか 2549 subplot_label: str | None = None, 2550 add_legend: bool = True, 2551 stack_bars: bool = True, # 追加:積み上げ方式を選択するパラメータ 2552 print_summary: bool = True, # 統計情報を表示するかどうか 2553 save_fig: bool = True, 2554 show_fig: bool = True, 2555 ) -> None: 2556 """CH4フラックスの都市ガス起源と生物起源の風配図を作成する関数 2557 2558 Parameters 2559 ------ 2560 df : pd.DataFrame 2561 風配図を作成するためのデータフレーム 2562 output_dir : str | Path | None 2563 生成された図を保存するディレクトリのパス 2564 output_filename : str 2565 保存するファイル名(デフォルトは"edp_wind_rose.png") 2566 col_ch4_flux : str 2567 CH4フラックスを示すカラム名 2568 col_c2h6_flux : str 2569 C2H6フラックスを示すカラム名 2570 col_wind_dir : str 2571 風向を示すカラム名 2572 color_bio : str 2573 生物起源のフラックスに対する色 2574 color_gas : str 2575 都市ガス起源のフラックスに対する色 2576 風向を示すカラム名 2577 label_bio : str 2578 生物起源のフラックスに対するラベル 2579 label_gas : str 2580 都市ガス起源のフラックスに対するラベル 2581 col_datetime : str 2582 日時を示すカラム名 2583 num_directions : int 2584 風向の数(デフォルトは8) 2585 gap_degrees : float 2586 セクター間の隙間の大きさ(度数)。0の場合は隙間なし。 2587 center_on_angles: bool 2588 Trueの場合、45度刻みの線を境界として扇形を描画します。 2589 Falseの場合、45度の中間(22.5度)を中心として扇形を描画します。 2590 subplot_label : str 2591 サブプロットに表示するラベル 2592 print_summary : bool 2593 統計情報を表示するかどうかのフラグ 2594 flux_unit : str 2595 フラックスの単位 2596 ymax : float | None 2597 y軸の上限値(指定しない場合はデータの最大値に基づいて自動設定) 2598 figsize : tuple[float, float] 2599 図のサイズ 2600 flux_alpha : float 2601 フラックスの透明度 2602 stack_bars : bool, optional 2603 Trueの場合、生物起源の上に都市ガス起源を積み上げます(デフォルト)。 2604 Falseの場合、両方を0から積み上げます。 2605 save_fig : bool 2606 図を保存するかどうかのフラグ 2607 show_fig : bool 2608 図を表示するかどうかのフラグ 2609 """ 2610 # 起源の計算 2611 df_with_sources = self._calculate_source_contributions( 2612 df=df, 2613 col_ch4_flux=col_ch4_flux, 2614 col_c2h6_flux=col_c2h6_flux, 2615 col_datetime=col_datetime, 2616 ) 2617 2618 # 方位の定義 2619 direction_ranges = self._define_direction_ranges( 2620 num_directions, center_on_angles 2621 ) 2622 2623 # 方位ごとのデータを集計 2624 direction_data = self._aggregate_direction_data( 2625 df_with_sources, col_wind_dir, direction_ranges 2626 ) 2627 2628 # プロットの作成 2629 fig = plt.figure(figsize=figsize) 2630 ax = fig.add_subplot(111, projection="polar") 2631 2632 # 方位の角度(ラジアン)を計算 2633 theta = np.array( 2634 [np.radians(angle) for angle in direction_data["center_angle"]] 2635 ) 2636 2637 # セクターの幅を計算(隙間を考慮) 2638 sector_width = np.radians((360.0 / num_directions) - gap_degrees) 2639 2640 # 積み上げ方式に応じてプロット 2641 if stack_bars: 2642 # 生物起源を基準として描画 2643 ax.bar( 2644 theta, 2645 direction_data["bio_flux"], 2646 width=sector_width, # 隙間を考慮した幅 2647 bottom=0.0, 2648 color=color_bio, 2649 alpha=flux_alpha, 2650 label=label_bio, 2651 ) 2652 # 都市ガス起源を生物起源の上に積み上げ 2653 ax.bar( 2654 theta, 2655 direction_data["gas_flux"], 2656 width=sector_width, # 隙間を考慮した幅 2657 bottom=direction_data["bio_flux"], 2658 color=color_gas, 2659 alpha=flux_alpha, 2660 label=label_gas, 2661 ) 2662 else: 2663 # 両方を0から積み上げ 2664 ax.bar( 2665 theta, 2666 direction_data["bio_flux"], 2667 width=sector_width, # 隙間を考慮した幅 2668 bottom=0.0, 2669 color=color_bio, 2670 alpha=flux_alpha, 2671 label=label_bio, 2672 ) 2673 ax.bar( 2674 theta, 2675 direction_data["gas_flux"], 2676 width=sector_width, # 隙間を考慮した幅 2677 bottom=0.0, 2678 color=color_gas, 2679 alpha=flux_alpha, 2680 label=label_gas, 2681 ) 2682 2683 # y軸の範囲を設定 2684 if ymax is not None: 2685 ax.set_ylim(0, ymax) 2686 else: 2687 # データの最大値に基づいて自動設定 2688 max_value = max( 2689 direction_data["bio_flux"].max(), direction_data["gas_flux"].max() 2690 ) 2691 ax.set_ylim(0, max_value * 1.1) # 最大値の1.1倍を上限に設定 2692 2693 # 方位ラベルの設定 2694 ax.set_theta_zero_location("N") # 北を上に設定 2695 ax.set_theta_direction(-1) # 時計回りに設定 2696 2697 # 方位ラベルの表示 2698 labels = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"] 2699 angles = np.radians(np.linspace(0, 360, len(labels), endpoint=False)) 2700 ax.set_xticks(angles) 2701 ax.set_xticklabels(labels) 2702 2703 # プロット領域の調整(上部と下部にスペースを確保) 2704 plt.subplots_adjust( 2705 top=0.8, # 上部に20%のスペースを確保 2706 bottom=0.2, # 下部に20%のスペースを確保(凡例用) 2707 ) 2708 2709 # サブプロットラベルの追加(デフォルトは左上) 2710 if subplot_label: 2711 ax.text( 2712 0.01, 2713 0.99, 2714 subplot_label, 2715 transform=ax.transAxes, 2716 ) 2717 2718 # 単位の追加(図の下部中央に配置) 2719 plt.figtext( 2720 0.5, # x位置(中央) 2721 0.1, # y位置(下部) 2722 flux_unit, 2723 ha="center", # 水平方向の位置揃え 2724 va="bottom", # 垂直方向の位置揃え 2725 ) 2726 2727 # 凡例の追加(単位の下に配置) 2728 if add_legend: 2729 # 最初のプロットから凡例のハンドルとラベルを取得 2730 handles, labels = ax.get_legend_handles_labels() 2731 # 図の下部に凡例を配置 2732 fig.legend( 2733 handles, 2734 labels, 2735 loc="center", 2736 bbox_to_anchor=(0.5, 0.05), # x=0.5で中央、y=0.05で下部に配置 2737 ncol=len(handles), # ハンドルの数だけ列を作成(一行に表示) 2738 ) 2739 2740 # グラフの保存 2741 if save_fig: 2742 if output_dir is None: 2743 raise ValueError( 2744 "save_fig=Trueのとき、output_dirに有効なパスを指定する必要があります。" 2745 ) 2746 # 出力ディレクトリの作成 2747 os.makedirs(output_dir, exist_ok=True) 2748 output_path: str = os.path.join(output_dir, output_filename) 2749 plt.savefig(output_path, dpi=300, bbox_inches="tight") 2750 2751 # グラフの表示 2752 if show_fig: 2753 plt.show() 2754 else: 2755 plt.close(fig=fig) 2756 2757 # 統計情報の表示 2758 if print_summary: 2759 for source in ["gas", "bio"]: 2760 flux_data = direction_data[f"{source}_flux"] 2761 mean_val = flux_data.mean() 2762 max_val = flux_data.max() 2763 max_dir = direction_data.loc[flux_data.idxmax(), "name"] 2764 2765 self.logger.info( 2766 f"{label_gas if source == 'gas' else label_bio}の統計:" 2767 ) 2768 print(f" 平均フラックス: {mean_val:.2f}") 2769 print(f" 最大フラックス: {max_val:.2f}") 2770 print(f" 最大フラックスの方位: {max_dir}")
CH4フラックスの都市ガス起源と生物起源の風配図を作成する関数
Parameters
df : pd.DataFrame
風配図を作成するためのデータフレーム
output_dir : str | Path | None
生成された図を保存するディレクトリのパス
output_filename : str
保存するファイル名(デフォルトは"edp_wind_rose.png")
col_ch4_flux : str
CH4フラックスを示すカラム名
col_c2h6_flux : str
C2H6フラックスを示すカラム名
col_wind_dir : str
風向を示すカラム名
color_bio : str
生物起源のフラックスに対する色
color_gas : str
都市ガス起源のフラックスに対する色
風向を示すカラム名
label_bio : str
生物起源のフラックスに対するラベル
label_gas : str
都市ガス起源のフラックスに対するラベル
col_datetime : str
日時を示すカラム名
num_directions : int
風向の数(デフォルトは8)
gap_degrees : float
セクター間の隙間の大きさ(度数)。0の場合は隙間なし。
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
フラックスの透明度
stack_bars : bool, optional
Trueの場合、生物起源の上に都市ガス起源を積み上げます(デフォルト)。
Falseの場合、両方を0から積み上げます。
save_fig : bool
図を保存するかどうかのフラグ
show_fig : bool
図を表示するかどうかのフラグ
3055 @staticmethod 3056 def get_valid_data(df: pd.DataFrame, x_col: str, y_col: str) -> pd.DataFrame: 3057 """ 3058 指定された列の有効なデータ(NaNを除いた)を取得します。 3059 3060 Parameters 3061 ------ 3062 df : pd.DataFrame 3063 データフレーム 3064 x_col : str 3065 X軸の列名 3066 y_col : str 3067 Y軸の列名 3068 3069 Returns 3070 ------ 3071 pd.DataFrame 3072 有効なデータのみを含むDataFrame 3073 """ 3074 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
3076 @staticmethod 3077 def setup_logger(logger: Logger | None, log_level: int = INFO) -> Logger: 3078 """ 3079 ロガーを設定します。 3080 3081 このメソッドは、ロギングの設定を行い、ログメッセージのフォーマットを指定します。 3082 ログメッセージには、日付、ログレベル、メッセージが含まれます。 3083 3084 渡されたロガーがNoneまたは不正な場合は、新たにロガーを作成し、標準出力に 3085 ログメッセージが表示されるようにStreamHandlerを追加します。ロガーのレベルは 3086 引数で指定されたlog_levelに基づいて設定されます。 3087 3088 Parameters 3089 ------ 3090 logger : Logger | None 3091 使用するロガー。Noneの場合は新しいロガーを作成します。 3092 log_level : int 3093 ロガーのログレベル。デフォルトはINFO。 3094 3095 Returns 3096 ------ 3097 Logger 3098 設定されたロガーオブジェクト。 3099 """ 3100 if logger is not None and isinstance(logger, Logger): 3101 return logger 3102 # 渡されたロガーがNoneまたは正しいものでない場合は独自に設定 3103 new_logger: Logger = getLogger() 3104 # 既存のハンドラーをすべて削除 3105 for handler in new_logger.handlers[:]: 3106 new_logger.removeHandler(handler) 3107 new_logger.setLevel(log_level) # ロガーのレベルを設定 3108 ch = StreamHandler() 3109 ch_formatter = Formatter("%(asctime)s - %(levelname)s - %(message)s") 3110 ch.setFormatter(ch_formatter) # フォーマッターをハンドラーに設定 3111 new_logger.addHandler(ch) # StreamHandlerの追加 3112 return new_logger
ロガーを設定します。
このメソッドは、ロギングの設定を行い、ログメッセージのフォーマットを指定します。 ログメッセージには、日付、ログレベル、メッセージが含まれます。
渡されたロガーがNoneまたは不正な場合は、新たにロガーを作成し、標準出力に ログメッセージが表示されるようにStreamHandlerを追加します。ロガーのレベルは 引数で指定されたlog_levelに基づいて設定されます。
Parameters
logger : Logger | None
使用するロガー。Noneの場合は新しいロガーを作成します。
log_level : int
ロガーのログレベル。デフォルトはINFO。
Returns
Logger
設定されたロガーオブジェクト。
3114 @staticmethod 3115 def plot_fluxes_distributions( 3116 flux_data: dict[str, pd.Series], 3117 month: int, 3118 output_dir: str | Path | None = None, 3119 output_filename: str = "flux_distribution.png", 3120 colors: dict[str, str] | None = None, 3121 xlim: tuple[float, float] = (-50, 200), 3122 bandwidth: float = 1.0, 3123 save_fig: bool = True, 3124 show_fig: bool = True, 3125 ) -> None: 3126 """複数のフラックスデータの分布を可視化 3127 3128 Parameters 3129 ------ 3130 flux_data : dict[str, pd.Series] 3131 各測器のフラックスデータを格納した辞書 3132 キー: 測器名, 値: フラックスデータ 3133 month : int 3134 測定月 3135 output_dir : str | Path | None 3136 出力ディレクトリ。指定しない場合はデフォルトのディレクトリに保存されます。 3137 output_filename : str 3138 出力ファイル名。デフォルトは"flux_distribution.png"です。 3139 colors : dict[str, str] | None 3140 各測器の色を指定する辞書。指定がない場合は自動で色を割り当てます。 3141 xlim : tuple[float, float] 3142 x軸の範囲。デフォルトは(-50, 200)です。 3143 bandwidth : float 3144 カーネル密度推定のバンド幅調整係数。デフォルトは1.0です。 3145 save_fig : bool 3146 図を保存するかどうか。デフォルトはTrueです。 3147 show_fig : bool 3148 図を表示するかどうか。デフォルトはTrueです。 3149 """ 3150 # デフォルトの色を設定 3151 default_colors = plt.rcParams["axes.prop_cycle"].by_key()["color"] 3152 if colors is None: 3153 colors = { 3154 name: default_colors[i % len(default_colors)] 3155 for i, name in enumerate(flux_data.keys()) 3156 } 3157 3158 fig = plt.figure(figsize=(10, 6)) 3159 3160 # 統計情報を格納する辞書 3161 stats_info = {} 3162 3163 # 各測器のデータをプロット 3164 for i, (name, flux) in enumerate(flux_data.items()): 3165 # nanを除去 3166 flux = flux.dropna() 3167 color = colors.get(name, default_colors[i % len(default_colors)]) 3168 3169 # KDEプロット 3170 sns.kdeplot( 3171 data=flux, 3172 label=name, 3173 color=color, 3174 alpha=0.5, 3175 bw_adjust=bandwidth, 3176 ) 3177 3178 # 平均値と中央値のマーカー 3179 mean_val = flux.mean() 3180 median_val = np.median(flux) 3181 plt.axvline( 3182 mean_val, 3183 color=color, 3184 linestyle="--", 3185 alpha=0.5, 3186 label=f"{name} mean", 3187 ) 3188 plt.axvline( 3189 median_val, 3190 color=color, 3191 linestyle=":", 3192 alpha=0.5, 3193 label=f"{name} median", 3194 ) 3195 3196 # 統計情報を保存 3197 stats_info[name] = { 3198 "mean": mean_val, 3199 "median": median_val, 3200 "std": flux.std(), 3201 } 3202 3203 # 軸ラベルとタイトル 3204 plt.xlabel(r"CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)") 3205 plt.ylabel("Probability Density") 3206 plt.title(f"Distribution of CH$_4$ fluxes - Month {month}") 3207 3208 # x軸の範囲設定 3209 plt.xlim(xlim) 3210 3211 # グリッド表示 3212 plt.grid(True, alpha=0.3) 3213 3214 # 統計情報のテキスト作成 3215 stats_text = "" 3216 for name, stats_item in stats_info.items(): 3217 stats_text += ( 3218 f"{name}:\n" 3219 f" Mean: {stats_item['mean']:.2f}\n" 3220 f" Median: {stats_item['median']:.2f}\n" 3221 f" Std: {stats_item['std']:.2f}\n" 3222 ) 3223 3224 # 統計情報の表示 3225 plt.text( 3226 0.02, 3227 0.98, 3228 stats_text.rstrip(), # 最後の改行を削除 3229 transform=plt.gca().transAxes, 3230 verticalalignment="top", 3231 fontsize=10, 3232 bbox=dict(boxstyle="round", facecolor="white", alpha=0.8), 3233 ) 3234 3235 # 凡例の表示 3236 plt.legend(loc="upper right") 3237 plt.tight_layout() 3238 3239 # グラフの保存 3240 if save_fig: 3241 if output_dir is None: 3242 raise ValueError( 3243 "save_fig=Trueのとき、output_dirに有効なディレクトリパスを指定する必要があります。" 3244 ) 3245 os.makedirs(output_dir, exist_ok=True) 3246 plt.savefig( 3247 os.path.join(output_dir, f"{output_filename.format(month=month)}"), 3248 dpi=300, 3249 bbox_inches="tight", 3250 ) 3251 if show_fig: 3252 plt.show() 3253 else: 3254 plt.close(fig=fig)
複数のフラックスデータの分布を可視化
Parameters
flux_data : dict[str, pd.Series]
各測器のフラックスデータを格納した辞書
キー: 測器名, 値: フラックスデータ
month : int
測定月
output_dir : str | Path | None
出力ディレクトリ。指定しない場合はデフォルトのディレクトリに保存されます。
output_filename : str
出力ファイル名。デフォルトは"flux_distribution.png"です。
colors : dict[str, str] | None
各測器の色を指定する辞書。指定がない場合は自動で色を割り当てます。
xlim : tuple[float, float]
x軸の範囲。デフォルトは(-50, 200)です。
bandwidth : float
カーネル密度推定のバンド幅調整係数。デフォルトは1.0です。
save_fig : bool
図を保存するかどうか。デフォルトはTrueです。
show_fig : bool
図を表示するかどうか。デフォルトはTrueです。
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_struct: 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_struct : 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_struct = output_dirs_struct or self.DEFAULT_OUTPUT_DIRS 67 self._good_data_path: str = os.path.join( 68 output_dir, self._output_dirs_struct["GOOD_DATA"] 69 ) 70 self._bad_data_path: str = os.path.join( 71 output_dir, self._output_dirs_struct["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_struct: 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_struct : 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_struct = output_dirs_struct or self.DEFAULT_OUTPUT_DIRS 67 self._good_data_path: str = os.path.join( 68 output_dir, self._output_dirs_struct["GOOD_DATA"] 69 ) 70 self._bad_data_path: str = os.path.join( 71 output_dir, self._output_dirs_struct["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_struct : 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
設定されたロガーオブジェクト。
10class TransferFunctionCalculator: 11 """ 12 このクラスは、CSVファイルからデータを読み込み、処理し、 13 伝達関数を計算してプロットするための機能を提供します。 14 15 この実装は Moore (1986) の論文に基づいています。 16 """ 17 18 def __init__( 19 self, 20 file_path: str, 21 col_freq: str, 22 cutoff_freq_low: float = 0.01, 23 cutoff_freq_high: float = 1, 24 ): 25 """ 26 TransferFunctionCalculatorクラスのコンストラクタ。 27 28 Parameters 29 ---------- 30 file_path : str 31 分析対象のCSVファイルのパス。 32 col_freq : str 33 周波数のキー。 34 cutoff_freq_low : float 35 カットオフ周波数の最低値。 36 cutoff_freq_high : float 37 カットオフ周波数の最高値。 38 """ 39 self._col_freq: str = col_freq 40 self._cutoff_freq_low: float = cutoff_freq_low 41 self._cutoff_freq_high: float = cutoff_freq_high 42 self._df: pd.DataFrame = TransferFunctionCalculator._load_data(file_path) 43 44 def calculate_transfer_function( 45 self, col_reference: str, col_target: str 46 ) -> tuple[float, float, pd.DataFrame]: 47 """ 48 伝達関数の係数を計算する。 49 50 Parameters 51 ---------- 52 col_reference : str 53 参照データのカラム名。 54 col_target : str 55 ターゲットデータのカラム名。 56 57 Returns 58 ---------- 59 tuple[float, float, pandas.DataFrame] 60 伝達関数の係数aとその標準誤差、および計算に用いたDataFrame。 61 """ 62 df_processed: pd.DataFrame = self.process_data( 63 col_reference=col_reference, col_target=col_target 64 ) 65 df_cutoff: pd.DataFrame = self._cutoff_df(df_processed) 66 67 array_x = np.array(df_cutoff.index) 68 array_y = np.array(df_cutoff["target"] / df_cutoff["reference"]) 69 70 # フィッティングパラメータと共分散行列を取得 71 popt, pcov = curve_fit( 72 TransferFunctionCalculator.transfer_function, array_x, array_y 73 ) 74 75 # 標準誤差を計算(共分散行列の対角成分の平方根) 76 perr = np.sqrt(np.diag(pcov)) 77 78 # 係数aとその標準誤差、および計算に用いたDataFrameを返す 79 return popt[0], perr[0], df_processed 80 81 def create_plot_co_spectra( 82 self, 83 col1: str, 84 col2: str, 85 color1: str = "gray", 86 color2: str = "red", 87 figsize: tuple[int, int] = (10, 8), 88 label1: str | None = None, 89 label2: str | None = None, 90 output_dir: str | Path | None = None, 91 output_basename: str = "co", 92 add_legend: bool = True, 93 add_xy_labels: bool = True, 94 show_fig: bool = True, 95 subplot_label: str | None = "(a)", 96 window_size: int = 5, # 移動平均の窓サイズ 97 markersize: float = 14, 98 ) -> None: 99 """ 100 2種類のコスペクトルをプロットする。 101 102 Parameters 103 ---------- 104 col1 : str 105 1つ目のコスペクトルデータのカラム名。 106 col2 : str 107 2つ目のコスペクトルデータのカラム名。 108 color1 : str, optional 109 1つ目のデータの色。デフォルトは'gray'。 110 color2 : str, optional 111 2つ目のデータの色。デフォルトは'red'。 112 figsize : tuple[int, int], optional 113 プロットのサイズ。デフォルトは(10, 8)。 114 label1 : str, optional 115 1つ目のデータのラベル名。デフォルトはNone。 116 label2 : str, optional 117 2つ目のデータのラベル名。デフォルトはNone。 118 output_dir : str | Path | None, optional 119 プロットを保存するディレクトリ。デフォルトはNoneで、保存しない。 120 output_basename : str, optional 121 保存するファイル名のベース。デフォルトは"co"。 122 show_fig : bool, optional 123 プロットを表示するかどうか。デフォルトはTrue。 124 subplot_label : str | None, optional 125 左上に表示するサブプロットラベル。デフォルトは"(a)"。 126 window_size : int, optional 127 移動平均の窓サイズ。デフォルトは5。 128 """ 129 df_copied: pd.DataFrame = self._df.copy() 130 # データの取得と移動平均の適用 131 data1 = df_copied[df_copied[col1] > 0].groupby(self._col_freq)[col1].median() 132 data2 = df_copied[df_copied[col2] > 0].groupby(self._col_freq)[col2].median() 133 134 data1 = data1.rolling(window=window_size, center=True, min_periods=1).mean() 135 data2 = data2.rolling(window=window_size, center=True, min_periods=1).mean() 136 137 fig = plt.figure(figsize=figsize) 138 ax = fig.add_subplot(111) 139 140 # マーカーサイズを設定して見やすくする 141 ax.plot( 142 data1.index, data1, "o", color=color1, label=label1, markersize=markersize 143 ) 144 ax.plot( 145 data2.index, data2, "o", color=color2, label=label2, markersize=markersize 146 ) 147 ax.plot([0.01, 10], [10, 0.001], "-", color="black") 148 ax.text(0.25, 0.4, "-4/3") 149 150 ax.grid(True, alpha=0.3) 151 ax.set_xscale("log") 152 ax.set_yscale("log") 153 ax.set_xlim(0.0001, 10) 154 ax.set_ylim(0.0001, 10) 155 if add_xy_labels: 156 ax.set_xlabel("f (Hz)") 157 ax.set_ylabel("無次元コスペクトル") 158 159 if add_legend: 160 ax.legend( 161 bbox_to_anchor=(0.05, 1), 162 loc="lower left", 163 fontsize=16, 164 ncol=3, 165 frameon=False, 166 ) 167 if subplot_label is not None: 168 ax.text(0.00015, 3, subplot_label) 169 fig.tight_layout() 170 171 if output_dir is not None: 172 os.makedirs(output_dir, exist_ok=True) 173 # プロットをPNG形式で保存 174 filename: str = f"{output_basename}.png" 175 fig.savefig(os.path.join(output_dir, filename), dpi=300) 176 if show_fig: 177 plt.show() 178 else: 179 plt.close(fig=fig) 180 181 def create_plot_ratio( 182 self, 183 df_processed: pd.DataFrame, 184 reference_name: str, 185 target_name: str, 186 figsize: tuple[int, int] = (10, 6), 187 output_dir: str | Path | None = None, 188 output_basename: str = "ratio", 189 show_fig: bool = True, 190 ) -> None: 191 """ 192 ターゲットと参照の比率をプロットする。 193 194 Parameters 195 ---------- 196 df_processed : pd.DataFrame 197 処理されたデータフレーム。 198 reference_name : str 199 参照の名前。 200 target_name : str 201 ターゲットの名前。 202 figsize : tuple[int, int], optional 203 プロットのサイズ。デフォルトは(10, 6)。 204 output_dir : str | Path | None, optional 205 プロットを保存するディレクトリ。デフォルトはNoneで、保存しない。 206 output_basename : str, optional 207 保存するファイル名のベース。デフォルトは"ratio"。 208 show_fig : bool, optional 209 プロットを表示するかどうか。デフォルトはTrue。 210 """ 211 fig = plt.figure(figsize=figsize) 212 ax = fig.add_subplot(111) 213 214 ax.plot( 215 df_processed.index, df_processed["target"] / df_processed["reference"], "o" 216 ) 217 ax.set_xscale("log") 218 ax.set_yscale("log") 219 ax.set_xlabel("f (Hz)") 220 ax.set_ylabel(f"{target_name} / {reference_name}") 221 ax.set_title(f"{target_name}と{reference_name}の比") 222 223 if output_dir is not None: 224 # プロットをPNG形式で保存 225 filename: str = f"{output_basename}-{reference_name}_{target_name}.png" 226 fig.savefig(os.path.join(output_dir, filename), dpi=300) 227 if show_fig: 228 plt.show() 229 else: 230 plt.close(fig=fig) 231 232 @classmethod 233 def create_plot_tf_curves_from_csv( 234 cls, 235 file_path: str, 236 gas_configs: list[tuple[str, str, str, str]], 237 output_dir: str | Path | None = None, 238 output_basename: str = "all_tf_curves", 239 col_datetime: str = "Date", 240 add_xlabel: bool = True, 241 label_x: str = "f (Hz)", 242 label_y: str = "無次元コスペクトル比", 243 label_avg: str = "Avg.", 244 label_co_ref: str = "Tv", 245 line_colors: list[str] | None = None, 246 font_family: list[str] = ["Arial", "MS Gothic"], 247 font_size: float = 20, 248 save_fig: bool = True, 249 show_fig: bool = True, 250 ) -> None: 251 """ 252 複数の伝達関数の係数をプロットし、各ガスの平均値を表示します。 253 各ガスのデータをCSVファイルから読み込み、指定された設定に基づいてプロットを生成します。 254 プロットはオプションで保存することも可能です。 255 256 Parameters 257 ---------- 258 file_path : str 259 伝達関数の係数が格納されたCSVファイルのパス。 260 gas_configs : list[tuple[str, str, str, str]] 261 ガスごとの設定のリスト。各タプルは以下の要素を含む: 262 (係数のカラム名, ガスの表示ラベル, 平均線の色, 出力ファイル用のガス名) 263 例: [("a_ch4-used", "CH$_4$", "red", "ch4")] 264 output_dir : str | Path | None, optional 265 出力ディレクトリ。Noneの場合は保存しない。 266 output_basename : str, optional 267 出力ファイル名のベース。デフォルトは"all_tf_curves"。 268 col_datetime : str, optional 269 日付情報が格納されているカラム名。デフォルトは"Date"。 270 add_xlabel : bool, optional 271 x軸ラベルを追加するかどうか。デフォルトはTrue。 272 label_x : str, optional 273 x軸のラベル。デフォルトは"f (Hz)"。 274 label_y : str, optional 275 y軸のラベル。デフォルトは"無次元コスペクトル比"。 276 label_avg : str, optional 277 平均値のラベル。デフォルトは"Avg."。 278 line_colors : list[str] | None, optional 279 各日付のデータに使用する色のリスト。 280 font_family : list[str], optional 281 使用するフォントファミリーのリスト。 282 font_size : float, optional 283 フォントサイズ。 284 save_fig : bool, optional 285 プロットを保存するかどうか。デフォルトはTrue。 286 show_fig : bool, optional 287 プロットを表示するかどうか。デフォルトはTrue。 288 """ 289 # プロットパラメータの設定 290 plt.rcParams.update( 291 { 292 "font.family": font_family, 293 "font.size": font_size, 294 "axes.labelsize": font_size, 295 "axes.titlesize": font_size, 296 "xtick.labelsize": font_size, 297 "ytick.labelsize": font_size, 298 "legend.fontsize": font_size, 299 } 300 ) 301 302 # CSVファイルを読み込む 303 df = pd.read_csv(file_path) 304 305 # 各ガスについてプロット 306 for col_coef_a, label_gas, base_color, gas_name in gas_configs: 307 fig = plt.figure(figsize=(10, 6)) 308 309 # データ数に応じたデフォルトの色リストを作成 310 if line_colors is None: 311 default_colors = [ 312 "#1f77b4", 313 "#ff7f0e", 314 "#2ca02c", 315 "#d62728", 316 "#9467bd", 317 "#8c564b", 318 "#e377c2", 319 "#7f7f7f", 320 "#bcbd22", 321 "#17becf", 322 ] 323 n_dates = len(df) 324 plot_colors = (default_colors * (n_dates // len(default_colors) + 1))[ 325 :n_dates 326 ] 327 else: 328 plot_colors = line_colors 329 330 # 全てのa値を用いて伝達関数をプロット 331 for i, row in enumerate(df.iterrows()): 332 a = row[1][col_coef_a] 333 date = row[1][col_datetime] 334 x_fit = np.logspace(-3, 1, 1000) 335 y_fit = cls.transfer_function(x_fit, a) 336 plt.plot( 337 x_fit, 338 y_fit, 339 "-", 340 color=plot_colors[i], 341 alpha=0.7, 342 label=f"{date} (a = {a:.3f})", 343 ) 344 345 # 平均のa値を用いた伝達関数をプロット 346 a_mean = df[col_coef_a].mean() 347 x_fit = np.logspace(-3, 1, 1000) 348 y_fit = cls.transfer_function(x_fit, a_mean) 349 plt.plot( 350 x_fit, 351 y_fit, 352 "-", 353 color=base_color, 354 linewidth=3, 355 label=f"{label_avg} (a = {a_mean:.3f})", 356 ) 357 358 # グラフの設定 359 label_y_formatted: str = f"{label_y}\n({label_gas} / {label_co_ref})" 360 plt.xscale("log") 361 if add_xlabel: 362 plt.xlabel(label_x) 363 plt.ylabel(label_y_formatted) 364 plt.legend(loc="lower left", fontsize=font_size - 6) 365 plt.grid(True, which="both", ls="-", alpha=0.2) 366 plt.tight_layout() 367 368 if save_fig: 369 if output_dir is None: 370 raise ValueError( 371 "save_fig=Trueのとき、output_dirに有効なディレクトリパスを指定する必要があります。" 372 ) 373 os.makedirs(output_dir, exist_ok=True) 374 output_path: str = os.path.join( 375 output_dir, f"{output_basename}-{gas_name}.png" 376 ) 377 plt.savefig(output_path, dpi=300, bbox_inches="tight") 378 if show_fig: 379 plt.show() 380 else: 381 plt.close(fig=fig) 382 383 def create_plot_transfer_function( 384 self, 385 a: float, 386 df_processed: pd.DataFrame, 387 reference_name: str, 388 target_name: str, 389 figsize: tuple[int, int] = (10, 6), 390 output_dir: str | Path | None = None, 391 output_basename: str = "tf", 392 show_fig: bool = True, 393 add_xlabel: bool = True, 394 label_x: str = "f (Hz)", 395 label_y: str = "コスペクトル比", 396 label_gas: str | None = None, 397 ) -> None: 398 """ 399 伝達関数とそのフィットをプロットする。 400 401 Parameters 402 ---------- 403 a : float 404 伝達関数の係数。 405 df_processed : pd.DataFrame 406 処理されたデータフレーム。 407 reference_name : str 408 参照の名前。 409 target_name : str 410 ターゲットの名前。 411 figsize : tuple[int, int], optional 412 プロットのサイズ。デフォルトは(10, 6)。 413 output_dir : str | Path | None, optional 414 プロットを保存するディレクトリ。デフォルトはNoneで、保存しない。 415 output_basename : str, optional 416 保存するファイル名のベース。デフォルトは"tf"。 417 show_fig : bool, optional 418 プロットを表示するかどうか。デフォルトはTrue。 419 """ 420 df_cutoff: pd.DataFrame = self._cutoff_df(df_processed) 421 422 fig = plt.figure(figsize=figsize) 423 ax = fig.add_subplot(111) 424 425 ax.plot( 426 df_cutoff.index, 427 df_cutoff["target"] / df_cutoff["reference"], 428 "o", 429 label=f"{target_name} / {reference_name}", 430 ) 431 432 x_fit = np.logspace( 433 np.log10(self._cutoff_freq_low), np.log10(self._cutoff_freq_high), 1000 434 ) 435 y_fit = self.transfer_function(x_fit, a) 436 ax.plot(x_fit, y_fit, "-", label=f"フィット (a = {a:.4f})") 437 438 ax.set_xscale("log") 439 # グラフの設定 440 label_y_formatted: str = f"{label_y}\n({label_gas} / 顕熱)" 441 plt.xscale("log") 442 if add_xlabel: 443 plt.xlabel(label_x) 444 plt.ylabel(label_y_formatted) 445 ax.legend() 446 447 if output_dir is not None: 448 # プロットをPNG形式で保存 449 filename: str = f"{output_basename}-{reference_name}_{target_name}.png" 450 fig.savefig(os.path.join(output_dir, filename), dpi=300) 451 if show_fig: 452 plt.show() 453 else: 454 plt.close(fig=fig) 455 456 def process_data(self, col_reference: str, col_target: str) -> pd.DataFrame: 457 """ 458 指定されたキーに基づいてデータを処理する。 459 460 Parameters 461 ---------- 462 col_reference : str 463 参照データのカラム名。 464 col_target : str 465 ターゲットデータのカラム名。 466 467 Returns 468 ---------- 469 pd.DataFrame 470 処理されたデータフレーム。 471 """ 472 df_copied: pd.DataFrame = self._df.copy() 473 col_freq: str = self._col_freq 474 475 # データ型の確認と変換 476 df_copied[col_freq] = pd.to_numeric(df_copied[col_freq], errors="coerce") 477 df_copied[col_reference] = pd.to_numeric(df_copied[col_reference], errors="coerce") 478 df_copied[col_target] = pd.to_numeric(df_copied[col_target], errors="coerce") 479 480 # NaNを含む行を削除 481 df_copied = df_copied.dropna(subset=[col_freq, col_reference, col_target]) 482 483 # グループ化と中央値の計算 484 grouped = df_copied.groupby(col_freq) 485 reference_data = grouped[col_reference].median() 486 target_data = grouped[col_target].median() 487 488 df_processed = pd.DataFrame( 489 {"reference": reference_data, "target": target_data} 490 ) 491 492 # 異常な比率を除去 493 df_processed.loc[ 494 ( 495 (df_processed["target"] / df_processed["reference"] > 1) 496 | (df_processed["target"] / df_processed["reference"] < 0) 497 ) 498 ] = np.nan 499 df_processed = df_processed.dropna() 500 501 return df_processed 502 503 def _cutoff_df(self, df: pd.DataFrame) -> pd.DataFrame: 504 """ 505 カットオフ周波数に基づいてDataFrameを加工するメソッド 506 507 Parameters 508 ---------- 509 df : pd.DataFrame 510 加工対象のデータフレーム。 511 512 Returns 513 ---------- 514 pd.DataFrame 515 カットオフ周波数に基づいて加工されたデータフレーム。 516 """ 517 df_cutoff: pd.DataFrame = df.loc[ 518 (self._cutoff_freq_low <= df.index) & (df.index <= self._cutoff_freq_high) 519 ] 520 return df_cutoff 521 522 @classmethod 523 def transfer_function(cls, x: np.ndarray, a: float) -> np.ndarray: 524 """ 525 伝達関数を計算する。 526 527 Parameters 528 ---------- 529 x : np.ndarray 530 周波数の配列。 531 a : float 532 伝達関数の係数。 533 534 Returns 535 ---------- 536 np.ndarray 537 伝達関数の値。 538 """ 539 return np.exp(-np.log(np.sqrt(2)) * np.power(x / a, 2)) 540 541 @staticmethod 542 def _load_data(file_path: str) -> pd.DataFrame: 543 """ 544 CSVファイルからデータを読み込む。 545 546 Parameters 547 ---------- 548 file_path : str 549 csvファイルのパス。 550 551 Returns 552 ---------- 553 pd.DataFrame 554 読み込まれたデータフレーム。 555 """ 556 tmp = pd.read_csv(file_path, header=None, nrows=1, skiprows=0) 557 header = tmp.loc[tmp.index[0]] 558 df = pd.read_csv(file_path, header=None, skiprows=1) 559 df.columns = header 560 return df
このクラスは、CSVファイルからデータを読み込み、処理し、 伝達関数を計算してプロットするための機能を提供します。
この実装は Moore (1986) の論文に基づいています。
18 def __init__( 19 self, 20 file_path: str, 21 col_freq: str, 22 cutoff_freq_low: float = 0.01, 23 cutoff_freq_high: float = 1, 24 ): 25 """ 26 TransferFunctionCalculatorクラスのコンストラクタ。 27 28 Parameters 29 ---------- 30 file_path : str 31 分析対象のCSVファイルのパス。 32 col_freq : str 33 周波数のキー。 34 cutoff_freq_low : float 35 カットオフ周波数の最低値。 36 cutoff_freq_high : float 37 カットオフ周波数の最高値。 38 """ 39 self._col_freq: str = col_freq 40 self._cutoff_freq_low: float = cutoff_freq_low 41 self._cutoff_freq_high: float = cutoff_freq_high 42 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
カットオフ周波数の最高値。
44 def calculate_transfer_function( 45 self, col_reference: str, col_target: str 46 ) -> tuple[float, float, pd.DataFrame]: 47 """ 48 伝達関数の係数を計算する。 49 50 Parameters 51 ---------- 52 col_reference : str 53 参照データのカラム名。 54 col_target : str 55 ターゲットデータのカラム名。 56 57 Returns 58 ---------- 59 tuple[float, float, pandas.DataFrame] 60 伝達関数の係数aとその標準誤差、および計算に用いたDataFrame。 61 """ 62 df_processed: pd.DataFrame = self.process_data( 63 col_reference=col_reference, col_target=col_target 64 ) 65 df_cutoff: pd.DataFrame = self._cutoff_df(df_processed) 66 67 array_x = np.array(df_cutoff.index) 68 array_y = np.array(df_cutoff["target"] / df_cutoff["reference"]) 69 70 # フィッティングパラメータと共分散行列を取得 71 popt, pcov = curve_fit( 72 TransferFunctionCalculator.transfer_function, array_x, array_y 73 ) 74 75 # 標準誤差を計算(共分散行列の対角成分の平方根) 76 perr = np.sqrt(np.diag(pcov)) 77 78 # 係数aとその標準誤差、および計算に用いたDataFrameを返す 79 return popt[0], perr[0], df_processed
伝達関数の係数を計算する。
Parameters
col_reference : str
参照データのカラム名。
col_target : str
ターゲットデータのカラム名。
Returns
tuple[float, float, pandas.DataFrame]
伝達関数の係数aとその標準誤差、および計算に用いたDataFrame。
81 def create_plot_co_spectra( 82 self, 83 col1: str, 84 col2: str, 85 color1: str = "gray", 86 color2: str = "red", 87 figsize: tuple[int, int] = (10, 8), 88 label1: str | None = None, 89 label2: str | None = None, 90 output_dir: str | Path | None = None, 91 output_basename: str = "co", 92 add_legend: bool = True, 93 add_xy_labels: bool = True, 94 show_fig: bool = True, 95 subplot_label: str | None = "(a)", 96 window_size: int = 5, # 移動平均の窓サイズ 97 markersize: float = 14, 98 ) -> None: 99 """ 100 2種類のコスペクトルをプロットする。 101 102 Parameters 103 ---------- 104 col1 : str 105 1つ目のコスペクトルデータのカラム名。 106 col2 : str 107 2つ目のコスペクトルデータのカラム名。 108 color1 : str, optional 109 1つ目のデータの色。デフォルトは'gray'。 110 color2 : str, optional 111 2つ目のデータの色。デフォルトは'red'。 112 figsize : tuple[int, int], optional 113 プロットのサイズ。デフォルトは(10, 8)。 114 label1 : str, optional 115 1つ目のデータのラベル名。デフォルトはNone。 116 label2 : str, optional 117 2つ目のデータのラベル名。デフォルトはNone。 118 output_dir : str | Path | None, optional 119 プロットを保存するディレクトリ。デフォルトはNoneで、保存しない。 120 output_basename : str, optional 121 保存するファイル名のベース。デフォルトは"co"。 122 show_fig : bool, optional 123 プロットを表示するかどうか。デフォルトはTrue。 124 subplot_label : str | None, optional 125 左上に表示するサブプロットラベル。デフォルトは"(a)"。 126 window_size : int, optional 127 移動平均の窓サイズ。デフォルトは5。 128 """ 129 df_copied: pd.DataFrame = self._df.copy() 130 # データの取得と移動平均の適用 131 data1 = df_copied[df_copied[col1] > 0].groupby(self._col_freq)[col1].median() 132 data2 = df_copied[df_copied[col2] > 0].groupby(self._col_freq)[col2].median() 133 134 data1 = data1.rolling(window=window_size, center=True, min_periods=1).mean() 135 data2 = data2.rolling(window=window_size, center=True, min_periods=1).mean() 136 137 fig = plt.figure(figsize=figsize) 138 ax = fig.add_subplot(111) 139 140 # マーカーサイズを設定して見やすくする 141 ax.plot( 142 data1.index, data1, "o", color=color1, label=label1, markersize=markersize 143 ) 144 ax.plot( 145 data2.index, data2, "o", color=color2, label=label2, markersize=markersize 146 ) 147 ax.plot([0.01, 10], [10, 0.001], "-", color="black") 148 ax.text(0.25, 0.4, "-4/3") 149 150 ax.grid(True, alpha=0.3) 151 ax.set_xscale("log") 152 ax.set_yscale("log") 153 ax.set_xlim(0.0001, 10) 154 ax.set_ylim(0.0001, 10) 155 if add_xy_labels: 156 ax.set_xlabel("f (Hz)") 157 ax.set_ylabel("無次元コスペクトル") 158 159 if add_legend: 160 ax.legend( 161 bbox_to_anchor=(0.05, 1), 162 loc="lower left", 163 fontsize=16, 164 ncol=3, 165 frameon=False, 166 ) 167 if subplot_label is not None: 168 ax.text(0.00015, 3, subplot_label) 169 fig.tight_layout() 170 171 if output_dir is not None: 172 os.makedirs(output_dir, exist_ok=True) 173 # プロットをPNG形式で保存 174 filename: str = f"{output_basename}.png" 175 fig.savefig(os.path.join(output_dir, filename), dpi=300) 176 if show_fig: 177 plt.show() 178 else: 179 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 | Path | None, optional
プロットを保存するディレクトリ。デフォルトはNoneで、保存しない。
output_basename : str, optional
保存するファイル名のベース。デフォルトは"co"。
show_fig : bool, optional
プロットを表示するかどうか。デフォルトはTrue。
subplot_label : str | None, optional
左上に表示するサブプロットラベル。デフォルトは"(a)"。
window_size : int, optional
移動平均の窓サイズ。デフォルトは5。
181 def create_plot_ratio( 182 self, 183 df_processed: pd.DataFrame, 184 reference_name: str, 185 target_name: str, 186 figsize: tuple[int, int] = (10, 6), 187 output_dir: str | Path | None = None, 188 output_basename: str = "ratio", 189 show_fig: bool = True, 190 ) -> None: 191 """ 192 ターゲットと参照の比率をプロットする。 193 194 Parameters 195 ---------- 196 df_processed : pd.DataFrame 197 処理されたデータフレーム。 198 reference_name : str 199 参照の名前。 200 target_name : str 201 ターゲットの名前。 202 figsize : tuple[int, int], optional 203 プロットのサイズ。デフォルトは(10, 6)。 204 output_dir : str | Path | None, optional 205 プロットを保存するディレクトリ。デフォルトはNoneで、保存しない。 206 output_basename : str, optional 207 保存するファイル名のベース。デフォルトは"ratio"。 208 show_fig : bool, optional 209 プロットを表示するかどうか。デフォルトはTrue。 210 """ 211 fig = plt.figure(figsize=figsize) 212 ax = fig.add_subplot(111) 213 214 ax.plot( 215 df_processed.index, df_processed["target"] / df_processed["reference"], "o" 216 ) 217 ax.set_xscale("log") 218 ax.set_yscale("log") 219 ax.set_xlabel("f (Hz)") 220 ax.set_ylabel(f"{target_name} / {reference_name}") 221 ax.set_title(f"{target_name}と{reference_name}の比") 222 223 if output_dir is not None: 224 # プロットをPNG形式で保存 225 filename: str = f"{output_basename}-{reference_name}_{target_name}.png" 226 fig.savefig(os.path.join(output_dir, filename), dpi=300) 227 if show_fig: 228 plt.show() 229 else: 230 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 | Path | None, optional
プロットを保存するディレクトリ。デフォルトはNoneで、保存しない。
output_basename : str, optional
保存するファイル名のベース。デフォルトは"ratio"。
show_fig : bool, optional
プロットを表示するかどうか。デフォルトはTrue。
232 @classmethod 233 def create_plot_tf_curves_from_csv( 234 cls, 235 file_path: str, 236 gas_configs: list[tuple[str, str, str, str]], 237 output_dir: str | Path | None = None, 238 output_basename: str = "all_tf_curves", 239 col_datetime: str = "Date", 240 add_xlabel: bool = True, 241 label_x: str = "f (Hz)", 242 label_y: str = "無次元コスペクトル比", 243 label_avg: str = "Avg.", 244 label_co_ref: str = "Tv", 245 line_colors: list[str] | None = None, 246 font_family: list[str] = ["Arial", "MS Gothic"], 247 font_size: float = 20, 248 save_fig: bool = True, 249 show_fig: bool = True, 250 ) -> None: 251 """ 252 複数の伝達関数の係数をプロットし、各ガスの平均値を表示します。 253 各ガスのデータをCSVファイルから読み込み、指定された設定に基づいてプロットを生成します。 254 プロットはオプションで保存することも可能です。 255 256 Parameters 257 ---------- 258 file_path : str 259 伝達関数の係数が格納されたCSVファイルのパス。 260 gas_configs : list[tuple[str, str, str, str]] 261 ガスごとの設定のリスト。各タプルは以下の要素を含む: 262 (係数のカラム名, ガスの表示ラベル, 平均線の色, 出力ファイル用のガス名) 263 例: [("a_ch4-used", "CH$_4$", "red", "ch4")] 264 output_dir : str | Path | None, optional 265 出力ディレクトリ。Noneの場合は保存しない。 266 output_basename : str, optional 267 出力ファイル名のベース。デフォルトは"all_tf_curves"。 268 col_datetime : str, optional 269 日付情報が格納されているカラム名。デフォルトは"Date"。 270 add_xlabel : bool, optional 271 x軸ラベルを追加するかどうか。デフォルトはTrue。 272 label_x : str, optional 273 x軸のラベル。デフォルトは"f (Hz)"。 274 label_y : str, optional 275 y軸のラベル。デフォルトは"無次元コスペクトル比"。 276 label_avg : str, optional 277 平均値のラベル。デフォルトは"Avg."。 278 line_colors : list[str] | None, optional 279 各日付のデータに使用する色のリスト。 280 font_family : list[str], optional 281 使用するフォントファミリーのリスト。 282 font_size : float, optional 283 フォントサイズ。 284 save_fig : bool, optional 285 プロットを保存するかどうか。デフォルトはTrue。 286 show_fig : bool, optional 287 プロットを表示するかどうか。デフォルトはTrue。 288 """ 289 # プロットパラメータの設定 290 plt.rcParams.update( 291 { 292 "font.family": font_family, 293 "font.size": font_size, 294 "axes.labelsize": font_size, 295 "axes.titlesize": font_size, 296 "xtick.labelsize": font_size, 297 "ytick.labelsize": font_size, 298 "legend.fontsize": font_size, 299 } 300 ) 301 302 # CSVファイルを読み込む 303 df = pd.read_csv(file_path) 304 305 # 各ガスについてプロット 306 for col_coef_a, label_gas, base_color, gas_name in gas_configs: 307 fig = plt.figure(figsize=(10, 6)) 308 309 # データ数に応じたデフォルトの色リストを作成 310 if line_colors is None: 311 default_colors = [ 312 "#1f77b4", 313 "#ff7f0e", 314 "#2ca02c", 315 "#d62728", 316 "#9467bd", 317 "#8c564b", 318 "#e377c2", 319 "#7f7f7f", 320 "#bcbd22", 321 "#17becf", 322 ] 323 n_dates = len(df) 324 plot_colors = (default_colors * (n_dates // len(default_colors) + 1))[ 325 :n_dates 326 ] 327 else: 328 plot_colors = line_colors 329 330 # 全てのa値を用いて伝達関数をプロット 331 for i, row in enumerate(df.iterrows()): 332 a = row[1][col_coef_a] 333 date = row[1][col_datetime] 334 x_fit = np.logspace(-3, 1, 1000) 335 y_fit = cls.transfer_function(x_fit, a) 336 plt.plot( 337 x_fit, 338 y_fit, 339 "-", 340 color=plot_colors[i], 341 alpha=0.7, 342 label=f"{date} (a = {a:.3f})", 343 ) 344 345 # 平均のa値を用いた伝達関数をプロット 346 a_mean = df[col_coef_a].mean() 347 x_fit = np.logspace(-3, 1, 1000) 348 y_fit = cls.transfer_function(x_fit, a_mean) 349 plt.plot( 350 x_fit, 351 y_fit, 352 "-", 353 color=base_color, 354 linewidth=3, 355 label=f"{label_avg} (a = {a_mean:.3f})", 356 ) 357 358 # グラフの設定 359 label_y_formatted: str = f"{label_y}\n({label_gas} / {label_co_ref})" 360 plt.xscale("log") 361 if add_xlabel: 362 plt.xlabel(label_x) 363 plt.ylabel(label_y_formatted) 364 plt.legend(loc="lower left", fontsize=font_size - 6) 365 plt.grid(True, which="both", ls="-", alpha=0.2) 366 plt.tight_layout() 367 368 if save_fig: 369 if output_dir is None: 370 raise ValueError( 371 "save_fig=Trueのとき、output_dirに有効なディレクトリパスを指定する必要があります。" 372 ) 373 os.makedirs(output_dir, exist_ok=True) 374 output_path: str = os.path.join( 375 output_dir, f"{output_basename}-{gas_name}.png" 376 ) 377 plt.savefig(output_path, dpi=300, bbox_inches="tight") 378 if show_fig: 379 plt.show() 380 else: 381 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 | Path | 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。
383 def create_plot_transfer_function( 384 self, 385 a: float, 386 df_processed: pd.DataFrame, 387 reference_name: str, 388 target_name: str, 389 figsize: tuple[int, int] = (10, 6), 390 output_dir: str | Path | None = None, 391 output_basename: str = "tf", 392 show_fig: bool = True, 393 add_xlabel: bool = True, 394 label_x: str = "f (Hz)", 395 label_y: str = "コスペクトル比", 396 label_gas: str | None = None, 397 ) -> None: 398 """ 399 伝達関数とそのフィットをプロットする。 400 401 Parameters 402 ---------- 403 a : float 404 伝達関数の係数。 405 df_processed : pd.DataFrame 406 処理されたデータフレーム。 407 reference_name : str 408 参照の名前。 409 target_name : str 410 ターゲットの名前。 411 figsize : tuple[int, int], optional 412 プロットのサイズ。デフォルトは(10, 6)。 413 output_dir : str | Path | None, optional 414 プロットを保存するディレクトリ。デフォルトはNoneで、保存しない。 415 output_basename : str, optional 416 保存するファイル名のベース。デフォルトは"tf"。 417 show_fig : bool, optional 418 プロットを表示するかどうか。デフォルトはTrue。 419 """ 420 df_cutoff: pd.DataFrame = self._cutoff_df(df_processed) 421 422 fig = plt.figure(figsize=figsize) 423 ax = fig.add_subplot(111) 424 425 ax.plot( 426 df_cutoff.index, 427 df_cutoff["target"] / df_cutoff["reference"], 428 "o", 429 label=f"{target_name} / {reference_name}", 430 ) 431 432 x_fit = np.logspace( 433 np.log10(self._cutoff_freq_low), np.log10(self._cutoff_freq_high), 1000 434 ) 435 y_fit = self.transfer_function(x_fit, a) 436 ax.plot(x_fit, y_fit, "-", label=f"フィット (a = {a:.4f})") 437 438 ax.set_xscale("log") 439 # グラフの設定 440 label_y_formatted: str = f"{label_y}\n({label_gas} / 顕熱)" 441 plt.xscale("log") 442 if add_xlabel: 443 plt.xlabel(label_x) 444 plt.ylabel(label_y_formatted) 445 ax.legend() 446 447 if output_dir is not None: 448 # プロットをPNG形式で保存 449 filename: str = f"{output_basename}-{reference_name}_{target_name}.png" 450 fig.savefig(os.path.join(output_dir, filename), dpi=300) 451 if show_fig: 452 plt.show() 453 else: 454 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 | Path | None, optional
プロットを保存するディレクトリ。デフォルトはNoneで、保存しない。
output_basename : str, optional
保存するファイル名のベース。デフォルトは"tf"。
show_fig : bool, optional
プロットを表示するかどうか。デフォルトはTrue。
456 def process_data(self, col_reference: str, col_target: str) -> pd.DataFrame: 457 """ 458 指定されたキーに基づいてデータを処理する。 459 460 Parameters 461 ---------- 462 col_reference : str 463 参照データのカラム名。 464 col_target : str 465 ターゲットデータのカラム名。 466 467 Returns 468 ---------- 469 pd.DataFrame 470 処理されたデータフレーム。 471 """ 472 df_copied: pd.DataFrame = self._df.copy() 473 col_freq: str = self._col_freq 474 475 # データ型の確認と変換 476 df_copied[col_freq] = pd.to_numeric(df_copied[col_freq], errors="coerce") 477 df_copied[col_reference] = pd.to_numeric(df_copied[col_reference], errors="coerce") 478 df_copied[col_target] = pd.to_numeric(df_copied[col_target], errors="coerce") 479 480 # NaNを含む行を削除 481 df_copied = df_copied.dropna(subset=[col_freq, col_reference, col_target]) 482 483 # グループ化と中央値の計算 484 grouped = df_copied.groupby(col_freq) 485 reference_data = grouped[col_reference].median() 486 target_data = grouped[col_target].median() 487 488 df_processed = pd.DataFrame( 489 {"reference": reference_data, "target": target_data} 490 ) 491 492 # 異常な比率を除去 493 df_processed.loc[ 494 ( 495 (df_processed["target"] / df_processed["reference"] > 1) 496 | (df_processed["target"] / df_processed["reference"] < 0) 497 ) 498 ] = np.nan 499 df_processed = df_processed.dropna() 500 501 return df_processed
指定されたキーに基づいてデータを処理する。
Parameters
col_reference : str
参照データのカラム名。
col_target : str
ターゲットデータのカラム名。
Returns
pd.DataFrame
処理されたデータフレーム。
522 @classmethod 523 def transfer_function(cls, x: np.ndarray, a: float) -> np.ndarray: 524 """ 525 伝達関数を計算する。 526 527 Parameters 528 ---------- 529 x : np.ndarray 530 周波数の配列。 531 a : float 532 伝達関数の係数。 533 534 Returns 535 ---------- 536 np.ndarray 537 伝達関数の値。 538 """ 539 return np.exp(-np.log(np.sqrt(2)) * np.power(x / a, 2))
伝達関数を計算する。
Parameters
x : np.ndarray
周波数の配列。
a : float
伝達関数の係数。
Returns
np.ndarray
伝達関数の値。