py_flux_tracer

 1from .campbell.eddy_data_preprocessor import EddyDataPreprocessor
 2from .campbell.spectrum_calculator import SpectrumCalculator
 3from .commons.hotspot_data import HotspotData, HotspotType
 4from .footprint.flux_footprint_analyzer import FluxFootprintAnalyzer
 5from .mobile.correcting_utils import CorrectingUtils, CORRECTION_TYPES_PATTERN
 6from .mobile.mobile_spatial_analyzer import (
 7    EmissionData,
 8    MobileSpatialAnalyzer,
 9    MSAInputConfig,
10)
11from .monthly.monthly_converter import MonthlyConverter
12from .monthly.monthly_figures_generator import MonthlyFiguresGenerator
13from .transfer_function.fft_files_reorganizer import FftFileReorganizer
14from .transfer_function.transfer_function_calculator import TransferFunctionCalculator
15
16"""
17versionを動的に設定する。
18`./_version.py`がない場合はsetuptools_scmを用いてGitからバージョン取得を試行
19それも失敗した場合にデフォルトバージョン(0.0.0)を設定
20"""
21try:
22    from ._version import __version__  # type:ignore
23except ImportError:
24    try:
25        from setuptools_scm import get_version
26
27        __version__ = get_version(root="..", relative_to=__file__)
28    except Exception:
29        __version__ = "0.0.0"
30
31__version__ = __version__
32"""
33@private
34このモジュールはバージョン情報の管理に使用され、ドキュメントには含めません。
35private属性を適用するために再宣言してdocstringを記述しています。
36"""
37
38# モジュールを __all__ にセット
39__all__ = [
40    "__version__",
41    "EddyDataPreprocessor",
42    "SpectrumCalculator",
43    "HotspotData",
44    "HotspotType",
45    "FluxFootprintAnalyzer",
46    "CorrectingUtils",
47    "CORRECTION_TYPES_PATTERN",
48    "EmissionData",
49    "MobileSpatialAnalyzer",
50    "MSAInputConfig",
51    "MonthlyConverter",
52    "MonthlyFiguresGenerator",
53    "FftFileReorganizer",
54    "TransferFunctionCalculator",
55]
class EddyDataPreprocessor:
  13class EddyDataPreprocessor:
  14    def __init__(
  15        self,
  16        fs: float = 10,
  17        logger: Logger | None = None,
  18        logging_debug: bool = False,
  19    ):
  20        """
  21        渦相関法によって記録されたデータファイルを処理するクラス。
  22
  23        Parameters
  24        ----------
  25            fs (float): サンプリング周波数。
  26            logger (Logger | None): 使用するロガー。Noneの場合は新しいロガーを作成します。
  27            logging_debug (bool): ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。
  28        """
  29        self.fs: float = fs
  30
  31        # ロガー
  32        log_level: int = INFO
  33        if logging_debug:
  34            log_level = DEBUG
  35        self.logger: Logger = EddyDataPreprocessor.setup_logger(logger, log_level)
  36
  37    def add_uvw_columns(self, df: pd.DataFrame) -> pd.DataFrame:
  38        """
  39        DataFrameに水平風速u、v、鉛直風速wの列を追加する関数。
  40        各成分のキーは`wind_u`、`wind_v`、`wind_w`である。
  41        
  42        Parameters
  43        -----
  44            df : pd.DataFrame
  45                風速データを含むDataFrame
  46
  47        Returns
  48        -----
  49            pd.DataFrame
  50                水平風速u、v、鉛直風速wの列を追加したDataFrame
  51        """
  52        required_columns: list[str] = ["Ux", "Uy", "Uz"]
  53        # 必要な列がDataFrameに存在するか確認
  54        for column in required_columns:
  55            if column not in df.columns:
  56                raise ValueError(f"必要な列 '{column}' がDataFrameに存在しません。")
  57
  58        processed_df: pd.DataFrame = df.copy()
  59        # pandasの.valuesを使用してnumpy配列を取得し、その型をnp.ndarrayに明示的にキャストする
  60        wind_x_array: np.ndarray = np.array(processed_df["Ux"].values)
  61        wind_y_array: np.ndarray = np.array(processed_df["Uy"].values)
  62        wind_z_array: np.ndarray = np.array(processed_df["Uz"].values)
  63
  64        # 平均風向を計算
  65        wind_direction: float = EddyDataPreprocessor._wind_direction(
  66            wind_x_array, wind_y_array
  67        )
  68
  69        # 水平方向に座標回転を行u, v成分を求める
  70        wind_u_array, wind_v_array = EddyDataPreprocessor._horizontal_wind_speed(
  71            wind_x_array, wind_y_array, wind_direction
  72        )
  73        wind_w_array: np.ndarray = wind_z_array  # wはz成分そのまま
  74
  75        # u, wから風の迎角を計算
  76        wind_inclination: float = EddyDataPreprocessor._wind_inclination(
  77            wind_u_array, wind_w_array
  78        )
  79
  80        # 2回座標回転を行い、u, wを求める
  81        wind_u_array_rotated, wind_w_array_rotated = (
  82            EddyDataPreprocessor._vertical_rotation(
  83                wind_u_array, wind_w_array, wind_inclination
  84            )
  85        )
  86
  87        processed_df["wind_u"] = wind_u_array_rotated
  88        processed_df["wind_v"] = wind_v_array
  89        processed_df["wind_w"] = wind_w_array_rotated
  90        processed_df["rad_wind_dir"] = wind_direction
  91        processed_df["rad_wind_inc"] = wind_inclination
  92        processed_df["degree_wind_dir"] = np.degrees(wind_direction)
  93        processed_df["degree_wind_inc"] = np.degrees(wind_inclination)
  94
  95        return processed_df
  96
  97    def analyze_lag_times(
  98        self,
  99        input_dir: str,
 100        figsize: tuple[float, float] = (10, 8),
 101        input_files_pattern: str = r"Eddy_(\d+)",
 102        input_files_suffix: str = ".dat",
 103        col1: str = "wind_w",
 104        col2_list: list[str] = ["Tv"],
 105        median_range: float = 20,
 106        metadata_rows: int = 4,
 107        output_dir: str | None = None,
 108        output_tag: str = "",
 109        plot_range_tuple: tuple = (-50, 200),
 110        print_results: bool = True,
 111        skiprows: list[int] = [0, 2, 3],
 112        use_resampling: bool = True,
 113    ) -> dict[str, float]:
 114        """
 115        遅れ時間(ラグ)の統計分析を行い、指定されたディレクトリ内のデータファイルを処理します。
 116        解析結果とメタデータはCSVファイルとして出力されます。
 117
 118        Parameters:
 119        -----
 120            input_dir : str
 121                入力データファイルが格納されているディレクトリのパス。
 122            figsize : tuple[float, float]
 123                プロットのサイズ(幅、高さ)。
 124            input_files_pattern : str
 125                入力ファイル名のパターン(正規表現)。
 126            input_files_suffix : str
 127                入力ファイルの拡張子。
 128            col1 : str
 129                基準変数の列名。
 130            col2_list : list[str]
 131                比較変数の列名のリスト。
 132            median_range : float
 133                中央値を中心とした範囲。
 134            metadata_rows : int
 135                メタデータの行数。
 136            output_dir : str | None
 137                出力ディレクトリのパス。Noneの場合は保存しない。
 138            output_tag : str
 139                出力ファイルに付与するタグ。デフォルトは空文字で、何も付与されない。
 140            plot_range_tuple : tuple
 141                ヒストグラムの表示範囲。
 142            print_results : bool
 143                結果をコンソールに表示するかどうか。
 144            skiprows : list[int]
 145                スキップする行番号のリスト。
 146            use_resampling : bool
 147                データをリサンプリングするかどうか。
 148                inputするファイルが既にリサンプリング済みの場合はFalseでよい。
 149                デフォルトはTrue。
 150
 151        Returns:
 152        -----
 153            dict[str, float]
 154                各変数の遅れ時間(平均値を採用)を含む辞書。
 155        """
 156        if output_dir is None:
 157            self.logger.warn(
 158                "output_dirが指定されていません。解析結果を保存する場合は、有効なディレクトリを指定してください。"
 159            )
 160        all_lags_indices: list[list[int]] = []
 161        results: dict[str, float] = {}
 162
 163        # メイン処理
 164        # ファイル名に含まれる数字に基づいてソート
 165        csv_files = EddyDataPreprocessor._get_sorted_files(
 166            input_dir, input_files_pattern, input_files_suffix
 167        )
 168        if not csv_files:
 169            raise FileNotFoundError(
 170                f"There is no '{input_files_suffix}' file to process; input_dir: '{input_dir}', input_files_suffix: '{input_files_suffix}'"
 171            )
 172
 173        for file in tqdm(csv_files, desc="Calculating"):
 174            path: str = os.path.join(input_dir, file)
 175            if use_resampling:
 176                df, _ = self.get_resampled_df(
 177                    filepath=path, metadata_rows=metadata_rows, skiprows=skiprows
 178                )
 179            else:
 180                df = pd.read_csv(path, skiprows=skiprows)
 181            df = self.add_uvw_columns(df)
 182            lags_list = EddyDataPreprocessor._calculate_lag_time(
 183                df,
 184                col1,
 185                col2_list,
 186            )
 187            all_lags_indices.append(lags_list)
 188        self.logger.info("すべてのCSVファイルにおける遅れ時間が計算されました。")
 189
 190        # Convert all_lags_indices to a DataFrame
 191        lags_indices_df: pd.DataFrame = pd.DataFrame(
 192            all_lags_indices, columns=col2_list
 193        )
 194
 195        # フォーマット用のキーの最大の長さ
 196        max_col_name_length: int = max(len(column) for column in lags_indices_df.columns)
 197
 198        if print_results:
 199            self.logger.info(f"カラム`{col1}`に対する遅れ時間を表示します。")
 200
 201        # 結果を格納するためのリスト
 202        output_data = []
 203
 204        for column in lags_indices_df.columns:
 205            data: pd.Series = lags_indices_df[column]
 206
 207            # ヒストグラムの作成
 208            plt.figure(figsize=figsize)
 209            plt.hist(data, bins=20, range=plot_range_tuple)
 210            plt.title(f"Delays of {column}")
 211            plt.xlabel("Seconds")
 212            plt.ylabel("Frequency")
 213            plt.xlim(plot_range_tuple)
 214
 215            # ファイルとして保存するか
 216            if output_dir is not None:
 217                os.makedirs(output_dir, exist_ok=True)
 218                filename: str = f"lags_histogram-{column}{output_tag}.png"
 219                filepath: str = os.path.join(output_dir, filename)
 220                plt.savefig(filepath, dpi=300, bbox_inches="tight")
 221                plt.close()
 222
 223            # 中央値を計算し、その周辺のデータのみを使用
 224            median_value = np.median(data)
 225            filtered_data: pd.Series = data[
 226                (data >= median_value - median_range)
 227                & (data <= median_value + median_range)
 228            ]
 229
 230            # 平均値を計算
 231            mean_value = np.mean(filtered_data)
 232            mean_seconds: float = float(mean_value / self.fs)  # 統計値を秒に変換
 233            results[column] = mean_seconds
 234
 235            # 結果とメタデータを出力データに追加
 236            output_data.append(
 237                {
 238                    "col1": col1,
 239                    "col2": column,
 240                    "col2_lag": round(mean_seconds, 2),  # 数値として小数点2桁を保持
 241                    "lag_unit": "s",
 242                    "median_range": median_range,
 243                }
 244            )
 245
 246            if print_results:
 247                print(f"{column:<{max_col_name_length}} : {mean_seconds:.2f} s")
 248
 249        # 結果をCSVファイルとして出力
 250        if output_dir is not None:
 251            output_df: pd.DataFrame = pd.DataFrame(output_data)
 252            csv_filepath: str = os.path.join(
 253                output_dir, f"lags_results{output_tag}.csv"
 254            )
 255            output_df.to_csv(csv_filepath, index=False, encoding="utf-8")
 256            self.logger.info(f"解析結果をCSVファイルに保存しました: {csv_filepath}")
 257
 258        return results
 259
 260    def get_resampled_df(
 261        self,
 262        filepath: str,
 263        index_column: str = "TIMESTAMP",
 264        index_format: str = "%Y-%m-%d %H:%M:%S.%f",
 265        interpolate: bool = True,
 266        numeric_columns: list[str] = [
 267            "Ux",
 268            "Uy",
 269            "Uz",
 270            "Tv",
 271            "diag_sonic",
 272            "CO2_new",
 273            "H2O",
 274            "diag_irga",
 275            "cell_tmpr",
 276            "cell_press",
 277            "Ultra_CH4_ppm",
 278            "Ultra_C2H6_ppb",
 279            "Ultra_H2O_ppm",
 280            "Ultra_CH4_ppm_C",
 281            "Ultra_C2H6_ppb_C",
 282        ],
 283        metadata_rows: int = 4,
 284        skiprows: list[int] = [0, 2, 3],
 285        is_already_resampled: bool = False,
 286    ) -> tuple[pd.DataFrame, list[str]]:
 287        """
 288        CSVファイルを読み込み、前処理を行う
 289
 290        前処理の手順は以下の通りです:
 291        1. 不要な行を削除する。デフォルト(`skiprows=[0, 2, 3]`)の場合は、2行目をヘッダーとして残し、1、3、4行目が削除される。
 292        2. 数値データを float 型に変換する
 293        3. TIMESTAMP列をDateTimeインデックスに設定する
 294        4. エラー値をNaNに置き換える
 295        5. 指定されたサンプリングレートでリサンプリングする
 296        6. 欠損値(NaN)を前後の値から線形補間する
 297        7. DateTimeインデックスを削除する
 298        
 299        Parameters:
 300        -----
 301            filepath : str
 302                読み込むCSVファイルのパス
 303            index_column : str, optional
 304                インデックスに使用する列名。デフォルトは'TIMESTAMP'。
 305            index_format : str, optional
 306                インデックスの日付形式。デフォルトは'%Y-%m-%d %H:%M:%S.%f'。
 307            interpolate : bool, optional
 308                欠損値の補完を適用するフラグ。デフォルトはTrue。
 309            numeric_columns : list[str], optional
 310                数値型に変換する列名のリスト。
 311                デフォルトは["Ux", "Uy", "Uz", "Tv", "diag_sonic", "CO2_new", "H2O", "diag_irga", "cell_tmpr", "cell_press", "Ultra_CH4_ppm", "Ultra_C2H6_ppb", "Ultra_H2O_ppm", "Ultra_CH4_ppm_C", "Ultra_C2H6_ppb_C"]。
 312            metadata_rows : int, optional
 313                メタデータとして読み込む行数。デフォルトは4。
 314            skiprows : list[int], optional
 315                スキップする行インデックスのリスト。デフォルトは[0, 2, 3]のため、1, 3, 4行目がスキップされる。
 316            is_already_resampled : bool
 317                既にリサンプリング&欠損補間されているか。Trueの場合はfloat変換などの処理のみ適用する。
 318
 319        Returns:
 320        -----
 321            tuple[pd.DataFrame, list[str]]
 322                前処理済みのデータフレームとメタデータのリスト。
 323        """
 324        # メタデータを読み込む
 325        metadata: list[str] = []
 326        with open(filepath, "r") as f:
 327            for _ in range(metadata_rows):
 328                line = f.readline().strip()
 329                metadata.append(line.replace('"', ""))
 330
 331        # CSVファイルを読み込む
 332        df: pd.DataFrame = pd.read_csv(filepath, skiprows=skiprows)
 333
 334        # 数値データをfloat型に変換する
 335        for col in numeric_columns:
 336            if col in df.columns:
 337                df[col] = pd.to_numeric(df[col], errors="coerce")
 338
 339        if not is_already_resampled:
 340            # μ秒がない場合は".0"を追加する
 341            df[index_column] = df[index_column].apply(
 342                lambda x: f"{x}.0" if "." not in x else x
 343            )
 344            # TIMESTAMPをDateTimeインデックスに設定する
 345            df[index_column] = pd.to_datetime(df[index_column], format=index_format)
 346            df = df.set_index(index_column)
 347
 348            # リサンプリング前の有効数字を取得
 349            decimal_places = {}
 350            for col in numeric_columns:
 351                if col in df.columns:
 352                    max_decimals = (
 353                        df[col].astype(str).str.extract(r"\.(\d+)")[0].str.len().max()
 354                    )
 355                    decimal_places[col] = (
 356                        int(max_decimals) if pd.notna(max_decimals) else 0
 357                    )
 358
 359            # リサンプリングを実行
 360            resampling_period: int = int(1000 / self.fs)
 361            df_resampled: pd.DataFrame = df.resample(f"{resampling_period}ms").mean(
 362                numeric_only=True
 363            )
 364
 365            if interpolate:
 366                # 補間を実行
 367                df_resampled = df_resampled.interpolate()
 368                # 有効数字を調整
 369                for col, decimals in decimal_places.items():
 370                    if col in df_resampled.columns:
 371                        df_resampled[col] = df_resampled[col].round(decimals)
 372
 373            # DateTimeインデックスを削除する
 374            df = df_resampled.reset_index()
 375            # ミリ秒を1桁にフォーマット
 376            df[index_column] = (
 377                df[index_column].dt.strftime("%Y-%m-%d %H:%M:%S.%f").str[:-5]
 378            )
 379
 380        return df, metadata
 381
 382    def output_resampled_data(
 383        self,
 384        input_dir: str,
 385        resampled_dir: str,
 386        ratio_dir: str,
 387        input_file_pattern: str = r"Eddy_(\d+)",
 388        input_files_suffix: str = ".dat",
 389        col_ch4_conc: str = "Ultra_CH4_ppm_C",
 390        col_c2h6_conc: str = "Ultra_C2H6_ppb",
 391        output_ratio: bool = True,
 392        output_resampled: bool = True,
 393        ratio_csv_prefix: str = "SAC.Ultra",
 394        index_column: str = "TIMESTAMP",
 395        index_format: str = "%Y-%m-%d %H:%M:%S.%f",
 396        interpolate: bool = True,
 397        numeric_columns: list[str] = [
 398            "Ux",
 399            "Uy",
 400            "Uz",
 401            "Tv",
 402            "diag_sonic",
 403            "CO2_new",
 404            "H2O",
 405            "diag_irga",
 406            "cell_tmpr",
 407            "cell_press",
 408            "Ultra_CH4_ppm",
 409            "Ultra_C2H6_ppb",
 410            "Ultra_H2O_ppm",
 411            "Ultra_CH4_ppm_C",
 412            "Ultra_C2H6_ppb_C",
 413        ],
 414        metadata_rows: int = 4,
 415        skiprows: list[int] = [0, 2, 3],
 416    ) -> None:
 417        """
 418        指定されたディレクトリ内のCSVファイルを処理し、リサンプリングと欠損値補間を行います。
 419
 420        このメソッドは、指定されたディレクトリ内のCSVファイルを読み込み、リサンプリングを行い、
 421        欠損値を補完します。処理結果として、リサンプリングされたCSVファイルを出力し、
 422        相関係数やC2H6/CH4比を計算してDataFrameに保存します。
 423        リサンプリングと欠損値補完は`get_resampled_df`と同様のロジックを使用します。
 424
 425        Parameters:
 426        -----
 427            input_dir : str
 428                入力CSVファイルが格納されているディレクトリのパス。
 429            resampled_dir : str
 430                リサンプリングされたCSVファイルを出力するディレクトリのパス。
 431            ratio_dir : str
 432                計算結果を保存するディレクトリのパス。
 433            input_file_pattern : str
 434                ファイル名からソートキーを抽出する正規表現パターン。デフォルトでは、最初の数字グループでソートします。
 435            input_files_suffix : str
 436                入力ファイルの拡張子(.datや.csvなど)。デフォルトは".dat"。
 437            col_ch4_conc : str
 438                CH4濃度を含む列名。デフォルトは'Ultra_CH4_ppm_C'。
 439            col_c2h6_conc : str
 440                C2H6濃度を含む列名。デフォルトは'Ultra_C2H6_ppb'。
 441            output_ratio : bool, optional
 442                線形回帰を行うかどうか。デフォルトはTrue。
 443            output_resampled : bool, optional
 444                リサンプリングされたCSVファイルを出力するかどうか。デフォルトはTrue。
 445            ratio_csv_prefix : str
 446                出力ファイルの接頭辞。デフォルトは'SAC.Ultra'で、出力時は'SAC.Ultra.2024.09.21.ratio.csv'のような形式となる。
 447            index_column : str
 448                日時情報を含む列名。デフォルトは'TIMESTAMP'。
 449            index_format : str, optional
 450                インデックスの日付形式。デフォルトは'%Y-%m-%d %H:%M:%S.%f'。
 451            interpolate : bool
 452                欠損値補間を行うかどうか。デフォルトはTrue。
 453            numeric_columns : list[str]
 454                数値データを含む列名のリスト。デフォルトは指定された列名のリスト。
 455            metadata_rows : int
 456                メタデータとして読み込む行数。デフォルトは4。
 457            skiprows : list[int]
 458                読み飛ばす行のインデックスリスト。デフォルトは[0, 2, 3]。
 459
 460        Raises:
 461        -----
 462            OSError
 463                ディレクトリの作成に失敗した場合。
 464            FileNotFoundError
 465                入力ファイルが見つからない場合。
 466            ValueError
 467                出力ディレクトリが指定されていない、またはデータの処理中にエラーが発生した場合。
 468        """
 469        # 出力オプションとディレクトリの検証
 470        if output_resampled and resampled_dir is None:
 471            raise ValueError("output_resampled が True の場合、resampled_dir を指定する必要があります")
 472        if output_ratio and ratio_dir is None:
 473            raise ValueError("output_ratio が True の場合、ratio_dir を指定する必要があります")
 474
 475        # ディレクトリの作成(必要な場合のみ)
 476        if output_resampled:
 477            os.makedirs(resampled_dir, exist_ok=True)
 478        if output_ratio:
 479            os.makedirs(ratio_dir, exist_ok=True)
 480
 481        ratio_data: list[dict[str, str | float]] = []
 482        latest_date: datetime = datetime.min
 483
 484        # csvファイル名のリスト
 485        csv_files: list[str] = EddyDataPreprocessor._get_sorted_files(
 486            input_dir, input_file_pattern, input_files_suffix
 487        )
 488
 489        for filename in tqdm(csv_files, desc="Processing files"):
 490            input_filepath: str = os.path.join(input_dir, filename)
 491            # リサンプリング&欠損値補間
 492            df, metadata = self.get_resampled_df(
 493                filepath=input_filepath,
 494                index_column=index_column,
 495                index_format=index_format,
 496                interpolate=interpolate,
 497                numeric_columns=numeric_columns,
 498                metadata_rows=metadata_rows,
 499                skiprows=skiprows,
 500            )
 501
 502            # 開始時間を取得
 503            start_time: datetime = pd.to_datetime(df[index_column].iloc[0])
 504            # 処理したファイルの中で最も最新の日付
 505            latest_date = max(latest_date, start_time)
 506
 507            # リサンプリング&欠損値補間したCSVを出力
 508            if output_resampled:
 509                base_filename: str = re.sub(rf"\{input_files_suffix}$", "", filename)
 510                output_csv_path: str = os.path.join(
 511                    resampled_dir, f"{base_filename}-resampled.csv"
 512                )
 513                # メタデータを先に書き込む
 514                with open(output_csv_path, "w") as f:
 515                    for line in metadata:
 516                        f.write(f"{line}\n")
 517                # データフレームを追記モードで書き込む
 518                df.to_csv(
 519                    output_csv_path, index=False, mode="a", quoting=3, header=False
 520                )
 521
 522            # 相関係数とC2H6/CH4比を計算
 523            if output_ratio:
 524                ch4_data: pd.Series = df[col_ch4_conc]
 525                c2h6_data: pd.Series = df[col_c2h6_conc]
 526
 527                ratio_row: dict[str, str | float] = {
 528                    "Date": start_time.strftime("%Y-%m-%d %H:%M:%S.%f"),
 529                    "slope": f"{np.nan}",
 530                    "intercept": f"{np.nan}",
 531                    "r_value": f"{np.nan}",
 532                    "p_value": f"{np.nan}",
 533                    "stderr": f"{np.nan}",
 534                }
 535                # 近似直線の傾き、切片、相関係数を計算
 536                try:
 537                    slope, intercept, r_value, p_value, stderr = stats.linregress(
 538                        ch4_data, c2h6_data
 539                    )
 540                    ratio_row: dict[str, str | float] = {
 541                        "Date": start_time.strftime("%Y-%m-%d %H:%M:%S.%f"),
 542                        "slope": f"{slope:.6f}",
 543                        "intercept": f"{intercept:.6f}",
 544                        "r_value": f"{r_value:.6f}",
 545                        "p_value": f"{p_value:.6f}",
 546                        "stderr": f"{stderr:.6f}",
 547                    }
 548                except Exception:
 549                    # 何もせず、デフォルトの ratio_row を使用する
 550                    pass
 551
 552                # 結果をリストに追加
 553                ratio_data.append(ratio_row)
 554
 555        if output_ratio:
 556            # DataFrameを作成し、Dateカラムで昇順ソート
 557            ratio_df: pd.DataFrame = pd.DataFrame(ratio_data)
 558            ratio_df["Date"] = pd.to_datetime(
 559                ratio_df["Date"]
 560            )  # Dateカラムをdatetime型に変換
 561            ratio_df = ratio_df.sort_values("Date")  # Dateカラムで昇順ソート
 562
 563            # CSVとして保存
 564            ratio_filename: str = (
 565                f"{ratio_csv_prefix}.{latest_date.strftime('%Y.%m.%d')}.ratio.csv"
 566            )
 567            ratio_path: str = os.path.join(ratio_dir, ratio_filename)
 568            ratio_df.to_csv(ratio_path, index=False)
 569    
 570    def resample_and_analyze_lag_times(
 571        self,
 572        input_dir: str,
 573        input_file_pattern: str = r"Eddy_(\d+)",
 574        input_files_suffix: str = ".dat",
 575        col_ch4_conc: str = "Ultra_CH4_ppm_C",
 576        col_c2h6_conc: str = "Ultra_C2H6_ppb",
 577        output_ratio: bool = True,
 578        ratio_dir: str | None = None,
 579        output_resampled: bool = True,
 580        resampled_dir: str | None = None,
 581        output_lag_times: bool = True,  # lag times解析の有効化フラグ
 582        lag_times_dir: str | None = None,  # lag timesの結果出力ディレクトリ
 583        lag_times_col1: str = "wind_w",  # 基準変数
 584        lag_times_col2_list: list[str] = ["Tv"],  # 比較変数のリスト
 585        lag_times_median_range: float = 20,  # 中央値を中心とした範囲
 586        lag_times_plot_range: tuple[float, float] = (
 587            -50,
 588            200,
 589        ),  # ヒストグラムの表示範囲
 590        lag_times_figsize: tuple[float, float] = (10, 8),  # プロットサイズ
 591        ratio_csv_prefix: str = "SAC.Ultra",
 592        index_column: str = "TIMESTAMP",
 593        index_format: str = "%Y-%m-%d %H:%M:%S.%f",
 594        interpolate: bool = True,
 595        numeric_columns: list[str] = [
 596            "Ux",
 597            "Uy",
 598            "Uz",
 599            "Tv",
 600            "diag_sonic",
 601            "CO2_new",
 602            "H2O",
 603            "diag_irga",
 604            "cell_tmpr",
 605            "cell_press",
 606            "Ultra_CH4_ppm",
 607            "Ultra_C2H6_ppb",
 608            "Ultra_H2O_ppm",
 609            "Ultra_CH4_ppm_C",
 610            "Ultra_C2H6_ppb_C",
 611        ],
 612        metadata_rows: int = 4,
 613        skiprows: list[int] = [0, 2, 3],
 614    ) -> None:
 615        """
 616        指定されたディレクトリ内のCSVファイルを処理し、リサンプリングと欠損値補間を行います。
 617
 618        このメソッドは、指定されたディレクトリ内のCSVファイルを読み込み、リサンプリングを行い、
 619        欠損値を補完します。処理結果として以下の出力が可能です:
 620        1. リサンプリングされたCSVファイル (output_resampled=True)
 621        2. 相関係数やC2H6/CH4比を計算したDataFrame (output_ratio=True)
 622        3. lag times解析結果 (output_lag_times=True)
 623
 624        Parameters:
 625        -----
 626            input_dir : str
 627                入力CSVファイルが格納されているディレクトリのパス。
 628            resampled_dir : str | None
 629                リサンプリングされたCSVファイルを出力するディレクトリのパス。
 630            ratio_dir : str | None
 631                C2H6/CH4比の計算結果を保存するディレクトリのパス。
 632            input_file_pattern : str
 633                ファイル名からソートキーを抽出する正規表現パターン。
 634            input_files_suffix : str
 635                入力ファイルの拡張子(.datや.csvなど)。デフォルトは".dat"。
 636            col_ch4_conc : str
 637                CH4濃度を含む列名。デフォルトは'Ultra_CH4_ppm_C'。
 638            col_c2h6_conc : str
 639                C2H6濃度を含む列名。デフォルトは'Ultra_C2H6_ppb'。
 640            output_ratio : bool
 641                線形回帰を行うかどうか。デフォルトはTrue。
 642            output_resampled : bool
 643                リサンプリングされたCSVファイルを出力するかどうか。デフォルトはTrue。
 644            output_lag_times : bool
 645                lag times解析を行うかどうか。デフォルトはFalse。
 646            lag_times_dir : str | None
 647                lag times解析結果の出力ディレクトリ。
 648            lag_times_col1 : str
 649                lag times解析の基準変数。デフォルトは"wind_w"。
 650            lag_times_col2_list : list[str]
 651                lag times解析の比較変数のリスト。デフォルトは["Tv"]。
 652            lag_times_median_range : float
 653                lag times解析の中央値を中心とした範囲。デフォルトは20。
 654            lag_times_plot_range : tuple[float, float]
 655                lag times解析のヒストグラム表示範囲。デフォルトは(-50, 200)。
 656            lag_times_figsize : tuple[float, float]
 657                lag times解析のプロットサイズ。デフォルトは(10, 8)。
 658            ratio_csv_prefix : str
 659                出力ファイルの接頭辞。
 660            index_column : str
 661                日時情報を含む列名。デフォルトは'TIMESTAMP'。
 662            index_format : str
 663                インデックスの日付形式。デフォルトは'%Y-%m-%d %H:%M:%S.%f'。
 664            interpolate : bool
 665                欠損値補間を行うかどうか。デフォルトはTrue。
 666            numeric_columns : list[str]
 667                数値データを含む列名のリスト。
 668            metadata_rows : int
 669                メタデータとして読み込む行数。デフォルトは4。
 670            skiprows : list[int]
 671                読み飛ばす行のインデックスリスト。デフォルトは[0, 2, 3]。
 672
 673        Raises:
 674        -----
 675            ValueError
 676                出力オプションが指定されているのにディレクトリが指定されていない場合。
 677            FileNotFoundError
 678                入力ファイルが見つからない場合。
 679            OSError
 680                ディレクトリの作成に失敗した場合。
 681        """
 682        # 出力オプションとディレクトリの検証
 683        if output_resampled and resampled_dir is None:
 684            raise ValueError(
 685                "output_resampled が True の場合、resampled_dir を指定する必要があります"
 686            )
 687        if output_ratio and ratio_dir is None:
 688            raise ValueError(
 689                "output_ratio が True の場合、ratio_dir を指定する必要があります"
 690            )
 691        if output_lag_times and lag_times_dir is None:
 692            raise ValueError(
 693                "output_lag_times が True の場合、lag_times_dir を指定する必要があります"
 694            )
 695
 696        # ディレクトリの作成(必要な場合のみ)
 697        if output_resampled and resampled_dir is not None:
 698            os.makedirs(resampled_dir, exist_ok=True)
 699        if output_ratio and ratio_dir is not None:
 700            os.makedirs(ratio_dir, exist_ok=True)
 701        if output_lag_times and lag_times_dir is not None:
 702            os.makedirs(lag_times_dir, exist_ok=True)
 703
 704        ratio_data: list[dict[str, str | float]] = []
 705        all_lags_indices: list[list[int]] = []
 706        latest_date: datetime = datetime.min
 707
 708        # csvファイル名のリスト
 709        csv_files: list[str] = EddyDataPreprocessor._get_sorted_files(
 710            input_dir, input_file_pattern, input_files_suffix
 711        )
 712
 713        if not csv_files:
 714            raise FileNotFoundError(
 715                f"There is no '{input_files_suffix}' file to process; input_dir: '{input_dir}'"
 716            )
 717
 718        for filename in tqdm(csv_files, desc="Processing files"):
 719            input_filepath: str = os.path.join(input_dir, filename)
 720            # リサンプリング&欠損値補間
 721            df, metadata = self.get_resampled_df(
 722                filepath=input_filepath,
 723                index_column=index_column,
 724                index_format=index_format,
 725                interpolate=interpolate,
 726                numeric_columns=numeric_columns,
 727                metadata_rows=metadata_rows,
 728                skiprows=skiprows,
 729            )
 730
 731            # 開始時間を取得
 732            start_time: datetime = pd.to_datetime(df[index_column].iloc[0])
 733            # 処理したファイルの中で最も最新の日付を更新
 734            latest_date = max(latest_date, start_time)
 735
 736            # リサンプリング&欠損値補間したCSVを出力
 737            if output_resampled and resampled_dir is not None:
 738                base_filename: str = re.sub(rf"\{input_files_suffix}$", "", filename)
 739                output_csv_path: str = os.path.join(
 740                    resampled_dir, f"{base_filename}-resampled.csv"
 741                )
 742                # メタデータを先に書き込む
 743                with open(output_csv_path, "w") as f:
 744                    for line in metadata:
 745                        f.write(f"{line}\n")
 746                # データフレームを追記モードで書き込む
 747                df.to_csv(
 748                    output_csv_path, index=False, mode="a", quoting=3, header=False
 749                )
 750
 751            # 相関係数とC2H6/CH4比を計算
 752            if output_ratio:
 753                ch4_data: pd.Series = df[col_ch4_conc]
 754                c2h6_data: pd.Series = df[col_c2h6_conc]
 755
 756                ratio_row: dict[str, str | float] = {
 757                    "Date": start_time.strftime("%Y-%m-%d %H:%M:%S.%f"),
 758                    "slope": f"{np.nan}",
 759                    "intercept": f"{np.nan}",
 760                    "r_value": f"{np.nan}",
 761                    "p_value": f"{np.nan}",
 762                    "stderr": f"{np.nan}",
 763                }
 764
 765                # 近似直線の傾き、切片、相関係数を計算
 766                try:
 767                    slope, intercept, r_value, p_value, stderr = stats.linregress(
 768                        ch4_data, c2h6_data
 769                    )
 770                    ratio_row = {
 771                        "Date": start_time.strftime("%Y-%m-%d %H:%M:%S.%f"),
 772                        "slope": f"{slope:.6f}",
 773                        "intercept": f"{intercept:.6f}",
 774                        "r_value": f"{r_value:.6f}",
 775                        "p_value": f"{p_value:.6f}",
 776                        "stderr": f"{stderr:.6f}",
 777                    }
 778                except Exception:
 779                    # 何もせず、デフォルトの ratio_row を使用する
 780                    pass
 781
 782                ratio_data.append(ratio_row)
 783
 784            # Lag times解析用のデータを収集
 785            if output_lag_times:
 786                df = self.add_uvw_columns(df)
 787                lags_list = EddyDataPreprocessor._calculate_lag_time(
 788                    df,
 789                    lag_times_col1,
 790                    lag_times_col2_list,
 791                )
 792                all_lags_indices.append(lags_list)
 793
 794        # Ratio解析結果の保存
 795        if output_ratio and ratio_dir is not None:
 796            # DataFrameを作成し、Dateカラムで昇順ソート
 797            ratio_df: pd.DataFrame = pd.DataFrame(ratio_data)
 798            ratio_df["Date"] = pd.to_datetime(ratio_df["Date"])
 799            ratio_df = ratio_df.sort_values("Date")
 800
 801            # CSVとして保存
 802            ratio_filename: str = (
 803                f"{ratio_csv_prefix}.{latest_date.strftime('%Y.%m.%d')}.ratio.csv"
 804            )
 805            ratio_path: str = os.path.join(ratio_dir, ratio_filename)
 806            ratio_df.to_csv(ratio_path, index=False)
 807            self.logger.info(f"Ratio解析結果を保存しました: {ratio_path}")
 808
 809        # Lag times解析結果の処理と保存
 810        if output_lag_times and lag_times_dir is not None:
 811            # lag timesの解析結果をDataFrameに変換
 812            lags_indices_df = pd.DataFrame(
 813                all_lags_indices, columns=lag_times_col2_list
 814            )
 815            lag_times_output_data = []
 816
 817            # 各変数に対する解析
 818            for column in lags_indices_df.columns:
 819                data = lags_indices_df[column]
 820
 821                # ヒストグラムの作成
 822                plt.figure(figsize=lag_times_figsize)
 823                plt.hist(data, bins=20, range=lag_times_plot_range)
 824                plt.title(f"Delays of {column}")
 825                plt.xlabel("Seconds")
 826                plt.ylabel("Frequency")
 827                plt.xlim(lag_times_plot_range)
 828
 829                # ヒストグラムの保存
 830                filename = f"lags_histogram-{column}.png"
 831                filepath = os.path.join(lag_times_dir, filename)
 832                plt.savefig(filepath, dpi=300, bbox_inches="tight")
 833                plt.close()
 834
 835                # 中央値を計算し、その周辺のデータのみを使用
 836                median_value = np.median(data)
 837                filtered_data = data[
 838                    (data >= median_value - lag_times_median_range)
 839                    & (data <= median_value + lag_times_median_range)
 840                ]
 841
 842                # 平均値を計算
 843                mean_value = np.mean(filtered_data)
 844                mean_seconds = float(mean_value / self.fs)
 845
 846                # 結果を格納
 847                lag_times_output_data.append(
 848                    {
 849                        "col1": lag_times_col1,
 850                        "col2": column,
 851                        "col2_lag": round(mean_seconds, 2),
 852                        "lag_unit": "s",
 853                        "median_range": lag_times_median_range,
 854                    }
 855                )
 856
 857            # 結果をCSVとして保存
 858            if lag_times_output_data:
 859                lag_times_df = pd.DataFrame(lag_times_output_data)
 860                lag_times_csv_path = os.path.join(lag_times_dir, "lags_results.csv")
 861                lag_times_df.to_csv(lag_times_csv_path, index=False, encoding="utf-8")
 862                self.logger.info(
 863                    f"Lag times解析結果を保存しました: {lag_times_csv_path}"
 864                )
 865
 866                # 遅れ時間を表示
 867                self.logger.info(f"カラム`{lag_times_col1}`に対する遅れ時間:")
 868                max_col_name_length = max(len(column) for column in lag_times_df["col2"])
 869                for _, row in lag_times_df.iterrows():
 870                    print(f"{row['col2']:<{max_col_name_length}} : {row['col2_lag']:.2f} s")
 871
 872    @staticmethod
 873    def _calculate_lag_time(
 874        df: pd.DataFrame,
 875        col1: str,
 876        col2_list: list[str],
 877    ) -> list[int]:
 878        """
 879        指定された基準変数(col1)と比較変数のリスト(col2_list)の間の遅れ時間(ディレイ)を計算する。
 880        周波数が10Hzでcol1がcol2より10.0秒遅れている場合は、+100がインデックスとして取得される
 881
 882        Parameters:
 883        -----
 884            df : pd.DataFrame
 885                遅れ時間の計算に使用するデータフレーム
 886            col1 : str
 887                基準変数の列名
 888            col2_list : list[str]
 889                比較変数の列名のリスト
 890
 891        Returns:
 892        -----
 893            list[int]
 894                各比較変数に対する遅れ時間(ディレイ)のリスト
 895        """
 896        lags_list: list[int] = []
 897        for col2 in col2_list:
 898            data1: np.ndarray = np.array(df[col1].values)
 899            data2: np.ndarray = np.array(df[col2].values)
 900
 901            # 平均を0に調整
 902            data1 = data1 - data1.mean()
 903            data2 = data2 - data2.mean()
 904
 905            data_length: int = len(data1)
 906
 907            # 相互相関の計算
 908            correlation: np.ndarray = np.correlate(
 909                data1, data2, mode="full"
 910            )  # data2とdata1の順序を入れ替え
 911
 912            # 相互相関のピークのインデックスを取得
 913            lag: int = int((data_length - 1) - correlation.argmax())  # 符号を反転
 914
 915            lags_list.append(lag)
 916        return lags_list
 917
 918    @staticmethod
 919    def _get_sorted_files(directory: str, pattern: str, suffix: str) -> list[str]:
 920        """
 921        指定されたディレクトリ内のファイルを、ファイル名に含まれる数字に基づいてソートして返す。
 922
 923        Parameters:
 924        -----
 925            directory : str
 926                ファイルが格納されているディレクトリのパス
 927            pattern : str
 928                ファイル名からソートキーを抽出する正規表現パターン
 929            suffix : str
 930                ファイルの拡張子
 931
 932        Returns:
 933        -----
 934            list[str]
 935                ソートされたファイル名のリスト
 936        """
 937        files: list[str] = [f for f in os.listdir(directory) if f.endswith(suffix)]
 938        files = [f for f in files if re.search(pattern, f)]
 939        files.sort(
 940            key=lambda x: int(re.search(pattern, x).group(1))  # type:ignore
 941            if re.search(pattern, x)
 942            else float("inf")
 943        )
 944        return files
 945
 946    @staticmethod
 947    def _horizontal_wind_speed(
 948        x_array: np.ndarray, y_array: np.ndarray, wind_dir: float
 949    ) -> tuple[np.ndarray, np.ndarray]:
 950        """
 951        風速のu成分とv成分を計算する関数
 952
 953        Parameters:
 954        -----
 955            x_array : numpy.ndarray
 956                x方向の風速成分の配列
 957            y_array : numpy.ndarray
 958                y方向の風速成分の配列
 959            wind_dir : float
 960                水平成分の風向(ラジアン)
 961
 962        Returns:
 963        -----
 964            tuple[numpy.ndarray, numpy.ndarray]
 965                u成分とv成分のタプル
 966        """
 967        # スカラー風速の計算
 968        scalar_hypotenuse: np.ndarray = np.sqrt(x_array**2 + y_array**2)
 969        # CSAT3では以下の補正が必要
 970        instantaneous_wind_directions = EddyDataPreprocessor._wind_direction(
 971            x_array=x_array, y_array=y_array
 972        )
 973        # ベクトル風速の計算
 974        vector_u: np.ndarray = scalar_hypotenuse * np.cos(
 975            instantaneous_wind_directions - wind_dir
 976        )
 977        vector_v: np.ndarray = scalar_hypotenuse * np.sin(
 978            instantaneous_wind_directions - wind_dir
 979        )
 980        return vector_u, vector_v
 981
 982    @staticmethod
 983    def setup_logger(logger: Logger | None, log_level: int = INFO) -> Logger:
 984        """
 985        ロガーを設定します。
 986
 987        このメソッドは、ロギングの設定を行い、ログメッセージのフォーマットを指定します。
 988        ログメッセージには、日付、ログレベル、メッセージが含まれます。
 989
 990        渡されたロガーがNoneまたは不正な場合は、新たにロガーを作成し、標準出力に
 991        ログメッセージが表示されるようにStreamHandlerを追加します。ロガーのレベルは
 992        引数で指定されたlog_levelに基づいて設定されます。
 993
 994        Parameters:
 995        -----
 996            logger : Logger | None
 997                使用するロガー。Noneの場合は新しいロガーを作成します。
 998            log_level : int
 999                ロガーのログレベル。デフォルトはINFO。
1000
1001        Returns:
1002        -----
1003            Logger
1004                設定されたロガーオブジェクト。
1005        """
1006        if logger is not None and isinstance(logger, Logger):
1007            return logger
1008        # 渡されたロガーがNoneまたは正しいものでない場合は独自に設定
1009        new_logger: Logger = getLogger()
1010        # 既存のハンドラーをすべて削除
1011        for handler in new_logger.handlers[:]:
1012            new_logger.removeHandler(handler)
1013        new_logger.setLevel(log_level)  # ロガーのレベルを設定
1014        ch = StreamHandler()
1015        ch_formatter = Formatter("%(asctime)s - %(levelname)s - %(message)s")
1016        ch.setFormatter(ch_formatter)  # フォーマッターをハンドラーに設定
1017        new_logger.addHandler(ch)  # StreamHandlerの追加
1018        return new_logger
1019
1020    @staticmethod
1021    def _vertical_rotation(
1022        u_array: np.ndarray,
1023        w_array: np.ndarray,
1024        wind_inc: float,
1025    ) -> tuple[np.ndarray, np.ndarray]:
1026        """
1027        鉛直方向の座標回転を行い、u, wを求める関数
1028
1029        Parameters:
1030        -----
1031            u_array (numpy.ndarray): u方向の風速
1032            w_array (numpy.ndarray): w方向の風速
1033            wind_inc (float): 平均風向に対する迎角(ラジアン)
1034
1035        Returns:
1036        -----
1037            tuple[numpy.ndarray, numpy.ndarray]: 回転後のu, w
1038        """
1039        # 迎角を用いて鉛直方向に座標回転
1040        u_rotated = u_array * np.cos(wind_inc) + w_array * np.sin(wind_inc)
1041        w_rotated = w_array * np.cos(wind_inc) - u_array * np.sin(wind_inc)
1042        return u_rotated, w_rotated
1043
1044    @staticmethod
1045    def _wind_direction(
1046        x_array: np.ndarray, y_array: np.ndarray, correction_angle: float = 0.0
1047    ) -> float:
1048        """
1049        水平方向の平均風向を計算する関数
1050
1051        Parameters:
1052        -----
1053            x_array (numpy.ndarray): 西方向の風速成分
1054            y_array (numpy.ndarray): 南北方向の風速成分
1055            correction_angle (float): 風向補正角度(ラジアン)。デフォルトは0.0。CSAT3の場合は0.0を指定。
1056
1057        Returns:
1058        -----
1059            wind_direction (float): 風向 (radians)
1060        """
1061        wind_direction: float = np.arctan2(np.mean(y_array), np.mean(x_array))
1062        # 補正角度を適用
1063        wind_direction = correction_angle - wind_direction
1064        return wind_direction
1065
1066    @staticmethod
1067    def _wind_inclination(u_array: np.ndarray, w_array: np.ndarray) -> float:
1068        """
1069        平均風向に対する迎角を計算する関数
1070
1071        Parameters:
1072        -----
1073            u_array (numpy.ndarray): u方向の瞬間風速
1074            w_array (numpy.ndarray): w方向の瞬間風速
1075
1076        Returns:
1077        -----
1078            wind_inc (float): 平均風向に対する迎角(ラジアン)
1079        """
1080        wind_inc: float = np.arctan2(np.mean(w_array), np.mean(u_array))
1081        return wind_inc
EddyDataPreprocessor( fs: float = 10, logger: logging.Logger | None = None, logging_debug: bool = False)
14    def __init__(
15        self,
16        fs: float = 10,
17        logger: Logger | None = None,
18        logging_debug: bool = False,
19    ):
20        """
21        渦相関法によって記録されたデータファイルを処理するクラス。
22
23        Parameters
24        ----------
25            fs (float): サンプリング周波数。
26            logger (Logger | None): 使用するロガー。Noneの場合は新しいロガーを作成します。
27            logging_debug (bool): ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。
28        """
29        self.fs: float = fs
30
31        # ロガー
32        log_level: int = INFO
33        if logging_debug:
34            log_level = DEBUG
35        self.logger: Logger = EddyDataPreprocessor.setup_logger(logger, log_level)

渦相関法によって記録されたデータファイルを処理するクラス。

Parameters

fs (float): サンプリング周波数。
logger (Logger | None): 使用するロガー。Noneの場合は新しいロガーを作成します。
logging_debug (bool): ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。
fs: float
logger: logging.Logger
def add_uvw_columns(self, df: pandas.core.frame.DataFrame) -> pandas.core.frame.DataFrame:
37    def add_uvw_columns(self, df: pd.DataFrame) -> pd.DataFrame:
38        """
39        DataFrameに水平風速u、v、鉛直風速wの列を追加する関数。
40        各成分のキーは`wind_u`、`wind_v`、`wind_w`である。
41        
42        Parameters
43        -----
44            df : pd.DataFrame
45                風速データを含むDataFrame
46
47        Returns
48        -----
49            pd.DataFrame
50                水平風速u、v、鉛直風速wの列を追加したDataFrame
51        """
52        required_columns: list[str] = ["Ux", "Uy", "Uz"]
53        # 必要な列がDataFrameに存在するか確認
54        for column in required_columns:
55            if column not in df.columns:
56                raise ValueError(f"必要な列 '{column}' がDataFrameに存在しません。")
57
58        processed_df: pd.DataFrame = df.copy()
59        # pandasの.valuesを使用してnumpy配列を取得し、その型をnp.ndarrayに明示的にキャストする
60        wind_x_array: np.ndarray = np.array(processed_df["Ux"].values)
61        wind_y_array: np.ndarray = np.array(processed_df["Uy"].values)
62        wind_z_array: np.ndarray = np.array(processed_df["Uz"].values)
63
64        # 平均風向を計算
65        wind_direction: float = EddyDataPreprocessor._wind_direction(
66            wind_x_array, wind_y_array
67        )
68
69        # 水平方向に座標回転を行u, v成分を求める
70        wind_u_array, wind_v_array = EddyDataPreprocessor._horizontal_wind_speed(
71            wind_x_array, wind_y_array, wind_direction
72        )
73        wind_w_array: np.ndarray = wind_z_array  # wはz成分そのまま
74
75        # u, wから風の迎角を計算
76        wind_inclination: float = EddyDataPreprocessor._wind_inclination(
77            wind_u_array, wind_w_array
78        )
79
80        # 2回座標回転を行い、u, wを求める
81        wind_u_array_rotated, wind_w_array_rotated = (
82            EddyDataPreprocessor._vertical_rotation(
83                wind_u_array, wind_w_array, wind_inclination
84            )
85        )
86
87        processed_df["wind_u"] = wind_u_array_rotated
88        processed_df["wind_v"] = wind_v_array
89        processed_df["wind_w"] = wind_w_array_rotated
90        processed_df["rad_wind_dir"] = wind_direction
91        processed_df["rad_wind_inc"] = wind_inclination
92        processed_df["degree_wind_dir"] = np.degrees(wind_direction)
93        processed_df["degree_wind_inc"] = np.degrees(wind_inclination)
94
95        return processed_df

DataFrameに水平風速u、v、鉛直風速wの列を追加する関数。 各成分のキーはwind_uwind_vwind_wである。

Parameters

df : pd.DataFrame
    風速データを含むDataFrame

Returns

pd.DataFrame
    水平風速u、v、鉛直風速wの列を追加したDataFrame
def analyze_lag_times( self, input_dir: str, figsize: tuple[float, float] = (10, 8), input_files_pattern: str = 'Eddy_(\\d+)', input_files_suffix: str = '.dat', col1: str = 'wind_w', col2_list: list[str] = ['Tv'], median_range: float = 20, metadata_rows: int = 4, output_dir: str | None = None, output_tag: str = '', plot_range_tuple: tuple = (-50, 200), print_results: bool = True, skiprows: list[int] = [0, 2, 3], use_resampling: bool = True) -> dict[str, float]:
 97    def analyze_lag_times(
 98        self,
 99        input_dir: str,
100        figsize: tuple[float, float] = (10, 8),
101        input_files_pattern: str = r"Eddy_(\d+)",
102        input_files_suffix: str = ".dat",
103        col1: str = "wind_w",
104        col2_list: list[str] = ["Tv"],
105        median_range: float = 20,
106        metadata_rows: int = 4,
107        output_dir: str | None = None,
108        output_tag: str = "",
109        plot_range_tuple: tuple = (-50, 200),
110        print_results: bool = True,
111        skiprows: list[int] = [0, 2, 3],
112        use_resampling: bool = True,
113    ) -> dict[str, float]:
114        """
115        遅れ時間(ラグ)の統計分析を行い、指定されたディレクトリ内のデータファイルを処理します。
116        解析結果とメタデータはCSVファイルとして出力されます。
117
118        Parameters:
119        -----
120            input_dir : str
121                入力データファイルが格納されているディレクトリのパス。
122            figsize : tuple[float, float]
123                プロットのサイズ(幅、高さ)。
124            input_files_pattern : str
125                入力ファイル名のパターン(正規表現)。
126            input_files_suffix : str
127                入力ファイルの拡張子。
128            col1 : str
129                基準変数の列名。
130            col2_list : list[str]
131                比較変数の列名のリスト。
132            median_range : float
133                中央値を中心とした範囲。
134            metadata_rows : int
135                メタデータの行数。
136            output_dir : str | None
137                出力ディレクトリのパス。Noneの場合は保存しない。
138            output_tag : str
139                出力ファイルに付与するタグ。デフォルトは空文字で、何も付与されない。
140            plot_range_tuple : tuple
141                ヒストグラムの表示範囲。
142            print_results : bool
143                結果をコンソールに表示するかどうか。
144            skiprows : list[int]
145                スキップする行番号のリスト。
146            use_resampling : bool
147                データをリサンプリングするかどうか。
148                inputするファイルが既にリサンプリング済みの場合はFalseでよい。
149                デフォルトはTrue。
150
151        Returns:
152        -----
153            dict[str, float]
154                各変数の遅れ時間(平均値を採用)を含む辞書。
155        """
156        if output_dir is None:
157            self.logger.warn(
158                "output_dirが指定されていません。解析結果を保存する場合は、有効なディレクトリを指定してください。"
159            )
160        all_lags_indices: list[list[int]] = []
161        results: dict[str, float] = {}
162
163        # メイン処理
164        # ファイル名に含まれる数字に基づいてソート
165        csv_files = EddyDataPreprocessor._get_sorted_files(
166            input_dir, input_files_pattern, input_files_suffix
167        )
168        if not csv_files:
169            raise FileNotFoundError(
170                f"There is no '{input_files_suffix}' file to process; input_dir: '{input_dir}', input_files_suffix: '{input_files_suffix}'"
171            )
172
173        for file in tqdm(csv_files, desc="Calculating"):
174            path: str = os.path.join(input_dir, file)
175            if use_resampling:
176                df, _ = self.get_resampled_df(
177                    filepath=path, metadata_rows=metadata_rows, skiprows=skiprows
178                )
179            else:
180                df = pd.read_csv(path, skiprows=skiprows)
181            df = self.add_uvw_columns(df)
182            lags_list = EddyDataPreprocessor._calculate_lag_time(
183                df,
184                col1,
185                col2_list,
186            )
187            all_lags_indices.append(lags_list)
188        self.logger.info("すべてのCSVファイルにおける遅れ時間が計算されました。")
189
190        # Convert all_lags_indices to a DataFrame
191        lags_indices_df: pd.DataFrame = pd.DataFrame(
192            all_lags_indices, columns=col2_list
193        )
194
195        # フォーマット用のキーの最大の長さ
196        max_col_name_length: int = max(len(column) for column in lags_indices_df.columns)
197
198        if print_results:
199            self.logger.info(f"カラム`{col1}`に対する遅れ時間を表示します。")
200
201        # 結果を格納するためのリスト
202        output_data = []
203
204        for column in lags_indices_df.columns:
205            data: pd.Series = lags_indices_df[column]
206
207            # ヒストグラムの作成
208            plt.figure(figsize=figsize)
209            plt.hist(data, bins=20, range=plot_range_tuple)
210            plt.title(f"Delays of {column}")
211            plt.xlabel("Seconds")
212            plt.ylabel("Frequency")
213            plt.xlim(plot_range_tuple)
214
215            # ファイルとして保存するか
216            if output_dir is not None:
217                os.makedirs(output_dir, exist_ok=True)
218                filename: str = f"lags_histogram-{column}{output_tag}.png"
219                filepath: str = os.path.join(output_dir, filename)
220                plt.savefig(filepath, dpi=300, bbox_inches="tight")
221                plt.close()
222
223            # 中央値を計算し、その周辺のデータのみを使用
224            median_value = np.median(data)
225            filtered_data: pd.Series = data[
226                (data >= median_value - median_range)
227                & (data <= median_value + median_range)
228            ]
229
230            # 平均値を計算
231            mean_value = np.mean(filtered_data)
232            mean_seconds: float = float(mean_value / self.fs)  # 統計値を秒に変換
233            results[column] = mean_seconds
234
235            # 結果とメタデータを出力データに追加
236            output_data.append(
237                {
238                    "col1": col1,
239                    "col2": column,
240                    "col2_lag": round(mean_seconds, 2),  # 数値として小数点2桁を保持
241                    "lag_unit": "s",
242                    "median_range": median_range,
243                }
244            )
245
246            if print_results:
247                print(f"{column:<{max_col_name_length}} : {mean_seconds:.2f} s")
248
249        # 結果をCSVファイルとして出力
250        if output_dir is not None:
251            output_df: pd.DataFrame = pd.DataFrame(output_data)
252            csv_filepath: str = os.path.join(
253                output_dir, f"lags_results{output_tag}.csv"
254            )
255            output_df.to_csv(csv_filepath, index=False, encoding="utf-8")
256            self.logger.info(f"解析結果をCSVファイルに保存しました: {csv_filepath}")
257
258        return results

遅れ時間(ラグ)の統計分析を行い、指定されたディレクトリ内のデータファイルを処理します。 解析結果とメタデータはCSVファイルとして出力されます。

Parameters:

input_dir : str
    入力データファイルが格納されているディレクトリのパス。
figsize : tuple[float, float]
    プロットのサイズ(幅、高さ)。
input_files_pattern : str
    入力ファイル名のパターン(正規表現)。
input_files_suffix : str
    入力ファイルの拡張子。
col1 : str
    基準変数の列名。
col2_list : list[str]
    比較変数の列名のリスト。
median_range : float
    中央値を中心とした範囲。
metadata_rows : int
    メタデータの行数。
output_dir : str | None
    出力ディレクトリのパス。Noneの場合は保存しない。
output_tag : str
    出力ファイルに付与するタグ。デフォルトは空文字で、何も付与されない。
plot_range_tuple : tuple
    ヒストグラムの表示範囲。
print_results : bool
    結果をコンソールに表示するかどうか。
skiprows : list[int]
    スキップする行番号のリスト。
use_resampling : bool
    データをリサンプリングするかどうか。
    inputするファイルが既にリサンプリング済みの場合はFalseでよい。
    デフォルトはTrue。

Returns:

dict[str, float]
    各変数の遅れ時間(平均値を採用)を含む辞書。
def get_resampled_df( self, filepath: str, index_column: str = 'TIMESTAMP', index_format: str = '%Y-%m-%d %H:%M:%S.%f', interpolate: bool = True, numeric_columns: list[str] = ['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 = 4, skiprows: list[int] = [0, 2, 3], is_already_resampled: bool = False) -> tuple[pandas.core.frame.DataFrame, list[str]]:
260    def get_resampled_df(
261        self,
262        filepath: str,
263        index_column: str = "TIMESTAMP",
264        index_format: str = "%Y-%m-%d %H:%M:%S.%f",
265        interpolate: bool = True,
266        numeric_columns: list[str] = [
267            "Ux",
268            "Uy",
269            "Uz",
270            "Tv",
271            "diag_sonic",
272            "CO2_new",
273            "H2O",
274            "diag_irga",
275            "cell_tmpr",
276            "cell_press",
277            "Ultra_CH4_ppm",
278            "Ultra_C2H6_ppb",
279            "Ultra_H2O_ppm",
280            "Ultra_CH4_ppm_C",
281            "Ultra_C2H6_ppb_C",
282        ],
283        metadata_rows: int = 4,
284        skiprows: list[int] = [0, 2, 3],
285        is_already_resampled: bool = False,
286    ) -> tuple[pd.DataFrame, list[str]]:
287        """
288        CSVファイルを読み込み、前処理を行う
289
290        前処理の手順は以下の通りです:
291        1. 不要な行を削除する。デフォルト(`skiprows=[0, 2, 3]`)の場合は、2行目をヘッダーとして残し、1、3、4行目が削除される。
292        2. 数値データを float 型に変換する
293        3. TIMESTAMP列をDateTimeインデックスに設定する
294        4. エラー値をNaNに置き換える
295        5. 指定されたサンプリングレートでリサンプリングする
296        6. 欠損値(NaN)を前後の値から線形補間する
297        7. DateTimeインデックスを削除する
298        
299        Parameters:
300        -----
301            filepath : str
302                読み込むCSVファイルのパス
303            index_column : str, optional
304                インデックスに使用する列名。デフォルトは'TIMESTAMP'。
305            index_format : str, optional
306                インデックスの日付形式。デフォルトは'%Y-%m-%d %H:%M:%S.%f'。
307            interpolate : bool, optional
308                欠損値の補完を適用するフラグ。デフォルトはTrue。
309            numeric_columns : list[str], optional
310                数値型に変換する列名のリスト。
311                デフォルトは["Ux", "Uy", "Uz", "Tv", "diag_sonic", "CO2_new", "H2O", "diag_irga", "cell_tmpr", "cell_press", "Ultra_CH4_ppm", "Ultra_C2H6_ppb", "Ultra_H2O_ppm", "Ultra_CH4_ppm_C", "Ultra_C2H6_ppb_C"]。
312            metadata_rows : int, optional
313                メタデータとして読み込む行数。デフォルトは4。
314            skiprows : list[int], optional
315                スキップする行インデックスのリスト。デフォルトは[0, 2, 3]のため、1, 3, 4行目がスキップされる。
316            is_already_resampled : bool
317                既にリサンプリング&欠損補間されているか。Trueの場合はfloat変換などの処理のみ適用する。
318
319        Returns:
320        -----
321            tuple[pd.DataFrame, list[str]]
322                前処理済みのデータフレームとメタデータのリスト。
323        """
324        # メタデータを読み込む
325        metadata: list[str] = []
326        with open(filepath, "r") as f:
327            for _ in range(metadata_rows):
328                line = f.readline().strip()
329                metadata.append(line.replace('"', ""))
330
331        # CSVファイルを読み込む
332        df: pd.DataFrame = pd.read_csv(filepath, skiprows=skiprows)
333
334        # 数値データをfloat型に変換する
335        for col in numeric_columns:
336            if col in df.columns:
337                df[col] = pd.to_numeric(df[col], errors="coerce")
338
339        if not is_already_resampled:
340            # μ秒がない場合は".0"を追加する
341            df[index_column] = df[index_column].apply(
342                lambda x: f"{x}.0" if "." not in x else x
343            )
344            # TIMESTAMPをDateTimeインデックスに設定する
345            df[index_column] = pd.to_datetime(df[index_column], format=index_format)
346            df = df.set_index(index_column)
347
348            # リサンプリング前の有効数字を取得
349            decimal_places = {}
350            for col in numeric_columns:
351                if col in df.columns:
352                    max_decimals = (
353                        df[col].astype(str).str.extract(r"\.(\d+)")[0].str.len().max()
354                    )
355                    decimal_places[col] = (
356                        int(max_decimals) if pd.notna(max_decimals) else 0
357                    )
358
359            # リサンプリングを実行
360            resampling_period: int = int(1000 / self.fs)
361            df_resampled: pd.DataFrame = df.resample(f"{resampling_period}ms").mean(
362                numeric_only=True
363            )
364
365            if interpolate:
366                # 補間を実行
367                df_resampled = df_resampled.interpolate()
368                # 有効数字を調整
369                for col, decimals in decimal_places.items():
370                    if col in df_resampled.columns:
371                        df_resampled[col] = df_resampled[col].round(decimals)
372
373            # DateTimeインデックスを削除する
374            df = df_resampled.reset_index()
375            # ミリ秒を1桁にフォーマット
376            df[index_column] = (
377                df[index_column].dt.strftime("%Y-%m-%d %H:%M:%S.%f").str[:-5]
378            )
379
380        return df, metadata

CSVファイルを読み込み、前処理を行う

前処理の手順は以下の通りです:

  1. 不要な行を削除する。デフォルト(skiprows=[0, 2, 3])の場合は、2行目をヘッダーとして残し、1、3、4行目が削除される。
  2. 数値データを float 型に変換する
  3. TIMESTAMP列をDateTimeインデックスに設定する
  4. エラー値をNaNに置き換える
  5. 指定されたサンプリングレートでリサンプリングする
  6. 欠損値(NaN)を前後の値から線形補間する
  7. 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]]
    前処理済みのデータフレームとメタデータのリスト。
def output_resampled_data( self, input_dir: str, resampled_dir: str, ratio_dir: str, input_file_pattern: str = 'Eddy_(\\d+)', input_files_suffix: str = '.dat', col_ch4_conc: str = 'Ultra_CH4_ppm_C', col_c2h6_conc: str = 'Ultra_C2H6_ppb', output_ratio: bool = True, output_resampled: bool = True, ratio_csv_prefix: str = 'SAC.Ultra', index_column: str = 'TIMESTAMP', index_format: str = '%Y-%m-%d %H:%M:%S.%f', interpolate: bool = True, numeric_columns: list[str] = ['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 = 4, skiprows: list[int] = [0, 2, 3]) -> None:
382    def output_resampled_data(
383        self,
384        input_dir: str,
385        resampled_dir: str,
386        ratio_dir: str,
387        input_file_pattern: str = r"Eddy_(\d+)",
388        input_files_suffix: str = ".dat",
389        col_ch4_conc: str = "Ultra_CH4_ppm_C",
390        col_c2h6_conc: str = "Ultra_C2H6_ppb",
391        output_ratio: bool = True,
392        output_resampled: bool = True,
393        ratio_csv_prefix: str = "SAC.Ultra",
394        index_column: str = "TIMESTAMP",
395        index_format: str = "%Y-%m-%d %H:%M:%S.%f",
396        interpolate: bool = True,
397        numeric_columns: list[str] = [
398            "Ux",
399            "Uy",
400            "Uz",
401            "Tv",
402            "diag_sonic",
403            "CO2_new",
404            "H2O",
405            "diag_irga",
406            "cell_tmpr",
407            "cell_press",
408            "Ultra_CH4_ppm",
409            "Ultra_C2H6_ppb",
410            "Ultra_H2O_ppm",
411            "Ultra_CH4_ppm_C",
412            "Ultra_C2H6_ppb_C",
413        ],
414        metadata_rows: int = 4,
415        skiprows: list[int] = [0, 2, 3],
416    ) -> None:
417        """
418        指定されたディレクトリ内のCSVファイルを処理し、リサンプリングと欠損値補間を行います。
419
420        このメソッドは、指定されたディレクトリ内のCSVファイルを読み込み、リサンプリングを行い、
421        欠損値を補完します。処理結果として、リサンプリングされたCSVファイルを出力し、
422        相関係数やC2H6/CH4比を計算してDataFrameに保存します。
423        リサンプリングと欠損値補完は`get_resampled_df`と同様のロジックを使用します。
424
425        Parameters:
426        -----
427            input_dir : str
428                入力CSVファイルが格納されているディレクトリのパス。
429            resampled_dir : str
430                リサンプリングされたCSVファイルを出力するディレクトリのパス。
431            ratio_dir : str
432                計算結果を保存するディレクトリのパス。
433            input_file_pattern : str
434                ファイル名からソートキーを抽出する正規表現パターン。デフォルトでは、最初の数字グループでソートします。
435            input_files_suffix : str
436                入力ファイルの拡張子(.datや.csvなど)。デフォルトは".dat"。
437            col_ch4_conc : str
438                CH4濃度を含む列名。デフォルトは'Ultra_CH4_ppm_C'。
439            col_c2h6_conc : str
440                C2H6濃度を含む列名。デフォルトは'Ultra_C2H6_ppb'。
441            output_ratio : bool, optional
442                線形回帰を行うかどうか。デフォルトはTrue。
443            output_resampled : bool, optional
444                リサンプリングされたCSVファイルを出力するかどうか。デフォルトはTrue。
445            ratio_csv_prefix : str
446                出力ファイルの接頭辞。デフォルトは'SAC.Ultra'で、出力時は'SAC.Ultra.2024.09.21.ratio.csv'のような形式となる。
447            index_column : str
448                日時情報を含む列名。デフォルトは'TIMESTAMP'。
449            index_format : str, optional
450                インデックスの日付形式。デフォルトは'%Y-%m-%d %H:%M:%S.%f'。
451            interpolate : bool
452                欠損値補間を行うかどうか。デフォルトはTrue。
453            numeric_columns : list[str]
454                数値データを含む列名のリスト。デフォルトは指定された列名のリスト。
455            metadata_rows : int
456                メタデータとして読み込む行数。デフォルトは4。
457            skiprows : list[int]
458                読み飛ばす行のインデックスリスト。デフォルトは[0, 2, 3]。
459
460        Raises:
461        -----
462            OSError
463                ディレクトリの作成に失敗した場合。
464            FileNotFoundError
465                入力ファイルが見つからない場合。
466            ValueError
467                出力ディレクトリが指定されていない、またはデータの処理中にエラーが発生した場合。
468        """
469        # 出力オプションとディレクトリの検証
470        if output_resampled and resampled_dir is None:
471            raise ValueError("output_resampled が True の場合、resampled_dir を指定する必要があります")
472        if output_ratio and ratio_dir is None:
473            raise ValueError("output_ratio が True の場合、ratio_dir を指定する必要があります")
474
475        # ディレクトリの作成(必要な場合のみ)
476        if output_resampled:
477            os.makedirs(resampled_dir, exist_ok=True)
478        if output_ratio:
479            os.makedirs(ratio_dir, exist_ok=True)
480
481        ratio_data: list[dict[str, str | float]] = []
482        latest_date: datetime = datetime.min
483
484        # csvファイル名のリスト
485        csv_files: list[str] = EddyDataPreprocessor._get_sorted_files(
486            input_dir, input_file_pattern, input_files_suffix
487        )
488
489        for filename in tqdm(csv_files, desc="Processing files"):
490            input_filepath: str = os.path.join(input_dir, filename)
491            # リサンプリング&欠損値補間
492            df, metadata = self.get_resampled_df(
493                filepath=input_filepath,
494                index_column=index_column,
495                index_format=index_format,
496                interpolate=interpolate,
497                numeric_columns=numeric_columns,
498                metadata_rows=metadata_rows,
499                skiprows=skiprows,
500            )
501
502            # 開始時間を取得
503            start_time: datetime = pd.to_datetime(df[index_column].iloc[0])
504            # 処理したファイルの中で最も最新の日付
505            latest_date = max(latest_date, start_time)
506
507            # リサンプリング&欠損値補間したCSVを出力
508            if output_resampled:
509                base_filename: str = re.sub(rf"\{input_files_suffix}$", "", filename)
510                output_csv_path: str = os.path.join(
511                    resampled_dir, f"{base_filename}-resampled.csv"
512                )
513                # メタデータを先に書き込む
514                with open(output_csv_path, "w") as f:
515                    for line in metadata:
516                        f.write(f"{line}\n")
517                # データフレームを追記モードで書き込む
518                df.to_csv(
519                    output_csv_path, index=False, mode="a", quoting=3, header=False
520                )
521
522            # 相関係数とC2H6/CH4比を計算
523            if output_ratio:
524                ch4_data: pd.Series = df[col_ch4_conc]
525                c2h6_data: pd.Series = df[col_c2h6_conc]
526
527                ratio_row: dict[str, str | float] = {
528                    "Date": start_time.strftime("%Y-%m-%d %H:%M:%S.%f"),
529                    "slope": f"{np.nan}",
530                    "intercept": f"{np.nan}",
531                    "r_value": f"{np.nan}",
532                    "p_value": f"{np.nan}",
533                    "stderr": f"{np.nan}",
534                }
535                # 近似直線の傾き、切片、相関係数を計算
536                try:
537                    slope, intercept, r_value, p_value, stderr = stats.linregress(
538                        ch4_data, c2h6_data
539                    )
540                    ratio_row: dict[str, str | float] = {
541                        "Date": start_time.strftime("%Y-%m-%d %H:%M:%S.%f"),
542                        "slope": f"{slope:.6f}",
543                        "intercept": f"{intercept:.6f}",
544                        "r_value": f"{r_value:.6f}",
545                        "p_value": f"{p_value:.6f}",
546                        "stderr": f"{stderr:.6f}",
547                    }
548                except Exception:
549                    # 何もせず、デフォルトの ratio_row を使用する
550                    pass
551
552                # 結果をリストに追加
553                ratio_data.append(ratio_row)
554
555        if output_ratio:
556            # DataFrameを作成し、Dateカラムで昇順ソート
557            ratio_df: pd.DataFrame = pd.DataFrame(ratio_data)
558            ratio_df["Date"] = pd.to_datetime(
559                ratio_df["Date"]
560            )  # Dateカラムをdatetime型に変換
561            ratio_df = ratio_df.sort_values("Date")  # Dateカラムで昇順ソート
562
563            # CSVとして保存
564            ratio_filename: str = (
565                f"{ratio_csv_prefix}.{latest_date.strftime('%Y.%m.%d')}.ratio.csv"
566            )
567            ratio_path: str = os.path.join(ratio_dir, ratio_filename)
568            ratio_df.to_csv(ratio_path, index=False)

指定されたディレクトリ内のCSVファイルを処理し、リサンプリングと欠損値補間を行います。

このメソッドは、指定されたディレクトリ内のCSVファイルを読み込み、リサンプリングを行い、 欠損値を補完します。処理結果として、リサンプリングされたCSVファイルを出力し、 相関係数やC2H6/CH4比を計算してDataFrameに保存します。 リサンプリングと欠損値補完はget_resampled_dfと同様のロジックを使用します。

Parameters:

input_dir : str
    入力CSVファイルが格納されているディレクトリのパス。
resampled_dir : str
    リサンプリングされたCSVファイルを出力するディレクトリのパス。
ratio_dir : str
    計算結果を保存するディレクトリのパス。
input_file_pattern : str
    ファイル名からソートキーを抽出する正規表現パターン。デフォルトでは、最初の数字グループでソートします。
input_files_suffix : str
    入力ファイルの拡張子(.datや.csvなど)。デフォルトは".dat"。
col_ch4_conc : str
    CH4濃度を含む列名。デフォルトは'Ultra_CH4_ppm_C'。
col_c2h6_conc : str
    C2H6濃度を含む列名。デフォルトは'Ultra_C2H6_ppb'。
output_ratio : bool, optional
    線形回帰を行うかどうか。デフォルトはTrue。
output_resampled : bool, optional
    リサンプリングされたCSVファイルを出力するかどうか。デフォルトはTrue。
ratio_csv_prefix : str
    出力ファイルの接頭辞。デフォルトは'SAC.Ultra'で、出力時は'SAC.Ultra.2024.09.21.ratio.csv'のような形式となる。
index_column : str
    日時情報を含む列名。デフォルトは'TIMESTAMP'。
index_format : str, optional
    インデックスの日付形式。デフォルトは'%Y-%m-%d %H:%M:%S.%f'。
interpolate : bool
    欠損値補間を行うかどうか。デフォルトはTrue。
numeric_columns : list[str]
    数値データを含む列名のリスト。デフォルトは指定された列名のリスト。
metadata_rows : int
    メタデータとして読み込む行数。デフォルトは4。
skiprows : list[int]
    読み飛ばす行のインデックスリスト。デフォルトは[0, 2, 3]。

Raises:

OSError
    ディレクトリの作成に失敗した場合。
FileNotFoundError
    入力ファイルが見つからない場合。
ValueError
    出力ディレクトリが指定されていない、またはデータの処理中にエラーが発生した場合。
def resample_and_analyze_lag_times( self, input_dir: str, input_file_pattern: str = 'Eddy_(\\d+)', input_files_suffix: str = '.dat', col_ch4_conc: str = 'Ultra_CH4_ppm_C', col_c2h6_conc: str = 'Ultra_C2H6_ppb', output_ratio: bool = True, ratio_dir: str | None = None, output_resampled: bool = True, resampled_dir: str | None = None, output_lag_times: bool = True, lag_times_dir: str | None = None, lag_times_col1: str = 'wind_w', lag_times_col2_list: list[str] = ['Tv'], lag_times_median_range: float = 20, lag_times_plot_range: tuple[float, float] = (-50, 200), lag_times_figsize: tuple[float, float] = (10, 8), ratio_csv_prefix: str = 'SAC.Ultra', index_column: str = 'TIMESTAMP', index_format: str = '%Y-%m-%d %H:%M:%S.%f', interpolate: bool = True, numeric_columns: list[str] = ['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 = 4, skiprows: list[int] = [0, 2, 3]) -> None:
570    def resample_and_analyze_lag_times(
571        self,
572        input_dir: str,
573        input_file_pattern: str = r"Eddy_(\d+)",
574        input_files_suffix: str = ".dat",
575        col_ch4_conc: str = "Ultra_CH4_ppm_C",
576        col_c2h6_conc: str = "Ultra_C2H6_ppb",
577        output_ratio: bool = True,
578        ratio_dir: str | None = None,
579        output_resampled: bool = True,
580        resampled_dir: str | None = None,
581        output_lag_times: bool = True,  # lag times解析の有効化フラグ
582        lag_times_dir: str | None = None,  # lag timesの結果出力ディレクトリ
583        lag_times_col1: str = "wind_w",  # 基準変数
584        lag_times_col2_list: list[str] = ["Tv"],  # 比較変数のリスト
585        lag_times_median_range: float = 20,  # 中央値を中心とした範囲
586        lag_times_plot_range: tuple[float, float] = (
587            -50,
588            200,
589        ),  # ヒストグラムの表示範囲
590        lag_times_figsize: tuple[float, float] = (10, 8),  # プロットサイズ
591        ratio_csv_prefix: str = "SAC.Ultra",
592        index_column: str = "TIMESTAMP",
593        index_format: str = "%Y-%m-%d %H:%M:%S.%f",
594        interpolate: bool = True,
595        numeric_columns: list[str] = [
596            "Ux",
597            "Uy",
598            "Uz",
599            "Tv",
600            "diag_sonic",
601            "CO2_new",
602            "H2O",
603            "diag_irga",
604            "cell_tmpr",
605            "cell_press",
606            "Ultra_CH4_ppm",
607            "Ultra_C2H6_ppb",
608            "Ultra_H2O_ppm",
609            "Ultra_CH4_ppm_C",
610            "Ultra_C2H6_ppb_C",
611        ],
612        metadata_rows: int = 4,
613        skiprows: list[int] = [0, 2, 3],
614    ) -> None:
615        """
616        指定されたディレクトリ内のCSVファイルを処理し、リサンプリングと欠損値補間を行います。
617
618        このメソッドは、指定されたディレクトリ内のCSVファイルを読み込み、リサンプリングを行い、
619        欠損値を補完します。処理結果として以下の出力が可能です:
620        1. リサンプリングされたCSVファイル (output_resampled=True)
621        2. 相関係数やC2H6/CH4比を計算したDataFrame (output_ratio=True)
622        3. lag times解析結果 (output_lag_times=True)
623
624        Parameters:
625        -----
626            input_dir : str
627                入力CSVファイルが格納されているディレクトリのパス。
628            resampled_dir : str | None
629                リサンプリングされたCSVファイルを出力するディレクトリのパス。
630            ratio_dir : str | None
631                C2H6/CH4比の計算結果を保存するディレクトリのパス。
632            input_file_pattern : str
633                ファイル名からソートキーを抽出する正規表現パターン。
634            input_files_suffix : str
635                入力ファイルの拡張子(.datや.csvなど)。デフォルトは".dat"。
636            col_ch4_conc : str
637                CH4濃度を含む列名。デフォルトは'Ultra_CH4_ppm_C'。
638            col_c2h6_conc : str
639                C2H6濃度を含む列名。デフォルトは'Ultra_C2H6_ppb'。
640            output_ratio : bool
641                線形回帰を行うかどうか。デフォルトはTrue。
642            output_resampled : bool
643                リサンプリングされたCSVファイルを出力するかどうか。デフォルトはTrue。
644            output_lag_times : bool
645                lag times解析を行うかどうか。デフォルトはFalse。
646            lag_times_dir : str | None
647                lag times解析結果の出力ディレクトリ。
648            lag_times_col1 : str
649                lag times解析の基準変数。デフォルトは"wind_w"。
650            lag_times_col2_list : list[str]
651                lag times解析の比較変数のリスト。デフォルトは["Tv"]。
652            lag_times_median_range : float
653                lag times解析の中央値を中心とした範囲。デフォルトは20。
654            lag_times_plot_range : tuple[float, float]
655                lag times解析のヒストグラム表示範囲。デフォルトは(-50, 200)。
656            lag_times_figsize : tuple[float, float]
657                lag times解析のプロットサイズ。デフォルトは(10, 8)。
658            ratio_csv_prefix : str
659                出力ファイルの接頭辞。
660            index_column : str
661                日時情報を含む列名。デフォルトは'TIMESTAMP'。
662            index_format : str
663                インデックスの日付形式。デフォルトは'%Y-%m-%d %H:%M:%S.%f'。
664            interpolate : bool
665                欠損値補間を行うかどうか。デフォルトはTrue。
666            numeric_columns : list[str]
667                数値データを含む列名のリスト。
668            metadata_rows : int
669                メタデータとして読み込む行数。デフォルトは4。
670            skiprows : list[int]
671                読み飛ばす行のインデックスリスト。デフォルトは[0, 2, 3]。
672
673        Raises:
674        -----
675            ValueError
676                出力オプションが指定されているのにディレクトリが指定されていない場合。
677            FileNotFoundError
678                入力ファイルが見つからない場合。
679            OSError
680                ディレクトリの作成に失敗した場合。
681        """
682        # 出力オプションとディレクトリの検証
683        if output_resampled and resampled_dir is None:
684            raise ValueError(
685                "output_resampled が True の場合、resampled_dir を指定する必要があります"
686            )
687        if output_ratio and ratio_dir is None:
688            raise ValueError(
689                "output_ratio が True の場合、ratio_dir を指定する必要があります"
690            )
691        if output_lag_times and lag_times_dir is None:
692            raise ValueError(
693                "output_lag_times が True の場合、lag_times_dir を指定する必要があります"
694            )
695
696        # ディレクトリの作成(必要な場合のみ)
697        if output_resampled and resampled_dir is not None:
698            os.makedirs(resampled_dir, exist_ok=True)
699        if output_ratio and ratio_dir is not None:
700            os.makedirs(ratio_dir, exist_ok=True)
701        if output_lag_times and lag_times_dir is not None:
702            os.makedirs(lag_times_dir, exist_ok=True)
703
704        ratio_data: list[dict[str, str | float]] = []
705        all_lags_indices: list[list[int]] = []
706        latest_date: datetime = datetime.min
707
708        # csvファイル名のリスト
709        csv_files: list[str] = EddyDataPreprocessor._get_sorted_files(
710            input_dir, input_file_pattern, input_files_suffix
711        )
712
713        if not csv_files:
714            raise FileNotFoundError(
715                f"There is no '{input_files_suffix}' file to process; input_dir: '{input_dir}'"
716            )
717
718        for filename in tqdm(csv_files, desc="Processing files"):
719            input_filepath: str = os.path.join(input_dir, filename)
720            # リサンプリング&欠損値補間
721            df, metadata = self.get_resampled_df(
722                filepath=input_filepath,
723                index_column=index_column,
724                index_format=index_format,
725                interpolate=interpolate,
726                numeric_columns=numeric_columns,
727                metadata_rows=metadata_rows,
728                skiprows=skiprows,
729            )
730
731            # 開始時間を取得
732            start_time: datetime = pd.to_datetime(df[index_column].iloc[0])
733            # 処理したファイルの中で最も最新の日付を更新
734            latest_date = max(latest_date, start_time)
735
736            # リサンプリング&欠損値補間したCSVを出力
737            if output_resampled and resampled_dir is not None:
738                base_filename: str = re.sub(rf"\{input_files_suffix}$", "", filename)
739                output_csv_path: str = os.path.join(
740                    resampled_dir, f"{base_filename}-resampled.csv"
741                )
742                # メタデータを先に書き込む
743                with open(output_csv_path, "w") as f:
744                    for line in metadata:
745                        f.write(f"{line}\n")
746                # データフレームを追記モードで書き込む
747                df.to_csv(
748                    output_csv_path, index=False, mode="a", quoting=3, header=False
749                )
750
751            # 相関係数とC2H6/CH4比を計算
752            if output_ratio:
753                ch4_data: pd.Series = df[col_ch4_conc]
754                c2h6_data: pd.Series = df[col_c2h6_conc]
755
756                ratio_row: dict[str, str | float] = {
757                    "Date": start_time.strftime("%Y-%m-%d %H:%M:%S.%f"),
758                    "slope": f"{np.nan}",
759                    "intercept": f"{np.nan}",
760                    "r_value": f"{np.nan}",
761                    "p_value": f"{np.nan}",
762                    "stderr": f"{np.nan}",
763                }
764
765                # 近似直線の傾き、切片、相関係数を計算
766                try:
767                    slope, intercept, r_value, p_value, stderr = stats.linregress(
768                        ch4_data, c2h6_data
769                    )
770                    ratio_row = {
771                        "Date": start_time.strftime("%Y-%m-%d %H:%M:%S.%f"),
772                        "slope": f"{slope:.6f}",
773                        "intercept": f"{intercept:.6f}",
774                        "r_value": f"{r_value:.6f}",
775                        "p_value": f"{p_value:.6f}",
776                        "stderr": f"{stderr:.6f}",
777                    }
778                except Exception:
779                    # 何もせず、デフォルトの ratio_row を使用する
780                    pass
781
782                ratio_data.append(ratio_row)
783
784            # Lag times解析用のデータを収集
785            if output_lag_times:
786                df = self.add_uvw_columns(df)
787                lags_list = EddyDataPreprocessor._calculate_lag_time(
788                    df,
789                    lag_times_col1,
790                    lag_times_col2_list,
791                )
792                all_lags_indices.append(lags_list)
793
794        # Ratio解析結果の保存
795        if output_ratio and ratio_dir is not None:
796            # DataFrameを作成し、Dateカラムで昇順ソート
797            ratio_df: pd.DataFrame = pd.DataFrame(ratio_data)
798            ratio_df["Date"] = pd.to_datetime(ratio_df["Date"])
799            ratio_df = ratio_df.sort_values("Date")
800
801            # CSVとして保存
802            ratio_filename: str = (
803                f"{ratio_csv_prefix}.{latest_date.strftime('%Y.%m.%d')}.ratio.csv"
804            )
805            ratio_path: str = os.path.join(ratio_dir, ratio_filename)
806            ratio_df.to_csv(ratio_path, index=False)
807            self.logger.info(f"Ratio解析結果を保存しました: {ratio_path}")
808
809        # Lag times解析結果の処理と保存
810        if output_lag_times and lag_times_dir is not None:
811            # lag timesの解析結果をDataFrameに変換
812            lags_indices_df = pd.DataFrame(
813                all_lags_indices, columns=lag_times_col2_list
814            )
815            lag_times_output_data = []
816
817            # 各変数に対する解析
818            for column in lags_indices_df.columns:
819                data = lags_indices_df[column]
820
821                # ヒストグラムの作成
822                plt.figure(figsize=lag_times_figsize)
823                plt.hist(data, bins=20, range=lag_times_plot_range)
824                plt.title(f"Delays of {column}")
825                plt.xlabel("Seconds")
826                plt.ylabel("Frequency")
827                plt.xlim(lag_times_plot_range)
828
829                # ヒストグラムの保存
830                filename = f"lags_histogram-{column}.png"
831                filepath = os.path.join(lag_times_dir, filename)
832                plt.savefig(filepath, dpi=300, bbox_inches="tight")
833                plt.close()
834
835                # 中央値を計算し、その周辺のデータのみを使用
836                median_value = np.median(data)
837                filtered_data = data[
838                    (data >= median_value - lag_times_median_range)
839                    & (data <= median_value + lag_times_median_range)
840                ]
841
842                # 平均値を計算
843                mean_value = np.mean(filtered_data)
844                mean_seconds = float(mean_value / self.fs)
845
846                # 結果を格納
847                lag_times_output_data.append(
848                    {
849                        "col1": lag_times_col1,
850                        "col2": column,
851                        "col2_lag": round(mean_seconds, 2),
852                        "lag_unit": "s",
853                        "median_range": lag_times_median_range,
854                    }
855                )
856
857            # 結果をCSVとして保存
858            if lag_times_output_data:
859                lag_times_df = pd.DataFrame(lag_times_output_data)
860                lag_times_csv_path = os.path.join(lag_times_dir, "lags_results.csv")
861                lag_times_df.to_csv(lag_times_csv_path, index=False, encoding="utf-8")
862                self.logger.info(
863                    f"Lag times解析結果を保存しました: {lag_times_csv_path}"
864                )
865
866                # 遅れ時間を表示
867                self.logger.info(f"カラム`{lag_times_col1}`に対する遅れ時間:")
868                max_col_name_length = max(len(column) for column in lag_times_df["col2"])
869                for _, row in lag_times_df.iterrows():
870                    print(f"{row['col2']:<{max_col_name_length}} : {row['col2_lag']:.2f} s")

指定されたディレクトリ内のCSVファイルを処理し、リサンプリングと欠損値補間を行います。

このメソッドは、指定されたディレクトリ内のCSVファイルを読み込み、リサンプリングを行い、 欠損値を補完します。処理結果として以下の出力が可能です:

  1. リサンプリングされたCSVファイル (output_resampled=True)
  2. 相関係数やC2H6/CH4比を計算したDataFrame (output_ratio=True)
  3. lag times解析結果 (output_lag_times=True)

Parameters:

input_dir : str
    入力CSVファイルが格納されているディレクトリのパス。
resampled_dir : str | None
    リサンプリングされたCSVファイルを出力するディレクトリのパス。
ratio_dir : str | None
    C2H6/CH4比の計算結果を保存するディレクトリのパス。
input_file_pattern : str
    ファイル名からソートキーを抽出する正規表現パターン。
input_files_suffix : str
    入力ファイルの拡張子(.datや.csvなど)。デフォルトは".dat"。
col_ch4_conc : str
    CH4濃度を含む列名。デフォルトは'Ultra_CH4_ppm_C'。
col_c2h6_conc : str
    C2H6濃度を含む列名。デフォルトは'Ultra_C2H6_ppb'。
output_ratio : bool
    線形回帰を行うかどうか。デフォルトはTrue。
output_resampled : bool
    リサンプリングされたCSVファイルを出力するかどうか。デフォルトはTrue。
output_lag_times : bool
    lag times解析を行うかどうか。デフォルトはFalse。
lag_times_dir : str | None
    lag times解析結果の出力ディレクトリ。
lag_times_col1 : str
    lag times解析の基準変数。デフォルトは"wind_w"。
lag_times_col2_list : list[str]
    lag times解析の比較変数のリスト。デフォルトは["Tv"]。
lag_times_median_range : float
    lag times解析の中央値を中心とした範囲。デフォルトは20。
lag_times_plot_range : tuple[float, float]
    lag times解析のヒストグラム表示範囲。デフォルトは(-50, 200)。
lag_times_figsize : tuple[float, float]
    lag times解析のプロットサイズ。デフォルトは(10, 8)。
ratio_csv_prefix : str
    出力ファイルの接頭辞。
index_column : str
    日時情報を含む列名。デフォルトは'TIMESTAMP'。
index_format : str
    インデックスの日付形式。デフォルトは'%Y-%m-%d %H:%M:%S.%f'。
interpolate : bool
    欠損値補間を行うかどうか。デフォルトはTrue。
numeric_columns : list[str]
    数値データを含む列名のリスト。
metadata_rows : int
    メタデータとして読み込む行数。デフォルトは4。
skiprows : list[int]
    読み飛ばす行のインデックスリスト。デフォルトは[0, 2, 3]。

Raises:

ValueError
    出力オプションが指定されているのにディレクトリが指定されていない場合。
FileNotFoundError
    入力ファイルが見つからない場合。
OSError
    ディレクトリの作成に失敗した場合。
@staticmethod
def setup_logger(logger: logging.Logger | None, log_level: int = 20) -> logging.Logger:
 982    @staticmethod
 983    def setup_logger(logger: Logger | None, log_level: int = INFO) -> Logger:
 984        """
 985        ロガーを設定します。
 986
 987        このメソッドは、ロギングの設定を行い、ログメッセージのフォーマットを指定します。
 988        ログメッセージには、日付、ログレベル、メッセージが含まれます。
 989
 990        渡されたロガーがNoneまたは不正な場合は、新たにロガーを作成し、標準出力に
 991        ログメッセージが表示されるようにStreamHandlerを追加します。ロガーのレベルは
 992        引数で指定されたlog_levelに基づいて設定されます。
 993
 994        Parameters:
 995        -----
 996            logger : Logger | None
 997                使用するロガー。Noneの場合は新しいロガーを作成します。
 998            log_level : int
 999                ロガーのログレベル。デフォルトはINFO。
1000
1001        Returns:
1002        -----
1003            Logger
1004                設定されたロガーオブジェクト。
1005        """
1006        if logger is not None and isinstance(logger, Logger):
1007            return logger
1008        # 渡されたロガーがNoneまたは正しいものでない場合は独自に設定
1009        new_logger: Logger = getLogger()
1010        # 既存のハンドラーをすべて削除
1011        for handler in new_logger.handlers[:]:
1012            new_logger.removeHandler(handler)
1013        new_logger.setLevel(log_level)  # ロガーのレベルを設定
1014        ch = StreamHandler()
1015        ch_formatter = Formatter("%(asctime)s - %(levelname)s - %(message)s")
1016        ch.setFormatter(ch_formatter)  # フォーマッターをハンドラーに設定
1017        new_logger.addHandler(ch)  # StreamHandlerの追加
1018        return new_logger

ロガーを設定します。

このメソッドは、ロギングの設定を行い、ログメッセージのフォーマットを指定します。 ログメッセージには、日付、ログレベル、メッセージが含まれます。

渡されたロガーがNoneまたは不正な場合は、新たにロガーを作成し、標準出力に ログメッセージが表示されるようにStreamHandlerを追加します。ロガーのレベルは 引数で指定されたlog_levelに基づいて設定されます。

Parameters:

logger : Logger | None
    使用するロガー。Noneの場合は新しいロガーを作成します。
log_level : int
    ロガーのログレベル。デフォルトはINFO。

Returns:

Logger
    設定されたロガーオブジェクト。
class SpectrumCalculator:
  7class SpectrumCalculator:
  8    def __init__(
  9        self,
 10        df: pd.DataFrame,
 11        fs: float,
 12        lag_second: float,
 13        cols_apply_lag_time: list[str],
 14        apply_window: bool = True,
 15        plots: int = 30,
 16        window_type: str = "hamming",
 17    ):
 18        """
 19        データロガーから取得したデータファイルを用いて計算を行うクラス。
 20
 21        Parameters:
 22        ------
 23            df : pd.DataFrame
 24                pandasのデータフレーム。解析対象のデータを含む。
 25            cols_apply_lag_time : list[str]
 26                コスペクトルの遅れ時間補正を適用するキーのリスト。
 27            fs : float
 28                サンプリング周波数(Hz)。データのサンプリングレートを指定。
 29            lag_second : float
 30                遅延時間(秒)。データの遅延を指定。
 31            apply_window : bool, optional
 32                窓関数を適用するフラグ。デフォルトはTrue。
 33            plots : int
 34                プロットする点の数。可視化のためのデータポイント数。
 35        """
 36        self._df: pd.DataFrame = df
 37        self._fs: float = fs
 38        self._cols_apply_lag_time: list[str] = cols_apply_lag_time
 39        self._apply_window: bool = apply_window
 40        self._lag_second: float = lag_second
 41        self._plots: int = plots
 42        self._window_type: str = window_type
 43
 44    def calculate_co_spectrum(
 45        self,
 46        col1: str,
 47        col2: str,
 48        dimensionless: bool = True,
 49        frequency_weighted: bool = True,
 50        interpolate_points: bool = True,
 51        scaling: str = "spectrum",
 52    ) -> tuple:
 53        """
 54        指定されたcol1とcol2のコスペクトルをDataFrameから計算するためのメソッド。
 55
 56        Parameters:
 57        ------
 58            col1 : str
 59                データの列名1。
 60            col2 : str
 61                データの列名2。
 62            dimensionless : bool, optional
 63                Trueの場合、分散で割って無次元化を行う。デフォルトはTrue。
 64            frequency_weighted : bool, optional
 65                周波数の重みづけを適用するかどうか。デフォルトはTrue。
 66            interpolate_points : bool, optional
 67                等間隔なデータ点を生成するかどうか(対数軸上で等間隔)。デフォルトはTrue。
 68            scaling : str
 69                "density"でスペクトル密度、"spectrum"でスペクトル。デフォルトは"spectrum"。
 70
 71        Returns:
 72        ------
 73            tuple
 74                (freqs, co_spectrum, corr_coef)
 75                - freqs : np.ndarray
 76                    周波数軸(対数スケールの場合は対数変換済み)。
 77                - co_spectrum : np.ndarray
 78                    コスペクトル(対数スケールの場合は対数変換済み)。
 79                - corr_coef : float
 80                    変数の相関係数。
 81        """
 82        freqs, co_spectrum, _, corr_coef = self.calculate_cross_spectrum(
 83            col1=col1,
 84            col2=col2,
 85            dimensionless=dimensionless,
 86            frequency_weighted=frequency_weighted,
 87            interpolate_points=interpolate_points,
 88            scaling=scaling,
 89        )
 90        return freqs, co_spectrum, corr_coef
 91
 92    def calculate_cross_spectrum(
 93        self,
 94        col1: str,
 95        col2: str,
 96        dimensionless: bool = True,
 97        frequency_weighted: bool = True,
 98        interpolate_points: bool = True,
 99        scaling: str = "spectrum",
100    ) -> tuple:
101        """
102        指定されたcol1とcol2のコスペクトルとクアドラチャスペクトルをDataFrameから計算するためのメソッド。
103
104        Parameters:
105        ------
106            col1 : str
107                データの列名1。
108            col2 : str
109                データの列名2。
110            dimensionless : bool, optional
111                Trueの場合、分散で割って無次元化を行う。デフォルトはTrue。
112            frequency_weighted : bool, optional
113                周波数の重みづけを適用するかどうか。デフォルトはTrue。
114            interpolate_points : bool, optional
115                等間隔なデータ点を生成するかどうか(対数軸上で等間隔)。デフォルトはTrue。
116            scaling : str
117                "density"でスペクトル密度、"spectrum"でスペクトル。デフォルトは"spectrum"。
118
119        Returns:
120        ------
121            tuple
122                (freqs, co_spectrum, quadrature_spectrum, corr_coef)
123                - freqs : np.ndarray
124                    周波数軸(対数スケールの場合は対数変換済み)。
125                - co_spectrum : np.ndarray
126                    コスペクトル(対数スケールの場合は対数変換済み)。
127                - quadrature_spectrum : np.ndarray
128                    クアドラチャスペクトル(対数スケールの場合は対数変換済み)。
129                - corr_coef : float
130                    変数の相関係数。
131        """
132        # バリデーション
133        valid_scaling_options = ["density", "spectrum"]
134        if scaling not in valid_scaling_options:
135            raise ValueError(
136                f"'scaling'は次のパラメータから選択してください: {valid_scaling_options}"
137            )
138
139        fs: float = self._fs
140        df: pd.DataFrame = self._df.copy()
141        # col1とcol2に一致するデータを取得
142        data1: np.ndarray = np.array(df[col1].values)
143        data2: np.ndarray = np.array(df[col2].values)
144
145        # 遅れ時間の補正
146        if col2 in self._cols_apply_lag_time:
147            data1, data2 = SpectrumCalculator._correct_lag_time(
148                data1=data1, data2=data2, fs=fs, lag_second=self._lag_second
149            )
150
151        # トレンド除去
152        data1 = SpectrumCalculator._detrend(data=data1, fs=fs, first=True)
153        data2 = SpectrumCalculator._detrend(data=data2, fs=fs, first=True)
154
155        # トレンド除去後のデータでパラメータを計算
156        data_length: int = len(data1)  # データ長
157        corr_coef: float = np.corrcoef(data1, data2)[0, 1]  # 相関係数の計算
158
159        # 窓関数の適用
160        window_scale = 1.0
161        if self._apply_window:
162            window = SpectrumCalculator._generate_window_function(
163                type=self._window_type, data_length=data_length
164            )
165            data1 *= window
166            data2 *= window
167            window_scale = np.mean(window**2)
168
169        # FFTの計算
170        fft1 = np.fft.rfft(data1)
171        fft2 = np.fft.rfft(data2)
172
173        # 周波数軸の作成
174        freqs: np.ndarray = np.fft.rfftfreq(data_length, 1.0 / self._fs)
175
176        # fft.cと同様のコスペクトル計算ロジック
177        co_spectrum = np.zeros(len(freqs))
178        quad_spectrum = np.zeros(len(freqs))
179
180        for i in range(1, len(freqs)):  # 0Hz成分を除外
181            z1 = fft1[i]
182            z2 = fft2[i]
183            z1_star = np.conj(z1)
184            z2_star = np.conj(z2)
185
186            # x1 = z1 + z1*, x2 = z2 + z2*
187            x1 = z1 + z1_star
188            x2 = z2 + z2_star
189            x1_re = x1.real
190            x1_im = x1.imag
191            x2_re = x2.real
192            x2_im = x2.imag
193
194            # y1 = z1 - z1*, y2 = z2 - z2*
195            y1 = z1 - z1_star
196            y2 = z2 - z2_star
197            # 虚部と実部を入れ替え
198            y1_re = y1.imag
199            y1_im = -y1.real
200            y2_re = y2.imag
201            y2_im = -y2.real
202
203            # コスペクトルとクァドラチャスペクトルの計算
204            conj_x1_x2 = complex(
205                x1_re * x2_re + x1_im * x2_im, x1_im * x2_re - x1_re * x2_im
206            )
207            conj_y1_y2 = complex(
208                y1_re * y2_re + y1_im * y2_im, y1_im * y2_re - y1_re * y2_im
209            )
210
211            # スケーリングパラメータを計算
212            scale_factor = 0.5 / (len(data1) * window_scale)  # spectrumの場合
213            # スペクトル密度の場合、周波数間隔で正規化
214            if scaling == "density":
215                df = freqs[1] - freqs[0]  # 周波数間隔
216                scale_factor = 0.5 / (len(data1) * window_scale * df)
217
218            # スケーリングを適用
219            co_spectrum[i] = conj_x1_x2.real * scale_factor
220            quad_spectrum[i] = conj_y1_y2.real * scale_factor
221
222        # 周波数の重みづけ
223        if frequency_weighted:
224            co_spectrum[1:] *= freqs[1:]
225            quad_spectrum[1:] *= freqs[1:]
226
227        # 無次元化
228        if dimensionless:
229            cov_matrix: np.ndarray = np.cov(data1, data2)
230            covariance: float = cov_matrix[0, 1]  # 共分散
231            co_spectrum /= covariance
232            quad_spectrum /= covariance
233
234        if interpolate_points:
235            # 補間処理(0Hz除外の前に実施)
236            log_freq_min = np.log10(0.001)
237            log_freq_max = np.log10(freqs[-1])
238            log_freq_resampled = np.logspace(log_freq_min, log_freq_max, self._plots)
239
240            # コスペクトルとクアドラチャスペクトルの補間
241            co_resampled = np.interp(
242                log_freq_resampled, freqs, co_spectrum, left=np.nan, right=np.nan
243            )
244            quad_resampled = np.interp(
245                log_freq_resampled, freqs, quad_spectrum, left=np.nan, right=np.nan
246            )
247
248            # NaNを除外
249            valid_mask = ~np.isnan(co_resampled)
250            freqs = log_freq_resampled[valid_mask]
251            co_spectrum = co_resampled[valid_mask]
252            quad_spectrum = quad_resampled[valid_mask]
253
254        # 0Hz成分を除外
255        nonzero_mask = freqs != 0
256        freqs = freqs[nonzero_mask]
257        co_spectrum = co_spectrum[nonzero_mask]
258        quad_spectrum = quad_spectrum[nonzero_mask]
259
260        return freqs, co_spectrum, quad_spectrum, corr_coef
261
262    def calculate_power_spectrum(
263        self,
264        col: str,
265        dimensionless: bool = True,
266        frequency_weighted: bool = True,
267        interpolate_points: bool = True,
268        scaling: str = "spectrum",
269    ) -> tuple:
270        """
271        指定されたcolに基づいてDataFrameからパワースペクトルと周波数軸を計算します。
272        scipy.signal.welchを使用してパワースペクトルを計算します。
273
274        Parameters:
275        ------
276            col : str
277                データの列名
278            dimensionless : bool, optional
279                Trueの場合、分散で割って無次元化を行います。デフォルトはTrueです。
280            frequency_weighted : bool, optional
281                周波数の重みづけを適用するかどうか。デフォルトはTrueです。
282            interpolate_points : bool, optional
283                等間隔なデータ点を生成するかどうか(対数軸上で等間隔)。
284            scaling : str, optional
285                "density"でスペクトル密度、"spectrum"でスペクトル。デフォルトは"spectrum"です。
286
287        Returns:
288        ------
289            tuple
290                - freqs (np.ndarray): 周波数軸(対数スケールの場合は対数変換済み)
291                - power_spectrum (np.ndarray): パワースペクトル(対数スケールの場合は対数変換済み)
292        """
293        # バリデーション
294        valid_scaling_options = ["density", "spectrum"]
295        if scaling not in valid_scaling_options:
296            raise ValueError(
297                f"'scaling'は次のパラメータから選択してください: {valid_scaling_options}"
298            )
299
300        # データの取得とトレンド除去
301        data: np.ndarray = np.array(self._df[col].values)
302        data = SpectrumCalculator._detrend(data, self._fs)
303
304        # welchメソッドでパワースペクトル計算
305        freqs, power_spectrum = signal.welch(
306            data, fs=self._fs, window=self._window_type, nperseg=1024, scaling=scaling
307        )
308
309        # 周波数の重みづけ(0Hz除外の前に実施)
310        if frequency_weighted:
311            power_spectrum = freqs * power_spectrum
312
313        # # 無次元化(0Hz除外の前に実施)
314        if dimensionless:
315            variance = np.var(data)
316            power_spectrum /= variance
317
318        if interpolate_points:
319            # 補間処理(0Hz除外の前に実施)
320            log_freq_min = np.log10(0.001)
321            log_freq_max = np.log10(freqs[-1])
322            log_freq_resampled = np.logspace(log_freq_min, log_freq_max, self._plots)
323
324            power_spectrum_resampled = np.interp(
325                log_freq_resampled, freqs, power_spectrum, left=np.nan, right=np.nan
326            )
327
328            # NaNを除外
329            valid_mask = ~np.isnan(power_spectrum_resampled)
330            freqs = log_freq_resampled[valid_mask]
331            power_spectrum = power_spectrum_resampled[valid_mask]
332
333        # 0Hz成分を最後に除外
334        nonzero_mask = freqs != 0
335        freqs = freqs[nonzero_mask]
336        power_spectrum = power_spectrum[nonzero_mask]
337
338        return freqs, power_spectrum
339
340    @staticmethod
341    def _correct_lag_time(
342        data1: np.ndarray,
343        data2: np.ndarray,
344        fs: float,
345        lag_second: float,
346    ) -> tuple:
347        """
348        相互相関関数を用いて遅れ時間を補正する。クロススペクトルの計算に使用。
349
350        Parameters:
351        ------
352            data1 : np.ndarray
353                基準データ
354            data2 : np.ndarray
355                遅れているデータ
356            fs : float
357                サンプリング周波数
358            lag_second : float
359                data1からdata2が遅れている時間(秒)。負の値は許可されない。
360
361        Returns:
362        ------
363            tuple
364                - data1 : np.ndarray
365                    補正された基準データ
366                - data2 : np.ndarray
367                    補正された遅れているデータ
368
369        Raises:
370        ------
371            ValueError
372                lag_secondが負の値の場合
373        """
374        if lag_second < 0:
375            raise ValueError("lag_second must be non-negative.")
376        # lag_secondをサンプリング周波数でスケーリングしてインデックスに変換
377        lag_index: int = int(lag_second * fs)
378        # データ1とデータ2の共通部分を抽出
379        data1 = data1[lag_index:]
380        data2 = data2[:-lag_index]
381        return data1, data2
382
383    @staticmethod
384    def _detrend(
385        data: np.ndarray, fs: float, first: bool = True, second: bool = False
386    ) -> np.ndarray:
387        """
388        データから一次トレンドおよび二次トレンドを除去します。
389
390        Parameters:
391        ------
392            data : np.ndarray
393                入力データ
394            fs : float
395                サンプリング周波数
396            first : bool, optional
397                一次トレンドを除去するかどうか. デフォルトはTrue.
398            second : bool, optional
399                二次トレンドを除去するかどうか. デフォルトはFalse.
400
401        Returns:
402        ------
403            np.ndarray
404                トレンド除去後のデータ
405
406        Raises:
407        ------
408            ValueError
409                first と second の両方がFalseの場合
410        """
411        if not (first or second):
412            raise ValueError("少なくとも一次または二次トレンドの除去を指定してください")
413
414        detrended_data: np.ndarray = data.copy()
415
416        # 一次トレンドの除去
417        if first:
418            detrended_data = signal.detrend(detrended_data)
419
420        # 二次トレンドの除去
421        if second:
422            # 二次トレンドを除去するために、まず一次トレンドを除去
423            detrended_data = signal.detrend(detrended_data, type="linear")
424            # 二次トレンドを除去するために、二次多項式フィッティングを行う
425            coeffs_second = np.polyfit(
426                np.arange(len(detrended_data)), detrended_data, 2
427            )
428            trend_second = np.polyval(coeffs_second, np.arange(len(detrended_data)))
429            detrended_data = detrended_data - trend_second
430
431        return detrended_data
432
433    @staticmethod
434    def _generate_window_function(type: str, data_length: int) -> np.ndarray:
435        """
436        指定された種類の窓関数を適用する
437
438        Parameters:
439        ------
440            type : str
441                窓関数の種類 ('hanning', 'hamming', 'blackman')
442            data_length : int
443                データ長
444
445        Returns:
446        ------
447            np.ndarray
448                適用された窓関数
449
450        Notes:
451        ------
452            - 指定された種類の窓関数を適用し、numpy配列として返す
453            - 無効な種類が指定された場合、警告を表示しHann窓を適用する
454        """
455        if type == "hanning":
456            return np.hanning(data_length)
457        elif type == "hamming":
458            return np.hamming(data_length)
459        elif type == "blackman":
460            return np.blackman(data_length)
461        else:
462            print('Warning: Invalid argument "type". Return hanning window.')
463            return np.hanning(data_length)
464
465    @staticmethod
466    def _smooth_spectrum(
467        spectrum: np.ndarray, frequencies: np.ndarray, freq_threshold: float = 0.1
468    ) -> np.ndarray:
469        """
470        高周波数領域に対して3点移動平均を適用する処理を行う。
471        この処理により、高周波数成分のノイズを低減し、スペクトルの滑らかさを向上させる。
472        
473        Parameters:
474        ------
475            spectrum : np.ndarray
476                スペクトルデータ
477            frequencies : np.ndarray
478                対応する周波数データ
479            freq_threshold : float
480                高周波数の閾値
481        
482        Returns:
483        ------
484            np.ndarray
485                スムーズ化されたスペクトルデータ
486        """
487        smoothed = spectrum.copy()  # オリジナルデータのコピーを作成
488
489        # 周波数閾値以上の部分のインデックスを取得
490        high_freq_mask = frequencies >= freq_threshold
491
492        # 高周波数領域のみを処理
493        high_freq_indices = np.where(high_freq_mask)[0]
494        if len(high_freq_indices) > 2:  # 最低3点必要
495            for i in high_freq_indices[1:-1]:  # 端点を除く
496                smoothed[i] = (
497                    0.25 * spectrum[i - 1] + 0.5 * spectrum[i] + 0.25 * spectrum[i + 1]
498                )
499
500            # 高周波領域の端点の処理
501            first_idx = high_freq_indices[0]
502            last_idx = high_freq_indices[-1]
503            smoothed[first_idx] = 0.5 * (spectrum[first_idx] + spectrum[first_idx + 1])
504            smoothed[last_idx] = 0.5 * (spectrum[last_idx - 1] + spectrum[last_idx])
505
506        return smoothed
SpectrumCalculator( df: pandas.core.frame.DataFrame, fs: float, lag_second: float, cols_apply_lag_time: list[str], apply_window: bool = True, plots: int = 30, window_type: str = 'hamming')
 8    def __init__(
 9        self,
10        df: pd.DataFrame,
11        fs: float,
12        lag_second: float,
13        cols_apply_lag_time: list[str],
14        apply_window: bool = True,
15        plots: int = 30,
16        window_type: str = "hamming",
17    ):
18        """
19        データロガーから取得したデータファイルを用いて計算を行うクラス。
20
21        Parameters:
22        ------
23            df : pd.DataFrame
24                pandasのデータフレーム。解析対象のデータを含む。
25            cols_apply_lag_time : list[str]
26                コスペクトルの遅れ時間補正を適用するキーのリスト。
27            fs : float
28                サンプリング周波数(Hz)。データのサンプリングレートを指定。
29            lag_second : float
30                遅延時間(秒)。データの遅延を指定。
31            apply_window : bool, optional
32                窓関数を適用するフラグ。デフォルトはTrue。
33            plots : int
34                プロットする点の数。可視化のためのデータポイント数。
35        """
36        self._df: pd.DataFrame = df
37        self._fs: float = fs
38        self._cols_apply_lag_time: list[str] = cols_apply_lag_time
39        self._apply_window: bool = apply_window
40        self._lag_second: float = lag_second
41        self._plots: int = plots
42        self._window_type: str = window_type

データロガーから取得したデータファイルを用いて計算を行うクラス。

Parameters:

df : pd.DataFrame
    pandasのデータフレーム。解析対象のデータを含む。
cols_apply_lag_time : list[str]
    コスペクトルの遅れ時間補正を適用するキーのリスト。
fs : float
    サンプリング周波数(Hz)。データのサンプリングレートを指定。
lag_second : float
    遅延時間(秒)。データの遅延を指定。
apply_window : bool, optional
    窓関数を適用するフラグ。デフォルトはTrue。
plots : int
    プロットする点の数。可視化のためのデータポイント数。
def calculate_co_spectrum( self, col1: str, col2: str, dimensionless: bool = True, frequency_weighted: bool = True, interpolate_points: bool = True, scaling: str = 'spectrum') -> tuple:
44    def calculate_co_spectrum(
45        self,
46        col1: str,
47        col2: str,
48        dimensionless: bool = True,
49        frequency_weighted: bool = True,
50        interpolate_points: bool = True,
51        scaling: str = "spectrum",
52    ) -> tuple:
53        """
54        指定されたcol1とcol2のコスペクトルをDataFrameから計算するためのメソッド。
55
56        Parameters:
57        ------
58            col1 : str
59                データの列名1。
60            col2 : str
61                データの列名2。
62            dimensionless : bool, optional
63                Trueの場合、分散で割って無次元化を行う。デフォルトはTrue。
64            frequency_weighted : bool, optional
65                周波数の重みづけを適用するかどうか。デフォルトはTrue。
66            interpolate_points : bool, optional
67                等間隔なデータ点を生成するかどうか(対数軸上で等間隔)。デフォルトはTrue。
68            scaling : str
69                "density"でスペクトル密度、"spectrum"でスペクトル。デフォルトは"spectrum"。
70
71        Returns:
72        ------
73            tuple
74                (freqs, co_spectrum, corr_coef)
75                - freqs : np.ndarray
76                    周波数軸(対数スケールの場合は対数変換済み)。
77                - co_spectrum : np.ndarray
78                    コスペクトル(対数スケールの場合は対数変換済み)。
79                - corr_coef : float
80                    変数の相関係数。
81        """
82        freqs, co_spectrum, _, corr_coef = self.calculate_cross_spectrum(
83            col1=col1,
84            col2=col2,
85            dimensionless=dimensionless,
86            frequency_weighted=frequency_weighted,
87            interpolate_points=interpolate_points,
88            scaling=scaling,
89        )
90        return freqs, co_spectrum, corr_coef

指定されたcol1とcol2のコスペクトルをDataFrameから計算するためのメソッド。

Parameters:

col1 : str
    データの列名1。
col2 : str
    データの列名2。
dimensionless : bool, optional
    Trueの場合、分散で割って無次元化を行う。デフォルトはTrue。
frequency_weighted : bool, optional
    周波数の重みづけを適用するかどうか。デフォルトはTrue。
interpolate_points : bool, optional
    等間隔なデータ点を生成するかどうか(対数軸上で等間隔)。デフォルトはTrue。
scaling : str
    "density"でスペクトル密度、"spectrum"でスペクトル。デフォルトは"spectrum"。

Returns:

tuple
    (freqs, co_spectrum, corr_coef)
    - freqs : np.ndarray
        周波数軸(対数スケールの場合は対数変換済み)。
    - co_spectrum : np.ndarray
        コスペクトル(対数スケールの場合は対数変換済み)。
    - corr_coef : float
        変数の相関係数。
def calculate_cross_spectrum( self, col1: str, col2: str, dimensionless: bool = True, frequency_weighted: bool = True, interpolate_points: bool = True, scaling: str = 'spectrum') -> tuple:
 92    def calculate_cross_spectrum(
 93        self,
 94        col1: str,
 95        col2: str,
 96        dimensionless: bool = True,
 97        frequency_weighted: bool = True,
 98        interpolate_points: bool = True,
 99        scaling: str = "spectrum",
100    ) -> tuple:
101        """
102        指定されたcol1とcol2のコスペクトルとクアドラチャスペクトルをDataFrameから計算するためのメソッド。
103
104        Parameters:
105        ------
106            col1 : str
107                データの列名1。
108            col2 : str
109                データの列名2。
110            dimensionless : bool, optional
111                Trueの場合、分散で割って無次元化を行う。デフォルトはTrue。
112            frequency_weighted : bool, optional
113                周波数の重みづけを適用するかどうか。デフォルトはTrue。
114            interpolate_points : bool, optional
115                等間隔なデータ点を生成するかどうか(対数軸上で等間隔)。デフォルトはTrue。
116            scaling : str
117                "density"でスペクトル密度、"spectrum"でスペクトル。デフォルトは"spectrum"。
118
119        Returns:
120        ------
121            tuple
122                (freqs, co_spectrum, quadrature_spectrum, corr_coef)
123                - freqs : np.ndarray
124                    周波数軸(対数スケールの場合は対数変換済み)。
125                - co_spectrum : np.ndarray
126                    コスペクトル(対数スケールの場合は対数変換済み)。
127                - quadrature_spectrum : np.ndarray
128                    クアドラチャスペクトル(対数スケールの場合は対数変換済み)。
129                - corr_coef : float
130                    変数の相関係数。
131        """
132        # バリデーション
133        valid_scaling_options = ["density", "spectrum"]
134        if scaling not in valid_scaling_options:
135            raise ValueError(
136                f"'scaling'は次のパラメータから選択してください: {valid_scaling_options}"
137            )
138
139        fs: float = self._fs
140        df: pd.DataFrame = self._df.copy()
141        # col1とcol2に一致するデータを取得
142        data1: np.ndarray = np.array(df[col1].values)
143        data2: np.ndarray = np.array(df[col2].values)
144
145        # 遅れ時間の補正
146        if col2 in self._cols_apply_lag_time:
147            data1, data2 = SpectrumCalculator._correct_lag_time(
148                data1=data1, data2=data2, fs=fs, lag_second=self._lag_second
149            )
150
151        # トレンド除去
152        data1 = SpectrumCalculator._detrend(data=data1, fs=fs, first=True)
153        data2 = SpectrumCalculator._detrend(data=data2, fs=fs, first=True)
154
155        # トレンド除去後のデータでパラメータを計算
156        data_length: int = len(data1)  # データ長
157        corr_coef: float = np.corrcoef(data1, data2)[0, 1]  # 相関係数の計算
158
159        # 窓関数の適用
160        window_scale = 1.0
161        if self._apply_window:
162            window = SpectrumCalculator._generate_window_function(
163                type=self._window_type, data_length=data_length
164            )
165            data1 *= window
166            data2 *= window
167            window_scale = np.mean(window**2)
168
169        # FFTの計算
170        fft1 = np.fft.rfft(data1)
171        fft2 = np.fft.rfft(data2)
172
173        # 周波数軸の作成
174        freqs: np.ndarray = np.fft.rfftfreq(data_length, 1.0 / self._fs)
175
176        # fft.cと同様のコスペクトル計算ロジック
177        co_spectrum = np.zeros(len(freqs))
178        quad_spectrum = np.zeros(len(freqs))
179
180        for i in range(1, len(freqs)):  # 0Hz成分を除外
181            z1 = fft1[i]
182            z2 = fft2[i]
183            z1_star = np.conj(z1)
184            z2_star = np.conj(z2)
185
186            # x1 = z1 + z1*, x2 = z2 + z2*
187            x1 = z1 + z1_star
188            x2 = z2 + z2_star
189            x1_re = x1.real
190            x1_im = x1.imag
191            x2_re = x2.real
192            x2_im = x2.imag
193
194            # y1 = z1 - z1*, y2 = z2 - z2*
195            y1 = z1 - z1_star
196            y2 = z2 - z2_star
197            # 虚部と実部を入れ替え
198            y1_re = y1.imag
199            y1_im = -y1.real
200            y2_re = y2.imag
201            y2_im = -y2.real
202
203            # コスペクトルとクァドラチャスペクトルの計算
204            conj_x1_x2 = complex(
205                x1_re * x2_re + x1_im * x2_im, x1_im * x2_re - x1_re * x2_im
206            )
207            conj_y1_y2 = complex(
208                y1_re * y2_re + y1_im * y2_im, y1_im * y2_re - y1_re * y2_im
209            )
210
211            # スケーリングパラメータを計算
212            scale_factor = 0.5 / (len(data1) * window_scale)  # spectrumの場合
213            # スペクトル密度の場合、周波数間隔で正規化
214            if scaling == "density":
215                df = freqs[1] - freqs[0]  # 周波数間隔
216                scale_factor = 0.5 / (len(data1) * window_scale * df)
217
218            # スケーリングを適用
219            co_spectrum[i] = conj_x1_x2.real * scale_factor
220            quad_spectrum[i] = conj_y1_y2.real * scale_factor
221
222        # 周波数の重みづけ
223        if frequency_weighted:
224            co_spectrum[1:] *= freqs[1:]
225            quad_spectrum[1:] *= freqs[1:]
226
227        # 無次元化
228        if dimensionless:
229            cov_matrix: np.ndarray = np.cov(data1, data2)
230            covariance: float = cov_matrix[0, 1]  # 共分散
231            co_spectrum /= covariance
232            quad_spectrum /= covariance
233
234        if interpolate_points:
235            # 補間処理(0Hz除外の前に実施)
236            log_freq_min = np.log10(0.001)
237            log_freq_max = np.log10(freqs[-1])
238            log_freq_resampled = np.logspace(log_freq_min, log_freq_max, self._plots)
239
240            # コスペクトルとクアドラチャスペクトルの補間
241            co_resampled = np.interp(
242                log_freq_resampled, freqs, co_spectrum, left=np.nan, right=np.nan
243            )
244            quad_resampled = np.interp(
245                log_freq_resampled, freqs, quad_spectrum, left=np.nan, right=np.nan
246            )
247
248            # NaNを除外
249            valid_mask = ~np.isnan(co_resampled)
250            freqs = log_freq_resampled[valid_mask]
251            co_spectrum = co_resampled[valid_mask]
252            quad_spectrum = quad_resampled[valid_mask]
253
254        # 0Hz成分を除外
255        nonzero_mask = freqs != 0
256        freqs = freqs[nonzero_mask]
257        co_spectrum = co_spectrum[nonzero_mask]
258        quad_spectrum = quad_spectrum[nonzero_mask]
259
260        return freqs, co_spectrum, quad_spectrum, corr_coef

指定されたcol1とcol2のコスペクトルとクアドラチャスペクトルをDataFrameから計算するためのメソッド。

Parameters:

col1 : str
    データの列名1。
col2 : str
    データの列名2。
dimensionless : bool, optional
    Trueの場合、分散で割って無次元化を行う。デフォルトはTrue。
frequency_weighted : bool, optional
    周波数の重みづけを適用するかどうか。デフォルトはTrue。
interpolate_points : bool, optional
    等間隔なデータ点を生成するかどうか(対数軸上で等間隔)。デフォルトはTrue。
scaling : str
    "density"でスペクトル密度、"spectrum"でスペクトル。デフォルトは"spectrum"。

Returns:

tuple
    (freqs, co_spectrum, quadrature_spectrum, corr_coef)
    - freqs : np.ndarray
        周波数軸(対数スケールの場合は対数変換済み)。
    - co_spectrum : np.ndarray
        コスペクトル(対数スケールの場合は対数変換済み)。
    - quadrature_spectrum : np.ndarray
        クアドラチャスペクトル(対数スケールの場合は対数変換済み)。
    - corr_coef : float
        変数の相関係数。
def calculate_power_spectrum( self, col: str, dimensionless: bool = True, frequency_weighted: bool = True, interpolate_points: bool = True, scaling: str = 'spectrum') -> tuple:
262    def calculate_power_spectrum(
263        self,
264        col: str,
265        dimensionless: bool = True,
266        frequency_weighted: bool = True,
267        interpolate_points: bool = True,
268        scaling: str = "spectrum",
269    ) -> tuple:
270        """
271        指定されたcolに基づいてDataFrameからパワースペクトルと周波数軸を計算します。
272        scipy.signal.welchを使用してパワースペクトルを計算します。
273
274        Parameters:
275        ------
276            col : str
277                データの列名
278            dimensionless : bool, optional
279                Trueの場合、分散で割って無次元化を行います。デフォルトはTrueです。
280            frequency_weighted : bool, optional
281                周波数の重みづけを適用するかどうか。デフォルトはTrueです。
282            interpolate_points : bool, optional
283                等間隔なデータ点を生成するかどうか(対数軸上で等間隔)。
284            scaling : str, optional
285                "density"でスペクトル密度、"spectrum"でスペクトル。デフォルトは"spectrum"です。
286
287        Returns:
288        ------
289            tuple
290                - freqs (np.ndarray): 周波数軸(対数スケールの場合は対数変換済み)
291                - power_spectrum (np.ndarray): パワースペクトル(対数スケールの場合は対数変換済み)
292        """
293        # バリデーション
294        valid_scaling_options = ["density", "spectrum"]
295        if scaling not in valid_scaling_options:
296            raise ValueError(
297                f"'scaling'は次のパラメータから選択してください: {valid_scaling_options}"
298            )
299
300        # データの取得とトレンド除去
301        data: np.ndarray = np.array(self._df[col].values)
302        data = SpectrumCalculator._detrend(data, self._fs)
303
304        # welchメソッドでパワースペクトル計算
305        freqs, power_spectrum = signal.welch(
306            data, fs=self._fs, window=self._window_type, nperseg=1024, scaling=scaling
307        )
308
309        # 周波数の重みづけ(0Hz除外の前に実施)
310        if frequency_weighted:
311            power_spectrum = freqs * power_spectrum
312
313        # # 無次元化(0Hz除外の前に実施)
314        if dimensionless:
315            variance = np.var(data)
316            power_spectrum /= variance
317
318        if interpolate_points:
319            # 補間処理(0Hz除外の前に実施)
320            log_freq_min = np.log10(0.001)
321            log_freq_max = np.log10(freqs[-1])
322            log_freq_resampled = np.logspace(log_freq_min, log_freq_max, self._plots)
323
324            power_spectrum_resampled = np.interp(
325                log_freq_resampled, freqs, power_spectrum, left=np.nan, right=np.nan
326            )
327
328            # NaNを除外
329            valid_mask = ~np.isnan(power_spectrum_resampled)
330            freqs = log_freq_resampled[valid_mask]
331            power_spectrum = power_spectrum_resampled[valid_mask]
332
333        # 0Hz成分を最後に除外
334        nonzero_mask = freqs != 0
335        freqs = freqs[nonzero_mask]
336        power_spectrum = power_spectrum[nonzero_mask]
337
338        return freqs, power_spectrum

指定されたcolに基づいてDataFrameからパワースペクトルと周波数軸を計算します。 scipy.signal.welchを使用してパワースペクトルを計算します。

Parameters:

col : str
    データの列名
dimensionless : bool, optional
    Trueの場合、分散で割って無次元化を行います。デフォルトはTrueです。
frequency_weighted : bool, optional
    周波数の重みづけを適用するかどうか。デフォルトはTrueです。
interpolate_points : bool, optional
    等間隔なデータ点を生成するかどうか(対数軸上で等間隔)。
scaling : str, optional
    "density"でスペクトル密度、"spectrum"でスペクトル。デフォルトは"spectrum"です。

Returns:

tuple
    - freqs (np.ndarray): 周波数軸(対数スケールの場合は対数変換済み)
    - power_spectrum (np.ndarray): パワースペクトル(対数スケールの場合は対数変換済み)
@dataclass
class HotspotData:
 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
    ホットスポットの種類
HotspotData( source: str, angle: float, avg_lat: float, avg_lon: float, delta_ch4: float, delta_c2h6: float, correlation: float, ratio: float, section: int, type: Literal['bio', 'gas', 'comb'])
source: str
angle: float
avg_lat: float
avg_lon: float
delta_ch4: float
delta_c2h6: float
correlation: float
ratio: float
section: int
type: Literal['bio', 'gas', 'comb']
HotspotType = typing.Literal['bio', 'gas', 'comb']
class FluxFootprintAnalyzer:
  18class FluxFootprintAnalyzer:
  19    """
  20    フラックスフットプリントを解析および可視化するクラス。
  21
  22    このクラスは、フラックスデータの処理、フットプリントの計算、
  23    および結果を衛星画像上に可視化するメソッドを提供します。
  24    座標系と単位に関する重要な注意:
  25    - すべての距離はメートル単位で計算されます
  26    - 座標系の原点(0,0)は測定タワーの位置に対応します
  27    - x軸は東西方向(正が東)
  28    - y軸は南北方向(正が北)
  29    - 風向は気象学的風向(北から時計回りに測定)を使用
  30
  31    この実装は、Kormann and Meixner (2001) および Takano et al. (2021)に基づいています。
  32    """
  33
  34    EARTH_RADIUS_METER: int = 6371000  # 地球の半径(メートル)
  35
  36    def __init__(
  37        self,
  38        z_m: float,
  39        labelsize: float = 20,
  40        ticksize: float = 16,
  41        plot_params=None,
  42        logger: Logger | None = None,
  43        logging_debug: bool = False,
  44    ):
  45        """
  46        衛星画像を用いて FluxFootprintAnalyzer を初期化します。
  47
  48        Parameters:
  49        ------
  50            z_m : float
  51                測定の高さ(メートル単位)。
  52            labelsize : float
  53                軸ラベルのフォントサイズ。デフォルトは20。
  54            ticksize : float
  55                軸目盛りのフォントサイズ。デフォルトは16。
  56            plot_params : Optional[Dict[str, any]]
  57                matplotlibのプロットパラメータを指定する辞書。
  58            logger : Logger | None
  59                使用するロガー。Noneの場合は新しいロガーを生成します。
  60            logging_debug : bool
  61                ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。
  62        """
  63        # 定数や共通の変数
  64        self._required_columns: list[str] = [
  65            "Date",
  66            "WS vector",
  67            "u*",
  68            "z/L",
  69            "Wind direction",
  70            "sigmaV",
  71        ]  # 必要なカラムの名前
  72        self._col_weekday: str = "ffa_is_weekday"  # クラスで生成するカラムのキー名
  73        self._z_m: float = z_m  # 測定高度
  74        # 状態を管理するフラグ
  75        self._got_satellite_image: bool = False
  76
  77        # 図表の初期設定
  78        FluxFootprintAnalyzer.setup_plot_params(labelsize, ticksize, plot_params)
  79        # ロガー
  80        log_level: int = INFO
  81        if logging_debug:
  82            log_level = DEBUG
  83        self.logger: Logger = FluxFootprintAnalyzer.setup_logger(logger, log_level)
  84
  85    @staticmethod
  86    def setup_logger(logger: Logger | None, log_level: int = INFO) -> Logger:
  87        """
  88        ロガーを設定します。
  89
  90        このメソッドは、ロギングの設定を行い、ログメッセージのフォーマットを指定します。
  91        ログメッセージには、日付、ログレベル、メッセージが含まれます。
  92
  93        渡されたロガーがNoneまたは不正な場合は、新たにロガーを作成し、標準出力に
  94        ログメッセージが表示されるようにStreamHandlerを追加します。ロガーのレベルは
  95        引数で指定されたlog_levelに基づいて設定されます。
  96
  97        Parameters:
  98        ------
  99            logger : Logger | None
 100                使用するロガー。Noneの場合は新しいロガーを作成します。
 101            log_level : int
 102                ロガーのログレベル。デフォルトはINFO。
 103
 104        Returns:
 105        ------
 106            Logger
 107                設定されたロガーオブジェクト。
 108        """
 109        if logger is not None and isinstance(logger, Logger):
 110            return logger
 111        # 渡されたロガーがNoneまたは正しいものでない場合は独自に設定
 112        new_logger: Logger = getLogger()
 113        # 既存のハンドラーをすべて削除
 114        for handler in new_logger.handlers[:]:
 115            new_logger.removeHandler(handler)
 116        new_logger.setLevel(log_level)  # ロガーのレベルを設定
 117        ch = StreamHandler()
 118        ch_formatter = Formatter("%(asctime)s - %(levelname)s - %(message)s")
 119        ch.setFormatter(ch_formatter)  # フォーマッターをハンドラーに設定
 120        new_logger.addHandler(ch)  # StreamHandlerの追加
 121        return new_logger
 122
 123    @staticmethod
 124    def setup_plot_params(labelsize: float, ticksize: float, plot_params=None) -> None:
 125        """
 126        matplotlibのプロットパラメータを設定します。
 127
 128        Parameters:
 129        ------
 130            labelsize : float
 131                軸ラベルのフォントサイズ。
 132            ticksize : float
 133                軸目盛りのフォントサイズ。
 134            plot_params : Optional[Dict[str, any]]
 135                matplotlibのプロットパラメータの辞書。
 136
 137        Returns:
 138        ------
 139            None
 140                このメソッドは戻り値を持ちませんが、プロットパラメータを更新します。
 141        """
 142        # デフォルトのプロットパラメータ
 143        default_params = {
 144            "font.family": ["Arial", "Dejavu Sans"],
 145            "axes.edgecolor": "None",
 146            "axes.labelcolor": "black",
 147            "text.color": "black",
 148            "xtick.color": "black",
 149            "ytick.color": "black",
 150            "grid.color": "gray",
 151            "axes.grid": False,
 152            "xtick.major.size": 0,
 153            "ytick.major.size": 0,
 154            "ytick.direction": "out",
 155            "ytick.major.width": 1.0,
 156            "axes.linewidth": 1.0,
 157            "grid.linewidth": 1.0,
 158            "font.size": labelsize,
 159            "xtick.labelsize": ticksize,
 160            "ytick.labelsize": ticksize,
 161        }
 162
 163        # plot_paramsが定義されている場合、デフォルトに追記
 164        if plot_params:
 165            default_params.update(plot_params)
 166
 167        plt.rcParams.update(default_params)  # プロットパラメータを更新
 168
 169    def calculate_flux_footprint(
 170        self,
 171        df: pd.DataFrame,
 172        col_flux: str,
 173        plot_count: int = 10000,
 174        start_time: str = "10:00",
 175        end_time: str = "16:00",
 176    ) -> tuple[list[float], list[float], list[float]]:
 177        """
 178        フラックスフットプリントを計算し、指定された時間帯のデータを基に可視化します。
 179
 180        Parameters:
 181        ------
 182            df : pd.DataFrame
 183                分析対象のデータフレーム。フラックスデータを含む。
 184            col_flux : str
 185                フラックスデータの列名。計算に使用される。
 186            plot_count : int, optional
 187                生成するプロットの数。デフォルトは10000。
 188            start_time : str, optional
 189                フットプリント計算に使用する開始時間。デフォルトは"10:00"。
 190            end_time : str, optional
 191                フットプリント計算に使用する終了時間。デフォルトは"16:00"。
 192
 193        Returns:
 194        ------
 195            tuple[list[float], list[float], list[float]]:
 196                x座標 (メートル): タワーを原点とした東西方向の距離
 197                y座標 (メートル): タワーを原点とした南北方向の距離
 198                対象スカラー量の値: 各地点でのフラックス値
 199
 200        Notes:
 201        ------
 202            - 返却される座標は測定タワーを原点(0,0)とした相対位置です
 203            - すべての距離はメートル単位で表されます
 204            - 正のx値は東方向、正のy値は北方向を示します
 205        """
 206        df: pd.DataFrame = df.copy()
 207
 208        # インデックスがdatetimeであることを確認し、必要に応じて変換
 209        if not isinstance(df.index, pd.DatetimeIndex):
 210            df.index = pd.to_datetime(df.index)
 211
 212        # DatetimeIndexから直接dateプロパティにアクセス
 213        datelist: np.ndarray = np.array(df.index.date)
 214
 215        # 各日付が平日かどうかを判定し、リストに格納
 216        numbers: list[int] = [
 217            FluxFootprintAnalyzer.is_weekday(date) for date in datelist
 218        ]
 219
 220        # col_weekdayに基づいてデータフレームに平日情報を追加
 221        df.loc[:, self._col_weekday] = numbers  # .locを使用して値を設定
 222
 223        # 値が1のもの(平日)をコピーする
 224        data_weekday: pd.DataFrame = df[df[self._col_weekday] == 1].copy()
 225        # 特定の時間帯を抽出
 226        data_weekday = data_weekday.between_time(
 227            start_time, end_time
 228        )  # 引数を使用して時間帯を抽出
 229        data_weekday = data_weekday.dropna(subset=[col_flux])
 230
 231        directions: list[float] = [
 232            wind_direction if wind_direction >= 0 else wind_direction + 360
 233            for wind_direction in data_weekday["Wind direction"]
 234        ]
 235
 236        data_weekday.loc[:, "Wind direction_360"] = directions
 237        data_weekday.loc[:, "radian"] = data_weekday["Wind direction_360"] / 180 * np.pi
 238
 239        # 風向が欠測なら除去
 240        data_weekday = data_weekday.dropna(subset=["Wind direction", col_flux])
 241
 242        # 数値型への変換を確実に行う
 243        numeric_columns: list[str] = ["u*", "WS vector", "sigmaV", "z/L"]
 244        for col in numeric_columns:
 245            data_weekday[col] = pd.to_numeric(data_weekday[col], errors="coerce")
 246
 247        # 地面修正量dの計算
 248        z_m: float = self._z_m
 249        Z_d: float = FluxFootprintAnalyzer._calculate_ground_correction(
 250            z_m=z_m,
 251            wind_speed=data_weekday["WS vector"].values,
 252            friction_velocity=data_weekday["u*"].values,
 253            stability_parameter=data_weekday["z/L"].values,
 254        )
 255
 256        x_list: list[float] = []
 257        y_list: list[float] = []
 258        c_list: list[float] | None = []
 259
 260        # tqdmを使用してプログレスバーを表示
 261        for i in tqdm(range(len(data_weekday)), desc="Calculating footprint"):
 262            dUstar: float = data_weekday["u*"].iloc[i]
 263            dU: float = data_weekday["WS vector"].iloc[i]
 264            sigmaV: float = data_weekday["sigmaV"].iloc[i]
 265            dzL: float = data_weekday["z/L"].iloc[i]
 266
 267            if pd.isna(dUstar) or pd.isna(dU) or pd.isna(sigmaV) or pd.isna(dzL):
 268                self.logger.warning(f"#N/A fields are exist.: i = {i}")
 269                continue
 270            elif dUstar < 5.0 and dUstar != 0.0 and dU > 0.1:
 271                phi_m, phi_c, n = FluxFootprintAnalyzer._calculate_stability_parameters(
 272                    dzL=dzL
 273                )
 274                m, U, r, mu, ksi = (
 275                    FluxFootprintAnalyzer._calculate_footprint_parameters(
 276                        dUstar=dUstar, dU=dU, Z_d=Z_d, phi_m=phi_m, phi_c=phi_c, n=n
 277                    )
 278                )
 279
 280                # 80%ソースエリアの計算
 281                x80: float = FluxFootprintAnalyzer._source_area_KM2001(
 282                    ksi=ksi, mu=mu, dU=dU, sigmaV=sigmaV, Z_d=Z_d, max_ratio=0.8
 283                )
 284
 285                if not np.isnan(x80):
 286                    x1, y1, flux1 = FluxFootprintAnalyzer._prepare_plot_data(
 287                        x80,
 288                        ksi,
 289                        mu,
 290                        r,
 291                        U,
 292                        m,
 293                        sigmaV,
 294                        data_weekday[col_flux].iloc[i],
 295                        plot_count=plot_count,
 296                    )
 297                    x1_, y1_ = FluxFootprintAnalyzer._rotate_coordinates(
 298                        x=x1, y=y1, radian=data_weekday["radian"].iloc[i]
 299                    )
 300
 301                    x_list.extend(x1_)
 302                    y_list.extend(y1_)
 303                    c_list.extend(flux1)
 304
 305        return (
 306            x_list,
 307            y_list,
 308            c_list,
 309        )
 310
 311    def combine_all_data(
 312        self, data_source: str | pd.DataFrame, source_type: str = "csv", **kwargs
 313    ) -> pd.DataFrame:
 314        """
 315        CSVファイルまたはMonthlyConverterからのデータを統合します
 316
 317        Parameters:
 318        ------
 319            data_source : str | pd.DataFrame
 320                CSVディレクトリパスまたはDataFrame
 321            source_type : str
 322                "csv" または "monthly"
 323            **kwargs :
 324                追加パラメータ
 325                - sheet_names : list[str]
 326                    Monthlyの場合のシート名
 327                - start_date : str
 328                    開始日
 329                - end_date : str
 330                    終了日
 331
 332        Returns:
 333        ------
 334            pd.DataFrame
 335                処理済みのデータフレーム
 336        """
 337        if source_type == "csv":
 338            # 既存のCSV処理ロジック
 339            return self._combine_all_csv(data_source)
 340        elif source_type == "monthly":
 341            # MonthlyConverterからのデータを処理
 342            if not isinstance(data_source, pd.DataFrame):
 343                raise ValueError("monthly形式の場合、DataFrameを直接渡す必要があります")
 344
 345            df = data_source.copy()
 346
 347            # required_columnsからDateを除外して欠損値チェックを行う
 348            check_columns = [col for col in self._required_columns if col != "Date"]
 349
 350            # インデックスがdatetimeであることを確認
 351            if not isinstance(df.index, pd.DatetimeIndex) and "Date" not in df.columns:
 352                raise ValueError("DatetimeIndexまたはDateカラムが必要です")
 353
 354            if "Date" in df.columns:
 355                df.set_index("Date", inplace=True)
 356
 357            # 必要なカラムの存在確認
 358            missing_columns = [
 359                col for col in check_columns if col not in df.columns.tolist()
 360            ]
 361            if missing_columns:
 362                missing_cols = "','".join(missing_columns)
 363                current_cols = "','".join(df.columns.tolist())
 364                raise ValueError(
 365                    f"必要なカラムが不足しています: '{missing_cols}'\n"
 366                    f"現在のカラム: '{current_cols}'"
 367                )
 368
 369            # 平日/休日の判定用カラムを追加
 370            df[self._col_weekday] = df.index.map(FluxFootprintAnalyzer.is_weekday)
 371
 372            # Dateを除外したカラムで欠損値の処理
 373            df = df.dropna(subset=check_columns)
 374
 375            # インデックスの重複を除去
 376            df = df.loc[~df.index.duplicated(), :]
 377
 378            return df
 379        else:
 380            raise ValueError("source_typeは'csv'または'monthly'である必要があります")
 381
 382    def get_satellite_image_from_api(
 383        self,
 384        api_key: str,
 385        center_lat: float,
 386        center_lon: float,
 387        output_path: str,
 388        scale: int = 1,
 389        size: tuple[int, int] = (2160, 2160),
 390        zoom: int = 13,
 391    ) -> ImageFile:
 392        """
 393        Google Maps Static APIを使用して衛星画像を取得します。
 394
 395        Parameters:
 396        ------
 397            api_key : str
 398                Google Maps Static APIのキー。
 399            center_lat : float
 400                中心の緯度。
 401            center_lon : float
 402                中心の経度。
 403            output_path : str
 404                画像の保存先パス。拡張子は'.png'のみ許可される。
 405            scale : int, optional
 406                画像の解像度スケール(1か2)。デフォルトは1。
 407            size : tuple[int, int], optional
 408                画像サイズ (幅, 高さ)。デフォルトは(2160, 2160)。
 409            zoom : int, optional
 410                ズームレベル(0-21)。デフォルトは13。
 411
 412        Returns:
 413        ------
 414            ImageFile
 415                取得した衛星画像
 416
 417        Raises:
 418        ------
 419            requests.RequestException
 420                API呼び出しに失敗した場合
 421        """
 422        # バリデーション
 423        if not output_path.endswith(".png"):
 424            raise ValueError("出力ファイル名は'.png'で終わる必要があります。")
 425
 426        # HTTPリクエストの定義
 427        base_url = "https://maps.googleapis.com/maps/api/staticmap"
 428        params = {
 429            "center": f"{center_lat},{center_lon}",
 430            "zoom": zoom,
 431            "size": f"{size[0]}x{size[1]}",
 432            "maptype": "satellite",
 433            "scale": scale,
 434            "key": api_key,
 435        }
 436
 437        try:
 438            response = requests.get(base_url, params=params)
 439            response.raise_for_status()
 440            # 画像ファイルに変換
 441            image = Image.open(io.BytesIO(response.content))
 442            image.save(output_path)
 443            self._got_satellite_image = True
 444            self.logger.info(f"リモート画像を取得し、保存しました: {output_path}")
 445            return image
 446        except requests.RequestException as e:
 447            self.logger.error(f"衛星画像の取得に失敗しました: {str(e)}")
 448            raise
 449
 450    def get_satellite_image_from_local(
 451        self,
 452        local_image_path: str,
 453    ) -> ImageFile:
 454        """
 455        ローカルファイルから衛星画像を読み込みます。
 456
 457        Parameters:
 458        ------
 459            local_image_path : str
 460                ローカル画像のパス
 461
 462        Returns:
 463        ------
 464            ImageFile
 465                読み込んだ衛星画像
 466
 467        Raises:
 468        ------
 469            FileNotFoundError
 470                指定されたパスにファイルが存在しない場合
 471        """
 472        if not os.path.exists(local_image_path):
 473            raise FileNotFoundError(
 474                f"指定されたローカル画像が存在しません: {local_image_path}"
 475            )
 476        image = Image.open(local_image_path)
 477        self._got_satellite_image = True
 478        self.logger.info(f"ローカル画像を使用しました: {local_image_path}")
 479        return image
 480
 481    def plot_flux_footprint(
 482        self,
 483        x_list: list[float],
 484        y_list: list[float],
 485        c_list: list[float] | None,
 486        center_lat: float,
 487        center_lon: float,
 488        vmin: float,
 489        vmax: float,
 490        add_cbar: bool = True,
 491        add_legend: bool = True,
 492        cbar_label: str | None = None,
 493        cbar_labelpad: int = 20,
 494        cmap: str = "jet",
 495        reduce_c_function: callable = np.mean,
 496        lat_correction: float = 1,
 497        lon_correction: float = 1,
 498        output_dir: str | None = None,
 499        output_filename: str = "footprint.png",
 500        save_fig: bool = True,
 501        show_fig: bool = True,
 502        satellite_image: ImageFile | None = None,
 503        xy_max: float = 5000,
 504    ) -> None:
 505        """
 506        フットプリントデータをプロットします。
 507
 508        このメソッドは、指定されたフットプリントデータのみを可視化します。
 509
 510        Parameters:
 511        ------
 512            x_list : list[float]
 513                フットプリントのx座標リスト(メートル単位)。
 514            y_list : list[float]
 515                フットプリントのy座標リスト(メートル単位)。
 516            c_list : list[float] | None
 517                フットプリントの強度を示す値のリスト。
 518            center_lat : float
 519                プロットの中心となる緯度。
 520            center_lon : float
 521                プロットの中心となる経度。
 522            cmap : str
 523                使用するカラーマップの名前。
 524            vmin : float
 525                カラーバーの最小値。
 526            vmax : float
 527                カラーバーの最大値。
 528            reduce_c_function : callable, optional
 529                フットプリントの集約関数(デフォルトはnp.mean)。
 530            cbar_label : str | None, optional
 531                カラーバーのラベル。
 532            cbar_labelpad : int, optional
 533                カラーバーラベルのパディング。
 534            lon_correction : float, optional
 535                経度方向の補正係数(デフォルトは1)。
 536            lat_correction : float, optional
 537                緯度方向の補正係数(デフォルトは1)。
 538            output_dir : str | None, optional
 539                プロット画像の保存先パス。
 540            output_filename : str
 541                プロット画像の保存ファイル名(拡張子を含む)。デフォルトは'footprint.png'。
 542            save_fig : bool
 543                図の保存を許可するフラグ。デフォルトはTrue。
 544            show_fig : bool
 545                図の表示を許可するフラグ。デフォルトはTrue。
 546            satellite_image : ImageFile | None, optional
 547                使用する衛星画像。指定がない場合はデフォルトの画像が生成されます。
 548            xy_max : float, optional
 549                表示範囲の最大値(デフォルトは4000)。
 550        """
 551        self.plot_flux_footprint_with_hotspots(
 552            x_list=x_list,
 553            y_list=y_list,
 554            c_list=c_list,
 555            center_lat=center_lat,
 556            center_lon=center_lon,
 557            vmin=vmin,
 558            vmax=vmax,
 559            add_cbar=add_cbar,
 560            add_legend=add_legend,
 561            cbar_label=cbar_label,
 562            cbar_labelpad=cbar_labelpad,
 563            cmap=cmap,
 564            reduce_c_function=reduce_c_function,
 565            hotspots=None,  # hotspotsをNoneに設定
 566            hotspot_colors=None,
 567            lat_correction=lat_correction,
 568            lon_correction=lon_correction,
 569            output_dir=output_dir,
 570            output_filename=output_filename,
 571            save_fig=save_fig,
 572            show_fig=show_fig,
 573            satellite_image=satellite_image,
 574            xy_max=xy_max,
 575        )
 576
 577    def plot_flux_footprint_with_hotspots(
 578        self,
 579        x_list: list[float],
 580        y_list: list[float],
 581        c_list: list[float] | None,
 582        center_lat: float,
 583        center_lon: float,
 584        vmin: float,
 585        vmax: float,
 586        add_cbar: bool = True,
 587        add_legend: bool = True,
 588        cbar_label: str | None = None,
 589        cbar_labelpad: int = 20,
 590        cmap: str = "jet",
 591        reduce_c_function: callable = np.mean,
 592        hotspots: list[HotspotData] | None = None,
 593        hotspot_colors: dict[HotspotType, str] | None = None,
 594        hotspot_markers: dict[HotspotType, str] | None = None,
 595        lat_correction: float = 1,
 596        lon_correction: float = 1,
 597        output_dir: str | None = None,
 598        output_filename: str = "footprint.png",
 599        save_fig: bool = True,
 600        show_fig: bool = True,
 601        satellite_image: ImageFile | None = None,
 602        xy_max: float = 5000,
 603    ) -> None:
 604        """
 605        Staticな衛星画像上にフットプリントデータとホットスポットをプロットします。
 606
 607        このメソッドは、指定されたフットプリントデータとホットスポットを可視化します。
 608        ホットスポットが指定されない場合は、フットプリントのみ作図します。
 609
 610        Parameters:
 611        ------
 612            x_list : list[float]
 613                フットプリントのx座標リスト(メートル単位)。
 614            y_list : list[float]
 615                フットプリントのy座標リスト(メートル単位)。
 616            c_list : list[float] | None
 617                フットプリントの強度を示す値のリスト。
 618            center_lat : float
 619                プロットの中心となる緯度。
 620            center_lon : float
 621                プロットの中心となる経度。
 622            vmin : float
 623                カラーバーの最小値。
 624            vmax : float
 625                カラーバーの最大値。
 626            add_cbar : bool, optional
 627                カラーバーを追加するかどうか(デフォルトはTrue)。
 628            add_legend : bool, optional
 629                凡例を追加するかどうか(デフォルトはTrue)。
 630            cbar_label : str | None, optional
 631                カラーバーのラベル。
 632            cbar_labelpad : int, optional
 633                カラーバーラベルのパディング。
 634            cmap : str
 635                使用するカラーマップの名前。
 636            reduce_c_function : callable
 637                フットプリントの集約関数(デフォルトはnp.mean)。
 638            hotspots : list[HotspotData] | None, optional
 639                ホットスポットデータのリスト。デフォルトはNone。
 640            hotspot_colors : dict[HotspotType, str] | None, optional
 641                ホットスポットの色を指定する辞書。
 642            hotspot_markers : dict[HotspotType, str] | None, optional
 643                ホットスポットの形状を指定する辞書。
 644                指定の例は {'bio': '^', 'gas': 'o', 'comb': 's'} (三角、丸、四角)など。
 645            lat_correction : float, optional
 646                緯度方向の補正係数(デフォルトは1)。
 647            lon_correction : float, optional
 648                経度方向の補正係数(デフォルトは1)。
 649            output_dir : str | None, optional
 650                プロット画像の保存先パス。
 651            output_filename : str
 652                プロット画像の保存ファイル名(拡張子を含む)。デフォルトは'footprint.png'。
 653            save_fig : bool
 654                図の保存を許可するフラグ。デフォルトはTrue。
 655            show_fig : bool
 656                図の表示を許可するフラグ。デフォルトはTrue。
 657            satellite_image : ImageFile | None, optional
 658                使用する衛星画像。指定がない場合はデフォルトの画像が生成されます。
 659            xy_max : float, optional
 660                表示範囲の最大値(デフォルトは5000)。
 661        """
 662        # 1. 引数のバリデーション
 663        valid_extensions: list[str] = [".png", ".jpg", ".jpeg", ".pdf", ".svg"]
 664        _, file_extension = os.path.splitext(output_filename)
 665        if file_extension.lower() not in valid_extensions:
 666            quoted_extensions: list[str] = [f'"{ext}"' for ext in valid_extensions]
 667            self.logger.error(
 668                f"`output_filename`は有効な拡張子ではありません。プロットを保存するには、次のいずれかを指定してください: {','.join(quoted_extensions)}"
 669            )
 670            return
 671
 672        # 2. フラグチェック
 673        if not self._got_satellite_image:
 674            raise ValueError(
 675                "`get_satellite_image_from_api`または`get_satellite_image_from_local`が実行されていません。"
 676            )
 677
 678        # 3. 衛星画像の取得
 679        if satellite_image is None:
 680            satellite_image = Image.new("RGB", (2160, 2160), "lightgray")
 681
 682        self.logger.info("プロットを作成中...")
 683
 684        # 4. 座標変換のための定数計算(1回だけ)
 685        meters_per_lat: float = self.EARTH_RADIUS_METER * (
 686            math.pi / 180
 687        )  # 緯度1度あたりのメートル
 688        meters_per_lon: float = meters_per_lat * math.cos(
 689            math.radians(center_lat)
 690        )  # 経度1度あたりのメートル
 691
 692        # 5. フットプリントデータの座標変換(まとめて1回で実行)
 693        x_deg = (
 694            np.array(x_list) / meters_per_lon * lon_correction
 695        )  # 補正係数も同時に適用
 696        y_deg = (
 697            np.array(y_list) / meters_per_lat * lat_correction
 698        )  # 補正係数も同時に適用
 699
 700        # 6. 中心点からの相対座標を実際の緯度経度に変換
 701        lons = center_lon + x_deg
 702        lats = center_lat + y_deg
 703
 704        # 7. 表示範囲の計算(変更なし)
 705        x_range: float = xy_max / meters_per_lon
 706        y_range: float = xy_max / meters_per_lat
 707        map_boundaries: tuple[float, float, float, float] = (
 708            center_lon - x_range,  # left_lon
 709            center_lon + x_range,  # right_lon
 710            center_lat - y_range,  # bottom_lat
 711            center_lat + y_range,  # top_lat
 712        )
 713        left_lon, right_lon, bottom_lat, top_lat = map_boundaries
 714
 715        # 8. プロットの作成
 716        plt.rcParams["axes.edgecolor"] = "None"
 717        fig: plt.Figure = plt.figure(figsize=(10, 8), dpi=300)
 718        ax_data: plt.Axes = fig.add_axes([0.05, 0.1, 0.8, 0.8])
 719
 720        # 9. フットプリントの描画
 721        # フットプリントの描画とカラーバー用の2つのhexbinを作成
 722        if c_list is not None:
 723            ax_data.hexbin(
 724                lons,
 725                lats,
 726                C=c_list,
 727                cmap=cmap,
 728                vmin=vmin,
 729                vmax=vmax,
 730                alpha=0.3,  # 実際のプロット用
 731                gridsize=100,
 732                linewidths=0,
 733                mincnt=100,
 734                extent=[left_lon, right_lon, bottom_lat, top_lat],
 735                reduce_C_function=reduce_c_function,
 736            )
 737
 738        # カラーバー用の非表示hexbin(alpha=1.0)
 739        hidden_hexbin = ax_data.hexbin(
 740            lons,
 741            lats,
 742            C=c_list,
 743            cmap=cmap,
 744            vmin=vmin,
 745            vmax=vmax,
 746            alpha=1.0,  # カラーバー用
 747            gridsize=100,
 748            linewidths=0,
 749            mincnt=100,
 750            extent=[left_lon, right_lon, bottom_lat, top_lat],
 751            reduce_C_function=reduce_c_function,
 752            visible=False,  # プロットには表示しない
 753        )
 754
 755        # 10. ホットスポットの描画
 756        spot_handles = []
 757        if hotspots is not None:
 758            default_colors: dict[HotspotType, str] = {
 759                "bio": "blue",
 760                "gas": "red",
 761                "comb": "green",
 762            }
 763
 764            # デフォルトのマーカー形状を定義
 765            default_markers: dict[HotspotType, str] = {
 766                "bio": "o",
 767                "gas": "o",
 768                "comb": "o",
 769            }
 770
 771            # 座標変換のための定数
 772            meters_per_lat: float = self.EARTH_RADIUS_METER * (math.pi / 180)
 773            meters_per_lon: float = meters_per_lat * math.cos(math.radians(center_lat))
 774
 775            for spot_type, color in (hotspot_colors or default_colors).items():
 776                spots_lon = []
 777                spots_lat = []
 778
 779                # 使用するマーカーを決定
 780                marker = (hotspot_markers or default_markers).get(spot_type, "o")
 781
 782                for spot in hotspots:
 783                    if spot.type == spot_type:
 784                        # 変換前の緯度経度をログ出力
 785                        self.logger.debug(
 786                            f"Before - Type: {spot_type}, Lat: {spot.avg_lat:.6f}, Lon: {spot.avg_lon:.6f}"
 787                        )
 788
 789                        # 中心からの相対距離を計算
 790                        dx: float = (spot.avg_lon - center_lon) * meters_per_lon
 791                        dy: float = (spot.avg_lat - center_lat) * meters_per_lat
 792
 793                        # 補正前の相対座標をログ出力
 794                        self.logger.debug(
 795                            f"Relative - Type: {spot_type}, X: {dx:.2f}m, Y: {dy:.2f}m"
 796                        )
 797
 798                        # 補正を適用
 799                        corrected_dx: float = dx * lon_correction
 800                        corrected_dy: float = dy * lat_correction
 801
 802                        # 補正後の緯度経度を計算
 803                        adjusted_lon: float = center_lon + corrected_dx / meters_per_lon
 804                        adjusted_lat: float = center_lat + corrected_dy / meters_per_lat
 805
 806                        # 変換後の緯度経度をログ出力
 807                        self.logger.debug(
 808                            f"After - Type: {spot_type}, Lat: {adjusted_lat:.6f}, Lon: {adjusted_lon:.6f}\n"
 809                        )
 810
 811                        if (
 812                            left_lon <= adjusted_lon <= right_lon
 813                            and bottom_lat <= adjusted_lat <= top_lat
 814                        ):
 815                            spots_lon.append(adjusted_lon)
 816                            spots_lat.append(adjusted_lat)
 817
 818                if spots_lon:
 819                    handle = ax_data.scatter(
 820                        spots_lon,
 821                        spots_lat,
 822                        c=color,
 823                        marker=marker,  # マーカー形状を指定
 824                        s=100,
 825                        alpha=0.7,
 826                        label=spot_type,  # "bio","gas","comb"
 827                        edgecolor="black",
 828                        linewidth=1,
 829                    )
 830                    spot_handles.append(handle)
 831
 832        # 11. 背景画像の設定
 833        ax_img = ax_data.twiny().twinx()
 834        ax_img.imshow(
 835            satellite_image,
 836            extent=[left_lon, right_lon, bottom_lat, top_lat],
 837            aspect="equal",
 838        )
 839
 840        # 12. 軸の設定
 841        for ax in [ax_data, ax_img]:
 842            ax.set_xlim(left_lon, right_lon)
 843            ax.set_ylim(bottom_lat, top_lat)
 844            ax.set_xticks([])
 845            ax.set_yticks([])
 846
 847        ax_data.set_zorder(2)
 848        ax_data.patch.set_alpha(0)
 849        ax_img.set_zorder(1)
 850
 851        # 13. カラーバーの追加
 852        if add_cbar:
 853            cbar_ax: plt.Axes = fig.add_axes([0.88, 0.1, 0.03, 0.8])
 854            cbar = fig.colorbar(hidden_hexbin, cax=cbar_ax)  # hidden_hexbinを使用
 855            # cbar_labelが指定されている場合のみラベルを設定
 856            if cbar_label:
 857                cbar.set_label(cbar_label, rotation=270, labelpad=cbar_labelpad)
 858
 859        # 14. ホットスポットの凡例追加
 860        if add_legend and hotspots and spot_handles:
 861            ax_data.legend(
 862                handles=spot_handles,
 863                loc="upper center",  # 位置を上部中央に
 864                bbox_to_anchor=(0.55, -0.01),  # 図の下に配置
 865                ncol=len(spot_handles),  # ハンドルの数に応じて列数を設定
 866            )
 867
 868        # 15. 画像の保存
 869        if save_fig:
 870            if output_dir is None:
 871                raise ValueError(
 872                    "save_fig=Trueの場合、output_dirを指定する必要があります。有効なディレクトリパスを指定してください。"
 873                )
 874            output_path: str = os.path.join(output_dir, output_filename)
 875            self.logger.info("プロットを保存中...")
 876            try:
 877                fig.savefig(output_path, bbox_inches="tight")
 878                self.logger.info(f"プロットが正常に保存されました: {output_path}")
 879            except Exception as e:
 880                self.logger.error(f"プロットの保存中にエラーが発生しました: {str(e)}")
 881        # 16. 画像の表示
 882        if show_fig:
 883            plt.show()
 884        else:
 885            plt.close(fig=fig)
 886
 887    def plot_flux_footprint_with_scale_checker(
 888        self,
 889        x_list: list[float],
 890        y_list: list[float],
 891        c_list: list[float] | None,
 892        center_lat: float,
 893        center_lon: float,
 894        check_points: list[tuple[float, float, str]] | None = None,
 895        vmin: float = 0,
 896        vmax: float = 100,
 897        add_cbar: bool = True,
 898        cbar_label: str | None = None,
 899        cbar_labelpad: int = 20,
 900        cmap: str = "jet",
 901        reduce_c_function: callable = np.mean,
 902        lat_correction: float = 1,
 903        lon_correction: float = 1,
 904        output_dir: str | None = None,
 905        output_filename: str = "footprint-scale_checker.png",
 906        save_fig: bool = True,
 907        show_fig: bool = True,
 908        satellite_image: ImageFile | None = None,
 909        xy_max: float = 5000,
 910    ) -> None:
 911        """
 912        Staticな衛星画像上にフットプリントデータとホットスポットをプロットします。
 913
 914        このメソッドは、指定されたフットプリントデータとホットスポットを可視化します。
 915        ホットスポットが指定されない場合は、フットプリントのみ作図します。
 916
 917        Parameters:
 918        ------
 919            x_list : list[float]
 920                フットプリントのx座標リスト(メートル単位)。
 921            y_list : list[float]
 922                フットプリントのy座標リスト(メートル単位)。
 923            c_list : list[float] | None
 924                フットプリントの強度を示す値のリスト。
 925            center_lat : float
 926                プロットの中心となる緯度。
 927            center_lon : float
 928                プロットの中心となる経度。
 929            check_points : list[tuple[float, float, str]] | None
 930                確認用の地点リスト。各要素は (緯度, 経度, ラベル) のタプル。
 931                Noneの場合は中心から500m、1000m、2000m、3000mの位置に仮想的な点を配置。
 932            cmap : str
 933                使用するカラーマップの名前。
 934            vmin : float
 935                カラーバーの最小値。
 936            vmax : float
 937                カラーバーの最大値。
 938            reduce_c_function : callable, optional
 939                フットプリントの集約関数(デフォルトはnp.mean)。
 940            cbar_label : str, optional
 941                カラーバーのラベル。
 942            cbar_labelpad : int, optional
 943                カラーバーラベルのパディング。
 944            hotspots : list[HotspotData] | None
 945                ホットスポットデータのリスト。デフォルトはNone。
 946            hotspot_colors : dict[str, str] | None, optional
 947                ホットスポットの色を指定する辞書。
 948            lon_correction : float, optional
 949                経度方向の補正係数(デフォルトは1)。
 950            lat_correction : float, optional
 951                緯度方向の補正係数(デフォルトは1)。
 952            output_dir : str | None, optional
 953                プロット画像の保存先パス。
 954            output_filename : str
 955                プロット画像の保存ファイル名(拡張子を含む)。デフォルトは'footprint.png'。
 956            save_fig : bool
 957                図の保存を許可するフラグ。デフォルトはTrue。
 958            show_fig : bool
 959                図の表示を許可するフラグ。デフォルトはTrue。
 960            satellite_image : ImageFile | None, optional
 961                使用する衛星画像。指定がない場合はデフォルトの画像が生成されます。
 962            xy_max : float, optional
 963                表示範囲の最大値(デフォルトは5000)。
 964        """
 965        if check_points is None:
 966            # デフォルトの確認ポイントを生成(従来の方式)
 967            default_points = [
 968                (500, "North", 90),  # 北 500m
 969                (1000, "East", 0),  # 東 1000m
 970                (2000, "South", 270),  # 南 2000m
 971                (3000, "West", 180),  # 西 3000m
 972            ]
 973
 974            dummy_hotspots = []
 975            for distance, direction, angle in default_points:
 976                rad = math.radians(angle)
 977                meters_per_lat = self.EARTH_RADIUS_METER * (math.pi / 180)
 978                meters_per_lon = meters_per_lat * math.cos(math.radians(center_lat))
 979
 980                dx = distance * math.cos(rad)
 981                dy = distance * math.sin(rad)
 982
 983                delta_lon = dx / meters_per_lon
 984                delta_lat = dy / meters_per_lat
 985
 986                hotspot = HotspotData(
 987                    avg_lat=center_lat + delta_lat,
 988                    avg_lon=center_lon + delta_lon,
 989                    delta_ch4=0.0,
 990                    delta_c2h6=0.0,
 991                    ratio=0.0,
 992                    type=f"{direction}_{distance}m",
 993                    section=0,
 994                    source="scale_check",
 995                    angle=0,
 996                    correlation=0,
 997                )
 998                dummy_hotspots.append(hotspot)
 999        else:
1000            # 指定された緯度経度を使用
1001            dummy_hotspots = []
1002            for lat, lon, label in check_points:
1003                hotspot = HotspotData(
1004                    avg_lat=lat,
1005                    avg_lon=lon,
1006                    delta_ch4=0.0,
1007                    delta_c2h6=0.0,
1008                    ratio=0.0,
1009                    type=label,
1010                    section=0,
1011                    source="scale_check",
1012                    angle=0,
1013                    correlation=0,
1014                )
1015                dummy_hotspots.append(hotspot)
1016
1017        # カスタムカラーマップの作成
1018        hotspot_colors = {
1019            spot.type: plt.cm.tab10(i % 10) for i, spot in enumerate(dummy_hotspots)
1020        }
1021
1022        # 既存のメソッドを呼び出してプロット
1023        self.plot_flux_footprint_with_hotspots(
1024            x_list=x_list,
1025            y_list=y_list,
1026            c_list=c_list,
1027            center_lat=center_lat,
1028            center_lon=center_lon,
1029            vmin=vmin,
1030            vmax=vmax,
1031            add_cbar=add_cbar,
1032            add_legend=True,
1033            cbar_label=cbar_label,
1034            cbar_labelpad=cbar_labelpad,
1035            cmap=cmap,
1036            reduce_c_function=reduce_c_function,
1037            hotspots=dummy_hotspots,
1038            hotspot_colors=hotspot_colors,
1039            lat_correction=lat_correction,
1040            lon_correction=lon_correction,
1041            output_dir=output_dir,
1042            output_filename=output_filename,
1043            save_fig=save_fig,
1044            show_fig=show_fig,
1045            satellite_image=satellite_image,
1046            xy_max=xy_max,
1047        )
1048
1049    def _combine_all_csv(self, csv_dir_path: str, suffix: str = ".csv") -> pd.DataFrame:
1050        """
1051        指定されたディレクトリ内の全CSVファイルを読み込み、処理し、結合します。
1052        Monthlyシートを結合することを想定しています。
1053
1054        Parameters:
1055        ------
1056            csv_dir_path : str
1057                CSVファイルが格納されているディレクトリのパス。
1058            suffix : str, optional
1059                読み込むファイルの拡張子。デフォルトは".csv"。
1060
1061        Returns:
1062        ------
1063            pandas.DataFrame
1064                結合および処理済みのデータフレーム。
1065
1066        Notes:
1067        ------
1068            - ディレクトリ内に少なくとも1つのCSVファイルが必要です。
1069        """
1070        csv_files = [f for f in os.listdir(csv_dir_path) if f.endswith(suffix)]
1071        if not csv_files:
1072            raise ValueError("指定されたディレクトリにCSVファイルが見つかりません。")
1073
1074        df_array: list[pd.DataFrame] = []
1075        for csv_file in csv_files:
1076            file_path: str = os.path.join(csv_dir_path, csv_file)
1077            df: pd.DataFrame = self._prepare_csv(file_path)
1078            df_array.append(df)
1079
1080        # 結合
1081        df_combined: pd.DataFrame = pd.concat(df_array, join="outer")
1082        df_combined = df_combined.loc[~df_combined.index.duplicated(), :]
1083
1084        # 平日と休日の判定に使用するカラムを作成
1085        df_combined[self._col_weekday] = df_combined.index.map(
1086            FluxFootprintAnalyzer.is_weekday
1087        )  # 共通の関数を使用
1088
1089        return df_combined
1090
1091    def _prepare_csv(self, file_path: str) -> pd.DataFrame:
1092        """
1093        フラックスデータを含むCSVファイルを読み込み、処理します。
1094
1095        Parameters:
1096        ------
1097            file_path : str
1098                CSVファイルのパス。
1099
1100        Returns:
1101        ------
1102            pandas.DataFrame
1103                処理済みのデータフレーム。
1104        """
1105        # CSVファイルの最初の行を読み込み、ヘッダーを取得するための一時データフレームを作成
1106        temp: pd.DataFrame = pd.read_csv(file_path, header=None, nrows=1, skiprows=0)
1107        header = temp.loc[temp.index[0]]
1108
1109        # 実際のデータを読み込み、必要な行をスキップし、欠損値を指定
1110        df: pd.DataFrame = pd.read_csv(
1111            file_path,
1112            header=None,
1113            skiprows=2,
1114            na_values=["#DIV/0!", "#VALUE!", "#REF!", "#N/A", "#NAME?", "NAN"],
1115            low_memory=False,
1116        )
1117        # 取得したヘッダーをデータフレームに設定
1118        df.columns = header
1119
1120        # self._required_columnsのカラムが存在するか確認
1121        missing_columns: list[str] = [
1122            col for col in self._required_columns if col not in df.columns.tolist()
1123        ]
1124        if missing_columns:
1125            raise ValueError(
1126                f"必要なカラムが不足しています: {', '.join(missing_columns)}"
1127            )
1128
1129        # "Date"カラムをインデックスに設定して返却
1130        df["Date"] = pd.to_datetime(df["Date"])
1131        df = df.dropna(subset=["Date"])
1132        df.set_index("Date", inplace=True)
1133        return df
1134
1135    @staticmethod
1136    def _calculate_footprint_parameters(
1137        dUstar: float, dU: float, Z_d: float, phi_m: float, phi_c: float, n: float
1138    ) -> tuple[float, float, float, float, float]:
1139        """
1140        フットプリントパラメータを計算します。
1141
1142        Parameters:
1143        ------
1144            dUstar : float
1145                摩擦速度
1146            dU : float
1147                風速
1148            Z_d : float
1149                地面修正後の測定高度
1150            phi_m : float
1151                運動量の安定度関数
1152            phi_c : float
1153                スカラーの安定度関数
1154            n : float
1155                安定度パラメータ
1156
1157        Returns:
1158        ------
1159            tuple[float, float, float, float, float]
1160                m (べき指数),
1161                U (基準高度での風速),
1162                r (べき指数の補正項),
1163                mu (形状パラメータ),
1164                ksi (フラックス長さスケール)
1165        """
1166        KARMAN: float = 0.4  # フォン・カルマン定数
1167        # パラメータの計算
1168        m: float = dUstar / KARMAN * phi_m / dU
1169        U: float = dU / pow(Z_d, m)
1170        r: float = 2.0 + m - n
1171        mu: float = (1.0 + m) / r
1172        kz: float = KARMAN * dUstar * Z_d / phi_c
1173        k: float = kz / pow(Z_d, n)
1174        ksi: float = U * pow(Z_d, r) / r / r / k
1175        return m, U, r, mu, ksi
1176
1177    @staticmethod
1178    def _calculate_ground_correction(
1179        z_m: float,
1180        wind_speed: np.ndarray,
1181        friction_velocity: np.ndarray,
1182        stability_parameter: np.ndarray,
1183    ) -> float:
1184        """
1185        地面修正量を計算します(Pennypacker and Baldocchi, 2016)。
1186
1187        この関数は、与えられた気象データを使用して地面修正量を計算します。
1188        計算は以下のステップで行われます:
1189        1. 変位高さ(d)を計算
1190        2. 中立条件外のデータを除外
1191        3. 平均変位高さを計算
1192        4. 地面修正量を返す
1193
1194        Parameters:
1195        ------
1196            z_m : float
1197                観測地点の高度
1198            wind_speed : np.ndarray
1199                風速データ配列 (WS vector)
1200            friction_velocity : np.ndarray
1201                摩擦速度データ配列 (u*)
1202            stability_parameter : np.ndarray
1203                安定度パラメータ配列 (z/L)
1204
1205        Returns:
1206        ------
1207            float
1208                計算された地面修正量
1209        """
1210        KARMAN: float = 0.4  # フォン・カルマン定数
1211        z: float = z_m
1212
1213        # 変位高さ(d)の計算
1214        displacement_height = 0.6 * (
1215            z / (0.6 + 0.1 * (np.exp((KARMAN * wind_speed) / friction_velocity)))
1216        )
1217
1218        # 中立条件外のデータをマスク(中立条件:-0.1 < z/L < 0.1)
1219        neutral_condition_mask = (stability_parameter < -0.1) | (
1220            0.1 < stability_parameter
1221        )
1222        displacement_height[neutral_condition_mask] = np.nan
1223
1224        # 平均変位高さを計算
1225        d: float = np.nanmean(displacement_height)
1226
1227        # 地面修正量を返す
1228        return z - d
1229
1230    @staticmethod
1231    def _calculate_stability_parameters(dzL: float) -> tuple[float, float, float]:
1232        """
1233        安定性パラメータを計算します。
1234        大気安定度に基づいて、運動量とスカラーの安定度関数、および安定度パラメータを計算します。
1235
1236        Parameters:
1237        ------
1238            dzL : float
1239                無次元高度 (z/L)、ここで z は測定高度、L はモニン・オブコフ長
1240
1241        Returns:
1242        ------
1243            tuple[float, float, float]
1244                phi_m : float
1245                    運動量の安定度関数
1246                phi_c : float
1247                    スカラーの安定度関数
1248                n : float
1249                    安定度パラメータ
1250        """
1251        phi_m: float = 0
1252        phi_c: float = 0
1253        n: float = 0
1254        if dzL > 0.0:
1255            # 安定成層の場合
1256            dzL = min(dzL, 2.0)
1257            phi_m = 1.0 + 5.0 * dzL
1258            phi_c = 1.0 + 5.0 * dzL
1259            n = 1.0 / (1.0 + 5.0 * dzL)
1260        else:
1261            # 不安定成層の場合
1262            phi_m = pow(1.0 - 16.0 * dzL, -0.25)
1263            phi_c = pow(1.0 - 16.0 * dzL, -0.50)
1264            n = (1.0 - 24.0 * dzL) / (1.0 - 16.0 * dzL)
1265        return phi_m, phi_c, n
1266
1267    @staticmethod
1268    def filter_data(
1269        df: pd.DataFrame,
1270        start_date: str | None = None,
1271        end_date: str | None = None,
1272        months: list[int] | None = None,
1273    ) -> pd.DataFrame:
1274        """
1275        指定された期間や月でデータをフィルタリングするメソッド。
1276
1277        Parameters:
1278        ------
1279            df : pd.DataFrame
1280                フィルタリングするデータフレーム
1281            start_date : str | None
1282                フィルタリングの開始日('YYYY-MM-DD'形式)。デフォルトはNone。
1283            end_date : str | None
1284                フィルタリングの終了日('YYYY-MM-DD'形式)。デフォルトはNone。
1285            months : list[int] | None
1286                フィルタリングする月のリスト(例:[1, 2, 12])。デフォルトはNone。
1287
1288        Returns:
1289        ------
1290            pd.DataFrame
1291                フィルタリングされたデータフレーム
1292
1293        Raises:
1294        ------
1295            ValueError
1296                インデックスがDatetimeIndexでない場合、または日付の形式が不正な場合
1297        """
1298        # インデックスの検証
1299        if not isinstance(df.index, pd.DatetimeIndex):
1300            raise ValueError(
1301                "DataFrameのインデックスはDatetimeIndexである必要があります"
1302            )
1303
1304        filtered_df: pd.DataFrame = df.copy()
1305
1306        # 日付形式の検証と変換
1307        try:
1308            if start_date is not None:
1309                start_date = pd.to_datetime(start_date)
1310            if end_date is not None:
1311                end_date = pd.to_datetime(end_date)
1312        except ValueError as e:
1313            raise ValueError(
1314                "日付の形式が不正です。'YYYY-MM-DD'形式で指定してください"
1315            ) from e
1316
1317        # 期間でフィルタリング
1318        if start_date is not None or end_date is not None:
1319            filtered_df = filtered_df.loc[start_date:end_date]
1320
1321        # 月のバリデーション
1322        if months is not None:
1323            if not all(isinstance(m, int) and 1 <= m <= 12 for m in months):
1324                raise ValueError(
1325                    "monthsは1から12までの整数のリストである必要があります"
1326                )
1327            filtered_df = filtered_df[filtered_df.index.month.isin(months)]
1328
1329        # フィルタリング後のデータが空でないことを確認
1330        if filtered_df.empty:
1331            raise ValueError("フィルタリング後のデータが空になりました")
1332
1333        return filtered_df
1334
1335    @staticmethod
1336    def is_weekday(date: datetime) -> int:
1337        """
1338        指定された日付が平日であるかどうかを判定します。
1339
1340        Parameters:
1341        ------
1342            date : datetime
1343                判定する日付。
1344
1345        Returns:
1346        ------
1347            int
1348                平日であれば1、そうでなければ0。
1349        """
1350        return 1 if not jpholiday.is_holiday(date) and date.weekday() < 5 else 0
1351
1352    @staticmethod
1353    def _prepare_plot_data(
1354        x80: float,
1355        ksi: float,
1356        mu: float,
1357        r: float,
1358        U: float,
1359        m: float,
1360        sigmaV: float,
1361        flux_value: float,
1362        plot_count: int,
1363    ) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
1364        """
1365        フットプリントのプロットデータを準備します。
1366
1367        Parameters:
1368        ------
1369            x80 : float
1370                80%寄与距離
1371            ksi : float
1372                フラックス長さスケール
1373            mu : float
1374                形状パラメータ
1375            r : float
1376                べき指数
1377            U : float
1378                風速
1379            m : float
1380                風速プロファイルのべき指数
1381            sigmaV : float
1382                風速の標準偏差
1383            flux_value : float
1384                フラックス値
1385            plot_count : int
1386                生成するプロット数
1387
1388        Returns:
1389        ------
1390            tuple[np.ndarray, np.ndarray, np.ndarray]
1391                x座標、y座標、フラックス値の配列のタプル
1392        """
1393        KARMAN: float = 0.4  # フォン・カルマン定数 (pp.210)
1394        x_lim: int = int(x80)
1395
1396        """
1397        各ランで生成するプロット数
1398        多いほどメモリに付加がかかるため注意
1399        """
1400        plot_num: int = plot_count  # 各ランで生成するプロット数
1401
1402        # x方向の距離配列を生成
1403        x_list: np.ndarray = np.arange(1, x_lim + 1, dtype="float64")
1404
1405        # クロスウィンド積分フットプリント関数を計算
1406        f_list: np.ndarray = (
1407            ksi**mu * np.exp(-ksi / x_list) / math.gamma(mu) / x_list ** (1.0 + mu)
1408        )
1409
1410        # プロット数に基づいてx座標を生成
1411        num_list: np.ndarray = np.round(f_list * plot_num).astype("int64")
1412        x1: np.ndarray = np.repeat(x_list, num_list)
1413
1414        # 風速プロファイルを計算
1415        Ux: np.ndarray = (
1416            (math.gamma(mu) / math.gamma(1 / r))
1417            * ((r**2 * KARMAN) / U) ** (m / r)
1418            * U
1419            * x1 ** (m / r)
1420        )
1421
1422        # y方向の分散を計算し、正規分布に従ってy座標を生成
1423        sigma_array: np.ndarray = sigmaV * x1 / Ux
1424        y1: np.ndarray = np.random.normal(0, sigma_array)
1425
1426        # フラックス値の配列を生成
1427        flux1 = np.full_like(x1, flux_value)
1428
1429        return x1, y1, flux1
1430
1431    @staticmethod
1432    def _rotate_coordinates(
1433        x: np.ndarray, y: np.ndarray, radian: float
1434    ) -> tuple[np.ndarray, np.ndarray]:
1435        """
1436        座標を指定された角度で回転させます。
1437
1438        この関数は、与えられたx座標とy座標を、指定された角度(ラジアン)で回転させます。
1439        回転は原点を中心に反時計回りに行われます。
1440
1441        Parameters:
1442        ------
1443            x : np.ndarray
1444                回転させるx座標の配列
1445            y : np.ndarray
1446                回転させるy座標の配列
1447            radian : float
1448                回転角度(ラジアン)
1449
1450        Returns:
1451        ------
1452            tuple[np.ndarray, np.ndarray]
1453                回転後の(x_, y_)座標の組
1454        """
1455        radian1: float = (radian - (np.pi / 2)) * (-1)
1456        x_: np.ndarray = x * np.cos(radian1) - y * np.sin(radian1)
1457        y_: np.ndarray = x * np.sin(radian1) + y * np.cos(radian1)
1458        return x_, y_
1459
1460    @staticmethod
1461    def _source_area_KM2001(
1462        ksi: float,
1463        mu: float,
1464        dU: float,
1465        sigmaV: float,
1466        Z_d: float,
1467        max_ratio: float = 0.8,
1468    ) -> float:
1469        """
1470        Kormann and Meixner (2001)のフットプリントモデルに基づいてソースエリアを計算します。
1471
1472        このメソッドは、与えられたパラメータを使用して、フラックスの寄与距離を計算します。
1473        計算は反復的に行われ、寄与率が'max_ratio'に達するまで、または最大反復回数に達するまで続けられます。
1474
1475        Parameters:
1476        ------
1477            ksi : float
1478                フラックス長さスケール
1479            mu : float
1480                形状パラメータ
1481            dU : float
1482                風速の変化率
1483            sigmaV : float
1484                風速の標準偏差
1485            Z_d : float
1486                ゼロ面変位高度
1487            max_ratio : float, optional
1488                寄与率の最大値。デフォルトは0.8。
1489
1490        Returns:
1491        ------
1492            float
1493                80%寄与距離(メートル単位)。計算が収束しない場合はnp.nan。
1494
1495        Notes:
1496        ------
1497            - 計算が収束しない場合(最大反復回数に達した場合)、結果はnp.nanとなります。
1498        """
1499        if max_ratio > 1:
1500            raise ValueError("max_ratio は0以上1以下である必要があります。")
1501        # 変数の初期値
1502        sum_f: float = 0.0  # 寄与率(0 < sum_f < 1.0)
1503        x1: float = 0.0
1504        dF_xd: float = 0.0
1505
1506        x_d: float = ksi / (
1507            1.0 + mu
1508        )  # Eq. 22 (x_d : クロスウィンド積分フラックスフットプリント最大位置)
1509
1510        dx: float = x_d / 100.0  # 等値線の拡がりの最大距離の100分の1(m)
1511
1512        # 寄与率が80%に達するまでfを積算
1513        while sum_f < (max_ratio / 1):
1514            x1 += dx
1515
1516            # Equation 21 (dF : クロスウィンド積分フットプリント)
1517            dF: float = (
1518                pow(ksi, mu) * math.exp(-ksi / x1) / math.gamma(mu) / pow(x1, 1.0 + mu)
1519            )
1520
1521            sum_f += dF  # Footprint を加えていく (0.0 < dF < 1.0)
1522            dx *= 2.0  # 距離は2倍ずつ増やしていく
1523
1524            if dx > 1.0:
1525                dx = 1.0  # 一気に、1 m 以上はインクリメントしない
1526            if x1 > Z_d * 1000.0:
1527                break  # ソースエリアが測定高度の1000倍以上となった場合、エラーとして止める
1528
1529        x_dst: float = x1  # 寄与率が80%に達するまでの積算距離
1530        f_last: float = (
1531            pow(ksi, mu)
1532            * math.exp(-ksi / x_dst)
1533            / math.gamma(mu)
1534            / pow(x_dst, 1.0 + mu)
1535        )  # Page 214 just below the Eq. 21.
1536
1537        # y方向の最大距離とその位置のxの距離
1538        dy: float = x_d / 100.0  # 等値線の拡がりの最大距離の100分の1
1539        y_dst: float = 0.0
1540        accumulated_y: float = 0.0  # y方向の積算距離を表す変数
1541
1542        # 最大反復回数を設定
1543        MAX_ITERATIONS: int = 100000
1544        for _ in range(MAX_ITERATIONS):
1545            accumulated_y += dy
1546            if accumulated_y >= x_dst:
1547                break
1548
1549            dF_xd = (
1550                pow(ksi, mu)
1551                * math.exp(-ksi / accumulated_y)
1552                / math.gamma(mu)
1553                / pow(accumulated_y, 1.0 + mu)
1554            )  # 式21の直下(214ページ)
1555
1556            aa: float = math.log(x_dst * dF_xd / f_last / accumulated_y)
1557            sigma: float = sigmaV * accumulated_y / dU  # 215ページ8行目
1558
1559            if 2.0 * aa >= 0:
1560                y_dst_new: float = sigma * math.sqrt(2.0 * aa)
1561                if y_dst_new <= y_dst:
1562                    break  # forループを抜ける
1563                y_dst = y_dst_new
1564
1565            dy = min(dy * 2.0, 1.0)
1566
1567        else:
1568            # ループが正常に終了しなかった場合(最大反復回数に達した場合)
1569            x_dst = np.nan
1570
1571        return x_dst

フラックスフットプリントを解析および可視化するクラス。

このクラスは、フラックスデータの処理、フットプリントの計算、 および結果を衛星画像上に可視化するメソッドを提供します。 座標系と単位に関する重要な注意:

  • すべての距離はメートル単位で計算されます
  • 座標系の原点(0,0)は測定タワーの位置に対応します
  • x軸は東西方向(正が東)
  • y軸は南北方向(正が北)
  • 風向は気象学的風向(北から時計回りに測定)を使用

この実装は、Kormann and Meixner (2001) および Takano et al. (2021)に基づいています。

FluxFootprintAnalyzer( z_m: float, labelsize: float = 20, ticksize: float = 16, plot_params=None, logger: logging.Logger | None = None, logging_debug: bool = False)
36    def __init__(
37        self,
38        z_m: float,
39        labelsize: float = 20,
40        ticksize: float = 16,
41        plot_params=None,
42        logger: Logger | None = None,
43        logging_debug: bool = False,
44    ):
45        """
46        衛星画像を用いて FluxFootprintAnalyzer を初期化します。
47
48        Parameters:
49        ------
50            z_m : float
51                測定の高さ(メートル単位)。
52            labelsize : float
53                軸ラベルのフォントサイズ。デフォルトは20。
54            ticksize : float
55                軸目盛りのフォントサイズ。デフォルトは16。
56            plot_params : Optional[Dict[str, any]]
57                matplotlibのプロットパラメータを指定する辞書。
58            logger : Logger | None
59                使用するロガー。Noneの場合は新しいロガーを生成します。
60            logging_debug : bool
61                ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。
62        """
63        # 定数や共通の変数
64        self._required_columns: list[str] = [
65            "Date",
66            "WS vector",
67            "u*",
68            "z/L",
69            "Wind direction",
70            "sigmaV",
71        ]  # 必要なカラムの名前
72        self._col_weekday: str = "ffa_is_weekday"  # クラスで生成するカラムのキー名
73        self._z_m: float = z_m  # 測定高度
74        # 状態を管理するフラグ
75        self._got_satellite_image: bool = False
76
77        # 図表の初期設定
78        FluxFootprintAnalyzer.setup_plot_params(labelsize, ticksize, plot_params)
79        # ロガー
80        log_level: int = INFO
81        if logging_debug:
82            log_level = DEBUG
83        self.logger: Logger = FluxFootprintAnalyzer.setup_logger(logger, log_level)

衛星画像を用いて FluxFootprintAnalyzer を初期化します。

Parameters:

z_m : float
    測定の高さ(メートル単位)。
labelsize : float
    軸ラベルのフォントサイズ。デフォルトは20。
ticksize : float
    軸目盛りのフォントサイズ。デフォルトは16。
plot_params : Optional[Dict[str, any]]
    matplotlibのプロットパラメータを指定する辞書。
logger : Logger | None
    使用するロガー。Noneの場合は新しいロガーを生成します。
logging_debug : bool
    ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。
EARTH_RADIUS_METER: int = 6371000
logger: logging.Logger
@staticmethod
def setup_logger(logger: logging.Logger | None, log_level: int = 20) -> logging.Logger:
 85    @staticmethod
 86    def setup_logger(logger: Logger | None, log_level: int = INFO) -> Logger:
 87        """
 88        ロガーを設定します。
 89
 90        このメソッドは、ロギングの設定を行い、ログメッセージのフォーマットを指定します。
 91        ログメッセージには、日付、ログレベル、メッセージが含まれます。
 92
 93        渡されたロガーがNoneまたは不正な場合は、新たにロガーを作成し、標準出力に
 94        ログメッセージが表示されるようにStreamHandlerを追加します。ロガーのレベルは
 95        引数で指定されたlog_levelに基づいて設定されます。
 96
 97        Parameters:
 98        ------
 99            logger : Logger | None
100                使用するロガー。Noneの場合は新しいロガーを作成します。
101            log_level : int
102                ロガーのログレベル。デフォルトはINFO。
103
104        Returns:
105        ------
106            Logger
107                設定されたロガーオブジェクト。
108        """
109        if logger is not None and isinstance(logger, Logger):
110            return logger
111        # 渡されたロガーがNoneまたは正しいものでない場合は独自に設定
112        new_logger: Logger = getLogger()
113        # 既存のハンドラーをすべて削除
114        for handler in new_logger.handlers[:]:
115            new_logger.removeHandler(handler)
116        new_logger.setLevel(log_level)  # ロガーのレベルを設定
117        ch = StreamHandler()
118        ch_formatter = Formatter("%(asctime)s - %(levelname)s - %(message)s")
119        ch.setFormatter(ch_formatter)  # フォーマッターをハンドラーに設定
120        new_logger.addHandler(ch)  # StreamHandlerの追加
121        return new_logger

ロガーを設定します。

このメソッドは、ロギングの設定を行い、ログメッセージのフォーマットを指定します。 ログメッセージには、日付、ログレベル、メッセージが含まれます。

渡されたロガーがNoneまたは不正な場合は、新たにロガーを作成し、標準出力に ログメッセージが表示されるようにStreamHandlerを追加します。ロガーのレベルは 引数で指定されたlog_levelに基づいて設定されます。

Parameters:

logger : Logger | None
    使用するロガー。Noneの場合は新しいロガーを作成します。
log_level : int
    ロガーのログレベル。デフォルトはINFO。

Returns:

Logger
    設定されたロガーオブジェクト。
@staticmethod
def setup_plot_params(labelsize: float, ticksize: float, plot_params=None) -> None:
123    @staticmethod
124    def setup_plot_params(labelsize: float, ticksize: float, plot_params=None) -> None:
125        """
126        matplotlibのプロットパラメータを設定します。
127
128        Parameters:
129        ------
130            labelsize : float
131                軸ラベルのフォントサイズ。
132            ticksize : float
133                軸目盛りのフォントサイズ。
134            plot_params : Optional[Dict[str, any]]
135                matplotlibのプロットパラメータの辞書。
136
137        Returns:
138        ------
139            None
140                このメソッドは戻り値を持ちませんが、プロットパラメータを更新します。
141        """
142        # デフォルトのプロットパラメータ
143        default_params = {
144            "font.family": ["Arial", "Dejavu Sans"],
145            "axes.edgecolor": "None",
146            "axes.labelcolor": "black",
147            "text.color": "black",
148            "xtick.color": "black",
149            "ytick.color": "black",
150            "grid.color": "gray",
151            "axes.grid": False,
152            "xtick.major.size": 0,
153            "ytick.major.size": 0,
154            "ytick.direction": "out",
155            "ytick.major.width": 1.0,
156            "axes.linewidth": 1.0,
157            "grid.linewidth": 1.0,
158            "font.size": labelsize,
159            "xtick.labelsize": ticksize,
160            "ytick.labelsize": ticksize,
161        }
162
163        # plot_paramsが定義されている場合、デフォルトに追記
164        if plot_params:
165            default_params.update(plot_params)
166
167        plt.rcParams.update(default_params)  # プロットパラメータを更新

matplotlibのプロットパラメータを設定します。

Parameters:

labelsize : float
    軸ラベルのフォントサイズ。
ticksize : float
    軸目盛りのフォントサイズ。
plot_params : Optional[Dict[str, any]]
    matplotlibのプロットパラメータの辞書。

Returns:

None
    このメソッドは戻り値を持ちませんが、プロットパラメータを更新します。
def calculate_flux_footprint( self, df: pandas.core.frame.DataFrame, col_flux: str, plot_count: int = 10000, start_time: str = '10:00', end_time: str = '16:00') -> tuple[list[float], list[float], list[float]]:
169    def calculate_flux_footprint(
170        self,
171        df: pd.DataFrame,
172        col_flux: str,
173        plot_count: int = 10000,
174        start_time: str = "10:00",
175        end_time: str = "16:00",
176    ) -> tuple[list[float], list[float], list[float]]:
177        """
178        フラックスフットプリントを計算し、指定された時間帯のデータを基に可視化します。
179
180        Parameters:
181        ------
182            df : pd.DataFrame
183                分析対象のデータフレーム。フラックスデータを含む。
184            col_flux : str
185                フラックスデータの列名。計算に使用される。
186            plot_count : int, optional
187                生成するプロットの数。デフォルトは10000。
188            start_time : str, optional
189                フットプリント計算に使用する開始時間。デフォルトは"10:00"。
190            end_time : str, optional
191                フットプリント計算に使用する終了時間。デフォルトは"16:00"。
192
193        Returns:
194        ------
195            tuple[list[float], list[float], list[float]]:
196                x座標 (メートル): タワーを原点とした東西方向の距離
197                y座標 (メートル): タワーを原点とした南北方向の距離
198                対象スカラー量の値: 各地点でのフラックス値
199
200        Notes:
201        ------
202            - 返却される座標は測定タワーを原点(0,0)とした相対位置です
203            - すべての距離はメートル単位で表されます
204            - 正のx値は東方向、正のy値は北方向を示します
205        """
206        df: pd.DataFrame = df.copy()
207
208        # インデックスがdatetimeであることを確認し、必要に応じて変換
209        if not isinstance(df.index, pd.DatetimeIndex):
210            df.index = pd.to_datetime(df.index)
211
212        # DatetimeIndexから直接dateプロパティにアクセス
213        datelist: np.ndarray = np.array(df.index.date)
214
215        # 各日付が平日かどうかを判定し、リストに格納
216        numbers: list[int] = [
217            FluxFootprintAnalyzer.is_weekday(date) for date in datelist
218        ]
219
220        # col_weekdayに基づいてデータフレームに平日情報を追加
221        df.loc[:, self._col_weekday] = numbers  # .locを使用して値を設定
222
223        # 値が1のもの(平日)をコピーする
224        data_weekday: pd.DataFrame = df[df[self._col_weekday] == 1].copy()
225        # 特定の時間帯を抽出
226        data_weekday = data_weekday.between_time(
227            start_time, end_time
228        )  # 引数を使用して時間帯を抽出
229        data_weekday = data_weekday.dropna(subset=[col_flux])
230
231        directions: list[float] = [
232            wind_direction if wind_direction >= 0 else wind_direction + 360
233            for wind_direction in data_weekday["Wind direction"]
234        ]
235
236        data_weekday.loc[:, "Wind direction_360"] = directions
237        data_weekday.loc[:, "radian"] = data_weekday["Wind direction_360"] / 180 * np.pi
238
239        # 風向が欠測なら除去
240        data_weekday = data_weekday.dropna(subset=["Wind direction", col_flux])
241
242        # 数値型への変換を確実に行う
243        numeric_columns: list[str] = ["u*", "WS vector", "sigmaV", "z/L"]
244        for col in numeric_columns:
245            data_weekday[col] = pd.to_numeric(data_weekday[col], errors="coerce")
246
247        # 地面修正量dの計算
248        z_m: float = self._z_m
249        Z_d: float = FluxFootprintAnalyzer._calculate_ground_correction(
250            z_m=z_m,
251            wind_speed=data_weekday["WS vector"].values,
252            friction_velocity=data_weekday["u*"].values,
253            stability_parameter=data_weekday["z/L"].values,
254        )
255
256        x_list: list[float] = []
257        y_list: list[float] = []
258        c_list: list[float] | None = []
259
260        # tqdmを使用してプログレスバーを表示
261        for i in tqdm(range(len(data_weekday)), desc="Calculating footprint"):
262            dUstar: float = data_weekday["u*"].iloc[i]
263            dU: float = data_weekday["WS vector"].iloc[i]
264            sigmaV: float = data_weekday["sigmaV"].iloc[i]
265            dzL: float = data_weekday["z/L"].iloc[i]
266
267            if pd.isna(dUstar) or pd.isna(dU) or pd.isna(sigmaV) or pd.isna(dzL):
268                self.logger.warning(f"#N/A fields are exist.: i = {i}")
269                continue
270            elif dUstar < 5.0 and dUstar != 0.0 and dU > 0.1:
271                phi_m, phi_c, n = FluxFootprintAnalyzer._calculate_stability_parameters(
272                    dzL=dzL
273                )
274                m, U, r, mu, ksi = (
275                    FluxFootprintAnalyzer._calculate_footprint_parameters(
276                        dUstar=dUstar, dU=dU, Z_d=Z_d, phi_m=phi_m, phi_c=phi_c, n=n
277                    )
278                )
279
280                # 80%ソースエリアの計算
281                x80: float = FluxFootprintAnalyzer._source_area_KM2001(
282                    ksi=ksi, mu=mu, dU=dU, sigmaV=sigmaV, Z_d=Z_d, max_ratio=0.8
283                )
284
285                if not np.isnan(x80):
286                    x1, y1, flux1 = FluxFootprintAnalyzer._prepare_plot_data(
287                        x80,
288                        ksi,
289                        mu,
290                        r,
291                        U,
292                        m,
293                        sigmaV,
294                        data_weekday[col_flux].iloc[i],
295                        plot_count=plot_count,
296                    )
297                    x1_, y1_ = FluxFootprintAnalyzer._rotate_coordinates(
298                        x=x1, y=y1, radian=data_weekday["radian"].iloc[i]
299                    )
300
301                    x_list.extend(x1_)
302                    y_list.extend(y1_)
303                    c_list.extend(flux1)
304
305        return (
306            x_list,
307            y_list,
308            c_list,
309        )

フラックスフットプリントを計算し、指定された時間帯のデータを基に可視化します。

Parameters:

df : pd.DataFrame
    分析対象のデータフレーム。フラックスデータを含む。
col_flux : str
    フラックスデータの列名。計算に使用される。
plot_count : int, optional
    生成するプロットの数。デフォルトは10000。
start_time : str, optional
    フットプリント計算に使用する開始時間。デフォルトは"10:00"。
end_time : str, optional
    フットプリント計算に使用する終了時間。デフォルトは"16:00"。

Returns:

tuple[list[float], list[float], list[float]]:
    x座標 (メートル): タワーを原点とした東西方向の距離
    y座標 (メートル): タワーを原点とした南北方向の距離
    対象スカラー量の値: 各地点でのフラックス値

Notes:

- 返却される座標は測定タワーを原点(0,0)とした相対位置です
- すべての距離はメートル単位で表されます
- 正のx値は東方向、正のy値は北方向を示します
def combine_all_data( self, data_source: str | pandas.core.frame.DataFrame, source_type: str = 'csv', **kwargs) -> pandas.core.frame.DataFrame:
311    def combine_all_data(
312        self, data_source: str | pd.DataFrame, source_type: str = "csv", **kwargs
313    ) -> pd.DataFrame:
314        """
315        CSVファイルまたはMonthlyConverterからのデータを統合します
316
317        Parameters:
318        ------
319            data_source : str | pd.DataFrame
320                CSVディレクトリパスまたはDataFrame
321            source_type : str
322                "csv" または "monthly"
323            **kwargs :
324                追加パラメータ
325                - sheet_names : list[str]
326                    Monthlyの場合のシート名
327                - start_date : str
328                    開始日
329                - end_date : str
330                    終了日
331
332        Returns:
333        ------
334            pd.DataFrame
335                処理済みのデータフレーム
336        """
337        if source_type == "csv":
338            # 既存のCSV処理ロジック
339            return self._combine_all_csv(data_source)
340        elif source_type == "monthly":
341            # MonthlyConverterからのデータを処理
342            if not isinstance(data_source, pd.DataFrame):
343                raise ValueError("monthly形式の場合、DataFrameを直接渡す必要があります")
344
345            df = data_source.copy()
346
347            # required_columnsからDateを除外して欠損値チェックを行う
348            check_columns = [col for col in self._required_columns if col != "Date"]
349
350            # インデックスがdatetimeであることを確認
351            if not isinstance(df.index, pd.DatetimeIndex) and "Date" not in df.columns:
352                raise ValueError("DatetimeIndexまたはDateカラムが必要です")
353
354            if "Date" in df.columns:
355                df.set_index("Date", inplace=True)
356
357            # 必要なカラムの存在確認
358            missing_columns = [
359                col for col in check_columns if col not in df.columns.tolist()
360            ]
361            if missing_columns:
362                missing_cols = "','".join(missing_columns)
363                current_cols = "','".join(df.columns.tolist())
364                raise ValueError(
365                    f"必要なカラムが不足しています: '{missing_cols}'\n"
366                    f"現在のカラム: '{current_cols}'"
367                )
368
369            # 平日/休日の判定用カラムを追加
370            df[self._col_weekday] = df.index.map(FluxFootprintAnalyzer.is_weekday)
371
372            # Dateを除外したカラムで欠損値の処理
373            df = df.dropna(subset=check_columns)
374
375            # インデックスの重複を除去
376            df = df.loc[~df.index.duplicated(), :]
377
378            return df
379        else:
380            raise ValueError("source_typeは'csv'または'monthly'である必要があります")

CSVファイルまたはMonthlyConverterからのデータを統合します

Parameters:

data_source : str | pd.DataFrame
    CSVディレクトリパスまたはDataFrame
source_type : str
    "csv" または "monthly"
**kwargs :
    追加パラメータ
    - sheet_names : list[str]
        Monthlyの場合のシート名
    - start_date : str
        開始日
    - end_date : str
        終了日

Returns:

pd.DataFrame
    処理済みのデータフレーム
def get_satellite_image_from_api( self, api_key: str, center_lat: float, center_lon: float, output_path: str, scale: int = 1, size: tuple[int, int] = (2160, 2160), zoom: int = 13) -> PIL.ImageFile.ImageFile:
382    def get_satellite_image_from_api(
383        self,
384        api_key: str,
385        center_lat: float,
386        center_lon: float,
387        output_path: str,
388        scale: int = 1,
389        size: tuple[int, int] = (2160, 2160),
390        zoom: int = 13,
391    ) -> ImageFile:
392        """
393        Google Maps Static APIを使用して衛星画像を取得します。
394
395        Parameters:
396        ------
397            api_key : str
398                Google Maps Static APIのキー。
399            center_lat : float
400                中心の緯度。
401            center_lon : float
402                中心の経度。
403            output_path : str
404                画像の保存先パス。拡張子は'.png'のみ許可される。
405            scale : int, optional
406                画像の解像度スケール(1か2)。デフォルトは1。
407            size : tuple[int, int], optional
408                画像サイズ (幅, 高さ)。デフォルトは(2160, 2160)。
409            zoom : int, optional
410                ズームレベル(0-21)。デフォルトは13。
411
412        Returns:
413        ------
414            ImageFile
415                取得した衛星画像
416
417        Raises:
418        ------
419            requests.RequestException
420                API呼び出しに失敗した場合
421        """
422        # バリデーション
423        if not output_path.endswith(".png"):
424            raise ValueError("出力ファイル名は'.png'で終わる必要があります。")
425
426        # HTTPリクエストの定義
427        base_url = "https://maps.googleapis.com/maps/api/staticmap"
428        params = {
429            "center": f"{center_lat},{center_lon}",
430            "zoom": zoom,
431            "size": f"{size[0]}x{size[1]}",
432            "maptype": "satellite",
433            "scale": scale,
434            "key": api_key,
435        }
436
437        try:
438            response = requests.get(base_url, params=params)
439            response.raise_for_status()
440            # 画像ファイルに変換
441            image = Image.open(io.BytesIO(response.content))
442            image.save(output_path)
443            self._got_satellite_image = True
444            self.logger.info(f"リモート画像を取得し、保存しました: {output_path}")
445            return image
446        except requests.RequestException as e:
447            self.logger.error(f"衛星画像の取得に失敗しました: {str(e)}")
448            raise

Google Maps Static APIを使用して衛星画像を取得します。

Parameters:

api_key : str
    Google Maps Static APIのキー。
center_lat : float
    中心の緯度。
center_lon : float
    中心の経度。
output_path : str
    画像の保存先パス。拡張子は'.png'のみ許可される。
scale : int, optional
    画像の解像度スケール(1か2)。デフォルトは1。
size : tuple[int, int], optional
    画像サイズ (幅, 高さ)。デフォルトは(2160, 2160)。
zoom : int, optional
    ズームレベル(0-21)。デフォルトは13。

Returns:

ImageFile
    取得した衛星画像

Raises:

requests.RequestException
    API呼び出しに失敗した場合
def get_satellite_image_from_local(self, local_image_path: str) -> PIL.ImageFile.ImageFile:
450    def get_satellite_image_from_local(
451        self,
452        local_image_path: str,
453    ) -> ImageFile:
454        """
455        ローカルファイルから衛星画像を読み込みます。
456
457        Parameters:
458        ------
459            local_image_path : str
460                ローカル画像のパス
461
462        Returns:
463        ------
464            ImageFile
465                読み込んだ衛星画像
466
467        Raises:
468        ------
469            FileNotFoundError
470                指定されたパスにファイルが存在しない場合
471        """
472        if not os.path.exists(local_image_path):
473            raise FileNotFoundError(
474                f"指定されたローカル画像が存在しません: {local_image_path}"
475            )
476        image = Image.open(local_image_path)
477        self._got_satellite_image = True
478        self.logger.info(f"ローカル画像を使用しました: {local_image_path}")
479        return image

ローカルファイルから衛星画像を読み込みます。

Parameters:

local_image_path : str
    ローカル画像のパス

Returns:

ImageFile
    読み込んだ衛星画像

Raises:

FileNotFoundError
    指定されたパスにファイルが存在しない場合
def plot_flux_footprint( self, x_list: list[float], y_list: list[float], c_list: list[float] | None, center_lat: float, center_lon: float, vmin: float, vmax: float, add_cbar: bool = True, add_legend: bool = True, cbar_label: str | None = None, cbar_labelpad: int = 20, cmap: str = 'jet', reduce_c_function: <built-in function callable> = <function mean>, lat_correction: float = 1, lon_correction: float = 1, output_dir: str | None = None, output_filename: str = 'footprint.png', save_fig: bool = True, show_fig: bool = True, satellite_image: PIL.ImageFile.ImageFile | None = None, xy_max: float = 5000) -> None:
481    def plot_flux_footprint(
482        self,
483        x_list: list[float],
484        y_list: list[float],
485        c_list: list[float] | None,
486        center_lat: float,
487        center_lon: float,
488        vmin: float,
489        vmax: float,
490        add_cbar: bool = True,
491        add_legend: bool = True,
492        cbar_label: str | None = None,
493        cbar_labelpad: int = 20,
494        cmap: str = "jet",
495        reduce_c_function: callable = np.mean,
496        lat_correction: float = 1,
497        lon_correction: float = 1,
498        output_dir: str | None = None,
499        output_filename: str = "footprint.png",
500        save_fig: bool = True,
501        show_fig: bool = True,
502        satellite_image: ImageFile | None = None,
503        xy_max: float = 5000,
504    ) -> None:
505        """
506        フットプリントデータをプロットします。
507
508        このメソッドは、指定されたフットプリントデータのみを可視化します。
509
510        Parameters:
511        ------
512            x_list : list[float]
513                フットプリントのx座標リスト(メートル単位)。
514            y_list : list[float]
515                フットプリントのy座標リスト(メートル単位)。
516            c_list : list[float] | None
517                フットプリントの強度を示す値のリスト。
518            center_lat : float
519                プロットの中心となる緯度。
520            center_lon : float
521                プロットの中心となる経度。
522            cmap : str
523                使用するカラーマップの名前。
524            vmin : float
525                カラーバーの最小値。
526            vmax : float
527                カラーバーの最大値。
528            reduce_c_function : callable, optional
529                フットプリントの集約関数(デフォルトはnp.mean)。
530            cbar_label : str | None, optional
531                カラーバーのラベル。
532            cbar_labelpad : int, optional
533                カラーバーラベルのパディング。
534            lon_correction : float, optional
535                経度方向の補正係数(デフォルトは1)。
536            lat_correction : float, optional
537                緯度方向の補正係数(デフォルトは1)。
538            output_dir : str | None, optional
539                プロット画像の保存先パス。
540            output_filename : str
541                プロット画像の保存ファイル名(拡張子を含む)。デフォルトは'footprint.png'。
542            save_fig : bool
543                図の保存を許可するフラグ。デフォルトはTrue。
544            show_fig : bool
545                図の表示を許可するフラグ。デフォルトはTrue。
546            satellite_image : ImageFile | None, optional
547                使用する衛星画像。指定がない場合はデフォルトの画像が生成されます。
548            xy_max : float, optional
549                表示範囲の最大値(デフォルトは4000)。
550        """
551        self.plot_flux_footprint_with_hotspots(
552            x_list=x_list,
553            y_list=y_list,
554            c_list=c_list,
555            center_lat=center_lat,
556            center_lon=center_lon,
557            vmin=vmin,
558            vmax=vmax,
559            add_cbar=add_cbar,
560            add_legend=add_legend,
561            cbar_label=cbar_label,
562            cbar_labelpad=cbar_labelpad,
563            cmap=cmap,
564            reduce_c_function=reduce_c_function,
565            hotspots=None,  # hotspotsをNoneに設定
566            hotspot_colors=None,
567            lat_correction=lat_correction,
568            lon_correction=lon_correction,
569            output_dir=output_dir,
570            output_filename=output_filename,
571            save_fig=save_fig,
572            show_fig=show_fig,
573            satellite_image=satellite_image,
574            xy_max=xy_max,
575        )

フットプリントデータをプロットします。

このメソッドは、指定されたフットプリントデータのみを可視化します。

Parameters:

x_list : list[float]
    フットプリントのx座標リスト(メートル単位)。
y_list : list[float]
    フットプリントのy座標リスト(メートル単位)。
c_list : list[float] | None
    フットプリントの強度を示す値のリスト。
center_lat : float
    プロットの中心となる緯度。
center_lon : float
    プロットの中心となる経度。
cmap : str
    使用するカラーマップの名前。
vmin : float
    カラーバーの最小値。
vmax : float
    カラーバーの最大値。
reduce_c_function : callable, optional
    フットプリントの集約関数(デフォルトはnp.mean)。
cbar_label : str | None, optional
    カラーバーのラベル。
cbar_labelpad : int, optional
    カラーバーラベルのパディング。
lon_correction : float, optional
    経度方向の補正係数(デフォルトは1)。
lat_correction : float, optional
    緯度方向の補正係数(デフォルトは1)。
output_dir : str | None, optional
    プロット画像の保存先パス。
output_filename : str
    プロット画像の保存ファイル名(拡張子を含む)。デフォルトは'footprint.png'。
save_fig : bool
    図の保存を許可するフラグ。デフォルトはTrue。
show_fig : bool
    図の表示を許可するフラグ。デフォルトはTrue。
satellite_image : ImageFile | None, optional
    使用する衛星画像。指定がない場合はデフォルトの画像が生成されます。
xy_max : float, optional
    表示範囲の最大値(デフォルトは4000)。
def plot_flux_footprint_with_hotspots( self, x_list: list[float], y_list: list[float], c_list: list[float] | None, center_lat: float, center_lon: float, vmin: float, vmax: float, add_cbar: bool = True, add_legend: bool = True, cbar_label: str | None = None, cbar_labelpad: int = 20, cmap: str = 'jet', reduce_c_function: <built-in function callable> = <function mean>, hotspots: list[HotspotData] | None = None, hotspot_colors: dict[typing.Literal['bio', 'gas', 'comb'], str] | None = None, hotspot_markers: dict[typing.Literal['bio', 'gas', 'comb'], str] | None = None, lat_correction: float = 1, lon_correction: float = 1, output_dir: str | None = None, output_filename: str = 'footprint.png', save_fig: bool = True, show_fig: bool = True, satellite_image: PIL.ImageFile.ImageFile | None = None, xy_max: float = 5000) -> None:
577    def plot_flux_footprint_with_hotspots(
578        self,
579        x_list: list[float],
580        y_list: list[float],
581        c_list: list[float] | None,
582        center_lat: float,
583        center_lon: float,
584        vmin: float,
585        vmax: float,
586        add_cbar: bool = True,
587        add_legend: bool = True,
588        cbar_label: str | None = None,
589        cbar_labelpad: int = 20,
590        cmap: str = "jet",
591        reduce_c_function: callable = np.mean,
592        hotspots: list[HotspotData] | None = None,
593        hotspot_colors: dict[HotspotType, str] | None = None,
594        hotspot_markers: dict[HotspotType, str] | None = None,
595        lat_correction: float = 1,
596        lon_correction: float = 1,
597        output_dir: str | None = None,
598        output_filename: str = "footprint.png",
599        save_fig: bool = True,
600        show_fig: bool = True,
601        satellite_image: ImageFile | None = None,
602        xy_max: float = 5000,
603    ) -> None:
604        """
605        Staticな衛星画像上にフットプリントデータとホットスポットをプロットします。
606
607        このメソッドは、指定されたフットプリントデータとホットスポットを可視化します。
608        ホットスポットが指定されない場合は、フットプリントのみ作図します。
609
610        Parameters:
611        ------
612            x_list : list[float]
613                フットプリントのx座標リスト(メートル単位)。
614            y_list : list[float]
615                フットプリントのy座標リスト(メートル単位)。
616            c_list : list[float] | None
617                フットプリントの強度を示す値のリスト。
618            center_lat : float
619                プロットの中心となる緯度。
620            center_lon : float
621                プロットの中心となる経度。
622            vmin : float
623                カラーバーの最小値。
624            vmax : float
625                カラーバーの最大値。
626            add_cbar : bool, optional
627                カラーバーを追加するかどうか(デフォルトはTrue)。
628            add_legend : bool, optional
629                凡例を追加するかどうか(デフォルトはTrue)。
630            cbar_label : str | None, optional
631                カラーバーのラベル。
632            cbar_labelpad : int, optional
633                カラーバーラベルのパディング。
634            cmap : str
635                使用するカラーマップの名前。
636            reduce_c_function : callable
637                フットプリントの集約関数(デフォルトはnp.mean)。
638            hotspots : list[HotspotData] | None, optional
639                ホットスポットデータのリスト。デフォルトはNone。
640            hotspot_colors : dict[HotspotType, str] | None, optional
641                ホットスポットの色を指定する辞書。
642            hotspot_markers : dict[HotspotType, str] | None, optional
643                ホットスポットの形状を指定する辞書。
644                指定の例は {'bio': '^', 'gas': 'o', 'comb': 's'} (三角、丸、四角)など。
645            lat_correction : float, optional
646                緯度方向の補正係数(デフォルトは1)。
647            lon_correction : float, optional
648                経度方向の補正係数(デフォルトは1)。
649            output_dir : str | None, optional
650                プロット画像の保存先パス。
651            output_filename : str
652                プロット画像の保存ファイル名(拡張子を含む)。デフォルトは'footprint.png'。
653            save_fig : bool
654                図の保存を許可するフラグ。デフォルトはTrue。
655            show_fig : bool
656                図の表示を許可するフラグ。デフォルトはTrue。
657            satellite_image : ImageFile | None, optional
658                使用する衛星画像。指定がない場合はデフォルトの画像が生成されます。
659            xy_max : float, optional
660                表示範囲の最大値(デフォルトは5000)。
661        """
662        # 1. 引数のバリデーション
663        valid_extensions: list[str] = [".png", ".jpg", ".jpeg", ".pdf", ".svg"]
664        _, file_extension = os.path.splitext(output_filename)
665        if file_extension.lower() not in valid_extensions:
666            quoted_extensions: list[str] = [f'"{ext}"' for ext in valid_extensions]
667            self.logger.error(
668                f"`output_filename`は有効な拡張子ではありません。プロットを保存するには、次のいずれかを指定してください: {','.join(quoted_extensions)}"
669            )
670            return
671
672        # 2. フラグチェック
673        if not self._got_satellite_image:
674            raise ValueError(
675                "`get_satellite_image_from_api`または`get_satellite_image_from_local`が実行されていません。"
676            )
677
678        # 3. 衛星画像の取得
679        if satellite_image is None:
680            satellite_image = Image.new("RGB", (2160, 2160), "lightgray")
681
682        self.logger.info("プロットを作成中...")
683
684        # 4. 座標変換のための定数計算(1回だけ)
685        meters_per_lat: float = self.EARTH_RADIUS_METER * (
686            math.pi / 180
687        )  # 緯度1度あたりのメートル
688        meters_per_lon: float = meters_per_lat * math.cos(
689            math.radians(center_lat)
690        )  # 経度1度あたりのメートル
691
692        # 5. フットプリントデータの座標変換(まとめて1回で実行)
693        x_deg = (
694            np.array(x_list) / meters_per_lon * lon_correction
695        )  # 補正係数も同時に適用
696        y_deg = (
697            np.array(y_list) / meters_per_lat * lat_correction
698        )  # 補正係数も同時に適用
699
700        # 6. 中心点からの相対座標を実際の緯度経度に変換
701        lons = center_lon + x_deg
702        lats = center_lat + y_deg
703
704        # 7. 表示範囲の計算(変更なし)
705        x_range: float = xy_max / meters_per_lon
706        y_range: float = xy_max / meters_per_lat
707        map_boundaries: tuple[float, float, float, float] = (
708            center_lon - x_range,  # left_lon
709            center_lon + x_range,  # right_lon
710            center_lat - y_range,  # bottom_lat
711            center_lat + y_range,  # top_lat
712        )
713        left_lon, right_lon, bottom_lat, top_lat = map_boundaries
714
715        # 8. プロットの作成
716        plt.rcParams["axes.edgecolor"] = "None"
717        fig: plt.Figure = plt.figure(figsize=(10, 8), dpi=300)
718        ax_data: plt.Axes = fig.add_axes([0.05, 0.1, 0.8, 0.8])
719
720        # 9. フットプリントの描画
721        # フットプリントの描画とカラーバー用の2つのhexbinを作成
722        if c_list is not None:
723            ax_data.hexbin(
724                lons,
725                lats,
726                C=c_list,
727                cmap=cmap,
728                vmin=vmin,
729                vmax=vmax,
730                alpha=0.3,  # 実際のプロット用
731                gridsize=100,
732                linewidths=0,
733                mincnt=100,
734                extent=[left_lon, right_lon, bottom_lat, top_lat],
735                reduce_C_function=reduce_c_function,
736            )
737
738        # カラーバー用の非表示hexbin(alpha=1.0)
739        hidden_hexbin = ax_data.hexbin(
740            lons,
741            lats,
742            C=c_list,
743            cmap=cmap,
744            vmin=vmin,
745            vmax=vmax,
746            alpha=1.0,  # カラーバー用
747            gridsize=100,
748            linewidths=0,
749            mincnt=100,
750            extent=[left_lon, right_lon, bottom_lat, top_lat],
751            reduce_C_function=reduce_c_function,
752            visible=False,  # プロットには表示しない
753        )
754
755        # 10. ホットスポットの描画
756        spot_handles = []
757        if hotspots is not None:
758            default_colors: dict[HotspotType, str] = {
759                "bio": "blue",
760                "gas": "red",
761                "comb": "green",
762            }
763
764            # デフォルトのマーカー形状を定義
765            default_markers: dict[HotspotType, str] = {
766                "bio": "o",
767                "gas": "o",
768                "comb": "o",
769            }
770
771            # 座標変換のための定数
772            meters_per_lat: float = self.EARTH_RADIUS_METER * (math.pi / 180)
773            meters_per_lon: float = meters_per_lat * math.cos(math.radians(center_lat))
774
775            for spot_type, color in (hotspot_colors or default_colors).items():
776                spots_lon = []
777                spots_lat = []
778
779                # 使用するマーカーを決定
780                marker = (hotspot_markers or default_markers).get(spot_type, "o")
781
782                for spot in hotspots:
783                    if spot.type == spot_type:
784                        # 変換前の緯度経度をログ出力
785                        self.logger.debug(
786                            f"Before - Type: {spot_type}, Lat: {spot.avg_lat:.6f}, Lon: {spot.avg_lon:.6f}"
787                        )
788
789                        # 中心からの相対距離を計算
790                        dx: float = (spot.avg_lon - center_lon) * meters_per_lon
791                        dy: float = (spot.avg_lat - center_lat) * meters_per_lat
792
793                        # 補正前の相対座標をログ出力
794                        self.logger.debug(
795                            f"Relative - Type: {spot_type}, X: {dx:.2f}m, Y: {dy:.2f}m"
796                        )
797
798                        # 補正を適用
799                        corrected_dx: float = dx * lon_correction
800                        corrected_dy: float = dy * lat_correction
801
802                        # 補正後の緯度経度を計算
803                        adjusted_lon: float = center_lon + corrected_dx / meters_per_lon
804                        adjusted_lat: float = center_lat + corrected_dy / meters_per_lat
805
806                        # 変換後の緯度経度をログ出力
807                        self.logger.debug(
808                            f"After - Type: {spot_type}, Lat: {adjusted_lat:.6f}, Lon: {adjusted_lon:.6f}\n"
809                        )
810
811                        if (
812                            left_lon <= adjusted_lon <= right_lon
813                            and bottom_lat <= adjusted_lat <= top_lat
814                        ):
815                            spots_lon.append(adjusted_lon)
816                            spots_lat.append(adjusted_lat)
817
818                if spots_lon:
819                    handle = ax_data.scatter(
820                        spots_lon,
821                        spots_lat,
822                        c=color,
823                        marker=marker,  # マーカー形状を指定
824                        s=100,
825                        alpha=0.7,
826                        label=spot_type,  # "bio","gas","comb"
827                        edgecolor="black",
828                        linewidth=1,
829                    )
830                    spot_handles.append(handle)
831
832        # 11. 背景画像の設定
833        ax_img = ax_data.twiny().twinx()
834        ax_img.imshow(
835            satellite_image,
836            extent=[left_lon, right_lon, bottom_lat, top_lat],
837            aspect="equal",
838        )
839
840        # 12. 軸の設定
841        for ax in [ax_data, ax_img]:
842            ax.set_xlim(left_lon, right_lon)
843            ax.set_ylim(bottom_lat, top_lat)
844            ax.set_xticks([])
845            ax.set_yticks([])
846
847        ax_data.set_zorder(2)
848        ax_data.patch.set_alpha(0)
849        ax_img.set_zorder(1)
850
851        # 13. カラーバーの追加
852        if add_cbar:
853            cbar_ax: plt.Axes = fig.add_axes([0.88, 0.1, 0.03, 0.8])
854            cbar = fig.colorbar(hidden_hexbin, cax=cbar_ax)  # hidden_hexbinを使用
855            # cbar_labelが指定されている場合のみラベルを設定
856            if cbar_label:
857                cbar.set_label(cbar_label, rotation=270, labelpad=cbar_labelpad)
858
859        # 14. ホットスポットの凡例追加
860        if add_legend and hotspots and spot_handles:
861            ax_data.legend(
862                handles=spot_handles,
863                loc="upper center",  # 位置を上部中央に
864                bbox_to_anchor=(0.55, -0.01),  # 図の下に配置
865                ncol=len(spot_handles),  # ハンドルの数に応じて列数を設定
866            )
867
868        # 15. 画像の保存
869        if save_fig:
870            if output_dir is None:
871                raise ValueError(
872                    "save_fig=Trueの場合、output_dirを指定する必要があります。有効なディレクトリパスを指定してください。"
873                )
874            output_path: str = os.path.join(output_dir, output_filename)
875            self.logger.info("プロットを保存中...")
876            try:
877                fig.savefig(output_path, bbox_inches="tight")
878                self.logger.info(f"プロットが正常に保存されました: {output_path}")
879            except Exception as e:
880                self.logger.error(f"プロットの保存中にエラーが発生しました: {str(e)}")
881        # 16. 画像の表示
882        if show_fig:
883            plt.show()
884        else:
885            plt.close(fig=fig)

Staticな衛星画像上にフットプリントデータとホットスポットをプロットします。

このメソッドは、指定されたフットプリントデータとホットスポットを可視化します。 ホットスポットが指定されない場合は、フットプリントのみ作図します。

Parameters:

x_list : list[float]
    フットプリントのx座標リスト(メートル単位)。
y_list : list[float]
    フットプリントのy座標リスト(メートル単位)。
c_list : list[float] | None
    フットプリントの強度を示す値のリスト。
center_lat : float
    プロットの中心となる緯度。
center_lon : float
    プロットの中心となる経度。
vmin : float
    カラーバーの最小値。
vmax : float
    カラーバーの最大値。
add_cbar : bool, optional
    カラーバーを追加するかどうか(デフォルトはTrue)。
add_legend : bool, optional
    凡例を追加するかどうか(デフォルトはTrue)。
cbar_label : str | None, optional
    カラーバーのラベル。
cbar_labelpad : int, optional
    カラーバーラベルのパディング。
cmap : str
    使用するカラーマップの名前。
reduce_c_function : callable
    フットプリントの集約関数(デフォルトはnp.mean)。
hotspots : list[HotspotData] | None, optional
    ホットスポットデータのリスト。デフォルトはNone。
hotspot_colors : dict[HotspotType, str] | None, optional
    ホットスポットの色を指定する辞書。
hotspot_markers : dict[HotspotType, str] | None, optional
    ホットスポットの形状を指定する辞書。
    指定の例は {'bio': '^', 'gas': 'o', 'comb': 's'} (三角、丸、四角)など。
lat_correction : float, optional
    緯度方向の補正係数(デフォルトは1)。
lon_correction : float, optional
    経度方向の補正係数(デフォルトは1)。
output_dir : str | None, optional
    プロット画像の保存先パス。
output_filename : str
    プロット画像の保存ファイル名(拡張子を含む)。デフォルトは'footprint.png'。
save_fig : bool
    図の保存を許可するフラグ。デフォルトはTrue。
show_fig : bool
    図の表示を許可するフラグ。デフォルトはTrue。
satellite_image : ImageFile | None, optional
    使用する衛星画像。指定がない場合はデフォルトの画像が生成されます。
xy_max : float, optional
    表示範囲の最大値(デフォルトは5000)。
def plot_flux_footprint_with_scale_checker( self, x_list: list[float], y_list: list[float], c_list: list[float] | None, center_lat: float, center_lon: float, check_points: list[tuple[float, float, str]] | None = None, vmin: float = 0, vmax: float = 100, add_cbar: bool = True, cbar_label: str | None = None, cbar_labelpad: int = 20, cmap: str = 'jet', reduce_c_function: <built-in function callable> = <function mean>, lat_correction: float = 1, lon_correction: float = 1, output_dir: str | None = None, output_filename: str = 'footprint-scale_checker.png', save_fig: bool = True, show_fig: bool = True, satellite_image: PIL.ImageFile.ImageFile | None = None, xy_max: float = 5000) -> None:
 887    def plot_flux_footprint_with_scale_checker(
 888        self,
 889        x_list: list[float],
 890        y_list: list[float],
 891        c_list: list[float] | None,
 892        center_lat: float,
 893        center_lon: float,
 894        check_points: list[tuple[float, float, str]] | None = None,
 895        vmin: float = 0,
 896        vmax: float = 100,
 897        add_cbar: bool = True,
 898        cbar_label: str | None = None,
 899        cbar_labelpad: int = 20,
 900        cmap: str = "jet",
 901        reduce_c_function: callable = np.mean,
 902        lat_correction: float = 1,
 903        lon_correction: float = 1,
 904        output_dir: str | None = None,
 905        output_filename: str = "footprint-scale_checker.png",
 906        save_fig: bool = True,
 907        show_fig: bool = True,
 908        satellite_image: ImageFile | None = None,
 909        xy_max: float = 5000,
 910    ) -> None:
 911        """
 912        Staticな衛星画像上にフットプリントデータとホットスポットをプロットします。
 913
 914        このメソッドは、指定されたフットプリントデータとホットスポットを可視化します。
 915        ホットスポットが指定されない場合は、フットプリントのみ作図します。
 916
 917        Parameters:
 918        ------
 919            x_list : list[float]
 920                フットプリントのx座標リスト(メートル単位)。
 921            y_list : list[float]
 922                フットプリントのy座標リスト(メートル単位)。
 923            c_list : list[float] | None
 924                フットプリントの強度を示す値のリスト。
 925            center_lat : float
 926                プロットの中心となる緯度。
 927            center_lon : float
 928                プロットの中心となる経度。
 929            check_points : list[tuple[float, float, str]] | None
 930                確認用の地点リスト。各要素は (緯度, 経度, ラベル) のタプル。
 931                Noneの場合は中心から500m、1000m、2000m、3000mの位置に仮想的な点を配置。
 932            cmap : str
 933                使用するカラーマップの名前。
 934            vmin : float
 935                カラーバーの最小値。
 936            vmax : float
 937                カラーバーの最大値。
 938            reduce_c_function : callable, optional
 939                フットプリントの集約関数(デフォルトはnp.mean)。
 940            cbar_label : str, optional
 941                カラーバーのラベル。
 942            cbar_labelpad : int, optional
 943                カラーバーラベルのパディング。
 944            hotspots : list[HotspotData] | None
 945                ホットスポットデータのリスト。デフォルトはNone。
 946            hotspot_colors : dict[str, str] | None, optional
 947                ホットスポットの色を指定する辞書。
 948            lon_correction : float, optional
 949                経度方向の補正係数(デフォルトは1)。
 950            lat_correction : float, optional
 951                緯度方向の補正係数(デフォルトは1)。
 952            output_dir : str | None, optional
 953                プロット画像の保存先パス。
 954            output_filename : str
 955                プロット画像の保存ファイル名(拡張子を含む)。デフォルトは'footprint.png'。
 956            save_fig : bool
 957                図の保存を許可するフラグ。デフォルトはTrue。
 958            show_fig : bool
 959                図の表示を許可するフラグ。デフォルトはTrue。
 960            satellite_image : ImageFile | None, optional
 961                使用する衛星画像。指定がない場合はデフォルトの画像が生成されます。
 962            xy_max : float, optional
 963                表示範囲の最大値(デフォルトは5000)。
 964        """
 965        if check_points is None:
 966            # デフォルトの確認ポイントを生成(従来の方式)
 967            default_points = [
 968                (500, "North", 90),  # 北 500m
 969                (1000, "East", 0),  # 東 1000m
 970                (2000, "South", 270),  # 南 2000m
 971                (3000, "West", 180),  # 西 3000m
 972            ]
 973
 974            dummy_hotspots = []
 975            for distance, direction, angle in default_points:
 976                rad = math.radians(angle)
 977                meters_per_lat = self.EARTH_RADIUS_METER * (math.pi / 180)
 978                meters_per_lon = meters_per_lat * math.cos(math.radians(center_lat))
 979
 980                dx = distance * math.cos(rad)
 981                dy = distance * math.sin(rad)
 982
 983                delta_lon = dx / meters_per_lon
 984                delta_lat = dy / meters_per_lat
 985
 986                hotspot = HotspotData(
 987                    avg_lat=center_lat + delta_lat,
 988                    avg_lon=center_lon + delta_lon,
 989                    delta_ch4=0.0,
 990                    delta_c2h6=0.0,
 991                    ratio=0.0,
 992                    type=f"{direction}_{distance}m",
 993                    section=0,
 994                    source="scale_check",
 995                    angle=0,
 996                    correlation=0,
 997                )
 998                dummy_hotspots.append(hotspot)
 999        else:
1000            # 指定された緯度経度を使用
1001            dummy_hotspots = []
1002            for lat, lon, label in check_points:
1003                hotspot = HotspotData(
1004                    avg_lat=lat,
1005                    avg_lon=lon,
1006                    delta_ch4=0.0,
1007                    delta_c2h6=0.0,
1008                    ratio=0.0,
1009                    type=label,
1010                    section=0,
1011                    source="scale_check",
1012                    angle=0,
1013                    correlation=0,
1014                )
1015                dummy_hotspots.append(hotspot)
1016
1017        # カスタムカラーマップの作成
1018        hotspot_colors = {
1019            spot.type: plt.cm.tab10(i % 10) for i, spot in enumerate(dummy_hotspots)
1020        }
1021
1022        # 既存のメソッドを呼び出してプロット
1023        self.plot_flux_footprint_with_hotspots(
1024            x_list=x_list,
1025            y_list=y_list,
1026            c_list=c_list,
1027            center_lat=center_lat,
1028            center_lon=center_lon,
1029            vmin=vmin,
1030            vmax=vmax,
1031            add_cbar=add_cbar,
1032            add_legend=True,
1033            cbar_label=cbar_label,
1034            cbar_labelpad=cbar_labelpad,
1035            cmap=cmap,
1036            reduce_c_function=reduce_c_function,
1037            hotspots=dummy_hotspots,
1038            hotspot_colors=hotspot_colors,
1039            lat_correction=lat_correction,
1040            lon_correction=lon_correction,
1041            output_dir=output_dir,
1042            output_filename=output_filename,
1043            save_fig=save_fig,
1044            show_fig=show_fig,
1045            satellite_image=satellite_image,
1046            xy_max=xy_max,
1047        )

Staticな衛星画像上にフットプリントデータとホットスポットをプロットします。

このメソッドは、指定されたフットプリントデータとホットスポットを可視化します。 ホットスポットが指定されない場合は、フットプリントのみ作図します。

Parameters:

x_list : list[float]
    フットプリントのx座標リスト(メートル単位)。
y_list : list[float]
    フットプリントのy座標リスト(メートル単位)。
c_list : list[float] | None
    フットプリントの強度を示す値のリスト。
center_lat : float
    プロットの中心となる緯度。
center_lon : float
    プロットの中心となる経度。
check_points : list[tuple[float, float, str]] | None
    確認用の地点リスト。各要素は (緯度, 経度, ラベル) のタプル。
    Noneの場合は中心から500m、1000m、2000m、3000mの位置に仮想的な点を配置。
cmap : str
    使用するカラーマップの名前。
vmin : float
    カラーバーの最小値。
vmax : float
    カラーバーの最大値。
reduce_c_function : callable, optional
    フットプリントの集約関数(デフォルトはnp.mean)。
cbar_label : str, optional
    カラーバーのラベル。
cbar_labelpad : int, optional
    カラーバーラベルのパディング。
hotspots : list[HotspotData] | None
    ホットスポットデータのリスト。デフォルトはNone。
hotspot_colors : dict[str, str] | None, optional
    ホットスポットの色を指定する辞書。
lon_correction : float, optional
    経度方向の補正係数(デフォルトは1)。
lat_correction : float, optional
    緯度方向の補正係数(デフォルトは1)。
output_dir : str | None, optional
    プロット画像の保存先パス。
output_filename : str
    プロット画像の保存ファイル名(拡張子を含む)。デフォルトは'footprint.png'。
save_fig : bool
    図の保存を許可するフラグ。デフォルトはTrue。
show_fig : bool
    図の表示を許可するフラグ。デフォルトはTrue。
satellite_image : ImageFile | None, optional
    使用する衛星画像。指定がない場合はデフォルトの画像が生成されます。
xy_max : float, optional
    表示範囲の最大値(デフォルトは5000)。
@staticmethod
def filter_data( df: pandas.core.frame.DataFrame, start_date: str | None = None, end_date: str | None = None, months: list[int] | None = None) -> pandas.core.frame.DataFrame:
1267    @staticmethod
1268    def filter_data(
1269        df: pd.DataFrame,
1270        start_date: str | None = None,
1271        end_date: str | None = None,
1272        months: list[int] | None = None,
1273    ) -> pd.DataFrame:
1274        """
1275        指定された期間や月でデータをフィルタリングするメソッド。
1276
1277        Parameters:
1278        ------
1279            df : pd.DataFrame
1280                フィルタリングするデータフレーム
1281            start_date : str | None
1282                フィルタリングの開始日('YYYY-MM-DD'形式)。デフォルトはNone。
1283            end_date : str | None
1284                フィルタリングの終了日('YYYY-MM-DD'形式)。デフォルトはNone。
1285            months : list[int] | None
1286                フィルタリングする月のリスト(例:[1, 2, 12])。デフォルトはNone。
1287
1288        Returns:
1289        ------
1290            pd.DataFrame
1291                フィルタリングされたデータフレーム
1292
1293        Raises:
1294        ------
1295            ValueError
1296                インデックスがDatetimeIndexでない場合、または日付の形式が不正な場合
1297        """
1298        # インデックスの検証
1299        if not isinstance(df.index, pd.DatetimeIndex):
1300            raise ValueError(
1301                "DataFrameのインデックスはDatetimeIndexである必要があります"
1302            )
1303
1304        filtered_df: pd.DataFrame = df.copy()
1305
1306        # 日付形式の検証と変換
1307        try:
1308            if start_date is not None:
1309                start_date = pd.to_datetime(start_date)
1310            if end_date is not None:
1311                end_date = pd.to_datetime(end_date)
1312        except ValueError as e:
1313            raise ValueError(
1314                "日付の形式が不正です。'YYYY-MM-DD'形式で指定してください"
1315            ) from e
1316
1317        # 期間でフィルタリング
1318        if start_date is not None or end_date is not None:
1319            filtered_df = filtered_df.loc[start_date:end_date]
1320
1321        # 月のバリデーション
1322        if months is not None:
1323            if not all(isinstance(m, int) and 1 <= m <= 12 for m in months):
1324                raise ValueError(
1325                    "monthsは1から12までの整数のリストである必要があります"
1326                )
1327            filtered_df = filtered_df[filtered_df.index.month.isin(months)]
1328
1329        # フィルタリング後のデータが空でないことを確認
1330        if filtered_df.empty:
1331            raise ValueError("フィルタリング後のデータが空になりました")
1332
1333        return filtered_df

指定された期間や月でデータをフィルタリングするメソッド。

Parameters:

df : pd.DataFrame
    フィルタリングするデータフレーム
start_date : str | None
    フィルタリングの開始日('YYYY-MM-DD'形式)。デフォルトはNone。
end_date : str | None
    フィルタリングの終了日('YYYY-MM-DD'形式)。デフォルトはNone。
months : list[int] | None
    フィルタリングする月のリスト(例:[1, 2, 12])。デフォルトはNone。

Returns:

pd.DataFrame
    フィルタリングされたデータフレーム

Raises:

ValueError
    インデックスがDatetimeIndexでない場合、または日付の形式が不正な場合
@staticmethod
def is_weekday(date: datetime.datetime) -> int:
1335    @staticmethod
1336    def is_weekday(date: datetime) -> int:
1337        """
1338        指定された日付が平日であるかどうかを判定します。
1339
1340        Parameters:
1341        ------
1342            date : datetime
1343                判定する日付。
1344
1345        Returns:
1346        ------
1347            int
1348                平日であれば1、そうでなければ0。
1349        """
1350        return 1 if not jpholiday.is_holiday(date) and date.weekday() < 5 else 0

指定された日付が平日であるかどうかを判定します。

Parameters:

date : datetime
    判定する日付。

Returns:

int
    平日であれば1、そうでなければ0。
class CorrectingUtils:
 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
@staticmethod
def correct_df_by_type( df: pandas.core.frame.DataFrame, correction_type: str) -> pandas.core.frame.DataFrame:
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
    無効な補正式が指定された場合。
CORRECTION_TYPES_PATTERN = ['pico_1']
@dataclass
class EmissionData:
 22@dataclass
 23class EmissionData:
 24    """
 25    ホットスポットの排出量データを格納するクラス。
 26
 27    Parameters:
 28    ------
 29        source : str
 30            データソース(日時)
 31        type : HotspotType
 32            ホットスポットの種類(`HotspotType`を参照)
 33        section : str | int | float
 34            セクション情報
 35        latitude : float
 36            緯度
 37        longitude : float
 38            経度
 39        delta_ch4 : float
 40            CH4の増加量 (ppm)
 41        delta_c2h6 : float
 42            C2H6の増加量 (ppb)
 43        ratio : float
 44            C2H6/CH4比
 45        emission_rate : float
 46            排出量 (L/min)
 47        daily_emission : float
 48            日排出量 (L/day)
 49        annual_emission : float
 50            年間排出量 (L/year)
 51    """
 52
 53    source: str
 54    type: HotspotType
 55    section: str | int | float
 56    latitude: float
 57    longitude: float
 58    delta_ch4: float
 59    delta_c2h6: float
 60    ratio: float
 61    emission_rate: float
 62    daily_emission: float
 63    annual_emission: float
 64
 65    def __post_init__(self) -> None:
 66        """
 67        Initialize時のバリデーションを行います。
 68
 69        Raises:
 70        ------
 71            ValueError: 入力値が不正な場合
 72        """
 73        # sourceのバリデーション
 74        if not isinstance(self.source, str) or not self.source.strip():
 75            raise ValueError("Source must be a non-empty string")
 76
 77        # typeのバリデーションは型システムによって保証されるため削除
 78        # HotspotTypeはLiteral["bio", "gas", "comb"]として定義されているため、
 79        # 不正な値は型チェック時に検出されます
 80
 81        # sectionのバリデーション(Noneは許可)
 82        if self.section is not None and not isinstance(self.section, (str, int, float)):
 83            raise ValueError("Section must be a string, int, float, or None")
 84
 85        # 緯度のバリデーション
 86        if (
 87            not isinstance(self.latitude, (int, float))
 88            or not -90 <= self.latitude <= 90
 89        ):
 90            raise ValueError("Latitude must be a number between -90 and 90")
 91
 92        # 経度のバリデーション
 93        if (
 94            not isinstance(self.longitude, (int, float))
 95            or not -180 <= self.longitude <= 180
 96        ):
 97            raise ValueError("Longitude must be a number between -180 and 180")
 98
 99        # delta_ch4のバリデーション
100        if not isinstance(self.delta_ch4, (int, float)) or self.delta_ch4 < 0:
101            raise ValueError("Delta CH4 must be a non-negative number")
102
103        # delta_c2h6のバリデーション
104        if not isinstance(self.delta_c2h6, (int, float)) or self.delta_c2h6 < 0:
105            raise ValueError("Delta C2H6 must be a non-negative number")
106
107        # ratioのバリデーション
108        if not isinstance(self.ratio, (int, float)) or self.ratio < 0:
109            raise ValueError("Ratio must be a non-negative number")
110
111        # emission_rateのバリデーション
112        if not isinstance(self.emission_rate, (int, float)) or self.emission_rate < 0:
113            raise ValueError("Emission rate must be a non-negative number")
114
115        # daily_emissionのバリデーション
116        expected_daily = self.emission_rate * 60 * 24
117        if not math.isclose(self.daily_emission, expected_daily, rel_tol=1e-10):
118            raise ValueError(
119                f"Daily emission ({self.daily_emission}) does not match "
120                f"calculated value from emission rate ({expected_daily})"
121            )
122
123        # annual_emissionのバリデーション
124        expected_annual = self.daily_emission * 365
125        if not math.isclose(self.annual_emission, expected_annual, rel_tol=1e-10):
126            raise ValueError(
127                f"Annual emission ({self.annual_emission}) does not match "
128                f"calculated value from daily emission ({expected_annual})"
129            )
130
131        # NaN値のチェック
132        numeric_fields = [
133            self.latitude,
134            self.longitude,
135            self.delta_ch4,
136            self.delta_c2h6,
137            self.ratio,
138            self.emission_rate,
139            self.daily_emission,
140            self.annual_emission,
141        ]
142        if any(math.isnan(x) for x in numeric_fields):
143            raise ValueError("Numeric fields cannot contain NaN values")
144
145    def to_dict(self) -> dict:
146        """
147        データクラスの内容を辞書形式に変換します。
148
149        Returns:
150        ------
151            dict: データクラスの属性と値を含む辞書
152        """
153        return {
154            "source": self.source,
155            "type": self.type,
156            "section": self.section,
157            "latitude": self.latitude,
158            "longitude": self.longitude,
159            "delta_ch4": self.delta_ch4,
160            "delta_c2h6": self.delta_c2h6,
161            "ratio": self.ratio,
162            "emission_rate": self.emission_rate,
163            "daily_emission": self.daily_emission,
164            "annual_emission": self.annual_emission,
165        }

ホットスポットの排出量データを格納するクラス。

Parameters:

source : str
    データソース(日時)
type : HotspotType
    ホットスポットの種類(`HotspotType`を参照)
section : str | int | float
    セクション情報
latitude : float
    緯度
longitude : float
    経度
delta_ch4 : float
    CH4の増加量 (ppm)
delta_c2h6 : float
    C2H6の増加量 (ppb)
ratio : float
    C2H6/CH4比
emission_rate : float
    排出量 (L/min)
daily_emission : float
    日排出量 (L/day)
annual_emission : float
    年間排出量 (L/year)
EmissionData( source: str, type: Literal['bio', 'gas', 'comb'], section: str | int | float, latitude: float, longitude: float, delta_ch4: float, delta_c2h6: float, ratio: float, emission_rate: float, daily_emission: float, annual_emission: float)
source: str
type: Literal['bio', 'gas', 'comb']
section: str | int | float
latitude: float
longitude: float
delta_ch4: float
delta_c2h6: float
ratio: float
emission_rate: float
daily_emission: float
annual_emission: float
def to_dict(self) -> dict:
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: データクラスの属性と値を含む辞書
class MobileSpatialAnalyzer:
 258class MobileSpatialAnalyzer:
 259    """
 260    移動観測で得られた測定データを解析するクラス
 261    """
 262
 263    EARTH_RADIUS_METERS: float = 6371000  # 地球の半径(メートル)
 264
 265    def __init__(
 266        self,
 267        center_lat: float,
 268        center_lon: float,
 269        inputs: list[MSAInputConfig] | list[tuple[float, float, str | Path]],
 270        num_sections: int = 4,
 271        ch4_enhance_threshold: float = 0.1,
 272        correlation_threshold: float = 0.7,
 273        hotspot_area_meter: float = 50,
 274        window_minutes: float = 5,
 275        column_mapping: dict[str, str] = {
 276            "Time Stamp": "timestamp",
 277            "CH4 (ppm)": "ch4_ppm",
 278            "C2H6 (ppb)": "c2h6_ppb",
 279            "H2O (ppm)": "h2o_ppm",
 280            "Latitude": "latitude",
 281            "Longitude": "longitude",
 282        },
 283        logger: Logger | None = None,
 284        logging_debug: bool = False,
 285    ):
 286        """
 287        測定データ解析クラスの初期化
 288
 289        Parameters:
 290        ------
 291            center_lat : float
 292                中心緯度
 293            center_lon : float
 294                中心経度
 295            inputs : list[MSAInputConfig] | list[tuple[float, float, str | Path]]
 296                入力ファイルのリスト
 297            num_sections : int
 298                分割する区画数。デフォルトは4。
 299            ch4_enhance_threshold : float
 300                CH4増加の閾値(ppm)。デフォルトは0.1。
 301            correlation_threshold : float
 302                相関係数の閾値。デフォルトは0.7。
 303            hotspot_area_meter : float
 304                ホットスポットの検出に使用するエリアの半径(メートル)。デフォルトは50メートル。
 305            window_minutes : float
 306                移動窓の大きさ(分)。デフォルトは5分。
 307            column_mapping : dict[str, str]
 308                元のデータファイルのヘッダーを汎用的な単語に変換するための辞書型データ。
 309                - timestamp,ch4_ppm,c2h6_ppm,h2o_ppm,latitude,longitudeをvalueに、それぞれに対応するカラム名をcolに指定してください。
 310            logger : Logger | None
 311                使用するロガー。Noneの場合は新しいロガーを作成します。
 312            logging_debug : bool
 313                ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。
 314
 315        Returns:
 316        ------
 317            None
 318                初期化処理が完了したことを示します。
 319        """
 320        # ロガー
 321        log_level: int = INFO
 322        if logging_debug:
 323            log_level = DEBUG
 324        self.logger: Logger = MobileSpatialAnalyzer.setup_logger(logger, log_level)
 325        # プライベートなプロパティ
 326        self._center_lat: float = center_lat
 327        self._center_lon: float = center_lon
 328        self._ch4_enhance_threshold: float = ch4_enhance_threshold
 329        self._correlation_threshold: float = correlation_threshold
 330        self._hotspot_area_meter: float = hotspot_area_meter
 331        self._column_mapping: dict[str, str] = column_mapping
 332        self._num_sections: int = num_sections
 333        # セクションの範囲
 334        section_size: float = 360 / num_sections
 335        self._section_size: float = section_size
 336        self._sections = MobileSpatialAnalyzer._initialize_sections(
 337            num_sections, section_size
 338        )
 339        # window_sizeをデータポイント数に変換(分→秒→データポイント数)
 340        self._window_size: int = MobileSpatialAnalyzer._calculate_window_size(
 341            window_minutes
 342        )
 343        # 入力設定の標準化
 344        normalized_input_configs: list[MSAInputConfig] = (
 345            MobileSpatialAnalyzer._normalize_inputs(inputs)
 346        )
 347        # 複数ファイルのデータを読み込み
 348        self._data: dict[str, pd.DataFrame] = self._load_all_data(
 349            normalized_input_configs
 350        )
 351
 352    def analyze_delta_ch4_stats(self, hotspots: list[HotspotData]) -> None:
 353        """
 354        各タイプのホットスポットについてΔCH4の統計情報を計算し、結果を表示します。
 355
 356        Parameters:
 357        ------
 358            hotspots : list[HotspotData]
 359                分析対象のホットスポットリスト
 360
 361        Returns:
 362        ------
 363            None
 364                統計情報の表示が完了したことを示します。
 365        """
 366        # タイプごとにホットスポットを分類
 367        hotspots_by_type: dict[HotspotType, list[HotspotData]] = {
 368            "bio": [h for h in hotspots if h.type == "bio"],
 369            "gas": [h for h in hotspots if h.type == "gas"],
 370            "comb": [h for h in hotspots if h.type == "comb"],
 371        }
 372
 373        # 統計情報を計算し、表示
 374        for spot_type, spots in hotspots_by_type.items():
 375            if spots:
 376                delta_ch4_values = [spot.delta_ch4 for spot in spots]
 377                max_value = max(delta_ch4_values)
 378                mean_value = sum(delta_ch4_values) / len(delta_ch4_values)
 379                median_value = sorted(delta_ch4_values)[len(delta_ch4_values) // 2]
 380                print(f"{spot_type}タイプのホットスポットの統計情報:")
 381                print(f"  最大値: {max_value}")
 382                print(f"  平均値: {mean_value}")
 383                print(f"  中央値: {median_value}")
 384            else:
 385                print(f"{spot_type}タイプのホットスポットは存在しません。")
 386
 387    def analyze_hotspots(
 388        self,
 389        duplicate_check_mode: str = "none",
 390        min_time_threshold_seconds: float = 300,
 391        max_time_threshold_hours: float = 12,
 392    ) -> list[HotspotData]:
 393        """
 394        ホットスポットを検出して分析します。
 395
 396        Parameters:
 397        ------
 398            duplicate_check_mode : str
 399                重複チェックのモード("none","time_window","time_all")。
 400                - "none": 重複チェックを行わない。
 401                - "time_window": 指定された時間窓内の重複のみを除外。
 402                - "time_all": すべての時間範囲で重複チェックを行う。
 403            min_time_threshold_seconds : float
 404                重複とみなす最小時間の閾値(秒)。デフォルトは300秒。
 405            max_time_threshold_hours : float
 406                重複チェックを一時的に無視する最大時間の閾値(時間)。デフォルトは12時間。
 407
 408        Returns:
 409        ------
 410            list[HotspotData]
 411                検出されたホットスポットのリスト。
 412        """
 413        # 不正な入力値に対するエラーチェック
 414        valid_modes = {"none", "time_window", "time_all"}
 415        if duplicate_check_mode not in valid_modes:
 416            raise ValueError(
 417                f"無効な重複チェックモード: {duplicate_check_mode}. 有効な値は {valid_modes} です。"
 418            )
 419
 420        all_hotspots: list[HotspotData] = []
 421
 422        # 各データソースに対して解析を実行
 423        for _, df in self._data.items():
 424            # パラメータの計算
 425            df = MobileSpatialAnalyzer._calculate_hotspots_parameters(
 426                df, self._window_size
 427            )
 428
 429            # ホットスポットの検出
 430            hotspots: list[HotspotData] = self._detect_hotspots(
 431                df,
 432                ch4_enhance_threshold=self._ch4_enhance_threshold,
 433            )
 434            all_hotspots.extend(hotspots)
 435
 436        # 重複チェックモードに応じて処理
 437        if duplicate_check_mode != "none":
 438            unique_hotspots = MobileSpatialAnalyzer.remove_hotspots_duplicates(
 439                all_hotspots,
 440                check_time_all=duplicate_check_mode == "time_all",
 441                min_time_threshold_seconds=min_time_threshold_seconds,
 442                max_time_threshold_hours=max_time_threshold_hours,
 443                hotspot_area_meter=self._hotspot_area_meter,
 444            )
 445            self.logger.info(
 446                f"重複除外: {len(all_hotspots)}{len(unique_hotspots)} ホットスポット"
 447            )
 448            return unique_hotspots
 449
 450        return all_hotspots
 451
 452    def calculate_measurement_stats(
 453        self,
 454        print_individual_stats: bool = True,
 455        print_total_stats: bool = True,
 456    ) -> tuple[float, timedelta]:
 457        """
 458        各ファイルの測定時間と走行距離を計算し、合計を返します。
 459
 460        Parameters:
 461        ------
 462            print_individual_stats : bool
 463                個別ファイルの統計を表示するかどうか。デフォルトはTrue。
 464            print_total_stats : bool
 465                合計統計を表示するかどうか。デフォルトはTrue。
 466
 467        Returns:
 468        ------
 469            tuple[float, timedelta]
 470                総距離(km)と総時間のタプル
 471        """
 472        total_distance: float = 0.0
 473        total_time: timedelta = timedelta()
 474        individual_stats: list[dict] = []  # 個別の統計情報を保存するリスト
 475
 476        # プログレスバーを表示しながら計算
 477        for source_name, df in tqdm(
 478            self._data.items(), desc="Calculating", unit="file"
 479        ):
 480            # 時間の計算
 481            time_spent = df.index[-1] - df.index[0]
 482
 483            # 距離の計算
 484            distance_km = 0.0
 485            for i in range(len(df) - 1):
 486                lat1, lon1 = df.iloc[i][["latitude", "longitude"]]
 487                lat2, lon2 = df.iloc[i + 1][["latitude", "longitude"]]
 488                distance_km += (
 489                    MobileSpatialAnalyzer._calculate_distance(
 490                        lat1=lat1, lon1=lon1, lat2=lat2, lon2=lon2
 491                    )
 492                    / 1000
 493                )
 494
 495            # 合計に加算
 496            total_distance += distance_km
 497            total_time += time_spent
 498
 499            # 統計情報を保存
 500            if print_individual_stats:
 501                average_speed = distance_km / (time_spent.total_seconds() / 3600)
 502                individual_stats.append(
 503                    {
 504                        "source": source_name,
 505                        "distance": distance_km,
 506                        "time": time_spent,
 507                        "speed": average_speed,
 508                    }
 509                )
 510
 511        # 計算完了後に統計情報を表示
 512        if print_individual_stats:
 513            self.logger.info("=== Individual Stats ===")
 514            for stat in individual_stats:
 515                print(f"File         : {stat['source']}")
 516                print(f"  Distance   : {stat['distance']:.2f} km")
 517                print(f"  Time       : {stat['time']}")
 518                print(f"  Avg. Speed : {stat['speed']:.1f} km/h\n")
 519
 520        # 合計を表示
 521        if print_total_stats:
 522            average_speed_total: float = total_distance / (
 523                total_time.total_seconds() / 3600
 524            )
 525            self.logger.info("=== Total Stats ===")
 526            print(f"  Distance   : {total_distance:.2f} km")
 527            print(f"  Time       : {total_time}")
 528            print(f"  Avg. Speed : {average_speed_total:.1f} km/h\n")
 529
 530        return total_distance, total_time
 531
 532    def create_hotspots_map(
 533        self,
 534        hotspots: list[HotspotData],
 535        output_dir: str | Path | None = None,
 536        output_filename: str = "hotspots_map.html",
 537        center_marker_label: str = "Center",
 538        plot_center_marker: bool = True,
 539        radius_meters: float = 3000,
 540        save_fig: bool = True,
 541    ) -> None:
 542        """
 543        ホットスポットの分布を地図上にプロットして保存
 544
 545        Parameters:
 546        ------
 547            hotspots : list[HotspotData]
 548                プロットするホットスポットのリスト
 549            output_dir : str | Path
 550                保存先のディレクトリパス
 551            output_filename : str
 552                保存するファイル名。デフォルトは"hotspots_map"。
 553            center_marker_label : str
 554                中心を示すマーカーのラベルテキスト。デフォルトは"Center"。
 555            plot_center_marker : bool
 556                中心を示すマーカーの有無。デフォルトはTrue。
 557            radius_meters : float
 558                区画分けを示す線の長さ。デフォルトは3000。
 559            save_fig : bool
 560                図の保存を許可するフラグ。デフォルトはTrue。
 561        """
 562        # 地図の作成
 563        m = folium.Map(
 564            location=[self._center_lat, self._center_lon],
 565            zoom_start=15,
 566            tiles="OpenStreetMap",
 567        )
 568
 569        # ホットスポットの種類ごとに異なる色でプロット
 570        for spot in hotspots:
 571            # NaN値チェックを追加
 572            if math.isnan(spot.avg_lat) or math.isnan(spot.avg_lon):
 573                continue
 574
 575            # default type
 576            color = "black"
 577            # タイプに応じて色を設定
 578            if spot.type == "comb":
 579                color = "green"
 580            elif spot.type == "gas":
 581                color = "red"
 582            elif spot.type == "bio":
 583                color = "blue"
 584
 585            # CSSのgrid layoutを使用してHTMLタグを含むテキストをフォーマット
 586            popup_html = f"""
 587            <div style='font-family: Arial; font-size: 12px; display: grid; grid-template-columns: auto auto auto; gap: 5px;'>
 588                <b>Date</b> <span>:</span> <span>{spot.source}</span>
 589                <b>Lat</b> <span>:</span> <span>{spot.avg_lat:.3f}</span>
 590                <b>Lon</b> <span>:</span> <span>{spot.avg_lon:.3f}</span>
 591                <b>ΔCH<sub>4</sub></b> <span>:</span> <span>{spot.delta_ch4:.3f}</span>
 592                <b>ΔC<sub>2</sub>H<sub>6</sub></b> <span>:</span> <span>{spot.delta_c2h6:.3f}</span>
 593                <b>Ratio</b> <span>:</span> <span>{spot.ratio:.3f}</span>
 594                <b>Type</b> <span>:</span> <span>{spot.type}</span>
 595                <b>Section</b> <span>:</span> <span>{spot.section}</span>
 596            </div>
 597            """
 598
 599            # ポップアップのサイズを指定
 600            popup = folium.Popup(
 601                folium.Html(popup_html, script=True),
 602                max_width=200,  # 最大幅(ピクセル)
 603            )
 604
 605            folium.CircleMarker(
 606                location=[spot.avg_lat, spot.avg_lon],
 607                radius=8,
 608                color=color,
 609                fill=True,
 610                popup=popup,
 611            ).add_to(m)
 612
 613        # 中心点のマーカー
 614        if plot_center_marker:
 615            folium.Marker(
 616                [self._center_lat, self._center_lon],
 617                popup=center_marker_label,
 618                icon=folium.Icon(color="green", icon="info-sign"),
 619            ).add_to(m)
 620
 621        # 区画の境界線を描画
 622        for section in range(self._num_sections):
 623            start_angle = math.radians(-180 + section * self._section_size)
 624
 625            R = self.EARTH_RADIUS_METERS
 626
 627            # 境界線の座標を計算
 628            lat1 = self._center_lat
 629            lon1 = self._center_lon
 630            lat2 = math.degrees(
 631                math.asin(
 632                    math.sin(math.radians(lat1)) * math.cos(radius_meters / R)
 633                    + math.cos(math.radians(lat1))
 634                    * math.sin(radius_meters / R)
 635                    * math.cos(start_angle)
 636                )
 637            )
 638            lon2 = self._center_lon + math.degrees(
 639                math.atan2(
 640                    math.sin(start_angle)
 641                    * math.sin(radius_meters / R)
 642                    * math.cos(math.radians(lat1)),
 643                    math.cos(radius_meters / R)
 644                    - math.sin(math.radians(lat1)) * math.sin(math.radians(lat2)),
 645                )
 646            )
 647
 648            # 境界線を描画
 649            folium.PolyLine(
 650                locations=[[lat1, lon1], [lat2, lon2]],
 651                color="black",
 652                weight=1,
 653                opacity=0.5,
 654            ).add_to(m)
 655
 656        # 地図を保存
 657        if save_fig and output_dir is None:
 658            raise ValueError(
 659                "save_fig=Trueの場合、output_dirを指定する必要があります。有効なディレクトリパスを指定してください。"
 660            )
 661            output_path: str = os.path.join(output_dir, output_filename)
 662            m.save(str(output_path))
 663            self.logger.info(f"地図を保存しました: {output_path}")
 664
 665    def export_hotspots_to_csv(
 666        self,
 667        hotspots: list[HotspotData],
 668        output_dir: str | Path | None = None,
 669        output_filename: str = "hotspots.csv",
 670    ) -> None:
 671        """
 672        ホットスポットの情報をCSVファイルに出力します。
 673
 674        Parameters:
 675        ------
 676            hotspots : list[HotspotData]
 677                出力するホットスポットのリスト
 678            output_dir : str | Path | None
 679                出力先ディレクトリ
 680            output_filename : str
 681                出力ファイル名
 682        """
 683        # 日時の昇順でソート
 684        sorted_hotspots = sorted(hotspots, key=lambda x: x.source)
 685
 686        # 出力用のデータを作成
 687        records = []
 688        for spot in sorted_hotspots:
 689            record = {
 690                "source": spot.source,
 691                "type": spot.type,
 692                "delta_ch4": spot.delta_ch4,
 693                "delta_c2h6": spot.delta_c2h6,
 694                "ratio": spot.ratio,
 695                "correlation": spot.correlation,
 696                "angle": spot.angle,
 697                "section": spot.section,
 698                "latitude": spot.avg_lat,
 699                "longitude": spot.avg_lon,
 700            }
 701            records.append(record)
 702
 703        # DataFrameに変換してCSVに出力
 704        if output_dir is None:
 705            raise ValueError(
 706                "output_dirが指定されていません。有効なディレクトリパスを指定してください。"
 707            )
 708        output_path: str = os.path.join(output_dir, output_filename)
 709        df = pd.DataFrame(records)
 710        df.to_csv(output_path, index=False)
 711        self.logger.info(
 712            f"ホットスポット情報をCSVファイルに出力しました: {output_path}"
 713        )
 714
 715    def get_preprocessed_data(
 716        self,
 717    ) -> pd.DataFrame:
 718        """
 719        データ前処理を行い、CH4とC2H6の相関解析に必要な形式に整えます。
 720        コンストラクタで読み込んだすべてのデータを前処理し、結合したDataFrameを返します。
 721
 722        Returns:
 723        ------
 724            pd.DataFrame
 725                前処理済みの結合されたDataFrame
 726        """
 727        processed_dfs: list[pd.DataFrame] = []
 728
 729        # 各データソースに対して解析を実行
 730        for source_name, df in self._data.items():
 731            # パラメータの計算
 732            processed_df = MobileSpatialAnalyzer._calculate_hotspots_parameters(
 733                df, self._window_size
 734            )
 735            # ソース名を列として追加
 736            processed_df["source"] = source_name
 737            processed_dfs.append(processed_df)
 738
 739        # すべてのDataFrameを結合
 740        if not processed_dfs:
 741            raise ValueError("処理対象のデータが存在しません。")
 742
 743        combined_df = pd.concat(processed_dfs, axis=0)
 744        return combined_df
 745
 746    def get_section_size(self) -> float:
 747        """
 748        セクションのサイズを取得するメソッド。
 749        このメソッドは、解析対象のデータを区画に分割する際の
 750        各区画の角度範囲を示すサイズを返します。
 751
 752        Returns:
 753        ------
 754            float
 755                1セクションのサイズ(度単位)
 756        """
 757        return self._section_size
 758
 759    def plot_ch4_delta_histogram(
 760        self,
 761        hotspots: list[HotspotData],
 762        output_dir: str | Path | None,
 763        output_filename: str = "ch4_delta_histogram.png",
 764        dpi: int = 200,
 765        figsize: tuple[int, int] = (8, 6),
 766        fontsize: float = 20,
 767        xlim: tuple[float, float] | None = None,
 768        ylim: tuple[float, float] | None = None,
 769        save_fig: bool = True,
 770        show_fig: bool = True,
 771        yscale_log: bool = True,
 772        print_bins_analysis: bool = False,
 773    ) -> None:
 774        """
 775        CH4の増加量(ΔCH4)の積み上げヒストグラムをプロットします。
 776
 777        Parameters:
 778        ------
 779            hotspots : list[HotspotData]
 780                プロットするホットスポットのリスト
 781            output_dir : str | Path | None
 782                保存先のディレクトリパス
 783            output_filename : str
 784                保存するファイル名。デフォルトは"ch4_delta_histogram.png"。
 785            dpi : int
 786                解像度。デフォルトは200。
 787            figsize : tuple[int, int]
 788                図のサイズ。デフォルトは(8, 6)。
 789            fontsize : float
 790                フォントサイズ。デフォルトは20。
 791            xlim : tuple[float, float] | None
 792                x軸の範囲。Noneの場合は自動設定。
 793            ylim : tuple[float, float] | None
 794                y軸の範囲。Noneの場合は自動設定。
 795            save_fig : bool
 796                図の保存を許可するフラグ。デフォルトはTrue。
 797            show_fig : bool
 798                図の表示を許可するフラグ。デフォルトはTrue。
 799            yscale_log : bool
 800                y軸をlogにするかどうか。デフォルトはTrue。
 801            print_bins_analysis : bool
 802                ビンごとの内訳を表示するオプション。
 803        """
 804        plt.rcParams["font.size"] = fontsize
 805        fig = plt.figure(figsize=figsize, dpi=dpi)
 806
 807        # ホットスポットからデータを抽出
 808        all_ch4_deltas = []
 809        all_types = []
 810        for spot in hotspots:
 811            all_ch4_deltas.append(spot.delta_ch4)
 812            all_types.append(spot.type)
 813
 814        # データをNumPy配列に変換
 815        all_ch4_deltas = np.array(all_ch4_deltas)
 816        all_types = np.array(all_types)
 817
 818        # 0.1刻みのビンを作成
 819        if xlim is not None:
 820            bins = np.arange(xlim[0], xlim[1] + 0.1, 0.1)
 821        else:
 822            max_val = np.ceil(np.max(all_ch4_deltas) * 10) / 10
 823            bins = np.arange(0, max_val + 0.1, 0.1)
 824
 825        # タイプごとのヒストグラムデータを計算
 826        hist_data = {}
 827        # HotspotTypeのリテラル値を使用してイテレーション
 828        for type_name in get_args(HotspotType):  # typing.get_argsをインポート
 829            mask = all_types == type_name
 830            if np.any(mask):
 831                counts, _ = np.histogram(all_ch4_deltas[mask], bins=bins)
 832                hist_data[type_name] = counts
 833
 834        # ビンごとの内訳を表示
 835        if print_bins_analysis:
 836            self.logger.info("各ビンの内訳:")
 837            print(f"{'Bin Range':15} {'bio':>8} {'gas':>8} {'comb':>8} {'Total':>8}")
 838            print("-" * 50)
 839
 840            for i in range(len(bins) - 1):
 841                bin_start = bins[i]
 842                bin_end = bins[i + 1]
 843                bio_count = hist_data.get("bio", np.zeros(len(bins) - 1))[i]
 844                gas_count = hist_data.get("gas", np.zeros(len(bins) - 1))[i]
 845                comb_count = hist_data.get("comb", np.zeros(len(bins) - 1))[i]
 846                total = bio_count + gas_count + comb_count
 847
 848                if total > 0:  # 合計が0のビンは表示しない
 849                    print(
 850                        f"{bin_start:4.1f}-{bin_end:<8.1f}"
 851                        f"{int(bio_count):8d}"
 852                        f"{int(gas_count):8d}"
 853                        f"{int(comb_count):8d}"
 854                        f"{int(total):8d}"
 855                    )
 856
 857        # 積み上げヒストグラムを作成
 858        bottom = np.zeros_like(hist_data.get("bio", np.zeros(len(bins) - 1)))
 859
 860        # 色の定義をHotspotTypeを使用して型安全に定義
 861        colors: dict[HotspotType, str] = {"bio": "blue", "gas": "red", "comb": "green"}
 862
 863        # HotspotTypeのリテラル値を使用してイテレーション
 864        for type_name in get_args(HotspotType):
 865            if type_name in hist_data:
 866                plt.bar(
 867                    bins[:-1],
 868                    hist_data[type_name],
 869                    width=np.diff(bins)[0],
 870                    bottom=bottom,
 871                    color=colors[type_name],
 872                    label=type_name,
 873                    alpha=0.6,
 874                    align="edge",
 875                )
 876                bottom += hist_data[type_name]
 877
 878        if yscale_log:
 879            plt.yscale("log")
 880        plt.xlabel("Δ$\\mathregular{CH_{4}}$ (ppm)")
 881        plt.ylabel("Frequency")
 882        plt.legend()
 883        plt.grid(True, which="both", ls="-", alpha=0.2)
 884
 885        # 軸の範囲を設定
 886        if xlim is not None:
 887            plt.xlim(xlim)
 888        if ylim is not None:
 889            plt.ylim(ylim)
 890
 891        # グラフの保存または表示
 892        if save_fig:
 893            if output_dir is None:
 894                raise ValueError(
 895                    "save_fig=Trueの場合、output_dirを指定する必要があります。有効なディレクトリパスを指定してください。"
 896                )
 897            os.makedirs(output_dir, exist_ok=True)
 898            output_path: str = os.path.join(output_dir, output_filename)
 899            plt.savefig(output_path, bbox_inches="tight")
 900            self.logger.info(f"ヒストグラムを保存しました: {output_path}")
 901        if show_fig:
 902            plt.show()
 903        else:
 904            plt.close(fig=fig)
 905
 906    def plot_mapbox(
 907        self,
 908        df: pd.DataFrame,
 909        col: str,
 910        mapbox_access_token: str,
 911        sort_value_column: bool = True,
 912        output_dir: str | Path | None = None,
 913        output_filename: str = "mapbox_plot.html",
 914        lat_column: str = "latitude",
 915        lon_column: str = "longitude",
 916        colorscale: str = "Jet",
 917        center_lat: float | None = None,
 918        center_lon: float | None = None,
 919        zoom: float = 12,
 920        width: int = 700,
 921        height: int = 700,
 922        tick_font_family: str = "Arial",
 923        title_font_family: str = "Arial",
 924        tick_font_size: int = 12,
 925        title_font_size: int = 14,
 926        marker_size: int = 4,
 927        colorbar_title: str | None = None,
 928        value_range: tuple[float, float] | None = None,
 929        save_fig: bool = True,
 930        show_fig: bool = True,
 931    ) -> None:
 932        """
 933        Plotlyを使用してMapbox上にデータをプロットします。
 934
 935        Parameters:
 936        ------
 937            df : pd.DataFrame
 938                プロットするデータを含むDataFrame
 939            col : str
 940                カラーマッピングに使用する列名
 941            mapbox_access_token : str
 942                Mapboxのアクセストークン
 943            sort_value_column : bool
 944                value_columnをソートするか否か。デフォルトはTrue。
 945            output_dir : str | Path | None
 946                出力ディレクトリのパス
 947            output_filename : str
 948                出力ファイル名。デフォルトは"mapbox_plot.html"
 949            lat_column : str
 950                緯度の列名。デフォルトは"latitude"
 951            lon_column : str
 952                経度の列名。デフォルトは"longitude"
 953            colorscale : str
 954                使用するカラースケール。デフォルトは"Jet"
 955            center_lat : float | None
 956                中心緯度。デフォルトはNoneで、self._center_latを使用
 957            center_lon : float | None
 958                中心経度。デフォルトはNoneで、self._center_lonを使用
 959            zoom : float
 960                マップの初期ズームレベル。デフォルトは12
 961            width : int
 962                プロットの幅(ピクセル)。デフォルトは700
 963            height : int
 964                プロットの高さ(ピクセル)。デフォルトは700
 965            tick_font_family : str
 966                カラーバーの目盛りフォントファミリー。デフォルトは"Arial"
 967            title_font_family : str
 968                カラーバーのラベルフォントファミリー。デフォルトは"Arial"
 969            tick_font_size : int
 970                カラーバーの目盛りフォントサイズ。デフォルトは12
 971            title_font_size : int
 972                カラーバーのラベルフォントサイズ。デフォルトは14
 973            marker_size : int
 974                マーカーのサイズ。デフォルトは4
 975            colorbar_title : str | None
 976                カラーバーのラベル
 977            value_range : tuple[float, float] | None
 978                カラーマッピングの範囲。デフォルトはNoneで、データの最小値と最大値を使用
 979            save_fig : bool
 980                図を保存するかどうか。デフォルトはTrue
 981            show_fig : bool
 982                図を表示するかどうか。デフォルトはTrue
 983        """
 984        df_mapping: pd.DataFrame = df.copy().dropna(subset=[col])
 985        if sort_value_column:
 986            df_mapping = df_mapping.sort_values(col)
 987        # 中心座標の設定
 988        center_lat = center_lat if center_lat is not None else self._center_lat
 989        center_lon = center_lon if center_lon is not None else self._center_lon
 990
 991        # カラーマッピングの範囲を設定
 992        cmin, cmax = 0, 0
 993        if value_range is None:
 994            cmin = df_mapping[col].min()
 995            cmax = df_mapping[col].max()
 996        else:
 997            cmin, cmax = value_range
 998
 999        # カラーバーのタイトルを設定
1000        title_text = colorbar_title if colorbar_title is not None else col
1001
1002        # Scattermapboxのデータを作成
1003        scatter_data = go.Scattermapbox(
1004            lat=df_mapping[lat_column],
1005            lon=df_mapping[lon_column],
1006            text=df_mapping[col].astype(str),
1007            hoverinfo="text",
1008            mode="markers",
1009            marker=dict(
1010                color=df_mapping[col],
1011                size=marker_size,
1012                reversescale=False,
1013                autocolorscale=False,
1014                colorscale=colorscale,
1015                cmin=cmin,
1016                cmax=cmax,
1017                colorbar=dict(
1018                    tickformat="3.2f",
1019                    outlinecolor="black",
1020                    outlinewidth=1.5,
1021                    ticks="outside",
1022                    ticklen=7,
1023                    tickwidth=1.5,
1024                    tickcolor="black",
1025                    tickfont=dict(
1026                        family=tick_font_family, color="black", size=tick_font_size
1027                    ),
1028                    title=dict(
1029                        text=title_text, side="top"
1030                    ),  # カラーバーのタイトルを設定
1031                    titlefont=dict(
1032                        family=title_font_family,
1033                        color="black",
1034                        size=title_font_size,
1035                    ),
1036                ),
1037            ),
1038        )
1039
1040        # レイアウトの設定
1041        layout = go.Layout(
1042            width=width,
1043            height=height,
1044            showlegend=False,
1045            mapbox=dict(
1046                accesstoken=mapbox_access_token,
1047                center=dict(lat=center_lat, lon=center_lon),
1048                zoom=zoom,
1049            ),
1050        )
1051
1052        # 図の作成
1053        fig = go.Figure(data=[scatter_data], layout=layout)
1054
1055        # 図の保存
1056        if save_fig:
1057            # 保存時の出力ディレクトリチェック
1058            if output_dir is None:
1059                raise ValueError(
1060                    "save_fig=Trueの場合、output_dirを指定する必要があります。"
1061                )
1062            os.makedirs(output_dir, exist_ok=True)
1063            output_path = os.path.join(output_dir, output_filename)
1064            pyo.plot(fig, filename=output_path, auto_open=False)
1065            self.logger.info(f"Mapboxプロットを保存しました: {output_path}")
1066        # 図の表示
1067        if show_fig:
1068            pyo.iplot(fig)
1069
1070    def plot_scatter_c2c1(
1071        self,
1072        hotspots: list[HotspotData],
1073        output_dir: str | Path | None = None,
1074        output_filename: str = "scatter_c2c1.png",
1075        dpi: int = 200,
1076        figsize: tuple[int, int] = (4, 4),
1077        fontsize: float = 12,
1078        save_fig: bool = True,
1079        show_fig: bool = True,
1080        ratio_labels: dict[float, tuple[float, float, str]] | None = None,
1081    ) -> None:
1082        """
1083        検出されたホットスポットのΔC2H6とΔCH4の散布図をプロットします。
1084
1085        Parameters:
1086        ------
1087            hotspots : list[HotspotData]
1088                プロットするホットスポットのリスト
1089            output_dir : str | Path | None
1090                保存先のディレクトリパス
1091            output_filename : str
1092                保存するファイル名。デフォルトは"scatter_c2c1.png"。
1093            dpi : int
1094                解像度。デフォルトは200。
1095            figsize : tuple[int, int]
1096                図のサイズ。デフォルトは(4, 4)。
1097            fontsize : float
1098                フォントサイズ。デフォルトは12。
1099            save_fig : bool
1100                図の保存を許可するフラグ。デフォルトはTrue。
1101            show_fig : bool
1102                図の表示を許可するフラグ。デフォルトはTrue。
1103            ratio_labels : dict[float, tuple[float, float, str]] | None
1104                比率線とラベルの設定。
1105                キーは比率値、値は (x位置, y位置, ラベルテキスト) のタプル。
1106                Noneの場合はデフォルト設定を使用。デフォルト値:
1107                {
1108                    0.001: (1.25, 2, "0.001"),
1109                    0.005: (1.25, 8, "0.005"),
1110                    0.010: (1.25, 15, "0.01"),
1111                    0.020: (1.25, 30, "0.02"),
1112                    0.030: (1.0, 40, "0.03"),
1113                    0.076: (0.20, 42, "0.076 (Osaka)")
1114                }
1115        """
1116        plt.rcParams["font.size"] = fontsize
1117        fig = plt.figure(figsize=figsize, dpi=dpi)
1118
1119        # タイプごとのデータを収集
1120        type_data: dict[HotspotType, list[tuple[float, float]]] = {
1121            "bio": [],
1122            "gas": [],
1123            "comb": [],
1124        }
1125        for spot in hotspots:
1126            type_data[spot.type].append((spot.delta_ch4, spot.delta_c2h6))
1127
1128        # 色とラベルの定義
1129        colors: dict[HotspotType, str] = {"bio": "blue", "gas": "red", "comb": "green"}
1130        labels: dict[HotspotType, str] = {"bio": "bio", "gas": "gas", "comb": "comb"}
1131
1132        # タイプごとにプロット(データが存在する場合のみ)
1133        for spot_type, data in type_data.items():
1134            if data:  # データが存在する場合のみプロット
1135                ch4_values, c2h6_values = zip(*data)
1136                plt.plot(
1137                    ch4_values,
1138                    c2h6_values,
1139                    "o",
1140                    c=colors[spot_type],
1141                    alpha=0.5,
1142                    ms=2,
1143                    label=labels[spot_type],
1144                )
1145
1146        # デフォルトの比率とラベル設定
1147        default_ratio_labels = {
1148            0.001: (1.25, 2, "0.001"),
1149            0.005: (1.25, 8, "0.005"),
1150            0.010: (1.25, 15, "0.01"),
1151            0.020: (1.25, 30, "0.02"),
1152            0.030: (1.0, 40, "0.03"),
1153            0.076: (0.20, 42, "0.076 (Osaka)"),
1154        }
1155
1156        ratio_labels = ratio_labels or default_ratio_labels
1157
1158        # プロット後、軸の設定前に比率の線を追加
1159        x = np.array([0, 5])
1160        base_ch4 = 0.0
1161        base = 0.0
1162
1163        # 各比率に対して線を引く
1164        for ratio, (x_pos, y_pos, label) in ratio_labels.items():
1165            y = (x - base_ch4) * 1000 * ratio + base
1166            plt.plot(x, y, "-", c="black", alpha=0.5)
1167            plt.text(x_pos, y_pos, label)
1168
1169        plt.ylim(0, 50)
1170        plt.xlim(0, 2.0)
1171        plt.ylabel("Δ$\\mathregular{C_{2}H_{6}}$ (ppb)")
1172        plt.xlabel("Δ$\\mathregular{CH_{4}}$ (ppm)")
1173        plt.legend()
1174
1175        # グラフの保存または表示
1176        if save_fig:
1177            if output_dir is None:
1178                raise ValueError(
1179                    "save_fig=Trueの場合、output_dirを指定する必要があります。有効なディレクトリパスを指定してください。"
1180                )
1181            output_path: str = os.path.join(output_dir, output_filename)
1182            plt.savefig(output_path, bbox_inches="tight")
1183            self.logger.info(f"散布図を保存しました: {output_path}")
1184        if show_fig:
1185            plt.show()
1186        else:
1187            plt.close(fig=fig)
1188
1189    def plot_timeseries(
1190        self,
1191        dpi: int = 200,
1192        source_name: str | None = None,
1193        figsize: tuple[float, float] = (8, 4),
1194        output_dir: str | Path | None = None,
1195        output_filename: str = "timeseries.png",
1196        save_fig: bool = False,
1197        show_fig: bool = True,
1198        col_ch4: str = "ch4_ppm",
1199        col_c2h6: str = "c2h6_ppb",
1200        col_h2o: str = "h2o_ppm",
1201        ylim_ch4: tuple[float, float] | None = None,
1202        ylim_c2h6: tuple[float, float] | None = None,
1203        ylim_h2o: tuple[float, float] | None = None,
1204    ) -> None:
1205        """
1206        時系列データをプロットします。
1207
1208        Parameters:
1209        ------
1210            dpi : int
1211                図の解像度を指定します。デフォルトは200です。
1212            source_name : str | None
1213                プロットするデータソースの名前。Noneの場合は最初のデータソースを使用します。
1214            figsize : tuple[float, float]
1215                図のサイズを指定します。デフォルトは(8, 4)です。
1216            output_dir : str | Path | None
1217                保存先のディレクトリを指定します。save_fig=Trueの場合は必須です。
1218            output_filename : str
1219                保存するファイル名を指定します。デフォルトは"time_series.png"です。
1220            save_fig : bool
1221                図を保存するかどうかを指定します。デフォルトはFalseです。
1222            show_fig : bool
1223                図を表示するかどうかを指定します。デフォルトはTrueです。
1224            col_ch4 : str
1225                CH4データのキーを指定します。デフォルトは"ch4_ppm"です。
1226            col_c2h6 : str
1227                C2H6データのキーを指定します。デフォルトは"c2h6_ppb"です。
1228            col_h2o : str
1229                H2Oデータのキーを指定します。デフォルトは"h2o_ppm"です。
1230            ylim_ch4 : tuple[float, float] | None
1231                CH4プロットのy軸範囲を指定します。デフォルトはNoneです。
1232            ylim_c2h6 : tuple[float, float] | None
1233                C2H6プロットのy軸範囲を指定します。デフォルトはNoneです。
1234            ylim_h2o : tuple[float, float] | None
1235                H2Oプロットのy軸範囲を指定します。デフォルトはNoneです。
1236        """
1237        dfs_dict: dict[str, pd.DataFrame] = self._data.copy()
1238        # データソースの選択
1239        if not dfs_dict:
1240            raise ValueError("データが読み込まれていません。")
1241
1242        if source_name is None:
1243            source_name = list(dfs_dict.keys())[0]
1244        elif source_name not in dfs_dict:
1245            raise ValueError(
1246                f"指定されたデータソース '{source_name}' が見つかりません。"
1247            )
1248
1249        df = dfs_dict[source_name]
1250
1251        # プロットの作成
1252        fig = plt.figure(figsize=figsize, dpi=dpi)
1253
1254        # CH4プロット
1255        ax1 = fig.add_subplot(3, 1, 1)
1256        ax1.plot(df.index, df[col_ch4], c="red")
1257        if ylim_ch4:
1258            ax1.set_ylim(ylim_ch4)
1259        ax1.set_ylabel("$\\mathregular{CH_{4}}$ (ppm)")
1260        ax1.grid(True, alpha=0.3)
1261
1262        # C2H6プロット
1263        ax2 = fig.add_subplot(3, 1, 2)
1264        ax2.plot(df.index, df[col_c2h6], c="red")
1265        if ylim_c2h6:
1266            ax2.set_ylim(ylim_c2h6)
1267        ax2.set_ylabel("$\\mathregular{C_{2}H_{6}}$ (ppb)")
1268        ax2.grid(True, alpha=0.3)
1269
1270        # H2Oプロット
1271        ax3 = fig.add_subplot(3, 1, 3)
1272        ax3.plot(df.index, df[col_h2o], c="red")
1273        if ylim_h2o:
1274            ax3.set_ylim(ylim_h2o)
1275        ax3.set_ylabel("$\\mathregular{H_{2}O}$ (ppm)")
1276        ax3.grid(True, alpha=0.3)
1277
1278        # x軸のフォーマット調整
1279        for ax in [ax1, ax2, ax3]:
1280            ax.xaxis.set_major_formatter(mdates.DateFormatter("%H:%M"))
1281
1282        plt.subplots_adjust(wspace=0.38, hspace=0.38)
1283
1284        # 図の保存
1285        if save_fig:
1286            if output_dir is None:
1287                raise ValueError(
1288                    "save_fig=Trueの場合、output_dirを指定する必要があります。有効なディレクトリパスを指定してください。"
1289                )
1290            output_path = os.path.join(output_dir, output_filename)
1291            plt.savefig(output_path, bbox_inches="tight")
1292            self.logger.info(f"時系列プロットを保存しました: {output_path}")
1293
1294        if show_fig:
1295            plt.show()
1296        else:
1297            plt.close(fig=fig)
1298
1299    def _detect_hotspots(
1300        self,
1301        df: pd.DataFrame,
1302        ch4_enhance_threshold: float,
1303    ) -> list[HotspotData]:
1304        """
1305        シンプル化したホットスポット検出
1306
1307        Parameters:
1308        ------
1309            df : pd.DataFrame
1310                入力データフレーム
1311            ch4_enhance_threshold : float
1312                CH4増加の閾値
1313
1314        Returns:
1315        ------
1316            list[HotspotData]
1317                検出されたホットスポットのリスト
1318        """
1319        hotspots: list[HotspotData] = []
1320
1321        # CH4増加量が閾値を超えるデータポイントを抽出
1322        enhanced_mask = df["ch4_ppm_delta"] >= ch4_enhance_threshold
1323
1324        if enhanced_mask.any():
1325            lat = df["latitude"][enhanced_mask]
1326            lon = df["longitude"][enhanced_mask]
1327            ratios = df["c2c1_ratio_delta"][enhanced_mask]
1328            delta_ch4 = df["ch4_ppm_delta"][enhanced_mask]
1329            delta_c2h6 = df["c2h6_ppb_delta"][enhanced_mask]
1330
1331            # 各ポイントに対してホットスポットを作成
1332            for i in range(len(lat)):
1333                if pd.notna(ratios.iloc[i]):
1334                    current_lat = lat.iloc[i]
1335                    current_lon = lon.iloc[i]
1336                    correlation = df["ch4_c2h6_correlation"].iloc[i]
1337
1338                    # 比率に基づいてタイプを決定
1339                    spot_type: HotspotType = "bio"
1340                    if ratios.iloc[i] >= 100:
1341                        spot_type = "comb"
1342                    elif ratios.iloc[i] >= 5:
1343                        spot_type = "gas"
1344
1345                    angle: float = MobileSpatialAnalyzer._calculate_angle(
1346                        lat=current_lat,
1347                        lon=current_lon,
1348                        center_lat=self._center_lat,
1349                        center_lon=self._center_lon,
1350                    )
1351                    section: int = self._determine_section(angle)
1352
1353                    hotspots.append(
1354                        HotspotData(
1355                            source=ratios.index[i].strftime("%Y-%m-%d %H:%M:%S"),
1356                            angle=angle,
1357                            avg_lat=current_lat,
1358                            avg_lon=current_lon,
1359                            delta_ch4=delta_ch4.iloc[i],
1360                            delta_c2h6=delta_c2h6.iloc[i],
1361                            correlation=max(-1, min(1, correlation)),
1362                            ratio=ratios.iloc[i],
1363                            section=section,
1364                            type=spot_type,
1365                        )
1366                    )
1367
1368        return hotspots
1369
1370    def _determine_section(self, angle: float) -> int:
1371        """
1372        角度に基づいて所属する区画を特定します。
1373
1374        Parameters:
1375        ------
1376            angle : float
1377                計算された角度
1378
1379        Returns:
1380        ------
1381            int
1382                区画番号(0-based-index)
1383        """
1384        for section_num, (start, end) in self._sections.items():
1385            if start <= angle < end:
1386                return section_num
1387        # -180度の場合は最後の区画に含める
1388        return self._num_sections - 1
1389
1390    def _load_all_data(
1391        self, input_configs: list[MSAInputConfig]
1392    ) -> dict[str, pd.DataFrame]:
1393        """
1394        全入力ファイルのデータを読み込み、データフレームの辞書を返します。
1395
1396        このメソッドは、指定された入力設定に基づいてすべてのデータファイルを読み込み、
1397        各ファイルのデータをデータフレームとして格納した辞書を生成します。
1398
1399        Parameters:
1400        ------
1401            input_configs : list[MSAInputConfig]
1402                読み込むファイルの設定リスト。
1403
1404        Returns:
1405        ------
1406            dict[str, pd.DataFrame]
1407                読み込まれたデータフレームの辞書。キーはファイル名、値はデータフレーム。
1408        """
1409        all_data: dict[str, pd.DataFrame] = {}
1410        for config in input_configs:
1411            df, source_name = self._load_data(config)
1412            all_data[source_name] = df
1413        return all_data
1414
1415    def _load_data(self, config: MSAInputConfig) -> tuple[pd.DataFrame, str]:
1416        """
1417        測定データを読み込み、前処理を行うメソッド。
1418
1419        Parameters:
1420        ------
1421            config : MSAInputConfig
1422                入力ファイルの設定を含むオブジェクト。
1423
1424        Returns:
1425        ------
1426            tuple[pd.DataFrame, str]
1427                読み込まれたデータフレームとそのソース名を含むタプル。
1428        """
1429        source_name: str = Path(config.path).stem
1430        df: pd.DataFrame = pd.read_csv(config.path, na_values=["No Data", "nan"])
1431
1432        # カラム名の標準化(測器に依存しない汎用的な名前に変更)
1433        df = df.rename(columns=self._column_mapping)
1434        df["timestamp"] = pd.to_datetime(df["timestamp"])
1435        # インデックスを設定(元のtimestampカラムは保持)
1436        df = df.set_index("timestamp", drop=False)
1437
1438        # 緯度経度のnanを削除
1439        df = df.dropna(subset=["latitude", "longitude"])
1440
1441        if config.lag < 0:
1442            raise ValueError(
1443                f"Invalid lag value: {config.lag}. Must be a non-negative float."
1444            )
1445
1446        # 遅れ時間の補正
1447        columns_to_shift: list[str] = ["ch4_ppm", "c2h6_ppb", "h2o_ppm"]
1448        # サンプリング周波数に応じてシフト量を調整
1449        shift_periods: float = -config.lag * config.fs  # fsを掛けて補正
1450
1451        for col in columns_to_shift:
1452            df[col] = df[col].shift(shift_periods)
1453
1454        df = df.dropna(subset=columns_to_shift)
1455
1456        # 水蒸気干渉などの補正式を適用
1457        if config.correction_type is not None:
1458            df = CorrectingUtils.correct_df_by_type(df, config.correction_type)
1459        else:
1460            self.logger.warn(
1461                f"'correction_type' is None, so no correction functions will be applied. Source: {source_name}"
1462            )
1463
1464        return df, source_name
1465
1466    @staticmethod
1467    def _calculate_angle(
1468        lat: float, lon: float, center_lat: float, center_lon: float
1469    ) -> float:
1470        """
1471        中心からの角度を計算
1472
1473        Parameters:
1474        ------
1475            lat : float
1476                対象地点の緯度
1477            lon : float
1478                対象地点の経度
1479            center_lat : float
1480                中心の緯度
1481            center_lon : float
1482                中心の経度
1483
1484        Returns:
1485        ------
1486            float
1487                真北を0°として時計回りの角度(-180°から180°)
1488        """
1489        d_lat: float = lat - center_lat
1490        d_lon: float = lon - center_lon
1491        # arctanを使用して角度を計算(ラジアン)
1492        angle_rad: float = math.atan2(d_lon, d_lat)
1493        # ラジアンから度に変換(-180から180の範囲)
1494        angle_deg: float = math.degrees(angle_rad)
1495        return angle_deg
1496
1497    @classmethod
1498    def _calculate_distance(
1499        cls, lat1: float, lon1: float, lat2: float, lon2: float
1500    ) -> float:
1501        """
1502        2点間の距離をメートル単位で計算(Haversine formula)
1503
1504        Parameters:
1505        ------
1506            lat1 : float
1507                地点1の緯度
1508            lon1 : float
1509                地点1の経度
1510            lat2 : float
1511                地点2の緯度
1512            lon2 : float
1513                地点2の経度
1514
1515        Returns:
1516        ------
1517            float
1518                2地点間の距離(メートル)
1519        """
1520        R = cls.EARTH_RADIUS_METERS
1521
1522        # 緯度経度をラジアンに変換
1523        lat1_rad: float = math.radians(lat1)
1524        lon1_rad: float = math.radians(lon1)
1525        lat2_rad: float = math.radians(lat2)
1526        lon2_rad: float = math.radians(lon2)
1527
1528        # 緯度と経度の差分
1529        dlat: float = lat2_rad - lat1_rad
1530        dlon: float = lon2_rad - lon1_rad
1531
1532        # Haversine formula
1533        a: float = (
1534            math.sin(dlat / 2) ** 2
1535            + math.cos(lat1_rad) * math.cos(lat2_rad) * math.sin(dlon / 2) ** 2
1536        )
1537        c: float = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
1538
1539        return R * c  # メートル単位での距離
1540
1541    @staticmethod
1542    def _calculate_hotspots_parameters(
1543        df: pd.DataFrame,
1544        window_size: int,
1545        col_ch4_ppm: str = "ch4_ppm",
1546        col_c2h6_ppb: str = "c2h6_ppb",
1547        ch4_threshold: float = 0.05,
1548        c2h6_threshold: float = 0.0,
1549    ) -> pd.DataFrame:
1550        """
1551        ホットスポットのパラメータを計算します。
1552        このメソッドは、指定されたデータフレームに対して移動平均や相関を計算し、
1553        各種のデルタ値や比率を追加します。これにより、ホットスポットの分析に必要な
1554        パラメータを整形します。
1555
1556        Parameters:
1557        ------
1558            df : pd.DataFrame
1559                入力データフレーム
1560            window_size : int
1561                移動窓のサイズ
1562            col_ch4_ppm : str
1563                CH4濃度を示すカラム名
1564            col_c2h6_ppb : str
1565                C2H6濃度を示すカラム名
1566            ch4_threshold : float
1567                CH4の閾値
1568            c2h6_threshold : float
1569                C2H6の閾値
1570
1571        Returns:
1572        ------
1573            pd.DataFrame
1574                計算されたパラメータを含むデータフレーム
1575        """
1576        # 移動平均の計算
1577        df["ch4_ppm_mv"] = (
1578            df[col_ch4_ppm]
1579            .rolling(window=window_size, center=True, min_periods=1)
1580            .mean()
1581        )
1582        df["c2h6_ppb_mv"] = (
1583            df[col_c2h6_ppb]
1584            .rolling(window=window_size, center=True, min_periods=1)
1585            .mean()
1586        )
1587
1588        # 移動相関の計算
1589        df["ch4_c2h6_correlation"] = (
1590            df[col_ch4_ppm]
1591            .rolling(window=window_size, min_periods=1)
1592            .corr(df[col_c2h6_ppb])
1593        )
1594
1595        # 移動平均からの偏差
1596        df["ch4_ppm_delta"] = df[col_ch4_ppm] - df["ch4_ppm_mv"]
1597        df["c2h6_ppb_delta"] = df[col_c2h6_ppb] - df["c2h6_ppb_mv"]
1598
1599        # C2H6/CH4の比率計算
1600        df["c2c1_ratio"] = df[col_c2h6_ppb] / df[col_ch4_ppm]
1601
1602        # デルタ値に基づく比の計算
1603        df["c2c1_ratio_delta"] = np.where(
1604            (df["ch4_ppm_delta"].abs() >= ch4_threshold)
1605            & (df["c2h6_ppb_delta"] >= c2h6_threshold),
1606            df["c2h6_ppb_delta"] / df["ch4_ppm_delta"],
1607            np.nan,
1608        )
1609
1610        return df
1611
1612    @staticmethod
1613    def _calculate_window_size(window_minutes: float) -> int:
1614        """
1615        時間窓からデータポイント数を計算
1616
1617        Parameters:
1618        ------
1619            window_minutes : float
1620                時間窓の大きさ(分)
1621
1622        Returns:
1623        ------
1624            int
1625                データポイント数
1626        """
1627        return int(60 * window_minutes)
1628
1629    @staticmethod
1630    def _initialize_sections(
1631        num_sections: int, section_size: float
1632    ) -> dict[int, tuple[float, float]]:
1633        """
1634        指定された区画数と区画サイズに基づいて、区画の範囲を初期化します。
1635
1636        Parameters:
1637        ------
1638            num_sections : int
1639                初期化する区画の数。
1640            section_size : float
1641                各区画の角度範囲のサイズ。
1642
1643        Returns:
1644        ------
1645            dict[int, tuple[float, float]]
1646                区画番号(0-based-index)とその範囲の辞書。各区画は-180度から180度の範囲に分割されます。
1647        """
1648        sections: dict[int, tuple[float, float]] = {}
1649        for i in range(num_sections):
1650            # -180から180の範囲で区画を設定
1651            start_angle = -180 + i * section_size
1652            end_angle = -180 + (i + 1) * section_size
1653            sections[i] = (start_angle, end_angle)
1654        return sections
1655
1656    @staticmethod
1657    def _is_duplicate_spot(
1658        current_lat: float,
1659        current_lon: float,
1660        current_time: str,
1661        used_positions: list[tuple[float, float, str, float]],
1662        check_time_all: bool,
1663        min_time_threshold_seconds: float,
1664        max_time_threshold_hours: float,
1665        hotspot_area_meter: float,
1666    ) -> bool:
1667        """
1668        与えられた地点が既存の地点と重複しているかを判定します。
1669
1670        Parameters:
1671        ------
1672            current_lat : float
1673                判定する地点の緯度
1674            current_lon : float
1675                判定する地点の経度
1676            current_time : str
1677                判定する地点の時刻
1678            used_positions : list[tuple[float, float, str, float]]
1679                既存の地点情報のリスト (lat, lon, time, value)
1680            check_time_all : bool
1681                時間に関係なく重複チェックを行うかどうか
1682            min_time_threshold_seconds : float
1683                重複とみなす最小時間の閾値(秒)
1684            max_time_threshold_hours : float
1685                重複チェックを一時的に無視する最大時間の閾値(時間)
1686            hotspot_area_meter : float
1687                重複とみなす距離の閾値(m)
1688
1689        Returns:
1690        ------
1691            bool
1692                重複している場合はTrue、そうでない場合はFalse
1693        """
1694        for used_lat, used_lon, used_time, _ in used_positions:
1695            # 距離チェック
1696            distance = MobileSpatialAnalyzer._calculate_distance(
1697                lat1=current_lat, lon1=current_lon, lat2=used_lat, lon2=used_lon
1698            )
1699
1700            if distance < hotspot_area_meter:
1701                # 時間差の計算(秒単位)
1702                time_diff = pd.Timedelta(
1703                    pd.to_datetime(current_time) - pd.to_datetime(used_time)
1704                ).total_seconds()
1705                time_diff_abs = abs(time_diff)
1706
1707                if check_time_all:
1708                    # 時間に関係なく、距離が近ければ重複とみなす
1709                    return True
1710                else:
1711                    # 時間窓による判定を行う
1712                    if time_diff_abs <= min_time_threshold_seconds:
1713                        # Case 1: 最小時間閾値以内は重複とみなす
1714                        return True
1715                    elif time_diff_abs > max_time_threshold_hours * 3600:
1716                        # Case 2: 最大時間閾値を超えた場合は重複チェックをスキップ
1717                        continue
1718                    # Case 3: その間の時間差の場合は、距離が近ければ重複とみなす
1719                    return True
1720
1721        return False
1722
1723    @staticmethod
1724    def _normalize_inputs(
1725        inputs: list[MSAInputConfig] | list[tuple[float, float, str | Path]],
1726    ) -> list[MSAInputConfig]:
1727        """
1728        入力設定を標準化
1729
1730        Parameters:
1731        ------
1732            inputs : list[MSAInputConfig] | list[tuple[float, float, str | Path]]
1733                入力設定のリスト
1734
1735        Returns:
1736        ------
1737            list[MSAInputConfig]
1738                標準化された入力設定のリスト
1739        """
1740        normalized: list[MSAInputConfig] = []
1741        for inp in inputs:
1742            if isinstance(inp, MSAInputConfig):
1743                normalized.append(inp)  # すでに検証済みのため、そのまま追加
1744            else:
1745                fs, lag, path = inp
1746                normalized.append(
1747                    MSAInputConfig.validate_and_create(fs=fs, lag=lag, path=path)
1748                )
1749        return normalized
1750
1751    def remove_c2c1_ratio_duplicates(
1752        self,
1753        df: pd.DataFrame,
1754        min_time_threshold_seconds: float = 300,  # 5分以内は重複とみなす
1755        max_time_threshold_hours: float = 12.0,  # 12時間以上離れている場合は別のポイントとして扱う
1756        check_time_all: bool = True,  # 時間閾値を超えた場合の重複チェックを継続するかどうか
1757        hotspot_area_meter: float = 50.0,  # 重複とみなす距離の閾値(メートル)
1758        col_ch4_ppm: str = "ch4_ppm",
1759        col_ch4_ppm_mv: str = "ch4_ppm_mv",
1760        col_ch4_ppm_delta: str = "ch4_ppm_delta",
1761    ):
1762        """
1763        メタン濃度の増加が閾値を超えた地点から、重複を除外してユニークなホットスポットを抽出する関数。
1764
1765        Parameters:
1766        ------
1767            df : pandas.DataFrame
1768                入力データフレーム。必須カラム:
1769                - ch4_ppm: メタン濃度(ppm)
1770                - ch4_ppm_mv: メタン濃度の移動平均(ppm)
1771                - ch4_ppm_delta: メタン濃度の増加量(ppm)
1772                - latitude: 緯度
1773                - longitude: 経度
1774            min_time_threshold_seconds : float, optional
1775                重複とみなす最小時間差(秒)。デフォルトは300秒(5分)。
1776            max_time_threshold_hours : float, optional
1777                別ポイントとして扱う最大時間差(時間)。デフォルトは12時間。
1778            check_time_all : bool, optional
1779                時間閾値を超えた場合の重複チェックを継続するかどうか。デフォルトはTrue。
1780            hotspot_area_meter : float, optional
1781                重複とみなす距離の閾値(メートル)。デフォルトは50メートル。
1782
1783        Returns:
1784        ------
1785            pandas.DataFrame
1786                ユニークなホットスポットのデータフレーム。
1787        """
1788        df_data: pd.DataFrame = df.copy()
1789        # メタン濃度の増加が閾値を超えた点を抽出
1790        mask = (
1791            df_data[col_ch4_ppm] - df_data[col_ch4_ppm_mv] > self._ch4_enhance_threshold
1792        )
1793        hotspot_candidates = df_data[mask].copy()
1794
1795        # ΔCH4の降順でソート
1796        sorted_hotspots = hotspot_candidates.sort_values(
1797            by=col_ch4_ppm_delta, ascending=False
1798        )
1799        used_positions = []
1800        unique_hotspots = pd.DataFrame()
1801
1802        for _, spot in sorted_hotspots.iterrows():
1803            should_add = True
1804            for used_lat, used_lon, used_time in used_positions:
1805                # 距離チェック
1806                distance = geodesic(
1807                    (spot.latitude, spot.longitude), (used_lat, used_lon)
1808                ).meters
1809
1810                if distance < hotspot_area_meter:
1811                    # 時間差の計算(秒単位)
1812                    time_diff = pd.Timedelta(
1813                        spot.name - pd.to_datetime(used_time)
1814                    ).total_seconds()
1815                    time_diff_abs = abs(time_diff)
1816
1817                    # 時間差に基づく判定
1818                    if check_time_all:
1819                        # 時間に関係なく、距離が近ければ重複とみなす
1820                        # ΔCH4が大きい方を残す(現在のスポットは必ず小さい)
1821                        should_add = False
1822                        break
1823                    else:
1824                        # 時間窓による判定を行う
1825                        if time_diff_abs <= min_time_threshold_seconds:
1826                            # Case 1: 最小時間閾値以内は重複とみなす
1827                            should_add = False
1828                            break
1829                        elif time_diff_abs > max_time_threshold_hours * 3600:
1830                            # Case 2: 最大時間閾値を超えた場合は重複チェックをスキップ
1831                            continue
1832                        # Case 3: その間の時間差の場合は、距離が近ければ重複とみなす
1833                        should_add = False
1834                        break
1835
1836            if should_add:
1837                unique_hotspots = pd.concat([unique_hotspots, pd.DataFrame([spot])])
1838                used_positions.append((spot.latitude, spot.longitude, spot.name))
1839
1840        return unique_hotspots
1841
1842    @staticmethod
1843    def remove_hotspots_duplicates(
1844        hotspots: list[HotspotData],
1845        check_time_all: bool,
1846        min_time_threshold_seconds: float = 300,
1847        max_time_threshold_hours: float = 12,
1848        hotspot_area_meter: float = 50,
1849    ) -> list[HotspotData]:
1850        """
1851        重複するホットスポットを除外します。
1852
1853        このメソッドは、与えられたホットスポットのリストから重複を検出し、
1854        一意のホットスポットのみを返します。重複の判定は、指定された
1855        時間および距離の閾値に基づいて行われます。
1856
1857        Parameters:
1858        ------
1859            hotspots : list[HotspotData]
1860                重複を除外する対象のホットスポットのリスト。
1861            check_time_all : bool
1862                時間に関係なく重複チェックを行うかどうか。
1863            min_time_threshold_seconds : float
1864                重複とみなす最小時間の閾値(秒)。
1865            max_time_threshold_hours : float
1866                重複チェックを一時的に無視する最大時間の閾値(時間)。
1867            hotspot_area_meter : float
1868                重複とみなす距離の閾値(メートル)。
1869
1870        Returns:
1871        ------
1872            list[HotspotData]
1873                重複を除去したホットスポットのリスト。
1874        """
1875        # ΔCH4の降順でソート
1876        sorted_hotspots: list[HotspotData] = sorted(
1877            hotspots, key=lambda x: x.delta_ch4, reverse=True
1878        )
1879        used_positions_by_type: dict[
1880            HotspotType, list[tuple[float, float, str, float]]
1881        ] = {
1882            "bio": [],
1883            "gas": [],
1884            "comb": [],
1885        }
1886        unique_hotspots: list[HotspotData] = []
1887
1888        for spot in sorted_hotspots:
1889            is_duplicate = MobileSpatialAnalyzer._is_duplicate_spot(
1890                current_lat=spot.avg_lat,
1891                current_lon=spot.avg_lon,
1892                current_time=spot.source,
1893                used_positions=used_positions_by_type[spot.type],
1894                check_time_all=check_time_all,
1895                min_time_threshold_seconds=min_time_threshold_seconds,
1896                max_time_threshold_hours=max_time_threshold_hours,
1897                hotspot_area_meter=hotspot_area_meter,
1898            )
1899
1900            if not is_duplicate:
1901                unique_hotspots.append(spot)
1902                used_positions_by_type[spot.type].append(
1903                    (spot.avg_lat, spot.avg_lon, spot.source, spot.delta_ch4)
1904                )
1905
1906        return unique_hotspots
1907
1908    @staticmethod
1909    def setup_logger(logger: Logger | None, log_level: int = INFO) -> Logger:
1910        """
1911        ロガーを設定します。
1912
1913        このメソッドは、ロギングの設定を行い、ログメッセージのフォーマットを指定します。
1914        ログメッセージには、日付、ログレベル、メッセージが含まれます。
1915
1916        渡されたロガーがNoneまたは不正な場合は、新たにロガーを作成し、標準出力に
1917        ログメッセージが表示されるようにStreamHandlerを追加します。ロガーのレベルは
1918        引数で指定されたlog_levelに基づいて設定されます。
1919
1920        Parameters:
1921        ------
1922            logger : Logger | None
1923                使用するロガー。Noneの場合は新しいロガーを作成します。
1924            log_level : int
1925                ロガーのログレベル。デフォルトはINFO。
1926
1927        Returns:
1928        ------
1929            Logger
1930                設定されたロガーオブジェクト。
1931        """
1932        if logger is not None and isinstance(logger, Logger):
1933            return logger
1934        # 渡されたロガーがNoneまたは正しいものでない場合は独自に設定
1935        new_logger: Logger = getLogger()
1936        # 既存のハンドラーをすべて削除
1937        for handler in new_logger.handlers[:]:
1938            new_logger.removeHandler(handler)
1939        new_logger.setLevel(log_level)  # ロガーのレベルを設定
1940        ch = StreamHandler()
1941        ch_formatter = Formatter("%(asctime)s - %(levelname)s - %(message)s")
1942        ch.setFormatter(ch_formatter)  # フォーマッターをハンドラーに設定
1943        new_logger.addHandler(ch)  # StreamHandlerの追加
1944        return new_logger
1945
1946    @staticmethod
1947    def calculate_emission_rates(
1948        hotspots: list[HotspotData],
1949        method: Literal["weller", "weitzel", "joo", "umezawa"] = "weller",
1950        print_summary: bool = True,
1951        custom_formulas: dict[str, dict[str, float]] | None = None,
1952    ) -> tuple[list[EmissionData], dict[str, dict[str, float]]]:
1953        """
1954        検出されたホットスポットのCH4漏出量を計算・解析し、統計情報を生成します。
1955
1956        Parameters:
1957        ------
1958            hotspots : list[HotspotData]
1959                分析対象のホットスポットのリスト
1960            method : Literal["weller", "weitzel", "joo", "umezawa"]
1961                使用する計算式。デフォルトは"weller"。
1962            print_summary : bool
1963                統計情報を表示するかどうか。デフォルトはTrue。
1964            custom_formulas : dict[str, dict[str, float]] | None
1965                カスタム計算式の係数。
1966                例: {"custom_method": {"a": 1.0, "b": 1.0}}
1967                Noneの場合はデフォルトの計算式を使用。
1968
1969        Returns:
1970        ------
1971            tuple[list[EmissionData], dict[str, dict[str, float]]]
1972                - 各ホットスポットの排出量データを含むリスト
1973                - タイプ別の統計情報を含む辞書
1974        """
1975        # デフォルトの経験式係数
1976        default_formulas = {
1977            "weller": {"a": 0.988, "b": 0.817},
1978            "weitzel": {"a": 0.521, "b": 0.795},
1979            "joo": {"a": 2.738, "b": 1.329},
1980            "umezawa": {"a": 2.716, "b": 0.741},
1981        }
1982
1983        # カスタム計算式がある場合は追加
1984        emission_formulas = default_formulas.copy()
1985        if custom_formulas:
1986            emission_formulas.update(custom_formulas)
1987
1988        if method not in emission_formulas:
1989            raise ValueError(f"Unknown method: {method}")
1990
1991        # 係数の取得
1992        a = emission_formulas[method]["a"]
1993        b = emission_formulas[method]["b"]
1994
1995        # 排出量の計算
1996        emission_data_list = []
1997        for spot in hotspots:
1998            # 漏出量の計算 (L/min)
1999            emission_rate = np.exp((np.log(spot.delta_ch4) + a) / b)
2000            # 日排出量 (L/day)
2001            daily_emission = emission_rate * 60 * 24
2002            # 年間排出量 (L/year)
2003            annual_emission = daily_emission * 365
2004
2005            emission_data = EmissionData(
2006                source=spot.source,
2007                type=spot.type,
2008                section=spot.section,
2009                latitude=spot.avg_lat,
2010                longitude=spot.avg_lon,
2011                delta_ch4=spot.delta_ch4,
2012                delta_c2h6=spot.delta_c2h6,
2013                ratio=spot.ratio,
2014                emission_rate=emission_rate,
2015                daily_emission=daily_emission,
2016                annual_emission=annual_emission,
2017            )
2018            emission_data_list.append(emission_data)
2019
2020        # 統計計算用にDataFrameを作成
2021        emission_df = pd.DataFrame([e.to_dict() for e in emission_data_list])
2022
2023        # タイプ別の統計情報を計算
2024        stats = {}
2025        # emission_formulas の定義の後に、排出量カテゴリーの閾値を定義
2026        emission_categories = {
2027            "low": {"min": 0, "max": 6},  # < 6 L/min
2028            "medium": {"min": 6, "max": 40},  # 6-40 L/min
2029            "high": {"min": 40, "max": float("inf")},  # > 40 L/min
2030        }
2031        # get_args(HotspotType)を使用して型安全なリストを作成
2032        types = list(get_args(HotspotType))
2033        for spot_type in types:
2034            df_type = emission_df[emission_df["type"] == spot_type]
2035            if len(df_type) > 0:
2036                # 既存の統計情報を計算
2037                type_stats = {
2038                    "count": len(df_type),
2039                    "emission_rate_min": df_type["emission_rate"].min(),
2040                    "emission_rate_max": df_type["emission_rate"].max(),
2041                    "emission_rate_mean": df_type["emission_rate"].mean(),
2042                    "emission_rate_median": df_type["emission_rate"].median(),
2043                    "total_annual_emission": df_type["annual_emission"].sum(),
2044                    "mean_annual_emission": df_type["annual_emission"].mean(),
2045                }
2046
2047                # 排出量カテゴリー別の統計を追加
2048                category_counts = {
2049                    "low": len(
2050                        df_type[
2051                            df_type["emission_rate"] < emission_categories["low"]["max"]
2052                        ]
2053                    ),
2054                    "medium": len(
2055                        df_type[
2056                            (
2057                                df_type["emission_rate"]
2058                                >= emission_categories["medium"]["min"]
2059                            )
2060                            & (
2061                                df_type["emission_rate"]
2062                                < emission_categories["medium"]["max"]
2063                            )
2064                        ]
2065                    ),
2066                    "high": len(
2067                        df_type[
2068                            df_type["emission_rate"]
2069                            >= emission_categories["high"]["min"]
2070                        ]
2071                    ),
2072                }
2073                type_stats["emission_categories"] = category_counts
2074
2075                stats[spot_type] = type_stats
2076
2077                if print_summary:
2078                    print(f"\n{spot_type}タイプの統計情報:")
2079                    print(f"  検出数: {type_stats['count']}")
2080                    print("  排出量 (L/min):")
2081                    print(f"    最小値: {type_stats['emission_rate_min']:.2f}")
2082                    print(f"    最大値: {type_stats['emission_rate_max']:.2f}")
2083                    print(f"    平均値: {type_stats['emission_rate_mean']:.2f}")
2084                    print(f"    中央値: {type_stats['emission_rate_median']:.2f}")
2085                    print("  排出量カテゴリー別の検出数:")
2086                    print(f"    低放出 (< 6 L/min): {category_counts['low']}")
2087                    print(f"    中放出 (6-40 L/min): {category_counts['medium']}")
2088                    print(f"    高放出 (> 40 L/min): {category_counts['high']}")
2089                    print("  年間排出量 (L/year):")
2090                    print(f"    合計: {type_stats['total_annual_emission']:.2f}")
2091                    print(f"    平均: {type_stats['mean_annual_emission']:.2f}")
2092
2093        return emission_data_list, stats
2094
2095    @staticmethod
2096    def plot_emission_analysis(
2097        emission_data_list: list[EmissionData],
2098        dpi: int = 300,
2099        output_dir: str | Path | None = None,
2100        output_filename: str = "emission_analysis.png",
2101        figsize: tuple[float, float] = (12, 5),
2102        add_legend: bool = True,
2103        hist_log_y: bool = False,
2104        hist_xlim: tuple[float, float] | None = None,
2105        hist_ylim: tuple[float, float] | None = None,
2106        scatter_xlim: tuple[float, float] | None = None,
2107        scatter_ylim: tuple[float, float] | None = None,
2108        hist_bin_width: float = 0.5,
2109        print_summary: bool = True,
2110        save_fig: bool = False,
2111        show_fig: bool = True,
2112        show_scatter: bool = True,  # 散布図の表示を制御するオプションを追加
2113    ) -> None:
2114        """
2115        排出量分析のプロットを作成する静的メソッド。
2116
2117        Parameters:
2118        ------
2119            emission_data_list : list[EmissionData]
2120                EmissionDataオブジェクトのリスト。
2121            output_dir : str | Path | None
2122                出力先ディレクトリのパス。
2123            output_filename : str
2124                保存するファイル名。デフォルトは"emission_analysis.png"。
2125            dpi : int
2126                プロットの解像度。デフォルトは300。
2127            figsize : tuple[float, float]
2128                プロットのサイズ。デフォルトは(12, 5)。
2129            add_legend : bool
2130                凡例を追加するかどうか。デフォルトはTrue。
2131            hist_log_y : bool
2132                ヒストグラムのy軸を対数スケールにするかどうか。デフォルトはFalse。
2133            hist_xlim : tuple[float, float] | None
2134                ヒストグラムのx軸の範囲。デフォルトはNone。
2135            hist_ylim : tuple[float, float] | None
2136                ヒストグラムのy軸の範囲。デフォルトはNone。
2137            scatter_xlim : tuple[float, float] | None
2138                散布図のx軸の範囲。デフォルトはNone。
2139            scatter_ylim : tuple[float, float] | None
2140                散布図のy軸の範囲。デフォルトはNone。
2141            hist_bin_width : float
2142                ヒストグラムのビンの幅。デフォルトは0.5。
2143            print_summary : bool
2144                集計結果を表示するかどうか。デフォルトはFalse。
2145            save_fig : bool
2146                図をファイルに保存するかどうか。デフォルトはFalse。
2147            show_fig : bool
2148                図を表示するかどうか。デフォルトはTrue。
2149            show_scatter : bool
2150                散布図(右図)を表示するかどうか。デフォルトはTrue。
2151        """
2152        # データをDataFrameに変換
2153        df = pd.DataFrame([e.to_dict() for e in emission_data_list])
2154
2155        # プロットの作成(散布図の有無に応じてサブプロット数を調整)
2156        if show_scatter:
2157            fig, (ax1, ax2) = plt.subplots(1, 2, figsize=figsize)
2158            axes = [ax1, ax2]
2159        else:
2160            fig, ax1 = plt.subplots(1, 1, figsize=(figsize[0] // 2, figsize[1]))
2161            axes = [ax1]
2162
2163        # カラーマップの定義
2164        colors: dict[HotspotType, str] = {"bio": "blue", "gas": "red", "comb": "green"}
2165
2166        # 存在するタイプを確認
2167        # HotspotTypeの定義順を基準にソート
2168        hotspot_types = list(get_args(HotspotType))
2169        existing_types = sorted(
2170            df["type"].unique(), key=lambda x: hotspot_types.index(x)
2171        )
2172
2173        # 左側: ヒストグラム
2174        # ビンの範囲を設定
2175        start = 0  # 必ず0から開始
2176        if hist_xlim is not None:
2177            end = hist_xlim[1]
2178        else:
2179            end = np.ceil(df["emission_rate"].max() * 1.05)
2180
2181        # ビン数を計算(end値をbin_widthで割り切れるように調整)
2182        n_bins = int(np.ceil(end / hist_bin_width))
2183        end = n_bins * hist_bin_width
2184
2185        # ビンの生成(0から開始し、bin_widthの倍数で区切る)
2186        bins = np.linspace(start, end, n_bins + 1)
2187
2188        # タイプごとにヒストグラムを積み上げ
2189        bottom = np.zeros(len(bins) - 1)
2190        for spot_type in existing_types:
2191            data = df[df["type"] == spot_type]["emission_rate"]
2192            if len(data) > 0:
2193                counts, _ = np.histogram(data, bins=bins)
2194                ax1.bar(
2195                    bins[:-1],
2196                    counts,
2197                    width=hist_bin_width,
2198                    bottom=bottom,
2199                    alpha=0.6,
2200                    label=spot_type,
2201                    color=colors[spot_type],
2202                )
2203                bottom += counts
2204
2205        ax1.set_xlabel("CH$_4$ Emission (L min$^{-1}$)")
2206        ax1.set_ylabel("Frequency")
2207        if hist_log_y:
2208            # ax1.set_yscale("log")
2209            # 非線形スケールを設定(linthreshで線形から対数への遷移点を指定)
2210            ax1.set_yscale("symlog", linthresh=1.0)
2211        if hist_xlim is not None:
2212            ax1.set_xlim(hist_xlim)
2213        else:
2214            ax1.set_xlim(0, np.ceil(df["emission_rate"].max() * 1.05))
2215
2216        if hist_ylim is not None:
2217            ax1.set_ylim(hist_ylim)
2218        else:
2219            ax1.set_ylim(0, ax1.get_ylim()[1])  # 下限を0に設定
2220
2221        if show_scatter:
2222            # 右側: 散布図
2223            for spot_type in existing_types:
2224                mask = df["type"] == spot_type
2225                ax2.scatter(
2226                    df[mask]["emission_rate"],
2227                    df[mask]["delta_ch4"],
2228                    alpha=0.6,
2229                    label=spot_type,
2230                    color=colors[spot_type],
2231                )
2232
2233            ax2.set_xlabel("Emission Rate (L min$^{-1}$)")
2234            ax2.set_ylabel("ΔCH$_4$ (ppm)")
2235            if scatter_xlim is not None:
2236                ax2.set_xlim(scatter_xlim)
2237            else:
2238                ax2.set_xlim(0, np.ceil(df["emission_rate"].max() * 1.05))
2239
2240            if scatter_ylim is not None:
2241                ax2.set_ylim(scatter_ylim)
2242            else:
2243                ax2.set_ylim(0, np.ceil(df["delta_ch4"].max() * 1.05))
2244
2245        # 凡例の表示
2246        if add_legend:
2247            for ax in axes:
2248                ax.legend(
2249                    bbox_to_anchor=(0.5, -0.30),
2250                    loc="upper center",
2251                    ncol=len(existing_types),
2252                )
2253
2254        plt.tight_layout()
2255
2256        # 図の保存
2257        if save_fig:
2258            if output_dir is None:
2259                raise ValueError(
2260                    "save_fig=Trueの場合、output_dirを指定する必要があります。有効なディレクトリパスを指定してください。"
2261                )
2262            os.makedirs(output_dir, exist_ok=True)
2263            output_path = os.path.join(output_dir, output_filename)
2264            plt.savefig(output_path, bbox_inches="tight", dpi=dpi)
2265        # 図の表示
2266        if show_fig:
2267            plt.show()
2268        else:
2269            plt.close(fig=fig)
2270
2271        if print_summary:
2272            # デバッグ用の出力
2273            print("\nビンごとの集計:")
2274            print(f"{'Range':>12} | {'bio':>8} | {'gas':>8} | {'total':>8}")
2275            print("-" * 50)
2276
2277            for i in range(len(bins) - 1):
2278                bin_start = bins[i]
2279                bin_end = bins[i + 1]
2280
2281                # 各タイプのカウントを計算
2282                counts_by_type: dict[HotspotType, int] = {"bio": 0, "gas": 0, "comb": 0}
2283                total = 0
2284                for spot_type in existing_types:
2285                    mask = (
2286                        (df["type"] == spot_type)
2287                        & (df["emission_rate"] >= bin_start)
2288                        & (df["emission_rate"] < bin_end)
2289                    )
2290                    count = len(df[mask])
2291                    counts_by_type[spot_type] = count
2292                    total += count
2293
2294                # カウントが0の場合はスキップ
2295                if total > 0:
2296                    range_str = f"{bin_start:5.1f}-{bin_end:<5.1f}"
2297                    bio_count = counts_by_type.get("bio", 0)
2298                    gas_count = counts_by_type.get("gas", 0)
2299                    print(
2300                        f"{range_str:>12} | {bio_count:8d} | {gas_count:8d} | {total:8d}"
2301                    )

移動観測で得られた測定データを解析するクラス

MobileSpatialAnalyzer( center_lat: float, center_lon: float, inputs: list[MSAInputConfig] | list[tuple[float, float, str | pathlib.Path]], num_sections: int = 4, ch4_enhance_threshold: float = 0.1, correlation_threshold: float = 0.7, hotspot_area_meter: float = 50, window_minutes: float = 5, column_mapping: dict[str, str] = {'Time Stamp': 'timestamp', 'CH4 (ppm)': 'ch4_ppm', 'C2H6 (ppb)': 'c2h6_ppb', 'H2O (ppm)': 'h2o_ppm', 'Latitude': 'latitude', 'Longitude': 'longitude'}, logger: logging.Logger | None = None, logging_debug: bool = False)
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
    初期化処理が完了したことを示します。
EARTH_RADIUS_METERS: float = 6371000
logger: logging.Logger
def analyze_delta_ch4_stats( self, hotspots: list[HotspotData]) -> 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
    統計情報の表示が完了したことを示します。
def analyze_hotspots( self, duplicate_check_mode: str = 'none', min_time_threshold_seconds: float = 300, max_time_threshold_hours: float = 12) -> list[HotspotData]:
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]
    検出されたホットスポットのリスト。
def calculate_measurement_stats( self, print_individual_stats: bool = True, print_total_stats: bool = True) -> tuple[float, datetime.timedelta]:
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)と総時間のタプル
def create_hotspots_map( self, hotspots: list[HotspotData], output_dir: str | pathlib.Path | None = None, output_filename: str = 'hotspots_map.html', center_marker_label: str = 'Center', plot_center_marker: bool = True, radius_meters: float = 3000, save_fig: bool = True) -> None:
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。
def export_hotspots_to_csv( self, hotspots: list[HotspotData], output_dir: str | pathlib.Path | None = None, output_filename: str = 'hotspots.csv') -> None:
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
    出力ファイル名
def get_preprocessed_data(self) -> pandas.core.frame.DataFrame:
715    def get_preprocessed_data(
716        self,
717    ) -> pd.DataFrame:
718        """
719        データ前処理を行い、CH4とC2H6の相関解析に必要な形式に整えます。
720        コンストラクタで読み込んだすべてのデータを前処理し、結合したDataFrameを返します。
721
722        Returns:
723        ------
724            pd.DataFrame
725                前処理済みの結合されたDataFrame
726        """
727        processed_dfs: list[pd.DataFrame] = []
728
729        # 各データソースに対して解析を実行
730        for source_name, df in self._data.items():
731            # パラメータの計算
732            processed_df = MobileSpatialAnalyzer._calculate_hotspots_parameters(
733                df, self._window_size
734            )
735            # ソース名を列として追加
736            processed_df["source"] = source_name
737            processed_dfs.append(processed_df)
738
739        # すべてのDataFrameを結合
740        if not processed_dfs:
741            raise ValueError("処理対象のデータが存在しません。")
742
743        combined_df = pd.concat(processed_dfs, axis=0)
744        return combined_df

データ前処理を行い、CH4とC2H6の相関解析に必要な形式に整えます。 コンストラクタで読み込んだすべてのデータを前処理し、結合したDataFrameを返します。

Returns:

pd.DataFrame
    前処理済みの結合されたDataFrame
def get_section_size(self) -> float:
746    def get_section_size(self) -> float:
747        """
748        セクションのサイズを取得するメソッド。
749        このメソッドは、解析対象のデータを区画に分割する際の
750        各区画の角度範囲を示すサイズを返します。
751
752        Returns:
753        ------
754            float
755                1セクションのサイズ(度単位)
756        """
757        return self._section_size

セクションのサイズを取得するメソッド。 このメソッドは、解析対象のデータを区画に分割する際の 各区画の角度範囲を示すサイズを返します。

Returns:

float
    1セクションのサイズ(度単位)
def plot_ch4_delta_histogram( self, hotspots: list[HotspotData], output_dir: str | pathlib.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 = None, ylim: tuple[float, float] | None = None, save_fig: bool = True, show_fig: bool = True, yscale_log: bool = True, print_bins_analysis: bool = False) -> None:
759    def plot_ch4_delta_histogram(
760        self,
761        hotspots: list[HotspotData],
762        output_dir: str | Path | None,
763        output_filename: str = "ch4_delta_histogram.png",
764        dpi: int = 200,
765        figsize: tuple[int, int] = (8, 6),
766        fontsize: float = 20,
767        xlim: tuple[float, float] | None = None,
768        ylim: tuple[float, float] | None = None,
769        save_fig: bool = True,
770        show_fig: bool = True,
771        yscale_log: bool = True,
772        print_bins_analysis: bool = False,
773    ) -> None:
774        """
775        CH4の増加量(ΔCH4)の積み上げヒストグラムをプロットします。
776
777        Parameters:
778        ------
779            hotspots : list[HotspotData]
780                プロットするホットスポットのリスト
781            output_dir : str | Path | None
782                保存先のディレクトリパス
783            output_filename : str
784                保存するファイル名。デフォルトは"ch4_delta_histogram.png"。
785            dpi : int
786                解像度。デフォルトは200。
787            figsize : tuple[int, int]
788                図のサイズ。デフォルトは(8, 6)。
789            fontsize : float
790                フォントサイズ。デフォルトは20。
791            xlim : tuple[float, float] | None
792                x軸の範囲。Noneの場合は自動設定。
793            ylim : tuple[float, float] | None
794                y軸の範囲。Noneの場合は自動設定。
795            save_fig : bool
796                図の保存を許可するフラグ。デフォルトはTrue。
797            show_fig : bool
798                図の表示を許可するフラグ。デフォルトはTrue。
799            yscale_log : bool
800                y軸をlogにするかどうか。デフォルトはTrue。
801            print_bins_analysis : bool
802                ビンごとの内訳を表示するオプション。
803        """
804        plt.rcParams["font.size"] = fontsize
805        fig = plt.figure(figsize=figsize, dpi=dpi)
806
807        # ホットスポットからデータを抽出
808        all_ch4_deltas = []
809        all_types = []
810        for spot in hotspots:
811            all_ch4_deltas.append(spot.delta_ch4)
812            all_types.append(spot.type)
813
814        # データをNumPy配列に変換
815        all_ch4_deltas = np.array(all_ch4_deltas)
816        all_types = np.array(all_types)
817
818        # 0.1刻みのビンを作成
819        if xlim is not None:
820            bins = np.arange(xlim[0], xlim[1] + 0.1, 0.1)
821        else:
822            max_val = np.ceil(np.max(all_ch4_deltas) * 10) / 10
823            bins = np.arange(0, max_val + 0.1, 0.1)
824
825        # タイプごとのヒストグラムデータを計算
826        hist_data = {}
827        # HotspotTypeのリテラル値を使用してイテレーション
828        for type_name in get_args(HotspotType):  # typing.get_argsをインポート
829            mask = all_types == type_name
830            if np.any(mask):
831                counts, _ = np.histogram(all_ch4_deltas[mask], bins=bins)
832                hist_data[type_name] = counts
833
834        # ビンごとの内訳を表示
835        if print_bins_analysis:
836            self.logger.info("各ビンの内訳:")
837            print(f"{'Bin Range':15} {'bio':>8} {'gas':>8} {'comb':>8} {'Total':>8}")
838            print("-" * 50)
839
840            for i in range(len(bins) - 1):
841                bin_start = bins[i]
842                bin_end = bins[i + 1]
843                bio_count = hist_data.get("bio", np.zeros(len(bins) - 1))[i]
844                gas_count = hist_data.get("gas", np.zeros(len(bins) - 1))[i]
845                comb_count = hist_data.get("comb", np.zeros(len(bins) - 1))[i]
846                total = bio_count + gas_count + comb_count
847
848                if total > 0:  # 合計が0のビンは表示しない
849                    print(
850                        f"{bin_start:4.1f}-{bin_end:<8.1f}"
851                        f"{int(bio_count):8d}"
852                        f"{int(gas_count):8d}"
853                        f"{int(comb_count):8d}"
854                        f"{int(total):8d}"
855                    )
856
857        # 積み上げヒストグラムを作成
858        bottom = np.zeros_like(hist_data.get("bio", np.zeros(len(bins) - 1)))
859
860        # 色の定義をHotspotTypeを使用して型安全に定義
861        colors: dict[HotspotType, str] = {"bio": "blue", "gas": "red", "comb": "green"}
862
863        # HotspotTypeのリテラル値を使用してイテレーション
864        for type_name in get_args(HotspotType):
865            if type_name in hist_data:
866                plt.bar(
867                    bins[:-1],
868                    hist_data[type_name],
869                    width=np.diff(bins)[0],
870                    bottom=bottom,
871                    color=colors[type_name],
872                    label=type_name,
873                    alpha=0.6,
874                    align="edge",
875                )
876                bottom += hist_data[type_name]
877
878        if yscale_log:
879            plt.yscale("log")
880        plt.xlabel("Δ$\\mathregular{CH_{4}}$ (ppm)")
881        plt.ylabel("Frequency")
882        plt.legend()
883        plt.grid(True, which="both", ls="-", alpha=0.2)
884
885        # 軸の範囲を設定
886        if xlim is not None:
887            plt.xlim(xlim)
888        if ylim is not None:
889            plt.ylim(ylim)
890
891        # グラフの保存または表示
892        if save_fig:
893            if output_dir is None:
894                raise ValueError(
895                    "save_fig=Trueの場合、output_dirを指定する必要があります。有効なディレクトリパスを指定してください。"
896                )
897            os.makedirs(output_dir, exist_ok=True)
898            output_path: str = os.path.join(output_dir, output_filename)
899            plt.savefig(output_path, bbox_inches="tight")
900            self.logger.info(f"ヒストグラムを保存しました: {output_path}")
901        if show_fig:
902            plt.show()
903        else:
904            plt.close(fig=fig)

CH4の増加量(ΔCH4)の積み上げヒストグラムをプロットします。

Parameters:

hotspots : list[HotspotData]
    プロットするホットスポットのリスト
output_dir : str | Path | None
    保存先のディレクトリパス
output_filename : str
    保存するファイル名。デフォルトは"ch4_delta_histogram.png"。
dpi : int
    解像度。デフォルトは200。
figsize : tuple[int, int]
    図のサイズ。デフォルトは(8, 6)。
fontsize : float
    フォントサイズ。デフォルトは20。
xlim : tuple[float, float] | None
    x軸の範囲。Noneの場合は自動設定。
ylim : tuple[float, float] | None
    y軸の範囲。Noneの場合は自動設定。
save_fig : bool
    図の保存を許可するフラグ。デフォルトはTrue。
show_fig : bool
    図の表示を許可するフラグ。デフォルトはTrue。
yscale_log : bool
    y軸をlogにするかどうか。デフォルトはTrue。
print_bins_analysis : bool
    ビンごとの内訳を表示するオプション。
def plot_mapbox( self, df: pandas.core.frame.DataFrame, col: str, mapbox_access_token: str, sort_value_column: bool = True, output_dir: str | pathlib.Path | None = None, output_filename: str = 'mapbox_plot.html', lat_column: str = 'latitude', lon_column: str = 'longitude', colorscale: str = 'Jet', center_lat: float | None = None, center_lon: float | None = None, 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 = None, value_range: tuple[float, float] | None = None, save_fig: bool = True, show_fig: bool = True) -> None:
 906    def plot_mapbox(
 907        self,
 908        df: pd.DataFrame,
 909        col: str,
 910        mapbox_access_token: str,
 911        sort_value_column: bool = True,
 912        output_dir: str | Path | None = None,
 913        output_filename: str = "mapbox_plot.html",
 914        lat_column: str = "latitude",
 915        lon_column: str = "longitude",
 916        colorscale: str = "Jet",
 917        center_lat: float | None = None,
 918        center_lon: float | None = None,
 919        zoom: float = 12,
 920        width: int = 700,
 921        height: int = 700,
 922        tick_font_family: str = "Arial",
 923        title_font_family: str = "Arial",
 924        tick_font_size: int = 12,
 925        title_font_size: int = 14,
 926        marker_size: int = 4,
 927        colorbar_title: str | None = None,
 928        value_range: tuple[float, float] | None = None,
 929        save_fig: bool = True,
 930        show_fig: bool = True,
 931    ) -> None:
 932        """
 933        Plotlyを使用してMapbox上にデータをプロットします。
 934
 935        Parameters:
 936        ------
 937            df : pd.DataFrame
 938                プロットするデータを含むDataFrame
 939            col : str
 940                カラーマッピングに使用する列名
 941            mapbox_access_token : str
 942                Mapboxのアクセストークン
 943            sort_value_column : bool
 944                value_columnをソートするか否か。デフォルトはTrue。
 945            output_dir : str | Path | None
 946                出力ディレクトリのパス
 947            output_filename : str
 948                出力ファイル名。デフォルトは"mapbox_plot.html"
 949            lat_column : str
 950                緯度の列名。デフォルトは"latitude"
 951            lon_column : str
 952                経度の列名。デフォルトは"longitude"
 953            colorscale : str
 954                使用するカラースケール。デフォルトは"Jet"
 955            center_lat : float | None
 956                中心緯度。デフォルトはNoneで、self._center_latを使用
 957            center_lon : float | None
 958                中心経度。デフォルトはNoneで、self._center_lonを使用
 959            zoom : float
 960                マップの初期ズームレベル。デフォルトは12
 961            width : int
 962                プロットの幅(ピクセル)。デフォルトは700
 963            height : int
 964                プロットの高さ(ピクセル)。デフォルトは700
 965            tick_font_family : str
 966                カラーバーの目盛りフォントファミリー。デフォルトは"Arial"
 967            title_font_family : str
 968                カラーバーのラベルフォントファミリー。デフォルトは"Arial"
 969            tick_font_size : int
 970                カラーバーの目盛りフォントサイズ。デフォルトは12
 971            title_font_size : int
 972                カラーバーのラベルフォントサイズ。デフォルトは14
 973            marker_size : int
 974                マーカーのサイズ。デフォルトは4
 975            colorbar_title : str | None
 976                カラーバーのラベル
 977            value_range : tuple[float, float] | None
 978                カラーマッピングの範囲。デフォルトはNoneで、データの最小値と最大値を使用
 979            save_fig : bool
 980                図を保存するかどうか。デフォルトはTrue
 981            show_fig : bool
 982                図を表示するかどうか。デフォルトはTrue
 983        """
 984        df_mapping: pd.DataFrame = df.copy().dropna(subset=[col])
 985        if sort_value_column:
 986            df_mapping = df_mapping.sort_values(col)
 987        # 中心座標の設定
 988        center_lat = center_lat if center_lat is not None else self._center_lat
 989        center_lon = center_lon if center_lon is not None else self._center_lon
 990
 991        # カラーマッピングの範囲を設定
 992        cmin, cmax = 0, 0
 993        if value_range is None:
 994            cmin = df_mapping[col].min()
 995            cmax = df_mapping[col].max()
 996        else:
 997            cmin, cmax = value_range
 998
 999        # カラーバーのタイトルを設定
1000        title_text = colorbar_title if colorbar_title is not None else col
1001
1002        # Scattermapboxのデータを作成
1003        scatter_data = go.Scattermapbox(
1004            lat=df_mapping[lat_column],
1005            lon=df_mapping[lon_column],
1006            text=df_mapping[col].astype(str),
1007            hoverinfo="text",
1008            mode="markers",
1009            marker=dict(
1010                color=df_mapping[col],
1011                size=marker_size,
1012                reversescale=False,
1013                autocolorscale=False,
1014                colorscale=colorscale,
1015                cmin=cmin,
1016                cmax=cmax,
1017                colorbar=dict(
1018                    tickformat="3.2f",
1019                    outlinecolor="black",
1020                    outlinewidth=1.5,
1021                    ticks="outside",
1022                    ticklen=7,
1023                    tickwidth=1.5,
1024                    tickcolor="black",
1025                    tickfont=dict(
1026                        family=tick_font_family, color="black", size=tick_font_size
1027                    ),
1028                    title=dict(
1029                        text=title_text, side="top"
1030                    ),  # カラーバーのタイトルを設定
1031                    titlefont=dict(
1032                        family=title_font_family,
1033                        color="black",
1034                        size=title_font_size,
1035                    ),
1036                ),
1037            ),
1038        )
1039
1040        # レイアウトの設定
1041        layout = go.Layout(
1042            width=width,
1043            height=height,
1044            showlegend=False,
1045            mapbox=dict(
1046                accesstoken=mapbox_access_token,
1047                center=dict(lat=center_lat, lon=center_lon),
1048                zoom=zoom,
1049            ),
1050        )
1051
1052        # 図の作成
1053        fig = go.Figure(data=[scatter_data], layout=layout)
1054
1055        # 図の保存
1056        if save_fig:
1057            # 保存時の出力ディレクトリチェック
1058            if output_dir is None:
1059                raise ValueError(
1060                    "save_fig=Trueの場合、output_dirを指定する必要があります。"
1061                )
1062            os.makedirs(output_dir, exist_ok=True)
1063            output_path = os.path.join(output_dir, output_filename)
1064            pyo.plot(fig, filename=output_path, auto_open=False)
1065            self.logger.info(f"Mapboxプロットを保存しました: {output_path}")
1066        # 図の表示
1067        if show_fig:
1068            pyo.iplot(fig)

Plotlyを使用してMapbox上にデータをプロットします。

Parameters:

df : pd.DataFrame
    プロットするデータを含むDataFrame
col : str
    カラーマッピングに使用する列名
mapbox_access_token : str
    Mapboxのアクセストークン
sort_value_column : bool
    value_columnをソートするか否か。デフォルトはTrue。
output_dir : str | Path | None
    出力ディレクトリのパス
output_filename : str
    出力ファイル名。デフォルトは"mapbox_plot.html"
lat_column : str
    緯度の列名。デフォルトは"latitude"
lon_column : str
    経度の列名。デフォルトは"longitude"
colorscale : str
    使用するカラースケール。デフォルトは"Jet"
center_lat : float | None
    中心緯度。デフォルトはNoneで、self._center_latを使用
center_lon : float | None
    中心経度。デフォルトはNoneで、self._center_lonを使用
zoom : float
    マップの初期ズームレベル。デフォルトは12
width : int
    プロットの幅(ピクセル)。デフォルトは700
height : int
    プロットの高さ(ピクセル)。デフォルトは700
tick_font_family : str
    カラーバーの目盛りフォントファミリー。デフォルトは"Arial"
title_font_family : str
    カラーバーのラベルフォントファミリー。デフォルトは"Arial"
tick_font_size : int
    カラーバーの目盛りフォントサイズ。デフォルトは12
title_font_size : int
    カラーバーのラベルフォントサイズ。デフォルトは14
marker_size : int
    マーカーのサイズ。デフォルトは4
colorbar_title : str | None
    カラーバーのラベル
value_range : tuple[float, float] | None
    カラーマッピングの範囲。デフォルトはNoneで、データの最小値と最大値を使用
save_fig : bool
    図を保存するかどうか。デフォルトはTrue
show_fig : bool
    図を表示するかどうか。デフォルトはTrue
def plot_scatter_c2c1( self, hotspots: list[HotspotData], output_dir: str | pathlib.Path | None = 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 = None) -> None:
1070    def plot_scatter_c2c1(
1071        self,
1072        hotspots: list[HotspotData],
1073        output_dir: str | Path | None = None,
1074        output_filename: str = "scatter_c2c1.png",
1075        dpi: int = 200,
1076        figsize: tuple[int, int] = (4, 4),
1077        fontsize: float = 12,
1078        save_fig: bool = True,
1079        show_fig: bool = True,
1080        ratio_labels: dict[float, tuple[float, float, str]] | None = None,
1081    ) -> None:
1082        """
1083        検出されたホットスポットのΔC2H6とΔCH4の散布図をプロットします。
1084
1085        Parameters:
1086        ------
1087            hotspots : list[HotspotData]
1088                プロットするホットスポットのリスト
1089            output_dir : str | Path | None
1090                保存先のディレクトリパス
1091            output_filename : str
1092                保存するファイル名。デフォルトは"scatter_c2c1.png"。
1093            dpi : int
1094                解像度。デフォルトは200。
1095            figsize : tuple[int, int]
1096                図のサイズ。デフォルトは(4, 4)。
1097            fontsize : float
1098                フォントサイズ。デフォルトは12。
1099            save_fig : bool
1100                図の保存を許可するフラグ。デフォルトはTrue。
1101            show_fig : bool
1102                図の表示を許可するフラグ。デフォルトはTrue。
1103            ratio_labels : dict[float, tuple[float, float, str]] | None
1104                比率線とラベルの設定。
1105                キーは比率値、値は (x位置, y位置, ラベルテキスト) のタプル。
1106                Noneの場合はデフォルト設定を使用。デフォルト値:
1107                {
1108                    0.001: (1.25, 2, "0.001"),
1109                    0.005: (1.25, 8, "0.005"),
1110                    0.010: (1.25, 15, "0.01"),
1111                    0.020: (1.25, 30, "0.02"),
1112                    0.030: (1.0, 40, "0.03"),
1113                    0.076: (0.20, 42, "0.076 (Osaka)")
1114                }
1115        """
1116        plt.rcParams["font.size"] = fontsize
1117        fig = plt.figure(figsize=figsize, dpi=dpi)
1118
1119        # タイプごとのデータを収集
1120        type_data: dict[HotspotType, list[tuple[float, float]]] = {
1121            "bio": [],
1122            "gas": [],
1123            "comb": [],
1124        }
1125        for spot in hotspots:
1126            type_data[spot.type].append((spot.delta_ch4, spot.delta_c2h6))
1127
1128        # 色とラベルの定義
1129        colors: dict[HotspotType, str] = {"bio": "blue", "gas": "red", "comb": "green"}
1130        labels: dict[HotspotType, str] = {"bio": "bio", "gas": "gas", "comb": "comb"}
1131
1132        # タイプごとにプロット(データが存在する場合のみ)
1133        for spot_type, data in type_data.items():
1134            if data:  # データが存在する場合のみプロット
1135                ch4_values, c2h6_values = zip(*data)
1136                plt.plot(
1137                    ch4_values,
1138                    c2h6_values,
1139                    "o",
1140                    c=colors[spot_type],
1141                    alpha=0.5,
1142                    ms=2,
1143                    label=labels[spot_type],
1144                )
1145
1146        # デフォルトの比率とラベル設定
1147        default_ratio_labels = {
1148            0.001: (1.25, 2, "0.001"),
1149            0.005: (1.25, 8, "0.005"),
1150            0.010: (1.25, 15, "0.01"),
1151            0.020: (1.25, 30, "0.02"),
1152            0.030: (1.0, 40, "0.03"),
1153            0.076: (0.20, 42, "0.076 (Osaka)"),
1154        }
1155
1156        ratio_labels = ratio_labels or default_ratio_labels
1157
1158        # プロット後、軸の設定前に比率の線を追加
1159        x = np.array([0, 5])
1160        base_ch4 = 0.0
1161        base = 0.0
1162
1163        # 各比率に対して線を引く
1164        for ratio, (x_pos, y_pos, label) in ratio_labels.items():
1165            y = (x - base_ch4) * 1000 * ratio + base
1166            plt.plot(x, y, "-", c="black", alpha=0.5)
1167            plt.text(x_pos, y_pos, label)
1168
1169        plt.ylim(0, 50)
1170        plt.xlim(0, 2.0)
1171        plt.ylabel("Δ$\\mathregular{C_{2}H_{6}}$ (ppb)")
1172        plt.xlabel("Δ$\\mathregular{CH_{4}}$ (ppm)")
1173        plt.legend()
1174
1175        # グラフの保存または表示
1176        if save_fig:
1177            if output_dir is None:
1178                raise ValueError(
1179                    "save_fig=Trueの場合、output_dirを指定する必要があります。有効なディレクトリパスを指定してください。"
1180                )
1181            output_path: str = os.path.join(output_dir, output_filename)
1182            plt.savefig(output_path, bbox_inches="tight")
1183            self.logger.info(f"散布図を保存しました: {output_path}")
1184        if show_fig:
1185            plt.show()
1186        else:
1187            plt.close(fig=fig)

検出されたホットスポットのΔC2H6とΔCH4の散布図をプロットします。

Parameters:

hotspots : list[HotspotData]
    プロットするホットスポットのリスト
output_dir : str | Path | None
    保存先のディレクトリパス
output_filename : str
    保存するファイル名。デフォルトは"scatter_c2c1.png"。
dpi : int
    解像度。デフォルトは200。
figsize : tuple[int, int]
    図のサイズ。デフォルトは(4, 4)。
fontsize : float
    フォントサイズ。デフォルトは12。
save_fig : bool
    図の保存を許可するフラグ。デフォルトはTrue。
show_fig : bool
    図の表示を許可するフラグ。デフォルトはTrue。
ratio_labels : dict[float, tuple[float, float, str]] | None
    比率線とラベルの設定。
    キーは比率値、値は (x位置, y位置, ラベルテキスト) のタプル。
    Noneの場合はデフォルト設定を使用。デフォルト値:
    {
        0.001: (1.25, 2, "0.001"),
        0.005: (1.25, 8, "0.005"),
        0.010: (1.25, 15, "0.01"),
        0.020: (1.25, 30, "0.02"),
        0.030: (1.0, 40, "0.03"),
        0.076: (0.20, 42, "0.076 (Osaka)")
    }
def plot_timeseries( self, dpi: int = 200, source_name: str | None = None, figsize: tuple[float, float] = (8, 4), output_dir: str | pathlib.Path | None = None, output_filename: str = 'timeseries.png', save_fig: bool = False, show_fig: bool = True, col_ch4: str = 'ch4_ppm', col_c2h6: str = 'c2h6_ppb', col_h2o: str = 'h2o_ppm', ylim_ch4: tuple[float, float] | None = None, ylim_c2h6: tuple[float, float] | None = None, ylim_h2o: tuple[float, float] | None = None) -> None:
1189    def plot_timeseries(
1190        self,
1191        dpi: int = 200,
1192        source_name: str | None = None,
1193        figsize: tuple[float, float] = (8, 4),
1194        output_dir: str | Path | None = None,
1195        output_filename: str = "timeseries.png",
1196        save_fig: bool = False,
1197        show_fig: bool = True,
1198        col_ch4: str = "ch4_ppm",
1199        col_c2h6: str = "c2h6_ppb",
1200        col_h2o: str = "h2o_ppm",
1201        ylim_ch4: tuple[float, float] | None = None,
1202        ylim_c2h6: tuple[float, float] | None = None,
1203        ylim_h2o: tuple[float, float] | None = None,
1204    ) -> None:
1205        """
1206        時系列データをプロットします。
1207
1208        Parameters:
1209        ------
1210            dpi : int
1211                図の解像度を指定します。デフォルトは200です。
1212            source_name : str | None
1213                プロットするデータソースの名前。Noneの場合は最初のデータソースを使用します。
1214            figsize : tuple[float, float]
1215                図のサイズを指定します。デフォルトは(8, 4)です。
1216            output_dir : str | Path | None
1217                保存先のディレクトリを指定します。save_fig=Trueの場合は必須です。
1218            output_filename : str
1219                保存するファイル名を指定します。デフォルトは"time_series.png"です。
1220            save_fig : bool
1221                図を保存するかどうかを指定します。デフォルトはFalseです。
1222            show_fig : bool
1223                図を表示するかどうかを指定します。デフォルトはTrueです。
1224            col_ch4 : str
1225                CH4データのキーを指定します。デフォルトは"ch4_ppm"です。
1226            col_c2h6 : str
1227                C2H6データのキーを指定します。デフォルトは"c2h6_ppb"です。
1228            col_h2o : str
1229                H2Oデータのキーを指定します。デフォルトは"h2o_ppm"です。
1230            ylim_ch4 : tuple[float, float] | None
1231                CH4プロットのy軸範囲を指定します。デフォルトはNoneです。
1232            ylim_c2h6 : tuple[float, float] | None
1233                C2H6プロットのy軸範囲を指定します。デフォルトはNoneです。
1234            ylim_h2o : tuple[float, float] | None
1235                H2Oプロットのy軸範囲を指定します。デフォルトはNoneです。
1236        """
1237        dfs_dict: dict[str, pd.DataFrame] = self._data.copy()
1238        # データソースの選択
1239        if not dfs_dict:
1240            raise ValueError("データが読み込まれていません。")
1241
1242        if source_name is None:
1243            source_name = list(dfs_dict.keys())[0]
1244        elif source_name not in dfs_dict:
1245            raise ValueError(
1246                f"指定されたデータソース '{source_name}' が見つかりません。"
1247            )
1248
1249        df = dfs_dict[source_name]
1250
1251        # プロットの作成
1252        fig = plt.figure(figsize=figsize, dpi=dpi)
1253
1254        # CH4プロット
1255        ax1 = fig.add_subplot(3, 1, 1)
1256        ax1.plot(df.index, df[col_ch4], c="red")
1257        if ylim_ch4:
1258            ax1.set_ylim(ylim_ch4)
1259        ax1.set_ylabel("$\\mathregular{CH_{4}}$ (ppm)")
1260        ax1.grid(True, alpha=0.3)
1261
1262        # C2H6プロット
1263        ax2 = fig.add_subplot(3, 1, 2)
1264        ax2.plot(df.index, df[col_c2h6], c="red")
1265        if ylim_c2h6:
1266            ax2.set_ylim(ylim_c2h6)
1267        ax2.set_ylabel("$\\mathregular{C_{2}H_{6}}$ (ppb)")
1268        ax2.grid(True, alpha=0.3)
1269
1270        # H2Oプロット
1271        ax3 = fig.add_subplot(3, 1, 3)
1272        ax3.plot(df.index, df[col_h2o], c="red")
1273        if ylim_h2o:
1274            ax3.set_ylim(ylim_h2o)
1275        ax3.set_ylabel("$\\mathregular{H_{2}O}$ (ppm)")
1276        ax3.grid(True, alpha=0.3)
1277
1278        # x軸のフォーマット調整
1279        for ax in [ax1, ax2, ax3]:
1280            ax.xaxis.set_major_formatter(mdates.DateFormatter("%H:%M"))
1281
1282        plt.subplots_adjust(wspace=0.38, hspace=0.38)
1283
1284        # 図の保存
1285        if save_fig:
1286            if output_dir is None:
1287                raise ValueError(
1288                    "save_fig=Trueの場合、output_dirを指定する必要があります。有効なディレクトリパスを指定してください。"
1289                )
1290            output_path = os.path.join(output_dir, output_filename)
1291            plt.savefig(output_path, bbox_inches="tight")
1292            self.logger.info(f"時系列プロットを保存しました: {output_path}")
1293
1294        if show_fig:
1295            plt.show()
1296        else:
1297            plt.close(fig=fig)

時系列データをプロットします。

Parameters:

dpi : int
    図の解像度を指定します。デフォルトは200です。
source_name : str | None
    プロットするデータソースの名前。Noneの場合は最初のデータソースを使用します。
figsize : tuple[float, float]
    図のサイズを指定します。デフォルトは(8, 4)です。
output_dir : str | Path | None
    保存先のディレクトリを指定します。save_fig=Trueの場合は必須です。
output_filename : str
    保存するファイル名を指定します。デフォルトは"time_series.png"です。
save_fig : bool
    図を保存するかどうかを指定します。デフォルトはFalseです。
show_fig : bool
    図を表示するかどうかを指定します。デフォルトはTrueです。
col_ch4 : str
    CH4データのキーを指定します。デフォルトは"ch4_ppm"です。
col_c2h6 : str
    C2H6データのキーを指定します。デフォルトは"c2h6_ppb"です。
col_h2o : str
    H2Oデータのキーを指定します。デフォルトは"h2o_ppm"です。
ylim_ch4 : tuple[float, float] | None
    CH4プロットのy軸範囲を指定します。デフォルトはNoneです。
ylim_c2h6 : tuple[float, float] | None
    C2H6プロットのy軸範囲を指定します。デフォルトはNoneです。
ylim_h2o : tuple[float, float] | None
    H2Oプロットのy軸範囲を指定します。デフォルトはNoneです。
def remove_c2c1_ratio_duplicates( self, df: pandas.core.frame.DataFrame, min_time_threshold_seconds: float = 300, max_time_threshold_hours: float = 12.0, check_time_all: bool = True, hotspot_area_meter: float = 50.0, col_ch4_ppm: str = 'ch4_ppm', col_ch4_ppm_mv: str = 'ch4_ppm_mv', col_ch4_ppm_delta: str = 'ch4_ppm_delta'):
1751    def remove_c2c1_ratio_duplicates(
1752        self,
1753        df: pd.DataFrame,
1754        min_time_threshold_seconds: float = 300,  # 5分以内は重複とみなす
1755        max_time_threshold_hours: float = 12.0,  # 12時間以上離れている場合は別のポイントとして扱う
1756        check_time_all: bool = True,  # 時間閾値を超えた場合の重複チェックを継続するかどうか
1757        hotspot_area_meter: float = 50.0,  # 重複とみなす距離の閾値(メートル)
1758        col_ch4_ppm: str = "ch4_ppm",
1759        col_ch4_ppm_mv: str = "ch4_ppm_mv",
1760        col_ch4_ppm_delta: str = "ch4_ppm_delta",
1761    ):
1762        """
1763        メタン濃度の増加が閾値を超えた地点から、重複を除外してユニークなホットスポットを抽出する関数。
1764
1765        Parameters:
1766        ------
1767            df : pandas.DataFrame
1768                入力データフレーム。必須カラム:
1769                - ch4_ppm: メタン濃度(ppm)
1770                - ch4_ppm_mv: メタン濃度の移動平均(ppm)
1771                - ch4_ppm_delta: メタン濃度の増加量(ppm)
1772                - latitude: 緯度
1773                - longitude: 経度
1774            min_time_threshold_seconds : float, optional
1775                重複とみなす最小時間差(秒)。デフォルトは300秒(5分)。
1776            max_time_threshold_hours : float, optional
1777                別ポイントとして扱う最大時間差(時間)。デフォルトは12時間。
1778            check_time_all : bool, optional
1779                時間閾値を超えた場合の重複チェックを継続するかどうか。デフォルトはTrue。
1780            hotspot_area_meter : float, optional
1781                重複とみなす距離の閾値(メートル)。デフォルトは50メートル。
1782
1783        Returns:
1784        ------
1785            pandas.DataFrame
1786                ユニークなホットスポットのデータフレーム。
1787        """
1788        df_data: pd.DataFrame = df.copy()
1789        # メタン濃度の増加が閾値を超えた点を抽出
1790        mask = (
1791            df_data[col_ch4_ppm] - df_data[col_ch4_ppm_mv] > self._ch4_enhance_threshold
1792        )
1793        hotspot_candidates = df_data[mask].copy()
1794
1795        # ΔCH4の降順でソート
1796        sorted_hotspots = hotspot_candidates.sort_values(
1797            by=col_ch4_ppm_delta, ascending=False
1798        )
1799        used_positions = []
1800        unique_hotspots = pd.DataFrame()
1801
1802        for _, spot in sorted_hotspots.iterrows():
1803            should_add = True
1804            for used_lat, used_lon, used_time in used_positions:
1805                # 距離チェック
1806                distance = geodesic(
1807                    (spot.latitude, spot.longitude), (used_lat, used_lon)
1808                ).meters
1809
1810                if distance < hotspot_area_meter:
1811                    # 時間差の計算(秒単位)
1812                    time_diff = pd.Timedelta(
1813                        spot.name - pd.to_datetime(used_time)
1814                    ).total_seconds()
1815                    time_diff_abs = abs(time_diff)
1816
1817                    # 時間差に基づく判定
1818                    if check_time_all:
1819                        # 時間に関係なく、距離が近ければ重複とみなす
1820                        # ΔCH4が大きい方を残す(現在のスポットは必ず小さい)
1821                        should_add = False
1822                        break
1823                    else:
1824                        # 時間窓による判定を行う
1825                        if time_diff_abs <= min_time_threshold_seconds:
1826                            # Case 1: 最小時間閾値以内は重複とみなす
1827                            should_add = False
1828                            break
1829                        elif time_diff_abs > max_time_threshold_hours * 3600:
1830                            # Case 2: 最大時間閾値を超えた場合は重複チェックをスキップ
1831                            continue
1832                        # Case 3: その間の時間差の場合は、距離が近ければ重複とみなす
1833                        should_add = False
1834                        break
1835
1836            if should_add:
1837                unique_hotspots = pd.concat([unique_hotspots, pd.DataFrame([spot])])
1838                used_positions.append((spot.latitude, spot.longitude, spot.name))
1839
1840        return unique_hotspots

メタン濃度の増加が閾値を超えた地点から、重複を除外してユニークなホットスポットを抽出する関数。

Parameters:

df : pandas.DataFrame
    入力データフレーム。必須カラム:
    - ch4_ppm: メタン濃度(ppm)
    - ch4_ppm_mv: メタン濃度の移動平均(ppm)
    - ch4_ppm_delta: メタン濃度の増加量(ppm)
    - latitude: 緯度
    - longitude: 経度
min_time_threshold_seconds : float, optional
    重複とみなす最小時間差(秒)。デフォルトは300秒(5分)。
max_time_threshold_hours : float, optional
    別ポイントとして扱う最大時間差(時間)。デフォルトは12時間。
check_time_all : bool, optional
    時間閾値を超えた場合の重複チェックを継続するかどうか。デフォルトはTrue。
hotspot_area_meter : float, optional
    重複とみなす距離の閾値(メートル)。デフォルトは50メートル。

Returns:

pandas.DataFrame
    ユニークなホットスポットのデータフレーム。
@staticmethod
def remove_hotspots_duplicates( hotspots: list[HotspotData], check_time_all: bool, min_time_threshold_seconds: float = 300, max_time_threshold_hours: float = 12, hotspot_area_meter: float = 50) -> list[HotspotData]:
1842    @staticmethod
1843    def remove_hotspots_duplicates(
1844        hotspots: list[HotspotData],
1845        check_time_all: bool,
1846        min_time_threshold_seconds: float = 300,
1847        max_time_threshold_hours: float = 12,
1848        hotspot_area_meter: float = 50,
1849    ) -> list[HotspotData]:
1850        """
1851        重複するホットスポットを除外します。
1852
1853        このメソッドは、与えられたホットスポットのリストから重複を検出し、
1854        一意のホットスポットのみを返します。重複の判定は、指定された
1855        時間および距離の閾値に基づいて行われます。
1856
1857        Parameters:
1858        ------
1859            hotspots : list[HotspotData]
1860                重複を除外する対象のホットスポットのリスト。
1861            check_time_all : bool
1862                時間に関係なく重複チェックを行うかどうか。
1863            min_time_threshold_seconds : float
1864                重複とみなす最小時間の閾値(秒)。
1865            max_time_threshold_hours : float
1866                重複チェックを一時的に無視する最大時間の閾値(時間)。
1867            hotspot_area_meter : float
1868                重複とみなす距離の閾値(メートル)。
1869
1870        Returns:
1871        ------
1872            list[HotspotData]
1873                重複を除去したホットスポットのリスト。
1874        """
1875        # ΔCH4の降順でソート
1876        sorted_hotspots: list[HotspotData] = sorted(
1877            hotspots, key=lambda x: x.delta_ch4, reverse=True
1878        )
1879        used_positions_by_type: dict[
1880            HotspotType, list[tuple[float, float, str, float]]
1881        ] = {
1882            "bio": [],
1883            "gas": [],
1884            "comb": [],
1885        }
1886        unique_hotspots: list[HotspotData] = []
1887
1888        for spot in sorted_hotspots:
1889            is_duplicate = MobileSpatialAnalyzer._is_duplicate_spot(
1890                current_lat=spot.avg_lat,
1891                current_lon=spot.avg_lon,
1892                current_time=spot.source,
1893                used_positions=used_positions_by_type[spot.type],
1894                check_time_all=check_time_all,
1895                min_time_threshold_seconds=min_time_threshold_seconds,
1896                max_time_threshold_hours=max_time_threshold_hours,
1897                hotspot_area_meter=hotspot_area_meter,
1898            )
1899
1900            if not is_duplicate:
1901                unique_hotspots.append(spot)
1902                used_positions_by_type[spot.type].append(
1903                    (spot.avg_lat, spot.avg_lon, spot.source, spot.delta_ch4)
1904                )
1905
1906        return unique_hotspots

重複するホットスポットを除外します。

このメソッドは、与えられたホットスポットのリストから重複を検出し、 一意のホットスポットのみを返します。重複の判定は、指定された 時間および距離の閾値に基づいて行われます。

Parameters:

hotspots : list[HotspotData]
    重複を除外する対象のホットスポットのリスト。
check_time_all : bool
    時間に関係なく重複チェックを行うかどうか。
min_time_threshold_seconds : float
    重複とみなす最小時間の閾値(秒)。
max_time_threshold_hours : float
    重複チェックを一時的に無視する最大時間の閾値(時間)。
hotspot_area_meter : float
    重複とみなす距離の閾値(メートル)。

Returns:

list[HotspotData]
    重複を除去したホットスポットのリスト。
@staticmethod
def setup_logger(logger: logging.Logger | None, log_level: int = 20) -> logging.Logger:
1908    @staticmethod
1909    def setup_logger(logger: Logger | None, log_level: int = INFO) -> Logger:
1910        """
1911        ロガーを設定します。
1912
1913        このメソッドは、ロギングの設定を行い、ログメッセージのフォーマットを指定します。
1914        ログメッセージには、日付、ログレベル、メッセージが含まれます。
1915
1916        渡されたロガーがNoneまたは不正な場合は、新たにロガーを作成し、標準出力に
1917        ログメッセージが表示されるようにStreamHandlerを追加します。ロガーのレベルは
1918        引数で指定されたlog_levelに基づいて設定されます。
1919
1920        Parameters:
1921        ------
1922            logger : Logger | None
1923                使用するロガー。Noneの場合は新しいロガーを作成します。
1924            log_level : int
1925                ロガーのログレベル。デフォルトはINFO。
1926
1927        Returns:
1928        ------
1929            Logger
1930                設定されたロガーオブジェクト。
1931        """
1932        if logger is not None and isinstance(logger, Logger):
1933            return logger
1934        # 渡されたロガーがNoneまたは正しいものでない場合は独自に設定
1935        new_logger: Logger = getLogger()
1936        # 既存のハンドラーをすべて削除
1937        for handler in new_logger.handlers[:]:
1938            new_logger.removeHandler(handler)
1939        new_logger.setLevel(log_level)  # ロガーのレベルを設定
1940        ch = StreamHandler()
1941        ch_formatter = Formatter("%(asctime)s - %(levelname)s - %(message)s")
1942        ch.setFormatter(ch_formatter)  # フォーマッターをハンドラーに設定
1943        new_logger.addHandler(ch)  # StreamHandlerの追加
1944        return new_logger

ロガーを設定します。

このメソッドは、ロギングの設定を行い、ログメッセージのフォーマットを指定します。 ログメッセージには、日付、ログレベル、メッセージが含まれます。

渡されたロガーがNoneまたは不正な場合は、新たにロガーを作成し、標準出力に ログメッセージが表示されるようにStreamHandlerを追加します。ロガーのレベルは 引数で指定されたlog_levelに基づいて設定されます。

Parameters:

logger : Logger | None
    使用するロガー。Noneの場合は新しいロガーを作成します。
log_level : int
    ロガーのログレベル。デフォルトはINFO。

Returns:

Logger
    設定されたロガーオブジェクト。
@staticmethod
def calculate_emission_rates( hotspots: list[HotspotData], method: Literal['weller', 'weitzel', 'joo', 'umezawa'] = 'weller', print_summary: bool = True, custom_formulas: dict[str, dict[str, float]] | None = None) -> tuple[list[EmissionData], dict[str, dict[str, float]]]:
1946    @staticmethod
1947    def calculate_emission_rates(
1948        hotspots: list[HotspotData],
1949        method: Literal["weller", "weitzel", "joo", "umezawa"] = "weller",
1950        print_summary: bool = True,
1951        custom_formulas: dict[str, dict[str, float]] | None = None,
1952    ) -> tuple[list[EmissionData], dict[str, dict[str, float]]]:
1953        """
1954        検出されたホットスポットのCH4漏出量を計算・解析し、統計情報を生成します。
1955
1956        Parameters:
1957        ------
1958            hotspots : list[HotspotData]
1959                分析対象のホットスポットのリスト
1960            method : Literal["weller", "weitzel", "joo", "umezawa"]
1961                使用する計算式。デフォルトは"weller"。
1962            print_summary : bool
1963                統計情報を表示するかどうか。デフォルトはTrue。
1964            custom_formulas : dict[str, dict[str, float]] | None
1965                カスタム計算式の係数。
1966                例: {"custom_method": {"a": 1.0, "b": 1.0}}
1967                Noneの場合はデフォルトの計算式を使用。
1968
1969        Returns:
1970        ------
1971            tuple[list[EmissionData], dict[str, dict[str, float]]]
1972                - 各ホットスポットの排出量データを含むリスト
1973                - タイプ別の統計情報を含む辞書
1974        """
1975        # デフォルトの経験式係数
1976        default_formulas = {
1977            "weller": {"a": 0.988, "b": 0.817},
1978            "weitzel": {"a": 0.521, "b": 0.795},
1979            "joo": {"a": 2.738, "b": 1.329},
1980            "umezawa": {"a": 2.716, "b": 0.741},
1981        }
1982
1983        # カスタム計算式がある場合は追加
1984        emission_formulas = default_formulas.copy()
1985        if custom_formulas:
1986            emission_formulas.update(custom_formulas)
1987
1988        if method not in emission_formulas:
1989            raise ValueError(f"Unknown method: {method}")
1990
1991        # 係数の取得
1992        a = emission_formulas[method]["a"]
1993        b = emission_formulas[method]["b"]
1994
1995        # 排出量の計算
1996        emission_data_list = []
1997        for spot in hotspots:
1998            # 漏出量の計算 (L/min)
1999            emission_rate = np.exp((np.log(spot.delta_ch4) + a) / b)
2000            # 日排出量 (L/day)
2001            daily_emission = emission_rate * 60 * 24
2002            # 年間排出量 (L/year)
2003            annual_emission = daily_emission * 365
2004
2005            emission_data = EmissionData(
2006                source=spot.source,
2007                type=spot.type,
2008                section=spot.section,
2009                latitude=spot.avg_lat,
2010                longitude=spot.avg_lon,
2011                delta_ch4=spot.delta_ch4,
2012                delta_c2h6=spot.delta_c2h6,
2013                ratio=spot.ratio,
2014                emission_rate=emission_rate,
2015                daily_emission=daily_emission,
2016                annual_emission=annual_emission,
2017            )
2018            emission_data_list.append(emission_data)
2019
2020        # 統計計算用にDataFrameを作成
2021        emission_df = pd.DataFrame([e.to_dict() for e in emission_data_list])
2022
2023        # タイプ別の統計情報を計算
2024        stats = {}
2025        # emission_formulas の定義の後に、排出量カテゴリーの閾値を定義
2026        emission_categories = {
2027            "low": {"min": 0, "max": 6},  # < 6 L/min
2028            "medium": {"min": 6, "max": 40},  # 6-40 L/min
2029            "high": {"min": 40, "max": float("inf")},  # > 40 L/min
2030        }
2031        # get_args(HotspotType)を使用して型安全なリストを作成
2032        types = list(get_args(HotspotType))
2033        for spot_type in types:
2034            df_type = emission_df[emission_df["type"] == spot_type]
2035            if len(df_type) > 0:
2036                # 既存の統計情報を計算
2037                type_stats = {
2038                    "count": len(df_type),
2039                    "emission_rate_min": df_type["emission_rate"].min(),
2040                    "emission_rate_max": df_type["emission_rate"].max(),
2041                    "emission_rate_mean": df_type["emission_rate"].mean(),
2042                    "emission_rate_median": df_type["emission_rate"].median(),
2043                    "total_annual_emission": df_type["annual_emission"].sum(),
2044                    "mean_annual_emission": df_type["annual_emission"].mean(),
2045                }
2046
2047                # 排出量カテゴリー別の統計を追加
2048                category_counts = {
2049                    "low": len(
2050                        df_type[
2051                            df_type["emission_rate"] < emission_categories["low"]["max"]
2052                        ]
2053                    ),
2054                    "medium": len(
2055                        df_type[
2056                            (
2057                                df_type["emission_rate"]
2058                                >= emission_categories["medium"]["min"]
2059                            )
2060                            & (
2061                                df_type["emission_rate"]
2062                                < emission_categories["medium"]["max"]
2063                            )
2064                        ]
2065                    ),
2066                    "high": len(
2067                        df_type[
2068                            df_type["emission_rate"]
2069                            >= emission_categories["high"]["min"]
2070                        ]
2071                    ),
2072                }
2073                type_stats["emission_categories"] = category_counts
2074
2075                stats[spot_type] = type_stats
2076
2077                if print_summary:
2078                    print(f"\n{spot_type}タイプの統計情報:")
2079                    print(f"  検出数: {type_stats['count']}")
2080                    print("  排出量 (L/min):")
2081                    print(f"    最小値: {type_stats['emission_rate_min']:.2f}")
2082                    print(f"    最大値: {type_stats['emission_rate_max']:.2f}")
2083                    print(f"    平均値: {type_stats['emission_rate_mean']:.2f}")
2084                    print(f"    中央値: {type_stats['emission_rate_median']:.2f}")
2085                    print("  排出量カテゴリー別の検出数:")
2086                    print(f"    低放出 (< 6 L/min): {category_counts['low']}")
2087                    print(f"    中放出 (6-40 L/min): {category_counts['medium']}")
2088                    print(f"    高放出 (> 40 L/min): {category_counts['high']}")
2089                    print("  年間排出量 (L/year):")
2090                    print(f"    合計: {type_stats['total_annual_emission']:.2f}")
2091                    print(f"    平均: {type_stats['mean_annual_emission']:.2f}")
2092
2093        return emission_data_list, stats

検出されたホットスポットのCH4漏出量を計算・解析し、統計情報を生成します。

Parameters:

hotspots : list[HotspotData]
    分析対象のホットスポットのリスト
method : Literal["weller", "weitzel", "joo", "umezawa"]
    使用する計算式。デフォルトは"weller"。
print_summary : bool
    統計情報を表示するかどうか。デフォルトはTrue。
custom_formulas : dict[str, dict[str, float]] | None
    カスタム計算式の係数。
    例: {"custom_method": {"a": 1.0, "b": 1.0}}
    Noneの場合はデフォルトの計算式を使用。

Returns:

tuple[list[EmissionData], dict[str, dict[str, float]]]
    - 各ホットスポットの排出量データを含むリスト
    - タイプ別の統計情報を含む辞書
@staticmethod
def plot_emission_analysis( emission_data_list: list[EmissionData], dpi: int = 300, output_dir: str | pathlib.Path | None = None, output_filename: str = 'emission_analysis.png', figsize: tuple[float, float] = (12, 5), add_legend: bool = True, hist_log_y: bool = False, hist_xlim: tuple[float, float] | None = None, hist_ylim: tuple[float, float] | None = None, scatter_xlim: tuple[float, float] | None = None, scatter_ylim: tuple[float, float] | None = None, hist_bin_width: float = 0.5, print_summary: bool = True, save_fig: bool = False, show_fig: bool = True, show_scatter: bool = True) -> None:
2095    @staticmethod
2096    def plot_emission_analysis(
2097        emission_data_list: list[EmissionData],
2098        dpi: int = 300,
2099        output_dir: str | Path | None = None,
2100        output_filename: str = "emission_analysis.png",
2101        figsize: tuple[float, float] = (12, 5),
2102        add_legend: bool = True,
2103        hist_log_y: bool = False,
2104        hist_xlim: tuple[float, float] | None = None,
2105        hist_ylim: tuple[float, float] | None = None,
2106        scatter_xlim: tuple[float, float] | None = None,
2107        scatter_ylim: tuple[float, float] | None = None,
2108        hist_bin_width: float = 0.5,
2109        print_summary: bool = True,
2110        save_fig: bool = False,
2111        show_fig: bool = True,
2112        show_scatter: bool = True,  # 散布図の表示を制御するオプションを追加
2113    ) -> None:
2114        """
2115        排出量分析のプロットを作成する静的メソッド。
2116
2117        Parameters:
2118        ------
2119            emission_data_list : list[EmissionData]
2120                EmissionDataオブジェクトのリスト。
2121            output_dir : str | Path | None
2122                出力先ディレクトリのパス。
2123            output_filename : str
2124                保存するファイル名。デフォルトは"emission_analysis.png"。
2125            dpi : int
2126                プロットの解像度。デフォルトは300。
2127            figsize : tuple[float, float]
2128                プロットのサイズ。デフォルトは(12, 5)。
2129            add_legend : bool
2130                凡例を追加するかどうか。デフォルトはTrue。
2131            hist_log_y : bool
2132                ヒストグラムのy軸を対数スケールにするかどうか。デフォルトはFalse。
2133            hist_xlim : tuple[float, float] | None
2134                ヒストグラムのx軸の範囲。デフォルトはNone。
2135            hist_ylim : tuple[float, float] | None
2136                ヒストグラムのy軸の範囲。デフォルトはNone。
2137            scatter_xlim : tuple[float, float] | None
2138                散布図のx軸の範囲。デフォルトはNone。
2139            scatter_ylim : tuple[float, float] | None
2140                散布図のy軸の範囲。デフォルトはNone。
2141            hist_bin_width : float
2142                ヒストグラムのビンの幅。デフォルトは0.5。
2143            print_summary : bool
2144                集計結果を表示するかどうか。デフォルトはFalse。
2145            save_fig : bool
2146                図をファイルに保存するかどうか。デフォルトはFalse。
2147            show_fig : bool
2148                図を表示するかどうか。デフォルトはTrue。
2149            show_scatter : bool
2150                散布図(右図)を表示するかどうか。デフォルトはTrue。
2151        """
2152        # データをDataFrameに変換
2153        df = pd.DataFrame([e.to_dict() for e in emission_data_list])
2154
2155        # プロットの作成(散布図の有無に応じてサブプロット数を調整)
2156        if show_scatter:
2157            fig, (ax1, ax2) = plt.subplots(1, 2, figsize=figsize)
2158            axes = [ax1, ax2]
2159        else:
2160            fig, ax1 = plt.subplots(1, 1, figsize=(figsize[0] // 2, figsize[1]))
2161            axes = [ax1]
2162
2163        # カラーマップの定義
2164        colors: dict[HotspotType, str] = {"bio": "blue", "gas": "red", "comb": "green"}
2165
2166        # 存在するタイプを確認
2167        # HotspotTypeの定義順を基準にソート
2168        hotspot_types = list(get_args(HotspotType))
2169        existing_types = sorted(
2170            df["type"].unique(), key=lambda x: hotspot_types.index(x)
2171        )
2172
2173        # 左側: ヒストグラム
2174        # ビンの範囲を設定
2175        start = 0  # 必ず0から開始
2176        if hist_xlim is not None:
2177            end = hist_xlim[1]
2178        else:
2179            end = np.ceil(df["emission_rate"].max() * 1.05)
2180
2181        # ビン数を計算(end値をbin_widthで割り切れるように調整)
2182        n_bins = int(np.ceil(end / hist_bin_width))
2183        end = n_bins * hist_bin_width
2184
2185        # ビンの生成(0から開始し、bin_widthの倍数で区切る)
2186        bins = np.linspace(start, end, n_bins + 1)
2187
2188        # タイプごとにヒストグラムを積み上げ
2189        bottom = np.zeros(len(bins) - 1)
2190        for spot_type in existing_types:
2191            data = df[df["type"] == spot_type]["emission_rate"]
2192            if len(data) > 0:
2193                counts, _ = np.histogram(data, bins=bins)
2194                ax1.bar(
2195                    bins[:-1],
2196                    counts,
2197                    width=hist_bin_width,
2198                    bottom=bottom,
2199                    alpha=0.6,
2200                    label=spot_type,
2201                    color=colors[spot_type],
2202                )
2203                bottom += counts
2204
2205        ax1.set_xlabel("CH$_4$ Emission (L min$^{-1}$)")
2206        ax1.set_ylabel("Frequency")
2207        if hist_log_y:
2208            # ax1.set_yscale("log")
2209            # 非線形スケールを設定(linthreshで線形から対数への遷移点を指定)
2210            ax1.set_yscale("symlog", linthresh=1.0)
2211        if hist_xlim is not None:
2212            ax1.set_xlim(hist_xlim)
2213        else:
2214            ax1.set_xlim(0, np.ceil(df["emission_rate"].max() * 1.05))
2215
2216        if hist_ylim is not None:
2217            ax1.set_ylim(hist_ylim)
2218        else:
2219            ax1.set_ylim(0, ax1.get_ylim()[1])  # 下限を0に設定
2220
2221        if show_scatter:
2222            # 右側: 散布図
2223            for spot_type in existing_types:
2224                mask = df["type"] == spot_type
2225                ax2.scatter(
2226                    df[mask]["emission_rate"],
2227                    df[mask]["delta_ch4"],
2228                    alpha=0.6,
2229                    label=spot_type,
2230                    color=colors[spot_type],
2231                )
2232
2233            ax2.set_xlabel("Emission Rate (L min$^{-1}$)")
2234            ax2.set_ylabel("ΔCH$_4$ (ppm)")
2235            if scatter_xlim is not None:
2236                ax2.set_xlim(scatter_xlim)
2237            else:
2238                ax2.set_xlim(0, np.ceil(df["emission_rate"].max() * 1.05))
2239
2240            if scatter_ylim is not None:
2241                ax2.set_ylim(scatter_ylim)
2242            else:
2243                ax2.set_ylim(0, np.ceil(df["delta_ch4"].max() * 1.05))
2244
2245        # 凡例の表示
2246        if add_legend:
2247            for ax in axes:
2248                ax.legend(
2249                    bbox_to_anchor=(0.5, -0.30),
2250                    loc="upper center",
2251                    ncol=len(existing_types),
2252                )
2253
2254        plt.tight_layout()
2255
2256        # 図の保存
2257        if save_fig:
2258            if output_dir is None:
2259                raise ValueError(
2260                    "save_fig=Trueの場合、output_dirを指定する必要があります。有効なディレクトリパスを指定してください。"
2261                )
2262            os.makedirs(output_dir, exist_ok=True)
2263            output_path = os.path.join(output_dir, output_filename)
2264            plt.savefig(output_path, bbox_inches="tight", dpi=dpi)
2265        # 図の表示
2266        if show_fig:
2267            plt.show()
2268        else:
2269            plt.close(fig=fig)
2270
2271        if print_summary:
2272            # デバッグ用の出力
2273            print("\nビンごとの集計:")
2274            print(f"{'Range':>12} | {'bio':>8} | {'gas':>8} | {'total':>8}")
2275            print("-" * 50)
2276
2277            for i in range(len(bins) - 1):
2278                bin_start = bins[i]
2279                bin_end = bins[i + 1]
2280
2281                # 各タイプのカウントを計算
2282                counts_by_type: dict[HotspotType, int] = {"bio": 0, "gas": 0, "comb": 0}
2283                total = 0
2284                for spot_type in existing_types:
2285                    mask = (
2286                        (df["type"] == spot_type)
2287                        & (df["emission_rate"] >= bin_start)
2288                        & (df["emission_rate"] < bin_end)
2289                    )
2290                    count = len(df[mask])
2291                    counts_by_type[spot_type] = count
2292                    total += count
2293
2294                # カウントが0の場合はスキップ
2295                if total > 0:
2296                    range_str = f"{bin_start:5.1f}-{bin_end:<5.1f}"
2297                    bio_count = counts_by_type.get("bio", 0)
2298                    gas_count = counts_by_type.get("gas", 0)
2299                    print(
2300                        f"{range_str:>12} | {bio_count:8d} | {gas_count:8d} | {total:8d}"
2301                    )

排出量分析のプロットを作成する静的メソッド。

Parameters:

emission_data_list : list[EmissionData]
    EmissionDataオブジェクトのリスト。
output_dir : str | Path | None
    出力先ディレクトリのパス。
output_filename : str
    保存するファイル名。デフォルトは"emission_analysis.png"。
dpi : int
    プロットの解像度。デフォルトは300。
figsize : tuple[float, float]
    プロットのサイズ。デフォルトは(12, 5)。
add_legend : bool
    凡例を追加するかどうか。デフォルトはTrue。
hist_log_y : bool
    ヒストグラムのy軸を対数スケールにするかどうか。デフォルトはFalse。
hist_xlim : tuple[float, float] | None
    ヒストグラムのx軸の範囲。デフォルトはNone。
hist_ylim : tuple[float, float] | None
    ヒストグラムのy軸の範囲。デフォルトはNone。
scatter_xlim : tuple[float, float] | None
    散布図のx軸の範囲。デフォルトはNone。
scatter_ylim : tuple[float, float] | None
    散布図のy軸の範囲。デフォルトはNone。
hist_bin_width : float
    ヒストグラムのビンの幅。デフォルトは0.5。
print_summary : bool
    集計結果を表示するかどうか。デフォルトはFalse。
save_fig : bool
    図をファイルに保存するかどうか。デフォルトはFalse。
show_fig : bool
    図を表示するかどうか。デフォルトはTrue。
show_scatter : bool
    散布図(右図)を表示するかどうか。デフォルトはTrue。
@dataclass
class MSAInputConfig:
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
    適用する補正式の種類を表す文字列
MSAInputConfig( fs: float, lag: float, path: pathlib.Path | str, correction_type: str | None = None)
fs: float
lag: float
path: pathlib.Path | str
correction_type: str | None = None
@classmethod
def validate_and_create( cls, fs: float, lag: float, path: pathlib.Path | str, correction_type: str | None) -> MSAInputConfig:
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オブジェクト。
class MonthlyConverter:
  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')。

MonthlyConverter( directory: str | pathlib.Path, file_pattern: str = 'SA.Ultra.*.xlsx', logger: logging.Logger | None = None, logging_debug: bool = False)
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以上のレベルのメッセージが出力されます。
FILE_DATE_FORMAT = '%Y.%m'
PERIOD_DATE_FORMAT = '%Y-%m-%d'
logger: logging.Logger
@staticmethod
def setup_logger(logger: logging.Logger | None, log_level: int = 20) -> logging.Logger:
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
    設定されたロガーオブジェクト。
def close(self) -> None:
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ファイルをクローズする

def get_available_dates(self) -> list[str]:
 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'形式の日付リスト
def get_sheet_names(self, file_name: str) -> list[str]:
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]
    シート名のリスト
def read_sheets( self, 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 = None, end_date: str | None = None, include_end_date: bool = True, sort_by_date: bool = True) -> pandas.core.frame.DataFrame:
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を返します。
@staticmethod
def extract_monthly_data( df: pandas.core.frame.DataFrame, target_months: list[int], start_day: int | None = None, end_day: int | None = None, datetime_column: str = 'Date') -> pandas.core.frame.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
    指定された期間のデータのみを含むデータフレーム。
@staticmethod
def merge_dataframes( df1: pandas.core.frame.DataFrame, df2: pandas.core.frame.DataFrame, date_column: str = 'Date') -> pandas.core.frame.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
class MonthlyFiguresGenerator:
  63class MonthlyFiguresGenerator:
  64    def __init__(
  65        self,
  66        logger: Logger | None = None,
  67        logging_debug: bool = False,
  68    ) -> None:
  69        """
  70        クラスのコンストラクタ
  71
  72        Parameters:
  73        ------
  74            logger : Logger | None
  75                使用するロガー。Noneの場合は新しいロガーを作成します。
  76            logging_debug : bool
  77                ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。
  78        """
  79        # ロガー
  80        log_level: int = INFO
  81        if logging_debug:
  82            log_level = DEBUG
  83        self.logger: Logger = MonthlyFiguresGenerator.setup_logger(logger, log_level)
  84
  85    def plot_c1c2_fluxes_timeseries(
  86        self,
  87        df,
  88        output_dir: str,
  89        output_filename: str = "timeseries.png",
  90        col_datetime: str = "Date",
  91        col_c1_flux: str = "Fch4_ultra",
  92        col_c2_flux: str = "Fc2h6_ultra",
  93    ):
  94        """
  95        月別のフラックスデータを時系列プロットとして出力する
  96
  97        Parameters:
  98        ------
  99            df : pd.DataFrame
 100                月別データを含むDataFrame
 101            output_dir : str
 102                出力ファイルを保存するディレクトリのパス
 103            output_filename : str
 104                出力ファイルの名前
 105            col_datetime : str
 106                日付を含む列の名前。デフォルトは"Date"。
 107            col_c1_flux : str
 108                CH4フラックスを含む列の名前。デフォルトは"Fch4_ultra"。
 109            col_c2_flux : str
 110                C2H6フラックスを含む列の名前。デフォルトは"Fc2h6_ultra"。
 111        """
 112        os.makedirs(output_dir, exist_ok=True)
 113        output_path: str = os.path.join(output_dir, output_filename)
 114
 115        # 図の作成
 116        _, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 8), sharex=True)
 117
 118        # CH4フラックスのプロット
 119        ax1.scatter(df[col_datetime], df[col_c1_flux], color="red", alpha=0.5, s=20)
 120        ax1.set_ylabel(r"CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)")
 121        ax1.set_ylim(-100, 600)
 122        ax1.text(0.02, 0.98, "(a)", transform=ax1.transAxes, va="top", fontsize=20)
 123        ax1.grid(True, alpha=0.3)
 124
 125        # C2H6フラックスのプロット
 126        ax2.scatter(
 127            df[col_datetime],
 128            df[col_c2_flux],
 129            color="orange",
 130            alpha=0.5,
 131            s=20,
 132        )
 133        ax2.set_ylabel(r"C$_2$H$_6$ flux (nmol m$^{-2}$ s$^{-1}$)")
 134        ax2.set_ylim(-20, 60)
 135        ax2.text(0.02, 0.98, "(b)", transform=ax2.transAxes, va="top", fontsize=20)
 136        ax2.grid(True, alpha=0.3)
 137
 138        # x軸の設定
 139        ax2.xaxis.set_major_locator(mdates.MonthLocator(interval=1))
 140        ax2.xaxis.set_major_formatter(mdates.DateFormatter("%m"))
 141        plt.setp(ax2.get_xticklabels(), rotation=0, ha="right")
 142        ax2.set_xlabel("Month")
 143
 144        # 図の保存
 145        plt.savefig(output_path, dpi=300, bbox_inches="tight")
 146        plt.close()
 147
 148    def plot_c1c2_concentrations_and_fluxes_timeseries(
 149        self,
 150        df: pd.DataFrame,
 151        output_dir: str,
 152        output_filename: str = "conc_flux_timeseries.png",
 153        col_datetime: str = "Date",
 154        col_ch4_conc: str = "CH4_ultra",
 155        col_ch4_flux: str = "Fch4_ultra",
 156        col_c2h6_conc: str = "C2H6_ultra",
 157        col_c2h6_flux: str = "Fc2h6_ultra",
 158        print_summary: bool = True,
 159    ) -> None:
 160        """
 161        CH4とC2H6の濃度とフラックスの時系列プロットを作成する
 162
 163        Parameters:
 164        ------
 165            df : pd.DataFrame
 166                月別データを含むDataFrame
 167            output_dir : str
 168                出力ディレクトリのパス
 169            output_filename : str
 170                出力ファイル名
 171            col_datetime : str
 172                日付列の名前
 173            col_ch4_conc : str
 174                CH4濃度列の名前
 175            col_ch4_flux : str
 176                CH4フラックス列の名前
 177            col_c2h6_conc : str
 178                C2H6濃度列の名前
 179            col_c2h6_flux : str
 180                C2H6フラックス列の名前
 181            print_summary : bool
 182                解析情報をprintするかどうか
 183        """
 184        # 出力ディレクトリの作成
 185        os.makedirs(output_dir, exist_ok=True)
 186        output_path: str = os.path.join(output_dir, output_filename)
 187
 188        if print_summary:
 189            # 統計情報の計算と表示
 190            for name, col in [
 191                ("CH4 concentration", col_ch4_conc),
 192                ("CH4 flux", col_ch4_flux),
 193                ("C2H6 concentration", col_c2h6_conc),
 194                ("C2H6 flux", col_c2h6_flux),
 195            ]:
 196                # NaNを除外してから統計量を計算
 197                valid_data = df[col].dropna()
 198
 199                if len(valid_data) > 0:
 200                    percentile_5 = np.nanpercentile(valid_data, 5)
 201                    percentile_95 = np.nanpercentile(valid_data, 95)
 202                    mean_value = np.nanmean(valid_data)
 203                    positive_ratio = (valid_data > 0).mean() * 100
 204
 205                    print(f"\n{name}:")
 206                    print(
 207                        f"90パーセンタイルレンジ: {percentile_5:.2f} - {percentile_95:.2f}"
 208                    )
 209                    print(f"平均値: {mean_value:.2f}")
 210                    print(f"正の値の割合: {positive_ratio:.1f}%")
 211                else:
 212                    print(f"\n{name}: データが存在しません")
 213
 214        # プロットの作成
 215        _, (ax1, ax2, ax3, ax4) = plt.subplots(4, 1, figsize=(12, 16), sharex=True)
 216
 217        # CH4濃度のプロット
 218        ax1.scatter(df[col_datetime], df[col_ch4_conc], color="red", alpha=0.5, s=20)
 219        ax1.set_ylabel("CH$_4$ Concentration\n(ppm)")
 220        ax1.set_ylim(1.8, 2.6)
 221        ax1.text(0.02, 0.98, "(a)", transform=ax1.transAxes, va="top", fontsize=20)
 222        ax1.grid(True, alpha=0.3)
 223
 224        # CH4フラックスのプロット
 225        ax2.scatter(df[col_datetime], df[col_ch4_flux], color="red", alpha=0.5, s=20)
 226        ax2.set_ylabel("CH$_4$ flux\n(nmol m$^{-2}$ s$^{-1}$)")
 227        ax2.set_ylim(-100, 600)
 228        # ax2.set_yticks([-100, 0, 200, 400, 600])
 229        ax2.text(0.02, 0.98, "(b)", transform=ax2.transAxes, va="top", fontsize=20)
 230        ax2.grid(True, alpha=0.3)
 231
 232        # C2H6濃度のプロット
 233        ax3.scatter(
 234            df[col_datetime], df[col_c2h6_conc], color="orange", alpha=0.5, s=20
 235        )
 236        ax3.set_ylabel("C$_2$H$_6$ Concentration\n(ppb)")
 237        ax3.text(0.02, 0.98, "(c)", transform=ax3.transAxes, va="top", fontsize=20)
 238        ax3.grid(True, alpha=0.3)
 239
 240        # C2H6フラックスのプロット
 241        ax4.scatter(
 242            df[col_datetime], df[col_c2h6_flux], color="orange", alpha=0.5, s=20
 243        )
 244        ax4.set_ylabel("C$_2$H$_6$ flux\n(nmol m$^{-2}$ s$^{-1}$)")
 245        ax4.set_ylim(-20, 40)
 246        ax4.text(0.02, 0.98, "(d)", transform=ax4.transAxes, va="top", fontsize=20)
 247        ax4.grid(True, alpha=0.3)
 248
 249        # x軸の設定
 250        ax4.xaxis.set_major_locator(mdates.MonthLocator(interval=1))
 251        ax4.xaxis.set_major_formatter(mdates.DateFormatter("%m"))
 252        plt.setp(ax4.get_xticklabels(), rotation=0, ha="right")
 253        ax4.set_xlabel("Month")
 254
 255        # レイアウトの調整と保存
 256        plt.tight_layout()
 257        plt.savefig(output_path, dpi=300, bbox_inches="tight")
 258        plt.close()
 259
 260        if print_summary:
 261
 262            def analyze_top_values(df, column_name, top_percent=20):
 263                print(f"\n{column_name}の上位{top_percent}%の分析:")
 264
 265                # DataFrameのコピーを作成し、日時関連の列を追加
 266                df_analysis = df.copy()
 267                df_analysis["hour"] = pd.to_datetime(df_analysis[col_datetime]).dt.hour
 268                df_analysis["month"] = pd.to_datetime(
 269                    df_analysis[col_datetime]
 270                ).dt.month
 271                df_analysis["weekday"] = pd.to_datetime(
 272                    df_analysis[col_datetime]
 273                ).dt.dayofweek
 274
 275                # 上位20%のしきい値を計算
 276                threshold = df[column_name].quantile(1 - top_percent / 100)
 277                high_values = df_analysis[df_analysis[column_name] > threshold]
 278
 279                # 月ごとの分析
 280                print("\n月別分布:")
 281                monthly_counts = high_values.groupby("month").size()
 282                total_counts = df_analysis.groupby("month").size()
 283                monthly_percentages = (monthly_counts / total_counts * 100).round(1)
 284
 285                # 月ごとのデータを安全に表示
 286                available_months = set(monthly_counts.index) & set(total_counts.index)
 287                for month in sorted(available_months):
 288                    print(
 289                        f"月{month}: {monthly_percentages[month]}% ({monthly_counts[month]}件/{total_counts[month]}件)"
 290                    )
 291
 292                # 時間帯ごとの分析(3時間区切り)
 293                print("\n時間帯別分布:")
 294                # copyを作成して新しい列を追加
 295                high_values = high_values.copy()
 296                high_values["time_block"] = high_values["hour"] // 3 * 3
 297                time_blocks = high_values.groupby("time_block").size()
 298                total_time_blocks = df_analysis.groupby(
 299                    df_analysis["hour"] // 3 * 3
 300                ).size()
 301                time_percentages = (time_blocks / total_time_blocks * 100).round(1)
 302
 303                # 時間帯ごとのデータを安全に表示
 304                available_blocks = set(time_blocks.index) & set(total_time_blocks.index)
 305                for block in sorted(available_blocks):
 306                    print(
 307                        f"{block:02d}:00-{block + 3:02d}:00: {time_percentages[block]}% ({time_blocks[block]}件/{total_time_blocks[block]}件)"
 308                    )
 309
 310                # 曜日ごとの分析
 311                print("\n曜日別分布:")
 312                weekday_names = ["月曜", "火曜", "水曜", "木曜", "金曜", "土曜", "日曜"]
 313                weekday_counts = high_values.groupby("weekday").size()
 314                total_weekdays = df_analysis.groupby("weekday").size()
 315                weekday_percentages = (weekday_counts / total_weekdays * 100).round(1)
 316
 317                # 曜日ごとのデータを安全に表示
 318                available_days = set(weekday_counts.index) & set(total_weekdays.index)
 319                for day in sorted(available_days):
 320                    if 0 <= day <= 6:  # 有効な曜日インデックスのチェック
 321                        print(
 322                            f"{weekday_names[day]}: {weekday_percentages[day]}% ({weekday_counts[day]}件/{total_weekdays[day]}件)"
 323                        )
 324
 325            # 濃度とフラックスそれぞれの分析を実行
 326            print("\n=== 上位値の時間帯・曜日分析 ===")
 327            analyze_top_values(df, col_ch4_conc)
 328            analyze_top_values(df, col_ch4_flux)
 329            analyze_top_values(df, col_c2h6_conc)
 330            analyze_top_values(df, col_c2h6_flux)
 331
 332    def plot_ch4c2h6_timeseries(
 333        self,
 334        df: pd.DataFrame,
 335        output_dir: str,
 336        col_ch4_flux: str,
 337        col_c2h6_flux: str,
 338        output_filename: str = "timeseries_year.png",
 339        col_datetime: str = "Date",
 340        window_size: int = 24 * 7,  # 1週間の移動平均のデフォルト値
 341        confidence_interval: float = 0.95,  # 95%信頼区間
 342        subplot_label_ch4: str | None = "(a)",
 343        subplot_label_c2h6: str | None = "(b)",
 344        subplot_fontsize: int = 20,
 345        show_ci: bool = True,
 346        ch4_ylim: tuple[float, float] | None = None,
 347        c2h6_ylim: tuple[float, float] | None = None,
 348        start_date: str | None = None,  # 追加:"YYYY-MM-DD"形式
 349        end_date: str | None = None,  # 追加:"YYYY-MM-DD"形式
 350        figsize: tuple[float, float] = (16, 6),
 351    ) -> None:
 352        """CH4とC2H6フラックスの時系列変動をプロット
 353
 354        Parameters:
 355        ------
 356            df : pd.DataFrame
 357                データフレーム
 358            output_dir : str
 359                出力ディレクトリのパス
 360            col_ch4_flux : str
 361                CH4フラックスのカラム名
 362            col_c2h6_flux : str
 363                C2H6フラックスのカラム名
 364            output_filename : str
 365                出力ファイル名
 366            col_datetime : str
 367                日時カラムの名前
 368            window_size : int
 369                移動平均の窓サイズ
 370            confidence_interval : float
 371                信頼区間(0-1)
 372            subplot_label_ch4 : str | None
 373                CH4プロットのラベル
 374            subplot_label_c2h6 : str | None
 375                C2H6プロットのラベル
 376            subplot_fontsize : int
 377                サブプロットのフォントサイズ
 378            show_ci : bool
 379                信頼区間を表示するか
 380            ch4_ylim : tuple[float, float] | None
 381                CH4のy軸範囲
 382            c2h6_ylim : tuple[float, float] | None
 383                C2H6のy軸範囲
 384            start_date : str | None
 385                開始日(YYYY-MM-DD形式)
 386            end_date : str | None
 387                終了日(YYYY-MM-DD形式)
 388        """
 389        # 出力ディレクトリの作成
 390        os.makedirs(output_dir, exist_ok=True)
 391        output_path: str = os.path.join(output_dir, output_filename)
 392
 393        # データの準備
 394        df = df.copy()
 395        if not isinstance(df.index, pd.DatetimeIndex):
 396            df[col_datetime] = pd.to_datetime(df[col_datetime])
 397            df.set_index(col_datetime, inplace=True)
 398
 399        # 日付範囲の処理
 400        if start_date is not None:
 401            start_dt = pd.to_datetime(start_date)
 402            if start_dt < df.index.min():
 403                self.logger.warning(
 404                    f"指定された開始日{start_date}がデータの開始日{df.index.min():%Y-%m-%d}より前です。"
 405                    f"データの開始日を使用します。"
 406                )
 407                start_dt = df.index.min()
 408        else:
 409            start_dt = df.index.min()
 410
 411        if end_date is not None:
 412            end_dt = pd.to_datetime(end_date)
 413            if end_dt > df.index.max():
 414                self.logger.warning(
 415                    f"指定された終了日{end_date}がデータの終了日{df.index.max():%Y-%m-%d}より後です。"
 416                    f"データの終了日を使用します。"
 417                )
 418                end_dt = df.index.max()
 419        else:
 420            end_dt = df.index.max()
 421
 422        # 指定された期間のデータを抽出
 423        mask = (df.index >= start_dt) & (df.index <= end_dt)
 424        df = df[mask]
 425
 426        # CH4とC2H6の移動平均と信頼区間を計算
 427        ch4_mean, ch4_lower, ch4_upper = calculate_rolling_stats(
 428            df[col_ch4_flux], window_size, confidence_interval
 429        )
 430        c2h6_mean, c2h6_lower, c2h6_upper = calculate_rolling_stats(
 431            df[col_c2h6_flux], window_size, confidence_interval
 432        )
 433
 434        # プロットの作成
 435        fig, (ax1, ax2) = plt.subplots(1, 2, figsize=figsize)
 436
 437        # CH4プロット
 438        ax1.plot(df.index, ch4_mean, "red", label="CH$_4$")
 439        if show_ci:
 440            ax1.fill_between(df.index, ch4_lower, ch4_upper, color="red", alpha=0.2)
 441        if subplot_label_ch4:
 442            ax1.text(
 443                0.02,
 444                0.98,
 445                subplot_label_ch4,
 446                transform=ax1.transAxes,
 447                va="top",
 448                fontsize=subplot_fontsize,
 449            )
 450        ax1.set_ylabel("CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)")
 451        if ch4_ylim is not None:
 452            ax1.set_ylim(ch4_ylim)
 453        ax1.grid(True, alpha=0.3)
 454
 455        # C2H6プロット
 456        ax2.plot(df.index, c2h6_mean, "orange", label="C$_2$H$_6$")
 457        if show_ci:
 458            ax2.fill_between(
 459                df.index, c2h6_lower, c2h6_upper, color="orange", alpha=0.2
 460            )
 461        if subplot_label_c2h6:
 462            ax2.text(
 463                0.02,
 464                0.98,
 465                subplot_label_c2h6,
 466                transform=ax2.transAxes,
 467                va="top",
 468                fontsize=subplot_fontsize,
 469            )
 470        ax2.set_ylabel("C$_2$H$_6$ flux (nmol m$^{-2}$ s$^{-1}$)")
 471        if c2h6_ylim is not None:
 472            ax2.set_ylim(c2h6_ylim)
 473        ax2.grid(True, alpha=0.3)
 474
 475        # x軸の設定
 476        for ax in [ax1, ax2]:
 477            ax.set_xlabel("Month")
 478            # x軸の範囲を設定
 479            ax.set_xlim(start_dt, end_dt)
 480
 481            # 1ヶ月ごとの主目盛り
 482            ax.xaxis.set_major_locator(mdates.MonthLocator(interval=1))
 483
 484            # カスタムフォーマッタの作成(数字を通常フォントで表示)
 485            def date_formatter(x, p):
 486                date = mdates.num2date(x)
 487                return f"{date.strftime('%m')}"
 488
 489            ax.xaxis.set_major_formatter(plt.FuncFormatter(date_formatter))
 490
 491            # 補助目盛りの設定
 492            ax.xaxis.set_minor_locator(mdates.MonthLocator())
 493            # ティックラベルの回転と位置調整
 494            plt.setp(ax.xaxis.get_majorticklabels(), ha="right")
 495
 496        plt.tight_layout()
 497        plt.savefig(output_path, dpi=300, bbox_inches="tight")
 498        plt.close(fig)
 499
 500    def plot_ch4_flux_comparison(
 501        self,
 502        df: pd.DataFrame,
 503        output_dir: str,
 504        col_g2401_flux: str,
 505        col_ultra_flux: str,
 506        output_filename: str = "ch4_flux_comparison.png",
 507        col_datetime: str = "Date",
 508        window_size: int = 24 * 7,  # 1週間の移動平均のデフォルト値
 509        confidence_interval: float = 0.95,  # 95%信頼区間
 510        subplot_label: str | None = None,
 511        subplot_fontsize: int = 20,
 512        show_ci: bool = True,
 513        y_lim: tuple[float, float] | None = None,
 514        start_date: str | None = None,
 515        end_date: str | None = None,
 516        figsize: tuple[float, float] = (12, 6),
 517        legend_loc: str = "upper right",
 518    ) -> None:
 519        """G2401とUltraによるCH4フラックスの時系列比較プロット
 520
 521        Parameters:
 522        ------
 523            df : pd.DataFrame
 524                データフレーム
 525            output_dir : str
 526                出力ディレクトリのパス
 527            col_g2401_flux : str
 528                G2401のCH4フラックスのカラム名
 529            col_ultra_flux : str
 530                UltraのCH4フラックスのカラム名
 531            output_filename : str
 532                出力ファイル名
 533            col_datetime : str
 534                日時カラムの名前
 535            window_size : int
 536                移動平均の窓サイズ
 537            confidence_interval : float
 538                信頼区間(0-1)
 539            subplot_label : str | None
 540                プロットのラベル
 541            subplot_fontsize : int
 542                サブプロットのフォントサイズ
 543            show_ci : bool
 544                信頼区間を表示するか
 545            y_lim : tuple[float, float] | None
 546                y軸の範囲
 547            start_date : str | None
 548                開始日(YYYY-MM-DD形式)
 549            end_date : str | None
 550                終了日(YYYY-MM-DD形式)
 551            figsize : tuple[float, float]
 552                図のサイズ
 553            legend_loc : str
 554                凡例の位置
 555        """
 556        # 出力ディレクトリの作成
 557        os.makedirs(output_dir, exist_ok=True)
 558        output_path: str = os.path.join(output_dir, output_filename)
 559
 560        # データの準備
 561        df = df.copy()
 562        if not isinstance(df.index, pd.DatetimeIndex):
 563            df[col_datetime] = pd.to_datetime(df[col_datetime])
 564            df.set_index(col_datetime, inplace=True)
 565
 566        # 日付範囲の処理(既存のコードと同様)
 567        if start_date is not None:
 568            start_dt = pd.to_datetime(start_date)
 569            if start_dt < df.index.min():
 570                self.logger.warning(
 571                    f"指定された開始日{start_date}がデータの開始日{df.index.min():%Y-%m-%d}より前です。"
 572                    f"データの開始日を使用します。"
 573                )
 574                start_dt = df.index.min()
 575        else:
 576            start_dt = df.index.min()
 577
 578        if end_date is not None:
 579            end_dt = pd.to_datetime(end_date)
 580            if end_dt > df.index.max():
 581                self.logger.warning(
 582                    f"指定された終了日{end_date}がデータの終了日{df.index.max():%Y-%m-%d}より後です。"
 583                    f"データの終了日を使用します。"
 584                )
 585                end_dt = df.index.max()
 586        else:
 587            end_dt = df.index.max()
 588
 589        # 指定された期間のデータを抽出
 590        mask = (df.index >= start_dt) & (df.index <= end_dt)
 591        df = df[mask]
 592
 593        # 移動平均の計算(既存の関数を使用)
 594        g2401_mean, g2401_lower, g2401_upper = calculate_rolling_stats(
 595            df[col_g2401_flux], window_size, confidence_interval
 596        )
 597        ultra_mean, ultra_lower, ultra_upper = calculate_rolling_stats(
 598            df[col_ultra_flux], window_size, confidence_interval
 599        )
 600
 601        # プロットの作成
 602        fig, ax = plt.subplots(figsize=figsize)
 603
 604        # G2401データのプロット
 605        ax.plot(df.index, g2401_mean, "blue", label="G2401", alpha=0.7)
 606        if show_ci:
 607            ax.fill_between(df.index, g2401_lower, g2401_upper, color="blue", alpha=0.2)
 608
 609        # Ultraデータのプロット
 610        ax.plot(df.index, ultra_mean, "red", label="Ultra", alpha=0.7)
 611        if show_ci:
 612            ax.fill_between(df.index, ultra_lower, ultra_upper, color="red", alpha=0.2)
 613
 614        # プロットの設定
 615        if subplot_label:
 616            ax.text(
 617                0.02,
 618                0.98,
 619                subplot_label,
 620                transform=ax.transAxes,
 621                va="top",
 622                fontsize=subplot_fontsize,
 623            )
 624
 625        ax.set_ylabel("CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)")
 626        ax.set_xlabel("Month")
 627
 628        if y_lim is not None:
 629            ax.set_ylim(y_lim)
 630
 631        ax.grid(True, alpha=0.3)
 632        ax.legend(loc=legend_loc)
 633
 634        # x軸の設定
 635        ax.set_xlim(start_dt, end_dt)
 636        ax.xaxis.set_major_locator(mdates.MonthLocator(interval=1))
 637
 638        # カスタムフォーマッタの作成(数字を通常フォントで表示)
 639        def date_formatter(x, p):
 640            date = mdates.num2date(x)
 641            return f"{date.strftime('%m')}"
 642
 643        ax.xaxis.set_major_formatter(plt.FuncFormatter(date_formatter))
 644        ax.xaxis.set_minor_locator(mdates.MonthLocator())
 645        plt.setp(ax.xaxis.get_majorticklabels(), ha="right")
 646
 647        plt.tight_layout()
 648        plt.savefig(output_path, dpi=300, bbox_inches="tight")
 649        plt.close(fig)
 650
 651    def plot_c1c2_fluxes_diurnal_patterns(
 652        self,
 653        df: pd.DataFrame,
 654        y_cols_ch4: list[str],
 655        y_cols_c2h6: list[str],
 656        labels_ch4: list[str],
 657        labels_c2h6: list[str],
 658        colors_ch4: list[str],
 659        colors_c2h6: list[str],
 660        output_dir: str,
 661        output_filename: str = "diurnal.png",
 662        legend_only_ch4: bool = False,
 663        show_label: bool = True,
 664        show_legend: bool = True,
 665        show_std: bool = False,  # 標準偏差表示のオプションを追加
 666        std_alpha: float = 0.2,  # 標準偏差の透明度
 667        subplot_fontsize: int = 20,
 668        subplot_label_ch4: str | None = "(a)",
 669        subplot_label_c2h6: str | None = "(b)",
 670        ax1_ylim: tuple[float, float] | None = None,
 671        ax2_ylim: tuple[float, float] | None = None,
 672    ) -> None:
 673        """CH4とC2H6の日変化パターンを1つの図に並べてプロットする
 674
 675        Parameters:
 676        ------
 677            df : pd.DataFrame
 678                入力データフレーム。
 679            y_cols_ch4 : list[str]
 680                CH4のプロットに使用するカラム名のリスト。
 681            y_cols_c2h6 : list[str]
 682                C2H6のプロットに使用するカラム名のリスト。
 683            labels_ch4 : list[str]
 684                CH4の各ラインに対応するラベルのリスト。
 685            labels_c2h6 : list[str]
 686                C2H6の各ラインに対応するラベルのリスト。
 687            colors_ch4 : list[str]
 688                CH4の各ラインに使用する色のリスト。
 689            colors_c2h6 : list[str]
 690                C2H6の各ラインに使用する色のリスト。
 691            output_dir : str
 692                出力先ディレクトリのパス。
 693            output_filename : str, optional
 694                出力ファイル名。デフォルトは"diurnal.png"。
 695            legend_only_ch4 : bool, optional
 696                CH4の凡例のみを表示するかどうか。デフォルトはFalse。
 697            show_label : bool, optional
 698                サブプロットラベルを表示するかどうか。デフォルトはTrue。
 699            show_legend : bool, optional
 700                凡例を表示するかどうか。デフォルトはTrue。
 701            show_std : bool, optional
 702                標準偏差を表示するかどうか。デフォルトはFalse。
 703            std_alpha : float, optional
 704                標準偏差の透明度。デフォルトは0.2。
 705            subplot_fontsize : int, optional
 706                サブプロットのフォントサイズ。デフォルトは20。
 707            subplot_label_ch4 : str | None, optional
 708                CH4プロットのラベル。デフォルトは"(a)"。
 709            subplot_label_c2h6 : str | None, optional
 710                C2H6プロットのラベル。デフォルトは"(b)"。
 711            ax1_ylim : tuple[float, float] | None, optional
 712                CH4プロットのy軸の範囲。デフォルトはNone。
 713            ax2_ylim : tuple[float, float] | None, optional
 714                C2H6プロットのy軸の範囲。デフォルトはNone。
 715        """
 716        os.makedirs(output_dir, exist_ok=True)
 717        output_path: str = os.path.join(output_dir, output_filename)
 718
 719        # データの準備
 720        target_columns = y_cols_ch4 + y_cols_c2h6
 721        hourly_means, time_points = self._prepare_diurnal_data(df, target_columns)
 722
 723        # 標準偏差の計算を追加
 724        hourly_stds = {}
 725        if show_std:
 726            hourly_stds = df.groupby(df.index.hour)[target_columns].std()
 727            # 24時間目のデータ点を追加
 728            last_hour = hourly_stds.iloc[0:1].copy()
 729            last_hour.index = [24]
 730            hourly_stds = pd.concat([hourly_stds, last_hour])
 731
 732        # プロットの作成
 733        fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
 734
 735        # CH4のプロット (左側)
 736        ch4_lines = []
 737        for y_col, label, color in zip(y_cols_ch4, labels_ch4, colors_ch4):
 738            mean_values = hourly_means["all"][y_col]
 739            line = ax1.plot(
 740                time_points,
 741                mean_values,
 742                "-o",
 743                label=label,
 744                color=color,
 745            )
 746            ch4_lines.extend(line)
 747
 748            # 標準偏差の表示
 749            if show_std:
 750                std_values = hourly_stds[y_col]
 751                ax1.fill_between(
 752                    time_points,
 753                    mean_values - std_values,
 754                    mean_values + std_values,
 755                    color=color,
 756                    alpha=std_alpha,
 757                )
 758
 759        # C2H6のプロット (右側)
 760        c2h6_lines = []
 761        for y_col, label, color in zip(y_cols_c2h6, labels_c2h6, colors_c2h6):
 762            mean_values = hourly_means["all"][y_col]
 763            line = ax2.plot(
 764                time_points,
 765                mean_values,
 766                "o-",
 767                label=label,
 768                color=color,
 769            )
 770            c2h6_lines.extend(line)
 771
 772            # 標準偏差の表示
 773            if show_std:
 774                std_values = hourly_stds[y_col]
 775                ax2.fill_between(
 776                    time_points,
 777                    mean_values - std_values,
 778                    mean_values + std_values,
 779                    color=color,
 780                    alpha=std_alpha,
 781                )
 782
 783        # 軸の設定
 784        for ax, ylabel, subplot_label in [
 785            (ax1, r"CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)", subplot_label_ch4),
 786            (ax2, r"C$_2$H$_6$ flux (nmol m$^{-2}$ s$^{-1}$)", subplot_label_c2h6),
 787        ]:
 788            self._setup_diurnal_axes(
 789                ax=ax,
 790                time_points=time_points,
 791                ylabel=ylabel,
 792                subplot_label=subplot_label,
 793                show_label=show_label,
 794                show_legend=False,  # 個別の凡例は表示しない
 795                subplot_fontsize=subplot_fontsize,
 796            )
 797
 798        if ax1_ylim is not None:
 799            ax1.set_ylim(ax1_ylim)
 800        ax1.yaxis.set_major_locator(MultipleLocator(20))
 801        ax1.yaxis.set_major_formatter(FuncFormatter(lambda x, p: f"{x:.0f}"))
 802
 803        if ax2_ylim is not None:
 804            ax2.set_ylim(ax2_ylim)
 805        ax2.yaxis.set_major_locator(MultipleLocator(1))
 806        ax2.yaxis.set_major_formatter(FuncFormatter(lambda x, p: f"{x:.1f}"))
 807
 808        plt.tight_layout()
 809
 810        # 共通の凡例
 811        if show_legend:
 812            all_lines = ch4_lines
 813            all_labels = [line.get_label() for line in ch4_lines]
 814            if not legend_only_ch4:
 815                all_lines += c2h6_lines
 816                all_labels += [line.get_label() for line in c2h6_lines]
 817            fig.legend(
 818                all_lines,
 819                all_labels,
 820                loc="center",
 821                bbox_to_anchor=(0.5, 0.02),
 822                ncol=len(all_lines),
 823            )
 824            plt.subplots_adjust(bottom=0.25)  # 下部に凡例用のスペースを確保
 825
 826        fig.savefig(output_path, dpi=300, bbox_inches="tight")
 827        plt.close(fig)
 828
 829    def plot_c1c2_fluxes_diurnal_patterns_by_date(
 830        self,
 831        df: pd.DataFrame,
 832        y_col_ch4: str,
 833        y_col_c2h6: str,
 834        output_dir: str,
 835        output_filename: str = "diurnal_by_date.png",
 836        plot_all: bool = True,
 837        plot_weekday: bool = True,
 838        plot_weekend: bool = True,
 839        plot_holiday: bool = True,
 840        show_label: bool = True,
 841        show_legend: bool = True,
 842        show_std: bool = False,  # 標準偏差表示のオプションを追加
 843        std_alpha: float = 0.2,  # 標準偏差の透明度
 844        legend_only_ch4: bool = False,
 845        subplot_fontsize: int = 20,
 846        subplot_label_ch4: str | None = "(a)",
 847        subplot_label_c2h6: str | None = "(b)",
 848        ax1_ylim: tuple[float, float] | None = None,
 849        ax2_ylim: tuple[float, float] | None = None,
 850        print_summary: bool = True,  # 追加: 統計情報を表示するかどうか
 851    ) -> None:
 852        """CH4とC2H6の日変化パターンを日付分類して1つの図に並べてプロットする
 853
 854        Parameters:
 855        ------
 856            df : pd.DataFrame
 857                入力データフレーム。
 858            y_col_ch4 : str
 859                CH4フラックスを含むカラム名。
 860            y_col_c2h6 : str
 861                C2H6フラックスを含むカラム名。
 862            output_dir : str
 863                出力先ディレクトリのパス。
 864            output_filename : str, optional
 865                出力ファイル名。デフォルトは"diurnal_by_date.png"。
 866            plot_all : bool, optional
 867                すべての日をプロットするかどうか。デフォルトはTrue。
 868            plot_weekday : bool, optional
 869                平日をプロットするかどうか。デフォルトはTrue。
 870            plot_weekend : bool, optional
 871                週末をプロットするかどうか。デフォルトはTrue。
 872            plot_holiday : bool, optional
 873                祝日をプロットするかどうか。デフォルトはTrue。
 874            show_label : bool, optional
 875                サブプロットラベルを表示するかどうか。デフォルトはTrue。
 876            show_legend : bool, optional
 877                凡例を表示するかどうか。デフォルトはTrue。
 878            show_std : bool, optional
 879                標準偏差を表示するかどうか。デフォルトはFalse。
 880            std_alpha : float, optional
 881                標準偏差の透明度。デフォルトは0.2。
 882            legend_only_ch4 : bool, optional
 883                CH4の凡例のみを表示するかどうか。デフォルトはFalse。
 884            subplot_fontsize : int, optional
 885                サブプロットのフォントサイズ。デフォルトは20。
 886            subplot_label_ch4 : str | None, optional
 887                CH4プロットのラベル。デフォルトは"(a)"。
 888            subplot_label_c2h6 : str | None, optional
 889                C2H6プロットのラベル。デフォルトは"(b)"。
 890            ax1_ylim : tuple[float, float] | None, optional
 891                CH4プロットのy軸の範囲。デフォルトはNone。
 892            ax2_ylim : tuple[float, float] | None, optional
 893                C2H6プロットのy軸の範囲。デフォルトはNone。
 894            print_summary : bool, optional
 895                統計情報を表示するかどうか。デフォルトはTrue。
 896        """
 897        os.makedirs(output_dir, exist_ok=True)
 898        output_path: str = os.path.join(output_dir, output_filename)
 899
 900        # データの準備
 901        target_columns = [y_col_ch4, y_col_c2h6]
 902        hourly_means, time_points = self._prepare_diurnal_data(
 903            df, target_columns, include_date_types=True
 904        )
 905
 906        # 標準偏差の計算を追加
 907        hourly_stds = {}
 908        if show_std:
 909            for condition in ["all", "weekday", "weekend", "holiday"]:
 910                if condition == "all":
 911                    condition_data = df
 912                elif condition == "weekday":
 913                    condition_data = df[
 914                        ~(
 915                            df.index.dayofweek.isin([5, 6])
 916                            | df.index.map(lambda x: jpholiday.is_holiday(x.date()))
 917                        )
 918                    ]
 919                elif condition == "weekend":
 920                    condition_data = df[df.index.dayofweek.isin([5, 6])]
 921                else:  # holiday
 922                    condition_data = df[
 923                        df.index.map(lambda x: jpholiday.is_holiday(x.date()))
 924                    ]
 925
 926                hourly_stds[condition] = condition_data.groupby(
 927                    condition_data.index.hour
 928                )[target_columns].std()
 929                # 24時間目のデータ点を追加
 930                last_hour = hourly_stds[condition].iloc[0:1].copy()
 931                last_hour.index = [24]
 932                hourly_stds[condition] = pd.concat([hourly_stds[condition], last_hour])
 933
 934        # プロットスタイルの設定
 935        styles = {
 936            "all": {
 937                "color": "black",
 938                "linestyle": "-",
 939                "alpha": 1.0,
 940                "label": "All days",
 941            },
 942            "weekday": {
 943                "color": "blue",
 944                "linestyle": "-",
 945                "alpha": 0.8,
 946                "label": "Weekdays",
 947            },
 948            "weekend": {
 949                "color": "red",
 950                "linestyle": "-",
 951                "alpha": 0.8,
 952                "label": "Weekends",
 953            },
 954            "holiday": {
 955                "color": "green",
 956                "linestyle": "-",
 957                "alpha": 0.8,
 958                "label": "Weekends & Holidays",
 959            },
 960        }
 961
 962        # プロット対象の条件を選択
 963        plot_conditions = {
 964            "all": plot_all,
 965            "weekday": plot_weekday,
 966            "weekend": plot_weekend,
 967            "holiday": plot_holiday,
 968        }
 969        selected_conditions = {
 970            col: means
 971            for col, means in hourly_means.items()
 972            if col in plot_conditions and plot_conditions[col]
 973        }
 974
 975        # プロットの作成
 976        fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
 977
 978        # CH4とC2H6のプロット用のラインオブジェクトを保存
 979        ch4_lines = []
 980        c2h6_lines = []
 981
 982        # CH4とC2H6のプロット
 983        for condition, means in selected_conditions.items():
 984            style = styles[condition].copy()
 985
 986            # CH4プロット
 987            mean_values_ch4 = means[y_col_ch4]
 988            line_ch4 = ax1.plot(time_points, mean_values_ch4, marker="o", **style)
 989            ch4_lines.extend(line_ch4)
 990
 991            if show_std and condition in hourly_stds:
 992                std_values = hourly_stds[condition][y_col_ch4]
 993                ax1.fill_between(
 994                    time_points,
 995                    mean_values_ch4 - std_values,
 996                    mean_values_ch4 + std_values,
 997                    color=style["color"],
 998                    alpha=std_alpha,
 999                )
1000
1001            # C2H6プロット
1002            style["linestyle"] = "--"
1003            mean_values_c2h6 = means[y_col_c2h6]
1004            line_c2h6 = ax2.plot(time_points, mean_values_c2h6, marker="o", **style)
1005            c2h6_lines.extend(line_c2h6)
1006
1007            if show_std and condition in hourly_stds:
1008                std_values = hourly_stds[condition][y_col_c2h6]
1009                ax2.fill_between(
1010                    time_points,
1011                    mean_values_c2h6 - std_values,
1012                    mean_values_c2h6 + std_values,
1013                    color=style["color"],
1014                    alpha=std_alpha,
1015                )
1016
1017        # 軸の設定
1018        for ax, ylabel, subplot_label in [
1019            (ax1, r"CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)", subplot_label_ch4),
1020            (ax2, r"C$_2$H$_6$ flux (nmol m$^{-2}$ s$^{-1}$)", subplot_label_c2h6),
1021        ]:
1022            self._setup_diurnal_axes(
1023                ax=ax,
1024                time_points=time_points,
1025                ylabel=ylabel,
1026                subplot_label=subplot_label,
1027                show_label=show_label,
1028                show_legend=False,
1029                subplot_fontsize=subplot_fontsize,
1030            )
1031
1032        if ax1_ylim is not None:
1033            ax1.set_ylim(ax1_ylim)
1034        ax1.yaxis.set_major_locator(MultipleLocator(20))
1035        ax1.yaxis.set_major_formatter(FuncFormatter(lambda x, p: f"{x:.0f}"))
1036
1037        if ax2_ylim is not None:
1038            ax2.set_ylim(ax2_ylim)
1039        ax2.yaxis.set_major_locator(MultipleLocator(1))
1040        ax2.yaxis.set_major_formatter(FuncFormatter(lambda x, p: f"{x:.1f}"))
1041
1042        plt.tight_layout()
1043
1044        # 共通の凡例を図の下部に配置
1045        if show_legend:
1046            lines_to_show = (
1047                ch4_lines if legend_only_ch4 else ch4_lines[: len(selected_conditions)]
1048            )
1049            fig.legend(
1050                lines_to_show,
1051                [
1052                    style["label"]
1053                    for style in list(styles.values())[: len(lines_to_show)]
1054                ],
1055                loc="center",
1056                bbox_to_anchor=(0.5, 0.02),
1057                ncol=len(lines_to_show),
1058            )
1059            plt.subplots_adjust(bottom=0.25)  # 下部に凡例用のスペースを確保
1060
1061        fig.savefig(output_path, dpi=300, bbox_inches="tight")
1062        plt.close(fig)
1063
1064        # 日変化パターンの統計分析を追加
1065        if print_summary:
1066            # 平日と休日のデータを準備
1067            dates = pd.to_datetime(df.index)
1068            is_weekend = dates.dayofweek.isin([5, 6])
1069            is_holiday = dates.map(lambda x: jpholiday.is_holiday(x.date()))
1070            is_weekday = ~(is_weekend | is_holiday)
1071
1072            weekday_data = df[is_weekday]
1073            holiday_data = df[is_weekend | is_holiday]
1074
1075            def get_diurnal_stats(data, column):
1076                # 時間ごとの平均値を計算
1077                hourly_means = data.groupby(data.index.hour)[column].mean()
1078
1079                # 8-16時の時間帯の統計
1080                daytime_means = hourly_means[
1081                    (hourly_means.index >= 8) & (hourly_means.index <= 16)
1082                ]
1083
1084                if len(daytime_means) == 0:
1085                    return None
1086
1087                return {
1088                    "mean": daytime_means.mean(),
1089                    "max": daytime_means.max(),
1090                    "max_hour": daytime_means.idxmax(),
1091                    "min": daytime_means.min(),
1092                    "min_hour": daytime_means.idxmin(),
1093                    "hours_count": len(daytime_means),
1094                }
1095
1096            # CH4とC2H6それぞれの統計を計算
1097            for col, gas_name in [(y_col_ch4, "CH4"), (y_col_c2h6, "C2H6")]:
1098                print(f"\n=== {gas_name} フラックス 8-16時の統計分析 ===")
1099
1100                weekday_stats = get_diurnal_stats(weekday_data, col)
1101                holiday_stats = get_diurnal_stats(holiday_data, col)
1102
1103                if weekday_stats and holiday_stats:
1104                    print("\n平日:")
1105                    print(f"  平均値: {weekday_stats['mean']:.2f}")
1106                    print(
1107                        f"  最大値: {weekday_stats['max']:.2f} ({weekday_stats['max_hour']}時)"
1108                    )
1109                    print(
1110                        f"  最小値: {weekday_stats['min']:.2f} ({weekday_stats['min_hour']}時)"
1111                    )
1112                    print(f"  集計時間数: {weekday_stats['hours_count']}")
1113
1114                    print("\n休日:")
1115                    print(f"  平均値: {holiday_stats['mean']:.2f}")
1116                    print(
1117                        f"  最大値: {holiday_stats['max']:.2f} ({holiday_stats['max_hour']}時)"
1118                    )
1119                    print(
1120                        f"  最小値: {holiday_stats['min']:.2f} ({holiday_stats['min_hour']}時)"
1121                    )
1122                    print(f"  集計時間数: {holiday_stats['hours_count']}")
1123
1124                    # 平日/休日の比率を計算
1125                    print("\n平日/休日の比率:")
1126                    print(
1127                        f"  平均値比: {weekday_stats['mean'] / holiday_stats['mean']:.2f}"
1128                    )
1129                    print(
1130                        f"  最大値比: {weekday_stats['max'] / holiday_stats['max']:.2f}"
1131                    )
1132                    print(
1133                        f"  最小値比: {weekday_stats['min'] / holiday_stats['min']:.2f}"
1134                    )
1135                else:
1136                    print("十分なデータがありません")
1137
1138    def plot_diurnal_concentrations(
1139        self,
1140        df: pd.DataFrame,
1141        output_dir: str,
1142        col_ch4_conc: str = "CH4_ultra_cal",
1143        col_c2h6_conc: str = "C2H6_ultra_cal",
1144        col_datetime: str = "Date",
1145        output_filename: str = "diurnal_concentrations.png",
1146        show_std: bool = True,
1147        alpha_std: float = 0.2,
1148        add_legend: bool = True,  # 凡例表示のオプションを追加
1149        print_summary: bool = True,
1150        subplot_label_ch4: str | None = None,
1151        subplot_label_c2h6: str | None = None,
1152        subplot_fontsize: int = 24,
1153        ch4_ylim: tuple[float, float] | None = None,
1154        c2h6_ylim: tuple[float, float] | None = None,
1155        interval: str = "1H",  # "30min" または "1H" を指定
1156    ) -> None:
1157        """CH4とC2H6の濃度の日内変動を描画する
1158
1159        Parameters:
1160        ------
1161            df : pd.DataFrame
1162                濃度データを含むDataFrame
1163            output_dir : str
1164                出力ディレクトリのパス
1165            col_ch4_conc : str
1166                CH4濃度のカラム名
1167            col_c2h6_conc : str
1168                C2H6濃度のカラム名
1169            col_datetime : str
1170                日時カラム名
1171            output_filename : str
1172                出力ファイル名
1173            show_std : bool
1174                標準偏差を表示するかどうか
1175            alpha_std : float
1176                標準偏差の透明度
1177            add_legend : bool
1178                凡例を追加するかどうか
1179            print_summary : bool
1180                統計情報を表示するかどうか
1181            subplot_label_ch4 : str | None
1182                CH4プロットのラベル
1183            subplot_label_c2h6 : str | None
1184                C2H6プロットのラベル
1185            subplot_fontsize : int
1186                サブプロットのフォントサイズ
1187            ch4_ylim : tuple[float, float] | None
1188                CH4のy軸範囲
1189            c2h6_ylim : tuple[float, float] | None
1190                C2H6のy軸範囲
1191            interval : str
1192                時間間隔。"30min"または"1H"を指定
1193        """
1194        # 出力ディレクトリの作成
1195        os.makedirs(output_dir, exist_ok=True)
1196        output_path: str = os.path.join(output_dir, output_filename)
1197
1198        # データの準備
1199        df = df.copy()
1200        if interval == "30min":
1201            # 30分間隔の場合、時間と30分を別々に取得
1202            df["hour"] = pd.to_datetime(df[col_datetime]).dt.hour
1203            df["minute"] = pd.to_datetime(df[col_datetime]).dt.minute
1204            df["time_bin"] = df["hour"] + df["minute"].map({0: 0, 30: 0.5})
1205        else:
1206            # 1時間間隔の場合
1207            df["time_bin"] = pd.to_datetime(df[col_datetime]).dt.hour
1208
1209        # 時間ごとの平均値と標準偏差を計算
1210        hourly_stats = df.groupby("time_bin")[[col_ch4_conc, col_c2h6_conc]].agg(
1211            ["mean", "std"]
1212        )
1213
1214        # 最後のデータポイントを追加(最初のデータを使用)
1215        last_point = hourly_stats.iloc[0:1].copy()
1216        last_point.index = [
1217            hourly_stats.index[-1] + (0.5 if interval == "30min" else 1)
1218        ]
1219        hourly_stats = pd.concat([hourly_stats, last_point])
1220
1221        # 時間軸の作成
1222        if interval == "30min":
1223            time_points = pd.date_range("2024-01-01", periods=49, freq="30min")
1224            x_ticks = [0, 6, 12, 18, 24]  # 主要な時間のティック
1225        else:
1226            time_points = pd.date_range("2024-01-01", periods=25, freq="1H")
1227            x_ticks = [0, 6, 12, 18, 24]
1228
1229        # プロットの作成
1230        fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
1231
1232        # CH4濃度プロット
1233        mean_ch4 = hourly_stats[col_ch4_conc]["mean"]
1234        if show_std:
1235            std_ch4 = hourly_stats[col_ch4_conc]["std"]
1236            ax1.fill_between(
1237                time_points,
1238                mean_ch4 - std_ch4,
1239                mean_ch4 + std_ch4,
1240                color="red",
1241                alpha=alpha_std,
1242            )
1243        ch4_line = ax1.plot(time_points, mean_ch4, "red", label="CH$_4$")[0]
1244
1245        ax1.set_ylabel("CH$_4$ (ppm)")
1246        if ch4_ylim is not None:
1247            ax1.set_ylim(ch4_ylim)
1248        if subplot_label_ch4:
1249            ax1.text(
1250                0.02,
1251                0.98,
1252                subplot_label_ch4,
1253                transform=ax1.transAxes,
1254                va="top",
1255                fontsize=subplot_fontsize,
1256            )
1257
1258        # C2H6濃度プロット
1259        mean_c2h6 = hourly_stats[col_c2h6_conc]["mean"]
1260        if show_std:
1261            std_c2h6 = hourly_stats[col_c2h6_conc]["std"]
1262            ax2.fill_between(
1263                time_points,
1264                mean_c2h6 - std_c2h6,
1265                mean_c2h6 + std_c2h6,
1266                color="orange",
1267                alpha=alpha_std,
1268            )
1269        c2h6_line = ax2.plot(time_points, mean_c2h6, "orange", label="C$_2$H$_6$")[0]
1270
1271        ax2.set_ylabel("C$_2$H$_6$ (ppb)")
1272        if c2h6_ylim is not None:
1273            ax2.set_ylim(c2h6_ylim)
1274        if subplot_label_c2h6:
1275            ax2.text(
1276                0.02,
1277                0.98,
1278                subplot_label_c2h6,
1279                transform=ax2.transAxes,
1280                va="top",
1281                fontsize=subplot_fontsize,
1282            )
1283
1284        # 両プロットの共通設定
1285        for ax in [ax1, ax2]:
1286            ax.set_xlabel("Time (hour)")
1287            ax.xaxis.set_major_formatter(mdates.DateFormatter("%-H"))
1288            ax.xaxis.set_major_locator(mdates.HourLocator(byhour=x_ticks))
1289            ax.set_xlim(time_points[0], time_points[-1])
1290            # 1時間ごとの縦線を表示
1291            ax.grid(True, which="major", alpha=0.3)
1292            # 補助目盛りは表示するが、グリッド線は表示しない
1293            # if interval == "30min":
1294            #     ax.xaxis.set_minor_locator(mdates.MinuteLocator(byminute=[30]))
1295            #     ax.tick_params(which='minor', length=4)
1296
1297        # 共通の凡例を図の下部に配置
1298        if add_legend:
1299            fig.legend(
1300                [ch4_line, c2h6_line],
1301                ["CH$_4$", "C$_2$H$_6$"],
1302                loc="center",
1303                bbox_to_anchor=(0.5, 0.02),
1304                ncol=2,
1305            )
1306        plt.subplots_adjust(bottom=0.2)
1307
1308        plt.tight_layout()
1309        plt.savefig(output_path, dpi=300, bbox_inches="tight")
1310        plt.close(fig)
1311
1312        if print_summary:
1313            # 統計情報の表示
1314            for name, col in [("CH4", col_ch4_conc), ("C2H6", col_c2h6_conc)]:
1315                stats = hourly_stats[col]
1316                mean_vals = stats["mean"]
1317
1318                print(f"\n{name}濃度の日内変動統計:")
1319                print(f"最小値: {mean_vals.min():.3f} (Hour: {mean_vals.idxmin()})")
1320                print(f"最大値: {mean_vals.max():.3f} (Hour: {mean_vals.idxmax()})")
1321                print(f"平均値: {mean_vals.mean():.3f}")
1322                print(f"日内変動幅: {mean_vals.max() - mean_vals.min():.3f}")
1323                print(f"最大/最小比: {mean_vals.max() / mean_vals.min():.3f}")
1324
1325    def plot_flux_diurnal_patterns_with_std(
1326        self,
1327        df: pd.DataFrame,
1328        output_dir: str,
1329        col_ch4_flux: str = "Fch4",
1330        col_c2h6_flux: str = "Fc2h6",
1331        ch4_label: str = r"$\mathregular{CH_{4}}$フラックス",
1332        c2h6_label: str = r"$\mathregular{C_{2}H_{6}}$フラックス",
1333        col_datetime: str = "Date",
1334        output_filename: str = "diurnal_patterns.png",
1335        window_size: int = 6,  # 移動平均の窓サイズ
1336        show_std: bool = True,  # 標準偏差の表示有無
1337        alpha_std: float = 0.1,  # 標準偏差の透明度
1338    ) -> None:
1339        """CH4とC2H6フラックスの日変化パターンをプロットする
1340
1341        Parameters:
1342        ------
1343            df : pd.DataFrame
1344                データフレーム
1345            output_dir : str
1346                出力ディレクトリのパス
1347            col_ch4_flux : str
1348                CH4フラックスのカラム名
1349            col_c2h6_flux : str
1350                C2H6フラックスのカラム名
1351            ch4_label : str
1352                CH4フラックスのラベル
1353            c2h6_label : str
1354                C2H6フラックスのラベル
1355            col_datetime : str
1356                日時カラムの名前
1357            output_filename : str
1358                出力ファイル名
1359            window_size : int
1360                移動平均の窓サイズ(デフォルト6)
1361            show_std : bool
1362                標準偏差を表示するかどうか
1363            alpha_std : float
1364                標準偏差の透明度(0-1)
1365        """
1366        # 出力ディレクトリの作成
1367        os.makedirs(output_dir, exist_ok=True)
1368        output_path: str = os.path.join(output_dir, output_filename)
1369
1370        # # プロットのスタイル設定
1371        # plt.rcParams.update({
1372        #     'font.size': 20,
1373        #     'axes.labelsize': 20,
1374        #     'axes.titlesize': 20,
1375        #     'xtick.labelsize': 20,
1376        #     'ytick.labelsize': 20,
1377        #     'legend.fontsize': 20,
1378        # })
1379
1380        # 日時インデックスの処理
1381        df = df.copy()
1382        if not isinstance(df.index, pd.DatetimeIndex):
1383            df[col_datetime] = pd.to_datetime(df[col_datetime])
1384            df.set_index(col_datetime, inplace=True)
1385
1386        # 時刻データの抽出とグループ化
1387        df["hour"] = df.index.hour
1388        hourly_means = df.groupby("hour")[[col_ch4_flux, col_c2h6_flux]].agg(
1389            ["mean", "std"]
1390        )
1391
1392        # 24時間目のデータ点を追加(0時のデータを使用)
1393        last_hour = hourly_means.iloc[0:1].copy()
1394        last_hour.index = [24]
1395        hourly_means = pd.concat([hourly_means, last_hour])
1396
1397        # 24時間分のデータポイントを作成
1398        time_points = pd.date_range("2024-01-01", periods=25, freq="h")
1399
1400        # プロットの作成
1401        fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
1402
1403        # 移動平均の計算と描画
1404        ch4_mean = (
1405            hourly_means[(col_ch4_flux, "mean")]
1406            .rolling(window=window_size, center=True, min_periods=1)
1407            .mean()
1408        )
1409        c2h6_mean = (
1410            hourly_means[(col_c2h6_flux, "mean")]
1411            .rolling(window=window_size, center=True, min_periods=1)
1412            .mean()
1413        )
1414
1415        if show_std:
1416            ch4_std = (
1417                hourly_means[(col_ch4_flux, "std")]
1418                .rolling(window=window_size, center=True, min_periods=1)
1419                .mean()
1420            )
1421            c2h6_std = (
1422                hourly_means[(col_c2h6_flux, "std")]
1423                .rolling(window=window_size, center=True, min_periods=1)
1424                .mean()
1425            )
1426
1427            ax1.fill_between(
1428                time_points,
1429                ch4_mean - ch4_std,
1430                ch4_mean + ch4_std,
1431                color="blue",
1432                alpha=alpha_std,
1433            )
1434            ax2.fill_between(
1435                time_points,
1436                c2h6_mean - c2h6_std,
1437                c2h6_mean + c2h6_std,
1438                color="red",
1439                alpha=alpha_std,
1440            )
1441
1442        # メインのラインプロット
1443        ax1.plot(time_points, ch4_mean, "blue", label=ch4_label)
1444        ax2.plot(time_points, c2h6_mean, "red", label=c2h6_label)
1445
1446        # 軸の設定
1447        for ax, ylabel in [
1448            (ax1, r"CH$_4$ (nmol m$^{-2}$ s$^{-1}$)"),
1449            (ax2, r"C$_2$H$_6$ (nmol m$^{-2}$ s$^{-1}$)"),
1450        ]:
1451            ax.set_xlabel("Time")
1452            ax.set_ylabel(ylabel)
1453            ax.xaxis.set_major_formatter(mdates.DateFormatter("%-H"))
1454            ax.xaxis.set_major_locator(mdates.HourLocator(byhour=[0, 6, 12, 18, 24]))
1455            ax.set_xlim(time_points[0], time_points[-1])
1456            ax.grid(True, alpha=0.3)
1457            ax.legend()
1458
1459        # グラフの保存
1460        plt.tight_layout()
1461        plt.savefig(output_path, dpi=300, bbox_inches="tight")
1462        plt.close()
1463
1464        # 統計情報の表示(オプション)
1465        for col, name in [(col_ch4_flux, "CH4"), (col_c2h6_flux, "C2H6")]:
1466            mean_val = hourly_means[(col, "mean")].mean()
1467            min_val = hourly_means[(col, "mean")].min()
1468            max_val = hourly_means[(col, "mean")].max()
1469            min_time = hourly_means[(col, "mean")].idxmin()
1470            max_time = hourly_means[(col, "mean")].idxmax()
1471
1472            self.logger.info(f"{name} Statistics:")
1473            self.logger.info(f"Mean: {mean_val:.2f}")
1474            self.logger.info(f"Min: {min_val:.2f} (Hour: {min_time})")
1475            self.logger.info(f"Max: {max_val:.2f} (Hour: {max_time})")
1476            self.logger.info(f"Max/Min ratio: {max_val / min_val:.2f}\n")
1477
1478    def plot_scatter(
1479        self,
1480        df: pd.DataFrame,
1481        x_col: str,
1482        y_col: str,
1483        output_dir: str,
1484        output_filename: str = "scatter.png",
1485        xlabel: str | None = None,
1486        ylabel: str | None = None,
1487        show_label: bool = True,
1488        x_axis_range: tuple | None = None,
1489        y_axis_range: tuple | None = None,
1490        fixed_slope: float = 0.076,
1491        show_fixed_slope: bool = False,
1492        x_scientific: bool = False,  # 追加:x軸を指数表記にするかどうか
1493        y_scientific: bool = False,  # 追加:y軸を指数表記にするかどうか
1494    ) -> None:
1495        """散布図を作成し、TLS回帰直線を描画します。
1496
1497        Parameters:
1498        ------
1499            df : pd.DataFrame
1500                プロットに使用するデータフレーム
1501            x_col : str
1502                x軸に使用する列名
1503            y_col : str
1504                y軸に使用する列名
1505            xlabel : str
1506                x軸のラベル
1507            ylabel : str
1508                y軸のラベル
1509            output_dir : str
1510                出力先ディレクトリ
1511            output_filename : str, optional
1512                出力ファイル名。デフォルトは"scatter.png"
1513            show_label : bool, optional
1514                軸ラベルを表示するかどうか。デフォルトはTrue
1515            x_axis_range : tuple, optional
1516                x軸の範囲。デフォルトはNone。
1517            y_axis_range : tuple, optional
1518                y軸の範囲。デフォルトはNone。
1519            fixed_slope : float, optional
1520                固定傾きを指定するための値。デフォルトは0.076
1521            show_fixed_slope : bool, optional
1522                固定傾きの線を表示するかどうか。デフォルトはFalse
1523        """
1524        os.makedirs(output_dir, exist_ok=True)
1525        output_path: str = os.path.join(output_dir, output_filename)
1526
1527        # 有効なデータの抽出
1528        df = MonthlyFiguresGenerator.get_valid_data(df, x_col, y_col)
1529
1530        # データの準備
1531        x = df[x_col].values
1532        y = df[y_col].values
1533
1534        # データの中心化
1535        x_mean = np.mean(x)
1536        y_mean = np.mean(y)
1537        x_c = x - x_mean
1538        y_c = y - y_mean
1539
1540        # TLS回帰の計算
1541        data_matrix = np.vstack((x_c, y_c))
1542        cov_matrix = np.cov(data_matrix)
1543        _, eigenvecs = linalg.eigh(cov_matrix)
1544        largest_eigenvec = eigenvecs[:, -1]
1545
1546        slope = largest_eigenvec[1] / largest_eigenvec[0]
1547        intercept = y_mean - slope * x_mean
1548
1549        # R²とRMSEの計算
1550        y_pred = slope * x + intercept
1551        r_squared = 1 - np.sum((y - y_pred) ** 2) / np.sum((y - np.mean(y)) ** 2)
1552        rmse = np.sqrt(np.mean((y - y_pred) ** 2))
1553
1554        # プロットの作成
1555        fig, ax = plt.subplots(figsize=(6, 6))
1556
1557        # データ点のプロット
1558        ax.scatter(x, y, color="black")
1559
1560        # データの範囲を取得
1561        if x_axis_range is None:
1562            x_axis_range = (df[x_col].min(), df[x_col].max())
1563        if y_axis_range is None:
1564            y_axis_range = (df[y_col].min(), df[y_col].max())
1565
1566        # 回帰直線のプロット
1567        x_range = np.linspace(x_axis_range[0], x_axis_range[1], 150)
1568        y_range = slope * x_range + intercept
1569        ax.plot(x_range, y_range, "r", label="TLS regression")
1570
1571        # 傾き固定の線を追加(フラグがTrueの場合)
1572        if show_fixed_slope:
1573            fixed_intercept = (
1574                y_mean - fixed_slope * x_mean
1575            )  # 中心点を通るように切片を計算
1576            y_fixed = fixed_slope * x_range + fixed_intercept
1577            ax.plot(x_range, y_fixed, "b--", label=f"Slope = {fixed_slope}", alpha=0.7)
1578
1579        # 軸の設定
1580        ax.set_xlim(x_axis_range)
1581        ax.set_ylim(y_axis_range)
1582
1583        # 指数表記の設定
1584        if x_scientific:
1585            ax.ticklabel_format(style="sci", axis="x", scilimits=(0, 0))
1586            ax.xaxis.get_offset_text().set_position((1.1, 0))  # 指数の位置調整
1587        if y_scientific:
1588            ax.ticklabel_format(style="sci", axis="y", scilimits=(0, 0))
1589            ax.yaxis.get_offset_text().set_position((0, 1.1))  # 指数の位置調整
1590
1591        if show_label:
1592            if xlabel is not None:
1593                ax.set_xlabel(xlabel)
1594            if ylabel is not None:
1595                ax.set_ylabel(ylabel)
1596
1597        # 1:1の関係を示す点線(軸の範囲が同じ場合のみ表示)
1598        if (
1599            x_axis_range is not None
1600            and y_axis_range is not None
1601            and x_axis_range == y_axis_range
1602        ):
1603            ax.plot(
1604                [x_axis_range[0], x_axis_range[1]],
1605                [x_axis_range[0], x_axis_range[1]],
1606                "k--",
1607                alpha=0.5,
1608            )
1609
1610        # 回帰情報の表示
1611        equation = (
1612            f"y = {slope:.2f}x {'+' if intercept >= 0 else '-'} {abs(intercept):.2f}"
1613        )
1614        position_x = 0.05
1615        fig_ha: str = "left"
1616        ax.text(
1617            position_x,
1618            0.95,
1619            equation,
1620            transform=ax.transAxes,
1621            va="top",
1622            ha=fig_ha,
1623            color="red",
1624        )
1625        ax.text(
1626            position_x,
1627            0.88,
1628            f"R² = {r_squared:.2f}",
1629            transform=ax.transAxes,
1630            va="top",
1631            ha=fig_ha,
1632            color="red",
1633        )
1634        ax.text(
1635            position_x,
1636            0.81,  # RMSEのための新しい位置
1637            f"RMSE = {rmse:.2f}",
1638            transform=ax.transAxes,
1639            va="top",
1640            ha=fig_ha,
1641            color="red",
1642        )
1643        # 目盛り線の設定
1644        ax.grid(True, alpha=0.3)
1645
1646        fig.savefig(output_path, dpi=300, bbox_inches="tight")
1647        plt.close(fig)
1648
1649    def plot_source_contributions_diurnal(
1650        self,
1651        df: pd.DataFrame,
1652        output_dir: str,
1653        col_ch4_flux: str,
1654        col_c2h6_flux: str,
1655        label_gas: str = "gas",
1656        label_bio: str = "bio",
1657        col_datetime: str = "Date",
1658        output_filename: str = "source_contributions.png",
1659        window_size: int = 6,  # 移動平均の窓サイズ
1660        print_summary: bool = True,  # 統計情報を表示するかどうか,
1661        show_legend: bool = False,
1662        smooth: bool = False,
1663        y_max: float = 100,  # y軸の上限値を追加
1664        subplot_label: str | None = None,
1665        subplot_fontsize: int = 20,
1666    ) -> None:
1667        """CH4フラックスの都市ガス起源と生物起源の日変化を積み上げグラフとして表示
1668
1669        Parameters:
1670        ------
1671            df : pd.DataFrame
1672                データフレーム
1673            output_dir : str
1674                出力ディレクトリのパス
1675            col_ch4_flux : str
1676                CH4フラックスのカラム名
1677            col_c2h6_flux : str
1678                C2H6フラックスのカラム名
1679            label_gas : str
1680                都市ガス起源のラベル
1681            label_bio : str
1682                生物起源のラベル
1683            col_datetime : str
1684                日時カラムの名前
1685            output_filename : str
1686                出力ファイル名
1687            window_size : int
1688                移動平均の窓サイズ
1689            print_summary : bool
1690                統計情報を表示するかどうか
1691            smooth : bool
1692                移動平均を適用するかどうか
1693            y_max : float
1694                y軸の上限値(デフォルト: 100)
1695        """
1696        # 出力ディレクトリの作成
1697        os.makedirs(output_dir, exist_ok=True)
1698        output_path: str = os.path.join(output_dir, output_filename)
1699
1700        # 起源の計算
1701        df_with_sources = self._calculate_source_contributions(
1702            df=df,
1703            col_ch4_flux=col_ch4_flux,
1704            col_c2h6_flux=col_c2h6_flux,
1705            col_datetime=col_datetime,
1706        )
1707
1708        # 時刻データの抽出とグループ化
1709        df_with_sources["hour"] = df_with_sources.index.hour
1710        hourly_means = df_with_sources.groupby("hour")[["ch4_gas", "ch4_bio"]].mean()
1711
1712        # 24時間目のデータ点を追加(0時のデータを使用)
1713        last_hour = hourly_means.iloc[0:1].copy()
1714        last_hour.index = [24]
1715        hourly_means = pd.concat([hourly_means, last_hour])
1716
1717        # 移動平均の適用
1718        hourly_means_smoothed = hourly_means
1719        if smooth:
1720            hourly_means_smoothed = hourly_means.rolling(
1721                window=window_size, center=True, min_periods=1
1722            ).mean()
1723
1724        # 24時間分のデータポイントを作成
1725        time_points = pd.date_range("2024-01-01", periods=25, freq="h")
1726
1727        # プロットの作成
1728        plt.figure(figsize=(10, 6))
1729        ax = plt.gca()
1730
1731        # サブプロットラベルの追加(subplot_labelが指定されている場合)
1732        if subplot_label:
1733            ax.text(
1734                0.02,  # x位置
1735                0.98,  # y位置
1736                subplot_label,
1737                transform=ax.transAxes,
1738                va="top",
1739                fontsize=subplot_fontsize,
1740            )
1741
1742        # 積み上げプロット
1743        ax.fill_between(
1744            time_points,
1745            0,
1746            hourly_means_smoothed["ch4_bio"],
1747            color="blue",
1748            alpha=0.6,
1749            label=label_bio,
1750        )
1751        ax.fill_between(
1752            time_points,
1753            hourly_means_smoothed["ch4_bio"],
1754            hourly_means_smoothed["ch4_bio"] + hourly_means_smoothed["ch4_gas"],
1755            color="red",
1756            alpha=0.6,
1757            label=label_gas,
1758        )
1759
1760        # 合計値のライン
1761        total_flux = hourly_means_smoothed["ch4_bio"] + hourly_means_smoothed["ch4_gas"]
1762        ax.plot(time_points, total_flux, "-", color="black", alpha=0.5)
1763
1764        # 軸の設定
1765        ax.set_xlabel("Time (hour)")
1766        ax.set_ylabel(r"CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)")
1767        ax.xaxis.set_major_formatter(mdates.DateFormatter("%-H"))
1768        ax.xaxis.set_major_locator(mdates.HourLocator(byhour=[0, 6, 12, 18, 24]))
1769        ax.set_xlim(time_points[0], time_points[-1])
1770        ax.set_ylim(0, y_max)  # y軸の範囲を設定
1771        ax.grid(True, alpha=0.3)
1772
1773        # 凡例を図の下部に配置
1774        if show_legend:
1775            handles, labels = ax.get_legend_handles_labels()
1776            fig = plt.gcf()  # 現在の図を取得
1777            fig.legend(
1778                handles,
1779                labels,
1780                loc="center",
1781                bbox_to_anchor=(0.5, 0.01),
1782                ncol=len(handles),
1783            )
1784            plt.subplots_adjust(bottom=0.2)  # 下部に凡例用のスペースを確保
1785
1786        # グラフの保存
1787        plt.tight_layout()
1788        plt.savefig(output_path, dpi=300, bbox_inches="tight")
1789        plt.close()
1790
1791        # 統計情報の表示
1792        if print_summary:
1793            stats = {
1794                "都市ガス起源": hourly_means["ch4_gas"],
1795                "生物起源": hourly_means["ch4_bio"],
1796                "合計": hourly_means["ch4_gas"] + hourly_means["ch4_bio"],
1797            }
1798
1799            for source, data in stats.items():
1800                mean_val = data.mean()
1801                min_val = data.min()
1802                max_val = data.max()
1803                min_time = data.idxmin()
1804                max_time = data.idxmax()
1805
1806                self.logger.info(f"{source}の統計:")
1807                print(f"  平均値: {mean_val:.2f}")
1808                print(f"  最小値: {min_val:.2f} (Hour: {min_time})")
1809                print(f"  最大値: {max_val:.2f} (Hour: {max_time})")
1810                if min_val != 0:
1811                    print(f"  最大/最小比: {max_val / min_val:.2f}")
1812
1813    def plot_source_contributions_diurnal_by_date(
1814        self,
1815        df: pd.DataFrame,
1816        output_dir: str,
1817        col_ch4_flux: str,
1818        col_c2h6_flux: str,
1819        label_gas: str = "gas",
1820        label_bio: str = "bio",
1821        col_datetime: str = "Date",
1822        output_filename: str = "source_contributions_by_date.png",
1823        show_label: bool = True,
1824        show_legend: bool = False,
1825        print_summary: bool = False,  # 統計情報を表示するかどうか,
1826        subplot_fontsize: int = 20,
1827        subplot_label_weekday: str | None = None,
1828        subplot_label_weekend: str | None = None,
1829        y_max: float | None = None,  # y軸の上限値
1830    ) -> None:
1831        """CH4フラックスの都市ガス起源と生物起源の日変化を平日・休日別に表示
1832
1833        Parameters:
1834        ------
1835            df : pd.DataFrame
1836                データフレーム
1837            output_dir : str
1838                出力ディレクトリのパス
1839            col_ch4_flux : str
1840                CH4フラックスのカラム名
1841            col_c2h6_flux : str
1842                C2H6フラックスのカラム名
1843            label_gas : str
1844                都市ガス起源のラベル
1845            label_bio : str
1846                生物起源のラベル
1847            col_datetime : str
1848                日時カラムの名前
1849            output_filename : str
1850                出力ファイル名
1851            show_label : bool
1852                ラベルを表示するか
1853            show_legend : bool
1854                凡例を表示するか
1855            subplot_fontsize : int
1856                サブプロットのフォントサイズ
1857            subplot_label_weekday : str | None
1858                平日グラフのラベル
1859            subplot_label_weekend : str | None
1860                休日グラフのラベル
1861            y_max : float | None
1862                y軸の上限値
1863        """
1864        # 出力ディレクトリの作成
1865        os.makedirs(output_dir, exist_ok=True)
1866        output_path: str = os.path.join(output_dir, output_filename)
1867
1868        # 起源の計算
1869        df_with_sources = self._calculate_source_contributions(
1870            df=df,
1871            col_ch4_flux=col_ch4_flux,
1872            col_c2h6_flux=col_c2h6_flux,
1873            col_datetime=col_datetime,
1874        )
1875
1876        # 日付タイプの分類
1877        dates = pd.to_datetime(df_with_sources.index)
1878        is_weekend = dates.dayofweek.isin([5, 6])
1879        is_holiday = dates.map(lambda x: jpholiday.is_holiday(x.date()))
1880        is_weekday = ~(is_weekend | is_holiday)
1881
1882        # データの分類
1883        data_weekday = df_with_sources[is_weekday]
1884        data_holiday = df_with_sources[is_weekend | is_holiday]
1885
1886        # プロットの作成
1887        fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
1888
1889        # 平日と休日それぞれのプロット
1890        for ax, data, label in [
1891            (ax1, data_weekday, "Weekdays"),
1892            (ax2, data_holiday, "Weekends & Holidays"),
1893        ]:
1894            # 時間ごとの平均値を計算
1895            hourly_means = data.groupby(data.index.hour)[["ch4_gas", "ch4_bio"]].mean()
1896
1897            # 24時間目のデータ点を追加
1898            last_hour = hourly_means.iloc[0:1].copy()
1899            last_hour.index = [24]
1900            hourly_means = pd.concat([hourly_means, last_hour])
1901
1902            # 24時間分のデータポイントを作成
1903            time_points = pd.date_range("2024-01-01", periods=25, freq="h")
1904
1905            # 積み上げプロット
1906            ax.fill_between(
1907                time_points,
1908                0,
1909                hourly_means["ch4_bio"],
1910                color="blue",
1911                alpha=0.6,
1912                label=label_bio,
1913            )
1914            ax.fill_between(
1915                time_points,
1916                hourly_means["ch4_bio"],
1917                hourly_means["ch4_bio"] + hourly_means["ch4_gas"],
1918                color="red",
1919                alpha=0.6,
1920                label=label_gas,
1921            )
1922
1923            # 合計値のライン
1924            total_flux = hourly_means["ch4_bio"] + hourly_means["ch4_gas"]
1925            ax.plot(time_points, total_flux, "-", color="black", alpha=0.5)
1926
1927            # 軸の設定
1928            if show_label:
1929                ax.set_xlabel("Time (hour)")
1930                if ax == ax1:  # 左側のプロットのラベル
1931                    ax.set_ylabel("Weekdays CH$_4$ flux\n" r"(nmol m$^{-2}$ s$^{-1}$)")
1932                else:  # 右側のプロットのラベル
1933                    ax.set_ylabel("Weekends CH$_4$ flux\n" r"(nmol m$^{-2}$ s$^{-1}$)")
1934
1935            ax.xaxis.set_major_formatter(mdates.DateFormatter("%-H"))
1936            ax.xaxis.set_major_locator(mdates.HourLocator(byhour=[0, 6, 12, 18, 24]))
1937            ax.set_xlim(time_points[0], time_points[-1])
1938            if y_max is not None:
1939                ax.set_ylim(0, y_max)
1940            ax.grid(True, alpha=0.3)
1941
1942        # サブプロットラベルの追加
1943        if subplot_label_weekday:
1944            ax1.text(
1945                0.02,
1946                0.98,
1947                subplot_label_weekday,
1948                transform=ax1.transAxes,
1949                va="top",
1950                fontsize=subplot_fontsize,
1951            )
1952        if subplot_label_weekend:
1953            ax2.text(
1954                0.02,
1955                0.98,
1956                subplot_label_weekend,
1957                transform=ax2.transAxes,
1958                va="top",
1959                fontsize=subplot_fontsize,
1960            )
1961
1962        # 凡例を図の下部に配置
1963        if show_legend:
1964            # 最初のプロットから凡例のハンドルとラベルを取得
1965            handles, labels = ax1.get_legend_handles_labels()
1966            # 図の下部に凡例を配置
1967            fig.legend(
1968                handles,
1969                labels,
1970                loc="center",
1971                bbox_to_anchor=(0.5, 0.01),  # x=0.5で中央、y=0.01で下部に配置
1972                ncol=len(handles),  # ハンドルの数だけ列を作成(一行に表示)
1973            )
1974            # 凡例用のスペースを確保
1975            plt.subplots_adjust(bottom=0.2)  # 下部に30%のスペースを確保
1976
1977        plt.tight_layout()
1978        plt.savefig(output_path, dpi=300, bbox_inches="tight")
1979        plt.close()
1980
1981        # 統計情報の表示
1982        if print_summary:
1983            for data, label in [
1984                (data_weekday, "Weekdays"),
1985                (data_holiday, "Weekends & Holidays"),
1986            ]:
1987                hourly_means = data.groupby(data.index.hour)[
1988                    ["ch4_gas", "ch4_bio"]
1989                ].mean()
1990                total_flux = hourly_means["ch4_gas"] + hourly_means["ch4_bio"]
1991
1992                print(f"\n{label}の統計:")
1993                print(f"  平均値: {total_flux.mean():.2f}")
1994                print(f"  最小値: {total_flux.min():.2f} (Hour: {total_flux.idxmin()})")
1995                print(f"  最大値: {total_flux.max():.2f} (Hour: {total_flux.idxmax()})")
1996                if total_flux.min() != 0:
1997                    print(f"  最大/最小比: {total_flux.max() / total_flux.min():.2f}")
1998
1999    def plot_spectra(
2000        self,
2001        input_dir: str,
2002        output_dir: str,
2003        fs: float,
2004        lag_second: float,
2005        col_ch4: str = "Ultra_CH4_ppm_C",
2006        col_c2h6: str = "Ultra_C2H6_ppb",
2007        label_ch4: str | None = None,
2008        label_c2h6: str | None = None,
2009        are_inputs_resampled: bool = True,
2010        file_pattern: str = "*.csv",
2011        output_basename: str = "spectrum",
2012        plot_power: bool = True,
2013        plot_co: bool = True,
2014        markersize: float = 14,
2015    ) -> None:
2016        """
2017        月間の平均パワースペクトル密度を計算してプロットする。
2018
2019        データファイルを指定されたディレクトリから読み込み、パワースペクトル密度を計算し、
2020        結果を指定された出力ディレクトリにプロットして保存します。
2021
2022        Parameters:
2023        ------
2024            input_dir : str
2025                データファイルが格納されているディレクトリ。
2026            output_dir : str
2027                出力先ディレクトリ。
2028            fs : float
2029                サンプリング周波数。
2030            lag_second : float
2031                ラグ時間(秒)。
2032            col_ch4 : str, optional
2033                CH4の濃度データが入ったカラムのキー。デフォルトは"Ultra_CH4_ppm_C"。
2034            col_c2h6 : str, optional
2035                C2H6の濃度データが入ったカラムのキー。デフォルトは"Ultra_C2H6_ppb"。
2036            are_inputs_resampled : bool, optional
2037                入力データが再サンプリングされているかどうか。デフォルトはTrue。
2038            file_pattern : str, optional
2039                処理対象のファイルパターン。デフォルトは"*.csv"。
2040            output_basename : str, optional
2041                出力ファイル名。デフォルトは"spectrum"。
2042        """
2043        # データの読み込みと結合
2044        edp = EddyDataPreprocessor()
2045
2046        # 各変数のパワースペクトルを格納する辞書
2047        power_spectra = {col_ch4: [], col_c2h6: []}
2048        co_spectra = {col_ch4: [], col_c2h6: []}
2049        freqs = None
2050
2051        # プログレスバーを表示しながらファイルを処理
2052        file_list = glob.glob(os.path.join(input_dir, file_pattern))
2053        for filepath in tqdm(file_list, desc="Processing files"):
2054            df, _ = edp.get_resampled_df(
2055                filepath=filepath, is_already_resampled=are_inputs_resampled
2056            )
2057
2058            # 風速成分の計算を追加
2059            df = edp.add_uvw_columns(df)
2060
2061            # NaNや無限大を含む行を削除
2062            df = df.replace([np.inf, -np.inf], np.nan).dropna(
2063                subset=[col_ch4, col_c2h6, "wind_w"]
2064            )
2065
2066            # データが十分な行数を持っているか確認
2067            if len(df) < 100:
2068                continue
2069
2070            # 各ファイルごとにスペクトル計算
2071            calculator = SpectrumCalculator(
2072                df=df,
2073                fs=fs,
2074                cols_apply_lag_time=[col_ch4, col_c2h6],
2075                lag_second=lag_second,
2076            )
2077
2078            # 各変数のパワースペクトルを計算して保存
2079            for col in power_spectra.keys():
2080                f, ps = calculator.calculate_power_spectrum(
2081                    col=col,
2082                    dimensionless=True,
2083                    frequency_weighted=True,
2084                    interpolate_points=True,
2085                    scaling="density",
2086                )
2087                # 最初のファイル処理時にfreqsを初期化
2088                if freqs is None:
2089                    freqs = f
2090                    power_spectra[col].append(ps)
2091                # 以降は周波数配列の長さが一致する場合のみ追加
2092                elif len(f) == len(freqs):
2093                    power_spectra[col].append(ps)
2094
2095                # コスペクトル
2096                _, cs, _ = calculator.calculate_co_spectrum(
2097                    col1="wind_w",
2098                    col2=col,
2099                    dimensionless=True,
2100                    frequency_weighted=True,
2101                    interpolate_points=True,
2102                    # scaling="density",
2103                    scaling="spectrum",
2104                )
2105                if freqs is not None and len(cs) == len(freqs):
2106                    co_spectra[col].append(cs)
2107
2108        # 各変数のスペクトルを平均化
2109        averaged_power_spectra = {
2110            col: np.mean(spectra, axis=0) for col, spectra in power_spectra.items()
2111        }
2112        averaged_co_spectra = {
2113            col: np.mean(spectra, axis=0) for col, spectra in co_spectra.items()
2114        }
2115
2116        # # プロット設定
2117        # plt.rcParams.update(
2118        #     {
2119        #         "font.size": 20,
2120        #         "axes.labelsize": 20,
2121        #         "axes.titlesize": 20,
2122        #         "xtick.labelsize": 20,
2123        #         "ytick.labelsize": 20,
2124        #         "legend.fontsize": 20,
2125        #     }
2126        # )
2127
2128        # プロット設定を修正
2129        plot_configs = [
2130            {
2131                "col": col_ch4,
2132                "psd_ylabel": r"$fS_{\mathrm{CH_4}} / s_{\mathrm{CH_4}}^2$",
2133                "co_ylabel": r"$fCo_{w\mathrm{CH_4}} / (\sigma_w \sigma_{\mathrm{CH_4}})$",
2134                "color": "red",
2135                "label": label_ch4,
2136            },
2137            {
2138                "col": col_c2h6,
2139                "psd_ylabel": r"$fS_{\mathrm{C_2H_6}} / s_{\mathrm{C_2H_6}}^2$",
2140                "co_ylabel": r"$fCo_{w\mathrm{C_2H_6}} / (\sigma_w \sigma_{\mathrm{C_2H_6}})$",
2141                "color": "orange",
2142                "label": label_c2h6,
2143            },
2144        ]
2145
2146        # # パワースペクトルの図を作成
2147        # if plot_power:
2148        #     _, axes_psd = plt.subplots(1, 2, figsize=(12, 5), sharex=True)
2149        #     for ax, config in zip(axes_psd, plot_configs):
2150        #         ax.scatter(
2151        #             freqs,
2152        #             averaged_power_spectra[config["col"]],
2153        #             c=config["color"],
2154        #             s=100,
2155        #         )
2156        #         ax.set_xscale("log")
2157        #         ax.set_yscale("log")
2158        #         ax.set_xlim(0.001, 10)
2159        #         ax.plot([0.01, 10], [1, 0.01], "-", color="black", alpha=0.5)
2160        #         ax.text(0.1, 0.06, "-2/3", fontsize=18)
2161        #         ax.set_ylabel(config["psd_ylabel"])
2162        #         if config["label"] is not None:
2163        #             ax.text(
2164        #                 0.02, 0.98, config["label"], transform=ax.transAxes, va="top"
2165        #             )
2166        #         ax.grid(True, alpha=0.3)
2167        #         ax.set_xlabel("f (Hz)")
2168
2169        # パワースペクトルの図を作成
2170        if plot_power:
2171            _, axes_psd = plt.subplots(1, 2, figsize=(12, 5), sharex=True)
2172            for ax, config in zip(axes_psd, plot_configs):
2173                ax.plot(
2174                    freqs,
2175                    averaged_power_spectra[config["col"]],
2176                    "o",  # マーカーを丸に設定
2177                    color=config["color"],
2178                    markersize=markersize,
2179                )
2180                ax.set_xscale("log")
2181                ax.set_yscale("log")
2182                ax.set_xlim(0.001, 10)
2183                ax.plot([0.01, 10], [1, 0.01], "-", color="black", alpha=0.5)
2184                ax.text(0.1, 0.06, "-2/3", fontsize=18)
2185                ax.set_ylabel(config["psd_ylabel"])
2186                if config["label"] is not None:
2187                    ax.text(
2188                        0.02, 0.98, config["label"], transform=ax.transAxes, va="top"
2189                    )
2190                ax.grid(True, alpha=0.3)
2191                ax.set_xlabel("f (Hz)")
2192
2193            plt.tight_layout()
2194            os.makedirs(output_dir, exist_ok=True)
2195            output_path_psd: str = os.path.join(
2196                output_dir, f"power_{output_basename}.png"
2197            )
2198            plt.savefig(
2199                output_path_psd,
2200                dpi=300,
2201                bbox_inches="tight",
2202            )
2203            plt.close()
2204
2205        # # コスペクトルの図を作成
2206        # if plot_co:
2207        #     _, axes_cosp = plt.subplots(1, 2, figsize=(12, 5), sharex=True)
2208        #     for ax, config in zip(axes_cosp, plot_configs):
2209        #         ax.scatter(
2210        #             freqs,
2211        #             averaged_co_spectra[config["col"]],
2212        #             c=config["color"],
2213        #             s=100,
2214        #         )
2215        #         ax.set_xscale("log")
2216        #         ax.set_yscale("log")
2217        #         ax.set_xlim(0.001, 10)
2218        #         ax.plot([0.01, 10], [1, 0.01], "-", color="black", alpha=0.5)
2219        #         ax.text(0.1, 0.1, "-4/3", fontsize=18)
2220        #         ax.set_ylabel(config["co_ylabel"])
2221        #         if config["label"] is not None:
2222        #             ax.text(
2223        #                 0.02, 0.98, config["label"], transform=ax.transAxes, va="top"
2224        #             )
2225        #         ax.grid(True, alpha=0.3)
2226        #         ax.set_xlabel("f (Hz)")
2227
2228        # コスペクトルの図を作成
2229        if plot_co:
2230            _, axes_cosp = plt.subplots(1, 2, figsize=(12, 5), sharex=True)
2231            for ax, config in zip(axes_cosp, plot_configs):
2232                ax.plot(
2233                    freqs,
2234                    averaged_co_spectra[config["col"]],
2235                    "o",  # マーカーを丸に設定
2236                    color=config["color"],
2237                    markersize=markersize,
2238                )
2239                ax.set_xscale("log")
2240                ax.set_yscale("log")
2241                ax.set_xlim(0.001, 10)
2242                ax.plot([0.01, 10], [1, 0.01], "-", color="black", alpha=0.5)
2243                ax.text(0.1, 0.1, "-4/3", fontsize=18)
2244                ax.set_ylabel(config["co_ylabel"])
2245                if config["label"] is not None:
2246                    ax.text(
2247                        0.02, 0.98, config["label"], transform=ax.transAxes, va="top"
2248                    )
2249                ax.grid(True, alpha=0.3)
2250                ax.set_xlabel("f (Hz)")
2251
2252            plt.tight_layout()
2253            output_path_csd: str = os.path.join(output_dir, f"co_{output_basename}.png")
2254            plt.savefig(
2255                output_path_csd,
2256                dpi=300,
2257                bbox_inches="tight",
2258            )
2259            plt.close()
2260
2261    def plot_turbulence(
2262        self,
2263        df: pd.DataFrame,
2264        output_dir: str,
2265        output_filename: str = "turbulence.png",
2266        col_uz: str = "Uz",
2267        col_ch4: str = "Ultra_CH4_ppm_C",
2268        col_c2h6: str = "Ultra_C2H6_ppb",
2269        col_timestamp: str = "TIMESTAMP",
2270        add_serial_labels: bool = True,
2271    ) -> None:
2272        """時系列データのプロットを作成する
2273
2274        Parameters:
2275        ------
2276            df : pd.DataFrame
2277                プロットするデータを含むDataFrame
2278            output_dir : str
2279                出力ディレクトリのパス
2280            output_filename : str
2281                出力ファイル名
2282            col_uz : str
2283                鉛直風速データのカラム名
2284            col_ch4 : str
2285                メタンデータのカラム名
2286            col_c2h6 : str
2287                エタンデータのカラム名
2288            col_timestamp : str
2289                タイムスタンプのカラム名
2290        """
2291        # 出力ディレクトリの作成
2292        os.makedirs(output_dir, exist_ok=True)
2293        output_path: str = os.path.join(output_dir, output_filename)
2294
2295        # データの前処理
2296        df = df.copy()
2297
2298        # タイムスタンプをインデックスに設定(まだ設定されていない場合)
2299        if not isinstance(df.index, pd.DatetimeIndex):
2300            df[col_timestamp] = pd.to_datetime(df[col_timestamp])
2301            df.set_index(col_timestamp, inplace=True)
2302
2303        # 開始時刻と終了時刻を取得
2304        start_time = df.index[0]
2305        end_time = df.index[-1]
2306
2307        # 開始時刻の分を取得
2308        start_minute = start_time.minute
2309
2310        # 時間軸の作成(実際の開始時刻からの経過分数)
2311        minutes_elapsed = (df.index - start_time).total_seconds() / 60
2312
2313        # プロットの作成
2314        _, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=(12, 10), sharex=True)
2315
2316        # 鉛直風速
2317        ax1.plot(minutes_elapsed, df[col_uz], "k-", linewidth=0.5)
2318        ax1.set_ylabel(r"$w$ (m s$^{-1}$)")
2319        if add_serial_labels:
2320            ax1.text(0.02, 0.98, "(a)", transform=ax1.transAxes, va="top")
2321        ax1.grid(True, alpha=0.3)
2322
2323        # CH4濃度
2324        ax2.plot(minutes_elapsed, df[col_ch4], "r-", linewidth=0.5)
2325        ax2.set_ylabel(r"$\mathrm{CH_4}$ (ppm)")
2326        if add_serial_labels:
2327            ax2.text(0.02, 0.98, "(b)", transform=ax2.transAxes, va="top")
2328        ax2.grid(True, alpha=0.3)
2329
2330        # C2H6濃度
2331        ax3.plot(minutes_elapsed, df[col_c2h6], "orange", linewidth=0.5)
2332        ax3.set_ylabel(r"$\mathrm{C_2H_6}$ (ppb)")
2333        if add_serial_labels:
2334            ax3.text(0.02, 0.98, "(c)", transform=ax3.transAxes, va="top")
2335        ax3.grid(True, alpha=0.3)
2336        ax3.set_xlabel("Time (minutes)")
2337
2338        # x軸の範囲を実際の開始時刻から30分後までに設定
2339        total_minutes = (end_time - start_time).total_seconds() / 60
2340        ax3.set_xlim(0, min(30, total_minutes))
2341
2342        # x軸の目盛りを5分間隔で設定
2343        np.arange(start_minute, start_minute + 35, 5)
2344        ax3.xaxis.set_major_locator(MultipleLocator(5))
2345
2346        # レイアウトの調整
2347        plt.tight_layout()
2348
2349        # 図の保存
2350        plt.savefig(output_path, dpi=300, bbox_inches="tight")
2351        plt.close()
2352
2353    def plot_wind_rose_sources(
2354        self,
2355        df: pd.DataFrame,
2356        output_dir: str | Path | None = None,
2357        output_filename: str = "wind_rose.png",
2358        col_datetime: str = "Date",
2359        col_ch4_flux: str = "Fch4",
2360        col_c2h6_flux: str = "Fc2h6",
2361        col_wind_dir: str = "Wind direction",
2362        flux_unit: str = r"(nmol m$^{-2}$ s$^{-1}$)",
2363        ymax: float | None = None,  # フラックスの上限値
2364        label_gas: str = "都市ガス起源",
2365        label_bio: str = "生物起源",
2366        figsize: tuple[float, float] = (8, 8),
2367        flux_alpha: float = 0.4,
2368        num_directions: int = 8,  # 方位の数(8方位)
2369        center_on_angles: bool = True,  # 追加:45度刻みの線を境界にするかどうか
2370        subplot_label: str | None = None,
2371        add_legend: bool = True,
2372        print_summary: bool = True,  # 統計情報を表示するかどうか
2373        save_fig: bool = True,
2374        show_fig: bool = True,
2375    ) -> None:
2376        """CH4フラックスの都市ガス起源と生物起源の風配図を作成する関数
2377
2378        Parameters:
2379        ------
2380            df : pd.DataFrame
2381                風配図を作成するためのデータフレーム
2382            output_dir : str | Path | None
2383                生成された図を保存するディレクトリのパス
2384            output_filename : str
2385                保存するファイル名(デフォルトは"wind_rose.png")
2386            col_ch4_flux : str
2387                CH4フラックスを示すカラム名
2388            col_c2h6_flux : str
2389                C2H6フラックスを示すカラム名
2390            col_wind_dir : str
2391                風向を示すカラム名
2392            label_gas : str
2393                都市ガス起源のフラックスに対するラベル
2394            label_bio : str
2395                生物起源のフラックスに対するラベル
2396            col_datetime : str
2397                日時を示すカラム名
2398            num_directions : int
2399                風向の数(デフォルトは8)
2400            center_on_angles: bool
2401                Trueの場合、45度刻みの線を境界として扇形を描画します。
2402                Falseの場合、45度の中間(22.5度)を中心として扇形を描画します。
2403            subplot_label : str
2404                サブプロットに表示するラベル
2405            print_summary : bool
2406                統計情報を表示するかどうかのフラグ
2407            flux_unit : str
2408                フラックスの単位
2409            ymax : float | None
2410                y軸の上限値(指定しない場合はデータの最大値に基づいて自動設定)
2411            figsize : tuple[float, float]
2412                図のサイズ
2413            flux_alpha : float
2414                フラックスの透明度
2415            save_fig : bool
2416                図を保存するかどうかのフラグ
2417            show_fig : bool
2418                図を表示するかどうかのフラグ
2419        """
2420        # 起源の計算
2421        df_with_sources = self._calculate_source_contributions(
2422            df=df,
2423            col_ch4_flux=col_ch4_flux,
2424            col_c2h6_flux=col_c2h6_flux,
2425            col_datetime=col_datetime,
2426        )
2427
2428        # 方位の定義
2429        direction_ranges = self._define_direction_ranges(
2430            num_directions, center_on_angles
2431        )
2432
2433        # 方位ごとのデータを集計
2434        direction_data = self._aggregate_direction_data(
2435            df_with_sources, col_wind_dir, direction_ranges
2436        )
2437
2438        # プロットの作成
2439        fig = plt.figure(figsize=figsize)
2440        ax = fig.add_subplot(111, projection="polar")
2441
2442        # 方位の角度(ラジアン)を計算
2443        theta = np.array(
2444            [np.radians(angle) for angle in direction_data["center_angle"]]
2445        )
2446
2447        # 生物起源と都市ガス起源を独立してプロット
2448        ax.bar(
2449            theta,
2450            direction_data["bio_flux"],
2451            width=np.radians(360 / num_directions),
2452            bottom=0.0,
2453            color="blue",
2454            alpha=flux_alpha,
2455            label=label_bio,
2456        )
2457
2458        ax.bar(
2459            theta,
2460            direction_data["gas_flux"],
2461            width=np.radians(360 / num_directions),
2462            bottom=0.0,
2463            color="red",
2464            alpha=flux_alpha,
2465            label=label_gas,
2466        )
2467
2468        # y軸の範囲を設定
2469        if ymax is not None:
2470            ax.set_ylim(0, ymax)
2471        else:
2472            # データの最大値に基づいて自動設定
2473            max_value = max(
2474                direction_data["bio_flux"].max(), direction_data["gas_flux"].max()
2475            )
2476            ax.set_ylim(0, max_value * 1.1)  # 最大値の1.1倍を上限に設定
2477
2478        # 方位ラベルの設定
2479        ax.set_theta_zero_location("N")  # 北を上に設定
2480        ax.set_theta_direction(-1)  # 時計回りに設定
2481
2482        # 方位ラベルの表示
2483        labels = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"]
2484        angles = np.radians(np.linspace(0, 360, len(labels), endpoint=False))
2485        ax.set_xticks(angles)
2486        ax.set_xticklabels(labels)
2487
2488        # プロット領域の調整(上部と下部にスペースを確保)
2489        plt.subplots_adjust(
2490            top=0.8,  # 上部に20%のスペースを確保
2491            bottom=0.2,  # 下部に20%のスペースを確保(凡例用)
2492        )
2493
2494        # サブプロットラベルの追加(デフォルトは左上)
2495        if subplot_label:
2496            ax.text(
2497                0.01,
2498                0.99,
2499                subplot_label,
2500                transform=ax.transAxes,
2501            )
2502
2503        # 単位の追加(図の下部中央に配置)
2504        plt.figtext(
2505            0.5,  # x位置(中央)
2506            0.1,  # y位置(下部)
2507            flux_unit,
2508            ha="center",  # 水平方向の位置揃え
2509            va="bottom",  # 垂直方向の位置揃え
2510        )
2511
2512        # 凡例の追加(単位の下に配置)
2513        if add_legend:
2514            # 最初のプロットから凡例のハンドルとラベルを取得
2515            handles, labels = ax.get_legend_handles_labels()
2516            # 図の下部に凡例を配置
2517            fig.legend(
2518                handles,
2519                labels,
2520                loc="center",
2521                bbox_to_anchor=(0.5, 0.05),  # x=0.5で中央、y=0.05で下部に配置
2522                ncol=len(handles),  # ハンドルの数だけ列を作成(一行に表示)
2523            )
2524
2525        # グラフの保存
2526        if save_fig:
2527            if output_dir is None:
2528                raise ValueError(
2529                    "save_fig=Trueのとき、output_dirに有効なパスを指定する必要があります。"
2530                )
2531            # 出力ディレクトリの作成
2532            os.makedirs(output_dir, exist_ok=True)
2533            output_path: str = os.path.join(output_dir, output_filename)
2534            plt.savefig(output_path, dpi=300, bbox_inches="tight")
2535
2536        # グラフの表示
2537        if show_fig:
2538            plt.show()
2539        else:
2540            plt.close(fig=fig)
2541
2542        # 統計情報の表示
2543        if print_summary:
2544            for source in ["gas", "bio"]:
2545                flux_data = direction_data[f"{source}_flux"]
2546                mean_val = flux_data.mean()
2547                max_val = flux_data.max()
2548                max_dir = direction_data.loc[flux_data.idxmax(), "name"]
2549
2550                self.logger.info(
2551                    f"{label_gas if source == 'gas' else label_bio}の統計:"
2552                )
2553                print(f"  平均フラックス: {mean_val:.2f}")
2554                print(f"  最大フラックス: {max_val:.2f}")
2555                print(f"  最大フラックスの方位: {max_dir}")
2556
2557    def _define_direction_ranges(
2558        self,
2559        num_directions: int = 8,
2560        center_on_angles: bool = False,
2561    ) -> pd.DataFrame:
2562        """方位の範囲を定義
2563
2564        Parameters:
2565        ------
2566            num_directions : int
2567                方位の数(デフォルトは8)
2568            center_on_angles : bool
2569                Trueの場合、45度刻みの線を境界として扇形を描画します。
2570                Falseの場合、45度の中間(22.5度)を中心として扇形を描画します。
2571
2572        Returns:
2573        ------
2574        pd.DataFrame
2575            方位の定義を含むDataFrame
2576        """
2577        if num_directions == 8:
2578            if center_on_angles:
2579                # 45度刻みの線を境界とする場合
2580                directions = pd.DataFrame(
2581                    {
2582                        "name": ["N", "NE", "E", "SE", "S", "SW", "W", "NW"],
2583                        "center_angle": [
2584                            22.5,
2585                            67.5,
2586                            112.5,
2587                            157.5,
2588                            202.5,
2589                            247.5,
2590                            292.5,
2591                            337.5,
2592                        ],
2593                    }
2594                )
2595            else:
2596                # 従来通り45度を中心とする場合
2597                directions = pd.DataFrame(
2598                    {
2599                        "name": ["N", "NE", "E", "SE", "S", "SW", "W", "NW"],
2600                        "center_angle": [0, 45, 90, 135, 180, 225, 270, 315],
2601                    }
2602                )
2603        else:
2604            raise ValueError(f"現在{num_directions}方位はサポートされていません")
2605
2606        # 各方位の範囲を計算
2607        angle_range = 360 / num_directions
2608        directions["start_angle"] = directions["center_angle"] - angle_range / 2
2609        directions["end_angle"] = directions["center_angle"] + angle_range / 2
2610
2611        # -180度から180度の範囲に正規化
2612        directions["start_angle"] = np.where(
2613            directions["start_angle"] > 180,
2614            directions["start_angle"] - 360,
2615            directions["start_angle"],
2616        )
2617        directions["end_angle"] = np.where(
2618            directions["end_angle"] > 180,
2619            directions["end_angle"] - 360,
2620            directions["end_angle"],
2621        )
2622
2623        return directions
2624
2625    def _aggregate_direction_data(
2626        self,
2627        df: pd.DataFrame,
2628        col_wind_dir: str,
2629        direction_ranges: pd.DataFrame,
2630    ) -> pd.DataFrame:
2631        """方位ごとのフラックスデータを集計
2632
2633        Parameters:
2634        ------
2635            df : pd.DataFrame
2636                ソース分離済みのデータフレーム
2637            col_wind_dir : str
2638                風向のカラム名
2639            direction_ranges : pd.DataFrame
2640                方位の定義
2641
2642        Returns:
2643        ------
2644            pd.DataFrame
2645                方位ごとの集計データ
2646        """
2647        result_data = direction_ranges.copy()
2648        result_data["gas_flux"] = 0.0
2649        result_data["bio_flux"] = 0.0
2650
2651        for idx, row in direction_ranges.iterrows():
2652            if row["start_angle"] < row["end_angle"]:
2653                mask = (df[col_wind_dir] > row["start_angle"]) & (
2654                    df[col_wind_dir] <= row["end_angle"]
2655                )
2656            else:  # 北方向など、-180度と180度をまたぐ場合
2657                mask = (df[col_wind_dir] > row["start_angle"]) | (
2658                    df[col_wind_dir] <= row["end_angle"]
2659                )
2660
2661            result_data.loc[idx, "gas_flux"] = df.loc[mask, "ch4_gas"].mean()
2662            result_data.loc[idx, "bio_flux"] = df.loc[mask, "ch4_bio"].mean()
2663
2664        # NaNを0に置換
2665        result_data = result_data.fillna(0)
2666
2667        return result_data
2668
2669    def _calculate_source_contributions(
2670        self,
2671        df: pd.DataFrame,
2672        col_ch4_flux: str,
2673        col_c2h6_flux: str,
2674        gas_ratio_c1c2: float = 0.076,
2675        col_datetime: str = "Date",
2676    ) -> pd.DataFrame:
2677        """
2678        CH4フラックスの都市ガス起源と生物起源の寄与を計算する。
2679        このロジックでは、燃焼起源のCH4フラックスは考慮せず計算している。
2680
2681        Parameters:
2682        ------
2683            df : pd.DataFrame
2684                入力データフレーム
2685            col_ch4_flux : str
2686                CH4フラックスのカラム名
2687            col_c2h6_flux : str
2688                C2H6フラックスのカラム名
2689            gas_ratio_c1c2 : float
2690                ガスのC2H6/CH4比(ppb/ppb)
2691            col_datetime : str
2692                日時カラムの名前
2693
2694        Returns:
2695        ------
2696            pd.DataFrame
2697                起源別のフラックス値を含むデータフレーム
2698        """
2699        df_processed = df.copy()
2700
2701        # 日時インデックスの処理
2702        if not isinstance(df_processed.index, pd.DatetimeIndex):
2703            df_processed[col_datetime] = pd.to_datetime(df_processed[col_datetime])
2704            df_processed.set_index(col_datetime, inplace=True)
2705
2706        # C2H6/CH4比の計算
2707        df_processed["c2c1_ratio"] = (
2708            df_processed[col_c2h6_flux] / df_processed[col_ch4_flux]
2709        )
2710
2711        # 都市ガスの標準組成に基づく都市ガス比率の計算
2712        df_processed["gas_ratio"] = df_processed["c2c1_ratio"] / gas_ratio_c1c2 * 100
2713
2714        # gas_ratioに基づいて都市ガス起源と生物起源の寄与を比例配分
2715        df_processed["ch4_gas"] = df_processed[col_ch4_flux] * np.clip(
2716            df_processed["gas_ratio"] / 100, 0, 1
2717        )
2718        df_processed["ch4_bio"] = df_processed[col_ch4_flux] * (
2719            1 - np.clip(df_processed["gas_ratio"] / 100, 0, 1)
2720        )
2721
2722        return df_processed
2723
2724    def _prepare_diurnal_data(
2725        self,
2726        df: pd.DataFrame,
2727        target_columns: list[str],
2728        include_date_types: bool = False,
2729    ) -> tuple[dict[str, pd.DataFrame], pd.DatetimeIndex]:
2730        """
2731        日変化パターンの計算に必要なデータを準備する。
2732
2733        Parameters:
2734        ------
2735            df : pd.DataFrame
2736                入力データフレーム
2737            target_columns : list[str]
2738                計算対象の列名のリスト
2739            include_date_types : bool
2740                日付タイプ(平日/休日など)の分類を含めるかどうか
2741
2742        Returns:
2743        ------
2744            tuple[dict[str, pd.DataFrame], pd.DatetimeIndex]
2745                - 時間帯ごとの平均値を含むDataFrameの辞書
2746                - 24時間分の時間点
2747        """
2748        df = df.copy()
2749        df["hour"] = pd.to_datetime(df["Date"]).dt.hour
2750
2751        # 時間ごとの平均値を計算する関数
2752        def calculate_hourly_means(data_df, condition=None):
2753            if condition is not None:
2754                data_df = data_df[condition]
2755            return data_df.groupby("hour")[target_columns].mean().reset_index()
2756
2757        # 基本の全日データを計算
2758        hourly_means = {"all": calculate_hourly_means(df)}
2759
2760        # 日付タイプによる分類が必要な場合
2761        if include_date_types:
2762            dates = pd.to_datetime(df["Date"])
2763            is_weekend = dates.dt.dayofweek.isin([5, 6])
2764            is_holiday = dates.map(lambda x: jpholiday.is_holiday(x.date()))
2765            is_weekday = ~(is_weekend | is_holiday)
2766
2767            hourly_means.update(
2768                {
2769                    "weekday": calculate_hourly_means(df, is_weekday),
2770                    "weekend": calculate_hourly_means(df, is_weekend),
2771                    "holiday": calculate_hourly_means(df, is_weekend | is_holiday),
2772                }
2773            )
2774
2775        # 24時目のデータを追加
2776        for col in hourly_means:
2777            last_row = hourly_means[col].iloc[0:1].copy()
2778            last_row["hour"] = 24
2779            hourly_means[col] = pd.concat(
2780                [hourly_means[col], last_row], ignore_index=True
2781            )
2782
2783        # 24時間分のデータポイントを作成
2784        time_points = pd.date_range("2024-01-01", periods=25, freq="h")
2785
2786        return hourly_means, time_points
2787
2788    def _setup_diurnal_axes(
2789        self,
2790        ax: plt.Axes,
2791        time_points: pd.DatetimeIndex,
2792        ylabel: str,
2793        subplot_label: str | None = None,
2794        show_label: bool = True,
2795        show_legend: bool = True,
2796        subplot_fontsize: int = 20,
2797    ) -> None:
2798        """日変化プロットの軸の設定を行う
2799
2800        Parameters:
2801        ------
2802            ax : plt.Axes
2803                設定対象の軸
2804            time_points : pd.DatetimeIndex
2805                時間軸のポイント
2806            ylabel : str
2807                y軸のラベル
2808            subplot_label : str | None
2809                サブプロットのラベル
2810            show_label : bool
2811                軸ラベルを表示するかどうか
2812            show_legend : bool
2813                凡例を表示するかどうか
2814            subplot_fontsize : int
2815                サブプロットのフォントサイズ
2816        """
2817        if show_label:
2818            ax.set_xlabel("Time (hour)")
2819            ax.set_ylabel(ylabel)
2820
2821        ax.xaxis.set_major_formatter(mdates.DateFormatter("%-H"))
2822        ax.xaxis.set_major_locator(mdates.HourLocator(byhour=[0, 6, 12, 18, 24]))
2823        ax.set_xlim(time_points[0], time_points[-1])
2824        ax.set_xticks(time_points[::6])
2825        ax.set_xticklabels(["0", "6", "12", "18", "24"])
2826
2827        if subplot_label:
2828            ax.text(
2829                0.02,
2830                0.98,
2831                subplot_label,
2832                transform=ax.transAxes,
2833                va="top",
2834                fontsize=subplot_fontsize,
2835            )
2836
2837        if show_legend:
2838            ax.legend()
2839
2840    @staticmethod
2841    def get_valid_data(df: pd.DataFrame, x_col: str, y_col: str) -> pd.DataFrame:
2842        """
2843        指定された列の有効なデータ(NaNを除いた)を取得します。
2844
2845        Parameters:
2846        ------
2847            df : pd.DataFrame
2848                データフレーム
2849            x_col : str
2850                X軸の列名
2851            y_col : str
2852                Y軸の列名
2853
2854        Returns:
2855        ------
2856            pd.DataFrame
2857                有効なデータのみを含むDataFrame
2858        """
2859        return df.copy().dropna(subset=[x_col, y_col])
2860
2861    @staticmethod
2862    def setup_logger(logger: Logger | None, log_level: int = INFO) -> Logger:
2863        """
2864        ロガーを設定します。
2865
2866        このメソッドは、ロギングの設定を行い、ログメッセージのフォーマットを指定します。
2867        ログメッセージには、日付、ログレベル、メッセージが含まれます。
2868
2869        渡されたロガーがNoneまたは不正な場合は、新たにロガーを作成し、標準出力に
2870        ログメッセージが表示されるようにStreamHandlerを追加します。ロガーのレベルは
2871        引数で指定されたlog_levelに基づいて設定されます。
2872
2873        Parameters:
2874        ------
2875            logger : Logger | None
2876                使用するロガー。Noneの場合は新しいロガーを作成します。
2877            log_level : int
2878                ロガーのログレベル。デフォルトはINFO。
2879
2880        Returns:
2881        ------
2882            Logger
2883                設定されたロガーオブジェクト。
2884        """
2885        if logger is not None and isinstance(logger, Logger):
2886            return logger
2887        # 渡されたロガーがNoneまたは正しいものでない場合は独自に設定
2888        new_logger: Logger = getLogger()
2889        # 既存のハンドラーをすべて削除
2890        for handler in new_logger.handlers[:]:
2891            new_logger.removeHandler(handler)
2892        new_logger.setLevel(log_level)  # ロガーのレベルを設定
2893        ch = StreamHandler()
2894        ch_formatter = Formatter("%(asctime)s - %(levelname)s - %(message)s")
2895        ch.setFormatter(ch_formatter)  # フォーマッターをハンドラーに設定
2896        new_logger.addHandler(ch)  # StreamHandlerの追加
2897        return new_logger
2898
2899    @staticmethod
2900    def setup_plot_params(
2901        font_family: list[str] = ["Arial", "Dejavu Sans"],
2902        font_size: float = 20,
2903        legend_size: float = 20,
2904        tick_size: float = 20,
2905        title_size: float = 20,
2906        plot_params=None,
2907    ) -> None:
2908        """
2909        matplotlibのプロットパラメータを設定します。
2910
2911        Parameters:
2912        ------
2913            font_family : list[str]
2914                使用するフォントファミリーのリスト。
2915            font_size : float
2916                軸ラベルのフォントサイズ。
2917            legend_size : float
2918                凡例のフォントサイズ。
2919            tick_size : float
2920                軸目盛りのフォントサイズ。
2921            title_size : float
2922                タイトルのフォントサイズ。
2923            plot_params : Optional[Dict[str, any]]
2924                matplotlibのプロットパラメータの辞書。
2925        """
2926        # デフォルトのプロットパラメータ
2927        default_params = {
2928            "axes.linewidth": 1.0,
2929            "axes.titlesize": title_size,  # タイトル
2930            "grid.color": "gray",
2931            "grid.linewidth": 1.0,
2932            "font.family": font_family,
2933            "font.size": font_size,  # 軸ラベル
2934            "legend.fontsize": legend_size,  # 凡例
2935            "text.color": "black",
2936            "xtick.color": "black",
2937            "ytick.color": "black",
2938            "xtick.labelsize": tick_size,  # 軸目盛
2939            "ytick.labelsize": tick_size,  # 軸目盛
2940            "xtick.major.size": 0,
2941            "ytick.major.size": 0,
2942            "ytick.direction": "out",
2943            "ytick.major.width": 1.0,
2944        }
2945
2946        # plot_paramsが定義されている場合、デフォルトに追記
2947        if plot_params:
2948            default_params.update(plot_params)
2949
2950        plt.rcParams.update(default_params)  # プロットパラメータを更新
2951
2952    @staticmethod
2953    def plot_flux_distributions(
2954        g2401_flux: pd.Series,
2955        ultra_flux: pd.Series,
2956        month: int,
2957        output_dir: str,
2958        xlim: tuple[float, float] = (-50, 200),
2959        bandwidth: float = 1.0,  # デフォルト値を1.0に設定
2960    ) -> None:
2961        """
2962        両測器のCH4フラックス分布を可視化
2963
2964        Parameters:
2965        ------
2966            g2401_flux : pd.Series
2967                G2401で測定されたフラックス値の配列
2968            ultra_flux : pd.Series
2969                Ultraで測定されたフラックス値の配列
2970            month : int
2971                測定月
2972            output_dir : str
2973                出力ディレクトリ
2974            xlim : tuple[float, float]
2975                x軸の範囲(タプル)
2976            bandwidth : float
2977                カーネル密度推定のバンド幅調整係数(デフォルト: 1.0)
2978        """
2979        # nanを除去
2980        g2401_flux = g2401_flux.dropna()
2981        ultra_flux = ultra_flux.dropna()
2982
2983        plt.figure(figsize=(10, 6))
2984
2985        # KDEプロット(確率密度推定)
2986        sns.kdeplot(
2987            data=g2401_flux, label="G2401", color="blue", alpha=0.5, bw_adjust=bandwidth
2988        )
2989        sns.kdeplot(
2990            data=ultra_flux, label="Ultra", color="red", alpha=0.5, bw_adjust=bandwidth
2991        )
2992
2993        # 平均値と中央値のマーカー
2994        plt.axvline(
2995            g2401_flux.mean(),
2996            color="blue",
2997            linestyle="--",
2998            alpha=0.5,
2999            label="G2401 mean",
3000        )
3001        plt.axvline(
3002            ultra_flux.mean(),
3003            color="red",
3004            linestyle="--",
3005            alpha=0.5,
3006            label="Ultra mean",
3007        )
3008        plt.axvline(
3009            np.median(g2401_flux),
3010            color="blue",
3011            linestyle=":",
3012            alpha=0.5,
3013            label="G2401 median",
3014        )
3015        plt.axvline(
3016            np.median(ultra_flux),
3017            color="red",
3018            linestyle=":",
3019            alpha=0.5,
3020            label="Ultra median",
3021        )
3022
3023        # 軸ラベルとタイトル
3024        plt.xlabel(r"CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)")
3025        plt.ylabel("Probability Density")
3026        plt.title(f"Distribution of CH$_4$ fluxes - Month {month}")
3027
3028        # x軸の範囲設定
3029        plt.xlim(xlim)
3030
3031        # グリッド表示
3032        plt.grid(True, alpha=0.3)
3033
3034        # 統計情報
3035        stats_text = (
3036            f"G2401:\n"
3037            f"  Mean: {g2401_flux.mean():.2f}\n"
3038            f"  Median: {np.median(g2401_flux):.2f}\n"
3039            f"  Std: {g2401_flux.std():.2f}\n"
3040            f"Ultra:\n"
3041            f"  Mean: {ultra_flux.mean():.2f}\n"
3042            f"  Median: {np.median(ultra_flux):.2f}\n"
3043            f"  Std: {ultra_flux.std():.2f}"
3044        )
3045        plt.text(
3046            0.02,
3047            0.98,
3048            stats_text,
3049            transform=plt.gca().transAxes,
3050            verticalalignment="top",
3051            fontsize=10,
3052            bbox=dict(boxstyle="round", facecolor="white", alpha=0.8),
3053        )
3054
3055        # 凡例の表示
3056        plt.legend(loc="upper right")
3057
3058        # グラフの保存
3059        os.makedirs(output_dir, exist_ok=True)
3060        plt.tight_layout()
3061        plt.savefig(
3062            os.path.join(output_dir, f"flux_distribution_month_{month}.png"),
3063            dpi=300,
3064            bbox_inches="tight",
3065        )
3066        plt.close()
MonthlyFiguresGenerator(logger: logging.Logger | None = None, logging_debug: bool = False)
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以上のレベルのメッセージが出力されます。
logger: logging.Logger
def plot_c1c2_fluxes_timeseries( self, df, output_dir: str, output_filename: str = 'timeseries.png', col_datetime: str = 'Date', col_c1_flux: str = 'Fch4_ultra', col_c2_flux: str = 'Fc2h6_ultra'):
 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"。
def plot_c1c2_concentrations_and_fluxes_timeseries( self, df: pandas.core.frame.DataFrame, output_dir: str, output_filename: str = 'conc_flux_timeseries.png', col_datetime: str = 'Date', col_ch4_conc: str = 'CH4_ultra', col_ch4_flux: str = 'Fch4_ultra', col_c2h6_conc: str = 'C2H6_ultra', col_c2h6_flux: str = 'Fc2h6_ultra', print_summary: bool = True) -> None:
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するかどうか
def plot_ch4c2h6_timeseries( self, df: pandas.core.frame.DataFrame, output_dir: str, col_ch4_flux: str, col_c2h6_flux: str, output_filename: str = 'timeseries_year.png', col_datetime: str = 'Date', window_size: int = 168, confidence_interval: float = 0.95, subplot_label_ch4: str | None = '(a)', subplot_label_c2h6: str | None = '(b)', subplot_fontsize: int = 20, show_ci: bool = True, ch4_ylim: tuple[float, float] | None = None, c2h6_ylim: tuple[float, float] | None = None, start_date: str | None = None, end_date: str | None = None, figsize: tuple[float, float] = (16, 6)) -> None:
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形式)
def plot_ch4_flux_comparison( self, df: pandas.core.frame.DataFrame, output_dir: str, col_g2401_flux: str, col_ultra_flux: str, output_filename: str = 'ch4_flux_comparison.png', col_datetime: str = 'Date', window_size: int = 168, confidence_interval: float = 0.95, subplot_label: str | None = None, subplot_fontsize: int = 20, show_ci: bool = True, y_lim: tuple[float, float] | None = None, start_date: str | None = None, end_date: str | None = None, figsize: tuple[float, float] = (12, 6), legend_loc: str = 'upper right') -> None:
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
    凡例の位置
def plot_c1c2_fluxes_diurnal_patterns( self, df: pandas.core.frame.DataFrame, y_cols_ch4: list[str], y_cols_c2h6: list[str], labels_ch4: list[str], labels_c2h6: list[str], colors_ch4: list[str], colors_c2h6: list[str], output_dir: str, output_filename: str = 'diurnal.png', legend_only_ch4: bool = False, show_label: bool = True, show_legend: bool = True, show_std: bool = False, std_alpha: float = 0.2, subplot_fontsize: int = 20, subplot_label_ch4: str | None = '(a)', subplot_label_c2h6: str | None = '(b)', ax1_ylim: tuple[float, float] | None = None, ax2_ylim: tuple[float, float] | None = None) -> None:
651    def plot_c1c2_fluxes_diurnal_patterns(
652        self,
653        df: pd.DataFrame,
654        y_cols_ch4: list[str],
655        y_cols_c2h6: list[str],
656        labels_ch4: list[str],
657        labels_c2h6: list[str],
658        colors_ch4: list[str],
659        colors_c2h6: list[str],
660        output_dir: str,
661        output_filename: str = "diurnal.png",
662        legend_only_ch4: bool = False,
663        show_label: bool = True,
664        show_legend: bool = True,
665        show_std: bool = False,  # 標準偏差表示のオプションを追加
666        std_alpha: float = 0.2,  # 標準偏差の透明度
667        subplot_fontsize: int = 20,
668        subplot_label_ch4: str | None = "(a)",
669        subplot_label_c2h6: str | None = "(b)",
670        ax1_ylim: tuple[float, float] | None = None,
671        ax2_ylim: tuple[float, float] | None = None,
672    ) -> None:
673        """CH4とC2H6の日変化パターンを1つの図に並べてプロットする
674
675        Parameters:
676        ------
677            df : pd.DataFrame
678                入力データフレーム。
679            y_cols_ch4 : list[str]
680                CH4のプロットに使用するカラム名のリスト。
681            y_cols_c2h6 : list[str]
682                C2H6のプロットに使用するカラム名のリスト。
683            labels_ch4 : list[str]
684                CH4の各ラインに対応するラベルのリスト。
685            labels_c2h6 : list[str]
686                C2H6の各ラインに対応するラベルのリスト。
687            colors_ch4 : list[str]
688                CH4の各ラインに使用する色のリスト。
689            colors_c2h6 : list[str]
690                C2H6の各ラインに使用する色のリスト。
691            output_dir : str
692                出力先ディレクトリのパス。
693            output_filename : str, optional
694                出力ファイル名。デフォルトは"diurnal.png"。
695            legend_only_ch4 : bool, optional
696                CH4の凡例のみを表示するかどうか。デフォルトはFalse。
697            show_label : bool, optional
698                サブプロットラベルを表示するかどうか。デフォルトはTrue。
699            show_legend : bool, optional
700                凡例を表示するかどうか。デフォルトはTrue。
701            show_std : bool, optional
702                標準偏差を表示するかどうか。デフォルトはFalse。
703            std_alpha : float, optional
704                標準偏差の透明度。デフォルトは0.2。
705            subplot_fontsize : int, optional
706                サブプロットのフォントサイズ。デフォルトは20。
707            subplot_label_ch4 : str | None, optional
708                CH4プロットのラベル。デフォルトは"(a)"。
709            subplot_label_c2h6 : str | None, optional
710                C2H6プロットのラベル。デフォルトは"(b)"。
711            ax1_ylim : tuple[float, float] | None, optional
712                CH4プロットのy軸の範囲。デフォルトはNone。
713            ax2_ylim : tuple[float, float] | None, optional
714                C2H6プロットのy軸の範囲。デフォルトはNone。
715        """
716        os.makedirs(output_dir, exist_ok=True)
717        output_path: str = os.path.join(output_dir, output_filename)
718
719        # データの準備
720        target_columns = y_cols_ch4 + y_cols_c2h6
721        hourly_means, time_points = self._prepare_diurnal_data(df, target_columns)
722
723        # 標準偏差の計算を追加
724        hourly_stds = {}
725        if show_std:
726            hourly_stds = df.groupby(df.index.hour)[target_columns].std()
727            # 24時間目のデータ点を追加
728            last_hour = hourly_stds.iloc[0:1].copy()
729            last_hour.index = [24]
730            hourly_stds = pd.concat([hourly_stds, last_hour])
731
732        # プロットの作成
733        fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
734
735        # CH4のプロット (左側)
736        ch4_lines = []
737        for y_col, label, color in zip(y_cols_ch4, labels_ch4, colors_ch4):
738            mean_values = hourly_means["all"][y_col]
739            line = ax1.plot(
740                time_points,
741                mean_values,
742                "-o",
743                label=label,
744                color=color,
745            )
746            ch4_lines.extend(line)
747
748            # 標準偏差の表示
749            if show_std:
750                std_values = hourly_stds[y_col]
751                ax1.fill_between(
752                    time_points,
753                    mean_values - std_values,
754                    mean_values + std_values,
755                    color=color,
756                    alpha=std_alpha,
757                )
758
759        # C2H6のプロット (右側)
760        c2h6_lines = []
761        for y_col, label, color in zip(y_cols_c2h6, labels_c2h6, colors_c2h6):
762            mean_values = hourly_means["all"][y_col]
763            line = ax2.plot(
764                time_points,
765                mean_values,
766                "o-",
767                label=label,
768                color=color,
769            )
770            c2h6_lines.extend(line)
771
772            # 標準偏差の表示
773            if show_std:
774                std_values = hourly_stds[y_col]
775                ax2.fill_between(
776                    time_points,
777                    mean_values - std_values,
778                    mean_values + std_values,
779                    color=color,
780                    alpha=std_alpha,
781                )
782
783        # 軸の設定
784        for ax, ylabel, subplot_label in [
785            (ax1, r"CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)", subplot_label_ch4),
786            (ax2, r"C$_2$H$_6$ flux (nmol m$^{-2}$ s$^{-1}$)", subplot_label_c2h6),
787        ]:
788            self._setup_diurnal_axes(
789                ax=ax,
790                time_points=time_points,
791                ylabel=ylabel,
792                subplot_label=subplot_label,
793                show_label=show_label,
794                show_legend=False,  # 個別の凡例は表示しない
795                subplot_fontsize=subplot_fontsize,
796            )
797
798        if ax1_ylim is not None:
799            ax1.set_ylim(ax1_ylim)
800        ax1.yaxis.set_major_locator(MultipleLocator(20))
801        ax1.yaxis.set_major_formatter(FuncFormatter(lambda x, p: f"{x:.0f}"))
802
803        if ax2_ylim is not None:
804            ax2.set_ylim(ax2_ylim)
805        ax2.yaxis.set_major_locator(MultipleLocator(1))
806        ax2.yaxis.set_major_formatter(FuncFormatter(lambda x, p: f"{x:.1f}"))
807
808        plt.tight_layout()
809
810        # 共通の凡例
811        if show_legend:
812            all_lines = ch4_lines
813            all_labels = [line.get_label() for line in ch4_lines]
814            if not legend_only_ch4:
815                all_lines += c2h6_lines
816                all_labels += [line.get_label() for line in c2h6_lines]
817            fig.legend(
818                all_lines,
819                all_labels,
820                loc="center",
821                bbox_to_anchor=(0.5, 0.02),
822                ncol=len(all_lines),
823            )
824            plt.subplots_adjust(bottom=0.25)  # 下部に凡例用のスペースを確保
825
826        fig.savefig(output_path, dpi=300, bbox_inches="tight")
827        plt.close(fig)

CH4とC2H6の日変化パターンを1つの図に並べてプロットする

Parameters:

df : pd.DataFrame
    入力データフレーム。
y_cols_ch4 : list[str]
    CH4のプロットに使用するカラム名のリスト。
y_cols_c2h6 : list[str]
    C2H6のプロットに使用するカラム名のリスト。
labels_ch4 : list[str]
    CH4の各ラインに対応するラベルのリスト。
labels_c2h6 : list[str]
    C2H6の各ラインに対応するラベルのリスト。
colors_ch4 : list[str]
    CH4の各ラインに使用する色のリスト。
colors_c2h6 : list[str]
    C2H6の各ラインに使用する色のリスト。
output_dir : str
    出力先ディレクトリのパス。
output_filename : str, optional
    出力ファイル名。デフォルトは"diurnal.png"。
legend_only_ch4 : bool, optional
    CH4の凡例のみを表示するかどうか。デフォルトはFalse。
show_label : bool, optional
    サブプロットラベルを表示するかどうか。デフォルトはTrue。
show_legend : bool, optional
    凡例を表示するかどうか。デフォルトはTrue。
show_std : bool, optional
    標準偏差を表示するかどうか。デフォルトはFalse。
std_alpha : float, optional
    標準偏差の透明度。デフォルトは0.2。
subplot_fontsize : int, optional
    サブプロットのフォントサイズ。デフォルトは20。
subplot_label_ch4 : str | None, optional
    CH4プロットのラベル。デフォルトは"(a)"。
subplot_label_c2h6 : str | None, optional
    C2H6プロットのラベル。デフォルトは"(b)"。
ax1_ylim : tuple[float, float] | None, optional
    CH4プロットのy軸の範囲。デフォルトはNone。
ax2_ylim : tuple[float, float] | None, optional
    C2H6プロットのy軸の範囲。デフォルトはNone。
def plot_c1c2_fluxes_diurnal_patterns_by_date( self, df: pandas.core.frame.DataFrame, y_col_ch4: str, y_col_c2h6: str, output_dir: str, output_filename: str = 'diurnal_by_date.png', plot_all: bool = True, plot_weekday: bool = True, plot_weekend: bool = True, plot_holiday: bool = True, show_label: bool = True, show_legend: bool = True, show_std: bool = False, std_alpha: float = 0.2, legend_only_ch4: bool = False, subplot_fontsize: int = 20, subplot_label_ch4: str | None = '(a)', subplot_label_c2h6: str | None = '(b)', ax1_ylim: tuple[float, float] | None = None, ax2_ylim: tuple[float, float] | None = None, print_summary: bool = True) -> None:
 829    def plot_c1c2_fluxes_diurnal_patterns_by_date(
 830        self,
 831        df: pd.DataFrame,
 832        y_col_ch4: str,
 833        y_col_c2h6: str,
 834        output_dir: str,
 835        output_filename: str = "diurnal_by_date.png",
 836        plot_all: bool = True,
 837        plot_weekday: bool = True,
 838        plot_weekend: bool = True,
 839        plot_holiday: bool = True,
 840        show_label: bool = True,
 841        show_legend: bool = True,
 842        show_std: bool = False,  # 標準偏差表示のオプションを追加
 843        std_alpha: float = 0.2,  # 標準偏差の透明度
 844        legend_only_ch4: bool = False,
 845        subplot_fontsize: int = 20,
 846        subplot_label_ch4: str | None = "(a)",
 847        subplot_label_c2h6: str | None = "(b)",
 848        ax1_ylim: tuple[float, float] | None = None,
 849        ax2_ylim: tuple[float, float] | None = None,
 850        print_summary: bool = True,  # 追加: 統計情報を表示するかどうか
 851    ) -> None:
 852        """CH4とC2H6の日変化パターンを日付分類して1つの図に並べてプロットする
 853
 854        Parameters:
 855        ------
 856            df : pd.DataFrame
 857                入力データフレーム。
 858            y_col_ch4 : str
 859                CH4フラックスを含むカラム名。
 860            y_col_c2h6 : str
 861                C2H6フラックスを含むカラム名。
 862            output_dir : str
 863                出力先ディレクトリのパス。
 864            output_filename : str, optional
 865                出力ファイル名。デフォルトは"diurnal_by_date.png"。
 866            plot_all : bool, optional
 867                すべての日をプロットするかどうか。デフォルトはTrue。
 868            plot_weekday : bool, optional
 869                平日をプロットするかどうか。デフォルトはTrue。
 870            plot_weekend : bool, optional
 871                週末をプロットするかどうか。デフォルトはTrue。
 872            plot_holiday : bool, optional
 873                祝日をプロットするかどうか。デフォルトはTrue。
 874            show_label : bool, optional
 875                サブプロットラベルを表示するかどうか。デフォルトはTrue。
 876            show_legend : bool, optional
 877                凡例を表示するかどうか。デフォルトはTrue。
 878            show_std : bool, optional
 879                標準偏差を表示するかどうか。デフォルトはFalse。
 880            std_alpha : float, optional
 881                標準偏差の透明度。デフォルトは0.2。
 882            legend_only_ch4 : bool, optional
 883                CH4の凡例のみを表示するかどうか。デフォルトはFalse。
 884            subplot_fontsize : int, optional
 885                サブプロットのフォントサイズ。デフォルトは20。
 886            subplot_label_ch4 : str | None, optional
 887                CH4プロットのラベル。デフォルトは"(a)"。
 888            subplot_label_c2h6 : str | None, optional
 889                C2H6プロットのラベル。デフォルトは"(b)"。
 890            ax1_ylim : tuple[float, float] | None, optional
 891                CH4プロットのy軸の範囲。デフォルトはNone。
 892            ax2_ylim : tuple[float, float] | None, optional
 893                C2H6プロットのy軸の範囲。デフォルトはNone。
 894            print_summary : bool, optional
 895                統計情報を表示するかどうか。デフォルトはTrue。
 896        """
 897        os.makedirs(output_dir, exist_ok=True)
 898        output_path: str = os.path.join(output_dir, output_filename)
 899
 900        # データの準備
 901        target_columns = [y_col_ch4, y_col_c2h6]
 902        hourly_means, time_points = self._prepare_diurnal_data(
 903            df, target_columns, include_date_types=True
 904        )
 905
 906        # 標準偏差の計算を追加
 907        hourly_stds = {}
 908        if show_std:
 909            for condition in ["all", "weekday", "weekend", "holiday"]:
 910                if condition == "all":
 911                    condition_data = df
 912                elif condition == "weekday":
 913                    condition_data = df[
 914                        ~(
 915                            df.index.dayofweek.isin([5, 6])
 916                            | df.index.map(lambda x: jpholiday.is_holiday(x.date()))
 917                        )
 918                    ]
 919                elif condition == "weekend":
 920                    condition_data = df[df.index.dayofweek.isin([5, 6])]
 921                else:  # holiday
 922                    condition_data = df[
 923                        df.index.map(lambda x: jpholiday.is_holiday(x.date()))
 924                    ]
 925
 926                hourly_stds[condition] = condition_data.groupby(
 927                    condition_data.index.hour
 928                )[target_columns].std()
 929                # 24時間目のデータ点を追加
 930                last_hour = hourly_stds[condition].iloc[0:1].copy()
 931                last_hour.index = [24]
 932                hourly_stds[condition] = pd.concat([hourly_stds[condition], last_hour])
 933
 934        # プロットスタイルの設定
 935        styles = {
 936            "all": {
 937                "color": "black",
 938                "linestyle": "-",
 939                "alpha": 1.0,
 940                "label": "All days",
 941            },
 942            "weekday": {
 943                "color": "blue",
 944                "linestyle": "-",
 945                "alpha": 0.8,
 946                "label": "Weekdays",
 947            },
 948            "weekend": {
 949                "color": "red",
 950                "linestyle": "-",
 951                "alpha": 0.8,
 952                "label": "Weekends",
 953            },
 954            "holiday": {
 955                "color": "green",
 956                "linestyle": "-",
 957                "alpha": 0.8,
 958                "label": "Weekends & Holidays",
 959            },
 960        }
 961
 962        # プロット対象の条件を選択
 963        plot_conditions = {
 964            "all": plot_all,
 965            "weekday": plot_weekday,
 966            "weekend": plot_weekend,
 967            "holiday": plot_holiday,
 968        }
 969        selected_conditions = {
 970            col: means
 971            for col, means in hourly_means.items()
 972            if col in plot_conditions and plot_conditions[col]
 973        }
 974
 975        # プロットの作成
 976        fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
 977
 978        # CH4とC2H6のプロット用のラインオブジェクトを保存
 979        ch4_lines = []
 980        c2h6_lines = []
 981
 982        # CH4とC2H6のプロット
 983        for condition, means in selected_conditions.items():
 984            style = styles[condition].copy()
 985
 986            # CH4プロット
 987            mean_values_ch4 = means[y_col_ch4]
 988            line_ch4 = ax1.plot(time_points, mean_values_ch4, marker="o", **style)
 989            ch4_lines.extend(line_ch4)
 990
 991            if show_std and condition in hourly_stds:
 992                std_values = hourly_stds[condition][y_col_ch4]
 993                ax1.fill_between(
 994                    time_points,
 995                    mean_values_ch4 - std_values,
 996                    mean_values_ch4 + std_values,
 997                    color=style["color"],
 998                    alpha=std_alpha,
 999                )
1000
1001            # C2H6プロット
1002            style["linestyle"] = "--"
1003            mean_values_c2h6 = means[y_col_c2h6]
1004            line_c2h6 = ax2.plot(time_points, mean_values_c2h6, marker="o", **style)
1005            c2h6_lines.extend(line_c2h6)
1006
1007            if show_std and condition in hourly_stds:
1008                std_values = hourly_stds[condition][y_col_c2h6]
1009                ax2.fill_between(
1010                    time_points,
1011                    mean_values_c2h6 - std_values,
1012                    mean_values_c2h6 + std_values,
1013                    color=style["color"],
1014                    alpha=std_alpha,
1015                )
1016
1017        # 軸の設定
1018        for ax, ylabel, subplot_label in [
1019            (ax1, r"CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)", subplot_label_ch4),
1020            (ax2, r"C$_2$H$_6$ flux (nmol m$^{-2}$ s$^{-1}$)", subplot_label_c2h6),
1021        ]:
1022            self._setup_diurnal_axes(
1023                ax=ax,
1024                time_points=time_points,
1025                ylabel=ylabel,
1026                subplot_label=subplot_label,
1027                show_label=show_label,
1028                show_legend=False,
1029                subplot_fontsize=subplot_fontsize,
1030            )
1031
1032        if ax1_ylim is not None:
1033            ax1.set_ylim(ax1_ylim)
1034        ax1.yaxis.set_major_locator(MultipleLocator(20))
1035        ax1.yaxis.set_major_formatter(FuncFormatter(lambda x, p: f"{x:.0f}"))
1036
1037        if ax2_ylim is not None:
1038            ax2.set_ylim(ax2_ylim)
1039        ax2.yaxis.set_major_locator(MultipleLocator(1))
1040        ax2.yaxis.set_major_formatter(FuncFormatter(lambda x, p: f"{x:.1f}"))
1041
1042        plt.tight_layout()
1043
1044        # 共通の凡例を図の下部に配置
1045        if show_legend:
1046            lines_to_show = (
1047                ch4_lines if legend_only_ch4 else ch4_lines[: len(selected_conditions)]
1048            )
1049            fig.legend(
1050                lines_to_show,
1051                [
1052                    style["label"]
1053                    for style in list(styles.values())[: len(lines_to_show)]
1054                ],
1055                loc="center",
1056                bbox_to_anchor=(0.5, 0.02),
1057                ncol=len(lines_to_show),
1058            )
1059            plt.subplots_adjust(bottom=0.25)  # 下部に凡例用のスペースを確保
1060
1061        fig.savefig(output_path, dpi=300, bbox_inches="tight")
1062        plt.close(fig)
1063
1064        # 日変化パターンの統計分析を追加
1065        if print_summary:
1066            # 平日と休日のデータを準備
1067            dates = pd.to_datetime(df.index)
1068            is_weekend = dates.dayofweek.isin([5, 6])
1069            is_holiday = dates.map(lambda x: jpholiday.is_holiday(x.date()))
1070            is_weekday = ~(is_weekend | is_holiday)
1071
1072            weekday_data = df[is_weekday]
1073            holiday_data = df[is_weekend | is_holiday]
1074
1075            def get_diurnal_stats(data, column):
1076                # 時間ごとの平均値を計算
1077                hourly_means = data.groupby(data.index.hour)[column].mean()
1078
1079                # 8-16時の時間帯の統計
1080                daytime_means = hourly_means[
1081                    (hourly_means.index >= 8) & (hourly_means.index <= 16)
1082                ]
1083
1084                if len(daytime_means) == 0:
1085                    return None
1086
1087                return {
1088                    "mean": daytime_means.mean(),
1089                    "max": daytime_means.max(),
1090                    "max_hour": daytime_means.idxmax(),
1091                    "min": daytime_means.min(),
1092                    "min_hour": daytime_means.idxmin(),
1093                    "hours_count": len(daytime_means),
1094                }
1095
1096            # CH4とC2H6それぞれの統計を計算
1097            for col, gas_name in [(y_col_ch4, "CH4"), (y_col_c2h6, "C2H6")]:
1098                print(f"\n=== {gas_name} フラックス 8-16時の統計分析 ===")
1099
1100                weekday_stats = get_diurnal_stats(weekday_data, col)
1101                holiday_stats = get_diurnal_stats(holiday_data, col)
1102
1103                if weekday_stats and holiday_stats:
1104                    print("\n平日:")
1105                    print(f"  平均値: {weekday_stats['mean']:.2f}")
1106                    print(
1107                        f"  最大値: {weekday_stats['max']:.2f} ({weekday_stats['max_hour']}時)"
1108                    )
1109                    print(
1110                        f"  最小値: {weekday_stats['min']:.2f} ({weekday_stats['min_hour']}時)"
1111                    )
1112                    print(f"  集計時間数: {weekday_stats['hours_count']}")
1113
1114                    print("\n休日:")
1115                    print(f"  平均値: {holiday_stats['mean']:.2f}")
1116                    print(
1117                        f"  最大値: {holiday_stats['max']:.2f} ({holiday_stats['max_hour']}時)"
1118                    )
1119                    print(
1120                        f"  最小値: {holiday_stats['min']:.2f} ({holiday_stats['min_hour']}時)"
1121                    )
1122                    print(f"  集計時間数: {holiday_stats['hours_count']}")
1123
1124                    # 平日/休日の比率を計算
1125                    print("\n平日/休日の比率:")
1126                    print(
1127                        f"  平均値比: {weekday_stats['mean'] / holiday_stats['mean']:.2f}"
1128                    )
1129                    print(
1130                        f"  最大値比: {weekday_stats['max'] / holiday_stats['max']:.2f}"
1131                    )
1132                    print(
1133                        f"  最小値比: {weekday_stats['min'] / holiday_stats['min']:.2f}"
1134                    )
1135                else:
1136                    print("十分なデータがありません")

CH4とC2H6の日変化パターンを日付分類して1つの図に並べてプロットする

Parameters:

df : pd.DataFrame
    入力データフレーム。
y_col_ch4 : str
    CH4フラックスを含むカラム名。
y_col_c2h6 : str
    C2H6フラックスを含むカラム名。
output_dir : str
    出力先ディレクトリのパス。
output_filename : str, optional
    出力ファイル名。デフォルトは"diurnal_by_date.png"。
plot_all : bool, optional
    すべての日をプロットするかどうか。デフォルトはTrue。
plot_weekday : bool, optional
    平日をプロットするかどうか。デフォルトはTrue。
plot_weekend : bool, optional
    週末をプロットするかどうか。デフォルトはTrue。
plot_holiday : bool, optional
    祝日をプロットするかどうか。デフォルトはTrue。
show_label : bool, optional
    サブプロットラベルを表示するかどうか。デフォルトはTrue。
show_legend : bool, optional
    凡例を表示するかどうか。デフォルトはTrue。
show_std : bool, optional
    標準偏差を表示するかどうか。デフォルトはFalse。
std_alpha : float, optional
    標準偏差の透明度。デフォルトは0.2。
legend_only_ch4 : bool, optional
    CH4の凡例のみを表示するかどうか。デフォルトはFalse。
subplot_fontsize : int, optional
    サブプロットのフォントサイズ。デフォルトは20。
subplot_label_ch4 : str | None, optional
    CH4プロットのラベル。デフォルトは"(a)"。
subplot_label_c2h6 : str | None, optional
    C2H6プロットのラベル。デフォルトは"(b)"。
ax1_ylim : tuple[float, float] | None, optional
    CH4プロットのy軸の範囲。デフォルトはNone。
ax2_ylim : tuple[float, float] | None, optional
    C2H6プロットのy軸の範囲。デフォルトはNone。
print_summary : bool, optional
    統計情報を表示するかどうか。デフォルトはTrue。
def plot_diurnal_concentrations( self, df: pandas.core.frame.DataFrame, output_dir: str, col_ch4_conc: str = 'CH4_ultra_cal', col_c2h6_conc: str = 'C2H6_ultra_cal', col_datetime: str = 'Date', output_filename: str = 'diurnal_concentrations.png', show_std: bool = True, alpha_std: float = 0.2, add_legend: bool = True, print_summary: bool = True, subplot_label_ch4: str | None = None, subplot_label_c2h6: str | None = None, subplot_fontsize: int = 24, ch4_ylim: tuple[float, float] | None = None, c2h6_ylim: tuple[float, float] | None = None, interval: str = '1H') -> None:
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"を指定
def plot_flux_diurnal_patterns_with_std( self, df: pandas.core.frame.DataFrame, output_dir: str, col_ch4_flux: str = 'Fch4', col_c2h6_flux: str = 'Fc2h6', ch4_label: str = '$\\mathregular{CH_{4}}$フラックス', c2h6_label: str = '$\\mathregular{C_{2}H_{6}}$フラックス', col_datetime: str = 'Date', output_filename: str = 'diurnal_patterns.png', window_size: int = 6, show_std: bool = True, alpha_std: float = 0.1) -> None:
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)
def plot_scatter( self, df: pandas.core.frame.DataFrame, x_col: str, y_col: str, output_dir: str, output_filename: str = 'scatter.png', xlabel: str | None = None, ylabel: str | None = None, show_label: bool = True, x_axis_range: tuple | None = None, y_axis_range: tuple | None = None, fixed_slope: float = 0.076, show_fixed_slope: bool = False, x_scientific: bool = False, y_scientific: bool = False) -> None:
1478    def plot_scatter(
1479        self,
1480        df: pd.DataFrame,
1481        x_col: str,
1482        y_col: str,
1483        output_dir: str,
1484        output_filename: str = "scatter.png",
1485        xlabel: str | None = None,
1486        ylabel: str | None = None,
1487        show_label: bool = True,
1488        x_axis_range: tuple | None = None,
1489        y_axis_range: tuple | None = None,
1490        fixed_slope: float = 0.076,
1491        show_fixed_slope: bool = False,
1492        x_scientific: bool = False,  # 追加:x軸を指数表記にするかどうか
1493        y_scientific: bool = False,  # 追加:y軸を指数表記にするかどうか
1494    ) -> None:
1495        """散布図を作成し、TLS回帰直線を描画します。
1496
1497        Parameters:
1498        ------
1499            df : pd.DataFrame
1500                プロットに使用するデータフレーム
1501            x_col : str
1502                x軸に使用する列名
1503            y_col : str
1504                y軸に使用する列名
1505            xlabel : str
1506                x軸のラベル
1507            ylabel : str
1508                y軸のラベル
1509            output_dir : str
1510                出力先ディレクトリ
1511            output_filename : str, optional
1512                出力ファイル名。デフォルトは"scatter.png"
1513            show_label : bool, optional
1514                軸ラベルを表示するかどうか。デフォルトはTrue
1515            x_axis_range : tuple, optional
1516                x軸の範囲。デフォルトはNone。
1517            y_axis_range : tuple, optional
1518                y軸の範囲。デフォルトはNone。
1519            fixed_slope : float, optional
1520                固定傾きを指定するための値。デフォルトは0.076
1521            show_fixed_slope : bool, optional
1522                固定傾きの線を表示するかどうか。デフォルトはFalse
1523        """
1524        os.makedirs(output_dir, exist_ok=True)
1525        output_path: str = os.path.join(output_dir, output_filename)
1526
1527        # 有効なデータの抽出
1528        df = MonthlyFiguresGenerator.get_valid_data(df, x_col, y_col)
1529
1530        # データの準備
1531        x = df[x_col].values
1532        y = df[y_col].values
1533
1534        # データの中心化
1535        x_mean = np.mean(x)
1536        y_mean = np.mean(y)
1537        x_c = x - x_mean
1538        y_c = y - y_mean
1539
1540        # TLS回帰の計算
1541        data_matrix = np.vstack((x_c, y_c))
1542        cov_matrix = np.cov(data_matrix)
1543        _, eigenvecs = linalg.eigh(cov_matrix)
1544        largest_eigenvec = eigenvecs[:, -1]
1545
1546        slope = largest_eigenvec[1] / largest_eigenvec[0]
1547        intercept = y_mean - slope * x_mean
1548
1549        # R²とRMSEの計算
1550        y_pred = slope * x + intercept
1551        r_squared = 1 - np.sum((y - y_pred) ** 2) / np.sum((y - np.mean(y)) ** 2)
1552        rmse = np.sqrt(np.mean((y - y_pred) ** 2))
1553
1554        # プロットの作成
1555        fig, ax = plt.subplots(figsize=(6, 6))
1556
1557        # データ点のプロット
1558        ax.scatter(x, y, color="black")
1559
1560        # データの範囲を取得
1561        if x_axis_range is None:
1562            x_axis_range = (df[x_col].min(), df[x_col].max())
1563        if y_axis_range is None:
1564            y_axis_range = (df[y_col].min(), df[y_col].max())
1565
1566        # 回帰直線のプロット
1567        x_range = np.linspace(x_axis_range[0], x_axis_range[1], 150)
1568        y_range = slope * x_range + intercept
1569        ax.plot(x_range, y_range, "r", label="TLS regression")
1570
1571        # 傾き固定の線を追加(フラグがTrueの場合)
1572        if show_fixed_slope:
1573            fixed_intercept = (
1574                y_mean - fixed_slope * x_mean
1575            )  # 中心点を通るように切片を計算
1576            y_fixed = fixed_slope * x_range + fixed_intercept
1577            ax.plot(x_range, y_fixed, "b--", label=f"Slope = {fixed_slope}", alpha=0.7)
1578
1579        # 軸の設定
1580        ax.set_xlim(x_axis_range)
1581        ax.set_ylim(y_axis_range)
1582
1583        # 指数表記の設定
1584        if x_scientific:
1585            ax.ticklabel_format(style="sci", axis="x", scilimits=(0, 0))
1586            ax.xaxis.get_offset_text().set_position((1.1, 0))  # 指数の位置調整
1587        if y_scientific:
1588            ax.ticklabel_format(style="sci", axis="y", scilimits=(0, 0))
1589            ax.yaxis.get_offset_text().set_position((0, 1.1))  # 指数の位置調整
1590
1591        if show_label:
1592            if xlabel is not None:
1593                ax.set_xlabel(xlabel)
1594            if ylabel is not None:
1595                ax.set_ylabel(ylabel)
1596
1597        # 1:1の関係を示す点線(軸の範囲が同じ場合のみ表示)
1598        if (
1599            x_axis_range is not None
1600            and y_axis_range is not None
1601            and x_axis_range == y_axis_range
1602        ):
1603            ax.plot(
1604                [x_axis_range[0], x_axis_range[1]],
1605                [x_axis_range[0], x_axis_range[1]],
1606                "k--",
1607                alpha=0.5,
1608            )
1609
1610        # 回帰情報の表示
1611        equation = (
1612            f"y = {slope:.2f}x {'+' if intercept >= 0 else '-'} {abs(intercept):.2f}"
1613        )
1614        position_x = 0.05
1615        fig_ha: str = "left"
1616        ax.text(
1617            position_x,
1618            0.95,
1619            equation,
1620            transform=ax.transAxes,
1621            va="top",
1622            ha=fig_ha,
1623            color="red",
1624        )
1625        ax.text(
1626            position_x,
1627            0.88,
1628            f"R² = {r_squared:.2f}",
1629            transform=ax.transAxes,
1630            va="top",
1631            ha=fig_ha,
1632            color="red",
1633        )
1634        ax.text(
1635            position_x,
1636            0.81,  # RMSEのための新しい位置
1637            f"RMSE = {rmse:.2f}",
1638            transform=ax.transAxes,
1639            va="top",
1640            ha=fig_ha,
1641            color="red",
1642        )
1643        # 目盛り線の設定
1644        ax.grid(True, alpha=0.3)
1645
1646        fig.savefig(output_path, dpi=300, bbox_inches="tight")
1647        plt.close(fig)

散布図を作成し、TLS回帰直線を描画します。

Parameters:

df : pd.DataFrame
    プロットに使用するデータフレーム
x_col : str
    x軸に使用する列名
y_col : str
    y軸に使用する列名
xlabel : str
    x軸のラベル
ylabel : str
    y軸のラベル
output_dir : str
    出力先ディレクトリ
output_filename : str, optional
    出力ファイル名。デフォルトは"scatter.png"
show_label : bool, optional
    軸ラベルを表示するかどうか。デフォルトはTrue
x_axis_range : tuple, optional
    x軸の範囲。デフォルトはNone。
y_axis_range : tuple, optional
    y軸の範囲。デフォルトはNone。
fixed_slope : float, optional
    固定傾きを指定するための値。デフォルトは0.076
show_fixed_slope : bool, optional
    固定傾きの線を表示するかどうか。デフォルトはFalse
def plot_source_contributions_diurnal( self, df: pandas.core.frame.DataFrame, output_dir: str, col_ch4_flux: str, col_c2h6_flux: str, label_gas: str = 'gas', label_bio: str = 'bio', col_datetime: str = 'Date', output_filename: str = 'source_contributions.png', window_size: int = 6, print_summary: bool = True, show_legend: bool = False, smooth: bool = False, y_max: float = 100, subplot_label: str | None = None, subplot_fontsize: int = 20) -> None:
1649    def plot_source_contributions_diurnal(
1650        self,
1651        df: pd.DataFrame,
1652        output_dir: str,
1653        col_ch4_flux: str,
1654        col_c2h6_flux: str,
1655        label_gas: str = "gas",
1656        label_bio: str = "bio",
1657        col_datetime: str = "Date",
1658        output_filename: str = "source_contributions.png",
1659        window_size: int = 6,  # 移動平均の窓サイズ
1660        print_summary: bool = True,  # 統計情報を表示するかどうか,
1661        show_legend: bool = False,
1662        smooth: bool = False,
1663        y_max: float = 100,  # y軸の上限値を追加
1664        subplot_label: str | None = None,
1665        subplot_fontsize: int = 20,
1666    ) -> None:
1667        """CH4フラックスの都市ガス起源と生物起源の日変化を積み上げグラフとして表示
1668
1669        Parameters:
1670        ------
1671            df : pd.DataFrame
1672                データフレーム
1673            output_dir : str
1674                出力ディレクトリのパス
1675            col_ch4_flux : str
1676                CH4フラックスのカラム名
1677            col_c2h6_flux : str
1678                C2H6フラックスのカラム名
1679            label_gas : str
1680                都市ガス起源のラベル
1681            label_bio : str
1682                生物起源のラベル
1683            col_datetime : str
1684                日時カラムの名前
1685            output_filename : str
1686                出力ファイル名
1687            window_size : int
1688                移動平均の窓サイズ
1689            print_summary : bool
1690                統計情報を表示するかどうか
1691            smooth : bool
1692                移動平均を適用するかどうか
1693            y_max : float
1694                y軸の上限値(デフォルト: 100)
1695        """
1696        # 出力ディレクトリの作成
1697        os.makedirs(output_dir, exist_ok=True)
1698        output_path: str = os.path.join(output_dir, output_filename)
1699
1700        # 起源の計算
1701        df_with_sources = self._calculate_source_contributions(
1702            df=df,
1703            col_ch4_flux=col_ch4_flux,
1704            col_c2h6_flux=col_c2h6_flux,
1705            col_datetime=col_datetime,
1706        )
1707
1708        # 時刻データの抽出とグループ化
1709        df_with_sources["hour"] = df_with_sources.index.hour
1710        hourly_means = df_with_sources.groupby("hour")[["ch4_gas", "ch4_bio"]].mean()
1711
1712        # 24時間目のデータ点を追加(0時のデータを使用)
1713        last_hour = hourly_means.iloc[0:1].copy()
1714        last_hour.index = [24]
1715        hourly_means = pd.concat([hourly_means, last_hour])
1716
1717        # 移動平均の適用
1718        hourly_means_smoothed = hourly_means
1719        if smooth:
1720            hourly_means_smoothed = hourly_means.rolling(
1721                window=window_size, center=True, min_periods=1
1722            ).mean()
1723
1724        # 24時間分のデータポイントを作成
1725        time_points = pd.date_range("2024-01-01", periods=25, freq="h")
1726
1727        # プロットの作成
1728        plt.figure(figsize=(10, 6))
1729        ax = plt.gca()
1730
1731        # サブプロットラベルの追加(subplot_labelが指定されている場合)
1732        if subplot_label:
1733            ax.text(
1734                0.02,  # x位置
1735                0.98,  # y位置
1736                subplot_label,
1737                transform=ax.transAxes,
1738                va="top",
1739                fontsize=subplot_fontsize,
1740            )
1741
1742        # 積み上げプロット
1743        ax.fill_between(
1744            time_points,
1745            0,
1746            hourly_means_smoothed["ch4_bio"],
1747            color="blue",
1748            alpha=0.6,
1749            label=label_bio,
1750        )
1751        ax.fill_between(
1752            time_points,
1753            hourly_means_smoothed["ch4_bio"],
1754            hourly_means_smoothed["ch4_bio"] + hourly_means_smoothed["ch4_gas"],
1755            color="red",
1756            alpha=0.6,
1757            label=label_gas,
1758        )
1759
1760        # 合計値のライン
1761        total_flux = hourly_means_smoothed["ch4_bio"] + hourly_means_smoothed["ch4_gas"]
1762        ax.plot(time_points, total_flux, "-", color="black", alpha=0.5)
1763
1764        # 軸の設定
1765        ax.set_xlabel("Time (hour)")
1766        ax.set_ylabel(r"CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)")
1767        ax.xaxis.set_major_formatter(mdates.DateFormatter("%-H"))
1768        ax.xaxis.set_major_locator(mdates.HourLocator(byhour=[0, 6, 12, 18, 24]))
1769        ax.set_xlim(time_points[0], time_points[-1])
1770        ax.set_ylim(0, y_max)  # y軸の範囲を設定
1771        ax.grid(True, alpha=0.3)
1772
1773        # 凡例を図の下部に配置
1774        if show_legend:
1775            handles, labels = ax.get_legend_handles_labels()
1776            fig = plt.gcf()  # 現在の図を取得
1777            fig.legend(
1778                handles,
1779                labels,
1780                loc="center",
1781                bbox_to_anchor=(0.5, 0.01),
1782                ncol=len(handles),
1783            )
1784            plt.subplots_adjust(bottom=0.2)  # 下部に凡例用のスペースを確保
1785
1786        # グラフの保存
1787        plt.tight_layout()
1788        plt.savefig(output_path, dpi=300, bbox_inches="tight")
1789        plt.close()
1790
1791        # 統計情報の表示
1792        if print_summary:
1793            stats = {
1794                "都市ガス起源": hourly_means["ch4_gas"],
1795                "生物起源": hourly_means["ch4_bio"],
1796                "合計": hourly_means["ch4_gas"] + hourly_means["ch4_bio"],
1797            }
1798
1799            for source, data in stats.items():
1800                mean_val = data.mean()
1801                min_val = data.min()
1802                max_val = data.max()
1803                min_time = data.idxmin()
1804                max_time = data.idxmax()
1805
1806                self.logger.info(f"{source}の統計:")
1807                print(f"  平均値: {mean_val:.2f}")
1808                print(f"  最小値: {min_val:.2f} (Hour: {min_time})")
1809                print(f"  最大値: {max_val:.2f} (Hour: {max_time})")
1810                if min_val != 0:
1811                    print(f"  最大/最小比: {max_val / min_val:.2f}")

CH4フラックスの都市ガス起源と生物起源の日変化を積み上げグラフとして表示

Parameters:

df : pd.DataFrame
    データフレーム
output_dir : str
    出力ディレクトリのパス
col_ch4_flux : str
    CH4フラックスのカラム名
col_c2h6_flux : str
    C2H6フラックスのカラム名
label_gas : str
    都市ガス起源のラベル
label_bio : str
    生物起源のラベル
col_datetime : str
    日時カラムの名前
output_filename : str
    出力ファイル名
window_size : int
    移動平均の窓サイズ
print_summary : bool
    統計情報を表示するかどうか
smooth : bool
    移動平均を適用するかどうか
y_max : float
    y軸の上限値(デフォルト: 100)
def plot_source_contributions_diurnal_by_date( self, df: pandas.core.frame.DataFrame, output_dir: str, col_ch4_flux: str, col_c2h6_flux: str, label_gas: str = 'gas', label_bio: str = 'bio', col_datetime: str = 'Date', output_filename: str = 'source_contributions_by_date.png', show_label: bool = True, show_legend: bool = False, print_summary: bool = False, subplot_fontsize: int = 20, subplot_label_weekday: str | None = None, subplot_label_weekend: str | None = None, y_max: float | None = None) -> None:
1813    def plot_source_contributions_diurnal_by_date(
1814        self,
1815        df: pd.DataFrame,
1816        output_dir: str,
1817        col_ch4_flux: str,
1818        col_c2h6_flux: str,
1819        label_gas: str = "gas",
1820        label_bio: str = "bio",
1821        col_datetime: str = "Date",
1822        output_filename: str = "source_contributions_by_date.png",
1823        show_label: bool = True,
1824        show_legend: bool = False,
1825        print_summary: bool = False,  # 統計情報を表示するかどうか,
1826        subplot_fontsize: int = 20,
1827        subplot_label_weekday: str | None = None,
1828        subplot_label_weekend: str | None = None,
1829        y_max: float | None = None,  # y軸の上限値
1830    ) -> None:
1831        """CH4フラックスの都市ガス起源と生物起源の日変化を平日・休日別に表示
1832
1833        Parameters:
1834        ------
1835            df : pd.DataFrame
1836                データフレーム
1837            output_dir : str
1838                出力ディレクトリのパス
1839            col_ch4_flux : str
1840                CH4フラックスのカラム名
1841            col_c2h6_flux : str
1842                C2H6フラックスのカラム名
1843            label_gas : str
1844                都市ガス起源のラベル
1845            label_bio : str
1846                生物起源のラベル
1847            col_datetime : str
1848                日時カラムの名前
1849            output_filename : str
1850                出力ファイル名
1851            show_label : bool
1852                ラベルを表示するか
1853            show_legend : bool
1854                凡例を表示するか
1855            subplot_fontsize : int
1856                サブプロットのフォントサイズ
1857            subplot_label_weekday : str | None
1858                平日グラフのラベル
1859            subplot_label_weekend : str | None
1860                休日グラフのラベル
1861            y_max : float | None
1862                y軸の上限値
1863        """
1864        # 出力ディレクトリの作成
1865        os.makedirs(output_dir, exist_ok=True)
1866        output_path: str = os.path.join(output_dir, output_filename)
1867
1868        # 起源の計算
1869        df_with_sources = self._calculate_source_contributions(
1870            df=df,
1871            col_ch4_flux=col_ch4_flux,
1872            col_c2h6_flux=col_c2h6_flux,
1873            col_datetime=col_datetime,
1874        )
1875
1876        # 日付タイプの分類
1877        dates = pd.to_datetime(df_with_sources.index)
1878        is_weekend = dates.dayofweek.isin([5, 6])
1879        is_holiday = dates.map(lambda x: jpholiday.is_holiday(x.date()))
1880        is_weekday = ~(is_weekend | is_holiday)
1881
1882        # データの分類
1883        data_weekday = df_with_sources[is_weekday]
1884        data_holiday = df_with_sources[is_weekend | is_holiday]
1885
1886        # プロットの作成
1887        fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
1888
1889        # 平日と休日それぞれのプロット
1890        for ax, data, label in [
1891            (ax1, data_weekday, "Weekdays"),
1892            (ax2, data_holiday, "Weekends & Holidays"),
1893        ]:
1894            # 時間ごとの平均値を計算
1895            hourly_means = data.groupby(data.index.hour)[["ch4_gas", "ch4_bio"]].mean()
1896
1897            # 24時間目のデータ点を追加
1898            last_hour = hourly_means.iloc[0:1].copy()
1899            last_hour.index = [24]
1900            hourly_means = pd.concat([hourly_means, last_hour])
1901
1902            # 24時間分のデータポイントを作成
1903            time_points = pd.date_range("2024-01-01", periods=25, freq="h")
1904
1905            # 積み上げプロット
1906            ax.fill_between(
1907                time_points,
1908                0,
1909                hourly_means["ch4_bio"],
1910                color="blue",
1911                alpha=0.6,
1912                label=label_bio,
1913            )
1914            ax.fill_between(
1915                time_points,
1916                hourly_means["ch4_bio"],
1917                hourly_means["ch4_bio"] + hourly_means["ch4_gas"],
1918                color="red",
1919                alpha=0.6,
1920                label=label_gas,
1921            )
1922
1923            # 合計値のライン
1924            total_flux = hourly_means["ch4_bio"] + hourly_means["ch4_gas"]
1925            ax.plot(time_points, total_flux, "-", color="black", alpha=0.5)
1926
1927            # 軸の設定
1928            if show_label:
1929                ax.set_xlabel("Time (hour)")
1930                if ax == ax1:  # 左側のプロットのラベル
1931                    ax.set_ylabel("Weekdays CH$_4$ flux\n" r"(nmol m$^{-2}$ s$^{-1}$)")
1932                else:  # 右側のプロットのラベル
1933                    ax.set_ylabel("Weekends CH$_4$ flux\n" r"(nmol m$^{-2}$ s$^{-1}$)")
1934
1935            ax.xaxis.set_major_formatter(mdates.DateFormatter("%-H"))
1936            ax.xaxis.set_major_locator(mdates.HourLocator(byhour=[0, 6, 12, 18, 24]))
1937            ax.set_xlim(time_points[0], time_points[-1])
1938            if y_max is not None:
1939                ax.set_ylim(0, y_max)
1940            ax.grid(True, alpha=0.3)
1941
1942        # サブプロットラベルの追加
1943        if subplot_label_weekday:
1944            ax1.text(
1945                0.02,
1946                0.98,
1947                subplot_label_weekday,
1948                transform=ax1.transAxes,
1949                va="top",
1950                fontsize=subplot_fontsize,
1951            )
1952        if subplot_label_weekend:
1953            ax2.text(
1954                0.02,
1955                0.98,
1956                subplot_label_weekend,
1957                transform=ax2.transAxes,
1958                va="top",
1959                fontsize=subplot_fontsize,
1960            )
1961
1962        # 凡例を図の下部に配置
1963        if show_legend:
1964            # 最初のプロットから凡例のハンドルとラベルを取得
1965            handles, labels = ax1.get_legend_handles_labels()
1966            # 図の下部に凡例を配置
1967            fig.legend(
1968                handles,
1969                labels,
1970                loc="center",
1971                bbox_to_anchor=(0.5, 0.01),  # x=0.5で中央、y=0.01で下部に配置
1972                ncol=len(handles),  # ハンドルの数だけ列を作成(一行に表示)
1973            )
1974            # 凡例用のスペースを確保
1975            plt.subplots_adjust(bottom=0.2)  # 下部に30%のスペースを確保
1976
1977        plt.tight_layout()
1978        plt.savefig(output_path, dpi=300, bbox_inches="tight")
1979        plt.close()
1980
1981        # 統計情報の表示
1982        if print_summary:
1983            for data, label in [
1984                (data_weekday, "Weekdays"),
1985                (data_holiday, "Weekends & Holidays"),
1986            ]:
1987                hourly_means = data.groupby(data.index.hour)[
1988                    ["ch4_gas", "ch4_bio"]
1989                ].mean()
1990                total_flux = hourly_means["ch4_gas"] + hourly_means["ch4_bio"]
1991
1992                print(f"\n{label}の統計:")
1993                print(f"  平均値: {total_flux.mean():.2f}")
1994                print(f"  最小値: {total_flux.min():.2f} (Hour: {total_flux.idxmin()})")
1995                print(f"  最大値: {total_flux.max():.2f} (Hour: {total_flux.idxmax()})")
1996                if total_flux.min() != 0:
1997                    print(f"  最大/最小比: {total_flux.max() / total_flux.min():.2f}")

CH4フラックスの都市ガス起源と生物起源の日変化を平日・休日別に表示

Parameters:

df : pd.DataFrame
    データフレーム
output_dir : str
    出力ディレクトリのパス
col_ch4_flux : str
    CH4フラックスのカラム名
col_c2h6_flux : str
    C2H6フラックスのカラム名
label_gas : str
    都市ガス起源のラベル
label_bio : str
    生物起源のラベル
col_datetime : str
    日時カラムの名前
output_filename : str
    出力ファイル名
show_label : bool
    ラベルを表示するか
show_legend : bool
    凡例を表示するか
subplot_fontsize : int
    サブプロットのフォントサイズ
subplot_label_weekday : str | None
    平日グラフのラベル
subplot_label_weekend : str | None
    休日グラフのラベル
y_max : float | None
    y軸の上限値
def plot_spectra( self, input_dir: str, output_dir: str, fs: float, lag_second: float, col_ch4: str = 'Ultra_CH4_ppm_C', col_c2h6: str = 'Ultra_C2H6_ppb', label_ch4: str | None = None, label_c2h6: str | None = None, are_inputs_resampled: bool = True, file_pattern: str = '*.csv', output_basename: str = 'spectrum', plot_power: bool = True, plot_co: bool = True, markersize: float = 14) -> None:
1999    def plot_spectra(
2000        self,
2001        input_dir: str,
2002        output_dir: str,
2003        fs: float,
2004        lag_second: float,
2005        col_ch4: str = "Ultra_CH4_ppm_C",
2006        col_c2h6: str = "Ultra_C2H6_ppb",
2007        label_ch4: str | None = None,
2008        label_c2h6: str | None = None,
2009        are_inputs_resampled: bool = True,
2010        file_pattern: str = "*.csv",
2011        output_basename: str = "spectrum",
2012        plot_power: bool = True,
2013        plot_co: bool = True,
2014        markersize: float = 14,
2015    ) -> None:
2016        """
2017        月間の平均パワースペクトル密度を計算してプロットする。
2018
2019        データファイルを指定されたディレクトリから読み込み、パワースペクトル密度を計算し、
2020        結果を指定された出力ディレクトリにプロットして保存します。
2021
2022        Parameters:
2023        ------
2024            input_dir : str
2025                データファイルが格納されているディレクトリ。
2026            output_dir : str
2027                出力先ディレクトリ。
2028            fs : float
2029                サンプリング周波数。
2030            lag_second : float
2031                ラグ時間(秒)。
2032            col_ch4 : str, optional
2033                CH4の濃度データが入ったカラムのキー。デフォルトは"Ultra_CH4_ppm_C"。
2034            col_c2h6 : str, optional
2035                C2H6の濃度データが入ったカラムのキー。デフォルトは"Ultra_C2H6_ppb"。
2036            are_inputs_resampled : bool, optional
2037                入力データが再サンプリングされているかどうか。デフォルトはTrue。
2038            file_pattern : str, optional
2039                処理対象のファイルパターン。デフォルトは"*.csv"。
2040            output_basename : str, optional
2041                出力ファイル名。デフォルトは"spectrum"。
2042        """
2043        # データの読み込みと結合
2044        edp = EddyDataPreprocessor()
2045
2046        # 各変数のパワースペクトルを格納する辞書
2047        power_spectra = {col_ch4: [], col_c2h6: []}
2048        co_spectra = {col_ch4: [], col_c2h6: []}
2049        freqs = None
2050
2051        # プログレスバーを表示しながらファイルを処理
2052        file_list = glob.glob(os.path.join(input_dir, file_pattern))
2053        for filepath in tqdm(file_list, desc="Processing files"):
2054            df, _ = edp.get_resampled_df(
2055                filepath=filepath, is_already_resampled=are_inputs_resampled
2056            )
2057
2058            # 風速成分の計算を追加
2059            df = edp.add_uvw_columns(df)
2060
2061            # NaNや無限大を含む行を削除
2062            df = df.replace([np.inf, -np.inf], np.nan).dropna(
2063                subset=[col_ch4, col_c2h6, "wind_w"]
2064            )
2065
2066            # データが十分な行数を持っているか確認
2067            if len(df) < 100:
2068                continue
2069
2070            # 各ファイルごとにスペクトル計算
2071            calculator = SpectrumCalculator(
2072                df=df,
2073                fs=fs,
2074                cols_apply_lag_time=[col_ch4, col_c2h6],
2075                lag_second=lag_second,
2076            )
2077
2078            # 各変数のパワースペクトルを計算して保存
2079            for col in power_spectra.keys():
2080                f, ps = calculator.calculate_power_spectrum(
2081                    col=col,
2082                    dimensionless=True,
2083                    frequency_weighted=True,
2084                    interpolate_points=True,
2085                    scaling="density",
2086                )
2087                # 最初のファイル処理時にfreqsを初期化
2088                if freqs is None:
2089                    freqs = f
2090                    power_spectra[col].append(ps)
2091                # 以降は周波数配列の長さが一致する場合のみ追加
2092                elif len(f) == len(freqs):
2093                    power_spectra[col].append(ps)
2094
2095                # コスペクトル
2096                _, cs, _ = calculator.calculate_co_spectrum(
2097                    col1="wind_w",
2098                    col2=col,
2099                    dimensionless=True,
2100                    frequency_weighted=True,
2101                    interpolate_points=True,
2102                    # scaling="density",
2103                    scaling="spectrum",
2104                )
2105                if freqs is not None and len(cs) == len(freqs):
2106                    co_spectra[col].append(cs)
2107
2108        # 各変数のスペクトルを平均化
2109        averaged_power_spectra = {
2110            col: np.mean(spectra, axis=0) for col, spectra in power_spectra.items()
2111        }
2112        averaged_co_spectra = {
2113            col: np.mean(spectra, axis=0) for col, spectra in co_spectra.items()
2114        }
2115
2116        # # プロット設定
2117        # plt.rcParams.update(
2118        #     {
2119        #         "font.size": 20,
2120        #         "axes.labelsize": 20,
2121        #         "axes.titlesize": 20,
2122        #         "xtick.labelsize": 20,
2123        #         "ytick.labelsize": 20,
2124        #         "legend.fontsize": 20,
2125        #     }
2126        # )
2127
2128        # プロット設定を修正
2129        plot_configs = [
2130            {
2131                "col": col_ch4,
2132                "psd_ylabel": r"$fS_{\mathrm{CH_4}} / s_{\mathrm{CH_4}}^2$",
2133                "co_ylabel": r"$fCo_{w\mathrm{CH_4}} / (\sigma_w \sigma_{\mathrm{CH_4}})$",
2134                "color": "red",
2135                "label": label_ch4,
2136            },
2137            {
2138                "col": col_c2h6,
2139                "psd_ylabel": r"$fS_{\mathrm{C_2H_6}} / s_{\mathrm{C_2H_6}}^2$",
2140                "co_ylabel": r"$fCo_{w\mathrm{C_2H_6}} / (\sigma_w \sigma_{\mathrm{C_2H_6}})$",
2141                "color": "orange",
2142                "label": label_c2h6,
2143            },
2144        ]
2145
2146        # # パワースペクトルの図を作成
2147        # if plot_power:
2148        #     _, axes_psd = plt.subplots(1, 2, figsize=(12, 5), sharex=True)
2149        #     for ax, config in zip(axes_psd, plot_configs):
2150        #         ax.scatter(
2151        #             freqs,
2152        #             averaged_power_spectra[config["col"]],
2153        #             c=config["color"],
2154        #             s=100,
2155        #         )
2156        #         ax.set_xscale("log")
2157        #         ax.set_yscale("log")
2158        #         ax.set_xlim(0.001, 10)
2159        #         ax.plot([0.01, 10], [1, 0.01], "-", color="black", alpha=0.5)
2160        #         ax.text(0.1, 0.06, "-2/3", fontsize=18)
2161        #         ax.set_ylabel(config["psd_ylabel"])
2162        #         if config["label"] is not None:
2163        #             ax.text(
2164        #                 0.02, 0.98, config["label"], transform=ax.transAxes, va="top"
2165        #             )
2166        #         ax.grid(True, alpha=0.3)
2167        #         ax.set_xlabel("f (Hz)")
2168
2169        # パワースペクトルの図を作成
2170        if plot_power:
2171            _, axes_psd = plt.subplots(1, 2, figsize=(12, 5), sharex=True)
2172            for ax, config in zip(axes_psd, plot_configs):
2173                ax.plot(
2174                    freqs,
2175                    averaged_power_spectra[config["col"]],
2176                    "o",  # マーカーを丸に設定
2177                    color=config["color"],
2178                    markersize=markersize,
2179                )
2180                ax.set_xscale("log")
2181                ax.set_yscale("log")
2182                ax.set_xlim(0.001, 10)
2183                ax.plot([0.01, 10], [1, 0.01], "-", color="black", alpha=0.5)
2184                ax.text(0.1, 0.06, "-2/3", fontsize=18)
2185                ax.set_ylabel(config["psd_ylabel"])
2186                if config["label"] is not None:
2187                    ax.text(
2188                        0.02, 0.98, config["label"], transform=ax.transAxes, va="top"
2189                    )
2190                ax.grid(True, alpha=0.3)
2191                ax.set_xlabel("f (Hz)")
2192
2193            plt.tight_layout()
2194            os.makedirs(output_dir, exist_ok=True)
2195            output_path_psd: str = os.path.join(
2196                output_dir, f"power_{output_basename}.png"
2197            )
2198            plt.savefig(
2199                output_path_psd,
2200                dpi=300,
2201                bbox_inches="tight",
2202            )
2203            plt.close()
2204
2205        # # コスペクトルの図を作成
2206        # if plot_co:
2207        #     _, axes_cosp = plt.subplots(1, 2, figsize=(12, 5), sharex=True)
2208        #     for ax, config in zip(axes_cosp, plot_configs):
2209        #         ax.scatter(
2210        #             freqs,
2211        #             averaged_co_spectra[config["col"]],
2212        #             c=config["color"],
2213        #             s=100,
2214        #         )
2215        #         ax.set_xscale("log")
2216        #         ax.set_yscale("log")
2217        #         ax.set_xlim(0.001, 10)
2218        #         ax.plot([0.01, 10], [1, 0.01], "-", color="black", alpha=0.5)
2219        #         ax.text(0.1, 0.1, "-4/3", fontsize=18)
2220        #         ax.set_ylabel(config["co_ylabel"])
2221        #         if config["label"] is not None:
2222        #             ax.text(
2223        #                 0.02, 0.98, config["label"], transform=ax.transAxes, va="top"
2224        #             )
2225        #         ax.grid(True, alpha=0.3)
2226        #         ax.set_xlabel("f (Hz)")
2227
2228        # コスペクトルの図を作成
2229        if plot_co:
2230            _, axes_cosp = plt.subplots(1, 2, figsize=(12, 5), sharex=True)
2231            for ax, config in zip(axes_cosp, plot_configs):
2232                ax.plot(
2233                    freqs,
2234                    averaged_co_spectra[config["col"]],
2235                    "o",  # マーカーを丸に設定
2236                    color=config["color"],
2237                    markersize=markersize,
2238                )
2239                ax.set_xscale("log")
2240                ax.set_yscale("log")
2241                ax.set_xlim(0.001, 10)
2242                ax.plot([0.01, 10], [1, 0.01], "-", color="black", alpha=0.5)
2243                ax.text(0.1, 0.1, "-4/3", fontsize=18)
2244                ax.set_ylabel(config["co_ylabel"])
2245                if config["label"] is not None:
2246                    ax.text(
2247                        0.02, 0.98, config["label"], transform=ax.transAxes, va="top"
2248                    )
2249                ax.grid(True, alpha=0.3)
2250                ax.set_xlabel("f (Hz)")
2251
2252            plt.tight_layout()
2253            output_path_csd: str = os.path.join(output_dir, f"co_{output_basename}.png")
2254            plt.savefig(
2255                output_path_csd,
2256                dpi=300,
2257                bbox_inches="tight",
2258            )
2259            plt.close()

月間の平均パワースペクトル密度を計算してプロットする。

データファイルを指定されたディレクトリから読み込み、パワースペクトル密度を計算し、 結果を指定された出力ディレクトリにプロットして保存します。

Parameters:

input_dir : str
    データファイルが格納されているディレクトリ。
output_dir : str
    出力先ディレクトリ。
fs : float
    サンプリング周波数。
lag_second : float
    ラグ時間(秒)。
col_ch4 : str, optional
    CH4の濃度データが入ったカラムのキー。デフォルトは"Ultra_CH4_ppm_C"。
col_c2h6 : str, optional
    C2H6の濃度データが入ったカラムのキー。デフォルトは"Ultra_C2H6_ppb"。
are_inputs_resampled : bool, optional
    入力データが再サンプリングされているかどうか。デフォルトはTrue。
file_pattern : str, optional
    処理対象のファイルパターン。デフォルトは"*.csv"。
output_basename : str, optional
    出力ファイル名。デフォルトは"spectrum"。
def plot_turbulence( self, df: pandas.core.frame.DataFrame, output_dir: str, output_filename: str = 'turbulence.png', col_uz: str = 'Uz', col_ch4: str = 'Ultra_CH4_ppm_C', col_c2h6: str = 'Ultra_C2H6_ppb', col_timestamp: str = 'TIMESTAMP', add_serial_labels: bool = True) -> None:
2261    def plot_turbulence(
2262        self,
2263        df: pd.DataFrame,
2264        output_dir: str,
2265        output_filename: str = "turbulence.png",
2266        col_uz: str = "Uz",
2267        col_ch4: str = "Ultra_CH4_ppm_C",
2268        col_c2h6: str = "Ultra_C2H6_ppb",
2269        col_timestamp: str = "TIMESTAMP",
2270        add_serial_labels: bool = True,
2271    ) -> None:
2272        """時系列データのプロットを作成する
2273
2274        Parameters:
2275        ------
2276            df : pd.DataFrame
2277                プロットするデータを含むDataFrame
2278            output_dir : str
2279                出力ディレクトリのパス
2280            output_filename : str
2281                出力ファイル名
2282            col_uz : str
2283                鉛直風速データのカラム名
2284            col_ch4 : str
2285                メタンデータのカラム名
2286            col_c2h6 : str
2287                エタンデータのカラム名
2288            col_timestamp : str
2289                タイムスタンプのカラム名
2290        """
2291        # 出力ディレクトリの作成
2292        os.makedirs(output_dir, exist_ok=True)
2293        output_path: str = os.path.join(output_dir, output_filename)
2294
2295        # データの前処理
2296        df = df.copy()
2297
2298        # タイムスタンプをインデックスに設定(まだ設定されていない場合)
2299        if not isinstance(df.index, pd.DatetimeIndex):
2300            df[col_timestamp] = pd.to_datetime(df[col_timestamp])
2301            df.set_index(col_timestamp, inplace=True)
2302
2303        # 開始時刻と終了時刻を取得
2304        start_time = df.index[0]
2305        end_time = df.index[-1]
2306
2307        # 開始時刻の分を取得
2308        start_minute = start_time.minute
2309
2310        # 時間軸の作成(実際の開始時刻からの経過分数)
2311        minutes_elapsed = (df.index - start_time).total_seconds() / 60
2312
2313        # プロットの作成
2314        _, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=(12, 10), sharex=True)
2315
2316        # 鉛直風速
2317        ax1.plot(minutes_elapsed, df[col_uz], "k-", linewidth=0.5)
2318        ax1.set_ylabel(r"$w$ (m s$^{-1}$)")
2319        if add_serial_labels:
2320            ax1.text(0.02, 0.98, "(a)", transform=ax1.transAxes, va="top")
2321        ax1.grid(True, alpha=0.3)
2322
2323        # CH4濃度
2324        ax2.plot(minutes_elapsed, df[col_ch4], "r-", linewidth=0.5)
2325        ax2.set_ylabel(r"$\mathrm{CH_4}$ (ppm)")
2326        if add_serial_labels:
2327            ax2.text(0.02, 0.98, "(b)", transform=ax2.transAxes, va="top")
2328        ax2.grid(True, alpha=0.3)
2329
2330        # C2H6濃度
2331        ax3.plot(minutes_elapsed, df[col_c2h6], "orange", linewidth=0.5)
2332        ax3.set_ylabel(r"$\mathrm{C_2H_6}$ (ppb)")
2333        if add_serial_labels:
2334            ax3.text(0.02, 0.98, "(c)", transform=ax3.transAxes, va="top")
2335        ax3.grid(True, alpha=0.3)
2336        ax3.set_xlabel("Time (minutes)")
2337
2338        # x軸の範囲を実際の開始時刻から30分後までに設定
2339        total_minutes = (end_time - start_time).total_seconds() / 60
2340        ax3.set_xlim(0, min(30, total_minutes))
2341
2342        # x軸の目盛りを5分間隔で設定
2343        np.arange(start_minute, start_minute + 35, 5)
2344        ax3.xaxis.set_major_locator(MultipleLocator(5))
2345
2346        # レイアウトの調整
2347        plt.tight_layout()
2348
2349        # 図の保存
2350        plt.savefig(output_path, dpi=300, bbox_inches="tight")
2351        plt.close()

時系列データのプロットを作成する

Parameters:

df : pd.DataFrame
    プロットするデータを含むDataFrame
output_dir : str
    出力ディレクトリのパス
output_filename : str
    出力ファイル名
col_uz : str
    鉛直風速データのカラム名
col_ch4 : str
    メタンデータのカラム名
col_c2h6 : str
    エタンデータのカラム名
col_timestamp : str
    タイムスタンプのカラム名
def plot_wind_rose_sources( self, df: pandas.core.frame.DataFrame, output_dir: str | pathlib.Path | None = None, output_filename: str = 'wind_rose.png', col_datetime: str = 'Date', col_ch4_flux: str = 'Fch4', col_c2h6_flux: str = 'Fc2h6', col_wind_dir: str = 'Wind direction', flux_unit: str = '(nmol m$^{-2}$ s$^{-1}$)', ymax: float | None = None, label_gas: str = '都市ガス起源', label_bio: str = '生物起源', figsize: tuple[float, float] = (8, 8), flux_alpha: float = 0.4, num_directions: int = 8, center_on_angles: bool = True, subplot_label: str | None = None, add_legend: bool = True, print_summary: bool = True, save_fig: bool = True, show_fig: bool = True) -> None:
2353    def plot_wind_rose_sources(
2354        self,
2355        df: pd.DataFrame,
2356        output_dir: str | Path | None = None,
2357        output_filename: str = "wind_rose.png",
2358        col_datetime: str = "Date",
2359        col_ch4_flux: str = "Fch4",
2360        col_c2h6_flux: str = "Fc2h6",
2361        col_wind_dir: str = "Wind direction",
2362        flux_unit: str = r"(nmol m$^{-2}$ s$^{-1}$)",
2363        ymax: float | None = None,  # フラックスの上限値
2364        label_gas: str = "都市ガス起源",
2365        label_bio: str = "生物起源",
2366        figsize: tuple[float, float] = (8, 8),
2367        flux_alpha: float = 0.4,
2368        num_directions: int = 8,  # 方位の数(8方位)
2369        center_on_angles: bool = True,  # 追加:45度刻みの線を境界にするかどうか
2370        subplot_label: str | None = None,
2371        add_legend: bool = True,
2372        print_summary: bool = True,  # 統計情報を表示するかどうか
2373        save_fig: bool = True,
2374        show_fig: bool = True,
2375    ) -> None:
2376        """CH4フラックスの都市ガス起源と生物起源の風配図を作成する関数
2377
2378        Parameters:
2379        ------
2380            df : pd.DataFrame
2381                風配図を作成するためのデータフレーム
2382            output_dir : str | Path | None
2383                生成された図を保存するディレクトリのパス
2384            output_filename : str
2385                保存するファイル名(デフォルトは"wind_rose.png")
2386            col_ch4_flux : str
2387                CH4フラックスを示すカラム名
2388            col_c2h6_flux : str
2389                C2H6フラックスを示すカラム名
2390            col_wind_dir : str
2391                風向を示すカラム名
2392            label_gas : str
2393                都市ガス起源のフラックスに対するラベル
2394            label_bio : str
2395                生物起源のフラックスに対するラベル
2396            col_datetime : str
2397                日時を示すカラム名
2398            num_directions : int
2399                風向の数(デフォルトは8)
2400            center_on_angles: bool
2401                Trueの場合、45度刻みの線を境界として扇形を描画します。
2402                Falseの場合、45度の中間(22.5度)を中心として扇形を描画します。
2403            subplot_label : str
2404                サブプロットに表示するラベル
2405            print_summary : bool
2406                統計情報を表示するかどうかのフラグ
2407            flux_unit : str
2408                フラックスの単位
2409            ymax : float | None
2410                y軸の上限値(指定しない場合はデータの最大値に基づいて自動設定)
2411            figsize : tuple[float, float]
2412                図のサイズ
2413            flux_alpha : float
2414                フラックスの透明度
2415            save_fig : bool
2416                図を保存するかどうかのフラグ
2417            show_fig : bool
2418                図を表示するかどうかのフラグ
2419        """
2420        # 起源の計算
2421        df_with_sources = self._calculate_source_contributions(
2422            df=df,
2423            col_ch4_flux=col_ch4_flux,
2424            col_c2h6_flux=col_c2h6_flux,
2425            col_datetime=col_datetime,
2426        )
2427
2428        # 方位の定義
2429        direction_ranges = self._define_direction_ranges(
2430            num_directions, center_on_angles
2431        )
2432
2433        # 方位ごとのデータを集計
2434        direction_data = self._aggregate_direction_data(
2435            df_with_sources, col_wind_dir, direction_ranges
2436        )
2437
2438        # プロットの作成
2439        fig = plt.figure(figsize=figsize)
2440        ax = fig.add_subplot(111, projection="polar")
2441
2442        # 方位の角度(ラジアン)を計算
2443        theta = np.array(
2444            [np.radians(angle) for angle in direction_data["center_angle"]]
2445        )
2446
2447        # 生物起源と都市ガス起源を独立してプロット
2448        ax.bar(
2449            theta,
2450            direction_data["bio_flux"],
2451            width=np.radians(360 / num_directions),
2452            bottom=0.0,
2453            color="blue",
2454            alpha=flux_alpha,
2455            label=label_bio,
2456        )
2457
2458        ax.bar(
2459            theta,
2460            direction_data["gas_flux"],
2461            width=np.radians(360 / num_directions),
2462            bottom=0.0,
2463            color="red",
2464            alpha=flux_alpha,
2465            label=label_gas,
2466        )
2467
2468        # y軸の範囲を設定
2469        if ymax is not None:
2470            ax.set_ylim(0, ymax)
2471        else:
2472            # データの最大値に基づいて自動設定
2473            max_value = max(
2474                direction_data["bio_flux"].max(), direction_data["gas_flux"].max()
2475            )
2476            ax.set_ylim(0, max_value * 1.1)  # 最大値の1.1倍を上限に設定
2477
2478        # 方位ラベルの設定
2479        ax.set_theta_zero_location("N")  # 北を上に設定
2480        ax.set_theta_direction(-1)  # 時計回りに設定
2481
2482        # 方位ラベルの表示
2483        labels = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"]
2484        angles = np.radians(np.linspace(0, 360, len(labels), endpoint=False))
2485        ax.set_xticks(angles)
2486        ax.set_xticklabels(labels)
2487
2488        # プロット領域の調整(上部と下部にスペースを確保)
2489        plt.subplots_adjust(
2490            top=0.8,  # 上部に20%のスペースを確保
2491            bottom=0.2,  # 下部に20%のスペースを確保(凡例用)
2492        )
2493
2494        # サブプロットラベルの追加(デフォルトは左上)
2495        if subplot_label:
2496            ax.text(
2497                0.01,
2498                0.99,
2499                subplot_label,
2500                transform=ax.transAxes,
2501            )
2502
2503        # 単位の追加(図の下部中央に配置)
2504        plt.figtext(
2505            0.5,  # x位置(中央)
2506            0.1,  # y位置(下部)
2507            flux_unit,
2508            ha="center",  # 水平方向の位置揃え
2509            va="bottom",  # 垂直方向の位置揃え
2510        )
2511
2512        # 凡例の追加(単位の下に配置)
2513        if add_legend:
2514            # 最初のプロットから凡例のハンドルとラベルを取得
2515            handles, labels = ax.get_legend_handles_labels()
2516            # 図の下部に凡例を配置
2517            fig.legend(
2518                handles,
2519                labels,
2520                loc="center",
2521                bbox_to_anchor=(0.5, 0.05),  # x=0.5で中央、y=0.05で下部に配置
2522                ncol=len(handles),  # ハンドルの数だけ列を作成(一行に表示)
2523            )
2524
2525        # グラフの保存
2526        if save_fig:
2527            if output_dir is None:
2528                raise ValueError(
2529                    "save_fig=Trueのとき、output_dirに有効なパスを指定する必要があります。"
2530                )
2531            # 出力ディレクトリの作成
2532            os.makedirs(output_dir, exist_ok=True)
2533            output_path: str = os.path.join(output_dir, output_filename)
2534            plt.savefig(output_path, dpi=300, bbox_inches="tight")
2535
2536        # グラフの表示
2537        if show_fig:
2538            plt.show()
2539        else:
2540            plt.close(fig=fig)
2541
2542        # 統計情報の表示
2543        if print_summary:
2544            for source in ["gas", "bio"]:
2545                flux_data = direction_data[f"{source}_flux"]
2546                mean_val = flux_data.mean()
2547                max_val = flux_data.max()
2548                max_dir = direction_data.loc[flux_data.idxmax(), "name"]
2549
2550                self.logger.info(
2551                    f"{label_gas if source == 'gas' else label_bio}の統計:"
2552                )
2553                print(f"  平均フラックス: {mean_val:.2f}")
2554                print(f"  最大フラックス: {max_val:.2f}")
2555                print(f"  最大フラックスの方位: {max_dir}")

CH4フラックスの都市ガス起源と生物起源の風配図を作成する関数

Parameters:

df : pd.DataFrame
    風配図を作成するためのデータフレーム
output_dir : str | Path | None
    生成された図を保存するディレクトリのパス
output_filename : str
    保存するファイル名(デフォルトは"wind_rose.png")
col_ch4_flux : str
    CH4フラックスを示すカラム名
col_c2h6_flux : str
    C2H6フラックスを示すカラム名
col_wind_dir : str
    風向を示すカラム名
label_gas : str
    都市ガス起源のフラックスに対するラベル
label_bio : str
    生物起源のフラックスに対するラベル
col_datetime : str
    日時を示すカラム名
num_directions : int
    風向の数(デフォルトは8)
center_on_angles: bool
    Trueの場合、45度刻みの線を境界として扇形を描画します。
    Falseの場合、45度の中間(22.5度)を中心として扇形を描画します。
subplot_label : str
    サブプロットに表示するラベル
print_summary : bool
    統計情報を表示するかどうかのフラグ
flux_unit : str
    フラックスの単位
ymax : float | None
    y軸の上限値(指定しない場合はデータの最大値に基づいて自動設定)
figsize : tuple[float, float]
    図のサイズ
flux_alpha : float
    フラックスの透明度
save_fig : bool
    図を保存するかどうかのフラグ
show_fig : bool
    図を表示するかどうかのフラグ
@staticmethod
def get_valid_data( df: pandas.core.frame.DataFrame, x_col: str, y_col: str) -> pandas.core.frame.DataFrame:
2840    @staticmethod
2841    def get_valid_data(df: pd.DataFrame, x_col: str, y_col: str) -> pd.DataFrame:
2842        """
2843        指定された列の有効なデータ(NaNを除いた)を取得します。
2844
2845        Parameters:
2846        ------
2847            df : pd.DataFrame
2848                データフレーム
2849            x_col : str
2850                X軸の列名
2851            y_col : str
2852                Y軸の列名
2853
2854        Returns:
2855        ------
2856            pd.DataFrame
2857                有効なデータのみを含むDataFrame
2858        """
2859        return df.copy().dropna(subset=[x_col, y_col])

指定された列の有効なデータ(NaNを除いた)を取得します。

Parameters:

df : pd.DataFrame
    データフレーム
x_col : str
    X軸の列名
y_col : str
    Y軸の列名

Returns:

pd.DataFrame
    有効なデータのみを含むDataFrame
@staticmethod
def setup_logger(logger: logging.Logger | None, log_level: int = 20) -> logging.Logger:
2861    @staticmethod
2862    def setup_logger(logger: Logger | None, log_level: int = INFO) -> Logger:
2863        """
2864        ロガーを設定します。
2865
2866        このメソッドは、ロギングの設定を行い、ログメッセージのフォーマットを指定します。
2867        ログメッセージには、日付、ログレベル、メッセージが含まれます。
2868
2869        渡されたロガーがNoneまたは不正な場合は、新たにロガーを作成し、標準出力に
2870        ログメッセージが表示されるようにStreamHandlerを追加します。ロガーのレベルは
2871        引数で指定されたlog_levelに基づいて設定されます。
2872
2873        Parameters:
2874        ------
2875            logger : Logger | None
2876                使用するロガー。Noneの場合は新しいロガーを作成します。
2877            log_level : int
2878                ロガーのログレベル。デフォルトはINFO。
2879
2880        Returns:
2881        ------
2882            Logger
2883                設定されたロガーオブジェクト。
2884        """
2885        if logger is not None and isinstance(logger, Logger):
2886            return logger
2887        # 渡されたロガーがNoneまたは正しいものでない場合は独自に設定
2888        new_logger: Logger = getLogger()
2889        # 既存のハンドラーをすべて削除
2890        for handler in new_logger.handlers[:]:
2891            new_logger.removeHandler(handler)
2892        new_logger.setLevel(log_level)  # ロガーのレベルを設定
2893        ch = StreamHandler()
2894        ch_formatter = Formatter("%(asctime)s - %(levelname)s - %(message)s")
2895        ch.setFormatter(ch_formatter)  # フォーマッターをハンドラーに設定
2896        new_logger.addHandler(ch)  # StreamHandlerの追加
2897        return new_logger

ロガーを設定します。

このメソッドは、ロギングの設定を行い、ログメッセージのフォーマットを指定します。 ログメッセージには、日付、ログレベル、メッセージが含まれます。

渡されたロガーがNoneまたは不正な場合は、新たにロガーを作成し、標準出力に ログメッセージが表示されるようにStreamHandlerを追加します。ロガーのレベルは 引数で指定されたlog_levelに基づいて設定されます。

Parameters:

logger : Logger | None
    使用するロガー。Noneの場合は新しいロガーを作成します。
log_level : int
    ロガーのログレベル。デフォルトはINFO。

Returns:

Logger
    設定されたロガーオブジェクト。
@staticmethod
def setup_plot_params( font_family: list[str] = ['Arial', 'Dejavu Sans'], font_size: float = 20, legend_size: float = 20, tick_size: float = 20, title_size: float = 20, plot_params=None) -> None:
2899    @staticmethod
2900    def setup_plot_params(
2901        font_family: list[str] = ["Arial", "Dejavu Sans"],
2902        font_size: float = 20,
2903        legend_size: float = 20,
2904        tick_size: float = 20,
2905        title_size: float = 20,
2906        plot_params=None,
2907    ) -> None:
2908        """
2909        matplotlibのプロットパラメータを設定します。
2910
2911        Parameters:
2912        ------
2913            font_family : list[str]
2914                使用するフォントファミリーのリスト。
2915            font_size : float
2916                軸ラベルのフォントサイズ。
2917            legend_size : float
2918                凡例のフォントサイズ。
2919            tick_size : float
2920                軸目盛りのフォントサイズ。
2921            title_size : float
2922                タイトルのフォントサイズ。
2923            plot_params : Optional[Dict[str, any]]
2924                matplotlibのプロットパラメータの辞書。
2925        """
2926        # デフォルトのプロットパラメータ
2927        default_params = {
2928            "axes.linewidth": 1.0,
2929            "axes.titlesize": title_size,  # タイトル
2930            "grid.color": "gray",
2931            "grid.linewidth": 1.0,
2932            "font.family": font_family,
2933            "font.size": font_size,  # 軸ラベル
2934            "legend.fontsize": legend_size,  # 凡例
2935            "text.color": "black",
2936            "xtick.color": "black",
2937            "ytick.color": "black",
2938            "xtick.labelsize": tick_size,  # 軸目盛
2939            "ytick.labelsize": tick_size,  # 軸目盛
2940            "xtick.major.size": 0,
2941            "ytick.major.size": 0,
2942            "ytick.direction": "out",
2943            "ytick.major.width": 1.0,
2944        }
2945
2946        # plot_paramsが定義されている場合、デフォルトに追記
2947        if plot_params:
2948            default_params.update(plot_params)
2949
2950        plt.rcParams.update(default_params)  # プロットパラメータを更新

matplotlibのプロットパラメータを設定します。

Parameters:

font_family : list[str]
    使用するフォントファミリーのリスト。
font_size : float
    軸ラベルのフォントサイズ。
legend_size : float
    凡例のフォントサイズ。
tick_size : float
    軸目盛りのフォントサイズ。
title_size : float
    タイトルのフォントサイズ。
plot_params : Optional[Dict[str, any]]
    matplotlibのプロットパラメータの辞書。
@staticmethod
def plot_flux_distributions( g2401_flux: pandas.core.series.Series, ultra_flux: pandas.core.series.Series, month: int, output_dir: str, xlim: tuple[float, float] = (-50, 200), bandwidth: float = 1.0) -> None:
2952    @staticmethod
2953    def plot_flux_distributions(
2954        g2401_flux: pd.Series,
2955        ultra_flux: pd.Series,
2956        month: int,
2957        output_dir: str,
2958        xlim: tuple[float, float] = (-50, 200),
2959        bandwidth: float = 1.0,  # デフォルト値を1.0に設定
2960    ) -> None:
2961        """
2962        両測器のCH4フラックス分布を可視化
2963
2964        Parameters:
2965        ------
2966            g2401_flux : pd.Series
2967                G2401で測定されたフラックス値の配列
2968            ultra_flux : pd.Series
2969                Ultraで測定されたフラックス値の配列
2970            month : int
2971                測定月
2972            output_dir : str
2973                出力ディレクトリ
2974            xlim : tuple[float, float]
2975                x軸の範囲(タプル)
2976            bandwidth : float
2977                カーネル密度推定のバンド幅調整係数(デフォルト: 1.0)
2978        """
2979        # nanを除去
2980        g2401_flux = g2401_flux.dropna()
2981        ultra_flux = ultra_flux.dropna()
2982
2983        plt.figure(figsize=(10, 6))
2984
2985        # KDEプロット(確率密度推定)
2986        sns.kdeplot(
2987            data=g2401_flux, label="G2401", color="blue", alpha=0.5, bw_adjust=bandwidth
2988        )
2989        sns.kdeplot(
2990            data=ultra_flux, label="Ultra", color="red", alpha=0.5, bw_adjust=bandwidth
2991        )
2992
2993        # 平均値と中央値のマーカー
2994        plt.axvline(
2995            g2401_flux.mean(),
2996            color="blue",
2997            linestyle="--",
2998            alpha=0.5,
2999            label="G2401 mean",
3000        )
3001        plt.axvline(
3002            ultra_flux.mean(),
3003            color="red",
3004            linestyle="--",
3005            alpha=0.5,
3006            label="Ultra mean",
3007        )
3008        plt.axvline(
3009            np.median(g2401_flux),
3010            color="blue",
3011            linestyle=":",
3012            alpha=0.5,
3013            label="G2401 median",
3014        )
3015        plt.axvline(
3016            np.median(ultra_flux),
3017            color="red",
3018            linestyle=":",
3019            alpha=0.5,
3020            label="Ultra median",
3021        )
3022
3023        # 軸ラベルとタイトル
3024        plt.xlabel(r"CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)")
3025        plt.ylabel("Probability Density")
3026        plt.title(f"Distribution of CH$_4$ fluxes - Month {month}")
3027
3028        # x軸の範囲設定
3029        plt.xlim(xlim)
3030
3031        # グリッド表示
3032        plt.grid(True, alpha=0.3)
3033
3034        # 統計情報
3035        stats_text = (
3036            f"G2401:\n"
3037            f"  Mean: {g2401_flux.mean():.2f}\n"
3038            f"  Median: {np.median(g2401_flux):.2f}\n"
3039            f"  Std: {g2401_flux.std():.2f}\n"
3040            f"Ultra:\n"
3041            f"  Mean: {ultra_flux.mean():.2f}\n"
3042            f"  Median: {np.median(ultra_flux):.2f}\n"
3043            f"  Std: {ultra_flux.std():.2f}"
3044        )
3045        plt.text(
3046            0.02,
3047            0.98,
3048            stats_text,
3049            transform=plt.gca().transAxes,
3050            verticalalignment="top",
3051            fontsize=10,
3052            bbox=dict(boxstyle="round", facecolor="white", alpha=0.8),
3053        )
3054
3055        # 凡例の表示
3056        plt.legend(loc="upper right")
3057
3058        # グラフの保存
3059        os.makedirs(output_dir, exist_ok=True)
3060        plt.tight_layout()
3061        plt.savefig(
3062            os.path.join(output_dir, f"flux_distribution_month_{month}.png"),
3063            dpi=300,
3064            bbox_inches="tight",
3065        )
3066        plt.close()

両測器のCH4フラックス分布を可視化

Parameters:

g2401_flux : pd.Series
    G2401で測定されたフラックス値の配列
ultra_flux : pd.Series
    Ultraで測定されたフラックス値の配列
month : int
    測定月
output_dir : str
    出力ディレクトリ
xlim : tuple[float, float]
    x軸の範囲(タプル)
bandwidth : float
    カーネル密度推定のバンド幅調整係数(デフォルト: 1.0)
class FftFileReorganizer:
 11class FftFileReorganizer:
 12    """
 13    FFTファイルを再編成するためのクラス。
 14
 15    入力ディレクトリからファイルを読み取り、フラグファイルに基づいて
 16    出力ディレクトリに再編成します。時間の完全一致を要求し、
 17    一致しないファイルはスキップして警告を出します。
 18    オプションで相対湿度(RH)に基づいたサブディレクトリへの分類も可能です。
 19    """
 20
 21    # クラス定数の定義
 22    DEFAULT_FILENAME_PATTERNS: list[str] = [
 23        r"FFT_TOA5_\d+\.SAC_Eddy_\d+_(\d{4})_(\d{2})_(\d{2})_(\d{4})(?:\+)?\.csv",
 24        r"FFT_TOA5_\d+\.SAC_Ultra\.Eddy_\d+_(\d{4})_(\d{2})_(\d{2})_(\d{4})(?:\+)?(?:-resampled)?\.csv",
 25    ]  # デフォルトのファイル名のパターン(正規表現)
 26    DEFAULT_OUTPUT_DIRS = {
 27        "GOOD_DATA": "good_data_all",
 28        "BAD_DATA": "bad_data",
 29    }  # 出力ディレクトリの構造に関する定数
 30
 31    def __init__(
 32        self,
 33        input_dir: str,
 34        output_dir: str,
 35        flag_csv_path: str,
 36        filename_patterns: list[str] | None = None,
 37        output_dirs: dict[str, str] | None = None,
 38        sort_by_rh: bool = True,
 39        logger: Logger | None = None,
 40        logging_debug: bool = False,
 41    ):
 42        """
 43        FftFileReorganizerクラスを初期化します。
 44
 45        Parameters:
 46        ------
 47            input_dir : str
 48                入力ファイルが格納されているディレクトリのパス
 49            output_dir : str
 50                出力ファイルを格納するディレクトリのパス
 51            flag_csv_path : str
 52                フラグ情報が記載されているCSVファイルのパス
 53            filename_patterns : list[str] | None
 54                ファイル名のパターン(正規表現)のリスト
 55            output_dirs : dict[str, str] | None
 56                出力ディレクトリの構造を定義する辞書
 57            sort_by_rh : bool
 58                RHに基づいてサブディレクトリにファイルを分類するかどうか
 59            logger : Logger | None
 60                使用するロガー
 61            logging_debug : bool
 62                ログレベルをDEBUGに設定するかどうか
 63        """
 64        self._fft_path: str = input_dir
 65        self._sorted_path: str = output_dir
 66        self._output_dirs = output_dirs or self.DEFAULT_OUTPUT_DIRS
 67        self._good_data_path: str = os.path.join(
 68            output_dir, self._output_dirs["GOOD_DATA"]
 69        )
 70        self._bad_data_path: str = os.path.join(
 71            output_dir, self._output_dirs["BAD_DATA"]
 72        )
 73        self._filename_patterns: list[str] = (
 74            self.DEFAULT_FILENAME_PATTERNS.copy()
 75            if filename_patterns is None
 76            else filename_patterns
 77        )
 78        self._flag_file_path: str = flag_csv_path
 79        self._sort_by_rh: bool = sort_by_rh
 80        self._flags = {}
 81        self._warnings = []
 82        # ロガー
 83        log_level: int = INFO
 84        if logging_debug:
 85            log_level = DEBUG
 86        self.logger: Logger = FftFileReorganizer.setup_logger(logger, log_level)
 87
 88    def reorganize(self):
 89        """
 90        ファイルの再編成プロセス全体を実行します。
 91        ディレクトリの準備、フラグファイルの読み込み、
 92        有効なファイルの取得、ファイルのコピーを順に行います。
 93        処理後、警告メッセージがあれば出力します。
 94        """
 95        self._prepare_directories()
 96        self._read_flag_file()
 97        valid_files = self._get_valid_files()
 98        self._copy_files(valid_files)
 99        self.logger.info("ファイルのコピーが完了しました。")
100
101        if self._warnings:
102            self.logger.warning("Warnings:")
103            for warning in self._warnings:
104                self.logger.warning(warning)
105
106    def _copy_files(self, valid_files):
107        """
108        有効なファイルを適切な出力ディレクトリにコピーします。
109        フラグファイルの時間と完全に一致するファイルのみを処理します。
110
111        Parameters:
112        ------
113            valid_files : list
114                コピーする有効なファイル名のリスト
115        """
116        with tqdm(total=len(valid_files)) as pbar:
117            for filename in valid_files:
118                src_file = os.path.join(self._fft_path, filename)
119                file_time = self._parse_datetime(filename)
120
121                if file_time in self._flags:
122                    flag = self._flags[file_time]["Flg"]
123                    rh = self._flags[file_time]["RH"]
124                    if flag == 0:
125                        # Copy to self._good_data_path
126                        dst_file_good = os.path.join(self._good_data_path, filename)
127                        shutil.copy2(src_file, dst_file_good)
128
129                        if self._sort_by_rh:
130                            # Copy to RH directory
131                            rh_dir = FftFileReorganizer.get_rh_directory(rh)
132                            dst_file_rh = os.path.join(
133                                self._sorted_path, rh_dir, filename
134                            )
135                            shutil.copy2(src_file, dst_file_rh)
136                    else:
137                        dst_file = os.path.join(self._bad_data_path, filename)
138                        shutil.copy2(src_file, dst_file)
139                else:
140                    self._warnings.append(
141                        f"{filename} に対応するフラグが見つかりません。スキップします。"
142                    )
143
144                pbar.update(1)
145
146    def _get_valid_files(self):
147        """
148        入力ディレクトリから有効なファイルのリストを取得します。
149
150        Parameters:
151        ------
152        なし
153
154        Returns:
155        ------
156            valid_files : list
157                日時でソートされた有効なファイル名のリスト
158        """
159        fft_files = os.listdir(self._fft_path)
160        valid_files = []
161        for file in fft_files:
162            try:
163                self._parse_datetime(file)
164                valid_files.append(file)
165            except ValueError as e:
166                self._warnings.append(f"{file} をスキップします: {str(e)}")
167        return sorted(valid_files, key=self._parse_datetime)
168
169    def _parse_datetime(self, filename: str) -> datetime:
170        """
171        ファイル名から日時情報を抽出します。
172
173        Parameters:
174        ------
175            filename : str
176                解析対象のファイル名
177
178        Returns:
179        ------
180            datetime : datetime
181                抽出された日時情報
182
183        Raises:
184        ------
185            ValueError
186                ファイル名から日時情報を抽出できない場合
187        """
188        for pattern in self._filename_patterns:
189            match = re.match(pattern, filename)
190            if match:
191                year, month, day, time = match.groups()
192                datetime_str: str = f"{year}{month}{day}{time}"
193                return datetime.strptime(datetime_str, "%Y%m%d%H%M")
194
195        raise ValueError(f"Could not parse datetime from filename: {filename}")
196
197    def _prepare_directories(self):
198        """
199        出力ディレクトリを準備します。
200        既存のディレクトリがある場合は削除し、新しく作成します。
201        """
202        for path in [self._sorted_path, self._good_data_path, self._bad_data_path]:
203            if os.path.exists(path):
204                shutil.rmtree(path)
205            os.makedirs(path, exist_ok=True)
206
207        if self._sort_by_rh:
208            for i in range(10, 101, 10):
209                rh_path = os.path.join(self._sorted_path, f"RH{i}")
210                os.makedirs(rh_path, exist_ok=True)
211
212    def _read_flag_file(self):
213        """
214        フラグファイルを読み込み、self._flagsディクショナリに格納します。
215        """
216        with open(self._flag_file_path, "r") as f:
217            reader = csv.DictReader(f)
218            for row in reader:
219                time = datetime.strptime(row["time"], "%Y/%m/%d %H:%M")
220                try:
221                    rh = float(row["RH"])
222                except ValueError:  # RHが#N/Aなどの数値に変換できない値の場合
223                    self.logger.debug(f"Invalid RH value at {time}: {row['RH']}")
224                    rh = -1  # 不正な値として扱うため、負の値を設定
225
226                self._flags[time] = {"Flg": int(row["Flg"]), "RH": rh}
227
228    @staticmethod
229    def get_rh_directory(rh: float):
230        """
231        すべての値を10刻みで切り上げる(例: 80.1 → RH90, 86.0 → RH90, 91.2 → RH100)
232        """
233        if rh < 0 or rh > 100:  # 相対湿度として不正な値を除外
234            return "bad_data"
235        elif rh == 0:  # 0の場合はRH0に入れる
236            return "RH0"
237        else:  # 10刻みで切り上げ
238            return f"RH{min(int((rh + 9.99) // 10 * 10), 100)}"
239
240    @staticmethod
241    def setup_logger(logger: Logger | None, log_level: int = INFO) -> Logger:
242        """
243        ロガーを設定します。
244
245        ロギングの設定を行い、ログメッセージのフォーマットを指定します。
246        ログメッセージには、日付、ログレベル、メッセージが含まれます。
247
248        渡されたロガーがNoneまたは不正な場合は、新たにロガーを作成し、標準出力に
249        ログメッセージが表示されるようにStreamHandlerを追加します。ロガーのレベルは
250        引数で指定されたlog_levelに基づいて設定されます。
251
252        Parameters:
253        ------
254            logger : Logger | None
255                使用するロガー。Noneの場合は新しいロガーを作成します。
256            log_level : int
257                ロガーのログレベル。デフォルトはINFO。
258
259        Returns:
260        ------
261            Logger
262                設定されたロガーオブジェクト。
263        """
264        if logger is not None and isinstance(logger, Logger):
265            return logger
266        # 渡されたロガーがNoneまたは正しいものでない場合は独自に設定
267        new_logger: Logger = getLogger()
268        # 既存のハンドラーをすべて削除
269        for handler in new_logger.handlers[:]:
270            new_logger.removeHandler(handler)
271        new_logger.setLevel(log_level)  # ロガーのレベルを設定
272        ch = StreamHandler()
273        ch_formatter = Formatter("%(asctime)s - %(levelname)s - %(message)s")
274        ch.setFormatter(ch_formatter)  # フォーマッターをハンドラーに設定
275        new_logger.addHandler(ch)  # StreamHandlerの追加
276        return new_logger

FFTファイルを再編成するためのクラス。

入力ディレクトリからファイルを読み取り、フラグファイルに基づいて 出力ディレクトリに再編成します。時間の完全一致を要求し、 一致しないファイルはスキップして警告を出します。 オプションで相対湿度(RH)に基づいたサブディレクトリへの分類も可能です。

FftFileReorganizer( input_dir: str, output_dir: str, flag_csv_path: str, filename_patterns: list[str] | None = None, output_dirs: dict[str, str] | None = None, sort_by_rh: bool = True, logger: logging.Logger | None = None, logging_debug: bool = False)
31    def __init__(
32        self,
33        input_dir: str,
34        output_dir: str,
35        flag_csv_path: str,
36        filename_patterns: list[str] | None = None,
37        output_dirs: dict[str, str] | None = None,
38        sort_by_rh: bool = True,
39        logger: Logger | None = None,
40        logging_debug: bool = False,
41    ):
42        """
43        FftFileReorganizerクラスを初期化します。
44
45        Parameters:
46        ------
47            input_dir : str
48                入力ファイルが格納されているディレクトリのパス
49            output_dir : str
50                出力ファイルを格納するディレクトリのパス
51            flag_csv_path : str
52                フラグ情報が記載されているCSVファイルのパス
53            filename_patterns : list[str] | None
54                ファイル名のパターン(正規表現)のリスト
55            output_dirs : dict[str, str] | None
56                出力ディレクトリの構造を定義する辞書
57            sort_by_rh : bool
58                RHに基づいてサブディレクトリにファイルを分類するかどうか
59            logger : Logger | None
60                使用するロガー
61            logging_debug : bool
62                ログレベルをDEBUGに設定するかどうか
63        """
64        self._fft_path: str = input_dir
65        self._sorted_path: str = output_dir
66        self._output_dirs = output_dirs or self.DEFAULT_OUTPUT_DIRS
67        self._good_data_path: str = os.path.join(
68            output_dir, self._output_dirs["GOOD_DATA"]
69        )
70        self._bad_data_path: str = os.path.join(
71            output_dir, self._output_dirs["BAD_DATA"]
72        )
73        self._filename_patterns: list[str] = (
74            self.DEFAULT_FILENAME_PATTERNS.copy()
75            if filename_patterns is None
76            else filename_patterns
77        )
78        self._flag_file_path: str = flag_csv_path
79        self._sort_by_rh: bool = sort_by_rh
80        self._flags = {}
81        self._warnings = []
82        # ロガー
83        log_level: int = INFO
84        if logging_debug:
85            log_level = DEBUG
86        self.logger: Logger = FftFileReorganizer.setup_logger(logger, log_level)

FftFileReorganizerクラスを初期化します。

Parameters:

input_dir : str
    入力ファイルが格納されているディレクトリのパス
output_dir : str
    出力ファイルを格納するディレクトリのパス
flag_csv_path : str
    フラグ情報が記載されているCSVファイルのパス
filename_patterns : list[str] | None
    ファイル名のパターン(正規表現)のリスト
output_dirs : dict[str, str] | None
    出力ディレクトリの構造を定義する辞書
sort_by_rh : bool
    RHに基づいてサブディレクトリにファイルを分類するかどうか
logger : Logger | None
    使用するロガー
logging_debug : bool
    ログレベルをDEBUGに設定するかどうか
DEFAULT_FILENAME_PATTERNS: list[str] = ['FFT_TOA5_\\d+\\.SAC_Eddy_\\d+_(\\d{4})_(\\d{2})_(\\d{2})_(\\d{4})(?:\\+)?\\.csv', 'FFT_TOA5_\\d+\\.SAC_Ultra\\.Eddy_\\d+_(\\d{4})_(\\d{2})_(\\d{2})_(\\d{4})(?:\\+)?(?:-resampled)?\\.csv']
DEFAULT_OUTPUT_DIRS = {'GOOD_DATA': 'good_data_all', 'BAD_DATA': 'bad_data'}
logger: logging.Logger
def reorganize(self):
 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)

ファイルの再編成プロセス全体を実行します。 ディレクトリの準備、フラグファイルの読み込み、 有効なファイルの取得、ファイルのコピーを順に行います。 処理後、警告メッセージがあれば出力します。

@staticmethod
def get_rh_directory(rh: float):
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)

@staticmethod
def setup_logger(logger: logging.Logger | None, log_level: int = 20) -> logging.Logger:
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
    設定されたロガーオブジェクト。
class TransferFunctionCalculator:
  9class TransferFunctionCalculator:
 10    """
 11    このクラスは、CSVファイルからデータを読み込み、処理し、
 12    伝達関数を計算してプロットするための機能を提供します。
 13
 14    この実装は Moore (1986) の論文に基づいています。
 15    """
 16
 17    def __init__(
 18        self,
 19        file_path: str,
 20        col_freq: str,
 21        cutoff_freq_low: float = 0.01,
 22        cutoff_freq_high: float = 1,
 23    ):
 24        """
 25        TransferFunctionCalculatorクラスのコンストラクタ。
 26
 27        Parameters:
 28        ------
 29            file_path : str
 30                分析対象のCSVファイルのパス。
 31            col_freq : str
 32                周波数のキー。
 33            cutoff_freq_low : float
 34                カットオフ周波数の最低値。
 35            cutoff_freq_high : float
 36                カットオフ周波数の最高値。
 37        """
 38        self._col_freq: str = col_freq
 39        self._cutoff_freq_low: float = cutoff_freq_low
 40        self._cutoff_freq_high: float = cutoff_freq_high
 41        self._df: pd.DataFrame = TransferFunctionCalculator._load_data(file_path)
 42
 43    def calculate_transfer_function(
 44        self, col_reference: str, col_target: str
 45    ) -> tuple[float, float, pd.DataFrame]:
 46        """
 47        伝達関数の係数を計算する。
 48
 49        Parameters:
 50        ------
 51            col_reference : str
 52                参照データのカラム名。
 53            col_target : str
 54                ターゲットデータのカラム名。
 55
 56        Returns:
 57        ------
 58            tuple[float, float, pandas.DataFrame]
 59                伝達関数の係数aとその標準誤差、および計算に用いたDataFrame。
 60        """
 61        df_processed: pd.DataFrame = self.process_data(
 62            col_reference=col_reference, col_target=col_target
 63        )
 64        df_cutoff: pd.DataFrame = self._cutoff_df(df_processed)
 65
 66        array_x = np.array(df_cutoff.index)
 67        array_y = np.array(df_cutoff["target"] / df_cutoff["reference"])
 68
 69        # フィッティングパラメータと共分散行列を取得
 70        popt, pcov = curve_fit(
 71            TransferFunctionCalculator.transfer_function, array_x, array_y
 72        )
 73
 74        # 標準誤差を計算(共分散行列の対角成分の平方根)
 75        perr = np.sqrt(np.diag(pcov))
 76
 77        # 係数aとその標準誤差、および計算に用いたDataFrameを返す
 78        return popt[0], perr[0], df_processed
 79
 80    def create_plot_co_spectra(
 81        self,
 82        col1: str,
 83        col2: str,
 84        color1: str = "gray",
 85        color2: str = "red",
 86        figsize: tuple[int, int] = (10, 8),
 87        label1: str | None = None,
 88        label2: str | None = None,
 89        output_dir: str | None = None,
 90        output_basename: str = "co",
 91        add_legend: bool = True,
 92        add_xy_labels: bool = True,
 93        show_fig: bool = True,
 94        subplot_label: str | None = "(a)",
 95        window_size: int = 5,  # 移動平均の窓サイズ
 96        markersize: float = 14,
 97    ) -> None:
 98        """
 99        2種類のコスペクトルをプロットする。
100
101        Parameters:
102        ------
103            col1 : str
104                1つ目のコスペクトルデータのカラム名。
105            col2 : str
106                2つ目のコスペクトルデータのカラム名。
107            color1 : str, optional
108                1つ目のデータの色。デフォルトは'gray'。
109            color2 : str, optional
110                2つ目のデータの色。デフォルトは'red'。
111            figsize : tuple[int, int], optional
112                プロットのサイズ。デフォルトは(10, 8)。
113            label1 : str, optional
114                1つ目のデータのラベル名。デフォルトはNone。
115            label2 : str, optional
116                2つ目のデータのラベル名。デフォルトはNone。
117            output_dir : str | None, optional
118                プロットを保存するディレクトリ。デフォルトはNoneで、保存しない。
119            output_basename : str, optional
120                保存するファイル名のベース。デフォルトは"co"。
121            show_fig : bool, optional
122                プロットを表示するかどうか。デフォルトはTrue。
123            subplot_label : str | None, optional
124                左上に表示するサブプロットラベル。デフォルトは"(a)"。
125            window_size : int, optional
126                移動平均の窓サイズ。デフォルトは5。
127        """
128        df: pd.DataFrame = self._df.copy()
129        # データの取得と移動平均の適用
130        data1 = df[df[col1] > 0].groupby(self._col_freq)[col1].median()
131        data2 = df[df[col2] > 0].groupby(self._col_freq)[col2].median()
132
133        data1 = data1.rolling(window=window_size, center=True, min_periods=1).mean()
134        data2 = data2.rolling(window=window_size, center=True, min_periods=1).mean()
135
136        fig = plt.figure(figsize=figsize)
137        ax = fig.add_subplot(111)
138
139        # マーカーサイズを設定して見やすくする
140        ax.plot(
141            data1.index, data1, "o", color=color1, label=label1, markersize=markersize
142        )
143        ax.plot(
144            data2.index, data2, "o", color=color2, label=label2, markersize=markersize
145        )
146        ax.plot([0.01, 10], [10, 0.001], "-", color="black")
147        ax.text(0.25, 0.4, "-4/3")
148
149        ax.grid(True, alpha=0.3)
150        ax.set_xscale("log")
151        ax.set_yscale("log")
152        ax.set_xlim(0.0001, 10)
153        ax.set_ylim(0.0001, 10)
154        if add_xy_labels:
155            ax.set_xlabel("f (Hz)")
156            ax.set_ylabel("無次元コスペクトル")
157
158        if add_legend:
159            ax.legend(
160                bbox_to_anchor=(0.05, 1),
161                loc="lower left",
162                fontsize=16,
163                ncol=3,
164                frameon=False,
165            )
166        if subplot_label is not None:
167            ax.text(0.00015, 3, subplot_label)
168        fig.tight_layout()
169
170        if output_dir is not None:
171            os.makedirs(output_dir, exist_ok=True)
172            # プロットをPNG形式で保存
173            filename: str = f"{output_basename}.png"
174            fig.savefig(os.path.join(output_dir, filename), dpi=300)
175        if show_fig:
176            plt.show()
177        else:
178            plt.close(fig=fig)
179
180    def create_plot_ratio(
181        self,
182        df_processed: pd.DataFrame,
183        reference_name: str,
184        target_name: str,
185        figsize: tuple[int, int] = (10, 6),
186        output_dir: str | None = None,
187        output_basename: str = "ratio",
188        show_fig: bool = True,
189    ) -> None:
190        """
191        ターゲットと参照の比率をプロットする。
192
193        Parameters:
194        ------
195            df_processed : pd.DataFrame
196                処理されたデータフレーム。
197            reference_name : str
198                参照の名前。
199            target_name : str
200                ターゲットの名前。
201            figsize : tuple[int, int], optional
202                プロットのサイズ。デフォルトは(10, 6)。
203            output_dir : str | None, optional
204                プロットを保存するディレクトリ。デフォルトはNoneで、保存しない。
205            output_basename : str, optional
206                保存するファイル名のベース。デフォルトは"ratio"。
207            show_fig : bool, optional
208                プロットを表示するかどうか。デフォルトはTrue。
209        """
210        fig = plt.figure(figsize=figsize)
211        ax = fig.add_subplot(111)
212
213        ax.plot(
214            df_processed.index, df_processed["target"] / df_processed["reference"], "o"
215        )
216        ax.set_xscale("log")
217        ax.set_yscale("log")
218        ax.set_xlabel("f (Hz)")
219        ax.set_ylabel(f"{target_name} / {reference_name}")
220        ax.set_title(f"{target_name}{reference_name}の比")
221
222        if output_dir is not None:
223            # プロットをPNG形式で保存
224            filename: str = f"{output_basename}-{reference_name}_{target_name}.png"
225            fig.savefig(os.path.join(output_dir, filename), dpi=300)
226        if show_fig:
227            plt.show()
228        else:
229            plt.close(fig=fig)
230
231    @classmethod
232    def create_plot_tf_curves_from_csv(
233        cls,
234        file_path: str,
235        gas_configs: list[tuple[str, str, str, str]],
236        output_dir: str | None = None,
237        output_basename: str = "all_tf_curves",
238        col_datetime: str = "Date",
239        add_xlabel: bool = True,
240        label_x: str = "f (Hz)",
241        label_y: str = "無次元コスペクトル比",
242        label_avg: str = "Avg.",
243        label_co_ref: str = "Tv",
244        line_colors: list[str] | None = None,
245        font_family: list[str] = ["Arial", "MS Gothic"],
246        font_size: float = 20,
247        save_fig: bool = True,
248        show_fig: bool = True,
249    ) -> None:
250        """
251        複数の伝達関数の係数をプロットし、各ガスの平均値を表示します。
252        各ガスのデータをCSVファイルから読み込み、指定された設定に基づいてプロットを生成します。
253        プロットはオプションで保存することも可能です。
254
255        Parameters:
256        ------
257            file_path : str
258                伝達関数の係数が格納されたCSVファイルのパス。
259            gas_configs : list[tuple[str, str, str, str]]
260                ガスごとの設定のリスト。各タプルは以下の要素を含む:
261                (係数のカラム名, ガスの表示ラベル, 平均線の色, 出力ファイル用のガス名)
262                例: [("a_ch4-used", "CH$_4$", "red", "ch4")]
263            output_dir : str | None, optional
264                出力ディレクトリ。Noneの場合は保存しない。
265            output_basename : str, optional
266                出力ファイル名のベース。デフォルトは"all_tf_curves"。
267            col_datetime : str, optional
268                日付情報が格納されているカラム名。デフォルトは"Date"。
269            add_xlabel : bool, optional
270                x軸ラベルを追加するかどうか。デフォルトはTrue。
271            label_x : str, optional
272                x軸のラベル。デフォルトは"f (Hz)"。
273            label_y : str, optional
274                y軸のラベル。デフォルトは"無次元コスペクトル比"。
275            label_avg : str, optional
276                平均値のラベル。デフォルトは"Avg."。
277            line_colors : list[str] | None, optional
278                各日付のデータに使用する色のリスト。
279            font_family : list[str], optional
280                使用するフォントファミリーのリスト。
281            font_size : float, optional
282                フォントサイズ。
283            save_fig : bool, optional
284                プロットを保存するかどうか。デフォルトはTrue。
285            show_fig : bool, optional
286                プロットを表示するかどうか。デフォルトはTrue。
287        """
288        # プロットパラメータの設定
289        plt.rcParams.update(
290            {
291                "font.family": font_family,
292                "font.size": font_size,
293                "axes.labelsize": font_size,
294                "axes.titlesize": font_size,
295                "xtick.labelsize": font_size,
296                "ytick.labelsize": font_size,
297                "legend.fontsize": font_size,
298            }
299        )
300
301        # CSVファイルを読み込む
302        df = pd.read_csv(file_path)
303
304        # 各ガスについてプロット
305        for col_coef_a, label_gas, base_color, gas_name in gas_configs:
306            fig = plt.figure(figsize=(10, 6))
307
308            # データ数に応じたデフォルトの色リストを作成
309            if line_colors is None:
310                default_colors = [
311                    "#1f77b4",
312                    "#ff7f0e",
313                    "#2ca02c",
314                    "#d62728",
315                    "#9467bd",
316                    "#8c564b",
317                    "#e377c2",
318                    "#7f7f7f",
319                    "#bcbd22",
320                    "#17becf",
321                ]
322                n_dates = len(df)
323                plot_colors = (default_colors * (n_dates // len(default_colors) + 1))[
324                    :n_dates
325                ]
326            else:
327                plot_colors = line_colors
328
329            # 全てのa値を用いて伝達関数をプロット
330            for i, row in enumerate(df.iterrows()):
331                a = row[1][col_coef_a]
332                date = row[1][col_datetime]
333                x_fit = np.logspace(-3, 1, 1000)
334                y_fit = cls.transfer_function(x_fit, a)
335                plt.plot(
336                    x_fit,
337                    y_fit,
338                    "-",
339                    color=plot_colors[i],
340                    alpha=0.7,
341                    label=f"{date} (a = {a:.3f})",
342                )
343
344            # 平均のa値を用いた伝達関数をプロット
345            a_mean = df[col_coef_a].mean()
346            x_fit = np.logspace(-3, 1, 1000)
347            y_fit = cls.transfer_function(x_fit, a_mean)
348            plt.plot(
349                x_fit,
350                y_fit,
351                "-",
352                color=base_color,
353                linewidth=3,
354                label=f"{label_avg} (a = {a_mean:.3f})",
355            )
356
357            # グラフの設定
358            label_y_formatted: str = f"{label_y}\n({label_gas} / {label_co_ref})"
359            plt.xscale("log")
360            if add_xlabel:
361                plt.xlabel(label_x)
362            plt.ylabel(label_y_formatted)
363            plt.legend(loc="lower left", fontsize=font_size - 6)
364            plt.grid(True, which="both", ls="-", alpha=0.2)
365            plt.tight_layout()
366
367            if save_fig:
368                if output_dir is None:
369                    raise ValueError(
370                        "save_fig=Trueのとき、output_dirに有効なディレクトリパスを指定する必要があります。"
371                    )
372                os.makedirs(output_dir, exist_ok=True)
373                output_path: str = os.path.join(
374                    output_dir, f"{output_basename}-{gas_name}.png"
375                )
376                plt.savefig(output_path, dpi=300, bbox_inches="tight")
377            if show_fig:
378                plt.show()
379            else:
380                plt.close(fig=fig)
381
382    def create_plot_transfer_function(
383        self,
384        a: float,
385        df_processed: pd.DataFrame,
386        reference_name: str,
387        target_name: str,
388        figsize: tuple[int, int] = (10, 6),
389        output_dir: str | None = None,
390        output_basename: str = "tf",
391        show_fig: bool = True,
392        add_xlabel: bool = True,
393        label_x: str = "f (Hz)",
394        label_y: str = "コスペクトル比",
395        label_gas: str | None = None,
396    ) -> None:
397        """
398        伝達関数とそのフィットをプロットする。
399
400        Parameters:
401        ------
402            a : float
403                伝達関数の係数。
404            df_processed : pd.DataFrame
405                処理されたデータフレーム。
406            reference_name : str
407                参照の名前。
408            target_name : str
409                ターゲットの名前。
410            figsize : tuple[int, int], optional
411                プロットのサイズ。デフォルトは(10, 6)。
412            output_dir : str | None, optional
413                プロットを保存するディレクトリ。デフォルトはNoneで、保存しない。
414            output_basename : str, optional
415                保存するファイル名のベース。デフォルトは"tf"。
416            show_fig : bool, optional
417                プロットを表示するかどうか。デフォルトはTrue。
418        """
419        df_cutoff: pd.DataFrame = self._cutoff_df(df_processed)
420
421        fig = plt.figure(figsize=figsize)
422        ax = fig.add_subplot(111)
423
424        ax.plot(
425            df_cutoff.index,
426            df_cutoff["target"] / df_cutoff["reference"],
427            "o",
428            label=f"{target_name} / {reference_name}",
429        )
430
431        x_fit = np.logspace(
432            np.log10(self._cutoff_freq_low), np.log10(self._cutoff_freq_high), 1000
433        )
434        y_fit = self.transfer_function(x_fit, a)
435        ax.plot(x_fit, y_fit, "-", label=f"フィット (a = {a:.4f})")
436
437        ax.set_xscale("log")
438        # グラフの設定
439        label_y_formatted: str = f"{label_y}\n({label_gas} / 顕熱)"
440        plt.xscale("log")
441        if add_xlabel:
442            plt.xlabel(label_x)
443        plt.ylabel(label_y_formatted)
444        ax.legend()
445
446        if output_dir is not None:
447            # プロットをPNG形式で保存
448            filename: str = f"{output_basename}-{reference_name}_{target_name}.png"
449            fig.savefig(os.path.join(output_dir, filename), dpi=300)
450        if show_fig:
451            plt.show()
452        else:
453            plt.close(fig=fig)
454
455    def process_data(self, col_reference: str, col_target: str) -> pd.DataFrame:
456        """
457        指定されたキーに基づいてデータを処理する。
458
459        Parameters:
460        ------
461            col_reference : str
462                参照データのカラム名。
463            col_target : str
464                ターゲットデータのカラム名。
465
466        Returns:
467        ------
468            pd.DataFrame
469                処理されたデータフレーム。
470        """
471        df: pd.DataFrame = self._df.copy()
472        col_freq: str = self._col_freq
473
474        # データ型の確認と変換
475        df[col_freq] = pd.to_numeric(df[col_freq], errors="coerce")
476        df[col_reference] = pd.to_numeric(df[col_reference], errors="coerce")
477        df[col_target] = pd.to_numeric(df[col_target], errors="coerce")
478
479        # NaNを含む行を削除
480        df = df.dropna(subset=[col_freq, col_reference, col_target])
481
482        # グループ化と中央値の計算
483        grouped = df.groupby(col_freq)
484        reference_data = grouped[col_reference].median()
485        target_data = grouped[col_target].median()
486
487        df_processed = pd.DataFrame(
488            {"reference": reference_data, "target": target_data}
489        )
490
491        # 異常な比率を除去
492        df_processed.loc[
493            (
494                (df_processed["target"] / df_processed["reference"] > 1)
495                | (df_processed["target"] / df_processed["reference"] < 0)
496            )
497        ] = np.nan
498        df_processed = df_processed.dropna()
499
500        return df_processed
501
502    def _cutoff_df(self, df: pd.DataFrame) -> pd.DataFrame:
503        """
504        カットオフ周波数に基づいてDataFrameを加工するメソッド
505
506        Parameters:
507        ------
508            df : pd.DataFrame
509                加工対象のデータフレーム。
510
511        Returns:
512        ------
513            pd.DataFrame
514                カットオフ周波数に基づいて加工されたデータフレーム。
515        """
516        df_cutoff: pd.DataFrame = df.loc[
517            (self._cutoff_freq_low <= df.index) & (df.index <= self._cutoff_freq_high)
518        ]
519        return df_cutoff
520
521    @classmethod
522    def transfer_function(cls, x: np.ndarray, a: float) -> np.ndarray:
523        """
524        伝達関数を計算する。
525
526        Parameters:
527        ------
528            x : np.ndarray
529                周波数の配列。
530            a : float
531                伝達関数の係数。
532
533        Returns:
534        ------
535            np.ndarray
536                伝達関数の値。
537        """
538        return np.exp(-np.log(np.sqrt(2)) * np.power(x / a, 2))
539
540    @staticmethod
541    def _load_data(file_path: str) -> pd.DataFrame:
542        """
543        CSVファイルからデータを読み込む。
544
545        Parameters:
546        ------
547            file_path : str
548                csvファイルのパス。
549
550        Returns:
551        ------
552            pd.DataFrame
553                読み込まれたデータフレーム。
554        """
555        tmp = pd.read_csv(file_path, header=None, nrows=1, skiprows=0)
556        header = tmp.loc[tmp.index[0]]
557        df = pd.read_csv(file_path, header=None, skiprows=1)
558        df.columns = header
559        return df
560
561    @staticmethod
562    def setup_plot_params(
563        font_family: list[str] = ["Arial", "Dejavu Sans"],
564        font_size: float = 20,
565        legend_size: float = 20,
566        tick_size: float = 20,
567        title_size: float = 20,
568        plot_params=None,
569    ) -> None:
570        """
571        matplotlibのプロットパラメータを設定します。
572
573        Parameters:
574        ------
575            font_family : list[str]
576                使用するフォントファミリーのリスト。
577            font_size : float
578                軸ラベルのフォントサイズ。
579            legend_size : float
580                凡例のフォントサイズ。
581            tick_size : float
582                軸目盛りのフォントサイズ。
583            title_size : float
584                タイトルのフォントサイズ。
585            plot_params : Optional[Dict[str, any]]
586                matplotlibのプロットパラメータの辞書。
587        """
588        # デフォルトのプロットパラメータ
589        default_params = {
590            "axes.linewidth": 1.0,
591            "axes.titlesize": title_size,  # タイトル
592            "grid.color": "gray",
593            "grid.linewidth": 1.0,
594            "font.family": font_family,
595            "font.size": font_size,  # 軸ラベル
596            "legend.fontsize": legend_size,  # 凡例
597            "text.color": "black",
598            "xtick.color": "black",
599            "ytick.color": "black",
600            "xtick.labelsize": tick_size,  # 軸目盛
601            "ytick.labelsize": tick_size,  # 軸目盛
602            "xtick.major.size": 0,
603            "ytick.major.size": 0,
604            "ytick.direction": "out",
605            "ytick.major.width": 1.0,
606        }
607
608        # plot_paramsが定義されている場合、デフォルトに追記
609        if plot_params:
610            default_params.update(plot_params)
611
612        plt.rcParams.update(default_params)  # プロットパラメータを更新

このクラスは、CSVファイルからデータを読み込み、処理し、 伝達関数を計算してプロットするための機能を提供します。

この実装は Moore (1986) の論文に基づいています。

TransferFunctionCalculator( file_path: str, col_freq: str, cutoff_freq_low: float = 0.01, cutoff_freq_high: float = 1)
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
    カットオフ周波数の最高値。
def calculate_transfer_function( self, col_reference: str, col_target: str) -> tuple[float, float, pandas.core.frame.DataFrame]:
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。
def create_plot_co_spectra( self, col1: str, col2: str, color1: str = 'gray', color2: str = 'red', figsize: tuple[int, int] = (10, 8), label1: str | None = None, label2: str | None = None, output_dir: str | None = None, output_basename: str = 'co', add_legend: bool = True, add_xy_labels: bool = True, show_fig: bool = True, subplot_label: str | None = '(a)', window_size: int = 5, markersize: float = 14) -> None:
 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。
def create_plot_ratio( self, df_processed: pandas.core.frame.DataFrame, reference_name: str, target_name: str, figsize: tuple[int, int] = (10, 6), output_dir: str | None = None, output_basename: str = 'ratio', show_fig: bool = True) -> None:
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。
@classmethod
def create_plot_tf_curves_from_csv( cls, file_path: str, gas_configs: list[tuple[str, str, str, str]], output_dir: str | None = None, output_basename: str = 'all_tf_curves', col_datetime: str = 'Date', add_xlabel: bool = True, label_x: str = 'f (Hz)', label_y: str = '無次元コスペクトル比', label_avg: str = 'Avg.', label_co_ref: str = 'Tv', line_colors: list[str] | None = None, font_family: list[str] = ['Arial', 'MS Gothic'], font_size: float = 20, save_fig: bool = True, show_fig: bool = True) -> None:
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。
def create_plot_transfer_function( self, a: float, df_processed: pandas.core.frame.DataFrame, reference_name: str, target_name: str, figsize: tuple[int, int] = (10, 6), output_dir: str | None = None, output_basename: str = 'tf', show_fig: bool = True, add_xlabel: bool = True, label_x: str = 'f (Hz)', label_y: str = 'コスペクトル比', label_gas: str | None = None) -> None:
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。
def process_data(self, col_reference: str, col_target: str) -> pandas.core.frame.DataFrame:
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
    処理されたデータフレーム。
@classmethod
def transfer_function(cls, x: numpy.ndarray, a: float) -> numpy.ndarray:
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
    伝達関数の値。
@staticmethod
def setup_plot_params( font_family: list[str] = ['Arial', 'Dejavu Sans'], font_size: float = 20, legend_size: float = 20, tick_size: float = 20, title_size: float = 20, plot_params=None) -> None:
561    @staticmethod
562    def setup_plot_params(
563        font_family: list[str] = ["Arial", "Dejavu Sans"],
564        font_size: float = 20,
565        legend_size: float = 20,
566        tick_size: float = 20,
567        title_size: float = 20,
568        plot_params=None,
569    ) -> None:
570        """
571        matplotlibのプロットパラメータを設定します。
572
573        Parameters:
574        ------
575            font_family : list[str]
576                使用するフォントファミリーのリスト。
577            font_size : float
578                軸ラベルのフォントサイズ。
579            legend_size : float
580                凡例のフォントサイズ。
581            tick_size : float
582                軸目盛りのフォントサイズ。
583            title_size : float
584                タイトルのフォントサイズ。
585            plot_params : Optional[Dict[str, any]]
586                matplotlibのプロットパラメータの辞書。
587        """
588        # デフォルトのプロットパラメータ
589        default_params = {
590            "axes.linewidth": 1.0,
591            "axes.titlesize": title_size,  # タイトル
592            "grid.color": "gray",
593            "grid.linewidth": 1.0,
594            "font.family": font_family,
595            "font.size": font_size,  # 軸ラベル
596            "legend.fontsize": legend_size,  # 凡例
597            "text.color": "black",
598            "xtick.color": "black",
599            "ytick.color": "black",
600            "xtick.labelsize": tick_size,  # 軸目盛
601            "ytick.labelsize": tick_size,  # 軸目盛
602            "xtick.major.size": 0,
603            "ytick.major.size": 0,
604            "ytick.direction": "out",
605            "ytick.major.width": 1.0,
606        }
607
608        # plot_paramsが定義されている場合、デフォルトに追記
609        if plot_params:
610            default_params.update(plot_params)
611
612        plt.rcParams.update(default_params)  # プロットパラメータを更新

matplotlibのプロットパラメータを設定します。

Parameters:

font_family : list[str]
    使用するフォントファミリーのリスト。
font_size : float
    軸ラベルのフォントサイズ。
legend_size : float
    凡例のフォントサイズ。
tick_size : float
    軸目盛りのフォントサイズ。
title_size : float
    タイトルのフォントサイズ。
plot_params : Optional[Dict[str, any]]
    matplotlibのプロットパラメータの辞書。