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 CorrectingUtils, CORRECTION_TYPES_PATTERN 7from .mobile.mobile_spatial_analyzer import ( 8 EmissionData, 9 MobileSpatialAnalyzer, 10 MSAInputConfig, 11) 12from .monthly.monthly_converter import MonthlyConverter 13from .monthly.monthly_figures_generator import MonthlyFiguresGenerator 14from .transfer_function.fft_files_reorganizer import FftFileReorganizer 15from .transfer_function.transfer_function_calculator import TransferFunctionCalculator 16 17""" 18versionを動的に設定する。 19`./_version.py`がない場合はsetuptools_scmを用いてGitからバージョン取得を試行 20それも失敗した場合にデフォルトバージョン(0.0.0)を設定 21""" 22try: 23 from ._version import __version__ # type:ignore 24except ImportError: 25 try: 26 from setuptools_scm import get_version 27 28 __version__ = get_version(root="..", relative_to=__file__) 29 except Exception: 30 __version__ = "0.0.0" 31 32__version__ = __version__ 33""" 34@private 35このモジュールはバージョン情報の管理に使用され、ドキュメントには含めません。 36private属性を適用するために再宣言してdocstringを記述しています。 37""" 38 39# モジュールを __all__ にセット 40__all__ = [ 41 "__version__", 42 "EddyDataPreprocessor", 43 "SpectrumCalculator", 44 "FigureUtils", 45 "HotspotData", 46 "HotspotType", 47 "FluxFootprintAnalyzer", 48 "CorrectingUtils", 49 "CORRECTION_TYPES_PATTERN", 50 "EmissionData", 51 "MobileSpatialAnalyzer", 52 "MSAInputConfig", 53 "MonthlyConverter", 54 "MonthlyFiguresGenerator", 55 "FftFileReorganizer", 56 "TransferFunctionCalculator", 57]
13class EddyDataPreprocessor: 14 # カラム名を定数として定義 15 WIND_U = "edp_wind_u" 16 WIND_V = "edp_wind_v" 17 WIND_W = "edp_wind_w" 18 RAD_WIND_DIR = "edp_rad_wind_dir" 19 RAD_WIND_INC = "edp_rad_wind_inc" 20 DEGREE_WIND_DIR = "edp_degree_wind_dir" 21 DEGREE_WIND_INC = "edp_degree_wind_inc" 22 23 def __init__( 24 self, 25 fs: float = 10, 26 logger: Logger | None = None, 27 logging_debug: bool = False, 28 ): 29 """ 30 渦相関法によって記録されたデータファイルを処理するクラス。 31 32 Parameters 33 ---------- 34 fs (float): サンプリング周波数。 35 logger (Logger | None): 使用するロガー。Noneの場合は新しいロガーを作成します。 36 logging_debug (bool): ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。 37 """ 38 self.fs: float = fs 39 40 # ロガー 41 log_level: int = INFO 42 if logging_debug: 43 log_level = DEBUG 44 self.logger: Logger = EddyDataPreprocessor.setup_logger(logger, log_level) 45 46 def add_uvw_columns(self, df: pd.DataFrame) -> pd.DataFrame: 47 """ 48 DataFrameに水平風速u、v、鉛直風速wの列を追加する関数。 49 各成分のキーは`wind_u`、`wind_v`、`wind_w`である。 50 51 Parameters 52 ----- 53 df : pd.DataFrame 54 風速データを含むDataFrame 55 56 Returns 57 ----- 58 pd.DataFrame 59 水平風速u、v、鉛直風速wの列を追加したDataFrame 60 """ 61 required_columns: list[str] = ["Ux", "Uy", "Uz"] 62 # 必要な列がDataFrameに存在するか確認 63 for column in required_columns: 64 if column not in df.columns: 65 raise ValueError(f"必要な列 '{column}' がDataFrameに存在しません。") 66 67 processed_df: pd.DataFrame = df.copy() 68 # pandasの.valuesを使用してnumpy配列を取得し、その型をnp.ndarrayに明示的にキャストする 69 wind_x_array: np.ndarray = np.array(processed_df["Ux"].values) 70 wind_y_array: np.ndarray = np.array(processed_df["Uy"].values) 71 wind_z_array: np.ndarray = np.array(processed_df["Uz"].values) 72 73 # 平均風向を計算 74 wind_direction: float = EddyDataPreprocessor._wind_direction( 75 wind_x_array, wind_y_array 76 ) 77 78 # 水平方向に座標回転を行u, v成分を求める 79 wind_u_array, wind_v_array = EddyDataPreprocessor._horizontal_wind_speed( 80 wind_x_array, wind_y_array, wind_direction 81 ) 82 wind_w_array: np.ndarray = wind_z_array # wはz成分そのまま 83 84 # u, wから風の迎角を計算 85 wind_inclination: float = EddyDataPreprocessor._wind_inclination( 86 wind_u_array, wind_w_array 87 ) 88 89 # 2回座標回転を行い、u, wを求める 90 wind_u_array_rotated, wind_w_array_rotated = ( 91 EddyDataPreprocessor._vertical_rotation( 92 wind_u_array, wind_w_array, wind_inclination 93 ) 94 ) 95 96 processed_df[self.WIND_U] = wind_u_array_rotated 97 processed_df[self.WIND_V] = wind_v_array 98 processed_df[self.WIND_W] = wind_w_array_rotated 99 processed_df[self.RAD_WIND_DIR] = wind_direction 100 processed_df[self.RAD_WIND_INC] = wind_inclination 101 processed_df[self.DEGREE_WIND_DIR] = np.degrees(wind_direction) 102 processed_df[self.DEGREE_WIND_INC] = np.degrees(wind_inclination) 103 104 return processed_df 105 106 def analyze_lag_times( 107 self, 108 input_dir: str, 109 figsize: tuple[float, float] = (10, 8), 110 input_files_pattern: str = r"Eddy_(\d+)", 111 input_files_suffix: str = ".dat", 112 col1: str = "edp_wind_w", 113 col2_list: list[str] = ["Tv"], 114 median_range: float = 20, 115 metadata_rows: int = 4, 116 output_dir: str | None = None, 117 output_tag: str = "", 118 plot_range_tuple: tuple = (-50, 200), 119 print_results: bool = True, 120 skiprows: list[int] = [0, 2, 3], 121 use_resampling: bool = True, 122 ) -> dict[str, float]: 123 """ 124 遅れ時間(ラグ)の統計分析を行い、指定されたディレクトリ内のデータファイルを処理します。 125 解析結果とメタデータはCSVファイルとして出力されます。 126 127 Parameters: 128 ----- 129 input_dir : str 130 入力データファイルが格納されているディレクトリのパス。 131 figsize : tuple[float, float] 132 プロットのサイズ(幅、高さ)。 133 input_files_pattern : str 134 入力ファイル名のパターン(正規表現)。 135 input_files_suffix : str 136 入力ファイルの拡張子。 137 col1 : str 138 基準変数の列名。 139 col2_list : list[str] 140 比較変数の列名のリスト。 141 median_range : float 142 中央値を中心とした範囲。 143 metadata_rows : int 144 メタデータの行数。 145 output_dir : str | None 146 出力ディレクトリのパス。Noneの場合は保存しない。 147 output_tag : str 148 出力ファイルに付与するタグ。デフォルトは空文字で、何も付与されない。 149 plot_range_tuple : tuple 150 ヒストグラムの表示範囲。 151 print_results : bool 152 結果をコンソールに表示するかどうか。 153 skiprows : list[int] 154 スキップする行番号のリスト。 155 use_resampling : bool 156 データをリサンプリングするかどうか。 157 inputするファイルが既にリサンプリング済みの場合はFalseでよい。 158 デフォルトはTrue。 159 160 Returns: 161 ----- 162 dict[str, float] 163 各変数の遅れ時間(平均値を採用)を含む辞書。 164 """ 165 if output_dir is None: 166 self.logger.warn( 167 "output_dirが指定されていません。解析結果を保存する場合は、有効なディレクトリを指定してください。" 168 ) 169 all_lags_indices: list[list[int]] = [] 170 results: dict[str, float] = {} 171 172 # メイン処理 173 # ファイル名に含まれる数字に基づいてソート 174 csv_files = EddyDataPreprocessor._get_sorted_files( 175 input_dir, input_files_pattern, input_files_suffix 176 ) 177 if not csv_files: 178 raise FileNotFoundError( 179 f"There is no '{input_files_suffix}' file to process; input_dir: '{input_dir}', input_files_suffix: '{input_files_suffix}'" 180 ) 181 182 for file in tqdm(csv_files, desc="Calculating"): 183 path: str = os.path.join(input_dir, file) 184 if use_resampling: 185 df, _ = self.get_resampled_df( 186 filepath=path, metadata_rows=metadata_rows, skiprows=skiprows 187 ) 188 else: 189 df = pd.read_csv(path, skiprows=skiprows) 190 df = self.add_uvw_columns(df) 191 lags_list = EddyDataPreprocessor._calculate_lag_time( 192 df, 193 col1, 194 col2_list, 195 ) 196 all_lags_indices.append(lags_list) 197 self.logger.info("すべてのCSVファイルにおける遅れ時間が計算されました。") 198 199 # Convert all_lags_indices to a DataFrame 200 lags_indices_df: pd.DataFrame = pd.DataFrame( 201 all_lags_indices, columns=col2_list 202 ) 203 204 # フォーマット用のキーの最大の長さ 205 max_col_name_length: int = max( 206 len(column) for column in lags_indices_df.columns 207 ) 208 209 if print_results: 210 self.logger.info(f"カラム`{col1}`に対する遅れ時間を表示します。") 211 212 # 結果を格納するためのリスト 213 output_data = [] 214 215 for column in lags_indices_df.columns: 216 data: pd.Series = lags_indices_df[column] 217 218 # ヒストグラムの作成 219 plt.figure(figsize=figsize) 220 plt.hist(data, bins=20, range=plot_range_tuple) 221 plt.title(f"Delays of {column}") 222 plt.xlabel("Seconds") 223 plt.ylabel("Frequency") 224 plt.xlim(plot_range_tuple) 225 226 # ファイルとして保存するか 227 if output_dir is not None: 228 os.makedirs(output_dir, exist_ok=True) 229 filename: str = f"lags_histogram-{column}{output_tag}.png" 230 filepath: str = os.path.join(output_dir, filename) 231 plt.savefig(filepath, dpi=300, bbox_inches="tight") 232 plt.close() 233 234 # 中央値を計算し、その周辺のデータのみを使用 235 median_value = np.median(data) 236 filtered_data: pd.Series = data[ 237 (data >= median_value - median_range) 238 & (data <= median_value + median_range) 239 ] 240 241 # 平均値を計算 242 mean_value = np.mean(filtered_data) 243 mean_seconds: float = float(mean_value / self.fs) # 統計値を秒に変換 244 results[column] = mean_seconds 245 246 # 結果とメタデータを出力データに追加 247 output_data.append( 248 { 249 "col1": col1, 250 "col2": column, 251 "col2_lag": round(mean_seconds, 2), # 数値として小数点2桁を保持 252 "lag_unit": "s", 253 "median_range": median_range, 254 } 255 ) 256 257 if print_results: 258 print(f"{column:<{max_col_name_length}} : {mean_seconds:.2f} s") 259 260 # 結果をCSVファイルとして出力 261 if output_dir is not None: 262 output_df: pd.DataFrame = pd.DataFrame(output_data) 263 csv_filepath: str = os.path.join( 264 output_dir, f"lags_results{output_tag}.csv" 265 ) 266 output_df.to_csv(csv_filepath, index=False, encoding="utf-8") 267 self.logger.info(f"解析結果をCSVファイルに保存しました: {csv_filepath}") 268 269 return results 270 271 def get_generated_columns_names(self) -> list[str]: 272 return [ 273 self.WIND_U, 274 self.WIND_V, 275 self.WIND_W, 276 self.RAD_WIND_DIR, 277 self.RAD_WIND_INC, 278 self.DEGREE_WIND_DIR, 279 self.DEGREE_WIND_INC, 280 ] 281 282 def get_resampled_df( 283 self, 284 filepath: str, 285 index_column: str = "TIMESTAMP", 286 index_format: str = "%Y-%m-%d %H:%M:%S.%f", 287 interpolate: bool = True, 288 numeric_columns: list[str] = [ 289 "Ux", 290 "Uy", 291 "Uz", 292 "Tv", 293 "diag_sonic", 294 "CO2_new", 295 "H2O", 296 "diag_irga", 297 "cell_tmpr", 298 "cell_press", 299 "Ultra_CH4_ppm", 300 "Ultra_C2H6_ppb", 301 "Ultra_H2O_ppm", 302 "Ultra_CH4_ppm_C", 303 "Ultra_C2H6_ppb_C", 304 ], 305 metadata_rows: int = 4, 306 skiprows: list[int] = [0, 2, 3], 307 is_already_resampled: bool = False, 308 ) -> tuple[pd.DataFrame, list[str]]: 309 """ 310 CSVファイルを読み込み、前処理を行う 311 312 前処理の手順は以下の通りです: 313 1. 不要な行を削除する。デフォルト(`skiprows=[0, 2, 3]`)の場合は、2行目をヘッダーとして残し、1、3、4行目が削除される。 314 2. 数値データを float 型に変換する 315 3. TIMESTAMP列をDateTimeインデックスに設定する 316 4. エラー値をNaNに置き換える 317 5. 指定されたサンプリングレートでリサンプリングする 318 6. 欠損値(NaN)を前後の値から線形補間する 319 7. DateTimeインデックスを削除する 320 321 Parameters: 322 ----- 323 filepath : str 324 読み込むCSVファイルのパス 325 index_column : str, optional 326 インデックスに使用する列名。デフォルトは'TIMESTAMP'。 327 index_format : str, optional 328 インデックスの日付形式。デフォルトは'%Y-%m-%d %H:%M:%S.%f'。 329 interpolate : bool, optional 330 欠損値の補完を適用するフラグ。デフォルトはTrue。 331 numeric_columns : list[str], optional 332 数値型に変換する列名のリスト。 333 デフォルトは["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"]。 334 metadata_rows : int, optional 335 メタデータとして読み込む行数。デフォルトは4。 336 skiprows : list[int], optional 337 スキップする行インデックスのリスト。デフォルトは[0, 2, 3]のため、1, 3, 4行目がスキップされる。 338 is_already_resampled : bool 339 既にリサンプリング&欠損補間されているか。Trueの場合はfloat変換などの処理のみ適用する。 340 341 Returns: 342 ----- 343 tuple[pd.DataFrame, list[str]] 344 前処理済みのデータフレームとメタデータのリスト。 345 """ 346 # メタデータを読み込む 347 metadata: list[str] = [] 348 with open(filepath, "r") as f: 349 for _ in range(metadata_rows): 350 line = f.readline().strip() 351 metadata.append(line.replace('"', "")) 352 353 # CSVファイルを読み込む 354 df: pd.DataFrame = pd.read_csv(filepath, skiprows=skiprows) 355 356 # 数値データをfloat型に変換する 357 for col in numeric_columns: 358 if col in df.columns: 359 df[col] = pd.to_numeric(df[col], errors="coerce") 360 361 if not is_already_resampled: 362 # μ秒がない場合は".0"を追加する 363 df[index_column] = df[index_column].apply( 364 lambda x: f"{x}.0" if "." not in x else x 365 ) 366 # TIMESTAMPをDateTimeインデックスに設定する 367 df[index_column] = pd.to_datetime(df[index_column], format=index_format) 368 df = df.set_index(index_column) 369 370 # リサンプリング前の有効数字を取得 371 decimal_places = {} 372 for col in numeric_columns: 373 if col in df.columns: 374 max_decimals = ( 375 df[col].astype(str).str.extract(r"\.(\d+)")[0].str.len().max() 376 ) 377 decimal_places[col] = ( 378 int(max_decimals) if pd.notna(max_decimals) else 0 379 ) 380 381 # リサンプリングを実行 382 resampling_period: int = int(1000 / self.fs) 383 df_resampled: pd.DataFrame = df.resample(f"{resampling_period}ms").mean( 384 numeric_only=True 385 ) 386 387 if interpolate: 388 # 補間を実行 389 df_resampled = df_resampled.interpolate() 390 # 有効数字を調整 391 for col, decimals in decimal_places.items(): 392 if col in df_resampled.columns: 393 df_resampled[col] = df_resampled[col].round(decimals) 394 395 # DateTimeインデックスを削除する 396 df = df_resampled.reset_index() 397 # ミリ秒を1桁にフォーマット 398 df[index_column] = ( 399 df[index_column].dt.strftime("%Y-%m-%d %H:%M:%S.%f").str[:-5] 400 ) 401 402 return df, metadata 403 404 def output_resampled_data( 405 self, 406 input_dir: str, 407 resampled_dir: str, 408 c2c1_ratio_dir: str, 409 input_file_pattern: str = r"Eddy_(\d+)", 410 input_files_suffix: str = ".dat", 411 col_c1: str = "Ultra_CH4_ppm_C", 412 col_c2: str = "Ultra_C2H6_ppb", 413 output_c2c1_ratio: bool = True, 414 output_resampled: bool = True, 415 c2c1_ratio_csv_prefix: str = "SAC.Ultra", 416 index_column: str = "TIMESTAMP", 417 index_format: str = "%Y-%m-%d %H:%M:%S.%f", 418 interpolate: bool = True, 419 numeric_columns: list[str] = [ 420 "Ux", 421 "Uy", 422 "Uz", 423 "Tv", 424 "diag_sonic", 425 "CO2_new", 426 "H2O", 427 "diag_irga", 428 "cell_tmpr", 429 "cell_press", 430 "Ultra_CH4_ppm", 431 "Ultra_C2H6_ppb", 432 "Ultra_H2O_ppm", 433 "Ultra_CH4_ppm_C", 434 "Ultra_C2H6_ppb_C", 435 ], 436 metadata_rows: int = 4, 437 skiprows: list[int] = [0, 2, 3], 438 ) -> None: 439 """ 440 指定されたディレクトリ内のCSVファイルを処理し、リサンプリングと欠損値補間を行います。 441 442 このメソッドは、指定されたディレクトリ内のCSVファイルを読み込み、リサンプリングを行い、 443 欠損値を補完します。処理結果として、リサンプリングされたCSVファイルを出力し、 444 相関係数やC2H6/CH4比を計算してDataFrameに保存します。 445 リサンプリングと欠損値補完は`get_resampled_df`と同様のロジックを使用します。 446 447 Parameters: 448 ----- 449 input_dir : str 450 入力CSVファイルが格納されているディレクトリのパス。 451 resampled_dir : str 452 リサンプリングされたCSVファイルを出力するディレクトリのパス。 453 c2c1_ratio_dir : str 454 計算結果を保存するディレクトリのパス。 455 input_file_pattern : str 456 ファイル名からソートキーを抽出する正規表現パターン。デフォルトでは、最初の数字グループでソートします。 457 input_files_suffix : str 458 入力ファイルの拡張子(.datや.csvなど)。デフォルトは".dat"。 459 col_c1 : str 460 CH4濃度を含む列名。デフォルトは'Ultra_CH4_ppm_C'。 461 col_c2 : str 462 C2H6濃度を含む列名。デフォルトは'Ultra_C2H6_ppb'。 463 output_c2c1_ratio : bool, optional 464 線形回帰を行うかどうか。デフォルトはTrue。 465 output_resampled : bool, optional 466 リサンプリングされたCSVファイルを出力するかどうか。デフォルトはTrue。 467 c2c1_ratio_csv_prefix : str 468 出力ファイルの接頭辞。デフォルトは'SAC.Ultra'で、出力時は'SAC.Ultra.2024.09.21.ratio.csv'のような形式となる。 469 index_column : str 470 日時情報を含む列名。デフォルトは'TIMESTAMP'。 471 index_format : str, optional 472 インデックスの日付形式。デフォルトは'%Y-%m-%d %H:%M:%S.%f'。 473 interpolate : bool 474 欠損値補間を行うかどうか。デフォルトはTrue。 475 numeric_columns : list[str] 476 数値データを含む列名のリスト。デフォルトは指定された列名のリスト。 477 metadata_rows : int 478 メタデータとして読み込む行数。デフォルトは4。 479 skiprows : list[int] 480 読み飛ばす行のインデックスリスト。デフォルトは[0, 2, 3]。 481 482 Raises: 483 ----- 484 OSError 485 ディレクトリの作成に失敗した場合。 486 FileNotFoundError 487 入力ファイルが見つからない場合。 488 ValueError 489 出力ディレクトリが指定されていない、またはデータの処理中にエラーが発生した場合。 490 """ 491 # 出力オプションとディレクトリの検証 492 if output_resampled and resampled_dir is None: 493 raise ValueError( 494 "output_resampled が True の場合、resampled_dir を指定する必要があります" 495 ) 496 if output_c2c1_ratio and c2c1_ratio_dir is None: 497 raise ValueError( 498 "output_c2c1_ratio が True の場合、c2c1_ratio_dir を指定する必要があります" 499 ) 500 501 # ディレクトリの作成(必要な場合のみ) 502 if output_resampled: 503 os.makedirs(resampled_dir, exist_ok=True) 504 if output_c2c1_ratio: 505 os.makedirs(c2c1_ratio_dir, exist_ok=True) 506 507 ratio_data: list[dict[str, str | float]] = [] 508 latest_date: datetime = datetime.min 509 510 # csvファイル名のリスト 511 csv_files: list[str] = EddyDataPreprocessor._get_sorted_files( 512 input_dir, input_file_pattern, input_files_suffix 513 ) 514 515 for filename in tqdm(csv_files, desc="Processing files"): 516 input_filepath: str = os.path.join(input_dir, filename) 517 # リサンプリング&欠損値補間 518 df, metadata = self.get_resampled_df( 519 filepath=input_filepath, 520 index_column=index_column, 521 index_format=index_format, 522 interpolate=interpolate, 523 numeric_columns=numeric_columns, 524 metadata_rows=metadata_rows, 525 skiprows=skiprows, 526 ) 527 528 # 開始時間を取得 529 start_time: datetime = pd.to_datetime(df[index_column].iloc[0]) 530 # 処理したファイルの中で最も最新の日付 531 latest_date = max(latest_date, start_time) 532 533 # リサンプリング&欠損値補間したCSVを出力 534 if output_resampled: 535 base_filename: str = re.sub(rf"\{input_files_suffix}$", "", filename) 536 output_csv_path: str = os.path.join( 537 resampled_dir, f"{base_filename}-resampled.csv" 538 ) 539 # メタデータを先に書き込む 540 with open(output_csv_path, "w") as f: 541 for line in metadata: 542 f.write(f"{line}\n") 543 # データフレームを追記モードで書き込む 544 df.to_csv( 545 output_csv_path, index=False, mode="a", quoting=3, header=False 546 ) 547 548 # 相関係数とC2H6/CH4比を計算 549 if output_c2c1_ratio: 550 ch4_data: pd.Series = df[col_c1] 551 c2h6_data: pd.Series = df[col_c2] 552 553 ratio_row: dict[str, str | float] = { 554 "Date": start_time.strftime("%Y-%m-%d %H:%M:%S.%f"), 555 "slope": f"{np.nan}", 556 "intercept": f"{np.nan}", 557 "r_value": f"{np.nan}", 558 "p_value": f"{np.nan}", 559 "stderr": f"{np.nan}", 560 } 561 # 近似直線の傾き、切片、相関係数を計算 562 try: 563 slope, intercept, r_value, p_value, stderr = stats.linregress( 564 ch4_data, c2h6_data 565 ) 566 ratio_row: dict[str, str | float] = { 567 "Date": start_time.strftime("%Y-%m-%d %H:%M:%S.%f"), 568 "slope": f"{slope:.6f}", 569 "intercept": f"{intercept:.6f}", 570 "r_value": f"{r_value:.6f}", 571 "p_value": f"{p_value:.6f}", 572 "stderr": f"{stderr:.6f}", 573 } 574 except Exception: 575 # 何もせず、デフォルトの ratio_row を使用する 576 pass 577 578 # 結果をリストに追加 579 ratio_data.append(ratio_row) 580 581 if output_c2c1_ratio: 582 # DataFrameを作成し、Dateカラムで昇順ソート 583 ratio_df: pd.DataFrame = pd.DataFrame(ratio_data) 584 ratio_df["Date"] = pd.to_datetime( 585 ratio_df["Date"] 586 ) # Dateカラムをdatetime型に変換 587 ratio_df = ratio_df.sort_values("Date") # Dateカラムで昇順ソート 588 589 # CSVとして保存 590 ratio_filename: str = ( 591 f"{c2c1_ratio_csv_prefix}.{latest_date.strftime('%Y.%m.%d')}.ratio.csv" 592 ) 593 ratio_path: str = os.path.join(c2c1_ratio_dir, ratio_filename) 594 ratio_df.to_csv(ratio_path, index=False) 595 596 @staticmethod 597 def _calculate_lag_time( 598 df: pd.DataFrame, 599 col1: str, 600 col2_list: list[str], 601 ) -> list[int]: 602 """ 603 指定された基準変数(col1)と比較変数のリスト(col2_list)の間の遅れ時間(ディレイ)を計算する。 604 周波数が10Hzでcol1がcol2より10.0秒遅れている場合は、+100がインデックスとして取得される 605 606 Parameters: 607 ----- 608 df : pd.DataFrame 609 遅れ時間の計算に使用するデータフレーム 610 col1 : str 611 基準変数の列名 612 col2_list : list[str] 613 比較変数の列名のリスト 614 615 Returns: 616 ----- 617 list[int] 618 各比較変数に対する遅れ時間(ディレイ)のリスト 619 """ 620 lags_list: list[int] = [] 621 for col2 in col2_list: 622 data1: np.ndarray = np.array(df[col1].values) 623 data2: np.ndarray = np.array(df[col2].values) 624 625 # 平均を0に調整 626 data1 = data1 - data1.mean() 627 data2 = data2 - data2.mean() 628 629 data_length: int = len(data1) 630 631 # 相互相関の計算 632 correlation: np.ndarray = np.correlate( 633 data1, data2, mode="full" 634 ) # data2とdata1の順序を入れ替え 635 636 # 相互相関のピークのインデックスを取得 637 lag: int = int((data_length - 1) - correlation.argmax()) # 符号を反転 638 639 lags_list.append(lag) 640 return lags_list 641 642 @staticmethod 643 def _get_sorted_files(directory: str, pattern: str, suffix: str) -> list[str]: 644 """ 645 指定されたディレクトリ内のファイルを、ファイル名に含まれる数字に基づいてソートして返す。 646 647 Parameters: 648 ----- 649 directory : str 650 ファイルが格納されているディレクトリのパス 651 pattern : str 652 ファイル名からソートキーを抽出する正規表現パターン 653 suffix : str 654 ファイルの拡張子 655 656 Returns: 657 ----- 658 list[str] 659 ソートされたファイル名のリスト 660 """ 661 files: list[str] = [f for f in os.listdir(directory) if f.endswith(suffix)] 662 files = [f for f in files if re.search(pattern, f)] 663 files.sort( 664 key=lambda x: int(re.search(pattern, x).group(1)) # type:ignore 665 if re.search(pattern, x) 666 else float("inf") 667 ) 668 return files 669 670 @staticmethod 671 def _horizontal_wind_speed( 672 x_array: np.ndarray, y_array: np.ndarray, wind_dir: float 673 ) -> tuple[np.ndarray, np.ndarray]: 674 """ 675 風速のu成分とv成分を計算する関数 676 677 Parameters: 678 ----- 679 x_array : numpy.ndarray 680 x方向の風速成分の配列 681 y_array : numpy.ndarray 682 y方向の風速成分の配列 683 wind_dir : float 684 水平成分の風向(ラジアン) 685 686 Returns: 687 ----- 688 tuple[numpy.ndarray, numpy.ndarray] 689 u成分とv成分のタプル 690 """ 691 # スカラー風速の計算 692 scalar_hypotenuse: np.ndarray = np.sqrt(x_array**2 + y_array**2) 693 # CSAT3では以下の補正が必要 694 instantaneous_wind_directions = EddyDataPreprocessor._wind_direction( 695 x_array=x_array, y_array=y_array 696 ) 697 # ベクトル風速の計算 698 vector_u: np.ndarray = scalar_hypotenuse * np.cos( 699 instantaneous_wind_directions - wind_dir 700 ) 701 vector_v: np.ndarray = scalar_hypotenuse * np.sin( 702 instantaneous_wind_directions - wind_dir 703 ) 704 return vector_u, vector_v 705 706 @staticmethod 707 def setup_logger(logger: Logger | None, log_level: int = INFO) -> Logger: 708 """ 709 ロガーを設定します。 710 711 このメソッドは、ロギングの設定を行い、ログメッセージのフォーマットを指定します。 712 ログメッセージには、日付、ログレベル、メッセージが含まれます。 713 714 渡されたロガーがNoneまたは不正な場合は、新たにロガーを作成し、標準出力に 715 ログメッセージが表示されるようにStreamHandlerを追加します。ロガーのレベルは 716 引数で指定されたlog_levelに基づいて設定されます。 717 718 Parameters: 719 ----- 720 logger : Logger | None 721 使用するロガー。Noneの場合は新しいロガーを作成します。 722 log_level : int 723 ロガーのログレベル。デフォルトはINFO。 724 725 Returns: 726 ----- 727 Logger 728 設定されたロガーオブジェクト。 729 """ 730 if logger is not None and isinstance(logger, Logger): 731 return logger 732 # 渡されたロガーがNoneまたは正しいものでない場合は独自に設定 733 new_logger: Logger = getLogger() 734 # 既存のハンドラーをすべて削除 735 for handler in new_logger.handlers[:]: 736 new_logger.removeHandler(handler) 737 new_logger.setLevel(log_level) # ロガーのレベルを設定 738 ch = StreamHandler() 739 ch_formatter = Formatter("%(asctime)s - %(levelname)s - %(message)s") 740 ch.setFormatter(ch_formatter) # フォーマッターをハンドラーに設定 741 new_logger.addHandler(ch) # StreamHandlerの追加 742 return new_logger 743 744 @staticmethod 745 def _vertical_rotation( 746 u_array: np.ndarray, 747 w_array: np.ndarray, 748 wind_inc: float, 749 ) -> tuple[np.ndarray, np.ndarray]: 750 """ 751 鉛直方向の座標回転を行い、u, wを求める関数 752 753 Parameters: 754 ----- 755 u_array (numpy.ndarray): u方向の風速 756 w_array (numpy.ndarray): w方向の風速 757 wind_inc (float): 平均風向に対する迎角(ラジアン) 758 759 Returns: 760 ----- 761 tuple[numpy.ndarray, numpy.ndarray]: 回転後のu, w 762 """ 763 # 迎角を用いて鉛直方向に座標回転 764 u_rotated = u_array * np.cos(wind_inc) + w_array * np.sin(wind_inc) 765 w_rotated = w_array * np.cos(wind_inc) - u_array * np.sin(wind_inc) 766 return u_rotated, w_rotated 767 768 @staticmethod 769 def _wind_direction( 770 x_array: np.ndarray, y_array: np.ndarray, correction_angle: float = 0.0 771 ) -> float: 772 """ 773 水平方向の平均風向を計算する関数 774 775 Parameters: 776 ----- 777 x_array (numpy.ndarray): 西方向の風速成分 778 y_array (numpy.ndarray): 南北方向の風速成分 779 correction_angle (float): 風向補正角度(ラジアン)。デフォルトは0.0。CSAT3の場合は0.0を指定。 780 781 Returns: 782 ----- 783 wind_direction (float): 風向 (radians) 784 """ 785 wind_direction: float = np.arctan2(np.mean(y_array), np.mean(x_array)) 786 # 補正角度を適用 787 wind_direction = correction_angle - wind_direction 788 return wind_direction 789 790 @staticmethod 791 def _wind_inclination(u_array: np.ndarray, w_array: np.ndarray) -> float: 792 """ 793 平均風向に対する迎角を計算する関数 794 795 Parameters: 796 ----- 797 u_array (numpy.ndarray): u方向の瞬間風速 798 w_array (numpy.ndarray): w方向の瞬間風速 799 800 Returns: 801 ----- 802 wind_inc (float): 平均風向に対する迎角(ラジアン) 803 """ 804 wind_inc: float = np.arctan2(np.mean(w_array), np.mean(u_array)) 805 return wind_inc
23 def __init__( 24 self, 25 fs: float = 10, 26 logger: Logger | None = None, 27 logging_debug: bool = False, 28 ): 29 """ 30 渦相関法によって記録されたデータファイルを処理するクラス。 31 32 Parameters 33 ---------- 34 fs (float): サンプリング周波数。 35 logger (Logger | None): 使用するロガー。Noneの場合は新しいロガーを作成します。 36 logging_debug (bool): ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。 37 """ 38 self.fs: float = fs 39 40 # ロガー 41 log_level: int = INFO 42 if logging_debug: 43 log_level = DEBUG 44 self.logger: Logger = EddyDataPreprocessor.setup_logger(logger, log_level)
渦相関法によって記録されたデータファイルを処理するクラス。
Parameters
fs (float): サンプリング周波数。
logger (Logger | None): 使用するロガー。Noneの場合は新しいロガーを作成します。
logging_debug (bool): ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。
46 def add_uvw_columns(self, df: pd.DataFrame) -> pd.DataFrame: 47 """ 48 DataFrameに水平風速u、v、鉛直風速wの列を追加する関数。 49 各成分のキーは`wind_u`、`wind_v`、`wind_w`である。 50 51 Parameters 52 ----- 53 df : pd.DataFrame 54 風速データを含むDataFrame 55 56 Returns 57 ----- 58 pd.DataFrame 59 水平風速u、v、鉛直風速wの列を追加したDataFrame 60 """ 61 required_columns: list[str] = ["Ux", "Uy", "Uz"] 62 # 必要な列がDataFrameに存在するか確認 63 for column in required_columns: 64 if column not in df.columns: 65 raise ValueError(f"必要な列 '{column}' がDataFrameに存在しません。") 66 67 processed_df: pd.DataFrame = df.copy() 68 # pandasの.valuesを使用してnumpy配列を取得し、その型をnp.ndarrayに明示的にキャストする 69 wind_x_array: np.ndarray = np.array(processed_df["Ux"].values) 70 wind_y_array: np.ndarray = np.array(processed_df["Uy"].values) 71 wind_z_array: np.ndarray = np.array(processed_df["Uz"].values) 72 73 # 平均風向を計算 74 wind_direction: float = EddyDataPreprocessor._wind_direction( 75 wind_x_array, wind_y_array 76 ) 77 78 # 水平方向に座標回転を行u, v成分を求める 79 wind_u_array, wind_v_array = EddyDataPreprocessor._horizontal_wind_speed( 80 wind_x_array, wind_y_array, wind_direction 81 ) 82 wind_w_array: np.ndarray = wind_z_array # wはz成分そのまま 83 84 # u, wから風の迎角を計算 85 wind_inclination: float = EddyDataPreprocessor._wind_inclination( 86 wind_u_array, wind_w_array 87 ) 88 89 # 2回座標回転を行い、u, wを求める 90 wind_u_array_rotated, wind_w_array_rotated = ( 91 EddyDataPreprocessor._vertical_rotation( 92 wind_u_array, wind_w_array, wind_inclination 93 ) 94 ) 95 96 processed_df[self.WIND_U] = wind_u_array_rotated 97 processed_df[self.WIND_V] = wind_v_array 98 processed_df[self.WIND_W] = wind_w_array_rotated 99 processed_df[self.RAD_WIND_DIR] = wind_direction 100 processed_df[self.RAD_WIND_INC] = wind_inclination 101 processed_df[self.DEGREE_WIND_DIR] = np.degrees(wind_direction) 102 processed_df[self.DEGREE_WIND_INC] = np.degrees(wind_inclination) 103 104 return processed_df
DataFrameに水平風速u、v、鉛直風速wの列を追加する関数。
各成分のキーはwind_u
、wind_v
、wind_w
である。
Parameters
df : pd.DataFrame
風速データを含むDataFrame
Returns
pd.DataFrame
水平風速u、v、鉛直風速wの列を追加したDataFrame
106 def analyze_lag_times( 107 self, 108 input_dir: str, 109 figsize: tuple[float, float] = (10, 8), 110 input_files_pattern: str = r"Eddy_(\d+)", 111 input_files_suffix: str = ".dat", 112 col1: str = "edp_wind_w", 113 col2_list: list[str] = ["Tv"], 114 median_range: float = 20, 115 metadata_rows: int = 4, 116 output_dir: str | None = None, 117 output_tag: str = "", 118 plot_range_tuple: tuple = (-50, 200), 119 print_results: bool = True, 120 skiprows: list[int] = [0, 2, 3], 121 use_resampling: bool = True, 122 ) -> dict[str, float]: 123 """ 124 遅れ時間(ラグ)の統計分析を行い、指定されたディレクトリ内のデータファイルを処理します。 125 解析結果とメタデータはCSVファイルとして出力されます。 126 127 Parameters: 128 ----- 129 input_dir : str 130 入力データファイルが格納されているディレクトリのパス。 131 figsize : tuple[float, float] 132 プロットのサイズ(幅、高さ)。 133 input_files_pattern : str 134 入力ファイル名のパターン(正規表現)。 135 input_files_suffix : str 136 入力ファイルの拡張子。 137 col1 : str 138 基準変数の列名。 139 col2_list : list[str] 140 比較変数の列名のリスト。 141 median_range : float 142 中央値を中心とした範囲。 143 metadata_rows : int 144 メタデータの行数。 145 output_dir : str | None 146 出力ディレクトリのパス。Noneの場合は保存しない。 147 output_tag : str 148 出力ファイルに付与するタグ。デフォルトは空文字で、何も付与されない。 149 plot_range_tuple : tuple 150 ヒストグラムの表示範囲。 151 print_results : bool 152 結果をコンソールに表示するかどうか。 153 skiprows : list[int] 154 スキップする行番号のリスト。 155 use_resampling : bool 156 データをリサンプリングするかどうか。 157 inputするファイルが既にリサンプリング済みの場合はFalseでよい。 158 デフォルトはTrue。 159 160 Returns: 161 ----- 162 dict[str, float] 163 各変数の遅れ時間(平均値を採用)を含む辞書。 164 """ 165 if output_dir is None: 166 self.logger.warn( 167 "output_dirが指定されていません。解析結果を保存する場合は、有効なディレクトリを指定してください。" 168 ) 169 all_lags_indices: list[list[int]] = [] 170 results: dict[str, float] = {} 171 172 # メイン処理 173 # ファイル名に含まれる数字に基づいてソート 174 csv_files = EddyDataPreprocessor._get_sorted_files( 175 input_dir, input_files_pattern, input_files_suffix 176 ) 177 if not csv_files: 178 raise FileNotFoundError( 179 f"There is no '{input_files_suffix}' file to process; input_dir: '{input_dir}', input_files_suffix: '{input_files_suffix}'" 180 ) 181 182 for file in tqdm(csv_files, desc="Calculating"): 183 path: str = os.path.join(input_dir, file) 184 if use_resampling: 185 df, _ = self.get_resampled_df( 186 filepath=path, metadata_rows=metadata_rows, skiprows=skiprows 187 ) 188 else: 189 df = pd.read_csv(path, skiprows=skiprows) 190 df = self.add_uvw_columns(df) 191 lags_list = EddyDataPreprocessor._calculate_lag_time( 192 df, 193 col1, 194 col2_list, 195 ) 196 all_lags_indices.append(lags_list) 197 self.logger.info("すべてのCSVファイルにおける遅れ時間が計算されました。") 198 199 # Convert all_lags_indices to a DataFrame 200 lags_indices_df: pd.DataFrame = pd.DataFrame( 201 all_lags_indices, columns=col2_list 202 ) 203 204 # フォーマット用のキーの最大の長さ 205 max_col_name_length: int = max( 206 len(column) for column in lags_indices_df.columns 207 ) 208 209 if print_results: 210 self.logger.info(f"カラム`{col1}`に対する遅れ時間を表示します。") 211 212 # 結果を格納するためのリスト 213 output_data = [] 214 215 for column in lags_indices_df.columns: 216 data: pd.Series = lags_indices_df[column] 217 218 # ヒストグラムの作成 219 plt.figure(figsize=figsize) 220 plt.hist(data, bins=20, range=plot_range_tuple) 221 plt.title(f"Delays of {column}") 222 plt.xlabel("Seconds") 223 plt.ylabel("Frequency") 224 plt.xlim(plot_range_tuple) 225 226 # ファイルとして保存するか 227 if output_dir is not None: 228 os.makedirs(output_dir, exist_ok=True) 229 filename: str = f"lags_histogram-{column}{output_tag}.png" 230 filepath: str = os.path.join(output_dir, filename) 231 plt.savefig(filepath, dpi=300, bbox_inches="tight") 232 plt.close() 233 234 # 中央値を計算し、その周辺のデータのみを使用 235 median_value = np.median(data) 236 filtered_data: pd.Series = data[ 237 (data >= median_value - median_range) 238 & (data <= median_value + median_range) 239 ] 240 241 # 平均値を計算 242 mean_value = np.mean(filtered_data) 243 mean_seconds: float = float(mean_value / self.fs) # 統計値を秒に変換 244 results[column] = mean_seconds 245 246 # 結果とメタデータを出力データに追加 247 output_data.append( 248 { 249 "col1": col1, 250 "col2": column, 251 "col2_lag": round(mean_seconds, 2), # 数値として小数点2桁を保持 252 "lag_unit": "s", 253 "median_range": median_range, 254 } 255 ) 256 257 if print_results: 258 print(f"{column:<{max_col_name_length}} : {mean_seconds:.2f} s") 259 260 # 結果をCSVファイルとして出力 261 if output_dir is not None: 262 output_df: pd.DataFrame = pd.DataFrame(output_data) 263 csv_filepath: str = os.path.join( 264 output_dir, f"lags_results{output_tag}.csv" 265 ) 266 output_df.to_csv(csv_filepath, index=False, encoding="utf-8") 267 self.logger.info(f"解析結果をCSVファイルに保存しました: {csv_filepath}") 268 269 return results
遅れ時間(ラグ)の統計分析を行い、指定されたディレクトリ内のデータファイルを処理します。 解析結果とメタデータはCSVファイルとして出力されます。
Parameters:
input_dir : str
入力データファイルが格納されているディレクトリのパス。
figsize : tuple[float, float]
プロットのサイズ(幅、高さ)。
input_files_pattern : str
入力ファイル名のパターン(正規表現)。
input_files_suffix : str
入力ファイルの拡張子。
col1 : str
基準変数の列名。
col2_list : list[str]
比較変数の列名のリスト。
median_range : float
中央値を中心とした範囲。
metadata_rows : int
メタデータの行数。
output_dir : str | None
出力ディレクトリのパス。Noneの場合は保存しない。
output_tag : str
出力ファイルに付与するタグ。デフォルトは空文字で、何も付与されない。
plot_range_tuple : tuple
ヒストグラムの表示範囲。
print_results : bool
結果をコンソールに表示するかどうか。
skiprows : list[int]
スキップする行番号のリスト。
use_resampling : bool
データをリサンプリングするかどうか。
inputするファイルが既にリサンプリング済みの場合はFalseでよい。
デフォルトはTrue。
Returns:
dict[str, float]
各変数の遅れ時間(平均値を採用)を含む辞書。
282 def get_resampled_df( 283 self, 284 filepath: str, 285 index_column: str = "TIMESTAMP", 286 index_format: str = "%Y-%m-%d %H:%M:%S.%f", 287 interpolate: bool = True, 288 numeric_columns: list[str] = [ 289 "Ux", 290 "Uy", 291 "Uz", 292 "Tv", 293 "diag_sonic", 294 "CO2_new", 295 "H2O", 296 "diag_irga", 297 "cell_tmpr", 298 "cell_press", 299 "Ultra_CH4_ppm", 300 "Ultra_C2H6_ppb", 301 "Ultra_H2O_ppm", 302 "Ultra_CH4_ppm_C", 303 "Ultra_C2H6_ppb_C", 304 ], 305 metadata_rows: int = 4, 306 skiprows: list[int] = [0, 2, 3], 307 is_already_resampled: bool = False, 308 ) -> tuple[pd.DataFrame, list[str]]: 309 """ 310 CSVファイルを読み込み、前処理を行う 311 312 前処理の手順は以下の通りです: 313 1. 不要な行を削除する。デフォルト(`skiprows=[0, 2, 3]`)の場合は、2行目をヘッダーとして残し、1、3、4行目が削除される。 314 2. 数値データを float 型に変換する 315 3. TIMESTAMP列をDateTimeインデックスに設定する 316 4. エラー値をNaNに置き換える 317 5. 指定されたサンプリングレートでリサンプリングする 318 6. 欠損値(NaN)を前後の値から線形補間する 319 7. DateTimeインデックスを削除する 320 321 Parameters: 322 ----- 323 filepath : str 324 読み込むCSVファイルのパス 325 index_column : str, optional 326 インデックスに使用する列名。デフォルトは'TIMESTAMP'。 327 index_format : str, optional 328 インデックスの日付形式。デフォルトは'%Y-%m-%d %H:%M:%S.%f'。 329 interpolate : bool, optional 330 欠損値の補完を適用するフラグ。デフォルトはTrue。 331 numeric_columns : list[str], optional 332 数値型に変換する列名のリスト。 333 デフォルトは["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"]。 334 metadata_rows : int, optional 335 メタデータとして読み込む行数。デフォルトは4。 336 skiprows : list[int], optional 337 スキップする行インデックスのリスト。デフォルトは[0, 2, 3]のため、1, 3, 4行目がスキップされる。 338 is_already_resampled : bool 339 既にリサンプリング&欠損補間されているか。Trueの場合はfloat変換などの処理のみ適用する。 340 341 Returns: 342 ----- 343 tuple[pd.DataFrame, list[str]] 344 前処理済みのデータフレームとメタデータのリスト。 345 """ 346 # メタデータを読み込む 347 metadata: list[str] = [] 348 with open(filepath, "r") as f: 349 for _ in range(metadata_rows): 350 line = f.readline().strip() 351 metadata.append(line.replace('"', "")) 352 353 # CSVファイルを読み込む 354 df: pd.DataFrame = pd.read_csv(filepath, skiprows=skiprows) 355 356 # 数値データをfloat型に変換する 357 for col in numeric_columns: 358 if col in df.columns: 359 df[col] = pd.to_numeric(df[col], errors="coerce") 360 361 if not is_already_resampled: 362 # μ秒がない場合は".0"を追加する 363 df[index_column] = df[index_column].apply( 364 lambda x: f"{x}.0" if "." not in x else x 365 ) 366 # TIMESTAMPをDateTimeインデックスに設定する 367 df[index_column] = pd.to_datetime(df[index_column], format=index_format) 368 df = df.set_index(index_column) 369 370 # リサンプリング前の有効数字を取得 371 decimal_places = {} 372 for col in numeric_columns: 373 if col in df.columns: 374 max_decimals = ( 375 df[col].astype(str).str.extract(r"\.(\d+)")[0].str.len().max() 376 ) 377 decimal_places[col] = ( 378 int(max_decimals) if pd.notna(max_decimals) else 0 379 ) 380 381 # リサンプリングを実行 382 resampling_period: int = int(1000 / self.fs) 383 df_resampled: pd.DataFrame = df.resample(f"{resampling_period}ms").mean( 384 numeric_only=True 385 ) 386 387 if interpolate: 388 # 補間を実行 389 df_resampled = df_resampled.interpolate() 390 # 有効数字を調整 391 for col, decimals in decimal_places.items(): 392 if col in df_resampled.columns: 393 df_resampled[col] = df_resampled[col].round(decimals) 394 395 # DateTimeインデックスを削除する 396 df = df_resampled.reset_index() 397 # ミリ秒を1桁にフォーマット 398 df[index_column] = ( 399 df[index_column].dt.strftime("%Y-%m-%d %H:%M:%S.%f").str[:-5] 400 ) 401 402 return df, metadata
CSVファイルを読み込み、前処理を行う
前処理の手順は以下の通りです:
- 不要な行を削除する。デフォルト(
skiprows=[0, 2, 3]
)の場合は、2行目をヘッダーとして残し、1、3、4行目が削除される。 - 数値データを float 型に変換する
- TIMESTAMP列をDateTimeインデックスに設定する
- エラー値をNaNに置き換える
- 指定されたサンプリングレートでリサンプリングする
- 欠損値(NaN)を前後の値から線形補間する
- DateTimeインデックスを削除する
Parameters:
filepath : str
読み込むCSVファイルのパス
index_column : str, optional
インデックスに使用する列名。デフォルトは'TIMESTAMP'。
index_format : str, optional
インデックスの日付形式。デフォルトは'%Y-%m-%d %H:%M:%S.%f'。
interpolate : bool, optional
欠損値の補完を適用するフラグ。デフォルトはTrue。
numeric_columns : list[str], optional
数値型に変換する列名のリスト。
デフォルトは["Ux", "Uy", "Uz", "Tv", "diag_sonic", "CO2_new", "H2O", "diag_irga", "cell_tmpr", "cell_press", "Ultra_CH4_ppm", "Ultra_C2H6_ppb", "Ultra_H2O_ppm", "Ultra_CH4_ppm_C", "Ultra_C2H6_ppb_C"]。
metadata_rows : int, optional
メタデータとして読み込む行数。デフォルトは4。
skiprows : list[int], optional
スキップする行インデックスのリスト。デフォルトは[0, 2, 3]のため、1, 3, 4行目がスキップされる。
is_already_resampled : bool
既にリサンプリング&欠損補間されているか。Trueの場合はfloat変換などの処理のみ適用する。
Returns:
tuple[pd.DataFrame, list[str]]
前処理済みのデータフレームとメタデータのリスト。
404 def output_resampled_data( 405 self, 406 input_dir: str, 407 resampled_dir: str, 408 c2c1_ratio_dir: str, 409 input_file_pattern: str = r"Eddy_(\d+)", 410 input_files_suffix: str = ".dat", 411 col_c1: str = "Ultra_CH4_ppm_C", 412 col_c2: str = "Ultra_C2H6_ppb", 413 output_c2c1_ratio: bool = True, 414 output_resampled: bool = True, 415 c2c1_ratio_csv_prefix: str = "SAC.Ultra", 416 index_column: str = "TIMESTAMP", 417 index_format: str = "%Y-%m-%d %H:%M:%S.%f", 418 interpolate: bool = True, 419 numeric_columns: list[str] = [ 420 "Ux", 421 "Uy", 422 "Uz", 423 "Tv", 424 "diag_sonic", 425 "CO2_new", 426 "H2O", 427 "diag_irga", 428 "cell_tmpr", 429 "cell_press", 430 "Ultra_CH4_ppm", 431 "Ultra_C2H6_ppb", 432 "Ultra_H2O_ppm", 433 "Ultra_CH4_ppm_C", 434 "Ultra_C2H6_ppb_C", 435 ], 436 metadata_rows: int = 4, 437 skiprows: list[int] = [0, 2, 3], 438 ) -> None: 439 """ 440 指定されたディレクトリ内のCSVファイルを処理し、リサンプリングと欠損値補間を行います。 441 442 このメソッドは、指定されたディレクトリ内のCSVファイルを読み込み、リサンプリングを行い、 443 欠損値を補完します。処理結果として、リサンプリングされたCSVファイルを出力し、 444 相関係数やC2H6/CH4比を計算してDataFrameに保存します。 445 リサンプリングと欠損値補完は`get_resampled_df`と同様のロジックを使用します。 446 447 Parameters: 448 ----- 449 input_dir : str 450 入力CSVファイルが格納されているディレクトリのパス。 451 resampled_dir : str 452 リサンプリングされたCSVファイルを出力するディレクトリのパス。 453 c2c1_ratio_dir : str 454 計算結果を保存するディレクトリのパス。 455 input_file_pattern : str 456 ファイル名からソートキーを抽出する正規表現パターン。デフォルトでは、最初の数字グループでソートします。 457 input_files_suffix : str 458 入力ファイルの拡張子(.datや.csvなど)。デフォルトは".dat"。 459 col_c1 : str 460 CH4濃度を含む列名。デフォルトは'Ultra_CH4_ppm_C'。 461 col_c2 : str 462 C2H6濃度を含む列名。デフォルトは'Ultra_C2H6_ppb'。 463 output_c2c1_ratio : bool, optional 464 線形回帰を行うかどうか。デフォルトはTrue。 465 output_resampled : bool, optional 466 リサンプリングされたCSVファイルを出力するかどうか。デフォルトはTrue。 467 c2c1_ratio_csv_prefix : str 468 出力ファイルの接頭辞。デフォルトは'SAC.Ultra'で、出力時は'SAC.Ultra.2024.09.21.ratio.csv'のような形式となる。 469 index_column : str 470 日時情報を含む列名。デフォルトは'TIMESTAMP'。 471 index_format : str, optional 472 インデックスの日付形式。デフォルトは'%Y-%m-%d %H:%M:%S.%f'。 473 interpolate : bool 474 欠損値補間を行うかどうか。デフォルトはTrue。 475 numeric_columns : list[str] 476 数値データを含む列名のリスト。デフォルトは指定された列名のリスト。 477 metadata_rows : int 478 メタデータとして読み込む行数。デフォルトは4。 479 skiprows : list[int] 480 読み飛ばす行のインデックスリスト。デフォルトは[0, 2, 3]。 481 482 Raises: 483 ----- 484 OSError 485 ディレクトリの作成に失敗した場合。 486 FileNotFoundError 487 入力ファイルが見つからない場合。 488 ValueError 489 出力ディレクトリが指定されていない、またはデータの処理中にエラーが発生した場合。 490 """ 491 # 出力オプションとディレクトリの検証 492 if output_resampled and resampled_dir is None: 493 raise ValueError( 494 "output_resampled が True の場合、resampled_dir を指定する必要があります" 495 ) 496 if output_c2c1_ratio and c2c1_ratio_dir is None: 497 raise ValueError( 498 "output_c2c1_ratio が True の場合、c2c1_ratio_dir を指定する必要があります" 499 ) 500 501 # ディレクトリの作成(必要な場合のみ) 502 if output_resampled: 503 os.makedirs(resampled_dir, exist_ok=True) 504 if output_c2c1_ratio: 505 os.makedirs(c2c1_ratio_dir, exist_ok=True) 506 507 ratio_data: list[dict[str, str | float]] = [] 508 latest_date: datetime = datetime.min 509 510 # csvファイル名のリスト 511 csv_files: list[str] = EddyDataPreprocessor._get_sorted_files( 512 input_dir, input_file_pattern, input_files_suffix 513 ) 514 515 for filename in tqdm(csv_files, desc="Processing files"): 516 input_filepath: str = os.path.join(input_dir, filename) 517 # リサンプリング&欠損値補間 518 df, metadata = self.get_resampled_df( 519 filepath=input_filepath, 520 index_column=index_column, 521 index_format=index_format, 522 interpolate=interpolate, 523 numeric_columns=numeric_columns, 524 metadata_rows=metadata_rows, 525 skiprows=skiprows, 526 ) 527 528 # 開始時間を取得 529 start_time: datetime = pd.to_datetime(df[index_column].iloc[0]) 530 # 処理したファイルの中で最も最新の日付 531 latest_date = max(latest_date, start_time) 532 533 # リサンプリング&欠損値補間したCSVを出力 534 if output_resampled: 535 base_filename: str = re.sub(rf"\{input_files_suffix}$", "", filename) 536 output_csv_path: str = os.path.join( 537 resampled_dir, f"{base_filename}-resampled.csv" 538 ) 539 # メタデータを先に書き込む 540 with open(output_csv_path, "w") as f: 541 for line in metadata: 542 f.write(f"{line}\n") 543 # データフレームを追記モードで書き込む 544 df.to_csv( 545 output_csv_path, index=False, mode="a", quoting=3, header=False 546 ) 547 548 # 相関係数とC2H6/CH4比を計算 549 if output_c2c1_ratio: 550 ch4_data: pd.Series = df[col_c1] 551 c2h6_data: pd.Series = df[col_c2] 552 553 ratio_row: dict[str, str | float] = { 554 "Date": start_time.strftime("%Y-%m-%d %H:%M:%S.%f"), 555 "slope": f"{np.nan}", 556 "intercept": f"{np.nan}", 557 "r_value": f"{np.nan}", 558 "p_value": f"{np.nan}", 559 "stderr": f"{np.nan}", 560 } 561 # 近似直線の傾き、切片、相関係数を計算 562 try: 563 slope, intercept, r_value, p_value, stderr = stats.linregress( 564 ch4_data, c2h6_data 565 ) 566 ratio_row: dict[str, str | float] = { 567 "Date": start_time.strftime("%Y-%m-%d %H:%M:%S.%f"), 568 "slope": f"{slope:.6f}", 569 "intercept": f"{intercept:.6f}", 570 "r_value": f"{r_value:.6f}", 571 "p_value": f"{p_value:.6f}", 572 "stderr": f"{stderr:.6f}", 573 } 574 except Exception: 575 # 何もせず、デフォルトの ratio_row を使用する 576 pass 577 578 # 結果をリストに追加 579 ratio_data.append(ratio_row) 580 581 if output_c2c1_ratio: 582 # DataFrameを作成し、Dateカラムで昇順ソート 583 ratio_df: pd.DataFrame = pd.DataFrame(ratio_data) 584 ratio_df["Date"] = pd.to_datetime( 585 ratio_df["Date"] 586 ) # Dateカラムをdatetime型に変換 587 ratio_df = ratio_df.sort_values("Date") # Dateカラムで昇順ソート 588 589 # CSVとして保存 590 ratio_filename: str = ( 591 f"{c2c1_ratio_csv_prefix}.{latest_date.strftime('%Y.%m.%d')}.ratio.csv" 592 ) 593 ratio_path: str = os.path.join(c2c1_ratio_dir, ratio_filename) 594 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'。
interpolate : bool
欠損値補間を行うかどうか。デフォルトはTrue。
numeric_columns : list[str]
数値データを含む列名のリスト。デフォルトは指定された列名のリスト。
metadata_rows : int
メタデータとして読み込む行数。デフォルトは4。
skiprows : list[int]
読み飛ばす行のインデックスリスト。デフォルトは[0, 2, 3]。
Raises:
OSError
ディレクトリの作成に失敗した場合。
FileNotFoundError
入力ファイルが見つからない場合。
ValueError
出力ディレクトリが指定されていない、またはデータの処理中にエラーが発生した場合。
706 @staticmethod 707 def setup_logger(logger: Logger | None, log_level: int = INFO) -> Logger: 708 """ 709 ロガーを設定します。 710 711 このメソッドは、ロギングの設定を行い、ログメッセージのフォーマットを指定します。 712 ログメッセージには、日付、ログレベル、メッセージが含まれます。 713 714 渡されたロガーがNoneまたは不正な場合は、新たにロガーを作成し、標準出力に 715 ログメッセージが表示されるようにStreamHandlerを追加します。ロガーのレベルは 716 引数で指定されたlog_levelに基づいて設定されます。 717 718 Parameters: 719 ----- 720 logger : Logger | None 721 使用するロガー。Noneの場合は新しいロガーを作成します。 722 log_level : int 723 ロガーのログレベル。デフォルトはINFO。 724 725 Returns: 726 ----- 727 Logger 728 設定されたロガーオブジェクト。 729 """ 730 if logger is not None and isinstance(logger, Logger): 731 return logger 732 # 渡されたロガーがNoneまたは正しいものでない場合は独自に設定 733 new_logger: Logger = getLogger() 734 # 既存のハンドラーをすべて削除 735 for handler in new_logger.handlers[:]: 736 new_logger.removeHandler(handler) 737 new_logger.setLevel(log_level) # ロガーのレベルを設定 738 ch = StreamHandler() 739 ch_formatter = Formatter("%(asctime)s - %(levelname)s - %(message)s") 740 ch.setFormatter(ch_formatter) # フォーマッターをハンドラーに設定 741 new_logger.addHandler(ch) # StreamHandlerの追加 742 return new_logger
ロガーを設定します。
このメソッドは、ロギングの設定を行い、ログメッセージのフォーマットを指定します。 ログメッセージには、日付、ログレベル、メッセージが含まれます。
渡されたロガーがNoneまたは不正な場合は、新たにロガーを作成し、標準出力に ログメッセージが表示されるようにStreamHandlerを追加します。ロガーのレベルは 引数で指定されたlog_levelに基づいて設定されます。
Parameters:
logger : Logger | None
使用するロガー。Noneの場合は新しいロガーを作成します。
log_level : int
ロガーのログレベル。デフォルトはINFO。
Returns:
Logger
設定されたロガーオブジェクト。
7class SpectrumCalculator: 8 def __init__( 9 self, 10 df: pd.DataFrame, 11 fs: float, 12 apply_window: bool = True, 13 plots: int = 30, 14 window_type: str = "hamming", 15 ): 16 """ 17 データロガーから取得したデータファイルを用いて計算を行うクラス。 18 19 Parameters: 20 ------ 21 df : pd.DataFrame 22 pandasのデータフレーム。解析対象のデータを含む。 23 fs : float 24 サンプリング周波数(Hz)。データのサンプリングレートを指定。 25 apply_window : bool, optional 26 窓関数を適用するフラグ。デフォルトはTrue。 27 plots : int 28 プロットする点の数。可視化のためのデータポイント数。 29 window_type : str 30 窓関数の種類。デフォルトは'hamming'。 31 """ 32 self._df: pd.DataFrame = df 33 self._fs: float = fs 34 self._apply_window: bool = apply_window 35 self._plots: int = plots 36 self._window_type: str = window_type 37 38 def calculate_co_spectrum( 39 self, 40 col1: str, 41 col2: str, 42 dimensionless: bool = True, 43 frequency_weighted: bool = True, 44 interpolate_points: bool = True, 45 scaling: str = "spectrum", 46 apply_lag_correction_to_col2: bool = True, 47 lag_second: float | None = None, 48 ) -> tuple: 49 """ 50 指定されたcol1とcol2のコスペクトルをDataFrameから計算するためのメソッド。 51 52 Parameters: 53 ------ 54 col1 : str 55 データの列名1。 56 col2 : str 57 データの列名2。 58 dimensionless : bool, optional 59 Trueの場合、分散で割って無次元化を行う。デフォルトはTrue。 60 frequency_weighted : bool, optional 61 周波数の重みづけを適用するかどうか。デフォルトはTrue。 62 interpolate_points : bool, optional 63 等間隔なデータ点を生成するかどうか(対数軸上で等間隔)。デフォルトはTrue。 64 scaling : str 65 "density"でスペクトル密度、"spectrum"でスペクトル。デフォルトは"spectrum"。 66 apply_lag_correction_to_col2 : bool, optional 67 col2に遅れ時間補正を適用するかどうか。デフォルトはTrue。 68 lag_second : float | None, optional 69 col1からcol2が遅れている時間(秒)。apply_lag_correction_to_col2がTrueの場合に必要。デフォルトはNone。 70 71 Returns: 72 ------ 73 tuple 74 (freqs, co_spectrum, corr_coef) 75 - freqs : np.ndarray 76 周波数軸(対数スケールの場合は対数変換済み)。 77 - co_spectrum : np.ndarray 78 コスペクトル(対数スケールの場合は対数変換済み)。 79 - corr_coef : float 80 変数の相関係数。 81 """ 82 freqs, co_spectrum, _, corr_coef = self.calculate_cross_spectrum( 83 col1=col1, 84 col2=col2, 85 dimensionless=dimensionless, 86 frequency_weighted=frequency_weighted, 87 interpolate_points=interpolate_points, 88 scaling=scaling, 89 apply_lag_correction_to_col2=apply_lag_correction_to_col2, 90 lag_second=lag_second, 91 ) 92 return freqs, co_spectrum, corr_coef 93 94 def calculate_cross_spectrum( 95 self, 96 col1: str, 97 col2: str, 98 dimensionless: bool = True, 99 frequency_weighted: bool = True, 100 interpolate_points: bool = True, 101 scaling: str = "spectrum", 102 apply_lag_correction_to_col2: bool = True, 103 lag_second: float | None = None, 104 ) -> tuple: 105 """ 106 指定されたcol1とcol2のクロススペクトルをDataFrameから計算するためのメソッド。 107 108 Parameters: 109 ------ 110 col1 : str 111 データの列名1。 112 col2 : str 113 データの列名2。 114 dimensionless : bool, optional 115 Trueの場合、分散で割って無次元化を行う。デフォルトはTrue。 116 frequency_weighted : bool, optional 117 周波数の重みづけを適用するかどうか。デフォルトはTrue。 118 interpolate_points : bool, optional 119 等間隔なデータ点を生成するかどうか(対数軸上で等間隔)。デフォルトはTrue。 120 scaling : str 121 "density"でスペクトル密度、"spectrum"でスペクトル。デフォルトは"spectrum"。 122 apply_lag_correction_to_col2 : bool, optional 123 col2に遅れ時間補正を適用するかどうか。デフォルトはTrue。 124 lag_second : float | None, optional 125 col1からcol2が遅れている時間(秒)。apply_lag_correction_to_col2がTrueの場合に必要。デフォルトはNone。 126 127 Returns: 128 ------ 129 tuple 130 (freqs, co_spectrum, corr_coef) 131 - freqs : np.ndarray 132 周波数軸(対数スケールの場合は対数変換済み)。 133 - co_spectrum : np.ndarray 134 クロススペクトル(対数スケールの場合は対数変換済み)。 135 - corr_coef : float 136 変数の相関係数。 137 """ 138 # バリデーション 139 valid_scaling_options = ["density", "spectrum"] 140 if scaling not in valid_scaling_options: 141 raise ValueError( 142 f"'scaling'は次のパラメータから選択してください: {valid_scaling_options}" 143 ) 144 145 fs: float = self._fs 146 df: pd.DataFrame = self._df.copy() 147 # データ取得と前処理 148 data1: np.ndarray = np.array(df[col1].values) 149 data2: np.ndarray = np.array(df[col2].values) 150 151 # 遅れ時間の補正 152 if apply_lag_correction_to_col2: 153 if lag_second is None: 154 raise ValueError( 155 "apply_lag_correction_to_col2=True の場合は lag_second に有効な遅れ時間(秒)を指定してください。" 156 ) 157 data1, data2 = SpectrumCalculator._correct_lag_time( 158 data1=data1, data2=data2, fs=fs, lag_second=lag_second 159 ) 160 161 # トレンド除去 162 data1 = SpectrumCalculator._detrend(data=data1, first=True) 163 data2 = SpectrumCalculator._detrend(data=data2, first=True) 164 165 # 相関係数の計算 166 corr_coef: float = np.corrcoef(data1, data2)[0, 1] 167 168 # クロススペクトル計算 169 freqs, Pxy = signal.csd( 170 data1, 171 data2, 172 fs=self._fs, 173 window=self._window_type, 174 nperseg=1024, 175 scaling=scaling, 176 ) 177 178 # コスペクトルとクアドラチャスペクトルの抽出 179 co_spectrum = np.real(Pxy) 180 quad_spectrum = np.imag(Pxy) 181 182 # 周波数の重みづけ 183 if frequency_weighted: 184 co_spectrum[1:] *= freqs[1:] 185 quad_spectrum[1:] *= freqs[1:] 186 187 # 無次元化 188 if dimensionless: 189 cov_matrix: np.ndarray = np.cov(data1, data2) 190 covariance: float = cov_matrix[0, 1] 191 co_spectrum /= covariance 192 quad_spectrum /= covariance 193 194 if interpolate_points: 195 # 補間処理 196 log_freq_min = np.log10(0.001) 197 log_freq_max = np.log10(freqs[-1]) 198 log_freq_resampled = np.logspace(log_freq_min, log_freq_max, self._plots) 199 200 # スペクトルの補間 201 co_resampled = np.interp( 202 log_freq_resampled, freqs, co_spectrum, left=np.nan, right=np.nan 203 ) 204 quad_resampled = np.interp( 205 log_freq_resampled, freqs, quad_spectrum, left=np.nan, right=np.nan 206 ) 207 208 # NaNを除外 209 valid_mask = ~np.isnan(co_resampled) 210 freqs = log_freq_resampled[valid_mask] 211 co_spectrum = co_resampled[valid_mask] 212 quad_spectrum = quad_resampled[valid_mask] 213 214 # 0Hz成分を除外 215 nonzero_mask = freqs != 0 216 freqs = freqs[nonzero_mask] 217 co_spectrum = co_spectrum[nonzero_mask] 218 quad_spectrum = quad_spectrum[nonzero_mask] 219 220 return freqs, co_spectrum, quad_spectrum, corr_coef 221 222 def calculate_power_spectrum( 223 self, 224 col: str, 225 dimensionless: bool = True, 226 frequency_weighted: bool = True, 227 interpolate_points: bool = True, 228 scaling: str = "spectrum", 229 ) -> tuple: 230 """ 231 指定されたcolに基づいてDataFrameからパワースペクトルと周波数軸を計算します。 232 scipy.signal.welchを使用してパワースペクトルを計算します。 233 234 Parameters: 235 ------ 236 col : str 237 データの列名 238 dimensionless : bool, optional 239 Trueの場合、分散で割って無次元化を行います。デフォルトはTrueです。 240 frequency_weighted : bool, optional 241 周波数の重みづけを適用するかどうか。デフォルトはTrueです。 242 interpolate_points : bool, optional 243 等間隔なデータ点を生成するかどうか(対数軸上で等間隔)。 244 scaling : str, optional 245 "density"でスペクトル密度、"spectrum"でスペクトル。デフォルトは"spectrum"です。 246 247 Returns: 248 ------ 249 tuple 250 - freqs (np.ndarray): 周波数軸(対数スケールの場合は対数変換済み) 251 - power_spectrum (np.ndarray): パワースペクトル(対数スケールの場合は対数変換済み) 252 """ 253 # バリデーション 254 valid_scaling_options = ["density", "spectrum"] 255 if scaling not in valid_scaling_options: 256 raise ValueError( 257 f"'scaling'は次のパラメータから選択してください: {valid_scaling_options}" 258 ) 259 260 # データの取得とトレンド除去 261 data: np.ndarray = np.array(self._df[col].values) 262 data = SpectrumCalculator._detrend(data) 263 264 # welchメソッドでパワースペクトル計算 265 freqs, power_spectrum = signal.welch( 266 data, fs=self._fs, window=self._window_type, nperseg=1024, scaling=scaling 267 ) 268 269 # 周波数の重みづけ(0Hz除外の前に実施) 270 if frequency_weighted: 271 power_spectrum = freqs * power_spectrum 272 273 # 無次元化(0Hz除外の前に実施) 274 if dimensionless: 275 variance = np.var(data) 276 power_spectrum /= variance 277 278 if interpolate_points: 279 # 補間処理(0Hz除外の前に実施) 280 log_freq_min = np.log10(0.001) 281 log_freq_max = np.log10(freqs[-1]) 282 log_freq_resampled = np.logspace(log_freq_min, log_freq_max, self._plots) 283 284 power_spectrum_resampled = np.interp( 285 log_freq_resampled, freqs, power_spectrum, left=np.nan, right=np.nan 286 ) 287 288 # NaNを除外 289 valid_mask = ~np.isnan(power_spectrum_resampled) 290 freqs = log_freq_resampled[valid_mask] 291 power_spectrum = power_spectrum_resampled[valid_mask] 292 293 # 0Hz成分を最後に除外 294 nonzero_mask = freqs != 0 295 freqs = freqs[nonzero_mask] 296 power_spectrum = power_spectrum[nonzero_mask] 297 298 return freqs, power_spectrum 299 300 @staticmethod 301 def _correct_lag_time( 302 data1: np.ndarray, 303 data2: np.ndarray, 304 fs: float, 305 lag_second: float, 306 ) -> tuple: 307 """ 308 相互相関関数を用いて遅れ時間を補正する。クロススペクトルの計算に使用。 309 310 Parameters: 311 ------ 312 data1 : np.ndarray 313 基準データ 314 data2 : np.ndarray 315 遅れているデータ 316 fs : float 317 サンプリング周波数 318 lag_second : float 319 data1からdata2が遅れている時間(秒)。負の値は許可されない。 320 321 Returns: 322 ------ 323 tuple 324 - data1 : np.ndarray 325 基準データ(シフトなし) 326 - data2 : np.ndarray 327 補正された遅れているデータ 328 """ 329 if lag_second < 0: 330 raise ValueError("lag_second must be non-negative.") 331 332 # lag_secondをサンプリング周波数でスケーリングしてインデックスに変換 333 lag_index: int = int(lag_second * fs) 334 335 # データの長さを取得 336 data_length = len(data1) 337 338 # data2のみをシフト(NaNで初期化) 339 shifted_data2 = np.full(data_length, np.nan) 340 shifted_data2[:-lag_index] = data2[lag_index:] if lag_index > 0 else data2 341 342 # NaNを含まない部分のみを抽出 343 valid_mask = ~np.isnan(shifted_data2) 344 data1 = data1[valid_mask] 345 data2 = shifted_data2[valid_mask] 346 347 return data1, data2 348 349 @staticmethod 350 def _detrend( 351 data: np.ndarray, first: bool = True, second: bool = False 352 ) -> np.ndarray: 353 """ 354 データから一次トレンドおよび二次トレンドを除去します。 355 356 Parameters: 357 ------ 358 data : np.ndarray 359 入力データ 360 first : bool, optional 361 一次トレンドを除去するかどうか. デフォルトはTrue. 362 second : bool, optional 363 二次トレンドを除去するかどうか. デフォルトはFalse. 364 365 Returns: 366 ------ 367 np.ndarray 368 トレンド除去後のデータ 369 370 Raises: 371 ------ 372 ValueError 373 first と second の両方がFalseの場合 374 """ 375 if not (first or second): 376 raise ValueError("少なくとも一次または二次トレンドの除去を指定してください") 377 378 detrended_data: np.ndarray = data.copy() 379 380 # 一次トレンドの除去 381 if first: 382 detrended_data = signal.detrend(detrended_data) 383 384 # 二次トレンドの除去 385 if second: 386 # 二次トレンドを除去するために、まず一次トレンドを除去 387 detrended_data = signal.detrend(detrended_data, type="linear") 388 # 二次トレンドを除去するために、二次多項式フィッティングを行う 389 coeffs_second = np.polyfit( 390 np.arange(len(detrended_data)), detrended_data, 2 391 ) 392 trend_second = np.polyval(coeffs_second, np.arange(len(detrended_data))) 393 detrended_data = detrended_data - trend_second 394 395 return detrended_data 396 397 @staticmethod 398 def _generate_window_function(type: str, data_length: int) -> np.ndarray: 399 """ 400 指定された種類の窓関数を適用する 401 402 Parameters: 403 ------ 404 type : str 405 窓関数の種類 ('hanning', 'hamming', 'blackman') 406 data_length : int 407 データ長 408 409 Returns: 410 ------ 411 np.ndarray 412 適用された窓関数 413 414 Notes: 415 ------ 416 - 指定された種類の窓関数を適用し、numpy配列として返す 417 - 無効な種類が指定された場合、警告を表示しHann窓を適用する 418 """ 419 if type == "hanning": 420 return np.hanning(data_length) 421 elif type == "hamming": 422 return np.hamming(data_length) 423 elif type == "blackman": 424 return np.blackman(data_length) 425 else: 426 print('Warning: Invalid argument "type". Return hanning window.') 427 return np.hanning(data_length) 428 429 @staticmethod 430 def _smooth_spectrum( 431 spectrum: np.ndarray, frequencies: np.ndarray, freq_threshold: float = 0.1 432 ) -> np.ndarray: 433 """ 434 高周波数領域に対して3点移動平均を適用する処理を行う。 435 この処理により、高周波数成分のノイズを低減し、スペクトルの滑らかさを向上させる。 436 437 Parameters: 438 ------ 439 spectrum : np.ndarray 440 スペクトルデータ 441 frequencies : np.ndarray 442 対応する周波数データ 443 freq_threshold : float 444 高周波数の閾値 445 446 Returns: 447 ------ 448 np.ndarray 449 スムーズ化されたスペクトルデータ 450 """ 451 smoothed = spectrum.copy() # オリジナルデータのコピーを作成 452 453 # 周波数閾値以上の部分のインデックスを取得 454 high_freq_mask = frequencies >= freq_threshold 455 456 # 高周波数領域のみを処理 457 high_freq_indices = np.where(high_freq_mask)[0] 458 if len(high_freq_indices) > 2: # 最低3点必要 459 for i in high_freq_indices[1:-1]: # 端点を除く 460 smoothed[i] = ( 461 0.25 * spectrum[i - 1] + 0.5 * spectrum[i] + 0.25 * spectrum[i + 1] 462 ) 463 464 # 高周波領域の端点の処理 465 first_idx = high_freq_indices[0] 466 last_idx = high_freq_indices[-1] 467 smoothed[first_idx] = 0.5 * (spectrum[first_idx] + spectrum[first_idx + 1]) 468 smoothed[last_idx] = 0.5 * (spectrum[last_idx - 1] + spectrum[last_idx]) 469 470 return smoothed
8 def __init__( 9 self, 10 df: pd.DataFrame, 11 fs: float, 12 apply_window: bool = True, 13 plots: int = 30, 14 window_type: str = "hamming", 15 ): 16 """ 17 データロガーから取得したデータファイルを用いて計算を行うクラス。 18 19 Parameters: 20 ------ 21 df : pd.DataFrame 22 pandasのデータフレーム。解析対象のデータを含む。 23 fs : float 24 サンプリング周波数(Hz)。データのサンプリングレートを指定。 25 apply_window : bool, optional 26 窓関数を適用するフラグ。デフォルトはTrue。 27 plots : int 28 プロットする点の数。可視化のためのデータポイント数。 29 window_type : str 30 窓関数の種類。デフォルトは'hamming'。 31 """ 32 self._df: pd.DataFrame = df 33 self._fs: float = fs 34 self._apply_window: bool = apply_window 35 self._plots: int = plots 36 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'。
38 def calculate_co_spectrum( 39 self, 40 col1: str, 41 col2: str, 42 dimensionless: bool = True, 43 frequency_weighted: bool = True, 44 interpolate_points: bool = True, 45 scaling: str = "spectrum", 46 apply_lag_correction_to_col2: bool = True, 47 lag_second: float | None = None, 48 ) -> tuple: 49 """ 50 指定されたcol1とcol2のコスペクトルをDataFrameから計算するためのメソッド。 51 52 Parameters: 53 ------ 54 col1 : str 55 データの列名1。 56 col2 : str 57 データの列名2。 58 dimensionless : bool, optional 59 Trueの場合、分散で割って無次元化を行う。デフォルトはTrue。 60 frequency_weighted : bool, optional 61 周波数の重みづけを適用するかどうか。デフォルトはTrue。 62 interpolate_points : bool, optional 63 等間隔なデータ点を生成するかどうか(対数軸上で等間隔)。デフォルトはTrue。 64 scaling : str 65 "density"でスペクトル密度、"spectrum"でスペクトル。デフォルトは"spectrum"。 66 apply_lag_correction_to_col2 : bool, optional 67 col2に遅れ時間補正を適用するかどうか。デフォルトはTrue。 68 lag_second : float | None, optional 69 col1からcol2が遅れている時間(秒)。apply_lag_correction_to_col2がTrueの場合に必要。デフォルトはNone。 70 71 Returns: 72 ------ 73 tuple 74 (freqs, co_spectrum, corr_coef) 75 - freqs : np.ndarray 76 周波数軸(対数スケールの場合は対数変換済み)。 77 - co_spectrum : np.ndarray 78 コスペクトル(対数スケールの場合は対数変換済み)。 79 - corr_coef : float 80 変数の相関係数。 81 """ 82 freqs, co_spectrum, _, corr_coef = self.calculate_cross_spectrum( 83 col1=col1, 84 col2=col2, 85 dimensionless=dimensionless, 86 frequency_weighted=frequency_weighted, 87 interpolate_points=interpolate_points, 88 scaling=scaling, 89 apply_lag_correction_to_col2=apply_lag_correction_to_col2, 90 lag_second=lag_second, 91 ) 92 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"。
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
変数の相関係数。
94 def calculate_cross_spectrum( 95 self, 96 col1: str, 97 col2: str, 98 dimensionless: bool = True, 99 frequency_weighted: bool = True, 100 interpolate_points: bool = True, 101 scaling: str = "spectrum", 102 apply_lag_correction_to_col2: bool = True, 103 lag_second: float | None = None, 104 ) -> tuple: 105 """ 106 指定されたcol1とcol2のクロススペクトルをDataFrameから計算するためのメソッド。 107 108 Parameters: 109 ------ 110 col1 : str 111 データの列名1。 112 col2 : str 113 データの列名2。 114 dimensionless : bool, optional 115 Trueの場合、分散で割って無次元化を行う。デフォルトはTrue。 116 frequency_weighted : bool, optional 117 周波数の重みづけを適用するかどうか。デフォルトはTrue。 118 interpolate_points : bool, optional 119 等間隔なデータ点を生成するかどうか(対数軸上で等間隔)。デフォルトはTrue。 120 scaling : str 121 "density"でスペクトル密度、"spectrum"でスペクトル。デフォルトは"spectrum"。 122 apply_lag_correction_to_col2 : bool, optional 123 col2に遅れ時間補正を適用するかどうか。デフォルトはTrue。 124 lag_second : float | None, optional 125 col1からcol2が遅れている時間(秒)。apply_lag_correction_to_col2がTrueの場合に必要。デフォルトはNone。 126 127 Returns: 128 ------ 129 tuple 130 (freqs, co_spectrum, corr_coef) 131 - freqs : np.ndarray 132 周波数軸(対数スケールの場合は対数変換済み)。 133 - co_spectrum : np.ndarray 134 クロススペクトル(対数スケールの場合は対数変換済み)。 135 - corr_coef : float 136 変数の相関係数。 137 """ 138 # バリデーション 139 valid_scaling_options = ["density", "spectrum"] 140 if scaling not in valid_scaling_options: 141 raise ValueError( 142 f"'scaling'は次のパラメータから選択してください: {valid_scaling_options}" 143 ) 144 145 fs: float = self._fs 146 df: pd.DataFrame = self._df.copy() 147 # データ取得と前処理 148 data1: np.ndarray = np.array(df[col1].values) 149 data2: np.ndarray = np.array(df[col2].values) 150 151 # 遅れ時間の補正 152 if apply_lag_correction_to_col2: 153 if lag_second is None: 154 raise ValueError( 155 "apply_lag_correction_to_col2=True の場合は lag_second に有効な遅れ時間(秒)を指定してください。" 156 ) 157 data1, data2 = SpectrumCalculator._correct_lag_time( 158 data1=data1, data2=data2, fs=fs, lag_second=lag_second 159 ) 160 161 # トレンド除去 162 data1 = SpectrumCalculator._detrend(data=data1, first=True) 163 data2 = SpectrumCalculator._detrend(data=data2, first=True) 164 165 # 相関係数の計算 166 corr_coef: float = np.corrcoef(data1, data2)[0, 1] 167 168 # クロススペクトル計算 169 freqs, Pxy = signal.csd( 170 data1, 171 data2, 172 fs=self._fs, 173 window=self._window_type, 174 nperseg=1024, 175 scaling=scaling, 176 ) 177 178 # コスペクトルとクアドラチャスペクトルの抽出 179 co_spectrum = np.real(Pxy) 180 quad_spectrum = np.imag(Pxy) 181 182 # 周波数の重みづけ 183 if frequency_weighted: 184 co_spectrum[1:] *= freqs[1:] 185 quad_spectrum[1:] *= freqs[1:] 186 187 # 無次元化 188 if dimensionless: 189 cov_matrix: np.ndarray = np.cov(data1, data2) 190 covariance: float = cov_matrix[0, 1] 191 co_spectrum /= covariance 192 quad_spectrum /= covariance 193 194 if interpolate_points: 195 # 補間処理 196 log_freq_min = np.log10(0.001) 197 log_freq_max = np.log10(freqs[-1]) 198 log_freq_resampled = np.logspace(log_freq_min, log_freq_max, self._plots) 199 200 # スペクトルの補間 201 co_resampled = np.interp( 202 log_freq_resampled, freqs, co_spectrum, left=np.nan, right=np.nan 203 ) 204 quad_resampled = np.interp( 205 log_freq_resampled, freqs, quad_spectrum, left=np.nan, right=np.nan 206 ) 207 208 # NaNを除外 209 valid_mask = ~np.isnan(co_resampled) 210 freqs = log_freq_resampled[valid_mask] 211 co_spectrum = co_resampled[valid_mask] 212 quad_spectrum = quad_resampled[valid_mask] 213 214 # 0Hz成分を除外 215 nonzero_mask = freqs != 0 216 freqs = freqs[nonzero_mask] 217 co_spectrum = co_spectrum[nonzero_mask] 218 quad_spectrum = quad_spectrum[nonzero_mask] 219 220 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"。
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
変数の相関係数。
222 def calculate_power_spectrum( 223 self, 224 col: str, 225 dimensionless: bool = True, 226 frequency_weighted: bool = True, 227 interpolate_points: bool = True, 228 scaling: str = "spectrum", 229 ) -> tuple: 230 """ 231 指定されたcolに基づいてDataFrameからパワースペクトルと周波数軸を計算します。 232 scipy.signal.welchを使用してパワースペクトルを計算します。 233 234 Parameters: 235 ------ 236 col : str 237 データの列名 238 dimensionless : bool, optional 239 Trueの場合、分散で割って無次元化を行います。デフォルトはTrueです。 240 frequency_weighted : bool, optional 241 周波数の重みづけを適用するかどうか。デフォルトはTrueです。 242 interpolate_points : bool, optional 243 等間隔なデータ点を生成するかどうか(対数軸上で等間隔)。 244 scaling : str, optional 245 "density"でスペクトル密度、"spectrum"でスペクトル。デフォルトは"spectrum"です。 246 247 Returns: 248 ------ 249 tuple 250 - freqs (np.ndarray): 周波数軸(対数スケールの場合は対数変換済み) 251 - power_spectrum (np.ndarray): パワースペクトル(対数スケールの場合は対数変換済み) 252 """ 253 # バリデーション 254 valid_scaling_options = ["density", "spectrum"] 255 if scaling not in valid_scaling_options: 256 raise ValueError( 257 f"'scaling'は次のパラメータから選択してください: {valid_scaling_options}" 258 ) 259 260 # データの取得とトレンド除去 261 data: np.ndarray = np.array(self._df[col].values) 262 data = SpectrumCalculator._detrend(data) 263 264 # welchメソッドでパワースペクトル計算 265 freqs, power_spectrum = signal.welch( 266 data, fs=self._fs, window=self._window_type, nperseg=1024, scaling=scaling 267 ) 268 269 # 周波数の重みづけ(0Hz除外の前に実施) 270 if frequency_weighted: 271 power_spectrum = freqs * power_spectrum 272 273 # 無次元化(0Hz除外の前に実施) 274 if dimensionless: 275 variance = np.var(data) 276 power_spectrum /= variance 277 278 if interpolate_points: 279 # 補間処理(0Hz除外の前に実施) 280 log_freq_min = np.log10(0.001) 281 log_freq_max = np.log10(freqs[-1]) 282 log_freq_resampled = np.logspace(log_freq_min, log_freq_max, self._plots) 283 284 power_spectrum_resampled = np.interp( 285 log_freq_resampled, freqs, power_spectrum, left=np.nan, right=np.nan 286 ) 287 288 # NaNを除外 289 valid_mask = ~np.isnan(power_spectrum_resampled) 290 freqs = log_freq_resampled[valid_mask] 291 power_spectrum = power_spectrum_resampled[valid_mask] 292 293 # 0Hz成分を最後に除外 294 nonzero_mask = freqs != 0 295 freqs = freqs[nonzero_mask] 296 power_spectrum = power_spectrum[nonzero_mask] 297 298 return freqs, power_spectrum
指定されたcolに基づいてDataFrameからパワースペクトルと周波数軸を計算します。 scipy.signal.welchを使用してパワースペクトルを計算します。
Parameters:
col : str
データの列名
dimensionless : bool, optional
Trueの場合、分散で割って無次元化を行います。デフォルトはTrueです。
frequency_weighted : bool, optional
周波数の重みづけを適用するかどうか。デフォルトはTrueです。
interpolate_points : bool, optional
等間隔なデータ点を生成するかどうか(対数軸上で等間隔)。
scaling : str, optional
"density"でスペクトル密度、"spectrum"でスペクトル。デフォルトは"spectrum"です。
Returns:
tuple
- freqs (np.ndarray): 周波数軸(対数スケールの場合は対数変換済み)
- power_spectrum (np.ndarray): パワースペクトル(対数スケールの場合は対数変換済み)
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
ホットスポットの種類
19class FluxFootprintAnalyzer: 20 """ 21 フラックスフットプリントを解析および可視化するクラス。 22 23 このクラスは、フラックスデータの処理、フットプリントの計算、 24 および結果を衛星画像上に可視化するメソッドを提供します。 25 座標系と単位に関する重要な注意: 26 - すべての距離はメートル単位で計算されます 27 - 座標系の原点(0,0)は測定タワーの位置に対応します 28 - x軸は東西方向(正が東) 29 - y軸は南北方向(正が北) 30 - 風向は気象学的風向(北から時計回りに測定)を使用 31 32 この実装は、Kormann and Meixner (2001) および Takano et al. (2021)に基づいています。 33 """ 34 35 EARTH_RADIUS_METER: int = 6371000 # 地球の半径(メートル) 36 37 def __init__( 38 self, 39 z_m: float, 40 labelsize: float = 20, 41 ticksize: float = 16, 42 plot_params=None, 43 logger: Logger | None = None, 44 logging_debug: bool = False, 45 ): 46 """ 47 衛星画像を用いて FluxFootprintAnalyzer を初期化します。 48 49 Parameters: 50 ------ 51 z_m : float 52 測定の高さ(メートル単位)。 53 labelsize : float 54 軸ラベルのフォントサイズ。デフォルトは20。 55 ticksize : float 56 軸目盛りのフォントサイズ。デフォルトは16。 57 plot_params : Optional[Dict[str, any]] 58 matplotlibのプロットパラメータを指定する辞書。 59 logger : Logger | None 60 使用するロガー。Noneの場合は新しいロガーを生成します。 61 logging_debug : bool 62 ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。 63 """ 64 # 定数や共通の変数 65 self._required_columns: list[str] = [ 66 "Date", 67 "WS vector", 68 "u*", 69 "z/L", 70 "Wind direction", 71 "sigmaV", 72 ] # 必要なカラムの名前 73 self._col_weekday: str = "ffa_is_weekday" # クラスで生成するカラムのキー名 74 self._z_m: float = z_m # 測定高度 75 # 状態を管理するフラグ 76 self._got_satellite_image: bool = False 77 78 # 図表の初期設定 79 FigureUtils.setup_plot_params( 80 font_size=labelsize, tick_size=ticksize, plot_params=plot_params 81 ) 82 # ロガー 83 log_level: int = INFO 84 if logging_debug: 85 log_level = DEBUG 86 self.logger: Logger = FluxFootprintAnalyzer.setup_logger(logger, log_level) 87 88 @staticmethod 89 def setup_logger(logger: Logger | None, log_level: int = INFO) -> Logger: 90 """ 91 ロガーを設定します。 92 93 このメソッドは、ロギングの設定を行い、ログメッセージのフォーマットを指定します。 94 ログメッセージには、日付、ログレベル、メッセージが含まれます。 95 96 渡されたロガーがNoneまたは不正な場合は、新たにロガーを作成し、標準出力に 97 ログメッセージが表示されるようにStreamHandlerを追加します。ロガーのレベルは 98 引数で指定されたlog_levelに基づいて設定されます。 99 100 Parameters: 101 ------ 102 logger : Logger | None 103 使用するロガー。Noneの場合は新しいロガーを作成します。 104 log_level : int 105 ロガーのログレベル。デフォルトはINFO。 106 107 Returns: 108 ------ 109 Logger 110 設定されたロガーオブジェクト。 111 """ 112 if logger is not None and isinstance(logger, Logger): 113 return logger 114 # 渡されたロガーがNoneまたは正しいものでない場合は独自に設定 115 new_logger: Logger = getLogger() 116 # 既存のハンドラーをすべて削除 117 for handler in new_logger.handlers[:]: 118 new_logger.removeHandler(handler) 119 new_logger.setLevel(log_level) # ロガーのレベルを設定 120 ch = StreamHandler() 121 ch_formatter = Formatter("%(asctime)s - %(levelname)s - %(message)s") 122 ch.setFormatter(ch_formatter) # フォーマッターをハンドラーに設定 123 new_logger.addHandler(ch) # StreamHandlerの追加 124 return new_logger 125 126 def calculate_flux_footprint( 127 self, 128 df: pd.DataFrame, 129 col_flux: str, 130 plot_count: int = 10000, 131 start_time: str = "10:00", 132 end_time: str = "16:00", 133 ) -> tuple[list[float], list[float], list[float]]: 134 """ 135 フラックスフットプリントを計算し、指定された時間帯のデータを基に可視化します。 136 137 Parameters: 138 ------ 139 df : pd.DataFrame 140 分析対象のデータフレーム。フラックスデータを含む。 141 col_flux : str 142 フラックスデータの列名。計算に使用される。 143 plot_count : int, optional 144 生成するプロットの数。デフォルトは10000。 145 start_time : str, optional 146 フットプリント計算に使用する開始時間。デフォルトは"10:00"。 147 end_time : str, optional 148 フットプリント計算に使用する終了時間。デフォルトは"16:00"。 149 150 Returns: 151 ------ 152 tuple[list[float], list[float], list[float]]: 153 x座標 (メートル): タワーを原点とした東西方向の距離 154 y座標 (メートル): タワーを原点とした南北方向の距離 155 対象スカラー量の値: 各地点でのフラックス値 156 157 Notes: 158 ------ 159 - 返却される座標は測定タワーを原点(0,0)とした相対位置です 160 - すべての距離はメートル単位で表されます 161 - 正のx値は東方向、正のy値は北方向を示します 162 """ 163 df: pd.DataFrame = df.copy() 164 165 # インデックスがdatetimeであることを確認し、必要に応じて変換 166 if not isinstance(df.index, pd.DatetimeIndex): 167 df.index = pd.to_datetime(df.index) 168 169 # DatetimeIndexから直接dateプロパティにアクセス 170 datelist: np.ndarray = np.array(df.index.date) 171 172 # 各日付が平日かどうかを判定し、リストに格納 173 numbers: list[int] = [ 174 FluxFootprintAnalyzer.is_weekday(date) for date in datelist 175 ] 176 177 # col_weekdayに基づいてデータフレームに平日情報を追加 178 df.loc[:, self._col_weekday] = numbers # .locを使用して値を設定 179 180 # 値が1のもの(平日)をコピーする 181 data_weekday: pd.DataFrame = df[df[self._col_weekday] == 1].copy() 182 # 特定の時間帯を抽出 183 data_weekday = data_weekday.between_time( 184 start_time, end_time 185 ) # 引数を使用して時間帯を抽出 186 data_weekday = data_weekday.dropna(subset=[col_flux]) 187 188 directions: list[float] = [ 189 wind_direction if wind_direction >= 0 else wind_direction + 360 190 for wind_direction in data_weekday["Wind direction"] 191 ] 192 193 data_weekday.loc[:, "Wind direction_360"] = directions 194 data_weekday.loc[:, "radian"] = data_weekday["Wind direction_360"] / 180 * np.pi 195 196 # 風向が欠測なら除去 197 data_weekday = data_weekday.dropna(subset=["Wind direction", col_flux]) 198 199 # 数値型への変換を確実に行う 200 numeric_columns: list[str] = ["u*", "WS vector", "sigmaV", "z/L"] 201 for col in numeric_columns: 202 data_weekday[col] = pd.to_numeric(data_weekday[col], errors="coerce") 203 204 # 地面修正量dの計算 205 z_m: float = self._z_m 206 Z_d: float = FluxFootprintAnalyzer._calculate_ground_correction( 207 z_m=z_m, 208 wind_speed=data_weekday["WS vector"].values, 209 friction_velocity=data_weekday["u*"].values, 210 stability_parameter=data_weekday["z/L"].values, 211 ) 212 213 x_list: list[float] = [] 214 y_list: list[float] = [] 215 c_list: list[float] | None = [] 216 217 # tqdmを使用してプログレスバーを表示 218 for i in tqdm(range(len(data_weekday)), desc="Calculating footprint"): 219 dUstar: float = data_weekday["u*"].iloc[i] 220 dU: float = data_weekday["WS vector"].iloc[i] 221 sigmaV: float = data_weekday["sigmaV"].iloc[i] 222 dzL: float = data_weekday["z/L"].iloc[i] 223 224 if pd.isna(dUstar) or pd.isna(dU) or pd.isna(sigmaV) or pd.isna(dzL): 225 self.logger.warning(f"#N/A fields are exist.: i = {i}") 226 continue 227 elif dUstar < 5.0 and dUstar != 0.0 and dU > 0.1: 228 phi_m, phi_c, n = FluxFootprintAnalyzer._calculate_stability_parameters( 229 dzL=dzL 230 ) 231 m, U, r, mu, ksi = ( 232 FluxFootprintAnalyzer._calculate_footprint_parameters( 233 dUstar=dUstar, dU=dU, Z_d=Z_d, phi_m=phi_m, phi_c=phi_c, n=n 234 ) 235 ) 236 237 # 80%ソースエリアの計算 238 x80: float = FluxFootprintAnalyzer._source_area_KM2001( 239 ksi=ksi, mu=mu, dU=dU, sigmaV=sigmaV, Z_d=Z_d, max_ratio=0.8 240 ) 241 242 if not np.isnan(x80): 243 x1, y1, flux1 = FluxFootprintAnalyzer._prepare_plot_data( 244 x80, 245 ksi, 246 mu, 247 r, 248 U, 249 m, 250 sigmaV, 251 data_weekday[col_flux].iloc[i], 252 plot_count=plot_count, 253 ) 254 x1_, y1_ = FluxFootprintAnalyzer._rotate_coordinates( 255 x=x1, y=y1, radian=data_weekday["radian"].iloc[i] 256 ) 257 258 x_list.extend(x1_) 259 y_list.extend(y1_) 260 c_list.extend(flux1) 261 262 return ( 263 x_list, 264 y_list, 265 c_list, 266 ) 267 268 def combine_all_data( 269 self, data_source: str | pd.DataFrame, source_type: str = "csv", **kwargs 270 ) -> pd.DataFrame: 271 """ 272 CSVファイルまたはMonthlyConverterからのデータを統合します 273 274 Parameters: 275 ------ 276 data_source : str | pd.DataFrame 277 CSVディレクトリパスまたはDataFrame 278 source_type : str 279 "csv" または "monthly" 280 **kwargs : 281 追加パラメータ 282 - sheet_names : list[str] 283 Monthlyの場合のシート名 284 - start_date : str 285 開始日 286 - end_date : str 287 終了日 288 289 Returns: 290 ------ 291 pd.DataFrame 292 処理済みのデータフレーム 293 """ 294 if source_type == "csv": 295 # 既存のCSV処理ロジック 296 return self._combine_all_csv(data_source) 297 elif source_type == "monthly": 298 # MonthlyConverterからのデータを処理 299 if not isinstance(data_source, pd.DataFrame): 300 raise ValueError("monthly形式の場合、DataFrameを直接渡す必要があります") 301 302 df = data_source.copy() 303 304 # required_columnsからDateを除外して欠損値チェックを行う 305 check_columns = [col for col in self._required_columns if col != "Date"] 306 307 # インデックスがdatetimeであることを確認 308 if not isinstance(df.index, pd.DatetimeIndex) and "Date" not in df.columns: 309 raise ValueError("DatetimeIndexまたはDateカラムが必要です") 310 311 if "Date" in df.columns: 312 df.set_index("Date", inplace=True) 313 314 # 必要なカラムの存在確認 315 missing_columns = [ 316 col for col in check_columns if col not in df.columns.tolist() 317 ] 318 if missing_columns: 319 missing_cols = "','".join(missing_columns) 320 current_cols = "','".join(df.columns.tolist()) 321 raise ValueError( 322 f"必要なカラムが不足しています: '{missing_cols}'\n" 323 f"現在のカラム: '{current_cols}'" 324 ) 325 326 # 平日/休日の判定用カラムを追加 327 df[self._col_weekday] = df.index.map(FluxFootprintAnalyzer.is_weekday) 328 329 # Dateを除外したカラムで欠損値の処理 330 df = df.dropna(subset=check_columns) 331 332 # インデックスの重複を除去 333 df = df.loc[~df.index.duplicated(), :] 334 335 return df 336 else: 337 raise ValueError("source_typeは'csv'または'monthly'である必要があります") 338 339 def get_satellite_image_from_api( 340 self, 341 api_key: str, 342 center_lat: float, 343 center_lon: float, 344 output_path: str, 345 scale: int = 1, 346 size: tuple[int, int] = (2160, 2160), 347 zoom: int = 13, 348 ) -> ImageFile: 349 """ 350 Google Maps Static APIを使用して衛星画像を取得します。 351 352 Parameters: 353 ------ 354 api_key : str 355 Google Maps Static APIのキー。 356 center_lat : float 357 中心の緯度。 358 center_lon : float 359 中心の経度。 360 output_path : str 361 画像の保存先パス。拡張子は'.png'のみ許可される。 362 scale : int, optional 363 画像の解像度スケール(1か2)。デフォルトは1。 364 size : tuple[int, int], optional 365 画像サイズ (幅, 高さ)。デフォルトは(2160, 2160)。 366 zoom : int, optional 367 ズームレベル(0-21)。デフォルトは13。 368 369 Returns: 370 ------ 371 ImageFile 372 取得した衛星画像 373 374 Raises: 375 ------ 376 requests.RequestException 377 API呼び出しに失敗した場合 378 """ 379 # バリデーション 380 if not output_path.endswith(".png"): 381 raise ValueError("出力ファイル名は'.png'で終わる必要があります。") 382 383 # HTTPリクエストの定義 384 base_url = "https://maps.googleapis.com/maps/api/staticmap" 385 params = { 386 "center": f"{center_lat},{center_lon}", 387 "zoom": zoom, 388 "size": f"{size[0]}x{size[1]}", 389 "maptype": "satellite", 390 "scale": scale, 391 "key": api_key, 392 } 393 394 try: 395 response = requests.get(base_url, params=params) 396 response.raise_for_status() 397 # 画像ファイルに変換 398 image = Image.open(io.BytesIO(response.content)) 399 image.save(output_path) 400 self._got_satellite_image = True 401 self.logger.info(f"リモート画像を取得し、保存しました: {output_path}") 402 return image 403 except requests.RequestException as e: 404 self.logger.error(f"衛星画像の取得に失敗しました: {str(e)}") 405 raise 406 407 def get_satellite_image_from_local( 408 self, 409 local_image_path: str, 410 alpha: float = 1.0, 411 ) -> ImageFile: 412 """ 413 ローカルファイルから衛星画像を読み込みます。 414 415 Parameters: 416 ------ 417 local_image_path : str 418 ローカル画像のパス 419 alpha : float, optional 420 画像の透過率(0.0~1.0)。デフォルトは1.0。 421 422 Returns: 423 ------ 424 ImageFile 425 読み込んだ衛星画像(透過設定済み) 426 427 Raises: 428 ------ 429 FileNotFoundError 430 指定されたパスにファイルが存在しない場合 431 """ 432 if not os.path.exists(local_image_path): 433 raise FileNotFoundError( 434 f"指定されたローカル画像が存在しません: {local_image_path}" 435 ) 436 # 画像を読み込む 437 image = Image.open(local_image_path) 438 439 # RGBAモードに変換して透過率を設定 440 if image.mode != "RGBA": 441 image = image.convert("RGBA") 442 443 # 透過率を設定 444 data = image.getdata() 445 new_data = [(r, g, b, int(255 * alpha)) for r, g, b, a in data] 446 image.putdata(new_data) 447 448 self._got_satellite_image = True 449 self.logger.info( 450 f"ローカル画像を使用しました(透過率: {alpha}): {local_image_path}" 451 ) 452 return image 453 454 def plot_flux_footprint( 455 self, 456 x_list: list[float], 457 y_list: list[float], 458 c_list: list[float] | None, 459 center_lat: float, 460 center_lon: float, 461 vmin: float, 462 vmax: float, 463 add_cbar: bool = True, 464 add_legend: bool = True, 465 cbar_label: str | None = None, 466 cbar_labelpad: int = 20, 467 cmap: str = "jet", 468 reduce_c_function: callable = np.mean, 469 lat_correction: float = 1, 470 lon_correction: float = 1, 471 output_dir: str | None = None, 472 output_filename: str = "footprint.png", 473 save_fig: bool = True, 474 show_fig: bool = True, 475 satellite_image: ImageFile | None = None, 476 xy_max: float = 5000, 477 ) -> None: 478 """ 479 フットプリントデータをプロットします。 480 481 このメソッドは、指定されたフットプリントデータのみを可視化します。 482 483 Parameters: 484 ------ 485 x_list : list[float] 486 フットプリントのx座標リスト(メートル単位)。 487 y_list : list[float] 488 フットプリントのy座標リスト(メートル単位)。 489 c_list : list[float] | None 490 フットプリントの強度を示す値のリスト。 491 center_lat : float 492 プロットの中心となる緯度。 493 center_lon : float 494 プロットの中心となる経度。 495 cmap : str 496 使用するカラーマップの名前。 497 vmin : float 498 カラーバーの最小値。 499 vmax : float 500 カラーバーの最大値。 501 reduce_c_function : callable, optional 502 フットプリントの集約関数(デフォルトはnp.mean)。 503 cbar_label : str | None, optional 504 カラーバーのラベル。 505 cbar_labelpad : int, optional 506 カラーバーラベルのパディング。 507 lon_correction : float, optional 508 経度方向の補正係数(デフォルトは1)。 509 lat_correction : float, optional 510 緯度方向の補正係数(デフォルトは1)。 511 output_dir : str | None, optional 512 プロット画像の保存先パス。 513 output_filename : str 514 プロット画像の保存ファイル名(拡張子を含む)。デフォルトは'footprint.png'。 515 save_fig : bool 516 図の保存を許可するフラグ。デフォルトはTrue。 517 show_fig : bool 518 図の表示を許可するフラグ。デフォルトはTrue。 519 satellite_image : ImageFile | None, optional 520 使用する衛星画像。指定がない場合はデフォルトの画像が生成されます。 521 xy_max : float, optional 522 表示範囲の最大値(デフォルトは4000)。 523 """ 524 self.plot_flux_footprint_with_hotspots( 525 x_list=x_list, 526 y_list=y_list, 527 c_list=c_list, 528 center_lat=center_lat, 529 center_lon=center_lon, 530 vmin=vmin, 531 vmax=vmax, 532 add_cbar=add_cbar, 533 add_legend=add_legend, 534 cbar_label=cbar_label, 535 cbar_labelpad=cbar_labelpad, 536 cmap=cmap, 537 reduce_c_function=reduce_c_function, 538 hotspots=None, # hotspotsをNoneに設定 539 hotspot_colors=None, 540 lat_correction=lat_correction, 541 lon_correction=lon_correction, 542 output_dir=output_dir, 543 output_filename=output_filename, 544 save_fig=save_fig, 545 show_fig=show_fig, 546 satellite_image=satellite_image, 547 xy_max=xy_max, 548 ) 549 550 def plot_flux_footprint_with_hotspots( 551 self, 552 x_list: list[float], 553 y_list: list[float], 554 c_list: list[float] | None, 555 center_lat: float, 556 center_lon: float, 557 vmin: float, 558 vmax: float, 559 add_cbar: bool = True, 560 add_legend: bool = True, 561 cbar_label: str | None = None, 562 cbar_labelpad: int = 20, 563 cmap: str = "jet", 564 reduce_c_function: callable = np.mean, 565 hotspots: list[HotspotData] | None = None, 566 hotspot_colors: dict[HotspotType, str] | None = None, 567 hotspot_labels: dict[HotspotType, str] | None = None, 568 hotspot_markers: dict[HotspotType, str] | None = None, 569 lat_correction: float = 1, 570 lon_correction: float = 1, 571 output_dir: str | None = None, 572 output_filename: str = "footprint.png", 573 save_fig: bool = True, 574 show_fig: bool = True, 575 satellite_image: ImageFile | None = None, 576 xy_max: float = 5000, 577 ) -> None: 578 """ 579 Staticな衛星画像上にフットプリントデータとホットスポットをプロットします。 580 581 このメソッドは、指定されたフットプリントデータとホットスポットを可視化します。 582 ホットスポットが指定されない場合は、フットプリントのみ作図します。 583 584 Parameters: 585 ------ 586 x_list : list[float] 587 フットプリントのx座標リスト(メートル単位)。 588 y_list : list[float] 589 フットプリントのy座標リスト(メートル単位)。 590 c_list : list[float] | None 591 フットプリントの強度を示す値のリスト。 592 center_lat : float 593 プロットの中心となる緯度。 594 center_lon : float 595 プロットの中心となる経度。 596 vmin : float 597 カラーバーの最小値。 598 vmax : float 599 カラーバーの最大値。 600 add_cbar : bool, optional 601 カラーバーを追加するかどうか(デフォルトはTrue)。 602 add_legend : bool, optional 603 凡例を追加するかどうか(デフォルトはTrue)。 604 cbar_label : str | None, optional 605 カラーバーのラベル。 606 cbar_labelpad : int, optional 607 カラーバーラベルのパディング。 608 cmap : str 609 使用するカラーマップの名前。 610 reduce_c_function : callable 611 フットプリントの集約関数(デフォルトはnp.mean)。 612 hotspots : list[HotspotData] | None, optional 613 ホットスポットデータのリスト。デフォルトはNone。 614 hotspot_colors : dict[HotspotType, str] | None, optional 615 ホットスポットの色を指定する辞書。 616 hotspot_labels : dict[HotspotType, str] | None, optional 617 ホットスポットの表示ラベルを指定する辞書。 618 例: {'bio': '生物', 'gas': 'ガス', 'comb': '燃焼'} 619 hotspot_markers : dict[HotspotType, str] | None, optional 620 ホットスポットの形状を指定する辞書。 621 指定の例は {'bio': '^', 'gas': 'o', 'comb': 's'} (三角、丸、四角)など。 622 lat_correction : float, optional 623 緯度方向の補正係数(デフォルトは1)。 624 lon_correction : float, optional 625 経度方向の補正係数(デフォルトは1)。 626 output_dir : str | None, optional 627 プロット画像の保存先パス。 628 output_filename : str 629 プロット画像の保存ファイル名(拡張子を含む)。デフォルトは'footprint.png'。 630 save_fig : bool 631 図の保存を許可するフラグ。デフォルトはTrue。 632 show_fig : bool 633 図の表示を許可するフラグ。デフォルトはTrue。 634 satellite_image : ImageFile | None, optional 635 使用する衛星画像。指定がない場合はデフォルトの画像が生成されます。 636 xy_max : float, optional 637 表示範囲の最大値(デフォルトは5000)。 638 """ 639 # 1. 引数のバリデーション 640 valid_extensions: list[str] = [".png", ".jpg", ".jpeg", ".pdf", ".svg"] 641 _, file_extension = os.path.splitext(output_filename) 642 if file_extension.lower() not in valid_extensions: 643 quoted_extensions: list[str] = [f'"{ext}"' for ext in valid_extensions] 644 self.logger.error( 645 f"`output_filename`は有効な拡張子ではありません。プロットを保存するには、次のいずれかを指定してください: {','.join(quoted_extensions)}" 646 ) 647 return 648 649 # 2. フラグチェック 650 if not self._got_satellite_image: 651 raise ValueError( 652 "`get_satellite_image_from_api`または`get_satellite_image_from_local`が実行されていません。" 653 ) 654 655 # 3. 衛星画像の取得 656 if satellite_image is None: 657 satellite_image = Image.new("RGB", (2160, 2160), "lightgray") 658 659 self.logger.info("プロットを作成中...") 660 661 # 4. 座標変換のための定数計算(1回だけ) 662 meters_per_lat: float = self.EARTH_RADIUS_METER * ( 663 math.pi / 180 664 ) # 緯度1度あたりのメートル 665 meters_per_lon: float = meters_per_lat * math.cos( 666 math.radians(center_lat) 667 ) # 経度1度あたりのメートル 668 669 # 5. フットプリントデータの座標変換(まとめて1回で実行) 670 x_deg = ( 671 np.array(x_list) / meters_per_lon * lon_correction 672 ) # 補正係数も同時に適用 673 y_deg = ( 674 np.array(y_list) / meters_per_lat * lat_correction 675 ) # 補正係数も同時に適用 676 677 # 6. 中心点からの相対座標を実際の緯度経度に変換 678 lons = center_lon + x_deg 679 lats = center_lat + y_deg 680 681 # 7. 表示範囲の計算(変更なし) 682 x_range: float = xy_max / meters_per_lon 683 y_range: float = xy_max / meters_per_lat 684 map_boundaries: tuple[float, float, float, float] = ( 685 center_lon - x_range, # left_lon 686 center_lon + x_range, # right_lon 687 center_lat - y_range, # bottom_lat 688 center_lat + y_range, # top_lat 689 ) 690 left_lon, right_lon, bottom_lat, top_lat = map_boundaries 691 692 # 8. プロットの作成 693 plt.rcParams["axes.edgecolor"] = "None" 694 fig: plt.Figure = plt.figure(figsize=(10, 8), dpi=300) 695 ax_data: plt.Axes = fig.add_axes([0.05, 0.1, 0.8, 0.8]) 696 697 # 9. フットプリントの描画 698 # フットプリントの描画とカラーバー用の2つのhexbinを作成 699 if c_list is not None: 700 ax_data.hexbin( 701 lons, 702 lats, 703 C=c_list, 704 cmap=cmap, 705 vmin=vmin, 706 vmax=vmax, 707 alpha=0.3, # 実際のプロット用 708 gridsize=100, 709 linewidths=0, 710 mincnt=100, 711 extent=[left_lon, right_lon, bottom_lat, top_lat], 712 reduce_C_function=reduce_c_function, 713 ) 714 715 # カラーバー用の非表示hexbin(alpha=1.0) 716 hidden_hexbin = ax_data.hexbin( 717 lons, 718 lats, 719 C=c_list, 720 cmap=cmap, 721 vmin=vmin, 722 vmax=vmax, 723 alpha=1.0, # カラーバー用 724 gridsize=100, 725 linewidths=0, 726 mincnt=100, 727 extent=[left_lon, right_lon, bottom_lat, top_lat], 728 reduce_C_function=reduce_c_function, 729 visible=False, # プロットには表示しない 730 ) 731 732 # 10. ホットスポットの描画 733 spot_handles = [] 734 if hotspots is not None: 735 default_colors: dict[HotspotType, str] = { 736 "bio": "blue", 737 "gas": "red", 738 "comb": "green", 739 } 740 741 # デフォルトのマーカー形状を定義 742 default_markers: dict[HotspotType, str] = { 743 "bio": "^", # 三角 744 "gas": "o", # 丸 745 "comb": "s", # 四角 746 } 747 748 # デフォルトのラベルを定義 749 default_labels: dict[HotspotType, str] = { 750 "bio": "bio", 751 "gas": "gas", 752 "comb": "comb", 753 } 754 755 # 座標変換のための定数 756 meters_per_lat: float = self.EARTH_RADIUS_METER * (math.pi / 180) 757 meters_per_lon: float = meters_per_lat * math.cos(math.radians(center_lat)) 758 759 for spot_type, color in (hotspot_colors or default_colors).items(): 760 spots_lon = [] 761 spots_lat = [] 762 763 # 使用するマーカーを決定 764 marker = (hotspot_markers or default_markers).get(spot_type, "o") 765 766 for spot in hotspots: 767 if spot.type == spot_type: 768 # 変換前の緯度経度をログ出力 769 self.logger.debug( 770 f"Before - Type: {spot_type}, Lat: {spot.avg_lat:.6f}, Lon: {spot.avg_lon:.6f}" 771 ) 772 773 # 中心からの相対距離を計算 774 dx: float = (spot.avg_lon - center_lon) * meters_per_lon 775 dy: float = (spot.avg_lat - center_lat) * meters_per_lat 776 777 # 補正前の相対座標をログ出力 778 self.logger.debug( 779 f"Relative - Type: {spot_type}, X: {dx:.2f}m, Y: {dy:.2f}m" 780 ) 781 782 # 補正を適用 783 corrected_dx: float = dx * lon_correction 784 corrected_dy: float = dy * lat_correction 785 786 # 補正後の緯度経度を計算 787 adjusted_lon: float = center_lon + corrected_dx / meters_per_lon 788 adjusted_lat: float = center_lat + corrected_dy / meters_per_lat 789 790 # 変換後の緯度経度をログ出力 791 self.logger.debug( 792 f"After - Type: {spot_type}, Lat: {adjusted_lat:.6f}, Lon: {adjusted_lon:.6f}\n" 793 ) 794 795 if ( 796 left_lon <= adjusted_lon <= right_lon 797 and bottom_lat <= adjusted_lat <= top_lat 798 ): 799 spots_lon.append(adjusted_lon) 800 spots_lat.append(adjusted_lat) 801 802 if spots_lon: 803 # 使用するラベルを決定 804 label = (hotspot_labels or default_labels).get(spot_type, spot_type) 805 806 handle = ax_data.scatter( 807 spots_lon, 808 spots_lat, 809 c=color, 810 marker=marker, # マーカー形状を指定 811 s=100, 812 alpha=0.7, 813 label=label, 814 edgecolor="black", 815 linewidth=1, 816 ) 817 spot_handles.append(handle) 818 819 # 11. 背景画像の設定 820 ax_img = ax_data.twiny().twinx() 821 ax_img.imshow( 822 satellite_image, 823 extent=[left_lon, right_lon, bottom_lat, top_lat], 824 aspect="equal", 825 ) 826 827 # 12. 軸の設定 828 for ax in [ax_data, ax_img]: 829 ax.set_xlim(left_lon, right_lon) 830 ax.set_ylim(bottom_lat, top_lat) 831 ax.set_xticks([]) 832 ax.set_yticks([]) 833 834 ax_data.set_zorder(2) 835 ax_data.patch.set_alpha(0) 836 ax_img.set_zorder(1) 837 838 # 13. カラーバーの追加 839 if add_cbar: 840 cbar_ax: plt.Axes = fig.add_axes([0.88, 0.1, 0.03, 0.8]) 841 cbar = fig.colorbar(hidden_hexbin, cax=cbar_ax) # hidden_hexbinを使用 842 # cbar_labelが指定されている場合のみラベルを設定 843 if cbar_label: 844 cbar.set_label(cbar_label, rotation=270, labelpad=cbar_labelpad) 845 846 # 14. ホットスポットの凡例追加 847 if add_legend and hotspots and spot_handles: 848 ax_data.legend( 849 handles=spot_handles, 850 loc="upper center", # 位置を上部中央に 851 bbox_to_anchor=(0.55, -0.01), # 図の下に配置 852 ncol=len(spot_handles), # ハンドルの数に応じて列数を設定 853 ) 854 855 # 15. 画像の保存 856 if save_fig: 857 if output_dir is None: 858 raise ValueError( 859 "save_fig=Trueの場合、output_dirを指定する必要があります。有効なディレクトリパスを指定してください。" 860 ) 861 output_path: str = os.path.join(output_dir, output_filename) 862 self.logger.info("プロットを保存中...") 863 try: 864 fig.savefig(output_path, bbox_inches="tight") 865 self.logger.info(f"プロットが正常に保存されました: {output_path}") 866 except Exception as e: 867 self.logger.error(f"プロットの保存中にエラーが発生しました: {str(e)}") 868 # 16. 画像の表示 869 if show_fig: 870 plt.show() 871 else: 872 plt.close(fig=fig) 873 874 def plot_flux_footprint_with_scale_checker( 875 self, 876 x_list: list[float], 877 y_list: list[float], 878 c_list: list[float] | None, 879 center_lat: float, 880 center_lon: float, 881 check_points: list[tuple[float, float, str]] | None = None, 882 vmin: float = 0, 883 vmax: float = 100, 884 add_cbar: bool = True, 885 cbar_label: str | None = None, 886 cbar_labelpad: int = 20, 887 cmap: str = "jet", 888 reduce_c_function: callable = np.mean, 889 lat_correction: float = 1, 890 lon_correction: float = 1, 891 output_dir: str | None = None, 892 output_filename: str = "footprint-scale_checker.png", 893 save_fig: bool = True, 894 show_fig: bool = True, 895 satellite_image: ImageFile | None = None, 896 xy_max: float = 5000, 897 ) -> None: 898 """ 899 Staticな衛星画像上にフットプリントデータとホットスポットをプロットします。 900 901 このメソッドは、指定されたフットプリントデータとホットスポットを可視化します。 902 ホットスポットが指定されない場合は、フットプリントのみ作図します。 903 904 Parameters: 905 ------ 906 x_list : list[float] 907 フットプリントのx座標リスト(メートル単位)。 908 y_list : list[float] 909 フットプリントのy座標リスト(メートル単位)。 910 c_list : list[float] | None 911 フットプリントの強度を示す値のリスト。 912 center_lat : float 913 プロットの中心となる緯度。 914 center_lon : float 915 プロットの中心となる経度。 916 check_points : list[tuple[float, float, str]] | None 917 確認用の地点リスト。各要素は (緯度, 経度, ラベル) のタプル。 918 Noneの場合は中心から500m、1000m、2000m、3000mの位置に仮想的な点を配置。 919 cmap : str 920 使用するカラーマップの名前。 921 vmin : float 922 カラーバーの最小値。 923 vmax : float 924 カラーバーの最大値。 925 reduce_c_function : callable, optional 926 フットプリントの集約関数(デフォルトはnp.mean)。 927 cbar_label : str, optional 928 カラーバーのラベル。 929 cbar_labelpad : int, optional 930 カラーバーラベルのパディング。 931 hotspots : list[HotspotData] | None 932 ホットスポットデータのリスト。デフォルトはNone。 933 hotspot_colors : dict[str, str] | None, optional 934 ホットスポットの色を指定する辞書。 935 lon_correction : float, optional 936 経度方向の補正係数(デフォルトは1)。 937 lat_correction : float, optional 938 緯度方向の補正係数(デフォルトは1)。 939 output_dir : str | None, optional 940 プロット画像の保存先パス。 941 output_filename : str 942 プロット画像の保存ファイル名(拡張子を含む)。デフォルトは'footprint.png'。 943 save_fig : bool 944 図の保存を許可するフラグ。デフォルトはTrue。 945 show_fig : bool 946 図の表示を許可するフラグ。デフォルトはTrue。 947 satellite_image : ImageFile | None, optional 948 使用する衛星画像。指定がない場合はデフォルトの画像が生成されます。 949 xy_max : float, optional 950 表示範囲の最大値(デフォルトは5000)。 951 """ 952 if check_points is None: 953 # デフォルトの確認ポイントを生成(従来の方式) 954 default_points = [ 955 (500, "North", 90), # 北 500m 956 (1000, "East", 0), # 東 1000m 957 (2000, "South", 270), # 南 2000m 958 (3000, "West", 180), # 西 3000m 959 ] 960 961 dummy_hotspots = [] 962 for distance, direction, angle in default_points: 963 rad = math.radians(angle) 964 meters_per_lat = self.EARTH_RADIUS_METER * (math.pi / 180) 965 meters_per_lon = meters_per_lat * math.cos(math.radians(center_lat)) 966 967 dx = distance * math.cos(rad) 968 dy = distance * math.sin(rad) 969 970 delta_lon = dx / meters_per_lon 971 delta_lat = dy / meters_per_lat 972 973 hotspot = HotspotData( 974 avg_lat=center_lat + delta_lat, 975 avg_lon=center_lon + delta_lon, 976 delta_ch4=0.0, 977 delta_c2h6=0.0, 978 ratio=0.0, 979 type=f"{direction}_{distance}m", 980 section=0, 981 source="scale_check", 982 angle=0, 983 correlation=0, 984 ) 985 dummy_hotspots.append(hotspot) 986 else: 987 # 指定された緯度経度を使用 988 dummy_hotspots = [] 989 for lat, lon, label in check_points: 990 hotspot = HotspotData( 991 avg_lat=lat, 992 avg_lon=lon, 993 delta_ch4=0.0, 994 delta_c2h6=0.0, 995 ratio=0.0, 996 type=label, 997 section=0, 998 source="scale_check", 999 angle=0, 1000 correlation=0, 1001 ) 1002 dummy_hotspots.append(hotspot) 1003 1004 # カスタムカラーマップの作成 1005 hotspot_colors = { 1006 spot.type: plt.cm.tab10(i % 10) for i, spot in enumerate(dummy_hotspots) 1007 } 1008 1009 # 既存のメソッドを呼び出してプロット 1010 self.plot_flux_footprint_with_hotspots( 1011 x_list=x_list, 1012 y_list=y_list, 1013 c_list=c_list, 1014 center_lat=center_lat, 1015 center_lon=center_lon, 1016 vmin=vmin, 1017 vmax=vmax, 1018 add_cbar=add_cbar, 1019 add_legend=True, 1020 cbar_label=cbar_label, 1021 cbar_labelpad=cbar_labelpad, 1022 cmap=cmap, 1023 reduce_c_function=reduce_c_function, 1024 hotspots=dummy_hotspots, 1025 hotspot_colors=hotspot_colors, 1026 lat_correction=lat_correction, 1027 lon_correction=lon_correction, 1028 output_dir=output_dir, 1029 output_filename=output_filename, 1030 save_fig=save_fig, 1031 show_fig=show_fig, 1032 satellite_image=satellite_image, 1033 xy_max=xy_max, 1034 ) 1035 1036 def _combine_all_csv(self, csv_dir_path: str, suffix: str = ".csv") -> pd.DataFrame: 1037 """ 1038 指定されたディレクトリ内の全CSVファイルを読み込み、処理し、結合します。 1039 Monthlyシートを結合することを想定しています。 1040 1041 Parameters: 1042 ------ 1043 csv_dir_path : str 1044 CSVファイルが格納されているディレクトリのパス。 1045 suffix : str, optional 1046 読み込むファイルの拡張子。デフォルトは".csv"。 1047 1048 Returns: 1049 ------ 1050 pandas.DataFrame 1051 結合および処理済みのデータフレーム。 1052 1053 Notes: 1054 ------ 1055 - ディレクトリ内に少なくとも1つのCSVファイルが必要です。 1056 """ 1057 csv_files = [f for f in os.listdir(csv_dir_path) if f.endswith(suffix)] 1058 if not csv_files: 1059 raise ValueError("指定されたディレクトリにCSVファイルが見つかりません。") 1060 1061 df_array: list[pd.DataFrame] = [] 1062 for csv_file in csv_files: 1063 file_path: str = os.path.join(csv_dir_path, csv_file) 1064 df: pd.DataFrame = self._prepare_csv(file_path) 1065 df_array.append(df) 1066 1067 # 結合 1068 df_combined: pd.DataFrame = pd.concat(df_array, join="outer") 1069 df_combined = df_combined.loc[~df_combined.index.duplicated(), :] 1070 1071 # 平日と休日の判定に使用するカラムを作成 1072 df_combined[self._col_weekday] = df_combined.index.map( 1073 FluxFootprintAnalyzer.is_weekday 1074 ) # 共通の関数を使用 1075 1076 return df_combined 1077 1078 def _prepare_csv(self, file_path: str) -> pd.DataFrame: 1079 """ 1080 フラックスデータを含むCSVファイルを読み込み、処理します。 1081 1082 Parameters: 1083 ------ 1084 file_path : str 1085 CSVファイルのパス。 1086 1087 Returns: 1088 ------ 1089 pandas.DataFrame 1090 処理済みのデータフレーム。 1091 """ 1092 # CSVファイルの最初の行を読み込み、ヘッダーを取得するための一時データフレームを作成 1093 temp: pd.DataFrame = pd.read_csv(file_path, header=None, nrows=1, skiprows=0) 1094 header = temp.loc[temp.index[0]] 1095 1096 # 実際のデータを読み込み、必要な行をスキップし、欠損値を指定 1097 df: pd.DataFrame = pd.read_csv( 1098 file_path, 1099 header=None, 1100 skiprows=2, 1101 na_values=["#DIV/0!", "#VALUE!", "#REF!", "#N/A", "#NAME?", "NAN"], 1102 low_memory=False, 1103 ) 1104 # 取得したヘッダーをデータフレームに設定 1105 df.columns = header 1106 1107 # self._required_columnsのカラムが存在するか確認 1108 missing_columns: list[str] = [ 1109 col for col in self._required_columns if col not in df.columns.tolist() 1110 ] 1111 if missing_columns: 1112 raise ValueError( 1113 f"必要なカラムが不足しています: {', '.join(missing_columns)}" 1114 ) 1115 1116 # "Date"カラムをインデックスに設定して返却 1117 df["Date"] = pd.to_datetime(df["Date"]) 1118 df = df.dropna(subset=["Date"]) 1119 df.set_index("Date", inplace=True) 1120 return df 1121 1122 @staticmethod 1123 def _calculate_footprint_parameters( 1124 dUstar: float, dU: float, Z_d: float, phi_m: float, phi_c: float, n: float 1125 ) -> tuple[float, float, float, float, float]: 1126 """ 1127 フットプリントパラメータを計算します。 1128 1129 Parameters: 1130 ------ 1131 dUstar : float 1132 摩擦速度 1133 dU : float 1134 風速 1135 Z_d : float 1136 地面修正後の測定高度 1137 phi_m : float 1138 運動量の安定度関数 1139 phi_c : float 1140 スカラーの安定度関数 1141 n : float 1142 安定度パラメータ 1143 1144 Returns: 1145 ------ 1146 tuple[float, float, float, float, float] 1147 m (べき指数), 1148 U (基準高度での風速), 1149 r (べき指数の補正項), 1150 mu (形状パラメータ), 1151 ksi (フラックス長さスケール) 1152 """ 1153 KARMAN: float = 0.4 # フォン・カルマン定数 1154 # パラメータの計算 1155 m: float = dUstar / KARMAN * phi_m / dU 1156 U: float = dU / pow(Z_d, m) 1157 r: float = 2.0 + m - n 1158 mu: float = (1.0 + m) / r 1159 kz: float = KARMAN * dUstar * Z_d / phi_c 1160 k: float = kz / pow(Z_d, n) 1161 ksi: float = U * pow(Z_d, r) / r / r / k 1162 return m, U, r, mu, ksi 1163 1164 @staticmethod 1165 def _calculate_ground_correction( 1166 z_m: float, 1167 wind_speed: np.ndarray, 1168 friction_velocity: np.ndarray, 1169 stability_parameter: np.ndarray, 1170 ) -> float: 1171 """ 1172 地面修正量を計算します(Pennypacker and Baldocchi, 2016)。 1173 1174 この関数は、与えられた気象データを使用して地面修正量を計算します。 1175 計算は以下のステップで行われます: 1176 1. 変位高さ(d)を計算 1177 2. 中立条件外のデータを除外 1178 3. 平均変位高さを計算 1179 4. 地面修正量を返す 1180 1181 Parameters: 1182 ------ 1183 z_m : float 1184 観測地点の高度 1185 wind_speed : np.ndarray 1186 風速データ配列 (WS vector) 1187 friction_velocity : np.ndarray 1188 摩擦速度データ配列 (u*) 1189 stability_parameter : np.ndarray 1190 安定度パラメータ配列 (z/L) 1191 1192 Returns: 1193 ------ 1194 float 1195 計算された地面修正量 1196 """ 1197 KARMAN: float = 0.4 # フォン・カルマン定数 1198 z: float = z_m 1199 1200 # 変位高さ(d)の計算 1201 displacement_height = 0.6 * ( 1202 z / (0.6 + 0.1 * (np.exp((KARMAN * wind_speed) / friction_velocity))) 1203 ) 1204 1205 # 中立条件外のデータをマスク(中立条件:-0.1 < z/L < 0.1) 1206 neutral_condition_mask = (stability_parameter < -0.1) | ( 1207 0.1 < stability_parameter 1208 ) 1209 displacement_height[neutral_condition_mask] = np.nan 1210 1211 # 平均変位高さを計算 1212 d: float = np.nanmean(displacement_height) 1213 1214 # 地面修正量を返す 1215 return z - d 1216 1217 @staticmethod 1218 def _calculate_stability_parameters(dzL: float) -> tuple[float, float, float]: 1219 """ 1220 安定性パラメータを計算します。 1221 大気安定度に基づいて、運動量とスカラーの安定度関数、および安定度パラメータを計算します。 1222 1223 Parameters: 1224 ------ 1225 dzL : float 1226 無次元高度 (z/L)、ここで z は測定高度、L はモニン・オブコフ長 1227 1228 Returns: 1229 ------ 1230 tuple[float, float, float] 1231 phi_m : float 1232 運動量の安定度関数 1233 phi_c : float 1234 スカラーの安定度関数 1235 n : float 1236 安定度パラメータ 1237 """ 1238 phi_m: float = 0 1239 phi_c: float = 0 1240 n: float = 0 1241 if dzL > 0.0: 1242 # 安定成層の場合 1243 dzL = min(dzL, 2.0) 1244 phi_m = 1.0 + 5.0 * dzL 1245 phi_c = 1.0 + 5.0 * dzL 1246 n = 1.0 / (1.0 + 5.0 * dzL) 1247 else: 1248 # 不安定成層の場合 1249 phi_m = pow(1.0 - 16.0 * dzL, -0.25) 1250 phi_c = pow(1.0 - 16.0 * dzL, -0.50) 1251 n = (1.0 - 24.0 * dzL) / (1.0 - 16.0 * dzL) 1252 return phi_m, phi_c, n 1253 1254 @staticmethod 1255 def filter_data( 1256 df: pd.DataFrame, 1257 start_date: str | None = None, 1258 end_date: str | None = None, 1259 months: list[int] | None = None, 1260 ) -> pd.DataFrame: 1261 """ 1262 指定された期間や月でデータをフィルタリングするメソッド。 1263 1264 Parameters: 1265 ------ 1266 df : pd.DataFrame 1267 フィルタリングするデータフレーム 1268 start_date : str | None 1269 フィルタリングの開始日('YYYY-MM-DD'形式)。デフォルトはNone。 1270 end_date : str | None 1271 フィルタリングの終了日('YYYY-MM-DD'形式)。デフォルトはNone。 1272 months : list[int] | None 1273 フィルタリングする月のリスト(例:[1, 2, 12])。デフォルトはNone。 1274 1275 Returns: 1276 ------ 1277 pd.DataFrame 1278 フィルタリングされたデータフレーム 1279 1280 Raises: 1281 ------ 1282 ValueError 1283 インデックスがDatetimeIndexでない場合、または日付の形式が不正な場合 1284 """ 1285 # インデックスの検証 1286 if not isinstance(df.index, pd.DatetimeIndex): 1287 raise ValueError( 1288 "DataFrameのインデックスはDatetimeIndexである必要があります" 1289 ) 1290 1291 filtered_df: pd.DataFrame = df.copy() 1292 1293 # 日付形式の検証と変換 1294 try: 1295 if start_date is not None: 1296 start_date = pd.to_datetime(start_date) 1297 if end_date is not None: 1298 end_date = pd.to_datetime(end_date) 1299 except ValueError as e: 1300 raise ValueError( 1301 "日付の形式が不正です。'YYYY-MM-DD'形式で指定してください" 1302 ) from e 1303 1304 # 期間でフィルタリング 1305 if start_date is not None or end_date is not None: 1306 filtered_df = filtered_df.loc[start_date:end_date] 1307 1308 # 月のバリデーション 1309 if months is not None: 1310 if not all(isinstance(m, int) and 1 <= m <= 12 for m in months): 1311 raise ValueError( 1312 "monthsは1から12までの整数のリストである必要があります" 1313 ) 1314 filtered_df = filtered_df[filtered_df.index.month.isin(months)] 1315 1316 # フィルタリング後のデータが空でないことを確認 1317 if filtered_df.empty: 1318 raise ValueError("フィルタリング後のデータが空になりました") 1319 1320 return filtered_df 1321 1322 @staticmethod 1323 def is_weekday(date: datetime) -> int: 1324 """ 1325 指定された日付が平日であるかどうかを判定します。 1326 1327 Parameters: 1328 ------ 1329 date : datetime 1330 判定する日付。 1331 1332 Returns: 1333 ------ 1334 int 1335 平日であれば1、そうでなければ0。 1336 """ 1337 return 1 if not jpholiday.is_holiday(date) and date.weekday() < 5 else 0 1338 1339 @staticmethod 1340 def _prepare_plot_data( 1341 x80: float, 1342 ksi: float, 1343 mu: float, 1344 r: float, 1345 U: float, 1346 m: float, 1347 sigmaV: float, 1348 flux_value: float, 1349 plot_count: int, 1350 ) -> tuple[np.ndarray, np.ndarray, np.ndarray]: 1351 """ 1352 フットプリントのプロットデータを準備します。 1353 1354 Parameters: 1355 ------ 1356 x80 : float 1357 80%寄与距離 1358 ksi : float 1359 フラックス長さスケール 1360 mu : float 1361 形状パラメータ 1362 r : float 1363 べき指数 1364 U : float 1365 風速 1366 m : float 1367 風速プロファイルのべき指数 1368 sigmaV : float 1369 風速の標準偏差 1370 flux_value : float 1371 フラックス値 1372 plot_count : int 1373 生成するプロット数 1374 1375 Returns: 1376 ------ 1377 tuple[np.ndarray, np.ndarray, np.ndarray] 1378 x座標、y座標、フラックス値の配列のタプル 1379 """ 1380 KARMAN: float = 0.4 # フォン・カルマン定数 (pp.210) 1381 x_lim: int = int(x80) 1382 1383 """ 1384 各ランで生成するプロット数 1385 多いほどメモリに付加がかかるため注意 1386 """ 1387 plot_num: int = plot_count # 各ランで生成するプロット数 1388 1389 # x方向の距離配列を生成 1390 x_list: np.ndarray = np.arange(1, x_lim + 1, dtype="float64") 1391 1392 # クロスウィンド積分フットプリント関数を計算 1393 f_list: np.ndarray = ( 1394 ksi**mu * np.exp(-ksi / x_list) / math.gamma(mu) / x_list ** (1.0 + mu) 1395 ) 1396 1397 # プロット数に基づいてx座標を生成 1398 num_list: np.ndarray = np.round(f_list * plot_num).astype("int64") 1399 x1: np.ndarray = np.repeat(x_list, num_list) 1400 1401 # 風速プロファイルを計算 1402 Ux: np.ndarray = ( 1403 (math.gamma(mu) / math.gamma(1 / r)) 1404 * ((r**2 * KARMAN) / U) ** (m / r) 1405 * U 1406 * x1 ** (m / r) 1407 ) 1408 1409 # y方向の分散を計算し、正規分布に従ってy座標を生成 1410 sigma_array: np.ndarray = sigmaV * x1 / Ux 1411 y1: np.ndarray = np.random.normal(0, sigma_array) 1412 1413 # フラックス値の配列を生成 1414 flux1 = np.full_like(x1, flux_value) 1415 1416 return x1, y1, flux1 1417 1418 @staticmethod 1419 def _rotate_coordinates( 1420 x: np.ndarray, y: np.ndarray, radian: float 1421 ) -> tuple[np.ndarray, np.ndarray]: 1422 """ 1423 座標を指定された角度で回転させます。 1424 1425 この関数は、与えられたx座標とy座標を、指定された角度(ラジアン)で回転させます。 1426 回転は原点を中心に反時計回りに行われます。 1427 1428 Parameters: 1429 ------ 1430 x : np.ndarray 1431 回転させるx座標の配列 1432 y : np.ndarray 1433 回転させるy座標の配列 1434 radian : float 1435 回転角度(ラジアン) 1436 1437 Returns: 1438 ------ 1439 tuple[np.ndarray, np.ndarray] 1440 回転後の(x_, y_)座標の組 1441 """ 1442 radian1: float = (radian - (np.pi / 2)) * (-1) 1443 x_: np.ndarray = x * np.cos(radian1) - y * np.sin(radian1) 1444 y_: np.ndarray = x * np.sin(radian1) + y * np.cos(radian1) 1445 return x_, y_ 1446 1447 @staticmethod 1448 def _source_area_KM2001( 1449 ksi: float, 1450 mu: float, 1451 dU: float, 1452 sigmaV: float, 1453 Z_d: float, 1454 max_ratio: float = 0.8, 1455 ) -> float: 1456 """ 1457 Kormann and Meixner (2001)のフットプリントモデルに基づいてソースエリアを計算します。 1458 1459 このメソッドは、与えられたパラメータを使用して、フラックスの寄与距離を計算します。 1460 計算は反復的に行われ、寄与率が'max_ratio'に達するまで、または最大反復回数に達するまで続けられます。 1461 1462 Parameters: 1463 ------ 1464 ksi : float 1465 フラックス長さスケール 1466 mu : float 1467 形状パラメータ 1468 dU : float 1469 風速の変化率 1470 sigmaV : float 1471 風速の標準偏差 1472 Z_d : float 1473 ゼロ面変位高度 1474 max_ratio : float, optional 1475 寄与率の最大値。デフォルトは0.8。 1476 1477 Returns: 1478 ------ 1479 float 1480 80%寄与距離(メートル単位)。計算が収束しない場合はnp.nan。 1481 1482 Notes: 1483 ------ 1484 - 計算が収束しない場合(最大反復回数に達した場合)、結果はnp.nanとなります。 1485 """ 1486 if max_ratio > 1: 1487 raise ValueError("max_ratio は0以上1以下である必要があります。") 1488 # 変数の初期値 1489 sum_f: float = 0.0 # 寄与率(0 < sum_f < 1.0) 1490 x1: float = 0.0 1491 dF_xd: float = 0.0 1492 1493 x_d: float = ksi / ( 1494 1.0 + mu 1495 ) # Eq. 22 (x_d : クロスウィンド積分フラックスフットプリント最大位置) 1496 1497 dx: float = x_d / 100.0 # 等値線の拡がりの最大距離の100分の1(m) 1498 1499 # 寄与率が80%に達するまでfを積算 1500 while sum_f < (max_ratio / 1): 1501 x1 += dx 1502 1503 # Equation 21 (dF : クロスウィンド積分フットプリント) 1504 dF: float = ( 1505 pow(ksi, mu) * math.exp(-ksi / x1) / math.gamma(mu) / pow(x1, 1.0 + mu) 1506 ) 1507 1508 sum_f += dF # Footprint を加えていく (0.0 < dF < 1.0) 1509 dx *= 2.0 # 距離は2倍ずつ増やしていく 1510 1511 if dx > 1.0: 1512 dx = 1.0 # 一気に、1 m 以上はインクリメントしない 1513 if x1 > Z_d * 1000.0: 1514 break # ソースエリアが測定高度の1000倍以上となった場合、エラーとして止める 1515 1516 x_dst: float = x1 # 寄与率が80%に達するまでの積算距離 1517 f_last: float = ( 1518 pow(ksi, mu) 1519 * math.exp(-ksi / x_dst) 1520 / math.gamma(mu) 1521 / pow(x_dst, 1.0 + mu) 1522 ) # Page 214 just below the Eq. 21. 1523 1524 # y方向の最大距離とその位置のxの距離 1525 dy: float = x_d / 100.0 # 等値線の拡がりの最大距離の100分の1 1526 y_dst: float = 0.0 1527 accumulated_y: float = 0.0 # y方向の積算距離を表す変数 1528 1529 # 最大反復回数を設定 1530 MAX_ITERATIONS: int = 100000 1531 for _ in range(MAX_ITERATIONS): 1532 accumulated_y += dy 1533 if accumulated_y >= x_dst: 1534 break 1535 1536 dF_xd = ( 1537 pow(ksi, mu) 1538 * math.exp(-ksi / accumulated_y) 1539 / math.gamma(mu) 1540 / pow(accumulated_y, 1.0 + mu) 1541 ) # 式21の直下(214ページ) 1542 1543 aa: float = math.log(x_dst * dF_xd / f_last / accumulated_y) 1544 sigma: float = sigmaV * accumulated_y / dU # 215ページ8行目 1545 1546 if 2.0 * aa >= 0: 1547 y_dst_new: float = sigma * math.sqrt(2.0 * aa) 1548 if y_dst_new <= y_dst: 1549 break # forループを抜ける 1550 y_dst = y_dst_new 1551 1552 dy = min(dy * 2.0, 1.0) 1553 1554 else: 1555 # ループが正常に終了しなかった場合(最大反復回数に達した場合) 1556 x_dst = np.nan 1557 1558 return x_dst
フラックスフットプリントを解析および可視化するクラス。
このクラスは、フラックスデータの処理、フットプリントの計算、 および結果を衛星画像上に可視化するメソッドを提供します。 座標系と単位に関する重要な注意:
- すべての距離はメートル単位で計算されます
- 座標系の原点(0,0)は測定タワーの位置に対応します
- x軸は東西方向(正が東)
- y軸は南北方向(正が北)
- 風向は気象学的風向(北から時計回りに測定)を使用
この実装は、Kormann and Meixner (2001) および Takano et al. (2021)に基づいています。
37 def __init__( 38 self, 39 z_m: float, 40 labelsize: float = 20, 41 ticksize: float = 16, 42 plot_params=None, 43 logger: Logger | None = None, 44 logging_debug: bool = False, 45 ): 46 """ 47 衛星画像を用いて FluxFootprintAnalyzer を初期化します。 48 49 Parameters: 50 ------ 51 z_m : float 52 測定の高さ(メートル単位)。 53 labelsize : float 54 軸ラベルのフォントサイズ。デフォルトは20。 55 ticksize : float 56 軸目盛りのフォントサイズ。デフォルトは16。 57 plot_params : Optional[Dict[str, any]] 58 matplotlibのプロットパラメータを指定する辞書。 59 logger : Logger | None 60 使用するロガー。Noneの場合は新しいロガーを生成します。 61 logging_debug : bool 62 ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。 63 """ 64 # 定数や共通の変数 65 self._required_columns: list[str] = [ 66 "Date", 67 "WS vector", 68 "u*", 69 "z/L", 70 "Wind direction", 71 "sigmaV", 72 ] # 必要なカラムの名前 73 self._col_weekday: str = "ffa_is_weekday" # クラスで生成するカラムのキー名 74 self._z_m: float = z_m # 測定高度 75 # 状態を管理するフラグ 76 self._got_satellite_image: bool = False 77 78 # 図表の初期設定 79 FigureUtils.setup_plot_params( 80 font_size=labelsize, tick_size=ticksize, plot_params=plot_params 81 ) 82 # ロガー 83 log_level: int = INFO 84 if logging_debug: 85 log_level = DEBUG 86 self.logger: Logger = FluxFootprintAnalyzer.setup_logger(logger, log_level)
衛星画像を用いて FluxFootprintAnalyzer を初期化します。
Parameters:
z_m : float
測定の高さ(メートル単位)。
labelsize : float
軸ラベルのフォントサイズ。デフォルトは20。
ticksize : float
軸目盛りのフォントサイズ。デフォルトは16。
plot_params : Optional[Dict[str, any]]
matplotlibのプロットパラメータを指定する辞書。
logger : Logger | None
使用するロガー。Noneの場合は新しいロガーを生成します。
logging_debug : bool
ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。
88 @staticmethod 89 def setup_logger(logger: Logger | None, log_level: int = INFO) -> Logger: 90 """ 91 ロガーを設定します。 92 93 このメソッドは、ロギングの設定を行い、ログメッセージのフォーマットを指定します。 94 ログメッセージには、日付、ログレベル、メッセージが含まれます。 95 96 渡されたロガーがNoneまたは不正な場合は、新たにロガーを作成し、標準出力に 97 ログメッセージが表示されるようにStreamHandlerを追加します。ロガーのレベルは 98 引数で指定されたlog_levelに基づいて設定されます。 99 100 Parameters: 101 ------ 102 logger : Logger | None 103 使用するロガー。Noneの場合は新しいロガーを作成します。 104 log_level : int 105 ロガーのログレベル。デフォルトはINFO。 106 107 Returns: 108 ------ 109 Logger 110 設定されたロガーオブジェクト。 111 """ 112 if logger is not None and isinstance(logger, Logger): 113 return logger 114 # 渡されたロガーがNoneまたは正しいものでない場合は独自に設定 115 new_logger: Logger = getLogger() 116 # 既存のハンドラーをすべて削除 117 for handler in new_logger.handlers[:]: 118 new_logger.removeHandler(handler) 119 new_logger.setLevel(log_level) # ロガーのレベルを設定 120 ch = StreamHandler() 121 ch_formatter = Formatter("%(asctime)s - %(levelname)s - %(message)s") 122 ch.setFormatter(ch_formatter) # フォーマッターをハンドラーに設定 123 new_logger.addHandler(ch) # StreamHandlerの追加 124 return new_logger
ロガーを設定します。
このメソッドは、ロギングの設定を行い、ログメッセージのフォーマットを指定します。 ログメッセージには、日付、ログレベル、メッセージが含まれます。
渡されたロガーがNoneまたは不正な場合は、新たにロガーを作成し、標準出力に ログメッセージが表示されるようにStreamHandlerを追加します。ロガーのレベルは 引数で指定されたlog_levelに基づいて設定されます。
Parameters:
logger : Logger | None
使用するロガー。Noneの場合は新しいロガーを作成します。
log_level : int
ロガーのログレベル。デフォルトはINFO。
Returns:
Logger
設定されたロガーオブジェクト。
126 def calculate_flux_footprint( 127 self, 128 df: pd.DataFrame, 129 col_flux: str, 130 plot_count: int = 10000, 131 start_time: str = "10:00", 132 end_time: str = "16:00", 133 ) -> tuple[list[float], list[float], list[float]]: 134 """ 135 フラックスフットプリントを計算し、指定された時間帯のデータを基に可視化します。 136 137 Parameters: 138 ------ 139 df : pd.DataFrame 140 分析対象のデータフレーム。フラックスデータを含む。 141 col_flux : str 142 フラックスデータの列名。計算に使用される。 143 plot_count : int, optional 144 生成するプロットの数。デフォルトは10000。 145 start_time : str, optional 146 フットプリント計算に使用する開始時間。デフォルトは"10:00"。 147 end_time : str, optional 148 フットプリント計算に使用する終了時間。デフォルトは"16:00"。 149 150 Returns: 151 ------ 152 tuple[list[float], list[float], list[float]]: 153 x座標 (メートル): タワーを原点とした東西方向の距離 154 y座標 (メートル): タワーを原点とした南北方向の距離 155 対象スカラー量の値: 各地点でのフラックス値 156 157 Notes: 158 ------ 159 - 返却される座標は測定タワーを原点(0,0)とした相対位置です 160 - すべての距離はメートル単位で表されます 161 - 正のx値は東方向、正のy値は北方向を示します 162 """ 163 df: pd.DataFrame = df.copy() 164 165 # インデックスがdatetimeであることを確認し、必要に応じて変換 166 if not isinstance(df.index, pd.DatetimeIndex): 167 df.index = pd.to_datetime(df.index) 168 169 # DatetimeIndexから直接dateプロパティにアクセス 170 datelist: np.ndarray = np.array(df.index.date) 171 172 # 各日付が平日かどうかを判定し、リストに格納 173 numbers: list[int] = [ 174 FluxFootprintAnalyzer.is_weekday(date) for date in datelist 175 ] 176 177 # col_weekdayに基づいてデータフレームに平日情報を追加 178 df.loc[:, self._col_weekday] = numbers # .locを使用して値を設定 179 180 # 値が1のもの(平日)をコピーする 181 data_weekday: pd.DataFrame = df[df[self._col_weekday] == 1].copy() 182 # 特定の時間帯を抽出 183 data_weekday = data_weekday.between_time( 184 start_time, end_time 185 ) # 引数を使用して時間帯を抽出 186 data_weekday = data_weekday.dropna(subset=[col_flux]) 187 188 directions: list[float] = [ 189 wind_direction if wind_direction >= 0 else wind_direction + 360 190 for wind_direction in data_weekday["Wind direction"] 191 ] 192 193 data_weekday.loc[:, "Wind direction_360"] = directions 194 data_weekday.loc[:, "radian"] = data_weekday["Wind direction_360"] / 180 * np.pi 195 196 # 風向が欠測なら除去 197 data_weekday = data_weekday.dropna(subset=["Wind direction", col_flux]) 198 199 # 数値型への変換を確実に行う 200 numeric_columns: list[str] = ["u*", "WS vector", "sigmaV", "z/L"] 201 for col in numeric_columns: 202 data_weekday[col] = pd.to_numeric(data_weekday[col], errors="coerce") 203 204 # 地面修正量dの計算 205 z_m: float = self._z_m 206 Z_d: float = FluxFootprintAnalyzer._calculate_ground_correction( 207 z_m=z_m, 208 wind_speed=data_weekday["WS vector"].values, 209 friction_velocity=data_weekday["u*"].values, 210 stability_parameter=data_weekday["z/L"].values, 211 ) 212 213 x_list: list[float] = [] 214 y_list: list[float] = [] 215 c_list: list[float] | None = [] 216 217 # tqdmを使用してプログレスバーを表示 218 for i in tqdm(range(len(data_weekday)), desc="Calculating footprint"): 219 dUstar: float = data_weekday["u*"].iloc[i] 220 dU: float = data_weekday["WS vector"].iloc[i] 221 sigmaV: float = data_weekday["sigmaV"].iloc[i] 222 dzL: float = data_weekday["z/L"].iloc[i] 223 224 if pd.isna(dUstar) or pd.isna(dU) or pd.isna(sigmaV) or pd.isna(dzL): 225 self.logger.warning(f"#N/A fields are exist.: i = {i}") 226 continue 227 elif dUstar < 5.0 and dUstar != 0.0 and dU > 0.1: 228 phi_m, phi_c, n = FluxFootprintAnalyzer._calculate_stability_parameters( 229 dzL=dzL 230 ) 231 m, U, r, mu, ksi = ( 232 FluxFootprintAnalyzer._calculate_footprint_parameters( 233 dUstar=dUstar, dU=dU, Z_d=Z_d, phi_m=phi_m, phi_c=phi_c, n=n 234 ) 235 ) 236 237 # 80%ソースエリアの計算 238 x80: float = FluxFootprintAnalyzer._source_area_KM2001( 239 ksi=ksi, mu=mu, dU=dU, sigmaV=sigmaV, Z_d=Z_d, max_ratio=0.8 240 ) 241 242 if not np.isnan(x80): 243 x1, y1, flux1 = FluxFootprintAnalyzer._prepare_plot_data( 244 x80, 245 ksi, 246 mu, 247 r, 248 U, 249 m, 250 sigmaV, 251 data_weekday[col_flux].iloc[i], 252 plot_count=plot_count, 253 ) 254 x1_, y1_ = FluxFootprintAnalyzer._rotate_coordinates( 255 x=x1, y=y1, radian=data_weekday["radian"].iloc[i] 256 ) 257 258 x_list.extend(x1_) 259 y_list.extend(y1_) 260 c_list.extend(flux1) 261 262 return ( 263 x_list, 264 y_list, 265 c_list, 266 )
フラックスフットプリントを計算し、指定された時間帯のデータを基に可視化します。
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値は北方向を示します
268 def combine_all_data( 269 self, data_source: str | pd.DataFrame, source_type: str = "csv", **kwargs 270 ) -> pd.DataFrame: 271 """ 272 CSVファイルまたはMonthlyConverterからのデータを統合します 273 274 Parameters: 275 ------ 276 data_source : str | pd.DataFrame 277 CSVディレクトリパスまたはDataFrame 278 source_type : str 279 "csv" または "monthly" 280 **kwargs : 281 追加パラメータ 282 - sheet_names : list[str] 283 Monthlyの場合のシート名 284 - start_date : str 285 開始日 286 - end_date : str 287 終了日 288 289 Returns: 290 ------ 291 pd.DataFrame 292 処理済みのデータフレーム 293 """ 294 if source_type == "csv": 295 # 既存のCSV処理ロジック 296 return self._combine_all_csv(data_source) 297 elif source_type == "monthly": 298 # MonthlyConverterからのデータを処理 299 if not isinstance(data_source, pd.DataFrame): 300 raise ValueError("monthly形式の場合、DataFrameを直接渡す必要があります") 301 302 df = data_source.copy() 303 304 # required_columnsからDateを除外して欠損値チェックを行う 305 check_columns = [col for col in self._required_columns if col != "Date"] 306 307 # インデックスがdatetimeであることを確認 308 if not isinstance(df.index, pd.DatetimeIndex) and "Date" not in df.columns: 309 raise ValueError("DatetimeIndexまたはDateカラムが必要です") 310 311 if "Date" in df.columns: 312 df.set_index("Date", inplace=True) 313 314 # 必要なカラムの存在確認 315 missing_columns = [ 316 col for col in check_columns if col not in df.columns.tolist() 317 ] 318 if missing_columns: 319 missing_cols = "','".join(missing_columns) 320 current_cols = "','".join(df.columns.tolist()) 321 raise ValueError( 322 f"必要なカラムが不足しています: '{missing_cols}'\n" 323 f"現在のカラム: '{current_cols}'" 324 ) 325 326 # 平日/休日の判定用カラムを追加 327 df[self._col_weekday] = df.index.map(FluxFootprintAnalyzer.is_weekday) 328 329 # Dateを除外したカラムで欠損値の処理 330 df = df.dropna(subset=check_columns) 331 332 # インデックスの重複を除去 333 df = df.loc[~df.index.duplicated(), :] 334 335 return df 336 else: 337 raise ValueError("source_typeは'csv'または'monthly'である必要があります")
CSVファイルまたはMonthlyConverterからのデータを統合します
Parameters:
data_source : str | pd.DataFrame
CSVディレクトリパスまたはDataFrame
source_type : str
"csv" または "monthly"
**kwargs :
追加パラメータ
- sheet_names : list[str]
Monthlyの場合のシート名
- start_date : str
開始日
- end_date : str
終了日
Returns:
pd.DataFrame
処理済みのデータフレーム
339 def get_satellite_image_from_api( 340 self, 341 api_key: str, 342 center_lat: float, 343 center_lon: float, 344 output_path: str, 345 scale: int = 1, 346 size: tuple[int, int] = (2160, 2160), 347 zoom: int = 13, 348 ) -> ImageFile: 349 """ 350 Google Maps Static APIを使用して衛星画像を取得します。 351 352 Parameters: 353 ------ 354 api_key : str 355 Google Maps Static APIのキー。 356 center_lat : float 357 中心の緯度。 358 center_lon : float 359 中心の経度。 360 output_path : str 361 画像の保存先パス。拡張子は'.png'のみ許可される。 362 scale : int, optional 363 画像の解像度スケール(1か2)。デフォルトは1。 364 size : tuple[int, int], optional 365 画像サイズ (幅, 高さ)。デフォルトは(2160, 2160)。 366 zoom : int, optional 367 ズームレベル(0-21)。デフォルトは13。 368 369 Returns: 370 ------ 371 ImageFile 372 取得した衛星画像 373 374 Raises: 375 ------ 376 requests.RequestException 377 API呼び出しに失敗した場合 378 """ 379 # バリデーション 380 if not output_path.endswith(".png"): 381 raise ValueError("出力ファイル名は'.png'で終わる必要があります。") 382 383 # HTTPリクエストの定義 384 base_url = "https://maps.googleapis.com/maps/api/staticmap" 385 params = { 386 "center": f"{center_lat},{center_lon}", 387 "zoom": zoom, 388 "size": f"{size[0]}x{size[1]}", 389 "maptype": "satellite", 390 "scale": scale, 391 "key": api_key, 392 } 393 394 try: 395 response = requests.get(base_url, params=params) 396 response.raise_for_status() 397 # 画像ファイルに変換 398 image = Image.open(io.BytesIO(response.content)) 399 image.save(output_path) 400 self._got_satellite_image = True 401 self.logger.info(f"リモート画像を取得し、保存しました: {output_path}") 402 return image 403 except requests.RequestException as e: 404 self.logger.error(f"衛星画像の取得に失敗しました: {str(e)}") 405 raise
Google Maps Static APIを使用して衛星画像を取得します。
Parameters:
api_key : str
Google Maps Static APIのキー。
center_lat : float
中心の緯度。
center_lon : float
中心の経度。
output_path : str
画像の保存先パス。拡張子は'.png'のみ許可される。
scale : int, optional
画像の解像度スケール(1か2)。デフォルトは1。
size : tuple[int, int], optional
画像サイズ (幅, 高さ)。デフォルトは(2160, 2160)。
zoom : int, optional
ズームレベル(0-21)。デフォルトは13。
Returns:
ImageFile
取得した衛星画像
Raises:
requests.RequestException
API呼び出しに失敗した場合
407 def get_satellite_image_from_local( 408 self, 409 local_image_path: str, 410 alpha: float = 1.0, 411 ) -> ImageFile: 412 """ 413 ローカルファイルから衛星画像を読み込みます。 414 415 Parameters: 416 ------ 417 local_image_path : str 418 ローカル画像のパス 419 alpha : float, optional 420 画像の透過率(0.0~1.0)。デフォルトは1.0。 421 422 Returns: 423 ------ 424 ImageFile 425 読み込んだ衛星画像(透過設定済み) 426 427 Raises: 428 ------ 429 FileNotFoundError 430 指定されたパスにファイルが存在しない場合 431 """ 432 if not os.path.exists(local_image_path): 433 raise FileNotFoundError( 434 f"指定されたローカル画像が存在しません: {local_image_path}" 435 ) 436 # 画像を読み込む 437 image = Image.open(local_image_path) 438 439 # RGBAモードに変換して透過率を設定 440 if image.mode != "RGBA": 441 image = image.convert("RGBA") 442 443 # 透過率を設定 444 data = image.getdata() 445 new_data = [(r, g, b, int(255 * alpha)) for r, g, b, a in data] 446 image.putdata(new_data) 447 448 self._got_satellite_image = True 449 self.logger.info( 450 f"ローカル画像を使用しました(透過率: {alpha}): {local_image_path}" 451 ) 452 return image
ローカルファイルから衛星画像を読み込みます。
Parameters:
local_image_path : str
ローカル画像のパス
alpha : float, optional
画像の透過率(0.0~1.0)。デフォルトは1.0。
Returns:
ImageFile
読み込んだ衛星画像(透過設定済み)
Raises:
FileNotFoundError
指定されたパスにファイルが存在しない場合
454 def plot_flux_footprint( 455 self, 456 x_list: list[float], 457 y_list: list[float], 458 c_list: list[float] | None, 459 center_lat: float, 460 center_lon: float, 461 vmin: float, 462 vmax: float, 463 add_cbar: bool = True, 464 add_legend: bool = True, 465 cbar_label: str | None = None, 466 cbar_labelpad: int = 20, 467 cmap: str = "jet", 468 reduce_c_function: callable = np.mean, 469 lat_correction: float = 1, 470 lon_correction: float = 1, 471 output_dir: str | None = None, 472 output_filename: str = "footprint.png", 473 save_fig: bool = True, 474 show_fig: bool = True, 475 satellite_image: ImageFile | None = None, 476 xy_max: float = 5000, 477 ) -> None: 478 """ 479 フットプリントデータをプロットします。 480 481 このメソッドは、指定されたフットプリントデータのみを可視化します。 482 483 Parameters: 484 ------ 485 x_list : list[float] 486 フットプリントのx座標リスト(メートル単位)。 487 y_list : list[float] 488 フットプリントのy座標リスト(メートル単位)。 489 c_list : list[float] | None 490 フットプリントの強度を示す値のリスト。 491 center_lat : float 492 プロットの中心となる緯度。 493 center_lon : float 494 プロットの中心となる経度。 495 cmap : str 496 使用するカラーマップの名前。 497 vmin : float 498 カラーバーの最小値。 499 vmax : float 500 カラーバーの最大値。 501 reduce_c_function : callable, optional 502 フットプリントの集約関数(デフォルトはnp.mean)。 503 cbar_label : str | None, optional 504 カラーバーのラベル。 505 cbar_labelpad : int, optional 506 カラーバーラベルのパディング。 507 lon_correction : float, optional 508 経度方向の補正係数(デフォルトは1)。 509 lat_correction : float, optional 510 緯度方向の補正係数(デフォルトは1)。 511 output_dir : str | None, optional 512 プロット画像の保存先パス。 513 output_filename : str 514 プロット画像の保存ファイル名(拡張子を含む)。デフォルトは'footprint.png'。 515 save_fig : bool 516 図の保存を許可するフラグ。デフォルトはTrue。 517 show_fig : bool 518 図の表示を許可するフラグ。デフォルトはTrue。 519 satellite_image : ImageFile | None, optional 520 使用する衛星画像。指定がない場合はデフォルトの画像が生成されます。 521 xy_max : float, optional 522 表示範囲の最大値(デフォルトは4000)。 523 """ 524 self.plot_flux_footprint_with_hotspots( 525 x_list=x_list, 526 y_list=y_list, 527 c_list=c_list, 528 center_lat=center_lat, 529 center_lon=center_lon, 530 vmin=vmin, 531 vmax=vmax, 532 add_cbar=add_cbar, 533 add_legend=add_legend, 534 cbar_label=cbar_label, 535 cbar_labelpad=cbar_labelpad, 536 cmap=cmap, 537 reduce_c_function=reduce_c_function, 538 hotspots=None, # hotspotsをNoneに設定 539 hotspot_colors=None, 540 lat_correction=lat_correction, 541 lon_correction=lon_correction, 542 output_dir=output_dir, 543 output_filename=output_filename, 544 save_fig=save_fig, 545 show_fig=show_fig, 546 satellite_image=satellite_image, 547 xy_max=xy_max, 548 )
フットプリントデータをプロットします。
このメソッドは、指定されたフットプリントデータのみを可視化します。
Parameters:
x_list : list[float]
フットプリントのx座標リスト(メートル単位)。
y_list : list[float]
フットプリントのy座標リスト(メートル単位)。
c_list : list[float] | None
フットプリントの強度を示す値のリスト。
center_lat : float
プロットの中心となる緯度。
center_lon : float
プロットの中心となる経度。
cmap : str
使用するカラーマップの名前。
vmin : float
カラーバーの最小値。
vmax : float
カラーバーの最大値。
reduce_c_function : callable, optional
フットプリントの集約関数(デフォルトはnp.mean)。
cbar_label : str | None, optional
カラーバーのラベル。
cbar_labelpad : int, optional
カラーバーラベルのパディング。
lon_correction : float, optional
経度方向の補正係数(デフォルトは1)。
lat_correction : float, optional
緯度方向の補正係数(デフォルトは1)。
output_dir : str | None, optional
プロット画像の保存先パス。
output_filename : str
プロット画像の保存ファイル名(拡張子を含む)。デフォルトは'footprint.png'。
save_fig : bool
図の保存を許可するフラグ。デフォルトはTrue。
show_fig : bool
図の表示を許可するフラグ。デフォルトはTrue。
satellite_image : ImageFile | None, optional
使用する衛星画像。指定がない場合はデフォルトの画像が生成されます。
xy_max : float, optional
表示範囲の最大値(デフォルトは4000)。
550 def plot_flux_footprint_with_hotspots( 551 self, 552 x_list: list[float], 553 y_list: list[float], 554 c_list: list[float] | None, 555 center_lat: float, 556 center_lon: float, 557 vmin: float, 558 vmax: float, 559 add_cbar: bool = True, 560 add_legend: bool = True, 561 cbar_label: str | None = None, 562 cbar_labelpad: int = 20, 563 cmap: str = "jet", 564 reduce_c_function: callable = np.mean, 565 hotspots: list[HotspotData] | None = None, 566 hotspot_colors: dict[HotspotType, str] | None = None, 567 hotspot_labels: dict[HotspotType, str] | None = None, 568 hotspot_markers: dict[HotspotType, str] | None = None, 569 lat_correction: float = 1, 570 lon_correction: float = 1, 571 output_dir: str | None = None, 572 output_filename: str = "footprint.png", 573 save_fig: bool = True, 574 show_fig: bool = True, 575 satellite_image: ImageFile | None = None, 576 xy_max: float = 5000, 577 ) -> None: 578 """ 579 Staticな衛星画像上にフットプリントデータとホットスポットをプロットします。 580 581 このメソッドは、指定されたフットプリントデータとホットスポットを可視化します。 582 ホットスポットが指定されない場合は、フットプリントのみ作図します。 583 584 Parameters: 585 ------ 586 x_list : list[float] 587 フットプリントのx座標リスト(メートル単位)。 588 y_list : list[float] 589 フットプリントのy座標リスト(メートル単位)。 590 c_list : list[float] | None 591 フットプリントの強度を示す値のリスト。 592 center_lat : float 593 プロットの中心となる緯度。 594 center_lon : float 595 プロットの中心となる経度。 596 vmin : float 597 カラーバーの最小値。 598 vmax : float 599 カラーバーの最大値。 600 add_cbar : bool, optional 601 カラーバーを追加するかどうか(デフォルトはTrue)。 602 add_legend : bool, optional 603 凡例を追加するかどうか(デフォルトはTrue)。 604 cbar_label : str | None, optional 605 カラーバーのラベル。 606 cbar_labelpad : int, optional 607 カラーバーラベルのパディング。 608 cmap : str 609 使用するカラーマップの名前。 610 reduce_c_function : callable 611 フットプリントの集約関数(デフォルトはnp.mean)。 612 hotspots : list[HotspotData] | None, optional 613 ホットスポットデータのリスト。デフォルトはNone。 614 hotspot_colors : dict[HotspotType, str] | None, optional 615 ホットスポットの色を指定する辞書。 616 hotspot_labels : dict[HotspotType, str] | None, optional 617 ホットスポットの表示ラベルを指定する辞書。 618 例: {'bio': '生物', 'gas': 'ガス', 'comb': '燃焼'} 619 hotspot_markers : dict[HotspotType, str] | None, optional 620 ホットスポットの形状を指定する辞書。 621 指定の例は {'bio': '^', 'gas': 'o', 'comb': 's'} (三角、丸、四角)など。 622 lat_correction : float, optional 623 緯度方向の補正係数(デフォルトは1)。 624 lon_correction : float, optional 625 経度方向の補正係数(デフォルトは1)。 626 output_dir : str | None, optional 627 プロット画像の保存先パス。 628 output_filename : str 629 プロット画像の保存ファイル名(拡張子を含む)。デフォルトは'footprint.png'。 630 save_fig : bool 631 図の保存を許可するフラグ。デフォルトはTrue。 632 show_fig : bool 633 図の表示を許可するフラグ。デフォルトはTrue。 634 satellite_image : ImageFile | None, optional 635 使用する衛星画像。指定がない場合はデフォルトの画像が生成されます。 636 xy_max : float, optional 637 表示範囲の最大値(デフォルトは5000)。 638 """ 639 # 1. 引数のバリデーション 640 valid_extensions: list[str] = [".png", ".jpg", ".jpeg", ".pdf", ".svg"] 641 _, file_extension = os.path.splitext(output_filename) 642 if file_extension.lower() not in valid_extensions: 643 quoted_extensions: list[str] = [f'"{ext}"' for ext in valid_extensions] 644 self.logger.error( 645 f"`output_filename`は有効な拡張子ではありません。プロットを保存するには、次のいずれかを指定してください: {','.join(quoted_extensions)}" 646 ) 647 return 648 649 # 2. フラグチェック 650 if not self._got_satellite_image: 651 raise ValueError( 652 "`get_satellite_image_from_api`または`get_satellite_image_from_local`が実行されていません。" 653 ) 654 655 # 3. 衛星画像の取得 656 if satellite_image is None: 657 satellite_image = Image.new("RGB", (2160, 2160), "lightgray") 658 659 self.logger.info("プロットを作成中...") 660 661 # 4. 座標変換のための定数計算(1回だけ) 662 meters_per_lat: float = self.EARTH_RADIUS_METER * ( 663 math.pi / 180 664 ) # 緯度1度あたりのメートル 665 meters_per_lon: float = meters_per_lat * math.cos( 666 math.radians(center_lat) 667 ) # 経度1度あたりのメートル 668 669 # 5. フットプリントデータの座標変換(まとめて1回で実行) 670 x_deg = ( 671 np.array(x_list) / meters_per_lon * lon_correction 672 ) # 補正係数も同時に適用 673 y_deg = ( 674 np.array(y_list) / meters_per_lat * lat_correction 675 ) # 補正係数も同時に適用 676 677 # 6. 中心点からの相対座標を実際の緯度経度に変換 678 lons = center_lon + x_deg 679 lats = center_lat + y_deg 680 681 # 7. 表示範囲の計算(変更なし) 682 x_range: float = xy_max / meters_per_lon 683 y_range: float = xy_max / meters_per_lat 684 map_boundaries: tuple[float, float, float, float] = ( 685 center_lon - x_range, # left_lon 686 center_lon + x_range, # right_lon 687 center_lat - y_range, # bottom_lat 688 center_lat + y_range, # top_lat 689 ) 690 left_lon, right_lon, bottom_lat, top_lat = map_boundaries 691 692 # 8. プロットの作成 693 plt.rcParams["axes.edgecolor"] = "None" 694 fig: plt.Figure = plt.figure(figsize=(10, 8), dpi=300) 695 ax_data: plt.Axes = fig.add_axes([0.05, 0.1, 0.8, 0.8]) 696 697 # 9. フットプリントの描画 698 # フットプリントの描画とカラーバー用の2つのhexbinを作成 699 if c_list is not None: 700 ax_data.hexbin( 701 lons, 702 lats, 703 C=c_list, 704 cmap=cmap, 705 vmin=vmin, 706 vmax=vmax, 707 alpha=0.3, # 実際のプロット用 708 gridsize=100, 709 linewidths=0, 710 mincnt=100, 711 extent=[left_lon, right_lon, bottom_lat, top_lat], 712 reduce_C_function=reduce_c_function, 713 ) 714 715 # カラーバー用の非表示hexbin(alpha=1.0) 716 hidden_hexbin = ax_data.hexbin( 717 lons, 718 lats, 719 C=c_list, 720 cmap=cmap, 721 vmin=vmin, 722 vmax=vmax, 723 alpha=1.0, # カラーバー用 724 gridsize=100, 725 linewidths=0, 726 mincnt=100, 727 extent=[left_lon, right_lon, bottom_lat, top_lat], 728 reduce_C_function=reduce_c_function, 729 visible=False, # プロットには表示しない 730 ) 731 732 # 10. ホットスポットの描画 733 spot_handles = [] 734 if hotspots is not None: 735 default_colors: dict[HotspotType, str] = { 736 "bio": "blue", 737 "gas": "red", 738 "comb": "green", 739 } 740 741 # デフォルトのマーカー形状を定義 742 default_markers: dict[HotspotType, str] = { 743 "bio": "^", # 三角 744 "gas": "o", # 丸 745 "comb": "s", # 四角 746 } 747 748 # デフォルトのラベルを定義 749 default_labels: dict[HotspotType, str] = { 750 "bio": "bio", 751 "gas": "gas", 752 "comb": "comb", 753 } 754 755 # 座標変換のための定数 756 meters_per_lat: float = self.EARTH_RADIUS_METER * (math.pi / 180) 757 meters_per_lon: float = meters_per_lat * math.cos(math.radians(center_lat)) 758 759 for spot_type, color in (hotspot_colors or default_colors).items(): 760 spots_lon = [] 761 spots_lat = [] 762 763 # 使用するマーカーを決定 764 marker = (hotspot_markers or default_markers).get(spot_type, "o") 765 766 for spot in hotspots: 767 if spot.type == spot_type: 768 # 変換前の緯度経度をログ出力 769 self.logger.debug( 770 f"Before - Type: {spot_type}, Lat: {spot.avg_lat:.6f}, Lon: {spot.avg_lon:.6f}" 771 ) 772 773 # 中心からの相対距離を計算 774 dx: float = (spot.avg_lon - center_lon) * meters_per_lon 775 dy: float = (spot.avg_lat - center_lat) * meters_per_lat 776 777 # 補正前の相対座標をログ出力 778 self.logger.debug( 779 f"Relative - Type: {spot_type}, X: {dx:.2f}m, Y: {dy:.2f}m" 780 ) 781 782 # 補正を適用 783 corrected_dx: float = dx * lon_correction 784 corrected_dy: float = dy * lat_correction 785 786 # 補正後の緯度経度を計算 787 adjusted_lon: float = center_lon + corrected_dx / meters_per_lon 788 adjusted_lat: float = center_lat + corrected_dy / meters_per_lat 789 790 # 変換後の緯度経度をログ出力 791 self.logger.debug( 792 f"After - Type: {spot_type}, Lat: {adjusted_lat:.6f}, Lon: {adjusted_lon:.6f}\n" 793 ) 794 795 if ( 796 left_lon <= adjusted_lon <= right_lon 797 and bottom_lat <= adjusted_lat <= top_lat 798 ): 799 spots_lon.append(adjusted_lon) 800 spots_lat.append(adjusted_lat) 801 802 if spots_lon: 803 # 使用するラベルを決定 804 label = (hotspot_labels or default_labels).get(spot_type, spot_type) 805 806 handle = ax_data.scatter( 807 spots_lon, 808 spots_lat, 809 c=color, 810 marker=marker, # マーカー形状を指定 811 s=100, 812 alpha=0.7, 813 label=label, 814 edgecolor="black", 815 linewidth=1, 816 ) 817 spot_handles.append(handle) 818 819 # 11. 背景画像の設定 820 ax_img = ax_data.twiny().twinx() 821 ax_img.imshow( 822 satellite_image, 823 extent=[left_lon, right_lon, bottom_lat, top_lat], 824 aspect="equal", 825 ) 826 827 # 12. 軸の設定 828 for ax in [ax_data, ax_img]: 829 ax.set_xlim(left_lon, right_lon) 830 ax.set_ylim(bottom_lat, top_lat) 831 ax.set_xticks([]) 832 ax.set_yticks([]) 833 834 ax_data.set_zorder(2) 835 ax_data.patch.set_alpha(0) 836 ax_img.set_zorder(1) 837 838 # 13. カラーバーの追加 839 if add_cbar: 840 cbar_ax: plt.Axes = fig.add_axes([0.88, 0.1, 0.03, 0.8]) 841 cbar = fig.colorbar(hidden_hexbin, cax=cbar_ax) # hidden_hexbinを使用 842 # cbar_labelが指定されている場合のみラベルを設定 843 if cbar_label: 844 cbar.set_label(cbar_label, rotation=270, labelpad=cbar_labelpad) 845 846 # 14. ホットスポットの凡例追加 847 if add_legend and hotspots and spot_handles: 848 ax_data.legend( 849 handles=spot_handles, 850 loc="upper center", # 位置を上部中央に 851 bbox_to_anchor=(0.55, -0.01), # 図の下に配置 852 ncol=len(spot_handles), # ハンドルの数に応じて列数を設定 853 ) 854 855 # 15. 画像の保存 856 if save_fig: 857 if output_dir is None: 858 raise ValueError( 859 "save_fig=Trueの場合、output_dirを指定する必要があります。有効なディレクトリパスを指定してください。" 860 ) 861 output_path: str = os.path.join(output_dir, output_filename) 862 self.logger.info("プロットを保存中...") 863 try: 864 fig.savefig(output_path, bbox_inches="tight") 865 self.logger.info(f"プロットが正常に保存されました: {output_path}") 866 except Exception as e: 867 self.logger.error(f"プロットの保存中にエラーが発生しました: {str(e)}") 868 # 16. 画像の表示 869 if show_fig: 870 plt.show() 871 else: 872 plt.close(fig=fig)
Staticな衛星画像上にフットプリントデータとホットスポットをプロットします。
このメソッドは、指定されたフットプリントデータとホットスポットを可視化します。 ホットスポットが指定されない場合は、フットプリントのみ作図します。
Parameters:
x_list : list[float]
フットプリントのx座標リスト(メートル単位)。
y_list : list[float]
フットプリントのy座標リスト(メートル単位)。
c_list : list[float] | None
フットプリントの強度を示す値のリスト。
center_lat : float
プロットの中心となる緯度。
center_lon : float
プロットの中心となる経度。
vmin : float
カラーバーの最小値。
vmax : float
カラーバーの最大値。
add_cbar : bool, optional
カラーバーを追加するかどうか(デフォルトはTrue)。
add_legend : bool, optional
凡例を追加するかどうか(デフォルトはTrue)。
cbar_label : str | None, optional
カラーバーのラベル。
cbar_labelpad : int, optional
カラーバーラベルのパディング。
cmap : str
使用するカラーマップの名前。
reduce_c_function : callable
フットプリントの集約関数(デフォルトはnp.mean)。
hotspots : list[HotspotData] | None, optional
ホットスポットデータのリスト。デフォルトはNone。
hotspot_colors : dict[HotspotType, str] | None, optional
ホットスポットの色を指定する辞書。
hotspot_labels : dict[HotspotType, str] | None, optional
ホットスポットの表示ラベルを指定する辞書。
例: {'bio': '生物', 'gas': 'ガス', 'comb': '燃焼'}
hotspot_markers : dict[HotspotType, str] | None, optional
ホットスポットの形状を指定する辞書。
指定の例は {'bio': '^', 'gas': 'o', 'comb': 's'} (三角、丸、四角)など。
lat_correction : float, optional
緯度方向の補正係数(デフォルトは1)。
lon_correction : float, optional
経度方向の補正係数(デフォルトは1)。
output_dir : str | None, optional
プロット画像の保存先パス。
output_filename : str
プロット画像の保存ファイル名(拡張子を含む)。デフォルトは'footprint.png'。
save_fig : bool
図の保存を許可するフラグ。デフォルトはTrue。
show_fig : bool
図の表示を許可するフラグ。デフォルトはTrue。
satellite_image : ImageFile | None, optional
使用する衛星画像。指定がない場合はデフォルトの画像が生成されます。
xy_max : float, optional
表示範囲の最大値(デフォルトは5000)。
874 def plot_flux_footprint_with_scale_checker( 875 self, 876 x_list: list[float], 877 y_list: list[float], 878 c_list: list[float] | None, 879 center_lat: float, 880 center_lon: float, 881 check_points: list[tuple[float, float, str]] | None = None, 882 vmin: float = 0, 883 vmax: float = 100, 884 add_cbar: bool = True, 885 cbar_label: str | None = None, 886 cbar_labelpad: int = 20, 887 cmap: str = "jet", 888 reduce_c_function: callable = np.mean, 889 lat_correction: float = 1, 890 lon_correction: float = 1, 891 output_dir: str | None = None, 892 output_filename: str = "footprint-scale_checker.png", 893 save_fig: bool = True, 894 show_fig: bool = True, 895 satellite_image: ImageFile | None = None, 896 xy_max: float = 5000, 897 ) -> None: 898 """ 899 Staticな衛星画像上にフットプリントデータとホットスポットをプロットします。 900 901 このメソッドは、指定されたフットプリントデータとホットスポットを可視化します。 902 ホットスポットが指定されない場合は、フットプリントのみ作図します。 903 904 Parameters: 905 ------ 906 x_list : list[float] 907 フットプリントのx座標リスト(メートル単位)。 908 y_list : list[float] 909 フットプリントのy座標リスト(メートル単位)。 910 c_list : list[float] | None 911 フットプリントの強度を示す値のリスト。 912 center_lat : float 913 プロットの中心となる緯度。 914 center_lon : float 915 プロットの中心となる経度。 916 check_points : list[tuple[float, float, str]] | None 917 確認用の地点リスト。各要素は (緯度, 経度, ラベル) のタプル。 918 Noneの場合は中心から500m、1000m、2000m、3000mの位置に仮想的な点を配置。 919 cmap : str 920 使用するカラーマップの名前。 921 vmin : float 922 カラーバーの最小値。 923 vmax : float 924 カラーバーの最大値。 925 reduce_c_function : callable, optional 926 フットプリントの集約関数(デフォルトはnp.mean)。 927 cbar_label : str, optional 928 カラーバーのラベル。 929 cbar_labelpad : int, optional 930 カラーバーラベルのパディング。 931 hotspots : list[HotspotData] | None 932 ホットスポットデータのリスト。デフォルトはNone。 933 hotspot_colors : dict[str, str] | None, optional 934 ホットスポットの色を指定する辞書。 935 lon_correction : float, optional 936 経度方向の補正係数(デフォルトは1)。 937 lat_correction : float, optional 938 緯度方向の補正係数(デフォルトは1)。 939 output_dir : str | None, optional 940 プロット画像の保存先パス。 941 output_filename : str 942 プロット画像の保存ファイル名(拡張子を含む)。デフォルトは'footprint.png'。 943 save_fig : bool 944 図の保存を許可するフラグ。デフォルトはTrue。 945 show_fig : bool 946 図の表示を許可するフラグ。デフォルトはTrue。 947 satellite_image : ImageFile | None, optional 948 使用する衛星画像。指定がない場合はデフォルトの画像が生成されます。 949 xy_max : float, optional 950 表示範囲の最大値(デフォルトは5000)。 951 """ 952 if check_points is None: 953 # デフォルトの確認ポイントを生成(従来の方式) 954 default_points = [ 955 (500, "North", 90), # 北 500m 956 (1000, "East", 0), # 東 1000m 957 (2000, "South", 270), # 南 2000m 958 (3000, "West", 180), # 西 3000m 959 ] 960 961 dummy_hotspots = [] 962 for distance, direction, angle in default_points: 963 rad = math.radians(angle) 964 meters_per_lat = self.EARTH_RADIUS_METER * (math.pi / 180) 965 meters_per_lon = meters_per_lat * math.cos(math.radians(center_lat)) 966 967 dx = distance * math.cos(rad) 968 dy = distance * math.sin(rad) 969 970 delta_lon = dx / meters_per_lon 971 delta_lat = dy / meters_per_lat 972 973 hotspot = HotspotData( 974 avg_lat=center_lat + delta_lat, 975 avg_lon=center_lon + delta_lon, 976 delta_ch4=0.0, 977 delta_c2h6=0.0, 978 ratio=0.0, 979 type=f"{direction}_{distance}m", 980 section=0, 981 source="scale_check", 982 angle=0, 983 correlation=0, 984 ) 985 dummy_hotspots.append(hotspot) 986 else: 987 # 指定された緯度経度を使用 988 dummy_hotspots = [] 989 for lat, lon, label in check_points: 990 hotspot = HotspotData( 991 avg_lat=lat, 992 avg_lon=lon, 993 delta_ch4=0.0, 994 delta_c2h6=0.0, 995 ratio=0.0, 996 type=label, 997 section=0, 998 source="scale_check", 999 angle=0, 1000 correlation=0, 1001 ) 1002 dummy_hotspots.append(hotspot) 1003 1004 # カスタムカラーマップの作成 1005 hotspot_colors = { 1006 spot.type: plt.cm.tab10(i % 10) for i, spot in enumerate(dummy_hotspots) 1007 } 1008 1009 # 既存のメソッドを呼び出してプロット 1010 self.plot_flux_footprint_with_hotspots( 1011 x_list=x_list, 1012 y_list=y_list, 1013 c_list=c_list, 1014 center_lat=center_lat, 1015 center_lon=center_lon, 1016 vmin=vmin, 1017 vmax=vmax, 1018 add_cbar=add_cbar, 1019 add_legend=True, 1020 cbar_label=cbar_label, 1021 cbar_labelpad=cbar_labelpad, 1022 cmap=cmap, 1023 reduce_c_function=reduce_c_function, 1024 hotspots=dummy_hotspots, 1025 hotspot_colors=hotspot_colors, 1026 lat_correction=lat_correction, 1027 lon_correction=lon_correction, 1028 output_dir=output_dir, 1029 output_filename=output_filename, 1030 save_fig=save_fig, 1031 show_fig=show_fig, 1032 satellite_image=satellite_image, 1033 xy_max=xy_max, 1034 )
Staticな衛星画像上にフットプリントデータとホットスポットをプロットします。
このメソッドは、指定されたフットプリントデータとホットスポットを可視化します。 ホットスポットが指定されない場合は、フットプリントのみ作図します。
Parameters:
x_list : list[float]
フットプリントのx座標リスト(メートル単位)。
y_list : list[float]
フットプリントのy座標リスト(メートル単位)。
c_list : list[float] | None
フットプリントの強度を示す値のリスト。
center_lat : float
プロットの中心となる緯度。
center_lon : float
プロットの中心となる経度。
check_points : list[tuple[float, float, str]] | None
確認用の地点リスト。各要素は (緯度, 経度, ラベル) のタプル。
Noneの場合は中心から500m、1000m、2000m、3000mの位置に仮想的な点を配置。
cmap : str
使用するカラーマップの名前。
vmin : float
カラーバーの最小値。
vmax : float
カラーバーの最大値。
reduce_c_function : callable, optional
フットプリントの集約関数(デフォルトはnp.mean)。
cbar_label : str, optional
カラーバーのラベル。
cbar_labelpad : int, optional
カラーバーラベルのパディング。
hotspots : list[HotspotData] | None
ホットスポットデータのリスト。デフォルトはNone。
hotspot_colors : dict[str, str] | None, optional
ホットスポットの色を指定する辞書。
lon_correction : float, optional
経度方向の補正係数(デフォルトは1)。
lat_correction : float, optional
緯度方向の補正係数(デフォルトは1)。
output_dir : str | None, optional
プロット画像の保存先パス。
output_filename : str
プロット画像の保存ファイル名(拡張子を含む)。デフォルトは'footprint.png'。
save_fig : bool
図の保存を許可するフラグ。デフォルトはTrue。
show_fig : bool
図の表示を許可するフラグ。デフォルトはTrue。
satellite_image : ImageFile | None, optional
使用する衛星画像。指定がない場合はデフォルトの画像が生成されます。
xy_max : float, optional
表示範囲の最大値(デフォルトは5000)。
1254 @staticmethod 1255 def filter_data( 1256 df: pd.DataFrame, 1257 start_date: str | None = None, 1258 end_date: str | None = None, 1259 months: list[int] | None = None, 1260 ) -> pd.DataFrame: 1261 """ 1262 指定された期間や月でデータをフィルタリングするメソッド。 1263 1264 Parameters: 1265 ------ 1266 df : pd.DataFrame 1267 フィルタリングするデータフレーム 1268 start_date : str | None 1269 フィルタリングの開始日('YYYY-MM-DD'形式)。デフォルトはNone。 1270 end_date : str | None 1271 フィルタリングの終了日('YYYY-MM-DD'形式)。デフォルトはNone。 1272 months : list[int] | None 1273 フィルタリングする月のリスト(例:[1, 2, 12])。デフォルトはNone。 1274 1275 Returns: 1276 ------ 1277 pd.DataFrame 1278 フィルタリングされたデータフレーム 1279 1280 Raises: 1281 ------ 1282 ValueError 1283 インデックスがDatetimeIndexでない場合、または日付の形式が不正な場合 1284 """ 1285 # インデックスの検証 1286 if not isinstance(df.index, pd.DatetimeIndex): 1287 raise ValueError( 1288 "DataFrameのインデックスはDatetimeIndexである必要があります" 1289 ) 1290 1291 filtered_df: pd.DataFrame = df.copy() 1292 1293 # 日付形式の検証と変換 1294 try: 1295 if start_date is not None: 1296 start_date = pd.to_datetime(start_date) 1297 if end_date is not None: 1298 end_date = pd.to_datetime(end_date) 1299 except ValueError as e: 1300 raise ValueError( 1301 "日付の形式が不正です。'YYYY-MM-DD'形式で指定してください" 1302 ) from e 1303 1304 # 期間でフィルタリング 1305 if start_date is not None or end_date is not None: 1306 filtered_df = filtered_df.loc[start_date:end_date] 1307 1308 # 月のバリデーション 1309 if months is not None: 1310 if not all(isinstance(m, int) and 1 <= m <= 12 for m in months): 1311 raise ValueError( 1312 "monthsは1から12までの整数のリストである必要があります" 1313 ) 1314 filtered_df = filtered_df[filtered_df.index.month.isin(months)] 1315 1316 # フィルタリング後のデータが空でないことを確認 1317 if filtered_df.empty: 1318 raise ValueError("フィルタリング後のデータが空になりました") 1319 1320 return filtered_df
指定された期間や月でデータをフィルタリングするメソッド。
Parameters:
df : pd.DataFrame
フィルタリングするデータフレーム
start_date : str | None
フィルタリングの開始日('YYYY-MM-DD'形式)。デフォルトはNone。
end_date : str | None
フィルタリングの終了日('YYYY-MM-DD'形式)。デフォルトはNone。
months : list[int] | None
フィルタリングする月のリスト(例:[1, 2, 12])。デフォルトはNone。
Returns:
pd.DataFrame
フィルタリングされたデータフレーム
Raises:
ValueError
インデックスがDatetimeIndexでない場合、または日付の形式が不正な場合
1322 @staticmethod 1323 def is_weekday(date: datetime) -> int: 1324 """ 1325 指定された日付が平日であるかどうかを判定します。 1326 1327 Parameters: 1328 ------ 1329 date : datetime 1330 判定する日付。 1331 1332 Returns: 1333 ------ 1334 int 1335 平日であれば1、そうでなければ0。 1336 """ 1337 return 1 if not jpholiday.is_holiday(date) and date.weekday() < 5 else 0
指定された日付が平日であるかどうかを判定します。
Parameters:
date : datetime
判定する日付。
Returns:
int
平日であれば1、そうでなければ0。
11class CorrectingUtils: 12 @staticmethod 13 def correct_df_by_type(df: pd.DataFrame, correction_type: str) -> pd.DataFrame: 14 """ 15 指定された補正式に基づいてデータフレームを補正します。 16 17 Parameters: 18 ------ 19 df : pd.DataFrame 20 補正対象のデータフレーム。 21 correction_type : str 22 適用する補正式の種類。CORRECTION_TYPES_PATTERNから選択する。 23 24 Returns: 25 ------ 26 pd.DataFrame 27 補正後のデータフレーム。 28 29 Raises: 30 ------ 31 ValueError 32 無効な補正式が指定された場合。 33 """ 34 if correction_type == "pico_1": 35 coef_a: float = 2.0631 # 切片 36 coef_b: float = 1.0111e-06 # 1次の係数 37 coef_c: float = -1.8683e-10 # 2次の係数 38 # 水蒸気補正 39 df_corrected: pd.DataFrame = CorrectingUtils._correct_h2o_interference( 40 df=df, 41 coef_a=coef_a, 42 coef_b=coef_b, 43 coef_c=coef_c, 44 col_ch4="ch4_ppm", 45 col_h2o="h2o_ppm", 46 h2o_threshold=2000, 47 ) 48 # 負の値のエタン濃度の補正など 49 df_corrected = CorrectingUtils._remove_bias( 50 df=df_corrected, col_ch4_ppm="ch4_ppm", col_c2h6_ppb="c2h6_ppb" 51 ) 52 return df_corrected 53 else: 54 raise ValueError(f"invalid correction_type: {correction_type}.") 55 56 @staticmethod 57 def _correct_h2o_interference( 58 df: pd.DataFrame, 59 coef_a: float, 60 coef_b: float, 61 coef_c: float, 62 col_ch4: str = "ch4_ppm", 63 col_h2o: str = "h2o_ppm", 64 h2o_threshold: float | None = 2000, 65 ) -> pd.DataFrame: 66 """ 67 水蒸気干渉を補正するためのメソッドです。 68 CH4濃度に対する水蒸気の影響を2次関数を用いて補正します。 69 70 References: 71 ------ 72 - Commane et al. (2023): Intercomparison of commercial analyzers for atmospheric ethane and methane observations 73 https://amt.copernicus.org/articles/16/1431/2023/, 74 https://amt.copernicus.org/articles/16/1431/2023/amt-16-1431-2023.pdf 75 76 Parameters: 77 ------ 78 df : pd.DataFrame 79 補正対象のデータフレーム 80 coef_a : float 81 補正曲線の切片 82 coef_b : float 83 補正曲線の1次係数 84 coef_c : float 85 補正曲線の2次係数 86 col_ch4 : str 87 CH4濃度を示すカラム名 88 col_h2o : str 89 水蒸気濃度を示すカラム名 90 h2o_threshold : float | None 91 水蒸気濃度の下限値(この値未満のデータは除外) 92 93 Returns: 94 ------ 95 pd.DataFrame 96 水蒸気干渉が補正されたデータフレーム 97 """ 98 # 元のデータを保護するためコピーを作成 99 df = df.copy() 100 # 水蒸気濃度の配列を取得 101 h2o = np.array(df[col_h2o]) 102 103 # 補正項の計算 104 correction_curve = coef_a + coef_b * h2o + coef_c * pow(h2o, 2) 105 max_correction = np.max(correction_curve) 106 correction_term = -(correction_curve - max_correction) 107 108 # CH4濃度の補正 109 df[col_ch4] = df[col_ch4] + correction_term 110 111 # 極端に低い水蒸気濃度のデータは信頼性が低いため除外 112 if h2o_threshold is not None: 113 df.loc[df[col_h2o] < h2o_threshold, col_ch4] = np.nan 114 df = df.dropna(subset=[col_ch4]) 115 116 return df 117 118 @staticmethod 119 def _remove_bias( 120 df: pd.DataFrame, 121 col_ch4_ppm: str = "ch4_ppm", 122 col_c2h6_ppb: str = "c2h6_ppb", 123 ) -> pd.DataFrame: 124 """ 125 データフレームからバイアスを除去します。 126 127 Parameters: 128 ------ 129 df : pd.DataFrame 130 バイアスを除去する対象のデータフレーム。 131 col_ch4_ppm : str 132 CH4濃度を示すカラム名。デフォルトは"ch4_ppm"。 133 col_c2h6_ppb : str 134 C2H6濃度を示すカラム名。デフォルトは"c2h6_ppb"。 135 136 Returns: 137 ------ 138 pd.DataFrame 139 バイアスが除去されたデータフレーム。 140 """ 141 df_processed: pd.DataFrame = df.copy() 142 c2h6_min = np.percentile(df_processed[col_c2h6_ppb], 5) 143 df_processed[col_c2h6_ppb] = df_processed[col_c2h6_ppb] - c2h6_min 144 ch4_min = np.percentile(df_processed[col_ch4_ppm], 5) 145 df_processed[col_ch4_ppm] = df_processed[col_ch4_ppm] - ch4_min + 2.0 146 return df_processed
12 @staticmethod 13 def correct_df_by_type(df: pd.DataFrame, correction_type: str) -> pd.DataFrame: 14 """ 15 指定された補正式に基づいてデータフレームを補正します。 16 17 Parameters: 18 ------ 19 df : pd.DataFrame 20 補正対象のデータフレーム。 21 correction_type : str 22 適用する補正式の種類。CORRECTION_TYPES_PATTERNから選択する。 23 24 Returns: 25 ------ 26 pd.DataFrame 27 補正後のデータフレーム。 28 29 Raises: 30 ------ 31 ValueError 32 無効な補正式が指定された場合。 33 """ 34 if correction_type == "pico_1": 35 coef_a: float = 2.0631 # 切片 36 coef_b: float = 1.0111e-06 # 1次の係数 37 coef_c: float = -1.8683e-10 # 2次の係数 38 # 水蒸気補正 39 df_corrected: pd.DataFrame = CorrectingUtils._correct_h2o_interference( 40 df=df, 41 coef_a=coef_a, 42 coef_b=coef_b, 43 coef_c=coef_c, 44 col_ch4="ch4_ppm", 45 col_h2o="h2o_ppm", 46 h2o_threshold=2000, 47 ) 48 # 負の値のエタン濃度の補正など 49 df_corrected = CorrectingUtils._remove_bias( 50 df=df_corrected, col_ch4_ppm="ch4_ppm", col_c2h6_ppb="c2h6_ppb" 51 ) 52 return df_corrected 53 else: 54 raise ValueError(f"invalid correction_type: {correction_type}.")
指定された補正式に基づいてデータフレームを補正します。
Parameters:
df : pd.DataFrame
補正対象のデータフレーム。
correction_type : str
適用する補正式の種類。CORRECTION_TYPES_PATTERNから選択する。
Returns:
pd.DataFrame
補正後のデータフレーム。
Raises:
ValueError
無効な補正式が指定された場合。
22@dataclass 23class EmissionData: 24 """ 25 ホットスポットの排出量データを格納するクラス。 26 27 Parameters: 28 ------ 29 source : str 30 データソース(日時) 31 type : HotspotType 32 ホットスポットの種類(`HotspotType`を参照) 33 section : str | int | float 34 セクション情報 35 latitude : float 36 緯度 37 longitude : float 38 経度 39 delta_ch4 : float 40 CH4の増加量 (ppm) 41 delta_c2h6 : float 42 C2H6の増加量 (ppb) 43 ratio : float 44 C2H6/CH4比 45 emission_rate : float 46 排出量 (L/min) 47 daily_emission : float 48 日排出量 (L/day) 49 annual_emission : float 50 年間排出量 (L/year) 51 """ 52 53 source: str 54 type: HotspotType 55 section: str | int | float 56 latitude: float 57 longitude: float 58 delta_ch4: float 59 delta_c2h6: float 60 ratio: float 61 emission_rate: float 62 daily_emission: float 63 annual_emission: float 64 65 def __post_init__(self) -> None: 66 """ 67 Initialize時のバリデーションを行います。 68 69 Raises: 70 ------ 71 ValueError: 入力値が不正な場合 72 """ 73 # sourceのバリデーション 74 if not isinstance(self.source, str) or not self.source.strip(): 75 raise ValueError("Source must be a non-empty string") 76 77 # typeのバリデーションは型システムによって保証されるため削除 78 # HotspotTypeはLiteral["bio", "gas", "comb"]として定義されているため、 79 # 不正な値は型チェック時に検出されます 80 81 # sectionのバリデーション(Noneは許可) 82 if self.section is not None and not isinstance(self.section, (str, int, float)): 83 raise ValueError("Section must be a string, int, float, or None") 84 85 # 緯度のバリデーション 86 if ( 87 not isinstance(self.latitude, (int, float)) 88 or not -90 <= self.latitude <= 90 89 ): 90 raise ValueError("Latitude must be a number between -90 and 90") 91 92 # 経度のバリデーション 93 if ( 94 not isinstance(self.longitude, (int, float)) 95 or not -180 <= self.longitude <= 180 96 ): 97 raise ValueError("Longitude must be a number between -180 and 180") 98 99 # delta_ch4のバリデーション 100 if not isinstance(self.delta_ch4, (int, float)) or self.delta_ch4 < 0: 101 raise ValueError("Delta CH4 must be a non-negative number") 102 103 # delta_c2h6のバリデーション 104 if not isinstance(self.delta_c2h6, (int, float)): 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: データクラスの属性と値を含む辞書
258class MobileSpatialAnalyzer: 259 """ 260 移動観測で得られた測定データを解析するクラス 261 """ 262 263 EARTH_RADIUS_METERS: float = 6371000 # 地球の半径(メートル) 264 265 def __init__( 266 self, 267 center_lat: float, 268 center_lon: float, 269 inputs: list[MSAInputConfig] | list[tuple[float, float, str | Path]], 270 num_sections: int = 4, 271 ch4_enhance_threshold: float = 0.1, 272 correlation_threshold: float = 0.7, 273 hotspot_area_meter: float = 50, 274 window_minutes: float = 5, 275 column_mapping: dict[str, str] = { 276 "Time Stamp": "timestamp", 277 "CH4 (ppm)": "ch4_ppm", 278 "C2H6 (ppb)": "c2h6_ppb", 279 "H2O (ppm)": "h2o_ppm", 280 "Latitude": "latitude", 281 "Longitude": "longitude", 282 }, 283 logger: Logger | None = None, 284 logging_debug: bool = False, 285 ): 286 """ 287 測定データ解析クラスの初期化 288 289 Parameters: 290 ------ 291 center_lat : float 292 中心緯度 293 center_lon : float 294 中心経度 295 inputs : list[MSAInputConfig] | list[tuple[float, float, str | Path]] 296 入力ファイルのリスト 297 num_sections : int 298 分割する区画数。デフォルトは4。 299 ch4_enhance_threshold : float 300 CH4増加の閾値(ppm)。デフォルトは0.1。 301 correlation_threshold : float 302 相関係数の閾値。デフォルトは0.7。 303 hotspot_area_meter : float 304 ホットスポットの検出に使用するエリアの半径(メートル)。デフォルトは50メートル。 305 window_minutes : float 306 移動窓の大きさ(分)。デフォルトは5分。 307 column_mapping : dict[str, str] 308 元のデータファイルのヘッダーを汎用的な単語に変換するための辞書型データ。 309 - timestamp,ch4_ppm,c2h6_ppm,h2o_ppm,latitude,longitudeをvalueに、それぞれに対応するカラム名をcolに指定してください。 310 logger : Logger | None 311 使用するロガー。Noneの場合は新しいロガーを作成します。 312 logging_debug : bool 313 ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。 314 315 Returns: 316 ------ 317 None 318 初期化処理が完了したことを示します。 319 """ 320 # ロガー 321 log_level: int = INFO 322 if logging_debug: 323 log_level = DEBUG 324 self.logger: Logger = MobileSpatialAnalyzer.setup_logger(logger, log_level) 325 # プライベートなプロパティ 326 self._center_lat: float = center_lat 327 self._center_lon: float = center_lon 328 self._ch4_enhance_threshold: float = ch4_enhance_threshold 329 self._correlation_threshold: float = correlation_threshold 330 self._hotspot_area_meter: float = hotspot_area_meter 331 self._column_mapping: dict[str, str] = column_mapping 332 self._num_sections: int = num_sections 333 # セクションの範囲 334 section_size: float = 360 / num_sections 335 self._section_size: float = section_size 336 self._sections = MobileSpatialAnalyzer._initialize_sections( 337 num_sections, section_size 338 ) 339 # window_sizeをデータポイント数に変換(分→秒→データポイント数) 340 self._window_size: int = MobileSpatialAnalyzer._calculate_window_size( 341 window_minutes 342 ) 343 # 入力設定の標準化 344 normalized_input_configs: list[MSAInputConfig] = ( 345 MobileSpatialAnalyzer._normalize_inputs(inputs) 346 ) 347 # 複数ファイルのデータを読み込み 348 self._data: dict[str, pd.DataFrame] = self._load_all_data( 349 normalized_input_configs 350 ) 351 352 def analyze_delta_ch4_stats(self, hotspots: list[HotspotData]) -> None: 353 """ 354 各タイプのホットスポットについてΔCH4の統計情報を計算し、結果を表示します。 355 356 Parameters: 357 ------ 358 hotspots : list[HotspotData] 359 分析対象のホットスポットリスト 360 361 Returns: 362 ------ 363 None 364 統計情報の表示が完了したことを示します。 365 """ 366 # タイプごとにホットスポットを分類 367 hotspots_by_type: dict[HotspotType, list[HotspotData]] = { 368 "bio": [h for h in hotspots if h.type == "bio"], 369 "gas": [h for h in hotspots if h.type == "gas"], 370 "comb": [h for h in hotspots if h.type == "comb"], 371 } 372 373 # 統計情報を計算し、表示 374 for spot_type, spots in hotspots_by_type.items(): 375 if spots: 376 delta_ch4_values = [spot.delta_ch4 for spot in spots] 377 max_value = max(delta_ch4_values) 378 mean_value = sum(delta_ch4_values) / len(delta_ch4_values) 379 median_value = sorted(delta_ch4_values)[len(delta_ch4_values) // 2] 380 print(f"{spot_type}タイプのホットスポットの統計情報:") 381 print(f" 最大値: {max_value}") 382 print(f" 平均値: {mean_value}") 383 print(f" 中央値: {median_value}") 384 else: 385 print(f"{spot_type}タイプのホットスポットは存在しません。") 386 387 def analyze_hotspots( 388 self, 389 duplicate_check_mode: str = "none", 390 min_time_threshold_seconds: float = 300, 391 max_time_threshold_hours: float = 12, 392 ) -> list[HotspotData]: 393 """ 394 ホットスポットを検出して分析します。 395 396 Parameters: 397 ------ 398 duplicate_check_mode : str 399 重複チェックのモード("none","time_window","time_all")。 400 - "none": 重複チェックを行わない。 401 - "time_window": 指定された時間窓内の重複のみを除外。 402 - "time_all": すべての時間範囲で重複チェックを行う。 403 min_time_threshold_seconds : float 404 重複とみなす最小時間の閾値(秒)。デフォルトは300秒。 405 max_time_threshold_hours : float 406 重複チェックを一時的に無視する最大時間の閾値(時間)。デフォルトは12時間。 407 408 Returns: 409 ------ 410 list[HotspotData] 411 検出されたホットスポットのリスト。 412 """ 413 # 不正な入力値に対するエラーチェック 414 valid_modes = {"none", "time_window", "time_all"} 415 if duplicate_check_mode not in valid_modes: 416 raise ValueError( 417 f"無効な重複チェックモード: {duplicate_check_mode}. 有効な値は {valid_modes} です。" 418 ) 419 420 all_hotspots: list[HotspotData] = [] 421 422 # 各データソースに対して解析を実行 423 for _, df in self._data.items(): 424 # パラメータの計算 425 df = MobileSpatialAnalyzer._calculate_hotspots_parameters( 426 df, self._window_size 427 ) 428 429 # ホットスポットの検出 430 hotspots: list[HotspotData] = self._detect_hotspots( 431 df, 432 ch4_enhance_threshold=self._ch4_enhance_threshold, 433 ) 434 all_hotspots.extend(hotspots) 435 436 # 重複チェックモードに応じて処理 437 if duplicate_check_mode != "none": 438 unique_hotspots = MobileSpatialAnalyzer.remove_hotspots_duplicates( 439 all_hotspots, 440 check_time_all=duplicate_check_mode == "time_all", 441 min_time_threshold_seconds=min_time_threshold_seconds, 442 max_time_threshold_hours=max_time_threshold_hours, 443 hotspot_area_meter=self._hotspot_area_meter, 444 ) 445 self.logger.info( 446 f"重複除外: {len(all_hotspots)} → {len(unique_hotspots)} ホットスポット" 447 ) 448 return unique_hotspots 449 450 return all_hotspots 451 452 def calculate_measurement_stats( 453 self, 454 print_individual_stats: bool = True, 455 print_total_stats: bool = True, 456 ) -> tuple[float, timedelta]: 457 """ 458 各ファイルの測定時間と走行距離を計算し、合計を返します。 459 460 Parameters: 461 ------ 462 print_individual_stats : bool 463 個別ファイルの統計を表示するかどうか。デフォルトはTrue。 464 print_total_stats : bool 465 合計統計を表示するかどうか。デフォルトはTrue。 466 467 Returns: 468 ------ 469 tuple[float, timedelta] 470 総距離(km)と総時間のタプル 471 """ 472 total_distance: float = 0.0 473 total_time: timedelta = timedelta() 474 individual_stats: list[dict] = [] # 個別の統計情報を保存するリスト 475 476 # プログレスバーを表示しながら計算 477 for source_name, df in tqdm( 478 self._data.items(), desc="Calculating", unit="file" 479 ): 480 # 時間の計算 481 time_spent = df.index[-1] - df.index[0] 482 483 # 距離の計算 484 distance_km = 0.0 485 for i in range(len(df) - 1): 486 lat1, lon1 = df.iloc[i][["latitude", "longitude"]] 487 lat2, lon2 = df.iloc[i + 1][["latitude", "longitude"]] 488 distance_km += ( 489 MobileSpatialAnalyzer._calculate_distance( 490 lat1=lat1, lon1=lon1, lat2=lat2, lon2=lon2 491 ) 492 / 1000 493 ) 494 495 # 合計に加算 496 total_distance += distance_km 497 total_time += time_spent 498 499 # 統計情報を保存 500 if print_individual_stats: 501 average_speed = distance_km / (time_spent.total_seconds() / 3600) 502 individual_stats.append( 503 { 504 "source": source_name, 505 "distance": distance_km, 506 "time": time_spent, 507 "speed": average_speed, 508 } 509 ) 510 511 # 計算完了後に統計情報を表示 512 if print_individual_stats: 513 self.logger.info("=== Individual Stats ===") 514 for stat in individual_stats: 515 print(f"File : {stat['source']}") 516 print(f" Distance : {stat['distance']:.2f} km") 517 print(f" Time : {stat['time']}") 518 print(f" Avg. Speed : {stat['speed']:.1f} km/h\n") 519 520 # 合計を表示 521 if print_total_stats: 522 average_speed_total: float = total_distance / ( 523 total_time.total_seconds() / 3600 524 ) 525 self.logger.info("=== Total Stats ===") 526 print(f" Distance : {total_distance:.2f} km") 527 print(f" Time : {total_time}") 528 print(f" Avg. Speed : {average_speed_total:.1f} km/h\n") 529 530 return total_distance, total_time 531 532 def create_hotspots_map( 533 self, 534 hotspots: list[HotspotData], 535 output_dir: str | Path | None = None, 536 output_filename: str = "hotspots_map.html", 537 center_marker_label: str = "Center", 538 plot_center_marker: bool = True, 539 radius_meters: float = 3000, 540 save_fig: bool = True, 541 ) -> None: 542 """ 543 ホットスポットの分布を地図上にプロットして保存 544 545 Parameters: 546 ------ 547 hotspots : list[HotspotData] 548 プロットするホットスポットのリスト 549 output_dir : str | Path 550 保存先のディレクトリパス 551 output_filename : str 552 保存するファイル名。デフォルトは"hotspots_map"。 553 center_marker_label : str 554 中心を示すマーカーのラベルテキスト。デフォルトは"Center"。 555 plot_center_marker : bool 556 中心を示すマーカーの有無。デフォルトはTrue。 557 radius_meters : float 558 区画分けを示す線の長さ。デフォルトは3000。 559 save_fig : bool 560 図の保存を許可するフラグ。デフォルトはTrue。 561 """ 562 # 地図の作成 563 m = folium.Map( 564 location=[self._center_lat, self._center_lon], 565 zoom_start=15, 566 tiles="OpenStreetMap", 567 ) 568 569 # ホットスポットの種類ごとに異なる色でプロット 570 for spot in hotspots: 571 # NaN値チェックを追加 572 if math.isnan(spot.avg_lat) or math.isnan(spot.avg_lon): 573 continue 574 575 # default type 576 color = "black" 577 # タイプに応じて色を設定 578 if spot.type == "comb": 579 color = "green" 580 elif spot.type == "gas": 581 color = "red" 582 elif spot.type == "bio": 583 color = "blue" 584 585 # CSSのgrid layoutを使用してHTMLタグを含むテキストをフォーマット 586 popup_html = f""" 587 <div style='font-family: Arial; font-size: 12px; display: grid; grid-template-columns: auto auto auto; gap: 5px;'> 588 <b>Date</b> <span>:</span> <span>{spot.source}</span> 589 <b>Lat</b> <span>:</span> <span>{spot.avg_lat:.3f}</span> 590 <b>Lon</b> <span>:</span> <span>{spot.avg_lon:.3f}</span> 591 <b>ΔCH<sub>4</sub></b> <span>:</span> <span>{spot.delta_ch4:.3f}</span> 592 <b>ΔC<sub>2</sub>H<sub>6</sub></b> <span>:</span> <span>{spot.delta_c2h6:.3f}</span> 593 <b>Ratio</b> <span>:</span> <span>{spot.ratio:.3f}</span> 594 <b>Type</b> <span>:</span> <span>{spot.type}</span> 595 <b>Section</b> <span>:</span> <span>{spot.section}</span> 596 </div> 597 """ 598 599 # ポップアップのサイズを指定 600 popup = folium.Popup( 601 folium.Html(popup_html, script=True), 602 max_width=200, # 最大幅(ピクセル) 603 ) 604 605 folium.CircleMarker( 606 location=[spot.avg_lat, spot.avg_lon], 607 radius=8, 608 color=color, 609 fill=True, 610 popup=popup, 611 ).add_to(m) 612 613 # 中心点のマーカー 614 if plot_center_marker: 615 folium.Marker( 616 [self._center_lat, self._center_lon], 617 popup=center_marker_label, 618 icon=folium.Icon(color="green", icon="info-sign"), 619 ).add_to(m) 620 621 # 区画の境界線を描画 622 for section in range(self._num_sections): 623 start_angle = math.radians(-180 + section * self._section_size) 624 625 R = self.EARTH_RADIUS_METERS 626 627 # 境界線の座標を計算 628 lat1 = self._center_lat 629 lon1 = self._center_lon 630 lat2 = math.degrees( 631 math.asin( 632 math.sin(math.radians(lat1)) * math.cos(radius_meters / R) 633 + math.cos(math.radians(lat1)) 634 * math.sin(radius_meters / R) 635 * math.cos(start_angle) 636 ) 637 ) 638 lon2 = self._center_lon + math.degrees( 639 math.atan2( 640 math.sin(start_angle) 641 * math.sin(radius_meters / R) 642 * math.cos(math.radians(lat1)), 643 math.cos(radius_meters / R) 644 - math.sin(math.radians(lat1)) * math.sin(math.radians(lat2)), 645 ) 646 ) 647 648 # 境界線を描画 649 folium.PolyLine( 650 locations=[[lat1, lon1], [lat2, lon2]], 651 color="black", 652 weight=1, 653 opacity=0.5, 654 ).add_to(m) 655 656 # 地図を保存 657 if save_fig and output_dir is None: 658 raise ValueError( 659 "save_fig=Trueの場合、output_dirを指定する必要があります。有効なディレクトリパスを指定してください。" 660 ) 661 output_path: str = os.path.join(output_dir, output_filename) 662 m.save(str(output_path)) 663 self.logger.info(f"地図を保存しました: {output_path}") 664 665 def export_hotspots_to_csv( 666 self, 667 hotspots: list[HotspotData], 668 output_dir: str | Path | None = None, 669 output_filename: str = "hotspots.csv", 670 ) -> None: 671 """ 672 ホットスポットの情報をCSVファイルに出力します。 673 674 Parameters: 675 ------ 676 hotspots : list[HotspotData] 677 出力するホットスポットのリスト 678 output_dir : str | Path | None 679 出力先ディレクトリ 680 output_filename : str 681 出力ファイル名 682 """ 683 # 日時の昇順でソート 684 sorted_hotspots = sorted(hotspots, key=lambda x: x.source) 685 686 # 出力用のデータを作成 687 records = [] 688 for spot in sorted_hotspots: 689 record = { 690 "source": spot.source, 691 "type": spot.type, 692 "delta_ch4": spot.delta_ch4, 693 "delta_c2h6": spot.delta_c2h6, 694 "ratio": spot.ratio, 695 "correlation": spot.correlation, 696 "angle": spot.angle, 697 "section": spot.section, 698 "latitude": spot.avg_lat, 699 "longitude": spot.avg_lon, 700 } 701 records.append(record) 702 703 # DataFrameに変換してCSVに出力 704 if output_dir is None: 705 raise ValueError( 706 "output_dirが指定されていません。有効なディレクトリパスを指定してください。" 707 ) 708 output_path: str = os.path.join(output_dir, output_filename) 709 df = pd.DataFrame(records) 710 df.to_csv(output_path, index=False) 711 self.logger.info( 712 f"ホットスポット情報をCSVファイルに出力しました: {output_path}" 713 ) 714 715 @staticmethod 716 def extract_source_name_from_path(path: str | Path) -> str: 717 """ 718 ファイルパスからソース名(拡張子なしのファイル名)を抽出します。 719 720 Parameters: 721 ------ 722 path : str | Path 723 ソース名を抽出するファイルパス 724 例: "/path/to/Pico100121_241017_092120+.txt" 725 726 Returns: 727 ------ 728 str 729 抽出されたソース名 730 例: "Pico100121_241017_092120+" 731 732 Examples: 733 ------ 734 >>> path = "/path/to/data/Pico100121_241017_092120+.txt" 735 >>> MobileSpatialAnalyzer.extract_source_from_path(path) 736 'Pico100121_241017_092120+' 737 """ 738 # Pathオブジェクトに変換 739 path_obj = Path(path) 740 # stem属性で拡張子なしのファイル名を取得 741 source_name = path_obj.stem 742 return source_name 743 744 def get_preprocessed_data( 745 self, 746 ) -> pd.DataFrame: 747 """ 748 データ前処理を行い、CH4とC2H6の相関解析に必要な形式に整えます。 749 コンストラクタで読み込んだすべてのデータを前処理し、結合したDataFrameを返します。 750 751 Returns: 752 ------ 753 pd.DataFrame 754 前処理済みの結合されたDataFrame 755 """ 756 processed_dfs: list[pd.DataFrame] = [] 757 758 # 各データソースに対して解析を実行 759 for source_name, df in self._data.items(): 760 # パラメータの計算 761 processed_df = MobileSpatialAnalyzer._calculate_hotspots_parameters( 762 df, self._window_size 763 ) 764 # ソース名を列として追加 765 processed_df["source"] = source_name 766 processed_dfs.append(processed_df) 767 768 # すべてのDataFrameを結合 769 if not processed_dfs: 770 raise ValueError("処理対象のデータが存在しません。") 771 772 combined_df = pd.concat(processed_dfs, axis=0) 773 return combined_df 774 775 def get_section_size(self) -> float: 776 """ 777 セクションのサイズを取得するメソッド。 778 このメソッドは、解析対象のデータを区画に分割する際の 779 各区画の角度範囲を示すサイズを返します。 780 781 Returns: 782 ------ 783 float 784 1セクションのサイズ(度単位) 785 """ 786 return self._section_size 787 788 def get_source_names(self, print_all: bool = False) -> list[str]: 789 """ 790 データソースの名前を取得します。 791 792 Parameters 793 ---------- 794 print_all : bool, optional 795 すべてのデータソース名を表示するかどうかを指定します。デフォルトはFalseです。 796 797 Returns 798 ------- 799 list[str] 800 データソース名のリスト 801 802 Raises 803 ------ 804 ValueError 805 データが読み込まれていない場合に発生します。 806 """ 807 dfs_dict: dict[str, pd.DataFrame] = self._data 808 # データソースの選択 809 if not dfs_dict: 810 raise ValueError("データが読み込まれていません。") 811 source_name_list: list[str] = list(dfs_dict.keys()) 812 if print_all: 813 print(source_name_list) 814 return source_name_list 815 816 def plot_ch4_delta_histogram( 817 self, 818 hotspots: list[HotspotData], 819 output_dir: str | Path | None, 820 output_filename: str = "ch4_delta_histogram.png", 821 dpi: int = 200, 822 figsize: tuple[int, int] = (8, 6), 823 fontsize: float = 20, 824 xlim: tuple[float, float] | None = None, 825 ylim: tuple[float, float] | None = None, 826 save_fig: bool = True, 827 show_fig: bool = True, 828 yscale_log: bool = True, 829 print_bins_analysis: bool = False, 830 ) -> None: 831 """ 832 CH4の増加量(ΔCH4)の積み上げヒストグラムをプロットします。 833 834 Parameters: 835 ------ 836 hotspots : list[HotspotData] 837 プロットするホットスポットのリスト 838 output_dir : str | Path | None 839 保存先のディレクトリパス 840 output_filename : str 841 保存するファイル名。デフォルトは"ch4_delta_histogram.png"。 842 dpi : int 843 解像度。デフォルトは200。 844 figsize : tuple[int, int] 845 図のサイズ。デフォルトは(8, 6)。 846 fontsize : float 847 フォントサイズ。デフォルトは20。 848 xlim : tuple[float, float] | None 849 x軸の範囲。Noneの場合は自動設定。 850 ylim : tuple[float, float] | None 851 y軸の範囲。Noneの場合は自動設定。 852 save_fig : bool 853 図の保存を許可するフラグ。デフォルトはTrue。 854 show_fig : bool 855 図の表示を許可するフラグ。デフォルトはTrue。 856 yscale_log : bool 857 y軸をlogにするかどうか。デフォルトはTrue。 858 print_bins_analysis : bool 859 ビンごとの内訳を表示するオプション。 860 """ 861 plt.rcParams["font.size"] = fontsize 862 fig = plt.figure(figsize=figsize, dpi=dpi) 863 864 # ホットスポットからデータを抽出 865 all_ch4_deltas = [] 866 all_types = [] 867 for spot in hotspots: 868 all_ch4_deltas.append(spot.delta_ch4) 869 all_types.append(spot.type) 870 871 # データをNumPy配列に変換 872 all_ch4_deltas = np.array(all_ch4_deltas) 873 all_types = np.array(all_types) 874 875 # 0.1刻みのビンを作成 876 if xlim is not None: 877 bins = np.arange(xlim[0], xlim[1] + 0.1, 0.1) 878 else: 879 max_val = np.ceil(np.max(all_ch4_deltas) * 10) / 10 880 bins = np.arange(0, max_val + 0.1, 0.1) 881 882 # タイプごとのヒストグラムデータを計算 883 hist_data = {} 884 # HotspotTypeのリテラル値を使用してイテレーション 885 for type_name in get_args(HotspotType): # typing.get_argsをインポート 886 mask = all_types == type_name 887 if np.any(mask): 888 counts, _ = np.histogram(all_ch4_deltas[mask], bins=bins) 889 hist_data[type_name] = counts 890 891 # ビンごとの内訳を表示 892 if print_bins_analysis: 893 self.logger.info("各ビンの内訳:") 894 print(f"{'Bin Range':15} {'bio':>8} {'gas':>8} {'comb':>8} {'Total':>8}") 895 print("-" * 50) 896 897 for i in range(len(bins) - 1): 898 bin_start = bins[i] 899 bin_end = bins[i + 1] 900 bio_count = hist_data.get("bio", np.zeros(len(bins) - 1))[i] 901 gas_count = hist_data.get("gas", np.zeros(len(bins) - 1))[i] 902 comb_count = hist_data.get("comb", np.zeros(len(bins) - 1))[i] 903 total = bio_count + gas_count + comb_count 904 905 if total > 0: # 合計が0のビンは表示しない 906 print( 907 f"{bin_start:4.1f}-{bin_end:<8.1f}" 908 f"{int(bio_count):8d}" 909 f"{int(gas_count):8d}" 910 f"{int(comb_count):8d}" 911 f"{int(total):8d}" 912 ) 913 914 # 積み上げヒストグラムを作成 915 bottom = np.zeros_like(hist_data.get("bio", np.zeros(len(bins) - 1))) 916 917 # 色の定義をHotspotTypeを使用して型安全に定義 918 colors: dict[HotspotType, str] = {"bio": "blue", "gas": "red", "comb": "green"} 919 920 # HotspotTypeのリテラル値を使用してイテレーション 921 for type_name in get_args(HotspotType): 922 if type_name in hist_data: 923 plt.bar( 924 bins[:-1], 925 hist_data[type_name], 926 width=np.diff(bins)[0], 927 bottom=bottom, 928 color=colors[type_name], 929 label=type_name, 930 alpha=0.6, 931 align="edge", 932 ) 933 bottom += hist_data[type_name] 934 935 if yscale_log: 936 plt.yscale("log") 937 plt.xlabel("Δ$\\mathregular{CH_{4}}$ (ppm)") 938 plt.ylabel("Frequency") 939 plt.legend() 940 plt.grid(True, which="both", ls="-", alpha=0.2) 941 942 # 軸の範囲を設定 943 if xlim is not None: 944 plt.xlim(xlim) 945 if ylim is not None: 946 plt.ylim(ylim) 947 948 # グラフの保存または表示 949 if save_fig: 950 if output_dir is None: 951 raise ValueError( 952 "save_fig=Trueの場合、output_dirを指定する必要があります。有効なディレクトリパスを指定してください。" 953 ) 954 os.makedirs(output_dir, exist_ok=True) 955 output_path: str = os.path.join(output_dir, output_filename) 956 plt.savefig(output_path, bbox_inches="tight") 957 self.logger.info(f"ヒストグラムを保存しました: {output_path}") 958 if show_fig: 959 plt.show() 960 else: 961 plt.close(fig=fig) 962 963 def plot_mapbox( 964 self, 965 df: pd.DataFrame, 966 col_conc: str, 967 mapbox_access_token: str, 968 sort_conc_column: bool = True, 969 output_dir: str | Path | None = None, 970 output_filename: str = "mapbox_plot.html", 971 col_lat: str = "latitude", 972 col_lon: str = "longitude", 973 colorscale: str = "Jet", 974 center_lat: float | None = None, 975 center_lon: float | None = None, 976 zoom: float = 12, 977 width: int = 700, 978 height: int = 700, 979 tick_font_family: str = "Arial", 980 title_font_family: str = "Arial", 981 tick_font_size: int = 12, 982 title_font_size: int = 14, 983 marker_size: int = 4, 984 colorbar_title: str | None = None, 985 value_range: tuple[float, float] | None = None, 986 save_fig: bool = True, 987 show_fig: bool = True, 988 ) -> None: 989 """ 990 Plotlyを使用してMapbox上にデータをプロットします。 991 992 Parameters: 993 ------ 994 df : pd.DataFrame 995 プロットするデータを含むDataFrame 996 col_conc : str 997 カラーマッピングに使用する列名 998 mapbox_access_token : str 999 Mapboxのアクセストークン 1000 sort_conc_column : bool 1001 value_columnをソートするか否か。デフォルトはTrue。 1002 output_dir : str | Path | None 1003 出力ディレクトリのパス 1004 output_filename : str 1005 出力ファイル名。デフォルトは"mapbox_plot.html" 1006 col_lat : str 1007 緯度の列名。デフォルトは"latitude" 1008 col_lon : str 1009 経度の列名。デフォルトは"longitude" 1010 colorscale : str 1011 使用するカラースケール。デフォルトは"Jet" 1012 center_lat : float | None 1013 中心緯度。デフォルトはNoneで、self._center_latを使用 1014 center_lon : float | None 1015 中心経度。デフォルトはNoneで、self._center_lonを使用 1016 zoom : float 1017 マップの初期ズームレベル。デフォルトは12 1018 width : int 1019 プロットの幅(ピクセル)。デフォルトは700 1020 height : int 1021 プロットの高さ(ピクセル)。デフォルトは700 1022 tick_font_family : str 1023 カラーバーの目盛りフォントファミリー。デフォルトは"Arial" 1024 title_font_family : str 1025 カラーバーのラベルフォントファミリー。デフォルトは"Arial" 1026 tick_font_size : int 1027 カラーバーの目盛りフォントサイズ。デフォルトは12 1028 title_font_size : int 1029 カラーバーのラベルフォントサイズ。デフォルトは14 1030 marker_size : int 1031 マーカーのサイズ。デフォルトは4 1032 colorbar_title : str | None 1033 カラーバーのラベル 1034 value_range : tuple[float, float] | None 1035 カラーマッピングの範囲。デフォルトはNoneで、データの最小値と最大値を使用 1036 save_fig : bool 1037 図を保存するかどうか。デフォルトはTrue 1038 show_fig : bool 1039 図を表示するかどうか。デフォルトはTrue 1040 """ 1041 df_mapping: pd.DataFrame = df.copy().dropna(subset=[col_conc]) 1042 if sort_conc_column: 1043 df_mapping = df_mapping.sort_values(col_conc) 1044 # 中心座標の設定 1045 center_lat = center_lat if center_lat is not None else self._center_lat 1046 center_lon = center_lon if center_lon is not None else self._center_lon 1047 1048 # カラーマッピングの範囲を設定 1049 cmin, cmax = 0, 0 1050 if value_range is None: 1051 cmin = df_mapping[col_conc].min() 1052 cmax = df_mapping[col_conc].max() 1053 else: 1054 cmin, cmax = value_range 1055 1056 # カラーバーのタイトルを設定 1057 title_text = colorbar_title if colorbar_title is not None else col_conc 1058 1059 # Scattermapboxのデータを作成 1060 scatter_data = go.Scattermapbox( 1061 lat=df_mapping[col_lat], 1062 lon=df_mapping[col_lon], 1063 text=df_mapping[col_conc].astype(str), 1064 hoverinfo="text", 1065 mode="markers", 1066 marker=dict( 1067 color=df_mapping[col_conc], 1068 size=marker_size, 1069 reversescale=False, 1070 autocolorscale=False, 1071 colorscale=colorscale, 1072 cmin=cmin, 1073 cmax=cmax, 1074 colorbar=dict( 1075 tickformat="3.2f", 1076 outlinecolor="black", 1077 outlinewidth=1.5, 1078 ticks="outside", 1079 ticklen=7, 1080 tickwidth=1.5, 1081 tickcolor="black", 1082 tickfont=dict( 1083 family=tick_font_family, color="black", size=tick_font_size 1084 ), 1085 title=dict( 1086 text=title_text, side="top" 1087 ), # カラーバーのタイトルを設定 1088 titlefont=dict( 1089 family=title_font_family, 1090 color="black", 1091 size=title_font_size, 1092 ), 1093 ), 1094 ), 1095 ) 1096 1097 # レイアウトの設定 1098 layout = go.Layout( 1099 width=width, 1100 height=height, 1101 showlegend=False, 1102 mapbox=dict( 1103 accesstoken=mapbox_access_token, 1104 center=dict(lat=center_lat, lon=center_lon), 1105 zoom=zoom, 1106 ), 1107 ) 1108 1109 # 図の作成 1110 fig = go.Figure(data=[scatter_data], layout=layout) 1111 1112 # 図の保存 1113 if save_fig: 1114 # 保存時の出力ディレクトリチェック 1115 if output_dir is None: 1116 raise ValueError( 1117 "save_fig=Trueの場合、output_dirを指定する必要があります。" 1118 ) 1119 os.makedirs(output_dir, exist_ok=True) 1120 output_path = os.path.join(output_dir, output_filename) 1121 pyo.plot(fig, filename=output_path, auto_open=False) 1122 self.logger.info(f"Mapboxプロットを保存しました: {output_path}") 1123 # 図の表示 1124 if show_fig: 1125 pyo.iplot(fig) 1126 1127 def plot_scatter_c2c1( 1128 self, 1129 hotspots: list[HotspotData], 1130 output_dir: str | Path | None = None, 1131 output_filename: str = "scatter_c2c1.png", 1132 dpi: int = 200, 1133 figsize: tuple[int, int] = (4, 4), 1134 fontsize: float = 12, 1135 save_fig: bool = True, 1136 show_fig: bool = True, 1137 ratio_labels: dict[float, tuple[float, float, str]] | None = None, 1138 ) -> None: 1139 """ 1140 検出されたホットスポットのΔC2H6とΔCH4の散布図をプロットします。 1141 1142 Parameters: 1143 ------ 1144 hotspots : list[HotspotData] 1145 プロットするホットスポットのリスト 1146 output_dir : str | Path | None 1147 保存先のディレクトリパス 1148 output_filename : str 1149 保存するファイル名。デフォルトは"scatter_c2c1.png"。 1150 dpi : int 1151 解像度。デフォルトは200。 1152 figsize : tuple[int, int] 1153 図のサイズ。デフォルトは(4, 4)。 1154 fontsize : float 1155 フォントサイズ。デフォルトは12。 1156 save_fig : bool 1157 図の保存を許可するフラグ。デフォルトはTrue。 1158 show_fig : bool 1159 図の表示を許可するフラグ。デフォルトはTrue。 1160 ratio_labels : dict[float, tuple[float, float, str]] | None 1161 比率線とラベルの設定。 1162 キーは比率値、値は (x位置, y位置, ラベルテキスト) のタプル。 1163 Noneの場合はデフォルト設定を使用。デフォルト値: 1164 { 1165 0.001: (1.25, 2, "0.001"), 1166 0.005: (1.25, 8, "0.005"), 1167 0.010: (1.25, 15, "0.01"), 1168 0.020: (1.25, 30, "0.02"), 1169 0.030: (1.0, 40, "0.03"), 1170 0.076: (0.20, 42, "0.076 (Osaka)") 1171 } 1172 """ 1173 plt.rcParams["font.size"] = fontsize 1174 fig = plt.figure(figsize=figsize, dpi=dpi) 1175 1176 # タイプごとのデータを収集 1177 type_data: dict[HotspotType, list[tuple[float, float]]] = { 1178 "bio": [], 1179 "gas": [], 1180 "comb": [], 1181 } 1182 for spot in hotspots: 1183 type_data[spot.type].append((spot.delta_ch4, spot.delta_c2h6)) 1184 1185 # 色とラベルの定義 1186 colors: dict[HotspotType, str] = {"bio": "blue", "gas": "red", "comb": "green"} 1187 labels: dict[HotspotType, str] = {"bio": "bio", "gas": "gas", "comb": "comb"} 1188 1189 # タイプごとにプロット(データが存在する場合のみ) 1190 for spot_type, data in type_data.items(): 1191 if data: # データが存在する場合のみプロット 1192 ch4_values, c2h6_values = zip(*data) 1193 plt.plot( 1194 ch4_values, 1195 c2h6_values, 1196 "o", 1197 c=colors[spot_type], 1198 alpha=0.5, 1199 ms=2, 1200 label=labels[spot_type], 1201 ) 1202 1203 # デフォルトの比率とラベル設定 1204 default_ratio_labels = { 1205 0.001: (1.25, 2, "0.001"), 1206 0.005: (1.25, 8, "0.005"), 1207 0.010: (1.25, 15, "0.01"), 1208 0.020: (1.25, 30, "0.02"), 1209 0.030: (1.0, 40, "0.03"), 1210 0.076: (0.20, 42, "0.076 (Osaka)"), 1211 } 1212 1213 ratio_labels = ratio_labels or default_ratio_labels 1214 1215 # プロット後、軸の設定前に比率の線を追加 1216 x = np.array([0, 5]) 1217 base_ch4 = 0.0 1218 base = 0.0 1219 1220 # 各比率に対して線を引く 1221 for ratio, (x_pos, y_pos, label) in ratio_labels.items(): 1222 y = (x - base_ch4) * 1000 * ratio + base 1223 plt.plot(x, y, "-", c="black", alpha=0.5) 1224 plt.text(x_pos, y_pos, label) 1225 1226 plt.ylim(0, 50) 1227 plt.xlim(0, 2.0) 1228 plt.ylabel("Δ$\\mathregular{C_{2}H_{6}}$ (ppb)") 1229 plt.xlabel("Δ$\\mathregular{CH_{4}}$ (ppm)") 1230 plt.legend() 1231 1232 # グラフの保存または表示 1233 if save_fig: 1234 if output_dir is None: 1235 raise ValueError( 1236 "save_fig=Trueの場合、output_dirを指定する必要があります。有効なディレクトリパスを指定してください。" 1237 ) 1238 output_path: str = os.path.join(output_dir, output_filename) 1239 plt.savefig(output_path, bbox_inches="tight") 1240 self.logger.info(f"散布図を保存しました: {output_path}") 1241 if show_fig: 1242 plt.show() 1243 else: 1244 plt.close(fig=fig) 1245 1246 def plot_conc_timeseries( 1247 self, 1248 source_name: str | None = None, 1249 output_dir: str | Path | None = None, 1250 output_filename: str = "timeseries.png", 1251 dpi: int = 200, 1252 figsize: tuple[float, float] = (8, 4), 1253 save_fig: bool = True, 1254 show_fig: bool = True, 1255 col_ch4: str = "ch4_ppm", 1256 col_c2h6: str = "c2h6_ppb", 1257 col_h2o: str = "h2o_ppm", 1258 ylim_ch4: tuple[float, float] | None = None, 1259 ylim_c2h6: tuple[float, float] | None = None, 1260 ylim_h2o: tuple[float, float] | None = None, 1261 font_size: float = 12, 1262 label_pad: float = 10, 1263 ) -> None: 1264 """ 1265 時系列データをプロットします。 1266 1267 Parameters: 1268 ------ 1269 dpi : int 1270 図の解像度を指定します。デフォルトは200です。 1271 source_name : str | None 1272 プロットするデータソースの名前。Noneの場合は最初のデータソースを使用します。 1273 figsize : tuple[float, float] 1274 図のサイズを指定します。デフォルトは(8, 4)です。 1275 output_dir : str | Path | None 1276 保存先のディレクトリを指定します。save_fig=Trueの場合は必須です。 1277 output_filename : str 1278 保存するファイル名を指定します。デフォルトは"time_series.png"です。 1279 save_fig : bool 1280 図を保存するかどうかを指定します。デフォルトはFalseです。 1281 show_fig : bool 1282 図を表示するかどうかを指定します。デフォルトはTrueです。 1283 col_ch4 : str 1284 CH4データのキーを指定します。デフォルトは"ch4_ppm"です。 1285 col_c2h6 : str 1286 C2H6データのキーを指定します。デフォルトは"c2h6_ppb"です。 1287 col_h2o : str 1288 H2Oデータのキーを指定します。デフォルトは"h2o_ppm"です。 1289 ylim_ch4 : tuple[float, float] | None 1290 CH4プロットのy軸範囲を指定します。デフォルトはNoneです。 1291 ylim_c2h6 : tuple[float, float] | None 1292 C2H6プロットのy軸範囲を指定します。デフォルトはNoneです。 1293 ylim_h2o : tuple[float, float] | None 1294 H2Oプロットのy軸範囲を指定します。デフォルトはNoneです。 1295 font_size : float 1296 基本フォントサイズ。デフォルトは12。 1297 label_pad : float 1298 y軸ラベルのパディング。デフォルトは10。 1299 """ 1300 # プロットパラメータの設定 1301 plt.rcParams.update( 1302 { 1303 "font.size": font_size, 1304 "axes.labelsize": font_size, 1305 "axes.titlesize": font_size, 1306 "xtick.labelsize": font_size, 1307 "ytick.labelsize": font_size, 1308 } 1309 ) 1310 dfs_dict: dict[str, pd.DataFrame] = self._data.copy() 1311 # データソースの選択 1312 if not dfs_dict: 1313 raise ValueError("データが読み込まれていません。") 1314 1315 if source_name not in dfs_dict: 1316 raise ValueError( 1317 f"指定されたデータソース '{source_name}' が見つかりません。" 1318 ) 1319 1320 df = dfs_dict[source_name] 1321 1322 # プロットの作成 1323 fig = plt.figure(figsize=figsize, dpi=dpi) 1324 1325 # CH4プロット 1326 ax1 = fig.add_subplot(3, 1, 1) 1327 ax1.plot(df.index, df[col_ch4], c="red") 1328 if ylim_ch4: 1329 ax1.set_ylim(ylim_ch4) 1330 ax1.set_ylabel("$\\mathregular{CH_{4}}$ (ppm)", labelpad=label_pad) 1331 ax1.grid(True, alpha=0.3) 1332 1333 # C2H6プロット 1334 ax2 = fig.add_subplot(3, 1, 2) 1335 ax2.plot(df.index, df[col_c2h6], c="red") 1336 if ylim_c2h6: 1337 ax2.set_ylim(ylim_c2h6) 1338 ax2.set_ylabel("$\\mathregular{C_{2}H_{6}}$ (ppb)", labelpad=label_pad) 1339 ax2.grid(True, alpha=0.3) 1340 1341 # H2Oプロット 1342 ax3 = fig.add_subplot(3, 1, 3) 1343 ax3.plot(df.index, df[col_h2o], c="red") 1344 if ylim_h2o: 1345 ax3.set_ylim(ylim_h2o) 1346 ax3.set_ylabel("$\\mathregular{H_{2}O}$ (ppm)", labelpad=label_pad) 1347 ax3.grid(True, alpha=0.3) 1348 1349 # x軸のフォーマット調整 1350 for ax in [ax1, ax2, ax3]: 1351 ax.xaxis.set_major_formatter(mdates.DateFormatter("%H:%M")) 1352 # 軸のラベルとグリッド線の調整 1353 ax.tick_params(axis="both", which="major", labelsize=font_size) 1354 ax.grid(True, alpha=0.3) 1355 1356 # サブプロット間の間隔調整 1357 plt.subplots_adjust(wspace=0.38, hspace=0.38) 1358 1359 # 図の保存 1360 if save_fig: 1361 if output_dir is None: 1362 raise ValueError( 1363 "save_fig=Trueの場合、output_dirを指定する必要があります。有効なディレクトリパスを指定してください。" 1364 ) 1365 os.makedirs(output_dir, exist_ok=True) 1366 output_path = os.path.join(output_dir, output_filename) 1367 plt.savefig(output_path, bbox_inches="tight") 1368 1369 if show_fig: 1370 plt.show() 1371 else: 1372 plt.close(fig=fig) 1373 1374 def _detect_hotspots( 1375 self, 1376 df: pd.DataFrame, 1377 ch4_enhance_threshold: float, 1378 ) -> list[HotspotData]: 1379 """ 1380 シンプル化したホットスポット検出 1381 1382 Parameters: 1383 ------ 1384 df : pd.DataFrame 1385 入力データフレーム 1386 ch4_enhance_threshold : float 1387 CH4増加の閾値 1388 1389 Returns: 1390 ------ 1391 list[HotspotData] 1392 検出されたホットスポットのリスト 1393 """ 1394 hotspots: list[HotspotData] = [] 1395 1396 # CH4増加量が閾値を超えるデータポイントを抽出 1397 enhanced_mask = df["ch4_ppm_delta"] >= ch4_enhance_threshold 1398 1399 if enhanced_mask.any(): 1400 lat = df["latitude"][enhanced_mask] 1401 lon = df["longitude"][enhanced_mask] 1402 ratios = df["c2c1_ratio_delta"][enhanced_mask] 1403 delta_ch4 = df["ch4_ppm_delta"][enhanced_mask] 1404 delta_c2h6 = df["c2h6_ppb_delta"][enhanced_mask] 1405 1406 # 各ポイントに対してホットスポットを作成 1407 for i in range(len(lat)): 1408 if pd.notna(ratios.iloc[i]): 1409 current_lat = lat.iloc[i] 1410 current_lon = lon.iloc[i] 1411 correlation = df["ch4_c2h6_correlation"].iloc[i] 1412 1413 # 比率に基づいてタイプを決定 1414 spot_type: HotspotType = "bio" 1415 if ratios.iloc[i] >= 100: 1416 spot_type = "comb" 1417 elif ratios.iloc[i] >= 5: 1418 spot_type = "gas" 1419 1420 angle: float = MobileSpatialAnalyzer._calculate_angle( 1421 lat=current_lat, 1422 lon=current_lon, 1423 center_lat=self._center_lat, 1424 center_lon=self._center_lon, 1425 ) 1426 section: int = self._determine_section(angle) 1427 1428 hotspots.append( 1429 HotspotData( 1430 source=ratios.index[i].strftime("%Y-%m-%d %H:%M:%S"), 1431 angle=angle, 1432 avg_lat=current_lat, 1433 avg_lon=current_lon, 1434 delta_ch4=delta_ch4.iloc[i], 1435 delta_c2h6=delta_c2h6.iloc[i], 1436 correlation=max(-1, min(1, correlation)), 1437 ratio=ratios.iloc[i], 1438 section=section, 1439 type=spot_type, 1440 ) 1441 ) 1442 1443 return hotspots 1444 1445 def _determine_section(self, angle: float) -> int: 1446 """ 1447 角度に基づいて所属する区画を特定します。 1448 1449 Parameters: 1450 ------ 1451 angle : float 1452 計算された角度 1453 1454 Returns: 1455 ------ 1456 int 1457 区画番号(0-based-index) 1458 """ 1459 for section_num, (start, end) in self._sections.items(): 1460 if start <= angle < end: 1461 return section_num 1462 # -180度の場合は最後の区画に含める 1463 return self._num_sections - 1 1464 1465 def _load_all_data( 1466 self, input_configs: list[MSAInputConfig] 1467 ) -> dict[str, pd.DataFrame]: 1468 """ 1469 全入力ファイルのデータを読み込み、データフレームの辞書を返します。 1470 1471 このメソッドは、指定された入力設定に基づいてすべてのデータファイルを読み込み、 1472 各ファイルのデータをデータフレームとして格納した辞書を生成します。 1473 1474 Parameters: 1475 ------ 1476 input_configs : list[MSAInputConfig] 1477 読み込むファイルの設定リスト。 1478 1479 Returns: 1480 ------ 1481 dict[str, pd.DataFrame] 1482 読み込まれたデータフレームの辞書。キーはファイル名、値はデータフレーム。 1483 """ 1484 all_data: dict[str, pd.DataFrame] = {} 1485 for config in input_configs: 1486 df, source_name = self._load_data(config) 1487 all_data[source_name] = df 1488 return all_data 1489 1490 def _load_data( 1491 self, 1492 config: MSAInputConfig, 1493 columns_to_shift: list[str] = ["ch4_ppm", "c2h6_ppb", "h2o_ppm"], 1494 ) -> tuple[pd.DataFrame, str]: 1495 """ 1496 測定データを読み込み、前処理を行うメソッド。 1497 1498 Parameters: 1499 ------ 1500 config : MSAInputConfig 1501 入力ファイルの設定を含むオブジェクト。ファイルパス、遅れ時間、サンプリング周波数、補正タイプなどの情報を持つ。 1502 columns_to_shift : list[str], optional 1503 シフトを適用するカラム名のリスト。デフォルトは["ch4_ppm", "c2h6_ppb", "h2o_ppm"]で、これらのカラムに対して遅れ時間の補正が行われる。 1504 1505 Returns: 1506 ------ 1507 tuple[pd.DataFrame, str] 1508 読み込まれたデータフレームとそのソース名を含むタプル。データフレームは前処理が施されており、ソース名はファイル名から抽出されたもの。 1509 """ 1510 source_name: str = MobileSpatialAnalyzer.extract_source_name_from_path( 1511 config.path 1512 ) 1513 df: pd.DataFrame = pd.read_csv(config.path, na_values=["No Data", "nan"]) 1514 1515 # カラム名の標準化(測器に依存しない汎用的な名前に変更) 1516 df = df.rename(columns=self._column_mapping) 1517 df["timestamp"] = pd.to_datetime(df["timestamp"]) 1518 # インデックスを設定(元のtimestampカラムは保持) 1519 df = df.set_index("timestamp", drop=False) 1520 1521 # 緯度経度のnanを削除 1522 df = df.dropna(subset=["latitude", "longitude"]) 1523 1524 if config.lag < 0: 1525 raise ValueError( 1526 f"Invalid lag value: {config.lag}. Must be a non-negative float." 1527 ) 1528 1529 # サンプリング周波数に応じてシフト量を調整 1530 shift_periods: float = -config.lag * config.fs # fsを掛けて補正 1531 1532 # 遅れ時間の補正 1533 for col in columns_to_shift: 1534 df[col] = df[col].shift(shift_periods) 1535 1536 df = df.dropna(subset=columns_to_shift) 1537 1538 # 水蒸気干渉などの補正式を適用 1539 if config.correction_type is not None: 1540 df = CorrectingUtils.correct_df_by_type(df, config.correction_type) 1541 else: 1542 self.logger.warn( 1543 f"'correction_type' is None, so no correction functions will be applied. Source: {source_name}" 1544 ) 1545 1546 return df, source_name 1547 1548 @staticmethod 1549 def _calculate_angle( 1550 lat: float, lon: float, center_lat: float, center_lon: float 1551 ) -> float: 1552 """ 1553 中心からの角度を計算 1554 1555 Parameters: 1556 ------ 1557 lat : float 1558 対象地点の緯度 1559 lon : float 1560 対象地点の経度 1561 center_lat : float 1562 中心の緯度 1563 center_lon : float 1564 中心の経度 1565 1566 Returns: 1567 ------ 1568 float 1569 真北を0°として時計回りの角度(-180°から180°) 1570 """ 1571 d_lat: float = lat - center_lat 1572 d_lon: float = lon - center_lon 1573 # arctanを使用して角度を計算(ラジアン) 1574 angle_rad: float = math.atan2(d_lon, d_lat) 1575 # ラジアンから度に変換(-180から180の範囲) 1576 angle_deg: float = math.degrees(angle_rad) 1577 return angle_deg 1578 1579 @classmethod 1580 def _calculate_distance( 1581 cls, lat1: float, lon1: float, lat2: float, lon2: float 1582 ) -> float: 1583 """ 1584 2点間の距離をメートル単位で計算(Haversine formula) 1585 1586 Parameters: 1587 ------ 1588 lat1 : float 1589 地点1の緯度 1590 lon1 : float 1591 地点1の経度 1592 lat2 : float 1593 地点2の緯度 1594 lon2 : float 1595 地点2の経度 1596 1597 Returns: 1598 ------ 1599 float 1600 2地点間の距離(メートル) 1601 """ 1602 R = cls.EARTH_RADIUS_METERS 1603 1604 # 緯度経度をラジアンに変換 1605 lat1_rad: float = math.radians(lat1) 1606 lon1_rad: float = math.radians(lon1) 1607 lat2_rad: float = math.radians(lat2) 1608 lon2_rad: float = math.radians(lon2) 1609 1610 # 緯度と経度の差分 1611 dlat: float = lat2_rad - lat1_rad 1612 dlon: float = lon2_rad - lon1_rad 1613 1614 # Haversine formula 1615 a: float = ( 1616 math.sin(dlat / 2) ** 2 1617 + math.cos(lat1_rad) * math.cos(lat2_rad) * math.sin(dlon / 2) ** 2 1618 ) 1619 c: float = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a)) 1620 1621 return R * c # メートル単位での距離 1622 1623 @staticmethod 1624 def _calculate_hotspots_parameters( 1625 df: pd.DataFrame, 1626 window_size: int, 1627 col_ch4_ppm: str = "ch4_ppm", 1628 col_c2h6_ppb: str = "c2h6_ppb", 1629 col_h2o_ppm: str = "h2o_ppm", 1630 ch4_threshold: float = 0.05, 1631 c2h6_threshold: float = 0.0, 1632 use_quantile: bool = True, 1633 ) -> pd.DataFrame: 1634 """ 1635 ホットスポットのパラメータを計算します。 1636 このメソッドは、指定されたデータフレームに対して移動平均(または5パーセンタイル)や相関を計算し、 1637 各種のデルタ値や比率を追加します。 1638 1639 Parameters: 1640 ------ 1641 df : pd.DataFrame 1642 入力データフレーム 1643 window_size : int 1644 移動窓のサイズ 1645 col_ch4_ppm : str 1646 CH4濃度を示すカラム名 1647 col_c2h6_ppb : str 1648 C2H6濃度を示すカラム名 1649 col_h2o_ppm : str 1650 H2O濃度を示すカラム名 1651 ch4_threshold : float 1652 CH4の閾値 1653 c2h6_threshold : float 1654 C2H6の閾値 1655 use_quantile : bool 1656 Trueの場合は5パーセンタイルを使用、Falseの場合は移動平均を使用 1657 1658 Returns: 1659 ------ 1660 pd.DataFrame 1661 計算されたパラメータを含むデータフレーム 1662 """ 1663 # データのコピーを作成 1664 df = df.copy() 1665 1666 # 移動相関の計算 1667 df["ch4_c2h6_correlation"] = ( 1668 df[col_ch4_ppm].rolling(window=window_size).corr(df[col_c2h6_ppb]) 1669 ) 1670 1671 # バックグラウンド値の計算(5パーセンタイルまたは移動平均) 1672 if use_quantile: 1673 df["ch4_ppm_mv"] = ( 1674 df[col_ch4_ppm] 1675 .rolling(window=window_size, center=True, min_periods=1) 1676 .quantile(0.05) 1677 ) 1678 df["c2h6_ppb_mv"] = ( 1679 df[col_c2h6_ppb] 1680 .rolling(window=window_size, center=True, min_periods=1) 1681 .quantile(0.05) 1682 ) 1683 else: 1684 df["ch4_ppm_mv"] = ( 1685 df[col_ch4_ppm] 1686 .rolling(window=window_size, center=True, min_periods=1) 1687 .mean() 1688 ) 1689 df["c2h6_ppb_mv"] = ( 1690 df[col_c2h6_ppb] 1691 .rolling(window=window_size, center=True, min_periods=1) 1692 .mean() 1693 ) 1694 1695 # デルタ値の計算 1696 df["ch4_ppm_delta"] = df[col_ch4_ppm] - df["ch4_ppm_mv"] 1697 df["c2h6_ppb_delta"] = df[col_c2h6_ppb] - df["c2h6_ppb_mv"] 1698 1699 # C2H6/CH4の比率計算 1700 df["c2c1_ratio"] = df[col_c2h6_ppb] / df[col_ch4_ppm] 1701 1702 # デルタ値に基づく比の計算とフィルタリング 1703 df["c2c1_ratio_delta"] = df["c2h6_ppb_delta"] / df["ch4_ppm_delta"] 1704 1705 # フィルタリング条件の適用 1706 df.loc[df["ch4_ppm_delta"] < ch4_threshold, "c2c1_ratio_delta"] = np.nan 1707 df.loc[df["c2h6_ppb_delta"] < -10.0, "c2h6_ppb_delta"] = np.nan 1708 df.loc[df["c2h6_ppb_delta"] > 1000.0, "c2h6_ppb_delta"] = np.nan 1709 df.loc[df["c2h6_ppb_delta"] < c2h6_threshold, "c2c1_ratio_delta"] = 0.0 1710 1711 # 水蒸気濃度によるフィルタリング 1712 df.loc[df[col_h2o_ppm] < 2000, [col_ch4_ppm, col_c2h6_ppb]] = np.nan 1713 1714 # 欠損値の除去 1715 df = df.dropna(subset=[col_ch4_ppm]) 1716 1717 return df 1718 1719 @staticmethod 1720 def _calculate_window_size(window_minutes: float) -> int: 1721 """ 1722 時間窓からデータポイント数を計算 1723 1724 Parameters: 1725 ------ 1726 window_minutes : float 1727 時間窓の大きさ(分) 1728 1729 Returns: 1730 ------ 1731 int 1732 データポイント数 1733 """ 1734 return int(60 * window_minutes) 1735 1736 @staticmethod 1737 def _initialize_sections( 1738 num_sections: int, section_size: float 1739 ) -> dict[int, tuple[float, float]]: 1740 """ 1741 指定された区画数と区画サイズに基づいて、区画の範囲を初期化します。 1742 1743 Parameters: 1744 ------ 1745 num_sections : int 1746 初期化する区画の数。 1747 section_size : float 1748 各区画の角度範囲のサイズ。 1749 1750 Returns: 1751 ------ 1752 dict[int, tuple[float, float]] 1753 区画番号(0-based-index)とその範囲の辞書。各区画は-180度から180度の範囲に分割されます。 1754 """ 1755 sections: dict[int, tuple[float, float]] = {} 1756 for i in range(num_sections): 1757 # -180から180の範囲で区画を設定 1758 start_angle = -180 + i * section_size 1759 end_angle = -180 + (i + 1) * section_size 1760 sections[i] = (start_angle, end_angle) 1761 return sections 1762 1763 @staticmethod 1764 def _is_duplicate_spot( 1765 current_lat: float, 1766 current_lon: float, 1767 current_time: str, 1768 used_positions: list[tuple[float, float, str, float]], 1769 check_time_all: bool, 1770 min_time_threshold_seconds: float, 1771 max_time_threshold_hours: float, 1772 hotspot_area_meter: float, 1773 ) -> bool: 1774 """ 1775 与えられた地点が既存の地点と重複しているかを判定します。 1776 1777 Parameters: 1778 ------ 1779 current_lat : float 1780 判定する地点の緯度 1781 current_lon : float 1782 判定する地点の経度 1783 current_time : str 1784 判定する地点の時刻 1785 used_positions : list[tuple[float, float, str, float]] 1786 既存の地点情報のリスト (lat, lon, time, value) 1787 check_time_all : bool 1788 時間に関係なく重複チェックを行うかどうか 1789 min_time_threshold_seconds : float 1790 重複とみなす最小時間の閾値(秒) 1791 max_time_threshold_hours : float 1792 重複チェックを一時的に無視する最大時間の閾値(時間) 1793 hotspot_area_meter : float 1794 重複とみなす距離の閾値(m) 1795 1796 Returns: 1797 ------ 1798 bool 1799 重複している場合はTrue、そうでない場合はFalse 1800 """ 1801 for used_lat, used_lon, used_time, _ in used_positions: 1802 # 距離チェック 1803 distance = MobileSpatialAnalyzer._calculate_distance( 1804 lat1=current_lat, lon1=current_lon, lat2=used_lat, lon2=used_lon 1805 ) 1806 1807 if distance < hotspot_area_meter: 1808 # 時間差の計算(秒単位) 1809 time_diff = pd.Timedelta( 1810 pd.to_datetime(current_time) - pd.to_datetime(used_time) 1811 ).total_seconds() 1812 time_diff_abs = abs(time_diff) 1813 1814 if check_time_all: 1815 # 時間に関係なく、距離が近ければ重複とみなす 1816 return True 1817 else: 1818 # 時間窓による判定を行う 1819 if time_diff_abs <= min_time_threshold_seconds: 1820 # Case 1: 最小時間閾値以内は重複とみなす 1821 return True 1822 elif time_diff_abs > max_time_threshold_hours * 3600: 1823 # Case 2: 最大時間閾値を超えた場合は重複チェックをスキップ 1824 continue 1825 # Case 3: その間の時間差の場合は、距離が近ければ重複とみなす 1826 return True 1827 1828 return False 1829 1830 @staticmethod 1831 def _normalize_inputs( 1832 inputs: list[MSAInputConfig] | list[tuple[float, float, str | Path]], 1833 ) -> list[MSAInputConfig]: 1834 """ 1835 入力設定を標準化 1836 1837 Parameters: 1838 ------ 1839 inputs : list[MSAInputConfig] | list[tuple[float, float, str | Path]] 1840 入力設定のリスト 1841 1842 Returns: 1843 ------ 1844 list[MSAInputConfig] 1845 標準化された入力設定のリスト 1846 """ 1847 normalized: list[MSAInputConfig] = [] 1848 for inp in inputs: 1849 if isinstance(inp, MSAInputConfig): 1850 normalized.append(inp) # すでに検証済みのため、そのまま追加 1851 else: 1852 fs, lag, path = inp 1853 normalized.append( 1854 MSAInputConfig.validate_and_create(fs=fs, lag=lag, path=path) 1855 ) 1856 return normalized 1857 1858 def remove_c2c1_ratio_duplicates( 1859 self, 1860 df: pd.DataFrame, 1861 min_time_threshold_seconds: float = 300, # 5分以内は重複とみなす 1862 max_time_threshold_hours: float = 12.0, # 12時間以上離れている場合は別のポイントとして扱う 1863 check_time_all: bool = True, # 時間閾値を超えた場合の重複チェックを継続するかどうか 1864 hotspot_area_meter: float = 50.0, # 重複とみなす距離の閾値(メートル) 1865 col_ch4_ppm: str = "ch4_ppm", 1866 col_ch4_ppm_mv: str = "ch4_ppm_mv", 1867 col_ch4_ppm_delta: str = "ch4_ppm_delta", 1868 ): 1869 """ 1870 メタン濃度の増加が閾値を超えた地点から、重複を除外してユニークなホットスポットを抽出する関数。 1871 1872 Parameters: 1873 ------ 1874 df : pandas.DataFrame 1875 入力データフレーム。必須カラム: 1876 - ch4_ppm: メタン濃度(ppm) 1877 - ch4_ppm_mv: メタン濃度の移動平均(ppm) 1878 - ch4_ppm_delta: メタン濃度の増加量(ppm) 1879 - latitude: 緯度 1880 - longitude: 経度 1881 min_time_threshold_seconds : float, optional 1882 重複とみなす最小時間差(秒)。デフォルトは300秒(5分)。 1883 max_time_threshold_hours : float, optional 1884 別ポイントとして扱う最大時間差(時間)。デフォルトは12時間。 1885 check_time_all : bool, optional 1886 時間閾値を超えた場合の重複チェックを継続するかどうか。デフォルトはTrue。 1887 hotspot_area_meter : float, optional 1888 重複とみなす距離の閾値(メートル)。デフォルトは50メートル。 1889 1890 Returns: 1891 ------ 1892 pandas.DataFrame 1893 ユニークなホットスポットのデータフレーム。 1894 """ 1895 df_data: pd.DataFrame = df.copy() 1896 # メタン濃度の増加が閾値を超えた点を抽出 1897 mask = ( 1898 df_data[col_ch4_ppm] - df_data[col_ch4_ppm_mv] > self._ch4_enhance_threshold 1899 ) 1900 hotspot_candidates = df_data[mask].copy() 1901 1902 # ΔCH4の降順でソート 1903 sorted_hotspots = hotspot_candidates.sort_values( 1904 by=col_ch4_ppm_delta, ascending=False 1905 ) 1906 used_positions = [] 1907 unique_hotspots = pd.DataFrame() 1908 1909 for _, spot in sorted_hotspots.iterrows(): 1910 should_add = True 1911 for used_lat, used_lon, used_time in used_positions: 1912 # 距離チェック 1913 distance = geodesic( 1914 (spot.latitude, spot.longitude), (used_lat, used_lon) 1915 ).meters 1916 1917 if distance < hotspot_area_meter: 1918 # 時間差の計算(秒単位) 1919 time_diff = pd.Timedelta( 1920 spot.name - pd.to_datetime(used_time) 1921 ).total_seconds() 1922 time_diff_abs = abs(time_diff) 1923 1924 # 時間差に基づく判定 1925 if check_time_all: 1926 # 時間に関係なく、距離が近ければ重複とみなす 1927 # ΔCH4が大きい方を残す(現在のスポットは必ず小さい) 1928 should_add = False 1929 break 1930 else: 1931 # 時間窓による判定を行う 1932 if time_diff_abs <= min_time_threshold_seconds: 1933 # Case 1: 最小時間閾値以内は重複とみなす 1934 should_add = False 1935 break 1936 elif time_diff_abs > max_time_threshold_hours * 3600: 1937 # Case 2: 最大時間閾値を超えた場合は重複チェックをスキップ 1938 continue 1939 # Case 3: その間の時間差の場合は、距離が近ければ重複とみなす 1940 should_add = False 1941 break 1942 1943 if should_add: 1944 unique_hotspots = pd.concat([unique_hotspots, pd.DataFrame([spot])]) 1945 used_positions.append((spot.latitude, spot.longitude, spot.name)) 1946 1947 return unique_hotspots 1948 1949 @staticmethod 1950 def remove_hotspots_duplicates( 1951 hotspots: list[HotspotData], 1952 check_time_all: bool, 1953 min_time_threshold_seconds: float = 300, 1954 max_time_threshold_hours: float = 12, 1955 hotspot_area_meter: float = 50, 1956 ) -> list[HotspotData]: 1957 """ 1958 重複するホットスポットを除外します。 1959 1960 このメソッドは、与えられたホットスポットのリストから重複を検出し、 1961 一意のホットスポットのみを返します。重複の判定は、指定された 1962 時間および距離の閾値に基づいて行われます。 1963 1964 Parameters: 1965 ------ 1966 hotspots : list[HotspotData] 1967 重複を除外する対象のホットスポットのリスト。 1968 check_time_all : bool 1969 時間に関係なく重複チェックを行うかどうか。 1970 min_time_threshold_seconds : float 1971 重複とみなす最小時間の閾値(秒)。 1972 max_time_threshold_hours : float 1973 重複チェックを一時的に無視する最大時間の閾値(時間)。 1974 hotspot_area_meter : float 1975 重複とみなす距離の閾値(メートル)。 1976 1977 Returns: 1978 ------ 1979 list[HotspotData] 1980 重複を除去したホットスポットのリスト。 1981 """ 1982 # ΔCH4の降順でソート 1983 sorted_hotspots: list[HotspotData] = sorted( 1984 hotspots, key=lambda x: x.delta_ch4, reverse=True 1985 ) 1986 used_positions_by_type: dict[ 1987 HotspotType, list[tuple[float, float, str, float]] 1988 ] = { 1989 "bio": [], 1990 "gas": [], 1991 "comb": [], 1992 } 1993 unique_hotspots: list[HotspotData] = [] 1994 1995 for spot in sorted_hotspots: 1996 is_duplicate = MobileSpatialAnalyzer._is_duplicate_spot( 1997 current_lat=spot.avg_lat, 1998 current_lon=spot.avg_lon, 1999 current_time=spot.source, 2000 used_positions=used_positions_by_type[spot.type], 2001 check_time_all=check_time_all, 2002 min_time_threshold_seconds=min_time_threshold_seconds, 2003 max_time_threshold_hours=max_time_threshold_hours, 2004 hotspot_area_meter=hotspot_area_meter, 2005 ) 2006 2007 if not is_duplicate: 2008 unique_hotspots.append(spot) 2009 used_positions_by_type[spot.type].append( 2010 (spot.avg_lat, spot.avg_lon, spot.source, spot.delta_ch4) 2011 ) 2012 2013 return unique_hotspots 2014 2015 @staticmethod 2016 def setup_logger(logger: Logger | None, log_level: int = INFO) -> Logger: 2017 """ 2018 ロガーを設定します。 2019 2020 このメソッドは、ロギングの設定を行い、ログメッセージのフォーマットを指定します。 2021 ログメッセージには、日付、ログレベル、メッセージが含まれます。 2022 2023 渡されたロガーがNoneまたは不正な場合は、新たにロガーを作成し、標準出力に 2024 ログメッセージが表示されるようにStreamHandlerを追加します。ロガーのレベルは 2025 引数で指定されたlog_levelに基づいて設定されます。 2026 2027 Parameters: 2028 ------ 2029 logger : Logger | None 2030 使用するロガー。Noneの場合は新しいロガーを作成します。 2031 log_level : int 2032 ロガーのログレベル。デフォルトはINFO。 2033 2034 Returns: 2035 ------ 2036 Logger 2037 設定されたロガーオブジェクト。 2038 """ 2039 if logger is not None and isinstance(logger, Logger): 2040 return logger 2041 # 渡されたロガーがNoneまたは正しいものでない場合は独自に設定 2042 new_logger: Logger = getLogger() 2043 # 既存のハンドラーをすべて削除 2044 for handler in new_logger.handlers[:]: 2045 new_logger.removeHandler(handler) 2046 new_logger.setLevel(log_level) # ロガーのレベルを設定 2047 ch = StreamHandler() 2048 ch_formatter = Formatter("%(asctime)s - %(levelname)s - %(message)s") 2049 ch.setFormatter(ch_formatter) # フォーマッターをハンドラーに設定 2050 new_logger.addHandler(ch) # StreamHandlerの追加 2051 return new_logger 2052 2053 @staticmethod 2054 def calculate_emission_rates( 2055 hotspots: list[HotspotData], 2056 method: Literal["weller", "weitzel", "joo", "umezawa"] = "weller", 2057 print_summary: bool = True, 2058 custom_formulas: dict[str, dict[str, float]] | None = None, 2059 ) -> tuple[list[EmissionData], dict[str, dict[str, float]]]: 2060 """ 2061 検出されたホットスポットのCH4漏出量を計算・解析し、統計情報を生成します。 2062 2063 Parameters: 2064 ------ 2065 hotspots : list[HotspotData] 2066 分析対象のホットスポットのリスト 2067 method : Literal["weller", "weitzel", "joo", "umezawa"] 2068 使用する計算式。デフォルトは"weller"。 2069 print_summary : bool 2070 統計情報を表示するかどうか。デフォルトはTrue。 2071 custom_formulas : dict[str, dict[str, float]] | None 2072 カスタム計算式の係数。 2073 例: {"custom_method": {"a": 1.0, "b": 1.0}} 2074 Noneの場合はデフォルトの計算式を使用。 2075 2076 Returns: 2077 ------ 2078 tuple[list[EmissionData], dict[str, dict[str, float]]] 2079 - 各ホットスポットの排出量データを含むリスト 2080 - タイプ別の統計情報を含む辞書 2081 """ 2082 # デフォルトの経験式係数 2083 default_formulas = { 2084 "weller": {"a": 0.988, "b": 0.817}, 2085 "weitzel": {"a": 0.521, "b": 0.795}, 2086 "joo": {"a": 2.738, "b": 1.329}, 2087 "umezawa": {"a": 2.716, "b": 0.741}, 2088 } 2089 2090 # カスタム計算式がある場合は追加 2091 emission_formulas = default_formulas.copy() 2092 if custom_formulas: 2093 emission_formulas.update(custom_formulas) 2094 2095 if method not in emission_formulas: 2096 raise ValueError(f"Unknown method: {method}") 2097 2098 # 係数の取得 2099 a = emission_formulas[method]["a"] 2100 b = emission_formulas[method]["b"] 2101 2102 # 排出量の計算 2103 emission_data_list = [] 2104 for spot in hotspots: 2105 # 漏出量の計算 (L/min) 2106 emission_rate = np.exp((np.log(spot.delta_ch4) + a) / b) 2107 # 日排出量 (L/day) 2108 daily_emission = emission_rate * 60 * 24 2109 # 年間排出量 (L/year) 2110 annual_emission = daily_emission * 365 2111 2112 emission_data = EmissionData( 2113 source=spot.source, 2114 type=spot.type, 2115 section=spot.section, 2116 latitude=spot.avg_lat, 2117 longitude=spot.avg_lon, 2118 delta_ch4=spot.delta_ch4, 2119 delta_c2h6=spot.delta_c2h6, 2120 ratio=spot.ratio, 2121 emission_rate=emission_rate, 2122 daily_emission=daily_emission, 2123 annual_emission=annual_emission, 2124 ) 2125 emission_data_list.append(emission_data) 2126 2127 # 統計計算用にDataFrameを作成 2128 emission_df = pd.DataFrame([e.to_dict() for e in emission_data_list]) 2129 2130 # タイプ別の統計情報を計算 2131 stats = {} 2132 # emission_formulas の定義の後に、排出量カテゴリーの閾値を定義 2133 emission_categories = { 2134 "low": {"min": 0, "max": 6}, # < 6 L/min 2135 "medium": {"min": 6, "max": 40}, # 6-40 L/min 2136 "high": {"min": 40, "max": float("inf")}, # > 40 L/min 2137 } 2138 # get_args(HotspotType)を使用して型安全なリストを作成 2139 types = list(get_args(HotspotType)) 2140 for spot_type in types: 2141 df_type = emission_df[emission_df["type"] == spot_type] 2142 if len(df_type) > 0: 2143 # 既存の統計情報を計算 2144 type_stats = { 2145 "count": len(df_type), 2146 "emission_rate_min": df_type["emission_rate"].min(), 2147 "emission_rate_max": df_type["emission_rate"].max(), 2148 "emission_rate_mean": df_type["emission_rate"].mean(), 2149 "emission_rate_median": df_type["emission_rate"].median(), 2150 "total_annual_emission": df_type["annual_emission"].sum(), 2151 "mean_annual_emission": df_type["annual_emission"].mean(), 2152 } 2153 2154 # 排出量カテゴリー別の統計を追加 2155 category_counts = { 2156 "low": len( 2157 df_type[ 2158 df_type["emission_rate"] < emission_categories["low"]["max"] 2159 ] 2160 ), 2161 "medium": len( 2162 df_type[ 2163 ( 2164 df_type["emission_rate"] 2165 >= emission_categories["medium"]["min"] 2166 ) 2167 & ( 2168 df_type["emission_rate"] 2169 < emission_categories["medium"]["max"] 2170 ) 2171 ] 2172 ), 2173 "high": len( 2174 df_type[ 2175 df_type["emission_rate"] 2176 >= emission_categories["high"]["min"] 2177 ] 2178 ), 2179 } 2180 type_stats["emission_categories"] = category_counts 2181 2182 stats[spot_type] = type_stats 2183 2184 if print_summary: 2185 print(f"\n{spot_type}タイプの統計情報:") 2186 print(f" 検出数: {type_stats['count']}") 2187 print(" 排出量 (L/min):") 2188 print(f" 最小値: {type_stats['emission_rate_min']:.2f}") 2189 print(f" 最大値: {type_stats['emission_rate_max']:.2f}") 2190 print(f" 平均値: {type_stats['emission_rate_mean']:.2f}") 2191 print(f" 中央値: {type_stats['emission_rate_median']:.2f}") 2192 print(" 排出量カテゴリー別の検出数:") 2193 print(f" 低放出 (< 6 L/min): {category_counts['low']}") 2194 print(f" 中放出 (6-40 L/min): {category_counts['medium']}") 2195 print(f" 高放出 (> 40 L/min): {category_counts['high']}") 2196 print(" 年間排出量 (L/year):") 2197 print(f" 合計: {type_stats['total_annual_emission']:.2f}") 2198 print(f" 平均: {type_stats['mean_annual_emission']:.2f}") 2199 2200 return emission_data_list, stats 2201 2202 @staticmethod 2203 def plot_emission_analysis( 2204 emission_data_list: list[EmissionData], 2205 dpi: int = 300, 2206 output_dir: str | Path | None = None, 2207 output_filename: str = "emission_analysis.png", 2208 figsize: tuple[float, float] = (12, 5), 2209 add_legend: bool = True, 2210 hist_log_y: bool = False, 2211 hist_xlim: tuple[float, float] | None = None, 2212 hist_ylim: tuple[float, float] | None = None, 2213 scatter_xlim: tuple[float, float] | None = None, 2214 scatter_ylim: tuple[float, float] | None = None, 2215 hist_bin_width: float = 0.5, 2216 print_summary: bool = True, 2217 save_fig: bool = False, 2218 show_fig: bool = True, 2219 show_scatter: bool = True, # 散布図の表示を制御するオプションを追加 2220 ) -> None: 2221 """ 2222 排出量分析のプロットを作成する静的メソッド。 2223 2224 Parameters: 2225 ------ 2226 emission_data_list : list[EmissionData] 2227 EmissionDataオブジェクトのリスト。 2228 output_dir : str | Path | None 2229 出力先ディレクトリのパス。 2230 output_filename : str 2231 保存するファイル名。デフォルトは"emission_analysis.png"。 2232 dpi : int 2233 プロットの解像度。デフォルトは300。 2234 figsize : tuple[float, float] 2235 プロットのサイズ。デフォルトは(12, 5)。 2236 add_legend : bool 2237 凡例を追加するかどうか。デフォルトはTrue。 2238 hist_log_y : bool 2239 ヒストグラムのy軸を対数スケールにするかどうか。デフォルトはFalse。 2240 hist_xlim : tuple[float, float] | None 2241 ヒストグラムのx軸の範囲。デフォルトはNone。 2242 hist_ylim : tuple[float, float] | None 2243 ヒストグラムのy軸の範囲。デフォルトはNone。 2244 scatter_xlim : tuple[float, float] | None 2245 散布図のx軸の範囲。デフォルトはNone。 2246 scatter_ylim : tuple[float, float] | None 2247 散布図のy軸の範囲。デフォルトはNone。 2248 hist_bin_width : float 2249 ヒストグラムのビンの幅。デフォルトは0.5。 2250 print_summary : bool 2251 集計結果を表示するかどうか。デフォルトはFalse。 2252 save_fig : bool 2253 図をファイルに保存するかどうか。デフォルトはFalse。 2254 show_fig : bool 2255 図を表示するかどうか。デフォルトはTrue。 2256 show_scatter : bool 2257 散布図(右図)を表示するかどうか。デフォルトはTrue。 2258 """ 2259 # データをDataFrameに変換 2260 df = pd.DataFrame([e.to_dict() for e in emission_data_list]) 2261 2262 # プロットの作成(散布図の有無に応じてサブプロット数を調整) 2263 if show_scatter: 2264 fig, (ax1, ax2) = plt.subplots(1, 2, figsize=figsize) 2265 axes = [ax1, ax2] 2266 else: 2267 fig, ax1 = plt.subplots(1, 1, figsize=(figsize[0] // 2, figsize[1])) 2268 axes = [ax1] 2269 2270 # カラーマップの定義 2271 colors: dict[HotspotType, str] = {"bio": "blue", "gas": "red", "comb": "green"} 2272 2273 # 存在するタイプを確認 2274 # HotspotTypeの定義順を基準にソート 2275 hotspot_types = list(get_args(HotspotType)) 2276 existing_types = sorted( 2277 df["type"].unique(), key=lambda x: hotspot_types.index(x) 2278 ) 2279 2280 # 左側: ヒストグラム 2281 # ビンの範囲を設定 2282 start = 0 # 必ず0から開始 2283 if hist_xlim is not None: 2284 end = hist_xlim[1] 2285 else: 2286 end = np.ceil(df["emission_rate"].max() * 1.05) 2287 2288 # ビン数を計算(end値をbin_widthで割り切れるように調整) 2289 n_bins = int(np.ceil(end / hist_bin_width)) 2290 end = n_bins * hist_bin_width 2291 2292 # ビンの生成(0から開始し、bin_widthの倍数で区切る) 2293 bins = np.linspace(start, end, n_bins + 1) 2294 2295 # タイプごとにヒストグラムを積み上げ 2296 bottom = np.zeros(len(bins) - 1) 2297 for spot_type in existing_types: 2298 data = df[df["type"] == spot_type]["emission_rate"] 2299 if len(data) > 0: 2300 counts, _ = np.histogram(data, bins=bins) 2301 ax1.bar( 2302 bins[:-1], 2303 counts, 2304 width=hist_bin_width, 2305 bottom=bottom, 2306 alpha=0.6, 2307 label=spot_type, 2308 color=colors[spot_type], 2309 ) 2310 bottom += counts 2311 2312 ax1.set_xlabel("CH$_4$ Emission (L min$^{-1}$)") 2313 ax1.set_ylabel("Frequency") 2314 if hist_log_y: 2315 # ax1.set_yscale("log") 2316 # 非線形スケールを設定(linthreshで線形から対数への遷移点を指定) 2317 ax1.set_yscale("symlog", linthresh=1.0) 2318 if hist_xlim is not None: 2319 ax1.set_xlim(hist_xlim) 2320 else: 2321 ax1.set_xlim(0, np.ceil(df["emission_rate"].max() * 1.05)) 2322 2323 if hist_ylim is not None: 2324 ax1.set_ylim(hist_ylim) 2325 else: 2326 ax1.set_ylim(0, ax1.get_ylim()[1]) # 下限を0に設定 2327 2328 if show_scatter: 2329 # 右側: 散布図 2330 for spot_type in existing_types: 2331 mask = df["type"] == spot_type 2332 ax2.scatter( 2333 df[mask]["emission_rate"], 2334 df[mask]["delta_ch4"], 2335 alpha=0.6, 2336 label=spot_type, 2337 color=colors[spot_type], 2338 ) 2339 2340 ax2.set_xlabel("Emission Rate (L min$^{-1}$)") 2341 ax2.set_ylabel("ΔCH$_4$ (ppm)") 2342 if scatter_xlim is not None: 2343 ax2.set_xlim(scatter_xlim) 2344 else: 2345 ax2.set_xlim(0, np.ceil(df["emission_rate"].max() * 1.05)) 2346 2347 if scatter_ylim is not None: 2348 ax2.set_ylim(scatter_ylim) 2349 else: 2350 ax2.set_ylim(0, np.ceil(df["delta_ch4"].max() * 1.05)) 2351 2352 # 凡例の表示 2353 if add_legend: 2354 for ax in axes: 2355 ax.legend( 2356 bbox_to_anchor=(0.5, -0.30), 2357 loc="upper center", 2358 ncol=len(existing_types), 2359 ) 2360 2361 plt.tight_layout() 2362 2363 # 図の保存 2364 if save_fig: 2365 if output_dir is None: 2366 raise ValueError( 2367 "save_fig=Trueの場合、output_dirを指定する必要があります。有効なディレクトリパスを指定してください。" 2368 ) 2369 os.makedirs(output_dir, exist_ok=True) 2370 output_path = os.path.join(output_dir, output_filename) 2371 plt.savefig(output_path, bbox_inches="tight", dpi=dpi) 2372 # 図の表示 2373 if show_fig: 2374 plt.show() 2375 else: 2376 plt.close(fig=fig) 2377 2378 if print_summary: 2379 # デバッグ用の出力 2380 print("\nビンごとの集計:") 2381 print(f"{'Range':>12} | {'bio':>8} | {'gas':>8} | {'total':>8}") 2382 print("-" * 50) 2383 2384 for i in range(len(bins) - 1): 2385 bin_start = bins[i] 2386 bin_end = bins[i + 1] 2387 2388 # 各タイプのカウントを計算 2389 counts_by_type: dict[HotspotType, int] = {"bio": 0, "gas": 0, "comb": 0} 2390 total = 0 2391 for spot_type in existing_types: 2392 mask = ( 2393 (df["type"] == spot_type) 2394 & (df["emission_rate"] >= bin_start) 2395 & (df["emission_rate"] < bin_end) 2396 ) 2397 count = len(df[mask]) 2398 counts_by_type[spot_type] = count 2399 total += count 2400 2401 # カウントが0の場合はスキップ 2402 if total > 0: 2403 range_str = f"{bin_start:5.1f}-{bin_end:<5.1f}" 2404 bio_count = counts_by_type.get("bio", 0) 2405 gas_count = counts_by_type.get("gas", 0) 2406 print( 2407 f"{range_str:>12} | {bio_count:8d} | {gas_count:8d} | {total:8d}" 2408 )
移動観測で得られた測定データを解析するクラス
265 def __init__( 266 self, 267 center_lat: float, 268 center_lon: float, 269 inputs: list[MSAInputConfig] | list[tuple[float, float, str | Path]], 270 num_sections: int = 4, 271 ch4_enhance_threshold: float = 0.1, 272 correlation_threshold: float = 0.7, 273 hotspot_area_meter: float = 50, 274 window_minutes: float = 5, 275 column_mapping: dict[str, str] = { 276 "Time Stamp": "timestamp", 277 "CH4 (ppm)": "ch4_ppm", 278 "C2H6 (ppb)": "c2h6_ppb", 279 "H2O (ppm)": "h2o_ppm", 280 "Latitude": "latitude", 281 "Longitude": "longitude", 282 }, 283 logger: Logger | None = None, 284 logging_debug: bool = False, 285 ): 286 """ 287 測定データ解析クラスの初期化 288 289 Parameters: 290 ------ 291 center_lat : float 292 中心緯度 293 center_lon : float 294 中心経度 295 inputs : list[MSAInputConfig] | list[tuple[float, float, str | Path]] 296 入力ファイルのリスト 297 num_sections : int 298 分割する区画数。デフォルトは4。 299 ch4_enhance_threshold : float 300 CH4増加の閾値(ppm)。デフォルトは0.1。 301 correlation_threshold : float 302 相関係数の閾値。デフォルトは0.7。 303 hotspot_area_meter : float 304 ホットスポットの検出に使用するエリアの半径(メートル)。デフォルトは50メートル。 305 window_minutes : float 306 移動窓の大きさ(分)。デフォルトは5分。 307 column_mapping : dict[str, str] 308 元のデータファイルのヘッダーを汎用的な単語に変換するための辞書型データ。 309 - timestamp,ch4_ppm,c2h6_ppm,h2o_ppm,latitude,longitudeをvalueに、それぞれに対応するカラム名をcolに指定してください。 310 logger : Logger | None 311 使用するロガー。Noneの場合は新しいロガーを作成します。 312 logging_debug : bool 313 ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。 314 315 Returns: 316 ------ 317 None 318 初期化処理が完了したことを示します。 319 """ 320 # ロガー 321 log_level: int = INFO 322 if logging_debug: 323 log_level = DEBUG 324 self.logger: Logger = MobileSpatialAnalyzer.setup_logger(logger, log_level) 325 # プライベートなプロパティ 326 self._center_lat: float = center_lat 327 self._center_lon: float = center_lon 328 self._ch4_enhance_threshold: float = ch4_enhance_threshold 329 self._correlation_threshold: float = correlation_threshold 330 self._hotspot_area_meter: float = hotspot_area_meter 331 self._column_mapping: dict[str, str] = column_mapping 332 self._num_sections: int = num_sections 333 # セクションの範囲 334 section_size: float = 360 / num_sections 335 self._section_size: float = section_size 336 self._sections = MobileSpatialAnalyzer._initialize_sections( 337 num_sections, section_size 338 ) 339 # window_sizeをデータポイント数に変換(分→秒→データポイント数) 340 self._window_size: int = MobileSpatialAnalyzer._calculate_window_size( 341 window_minutes 342 ) 343 # 入力設定の標準化 344 normalized_input_configs: list[MSAInputConfig] = ( 345 MobileSpatialAnalyzer._normalize_inputs(inputs) 346 ) 347 # 複数ファイルのデータを読み込み 348 self._data: dict[str, pd.DataFrame] = self._load_all_data( 349 normalized_input_configs 350 )
測定データ解析クラスの初期化
Parameters:
center_lat : float
中心緯度
center_lon : float
中心経度
inputs : list[MSAInputConfig] | list[tuple[float, float, str | Path]]
入力ファイルのリスト
num_sections : int
分割する区画数。デフォルトは4。
ch4_enhance_threshold : float
CH4増加の閾値(ppm)。デフォルトは0.1。
correlation_threshold : float
相関係数の閾値。デフォルトは0.7。
hotspot_area_meter : float
ホットスポットの検出に使用するエリアの半径(メートル)。デフォルトは50メートル。
window_minutes : float
移動窓の大きさ(分)。デフォルトは5分。
column_mapping : dict[str, str]
元のデータファイルのヘッダーを汎用的な単語に変換するための辞書型データ。
- timestamp,ch4_ppm,c2h6_ppm,h2o_ppm,latitude,longitudeをvalueに、それぞれに対応するカラム名をcolに指定してください。
logger : Logger | None
使用するロガー。Noneの場合は新しいロガーを作成します。
logging_debug : bool
ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。
Returns:
None
初期化処理が完了したことを示します。
352 def analyze_delta_ch4_stats(self, hotspots: list[HotspotData]) -> None: 353 """ 354 各タイプのホットスポットについてΔCH4の統計情報を計算し、結果を表示します。 355 356 Parameters: 357 ------ 358 hotspots : list[HotspotData] 359 分析対象のホットスポットリスト 360 361 Returns: 362 ------ 363 None 364 統計情報の表示が完了したことを示します。 365 """ 366 # タイプごとにホットスポットを分類 367 hotspots_by_type: dict[HotspotType, list[HotspotData]] = { 368 "bio": [h for h in hotspots if h.type == "bio"], 369 "gas": [h for h in hotspots if h.type == "gas"], 370 "comb": [h for h in hotspots if h.type == "comb"], 371 } 372 373 # 統計情報を計算し、表示 374 for spot_type, spots in hotspots_by_type.items(): 375 if spots: 376 delta_ch4_values = [spot.delta_ch4 for spot in spots] 377 max_value = max(delta_ch4_values) 378 mean_value = sum(delta_ch4_values) / len(delta_ch4_values) 379 median_value = sorted(delta_ch4_values)[len(delta_ch4_values) // 2] 380 print(f"{spot_type}タイプのホットスポットの統計情報:") 381 print(f" 最大値: {max_value}") 382 print(f" 平均値: {mean_value}") 383 print(f" 中央値: {median_value}") 384 else: 385 print(f"{spot_type}タイプのホットスポットは存在しません。")
各タイプのホットスポットについてΔCH4の統計情報を計算し、結果を表示します。
Parameters:
hotspots : list[HotspotData]
分析対象のホットスポットリスト
Returns:
None
統計情報の表示が完了したことを示します。
387 def analyze_hotspots( 388 self, 389 duplicate_check_mode: str = "none", 390 min_time_threshold_seconds: float = 300, 391 max_time_threshold_hours: float = 12, 392 ) -> list[HotspotData]: 393 """ 394 ホットスポットを検出して分析します。 395 396 Parameters: 397 ------ 398 duplicate_check_mode : str 399 重複チェックのモード("none","time_window","time_all")。 400 - "none": 重複チェックを行わない。 401 - "time_window": 指定された時間窓内の重複のみを除外。 402 - "time_all": すべての時間範囲で重複チェックを行う。 403 min_time_threshold_seconds : float 404 重複とみなす最小時間の閾値(秒)。デフォルトは300秒。 405 max_time_threshold_hours : float 406 重複チェックを一時的に無視する最大時間の閾値(時間)。デフォルトは12時間。 407 408 Returns: 409 ------ 410 list[HotspotData] 411 検出されたホットスポットのリスト。 412 """ 413 # 不正な入力値に対するエラーチェック 414 valid_modes = {"none", "time_window", "time_all"} 415 if duplicate_check_mode not in valid_modes: 416 raise ValueError( 417 f"無効な重複チェックモード: {duplicate_check_mode}. 有効な値は {valid_modes} です。" 418 ) 419 420 all_hotspots: list[HotspotData] = [] 421 422 # 各データソースに対して解析を実行 423 for _, df in self._data.items(): 424 # パラメータの計算 425 df = MobileSpatialAnalyzer._calculate_hotspots_parameters( 426 df, self._window_size 427 ) 428 429 # ホットスポットの検出 430 hotspots: list[HotspotData] = self._detect_hotspots( 431 df, 432 ch4_enhance_threshold=self._ch4_enhance_threshold, 433 ) 434 all_hotspots.extend(hotspots) 435 436 # 重複チェックモードに応じて処理 437 if duplicate_check_mode != "none": 438 unique_hotspots = MobileSpatialAnalyzer.remove_hotspots_duplicates( 439 all_hotspots, 440 check_time_all=duplicate_check_mode == "time_all", 441 min_time_threshold_seconds=min_time_threshold_seconds, 442 max_time_threshold_hours=max_time_threshold_hours, 443 hotspot_area_meter=self._hotspot_area_meter, 444 ) 445 self.logger.info( 446 f"重複除外: {len(all_hotspots)} → {len(unique_hotspots)} ホットスポット" 447 ) 448 return unique_hotspots 449 450 return all_hotspots
ホットスポットを検出して分析します。
Parameters:
duplicate_check_mode : str
重複チェックのモード("none","time_window","time_all")。
- "none": 重複チェックを行わない。
- "time_window": 指定された時間窓内の重複のみを除外。
- "time_all": すべての時間範囲で重複チェックを行う。
min_time_threshold_seconds : float
重複とみなす最小時間の閾値(秒)。デフォルトは300秒。
max_time_threshold_hours : float
重複チェックを一時的に無視する最大時間の閾値(時間)。デフォルトは12時間。
Returns:
list[HotspotData]
検出されたホットスポットのリスト。
452 def calculate_measurement_stats( 453 self, 454 print_individual_stats: bool = True, 455 print_total_stats: bool = True, 456 ) -> tuple[float, timedelta]: 457 """ 458 各ファイルの測定時間と走行距離を計算し、合計を返します。 459 460 Parameters: 461 ------ 462 print_individual_stats : bool 463 個別ファイルの統計を表示するかどうか。デフォルトはTrue。 464 print_total_stats : bool 465 合計統計を表示するかどうか。デフォルトはTrue。 466 467 Returns: 468 ------ 469 tuple[float, timedelta] 470 総距離(km)と総時間のタプル 471 """ 472 total_distance: float = 0.0 473 total_time: timedelta = timedelta() 474 individual_stats: list[dict] = [] # 個別の統計情報を保存するリスト 475 476 # プログレスバーを表示しながら計算 477 for source_name, df in tqdm( 478 self._data.items(), desc="Calculating", unit="file" 479 ): 480 # 時間の計算 481 time_spent = df.index[-1] - df.index[0] 482 483 # 距離の計算 484 distance_km = 0.0 485 for i in range(len(df) - 1): 486 lat1, lon1 = df.iloc[i][["latitude", "longitude"]] 487 lat2, lon2 = df.iloc[i + 1][["latitude", "longitude"]] 488 distance_km += ( 489 MobileSpatialAnalyzer._calculate_distance( 490 lat1=lat1, lon1=lon1, lat2=lat2, lon2=lon2 491 ) 492 / 1000 493 ) 494 495 # 合計に加算 496 total_distance += distance_km 497 total_time += time_spent 498 499 # 統計情報を保存 500 if print_individual_stats: 501 average_speed = distance_km / (time_spent.total_seconds() / 3600) 502 individual_stats.append( 503 { 504 "source": source_name, 505 "distance": distance_km, 506 "time": time_spent, 507 "speed": average_speed, 508 } 509 ) 510 511 # 計算完了後に統計情報を表示 512 if print_individual_stats: 513 self.logger.info("=== Individual Stats ===") 514 for stat in individual_stats: 515 print(f"File : {stat['source']}") 516 print(f" Distance : {stat['distance']:.2f} km") 517 print(f" Time : {stat['time']}") 518 print(f" Avg. Speed : {stat['speed']:.1f} km/h\n") 519 520 # 合計を表示 521 if print_total_stats: 522 average_speed_total: float = total_distance / ( 523 total_time.total_seconds() / 3600 524 ) 525 self.logger.info("=== Total Stats ===") 526 print(f" Distance : {total_distance:.2f} km") 527 print(f" Time : {total_time}") 528 print(f" Avg. Speed : {average_speed_total:.1f} km/h\n") 529 530 return total_distance, total_time
各ファイルの測定時間と走行距離を計算し、合計を返します。
Parameters:
print_individual_stats : bool
個別ファイルの統計を表示するかどうか。デフォルトはTrue。
print_total_stats : bool
合計統計を表示するかどうか。デフォルトはTrue。
Returns:
tuple[float, timedelta]
総距離(km)と総時間のタプル
532 def create_hotspots_map( 533 self, 534 hotspots: list[HotspotData], 535 output_dir: str | Path | None = None, 536 output_filename: str = "hotspots_map.html", 537 center_marker_label: str = "Center", 538 plot_center_marker: bool = True, 539 radius_meters: float = 3000, 540 save_fig: bool = True, 541 ) -> None: 542 """ 543 ホットスポットの分布を地図上にプロットして保存 544 545 Parameters: 546 ------ 547 hotspots : list[HotspotData] 548 プロットするホットスポットのリスト 549 output_dir : str | Path 550 保存先のディレクトリパス 551 output_filename : str 552 保存するファイル名。デフォルトは"hotspots_map"。 553 center_marker_label : str 554 中心を示すマーカーのラベルテキスト。デフォルトは"Center"。 555 plot_center_marker : bool 556 中心を示すマーカーの有無。デフォルトはTrue。 557 radius_meters : float 558 区画分けを示す線の長さ。デフォルトは3000。 559 save_fig : bool 560 図の保存を許可するフラグ。デフォルトはTrue。 561 """ 562 # 地図の作成 563 m = folium.Map( 564 location=[self._center_lat, self._center_lon], 565 zoom_start=15, 566 tiles="OpenStreetMap", 567 ) 568 569 # ホットスポットの種類ごとに異なる色でプロット 570 for spot in hotspots: 571 # NaN値チェックを追加 572 if math.isnan(spot.avg_lat) or math.isnan(spot.avg_lon): 573 continue 574 575 # default type 576 color = "black" 577 # タイプに応じて色を設定 578 if spot.type == "comb": 579 color = "green" 580 elif spot.type == "gas": 581 color = "red" 582 elif spot.type == "bio": 583 color = "blue" 584 585 # CSSのgrid layoutを使用してHTMLタグを含むテキストをフォーマット 586 popup_html = f""" 587 <div style='font-family: Arial; font-size: 12px; display: grid; grid-template-columns: auto auto auto; gap: 5px;'> 588 <b>Date</b> <span>:</span> <span>{spot.source}</span> 589 <b>Lat</b> <span>:</span> <span>{spot.avg_lat:.3f}</span> 590 <b>Lon</b> <span>:</span> <span>{spot.avg_lon:.3f}</span> 591 <b>ΔCH<sub>4</sub></b> <span>:</span> <span>{spot.delta_ch4:.3f}</span> 592 <b>ΔC<sub>2</sub>H<sub>6</sub></b> <span>:</span> <span>{spot.delta_c2h6:.3f}</span> 593 <b>Ratio</b> <span>:</span> <span>{spot.ratio:.3f}</span> 594 <b>Type</b> <span>:</span> <span>{spot.type}</span> 595 <b>Section</b> <span>:</span> <span>{spot.section}</span> 596 </div> 597 """ 598 599 # ポップアップのサイズを指定 600 popup = folium.Popup( 601 folium.Html(popup_html, script=True), 602 max_width=200, # 最大幅(ピクセル) 603 ) 604 605 folium.CircleMarker( 606 location=[spot.avg_lat, spot.avg_lon], 607 radius=8, 608 color=color, 609 fill=True, 610 popup=popup, 611 ).add_to(m) 612 613 # 中心点のマーカー 614 if plot_center_marker: 615 folium.Marker( 616 [self._center_lat, self._center_lon], 617 popup=center_marker_label, 618 icon=folium.Icon(color="green", icon="info-sign"), 619 ).add_to(m) 620 621 # 区画の境界線を描画 622 for section in range(self._num_sections): 623 start_angle = math.radians(-180 + section * self._section_size) 624 625 R = self.EARTH_RADIUS_METERS 626 627 # 境界線の座標を計算 628 lat1 = self._center_lat 629 lon1 = self._center_lon 630 lat2 = math.degrees( 631 math.asin( 632 math.sin(math.radians(lat1)) * math.cos(radius_meters / R) 633 + math.cos(math.radians(lat1)) 634 * math.sin(radius_meters / R) 635 * math.cos(start_angle) 636 ) 637 ) 638 lon2 = self._center_lon + math.degrees( 639 math.atan2( 640 math.sin(start_angle) 641 * math.sin(radius_meters / R) 642 * math.cos(math.radians(lat1)), 643 math.cos(radius_meters / R) 644 - math.sin(math.radians(lat1)) * math.sin(math.radians(lat2)), 645 ) 646 ) 647 648 # 境界線を描画 649 folium.PolyLine( 650 locations=[[lat1, lon1], [lat2, lon2]], 651 color="black", 652 weight=1, 653 opacity=0.5, 654 ).add_to(m) 655 656 # 地図を保存 657 if save_fig and output_dir is None: 658 raise ValueError( 659 "save_fig=Trueの場合、output_dirを指定する必要があります。有効なディレクトリパスを指定してください。" 660 ) 661 output_path: str = os.path.join(output_dir, output_filename) 662 m.save(str(output_path)) 663 self.logger.info(f"地図を保存しました: {output_path}")
ホットスポットの分布を地図上にプロットして保存
Parameters:
hotspots : list[HotspotData]
プロットするホットスポットのリスト
output_dir : str | Path
保存先のディレクトリパス
output_filename : str
保存するファイル名。デフォルトは"hotspots_map"。
center_marker_label : str
中心を示すマーカーのラベルテキスト。デフォルトは"Center"。
plot_center_marker : bool
中心を示すマーカーの有無。デフォルトはTrue。
radius_meters : float
区画分けを示す線の長さ。デフォルトは3000。
save_fig : bool
図の保存を許可するフラグ。デフォルトはTrue。
665 def export_hotspots_to_csv( 666 self, 667 hotspots: list[HotspotData], 668 output_dir: str | Path | None = None, 669 output_filename: str = "hotspots.csv", 670 ) -> None: 671 """ 672 ホットスポットの情報をCSVファイルに出力します。 673 674 Parameters: 675 ------ 676 hotspots : list[HotspotData] 677 出力するホットスポットのリスト 678 output_dir : str | Path | None 679 出力先ディレクトリ 680 output_filename : str 681 出力ファイル名 682 """ 683 # 日時の昇順でソート 684 sorted_hotspots = sorted(hotspots, key=lambda x: x.source) 685 686 # 出力用のデータを作成 687 records = [] 688 for spot in sorted_hotspots: 689 record = { 690 "source": spot.source, 691 "type": spot.type, 692 "delta_ch4": spot.delta_ch4, 693 "delta_c2h6": spot.delta_c2h6, 694 "ratio": spot.ratio, 695 "correlation": spot.correlation, 696 "angle": spot.angle, 697 "section": spot.section, 698 "latitude": spot.avg_lat, 699 "longitude": spot.avg_lon, 700 } 701 records.append(record) 702 703 # DataFrameに変換してCSVに出力 704 if output_dir is None: 705 raise ValueError( 706 "output_dirが指定されていません。有効なディレクトリパスを指定してください。" 707 ) 708 output_path: str = os.path.join(output_dir, output_filename) 709 df = pd.DataFrame(records) 710 df.to_csv(output_path, index=False) 711 self.logger.info( 712 f"ホットスポット情報をCSVファイルに出力しました: {output_path}" 713 )
ホットスポットの情報をCSVファイルに出力します。
Parameters:
hotspots : list[HotspotData]
出力するホットスポットのリスト
output_dir : str | Path | None
出力先ディレクトリ
output_filename : str
出力ファイル名
715 @staticmethod 716 def extract_source_name_from_path(path: str | Path) -> str: 717 """ 718 ファイルパスからソース名(拡張子なしのファイル名)を抽出します。 719 720 Parameters: 721 ------ 722 path : str | Path 723 ソース名を抽出するファイルパス 724 例: "/path/to/Pico100121_241017_092120+.txt" 725 726 Returns: 727 ------ 728 str 729 抽出されたソース名 730 例: "Pico100121_241017_092120+" 731 732 Examples: 733 ------ 734 >>> path = "/path/to/data/Pico100121_241017_092120+.txt" 735 >>> MobileSpatialAnalyzer.extract_source_from_path(path) 736 'Pico100121_241017_092120+' 737 """ 738 # Pathオブジェクトに変換 739 path_obj = Path(path) 740 # stem属性で拡張子なしのファイル名を取得 741 source_name = path_obj.stem 742 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+'
744 def get_preprocessed_data( 745 self, 746 ) -> pd.DataFrame: 747 """ 748 データ前処理を行い、CH4とC2H6の相関解析に必要な形式に整えます。 749 コンストラクタで読み込んだすべてのデータを前処理し、結合したDataFrameを返します。 750 751 Returns: 752 ------ 753 pd.DataFrame 754 前処理済みの結合されたDataFrame 755 """ 756 processed_dfs: list[pd.DataFrame] = [] 757 758 # 各データソースに対して解析を実行 759 for source_name, df in self._data.items(): 760 # パラメータの計算 761 processed_df = MobileSpatialAnalyzer._calculate_hotspots_parameters( 762 df, self._window_size 763 ) 764 # ソース名を列として追加 765 processed_df["source"] = source_name 766 processed_dfs.append(processed_df) 767 768 # すべてのDataFrameを結合 769 if not processed_dfs: 770 raise ValueError("処理対象のデータが存在しません。") 771 772 combined_df = pd.concat(processed_dfs, axis=0) 773 return combined_df
データ前処理を行い、CH4とC2H6の相関解析に必要な形式に整えます。 コンストラクタで読み込んだすべてのデータを前処理し、結合したDataFrameを返します。
Returns:
pd.DataFrame
前処理済みの結合されたDataFrame
775 def get_section_size(self) -> float: 776 """ 777 セクションのサイズを取得するメソッド。 778 このメソッドは、解析対象のデータを区画に分割する際の 779 各区画の角度範囲を示すサイズを返します。 780 781 Returns: 782 ------ 783 float 784 1セクションのサイズ(度単位) 785 """ 786 return self._section_size
セクションのサイズを取得するメソッド。 このメソッドは、解析対象のデータを区画に分割する際の 各区画の角度範囲を示すサイズを返します。
Returns:
float
1セクションのサイズ(度単位)
788 def get_source_names(self, print_all: bool = False) -> list[str]: 789 """ 790 データソースの名前を取得します。 791 792 Parameters 793 ---------- 794 print_all : bool, optional 795 すべてのデータソース名を表示するかどうかを指定します。デフォルトはFalseです。 796 797 Returns 798 ------- 799 list[str] 800 データソース名のリスト 801 802 Raises 803 ------ 804 ValueError 805 データが読み込まれていない場合に発生します。 806 """ 807 dfs_dict: dict[str, pd.DataFrame] = self._data 808 # データソースの選択 809 if not dfs_dict: 810 raise ValueError("データが読み込まれていません。") 811 source_name_list: list[str] = list(dfs_dict.keys()) 812 if print_all: 813 print(source_name_list) 814 return source_name_list
データソースの名前を取得します。
Parameters
print_all : bool, optional すべてのデータソース名を表示するかどうかを指定します。デフォルトはFalseです。
Returns
list[str] データソース名のリスト
Raises
ValueError データが読み込まれていない場合に発生します。
816 def plot_ch4_delta_histogram( 817 self, 818 hotspots: list[HotspotData], 819 output_dir: str | Path | None, 820 output_filename: str = "ch4_delta_histogram.png", 821 dpi: int = 200, 822 figsize: tuple[int, int] = (8, 6), 823 fontsize: float = 20, 824 xlim: tuple[float, float] | None = None, 825 ylim: tuple[float, float] | None = None, 826 save_fig: bool = True, 827 show_fig: bool = True, 828 yscale_log: bool = True, 829 print_bins_analysis: bool = False, 830 ) -> None: 831 """ 832 CH4の増加量(ΔCH4)の積み上げヒストグラムをプロットします。 833 834 Parameters: 835 ------ 836 hotspots : list[HotspotData] 837 プロットするホットスポットのリスト 838 output_dir : str | Path | None 839 保存先のディレクトリパス 840 output_filename : str 841 保存するファイル名。デフォルトは"ch4_delta_histogram.png"。 842 dpi : int 843 解像度。デフォルトは200。 844 figsize : tuple[int, int] 845 図のサイズ。デフォルトは(8, 6)。 846 fontsize : float 847 フォントサイズ。デフォルトは20。 848 xlim : tuple[float, float] | None 849 x軸の範囲。Noneの場合は自動設定。 850 ylim : tuple[float, float] | None 851 y軸の範囲。Noneの場合は自動設定。 852 save_fig : bool 853 図の保存を許可するフラグ。デフォルトはTrue。 854 show_fig : bool 855 図の表示を許可するフラグ。デフォルトはTrue。 856 yscale_log : bool 857 y軸をlogにするかどうか。デフォルトはTrue。 858 print_bins_analysis : bool 859 ビンごとの内訳を表示するオプション。 860 """ 861 plt.rcParams["font.size"] = fontsize 862 fig = plt.figure(figsize=figsize, dpi=dpi) 863 864 # ホットスポットからデータを抽出 865 all_ch4_deltas = [] 866 all_types = [] 867 for spot in hotspots: 868 all_ch4_deltas.append(spot.delta_ch4) 869 all_types.append(spot.type) 870 871 # データをNumPy配列に変換 872 all_ch4_deltas = np.array(all_ch4_deltas) 873 all_types = np.array(all_types) 874 875 # 0.1刻みのビンを作成 876 if xlim is not None: 877 bins = np.arange(xlim[0], xlim[1] + 0.1, 0.1) 878 else: 879 max_val = np.ceil(np.max(all_ch4_deltas) * 10) / 10 880 bins = np.arange(0, max_val + 0.1, 0.1) 881 882 # タイプごとのヒストグラムデータを計算 883 hist_data = {} 884 # HotspotTypeのリテラル値を使用してイテレーション 885 for type_name in get_args(HotspotType): # typing.get_argsをインポート 886 mask = all_types == type_name 887 if np.any(mask): 888 counts, _ = np.histogram(all_ch4_deltas[mask], bins=bins) 889 hist_data[type_name] = counts 890 891 # ビンごとの内訳を表示 892 if print_bins_analysis: 893 self.logger.info("各ビンの内訳:") 894 print(f"{'Bin Range':15} {'bio':>8} {'gas':>8} {'comb':>8} {'Total':>8}") 895 print("-" * 50) 896 897 for i in range(len(bins) - 1): 898 bin_start = bins[i] 899 bin_end = bins[i + 1] 900 bio_count = hist_data.get("bio", np.zeros(len(bins) - 1))[i] 901 gas_count = hist_data.get("gas", np.zeros(len(bins) - 1))[i] 902 comb_count = hist_data.get("comb", np.zeros(len(bins) - 1))[i] 903 total = bio_count + gas_count + comb_count 904 905 if total > 0: # 合計が0のビンは表示しない 906 print( 907 f"{bin_start:4.1f}-{bin_end:<8.1f}" 908 f"{int(bio_count):8d}" 909 f"{int(gas_count):8d}" 910 f"{int(comb_count):8d}" 911 f"{int(total):8d}" 912 ) 913 914 # 積み上げヒストグラムを作成 915 bottom = np.zeros_like(hist_data.get("bio", np.zeros(len(bins) - 1))) 916 917 # 色の定義をHotspotTypeを使用して型安全に定義 918 colors: dict[HotspotType, str] = {"bio": "blue", "gas": "red", "comb": "green"} 919 920 # HotspotTypeのリテラル値を使用してイテレーション 921 for type_name in get_args(HotspotType): 922 if type_name in hist_data: 923 plt.bar( 924 bins[:-1], 925 hist_data[type_name], 926 width=np.diff(bins)[0], 927 bottom=bottom, 928 color=colors[type_name], 929 label=type_name, 930 alpha=0.6, 931 align="edge", 932 ) 933 bottom += hist_data[type_name] 934 935 if yscale_log: 936 plt.yscale("log") 937 plt.xlabel("Δ$\\mathregular{CH_{4}}$ (ppm)") 938 plt.ylabel("Frequency") 939 plt.legend() 940 plt.grid(True, which="both", ls="-", alpha=0.2) 941 942 # 軸の範囲を設定 943 if xlim is not None: 944 plt.xlim(xlim) 945 if ylim is not None: 946 plt.ylim(ylim) 947 948 # グラフの保存または表示 949 if save_fig: 950 if output_dir is None: 951 raise ValueError( 952 "save_fig=Trueの場合、output_dirを指定する必要があります。有効なディレクトリパスを指定してください。" 953 ) 954 os.makedirs(output_dir, exist_ok=True) 955 output_path: str = os.path.join(output_dir, output_filename) 956 plt.savefig(output_path, bbox_inches="tight") 957 self.logger.info(f"ヒストグラムを保存しました: {output_path}") 958 if show_fig: 959 plt.show() 960 else: 961 plt.close(fig=fig)
CH4の増加量(ΔCH4)の積み上げヒストグラムをプロットします。
Parameters:
hotspots : list[HotspotData]
プロットするホットスポットのリスト
output_dir : str | Path | None
保存先のディレクトリパス
output_filename : str
保存するファイル名。デフォルトは"ch4_delta_histogram.png"。
dpi : int
解像度。デフォルトは200。
figsize : tuple[int, int]
図のサイズ。デフォルトは(8, 6)。
fontsize : float
フォントサイズ。デフォルトは20。
xlim : tuple[float, float] | None
x軸の範囲。Noneの場合は自動設定。
ylim : tuple[float, float] | None
y軸の範囲。Noneの場合は自動設定。
save_fig : bool
図の保存を許可するフラグ。デフォルトはTrue。
show_fig : bool
図の表示を許可するフラグ。デフォルトはTrue。
yscale_log : bool
y軸をlogにするかどうか。デフォルトはTrue。
print_bins_analysis : bool
ビンごとの内訳を表示するオプション。
963 def plot_mapbox( 964 self, 965 df: pd.DataFrame, 966 col_conc: str, 967 mapbox_access_token: str, 968 sort_conc_column: bool = True, 969 output_dir: str | Path | None = None, 970 output_filename: str = "mapbox_plot.html", 971 col_lat: str = "latitude", 972 col_lon: str = "longitude", 973 colorscale: str = "Jet", 974 center_lat: float | None = None, 975 center_lon: float | None = None, 976 zoom: float = 12, 977 width: int = 700, 978 height: int = 700, 979 tick_font_family: str = "Arial", 980 title_font_family: str = "Arial", 981 tick_font_size: int = 12, 982 title_font_size: int = 14, 983 marker_size: int = 4, 984 colorbar_title: str | None = None, 985 value_range: tuple[float, float] | None = None, 986 save_fig: bool = True, 987 show_fig: bool = True, 988 ) -> None: 989 """ 990 Plotlyを使用してMapbox上にデータをプロットします。 991 992 Parameters: 993 ------ 994 df : pd.DataFrame 995 プロットするデータを含むDataFrame 996 col_conc : str 997 カラーマッピングに使用する列名 998 mapbox_access_token : str 999 Mapboxのアクセストークン 1000 sort_conc_column : bool 1001 value_columnをソートするか否か。デフォルトはTrue。 1002 output_dir : str | Path | None 1003 出力ディレクトリのパス 1004 output_filename : str 1005 出力ファイル名。デフォルトは"mapbox_plot.html" 1006 col_lat : str 1007 緯度の列名。デフォルトは"latitude" 1008 col_lon : str 1009 経度の列名。デフォルトは"longitude" 1010 colorscale : str 1011 使用するカラースケール。デフォルトは"Jet" 1012 center_lat : float | None 1013 中心緯度。デフォルトはNoneで、self._center_latを使用 1014 center_lon : float | None 1015 中心経度。デフォルトはNoneで、self._center_lonを使用 1016 zoom : float 1017 マップの初期ズームレベル。デフォルトは12 1018 width : int 1019 プロットの幅(ピクセル)。デフォルトは700 1020 height : int 1021 プロットの高さ(ピクセル)。デフォルトは700 1022 tick_font_family : str 1023 カラーバーの目盛りフォントファミリー。デフォルトは"Arial" 1024 title_font_family : str 1025 カラーバーのラベルフォントファミリー。デフォルトは"Arial" 1026 tick_font_size : int 1027 カラーバーの目盛りフォントサイズ。デフォルトは12 1028 title_font_size : int 1029 カラーバーのラベルフォントサイズ。デフォルトは14 1030 marker_size : int 1031 マーカーのサイズ。デフォルトは4 1032 colorbar_title : str | None 1033 カラーバーのラベル 1034 value_range : tuple[float, float] | None 1035 カラーマッピングの範囲。デフォルトはNoneで、データの最小値と最大値を使用 1036 save_fig : bool 1037 図を保存するかどうか。デフォルトはTrue 1038 show_fig : bool 1039 図を表示するかどうか。デフォルトはTrue 1040 """ 1041 df_mapping: pd.DataFrame = df.copy().dropna(subset=[col_conc]) 1042 if sort_conc_column: 1043 df_mapping = df_mapping.sort_values(col_conc) 1044 # 中心座標の設定 1045 center_lat = center_lat if center_lat is not None else self._center_lat 1046 center_lon = center_lon if center_lon is not None else self._center_lon 1047 1048 # カラーマッピングの範囲を設定 1049 cmin, cmax = 0, 0 1050 if value_range is None: 1051 cmin = df_mapping[col_conc].min() 1052 cmax = df_mapping[col_conc].max() 1053 else: 1054 cmin, cmax = value_range 1055 1056 # カラーバーのタイトルを設定 1057 title_text = colorbar_title if colorbar_title is not None else col_conc 1058 1059 # Scattermapboxのデータを作成 1060 scatter_data = go.Scattermapbox( 1061 lat=df_mapping[col_lat], 1062 lon=df_mapping[col_lon], 1063 text=df_mapping[col_conc].astype(str), 1064 hoverinfo="text", 1065 mode="markers", 1066 marker=dict( 1067 color=df_mapping[col_conc], 1068 size=marker_size, 1069 reversescale=False, 1070 autocolorscale=False, 1071 colorscale=colorscale, 1072 cmin=cmin, 1073 cmax=cmax, 1074 colorbar=dict( 1075 tickformat="3.2f", 1076 outlinecolor="black", 1077 outlinewidth=1.5, 1078 ticks="outside", 1079 ticklen=7, 1080 tickwidth=1.5, 1081 tickcolor="black", 1082 tickfont=dict( 1083 family=tick_font_family, color="black", size=tick_font_size 1084 ), 1085 title=dict( 1086 text=title_text, side="top" 1087 ), # カラーバーのタイトルを設定 1088 titlefont=dict( 1089 family=title_font_family, 1090 color="black", 1091 size=title_font_size, 1092 ), 1093 ), 1094 ), 1095 ) 1096 1097 # レイアウトの設定 1098 layout = go.Layout( 1099 width=width, 1100 height=height, 1101 showlegend=False, 1102 mapbox=dict( 1103 accesstoken=mapbox_access_token, 1104 center=dict(lat=center_lat, lon=center_lon), 1105 zoom=zoom, 1106 ), 1107 ) 1108 1109 # 図の作成 1110 fig = go.Figure(data=[scatter_data], layout=layout) 1111 1112 # 図の保存 1113 if save_fig: 1114 # 保存時の出力ディレクトリチェック 1115 if output_dir is None: 1116 raise ValueError( 1117 "save_fig=Trueの場合、output_dirを指定する必要があります。" 1118 ) 1119 os.makedirs(output_dir, exist_ok=True) 1120 output_path = os.path.join(output_dir, output_filename) 1121 pyo.plot(fig, filename=output_path, auto_open=False) 1122 self.logger.info(f"Mapboxプロットを保存しました: {output_path}") 1123 # 図の表示 1124 if show_fig: 1125 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
1127 def plot_scatter_c2c1( 1128 self, 1129 hotspots: list[HotspotData], 1130 output_dir: str | Path | None = None, 1131 output_filename: str = "scatter_c2c1.png", 1132 dpi: int = 200, 1133 figsize: tuple[int, int] = (4, 4), 1134 fontsize: float = 12, 1135 save_fig: bool = True, 1136 show_fig: bool = True, 1137 ratio_labels: dict[float, tuple[float, float, str]] | None = None, 1138 ) -> None: 1139 """ 1140 検出されたホットスポットのΔC2H6とΔCH4の散布図をプロットします。 1141 1142 Parameters: 1143 ------ 1144 hotspots : list[HotspotData] 1145 プロットするホットスポットのリスト 1146 output_dir : str | Path | None 1147 保存先のディレクトリパス 1148 output_filename : str 1149 保存するファイル名。デフォルトは"scatter_c2c1.png"。 1150 dpi : int 1151 解像度。デフォルトは200。 1152 figsize : tuple[int, int] 1153 図のサイズ。デフォルトは(4, 4)。 1154 fontsize : float 1155 フォントサイズ。デフォルトは12。 1156 save_fig : bool 1157 図の保存を許可するフラグ。デフォルトはTrue。 1158 show_fig : bool 1159 図の表示を許可するフラグ。デフォルトはTrue。 1160 ratio_labels : dict[float, tuple[float, float, str]] | None 1161 比率線とラベルの設定。 1162 キーは比率値、値は (x位置, y位置, ラベルテキスト) のタプル。 1163 Noneの場合はデフォルト設定を使用。デフォルト値: 1164 { 1165 0.001: (1.25, 2, "0.001"), 1166 0.005: (1.25, 8, "0.005"), 1167 0.010: (1.25, 15, "0.01"), 1168 0.020: (1.25, 30, "0.02"), 1169 0.030: (1.0, 40, "0.03"), 1170 0.076: (0.20, 42, "0.076 (Osaka)") 1171 } 1172 """ 1173 plt.rcParams["font.size"] = fontsize 1174 fig = plt.figure(figsize=figsize, dpi=dpi) 1175 1176 # タイプごとのデータを収集 1177 type_data: dict[HotspotType, list[tuple[float, float]]] = { 1178 "bio": [], 1179 "gas": [], 1180 "comb": [], 1181 } 1182 for spot in hotspots: 1183 type_data[spot.type].append((spot.delta_ch4, spot.delta_c2h6)) 1184 1185 # 色とラベルの定義 1186 colors: dict[HotspotType, str] = {"bio": "blue", "gas": "red", "comb": "green"} 1187 labels: dict[HotspotType, str] = {"bio": "bio", "gas": "gas", "comb": "comb"} 1188 1189 # タイプごとにプロット(データが存在する場合のみ) 1190 for spot_type, data in type_data.items(): 1191 if data: # データが存在する場合のみプロット 1192 ch4_values, c2h6_values = zip(*data) 1193 plt.plot( 1194 ch4_values, 1195 c2h6_values, 1196 "o", 1197 c=colors[spot_type], 1198 alpha=0.5, 1199 ms=2, 1200 label=labels[spot_type], 1201 ) 1202 1203 # デフォルトの比率とラベル設定 1204 default_ratio_labels = { 1205 0.001: (1.25, 2, "0.001"), 1206 0.005: (1.25, 8, "0.005"), 1207 0.010: (1.25, 15, "0.01"), 1208 0.020: (1.25, 30, "0.02"), 1209 0.030: (1.0, 40, "0.03"), 1210 0.076: (0.20, 42, "0.076 (Osaka)"), 1211 } 1212 1213 ratio_labels = ratio_labels or default_ratio_labels 1214 1215 # プロット後、軸の設定前に比率の線を追加 1216 x = np.array([0, 5]) 1217 base_ch4 = 0.0 1218 base = 0.0 1219 1220 # 各比率に対して線を引く 1221 for ratio, (x_pos, y_pos, label) in ratio_labels.items(): 1222 y = (x - base_ch4) * 1000 * ratio + base 1223 plt.plot(x, y, "-", c="black", alpha=0.5) 1224 plt.text(x_pos, y_pos, label) 1225 1226 plt.ylim(0, 50) 1227 plt.xlim(0, 2.0) 1228 plt.ylabel("Δ$\\mathregular{C_{2}H_{6}}$ (ppb)") 1229 plt.xlabel("Δ$\\mathregular{CH_{4}}$ (ppm)") 1230 plt.legend() 1231 1232 # グラフの保存または表示 1233 if save_fig: 1234 if output_dir is None: 1235 raise ValueError( 1236 "save_fig=Trueの場合、output_dirを指定する必要があります。有効なディレクトリパスを指定してください。" 1237 ) 1238 output_path: str = os.path.join(output_dir, output_filename) 1239 plt.savefig(output_path, bbox_inches="tight") 1240 self.logger.info(f"散布図を保存しました: {output_path}") 1241 if show_fig: 1242 plt.show() 1243 else: 1244 plt.close(fig=fig)
検出されたホットスポットのΔC2H6とΔCH4の散布図をプロットします。
Parameters:
hotspots : list[HotspotData]
プロットするホットスポットのリスト
output_dir : str | Path | None
保存先のディレクトリパス
output_filename : str
保存するファイル名。デフォルトは"scatter_c2c1.png"。
dpi : int
解像度。デフォルトは200。
figsize : tuple[int, int]
図のサイズ。デフォルトは(4, 4)。
fontsize : float
フォントサイズ。デフォルトは12。
save_fig : bool
図の保存を許可するフラグ。デフォルトはTrue。
show_fig : bool
図の表示を許可するフラグ。デフォルトはTrue。
ratio_labels : dict[float, tuple[float, float, str]] | None
比率線とラベルの設定。
キーは比率値、値は (x位置, y位置, ラベルテキスト) のタプル。
Noneの場合はデフォルト設定を使用。デフォルト値:
{
0.001: (1.25, 2, "0.001"),
0.005: (1.25, 8, "0.005"),
0.010: (1.25, 15, "0.01"),
0.020: (1.25, 30, "0.02"),
0.030: (1.0, 40, "0.03"),
0.076: (0.20, 42, "0.076 (Osaka)")
}
1246 def plot_conc_timeseries( 1247 self, 1248 source_name: str | None = None, 1249 output_dir: str | Path | None = None, 1250 output_filename: str = "timeseries.png", 1251 dpi: int = 200, 1252 figsize: tuple[float, float] = (8, 4), 1253 save_fig: bool = True, 1254 show_fig: bool = True, 1255 col_ch4: str = "ch4_ppm", 1256 col_c2h6: str = "c2h6_ppb", 1257 col_h2o: str = "h2o_ppm", 1258 ylim_ch4: tuple[float, float] | None = None, 1259 ylim_c2h6: tuple[float, float] | None = None, 1260 ylim_h2o: tuple[float, float] | None = None, 1261 font_size: float = 12, 1262 label_pad: float = 10, 1263 ) -> None: 1264 """ 1265 時系列データをプロットします。 1266 1267 Parameters: 1268 ------ 1269 dpi : int 1270 図の解像度を指定します。デフォルトは200です。 1271 source_name : str | None 1272 プロットするデータソースの名前。Noneの場合は最初のデータソースを使用します。 1273 figsize : tuple[float, float] 1274 図のサイズを指定します。デフォルトは(8, 4)です。 1275 output_dir : str | Path | None 1276 保存先のディレクトリを指定します。save_fig=Trueの場合は必須です。 1277 output_filename : str 1278 保存するファイル名を指定します。デフォルトは"time_series.png"です。 1279 save_fig : bool 1280 図を保存するかどうかを指定します。デフォルトはFalseです。 1281 show_fig : bool 1282 図を表示するかどうかを指定します。デフォルトはTrueです。 1283 col_ch4 : str 1284 CH4データのキーを指定します。デフォルトは"ch4_ppm"です。 1285 col_c2h6 : str 1286 C2H6データのキーを指定します。デフォルトは"c2h6_ppb"です。 1287 col_h2o : str 1288 H2Oデータのキーを指定します。デフォルトは"h2o_ppm"です。 1289 ylim_ch4 : tuple[float, float] | None 1290 CH4プロットのy軸範囲を指定します。デフォルトはNoneです。 1291 ylim_c2h6 : tuple[float, float] | None 1292 C2H6プロットのy軸範囲を指定します。デフォルトはNoneです。 1293 ylim_h2o : tuple[float, float] | None 1294 H2Oプロットのy軸範囲を指定します。デフォルトはNoneです。 1295 font_size : float 1296 基本フォントサイズ。デフォルトは12。 1297 label_pad : float 1298 y軸ラベルのパディング。デフォルトは10。 1299 """ 1300 # プロットパラメータの設定 1301 plt.rcParams.update( 1302 { 1303 "font.size": font_size, 1304 "axes.labelsize": font_size, 1305 "axes.titlesize": font_size, 1306 "xtick.labelsize": font_size, 1307 "ytick.labelsize": font_size, 1308 } 1309 ) 1310 dfs_dict: dict[str, pd.DataFrame] = self._data.copy() 1311 # データソースの選択 1312 if not dfs_dict: 1313 raise ValueError("データが読み込まれていません。") 1314 1315 if source_name not in dfs_dict: 1316 raise ValueError( 1317 f"指定されたデータソース '{source_name}' が見つかりません。" 1318 ) 1319 1320 df = dfs_dict[source_name] 1321 1322 # プロットの作成 1323 fig = plt.figure(figsize=figsize, dpi=dpi) 1324 1325 # CH4プロット 1326 ax1 = fig.add_subplot(3, 1, 1) 1327 ax1.plot(df.index, df[col_ch4], c="red") 1328 if ylim_ch4: 1329 ax1.set_ylim(ylim_ch4) 1330 ax1.set_ylabel("$\\mathregular{CH_{4}}$ (ppm)", labelpad=label_pad) 1331 ax1.grid(True, alpha=0.3) 1332 1333 # C2H6プロット 1334 ax2 = fig.add_subplot(3, 1, 2) 1335 ax2.plot(df.index, df[col_c2h6], c="red") 1336 if ylim_c2h6: 1337 ax2.set_ylim(ylim_c2h6) 1338 ax2.set_ylabel("$\\mathregular{C_{2}H_{6}}$ (ppb)", labelpad=label_pad) 1339 ax2.grid(True, alpha=0.3) 1340 1341 # H2Oプロット 1342 ax3 = fig.add_subplot(3, 1, 3) 1343 ax3.plot(df.index, df[col_h2o], c="red") 1344 if ylim_h2o: 1345 ax3.set_ylim(ylim_h2o) 1346 ax3.set_ylabel("$\\mathregular{H_{2}O}$ (ppm)", labelpad=label_pad) 1347 ax3.grid(True, alpha=0.3) 1348 1349 # x軸のフォーマット調整 1350 for ax in [ax1, ax2, ax3]: 1351 ax.xaxis.set_major_formatter(mdates.DateFormatter("%H:%M")) 1352 # 軸のラベルとグリッド線の調整 1353 ax.tick_params(axis="both", which="major", labelsize=font_size) 1354 ax.grid(True, alpha=0.3) 1355 1356 # サブプロット間の間隔調整 1357 plt.subplots_adjust(wspace=0.38, hspace=0.38) 1358 1359 # 図の保存 1360 if save_fig: 1361 if output_dir is None: 1362 raise ValueError( 1363 "save_fig=Trueの場合、output_dirを指定する必要があります。有効なディレクトリパスを指定してください。" 1364 ) 1365 os.makedirs(output_dir, exist_ok=True) 1366 output_path = os.path.join(output_dir, output_filename) 1367 plt.savefig(output_path, bbox_inches="tight") 1368 1369 if show_fig: 1370 plt.show() 1371 else: 1372 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。
1858 def remove_c2c1_ratio_duplicates( 1859 self, 1860 df: pd.DataFrame, 1861 min_time_threshold_seconds: float = 300, # 5分以内は重複とみなす 1862 max_time_threshold_hours: float = 12.0, # 12時間以上離れている場合は別のポイントとして扱う 1863 check_time_all: bool = True, # 時間閾値を超えた場合の重複チェックを継続するかどうか 1864 hotspot_area_meter: float = 50.0, # 重複とみなす距離の閾値(メートル) 1865 col_ch4_ppm: str = "ch4_ppm", 1866 col_ch4_ppm_mv: str = "ch4_ppm_mv", 1867 col_ch4_ppm_delta: str = "ch4_ppm_delta", 1868 ): 1869 """ 1870 メタン濃度の増加が閾値を超えた地点から、重複を除外してユニークなホットスポットを抽出する関数。 1871 1872 Parameters: 1873 ------ 1874 df : pandas.DataFrame 1875 入力データフレーム。必須カラム: 1876 - ch4_ppm: メタン濃度(ppm) 1877 - ch4_ppm_mv: メタン濃度の移動平均(ppm) 1878 - ch4_ppm_delta: メタン濃度の増加量(ppm) 1879 - latitude: 緯度 1880 - longitude: 経度 1881 min_time_threshold_seconds : float, optional 1882 重複とみなす最小時間差(秒)。デフォルトは300秒(5分)。 1883 max_time_threshold_hours : float, optional 1884 別ポイントとして扱う最大時間差(時間)。デフォルトは12時間。 1885 check_time_all : bool, optional 1886 時間閾値を超えた場合の重複チェックを継続するかどうか。デフォルトはTrue。 1887 hotspot_area_meter : float, optional 1888 重複とみなす距離の閾値(メートル)。デフォルトは50メートル。 1889 1890 Returns: 1891 ------ 1892 pandas.DataFrame 1893 ユニークなホットスポットのデータフレーム。 1894 """ 1895 df_data: pd.DataFrame = df.copy() 1896 # メタン濃度の増加が閾値を超えた点を抽出 1897 mask = ( 1898 df_data[col_ch4_ppm] - df_data[col_ch4_ppm_mv] > self._ch4_enhance_threshold 1899 ) 1900 hotspot_candidates = df_data[mask].copy() 1901 1902 # ΔCH4の降順でソート 1903 sorted_hotspots = hotspot_candidates.sort_values( 1904 by=col_ch4_ppm_delta, ascending=False 1905 ) 1906 used_positions = [] 1907 unique_hotspots = pd.DataFrame() 1908 1909 for _, spot in sorted_hotspots.iterrows(): 1910 should_add = True 1911 for used_lat, used_lon, used_time in used_positions: 1912 # 距離チェック 1913 distance = geodesic( 1914 (spot.latitude, spot.longitude), (used_lat, used_lon) 1915 ).meters 1916 1917 if distance < hotspot_area_meter: 1918 # 時間差の計算(秒単位) 1919 time_diff = pd.Timedelta( 1920 spot.name - pd.to_datetime(used_time) 1921 ).total_seconds() 1922 time_diff_abs = abs(time_diff) 1923 1924 # 時間差に基づく判定 1925 if check_time_all: 1926 # 時間に関係なく、距離が近ければ重複とみなす 1927 # ΔCH4が大きい方を残す(現在のスポットは必ず小さい) 1928 should_add = False 1929 break 1930 else: 1931 # 時間窓による判定を行う 1932 if time_diff_abs <= min_time_threshold_seconds: 1933 # Case 1: 最小時間閾値以内は重複とみなす 1934 should_add = False 1935 break 1936 elif time_diff_abs > max_time_threshold_hours * 3600: 1937 # Case 2: 最大時間閾値を超えた場合は重複チェックをスキップ 1938 continue 1939 # Case 3: その間の時間差の場合は、距離が近ければ重複とみなす 1940 should_add = False 1941 break 1942 1943 if should_add: 1944 unique_hotspots = pd.concat([unique_hotspots, pd.DataFrame([spot])]) 1945 used_positions.append((spot.latitude, spot.longitude, spot.name)) 1946 1947 return unique_hotspots
メタン濃度の増加が閾値を超えた地点から、重複を除外してユニークなホットスポットを抽出する関数。
Parameters:
df : pandas.DataFrame
入力データフレーム。必須カラム:
- ch4_ppm: メタン濃度(ppm)
- ch4_ppm_mv: メタン濃度の移動平均(ppm)
- ch4_ppm_delta: メタン濃度の増加量(ppm)
- latitude: 緯度
- longitude: 経度
min_time_threshold_seconds : float, optional
重複とみなす最小時間差(秒)。デフォルトは300秒(5分)。
max_time_threshold_hours : float, optional
別ポイントとして扱う最大時間差(時間)。デフォルトは12時間。
check_time_all : bool, optional
時間閾値を超えた場合の重複チェックを継続するかどうか。デフォルトはTrue。
hotspot_area_meter : float, optional
重複とみなす距離の閾値(メートル)。デフォルトは50メートル。
Returns:
pandas.DataFrame
ユニークなホットスポットのデータフレーム。
1949 @staticmethod 1950 def remove_hotspots_duplicates( 1951 hotspots: list[HotspotData], 1952 check_time_all: bool, 1953 min_time_threshold_seconds: float = 300, 1954 max_time_threshold_hours: float = 12, 1955 hotspot_area_meter: float = 50, 1956 ) -> list[HotspotData]: 1957 """ 1958 重複するホットスポットを除外します。 1959 1960 このメソッドは、与えられたホットスポットのリストから重複を検出し、 1961 一意のホットスポットのみを返します。重複の判定は、指定された 1962 時間および距離の閾値に基づいて行われます。 1963 1964 Parameters: 1965 ------ 1966 hotspots : list[HotspotData] 1967 重複を除外する対象のホットスポットのリスト。 1968 check_time_all : bool 1969 時間に関係なく重複チェックを行うかどうか。 1970 min_time_threshold_seconds : float 1971 重複とみなす最小時間の閾値(秒)。 1972 max_time_threshold_hours : float 1973 重複チェックを一時的に無視する最大時間の閾値(時間)。 1974 hotspot_area_meter : float 1975 重複とみなす距離の閾値(メートル)。 1976 1977 Returns: 1978 ------ 1979 list[HotspotData] 1980 重複を除去したホットスポットのリスト。 1981 """ 1982 # ΔCH4の降順でソート 1983 sorted_hotspots: list[HotspotData] = sorted( 1984 hotspots, key=lambda x: x.delta_ch4, reverse=True 1985 ) 1986 used_positions_by_type: dict[ 1987 HotspotType, list[tuple[float, float, str, float]] 1988 ] = { 1989 "bio": [], 1990 "gas": [], 1991 "comb": [], 1992 } 1993 unique_hotspots: list[HotspotData] = [] 1994 1995 for spot in sorted_hotspots: 1996 is_duplicate = MobileSpatialAnalyzer._is_duplicate_spot( 1997 current_lat=spot.avg_lat, 1998 current_lon=spot.avg_lon, 1999 current_time=spot.source, 2000 used_positions=used_positions_by_type[spot.type], 2001 check_time_all=check_time_all, 2002 min_time_threshold_seconds=min_time_threshold_seconds, 2003 max_time_threshold_hours=max_time_threshold_hours, 2004 hotspot_area_meter=hotspot_area_meter, 2005 ) 2006 2007 if not is_duplicate: 2008 unique_hotspots.append(spot) 2009 used_positions_by_type[spot.type].append( 2010 (spot.avg_lat, spot.avg_lon, spot.source, spot.delta_ch4) 2011 ) 2012 2013 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]
重複を除去したホットスポットのリスト。
2015 @staticmethod 2016 def setup_logger(logger: Logger | None, log_level: int = INFO) -> Logger: 2017 """ 2018 ロガーを設定します。 2019 2020 このメソッドは、ロギングの設定を行い、ログメッセージのフォーマットを指定します。 2021 ログメッセージには、日付、ログレベル、メッセージが含まれます。 2022 2023 渡されたロガーがNoneまたは不正な場合は、新たにロガーを作成し、標準出力に 2024 ログメッセージが表示されるようにStreamHandlerを追加します。ロガーのレベルは 2025 引数で指定されたlog_levelに基づいて設定されます。 2026 2027 Parameters: 2028 ------ 2029 logger : Logger | None 2030 使用するロガー。Noneの場合は新しいロガーを作成します。 2031 log_level : int 2032 ロガーのログレベル。デフォルトはINFO。 2033 2034 Returns: 2035 ------ 2036 Logger 2037 設定されたロガーオブジェクト。 2038 """ 2039 if logger is not None and isinstance(logger, Logger): 2040 return logger 2041 # 渡されたロガーがNoneまたは正しいものでない場合は独自に設定 2042 new_logger: Logger = getLogger() 2043 # 既存のハンドラーをすべて削除 2044 for handler in new_logger.handlers[:]: 2045 new_logger.removeHandler(handler) 2046 new_logger.setLevel(log_level) # ロガーのレベルを設定 2047 ch = StreamHandler() 2048 ch_formatter = Formatter("%(asctime)s - %(levelname)s - %(message)s") 2049 ch.setFormatter(ch_formatter) # フォーマッターをハンドラーに設定 2050 new_logger.addHandler(ch) # StreamHandlerの追加 2051 return new_logger
ロガーを設定します。
このメソッドは、ロギングの設定を行い、ログメッセージのフォーマットを指定します。 ログメッセージには、日付、ログレベル、メッセージが含まれます。
渡されたロガーがNoneまたは不正な場合は、新たにロガーを作成し、標準出力に ログメッセージが表示されるようにStreamHandlerを追加します。ロガーのレベルは 引数で指定されたlog_levelに基づいて設定されます。
Parameters:
logger : Logger | None
使用するロガー。Noneの場合は新しいロガーを作成します。
log_level : int
ロガーのログレベル。デフォルトはINFO。
Returns:
Logger
設定されたロガーオブジェクト。
2053 @staticmethod 2054 def calculate_emission_rates( 2055 hotspots: list[HotspotData], 2056 method: Literal["weller", "weitzel", "joo", "umezawa"] = "weller", 2057 print_summary: bool = True, 2058 custom_formulas: dict[str, dict[str, float]] | None = None, 2059 ) -> tuple[list[EmissionData], dict[str, dict[str, float]]]: 2060 """ 2061 検出されたホットスポットのCH4漏出量を計算・解析し、統計情報を生成します。 2062 2063 Parameters: 2064 ------ 2065 hotspots : list[HotspotData] 2066 分析対象のホットスポットのリスト 2067 method : Literal["weller", "weitzel", "joo", "umezawa"] 2068 使用する計算式。デフォルトは"weller"。 2069 print_summary : bool 2070 統計情報を表示するかどうか。デフォルトはTrue。 2071 custom_formulas : dict[str, dict[str, float]] | None 2072 カスタム計算式の係数。 2073 例: {"custom_method": {"a": 1.0, "b": 1.0}} 2074 Noneの場合はデフォルトの計算式を使用。 2075 2076 Returns: 2077 ------ 2078 tuple[list[EmissionData], dict[str, dict[str, float]]] 2079 - 各ホットスポットの排出量データを含むリスト 2080 - タイプ別の統計情報を含む辞書 2081 """ 2082 # デフォルトの経験式係数 2083 default_formulas = { 2084 "weller": {"a": 0.988, "b": 0.817}, 2085 "weitzel": {"a": 0.521, "b": 0.795}, 2086 "joo": {"a": 2.738, "b": 1.329}, 2087 "umezawa": {"a": 2.716, "b": 0.741}, 2088 } 2089 2090 # カスタム計算式がある場合は追加 2091 emission_formulas = default_formulas.copy() 2092 if custom_formulas: 2093 emission_formulas.update(custom_formulas) 2094 2095 if method not in emission_formulas: 2096 raise ValueError(f"Unknown method: {method}") 2097 2098 # 係数の取得 2099 a = emission_formulas[method]["a"] 2100 b = emission_formulas[method]["b"] 2101 2102 # 排出量の計算 2103 emission_data_list = [] 2104 for spot in hotspots: 2105 # 漏出量の計算 (L/min) 2106 emission_rate = np.exp((np.log(spot.delta_ch4) + a) / b) 2107 # 日排出量 (L/day) 2108 daily_emission = emission_rate * 60 * 24 2109 # 年間排出量 (L/year) 2110 annual_emission = daily_emission * 365 2111 2112 emission_data = EmissionData( 2113 source=spot.source, 2114 type=spot.type, 2115 section=spot.section, 2116 latitude=spot.avg_lat, 2117 longitude=spot.avg_lon, 2118 delta_ch4=spot.delta_ch4, 2119 delta_c2h6=spot.delta_c2h6, 2120 ratio=spot.ratio, 2121 emission_rate=emission_rate, 2122 daily_emission=daily_emission, 2123 annual_emission=annual_emission, 2124 ) 2125 emission_data_list.append(emission_data) 2126 2127 # 統計計算用にDataFrameを作成 2128 emission_df = pd.DataFrame([e.to_dict() for e in emission_data_list]) 2129 2130 # タイプ別の統計情報を計算 2131 stats = {} 2132 # emission_formulas の定義の後に、排出量カテゴリーの閾値を定義 2133 emission_categories = { 2134 "low": {"min": 0, "max": 6}, # < 6 L/min 2135 "medium": {"min": 6, "max": 40}, # 6-40 L/min 2136 "high": {"min": 40, "max": float("inf")}, # > 40 L/min 2137 } 2138 # get_args(HotspotType)を使用して型安全なリストを作成 2139 types = list(get_args(HotspotType)) 2140 for spot_type in types: 2141 df_type = emission_df[emission_df["type"] == spot_type] 2142 if len(df_type) > 0: 2143 # 既存の統計情報を計算 2144 type_stats = { 2145 "count": len(df_type), 2146 "emission_rate_min": df_type["emission_rate"].min(), 2147 "emission_rate_max": df_type["emission_rate"].max(), 2148 "emission_rate_mean": df_type["emission_rate"].mean(), 2149 "emission_rate_median": df_type["emission_rate"].median(), 2150 "total_annual_emission": df_type["annual_emission"].sum(), 2151 "mean_annual_emission": df_type["annual_emission"].mean(), 2152 } 2153 2154 # 排出量カテゴリー別の統計を追加 2155 category_counts = { 2156 "low": len( 2157 df_type[ 2158 df_type["emission_rate"] < emission_categories["low"]["max"] 2159 ] 2160 ), 2161 "medium": len( 2162 df_type[ 2163 ( 2164 df_type["emission_rate"] 2165 >= emission_categories["medium"]["min"] 2166 ) 2167 & ( 2168 df_type["emission_rate"] 2169 < emission_categories["medium"]["max"] 2170 ) 2171 ] 2172 ), 2173 "high": len( 2174 df_type[ 2175 df_type["emission_rate"] 2176 >= emission_categories["high"]["min"] 2177 ] 2178 ), 2179 } 2180 type_stats["emission_categories"] = category_counts 2181 2182 stats[spot_type] = type_stats 2183 2184 if print_summary: 2185 print(f"\n{spot_type}タイプの統計情報:") 2186 print(f" 検出数: {type_stats['count']}") 2187 print(" 排出量 (L/min):") 2188 print(f" 最小値: {type_stats['emission_rate_min']:.2f}") 2189 print(f" 最大値: {type_stats['emission_rate_max']:.2f}") 2190 print(f" 平均値: {type_stats['emission_rate_mean']:.2f}") 2191 print(f" 中央値: {type_stats['emission_rate_median']:.2f}") 2192 print(" 排出量カテゴリー別の検出数:") 2193 print(f" 低放出 (< 6 L/min): {category_counts['low']}") 2194 print(f" 中放出 (6-40 L/min): {category_counts['medium']}") 2195 print(f" 高放出 (> 40 L/min): {category_counts['high']}") 2196 print(" 年間排出量 (L/year):") 2197 print(f" 合計: {type_stats['total_annual_emission']:.2f}") 2198 print(f" 平均: {type_stats['mean_annual_emission']:.2f}") 2199 2200 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]]]
- 各ホットスポットの排出量データを含むリスト
- タイプ別の統計情報を含む辞書
2202 @staticmethod 2203 def plot_emission_analysis( 2204 emission_data_list: list[EmissionData], 2205 dpi: int = 300, 2206 output_dir: str | Path | None = None, 2207 output_filename: str = "emission_analysis.png", 2208 figsize: tuple[float, float] = (12, 5), 2209 add_legend: bool = True, 2210 hist_log_y: bool = False, 2211 hist_xlim: tuple[float, float] | None = None, 2212 hist_ylim: tuple[float, float] | None = None, 2213 scatter_xlim: tuple[float, float] | None = None, 2214 scatter_ylim: tuple[float, float] | None = None, 2215 hist_bin_width: float = 0.5, 2216 print_summary: bool = True, 2217 save_fig: bool = False, 2218 show_fig: bool = True, 2219 show_scatter: bool = True, # 散布図の表示を制御するオプションを追加 2220 ) -> None: 2221 """ 2222 排出量分析のプロットを作成する静的メソッド。 2223 2224 Parameters: 2225 ------ 2226 emission_data_list : list[EmissionData] 2227 EmissionDataオブジェクトのリスト。 2228 output_dir : str | Path | None 2229 出力先ディレクトリのパス。 2230 output_filename : str 2231 保存するファイル名。デフォルトは"emission_analysis.png"。 2232 dpi : int 2233 プロットの解像度。デフォルトは300。 2234 figsize : tuple[float, float] 2235 プロットのサイズ。デフォルトは(12, 5)。 2236 add_legend : bool 2237 凡例を追加するかどうか。デフォルトはTrue。 2238 hist_log_y : bool 2239 ヒストグラムのy軸を対数スケールにするかどうか。デフォルトはFalse。 2240 hist_xlim : tuple[float, float] | None 2241 ヒストグラムのx軸の範囲。デフォルトはNone。 2242 hist_ylim : tuple[float, float] | None 2243 ヒストグラムのy軸の範囲。デフォルトはNone。 2244 scatter_xlim : tuple[float, float] | None 2245 散布図のx軸の範囲。デフォルトはNone。 2246 scatter_ylim : tuple[float, float] | None 2247 散布図のy軸の範囲。デフォルトはNone。 2248 hist_bin_width : float 2249 ヒストグラムのビンの幅。デフォルトは0.5。 2250 print_summary : bool 2251 集計結果を表示するかどうか。デフォルトはFalse。 2252 save_fig : bool 2253 図をファイルに保存するかどうか。デフォルトはFalse。 2254 show_fig : bool 2255 図を表示するかどうか。デフォルトはTrue。 2256 show_scatter : bool 2257 散布図(右図)を表示するかどうか。デフォルトはTrue。 2258 """ 2259 # データをDataFrameに変換 2260 df = pd.DataFrame([e.to_dict() for e in emission_data_list]) 2261 2262 # プロットの作成(散布図の有無に応じてサブプロット数を調整) 2263 if show_scatter: 2264 fig, (ax1, ax2) = plt.subplots(1, 2, figsize=figsize) 2265 axes = [ax1, ax2] 2266 else: 2267 fig, ax1 = plt.subplots(1, 1, figsize=(figsize[0] // 2, figsize[1])) 2268 axes = [ax1] 2269 2270 # カラーマップの定義 2271 colors: dict[HotspotType, str] = {"bio": "blue", "gas": "red", "comb": "green"} 2272 2273 # 存在するタイプを確認 2274 # HotspotTypeの定義順を基準にソート 2275 hotspot_types = list(get_args(HotspotType)) 2276 existing_types = sorted( 2277 df["type"].unique(), key=lambda x: hotspot_types.index(x) 2278 ) 2279 2280 # 左側: ヒストグラム 2281 # ビンの範囲を設定 2282 start = 0 # 必ず0から開始 2283 if hist_xlim is not None: 2284 end = hist_xlim[1] 2285 else: 2286 end = np.ceil(df["emission_rate"].max() * 1.05) 2287 2288 # ビン数を計算(end値をbin_widthで割り切れるように調整) 2289 n_bins = int(np.ceil(end / hist_bin_width)) 2290 end = n_bins * hist_bin_width 2291 2292 # ビンの生成(0から開始し、bin_widthの倍数で区切る) 2293 bins = np.linspace(start, end, n_bins + 1) 2294 2295 # タイプごとにヒストグラムを積み上げ 2296 bottom = np.zeros(len(bins) - 1) 2297 for spot_type in existing_types: 2298 data = df[df["type"] == spot_type]["emission_rate"] 2299 if len(data) > 0: 2300 counts, _ = np.histogram(data, bins=bins) 2301 ax1.bar( 2302 bins[:-1], 2303 counts, 2304 width=hist_bin_width, 2305 bottom=bottom, 2306 alpha=0.6, 2307 label=spot_type, 2308 color=colors[spot_type], 2309 ) 2310 bottom += counts 2311 2312 ax1.set_xlabel("CH$_4$ Emission (L min$^{-1}$)") 2313 ax1.set_ylabel("Frequency") 2314 if hist_log_y: 2315 # ax1.set_yscale("log") 2316 # 非線形スケールを設定(linthreshで線形から対数への遷移点を指定) 2317 ax1.set_yscale("symlog", linthresh=1.0) 2318 if hist_xlim is not None: 2319 ax1.set_xlim(hist_xlim) 2320 else: 2321 ax1.set_xlim(0, np.ceil(df["emission_rate"].max() * 1.05)) 2322 2323 if hist_ylim is not None: 2324 ax1.set_ylim(hist_ylim) 2325 else: 2326 ax1.set_ylim(0, ax1.get_ylim()[1]) # 下限を0に設定 2327 2328 if show_scatter: 2329 # 右側: 散布図 2330 for spot_type in existing_types: 2331 mask = df["type"] == spot_type 2332 ax2.scatter( 2333 df[mask]["emission_rate"], 2334 df[mask]["delta_ch4"], 2335 alpha=0.6, 2336 label=spot_type, 2337 color=colors[spot_type], 2338 ) 2339 2340 ax2.set_xlabel("Emission Rate (L min$^{-1}$)") 2341 ax2.set_ylabel("ΔCH$_4$ (ppm)") 2342 if scatter_xlim is not None: 2343 ax2.set_xlim(scatter_xlim) 2344 else: 2345 ax2.set_xlim(0, np.ceil(df["emission_rate"].max() * 1.05)) 2346 2347 if scatter_ylim is not None: 2348 ax2.set_ylim(scatter_ylim) 2349 else: 2350 ax2.set_ylim(0, np.ceil(df["delta_ch4"].max() * 1.05)) 2351 2352 # 凡例の表示 2353 if add_legend: 2354 for ax in axes: 2355 ax.legend( 2356 bbox_to_anchor=(0.5, -0.30), 2357 loc="upper center", 2358 ncol=len(existing_types), 2359 ) 2360 2361 plt.tight_layout() 2362 2363 # 図の保存 2364 if save_fig: 2365 if output_dir is None: 2366 raise ValueError( 2367 "save_fig=Trueの場合、output_dirを指定する必要があります。有効なディレクトリパスを指定してください。" 2368 ) 2369 os.makedirs(output_dir, exist_ok=True) 2370 output_path = os.path.join(output_dir, output_filename) 2371 plt.savefig(output_path, bbox_inches="tight", dpi=dpi) 2372 # 図の表示 2373 if show_fig: 2374 plt.show() 2375 else: 2376 plt.close(fig=fig) 2377 2378 if print_summary: 2379 # デバッグ用の出力 2380 print("\nビンごとの集計:") 2381 print(f"{'Range':>12} | {'bio':>8} | {'gas':>8} | {'total':>8}") 2382 print("-" * 50) 2383 2384 for i in range(len(bins) - 1): 2385 bin_start = bins[i] 2386 bin_end = bins[i + 1] 2387 2388 # 各タイプのカウントを計算 2389 counts_by_type: dict[HotspotType, int] = {"bio": 0, "gas": 0, "comb": 0} 2390 total = 0 2391 for spot_type in existing_types: 2392 mask = ( 2393 (df["type"] == spot_type) 2394 & (df["emission_rate"] >= bin_start) 2395 & (df["emission_rate"] < bin_end) 2396 ) 2397 count = len(df[mask]) 2398 counts_by_type[spot_type] = count 2399 total += count 2400 2401 # カウントが0の場合はスキップ 2402 if total > 0: 2403 range_str = f"{bin_start:5.1f}-{bin_end:<5.1f}" 2404 bio_count = counts_by_type.get("bio", 0) 2405 gas_count = counts_by_type.get("gas", 0) 2406 print( 2407 f"{range_str:>12} | {bio_count:8d} | {gas_count:8d} | {total:8d}" 2408 )
排出量分析のプロットを作成する静的メソッド。
Parameters:
emission_data_list : list[EmissionData]
EmissionDataオブジェクトのリスト。
output_dir : str | Path | None
出力先ディレクトリのパス。
output_filename : str
保存するファイル名。デフォルトは"emission_analysis.png"。
dpi : int
プロットの解像度。デフォルトは300。
figsize : tuple[float, float]
プロットのサイズ。デフォルトは(12, 5)。
add_legend : bool
凡例を追加するかどうか。デフォルトはTrue。
hist_log_y : bool
ヒストグラムのy軸を対数スケールにするかどうか。デフォルトはFalse。
hist_xlim : tuple[float, float] | None
ヒストグラムのx軸の範囲。デフォルトはNone。
hist_ylim : tuple[float, float] | None
ヒストグラムのy軸の範囲。デフォルトはNone。
scatter_xlim : tuple[float, float] | None
散布図のx軸の範囲。デフォルトはNone。
scatter_ylim : tuple[float, float] | None
散布図のy軸の範囲。デフォルトはNone。
hist_bin_width : float
ヒストグラムのビンの幅。デフォルトは0.5。
print_summary : bool
集計結果を表示するかどうか。デフォルトはFalse。
save_fig : bool
図をファイルに保存するかどうか。デフォルトはFalse。
show_fig : bool
図を表示するかどうか。デフォルトはTrue。
show_scatter : bool
散布図(右図)を表示するかどうか。デフォルトはTrue。
168@dataclass 169class MSAInputConfig: 170 """入力ファイルの設定を保持するデータクラス 171 172 Parameters: 173 ------ 174 fs : float 175 サンプリング周波数(Hz) 176 lag : float 177 測器の遅れ時間(秒) 178 path : Path | str 179 ファイルパス 180 correction_type : str | None 181 適用する補正式の種類を表す文字列 182 """ 183 184 fs: float # サンプリング周波数(Hz) 185 lag: float # 測器の遅れ時間(秒) 186 path: Path | str # ファイルパス 187 correction_type: str | None = None # 適用する補正式の種類を表す文字列 188 189 def __post_init__(self) -> None: 190 """ 191 インスタンス生成後に入力値の検証を行います。 192 193 Raises: 194 ------ 195 ValueError: 遅延時間が負の値である場合、またはサポートされていないファイル拡張子の場合。 196 """ 197 # fsが有効かを確認 198 if not isinstance(self.fs, (int, float)) or self.fs <= 0: 199 raise ValueError( 200 f"Invalid sampling frequency: {self.fs}. Must be a positive float." 201 ) 202 # lagが0以上のfloatかを確認 203 if not isinstance(self.lag, (int, float)) or self.lag < 0: 204 raise ValueError( 205 f"Invalid lag value: {self.lag}. Must be a non-negative float." 206 ) 207 # 拡張子の確認 208 supported_extensions: list[str] = [".txt", ".csv"] 209 extension = Path(self.path).suffix 210 if extension not in supported_extensions: 211 raise ValueError( 212 f"Unsupported file extension: '{extension}'. Supported: {supported_extensions}" 213 ) 214 # 与えられたcorrection_typeがNoneでない場合、CORRECTION_TYPES_PATTERNに含まれているかを検証します 215 if self.correction_type is not None: 216 if not isinstance(self.correction_type, str): 217 raise ValueError( 218 f"Invalid correction_type: {self.correction_type}. Must be a str instance." 219 ) 220 if self.correction_type not in CORRECTION_TYPES_PATTERN: 221 raise ValueError( 222 f"Invalid correction_type: {self.correction_type}. Must be one of {CORRECTION_TYPES_PATTERN}." 223 ) 224 225 @classmethod 226 def validate_and_create( 227 cls, 228 fs: float, 229 lag: float, 230 path: Path | str, 231 correction_type: str | None, 232 ) -> "MSAInputConfig": 233 """ 234 入力値を検証し、MSAInputConfigインスタンスを生成するファクトリメソッドです。 235 236 指定された遅延時間、サンプリング周波数、およびファイルパスが有効であることを確認し、 237 有効な場合に新しいMSAInputConfigオブジェクトを返します。 238 239 Parameters: 240 ------ 241 fs : float 242 サンプリング周波数。正のfloatである必要があります。 243 lag : float 244 遅延時間。0以上のfloatである必要があります。 245 path : Path | str 246 入力ファイルのパス。サポートされている拡張子は.txtと.csvです。 247 correction_type : str | None 248 適用する補正式の種類を表す文字列。 249 250 Returns: 251 ------ 252 MSAInputConfig 253 検証された入力設定を持つMSAInputConfigオブジェクト。 254 """ 255 return cls(fs=fs, lag=lag, path=path, correction_type=correction_type)
入力ファイルの設定を保持するデータクラス
Parameters:
fs : float
サンプリング周波数(Hz)
lag : float
測器の遅れ時間(秒)
path : Path | str
ファイルパス
correction_type : str | None
適用する補正式の種類を表す文字列
225 @classmethod 226 def validate_and_create( 227 cls, 228 fs: float, 229 lag: float, 230 path: Path | str, 231 correction_type: str | None, 232 ) -> "MSAInputConfig": 233 """ 234 入力値を検証し、MSAInputConfigインスタンスを生成するファクトリメソッドです。 235 236 指定された遅延時間、サンプリング周波数、およびファイルパスが有効であることを確認し、 237 有効な場合に新しいMSAInputConfigオブジェクトを返します。 238 239 Parameters: 240 ------ 241 fs : float 242 サンプリング周波数。正のfloatである必要があります。 243 lag : float 244 遅延時間。0以上のfloatである必要があります。 245 path : Path | str 246 入力ファイルのパス。サポートされている拡張子は.txtと.csvです。 247 correction_type : str | None 248 適用する補正式の種類を表す文字列。 249 250 Returns: 251 ------ 252 MSAInputConfig 253 検証された入力設定を持つMSAInputConfigオブジェクト。 254 """ 255 return cls(fs=fs, lag=lag, path=path, correction_type=correction_type)
入力値を検証し、MSAInputConfigインスタンスを生成するファクトリメソッドです。
指定された遅延時間、サンプリング周波数、およびファイルパスが有効であることを確認し、 有効な場合に新しいMSAInputConfigオブジェクトを返します。
Parameters:
fs : float
サンプリング周波数。正のfloatである必要があります。
lag : float
遅延時間。0以上のfloatである必要があります。
path : Path | str
入力ファイルのパス。サポートされている拡張子は.txtと.csvです。
correction_type : str | None
適用する補正式の種類を表す文字列。
Returns:
MSAInputConfig
検証された入力設定を持つMSAInputConfigオブジェクト。
8class MonthlyConverter: 9 """ 10 Monthlyシート(Excel)を一括で読み込み、DataFrameに変換するクラス。 11 デフォルトは'SA.Ultra.*.xlsx'に対応していますが、コンストラクタのfile_patternを 12 変更すると別のシートにも対応可能です(例: 'SA.Picaro.*.xlsx')。 13 """ 14 15 FILE_DATE_FORMAT = "%Y.%m" # ファイル名用 16 PERIOD_DATE_FORMAT = "%Y-%m-%d" # 期間指定用 17 18 def __init__( 19 self, 20 directory: str | Path, 21 file_pattern: str = "SA.Ultra.*.xlsx", 22 logger: Logger | None = None, 23 logging_debug: bool = False, 24 ): 25 """ 26 MonthlyConverterクラスのコンストラクタ 27 28 Parameters: 29 ------ 30 directory : str | Path 31 Excelファイルが格納されているディレクトリのパス 32 file_pattern : str 33 ファイル名のパターン。デフォルトは'SA.Ultra.*.xlsx'。 34 logger : Logger | None 35 使用するロガー。Noneの場合は新しいロガーを作成します。 36 logging_debug : bool 37 ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。 38 """ 39 # ロガー 40 log_level: int = INFO 41 if logging_debug: 42 log_level = DEBUG 43 self.logger: Logger = MonthlyConverter.setup_logger(logger, log_level) 44 45 self._directory = Path(directory) 46 if not self._directory.exists(): 47 raise NotADirectoryError(f"Directory not found: {self._directory}") 48 49 # Excelファイルのパスを保持 50 self._excel_files: dict[str, pd.ExcelFile] = {} 51 self._file_pattern: str = file_pattern 52 53 @staticmethod 54 def setup_logger(logger: Logger | None, log_level: int = INFO) -> Logger: 55 """ 56 ロガーを設定します。 57 58 このメソッドは、ロギングの設定を行い、ログメッセージのフォーマットを指定します。 59 ログメッセージには、日付、ログレベル、メッセージが含まれます。 60 61 渡されたロガーがNoneまたは不正な場合は、新たにロガーを作成し、標準出力に 62 ログメッセージが表示されるようにStreamHandlerを追加します。ロガーのレベルは 63 引数で指定されたlog_levelに基づいて設定されます。 64 65 Parameters: 66 ------ 67 logger : Logger | None 68 使用するロガー。Noneの場合は新しいロガーを作成します。 69 log_level : int 70 ロガーのログレベル。デフォルトはINFO。 71 72 Returns: 73 ------ 74 Logger 75 設定されたロガーオブジェクト。 76 """ 77 if logger is not None and isinstance(logger, Logger): 78 return logger 79 # 渡されたロガーがNoneまたは正しいものでない場合は独自に設定 80 new_logger: Logger = getLogger() 81 # 既存のハンドラーをすべて削除 82 for handler in new_logger.handlers[:]: 83 new_logger.removeHandler(handler) 84 new_logger.setLevel(log_level) # ロガーのレベルを設定 85 ch = StreamHandler() 86 ch_formatter = Formatter("%(asctime)s - %(levelname)s - %(message)s") 87 ch.setFormatter(ch_formatter) # フォーマッターをハンドラーに設定 88 new_logger.addHandler(ch) # StreamHandlerの追加 89 return new_logger 90 91 def close(self) -> None: 92 """ 93 すべてのExcelファイルをクローズする 94 """ 95 for excel_file in self._excel_files.values(): 96 excel_file.close() 97 self._excel_files.clear() 98 99 def get_available_dates(self) -> list[str]: 100 """ 101 利用可能なファイルの日付一覧を返却します。 102 103 Returns: 104 ------ 105 list[str] 106 'yyyy.MM'形式の日付リスト 107 """ 108 dates = [] 109 for file_name in self._directory.glob(self._file_pattern): 110 try: 111 date = self._extract_date(file_name.name) 112 dates.append(date.strftime(self.FILE_DATE_FORMAT)) 113 except ValueError: 114 continue 115 return sorted(dates) 116 117 def get_sheet_names(self, file_name: str) -> list[str]: 118 """ 119 指定されたファイルで利用可能なシート名の一覧を返却する 120 121 Parameters: 122 ------ 123 file_name : str 124 Excelファイル名 125 126 Returns: 127 ------ 128 list[str] 129 シート名のリスト 130 """ 131 if file_name not in self._excel_files: 132 file_path = self._directory / file_name 133 if not file_path.exists(): 134 raise FileNotFoundError(f"File not found: {file_path}") 135 self._excel_files[file_name] = pd.ExcelFile(file_path) 136 return self._excel_files[file_name].sheet_names 137 138 def read_sheets( 139 self, 140 sheet_names: str | list[str], 141 columns: list[str] | None = None, # 新しいパラメータを追加 142 col_datetime: str = "Date", 143 header: int = 0, 144 skiprows: int | list[int] = [1], 145 start_date: str | None = None, 146 end_date: str | None = None, 147 include_end_date: bool = True, 148 sort_by_date: bool = True, 149 ) -> pd.DataFrame: 150 """ 151 指定されたシートを読み込み、DataFrameとして返却します。 152 デフォルトでは2行目(単位の行)はスキップされます。 153 重複するカラム名がある場合は、より先に指定されたシートに存在するカラムの値を保持します。 154 155 Parameters: 156 ------ 157 sheet_names : str | list[str] 158 読み込むシート名。文字列または文字列のリストを指定できます。 159 columns : list[str] | None 160 残すカラム名のリスト。Noneの場合は全てのカラムを保持します。 161 col_datetime : str 162 日付と時刻の情報が含まれるカラム名。デフォルトは'Date'。 163 header : int 164 データのヘッダー行を指定します。デフォルトは0。 165 skiprows : int | list[int] 166 スキップする行数。デフォルトでは1行目をスキップします。 167 start_date : str | None 168 開始日 ('yyyy-MM-dd')。この日付の'00:00:00'のデータが開始行となります。 169 end_date : str | None 170 終了日 ('yyyy-MM-dd')。この日付をデータに含めるかはinclude_end_dateフラグによって変わります。 171 include_end_date : bool 172 終了日を含めるかどうか。デフォルトはTrueです。 173 sort_by_date : bool 174 ファイルの日付でソートするかどうか。デフォルトはTrueです。 175 176 Returns: 177 ------ 178 pd.DataFrame 179 読み込まれたデータを結合したDataFrameを返します。 180 """ 181 if isinstance(sheet_names, str): 182 sheet_names = [sheet_names] 183 184 self._load_excel_files(start_date, end_date) 185 186 if not self._excel_files: 187 raise ValueError("No Excel files found matching the criteria") 188 189 # ファイルを日付順にソート 190 sorted_files = ( 191 sorted(self._excel_files.items(), key=lambda x: self._extract_date(x[0])) 192 if sort_by_date 193 else self._excel_files.items() 194 ) 195 196 # 各シートのデータを格納するリスト 197 sheet_dfs = {sheet_name: [] for sheet_name in sheet_names} 198 199 # 各ファイルからデータを読み込む 200 for file_name, excel_file in sorted_files: 201 file_date = self._extract_date(file_name) 202 203 for sheet_name in sheet_names: 204 if sheet_name in excel_file.sheet_names: 205 df = pd.read_excel( 206 excel_file, 207 sheet_name=sheet_name, 208 header=header, 209 skiprows=skiprows, 210 na_values=[ 211 "#DIV/0!", 212 "#VALUE!", 213 "#REF!", 214 "#N/A", 215 "#NAME?", 216 "NAN", 217 ], 218 ) 219 # 年と月を追加 220 df["year"] = file_date.year 221 df["month"] = file_date.month 222 sheet_dfs[sheet_name].append(df) 223 224 if not any(sheet_dfs.values()): 225 raise ValueError(f"No sheets found matching: {sheet_names}") 226 227 # 各シートのデータを結合 228 combined_sheets = {} 229 for sheet_name, dfs in sheet_dfs.items(): 230 if dfs: # シートにデータがある場合のみ結合 231 combined_sheets[sheet_name] = pd.concat(dfs, ignore_index=True) 232 233 # 最初のシートをベースにする 234 base_df = combined_sheets[sheet_names[0]] 235 236 # 2つ目以降のシートを結合 237 for sheet_name in sheet_names[1:]: 238 if sheet_name in combined_sheets: 239 base_df = self.merge_dataframes( 240 base_df, combined_sheets[sheet_name], date_column=col_datetime 241 ) 242 243 # 日付でフィルタリング 244 if start_date: 245 start_dt = pd.to_datetime(start_date) 246 base_df = base_df[base_df[col_datetime] >= start_dt] 247 248 if end_date: 249 end_dt = pd.to_datetime(end_date) 250 if include_end_date: 251 end_dt += pd.Timedelta(days=1) 252 base_df = base_df[base_df[col_datetime] < end_dt] 253 254 # カラムの選択 255 if columns is not None: 256 required_columns = [col_datetime, "year", "month"] 257 available_columns = base_df.columns.tolist() # 利用可能なカラムを取得 258 if not all(col in available_columns for col in columns): 259 raise ValueError( 260 f"指定されたカラムが見つかりません: {columns}. 利用可能なカラム: {available_columns}" 261 ) 262 selected_columns = list(set(columns + required_columns)) 263 base_df = base_df[selected_columns] 264 265 return base_df 266 267 def __enter__(self) -> "MonthlyConverter": 268 return self 269 270 def __exit__(self, exc_type, exc_val, exc_tb) -> None: 271 self.close() 272 273 def _extract_date(self, file_name: str) -> datetime: 274 """ 275 ファイル名から日付を抽出する 276 277 Parameters: 278 ------ 279 file_name : str 280 "SA.Ultra.yyyy.MM.xlsx"または"SA.Picaro.yyyy.MM.xlsx"形式のファイル名 281 282 Returns: 283 ------ 284 datetime 285 抽出された日付 286 """ 287 # ファイル名から日付部分を抽出 288 date_str = ".".join(file_name.split(".")[-3:-1]) # "yyyy.MM"の部分を取得 289 return datetime.strptime(date_str, self.FILE_DATE_FORMAT) 290 291 def _load_excel_files( 292 self, start_date: str | None = None, end_date: str | None = None 293 ) -> None: 294 """ 295 指定された日付範囲のExcelファイルを読み込む 296 297 Parameters: 298 ------ 299 start_date : str | None 300 開始日 ('yyyy-MM-dd'形式) 301 end_date : str | None 302 終了日 ('yyyy-MM-dd'形式) 303 """ 304 # 期間指定がある場合は、yyyy-MM-dd形式から年月のみを抽出 305 start_dt = None 306 end_dt = None 307 if start_date: 308 temp_dt = datetime.strptime(start_date, self.PERIOD_DATE_FORMAT) 309 start_dt = datetime(temp_dt.year, temp_dt.month, 1) 310 if end_date: 311 temp_dt = datetime.strptime(end_date, self.PERIOD_DATE_FORMAT) 312 end_dt = datetime(temp_dt.year, temp_dt.month, 1) 313 314 # 既存のファイルをクリア 315 self.close() 316 317 for excel_path in self._directory.glob(self._file_pattern): 318 try: 319 file_date = self._extract_date(excel_path.name) 320 321 # 日付範囲チェック 322 if start_dt and file_date < start_dt: 323 continue 324 if end_dt and file_date > end_dt: 325 continue 326 327 if excel_path.name not in self._excel_files: 328 self._excel_files[excel_path.name] = pd.ExcelFile(excel_path) 329 330 except ValueError as e: 331 self.logger.warning( 332 f"Could not parse date from file {excel_path.name}: {e}" 333 ) 334 335 @staticmethod 336 def extract_monthly_data( 337 df: pd.DataFrame, 338 target_months: list[int], 339 start_day: int | None = None, 340 end_day: int | None = None, 341 datetime_column: str = "Date", 342 ) -> pd.DataFrame: 343 """ 344 指定された月と期間のデータを抽出します。 345 346 Parameters: 347 ------ 348 df : pd.DataFrame 349 入力データフレーム。 350 target_months : list[int] 351 抽出したい月のリスト(1から12の整数)。 352 start_day : int | None 353 開始日(1から31の整数)。Noneの場合は月初め。 354 end_day : int | None 355 終了日(1から31の整数)。Noneの場合は月末。 356 datetime_column : str, optional 357 日付を含む列の名前。デフォルトは"Date"。 358 359 Returns: 360 ------ 361 pd.DataFrame 362 指定された期間のデータのみを含むデータフレーム。 363 """ 364 # 入力チェック 365 if not all(1 <= month <= 12 for month in target_months): 366 raise ValueError("target_monthsは1から12の間である必要があります") 367 368 if start_day is not None and not 1 <= start_day <= 31: 369 raise ValueError("start_dayは1から31の間である必要があります") 370 371 if end_day is not None and not 1 <= end_day <= 31: 372 raise ValueError("end_dayは1から31の間である必要があります") 373 374 if start_day is not None and end_day is not None and start_day > end_day: 375 raise ValueError("start_dayはend_day以下である必要があります") 376 377 # datetime_column をDatetime型に変換 378 df = df.copy() 379 df[datetime_column] = pd.to_datetime(df[datetime_column]) 380 381 # 月でフィルタリング 382 monthly_data = df[df[datetime_column].dt.month.isin(target_months)] 383 384 # 日付範囲でフィルタリング 385 if start_day is not None: 386 monthly_data = monthly_data[ 387 monthly_data[datetime_column].dt.day >= start_day 388 ] 389 if end_day is not None: 390 monthly_data = monthly_data[monthly_data[datetime_column].dt.day <= end_day] 391 392 return monthly_data 393 394 @staticmethod 395 def merge_dataframes( 396 df1: pd.DataFrame, df2: pd.DataFrame, date_column: str = "Date" 397 ) -> pd.DataFrame: 398 """ 399 2つのDataFrameを結合します。重複するカラムは元の名前とサフィックス付きの両方を保持します。 400 401 Parameters: 402 ------ 403 df1 : pd.DataFrame 404 ベースとなるDataFrame 405 df2 : pd.DataFrame 406 結合するDataFrame 407 date_column : str 408 日付カラムの名前。デフォルトは"Date" 409 410 Returns: 411 ------ 412 pd.DataFrame 413 結合されたDataFrame 414 """ 415 # インデックスをリセット 416 df1 = df1.reset_index(drop=True) 417 df2 = df2.reset_index(drop=True) 418 419 # 日付カラムを統一 420 df2[date_column] = df1[date_column] 421 422 # 重複しないカラムと重複するカラムを分離 423 duplicate_cols = [date_column, "year", "month"] # 常に除外するカラム 424 overlapping_cols = [ 425 col 426 for col in df2.columns 427 if col in df1.columns and col not in duplicate_cols 428 ] 429 unique_cols = [ 430 col 431 for col in df2.columns 432 if col not in df1.columns and col not in duplicate_cols 433 ] 434 435 # 結果のDataFrameを作成 436 result = df1.copy() 437 438 # 重複しないカラムを追加 439 for col in unique_cols: 440 result[col] = df2[col] 441 442 # 重複するカラムを処理 443 for col in overlapping_cols: 444 # 元のカラムはdf1の値を保持(既に result に含まれている) 445 # _x サフィックスでdf1の値を追加 446 result[f"{col}_x"] = df1[col] 447 # _y サフィックスでdf2の値を追加 448 result[f"{col}_y"] = df2[col] 449 450 return result
Monthlyシート(Excel)を一括で読み込み、DataFrameに変換するクラス。 デフォルトは'SA.Ultra..xlsx'に対応していますが、コンストラクタのfile_patternを 変更すると別のシートにも対応可能です(例: 'SA.Picaro..xlsx')。
18 def __init__( 19 self, 20 directory: str | Path, 21 file_pattern: str = "SA.Ultra.*.xlsx", 22 logger: Logger | None = None, 23 logging_debug: bool = False, 24 ): 25 """ 26 MonthlyConverterクラスのコンストラクタ 27 28 Parameters: 29 ------ 30 directory : str | Path 31 Excelファイルが格納されているディレクトリのパス 32 file_pattern : str 33 ファイル名のパターン。デフォルトは'SA.Ultra.*.xlsx'。 34 logger : Logger | None 35 使用するロガー。Noneの場合は新しいロガーを作成します。 36 logging_debug : bool 37 ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。 38 """ 39 # ロガー 40 log_level: int = INFO 41 if logging_debug: 42 log_level = DEBUG 43 self.logger: Logger = MonthlyConverter.setup_logger(logger, log_level) 44 45 self._directory = Path(directory) 46 if not self._directory.exists(): 47 raise NotADirectoryError(f"Directory not found: {self._directory}") 48 49 # Excelファイルのパスを保持 50 self._excel_files: dict[str, pd.ExcelFile] = {} 51 self._file_pattern: str = file_pattern
MonthlyConverterクラスのコンストラクタ
Parameters:
directory : str | Path
Excelファイルが格納されているディレクトリのパス
file_pattern : str
ファイル名のパターン。デフォルトは'SA.Ultra.*.xlsx'。
logger : Logger | None
使用するロガー。Noneの場合は新しいロガーを作成します。
logging_debug : bool
ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。
53 @staticmethod 54 def setup_logger(logger: Logger | None, log_level: int = INFO) -> Logger: 55 """ 56 ロガーを設定します。 57 58 このメソッドは、ロギングの設定を行い、ログメッセージのフォーマットを指定します。 59 ログメッセージには、日付、ログレベル、メッセージが含まれます。 60 61 渡されたロガーがNoneまたは不正な場合は、新たにロガーを作成し、標準出力に 62 ログメッセージが表示されるようにStreamHandlerを追加します。ロガーのレベルは 63 引数で指定されたlog_levelに基づいて設定されます。 64 65 Parameters: 66 ------ 67 logger : Logger | None 68 使用するロガー。Noneの場合は新しいロガーを作成します。 69 log_level : int 70 ロガーのログレベル。デフォルトはINFO。 71 72 Returns: 73 ------ 74 Logger 75 設定されたロガーオブジェクト。 76 """ 77 if logger is not None and isinstance(logger, Logger): 78 return logger 79 # 渡されたロガーがNoneまたは正しいものでない場合は独自に設定 80 new_logger: Logger = getLogger() 81 # 既存のハンドラーをすべて削除 82 for handler in new_logger.handlers[:]: 83 new_logger.removeHandler(handler) 84 new_logger.setLevel(log_level) # ロガーのレベルを設定 85 ch = StreamHandler() 86 ch_formatter = Formatter("%(asctime)s - %(levelname)s - %(message)s") 87 ch.setFormatter(ch_formatter) # フォーマッターをハンドラーに設定 88 new_logger.addHandler(ch) # StreamHandlerの追加 89 return new_logger
ロガーを設定します。
このメソッドは、ロギングの設定を行い、ログメッセージのフォーマットを指定します。 ログメッセージには、日付、ログレベル、メッセージが含まれます。
渡されたロガーがNoneまたは不正な場合は、新たにロガーを作成し、標準出力に ログメッセージが表示されるようにStreamHandlerを追加します。ロガーのレベルは 引数で指定されたlog_levelに基づいて設定されます。
Parameters:
logger : Logger | None
使用するロガー。Noneの場合は新しいロガーを作成します。
log_level : int
ロガーのログレベル。デフォルトはINFO。
Returns:
Logger
設定されたロガーオブジェクト。
91 def close(self) -> None: 92 """ 93 すべてのExcelファイルをクローズする 94 """ 95 for excel_file in self._excel_files.values(): 96 excel_file.close() 97 self._excel_files.clear()
すべてのExcelファイルをクローズする
99 def get_available_dates(self) -> list[str]: 100 """ 101 利用可能なファイルの日付一覧を返却します。 102 103 Returns: 104 ------ 105 list[str] 106 'yyyy.MM'形式の日付リスト 107 """ 108 dates = [] 109 for file_name in self._directory.glob(self._file_pattern): 110 try: 111 date = self._extract_date(file_name.name) 112 dates.append(date.strftime(self.FILE_DATE_FORMAT)) 113 except ValueError: 114 continue 115 return sorted(dates)
利用可能なファイルの日付一覧を返却します。
Returns:
list[str]
'yyyy.MM'形式の日付リスト
117 def get_sheet_names(self, file_name: str) -> list[str]: 118 """ 119 指定されたファイルで利用可能なシート名の一覧を返却する 120 121 Parameters: 122 ------ 123 file_name : str 124 Excelファイル名 125 126 Returns: 127 ------ 128 list[str] 129 シート名のリスト 130 """ 131 if file_name not in self._excel_files: 132 file_path = self._directory / file_name 133 if not file_path.exists(): 134 raise FileNotFoundError(f"File not found: {file_path}") 135 self._excel_files[file_name] = pd.ExcelFile(file_path) 136 return self._excel_files[file_name].sheet_names
指定されたファイルで利用可能なシート名の一覧を返却する
Parameters:
file_name : str
Excelファイル名
Returns:
list[str]
シート名のリスト
138 def read_sheets( 139 self, 140 sheet_names: str | list[str], 141 columns: list[str] | None = None, # 新しいパラメータを追加 142 col_datetime: str = "Date", 143 header: int = 0, 144 skiprows: int | list[int] = [1], 145 start_date: str | None = None, 146 end_date: str | None = None, 147 include_end_date: bool = True, 148 sort_by_date: bool = True, 149 ) -> pd.DataFrame: 150 """ 151 指定されたシートを読み込み、DataFrameとして返却します。 152 デフォルトでは2行目(単位の行)はスキップされます。 153 重複するカラム名がある場合は、より先に指定されたシートに存在するカラムの値を保持します。 154 155 Parameters: 156 ------ 157 sheet_names : str | list[str] 158 読み込むシート名。文字列または文字列のリストを指定できます。 159 columns : list[str] | None 160 残すカラム名のリスト。Noneの場合は全てのカラムを保持します。 161 col_datetime : str 162 日付と時刻の情報が含まれるカラム名。デフォルトは'Date'。 163 header : int 164 データのヘッダー行を指定します。デフォルトは0。 165 skiprows : int | list[int] 166 スキップする行数。デフォルトでは1行目をスキップします。 167 start_date : str | None 168 開始日 ('yyyy-MM-dd')。この日付の'00:00:00'のデータが開始行となります。 169 end_date : str | None 170 終了日 ('yyyy-MM-dd')。この日付をデータに含めるかはinclude_end_dateフラグによって変わります。 171 include_end_date : bool 172 終了日を含めるかどうか。デフォルトはTrueです。 173 sort_by_date : bool 174 ファイルの日付でソートするかどうか。デフォルトはTrueです。 175 176 Returns: 177 ------ 178 pd.DataFrame 179 読み込まれたデータを結合したDataFrameを返します。 180 """ 181 if isinstance(sheet_names, str): 182 sheet_names = [sheet_names] 183 184 self._load_excel_files(start_date, end_date) 185 186 if not self._excel_files: 187 raise ValueError("No Excel files found matching the criteria") 188 189 # ファイルを日付順にソート 190 sorted_files = ( 191 sorted(self._excel_files.items(), key=lambda x: self._extract_date(x[0])) 192 if sort_by_date 193 else self._excel_files.items() 194 ) 195 196 # 各シートのデータを格納するリスト 197 sheet_dfs = {sheet_name: [] for sheet_name in sheet_names} 198 199 # 各ファイルからデータを読み込む 200 for file_name, excel_file in sorted_files: 201 file_date = self._extract_date(file_name) 202 203 for sheet_name in sheet_names: 204 if sheet_name in excel_file.sheet_names: 205 df = pd.read_excel( 206 excel_file, 207 sheet_name=sheet_name, 208 header=header, 209 skiprows=skiprows, 210 na_values=[ 211 "#DIV/0!", 212 "#VALUE!", 213 "#REF!", 214 "#N/A", 215 "#NAME?", 216 "NAN", 217 ], 218 ) 219 # 年と月を追加 220 df["year"] = file_date.year 221 df["month"] = file_date.month 222 sheet_dfs[sheet_name].append(df) 223 224 if not any(sheet_dfs.values()): 225 raise ValueError(f"No sheets found matching: {sheet_names}") 226 227 # 各シートのデータを結合 228 combined_sheets = {} 229 for sheet_name, dfs in sheet_dfs.items(): 230 if dfs: # シートにデータがある場合のみ結合 231 combined_sheets[sheet_name] = pd.concat(dfs, ignore_index=True) 232 233 # 最初のシートをベースにする 234 base_df = combined_sheets[sheet_names[0]] 235 236 # 2つ目以降のシートを結合 237 for sheet_name in sheet_names[1:]: 238 if sheet_name in combined_sheets: 239 base_df = self.merge_dataframes( 240 base_df, combined_sheets[sheet_name], date_column=col_datetime 241 ) 242 243 # 日付でフィルタリング 244 if start_date: 245 start_dt = pd.to_datetime(start_date) 246 base_df = base_df[base_df[col_datetime] >= start_dt] 247 248 if end_date: 249 end_dt = pd.to_datetime(end_date) 250 if include_end_date: 251 end_dt += pd.Timedelta(days=1) 252 base_df = base_df[base_df[col_datetime] < end_dt] 253 254 # カラムの選択 255 if columns is not None: 256 required_columns = [col_datetime, "year", "month"] 257 available_columns = base_df.columns.tolist() # 利用可能なカラムを取得 258 if not all(col in available_columns for col in columns): 259 raise ValueError( 260 f"指定されたカラムが見つかりません: {columns}. 利用可能なカラム: {available_columns}" 261 ) 262 selected_columns = list(set(columns + required_columns)) 263 base_df = base_df[selected_columns] 264 265 return base_df
指定されたシートを読み込み、DataFrameとして返却します。 デフォルトでは2行目(単位の行)はスキップされます。 重複するカラム名がある場合は、より先に指定されたシートに存在するカラムの値を保持します。
Parameters:
sheet_names : str | list[str]
読み込むシート名。文字列または文字列のリストを指定できます。
columns : list[str] | None
残すカラム名のリスト。Noneの場合は全てのカラムを保持します。
col_datetime : str
日付と時刻の情報が含まれるカラム名。デフォルトは'Date'。
header : int
データのヘッダー行を指定します。デフォルトは0。
skiprows : int | list[int]
スキップする行数。デフォルトでは1行目をスキップします。
start_date : str | None
開始日 ('yyyy-MM-dd')。この日付の'00:00:00'のデータが開始行となります。
end_date : str | None
終了日 ('yyyy-MM-dd')。この日付をデータに含めるかはinclude_end_dateフラグによって変わります。
include_end_date : bool
終了日を含めるかどうか。デフォルトはTrueです。
sort_by_date : bool
ファイルの日付でソートするかどうか。デフォルトはTrueです。
Returns:
pd.DataFrame
読み込まれたデータを結合したDataFrameを返します。
335 @staticmethod 336 def extract_monthly_data( 337 df: pd.DataFrame, 338 target_months: list[int], 339 start_day: int | None = None, 340 end_day: int | None = None, 341 datetime_column: str = "Date", 342 ) -> pd.DataFrame: 343 """ 344 指定された月と期間のデータを抽出します。 345 346 Parameters: 347 ------ 348 df : pd.DataFrame 349 入力データフレーム。 350 target_months : list[int] 351 抽出したい月のリスト(1から12の整数)。 352 start_day : int | None 353 開始日(1から31の整数)。Noneの場合は月初め。 354 end_day : int | None 355 終了日(1から31の整数)。Noneの場合は月末。 356 datetime_column : str, optional 357 日付を含む列の名前。デフォルトは"Date"。 358 359 Returns: 360 ------ 361 pd.DataFrame 362 指定された期間のデータのみを含むデータフレーム。 363 """ 364 # 入力チェック 365 if not all(1 <= month <= 12 for month in target_months): 366 raise ValueError("target_monthsは1から12の間である必要があります") 367 368 if start_day is not None and not 1 <= start_day <= 31: 369 raise ValueError("start_dayは1から31の間である必要があります") 370 371 if end_day is not None and not 1 <= end_day <= 31: 372 raise ValueError("end_dayは1から31の間である必要があります") 373 374 if start_day is not None and end_day is not None and start_day > end_day: 375 raise ValueError("start_dayはend_day以下である必要があります") 376 377 # datetime_column をDatetime型に変換 378 df = df.copy() 379 df[datetime_column] = pd.to_datetime(df[datetime_column]) 380 381 # 月でフィルタリング 382 monthly_data = df[df[datetime_column].dt.month.isin(target_months)] 383 384 # 日付範囲でフィルタリング 385 if start_day is not None: 386 monthly_data = monthly_data[ 387 monthly_data[datetime_column].dt.day >= start_day 388 ] 389 if end_day is not None: 390 monthly_data = monthly_data[monthly_data[datetime_column].dt.day <= end_day] 391 392 return monthly_data
指定された月と期間のデータを抽出します。
Parameters:
df : pd.DataFrame
入力データフレーム。
target_months : list[int]
抽出したい月のリスト(1から12の整数)。
start_day : int | None
開始日(1から31の整数)。Noneの場合は月初め。
end_day : int | None
終了日(1から31の整数)。Noneの場合は月末。
datetime_column : str, optional
日付を含む列の名前。デフォルトは"Date"。
Returns:
pd.DataFrame
指定された期間のデータのみを含むデータフレーム。
394 @staticmethod 395 def merge_dataframes( 396 df1: pd.DataFrame, df2: pd.DataFrame, date_column: str = "Date" 397 ) -> pd.DataFrame: 398 """ 399 2つのDataFrameを結合します。重複するカラムは元の名前とサフィックス付きの両方を保持します。 400 401 Parameters: 402 ------ 403 df1 : pd.DataFrame 404 ベースとなるDataFrame 405 df2 : pd.DataFrame 406 結合するDataFrame 407 date_column : str 408 日付カラムの名前。デフォルトは"Date" 409 410 Returns: 411 ------ 412 pd.DataFrame 413 結合されたDataFrame 414 """ 415 # インデックスをリセット 416 df1 = df1.reset_index(drop=True) 417 df2 = df2.reset_index(drop=True) 418 419 # 日付カラムを統一 420 df2[date_column] = df1[date_column] 421 422 # 重複しないカラムと重複するカラムを分離 423 duplicate_cols = [date_column, "year", "month"] # 常に除外するカラム 424 overlapping_cols = [ 425 col 426 for col in df2.columns 427 if col in df1.columns and col not in duplicate_cols 428 ] 429 unique_cols = [ 430 col 431 for col in df2.columns 432 if col not in df1.columns and col not in duplicate_cols 433 ] 434 435 # 結果のDataFrameを作成 436 result = df1.copy() 437 438 # 重複しないカラムを追加 439 for col in unique_cols: 440 result[col] = df2[col] 441 442 # 重複するカラムを処理 443 for col in overlapping_cols: 444 # 元のカラムはdf1の値を保持(既に result に含まれている) 445 # _x サフィックスでdf1の値を追加 446 result[f"{col}_x"] = df1[col] 447 # _y サフィックスでdf2の値を追加 448 result[f"{col}_y"] = df2[col] 449 450 return result
2つのDataFrameを結合します。重複するカラムは元の名前とサフィックス付きの両方を保持します。
Parameters:
df1 : pd.DataFrame
ベースとなるDataFrame
df2 : pd.DataFrame
結合するDataFrame
date_column : str
日付カラムの名前。デフォルトは"Date"
Returns:
pd.DataFrame
結合されたDataFrame
63class MonthlyFiguresGenerator: 64 def __init__( 65 self, 66 logger: Logger | None = None, 67 logging_debug: bool = False, 68 ) -> None: 69 """ 70 クラスのコンストラクタ 71 72 Parameters: 73 ------ 74 logger : Logger | None 75 使用するロガー。Noneの場合は新しいロガーを作成します。 76 logging_debug : bool 77 ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。 78 """ 79 # ロガー 80 log_level: int = INFO 81 if logging_debug: 82 log_level = DEBUG 83 self.logger: Logger = MonthlyFiguresGenerator.setup_logger(logger, log_level) 84 85 def plot_c1c2_fluxes_timeseries( 86 self, 87 df, 88 output_dir: str, 89 output_filename: str = "timeseries.png", 90 col_datetime: str = "Date", 91 col_c1_flux: str = "Fch4_ultra", 92 col_c2_flux: str = "Fc2h6_ultra", 93 ): 94 """ 95 月別のフラックスデータを時系列プロットとして出力する 96 97 Parameters: 98 ------ 99 df : pd.DataFrame 100 月別データを含むDataFrame 101 output_dir : str 102 出力ファイルを保存するディレクトリのパス 103 output_filename : str 104 出力ファイルの名前 105 col_datetime : str 106 日付を含む列の名前。デフォルトは"Date"。 107 col_c1_flux : str 108 CH4フラックスを含む列の名前。デフォルトは"Fch4_ultra"。 109 col_c2_flux : str 110 C2H6フラックスを含む列の名前。デフォルトは"Fc2h6_ultra"。 111 """ 112 os.makedirs(output_dir, exist_ok=True) 113 output_path: str = os.path.join(output_dir, output_filename) 114 115 # 図の作成 116 _, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 8), sharex=True) 117 118 # CH4フラックスのプロット 119 ax1.scatter(df[col_datetime], df[col_c1_flux], color="red", alpha=0.5, s=20) 120 ax1.set_ylabel(r"CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)") 121 ax1.set_ylim(-100, 600) 122 ax1.text(0.02, 0.98, "(a)", transform=ax1.transAxes, va="top", fontsize=20) 123 ax1.grid(True, alpha=0.3) 124 125 # C2H6フラックスのプロット 126 ax2.scatter( 127 df[col_datetime], 128 df[col_c2_flux], 129 color="orange", 130 alpha=0.5, 131 s=20, 132 ) 133 ax2.set_ylabel(r"C$_2$H$_6$ flux (nmol m$^{-2}$ s$^{-1}$)") 134 ax2.set_ylim(-20, 60) 135 ax2.text(0.02, 0.98, "(b)", transform=ax2.transAxes, va="top", fontsize=20) 136 ax2.grid(True, alpha=0.3) 137 138 # x軸の設定 139 ax2.xaxis.set_major_locator(mdates.MonthLocator(interval=1)) 140 ax2.xaxis.set_major_formatter(mdates.DateFormatter("%m")) 141 plt.setp(ax2.get_xticklabels(), rotation=0, ha="right") 142 ax2.set_xlabel("Month") 143 144 # 図の保存 145 plt.savefig(output_path, dpi=300, bbox_inches="tight") 146 plt.close() 147 148 def plot_c1c2_concentrations_and_fluxes_timeseries( 149 self, 150 df: pd.DataFrame, 151 output_dir: str, 152 output_filename: str = "conc_flux_timeseries.png", 153 col_datetime: str = "Date", 154 col_ch4_conc: str = "CH4_ultra", 155 col_ch4_flux: str = "Fch4_ultra", 156 col_c2h6_conc: str = "C2H6_ultra", 157 col_c2h6_flux: str = "Fc2h6_ultra", 158 print_summary: bool = True, 159 ) -> None: 160 """ 161 CH4とC2H6の濃度とフラックスの時系列プロットを作成する 162 163 Parameters: 164 ------ 165 df : pd.DataFrame 166 月別データを含むDataFrame 167 output_dir : str 168 出力ディレクトリのパス 169 output_filename : str 170 出力ファイル名 171 col_datetime : str 172 日付列の名前 173 col_ch4_conc : str 174 CH4濃度列の名前 175 col_ch4_flux : str 176 CH4フラックス列の名前 177 col_c2h6_conc : str 178 C2H6濃度列の名前 179 col_c2h6_flux : str 180 C2H6フラックス列の名前 181 print_summary : bool 182 解析情報をprintするかどうか 183 """ 184 # 出力ディレクトリの作成 185 os.makedirs(output_dir, exist_ok=True) 186 output_path: str = os.path.join(output_dir, output_filename) 187 188 if print_summary: 189 # 統計情報の計算と表示 190 for name, col in [ 191 ("CH4 concentration", col_ch4_conc), 192 ("CH4 flux", col_ch4_flux), 193 ("C2H6 concentration", col_c2h6_conc), 194 ("C2H6 flux", col_c2h6_flux), 195 ]: 196 # NaNを除外してから統計量を計算 197 valid_data = df[col].dropna() 198 199 if len(valid_data) > 0: 200 percentile_5 = np.nanpercentile(valid_data, 5) 201 percentile_95 = np.nanpercentile(valid_data, 95) 202 mean_value = np.nanmean(valid_data) 203 positive_ratio = (valid_data > 0).mean() * 100 204 205 print(f"\n{name}:") 206 print( 207 f"90パーセンタイルレンジ: {percentile_5:.2f} - {percentile_95:.2f}" 208 ) 209 print(f"平均値: {mean_value:.2f}") 210 print(f"正の値の割合: {positive_ratio:.1f}%") 211 else: 212 print(f"\n{name}: データが存在しません") 213 214 # プロットの作成 215 _, (ax1, ax2, ax3, ax4) = plt.subplots(4, 1, figsize=(12, 16), sharex=True) 216 217 # CH4濃度のプロット 218 ax1.scatter(df[col_datetime], df[col_ch4_conc], color="red", alpha=0.5, s=20) 219 ax1.set_ylabel("CH$_4$ Concentration\n(ppm)") 220 ax1.set_ylim(1.8, 2.6) 221 ax1.text(0.02, 0.98, "(a)", transform=ax1.transAxes, va="top", fontsize=20) 222 ax1.grid(True, alpha=0.3) 223 224 # CH4フラックスのプロット 225 ax2.scatter(df[col_datetime], df[col_ch4_flux], color="red", alpha=0.5, s=20) 226 ax2.set_ylabel("CH$_4$ flux\n(nmol m$^{-2}$ s$^{-1}$)") 227 ax2.set_ylim(-100, 600) 228 # ax2.set_yticks([-100, 0, 200, 400, 600]) 229 ax2.text(0.02, 0.98, "(b)", transform=ax2.transAxes, va="top", fontsize=20) 230 ax2.grid(True, alpha=0.3) 231 232 # C2H6濃度のプロット 233 ax3.scatter( 234 df[col_datetime], df[col_c2h6_conc], color="orange", alpha=0.5, s=20 235 ) 236 ax3.set_ylabel("C$_2$H$_6$ Concentration\n(ppb)") 237 ax3.text(0.02, 0.98, "(c)", transform=ax3.transAxes, va="top", fontsize=20) 238 ax3.grid(True, alpha=0.3) 239 240 # C2H6フラックスのプロット 241 ax4.scatter( 242 df[col_datetime], df[col_c2h6_flux], color="orange", alpha=0.5, s=20 243 ) 244 ax4.set_ylabel("C$_2$H$_6$ flux\n(nmol m$^{-2}$ s$^{-1}$)") 245 ax4.set_ylim(-20, 40) 246 ax4.text(0.02, 0.98, "(d)", transform=ax4.transAxes, va="top", fontsize=20) 247 ax4.grid(True, alpha=0.3) 248 249 # x軸の設定 250 ax4.xaxis.set_major_locator(mdates.MonthLocator(interval=1)) 251 ax4.xaxis.set_major_formatter(mdates.DateFormatter("%m")) 252 plt.setp(ax4.get_xticklabels(), rotation=0, ha="right") 253 ax4.set_xlabel("Month") 254 255 # レイアウトの調整と保存 256 plt.tight_layout() 257 plt.savefig(output_path, dpi=300, bbox_inches="tight") 258 plt.close() 259 260 if print_summary: 261 262 def analyze_top_values(df, column_name, top_percent=20): 263 print(f"\n{column_name}の上位{top_percent}%の分析:") 264 265 # DataFrameのコピーを作成し、日時関連の列を追加 266 df_analysis = df.copy() 267 df_analysis["hour"] = pd.to_datetime(df_analysis[col_datetime]).dt.hour 268 df_analysis["month"] = pd.to_datetime( 269 df_analysis[col_datetime] 270 ).dt.month 271 df_analysis["weekday"] = pd.to_datetime( 272 df_analysis[col_datetime] 273 ).dt.dayofweek 274 275 # 上位20%のしきい値を計算 276 threshold = df[column_name].quantile(1 - top_percent / 100) 277 high_values = df_analysis[df_analysis[column_name] > threshold] 278 279 # 月ごとの分析 280 print("\n月別分布:") 281 monthly_counts = high_values.groupby("month").size() 282 total_counts = df_analysis.groupby("month").size() 283 monthly_percentages = (monthly_counts / total_counts * 100).round(1) 284 285 # 月ごとのデータを安全に表示 286 available_months = set(monthly_counts.index) & set(total_counts.index) 287 for month in sorted(available_months): 288 print( 289 f"月{month}: {monthly_percentages[month]}% ({monthly_counts[month]}件/{total_counts[month]}件)" 290 ) 291 292 # 時間帯ごとの分析(3時間区切り) 293 print("\n時間帯別分布:") 294 # copyを作成して新しい列を追加 295 high_values = high_values.copy() 296 high_values["time_block"] = high_values["hour"] // 3 * 3 297 time_blocks = high_values.groupby("time_block").size() 298 total_time_blocks = df_analysis.groupby( 299 df_analysis["hour"] // 3 * 3 300 ).size() 301 time_percentages = (time_blocks / total_time_blocks * 100).round(1) 302 303 # 時間帯ごとのデータを安全に表示 304 available_blocks = set(time_blocks.index) & set(total_time_blocks.index) 305 for block in sorted(available_blocks): 306 print( 307 f"{block:02d}:00-{block + 3:02d}:00: {time_percentages[block]}% ({time_blocks[block]}件/{total_time_blocks[block]}件)" 308 ) 309 310 # 曜日ごとの分析 311 print("\n曜日別分布:") 312 weekday_names = ["月曜", "火曜", "水曜", "木曜", "金曜", "土曜", "日曜"] 313 weekday_counts = high_values.groupby("weekday").size() 314 total_weekdays = df_analysis.groupby("weekday").size() 315 weekday_percentages = (weekday_counts / total_weekdays * 100).round(1) 316 317 # 曜日ごとのデータを安全に表示 318 available_days = set(weekday_counts.index) & set(total_weekdays.index) 319 for day in sorted(available_days): 320 if 0 <= day <= 6: # 有効な曜日インデックスのチェック 321 print( 322 f"{weekday_names[day]}: {weekday_percentages[day]}% ({weekday_counts[day]}件/{total_weekdays[day]}件)" 323 ) 324 325 # 濃度とフラックスそれぞれの分析を実行 326 print("\n=== 上位値の時間帯・曜日分析 ===") 327 analyze_top_values(df, col_ch4_conc) 328 analyze_top_values(df, col_ch4_flux) 329 analyze_top_values(df, col_c2h6_conc) 330 analyze_top_values(df, col_c2h6_flux) 331 332 def plot_ch4c2h6_timeseries( 333 self, 334 df: pd.DataFrame, 335 output_dir: str, 336 col_ch4_flux: str, 337 col_c2h6_flux: str, 338 output_filename: str = "timeseries_year.png", 339 col_datetime: str = "Date", 340 window_size: int = 24 * 7, # 1週間の移動平均のデフォルト値 341 confidence_interval: float = 0.95, # 95%信頼区間 342 subplot_label_ch4: str | None = "(a)", 343 subplot_label_c2h6: str | None = "(b)", 344 subplot_fontsize: int = 20, 345 show_ci: bool = True, 346 ch4_ylim: tuple[float, float] | None = None, 347 c2h6_ylim: tuple[float, float] | None = None, 348 start_date: str | None = None, # 追加:"YYYY-MM-DD"形式 349 end_date: str | None = None, # 追加:"YYYY-MM-DD"形式 350 figsize: tuple[float, float] = (16, 6), 351 ) -> None: 352 """CH4とC2H6フラックスの時系列変動をプロット 353 354 Parameters: 355 ------ 356 df : pd.DataFrame 357 データフレーム 358 output_dir : str 359 出力ディレクトリのパス 360 col_ch4_flux : str 361 CH4フラックスのカラム名 362 col_c2h6_flux : str 363 C2H6フラックスのカラム名 364 output_filename : str 365 出力ファイル名 366 col_datetime : str 367 日時カラムの名前 368 window_size : int 369 移動平均の窓サイズ 370 confidence_interval : float 371 信頼区間(0-1) 372 subplot_label_ch4 : str | None 373 CH4プロットのラベル 374 subplot_label_c2h6 : str | None 375 C2H6プロットのラベル 376 subplot_fontsize : int 377 サブプロットのフォントサイズ 378 show_ci : bool 379 信頼区間を表示するか 380 ch4_ylim : tuple[float, float] | None 381 CH4のy軸範囲 382 c2h6_ylim : tuple[float, float] | None 383 C2H6のy軸範囲 384 start_date : str | None 385 開始日(YYYY-MM-DD形式) 386 end_date : str | None 387 終了日(YYYY-MM-DD形式) 388 """ 389 # 出力ディレクトリの作成 390 os.makedirs(output_dir, exist_ok=True) 391 output_path: str = os.path.join(output_dir, output_filename) 392 393 # データの準備 394 df = df.copy() 395 if not isinstance(df.index, pd.DatetimeIndex): 396 df[col_datetime] = pd.to_datetime(df[col_datetime]) 397 df.set_index(col_datetime, inplace=True) 398 399 # 日付範囲の処理 400 if start_date is not None: 401 start_dt = pd.to_datetime(start_date) 402 if start_dt < df.index.min(): 403 self.logger.warning( 404 f"指定された開始日{start_date}がデータの開始日{df.index.min():%Y-%m-%d}より前です。" 405 f"データの開始日を使用します。" 406 ) 407 start_dt = df.index.min() 408 else: 409 start_dt = df.index.min() 410 411 if end_date is not None: 412 end_dt = pd.to_datetime(end_date) 413 if end_dt > df.index.max(): 414 self.logger.warning( 415 f"指定された終了日{end_date}がデータの終了日{df.index.max():%Y-%m-%d}より後です。" 416 f"データの終了日を使用します。" 417 ) 418 end_dt = df.index.max() 419 else: 420 end_dt = df.index.max() 421 422 # 指定された期間のデータを抽出 423 mask = (df.index >= start_dt) & (df.index <= end_dt) 424 df = df[mask] 425 426 # CH4とC2H6の移動平均と信頼区間を計算 427 ch4_mean, ch4_lower, ch4_upper = calculate_rolling_stats( 428 df[col_ch4_flux], window_size, confidence_interval 429 ) 430 c2h6_mean, c2h6_lower, c2h6_upper = calculate_rolling_stats( 431 df[col_c2h6_flux], window_size, confidence_interval 432 ) 433 434 # プロットの作成 435 fig, (ax1, ax2) = plt.subplots(1, 2, figsize=figsize) 436 437 # CH4プロット 438 ax1.plot(df.index, ch4_mean, "red", label="CH$_4$") 439 if show_ci: 440 ax1.fill_between(df.index, ch4_lower, ch4_upper, color="red", alpha=0.2) 441 if subplot_label_ch4: 442 ax1.text( 443 0.02, 444 0.98, 445 subplot_label_ch4, 446 transform=ax1.transAxes, 447 va="top", 448 fontsize=subplot_fontsize, 449 ) 450 ax1.set_ylabel("CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)") 451 if ch4_ylim is not None: 452 ax1.set_ylim(ch4_ylim) 453 ax1.grid(True, alpha=0.3) 454 455 # C2H6プロット 456 ax2.plot(df.index, c2h6_mean, "orange", label="C$_2$H$_6$") 457 if show_ci: 458 ax2.fill_between( 459 df.index, c2h6_lower, c2h6_upper, color="orange", alpha=0.2 460 ) 461 if subplot_label_c2h6: 462 ax2.text( 463 0.02, 464 0.98, 465 subplot_label_c2h6, 466 transform=ax2.transAxes, 467 va="top", 468 fontsize=subplot_fontsize, 469 ) 470 ax2.set_ylabel("C$_2$H$_6$ flux (nmol m$^{-2}$ s$^{-1}$)") 471 if c2h6_ylim is not None: 472 ax2.set_ylim(c2h6_ylim) 473 ax2.grid(True, alpha=0.3) 474 475 # x軸の設定 476 for ax in [ax1, ax2]: 477 ax.set_xlabel("Month") 478 # x軸の範囲を設定 479 ax.set_xlim(start_dt, end_dt) 480 481 # 1ヶ月ごとの主目盛り 482 ax.xaxis.set_major_locator(mdates.MonthLocator(interval=1)) 483 484 # カスタムフォーマッタの作成(数字を通常フォントで表示) 485 def date_formatter(x, p): 486 date = mdates.num2date(x) 487 return f"{date.strftime('%m')}" 488 489 ax.xaxis.set_major_formatter(plt.FuncFormatter(date_formatter)) 490 491 # 補助目盛りの設定 492 ax.xaxis.set_minor_locator(mdates.MonthLocator()) 493 # ティックラベルの回転と位置調整 494 plt.setp(ax.xaxis.get_majorticklabels(), ha="right") 495 496 plt.tight_layout() 497 plt.savefig(output_path, dpi=300, bbox_inches="tight") 498 plt.close(fig) 499 500 def plot_ch4_flux_comparison( 501 self, 502 df: pd.DataFrame, 503 output_dir: str, 504 col_g2401_flux: str, 505 col_ultra_flux: str, 506 output_filename: str = "ch4_flux_comparison.png", 507 col_datetime: str = "Date", 508 window_size: int = 24 * 7, # 1週間の移動平均のデフォルト値 509 confidence_interval: float = 0.95, # 95%信頼区間 510 subplot_label: str | None = None, 511 subplot_fontsize: int = 20, 512 show_ci: bool = True, 513 y_lim: tuple[float, float] | None = None, 514 start_date: str | None = None, 515 end_date: str | None = None, 516 figsize: tuple[float, float] = (12, 6), 517 legend_loc: str = "upper right", 518 ) -> None: 519 """G2401とUltraによるCH4フラックスの時系列比較プロット 520 521 Parameters: 522 ------ 523 df : pd.DataFrame 524 データフレーム 525 output_dir : str 526 出力ディレクトリのパス 527 col_g2401_flux : str 528 G2401のCH4フラックスのカラム名 529 col_ultra_flux : str 530 UltraのCH4フラックスのカラム名 531 output_filename : str 532 出力ファイル名 533 col_datetime : str 534 日時カラムの名前 535 window_size : int 536 移動平均の窓サイズ 537 confidence_interval : float 538 信頼区間(0-1) 539 subplot_label : str | None 540 プロットのラベル 541 subplot_fontsize : int 542 サブプロットのフォントサイズ 543 show_ci : bool 544 信頼区間を表示するか 545 y_lim : tuple[float, float] | None 546 y軸の範囲 547 start_date : str | None 548 開始日(YYYY-MM-DD形式) 549 end_date : str | None 550 終了日(YYYY-MM-DD形式) 551 figsize : tuple[float, float] 552 図のサイズ 553 legend_loc : str 554 凡例の位置 555 """ 556 # 出力ディレクトリの作成 557 os.makedirs(output_dir, exist_ok=True) 558 output_path: str = os.path.join(output_dir, output_filename) 559 560 # データの準備 561 df = df.copy() 562 if not isinstance(df.index, pd.DatetimeIndex): 563 df[col_datetime] = pd.to_datetime(df[col_datetime]) 564 df.set_index(col_datetime, inplace=True) 565 566 # 日付範囲の処理(既存のコードと同様) 567 if start_date is not None: 568 start_dt = pd.to_datetime(start_date) 569 if start_dt < df.index.min(): 570 self.logger.warning( 571 f"指定された開始日{start_date}がデータの開始日{df.index.min():%Y-%m-%d}より前です。" 572 f"データの開始日を使用します。" 573 ) 574 start_dt = df.index.min() 575 else: 576 start_dt = df.index.min() 577 578 if end_date is not None: 579 end_dt = pd.to_datetime(end_date) 580 if end_dt > df.index.max(): 581 self.logger.warning( 582 f"指定された終了日{end_date}がデータの終了日{df.index.max():%Y-%m-%d}より後です。" 583 f"データの終了日を使用します。" 584 ) 585 end_dt = df.index.max() 586 else: 587 end_dt = df.index.max() 588 589 # 指定された期間のデータを抽出 590 mask = (df.index >= start_dt) & (df.index <= end_dt) 591 df = df[mask] 592 593 # 移動平均の計算(既存の関数を使用) 594 g2401_mean, g2401_lower, g2401_upper = calculate_rolling_stats( 595 df[col_g2401_flux], window_size, confidence_interval 596 ) 597 ultra_mean, ultra_lower, ultra_upper = calculate_rolling_stats( 598 df[col_ultra_flux], window_size, confidence_interval 599 ) 600 601 # プロットの作成 602 fig, ax = plt.subplots(figsize=figsize) 603 604 # G2401データのプロット 605 ax.plot(df.index, g2401_mean, "blue", label="G2401", alpha=0.7) 606 if show_ci: 607 ax.fill_between(df.index, g2401_lower, g2401_upper, color="blue", alpha=0.2) 608 609 # Ultraデータのプロット 610 ax.plot(df.index, ultra_mean, "red", label="Ultra", alpha=0.7) 611 if show_ci: 612 ax.fill_between(df.index, ultra_lower, ultra_upper, color="red", alpha=0.2) 613 614 # プロットの設定 615 if subplot_label: 616 ax.text( 617 0.02, 618 0.98, 619 subplot_label, 620 transform=ax.transAxes, 621 va="top", 622 fontsize=subplot_fontsize, 623 ) 624 625 ax.set_ylabel("CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)") 626 ax.set_xlabel("Month") 627 628 if y_lim is not None: 629 ax.set_ylim(y_lim) 630 631 ax.grid(True, alpha=0.3) 632 ax.legend(loc=legend_loc) 633 634 # x軸の設定 635 ax.set_xlim(start_dt, end_dt) 636 ax.xaxis.set_major_locator(mdates.MonthLocator(interval=1)) 637 638 # カスタムフォーマッタの作成(数字を通常フォントで表示) 639 def date_formatter(x, p): 640 date = mdates.num2date(x) 641 return f"{date.strftime('%m')}" 642 643 ax.xaxis.set_major_formatter(plt.FuncFormatter(date_formatter)) 644 ax.xaxis.set_minor_locator(mdates.MonthLocator()) 645 plt.setp(ax.xaxis.get_majorticklabels(), ha="right") 646 647 plt.tight_layout() 648 plt.savefig(output_path, dpi=300, bbox_inches="tight") 649 plt.close(fig) 650 651 def plot_c1c2_fluxes_diurnal_patterns( 652 self, 653 df: pd.DataFrame, 654 y_cols_ch4: list[str], 655 y_cols_c2h6: list[str], 656 labels_ch4: list[str], 657 labels_c2h6: list[str], 658 colors_ch4: list[str], 659 colors_c2h6: list[str], 660 output_dir: str, 661 output_filename: str = "diurnal.png", 662 legend_only_ch4: bool = False, 663 add_label: bool = True, 664 add_legend: bool = True, 665 show_std: bool = False, # 標準偏差表示のオプションを追加 666 std_alpha: float = 0.2, # 標準偏差の透明度 667 subplot_fontsize: int = 20, 668 subplot_label_ch4: str | None = "(a)", 669 subplot_label_c2h6: str | None = "(b)", 670 ax1_ylim: tuple[float, float] | None = None, 671 ax2_ylim: tuple[float, float] | None = None, 672 ) -> None: 673 """CH4とC2H6の日変化パターンを1つの図に並べてプロットする 674 675 Parameters: 676 ------ 677 df : pd.DataFrame 678 入力データフレーム。 679 y_cols_ch4 : list[str] 680 CH4のプロットに使用するカラム名のリスト。 681 y_cols_c2h6 : list[str] 682 C2H6のプロットに使用するカラム名のリスト。 683 labels_ch4 : list[str] 684 CH4の各ラインに対応するラベルのリスト。 685 labels_c2h6 : list[str] 686 C2H6の各ラインに対応するラベルのリスト。 687 colors_ch4 : list[str] 688 CH4の各ラインに使用する色のリスト。 689 colors_c2h6 : list[str] 690 C2H6の各ラインに使用する色のリスト。 691 output_dir : str 692 出力先ディレクトリのパス。 693 output_filename : str, optional 694 出力ファイル名。デフォルトは"diurnal.png"。 695 legend_only_ch4 : bool, optional 696 CH4の凡例のみを表示するかどうか。デフォルトはFalse。 697 add_label : bool, optional 698 サブプロットラベルを表示するかどうか。デフォルトはTrue。 699 add_legend : bool, optional 700 凡例を表示するかどうか。デフォルトはTrue。 701 show_std : bool, optional 702 標準偏差を表示するかどうか。デフォルトはFalse。 703 std_alpha : float, optional 704 標準偏差の透明度。デフォルトは0.2。 705 subplot_fontsize : int, optional 706 サブプロットのフォントサイズ。デフォルトは20。 707 subplot_label_ch4 : str | None, optional 708 CH4プロットのラベル。デフォルトは"(a)"。 709 subplot_label_c2h6 : str | None, optional 710 C2H6プロットのラベル。デフォルトは"(b)"。 711 ax1_ylim : tuple[float, float] | None, optional 712 CH4プロットのy軸の範囲。デフォルトはNone。 713 ax2_ylim : tuple[float, float] | None, optional 714 C2H6プロットのy軸の範囲。デフォルトはNone。 715 """ 716 os.makedirs(output_dir, exist_ok=True) 717 output_path: str = os.path.join(output_dir, output_filename) 718 719 # データの準備 720 target_columns = y_cols_ch4 + y_cols_c2h6 721 hourly_means, time_points = self._prepare_diurnal_data(df, target_columns) 722 723 # 標準偏差の計算を追加 724 hourly_stds = {} 725 if show_std: 726 hourly_stds = df.groupby(df.index.hour)[target_columns].std() 727 # 24時間目のデータ点を追加 728 last_hour = hourly_stds.iloc[0:1].copy() 729 last_hour.index = [24] 730 hourly_stds = pd.concat([hourly_stds, last_hour]) 731 732 # プロットの作成 733 fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5)) 734 735 # CH4のプロット (左側) 736 ch4_lines = [] 737 for y_col, label, color in zip(y_cols_ch4, labels_ch4, colors_ch4): 738 mean_values = hourly_means["all"][y_col] 739 line = ax1.plot( 740 time_points, 741 mean_values, 742 "-o", 743 label=label, 744 color=color, 745 ) 746 ch4_lines.extend(line) 747 748 # 標準偏差の表示 749 if show_std: 750 std_values = hourly_stds[y_col] 751 ax1.fill_between( 752 time_points, 753 mean_values - std_values, 754 mean_values + std_values, 755 color=color, 756 alpha=std_alpha, 757 ) 758 759 # C2H6のプロット (右側) 760 c2h6_lines = [] 761 for y_col, label, color in zip(y_cols_c2h6, labels_c2h6, colors_c2h6): 762 mean_values = hourly_means["all"][y_col] 763 line = ax2.plot( 764 time_points, 765 mean_values, 766 "o-", 767 label=label, 768 color=color, 769 ) 770 c2h6_lines.extend(line) 771 772 # 標準偏差の表示 773 if show_std: 774 std_values = hourly_stds[y_col] 775 ax2.fill_between( 776 time_points, 777 mean_values - std_values, 778 mean_values + std_values, 779 color=color, 780 alpha=std_alpha, 781 ) 782 783 # 軸の設定 784 for ax, ylabel, subplot_label in [ 785 (ax1, r"CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)", subplot_label_ch4), 786 (ax2, r"C$_2$H$_6$ flux (nmol m$^{-2}$ s$^{-1}$)", subplot_label_c2h6), 787 ]: 788 self._setup_diurnal_axes( 789 ax=ax, 790 time_points=time_points, 791 ylabel=ylabel, 792 subplot_label=subplot_label, 793 add_label=add_label, 794 add_legend=False, # 個別の凡例は表示しない 795 subplot_fontsize=subplot_fontsize, 796 ) 797 798 if ax1_ylim is not None: 799 ax1.set_ylim(ax1_ylim) 800 ax1.yaxis.set_major_locator(MultipleLocator(20)) 801 ax1.yaxis.set_major_formatter(FuncFormatter(lambda x, p: f"{x:.0f}")) 802 803 if ax2_ylim is not None: 804 ax2.set_ylim(ax2_ylim) 805 ax2.yaxis.set_major_locator(MultipleLocator(1)) 806 ax2.yaxis.set_major_formatter(FuncFormatter(lambda x, p: f"{x:.1f}")) 807 808 plt.tight_layout() 809 810 # 共通の凡例 811 if add_legend: 812 all_lines = ch4_lines 813 all_labels = [line.get_label() for line in ch4_lines] 814 if not legend_only_ch4: 815 all_lines += c2h6_lines 816 all_labels += [line.get_label() for line in c2h6_lines] 817 fig.legend( 818 all_lines, 819 all_labels, 820 loc="center", 821 bbox_to_anchor=(0.5, 0.02), 822 ncol=len(all_lines), 823 ) 824 plt.subplots_adjust(bottom=0.25) # 下部に凡例用のスペースを確保 825 826 fig.savefig(output_path, dpi=300, bbox_inches="tight") 827 plt.close(fig) 828 829 def plot_c1c2_fluxes_diurnal_patterns_by_date( 830 self, 831 df: pd.DataFrame, 832 y_col_ch4: str, 833 y_col_c2h6: str, 834 output_dir: str, 835 output_filename: str = "diurnal_by_date.png", 836 plot_all: bool = True, 837 plot_weekday: bool = True, 838 plot_weekend: bool = True, 839 plot_holiday: bool = True, 840 add_label: bool = True, 841 add_legend: bool = True, 842 show_std: bool = False, # 標準偏差表示のオプションを追加 843 std_alpha: float = 0.2, # 標準偏差の透明度 844 legend_only_ch4: bool = False, 845 subplot_fontsize: int = 20, 846 subplot_label_ch4: str | None = "(a)", 847 subplot_label_c2h6: str | None = "(b)", 848 ax1_ylim: tuple[float, float] | None = None, 849 ax2_ylim: tuple[float, float] | None = None, 850 print_summary: bool = True, # 追加: 統計情報を表示するかどうか 851 ) -> None: 852 """CH4とC2H6の日変化パターンを日付分類して1つの図に並べてプロットする 853 854 Parameters: 855 ------ 856 df : pd.DataFrame 857 入力データフレーム。 858 y_col_ch4 : str 859 CH4フラックスを含むカラム名。 860 y_col_c2h6 : str 861 C2H6フラックスを含むカラム名。 862 output_dir : str 863 出力先ディレクトリのパス。 864 output_filename : str, optional 865 出力ファイル名。デフォルトは"diurnal_by_date.png"。 866 plot_all : bool, optional 867 すべての日をプロットするかどうか。デフォルトはTrue。 868 plot_weekday : bool, optional 869 平日をプロットするかどうか。デフォルトはTrue。 870 plot_weekend : bool, optional 871 週末をプロットするかどうか。デフォルトはTrue。 872 plot_holiday : bool, optional 873 祝日をプロットするかどうか。デフォルトはTrue。 874 add_label : bool, optional 875 サブプロットラベルを表示するかどうか。デフォルトはTrue。 876 add_legend : bool, optional 877 凡例を表示するかどうか。デフォルトはTrue。 878 show_std : bool, optional 879 標準偏差を表示するかどうか。デフォルトはFalse。 880 std_alpha : float, optional 881 標準偏差の透明度。デフォルトは0.2。 882 legend_only_ch4 : bool, optional 883 CH4の凡例のみを表示するかどうか。デフォルトはFalse。 884 subplot_fontsize : int, optional 885 サブプロットのフォントサイズ。デフォルトは20。 886 subplot_label_ch4 : str | None, optional 887 CH4プロットのラベル。デフォルトは"(a)"。 888 subplot_label_c2h6 : str | None, optional 889 C2H6プロットのラベル。デフォルトは"(b)"。 890 ax1_ylim : tuple[float, float] | None, optional 891 CH4プロットのy軸の範囲。デフォルトはNone。 892 ax2_ylim : tuple[float, float] | None, optional 893 C2H6プロットのy軸の範囲。デフォルトはNone。 894 print_summary : bool, optional 895 統計情報を表示するかどうか。デフォルトはTrue。 896 """ 897 os.makedirs(output_dir, exist_ok=True) 898 output_path: str = os.path.join(output_dir, output_filename) 899 900 # データの準備 901 target_columns = [y_col_ch4, y_col_c2h6] 902 hourly_means, time_points = self._prepare_diurnal_data( 903 df, target_columns, include_date_types=True 904 ) 905 906 # 標準偏差の計算を追加 907 hourly_stds = {} 908 if show_std: 909 for condition in ["all", "weekday", "weekend", "holiday"]: 910 if condition == "all": 911 condition_data = df 912 elif condition == "weekday": 913 condition_data = df[ 914 ~( 915 df.index.dayofweek.isin([5, 6]) 916 | df.index.map(lambda x: jpholiday.is_holiday(x.date())) 917 ) 918 ] 919 elif condition == "weekend": 920 condition_data = df[df.index.dayofweek.isin([5, 6])] 921 else: # holiday 922 condition_data = df[ 923 df.index.map(lambda x: jpholiday.is_holiday(x.date())) 924 ] 925 926 hourly_stds[condition] = condition_data.groupby( 927 condition_data.index.hour 928 )[target_columns].std() 929 # 24時間目のデータ点を追加 930 last_hour = hourly_stds[condition].iloc[0:1].copy() 931 last_hour.index = [24] 932 hourly_stds[condition] = pd.concat([hourly_stds[condition], last_hour]) 933 934 # プロットスタイルの設定 935 styles = { 936 "all": { 937 "color": "black", 938 "linestyle": "-", 939 "alpha": 1.0, 940 "label": "All days", 941 }, 942 "weekday": { 943 "color": "blue", 944 "linestyle": "-", 945 "alpha": 0.8, 946 "label": "Weekdays", 947 }, 948 "weekend": { 949 "color": "red", 950 "linestyle": "-", 951 "alpha": 0.8, 952 "label": "Weekends", 953 }, 954 "holiday": { 955 "color": "green", 956 "linestyle": "-", 957 "alpha": 0.8, 958 "label": "Weekends & Holidays", 959 }, 960 } 961 962 # プロット対象の条件を選択 963 plot_conditions = { 964 "all": plot_all, 965 "weekday": plot_weekday, 966 "weekend": plot_weekend, 967 "holiday": plot_holiday, 968 } 969 selected_conditions = { 970 col: means 971 for col, means in hourly_means.items() 972 if col in plot_conditions and plot_conditions[col] 973 } 974 975 # プロットの作成 976 fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5)) 977 978 # CH4とC2H6のプロット用のラインオブジェクトを保存 979 ch4_lines = [] 980 c2h6_lines = [] 981 982 # CH4とC2H6のプロット 983 for condition, means in selected_conditions.items(): 984 style = styles[condition].copy() 985 986 # CH4プロット 987 mean_values_ch4 = means[y_col_ch4] 988 line_ch4 = ax1.plot(time_points, mean_values_ch4, marker="o", **style) 989 ch4_lines.extend(line_ch4) 990 991 if show_std and condition in hourly_stds: 992 std_values = hourly_stds[condition][y_col_ch4] 993 ax1.fill_between( 994 time_points, 995 mean_values_ch4 - std_values, 996 mean_values_ch4 + std_values, 997 color=style["color"], 998 alpha=std_alpha, 999 ) 1000 1001 # C2H6プロット 1002 style["linestyle"] = "--" 1003 mean_values_c2h6 = means[y_col_c2h6] 1004 line_c2h6 = ax2.plot(time_points, mean_values_c2h6, marker="o", **style) 1005 c2h6_lines.extend(line_c2h6) 1006 1007 if show_std and condition in hourly_stds: 1008 std_values = hourly_stds[condition][y_col_c2h6] 1009 ax2.fill_between( 1010 time_points, 1011 mean_values_c2h6 - std_values, 1012 mean_values_c2h6 + std_values, 1013 color=style["color"], 1014 alpha=std_alpha, 1015 ) 1016 1017 # 軸の設定 1018 for ax, ylabel, subplot_label in [ 1019 (ax1, r"CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)", subplot_label_ch4), 1020 (ax2, r"C$_2$H$_6$ flux (nmol m$^{-2}$ s$^{-1}$)", subplot_label_c2h6), 1021 ]: 1022 self._setup_diurnal_axes( 1023 ax=ax, 1024 time_points=time_points, 1025 ylabel=ylabel, 1026 subplot_label=subplot_label, 1027 add_label=add_label, 1028 add_legend=False, 1029 subplot_fontsize=subplot_fontsize, 1030 ) 1031 1032 if ax1_ylim is not None: 1033 ax1.set_ylim(ax1_ylim) 1034 ax1.yaxis.set_major_locator(MultipleLocator(20)) 1035 ax1.yaxis.set_major_formatter(FuncFormatter(lambda x, p: f"{x:.0f}")) 1036 1037 if ax2_ylim is not None: 1038 ax2.set_ylim(ax2_ylim) 1039 ax2.yaxis.set_major_locator(MultipleLocator(1)) 1040 ax2.yaxis.set_major_formatter(FuncFormatter(lambda x, p: f"{x:.1f}")) 1041 1042 plt.tight_layout() 1043 1044 # 共通の凡例を図の下部に配置 1045 if add_legend: 1046 lines_to_show = ( 1047 ch4_lines if legend_only_ch4 else ch4_lines[: len(selected_conditions)] 1048 ) 1049 fig.legend( 1050 lines_to_show, 1051 [ 1052 style["label"] 1053 for style in list(styles.values())[: len(lines_to_show)] 1054 ], 1055 loc="center", 1056 bbox_to_anchor=(0.5, 0.02), 1057 ncol=len(lines_to_show), 1058 ) 1059 plt.subplots_adjust(bottom=0.25) # 下部に凡例用のスペースを確保 1060 1061 fig.savefig(output_path, dpi=300, bbox_inches="tight") 1062 plt.close(fig) 1063 1064 # 日変化パターンの統計分析を追加 1065 if print_summary: 1066 # 平日と休日のデータを準備 1067 dates = pd.to_datetime(df.index) 1068 is_weekend = dates.dayofweek.isin([5, 6]) 1069 is_holiday = dates.map(lambda x: jpholiday.is_holiday(x.date())) 1070 is_weekday = ~(is_weekend | is_holiday) 1071 1072 weekday_data = df[is_weekday] 1073 holiday_data = df[is_weekend | is_holiday] 1074 1075 def get_diurnal_stats(data, column): 1076 # 時間ごとの平均値を計算 1077 hourly_means = data.groupby(data.index.hour)[column].mean() 1078 1079 # 8-16時の時間帯の統計 1080 daytime_means = hourly_means[ 1081 (hourly_means.index >= 8) & (hourly_means.index <= 16) 1082 ] 1083 1084 if len(daytime_means) == 0: 1085 return None 1086 1087 return { 1088 "mean": daytime_means.mean(), 1089 "max": daytime_means.max(), 1090 "max_hour": daytime_means.idxmax(), 1091 "min": daytime_means.min(), 1092 "min_hour": daytime_means.idxmin(), 1093 "hours_count": len(daytime_means), 1094 } 1095 1096 # CH4とC2H6それぞれの統計を計算 1097 for col, gas_name in [(y_col_ch4, "CH4"), (y_col_c2h6, "C2H6")]: 1098 print(f"\n=== {gas_name} フラックス 8-16時の統計分析 ===") 1099 1100 weekday_stats = get_diurnal_stats(weekday_data, col) 1101 holiday_stats = get_diurnal_stats(holiday_data, col) 1102 1103 if weekday_stats and holiday_stats: 1104 print("\n平日:") 1105 print(f" 平均値: {weekday_stats['mean']:.2f}") 1106 print( 1107 f" 最大値: {weekday_stats['max']:.2f} ({weekday_stats['max_hour']}時)" 1108 ) 1109 print( 1110 f" 最小値: {weekday_stats['min']:.2f} ({weekday_stats['min_hour']}時)" 1111 ) 1112 print(f" 集計時間数: {weekday_stats['hours_count']}") 1113 1114 print("\n休日:") 1115 print(f" 平均値: {holiday_stats['mean']:.2f}") 1116 print( 1117 f" 最大値: {holiday_stats['max']:.2f} ({holiday_stats['max_hour']}時)" 1118 ) 1119 print( 1120 f" 最小値: {holiday_stats['min']:.2f} ({holiday_stats['min_hour']}時)" 1121 ) 1122 print(f" 集計時間数: {holiday_stats['hours_count']}") 1123 1124 # 平日/休日の比率を計算 1125 print("\n平日/休日の比率:") 1126 print( 1127 f" 平均値比: {weekday_stats['mean'] / holiday_stats['mean']:.2f}" 1128 ) 1129 print( 1130 f" 最大値比: {weekday_stats['max'] / holiday_stats['max']:.2f}" 1131 ) 1132 print( 1133 f" 最小値比: {weekday_stats['min'] / holiday_stats['min']:.2f}" 1134 ) 1135 else: 1136 print("十分なデータがありません") 1137 1138 def plot_diurnal_concentrations( 1139 self, 1140 df: pd.DataFrame, 1141 output_dir: str, 1142 col_ch4_conc: str = "CH4_ultra_cal", 1143 col_c2h6_conc: str = "C2H6_ultra_cal", 1144 col_datetime: str = "Date", 1145 output_filename: str = "diurnal_concentrations.png", 1146 show_std: bool = True, 1147 alpha_std: float = 0.2, 1148 add_legend: bool = True, # 凡例表示のオプションを追加 1149 print_summary: bool = True, 1150 subplot_label_ch4: str | None = None, 1151 subplot_label_c2h6: str | None = None, 1152 subplot_fontsize: int = 24, 1153 ch4_ylim: tuple[float, float] | None = None, 1154 c2h6_ylim: tuple[float, float] | None = None, 1155 interval: str = "1H", # "30min" または "1H" を指定 1156 ) -> None: 1157 """CH4とC2H6の濃度の日内変動を描画する 1158 1159 Parameters: 1160 ------ 1161 df : pd.DataFrame 1162 濃度データを含むDataFrame 1163 output_dir : str 1164 出力ディレクトリのパス 1165 col_ch4_conc : str 1166 CH4濃度のカラム名 1167 col_c2h6_conc : str 1168 C2H6濃度のカラム名 1169 col_datetime : str 1170 日時カラム名 1171 output_filename : str 1172 出力ファイル名 1173 show_std : bool 1174 標準偏差を表示するかどうか 1175 alpha_std : float 1176 標準偏差の透明度 1177 add_legend : bool 1178 凡例を追加するかどうか 1179 print_summary : bool 1180 統計情報を表示するかどうか 1181 subplot_label_ch4 : str | None 1182 CH4プロットのラベル 1183 subplot_label_c2h6 : str | None 1184 C2H6プロットのラベル 1185 subplot_fontsize : int 1186 サブプロットのフォントサイズ 1187 ch4_ylim : tuple[float, float] | None 1188 CH4のy軸範囲 1189 c2h6_ylim : tuple[float, float] | None 1190 C2H6のy軸範囲 1191 interval : str 1192 時間間隔。"30min"または"1H"を指定 1193 """ 1194 # 出力ディレクトリの作成 1195 os.makedirs(output_dir, exist_ok=True) 1196 output_path: str = os.path.join(output_dir, output_filename) 1197 1198 # データの準備 1199 df = df.copy() 1200 if interval == "30min": 1201 # 30分間隔の場合、時間と30分を別々に取得 1202 df["hour"] = pd.to_datetime(df[col_datetime]).dt.hour 1203 df["minute"] = pd.to_datetime(df[col_datetime]).dt.minute 1204 df["time_bin"] = df["hour"] + df["minute"].map({0: 0, 30: 0.5}) 1205 else: 1206 # 1時間間隔の場合 1207 df["time_bin"] = pd.to_datetime(df[col_datetime]).dt.hour 1208 1209 # 時間ごとの平均値と標準偏差を計算 1210 hourly_stats = df.groupby("time_bin")[[col_ch4_conc, col_c2h6_conc]].agg( 1211 ["mean", "std"] 1212 ) 1213 1214 # 最後のデータポイントを追加(最初のデータを使用) 1215 last_point = hourly_stats.iloc[0:1].copy() 1216 last_point.index = [ 1217 hourly_stats.index[-1] + (0.5 if interval == "30min" else 1) 1218 ] 1219 hourly_stats = pd.concat([hourly_stats, last_point]) 1220 1221 # 時間軸の作成 1222 if interval == "30min": 1223 time_points = pd.date_range("2024-01-01", periods=49, freq="30min") 1224 x_ticks = [0, 6, 12, 18, 24] # 主要な時間のティック 1225 else: 1226 time_points = pd.date_range("2024-01-01", periods=25, freq="1H") 1227 x_ticks = [0, 6, 12, 18, 24] 1228 1229 # プロットの作成 1230 fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5)) 1231 1232 # CH4濃度プロット 1233 mean_ch4 = hourly_stats[col_ch4_conc]["mean"] 1234 if show_std: 1235 std_ch4 = hourly_stats[col_ch4_conc]["std"] 1236 ax1.fill_between( 1237 time_points, 1238 mean_ch4 - std_ch4, 1239 mean_ch4 + std_ch4, 1240 color="red", 1241 alpha=alpha_std, 1242 ) 1243 ch4_line = ax1.plot(time_points, mean_ch4, "red", label="CH$_4$")[0] 1244 1245 ax1.set_ylabel("CH$_4$ (ppm)") 1246 if ch4_ylim is not None: 1247 ax1.set_ylim(ch4_ylim) 1248 if subplot_label_ch4: 1249 ax1.text( 1250 0.02, 1251 0.98, 1252 subplot_label_ch4, 1253 transform=ax1.transAxes, 1254 va="top", 1255 fontsize=subplot_fontsize, 1256 ) 1257 1258 # C2H6濃度プロット 1259 mean_c2h6 = hourly_stats[col_c2h6_conc]["mean"] 1260 if show_std: 1261 std_c2h6 = hourly_stats[col_c2h6_conc]["std"] 1262 ax2.fill_between( 1263 time_points, 1264 mean_c2h6 - std_c2h6, 1265 mean_c2h6 + std_c2h6, 1266 color="orange", 1267 alpha=alpha_std, 1268 ) 1269 c2h6_line = ax2.plot(time_points, mean_c2h6, "orange", label="C$_2$H$_6$")[0] 1270 1271 ax2.set_ylabel("C$_2$H$_6$ (ppb)") 1272 if c2h6_ylim is not None: 1273 ax2.set_ylim(c2h6_ylim) 1274 if subplot_label_c2h6: 1275 ax2.text( 1276 0.02, 1277 0.98, 1278 subplot_label_c2h6, 1279 transform=ax2.transAxes, 1280 va="top", 1281 fontsize=subplot_fontsize, 1282 ) 1283 1284 # 両プロットの共通設定 1285 for ax in [ax1, ax2]: 1286 ax.set_xlabel("Time (hour)") 1287 ax.xaxis.set_major_formatter(mdates.DateFormatter("%-H")) 1288 ax.xaxis.set_major_locator(mdates.HourLocator(byhour=x_ticks)) 1289 ax.set_xlim(time_points[0], time_points[-1]) 1290 # 1時間ごとの縦線を表示 1291 ax.grid(True, which="major", alpha=0.3) 1292 # 補助目盛りは表示するが、グリッド線は表示しない 1293 # if interval == "30min": 1294 # ax.xaxis.set_minor_locator(mdates.MinuteLocator(byminute=[30])) 1295 # ax.tick_params(which='minor', length=4) 1296 1297 # 共通の凡例を図の下部に配置 1298 if add_legend: 1299 fig.legend( 1300 [ch4_line, c2h6_line], 1301 ["CH$_4$", "C$_2$H$_6$"], 1302 loc="center", 1303 bbox_to_anchor=(0.5, 0.02), 1304 ncol=2, 1305 ) 1306 plt.subplots_adjust(bottom=0.2) 1307 1308 plt.tight_layout() 1309 plt.savefig(output_path, dpi=300, bbox_inches="tight") 1310 plt.close(fig) 1311 1312 if print_summary: 1313 # 統計情報の表示 1314 for name, col in [("CH4", col_ch4_conc), ("C2H6", col_c2h6_conc)]: 1315 stats = hourly_stats[col] 1316 mean_vals = stats["mean"] 1317 1318 print(f"\n{name}濃度の日内変動統計:") 1319 print(f"最小値: {mean_vals.min():.3f} (Hour: {mean_vals.idxmin()})") 1320 print(f"最大値: {mean_vals.max():.3f} (Hour: {mean_vals.idxmax()})") 1321 print(f"平均値: {mean_vals.mean():.3f}") 1322 print(f"日内変動幅: {mean_vals.max() - mean_vals.min():.3f}") 1323 print(f"最大/最小比: {mean_vals.max() / mean_vals.min():.3f}") 1324 1325 def plot_flux_diurnal_patterns_with_std( 1326 self, 1327 df: pd.DataFrame, 1328 output_dir: str, 1329 col_ch4_flux: str = "Fch4", 1330 col_c2h6_flux: str = "Fc2h6", 1331 ch4_label: str = r"$\mathregular{CH_{4}}$フラックス", 1332 c2h6_label: str = r"$\mathregular{C_{2}H_{6}}$フラックス", 1333 col_datetime: str = "Date", 1334 output_filename: str = "diurnal_patterns.png", 1335 window_size: int = 6, # 移動平均の窓サイズ 1336 show_std: bool = True, # 標準偏差の表示有無 1337 alpha_std: float = 0.1, # 標準偏差の透明度 1338 ) -> None: 1339 """CH4とC2H6フラックスの日変化パターンをプロットする 1340 1341 Parameters: 1342 ------ 1343 df : pd.DataFrame 1344 データフレーム 1345 output_dir : str 1346 出力ディレクトリのパス 1347 col_ch4_flux : str 1348 CH4フラックスのカラム名 1349 col_c2h6_flux : str 1350 C2H6フラックスのカラム名 1351 ch4_label : str 1352 CH4フラックスのラベル 1353 c2h6_label : str 1354 C2H6フラックスのラベル 1355 col_datetime : str 1356 日時カラムの名前 1357 output_filename : str 1358 出力ファイル名 1359 window_size : int 1360 移動平均の窓サイズ(デフォルト6) 1361 show_std : bool 1362 標準偏差を表示するかどうか 1363 alpha_std : float 1364 標準偏差の透明度(0-1) 1365 """ 1366 # 出力ディレクトリの作成 1367 os.makedirs(output_dir, exist_ok=True) 1368 output_path: str = os.path.join(output_dir, output_filename) 1369 1370 # # プロットのスタイル設定 1371 # plt.rcParams.update({ 1372 # 'font.size': 20, 1373 # 'axes.labelsize': 20, 1374 # 'axes.titlesize': 20, 1375 # 'xtick.labelsize': 20, 1376 # 'ytick.labelsize': 20, 1377 # 'legend.fontsize': 20, 1378 # }) 1379 1380 # 日時インデックスの処理 1381 df = df.copy() 1382 if not isinstance(df.index, pd.DatetimeIndex): 1383 df[col_datetime] = pd.to_datetime(df[col_datetime]) 1384 df.set_index(col_datetime, inplace=True) 1385 1386 # 時刻データの抽出とグループ化 1387 df["hour"] = df.index.hour 1388 hourly_means = df.groupby("hour")[[col_ch4_flux, col_c2h6_flux]].agg( 1389 ["mean", "std"] 1390 ) 1391 1392 # 24時間目のデータ点を追加(0時のデータを使用) 1393 last_hour = hourly_means.iloc[0:1].copy() 1394 last_hour.index = [24] 1395 hourly_means = pd.concat([hourly_means, last_hour]) 1396 1397 # 24時間分のデータポイントを作成 1398 time_points = pd.date_range("2024-01-01", periods=25, freq="h") 1399 1400 # プロットの作成 1401 fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5)) 1402 1403 # 移動平均の計算と描画 1404 ch4_mean = ( 1405 hourly_means[(col_ch4_flux, "mean")] 1406 .rolling(window=window_size, center=True, min_periods=1) 1407 .mean() 1408 ) 1409 c2h6_mean = ( 1410 hourly_means[(col_c2h6_flux, "mean")] 1411 .rolling(window=window_size, center=True, min_periods=1) 1412 .mean() 1413 ) 1414 1415 if show_std: 1416 ch4_std = ( 1417 hourly_means[(col_ch4_flux, "std")] 1418 .rolling(window=window_size, center=True, min_periods=1) 1419 .mean() 1420 ) 1421 c2h6_std = ( 1422 hourly_means[(col_c2h6_flux, "std")] 1423 .rolling(window=window_size, center=True, min_periods=1) 1424 .mean() 1425 ) 1426 1427 ax1.fill_between( 1428 time_points, 1429 ch4_mean - ch4_std, 1430 ch4_mean + ch4_std, 1431 color="blue", 1432 alpha=alpha_std, 1433 ) 1434 ax2.fill_between( 1435 time_points, 1436 c2h6_mean - c2h6_std, 1437 c2h6_mean + c2h6_std, 1438 color="red", 1439 alpha=alpha_std, 1440 ) 1441 1442 # メインのラインプロット 1443 ax1.plot(time_points, ch4_mean, "blue", label=ch4_label) 1444 ax2.plot(time_points, c2h6_mean, "red", label=c2h6_label) 1445 1446 # 軸の設定 1447 for ax, ylabel in [ 1448 (ax1, r"CH$_4$ (nmol m$^{-2}$ s$^{-1}$)"), 1449 (ax2, r"C$_2$H$_6$ (nmol m$^{-2}$ s$^{-1}$)"), 1450 ]: 1451 ax.set_xlabel("Time") 1452 ax.set_ylabel(ylabel) 1453 ax.xaxis.set_major_formatter(mdates.DateFormatter("%-H")) 1454 ax.xaxis.set_major_locator(mdates.HourLocator(byhour=[0, 6, 12, 18, 24])) 1455 ax.set_xlim(time_points[0], time_points[-1]) 1456 ax.grid(True, alpha=0.3) 1457 ax.legend() 1458 1459 # グラフの保存 1460 plt.tight_layout() 1461 plt.savefig(output_path, dpi=300, bbox_inches="tight") 1462 plt.close() 1463 1464 # 統計情報の表示(オプション) 1465 for col, name in [(col_ch4_flux, "CH4"), (col_c2h6_flux, "C2H6")]: 1466 mean_val = hourly_means[(col, "mean")].mean() 1467 min_val = hourly_means[(col, "mean")].min() 1468 max_val = hourly_means[(col, "mean")].max() 1469 min_time = hourly_means[(col, "mean")].idxmin() 1470 max_time = hourly_means[(col, "mean")].idxmax() 1471 1472 self.logger.info(f"{name} Statistics:") 1473 self.logger.info(f"Mean: {mean_val:.2f}") 1474 self.logger.info(f"Min: {min_val:.2f} (Hour: {min_time})") 1475 self.logger.info(f"Max: {max_val:.2f} (Hour: {max_time})") 1476 self.logger.info(f"Max/Min ratio: {max_val / min_val:.2f}\n") 1477 1478 def plot_scatter( 1479 self, 1480 df: pd.DataFrame, 1481 x_col: str, 1482 y_col: str, 1483 output_dir: str, 1484 output_filename: str = "scatter.png", 1485 xlabel: str | None = None, 1486 ylabel: str | None = None, 1487 add_label: bool = True, 1488 x_axis_range: tuple | None = None, 1489 y_axis_range: tuple | None = None, 1490 fixed_slope: float = 0.076, 1491 show_fixed_slope: bool = False, 1492 x_scientific: bool = False, # 追加:x軸を指数表記にするかどうか 1493 y_scientific: bool = False, # 追加:y軸を指数表記にするかどうか 1494 ) -> None: 1495 """散布図を作成し、TLS回帰直線を描画します。 1496 1497 Parameters: 1498 ------ 1499 df : pd.DataFrame 1500 プロットに使用するデータフレーム 1501 x_col : str 1502 x軸に使用する列名 1503 y_col : str 1504 y軸に使用する列名 1505 xlabel : str 1506 x軸のラベル 1507 ylabel : str 1508 y軸のラベル 1509 output_dir : str 1510 出力先ディレクトリ 1511 output_filename : str, optional 1512 出力ファイル名。デフォルトは"scatter.png" 1513 add_label : bool, optional 1514 軸ラベルを表示するかどうか。デフォルトはTrue 1515 x_axis_range : tuple, optional 1516 x軸の範囲。デフォルトはNone。 1517 y_axis_range : tuple, optional 1518 y軸の範囲。デフォルトはNone。 1519 fixed_slope : float, optional 1520 固定傾きを指定するための値。デフォルトは0.076 1521 show_fixed_slope : bool, optional 1522 固定傾きの線を表示するかどうか。デフォルトはFalse 1523 """ 1524 os.makedirs(output_dir, exist_ok=True) 1525 output_path: str = os.path.join(output_dir, output_filename) 1526 1527 # 有効なデータの抽出 1528 df = MonthlyFiguresGenerator.get_valid_data(df, x_col, y_col) 1529 1530 # データの準備 1531 x = df[x_col].values 1532 y = df[y_col].values 1533 1534 # データの中心化 1535 x_mean = np.mean(x) 1536 y_mean = np.mean(y) 1537 x_c = x - x_mean 1538 y_c = y - y_mean 1539 1540 # TLS回帰の計算 1541 data_matrix = np.vstack((x_c, y_c)) 1542 cov_matrix = np.cov(data_matrix) 1543 _, eigenvecs = linalg.eigh(cov_matrix) 1544 largest_eigenvec = eigenvecs[:, -1] 1545 1546 slope = largest_eigenvec[1] / largest_eigenvec[0] 1547 intercept = y_mean - slope * x_mean 1548 1549 # R²とRMSEの計算 1550 y_pred = slope * x + intercept 1551 r_squared = 1 - np.sum((y - y_pred) ** 2) / np.sum((y - np.mean(y)) ** 2) 1552 rmse = np.sqrt(np.mean((y - y_pred) ** 2)) 1553 1554 # プロットの作成 1555 fig, ax = plt.subplots(figsize=(6, 6)) 1556 1557 # データ点のプロット 1558 ax.scatter(x, y, color="black") 1559 1560 # データの範囲を取得 1561 if x_axis_range is None: 1562 x_axis_range = (df[x_col].min(), df[x_col].max()) 1563 if y_axis_range is None: 1564 y_axis_range = (df[y_col].min(), df[y_col].max()) 1565 1566 # 回帰直線のプロット 1567 x_range = np.linspace(x_axis_range[0], x_axis_range[1], 150) 1568 y_range = slope * x_range + intercept 1569 ax.plot(x_range, y_range, "r", label="TLS regression") 1570 1571 # 傾き固定の線を追加(フラグがTrueの場合) 1572 if show_fixed_slope: 1573 fixed_intercept = ( 1574 y_mean - fixed_slope * x_mean 1575 ) # 中心点を通るように切片を計算 1576 y_fixed = fixed_slope * x_range + fixed_intercept 1577 ax.plot(x_range, y_fixed, "b--", label=f"Slope = {fixed_slope}", alpha=0.7) 1578 1579 # 軸の設定 1580 ax.set_xlim(x_axis_range) 1581 ax.set_ylim(y_axis_range) 1582 1583 # 指数表記の設定 1584 if x_scientific: 1585 ax.ticklabel_format(style="sci", axis="x", scilimits=(0, 0)) 1586 ax.xaxis.get_offset_text().set_position((1.1, 0)) # 指数の位置調整 1587 if y_scientific: 1588 ax.ticklabel_format(style="sci", axis="y", scilimits=(0, 0)) 1589 ax.yaxis.get_offset_text().set_position((0, 1.1)) # 指数の位置調整 1590 1591 if add_label: 1592 if xlabel is not None: 1593 ax.set_xlabel(xlabel) 1594 if ylabel is not None: 1595 ax.set_ylabel(ylabel) 1596 1597 # 1:1の関係を示す点線(軸の範囲が同じ場合のみ表示) 1598 if ( 1599 x_axis_range is not None 1600 and y_axis_range is not None 1601 and x_axis_range == y_axis_range 1602 ): 1603 ax.plot( 1604 [x_axis_range[0], x_axis_range[1]], 1605 [x_axis_range[0], x_axis_range[1]], 1606 "k--", 1607 alpha=0.5, 1608 ) 1609 1610 # 回帰情報の表示 1611 equation = ( 1612 f"y = {slope:.2f}x {'+' if intercept >= 0 else '-'} {abs(intercept):.2f}" 1613 ) 1614 position_x = 0.05 1615 fig_ha: str = "left" 1616 ax.text( 1617 position_x, 1618 0.95, 1619 equation, 1620 transform=ax.transAxes, 1621 va="top", 1622 ha=fig_ha, 1623 color="red", 1624 ) 1625 ax.text( 1626 position_x, 1627 0.88, 1628 f"R² = {r_squared:.2f}", 1629 transform=ax.transAxes, 1630 va="top", 1631 ha=fig_ha, 1632 color="red", 1633 ) 1634 ax.text( 1635 position_x, 1636 0.81, # RMSEのための新しい位置 1637 f"RMSE = {rmse:.2f}", 1638 transform=ax.transAxes, 1639 va="top", 1640 ha=fig_ha, 1641 color="red", 1642 ) 1643 # 目盛り線の設定 1644 ax.grid(True, alpha=0.3) 1645 1646 fig.savefig(output_path, dpi=300, bbox_inches="tight") 1647 plt.close(fig) 1648 1649 def plot_source_contributions_diurnal( 1650 self, 1651 df: pd.DataFrame, 1652 output_dir: str, 1653 col_ch4_flux: str, 1654 col_c2h6_flux: str, 1655 label_gas: str = "gas", 1656 label_bio: str = "bio", 1657 col_datetime: str = "Date", 1658 output_filename: str = "source_contributions.png", 1659 window_size: int = 6, # 移動平均の窓サイズ 1660 print_summary: bool = True, # 統計情報を表示するかどうか, 1661 add_legend: bool = False, 1662 smooth: bool = False, 1663 y_max: float = 100, # y軸の上限値を追加 1664 subplot_label: str | None = None, 1665 subplot_fontsize: int = 20, 1666 ) -> None: 1667 """CH4フラックスの都市ガス起源と生物起源の日変化を積み上げグラフとして表示 1668 1669 Parameters: 1670 ------ 1671 df : pd.DataFrame 1672 データフレーム 1673 output_dir : str 1674 出力ディレクトリのパス 1675 col_ch4_flux : str 1676 CH4フラックスのカラム名 1677 col_c2h6_flux : str 1678 C2H6フラックスのカラム名 1679 label_gas : str 1680 都市ガス起源のラベル 1681 label_bio : str 1682 生物起源のラベル 1683 col_datetime : str 1684 日時カラムの名前 1685 output_filename : str 1686 出力ファイル名 1687 window_size : int 1688 移動平均の窓サイズ 1689 print_summary : bool 1690 統計情報を表示するかどうか 1691 smooth : bool 1692 移動平均を適用するかどうか 1693 y_max : float 1694 y軸の上限値(デフォルト: 100) 1695 """ 1696 # 出力ディレクトリの作成 1697 os.makedirs(output_dir, exist_ok=True) 1698 output_path: str = os.path.join(output_dir, output_filename) 1699 1700 # 起源の計算 1701 df_with_sources = self._calculate_source_contributions( 1702 df=df, 1703 col_ch4_flux=col_ch4_flux, 1704 col_c2h6_flux=col_c2h6_flux, 1705 col_datetime=col_datetime, 1706 ) 1707 1708 # 時刻データの抽出とグループ化 1709 df_with_sources["hour"] = df_with_sources.index.hour 1710 hourly_means = df_with_sources.groupby("hour")[["ch4_gas", "ch4_bio"]].mean() 1711 1712 # 24時間目のデータ点を追加(0時のデータを使用) 1713 last_hour = hourly_means.iloc[0:1].copy() 1714 last_hour.index = [24] 1715 hourly_means = pd.concat([hourly_means, last_hour]) 1716 1717 # 移動平均の適用 1718 hourly_means_smoothed = hourly_means 1719 if smooth: 1720 hourly_means_smoothed = hourly_means.rolling( 1721 window=window_size, center=True, min_periods=1 1722 ).mean() 1723 1724 # 24時間分のデータポイントを作成 1725 time_points = pd.date_range("2024-01-01", periods=25, freq="h") 1726 1727 # プロットの作成 1728 plt.figure(figsize=(10, 6)) 1729 ax = plt.gca() 1730 1731 # サブプロットラベルの追加(subplot_labelが指定されている場合) 1732 if subplot_label: 1733 ax.text( 1734 0.02, # x位置 1735 0.98, # y位置 1736 subplot_label, 1737 transform=ax.transAxes, 1738 va="top", 1739 fontsize=subplot_fontsize, 1740 ) 1741 1742 # 積み上げプロット 1743 ax.fill_between( 1744 time_points, 1745 0, 1746 hourly_means_smoothed["ch4_bio"], 1747 color="blue", 1748 alpha=0.6, 1749 label=label_bio, 1750 ) 1751 ax.fill_between( 1752 time_points, 1753 hourly_means_smoothed["ch4_bio"], 1754 hourly_means_smoothed["ch4_bio"] + hourly_means_smoothed["ch4_gas"], 1755 color="red", 1756 alpha=0.6, 1757 label=label_gas, 1758 ) 1759 1760 # 合計値のライン 1761 total_flux = hourly_means_smoothed["ch4_bio"] + hourly_means_smoothed["ch4_gas"] 1762 ax.plot(time_points, total_flux, "-", color="black", alpha=0.5) 1763 1764 # 軸の設定 1765 ax.set_xlabel("Time (hour)") 1766 ax.set_ylabel(r"CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)") 1767 ax.xaxis.set_major_formatter(mdates.DateFormatter("%-H")) 1768 ax.xaxis.set_major_locator(mdates.HourLocator(byhour=[0, 6, 12, 18, 24])) 1769 ax.set_xlim(time_points[0], time_points[-1]) 1770 ax.set_ylim(0, y_max) # y軸の範囲を設定 1771 ax.grid(True, alpha=0.3) 1772 1773 # 凡例を図の下部に配置 1774 if add_legend: 1775 handles, labels = ax.get_legend_handles_labels() 1776 fig = plt.gcf() # 現在の図を取得 1777 fig.legend( 1778 handles, 1779 labels, 1780 loc="center", 1781 bbox_to_anchor=(0.5, 0.01), 1782 ncol=len(handles), 1783 ) 1784 plt.subplots_adjust(bottom=0.2) # 下部に凡例用のスペースを確保 1785 1786 # グラフの保存 1787 plt.tight_layout() 1788 plt.savefig(output_path, dpi=300, bbox_inches="tight") 1789 plt.close() 1790 1791 # 統計情報の表示 1792 if print_summary: 1793 stats = { 1794 "都市ガス起源": hourly_means["ch4_gas"], 1795 "生物起源": hourly_means["ch4_bio"], 1796 "合計": hourly_means["ch4_gas"] + hourly_means["ch4_bio"], 1797 } 1798 1799 for source, data in stats.items(): 1800 mean_val = data.mean() 1801 min_val = data.min() 1802 max_val = data.max() 1803 min_time = data.idxmin() 1804 max_time = data.idxmax() 1805 1806 self.logger.info(f"{source}の統計:") 1807 print(f" 平均値: {mean_val:.2f}") 1808 print(f" 最小値: {min_val:.2f} (Hour: {min_time})") 1809 print(f" 最大値: {max_val:.2f} (Hour: {max_time})") 1810 if min_val != 0: 1811 print(f" 最大/最小比: {max_val / min_val:.2f}") 1812 1813 def plot_source_contributions_diurnal_by_date( 1814 self, 1815 df: pd.DataFrame, 1816 output_dir: str, 1817 col_ch4_flux: str, 1818 col_c2h6_flux: str, 1819 label_gas: str = "gas", 1820 label_bio: str = "bio", 1821 col_datetime: str = "Date", 1822 output_filename: str = "source_contributions_by_date.png", 1823 add_label: bool = True, 1824 add_legend: bool = False, 1825 print_summary: bool = False, # 統計情報を表示するかどうか, 1826 subplot_fontsize: int = 20, 1827 subplot_label_weekday: str | None = None, 1828 subplot_label_weekend: str | None = None, 1829 y_max: float | None = None, # y軸の上限値 1830 ) -> None: 1831 """CH4フラックスの都市ガス起源と生物起源の日変化を平日・休日別に表示 1832 1833 Parameters: 1834 ------ 1835 df : pd.DataFrame 1836 データフレーム 1837 output_dir : str 1838 出力ディレクトリのパス 1839 col_ch4_flux : str 1840 CH4フラックスのカラム名 1841 col_c2h6_flux : str 1842 C2H6フラックスのカラム名 1843 label_gas : str 1844 都市ガス起源のラベル 1845 label_bio : str 1846 生物起源のラベル 1847 col_datetime : str 1848 日時カラムの名前 1849 output_filename : str 1850 出力ファイル名 1851 add_label : bool 1852 ラベルを表示するか 1853 add_legend : bool 1854 凡例を表示するか 1855 subplot_fontsize : int 1856 サブプロットのフォントサイズ 1857 subplot_label_weekday : str | None 1858 平日グラフのラベル 1859 subplot_label_weekend : str | None 1860 休日グラフのラベル 1861 y_max : float | None 1862 y軸の上限値 1863 """ 1864 # 出力ディレクトリの作成 1865 os.makedirs(output_dir, exist_ok=True) 1866 output_path: str = os.path.join(output_dir, output_filename) 1867 1868 # 起源の計算 1869 df_with_sources = self._calculate_source_contributions( 1870 df=df, 1871 col_ch4_flux=col_ch4_flux, 1872 col_c2h6_flux=col_c2h6_flux, 1873 col_datetime=col_datetime, 1874 ) 1875 1876 # 日付タイプの分類 1877 dates = pd.to_datetime(df_with_sources.index) 1878 is_weekend = dates.dayofweek.isin([5, 6]) 1879 is_holiday = dates.map(lambda x: jpholiday.is_holiday(x.date())) 1880 is_weekday = ~(is_weekend | is_holiday) 1881 1882 # データの分類 1883 data_weekday = df_with_sources[is_weekday] 1884 data_holiday = df_with_sources[is_weekend | is_holiday] 1885 1886 # プロットの作成 1887 fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5)) 1888 1889 # 平日と休日それぞれのプロット 1890 for ax, data, label in [ 1891 (ax1, data_weekday, "Weekdays"), 1892 (ax2, data_holiday, "Weekends & Holidays"), 1893 ]: 1894 # 時間ごとの平均値を計算 1895 hourly_means = data.groupby(data.index.hour)[["ch4_gas", "ch4_bio"]].mean() 1896 1897 # 24時間目のデータ点を追加 1898 last_hour = hourly_means.iloc[0:1].copy() 1899 last_hour.index = [24] 1900 hourly_means = pd.concat([hourly_means, last_hour]) 1901 1902 # 24時間分のデータポイントを作成 1903 time_points = pd.date_range("2024-01-01", periods=25, freq="h") 1904 1905 # 積み上げプロット 1906 ax.fill_between( 1907 time_points, 1908 0, 1909 hourly_means["ch4_bio"], 1910 color="blue", 1911 alpha=0.6, 1912 label=label_bio, 1913 ) 1914 ax.fill_between( 1915 time_points, 1916 hourly_means["ch4_bio"], 1917 hourly_means["ch4_bio"] + hourly_means["ch4_gas"], 1918 color="red", 1919 alpha=0.6, 1920 label=label_gas, 1921 ) 1922 1923 # 合計値のライン 1924 total_flux = hourly_means["ch4_bio"] + hourly_means["ch4_gas"] 1925 ax.plot(time_points, total_flux, "-", color="black", alpha=0.5) 1926 1927 # 軸の設定 1928 if add_label: 1929 ax.set_xlabel("Time (hour)") 1930 if ax == ax1: # 左側のプロットのラベル 1931 ax.set_ylabel("Weekdays CH$_4$ flux\n" r"(nmol m$^{-2}$ s$^{-1}$)") 1932 else: # 右側のプロットのラベル 1933 ax.set_ylabel("Weekends CH$_4$ flux\n" r"(nmol m$^{-2}$ s$^{-1}$)") 1934 1935 ax.xaxis.set_major_formatter(mdates.DateFormatter("%-H")) 1936 ax.xaxis.set_major_locator(mdates.HourLocator(byhour=[0, 6, 12, 18, 24])) 1937 ax.set_xlim(time_points[0], time_points[-1]) 1938 if y_max is not None: 1939 ax.set_ylim(0, y_max) 1940 ax.grid(True, alpha=0.3) 1941 1942 # サブプロットラベルの追加 1943 if subplot_label_weekday: 1944 ax1.text( 1945 0.02, 1946 0.98, 1947 subplot_label_weekday, 1948 transform=ax1.transAxes, 1949 va="top", 1950 fontsize=subplot_fontsize, 1951 ) 1952 if subplot_label_weekend: 1953 ax2.text( 1954 0.02, 1955 0.98, 1956 subplot_label_weekend, 1957 transform=ax2.transAxes, 1958 va="top", 1959 fontsize=subplot_fontsize, 1960 ) 1961 1962 # 凡例を図の下部に配置 1963 if add_legend: 1964 # 最初のプロットから凡例のハンドルとラベルを取得 1965 handles, labels = ax1.get_legend_handles_labels() 1966 # 図の下部に凡例を配置 1967 fig.legend( 1968 handles, 1969 labels, 1970 loc="center", 1971 bbox_to_anchor=(0.5, 0.01), # x=0.5で中央、y=0.01で下部に配置 1972 ncol=len(handles), # ハンドルの数だけ列を作成(一行に表示) 1973 ) 1974 # 凡例用のスペースを確保 1975 plt.subplots_adjust(bottom=0.2) # 下部に30%のスペースを確保 1976 1977 plt.tight_layout() 1978 plt.savefig(output_path, dpi=300, bbox_inches="tight") 1979 plt.close() 1980 1981 # 統計情報の表示 1982 if print_summary: 1983 for data, label in [ 1984 (data_weekday, "Weekdays"), 1985 (data_holiday, "Weekends & Holidays"), 1986 ]: 1987 hourly_means = data.groupby(data.index.hour)[ 1988 ["ch4_gas", "ch4_bio"] 1989 ].mean() 1990 total_flux = hourly_means["ch4_gas"] + hourly_means["ch4_bio"] 1991 1992 print(f"\n{label}の統計:") 1993 print(f" 平均値: {total_flux.mean():.2f}") 1994 print(f" 最小値: {total_flux.min():.2f} (Hour: {total_flux.idxmin()})") 1995 print(f" 最大値: {total_flux.max():.2f} (Hour: {total_flux.idxmax()})") 1996 if total_flux.min() != 0: 1997 print(f" 最大/最小比: {total_flux.max() / total_flux.min():.2f}") 1998 1999 def plot_spectra( 2000 self, 2001 fs: float, 2002 lag_second: float, 2003 input_dir: str | Path | None, 2004 output_dir: str | Path | None, 2005 output_basename: str = "spectrum", 2006 col_ch4: str = "Ultra_CH4_ppm_C", 2007 col_c2h6: str = "Ultra_C2H6_ppb", 2008 col_tv: str = "Tv", 2009 label_ch4: str | None = None, 2010 label_c2h6: str | None = None, 2011 label_tv: str | None = None, 2012 file_pattern: str = "*.csv", 2013 markersize: float = 14, 2014 are_inputs_resampled: bool = True, 2015 save_fig: bool = True, 2016 show_fig: bool = True, 2017 plot_power: bool = True, 2018 plot_co: bool = True, 2019 add_tv_in_co: bool = True, 2020 ) -> None: 2021 """ 2022 月間の平均パワースペクトル密度を計算してプロットする。 2023 2024 データファイルを指定されたディレクトリから読み込み、パワースペクトル密度を計算し、 2025 結果を指定された出力ディレクトリにプロットして保存します。 2026 2027 Parameters: 2028 ------ 2029 fs : float 2030 サンプリング周波数。 2031 lag_second : float 2032 ラグ時間(秒)。 2033 input_dir : str | Path | None 2034 データファイルが格納されているディレクトリ。 2035 output_dir : str | Path | None 2036 出力先ディレクトリ。 2037 col_ch4 : str, optional 2038 CH4の濃度データが入ったカラムのキー。デフォルトは"Ultra_CH4_ppm_C"。 2039 col_c2h6 : str, optional 2040 C2H6の濃度データが入ったカラムのキー。デフォルトは"Ultra_C2H6_ppb"。 2041 col_tv : str, optional 2042 気温データが入ったカラムのキー。デフォルトは"Tv"。 2043 label_ch4 : str | None, optional 2044 CH4のラベル。デフォルトはNone。 2045 label_c2h6 : str | None, optional 2046 C2H6のラベル。デフォルトはNone。 2047 label_tv : str | None, optional 2048 気温のラベル。デフォルトはNone。 2049 file_pattern : str, optional 2050 処理対象のファイルパターン。デフォルトは"*.csv"。 2051 markersize : float, optional 2052 プロットマーカーのサイズ。デフォルトは14。 2053 are_inputs_resampled : bool, optional 2054 入力データが再サンプリングされているかどうか。デフォルトはTrue。 2055 save_fig : bool, optional 2056 図を保存するかどうか。デフォルトはTrue。 2057 show_fig : bool, optional 2058 図を表示するかどうか。デフォルトはTrue。 2059 plot_power : bool, optional 2060 パワースペクトルをプロットするかどうか。デフォルトはTrue。 2061 plot_co : bool, optional 2062 COのスペクトルをプロットするかどうか。デフォルトはTrue。 2063 add_tv_in_co : bool, optional 2064 顕熱フラックスのコスペクトルを表示するかどうか。デフォルトはTrue。 2065 """ 2066 # 出力ディレクトリの作成 2067 if save_fig: 2068 if output_dir is None: 2069 raise ValueError( 2070 "save_fig=Trueのとき、output_dirに有効なディレクトリパスを指定する必要があります。" 2071 ) 2072 os.makedirs(output_dir, exist_ok=True) 2073 2074 # データの読み込みと結合 2075 edp = EddyDataPreprocessor(fs=fs) 2076 col_wind_w: str = EddyDataPreprocessor.WIND_W 2077 2078 # 各変数のパワースペクトルを格納する辞書 2079 power_spectra = {col_ch4: [], col_c2h6: []} 2080 co_spectra = {col_ch4: [], col_c2h6: [], col_tv: []} 2081 freqs = None 2082 2083 # プログレスバーを表示しながらファイルを処理 2084 file_list = glob.glob(os.path.join(input_dir, file_pattern)) 2085 for filepath in tqdm(file_list, desc="Processing files"): 2086 df, _ = edp.get_resampled_df( 2087 filepath=filepath, is_already_resampled=are_inputs_resampled 2088 ) 2089 2090 # 風速成分の計算を追加 2091 df = edp.add_uvw_columns(df) 2092 2093 # NaNや無限大を含む行を削除 2094 df = df.replace([np.inf, -np.inf], np.nan).dropna( 2095 subset=[col_ch4, col_c2h6, col_tv, col_wind_w] 2096 ) 2097 2098 # データが十分な行数を持っているか確認 2099 if len(df) < 100: 2100 continue 2101 2102 # 各ファイルごとにスペクトル計算 2103 calculator = SpectrumCalculator( 2104 df=df, 2105 fs=fs, 2106 ) 2107 2108 for col in power_spectra.keys(): 2109 # 各変数のパワースペクトルを計算して保存 2110 if plot_power: 2111 f, ps = calculator.calculate_power_spectrum( 2112 col=col, 2113 dimensionless=True, 2114 frequency_weighted=True, 2115 interpolate_points=True, 2116 scaling="density", 2117 ) 2118 # 最初のファイル処理時にfreqsを初期化 2119 if freqs is None: 2120 freqs = f 2121 power_spectra[col].append(ps) 2122 # 以降は周波数配列の長さが一致する場合のみ追加 2123 elif len(f) == len(freqs): 2124 power_spectra[col].append(ps) 2125 2126 # コスペクトル 2127 if plot_co: 2128 _, cs, _ = calculator.calculate_co_spectrum( 2129 col1=col_wind_w, 2130 col2=col, 2131 dimensionless=True, 2132 frequency_weighted=True, 2133 interpolate_points=True, 2134 scaling="spectrum", 2135 apply_lag_correction_to_col2=True, 2136 lag_second=lag_second, 2137 ) 2138 if freqs is not None and len(cs) == len(freqs): 2139 co_spectra[col].append(cs) 2140 2141 # 顕熱フラックスのコスペクトル計算を追加 2142 if plot_co and add_tv_in_co: 2143 _, cs_heat, _ = calculator.calculate_co_spectrum( 2144 col1=col_wind_w, 2145 col2=col_tv, 2146 dimensionless=True, 2147 frequency_weighted=True, 2148 interpolate_points=True, 2149 scaling="spectrum", 2150 ) 2151 if freqs is not None and len(cs_heat) == len(freqs): 2152 co_spectra[col_tv].append(cs_heat) 2153 2154 # 各変数のスペクトルを平均化 2155 if plot_power: 2156 averaged_power_spectra = { 2157 col: np.mean(spectra, axis=0) for col, spectra in power_spectra.items() 2158 } 2159 if plot_co: 2160 averaged_co_spectra = { 2161 col: np.mean(spectra, axis=0) for col, spectra in co_spectra.items() 2162 } 2163 # 顕熱フラックスの平均コスペクトル計算 2164 if plot_co and add_tv_in_co and co_spectra[col_tv]: 2165 averaged_heat_co_spectra = np.mean(co_spectra[col_tv], axis=0) 2166 2167 # プロット設定を修正 2168 plot_configs = [ 2169 { 2170 "col": col_ch4, 2171 "psd_ylabel": r"$fS_{\mathrm{CH_4}} / s_{\mathrm{CH_4}}^2$", 2172 "co_ylabel": r"$fC_{w\mathrm{CH_4}} / \overline{w'\mathrm{CH_4}'}$", 2173 "color": "red", 2174 "label": label_ch4, 2175 }, 2176 { 2177 "col": col_c2h6, 2178 "psd_ylabel": r"$fS_{\mathrm{C_2H_6}} / s_{\mathrm{C_2H_6}}^2$", 2179 "co_ylabel": r"$fC_{w\mathrm{C_2H_6}} / \overline{w'\mathrm{C_2H_6}'}$", 2180 "color": "orange", 2181 "label": label_c2h6, 2182 }, 2183 ] 2184 plot_tv_config = { 2185 "col": col_tv, 2186 "psd_ylabel": r"$fS_{T_v} / s_{T_v}^2$", 2187 "co_ylabel": r"$fC_{wT_v} / \overline{w'T_v'}$", 2188 "color": "blue", 2189 "label": label_tv, 2190 } 2191 2192 # パワースペクトルの図を作成 2193 if plot_power: 2194 fig_power, axes_psd = plt.subplots(1, 2, figsize=(12, 5), sharex=True) 2195 for ax, config in zip(axes_psd, plot_configs): 2196 ax.plot( 2197 freqs, 2198 averaged_power_spectra[config["col"]], 2199 "o", # マーカーを丸に設定 2200 color=config["color"], 2201 markersize=markersize, 2202 ) 2203 ax.set_xscale("log") 2204 ax.set_yscale("log") 2205 ax.set_xlim(0.001, 10) 2206 ax.plot([0.01, 10], [1, 0.01], "-", color="black", alpha=0.5) 2207 ax.text(0.1, 0.06, "-2/3", fontsize=18) 2208 ax.set_ylabel(config["psd_ylabel"]) 2209 if config["label"] is not None: 2210 ax.text( 2211 0.02, 0.98, config["label"], transform=ax.transAxes, va="top" 2212 ) 2213 ax.grid(True, alpha=0.3) 2214 ax.set_xlabel("f (Hz)") 2215 2216 plt.tight_layout() 2217 2218 if save_fig: 2219 output_path_psd: str = os.path.join( 2220 output_dir, f"power_{output_basename}.png" 2221 ) 2222 plt.savefig( 2223 output_path_psd, 2224 dpi=300, 2225 bbox_inches="tight", 2226 ) 2227 if show_fig: 2228 plt.show() 2229 else: 2230 plt.close(fig=fig_power) 2231 2232 # コスペクトルの図を作成 2233 if plot_co: 2234 fig_co, axes_cosp = plt.subplots(1, 2, figsize=(12, 5), sharex=True) 2235 for ax, config in zip(axes_cosp, plot_configs): 2236 # 顕熱フラックスのコスペクトルを先に描画(背景として) 2237 if add_tv_in_co and len(co_spectra[col_tv]) > 0: 2238 ax.plot( 2239 freqs, 2240 averaged_heat_co_spectra, 2241 "o", 2242 color="gray", 2243 alpha=0.3, 2244 markersize=markersize, 2245 label=plot_tv_config["label"] 2246 if plot_tv_config["label"] 2247 else None, 2248 ) 2249 2250 # CH4またはC2H6のコスペクトルを描画 2251 ax.plot( 2252 freqs, 2253 averaged_co_spectra[config["col"]], 2254 "o", 2255 color=config["color"], 2256 markersize=markersize, 2257 label=config["label"] if config["label"] else None, 2258 ) 2259 ax.set_xscale("log") 2260 ax.set_yscale("log") 2261 ax.set_xlim(0.001, 10) 2262 # ax.plot([0.01, 10], [1, 0.01], "-", color="black", alpha=0.5) 2263 # ax.text(0.1, 0.1, "-4/3", fontsize=18) 2264 ax.set_ylabel(config["co_ylabel"]) 2265 if config["label"] is not None: 2266 ax.text( 2267 0.02, 0.98, config["label"], transform=ax.transAxes, va="top" 2268 ) 2269 ax.grid(True, alpha=0.3) 2270 ax.set_xlabel("f (Hz)") 2271 # 凡例を追加(顕熱フラックスが含まれる場合) 2272 if add_tv_in_co and label_tv: 2273 ax.legend(loc="lower left") 2274 2275 plt.tight_layout() 2276 if save_fig: 2277 output_path_csd: str = os.path.join( 2278 output_dir, f"co_{output_basename}.png" 2279 ) 2280 plt.savefig( 2281 output_path_csd, 2282 dpi=300, 2283 bbox_inches="tight", 2284 ) 2285 if show_fig: 2286 plt.show() 2287 else: 2288 plt.close(fig=fig_co) 2289 2290 def plot_turbulence( 2291 self, 2292 df: pd.DataFrame, 2293 output_dir: str, 2294 output_filename: str = "turbulence.png", 2295 col_uz: str = "Uz", 2296 col_ch4: str = "Ultra_CH4_ppm_C", 2297 col_c2h6: str = "Ultra_C2H6_ppb", 2298 col_timestamp: str = "TIMESTAMP", 2299 add_serial_labels: bool = True, 2300 ) -> None: 2301 """時系列データのプロットを作成する 2302 2303 Parameters: 2304 ------ 2305 df : pd.DataFrame 2306 プロットするデータを含むDataFrame 2307 output_dir : str 2308 出力ディレクトリのパス 2309 output_filename : str 2310 出力ファイル名 2311 col_uz : str 2312 鉛直風速データのカラム名 2313 col_ch4 : str 2314 メタンデータのカラム名 2315 col_c2h6 : str 2316 エタンデータのカラム名 2317 col_timestamp : str 2318 タイムスタンプのカラム名 2319 """ 2320 # 出力ディレクトリの作成 2321 os.makedirs(output_dir, exist_ok=True) 2322 output_path: str = os.path.join(output_dir, output_filename) 2323 2324 # データの前処理 2325 df = df.copy() 2326 2327 # タイムスタンプをインデックスに設定(まだ設定されていない場合) 2328 if not isinstance(df.index, pd.DatetimeIndex): 2329 df[col_timestamp] = pd.to_datetime(df[col_timestamp]) 2330 df.set_index(col_timestamp, inplace=True) 2331 2332 # 開始時刻と終了時刻を取得 2333 start_time = df.index[0] 2334 end_time = df.index[-1] 2335 2336 # 開始時刻の分を取得 2337 start_minute = start_time.minute 2338 2339 # 時間軸の作成(実際の開始時刻からの経過分数) 2340 minutes_elapsed = (df.index - start_time).total_seconds() / 60 2341 2342 # プロットの作成 2343 _, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=(12, 10), sharex=True) 2344 2345 # 鉛直風速 2346 ax1.plot(minutes_elapsed, df[col_uz], "k-", linewidth=0.5) 2347 ax1.set_ylabel(r"$w$ (m s$^{-1}$)") 2348 if add_serial_labels: 2349 ax1.text(0.02, 0.98, "(a)", transform=ax1.transAxes, va="top") 2350 ax1.grid(True, alpha=0.3) 2351 2352 # CH4濃度 2353 ax2.plot(minutes_elapsed, df[col_ch4], "r-", linewidth=0.5) 2354 ax2.set_ylabel(r"$\mathrm{CH_4}$ (ppm)") 2355 if add_serial_labels: 2356 ax2.text(0.02, 0.98, "(b)", transform=ax2.transAxes, va="top") 2357 ax2.grid(True, alpha=0.3) 2358 2359 # C2H6濃度 2360 ax3.plot(minutes_elapsed, df[col_c2h6], "orange", linewidth=0.5) 2361 ax3.set_ylabel(r"$\mathrm{C_2H_6}$ (ppb)") 2362 if add_serial_labels: 2363 ax3.text(0.02, 0.98, "(c)", transform=ax3.transAxes, va="top") 2364 ax3.grid(True, alpha=0.3) 2365 ax3.set_xlabel("Time (minutes)") 2366 2367 # x軸の範囲を実際の開始時刻から30分後までに設定 2368 total_minutes = (end_time - start_time).total_seconds() / 60 2369 ax3.set_xlim(0, min(30, total_minutes)) 2370 2371 # x軸の目盛りを5分間隔で設定 2372 np.arange(start_minute, start_minute + 35, 5) 2373 ax3.xaxis.set_major_locator(MultipleLocator(5)) 2374 2375 # レイアウトの調整 2376 plt.tight_layout() 2377 2378 # 図の保存 2379 plt.savefig(output_path, dpi=300, bbox_inches="tight") 2380 plt.close() 2381 2382 def plot_wind_rose_sources( 2383 self, 2384 df: pd.DataFrame, 2385 output_dir: str | Path | None = None, 2386 output_filename: str = "edp_wind_rose.png", 2387 col_datetime: str = "Date", 2388 col_ch4_flux: str = "Fch4", 2389 col_c2h6_flux: str = "Fc2h6", 2390 col_wind_dir: str = "Wind direction", 2391 flux_unit: str = r"(nmol m$^{-2}$ s$^{-1}$)", 2392 ymax: float | None = None, # フラックスの上限値 2393 label_gas: str = "都市ガス起源", 2394 label_bio: str = "生物起源", 2395 figsize: tuple[float, float] = (8, 8), 2396 flux_alpha: float = 0.4, 2397 num_directions: int = 8, # 方位の数(8方位) 2398 center_on_angles: bool = True, # 追加:45度刻みの線を境界にするかどうか 2399 subplot_label: str | None = None, 2400 add_legend: bool = True, 2401 stack_bars: bool = False, # 追加:積み上げ方式を選択するパラメータ 2402 print_summary: bool = True, # 統計情報を表示するかどうか 2403 save_fig: bool = True, 2404 show_fig: bool = True, 2405 ) -> None: 2406 """CH4フラックスの都市ガス起源と生物起源の風配図を作成する関数 2407 2408 Parameters: 2409 ------ 2410 df : pd.DataFrame 2411 風配図を作成するためのデータフレーム 2412 output_dir : str | Path | None 2413 生成された図を保存するディレクトリのパス 2414 output_filename : str 2415 保存するファイル名(デフォルトは"edp_wind_rose.png") 2416 col_ch4_flux : str 2417 CH4フラックスを示すカラム名 2418 col_c2h6_flux : str 2419 C2H6フラックスを示すカラム名 2420 col_wind_dir : str 2421 風向を示すカラム名 2422 label_gas : str 2423 都市ガス起源のフラックスに対するラベル 2424 label_bio : str 2425 生物起源のフラックスに対するラベル 2426 col_datetime : str 2427 日時を示すカラム名 2428 num_directions : int 2429 風向の数(デフォルトは8) 2430 center_on_angles: bool 2431 Trueの場合、45度刻みの線を境界として扇形を描画します。 2432 Falseの場合、45度の中間(22.5度)を中心として扇形を描画します。 2433 subplot_label : str 2434 サブプロットに表示するラベル 2435 print_summary : bool 2436 統計情報を表示するかどうかのフラグ 2437 flux_unit : str 2438 フラックスの単位 2439 ymax : float | None 2440 y軸の上限値(指定しない場合はデータの最大値に基づいて自動設定) 2441 figsize : tuple[float, float] 2442 図のサイズ 2443 flux_alpha : float 2444 フラックスの透明度 2445 stack_bars : bool, optional 2446 Trueの場合、生物起源の上に都市ガス起源を積み上げます。 2447 Falseの場合、両方を0から積み上げます(デフォルト)。 2448 save_fig : bool 2449 図を保存するかどうかのフラグ 2450 show_fig : bool 2451 図を表示するかどうかのフラグ 2452 """ 2453 # 起源の計算 2454 df_with_sources = self._calculate_source_contributions( 2455 df=df, 2456 col_ch4_flux=col_ch4_flux, 2457 col_c2h6_flux=col_c2h6_flux, 2458 col_datetime=col_datetime, 2459 ) 2460 2461 # 方位の定義 2462 direction_ranges = self._define_direction_ranges( 2463 num_directions, center_on_angles 2464 ) 2465 2466 # 方位ごとのデータを集計 2467 direction_data = self._aggregate_direction_data( 2468 df_with_sources, col_wind_dir, direction_ranges 2469 ) 2470 2471 # プロットの作成 2472 fig = plt.figure(figsize=figsize) 2473 ax = fig.add_subplot(111, projection="polar") 2474 2475 # 方位の角度(ラジアン)を計算 2476 theta = np.array( 2477 [np.radians(angle) for angle in direction_data["center_angle"]] 2478 ) 2479 2480 # 積み上げ方式に応じてプロット 2481 if stack_bars: 2482 # 生物起源を基準として描画 2483 ax.bar( 2484 theta, 2485 direction_data["bio_flux"], 2486 width=np.radians(360 / num_directions), 2487 bottom=0.0, 2488 color="blue", 2489 alpha=flux_alpha, 2490 label=label_bio, 2491 ) 2492 # 都市ガス起源を生物起源の上に積み上げ 2493 ax.bar( 2494 theta, 2495 direction_data["gas_flux"], 2496 width=np.radians(360 / num_directions), 2497 bottom=direction_data["bio_flux"], # 生物起源の上に積み上げ 2498 color="red", 2499 alpha=flux_alpha, 2500 label=label_gas, 2501 ) 2502 else: 2503 # 両方を0から積み上げ(デフォルト) 2504 ax.bar( 2505 theta, 2506 direction_data["bio_flux"], 2507 width=np.radians(360 / num_directions), 2508 bottom=0.0, 2509 color="blue", 2510 alpha=flux_alpha, 2511 label=label_bio, 2512 ) 2513 ax.bar( 2514 theta, 2515 direction_data["gas_flux"], 2516 width=np.radians(360 / num_directions), 2517 bottom=0.0, 2518 color="red", 2519 alpha=flux_alpha, 2520 label=label_gas, 2521 ) 2522 2523 # y軸の範囲を設定 2524 if ymax is not None: 2525 ax.set_ylim(0, ymax) 2526 else: 2527 # データの最大値に基づいて自動設定 2528 max_value = max( 2529 direction_data["bio_flux"].max(), direction_data["gas_flux"].max() 2530 ) 2531 ax.set_ylim(0, max_value * 1.1) # 最大値の1.1倍を上限に設定 2532 2533 # 方位ラベルの設定 2534 ax.set_theta_zero_location("N") # 北を上に設定 2535 ax.set_theta_direction(-1) # 時計回りに設定 2536 2537 # 方位ラベルの表示 2538 labels = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"] 2539 angles = np.radians(np.linspace(0, 360, len(labels), endpoint=False)) 2540 ax.set_xticks(angles) 2541 ax.set_xticklabels(labels) 2542 2543 # プロット領域の調整(上部と下部にスペースを確保) 2544 plt.subplots_adjust( 2545 top=0.8, # 上部に20%のスペースを確保 2546 bottom=0.2, # 下部に20%のスペースを確保(凡例用) 2547 ) 2548 2549 # サブプロットラベルの追加(デフォルトは左上) 2550 if subplot_label: 2551 ax.text( 2552 0.01, 2553 0.99, 2554 subplot_label, 2555 transform=ax.transAxes, 2556 ) 2557 2558 # 単位の追加(図の下部中央に配置) 2559 plt.figtext( 2560 0.5, # x位置(中央) 2561 0.1, # y位置(下部) 2562 flux_unit, 2563 ha="center", # 水平方向の位置揃え 2564 va="bottom", # 垂直方向の位置揃え 2565 ) 2566 2567 # 凡例の追加(単位の下に配置) 2568 if add_legend: 2569 # 最初のプロットから凡例のハンドルとラベルを取得 2570 handles, labels = ax.get_legend_handles_labels() 2571 # 図の下部に凡例を配置 2572 fig.legend( 2573 handles, 2574 labels, 2575 loc="center", 2576 bbox_to_anchor=(0.5, 0.05), # x=0.5で中央、y=0.05で下部に配置 2577 ncol=len(handles), # ハンドルの数だけ列を作成(一行に表示) 2578 ) 2579 2580 # グラフの保存 2581 if save_fig: 2582 if output_dir is None: 2583 raise ValueError( 2584 "save_fig=Trueのとき、output_dirに有効なパスを指定する必要があります。" 2585 ) 2586 # 出力ディレクトリの作成 2587 os.makedirs(output_dir, exist_ok=True) 2588 output_path: str = os.path.join(output_dir, output_filename) 2589 plt.savefig(output_path, dpi=300, bbox_inches="tight") 2590 2591 # グラフの表示 2592 if show_fig: 2593 plt.show() 2594 else: 2595 plt.close(fig=fig) 2596 2597 # 統計情報の表示 2598 if print_summary: 2599 for source in ["gas", "bio"]: 2600 flux_data = direction_data[f"{source}_flux"] 2601 mean_val = flux_data.mean() 2602 max_val = flux_data.max() 2603 max_dir = direction_data.loc[flux_data.idxmax(), "name"] 2604 2605 self.logger.info( 2606 f"{label_gas if source == 'gas' else label_bio}の統計:" 2607 ) 2608 print(f" 平均フラックス: {mean_val:.2f}") 2609 print(f" 最大フラックス: {max_val:.2f}") 2610 print(f" 最大フラックスの方位: {max_dir}") 2611 2612 def _define_direction_ranges( 2613 self, 2614 num_directions: int = 8, 2615 center_on_angles: bool = False, 2616 ) -> pd.DataFrame: 2617 """方位の範囲を定義 2618 2619 Parameters: 2620 ------ 2621 num_directions : int 2622 方位の数(デフォルトは8) 2623 center_on_angles : bool 2624 Trueの場合、45度刻みの線を境界として扇形を描画します。 2625 Falseの場合、45度の中間(22.5度)を中心として扇形を描画します。 2626 2627 Returns: 2628 ------ 2629 pd.DataFrame 2630 方位の定義を含むDataFrame 2631 """ 2632 if num_directions == 8: 2633 if center_on_angles: 2634 # 45度刻みの線を境界とする場合 2635 directions = pd.DataFrame( 2636 { 2637 "name": ["N", "NE", "E", "SE", "S", "SW", "W", "NW"], 2638 "center_angle": [ 2639 22.5, 2640 67.5, 2641 112.5, 2642 157.5, 2643 202.5, 2644 247.5, 2645 292.5, 2646 337.5, 2647 ], 2648 } 2649 ) 2650 else: 2651 # 従来通り45度を中心とする場合 2652 directions = pd.DataFrame( 2653 { 2654 "name": ["N", "NE", "E", "SE", "S", "SW", "W", "NW"], 2655 "center_angle": [0, 45, 90, 135, 180, 225, 270, 315], 2656 } 2657 ) 2658 else: 2659 raise ValueError(f"現在{num_directions}方位はサポートされていません") 2660 2661 # 各方位の範囲を計算 2662 angle_range = 360 / num_directions 2663 directions["start_angle"] = directions["center_angle"] - angle_range / 2 2664 directions["end_angle"] = directions["center_angle"] + angle_range / 2 2665 2666 # -180度から180度の範囲に正規化 2667 directions["start_angle"] = np.where( 2668 directions["start_angle"] > 180, 2669 directions["start_angle"] - 360, 2670 directions["start_angle"], 2671 ) 2672 directions["end_angle"] = np.where( 2673 directions["end_angle"] > 180, 2674 directions["end_angle"] - 360, 2675 directions["end_angle"], 2676 ) 2677 2678 return directions 2679 2680 def _aggregate_direction_data( 2681 self, 2682 df: pd.DataFrame, 2683 col_wind_dir: str, 2684 direction_ranges: pd.DataFrame, 2685 ) -> pd.DataFrame: 2686 """方位ごとのフラックスデータを集計 2687 2688 Parameters: 2689 ------ 2690 df : pd.DataFrame 2691 ソース分離済みのデータフレーム 2692 col_wind_dir : str 2693 風向のカラム名 2694 direction_ranges : pd.DataFrame 2695 方位の定義 2696 2697 Returns: 2698 ------ 2699 pd.DataFrame 2700 方位ごとの集計データ 2701 """ 2702 result_data = direction_ranges.copy() 2703 result_data["gas_flux"] = 0.0 2704 result_data["bio_flux"] = 0.0 2705 2706 for idx, row in direction_ranges.iterrows(): 2707 if row["start_angle"] < row["end_angle"]: 2708 mask = (df[col_wind_dir] > row["start_angle"]) & ( 2709 df[col_wind_dir] <= row["end_angle"] 2710 ) 2711 else: # 北方向など、-180度と180度をまたぐ場合 2712 mask = (df[col_wind_dir] > row["start_angle"]) | ( 2713 df[col_wind_dir] <= row["end_angle"] 2714 ) 2715 2716 result_data.loc[idx, "gas_flux"] = df.loc[mask, "ch4_gas"].mean() 2717 result_data.loc[idx, "bio_flux"] = df.loc[mask, "ch4_bio"].mean() 2718 2719 # NaNを0に置換 2720 result_data = result_data.fillna(0) 2721 2722 return result_data 2723 2724 def _calculate_source_contributions( 2725 self, 2726 df: pd.DataFrame, 2727 col_ch4_flux: str, 2728 col_c2h6_flux: str, 2729 gas_ratio_c1c2: float = 0.076, 2730 col_datetime: str = "Date", 2731 ) -> pd.DataFrame: 2732 """ 2733 CH4フラックスの都市ガス起源と生物起源の寄与を計算する。 2734 このロジックでは、燃焼起源のCH4フラックスは考慮せず計算している。 2735 2736 Parameters: 2737 ------ 2738 df : pd.DataFrame 2739 入力データフレーム 2740 col_ch4_flux : str 2741 CH4フラックスのカラム名 2742 col_c2h6_flux : str 2743 C2H6フラックスのカラム名 2744 gas_ratio_c1c2 : float 2745 ガスのC2H6/CH4比(ppb/ppb) 2746 col_datetime : str 2747 日時カラムの名前 2748 2749 Returns: 2750 ------ 2751 pd.DataFrame 2752 起源別のフラックス値を含むデータフレーム 2753 """ 2754 df_processed = df.copy() 2755 2756 # 日時インデックスの処理 2757 if not isinstance(df_processed.index, pd.DatetimeIndex): 2758 df_processed[col_datetime] = pd.to_datetime(df_processed[col_datetime]) 2759 df_processed.set_index(col_datetime, inplace=True) 2760 2761 # C2H6/CH4比の計算 2762 df_processed["c2c1_ratio"] = ( 2763 df_processed[col_c2h6_flux] / df_processed[col_ch4_flux] 2764 ) 2765 2766 # 都市ガスの標準組成に基づく都市ガス比率の計算 2767 df_processed["gas_ratio"] = df_processed["c2c1_ratio"] / gas_ratio_c1c2 * 100 2768 2769 # gas_ratioに基づいて都市ガス起源と生物起源の寄与を比例配分 2770 df_processed["ch4_gas"] = df_processed[col_ch4_flux] * np.clip( 2771 df_processed["gas_ratio"] / 100, 0, 1 2772 ) 2773 df_processed["ch4_bio"] = df_processed[col_ch4_flux] * ( 2774 1 - np.clip(df_processed["gas_ratio"] / 100, 0, 1) 2775 ) 2776 2777 return df_processed 2778 2779 def _prepare_diurnal_data( 2780 self, 2781 df: pd.DataFrame, 2782 target_columns: list[str], 2783 include_date_types: bool = False, 2784 ) -> tuple[dict[str, pd.DataFrame], pd.DatetimeIndex]: 2785 """ 2786 日変化パターンの計算に必要なデータを準備する。 2787 2788 Parameters: 2789 ------ 2790 df : pd.DataFrame 2791 入力データフレーム 2792 target_columns : list[str] 2793 計算対象の列名のリスト 2794 include_date_types : bool 2795 日付タイプ(平日/休日など)の分類を含めるかどうか 2796 2797 Returns: 2798 ------ 2799 tuple[dict[str, pd.DataFrame], pd.DatetimeIndex] 2800 - 時間帯ごとの平均値を含むDataFrameの辞書 2801 - 24時間分の時間点 2802 """ 2803 df = df.copy() 2804 df["hour"] = pd.to_datetime(df["Date"]).dt.hour 2805 2806 # 時間ごとの平均値を計算する関数 2807 def calculate_hourly_means(data_df, condition=None): 2808 if condition is not None: 2809 data_df = data_df[condition] 2810 return data_df.groupby("hour")[target_columns].mean().reset_index() 2811 2812 # 基本の全日データを計算 2813 hourly_means = {"all": calculate_hourly_means(df)} 2814 2815 # 日付タイプによる分類が必要な場合 2816 if include_date_types: 2817 dates = pd.to_datetime(df["Date"]) 2818 is_weekend = dates.dt.dayofweek.isin([5, 6]) 2819 is_holiday = dates.map(lambda x: jpholiday.is_holiday(x.date())) 2820 is_weekday = ~(is_weekend | is_holiday) 2821 2822 hourly_means.update( 2823 { 2824 "weekday": calculate_hourly_means(df, is_weekday), 2825 "weekend": calculate_hourly_means(df, is_weekend), 2826 "holiday": calculate_hourly_means(df, is_weekend | is_holiday), 2827 } 2828 ) 2829 2830 # 24時目のデータを追加 2831 for col in hourly_means: 2832 last_row = hourly_means[col].iloc[0:1].copy() 2833 last_row["hour"] = 24 2834 hourly_means[col] = pd.concat( 2835 [hourly_means[col], last_row], ignore_index=True 2836 ) 2837 2838 # 24時間分のデータポイントを作成 2839 time_points = pd.date_range("2024-01-01", periods=25, freq="h") 2840 2841 return hourly_means, time_points 2842 2843 def _setup_diurnal_axes( 2844 self, 2845 ax: plt.Axes, 2846 time_points: pd.DatetimeIndex, 2847 ylabel: str, 2848 subplot_label: str | None = None, 2849 add_label: bool = True, 2850 add_legend: bool = True, 2851 subplot_fontsize: int = 20, 2852 ) -> None: 2853 """日変化プロットの軸の設定を行う 2854 2855 Parameters: 2856 ------ 2857 ax : plt.Axes 2858 設定対象の軸 2859 time_points : pd.DatetimeIndex 2860 時間軸のポイント 2861 ylabel : str 2862 y軸のラベル 2863 subplot_label : str | None 2864 サブプロットのラベル 2865 add_label : bool 2866 軸ラベルを表示するかどうか 2867 add_legend : bool 2868 凡例を表示するかどうか 2869 subplot_fontsize : int 2870 サブプロットのフォントサイズ 2871 """ 2872 if add_label: 2873 ax.set_xlabel("Time (hour)") 2874 ax.set_ylabel(ylabel) 2875 2876 ax.xaxis.set_major_formatter(mdates.DateFormatter("%-H")) 2877 ax.xaxis.set_major_locator(mdates.HourLocator(byhour=[0, 6, 12, 18, 24])) 2878 ax.set_xlim(time_points[0], time_points[-1]) 2879 ax.set_xticks(time_points[::6]) 2880 ax.set_xticklabels(["0", "6", "12", "18", "24"]) 2881 2882 if subplot_label: 2883 ax.text( 2884 0.02, 2885 0.98, 2886 subplot_label, 2887 transform=ax.transAxes, 2888 va="top", 2889 fontsize=subplot_fontsize, 2890 ) 2891 2892 if add_legend: 2893 ax.legend() 2894 2895 @staticmethod 2896 def get_valid_data(df: pd.DataFrame, x_col: str, y_col: str) -> pd.DataFrame: 2897 """ 2898 指定された列の有効なデータ(NaNを除いた)を取得します。 2899 2900 Parameters: 2901 ------ 2902 df : pd.DataFrame 2903 データフレーム 2904 x_col : str 2905 X軸の列名 2906 y_col : str 2907 Y軸の列名 2908 2909 Returns: 2910 ------ 2911 pd.DataFrame 2912 有効なデータのみを含むDataFrame 2913 """ 2914 return df.copy().dropna(subset=[x_col, y_col]) 2915 2916 @staticmethod 2917 def setup_logger(logger: Logger | None, log_level: int = INFO) -> Logger: 2918 """ 2919 ロガーを設定します。 2920 2921 このメソッドは、ロギングの設定を行い、ログメッセージのフォーマットを指定します。 2922 ログメッセージには、日付、ログレベル、メッセージが含まれます。 2923 2924 渡されたロガーがNoneまたは不正な場合は、新たにロガーを作成し、標準出力に 2925 ログメッセージが表示されるようにStreamHandlerを追加します。ロガーのレベルは 2926 引数で指定されたlog_levelに基づいて設定されます。 2927 2928 Parameters: 2929 ------ 2930 logger : Logger | None 2931 使用するロガー。Noneの場合は新しいロガーを作成します。 2932 log_level : int 2933 ロガーのログレベル。デフォルトはINFO。 2934 2935 Returns: 2936 ------ 2937 Logger 2938 設定されたロガーオブジェクト。 2939 """ 2940 if logger is not None and isinstance(logger, Logger): 2941 return logger 2942 # 渡されたロガーがNoneまたは正しいものでない場合は独自に設定 2943 new_logger: Logger = getLogger() 2944 # 既存のハンドラーをすべて削除 2945 for handler in new_logger.handlers[:]: 2946 new_logger.removeHandler(handler) 2947 new_logger.setLevel(log_level) # ロガーのレベルを設定 2948 ch = StreamHandler() 2949 ch_formatter = Formatter("%(asctime)s - %(levelname)s - %(message)s") 2950 ch.setFormatter(ch_formatter) # フォーマッターをハンドラーに設定 2951 new_logger.addHandler(ch) # StreamHandlerの追加 2952 return new_logger 2953 2954 @staticmethod 2955 def plot_flux_distributions( 2956 g2401_flux: pd.Series, 2957 ultra_flux: pd.Series, 2958 month: int, 2959 output_dir: str, 2960 xlim: tuple[float, float] = (-50, 200), 2961 bandwidth: float = 1.0, # デフォルト値を1.0に設定 2962 ) -> None: 2963 """ 2964 両測器のCH4フラックス分布を可視化 2965 2966 Parameters: 2967 ------ 2968 g2401_flux : pd.Series 2969 G2401で測定されたフラックス値の配列 2970 ultra_flux : pd.Series 2971 Ultraで測定されたフラックス値の配列 2972 month : int 2973 測定月 2974 output_dir : str 2975 出力ディレクトリ 2976 xlim : tuple[float, float] 2977 x軸の範囲(タプル) 2978 bandwidth : float 2979 カーネル密度推定のバンド幅調整係数(デフォルト: 1.0) 2980 """ 2981 # nanを除去 2982 g2401_flux = g2401_flux.dropna() 2983 ultra_flux = ultra_flux.dropna() 2984 2985 plt.figure(figsize=(10, 6)) 2986 2987 # KDEプロット(確率密度推定) 2988 sns.kdeplot( 2989 data=g2401_flux, label="G2401", color="blue", alpha=0.5, bw_adjust=bandwidth 2990 ) 2991 sns.kdeplot( 2992 data=ultra_flux, label="Ultra", color="red", alpha=0.5, bw_adjust=bandwidth 2993 ) 2994 2995 # 平均値と中央値のマーカー 2996 plt.axvline( 2997 g2401_flux.mean(), 2998 color="blue", 2999 linestyle="--", 3000 alpha=0.5, 3001 label="G2401 mean", 3002 ) 3003 plt.axvline( 3004 ultra_flux.mean(), 3005 color="red", 3006 linestyle="--", 3007 alpha=0.5, 3008 label="Ultra mean", 3009 ) 3010 plt.axvline( 3011 np.median(g2401_flux), 3012 color="blue", 3013 linestyle=":", 3014 alpha=0.5, 3015 label="G2401 median", 3016 ) 3017 plt.axvline( 3018 np.median(ultra_flux), 3019 color="red", 3020 linestyle=":", 3021 alpha=0.5, 3022 label="Ultra median", 3023 ) 3024 3025 # 軸ラベルとタイトル 3026 plt.xlabel(r"CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)") 3027 plt.ylabel("Probability Density") 3028 plt.title(f"Distribution of CH$_4$ fluxes - Month {month}") 3029 3030 # x軸の範囲設定 3031 plt.xlim(xlim) 3032 3033 # グリッド表示 3034 plt.grid(True, alpha=0.3) 3035 3036 # 統計情報 3037 stats_text = ( 3038 f"G2401:\n" 3039 f" Mean: {g2401_flux.mean():.2f}\n" 3040 f" Median: {np.median(g2401_flux):.2f}\n" 3041 f" Std: {g2401_flux.std():.2f}\n" 3042 f"Ultra:\n" 3043 f" Mean: {ultra_flux.mean():.2f}\n" 3044 f" Median: {np.median(ultra_flux):.2f}\n" 3045 f" Std: {ultra_flux.std():.2f}" 3046 ) 3047 plt.text( 3048 0.02, 3049 0.98, 3050 stats_text, 3051 transform=plt.gca().transAxes, 3052 verticalalignment="top", 3053 fontsize=10, 3054 bbox=dict(boxstyle="round", facecolor="white", alpha=0.8), 3055 ) 3056 3057 # 凡例の表示 3058 plt.legend(loc="upper right") 3059 3060 # グラフの保存 3061 os.makedirs(output_dir, exist_ok=True) 3062 plt.tight_layout() 3063 plt.savefig( 3064 os.path.join(output_dir, f"flux_distribution_month_{month}.png"), 3065 dpi=300, 3066 bbox_inches="tight", 3067 ) 3068 plt.close()
64 def __init__( 65 self, 66 logger: Logger | None = None, 67 logging_debug: bool = False, 68 ) -> None: 69 """ 70 クラスのコンストラクタ 71 72 Parameters: 73 ------ 74 logger : Logger | None 75 使用するロガー。Noneの場合は新しいロガーを作成します。 76 logging_debug : bool 77 ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。 78 """ 79 # ロガー 80 log_level: int = INFO 81 if logging_debug: 82 log_level = DEBUG 83 self.logger: Logger = MonthlyFiguresGenerator.setup_logger(logger, log_level)
クラスのコンストラクタ
Parameters:
logger : Logger | None
使用するロガー。Noneの場合は新しいロガーを作成します。
logging_debug : bool
ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。
85 def plot_c1c2_fluxes_timeseries( 86 self, 87 df, 88 output_dir: str, 89 output_filename: str = "timeseries.png", 90 col_datetime: str = "Date", 91 col_c1_flux: str = "Fch4_ultra", 92 col_c2_flux: str = "Fc2h6_ultra", 93 ): 94 """ 95 月別のフラックスデータを時系列プロットとして出力する 96 97 Parameters: 98 ------ 99 df : pd.DataFrame 100 月別データを含むDataFrame 101 output_dir : str 102 出力ファイルを保存するディレクトリのパス 103 output_filename : str 104 出力ファイルの名前 105 col_datetime : str 106 日付を含む列の名前。デフォルトは"Date"。 107 col_c1_flux : str 108 CH4フラックスを含む列の名前。デフォルトは"Fch4_ultra"。 109 col_c2_flux : str 110 C2H6フラックスを含む列の名前。デフォルトは"Fc2h6_ultra"。 111 """ 112 os.makedirs(output_dir, exist_ok=True) 113 output_path: str = os.path.join(output_dir, output_filename) 114 115 # 図の作成 116 _, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 8), sharex=True) 117 118 # CH4フラックスのプロット 119 ax1.scatter(df[col_datetime], df[col_c1_flux], color="red", alpha=0.5, s=20) 120 ax1.set_ylabel(r"CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)") 121 ax1.set_ylim(-100, 600) 122 ax1.text(0.02, 0.98, "(a)", transform=ax1.transAxes, va="top", fontsize=20) 123 ax1.grid(True, alpha=0.3) 124 125 # C2H6フラックスのプロット 126 ax2.scatter( 127 df[col_datetime], 128 df[col_c2_flux], 129 color="orange", 130 alpha=0.5, 131 s=20, 132 ) 133 ax2.set_ylabel(r"C$_2$H$_6$ flux (nmol m$^{-2}$ s$^{-1}$)") 134 ax2.set_ylim(-20, 60) 135 ax2.text(0.02, 0.98, "(b)", transform=ax2.transAxes, va="top", fontsize=20) 136 ax2.grid(True, alpha=0.3) 137 138 # x軸の設定 139 ax2.xaxis.set_major_locator(mdates.MonthLocator(interval=1)) 140 ax2.xaxis.set_major_formatter(mdates.DateFormatter("%m")) 141 plt.setp(ax2.get_xticklabels(), rotation=0, ha="right") 142 ax2.set_xlabel("Month") 143 144 # 図の保存 145 plt.savefig(output_path, dpi=300, bbox_inches="tight") 146 plt.close()
月別のフラックスデータを時系列プロットとして出力する
Parameters:
df : pd.DataFrame
月別データを含むDataFrame
output_dir : str
出力ファイルを保存するディレクトリのパス
output_filename : str
出力ファイルの名前
col_datetime : str
日付を含む列の名前。デフォルトは"Date"。
col_c1_flux : str
CH4フラックスを含む列の名前。デフォルトは"Fch4_ultra"。
col_c2_flux : str
C2H6フラックスを含む列の名前。デフォルトは"Fc2h6_ultra"。
148 def plot_c1c2_concentrations_and_fluxes_timeseries( 149 self, 150 df: pd.DataFrame, 151 output_dir: str, 152 output_filename: str = "conc_flux_timeseries.png", 153 col_datetime: str = "Date", 154 col_ch4_conc: str = "CH4_ultra", 155 col_ch4_flux: str = "Fch4_ultra", 156 col_c2h6_conc: str = "C2H6_ultra", 157 col_c2h6_flux: str = "Fc2h6_ultra", 158 print_summary: bool = True, 159 ) -> None: 160 """ 161 CH4とC2H6の濃度とフラックスの時系列プロットを作成する 162 163 Parameters: 164 ------ 165 df : pd.DataFrame 166 月別データを含むDataFrame 167 output_dir : str 168 出力ディレクトリのパス 169 output_filename : str 170 出力ファイル名 171 col_datetime : str 172 日付列の名前 173 col_ch4_conc : str 174 CH4濃度列の名前 175 col_ch4_flux : str 176 CH4フラックス列の名前 177 col_c2h6_conc : str 178 C2H6濃度列の名前 179 col_c2h6_flux : str 180 C2H6フラックス列の名前 181 print_summary : bool 182 解析情報をprintするかどうか 183 """ 184 # 出力ディレクトリの作成 185 os.makedirs(output_dir, exist_ok=True) 186 output_path: str = os.path.join(output_dir, output_filename) 187 188 if print_summary: 189 # 統計情報の計算と表示 190 for name, col in [ 191 ("CH4 concentration", col_ch4_conc), 192 ("CH4 flux", col_ch4_flux), 193 ("C2H6 concentration", col_c2h6_conc), 194 ("C2H6 flux", col_c2h6_flux), 195 ]: 196 # NaNを除外してから統計量を計算 197 valid_data = df[col].dropna() 198 199 if len(valid_data) > 0: 200 percentile_5 = np.nanpercentile(valid_data, 5) 201 percentile_95 = np.nanpercentile(valid_data, 95) 202 mean_value = np.nanmean(valid_data) 203 positive_ratio = (valid_data > 0).mean() * 100 204 205 print(f"\n{name}:") 206 print( 207 f"90パーセンタイルレンジ: {percentile_5:.2f} - {percentile_95:.2f}" 208 ) 209 print(f"平均値: {mean_value:.2f}") 210 print(f"正の値の割合: {positive_ratio:.1f}%") 211 else: 212 print(f"\n{name}: データが存在しません") 213 214 # プロットの作成 215 _, (ax1, ax2, ax3, ax4) = plt.subplots(4, 1, figsize=(12, 16), sharex=True) 216 217 # CH4濃度のプロット 218 ax1.scatter(df[col_datetime], df[col_ch4_conc], color="red", alpha=0.5, s=20) 219 ax1.set_ylabel("CH$_4$ Concentration\n(ppm)") 220 ax1.set_ylim(1.8, 2.6) 221 ax1.text(0.02, 0.98, "(a)", transform=ax1.transAxes, va="top", fontsize=20) 222 ax1.grid(True, alpha=0.3) 223 224 # CH4フラックスのプロット 225 ax2.scatter(df[col_datetime], df[col_ch4_flux], color="red", alpha=0.5, s=20) 226 ax2.set_ylabel("CH$_4$ flux\n(nmol m$^{-2}$ s$^{-1}$)") 227 ax2.set_ylim(-100, 600) 228 # ax2.set_yticks([-100, 0, 200, 400, 600]) 229 ax2.text(0.02, 0.98, "(b)", transform=ax2.transAxes, va="top", fontsize=20) 230 ax2.grid(True, alpha=0.3) 231 232 # C2H6濃度のプロット 233 ax3.scatter( 234 df[col_datetime], df[col_c2h6_conc], color="orange", alpha=0.5, s=20 235 ) 236 ax3.set_ylabel("C$_2$H$_6$ Concentration\n(ppb)") 237 ax3.text(0.02, 0.98, "(c)", transform=ax3.transAxes, va="top", fontsize=20) 238 ax3.grid(True, alpha=0.3) 239 240 # C2H6フラックスのプロット 241 ax4.scatter( 242 df[col_datetime], df[col_c2h6_flux], color="orange", alpha=0.5, s=20 243 ) 244 ax4.set_ylabel("C$_2$H$_6$ flux\n(nmol m$^{-2}$ s$^{-1}$)") 245 ax4.set_ylim(-20, 40) 246 ax4.text(0.02, 0.98, "(d)", transform=ax4.transAxes, va="top", fontsize=20) 247 ax4.grid(True, alpha=0.3) 248 249 # x軸の設定 250 ax4.xaxis.set_major_locator(mdates.MonthLocator(interval=1)) 251 ax4.xaxis.set_major_formatter(mdates.DateFormatter("%m")) 252 plt.setp(ax4.get_xticklabels(), rotation=0, ha="right") 253 ax4.set_xlabel("Month") 254 255 # レイアウトの調整と保存 256 plt.tight_layout() 257 plt.savefig(output_path, dpi=300, bbox_inches="tight") 258 plt.close() 259 260 if print_summary: 261 262 def analyze_top_values(df, column_name, top_percent=20): 263 print(f"\n{column_name}の上位{top_percent}%の分析:") 264 265 # DataFrameのコピーを作成し、日時関連の列を追加 266 df_analysis = df.copy() 267 df_analysis["hour"] = pd.to_datetime(df_analysis[col_datetime]).dt.hour 268 df_analysis["month"] = pd.to_datetime( 269 df_analysis[col_datetime] 270 ).dt.month 271 df_analysis["weekday"] = pd.to_datetime( 272 df_analysis[col_datetime] 273 ).dt.dayofweek 274 275 # 上位20%のしきい値を計算 276 threshold = df[column_name].quantile(1 - top_percent / 100) 277 high_values = df_analysis[df_analysis[column_name] > threshold] 278 279 # 月ごとの分析 280 print("\n月別分布:") 281 monthly_counts = high_values.groupby("month").size() 282 total_counts = df_analysis.groupby("month").size() 283 monthly_percentages = (monthly_counts / total_counts * 100).round(1) 284 285 # 月ごとのデータを安全に表示 286 available_months = set(monthly_counts.index) & set(total_counts.index) 287 for month in sorted(available_months): 288 print( 289 f"月{month}: {monthly_percentages[month]}% ({monthly_counts[month]}件/{total_counts[month]}件)" 290 ) 291 292 # 時間帯ごとの分析(3時間区切り) 293 print("\n時間帯別分布:") 294 # copyを作成して新しい列を追加 295 high_values = high_values.copy() 296 high_values["time_block"] = high_values["hour"] // 3 * 3 297 time_blocks = high_values.groupby("time_block").size() 298 total_time_blocks = df_analysis.groupby( 299 df_analysis["hour"] // 3 * 3 300 ).size() 301 time_percentages = (time_blocks / total_time_blocks * 100).round(1) 302 303 # 時間帯ごとのデータを安全に表示 304 available_blocks = set(time_blocks.index) & set(total_time_blocks.index) 305 for block in sorted(available_blocks): 306 print( 307 f"{block:02d}:00-{block + 3:02d}:00: {time_percentages[block]}% ({time_blocks[block]}件/{total_time_blocks[block]}件)" 308 ) 309 310 # 曜日ごとの分析 311 print("\n曜日別分布:") 312 weekday_names = ["月曜", "火曜", "水曜", "木曜", "金曜", "土曜", "日曜"] 313 weekday_counts = high_values.groupby("weekday").size() 314 total_weekdays = df_analysis.groupby("weekday").size() 315 weekday_percentages = (weekday_counts / total_weekdays * 100).round(1) 316 317 # 曜日ごとのデータを安全に表示 318 available_days = set(weekday_counts.index) & set(total_weekdays.index) 319 for day in sorted(available_days): 320 if 0 <= day <= 6: # 有効な曜日インデックスのチェック 321 print( 322 f"{weekday_names[day]}: {weekday_percentages[day]}% ({weekday_counts[day]}件/{total_weekdays[day]}件)" 323 ) 324 325 # 濃度とフラックスそれぞれの分析を実行 326 print("\n=== 上位値の時間帯・曜日分析 ===") 327 analyze_top_values(df, col_ch4_conc) 328 analyze_top_values(df, col_ch4_flux) 329 analyze_top_values(df, col_c2h6_conc) 330 analyze_top_values(df, col_c2h6_flux)
CH4とC2H6の濃度とフラックスの時系列プロットを作成する
Parameters:
df : pd.DataFrame
月別データを含むDataFrame
output_dir : str
出力ディレクトリのパス
output_filename : str
出力ファイル名
col_datetime : str
日付列の名前
col_ch4_conc : str
CH4濃度列の名前
col_ch4_flux : str
CH4フラックス列の名前
col_c2h6_conc : str
C2H6濃度列の名前
col_c2h6_flux : str
C2H6フラックス列の名前
print_summary : bool
解析情報をprintするかどうか
332 def plot_ch4c2h6_timeseries( 333 self, 334 df: pd.DataFrame, 335 output_dir: str, 336 col_ch4_flux: str, 337 col_c2h6_flux: str, 338 output_filename: str = "timeseries_year.png", 339 col_datetime: str = "Date", 340 window_size: int = 24 * 7, # 1週間の移動平均のデフォルト値 341 confidence_interval: float = 0.95, # 95%信頼区間 342 subplot_label_ch4: str | None = "(a)", 343 subplot_label_c2h6: str | None = "(b)", 344 subplot_fontsize: int = 20, 345 show_ci: bool = True, 346 ch4_ylim: tuple[float, float] | None = None, 347 c2h6_ylim: tuple[float, float] | None = None, 348 start_date: str | None = None, # 追加:"YYYY-MM-DD"形式 349 end_date: str | None = None, # 追加:"YYYY-MM-DD"形式 350 figsize: tuple[float, float] = (16, 6), 351 ) -> None: 352 """CH4とC2H6フラックスの時系列変動をプロット 353 354 Parameters: 355 ------ 356 df : pd.DataFrame 357 データフレーム 358 output_dir : str 359 出力ディレクトリのパス 360 col_ch4_flux : str 361 CH4フラックスのカラム名 362 col_c2h6_flux : str 363 C2H6フラックスのカラム名 364 output_filename : str 365 出力ファイル名 366 col_datetime : str 367 日時カラムの名前 368 window_size : int 369 移動平均の窓サイズ 370 confidence_interval : float 371 信頼区間(0-1) 372 subplot_label_ch4 : str | None 373 CH4プロットのラベル 374 subplot_label_c2h6 : str | None 375 C2H6プロットのラベル 376 subplot_fontsize : int 377 サブプロットのフォントサイズ 378 show_ci : bool 379 信頼区間を表示するか 380 ch4_ylim : tuple[float, float] | None 381 CH4のy軸範囲 382 c2h6_ylim : tuple[float, float] | None 383 C2H6のy軸範囲 384 start_date : str | None 385 開始日(YYYY-MM-DD形式) 386 end_date : str | None 387 終了日(YYYY-MM-DD形式) 388 """ 389 # 出力ディレクトリの作成 390 os.makedirs(output_dir, exist_ok=True) 391 output_path: str = os.path.join(output_dir, output_filename) 392 393 # データの準備 394 df = df.copy() 395 if not isinstance(df.index, pd.DatetimeIndex): 396 df[col_datetime] = pd.to_datetime(df[col_datetime]) 397 df.set_index(col_datetime, inplace=True) 398 399 # 日付範囲の処理 400 if start_date is not None: 401 start_dt = pd.to_datetime(start_date) 402 if start_dt < df.index.min(): 403 self.logger.warning( 404 f"指定された開始日{start_date}がデータの開始日{df.index.min():%Y-%m-%d}より前です。" 405 f"データの開始日を使用します。" 406 ) 407 start_dt = df.index.min() 408 else: 409 start_dt = df.index.min() 410 411 if end_date is not None: 412 end_dt = pd.to_datetime(end_date) 413 if end_dt > df.index.max(): 414 self.logger.warning( 415 f"指定された終了日{end_date}がデータの終了日{df.index.max():%Y-%m-%d}より後です。" 416 f"データの終了日を使用します。" 417 ) 418 end_dt = df.index.max() 419 else: 420 end_dt = df.index.max() 421 422 # 指定された期間のデータを抽出 423 mask = (df.index >= start_dt) & (df.index <= end_dt) 424 df = df[mask] 425 426 # CH4とC2H6の移動平均と信頼区間を計算 427 ch4_mean, ch4_lower, ch4_upper = calculate_rolling_stats( 428 df[col_ch4_flux], window_size, confidence_interval 429 ) 430 c2h6_mean, c2h6_lower, c2h6_upper = calculate_rolling_stats( 431 df[col_c2h6_flux], window_size, confidence_interval 432 ) 433 434 # プロットの作成 435 fig, (ax1, ax2) = plt.subplots(1, 2, figsize=figsize) 436 437 # CH4プロット 438 ax1.plot(df.index, ch4_mean, "red", label="CH$_4$") 439 if show_ci: 440 ax1.fill_between(df.index, ch4_lower, ch4_upper, color="red", alpha=0.2) 441 if subplot_label_ch4: 442 ax1.text( 443 0.02, 444 0.98, 445 subplot_label_ch4, 446 transform=ax1.transAxes, 447 va="top", 448 fontsize=subplot_fontsize, 449 ) 450 ax1.set_ylabel("CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)") 451 if ch4_ylim is not None: 452 ax1.set_ylim(ch4_ylim) 453 ax1.grid(True, alpha=0.3) 454 455 # C2H6プロット 456 ax2.plot(df.index, c2h6_mean, "orange", label="C$_2$H$_6$") 457 if show_ci: 458 ax2.fill_between( 459 df.index, c2h6_lower, c2h6_upper, color="orange", alpha=0.2 460 ) 461 if subplot_label_c2h6: 462 ax2.text( 463 0.02, 464 0.98, 465 subplot_label_c2h6, 466 transform=ax2.transAxes, 467 va="top", 468 fontsize=subplot_fontsize, 469 ) 470 ax2.set_ylabel("C$_2$H$_6$ flux (nmol m$^{-2}$ s$^{-1}$)") 471 if c2h6_ylim is not None: 472 ax2.set_ylim(c2h6_ylim) 473 ax2.grid(True, alpha=0.3) 474 475 # x軸の設定 476 for ax in [ax1, ax2]: 477 ax.set_xlabel("Month") 478 # x軸の範囲を設定 479 ax.set_xlim(start_dt, end_dt) 480 481 # 1ヶ月ごとの主目盛り 482 ax.xaxis.set_major_locator(mdates.MonthLocator(interval=1)) 483 484 # カスタムフォーマッタの作成(数字を通常フォントで表示) 485 def date_formatter(x, p): 486 date = mdates.num2date(x) 487 return f"{date.strftime('%m')}" 488 489 ax.xaxis.set_major_formatter(plt.FuncFormatter(date_formatter)) 490 491 # 補助目盛りの設定 492 ax.xaxis.set_minor_locator(mdates.MonthLocator()) 493 # ティックラベルの回転と位置調整 494 plt.setp(ax.xaxis.get_majorticklabels(), ha="right") 495 496 plt.tight_layout() 497 plt.savefig(output_path, dpi=300, bbox_inches="tight") 498 plt.close(fig)
CH4とC2H6フラックスの時系列変動をプロット
Parameters:
df : pd.DataFrame
データフレーム
output_dir : str
出力ディレクトリのパス
col_ch4_flux : str
CH4フラックスのカラム名
col_c2h6_flux : str
C2H6フラックスのカラム名
output_filename : str
出力ファイル名
col_datetime : str
日時カラムの名前
window_size : int
移動平均の窓サイズ
confidence_interval : float
信頼区間(0-1)
subplot_label_ch4 : str | None
CH4プロットのラベル
subplot_label_c2h6 : str | None
C2H6プロットのラベル
subplot_fontsize : int
サブプロットのフォントサイズ
show_ci : bool
信頼区間を表示するか
ch4_ylim : tuple[float, float] | None
CH4のy軸範囲
c2h6_ylim : tuple[float, float] | None
C2H6のy軸範囲
start_date : str | None
開始日(YYYY-MM-DD形式)
end_date : str | None
終了日(YYYY-MM-DD形式)
500 def plot_ch4_flux_comparison( 501 self, 502 df: pd.DataFrame, 503 output_dir: str, 504 col_g2401_flux: str, 505 col_ultra_flux: str, 506 output_filename: str = "ch4_flux_comparison.png", 507 col_datetime: str = "Date", 508 window_size: int = 24 * 7, # 1週間の移動平均のデフォルト値 509 confidence_interval: float = 0.95, # 95%信頼区間 510 subplot_label: str | None = None, 511 subplot_fontsize: int = 20, 512 show_ci: bool = True, 513 y_lim: tuple[float, float] | None = None, 514 start_date: str | None = None, 515 end_date: str | None = None, 516 figsize: tuple[float, float] = (12, 6), 517 legend_loc: str = "upper right", 518 ) -> None: 519 """G2401とUltraによるCH4フラックスの時系列比較プロット 520 521 Parameters: 522 ------ 523 df : pd.DataFrame 524 データフレーム 525 output_dir : str 526 出力ディレクトリのパス 527 col_g2401_flux : str 528 G2401のCH4フラックスのカラム名 529 col_ultra_flux : str 530 UltraのCH4フラックスのカラム名 531 output_filename : str 532 出力ファイル名 533 col_datetime : str 534 日時カラムの名前 535 window_size : int 536 移動平均の窓サイズ 537 confidence_interval : float 538 信頼区間(0-1) 539 subplot_label : str | None 540 プロットのラベル 541 subplot_fontsize : int 542 サブプロットのフォントサイズ 543 show_ci : bool 544 信頼区間を表示するか 545 y_lim : tuple[float, float] | None 546 y軸の範囲 547 start_date : str | None 548 開始日(YYYY-MM-DD形式) 549 end_date : str | None 550 終了日(YYYY-MM-DD形式) 551 figsize : tuple[float, float] 552 図のサイズ 553 legend_loc : str 554 凡例の位置 555 """ 556 # 出力ディレクトリの作成 557 os.makedirs(output_dir, exist_ok=True) 558 output_path: str = os.path.join(output_dir, output_filename) 559 560 # データの準備 561 df = df.copy() 562 if not isinstance(df.index, pd.DatetimeIndex): 563 df[col_datetime] = pd.to_datetime(df[col_datetime]) 564 df.set_index(col_datetime, inplace=True) 565 566 # 日付範囲の処理(既存のコードと同様) 567 if start_date is not None: 568 start_dt = pd.to_datetime(start_date) 569 if start_dt < df.index.min(): 570 self.logger.warning( 571 f"指定された開始日{start_date}がデータの開始日{df.index.min():%Y-%m-%d}より前です。" 572 f"データの開始日を使用します。" 573 ) 574 start_dt = df.index.min() 575 else: 576 start_dt = df.index.min() 577 578 if end_date is not None: 579 end_dt = pd.to_datetime(end_date) 580 if end_dt > df.index.max(): 581 self.logger.warning( 582 f"指定された終了日{end_date}がデータの終了日{df.index.max():%Y-%m-%d}より後です。" 583 f"データの終了日を使用します。" 584 ) 585 end_dt = df.index.max() 586 else: 587 end_dt = df.index.max() 588 589 # 指定された期間のデータを抽出 590 mask = (df.index >= start_dt) & (df.index <= end_dt) 591 df = df[mask] 592 593 # 移動平均の計算(既存の関数を使用) 594 g2401_mean, g2401_lower, g2401_upper = calculate_rolling_stats( 595 df[col_g2401_flux], window_size, confidence_interval 596 ) 597 ultra_mean, ultra_lower, ultra_upper = calculate_rolling_stats( 598 df[col_ultra_flux], window_size, confidence_interval 599 ) 600 601 # プロットの作成 602 fig, ax = plt.subplots(figsize=figsize) 603 604 # G2401データのプロット 605 ax.plot(df.index, g2401_mean, "blue", label="G2401", alpha=0.7) 606 if show_ci: 607 ax.fill_between(df.index, g2401_lower, g2401_upper, color="blue", alpha=0.2) 608 609 # Ultraデータのプロット 610 ax.plot(df.index, ultra_mean, "red", label="Ultra", alpha=0.7) 611 if show_ci: 612 ax.fill_between(df.index, ultra_lower, ultra_upper, color="red", alpha=0.2) 613 614 # プロットの設定 615 if subplot_label: 616 ax.text( 617 0.02, 618 0.98, 619 subplot_label, 620 transform=ax.transAxes, 621 va="top", 622 fontsize=subplot_fontsize, 623 ) 624 625 ax.set_ylabel("CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)") 626 ax.set_xlabel("Month") 627 628 if y_lim is not None: 629 ax.set_ylim(y_lim) 630 631 ax.grid(True, alpha=0.3) 632 ax.legend(loc=legend_loc) 633 634 # x軸の設定 635 ax.set_xlim(start_dt, end_dt) 636 ax.xaxis.set_major_locator(mdates.MonthLocator(interval=1)) 637 638 # カスタムフォーマッタの作成(数字を通常フォントで表示) 639 def date_formatter(x, p): 640 date = mdates.num2date(x) 641 return f"{date.strftime('%m')}" 642 643 ax.xaxis.set_major_formatter(plt.FuncFormatter(date_formatter)) 644 ax.xaxis.set_minor_locator(mdates.MonthLocator()) 645 plt.setp(ax.xaxis.get_majorticklabels(), ha="right") 646 647 plt.tight_layout() 648 plt.savefig(output_path, dpi=300, bbox_inches="tight") 649 plt.close(fig)
G2401とUltraによるCH4フラックスの時系列比較プロット
Parameters:
df : pd.DataFrame
データフレーム
output_dir : str
出力ディレクトリのパス
col_g2401_flux : str
G2401のCH4フラックスのカラム名
col_ultra_flux : str
UltraのCH4フラックスのカラム名
output_filename : str
出力ファイル名
col_datetime : str
日時カラムの名前
window_size : int
移動平均の窓サイズ
confidence_interval : float
信頼区間(0-1)
subplot_label : str | None
プロットのラベル
subplot_fontsize : int
サブプロットのフォントサイズ
show_ci : bool
信頼区間を表示するか
y_lim : tuple[float, float] | None
y軸の範囲
start_date : str | None
開始日(YYYY-MM-DD形式)
end_date : str | None
終了日(YYYY-MM-DD形式)
figsize : tuple[float, float]
図のサイズ
legend_loc : str
凡例の位置
651 def plot_c1c2_fluxes_diurnal_patterns( 652 self, 653 df: pd.DataFrame, 654 y_cols_ch4: list[str], 655 y_cols_c2h6: list[str], 656 labels_ch4: list[str], 657 labels_c2h6: list[str], 658 colors_ch4: list[str], 659 colors_c2h6: list[str], 660 output_dir: str, 661 output_filename: str = "diurnal.png", 662 legend_only_ch4: bool = False, 663 add_label: bool = True, 664 add_legend: bool = True, 665 show_std: bool = False, # 標準偏差表示のオプションを追加 666 std_alpha: float = 0.2, # 標準偏差の透明度 667 subplot_fontsize: int = 20, 668 subplot_label_ch4: str | None = "(a)", 669 subplot_label_c2h6: str | None = "(b)", 670 ax1_ylim: tuple[float, float] | None = None, 671 ax2_ylim: tuple[float, float] | None = None, 672 ) -> None: 673 """CH4とC2H6の日変化パターンを1つの図に並べてプロットする 674 675 Parameters: 676 ------ 677 df : pd.DataFrame 678 入力データフレーム。 679 y_cols_ch4 : list[str] 680 CH4のプロットに使用するカラム名のリスト。 681 y_cols_c2h6 : list[str] 682 C2H6のプロットに使用するカラム名のリスト。 683 labels_ch4 : list[str] 684 CH4の各ラインに対応するラベルのリスト。 685 labels_c2h6 : list[str] 686 C2H6の各ラインに対応するラベルのリスト。 687 colors_ch4 : list[str] 688 CH4の各ラインに使用する色のリスト。 689 colors_c2h6 : list[str] 690 C2H6の各ラインに使用する色のリスト。 691 output_dir : str 692 出力先ディレクトリのパス。 693 output_filename : str, optional 694 出力ファイル名。デフォルトは"diurnal.png"。 695 legend_only_ch4 : bool, optional 696 CH4の凡例のみを表示するかどうか。デフォルトはFalse。 697 add_label : bool, optional 698 サブプロットラベルを表示するかどうか。デフォルトはTrue。 699 add_legend : bool, optional 700 凡例を表示するかどうか。デフォルトはTrue。 701 show_std : bool, optional 702 標準偏差を表示するかどうか。デフォルトはFalse。 703 std_alpha : float, optional 704 標準偏差の透明度。デフォルトは0.2。 705 subplot_fontsize : int, optional 706 サブプロットのフォントサイズ。デフォルトは20。 707 subplot_label_ch4 : str | None, optional 708 CH4プロットのラベル。デフォルトは"(a)"。 709 subplot_label_c2h6 : str | None, optional 710 C2H6プロットのラベル。デフォルトは"(b)"。 711 ax1_ylim : tuple[float, float] | None, optional 712 CH4プロットのy軸の範囲。デフォルトはNone。 713 ax2_ylim : tuple[float, float] | None, optional 714 C2H6プロットのy軸の範囲。デフォルトはNone。 715 """ 716 os.makedirs(output_dir, exist_ok=True) 717 output_path: str = os.path.join(output_dir, output_filename) 718 719 # データの準備 720 target_columns = y_cols_ch4 + y_cols_c2h6 721 hourly_means, time_points = self._prepare_diurnal_data(df, target_columns) 722 723 # 標準偏差の計算を追加 724 hourly_stds = {} 725 if show_std: 726 hourly_stds = df.groupby(df.index.hour)[target_columns].std() 727 # 24時間目のデータ点を追加 728 last_hour = hourly_stds.iloc[0:1].copy() 729 last_hour.index = [24] 730 hourly_stds = pd.concat([hourly_stds, last_hour]) 731 732 # プロットの作成 733 fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5)) 734 735 # CH4のプロット (左側) 736 ch4_lines = [] 737 for y_col, label, color in zip(y_cols_ch4, labels_ch4, colors_ch4): 738 mean_values = hourly_means["all"][y_col] 739 line = ax1.plot( 740 time_points, 741 mean_values, 742 "-o", 743 label=label, 744 color=color, 745 ) 746 ch4_lines.extend(line) 747 748 # 標準偏差の表示 749 if show_std: 750 std_values = hourly_stds[y_col] 751 ax1.fill_between( 752 time_points, 753 mean_values - std_values, 754 mean_values + std_values, 755 color=color, 756 alpha=std_alpha, 757 ) 758 759 # C2H6のプロット (右側) 760 c2h6_lines = [] 761 for y_col, label, color in zip(y_cols_c2h6, labels_c2h6, colors_c2h6): 762 mean_values = hourly_means["all"][y_col] 763 line = ax2.plot( 764 time_points, 765 mean_values, 766 "o-", 767 label=label, 768 color=color, 769 ) 770 c2h6_lines.extend(line) 771 772 # 標準偏差の表示 773 if show_std: 774 std_values = hourly_stds[y_col] 775 ax2.fill_between( 776 time_points, 777 mean_values - std_values, 778 mean_values + std_values, 779 color=color, 780 alpha=std_alpha, 781 ) 782 783 # 軸の設定 784 for ax, ylabel, subplot_label in [ 785 (ax1, r"CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)", subplot_label_ch4), 786 (ax2, r"C$_2$H$_6$ flux (nmol m$^{-2}$ s$^{-1}$)", subplot_label_c2h6), 787 ]: 788 self._setup_diurnal_axes( 789 ax=ax, 790 time_points=time_points, 791 ylabel=ylabel, 792 subplot_label=subplot_label, 793 add_label=add_label, 794 add_legend=False, # 個別の凡例は表示しない 795 subplot_fontsize=subplot_fontsize, 796 ) 797 798 if ax1_ylim is not None: 799 ax1.set_ylim(ax1_ylim) 800 ax1.yaxis.set_major_locator(MultipleLocator(20)) 801 ax1.yaxis.set_major_formatter(FuncFormatter(lambda x, p: f"{x:.0f}")) 802 803 if ax2_ylim is not None: 804 ax2.set_ylim(ax2_ylim) 805 ax2.yaxis.set_major_locator(MultipleLocator(1)) 806 ax2.yaxis.set_major_formatter(FuncFormatter(lambda x, p: f"{x:.1f}")) 807 808 plt.tight_layout() 809 810 # 共通の凡例 811 if add_legend: 812 all_lines = ch4_lines 813 all_labels = [line.get_label() for line in ch4_lines] 814 if not legend_only_ch4: 815 all_lines += c2h6_lines 816 all_labels += [line.get_label() for line in c2h6_lines] 817 fig.legend( 818 all_lines, 819 all_labels, 820 loc="center", 821 bbox_to_anchor=(0.5, 0.02), 822 ncol=len(all_lines), 823 ) 824 plt.subplots_adjust(bottom=0.25) # 下部に凡例用のスペースを確保 825 826 fig.savefig(output_path, dpi=300, bbox_inches="tight") 827 plt.close(fig)
CH4とC2H6の日変化パターンを1つの図に並べてプロットする
Parameters:
df : pd.DataFrame
入力データフレーム。
y_cols_ch4 : list[str]
CH4のプロットに使用するカラム名のリスト。
y_cols_c2h6 : list[str]
C2H6のプロットに使用するカラム名のリスト。
labels_ch4 : list[str]
CH4の各ラインに対応するラベルのリスト。
labels_c2h6 : list[str]
C2H6の各ラインに対応するラベルのリスト。
colors_ch4 : list[str]
CH4の各ラインに使用する色のリスト。
colors_c2h6 : list[str]
C2H6の各ラインに使用する色のリスト。
output_dir : str
出力先ディレクトリのパス。
output_filename : str, optional
出力ファイル名。デフォルトは"diurnal.png"。
legend_only_ch4 : bool, optional
CH4の凡例のみを表示するかどうか。デフォルトはFalse。
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。
829 def plot_c1c2_fluxes_diurnal_patterns_by_date( 830 self, 831 df: pd.DataFrame, 832 y_col_ch4: str, 833 y_col_c2h6: str, 834 output_dir: str, 835 output_filename: str = "diurnal_by_date.png", 836 plot_all: bool = True, 837 plot_weekday: bool = True, 838 plot_weekend: bool = True, 839 plot_holiday: bool = True, 840 add_label: bool = True, 841 add_legend: bool = True, 842 show_std: bool = False, # 標準偏差表示のオプションを追加 843 std_alpha: float = 0.2, # 標準偏差の透明度 844 legend_only_ch4: bool = False, 845 subplot_fontsize: int = 20, 846 subplot_label_ch4: str | None = "(a)", 847 subplot_label_c2h6: str | None = "(b)", 848 ax1_ylim: tuple[float, float] | None = None, 849 ax2_ylim: tuple[float, float] | None = None, 850 print_summary: bool = True, # 追加: 統計情報を表示するかどうか 851 ) -> None: 852 """CH4とC2H6の日変化パターンを日付分類して1つの図に並べてプロットする 853 854 Parameters: 855 ------ 856 df : pd.DataFrame 857 入力データフレーム。 858 y_col_ch4 : str 859 CH4フラックスを含むカラム名。 860 y_col_c2h6 : str 861 C2H6フラックスを含むカラム名。 862 output_dir : str 863 出力先ディレクトリのパス。 864 output_filename : str, optional 865 出力ファイル名。デフォルトは"diurnal_by_date.png"。 866 plot_all : bool, optional 867 すべての日をプロットするかどうか。デフォルトはTrue。 868 plot_weekday : bool, optional 869 平日をプロットするかどうか。デフォルトはTrue。 870 plot_weekend : bool, optional 871 週末をプロットするかどうか。デフォルトはTrue。 872 plot_holiday : bool, optional 873 祝日をプロットするかどうか。デフォルトはTrue。 874 add_label : bool, optional 875 サブプロットラベルを表示するかどうか。デフォルトはTrue。 876 add_legend : bool, optional 877 凡例を表示するかどうか。デフォルトはTrue。 878 show_std : bool, optional 879 標準偏差を表示するかどうか。デフォルトはFalse。 880 std_alpha : float, optional 881 標準偏差の透明度。デフォルトは0.2。 882 legend_only_ch4 : bool, optional 883 CH4の凡例のみを表示するかどうか。デフォルトはFalse。 884 subplot_fontsize : int, optional 885 サブプロットのフォントサイズ。デフォルトは20。 886 subplot_label_ch4 : str | None, optional 887 CH4プロットのラベル。デフォルトは"(a)"。 888 subplot_label_c2h6 : str | None, optional 889 C2H6プロットのラベル。デフォルトは"(b)"。 890 ax1_ylim : tuple[float, float] | None, optional 891 CH4プロットのy軸の範囲。デフォルトはNone。 892 ax2_ylim : tuple[float, float] | None, optional 893 C2H6プロットのy軸の範囲。デフォルトはNone。 894 print_summary : bool, optional 895 統計情報を表示するかどうか。デフォルトはTrue。 896 """ 897 os.makedirs(output_dir, exist_ok=True) 898 output_path: str = os.path.join(output_dir, output_filename) 899 900 # データの準備 901 target_columns = [y_col_ch4, y_col_c2h6] 902 hourly_means, time_points = self._prepare_diurnal_data( 903 df, target_columns, include_date_types=True 904 ) 905 906 # 標準偏差の計算を追加 907 hourly_stds = {} 908 if show_std: 909 for condition in ["all", "weekday", "weekend", "holiday"]: 910 if condition == "all": 911 condition_data = df 912 elif condition == "weekday": 913 condition_data = df[ 914 ~( 915 df.index.dayofweek.isin([5, 6]) 916 | df.index.map(lambda x: jpholiday.is_holiday(x.date())) 917 ) 918 ] 919 elif condition == "weekend": 920 condition_data = df[df.index.dayofweek.isin([5, 6])] 921 else: # holiday 922 condition_data = df[ 923 df.index.map(lambda x: jpholiday.is_holiday(x.date())) 924 ] 925 926 hourly_stds[condition] = condition_data.groupby( 927 condition_data.index.hour 928 )[target_columns].std() 929 # 24時間目のデータ点を追加 930 last_hour = hourly_stds[condition].iloc[0:1].copy() 931 last_hour.index = [24] 932 hourly_stds[condition] = pd.concat([hourly_stds[condition], last_hour]) 933 934 # プロットスタイルの設定 935 styles = { 936 "all": { 937 "color": "black", 938 "linestyle": "-", 939 "alpha": 1.0, 940 "label": "All days", 941 }, 942 "weekday": { 943 "color": "blue", 944 "linestyle": "-", 945 "alpha": 0.8, 946 "label": "Weekdays", 947 }, 948 "weekend": { 949 "color": "red", 950 "linestyle": "-", 951 "alpha": 0.8, 952 "label": "Weekends", 953 }, 954 "holiday": { 955 "color": "green", 956 "linestyle": "-", 957 "alpha": 0.8, 958 "label": "Weekends & Holidays", 959 }, 960 } 961 962 # プロット対象の条件を選択 963 plot_conditions = { 964 "all": plot_all, 965 "weekday": plot_weekday, 966 "weekend": plot_weekend, 967 "holiday": plot_holiday, 968 } 969 selected_conditions = { 970 col: means 971 for col, means in hourly_means.items() 972 if col in plot_conditions and plot_conditions[col] 973 } 974 975 # プロットの作成 976 fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5)) 977 978 # CH4とC2H6のプロット用のラインオブジェクトを保存 979 ch4_lines = [] 980 c2h6_lines = [] 981 982 # CH4とC2H6のプロット 983 for condition, means in selected_conditions.items(): 984 style = styles[condition].copy() 985 986 # CH4プロット 987 mean_values_ch4 = means[y_col_ch4] 988 line_ch4 = ax1.plot(time_points, mean_values_ch4, marker="o", **style) 989 ch4_lines.extend(line_ch4) 990 991 if show_std and condition in hourly_stds: 992 std_values = hourly_stds[condition][y_col_ch4] 993 ax1.fill_between( 994 time_points, 995 mean_values_ch4 - std_values, 996 mean_values_ch4 + std_values, 997 color=style["color"], 998 alpha=std_alpha, 999 ) 1000 1001 # C2H6プロット 1002 style["linestyle"] = "--" 1003 mean_values_c2h6 = means[y_col_c2h6] 1004 line_c2h6 = ax2.plot(time_points, mean_values_c2h6, marker="o", **style) 1005 c2h6_lines.extend(line_c2h6) 1006 1007 if show_std and condition in hourly_stds: 1008 std_values = hourly_stds[condition][y_col_c2h6] 1009 ax2.fill_between( 1010 time_points, 1011 mean_values_c2h6 - std_values, 1012 mean_values_c2h6 + std_values, 1013 color=style["color"], 1014 alpha=std_alpha, 1015 ) 1016 1017 # 軸の設定 1018 for ax, ylabel, subplot_label in [ 1019 (ax1, r"CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)", subplot_label_ch4), 1020 (ax2, r"C$_2$H$_6$ flux (nmol m$^{-2}$ s$^{-1}$)", subplot_label_c2h6), 1021 ]: 1022 self._setup_diurnal_axes( 1023 ax=ax, 1024 time_points=time_points, 1025 ylabel=ylabel, 1026 subplot_label=subplot_label, 1027 add_label=add_label, 1028 add_legend=False, 1029 subplot_fontsize=subplot_fontsize, 1030 ) 1031 1032 if ax1_ylim is not None: 1033 ax1.set_ylim(ax1_ylim) 1034 ax1.yaxis.set_major_locator(MultipleLocator(20)) 1035 ax1.yaxis.set_major_formatter(FuncFormatter(lambda x, p: f"{x:.0f}")) 1036 1037 if ax2_ylim is not None: 1038 ax2.set_ylim(ax2_ylim) 1039 ax2.yaxis.set_major_locator(MultipleLocator(1)) 1040 ax2.yaxis.set_major_formatter(FuncFormatter(lambda x, p: f"{x:.1f}")) 1041 1042 plt.tight_layout() 1043 1044 # 共通の凡例を図の下部に配置 1045 if add_legend: 1046 lines_to_show = ( 1047 ch4_lines if legend_only_ch4 else ch4_lines[: len(selected_conditions)] 1048 ) 1049 fig.legend( 1050 lines_to_show, 1051 [ 1052 style["label"] 1053 for style in list(styles.values())[: len(lines_to_show)] 1054 ], 1055 loc="center", 1056 bbox_to_anchor=(0.5, 0.02), 1057 ncol=len(lines_to_show), 1058 ) 1059 plt.subplots_adjust(bottom=0.25) # 下部に凡例用のスペースを確保 1060 1061 fig.savefig(output_path, dpi=300, bbox_inches="tight") 1062 plt.close(fig) 1063 1064 # 日変化パターンの統計分析を追加 1065 if print_summary: 1066 # 平日と休日のデータを準備 1067 dates = pd.to_datetime(df.index) 1068 is_weekend = dates.dayofweek.isin([5, 6]) 1069 is_holiday = dates.map(lambda x: jpholiday.is_holiday(x.date())) 1070 is_weekday = ~(is_weekend | is_holiday) 1071 1072 weekday_data = df[is_weekday] 1073 holiday_data = df[is_weekend | is_holiday] 1074 1075 def get_diurnal_stats(data, column): 1076 # 時間ごとの平均値を計算 1077 hourly_means = data.groupby(data.index.hour)[column].mean() 1078 1079 # 8-16時の時間帯の統計 1080 daytime_means = hourly_means[ 1081 (hourly_means.index >= 8) & (hourly_means.index <= 16) 1082 ] 1083 1084 if len(daytime_means) == 0: 1085 return None 1086 1087 return { 1088 "mean": daytime_means.mean(), 1089 "max": daytime_means.max(), 1090 "max_hour": daytime_means.idxmax(), 1091 "min": daytime_means.min(), 1092 "min_hour": daytime_means.idxmin(), 1093 "hours_count": len(daytime_means), 1094 } 1095 1096 # CH4とC2H6それぞれの統計を計算 1097 for col, gas_name in [(y_col_ch4, "CH4"), (y_col_c2h6, "C2H6")]: 1098 print(f"\n=== {gas_name} フラックス 8-16時の統計分析 ===") 1099 1100 weekday_stats = get_diurnal_stats(weekday_data, col) 1101 holiday_stats = get_diurnal_stats(holiday_data, col) 1102 1103 if weekday_stats and holiday_stats: 1104 print("\n平日:") 1105 print(f" 平均値: {weekday_stats['mean']:.2f}") 1106 print( 1107 f" 最大値: {weekday_stats['max']:.2f} ({weekday_stats['max_hour']}時)" 1108 ) 1109 print( 1110 f" 最小値: {weekday_stats['min']:.2f} ({weekday_stats['min_hour']}時)" 1111 ) 1112 print(f" 集計時間数: {weekday_stats['hours_count']}") 1113 1114 print("\n休日:") 1115 print(f" 平均値: {holiday_stats['mean']:.2f}") 1116 print( 1117 f" 最大値: {holiday_stats['max']:.2f} ({holiday_stats['max_hour']}時)" 1118 ) 1119 print( 1120 f" 最小値: {holiday_stats['min']:.2f} ({holiday_stats['min_hour']}時)" 1121 ) 1122 print(f" 集計時間数: {holiday_stats['hours_count']}") 1123 1124 # 平日/休日の比率を計算 1125 print("\n平日/休日の比率:") 1126 print( 1127 f" 平均値比: {weekday_stats['mean'] / holiday_stats['mean']:.2f}" 1128 ) 1129 print( 1130 f" 最大値比: {weekday_stats['max'] / holiday_stats['max']:.2f}" 1131 ) 1132 print( 1133 f" 最小値比: {weekday_stats['min'] / holiday_stats['min']:.2f}" 1134 ) 1135 else: 1136 print("十分なデータがありません")
CH4とC2H6の日変化パターンを日付分類して1つの図に並べてプロットする
Parameters:
df : pd.DataFrame
入力データフレーム。
y_col_ch4 : str
CH4フラックスを含むカラム名。
y_col_c2h6 : str
C2H6フラックスを含むカラム名。
output_dir : str
出力先ディレクトリのパス。
output_filename : str, optional
出力ファイル名。デフォルトは"diurnal_by_date.png"。
plot_all : bool, optional
すべての日をプロットするかどうか。デフォルトはTrue。
plot_weekday : bool, optional
平日をプロットするかどうか。デフォルトはTrue。
plot_weekend : bool, optional
週末をプロットするかどうか。デフォルトはTrue。
plot_holiday : bool, optional
祝日をプロットするかどうか。デフォルトはTrue。
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。
1138 def plot_diurnal_concentrations( 1139 self, 1140 df: pd.DataFrame, 1141 output_dir: str, 1142 col_ch4_conc: str = "CH4_ultra_cal", 1143 col_c2h6_conc: str = "C2H6_ultra_cal", 1144 col_datetime: str = "Date", 1145 output_filename: str = "diurnal_concentrations.png", 1146 show_std: bool = True, 1147 alpha_std: float = 0.2, 1148 add_legend: bool = True, # 凡例表示のオプションを追加 1149 print_summary: bool = True, 1150 subplot_label_ch4: str | None = None, 1151 subplot_label_c2h6: str | None = None, 1152 subplot_fontsize: int = 24, 1153 ch4_ylim: tuple[float, float] | None = None, 1154 c2h6_ylim: tuple[float, float] | None = None, 1155 interval: str = "1H", # "30min" または "1H" を指定 1156 ) -> None: 1157 """CH4とC2H6の濃度の日内変動を描画する 1158 1159 Parameters: 1160 ------ 1161 df : pd.DataFrame 1162 濃度データを含むDataFrame 1163 output_dir : str 1164 出力ディレクトリのパス 1165 col_ch4_conc : str 1166 CH4濃度のカラム名 1167 col_c2h6_conc : str 1168 C2H6濃度のカラム名 1169 col_datetime : str 1170 日時カラム名 1171 output_filename : str 1172 出力ファイル名 1173 show_std : bool 1174 標準偏差を表示するかどうか 1175 alpha_std : float 1176 標準偏差の透明度 1177 add_legend : bool 1178 凡例を追加するかどうか 1179 print_summary : bool 1180 統計情報を表示するかどうか 1181 subplot_label_ch4 : str | None 1182 CH4プロットのラベル 1183 subplot_label_c2h6 : str | None 1184 C2H6プロットのラベル 1185 subplot_fontsize : int 1186 サブプロットのフォントサイズ 1187 ch4_ylim : tuple[float, float] | None 1188 CH4のy軸範囲 1189 c2h6_ylim : tuple[float, float] | None 1190 C2H6のy軸範囲 1191 interval : str 1192 時間間隔。"30min"または"1H"を指定 1193 """ 1194 # 出力ディレクトリの作成 1195 os.makedirs(output_dir, exist_ok=True) 1196 output_path: str = os.path.join(output_dir, output_filename) 1197 1198 # データの準備 1199 df = df.copy() 1200 if interval == "30min": 1201 # 30分間隔の場合、時間と30分を別々に取得 1202 df["hour"] = pd.to_datetime(df[col_datetime]).dt.hour 1203 df["minute"] = pd.to_datetime(df[col_datetime]).dt.minute 1204 df["time_bin"] = df["hour"] + df["minute"].map({0: 0, 30: 0.5}) 1205 else: 1206 # 1時間間隔の場合 1207 df["time_bin"] = pd.to_datetime(df[col_datetime]).dt.hour 1208 1209 # 時間ごとの平均値と標準偏差を計算 1210 hourly_stats = df.groupby("time_bin")[[col_ch4_conc, col_c2h6_conc]].agg( 1211 ["mean", "std"] 1212 ) 1213 1214 # 最後のデータポイントを追加(最初のデータを使用) 1215 last_point = hourly_stats.iloc[0:1].copy() 1216 last_point.index = [ 1217 hourly_stats.index[-1] + (0.5 if interval == "30min" else 1) 1218 ] 1219 hourly_stats = pd.concat([hourly_stats, last_point]) 1220 1221 # 時間軸の作成 1222 if interval == "30min": 1223 time_points = pd.date_range("2024-01-01", periods=49, freq="30min") 1224 x_ticks = [0, 6, 12, 18, 24] # 主要な時間のティック 1225 else: 1226 time_points = pd.date_range("2024-01-01", periods=25, freq="1H") 1227 x_ticks = [0, 6, 12, 18, 24] 1228 1229 # プロットの作成 1230 fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5)) 1231 1232 # CH4濃度プロット 1233 mean_ch4 = hourly_stats[col_ch4_conc]["mean"] 1234 if show_std: 1235 std_ch4 = hourly_stats[col_ch4_conc]["std"] 1236 ax1.fill_between( 1237 time_points, 1238 mean_ch4 - std_ch4, 1239 mean_ch4 + std_ch4, 1240 color="red", 1241 alpha=alpha_std, 1242 ) 1243 ch4_line = ax1.plot(time_points, mean_ch4, "red", label="CH$_4$")[0] 1244 1245 ax1.set_ylabel("CH$_4$ (ppm)") 1246 if ch4_ylim is not None: 1247 ax1.set_ylim(ch4_ylim) 1248 if subplot_label_ch4: 1249 ax1.text( 1250 0.02, 1251 0.98, 1252 subplot_label_ch4, 1253 transform=ax1.transAxes, 1254 va="top", 1255 fontsize=subplot_fontsize, 1256 ) 1257 1258 # C2H6濃度プロット 1259 mean_c2h6 = hourly_stats[col_c2h6_conc]["mean"] 1260 if show_std: 1261 std_c2h6 = hourly_stats[col_c2h6_conc]["std"] 1262 ax2.fill_between( 1263 time_points, 1264 mean_c2h6 - std_c2h6, 1265 mean_c2h6 + std_c2h6, 1266 color="orange", 1267 alpha=alpha_std, 1268 ) 1269 c2h6_line = ax2.plot(time_points, mean_c2h6, "orange", label="C$_2$H$_6$")[0] 1270 1271 ax2.set_ylabel("C$_2$H$_6$ (ppb)") 1272 if c2h6_ylim is not None: 1273 ax2.set_ylim(c2h6_ylim) 1274 if subplot_label_c2h6: 1275 ax2.text( 1276 0.02, 1277 0.98, 1278 subplot_label_c2h6, 1279 transform=ax2.transAxes, 1280 va="top", 1281 fontsize=subplot_fontsize, 1282 ) 1283 1284 # 両プロットの共通設定 1285 for ax in [ax1, ax2]: 1286 ax.set_xlabel("Time (hour)") 1287 ax.xaxis.set_major_formatter(mdates.DateFormatter("%-H")) 1288 ax.xaxis.set_major_locator(mdates.HourLocator(byhour=x_ticks)) 1289 ax.set_xlim(time_points[0], time_points[-1]) 1290 # 1時間ごとの縦線を表示 1291 ax.grid(True, which="major", alpha=0.3) 1292 # 補助目盛りは表示するが、グリッド線は表示しない 1293 # if interval == "30min": 1294 # ax.xaxis.set_minor_locator(mdates.MinuteLocator(byminute=[30])) 1295 # ax.tick_params(which='minor', length=4) 1296 1297 # 共通の凡例を図の下部に配置 1298 if add_legend: 1299 fig.legend( 1300 [ch4_line, c2h6_line], 1301 ["CH$_4$", "C$_2$H$_6$"], 1302 loc="center", 1303 bbox_to_anchor=(0.5, 0.02), 1304 ncol=2, 1305 ) 1306 plt.subplots_adjust(bottom=0.2) 1307 1308 plt.tight_layout() 1309 plt.savefig(output_path, dpi=300, bbox_inches="tight") 1310 plt.close(fig) 1311 1312 if print_summary: 1313 # 統計情報の表示 1314 for name, col in [("CH4", col_ch4_conc), ("C2H6", col_c2h6_conc)]: 1315 stats = hourly_stats[col] 1316 mean_vals = stats["mean"] 1317 1318 print(f"\n{name}濃度の日内変動統計:") 1319 print(f"最小値: {mean_vals.min():.3f} (Hour: {mean_vals.idxmin()})") 1320 print(f"最大値: {mean_vals.max():.3f} (Hour: {mean_vals.idxmax()})") 1321 print(f"平均値: {mean_vals.mean():.3f}") 1322 print(f"日内変動幅: {mean_vals.max() - mean_vals.min():.3f}") 1323 print(f"最大/最小比: {mean_vals.max() / mean_vals.min():.3f}")
CH4とC2H6の濃度の日内変動を描画する
Parameters:
df : pd.DataFrame
濃度データを含むDataFrame
output_dir : str
出力ディレクトリのパス
col_ch4_conc : str
CH4濃度のカラム名
col_c2h6_conc : str
C2H6濃度のカラム名
col_datetime : str
日時カラム名
output_filename : str
出力ファイル名
show_std : bool
標準偏差を表示するかどうか
alpha_std : float
標準偏差の透明度
add_legend : bool
凡例を追加するかどうか
print_summary : bool
統計情報を表示するかどうか
subplot_label_ch4 : str | None
CH4プロットのラベル
subplot_label_c2h6 : str | None
C2H6プロットのラベル
subplot_fontsize : int
サブプロットのフォントサイズ
ch4_ylim : tuple[float, float] | None
CH4のy軸範囲
c2h6_ylim : tuple[float, float] | None
C2H6のy軸範囲
interval : str
時間間隔。"30min"または"1H"を指定
1325 def plot_flux_diurnal_patterns_with_std( 1326 self, 1327 df: pd.DataFrame, 1328 output_dir: str, 1329 col_ch4_flux: str = "Fch4", 1330 col_c2h6_flux: str = "Fc2h6", 1331 ch4_label: str = r"$\mathregular{CH_{4}}$フラックス", 1332 c2h6_label: str = r"$\mathregular{C_{2}H_{6}}$フラックス", 1333 col_datetime: str = "Date", 1334 output_filename: str = "diurnal_patterns.png", 1335 window_size: int = 6, # 移動平均の窓サイズ 1336 show_std: bool = True, # 標準偏差の表示有無 1337 alpha_std: float = 0.1, # 標準偏差の透明度 1338 ) -> None: 1339 """CH4とC2H6フラックスの日変化パターンをプロットする 1340 1341 Parameters: 1342 ------ 1343 df : pd.DataFrame 1344 データフレーム 1345 output_dir : str 1346 出力ディレクトリのパス 1347 col_ch4_flux : str 1348 CH4フラックスのカラム名 1349 col_c2h6_flux : str 1350 C2H6フラックスのカラム名 1351 ch4_label : str 1352 CH4フラックスのラベル 1353 c2h6_label : str 1354 C2H6フラックスのラベル 1355 col_datetime : str 1356 日時カラムの名前 1357 output_filename : str 1358 出力ファイル名 1359 window_size : int 1360 移動平均の窓サイズ(デフォルト6) 1361 show_std : bool 1362 標準偏差を表示するかどうか 1363 alpha_std : float 1364 標準偏差の透明度(0-1) 1365 """ 1366 # 出力ディレクトリの作成 1367 os.makedirs(output_dir, exist_ok=True) 1368 output_path: str = os.path.join(output_dir, output_filename) 1369 1370 # # プロットのスタイル設定 1371 # plt.rcParams.update({ 1372 # 'font.size': 20, 1373 # 'axes.labelsize': 20, 1374 # 'axes.titlesize': 20, 1375 # 'xtick.labelsize': 20, 1376 # 'ytick.labelsize': 20, 1377 # 'legend.fontsize': 20, 1378 # }) 1379 1380 # 日時インデックスの処理 1381 df = df.copy() 1382 if not isinstance(df.index, pd.DatetimeIndex): 1383 df[col_datetime] = pd.to_datetime(df[col_datetime]) 1384 df.set_index(col_datetime, inplace=True) 1385 1386 # 時刻データの抽出とグループ化 1387 df["hour"] = df.index.hour 1388 hourly_means = df.groupby("hour")[[col_ch4_flux, col_c2h6_flux]].agg( 1389 ["mean", "std"] 1390 ) 1391 1392 # 24時間目のデータ点を追加(0時のデータを使用) 1393 last_hour = hourly_means.iloc[0:1].copy() 1394 last_hour.index = [24] 1395 hourly_means = pd.concat([hourly_means, last_hour]) 1396 1397 # 24時間分のデータポイントを作成 1398 time_points = pd.date_range("2024-01-01", periods=25, freq="h") 1399 1400 # プロットの作成 1401 fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5)) 1402 1403 # 移動平均の計算と描画 1404 ch4_mean = ( 1405 hourly_means[(col_ch4_flux, "mean")] 1406 .rolling(window=window_size, center=True, min_periods=1) 1407 .mean() 1408 ) 1409 c2h6_mean = ( 1410 hourly_means[(col_c2h6_flux, "mean")] 1411 .rolling(window=window_size, center=True, min_periods=1) 1412 .mean() 1413 ) 1414 1415 if show_std: 1416 ch4_std = ( 1417 hourly_means[(col_ch4_flux, "std")] 1418 .rolling(window=window_size, center=True, min_periods=1) 1419 .mean() 1420 ) 1421 c2h6_std = ( 1422 hourly_means[(col_c2h6_flux, "std")] 1423 .rolling(window=window_size, center=True, min_periods=1) 1424 .mean() 1425 ) 1426 1427 ax1.fill_between( 1428 time_points, 1429 ch4_mean - ch4_std, 1430 ch4_mean + ch4_std, 1431 color="blue", 1432 alpha=alpha_std, 1433 ) 1434 ax2.fill_between( 1435 time_points, 1436 c2h6_mean - c2h6_std, 1437 c2h6_mean + c2h6_std, 1438 color="red", 1439 alpha=alpha_std, 1440 ) 1441 1442 # メインのラインプロット 1443 ax1.plot(time_points, ch4_mean, "blue", label=ch4_label) 1444 ax2.plot(time_points, c2h6_mean, "red", label=c2h6_label) 1445 1446 # 軸の設定 1447 for ax, ylabel in [ 1448 (ax1, r"CH$_4$ (nmol m$^{-2}$ s$^{-1}$)"), 1449 (ax2, r"C$_2$H$_6$ (nmol m$^{-2}$ s$^{-1}$)"), 1450 ]: 1451 ax.set_xlabel("Time") 1452 ax.set_ylabel(ylabel) 1453 ax.xaxis.set_major_formatter(mdates.DateFormatter("%-H")) 1454 ax.xaxis.set_major_locator(mdates.HourLocator(byhour=[0, 6, 12, 18, 24])) 1455 ax.set_xlim(time_points[0], time_points[-1]) 1456 ax.grid(True, alpha=0.3) 1457 ax.legend() 1458 1459 # グラフの保存 1460 plt.tight_layout() 1461 plt.savefig(output_path, dpi=300, bbox_inches="tight") 1462 plt.close() 1463 1464 # 統計情報の表示(オプション) 1465 for col, name in [(col_ch4_flux, "CH4"), (col_c2h6_flux, "C2H6")]: 1466 mean_val = hourly_means[(col, "mean")].mean() 1467 min_val = hourly_means[(col, "mean")].min() 1468 max_val = hourly_means[(col, "mean")].max() 1469 min_time = hourly_means[(col, "mean")].idxmin() 1470 max_time = hourly_means[(col, "mean")].idxmax() 1471 1472 self.logger.info(f"{name} Statistics:") 1473 self.logger.info(f"Mean: {mean_val:.2f}") 1474 self.logger.info(f"Min: {min_val:.2f} (Hour: {min_time})") 1475 self.logger.info(f"Max: {max_val:.2f} (Hour: {max_time})") 1476 self.logger.info(f"Max/Min ratio: {max_val / min_val:.2f}\n")
CH4とC2H6フラックスの日変化パターンをプロットする
Parameters:
df : pd.DataFrame
データフレーム
output_dir : str
出力ディレクトリのパス
col_ch4_flux : str
CH4フラックスのカラム名
col_c2h6_flux : str
C2H6フラックスのカラム名
ch4_label : str
CH4フラックスのラベル
c2h6_label : str
C2H6フラックスのラベル
col_datetime : str
日時カラムの名前
output_filename : str
出力ファイル名
window_size : int
移動平均の窓サイズ(デフォルト6)
show_std : bool
標準偏差を表示するかどうか
alpha_std : float
標準偏差の透明度(0-1)
1478 def plot_scatter( 1479 self, 1480 df: pd.DataFrame, 1481 x_col: str, 1482 y_col: str, 1483 output_dir: str, 1484 output_filename: str = "scatter.png", 1485 xlabel: str | None = None, 1486 ylabel: str | None = None, 1487 add_label: bool = True, 1488 x_axis_range: tuple | None = None, 1489 y_axis_range: tuple | None = None, 1490 fixed_slope: float = 0.076, 1491 show_fixed_slope: bool = False, 1492 x_scientific: bool = False, # 追加:x軸を指数表記にするかどうか 1493 y_scientific: bool = False, # 追加:y軸を指数表記にするかどうか 1494 ) -> None: 1495 """散布図を作成し、TLS回帰直線を描画します。 1496 1497 Parameters: 1498 ------ 1499 df : pd.DataFrame 1500 プロットに使用するデータフレーム 1501 x_col : str 1502 x軸に使用する列名 1503 y_col : str 1504 y軸に使用する列名 1505 xlabel : str 1506 x軸のラベル 1507 ylabel : str 1508 y軸のラベル 1509 output_dir : str 1510 出力先ディレクトリ 1511 output_filename : str, optional 1512 出力ファイル名。デフォルトは"scatter.png" 1513 add_label : bool, optional 1514 軸ラベルを表示するかどうか。デフォルトはTrue 1515 x_axis_range : tuple, optional 1516 x軸の範囲。デフォルトはNone。 1517 y_axis_range : tuple, optional 1518 y軸の範囲。デフォルトはNone。 1519 fixed_slope : float, optional 1520 固定傾きを指定するための値。デフォルトは0.076 1521 show_fixed_slope : bool, optional 1522 固定傾きの線を表示するかどうか。デフォルトはFalse 1523 """ 1524 os.makedirs(output_dir, exist_ok=True) 1525 output_path: str = os.path.join(output_dir, output_filename) 1526 1527 # 有効なデータの抽出 1528 df = MonthlyFiguresGenerator.get_valid_data(df, x_col, y_col) 1529 1530 # データの準備 1531 x = df[x_col].values 1532 y = df[y_col].values 1533 1534 # データの中心化 1535 x_mean = np.mean(x) 1536 y_mean = np.mean(y) 1537 x_c = x - x_mean 1538 y_c = y - y_mean 1539 1540 # TLS回帰の計算 1541 data_matrix = np.vstack((x_c, y_c)) 1542 cov_matrix = np.cov(data_matrix) 1543 _, eigenvecs = linalg.eigh(cov_matrix) 1544 largest_eigenvec = eigenvecs[:, -1] 1545 1546 slope = largest_eigenvec[1] / largest_eigenvec[0] 1547 intercept = y_mean - slope * x_mean 1548 1549 # R²とRMSEの計算 1550 y_pred = slope * x + intercept 1551 r_squared = 1 - np.sum((y - y_pred) ** 2) / np.sum((y - np.mean(y)) ** 2) 1552 rmse = np.sqrt(np.mean((y - y_pred) ** 2)) 1553 1554 # プロットの作成 1555 fig, ax = plt.subplots(figsize=(6, 6)) 1556 1557 # データ点のプロット 1558 ax.scatter(x, y, color="black") 1559 1560 # データの範囲を取得 1561 if x_axis_range is None: 1562 x_axis_range = (df[x_col].min(), df[x_col].max()) 1563 if y_axis_range is None: 1564 y_axis_range = (df[y_col].min(), df[y_col].max()) 1565 1566 # 回帰直線のプロット 1567 x_range = np.linspace(x_axis_range[0], x_axis_range[1], 150) 1568 y_range = slope * x_range + intercept 1569 ax.plot(x_range, y_range, "r", label="TLS regression") 1570 1571 # 傾き固定の線を追加(フラグがTrueの場合) 1572 if show_fixed_slope: 1573 fixed_intercept = ( 1574 y_mean - fixed_slope * x_mean 1575 ) # 中心点を通るように切片を計算 1576 y_fixed = fixed_slope * x_range + fixed_intercept 1577 ax.plot(x_range, y_fixed, "b--", label=f"Slope = {fixed_slope}", alpha=0.7) 1578 1579 # 軸の設定 1580 ax.set_xlim(x_axis_range) 1581 ax.set_ylim(y_axis_range) 1582 1583 # 指数表記の設定 1584 if x_scientific: 1585 ax.ticklabel_format(style="sci", axis="x", scilimits=(0, 0)) 1586 ax.xaxis.get_offset_text().set_position((1.1, 0)) # 指数の位置調整 1587 if y_scientific: 1588 ax.ticklabel_format(style="sci", axis="y", scilimits=(0, 0)) 1589 ax.yaxis.get_offset_text().set_position((0, 1.1)) # 指数の位置調整 1590 1591 if add_label: 1592 if xlabel is not None: 1593 ax.set_xlabel(xlabel) 1594 if ylabel is not None: 1595 ax.set_ylabel(ylabel) 1596 1597 # 1:1の関係を示す点線(軸の範囲が同じ場合のみ表示) 1598 if ( 1599 x_axis_range is not None 1600 and y_axis_range is not None 1601 and x_axis_range == y_axis_range 1602 ): 1603 ax.plot( 1604 [x_axis_range[0], x_axis_range[1]], 1605 [x_axis_range[0], x_axis_range[1]], 1606 "k--", 1607 alpha=0.5, 1608 ) 1609 1610 # 回帰情報の表示 1611 equation = ( 1612 f"y = {slope:.2f}x {'+' if intercept >= 0 else '-'} {abs(intercept):.2f}" 1613 ) 1614 position_x = 0.05 1615 fig_ha: str = "left" 1616 ax.text( 1617 position_x, 1618 0.95, 1619 equation, 1620 transform=ax.transAxes, 1621 va="top", 1622 ha=fig_ha, 1623 color="red", 1624 ) 1625 ax.text( 1626 position_x, 1627 0.88, 1628 f"R² = {r_squared:.2f}", 1629 transform=ax.transAxes, 1630 va="top", 1631 ha=fig_ha, 1632 color="red", 1633 ) 1634 ax.text( 1635 position_x, 1636 0.81, # RMSEのための新しい位置 1637 f"RMSE = {rmse:.2f}", 1638 transform=ax.transAxes, 1639 va="top", 1640 ha=fig_ha, 1641 color="red", 1642 ) 1643 # 目盛り線の設定 1644 ax.grid(True, alpha=0.3) 1645 1646 fig.savefig(output_path, dpi=300, bbox_inches="tight") 1647 plt.close(fig)
散布図を作成し、TLS回帰直線を描画します。
Parameters:
df : pd.DataFrame
プロットに使用するデータフレーム
x_col : str
x軸に使用する列名
y_col : str
y軸に使用する列名
xlabel : str
x軸のラベル
ylabel : str
y軸のラベル
output_dir : str
出力先ディレクトリ
output_filename : str, optional
出力ファイル名。デフォルトは"scatter.png"
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
1649 def plot_source_contributions_diurnal( 1650 self, 1651 df: pd.DataFrame, 1652 output_dir: str, 1653 col_ch4_flux: str, 1654 col_c2h6_flux: str, 1655 label_gas: str = "gas", 1656 label_bio: str = "bio", 1657 col_datetime: str = "Date", 1658 output_filename: str = "source_contributions.png", 1659 window_size: int = 6, # 移動平均の窓サイズ 1660 print_summary: bool = True, # 統計情報を表示するかどうか, 1661 add_legend: bool = False, 1662 smooth: bool = False, 1663 y_max: float = 100, # y軸の上限値を追加 1664 subplot_label: str | None = None, 1665 subplot_fontsize: int = 20, 1666 ) -> None: 1667 """CH4フラックスの都市ガス起源と生物起源の日変化を積み上げグラフとして表示 1668 1669 Parameters: 1670 ------ 1671 df : pd.DataFrame 1672 データフレーム 1673 output_dir : str 1674 出力ディレクトリのパス 1675 col_ch4_flux : str 1676 CH4フラックスのカラム名 1677 col_c2h6_flux : str 1678 C2H6フラックスのカラム名 1679 label_gas : str 1680 都市ガス起源のラベル 1681 label_bio : str 1682 生物起源のラベル 1683 col_datetime : str 1684 日時カラムの名前 1685 output_filename : str 1686 出力ファイル名 1687 window_size : int 1688 移動平均の窓サイズ 1689 print_summary : bool 1690 統計情報を表示するかどうか 1691 smooth : bool 1692 移動平均を適用するかどうか 1693 y_max : float 1694 y軸の上限値(デフォルト: 100) 1695 """ 1696 # 出力ディレクトリの作成 1697 os.makedirs(output_dir, exist_ok=True) 1698 output_path: str = os.path.join(output_dir, output_filename) 1699 1700 # 起源の計算 1701 df_with_sources = self._calculate_source_contributions( 1702 df=df, 1703 col_ch4_flux=col_ch4_flux, 1704 col_c2h6_flux=col_c2h6_flux, 1705 col_datetime=col_datetime, 1706 ) 1707 1708 # 時刻データの抽出とグループ化 1709 df_with_sources["hour"] = df_with_sources.index.hour 1710 hourly_means = df_with_sources.groupby("hour")[["ch4_gas", "ch4_bio"]].mean() 1711 1712 # 24時間目のデータ点を追加(0時のデータを使用) 1713 last_hour = hourly_means.iloc[0:1].copy() 1714 last_hour.index = [24] 1715 hourly_means = pd.concat([hourly_means, last_hour]) 1716 1717 # 移動平均の適用 1718 hourly_means_smoothed = hourly_means 1719 if smooth: 1720 hourly_means_smoothed = hourly_means.rolling( 1721 window=window_size, center=True, min_periods=1 1722 ).mean() 1723 1724 # 24時間分のデータポイントを作成 1725 time_points = pd.date_range("2024-01-01", periods=25, freq="h") 1726 1727 # プロットの作成 1728 plt.figure(figsize=(10, 6)) 1729 ax = plt.gca() 1730 1731 # サブプロットラベルの追加(subplot_labelが指定されている場合) 1732 if subplot_label: 1733 ax.text( 1734 0.02, # x位置 1735 0.98, # y位置 1736 subplot_label, 1737 transform=ax.transAxes, 1738 va="top", 1739 fontsize=subplot_fontsize, 1740 ) 1741 1742 # 積み上げプロット 1743 ax.fill_between( 1744 time_points, 1745 0, 1746 hourly_means_smoothed["ch4_bio"], 1747 color="blue", 1748 alpha=0.6, 1749 label=label_bio, 1750 ) 1751 ax.fill_between( 1752 time_points, 1753 hourly_means_smoothed["ch4_bio"], 1754 hourly_means_smoothed["ch4_bio"] + hourly_means_smoothed["ch4_gas"], 1755 color="red", 1756 alpha=0.6, 1757 label=label_gas, 1758 ) 1759 1760 # 合計値のライン 1761 total_flux = hourly_means_smoothed["ch4_bio"] + hourly_means_smoothed["ch4_gas"] 1762 ax.plot(time_points, total_flux, "-", color="black", alpha=0.5) 1763 1764 # 軸の設定 1765 ax.set_xlabel("Time (hour)") 1766 ax.set_ylabel(r"CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)") 1767 ax.xaxis.set_major_formatter(mdates.DateFormatter("%-H")) 1768 ax.xaxis.set_major_locator(mdates.HourLocator(byhour=[0, 6, 12, 18, 24])) 1769 ax.set_xlim(time_points[0], time_points[-1]) 1770 ax.set_ylim(0, y_max) # y軸の範囲を設定 1771 ax.grid(True, alpha=0.3) 1772 1773 # 凡例を図の下部に配置 1774 if add_legend: 1775 handles, labels = ax.get_legend_handles_labels() 1776 fig = plt.gcf() # 現在の図を取得 1777 fig.legend( 1778 handles, 1779 labels, 1780 loc="center", 1781 bbox_to_anchor=(0.5, 0.01), 1782 ncol=len(handles), 1783 ) 1784 plt.subplots_adjust(bottom=0.2) # 下部に凡例用のスペースを確保 1785 1786 # グラフの保存 1787 plt.tight_layout() 1788 plt.savefig(output_path, dpi=300, bbox_inches="tight") 1789 plt.close() 1790 1791 # 統計情報の表示 1792 if print_summary: 1793 stats = { 1794 "都市ガス起源": hourly_means["ch4_gas"], 1795 "生物起源": hourly_means["ch4_bio"], 1796 "合計": hourly_means["ch4_gas"] + hourly_means["ch4_bio"], 1797 } 1798 1799 for source, data in stats.items(): 1800 mean_val = data.mean() 1801 min_val = data.min() 1802 max_val = data.max() 1803 min_time = data.idxmin() 1804 max_time = data.idxmax() 1805 1806 self.logger.info(f"{source}の統計:") 1807 print(f" 平均値: {mean_val:.2f}") 1808 print(f" 最小値: {min_val:.2f} (Hour: {min_time})") 1809 print(f" 最大値: {max_val:.2f} (Hour: {max_time})") 1810 if min_val != 0: 1811 print(f" 最大/最小比: {max_val / min_val:.2f}")
CH4フラックスの都市ガス起源と生物起源の日変化を積み上げグラフとして表示
Parameters:
df : pd.DataFrame
データフレーム
output_dir : str
出力ディレクトリのパス
col_ch4_flux : str
CH4フラックスのカラム名
col_c2h6_flux : str
C2H6フラックスのカラム名
label_gas : str
都市ガス起源のラベル
label_bio : str
生物起源のラベル
col_datetime : str
日時カラムの名前
output_filename : str
出力ファイル名
window_size : int
移動平均の窓サイズ
print_summary : bool
統計情報を表示するかどうか
smooth : bool
移動平均を適用するかどうか
y_max : float
y軸の上限値(デフォルト: 100)
1813 def plot_source_contributions_diurnal_by_date( 1814 self, 1815 df: pd.DataFrame, 1816 output_dir: str, 1817 col_ch4_flux: str, 1818 col_c2h6_flux: str, 1819 label_gas: str = "gas", 1820 label_bio: str = "bio", 1821 col_datetime: str = "Date", 1822 output_filename: str = "source_contributions_by_date.png", 1823 add_label: bool = True, 1824 add_legend: bool = False, 1825 print_summary: bool = False, # 統計情報を表示するかどうか, 1826 subplot_fontsize: int = 20, 1827 subplot_label_weekday: str | None = None, 1828 subplot_label_weekend: str | None = None, 1829 y_max: float | None = None, # y軸の上限値 1830 ) -> None: 1831 """CH4フラックスの都市ガス起源と生物起源の日変化を平日・休日別に表示 1832 1833 Parameters: 1834 ------ 1835 df : pd.DataFrame 1836 データフレーム 1837 output_dir : str 1838 出力ディレクトリのパス 1839 col_ch4_flux : str 1840 CH4フラックスのカラム名 1841 col_c2h6_flux : str 1842 C2H6フラックスのカラム名 1843 label_gas : str 1844 都市ガス起源のラベル 1845 label_bio : str 1846 生物起源のラベル 1847 col_datetime : str 1848 日時カラムの名前 1849 output_filename : str 1850 出力ファイル名 1851 add_label : bool 1852 ラベルを表示するか 1853 add_legend : bool 1854 凡例を表示するか 1855 subplot_fontsize : int 1856 サブプロットのフォントサイズ 1857 subplot_label_weekday : str | None 1858 平日グラフのラベル 1859 subplot_label_weekend : str | None 1860 休日グラフのラベル 1861 y_max : float | None 1862 y軸の上限値 1863 """ 1864 # 出力ディレクトリの作成 1865 os.makedirs(output_dir, exist_ok=True) 1866 output_path: str = os.path.join(output_dir, output_filename) 1867 1868 # 起源の計算 1869 df_with_sources = self._calculate_source_contributions( 1870 df=df, 1871 col_ch4_flux=col_ch4_flux, 1872 col_c2h6_flux=col_c2h6_flux, 1873 col_datetime=col_datetime, 1874 ) 1875 1876 # 日付タイプの分類 1877 dates = pd.to_datetime(df_with_sources.index) 1878 is_weekend = dates.dayofweek.isin([5, 6]) 1879 is_holiday = dates.map(lambda x: jpholiday.is_holiday(x.date())) 1880 is_weekday = ~(is_weekend | is_holiday) 1881 1882 # データの分類 1883 data_weekday = df_with_sources[is_weekday] 1884 data_holiday = df_with_sources[is_weekend | is_holiday] 1885 1886 # プロットの作成 1887 fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5)) 1888 1889 # 平日と休日それぞれのプロット 1890 for ax, data, label in [ 1891 (ax1, data_weekday, "Weekdays"), 1892 (ax2, data_holiday, "Weekends & Holidays"), 1893 ]: 1894 # 時間ごとの平均値を計算 1895 hourly_means = data.groupby(data.index.hour)[["ch4_gas", "ch4_bio"]].mean() 1896 1897 # 24時間目のデータ点を追加 1898 last_hour = hourly_means.iloc[0:1].copy() 1899 last_hour.index = [24] 1900 hourly_means = pd.concat([hourly_means, last_hour]) 1901 1902 # 24時間分のデータポイントを作成 1903 time_points = pd.date_range("2024-01-01", periods=25, freq="h") 1904 1905 # 積み上げプロット 1906 ax.fill_between( 1907 time_points, 1908 0, 1909 hourly_means["ch4_bio"], 1910 color="blue", 1911 alpha=0.6, 1912 label=label_bio, 1913 ) 1914 ax.fill_between( 1915 time_points, 1916 hourly_means["ch4_bio"], 1917 hourly_means["ch4_bio"] + hourly_means["ch4_gas"], 1918 color="red", 1919 alpha=0.6, 1920 label=label_gas, 1921 ) 1922 1923 # 合計値のライン 1924 total_flux = hourly_means["ch4_bio"] + hourly_means["ch4_gas"] 1925 ax.plot(time_points, total_flux, "-", color="black", alpha=0.5) 1926 1927 # 軸の設定 1928 if add_label: 1929 ax.set_xlabel("Time (hour)") 1930 if ax == ax1: # 左側のプロットのラベル 1931 ax.set_ylabel("Weekdays CH$_4$ flux\n" r"(nmol m$^{-2}$ s$^{-1}$)") 1932 else: # 右側のプロットのラベル 1933 ax.set_ylabel("Weekends CH$_4$ flux\n" r"(nmol m$^{-2}$ s$^{-1}$)") 1934 1935 ax.xaxis.set_major_formatter(mdates.DateFormatter("%-H")) 1936 ax.xaxis.set_major_locator(mdates.HourLocator(byhour=[0, 6, 12, 18, 24])) 1937 ax.set_xlim(time_points[0], time_points[-1]) 1938 if y_max is not None: 1939 ax.set_ylim(0, y_max) 1940 ax.grid(True, alpha=0.3) 1941 1942 # サブプロットラベルの追加 1943 if subplot_label_weekday: 1944 ax1.text( 1945 0.02, 1946 0.98, 1947 subplot_label_weekday, 1948 transform=ax1.transAxes, 1949 va="top", 1950 fontsize=subplot_fontsize, 1951 ) 1952 if subplot_label_weekend: 1953 ax2.text( 1954 0.02, 1955 0.98, 1956 subplot_label_weekend, 1957 transform=ax2.transAxes, 1958 va="top", 1959 fontsize=subplot_fontsize, 1960 ) 1961 1962 # 凡例を図の下部に配置 1963 if add_legend: 1964 # 最初のプロットから凡例のハンドルとラベルを取得 1965 handles, labels = ax1.get_legend_handles_labels() 1966 # 図の下部に凡例を配置 1967 fig.legend( 1968 handles, 1969 labels, 1970 loc="center", 1971 bbox_to_anchor=(0.5, 0.01), # x=0.5で中央、y=0.01で下部に配置 1972 ncol=len(handles), # ハンドルの数だけ列を作成(一行に表示) 1973 ) 1974 # 凡例用のスペースを確保 1975 plt.subplots_adjust(bottom=0.2) # 下部に30%のスペースを確保 1976 1977 plt.tight_layout() 1978 plt.savefig(output_path, dpi=300, bbox_inches="tight") 1979 plt.close() 1980 1981 # 統計情報の表示 1982 if print_summary: 1983 for data, label in [ 1984 (data_weekday, "Weekdays"), 1985 (data_holiday, "Weekends & Holidays"), 1986 ]: 1987 hourly_means = data.groupby(data.index.hour)[ 1988 ["ch4_gas", "ch4_bio"] 1989 ].mean() 1990 total_flux = hourly_means["ch4_gas"] + hourly_means["ch4_bio"] 1991 1992 print(f"\n{label}の統計:") 1993 print(f" 平均値: {total_flux.mean():.2f}") 1994 print(f" 最小値: {total_flux.min():.2f} (Hour: {total_flux.idxmin()})") 1995 print(f" 最大値: {total_flux.max():.2f} (Hour: {total_flux.idxmax()})") 1996 if total_flux.min() != 0: 1997 print(f" 最大/最小比: {total_flux.max() / total_flux.min():.2f}")
CH4フラックスの都市ガス起源と生物起源の日変化を平日・休日別に表示
Parameters:
df : pd.DataFrame
データフレーム
output_dir : str
出力ディレクトリのパス
col_ch4_flux : str
CH4フラックスのカラム名
col_c2h6_flux : str
C2H6フラックスのカラム名
label_gas : str
都市ガス起源のラベル
label_bio : str
生物起源のラベル
col_datetime : str
日時カラムの名前
output_filename : str
出力ファイル名
add_label : bool
ラベルを表示するか
add_legend : bool
凡例を表示するか
subplot_fontsize : int
サブプロットのフォントサイズ
subplot_label_weekday : str | None
平日グラフのラベル
subplot_label_weekend : str | None
休日グラフのラベル
y_max : float | None
y軸の上限値
1999 def plot_spectra( 2000 self, 2001 fs: float, 2002 lag_second: float, 2003 input_dir: str | Path | None, 2004 output_dir: str | Path | None, 2005 output_basename: str = "spectrum", 2006 col_ch4: str = "Ultra_CH4_ppm_C", 2007 col_c2h6: str = "Ultra_C2H6_ppb", 2008 col_tv: str = "Tv", 2009 label_ch4: str | None = None, 2010 label_c2h6: str | None = None, 2011 label_tv: str | None = None, 2012 file_pattern: str = "*.csv", 2013 markersize: float = 14, 2014 are_inputs_resampled: bool = True, 2015 save_fig: bool = True, 2016 show_fig: bool = True, 2017 plot_power: bool = True, 2018 plot_co: bool = True, 2019 add_tv_in_co: bool = True, 2020 ) -> None: 2021 """ 2022 月間の平均パワースペクトル密度を計算してプロットする。 2023 2024 データファイルを指定されたディレクトリから読み込み、パワースペクトル密度を計算し、 2025 結果を指定された出力ディレクトリにプロットして保存します。 2026 2027 Parameters: 2028 ------ 2029 fs : float 2030 サンプリング周波数。 2031 lag_second : float 2032 ラグ時間(秒)。 2033 input_dir : str | Path | None 2034 データファイルが格納されているディレクトリ。 2035 output_dir : str | Path | None 2036 出力先ディレクトリ。 2037 col_ch4 : str, optional 2038 CH4の濃度データが入ったカラムのキー。デフォルトは"Ultra_CH4_ppm_C"。 2039 col_c2h6 : str, optional 2040 C2H6の濃度データが入ったカラムのキー。デフォルトは"Ultra_C2H6_ppb"。 2041 col_tv : str, optional 2042 気温データが入ったカラムのキー。デフォルトは"Tv"。 2043 label_ch4 : str | None, optional 2044 CH4のラベル。デフォルトはNone。 2045 label_c2h6 : str | None, optional 2046 C2H6のラベル。デフォルトはNone。 2047 label_tv : str | None, optional 2048 気温のラベル。デフォルトはNone。 2049 file_pattern : str, optional 2050 処理対象のファイルパターン。デフォルトは"*.csv"。 2051 markersize : float, optional 2052 プロットマーカーのサイズ。デフォルトは14。 2053 are_inputs_resampled : bool, optional 2054 入力データが再サンプリングされているかどうか。デフォルトはTrue。 2055 save_fig : bool, optional 2056 図を保存するかどうか。デフォルトはTrue。 2057 show_fig : bool, optional 2058 図を表示するかどうか。デフォルトはTrue。 2059 plot_power : bool, optional 2060 パワースペクトルをプロットするかどうか。デフォルトはTrue。 2061 plot_co : bool, optional 2062 COのスペクトルをプロットするかどうか。デフォルトはTrue。 2063 add_tv_in_co : bool, optional 2064 顕熱フラックスのコスペクトルを表示するかどうか。デフォルトはTrue。 2065 """ 2066 # 出力ディレクトリの作成 2067 if save_fig: 2068 if output_dir is None: 2069 raise ValueError( 2070 "save_fig=Trueのとき、output_dirに有効なディレクトリパスを指定する必要があります。" 2071 ) 2072 os.makedirs(output_dir, exist_ok=True) 2073 2074 # データの読み込みと結合 2075 edp = EddyDataPreprocessor(fs=fs) 2076 col_wind_w: str = EddyDataPreprocessor.WIND_W 2077 2078 # 各変数のパワースペクトルを格納する辞書 2079 power_spectra = {col_ch4: [], col_c2h6: []} 2080 co_spectra = {col_ch4: [], col_c2h6: [], col_tv: []} 2081 freqs = None 2082 2083 # プログレスバーを表示しながらファイルを処理 2084 file_list = glob.glob(os.path.join(input_dir, file_pattern)) 2085 for filepath in tqdm(file_list, desc="Processing files"): 2086 df, _ = edp.get_resampled_df( 2087 filepath=filepath, is_already_resampled=are_inputs_resampled 2088 ) 2089 2090 # 風速成分の計算を追加 2091 df = edp.add_uvw_columns(df) 2092 2093 # NaNや無限大を含む行を削除 2094 df = df.replace([np.inf, -np.inf], np.nan).dropna( 2095 subset=[col_ch4, col_c2h6, col_tv, col_wind_w] 2096 ) 2097 2098 # データが十分な行数を持っているか確認 2099 if len(df) < 100: 2100 continue 2101 2102 # 各ファイルごとにスペクトル計算 2103 calculator = SpectrumCalculator( 2104 df=df, 2105 fs=fs, 2106 ) 2107 2108 for col in power_spectra.keys(): 2109 # 各変数のパワースペクトルを計算して保存 2110 if plot_power: 2111 f, ps = calculator.calculate_power_spectrum( 2112 col=col, 2113 dimensionless=True, 2114 frequency_weighted=True, 2115 interpolate_points=True, 2116 scaling="density", 2117 ) 2118 # 最初のファイル処理時にfreqsを初期化 2119 if freqs is None: 2120 freqs = f 2121 power_spectra[col].append(ps) 2122 # 以降は周波数配列の長さが一致する場合のみ追加 2123 elif len(f) == len(freqs): 2124 power_spectra[col].append(ps) 2125 2126 # コスペクトル 2127 if plot_co: 2128 _, cs, _ = calculator.calculate_co_spectrum( 2129 col1=col_wind_w, 2130 col2=col, 2131 dimensionless=True, 2132 frequency_weighted=True, 2133 interpolate_points=True, 2134 scaling="spectrum", 2135 apply_lag_correction_to_col2=True, 2136 lag_second=lag_second, 2137 ) 2138 if freqs is not None and len(cs) == len(freqs): 2139 co_spectra[col].append(cs) 2140 2141 # 顕熱フラックスのコスペクトル計算を追加 2142 if plot_co and add_tv_in_co: 2143 _, cs_heat, _ = calculator.calculate_co_spectrum( 2144 col1=col_wind_w, 2145 col2=col_tv, 2146 dimensionless=True, 2147 frequency_weighted=True, 2148 interpolate_points=True, 2149 scaling="spectrum", 2150 ) 2151 if freqs is not None and len(cs_heat) == len(freqs): 2152 co_spectra[col_tv].append(cs_heat) 2153 2154 # 各変数のスペクトルを平均化 2155 if plot_power: 2156 averaged_power_spectra = { 2157 col: np.mean(spectra, axis=0) for col, spectra in power_spectra.items() 2158 } 2159 if plot_co: 2160 averaged_co_spectra = { 2161 col: np.mean(spectra, axis=0) for col, spectra in co_spectra.items() 2162 } 2163 # 顕熱フラックスの平均コスペクトル計算 2164 if plot_co and add_tv_in_co and co_spectra[col_tv]: 2165 averaged_heat_co_spectra = np.mean(co_spectra[col_tv], axis=0) 2166 2167 # プロット設定を修正 2168 plot_configs = [ 2169 { 2170 "col": col_ch4, 2171 "psd_ylabel": r"$fS_{\mathrm{CH_4}} / s_{\mathrm{CH_4}}^2$", 2172 "co_ylabel": r"$fC_{w\mathrm{CH_4}} / \overline{w'\mathrm{CH_4}'}$", 2173 "color": "red", 2174 "label": label_ch4, 2175 }, 2176 { 2177 "col": col_c2h6, 2178 "psd_ylabel": r"$fS_{\mathrm{C_2H_6}} / s_{\mathrm{C_2H_6}}^2$", 2179 "co_ylabel": r"$fC_{w\mathrm{C_2H_6}} / \overline{w'\mathrm{C_2H_6}'}$", 2180 "color": "orange", 2181 "label": label_c2h6, 2182 }, 2183 ] 2184 plot_tv_config = { 2185 "col": col_tv, 2186 "psd_ylabel": r"$fS_{T_v} / s_{T_v}^2$", 2187 "co_ylabel": r"$fC_{wT_v} / \overline{w'T_v'}$", 2188 "color": "blue", 2189 "label": label_tv, 2190 } 2191 2192 # パワースペクトルの図を作成 2193 if plot_power: 2194 fig_power, axes_psd = plt.subplots(1, 2, figsize=(12, 5), sharex=True) 2195 for ax, config in zip(axes_psd, plot_configs): 2196 ax.plot( 2197 freqs, 2198 averaged_power_spectra[config["col"]], 2199 "o", # マーカーを丸に設定 2200 color=config["color"], 2201 markersize=markersize, 2202 ) 2203 ax.set_xscale("log") 2204 ax.set_yscale("log") 2205 ax.set_xlim(0.001, 10) 2206 ax.plot([0.01, 10], [1, 0.01], "-", color="black", alpha=0.5) 2207 ax.text(0.1, 0.06, "-2/3", fontsize=18) 2208 ax.set_ylabel(config["psd_ylabel"]) 2209 if config["label"] is not None: 2210 ax.text( 2211 0.02, 0.98, config["label"], transform=ax.transAxes, va="top" 2212 ) 2213 ax.grid(True, alpha=0.3) 2214 ax.set_xlabel("f (Hz)") 2215 2216 plt.tight_layout() 2217 2218 if save_fig: 2219 output_path_psd: str = os.path.join( 2220 output_dir, f"power_{output_basename}.png" 2221 ) 2222 plt.savefig( 2223 output_path_psd, 2224 dpi=300, 2225 bbox_inches="tight", 2226 ) 2227 if show_fig: 2228 plt.show() 2229 else: 2230 plt.close(fig=fig_power) 2231 2232 # コスペクトルの図を作成 2233 if plot_co: 2234 fig_co, axes_cosp = plt.subplots(1, 2, figsize=(12, 5), sharex=True) 2235 for ax, config in zip(axes_cosp, plot_configs): 2236 # 顕熱フラックスのコスペクトルを先に描画(背景として) 2237 if add_tv_in_co and len(co_spectra[col_tv]) > 0: 2238 ax.plot( 2239 freqs, 2240 averaged_heat_co_spectra, 2241 "o", 2242 color="gray", 2243 alpha=0.3, 2244 markersize=markersize, 2245 label=plot_tv_config["label"] 2246 if plot_tv_config["label"] 2247 else None, 2248 ) 2249 2250 # CH4またはC2H6のコスペクトルを描画 2251 ax.plot( 2252 freqs, 2253 averaged_co_spectra[config["col"]], 2254 "o", 2255 color=config["color"], 2256 markersize=markersize, 2257 label=config["label"] if config["label"] else None, 2258 ) 2259 ax.set_xscale("log") 2260 ax.set_yscale("log") 2261 ax.set_xlim(0.001, 10) 2262 # ax.plot([0.01, 10], [1, 0.01], "-", color="black", alpha=0.5) 2263 # ax.text(0.1, 0.1, "-4/3", fontsize=18) 2264 ax.set_ylabel(config["co_ylabel"]) 2265 if config["label"] is not None: 2266 ax.text( 2267 0.02, 0.98, config["label"], transform=ax.transAxes, va="top" 2268 ) 2269 ax.grid(True, alpha=0.3) 2270 ax.set_xlabel("f (Hz)") 2271 # 凡例を追加(顕熱フラックスが含まれる場合) 2272 if add_tv_in_co and label_tv: 2273 ax.legend(loc="lower left") 2274 2275 plt.tight_layout() 2276 if save_fig: 2277 output_path_csd: str = os.path.join( 2278 output_dir, f"co_{output_basename}.png" 2279 ) 2280 plt.savefig( 2281 output_path_csd, 2282 dpi=300, 2283 bbox_inches="tight", 2284 ) 2285 if show_fig: 2286 plt.show() 2287 else: 2288 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。
2290 def plot_turbulence( 2291 self, 2292 df: pd.DataFrame, 2293 output_dir: str, 2294 output_filename: str = "turbulence.png", 2295 col_uz: str = "Uz", 2296 col_ch4: str = "Ultra_CH4_ppm_C", 2297 col_c2h6: str = "Ultra_C2H6_ppb", 2298 col_timestamp: str = "TIMESTAMP", 2299 add_serial_labels: bool = True, 2300 ) -> None: 2301 """時系列データのプロットを作成する 2302 2303 Parameters: 2304 ------ 2305 df : pd.DataFrame 2306 プロットするデータを含むDataFrame 2307 output_dir : str 2308 出力ディレクトリのパス 2309 output_filename : str 2310 出力ファイル名 2311 col_uz : str 2312 鉛直風速データのカラム名 2313 col_ch4 : str 2314 メタンデータのカラム名 2315 col_c2h6 : str 2316 エタンデータのカラム名 2317 col_timestamp : str 2318 タイムスタンプのカラム名 2319 """ 2320 # 出力ディレクトリの作成 2321 os.makedirs(output_dir, exist_ok=True) 2322 output_path: str = os.path.join(output_dir, output_filename) 2323 2324 # データの前処理 2325 df = df.copy() 2326 2327 # タイムスタンプをインデックスに設定(まだ設定されていない場合) 2328 if not isinstance(df.index, pd.DatetimeIndex): 2329 df[col_timestamp] = pd.to_datetime(df[col_timestamp]) 2330 df.set_index(col_timestamp, inplace=True) 2331 2332 # 開始時刻と終了時刻を取得 2333 start_time = df.index[0] 2334 end_time = df.index[-1] 2335 2336 # 開始時刻の分を取得 2337 start_minute = start_time.minute 2338 2339 # 時間軸の作成(実際の開始時刻からの経過分数) 2340 minutes_elapsed = (df.index - start_time).total_seconds() / 60 2341 2342 # プロットの作成 2343 _, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=(12, 10), sharex=True) 2344 2345 # 鉛直風速 2346 ax1.plot(minutes_elapsed, df[col_uz], "k-", linewidth=0.5) 2347 ax1.set_ylabel(r"$w$ (m s$^{-1}$)") 2348 if add_serial_labels: 2349 ax1.text(0.02, 0.98, "(a)", transform=ax1.transAxes, va="top") 2350 ax1.grid(True, alpha=0.3) 2351 2352 # CH4濃度 2353 ax2.plot(minutes_elapsed, df[col_ch4], "r-", linewidth=0.5) 2354 ax2.set_ylabel(r"$\mathrm{CH_4}$ (ppm)") 2355 if add_serial_labels: 2356 ax2.text(0.02, 0.98, "(b)", transform=ax2.transAxes, va="top") 2357 ax2.grid(True, alpha=0.3) 2358 2359 # C2H6濃度 2360 ax3.plot(minutes_elapsed, df[col_c2h6], "orange", linewidth=0.5) 2361 ax3.set_ylabel(r"$\mathrm{C_2H_6}$ (ppb)") 2362 if add_serial_labels: 2363 ax3.text(0.02, 0.98, "(c)", transform=ax3.transAxes, va="top") 2364 ax3.grid(True, alpha=0.3) 2365 ax3.set_xlabel("Time (minutes)") 2366 2367 # x軸の範囲を実際の開始時刻から30分後までに設定 2368 total_minutes = (end_time - start_time).total_seconds() / 60 2369 ax3.set_xlim(0, min(30, total_minutes)) 2370 2371 # x軸の目盛りを5分間隔で設定 2372 np.arange(start_minute, start_minute + 35, 5) 2373 ax3.xaxis.set_major_locator(MultipleLocator(5)) 2374 2375 # レイアウトの調整 2376 plt.tight_layout() 2377 2378 # 図の保存 2379 plt.savefig(output_path, dpi=300, bbox_inches="tight") 2380 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
タイムスタンプのカラム名
2382 def plot_wind_rose_sources( 2383 self, 2384 df: pd.DataFrame, 2385 output_dir: str | Path | None = None, 2386 output_filename: str = "edp_wind_rose.png", 2387 col_datetime: str = "Date", 2388 col_ch4_flux: str = "Fch4", 2389 col_c2h6_flux: str = "Fc2h6", 2390 col_wind_dir: str = "Wind direction", 2391 flux_unit: str = r"(nmol m$^{-2}$ s$^{-1}$)", 2392 ymax: float | None = None, # フラックスの上限値 2393 label_gas: str = "都市ガス起源", 2394 label_bio: str = "生物起源", 2395 figsize: tuple[float, float] = (8, 8), 2396 flux_alpha: float = 0.4, 2397 num_directions: int = 8, # 方位の数(8方位) 2398 center_on_angles: bool = True, # 追加:45度刻みの線を境界にするかどうか 2399 subplot_label: str | None = None, 2400 add_legend: bool = True, 2401 stack_bars: bool = False, # 追加:積み上げ方式を選択するパラメータ 2402 print_summary: bool = True, # 統計情報を表示するかどうか 2403 save_fig: bool = True, 2404 show_fig: bool = True, 2405 ) -> None: 2406 """CH4フラックスの都市ガス起源と生物起源の風配図を作成する関数 2407 2408 Parameters: 2409 ------ 2410 df : pd.DataFrame 2411 風配図を作成するためのデータフレーム 2412 output_dir : str | Path | None 2413 生成された図を保存するディレクトリのパス 2414 output_filename : str 2415 保存するファイル名(デフォルトは"edp_wind_rose.png") 2416 col_ch4_flux : str 2417 CH4フラックスを示すカラム名 2418 col_c2h6_flux : str 2419 C2H6フラックスを示すカラム名 2420 col_wind_dir : str 2421 風向を示すカラム名 2422 label_gas : str 2423 都市ガス起源のフラックスに対するラベル 2424 label_bio : str 2425 生物起源のフラックスに対するラベル 2426 col_datetime : str 2427 日時を示すカラム名 2428 num_directions : int 2429 風向の数(デフォルトは8) 2430 center_on_angles: bool 2431 Trueの場合、45度刻みの線を境界として扇形を描画します。 2432 Falseの場合、45度の中間(22.5度)を中心として扇形を描画します。 2433 subplot_label : str 2434 サブプロットに表示するラベル 2435 print_summary : bool 2436 統計情報を表示するかどうかのフラグ 2437 flux_unit : str 2438 フラックスの単位 2439 ymax : float | None 2440 y軸の上限値(指定しない場合はデータの最大値に基づいて自動設定) 2441 figsize : tuple[float, float] 2442 図のサイズ 2443 flux_alpha : float 2444 フラックスの透明度 2445 stack_bars : bool, optional 2446 Trueの場合、生物起源の上に都市ガス起源を積み上げます。 2447 Falseの場合、両方を0から積み上げます(デフォルト)。 2448 save_fig : bool 2449 図を保存するかどうかのフラグ 2450 show_fig : bool 2451 図を表示するかどうかのフラグ 2452 """ 2453 # 起源の計算 2454 df_with_sources = self._calculate_source_contributions( 2455 df=df, 2456 col_ch4_flux=col_ch4_flux, 2457 col_c2h6_flux=col_c2h6_flux, 2458 col_datetime=col_datetime, 2459 ) 2460 2461 # 方位の定義 2462 direction_ranges = self._define_direction_ranges( 2463 num_directions, center_on_angles 2464 ) 2465 2466 # 方位ごとのデータを集計 2467 direction_data = self._aggregate_direction_data( 2468 df_with_sources, col_wind_dir, direction_ranges 2469 ) 2470 2471 # プロットの作成 2472 fig = plt.figure(figsize=figsize) 2473 ax = fig.add_subplot(111, projection="polar") 2474 2475 # 方位の角度(ラジアン)を計算 2476 theta = np.array( 2477 [np.radians(angle) for angle in direction_data["center_angle"]] 2478 ) 2479 2480 # 積み上げ方式に応じてプロット 2481 if stack_bars: 2482 # 生物起源を基準として描画 2483 ax.bar( 2484 theta, 2485 direction_data["bio_flux"], 2486 width=np.radians(360 / num_directions), 2487 bottom=0.0, 2488 color="blue", 2489 alpha=flux_alpha, 2490 label=label_bio, 2491 ) 2492 # 都市ガス起源を生物起源の上に積み上げ 2493 ax.bar( 2494 theta, 2495 direction_data["gas_flux"], 2496 width=np.radians(360 / num_directions), 2497 bottom=direction_data["bio_flux"], # 生物起源の上に積み上げ 2498 color="red", 2499 alpha=flux_alpha, 2500 label=label_gas, 2501 ) 2502 else: 2503 # 両方を0から積み上げ(デフォルト) 2504 ax.bar( 2505 theta, 2506 direction_data["bio_flux"], 2507 width=np.radians(360 / num_directions), 2508 bottom=0.0, 2509 color="blue", 2510 alpha=flux_alpha, 2511 label=label_bio, 2512 ) 2513 ax.bar( 2514 theta, 2515 direction_data["gas_flux"], 2516 width=np.radians(360 / num_directions), 2517 bottom=0.0, 2518 color="red", 2519 alpha=flux_alpha, 2520 label=label_gas, 2521 ) 2522 2523 # y軸の範囲を設定 2524 if ymax is not None: 2525 ax.set_ylim(0, ymax) 2526 else: 2527 # データの最大値に基づいて自動設定 2528 max_value = max( 2529 direction_data["bio_flux"].max(), direction_data["gas_flux"].max() 2530 ) 2531 ax.set_ylim(0, max_value * 1.1) # 最大値の1.1倍を上限に設定 2532 2533 # 方位ラベルの設定 2534 ax.set_theta_zero_location("N") # 北を上に設定 2535 ax.set_theta_direction(-1) # 時計回りに設定 2536 2537 # 方位ラベルの表示 2538 labels = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"] 2539 angles = np.radians(np.linspace(0, 360, len(labels), endpoint=False)) 2540 ax.set_xticks(angles) 2541 ax.set_xticklabels(labels) 2542 2543 # プロット領域の調整(上部と下部にスペースを確保) 2544 plt.subplots_adjust( 2545 top=0.8, # 上部に20%のスペースを確保 2546 bottom=0.2, # 下部に20%のスペースを確保(凡例用) 2547 ) 2548 2549 # サブプロットラベルの追加(デフォルトは左上) 2550 if subplot_label: 2551 ax.text( 2552 0.01, 2553 0.99, 2554 subplot_label, 2555 transform=ax.transAxes, 2556 ) 2557 2558 # 単位の追加(図の下部中央に配置) 2559 plt.figtext( 2560 0.5, # x位置(中央) 2561 0.1, # y位置(下部) 2562 flux_unit, 2563 ha="center", # 水平方向の位置揃え 2564 va="bottom", # 垂直方向の位置揃え 2565 ) 2566 2567 # 凡例の追加(単位の下に配置) 2568 if add_legend: 2569 # 最初のプロットから凡例のハンドルとラベルを取得 2570 handles, labels = ax.get_legend_handles_labels() 2571 # 図の下部に凡例を配置 2572 fig.legend( 2573 handles, 2574 labels, 2575 loc="center", 2576 bbox_to_anchor=(0.5, 0.05), # x=0.5で中央、y=0.05で下部に配置 2577 ncol=len(handles), # ハンドルの数だけ列を作成(一行に表示) 2578 ) 2579 2580 # グラフの保存 2581 if save_fig: 2582 if output_dir is None: 2583 raise ValueError( 2584 "save_fig=Trueのとき、output_dirに有効なパスを指定する必要があります。" 2585 ) 2586 # 出力ディレクトリの作成 2587 os.makedirs(output_dir, exist_ok=True) 2588 output_path: str = os.path.join(output_dir, output_filename) 2589 plt.savefig(output_path, dpi=300, bbox_inches="tight") 2590 2591 # グラフの表示 2592 if show_fig: 2593 plt.show() 2594 else: 2595 plt.close(fig=fig) 2596 2597 # 統計情報の表示 2598 if print_summary: 2599 for source in ["gas", "bio"]: 2600 flux_data = direction_data[f"{source}_flux"] 2601 mean_val = flux_data.mean() 2602 max_val = flux_data.max() 2603 max_dir = direction_data.loc[flux_data.idxmax(), "name"] 2604 2605 self.logger.info( 2606 f"{label_gas if source == 'gas' else label_bio}の統計:" 2607 ) 2608 print(f" 平均フラックス: {mean_val:.2f}") 2609 print(f" 最大フラックス: {max_val:.2f}") 2610 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
風向を示すカラム名
label_gas : str
都市ガス起源のフラックスに対するラベル
label_bio : str
生物起源のフラックスに対するラベル
col_datetime : str
日時を示すカラム名
num_directions : int
風向の数(デフォルトは8)
center_on_angles: bool
Trueの場合、45度刻みの線を境界として扇形を描画します。
Falseの場合、45度の中間(22.5度)を中心として扇形を描画します。
subplot_label : str
サブプロットに表示するラベル
print_summary : bool
統計情報を表示するかどうかのフラグ
flux_unit : str
フラックスの単位
ymax : float | None
y軸の上限値(指定しない場合はデータの最大値に基づいて自動設定)
figsize : tuple[float, float]
図のサイズ
flux_alpha : float
フラックスの透明度
stack_bars : bool, optional
Trueの場合、生物起源の上に都市ガス起源を積み上げます。
Falseの場合、両方を0から積み上げます(デフォルト)。
save_fig : bool
図を保存するかどうかのフラグ
show_fig : bool
図を表示するかどうかのフラグ
2895 @staticmethod 2896 def get_valid_data(df: pd.DataFrame, x_col: str, y_col: str) -> pd.DataFrame: 2897 """ 2898 指定された列の有効なデータ(NaNを除いた)を取得します。 2899 2900 Parameters: 2901 ------ 2902 df : pd.DataFrame 2903 データフレーム 2904 x_col : str 2905 X軸の列名 2906 y_col : str 2907 Y軸の列名 2908 2909 Returns: 2910 ------ 2911 pd.DataFrame 2912 有効なデータのみを含むDataFrame 2913 """ 2914 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
2916 @staticmethod 2917 def setup_logger(logger: Logger | None, log_level: int = INFO) -> Logger: 2918 """ 2919 ロガーを設定します。 2920 2921 このメソッドは、ロギングの設定を行い、ログメッセージのフォーマットを指定します。 2922 ログメッセージには、日付、ログレベル、メッセージが含まれます。 2923 2924 渡されたロガーがNoneまたは不正な場合は、新たにロガーを作成し、標準出力に 2925 ログメッセージが表示されるようにStreamHandlerを追加します。ロガーのレベルは 2926 引数で指定されたlog_levelに基づいて設定されます。 2927 2928 Parameters: 2929 ------ 2930 logger : Logger | None 2931 使用するロガー。Noneの場合は新しいロガーを作成します。 2932 log_level : int 2933 ロガーのログレベル。デフォルトはINFO。 2934 2935 Returns: 2936 ------ 2937 Logger 2938 設定されたロガーオブジェクト。 2939 """ 2940 if logger is not None and isinstance(logger, Logger): 2941 return logger 2942 # 渡されたロガーがNoneまたは正しいものでない場合は独自に設定 2943 new_logger: Logger = getLogger() 2944 # 既存のハンドラーをすべて削除 2945 for handler in new_logger.handlers[:]: 2946 new_logger.removeHandler(handler) 2947 new_logger.setLevel(log_level) # ロガーのレベルを設定 2948 ch = StreamHandler() 2949 ch_formatter = Formatter("%(asctime)s - %(levelname)s - %(message)s") 2950 ch.setFormatter(ch_formatter) # フォーマッターをハンドラーに設定 2951 new_logger.addHandler(ch) # StreamHandlerの追加 2952 return new_logger
ロガーを設定します。
このメソッドは、ロギングの設定を行い、ログメッセージのフォーマットを指定します。 ログメッセージには、日付、ログレベル、メッセージが含まれます。
渡されたロガーがNoneまたは不正な場合は、新たにロガーを作成し、標準出力に ログメッセージが表示されるようにStreamHandlerを追加します。ロガーのレベルは 引数で指定されたlog_levelに基づいて設定されます。
Parameters:
logger : Logger | None
使用するロガー。Noneの場合は新しいロガーを作成します。
log_level : int
ロガーのログレベル。デフォルトはINFO。
Returns:
Logger
設定されたロガーオブジェクト。
2954 @staticmethod 2955 def plot_flux_distributions( 2956 g2401_flux: pd.Series, 2957 ultra_flux: pd.Series, 2958 month: int, 2959 output_dir: str, 2960 xlim: tuple[float, float] = (-50, 200), 2961 bandwidth: float = 1.0, # デフォルト値を1.0に設定 2962 ) -> None: 2963 """ 2964 両測器のCH4フラックス分布を可視化 2965 2966 Parameters: 2967 ------ 2968 g2401_flux : pd.Series 2969 G2401で測定されたフラックス値の配列 2970 ultra_flux : pd.Series 2971 Ultraで測定されたフラックス値の配列 2972 month : int 2973 測定月 2974 output_dir : str 2975 出力ディレクトリ 2976 xlim : tuple[float, float] 2977 x軸の範囲(タプル) 2978 bandwidth : float 2979 カーネル密度推定のバンド幅調整係数(デフォルト: 1.0) 2980 """ 2981 # nanを除去 2982 g2401_flux = g2401_flux.dropna() 2983 ultra_flux = ultra_flux.dropna() 2984 2985 plt.figure(figsize=(10, 6)) 2986 2987 # KDEプロット(確率密度推定) 2988 sns.kdeplot( 2989 data=g2401_flux, label="G2401", color="blue", alpha=0.5, bw_adjust=bandwidth 2990 ) 2991 sns.kdeplot( 2992 data=ultra_flux, label="Ultra", color="red", alpha=0.5, bw_adjust=bandwidth 2993 ) 2994 2995 # 平均値と中央値のマーカー 2996 plt.axvline( 2997 g2401_flux.mean(), 2998 color="blue", 2999 linestyle="--", 3000 alpha=0.5, 3001 label="G2401 mean", 3002 ) 3003 plt.axvline( 3004 ultra_flux.mean(), 3005 color="red", 3006 linestyle="--", 3007 alpha=0.5, 3008 label="Ultra mean", 3009 ) 3010 plt.axvline( 3011 np.median(g2401_flux), 3012 color="blue", 3013 linestyle=":", 3014 alpha=0.5, 3015 label="G2401 median", 3016 ) 3017 plt.axvline( 3018 np.median(ultra_flux), 3019 color="red", 3020 linestyle=":", 3021 alpha=0.5, 3022 label="Ultra median", 3023 ) 3024 3025 # 軸ラベルとタイトル 3026 plt.xlabel(r"CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)") 3027 plt.ylabel("Probability Density") 3028 plt.title(f"Distribution of CH$_4$ fluxes - Month {month}") 3029 3030 # x軸の範囲設定 3031 plt.xlim(xlim) 3032 3033 # グリッド表示 3034 plt.grid(True, alpha=0.3) 3035 3036 # 統計情報 3037 stats_text = ( 3038 f"G2401:\n" 3039 f" Mean: {g2401_flux.mean():.2f}\n" 3040 f" Median: {np.median(g2401_flux):.2f}\n" 3041 f" Std: {g2401_flux.std():.2f}\n" 3042 f"Ultra:\n" 3043 f" Mean: {ultra_flux.mean():.2f}\n" 3044 f" Median: {np.median(ultra_flux):.2f}\n" 3045 f" Std: {ultra_flux.std():.2f}" 3046 ) 3047 plt.text( 3048 0.02, 3049 0.98, 3050 stats_text, 3051 transform=plt.gca().transAxes, 3052 verticalalignment="top", 3053 fontsize=10, 3054 bbox=dict(boxstyle="round", facecolor="white", alpha=0.8), 3055 ) 3056 3057 # 凡例の表示 3058 plt.legend(loc="upper right") 3059 3060 # グラフの保存 3061 os.makedirs(output_dir, exist_ok=True) 3062 plt.tight_layout() 3063 plt.savefig( 3064 os.path.join(output_dir, f"flux_distribution_month_{month}.png"), 3065 dpi=300, 3066 bbox_inches="tight", 3067 ) 3068 plt.close()
両測器のCH4フラックス分布を可視化
Parameters:
g2401_flux : pd.Series
G2401で測定されたフラックス値の配列
ultra_flux : pd.Series
Ultraで測定されたフラックス値の配列
month : int
測定月
output_dir : str
出力ディレクトリ
xlim : tuple[float, float]
x軸の範囲(タプル)
bandwidth : float
カーネル密度推定のバンド幅調整係数(デフォルト: 1.0)
11class FftFileReorganizer: 12 """ 13 FFTファイルを再編成するためのクラス。 14 15 入力ディレクトリからファイルを読み取り、フラグファイルに基づいて 16 出力ディレクトリに再編成します。時間の完全一致を要求し、 17 一致しないファイルはスキップして警告を出します。 18 オプションで相対湿度(RH)に基づいたサブディレクトリへの分類も可能です。 19 """ 20 21 # クラス定数の定義 22 DEFAULT_FILENAME_PATTERNS: list[str] = [ 23 r"FFT_TOA5_\d+\.SAC_Eddy_\d+_(\d{4})_(\d{2})_(\d{2})_(\d{4})(?:\+)?\.csv", 24 r"FFT_TOA5_\d+\.SAC_Ultra\.Eddy_\d+_(\d{4})_(\d{2})_(\d{2})_(\d{4})(?:\+)?(?:-resampled)?\.csv", 25 ] # デフォルトのファイル名のパターン(正規表現) 26 DEFAULT_OUTPUT_DIRS = { 27 "GOOD_DATA": "good_data_all", 28 "BAD_DATA": "bad_data", 29 } # 出力ディレクトリの構造に関する定数 30 31 def __init__( 32 self, 33 input_dir: str, 34 output_dir: str, 35 flag_csv_path: str, 36 filename_patterns: list[str] | None = None, 37 output_dirs_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
設定されたロガーオブジェクト。
9class TransferFunctionCalculator: 10 """ 11 このクラスは、CSVファイルからデータを読み込み、処理し、 12 伝達関数を計算してプロットするための機能を提供します。 13 14 この実装は Moore (1986) の論文に基づいています。 15 """ 16 17 def __init__( 18 self, 19 file_path: str, 20 col_freq: str, 21 cutoff_freq_low: float = 0.01, 22 cutoff_freq_high: float = 1, 23 ): 24 """ 25 TransferFunctionCalculatorクラスのコンストラクタ。 26 27 Parameters: 28 ------ 29 file_path : str 30 分析対象のCSVファイルのパス。 31 col_freq : str 32 周波数のキー。 33 cutoff_freq_low : float 34 カットオフ周波数の最低値。 35 cutoff_freq_high : float 36 カットオフ周波数の最高値。 37 """ 38 self._col_freq: str = col_freq 39 self._cutoff_freq_low: float = cutoff_freq_low 40 self._cutoff_freq_high: float = cutoff_freq_high 41 self._df: pd.DataFrame = TransferFunctionCalculator._load_data(file_path) 42 43 def calculate_transfer_function( 44 self, col_reference: str, col_target: str 45 ) -> tuple[float, float, pd.DataFrame]: 46 """ 47 伝達関数の係数を計算する。 48 49 Parameters: 50 ------ 51 col_reference : str 52 参照データのカラム名。 53 col_target : str 54 ターゲットデータのカラム名。 55 56 Returns: 57 ------ 58 tuple[float, float, pandas.DataFrame] 59 伝達関数の係数aとその標準誤差、および計算に用いたDataFrame。 60 """ 61 df_processed: pd.DataFrame = self.process_data( 62 col_reference=col_reference, col_target=col_target 63 ) 64 df_cutoff: pd.DataFrame = self._cutoff_df(df_processed) 65 66 array_x = np.array(df_cutoff.index) 67 array_y = np.array(df_cutoff["target"] / df_cutoff["reference"]) 68 69 # フィッティングパラメータと共分散行列を取得 70 popt, pcov = curve_fit( 71 TransferFunctionCalculator.transfer_function, array_x, array_y 72 ) 73 74 # 標準誤差を計算(共分散行列の対角成分の平方根) 75 perr = np.sqrt(np.diag(pcov)) 76 77 # 係数aとその標準誤差、および計算に用いたDataFrameを返す 78 return popt[0], perr[0], df_processed 79 80 def create_plot_co_spectra( 81 self, 82 col1: str, 83 col2: str, 84 color1: str = "gray", 85 color2: str = "red", 86 figsize: tuple[int, int] = (10, 8), 87 label1: str | None = None, 88 label2: str | None = None, 89 output_dir: str | None = None, 90 output_basename: str = "co", 91 add_legend: bool = True, 92 add_xy_labels: bool = True, 93 show_fig: bool = True, 94 subplot_label: str | None = "(a)", 95 window_size: int = 5, # 移動平均の窓サイズ 96 markersize: float = 14, 97 ) -> None: 98 """ 99 2種類のコスペクトルをプロットする。 100 101 Parameters: 102 ------ 103 col1 : str 104 1つ目のコスペクトルデータのカラム名。 105 col2 : str 106 2つ目のコスペクトルデータのカラム名。 107 color1 : str, optional 108 1つ目のデータの色。デフォルトは'gray'。 109 color2 : str, optional 110 2つ目のデータの色。デフォルトは'red'。 111 figsize : tuple[int, int], optional 112 プロットのサイズ。デフォルトは(10, 8)。 113 label1 : str, optional 114 1つ目のデータのラベル名。デフォルトはNone。 115 label2 : str, optional 116 2つ目のデータのラベル名。デフォルトはNone。 117 output_dir : str | None, optional 118 プロットを保存するディレクトリ。デフォルトはNoneで、保存しない。 119 output_basename : str, optional 120 保存するファイル名のベース。デフォルトは"co"。 121 show_fig : bool, optional 122 プロットを表示するかどうか。デフォルトはTrue。 123 subplot_label : str | None, optional 124 左上に表示するサブプロットラベル。デフォルトは"(a)"。 125 window_size : int, optional 126 移動平均の窓サイズ。デフォルトは5。 127 """ 128 df: pd.DataFrame = self._df.copy() 129 # データの取得と移動平均の適用 130 data1 = df[df[col1] > 0].groupby(self._col_freq)[col1].median() 131 data2 = df[df[col2] > 0].groupby(self._col_freq)[col2].median() 132 133 data1 = data1.rolling(window=window_size, center=True, min_periods=1).mean() 134 data2 = data2.rolling(window=window_size, center=True, min_periods=1).mean() 135 136 fig = plt.figure(figsize=figsize) 137 ax = fig.add_subplot(111) 138 139 # マーカーサイズを設定して見やすくする 140 ax.plot( 141 data1.index, data1, "o", color=color1, label=label1, markersize=markersize 142 ) 143 ax.plot( 144 data2.index, data2, "o", color=color2, label=label2, markersize=markersize 145 ) 146 ax.plot([0.01, 10], [10, 0.001], "-", color="black") 147 ax.text(0.25, 0.4, "-4/3") 148 149 ax.grid(True, alpha=0.3) 150 ax.set_xscale("log") 151 ax.set_yscale("log") 152 ax.set_xlim(0.0001, 10) 153 ax.set_ylim(0.0001, 10) 154 if add_xy_labels: 155 ax.set_xlabel("f (Hz)") 156 ax.set_ylabel("無次元コスペクトル") 157 158 if add_legend: 159 ax.legend( 160 bbox_to_anchor=(0.05, 1), 161 loc="lower left", 162 fontsize=16, 163 ncol=3, 164 frameon=False, 165 ) 166 if subplot_label is not None: 167 ax.text(0.00015, 3, subplot_label) 168 fig.tight_layout() 169 170 if output_dir is not None: 171 os.makedirs(output_dir, exist_ok=True) 172 # プロットをPNG形式で保存 173 filename: str = f"{output_basename}.png" 174 fig.savefig(os.path.join(output_dir, filename), dpi=300) 175 if show_fig: 176 plt.show() 177 else: 178 plt.close(fig=fig) 179 180 def create_plot_ratio( 181 self, 182 df_processed: pd.DataFrame, 183 reference_name: str, 184 target_name: str, 185 figsize: tuple[int, int] = (10, 6), 186 output_dir: str | None = None, 187 output_basename: str = "ratio", 188 show_fig: bool = True, 189 ) -> None: 190 """ 191 ターゲットと参照の比率をプロットする。 192 193 Parameters: 194 ------ 195 df_processed : pd.DataFrame 196 処理されたデータフレーム。 197 reference_name : str 198 参照の名前。 199 target_name : str 200 ターゲットの名前。 201 figsize : tuple[int, int], optional 202 プロットのサイズ。デフォルトは(10, 6)。 203 output_dir : str | None, optional 204 プロットを保存するディレクトリ。デフォルトはNoneで、保存しない。 205 output_basename : str, optional 206 保存するファイル名のベース。デフォルトは"ratio"。 207 show_fig : bool, optional 208 プロットを表示するかどうか。デフォルトはTrue。 209 """ 210 fig = plt.figure(figsize=figsize) 211 ax = fig.add_subplot(111) 212 213 ax.plot( 214 df_processed.index, df_processed["target"] / df_processed["reference"], "o" 215 ) 216 ax.set_xscale("log") 217 ax.set_yscale("log") 218 ax.set_xlabel("f (Hz)") 219 ax.set_ylabel(f"{target_name} / {reference_name}") 220 ax.set_title(f"{target_name}と{reference_name}の比") 221 222 if output_dir is not None: 223 # プロットをPNG形式で保存 224 filename: str = f"{output_basename}-{reference_name}_{target_name}.png" 225 fig.savefig(os.path.join(output_dir, filename), dpi=300) 226 if show_fig: 227 plt.show() 228 else: 229 plt.close(fig=fig) 230 231 @classmethod 232 def create_plot_tf_curves_from_csv( 233 cls, 234 file_path: str, 235 gas_configs: list[tuple[str, str, str, str]], 236 output_dir: str | None = None, 237 output_basename: str = "all_tf_curves", 238 col_datetime: str = "Date", 239 add_xlabel: bool = True, 240 label_x: str = "f (Hz)", 241 label_y: str = "無次元コスペクトル比", 242 label_avg: str = "Avg.", 243 label_co_ref: str = "Tv", 244 line_colors: list[str] | None = None, 245 font_family: list[str] = ["Arial", "MS Gothic"], 246 font_size: float = 20, 247 save_fig: bool = True, 248 show_fig: bool = True, 249 ) -> None: 250 """ 251 複数の伝達関数の係数をプロットし、各ガスの平均値を表示します。 252 各ガスのデータをCSVファイルから読み込み、指定された設定に基づいてプロットを生成します。 253 プロットはオプションで保存することも可能です。 254 255 Parameters: 256 ------ 257 file_path : str 258 伝達関数の係数が格納されたCSVファイルのパス。 259 gas_configs : list[tuple[str, str, str, str]] 260 ガスごとの設定のリスト。各タプルは以下の要素を含む: 261 (係数のカラム名, ガスの表示ラベル, 平均線の色, 出力ファイル用のガス名) 262 例: [("a_ch4-used", "CH$_4$", "red", "ch4")] 263 output_dir : str | None, optional 264 出力ディレクトリ。Noneの場合は保存しない。 265 output_basename : str, optional 266 出力ファイル名のベース。デフォルトは"all_tf_curves"。 267 col_datetime : str, optional 268 日付情報が格納されているカラム名。デフォルトは"Date"。 269 add_xlabel : bool, optional 270 x軸ラベルを追加するかどうか。デフォルトはTrue。 271 label_x : str, optional 272 x軸のラベル。デフォルトは"f (Hz)"。 273 label_y : str, optional 274 y軸のラベル。デフォルトは"無次元コスペクトル比"。 275 label_avg : str, optional 276 平均値のラベル。デフォルトは"Avg."。 277 line_colors : list[str] | None, optional 278 各日付のデータに使用する色のリスト。 279 font_family : list[str], optional 280 使用するフォントファミリーのリスト。 281 font_size : float, optional 282 フォントサイズ。 283 save_fig : bool, optional 284 プロットを保存するかどうか。デフォルトはTrue。 285 show_fig : bool, optional 286 プロットを表示するかどうか。デフォルトはTrue。 287 """ 288 # プロットパラメータの設定 289 plt.rcParams.update( 290 { 291 "font.family": font_family, 292 "font.size": font_size, 293 "axes.labelsize": font_size, 294 "axes.titlesize": font_size, 295 "xtick.labelsize": font_size, 296 "ytick.labelsize": font_size, 297 "legend.fontsize": font_size, 298 } 299 ) 300 301 # CSVファイルを読み込む 302 df = pd.read_csv(file_path) 303 304 # 各ガスについてプロット 305 for col_coef_a, label_gas, base_color, gas_name in gas_configs: 306 fig = plt.figure(figsize=(10, 6)) 307 308 # データ数に応じたデフォルトの色リストを作成 309 if line_colors is None: 310 default_colors = [ 311 "#1f77b4", 312 "#ff7f0e", 313 "#2ca02c", 314 "#d62728", 315 "#9467bd", 316 "#8c564b", 317 "#e377c2", 318 "#7f7f7f", 319 "#bcbd22", 320 "#17becf", 321 ] 322 n_dates = len(df) 323 plot_colors = (default_colors * (n_dates // len(default_colors) + 1))[ 324 :n_dates 325 ] 326 else: 327 plot_colors = line_colors 328 329 # 全てのa値を用いて伝達関数をプロット 330 for i, row in enumerate(df.iterrows()): 331 a = row[1][col_coef_a] 332 date = row[1][col_datetime] 333 x_fit = np.logspace(-3, 1, 1000) 334 y_fit = cls.transfer_function(x_fit, a) 335 plt.plot( 336 x_fit, 337 y_fit, 338 "-", 339 color=plot_colors[i], 340 alpha=0.7, 341 label=f"{date} (a = {a:.3f})", 342 ) 343 344 # 平均のa値を用いた伝達関数をプロット 345 a_mean = df[col_coef_a].mean() 346 x_fit = np.logspace(-3, 1, 1000) 347 y_fit = cls.transfer_function(x_fit, a_mean) 348 plt.plot( 349 x_fit, 350 y_fit, 351 "-", 352 color=base_color, 353 linewidth=3, 354 label=f"{label_avg} (a = {a_mean:.3f})", 355 ) 356 357 # グラフの設定 358 label_y_formatted: str = f"{label_y}\n({label_gas} / {label_co_ref})" 359 plt.xscale("log") 360 if add_xlabel: 361 plt.xlabel(label_x) 362 plt.ylabel(label_y_formatted) 363 plt.legend(loc="lower left", fontsize=font_size - 6) 364 plt.grid(True, which="both", ls="-", alpha=0.2) 365 plt.tight_layout() 366 367 if save_fig: 368 if output_dir is None: 369 raise ValueError( 370 "save_fig=Trueのとき、output_dirに有効なディレクトリパスを指定する必要があります。" 371 ) 372 os.makedirs(output_dir, exist_ok=True) 373 output_path: str = os.path.join( 374 output_dir, f"{output_basename}-{gas_name}.png" 375 ) 376 plt.savefig(output_path, dpi=300, bbox_inches="tight") 377 if show_fig: 378 plt.show() 379 else: 380 plt.close(fig=fig) 381 382 def create_plot_transfer_function( 383 self, 384 a: float, 385 df_processed: pd.DataFrame, 386 reference_name: str, 387 target_name: str, 388 figsize: tuple[int, int] = (10, 6), 389 output_dir: str | None = None, 390 output_basename: str = "tf", 391 show_fig: bool = True, 392 add_xlabel: bool = True, 393 label_x: str = "f (Hz)", 394 label_y: str = "コスペクトル比", 395 label_gas: str | None = None, 396 ) -> None: 397 """ 398 伝達関数とそのフィットをプロットする。 399 400 Parameters: 401 ------ 402 a : float 403 伝達関数の係数。 404 df_processed : pd.DataFrame 405 処理されたデータフレーム。 406 reference_name : str 407 参照の名前。 408 target_name : str 409 ターゲットの名前。 410 figsize : tuple[int, int], optional 411 プロットのサイズ。デフォルトは(10, 6)。 412 output_dir : str | None, optional 413 プロットを保存するディレクトリ。デフォルトはNoneで、保存しない。 414 output_basename : str, optional 415 保存するファイル名のベース。デフォルトは"tf"。 416 show_fig : bool, optional 417 プロットを表示するかどうか。デフォルトはTrue。 418 """ 419 df_cutoff: pd.DataFrame = self._cutoff_df(df_processed) 420 421 fig = plt.figure(figsize=figsize) 422 ax = fig.add_subplot(111) 423 424 ax.plot( 425 df_cutoff.index, 426 df_cutoff["target"] / df_cutoff["reference"], 427 "o", 428 label=f"{target_name} / {reference_name}", 429 ) 430 431 x_fit = np.logspace( 432 np.log10(self._cutoff_freq_low), np.log10(self._cutoff_freq_high), 1000 433 ) 434 y_fit = self.transfer_function(x_fit, a) 435 ax.plot(x_fit, y_fit, "-", label=f"フィット (a = {a:.4f})") 436 437 ax.set_xscale("log") 438 # グラフの設定 439 label_y_formatted: str = f"{label_y}\n({label_gas} / 顕熱)" 440 plt.xscale("log") 441 if add_xlabel: 442 plt.xlabel(label_x) 443 plt.ylabel(label_y_formatted) 444 ax.legend() 445 446 if output_dir is not None: 447 # プロットをPNG形式で保存 448 filename: str = f"{output_basename}-{reference_name}_{target_name}.png" 449 fig.savefig(os.path.join(output_dir, filename), dpi=300) 450 if show_fig: 451 plt.show() 452 else: 453 plt.close(fig=fig) 454 455 def process_data(self, col_reference: str, col_target: str) -> pd.DataFrame: 456 """ 457 指定されたキーに基づいてデータを処理する。 458 459 Parameters: 460 ------ 461 col_reference : str 462 参照データのカラム名。 463 col_target : str 464 ターゲットデータのカラム名。 465 466 Returns: 467 ------ 468 pd.DataFrame 469 処理されたデータフレーム。 470 """ 471 df: pd.DataFrame = self._df.copy() 472 col_freq: str = self._col_freq 473 474 # データ型の確認と変換 475 df[col_freq] = pd.to_numeric(df[col_freq], errors="coerce") 476 df[col_reference] = pd.to_numeric(df[col_reference], errors="coerce") 477 df[col_target] = pd.to_numeric(df[col_target], errors="coerce") 478 479 # NaNを含む行を削除 480 df = df.dropna(subset=[col_freq, col_reference, col_target]) 481 482 # グループ化と中央値の計算 483 grouped = df.groupby(col_freq) 484 reference_data = grouped[col_reference].median() 485 target_data = grouped[col_target].median() 486 487 df_processed = pd.DataFrame( 488 {"reference": reference_data, "target": target_data} 489 ) 490 491 # 異常な比率を除去 492 df_processed.loc[ 493 ( 494 (df_processed["target"] / df_processed["reference"] > 1) 495 | (df_processed["target"] / df_processed["reference"] < 0) 496 ) 497 ] = np.nan 498 df_processed = df_processed.dropna() 499 500 return df_processed 501 502 def _cutoff_df(self, df: pd.DataFrame) -> pd.DataFrame: 503 """ 504 カットオフ周波数に基づいてDataFrameを加工するメソッド 505 506 Parameters: 507 ------ 508 df : pd.DataFrame 509 加工対象のデータフレーム。 510 511 Returns: 512 ------ 513 pd.DataFrame 514 カットオフ周波数に基づいて加工されたデータフレーム。 515 """ 516 df_cutoff: pd.DataFrame = df.loc[ 517 (self._cutoff_freq_low <= df.index) & (df.index <= self._cutoff_freq_high) 518 ] 519 return df_cutoff 520 521 @classmethod 522 def transfer_function(cls, x: np.ndarray, a: float) -> np.ndarray: 523 """ 524 伝達関数を計算する。 525 526 Parameters: 527 ------ 528 x : np.ndarray 529 周波数の配列。 530 a : float 531 伝達関数の係数。 532 533 Returns: 534 ------ 535 np.ndarray 536 伝達関数の値。 537 """ 538 return np.exp(-np.log(np.sqrt(2)) * np.power(x / a, 2)) 539 540 @staticmethod 541 def _load_data(file_path: str) -> pd.DataFrame: 542 """ 543 CSVファイルからデータを読み込む。 544 545 Parameters: 546 ------ 547 file_path : str 548 csvファイルのパス。 549 550 Returns: 551 ------ 552 pd.DataFrame 553 読み込まれたデータフレーム。 554 """ 555 tmp = pd.read_csv(file_path, header=None, nrows=1, skiprows=0) 556 header = tmp.loc[tmp.index[0]] 557 df = pd.read_csv(file_path, header=None, skiprows=1) 558 df.columns = header 559 return df
このクラスは、CSVファイルからデータを読み込み、処理し、 伝達関数を計算してプロットするための機能を提供します。
この実装は Moore (1986) の論文に基づいています。
17 def __init__( 18 self, 19 file_path: str, 20 col_freq: str, 21 cutoff_freq_low: float = 0.01, 22 cutoff_freq_high: float = 1, 23 ): 24 """ 25 TransferFunctionCalculatorクラスのコンストラクタ。 26 27 Parameters: 28 ------ 29 file_path : str 30 分析対象のCSVファイルのパス。 31 col_freq : str 32 周波数のキー。 33 cutoff_freq_low : float 34 カットオフ周波数の最低値。 35 cutoff_freq_high : float 36 カットオフ周波数の最高値。 37 """ 38 self._col_freq: str = col_freq 39 self._cutoff_freq_low: float = cutoff_freq_low 40 self._cutoff_freq_high: float = cutoff_freq_high 41 self._df: pd.DataFrame = TransferFunctionCalculator._load_data(file_path)
TransferFunctionCalculatorクラスのコンストラクタ。
Parameters:
file_path : str
分析対象のCSVファイルのパス。
col_freq : str
周波数のキー。
cutoff_freq_low : float
カットオフ周波数の最低値。
cutoff_freq_high : float
カットオフ周波数の最高値。
43 def calculate_transfer_function( 44 self, col_reference: str, col_target: str 45 ) -> tuple[float, float, pd.DataFrame]: 46 """ 47 伝達関数の係数を計算する。 48 49 Parameters: 50 ------ 51 col_reference : str 52 参照データのカラム名。 53 col_target : str 54 ターゲットデータのカラム名。 55 56 Returns: 57 ------ 58 tuple[float, float, pandas.DataFrame] 59 伝達関数の係数aとその標準誤差、および計算に用いたDataFrame。 60 """ 61 df_processed: pd.DataFrame = self.process_data( 62 col_reference=col_reference, col_target=col_target 63 ) 64 df_cutoff: pd.DataFrame = self._cutoff_df(df_processed) 65 66 array_x = np.array(df_cutoff.index) 67 array_y = np.array(df_cutoff["target"] / df_cutoff["reference"]) 68 69 # フィッティングパラメータと共分散行列を取得 70 popt, pcov = curve_fit( 71 TransferFunctionCalculator.transfer_function, array_x, array_y 72 ) 73 74 # 標準誤差を計算(共分散行列の対角成分の平方根) 75 perr = np.sqrt(np.diag(pcov)) 76 77 # 係数aとその標準誤差、および計算に用いたDataFrameを返す 78 return popt[0], perr[0], df_processed
伝達関数の係数を計算する。
Parameters:
col_reference : str
参照データのカラム名。
col_target : str
ターゲットデータのカラム名。
Returns:
tuple[float, float, pandas.DataFrame]
伝達関数の係数aとその標準誤差、および計算に用いたDataFrame。
80 def create_plot_co_spectra( 81 self, 82 col1: str, 83 col2: str, 84 color1: str = "gray", 85 color2: str = "red", 86 figsize: tuple[int, int] = (10, 8), 87 label1: str | None = None, 88 label2: str | None = None, 89 output_dir: str | None = None, 90 output_basename: str = "co", 91 add_legend: bool = True, 92 add_xy_labels: bool = True, 93 show_fig: bool = True, 94 subplot_label: str | None = "(a)", 95 window_size: int = 5, # 移動平均の窓サイズ 96 markersize: float = 14, 97 ) -> None: 98 """ 99 2種類のコスペクトルをプロットする。 100 101 Parameters: 102 ------ 103 col1 : str 104 1つ目のコスペクトルデータのカラム名。 105 col2 : str 106 2つ目のコスペクトルデータのカラム名。 107 color1 : str, optional 108 1つ目のデータの色。デフォルトは'gray'。 109 color2 : str, optional 110 2つ目のデータの色。デフォルトは'red'。 111 figsize : tuple[int, int], optional 112 プロットのサイズ。デフォルトは(10, 8)。 113 label1 : str, optional 114 1つ目のデータのラベル名。デフォルトはNone。 115 label2 : str, optional 116 2つ目のデータのラベル名。デフォルトはNone。 117 output_dir : str | None, optional 118 プロットを保存するディレクトリ。デフォルトはNoneで、保存しない。 119 output_basename : str, optional 120 保存するファイル名のベース。デフォルトは"co"。 121 show_fig : bool, optional 122 プロットを表示するかどうか。デフォルトはTrue。 123 subplot_label : str | None, optional 124 左上に表示するサブプロットラベル。デフォルトは"(a)"。 125 window_size : int, optional 126 移動平均の窓サイズ。デフォルトは5。 127 """ 128 df: pd.DataFrame = self._df.copy() 129 # データの取得と移動平均の適用 130 data1 = df[df[col1] > 0].groupby(self._col_freq)[col1].median() 131 data2 = df[df[col2] > 0].groupby(self._col_freq)[col2].median() 132 133 data1 = data1.rolling(window=window_size, center=True, min_periods=1).mean() 134 data2 = data2.rolling(window=window_size, center=True, min_periods=1).mean() 135 136 fig = plt.figure(figsize=figsize) 137 ax = fig.add_subplot(111) 138 139 # マーカーサイズを設定して見やすくする 140 ax.plot( 141 data1.index, data1, "o", color=color1, label=label1, markersize=markersize 142 ) 143 ax.plot( 144 data2.index, data2, "o", color=color2, label=label2, markersize=markersize 145 ) 146 ax.plot([0.01, 10], [10, 0.001], "-", color="black") 147 ax.text(0.25, 0.4, "-4/3") 148 149 ax.grid(True, alpha=0.3) 150 ax.set_xscale("log") 151 ax.set_yscale("log") 152 ax.set_xlim(0.0001, 10) 153 ax.set_ylim(0.0001, 10) 154 if add_xy_labels: 155 ax.set_xlabel("f (Hz)") 156 ax.set_ylabel("無次元コスペクトル") 157 158 if add_legend: 159 ax.legend( 160 bbox_to_anchor=(0.05, 1), 161 loc="lower left", 162 fontsize=16, 163 ncol=3, 164 frameon=False, 165 ) 166 if subplot_label is not None: 167 ax.text(0.00015, 3, subplot_label) 168 fig.tight_layout() 169 170 if output_dir is not None: 171 os.makedirs(output_dir, exist_ok=True) 172 # プロットをPNG形式で保存 173 filename: str = f"{output_basename}.png" 174 fig.savefig(os.path.join(output_dir, filename), dpi=300) 175 if show_fig: 176 plt.show() 177 else: 178 plt.close(fig=fig)
2種類のコスペクトルをプロットする。
Parameters:
col1 : str
1つ目のコスペクトルデータのカラム名。
col2 : str
2つ目のコスペクトルデータのカラム名。
color1 : str, optional
1つ目のデータの色。デフォルトは'gray'。
color2 : str, optional
2つ目のデータの色。デフォルトは'red'。
figsize : tuple[int, int], optional
プロットのサイズ。デフォルトは(10, 8)。
label1 : str, optional
1つ目のデータのラベル名。デフォルトはNone。
label2 : str, optional
2つ目のデータのラベル名。デフォルトはNone。
output_dir : str | None, optional
プロットを保存するディレクトリ。デフォルトはNoneで、保存しない。
output_basename : str, optional
保存するファイル名のベース。デフォルトは"co"。
show_fig : bool, optional
プロットを表示するかどうか。デフォルトはTrue。
subplot_label : str | None, optional
左上に表示するサブプロットラベル。デフォルトは"(a)"。
window_size : int, optional
移動平均の窓サイズ。デフォルトは5。
180 def create_plot_ratio( 181 self, 182 df_processed: pd.DataFrame, 183 reference_name: str, 184 target_name: str, 185 figsize: tuple[int, int] = (10, 6), 186 output_dir: str | None = None, 187 output_basename: str = "ratio", 188 show_fig: bool = True, 189 ) -> None: 190 """ 191 ターゲットと参照の比率をプロットする。 192 193 Parameters: 194 ------ 195 df_processed : pd.DataFrame 196 処理されたデータフレーム。 197 reference_name : str 198 参照の名前。 199 target_name : str 200 ターゲットの名前。 201 figsize : tuple[int, int], optional 202 プロットのサイズ。デフォルトは(10, 6)。 203 output_dir : str | None, optional 204 プロットを保存するディレクトリ。デフォルトはNoneで、保存しない。 205 output_basename : str, optional 206 保存するファイル名のベース。デフォルトは"ratio"。 207 show_fig : bool, optional 208 プロットを表示するかどうか。デフォルトはTrue。 209 """ 210 fig = plt.figure(figsize=figsize) 211 ax = fig.add_subplot(111) 212 213 ax.plot( 214 df_processed.index, df_processed["target"] / df_processed["reference"], "o" 215 ) 216 ax.set_xscale("log") 217 ax.set_yscale("log") 218 ax.set_xlabel("f (Hz)") 219 ax.set_ylabel(f"{target_name} / {reference_name}") 220 ax.set_title(f"{target_name}と{reference_name}の比") 221 222 if output_dir is not None: 223 # プロットをPNG形式で保存 224 filename: str = f"{output_basename}-{reference_name}_{target_name}.png" 225 fig.savefig(os.path.join(output_dir, filename), dpi=300) 226 if show_fig: 227 plt.show() 228 else: 229 plt.close(fig=fig)
ターゲットと参照の比率をプロットする。
Parameters:
df_processed : pd.DataFrame
処理されたデータフレーム。
reference_name : str
参照の名前。
target_name : str
ターゲットの名前。
figsize : tuple[int, int], optional
プロットのサイズ。デフォルトは(10, 6)。
output_dir : str | None, optional
プロットを保存するディレクトリ。デフォルトはNoneで、保存しない。
output_basename : str, optional
保存するファイル名のベース。デフォルトは"ratio"。
show_fig : bool, optional
プロットを表示するかどうか。デフォルトはTrue。
231 @classmethod 232 def create_plot_tf_curves_from_csv( 233 cls, 234 file_path: str, 235 gas_configs: list[tuple[str, str, str, str]], 236 output_dir: str | None = None, 237 output_basename: str = "all_tf_curves", 238 col_datetime: str = "Date", 239 add_xlabel: bool = True, 240 label_x: str = "f (Hz)", 241 label_y: str = "無次元コスペクトル比", 242 label_avg: str = "Avg.", 243 label_co_ref: str = "Tv", 244 line_colors: list[str] | None = None, 245 font_family: list[str] = ["Arial", "MS Gothic"], 246 font_size: float = 20, 247 save_fig: bool = True, 248 show_fig: bool = True, 249 ) -> None: 250 """ 251 複数の伝達関数の係数をプロットし、各ガスの平均値を表示します。 252 各ガスのデータをCSVファイルから読み込み、指定された設定に基づいてプロットを生成します。 253 プロットはオプションで保存することも可能です。 254 255 Parameters: 256 ------ 257 file_path : str 258 伝達関数の係数が格納されたCSVファイルのパス。 259 gas_configs : list[tuple[str, str, str, str]] 260 ガスごとの設定のリスト。各タプルは以下の要素を含む: 261 (係数のカラム名, ガスの表示ラベル, 平均線の色, 出力ファイル用のガス名) 262 例: [("a_ch4-used", "CH$_4$", "red", "ch4")] 263 output_dir : str | None, optional 264 出力ディレクトリ。Noneの場合は保存しない。 265 output_basename : str, optional 266 出力ファイル名のベース。デフォルトは"all_tf_curves"。 267 col_datetime : str, optional 268 日付情報が格納されているカラム名。デフォルトは"Date"。 269 add_xlabel : bool, optional 270 x軸ラベルを追加するかどうか。デフォルトはTrue。 271 label_x : str, optional 272 x軸のラベル。デフォルトは"f (Hz)"。 273 label_y : str, optional 274 y軸のラベル。デフォルトは"無次元コスペクトル比"。 275 label_avg : str, optional 276 平均値のラベル。デフォルトは"Avg."。 277 line_colors : list[str] | None, optional 278 各日付のデータに使用する色のリスト。 279 font_family : list[str], optional 280 使用するフォントファミリーのリスト。 281 font_size : float, optional 282 フォントサイズ。 283 save_fig : bool, optional 284 プロットを保存するかどうか。デフォルトはTrue。 285 show_fig : bool, optional 286 プロットを表示するかどうか。デフォルトはTrue。 287 """ 288 # プロットパラメータの設定 289 plt.rcParams.update( 290 { 291 "font.family": font_family, 292 "font.size": font_size, 293 "axes.labelsize": font_size, 294 "axes.titlesize": font_size, 295 "xtick.labelsize": font_size, 296 "ytick.labelsize": font_size, 297 "legend.fontsize": font_size, 298 } 299 ) 300 301 # CSVファイルを読み込む 302 df = pd.read_csv(file_path) 303 304 # 各ガスについてプロット 305 for col_coef_a, label_gas, base_color, gas_name in gas_configs: 306 fig = plt.figure(figsize=(10, 6)) 307 308 # データ数に応じたデフォルトの色リストを作成 309 if line_colors is None: 310 default_colors = [ 311 "#1f77b4", 312 "#ff7f0e", 313 "#2ca02c", 314 "#d62728", 315 "#9467bd", 316 "#8c564b", 317 "#e377c2", 318 "#7f7f7f", 319 "#bcbd22", 320 "#17becf", 321 ] 322 n_dates = len(df) 323 plot_colors = (default_colors * (n_dates // len(default_colors) + 1))[ 324 :n_dates 325 ] 326 else: 327 plot_colors = line_colors 328 329 # 全てのa値を用いて伝達関数をプロット 330 for i, row in enumerate(df.iterrows()): 331 a = row[1][col_coef_a] 332 date = row[1][col_datetime] 333 x_fit = np.logspace(-3, 1, 1000) 334 y_fit = cls.transfer_function(x_fit, a) 335 plt.plot( 336 x_fit, 337 y_fit, 338 "-", 339 color=plot_colors[i], 340 alpha=0.7, 341 label=f"{date} (a = {a:.3f})", 342 ) 343 344 # 平均のa値を用いた伝達関数をプロット 345 a_mean = df[col_coef_a].mean() 346 x_fit = np.logspace(-3, 1, 1000) 347 y_fit = cls.transfer_function(x_fit, a_mean) 348 plt.plot( 349 x_fit, 350 y_fit, 351 "-", 352 color=base_color, 353 linewidth=3, 354 label=f"{label_avg} (a = {a_mean:.3f})", 355 ) 356 357 # グラフの設定 358 label_y_formatted: str = f"{label_y}\n({label_gas} / {label_co_ref})" 359 plt.xscale("log") 360 if add_xlabel: 361 plt.xlabel(label_x) 362 plt.ylabel(label_y_formatted) 363 plt.legend(loc="lower left", fontsize=font_size - 6) 364 plt.grid(True, which="both", ls="-", alpha=0.2) 365 plt.tight_layout() 366 367 if save_fig: 368 if output_dir is None: 369 raise ValueError( 370 "save_fig=Trueのとき、output_dirに有効なディレクトリパスを指定する必要があります。" 371 ) 372 os.makedirs(output_dir, exist_ok=True) 373 output_path: str = os.path.join( 374 output_dir, f"{output_basename}-{gas_name}.png" 375 ) 376 plt.savefig(output_path, dpi=300, bbox_inches="tight") 377 if show_fig: 378 plt.show() 379 else: 380 plt.close(fig=fig)
複数の伝達関数の係数をプロットし、各ガスの平均値を表示します。 各ガスのデータをCSVファイルから読み込み、指定された設定に基づいてプロットを生成します。 プロットはオプションで保存することも可能です。
Parameters:
file_path : str
伝達関数の係数が格納されたCSVファイルのパス。
gas_configs : list[tuple[str, str, str, str]]
ガスごとの設定のリスト。各タプルは以下の要素を含む:
(係数のカラム名, ガスの表示ラベル, 平均線の色, 出力ファイル用のガス名)
例: [("a_ch4-used", "CH$_4$", "red", "ch4")]
output_dir : str | None, optional
出力ディレクトリ。Noneの場合は保存しない。
output_basename : str, optional
出力ファイル名のベース。デフォルトは"all_tf_curves"。
col_datetime : str, optional
日付情報が格納されているカラム名。デフォルトは"Date"。
add_xlabel : bool, optional
x軸ラベルを追加するかどうか。デフォルトはTrue。
label_x : str, optional
x軸のラベル。デフォルトは"f (Hz)"。
label_y : str, optional
y軸のラベル。デフォルトは"無次元コスペクトル比"。
label_avg : str, optional
平均値のラベル。デフォルトは"Avg."。
line_colors : list[str] | None, optional
各日付のデータに使用する色のリスト。
font_family : list[str], optional
使用するフォントファミリーのリスト。
font_size : float, optional
フォントサイズ。
save_fig : bool, optional
プロットを保存するかどうか。デフォルトはTrue。
show_fig : bool, optional
プロットを表示するかどうか。デフォルトはTrue。
382 def create_plot_transfer_function( 383 self, 384 a: float, 385 df_processed: pd.DataFrame, 386 reference_name: str, 387 target_name: str, 388 figsize: tuple[int, int] = (10, 6), 389 output_dir: str | None = None, 390 output_basename: str = "tf", 391 show_fig: bool = True, 392 add_xlabel: bool = True, 393 label_x: str = "f (Hz)", 394 label_y: str = "コスペクトル比", 395 label_gas: str | None = None, 396 ) -> None: 397 """ 398 伝達関数とそのフィットをプロットする。 399 400 Parameters: 401 ------ 402 a : float 403 伝達関数の係数。 404 df_processed : pd.DataFrame 405 処理されたデータフレーム。 406 reference_name : str 407 参照の名前。 408 target_name : str 409 ターゲットの名前。 410 figsize : tuple[int, int], optional 411 プロットのサイズ。デフォルトは(10, 6)。 412 output_dir : str | None, optional 413 プロットを保存するディレクトリ。デフォルトはNoneで、保存しない。 414 output_basename : str, optional 415 保存するファイル名のベース。デフォルトは"tf"。 416 show_fig : bool, optional 417 プロットを表示するかどうか。デフォルトはTrue。 418 """ 419 df_cutoff: pd.DataFrame = self._cutoff_df(df_processed) 420 421 fig = plt.figure(figsize=figsize) 422 ax = fig.add_subplot(111) 423 424 ax.plot( 425 df_cutoff.index, 426 df_cutoff["target"] / df_cutoff["reference"], 427 "o", 428 label=f"{target_name} / {reference_name}", 429 ) 430 431 x_fit = np.logspace( 432 np.log10(self._cutoff_freq_low), np.log10(self._cutoff_freq_high), 1000 433 ) 434 y_fit = self.transfer_function(x_fit, a) 435 ax.plot(x_fit, y_fit, "-", label=f"フィット (a = {a:.4f})") 436 437 ax.set_xscale("log") 438 # グラフの設定 439 label_y_formatted: str = f"{label_y}\n({label_gas} / 顕熱)" 440 plt.xscale("log") 441 if add_xlabel: 442 plt.xlabel(label_x) 443 plt.ylabel(label_y_formatted) 444 ax.legend() 445 446 if output_dir is not None: 447 # プロットをPNG形式で保存 448 filename: str = f"{output_basename}-{reference_name}_{target_name}.png" 449 fig.savefig(os.path.join(output_dir, filename), dpi=300) 450 if show_fig: 451 plt.show() 452 else: 453 plt.close(fig=fig)
伝達関数とそのフィットをプロットする。
Parameters:
a : float
伝達関数の係数。
df_processed : pd.DataFrame
処理されたデータフレーム。
reference_name : str
参照の名前。
target_name : str
ターゲットの名前。
figsize : tuple[int, int], optional
プロットのサイズ。デフォルトは(10, 6)。
output_dir : str | None, optional
プロットを保存するディレクトリ。デフォルトはNoneで、保存しない。
output_basename : str, optional
保存するファイル名のベース。デフォルトは"tf"。
show_fig : bool, optional
プロットを表示するかどうか。デフォルトはTrue。
455 def process_data(self, col_reference: str, col_target: str) -> pd.DataFrame: 456 """ 457 指定されたキーに基づいてデータを処理する。 458 459 Parameters: 460 ------ 461 col_reference : str 462 参照データのカラム名。 463 col_target : str 464 ターゲットデータのカラム名。 465 466 Returns: 467 ------ 468 pd.DataFrame 469 処理されたデータフレーム。 470 """ 471 df: pd.DataFrame = self._df.copy() 472 col_freq: str = self._col_freq 473 474 # データ型の確認と変換 475 df[col_freq] = pd.to_numeric(df[col_freq], errors="coerce") 476 df[col_reference] = pd.to_numeric(df[col_reference], errors="coerce") 477 df[col_target] = pd.to_numeric(df[col_target], errors="coerce") 478 479 # NaNを含む行を削除 480 df = df.dropna(subset=[col_freq, col_reference, col_target]) 481 482 # グループ化と中央値の計算 483 grouped = df.groupby(col_freq) 484 reference_data = grouped[col_reference].median() 485 target_data = grouped[col_target].median() 486 487 df_processed = pd.DataFrame( 488 {"reference": reference_data, "target": target_data} 489 ) 490 491 # 異常な比率を除去 492 df_processed.loc[ 493 ( 494 (df_processed["target"] / df_processed["reference"] > 1) 495 | (df_processed["target"] / df_processed["reference"] < 0) 496 ) 497 ] = np.nan 498 df_processed = df_processed.dropna() 499 500 return df_processed
指定されたキーに基づいてデータを処理する。
Parameters:
col_reference : str
参照データのカラム名。
col_target : str
ターゲットデータのカラム名。
Returns:
pd.DataFrame
処理されたデータフレーム。
521 @classmethod 522 def transfer_function(cls, x: np.ndarray, a: float) -> np.ndarray: 523 """ 524 伝達関数を計算する。 525 526 Parameters: 527 ------ 528 x : np.ndarray 529 周波数の配列。 530 a : float 531 伝達関数の係数。 532 533 Returns: 534 ------ 535 np.ndarray 536 伝達関数の値。 537 """ 538 return np.exp(-np.log(np.sqrt(2)) * np.power(x / a, 2))
伝達関数を計算する。
Parameters:
x : np.ndarray
周波数の配列。
a : float
伝達関数の係数。
Returns:
np.ndarray
伝達関数の値。