py_flux_tracer

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

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

Parameters

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

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

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

Parameters:

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

Returns:

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

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

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

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

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

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

Parameters:

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

Raises:

OSError
    ディレクトリの作成に失敗した場合。
FileNotFoundError
    入力ファイルが見つからない場合。
ValueError
    出力ディレクトリが指定されていない、またはデータの処理中にエラーが発生した場合。
@staticmethod
def setup_logger(logger: logging.Logger | None, log_level: int = 20) -> logging.Logger:
706    @staticmethod
707    def setup_logger(logger: Logger | None, log_level: int = INFO) -> Logger:
708        """
709        ロガーを設定します。
710
711        このメソッドは、ロギングの設定を行い、ログメッセージのフォーマットを指定します。
712        ログメッセージには、日付、ログレベル、メッセージが含まれます。
713
714        渡されたロガーがNoneまたは不正な場合は、新たにロガーを作成し、標準出力に
715        ログメッセージが表示されるようにStreamHandlerを追加します。ロガーのレベルは
716        引数で指定されたlog_levelに基づいて設定されます。
717
718        Parameters:
719        -----
720            logger : Logger | None
721                使用するロガー。Noneの場合は新しいロガーを作成します。
722            log_level : int
723                ロガーのログレベル。デフォルトはINFO。
724
725        Returns:
726        -----
727            Logger
728                設定されたロガーオブジェクト。
729        """
730        if logger is not None and isinstance(logger, Logger):
731            return logger
732        # 渡されたロガーがNoneまたは正しいものでない場合は独自に設定
733        new_logger: Logger = getLogger()
734        # 既存のハンドラーをすべて削除
735        for handler in new_logger.handlers[:]:
736            new_logger.removeHandler(handler)
737        new_logger.setLevel(log_level)  # ロガーのレベルを設定
738        ch = StreamHandler()
739        ch_formatter = Formatter("%(asctime)s - %(levelname)s - %(message)s")
740        ch.setFormatter(ch_formatter)  # フォーマッターをハンドラーに設定
741        new_logger.addHandler(ch)  # StreamHandlerの追加
742        return new_logger

ロガーを設定します。

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

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

Parameters:

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

Returns:

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

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

Parameters:

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

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

Parameters:

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

Returns:

tuple
    (freqs, co_spectrum, corr_coef)
    - freqs : np.ndarray
        周波数軸(対数スケールの場合は対数変換済み)。
    - co_spectrum : np.ndarray
        コスペクトル(対数スケールの場合は対数変換済み)。
    - corr_coef : float
        変数の相関係数。
def calculate_cross_spectrum( self, col1: str, col2: str, dimensionless: bool = True, frequency_weighted: bool = True, interpolate_points: bool = True, scaling: str = 'spectrum', apply_lag_correction_to_col2: bool = True, lag_second: float | None = None) -> tuple:
 94    def calculate_cross_spectrum(
 95        self,
 96        col1: str,
 97        col2: str,
 98        dimensionless: bool = True,
 99        frequency_weighted: bool = True,
100        interpolate_points: bool = True,
101        scaling: str = "spectrum",
102        apply_lag_correction_to_col2: bool = True,
103        lag_second: float | None = None,
104    ) -> tuple:
105        """
106        指定されたcol1とcol2のクロススペクトルをDataFrameから計算するためのメソッド。
107
108        Parameters:
109        ------
110            col1 : str
111                データの列名1。
112            col2 : str
113                データの列名2。
114            dimensionless : bool, optional
115                Trueの場合、分散で割って無次元化を行う。デフォルトはTrue。
116            frequency_weighted : bool, optional
117                周波数の重みづけを適用するかどうか。デフォルトはTrue。
118            interpolate_points : bool, optional
119                等間隔なデータ点を生成するかどうか(対数軸上で等間隔)。デフォルトはTrue。
120            scaling : str
121                "density"でスペクトル密度、"spectrum"でスペクトル。デフォルトは"spectrum"。
122            apply_lag_correction_to_col2 : bool, optional
123                col2に遅れ時間補正を適用するかどうか。デフォルトはTrue。
124            lag_second : float | None, optional
125                col1からcol2が遅れている時間(秒)。apply_lag_correction_to_col2がTrueの場合に必要。デフォルトはNone。
126
127        Returns:
128        ------
129            tuple
130                (freqs, co_spectrum, corr_coef)
131                - freqs : np.ndarray
132                    周波数軸(対数スケールの場合は対数変換済み)。
133                - co_spectrum : np.ndarray
134                    クロススペクトル(対数スケールの場合は対数変換済み)。
135                - corr_coef : float
136                    変数の相関係数。
137        """
138        # バリデーション
139        valid_scaling_options = ["density", "spectrum"]
140        if scaling not in valid_scaling_options:
141            raise ValueError(
142                f"'scaling'は次のパラメータから選択してください: {valid_scaling_options}"
143            )
144
145        fs: float = self._fs
146        df: pd.DataFrame = self._df.copy()
147        # データ取得と前処理
148        data1: np.ndarray = np.array(df[col1].values)
149        data2: np.ndarray = np.array(df[col2].values)
150
151        # 遅れ時間の補正
152        if apply_lag_correction_to_col2:
153            if lag_second is None:
154                raise ValueError(
155                    "apply_lag_correction_to_col2=True の場合は lag_second に有効な遅れ時間(秒)を指定してください。"
156                )
157            data1, data2 = SpectrumCalculator._correct_lag_time(
158                data1=data1, data2=data2, fs=fs, lag_second=lag_second
159            )
160
161        # トレンド除去
162        data1 = SpectrumCalculator._detrend(data=data1, first=True)
163        data2 = SpectrumCalculator._detrend(data=data2, first=True)
164
165        # 相関係数の計算
166        corr_coef: float = np.corrcoef(data1, data2)[0, 1]
167
168        # クロススペクトル計算
169        freqs, Pxy = signal.csd(
170            data1,
171            data2,
172            fs=self._fs,
173            window=self._window_type,
174            nperseg=1024,
175            scaling=scaling,
176        )
177
178        # コスペクトルとクアドラチャスペクトルの抽出
179        co_spectrum = np.real(Pxy)
180        quad_spectrum = np.imag(Pxy)
181
182        # 周波数の重みづけ
183        if frequency_weighted:
184            co_spectrum[1:] *= freqs[1:]
185            quad_spectrum[1:] *= freqs[1:]
186
187        # 無次元化
188        if dimensionless:
189            cov_matrix: np.ndarray = np.cov(data1, data2)
190            covariance: float = cov_matrix[0, 1]
191            co_spectrum /= covariance
192            quad_spectrum /= covariance
193
194        if interpolate_points:
195            # 補間処理
196            log_freq_min = np.log10(0.001)
197            log_freq_max = np.log10(freqs[-1])
198            log_freq_resampled = np.logspace(log_freq_min, log_freq_max, self._plots)
199
200            # スペクトルの補間
201            co_resampled = np.interp(
202                log_freq_resampled, freqs, co_spectrum, left=np.nan, right=np.nan
203            )
204            quad_resampled = np.interp(
205                log_freq_resampled, freqs, quad_spectrum, left=np.nan, right=np.nan
206            )
207
208            # NaNを除外
209            valid_mask = ~np.isnan(co_resampled)
210            freqs = log_freq_resampled[valid_mask]
211            co_spectrum = co_resampled[valid_mask]
212            quad_spectrum = quad_resampled[valid_mask]
213
214        # 0Hz成分を除外
215        nonzero_mask = freqs != 0
216        freqs = freqs[nonzero_mask]
217        co_spectrum = co_spectrum[nonzero_mask]
218        quad_spectrum = quad_spectrum[nonzero_mask]
219
220        return freqs, co_spectrum, quad_spectrum, corr_coef

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

Parameters:

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

Returns:

tuple
    (freqs, co_spectrum, corr_coef)
    - freqs : np.ndarray
        周波数軸(対数スケールの場合は対数変換済み)。
    - co_spectrum : np.ndarray
        クロススペクトル(対数スケールの場合は対数変換済み)。
    - corr_coef : float
        変数の相関係数。
def calculate_power_spectrum( self, col: str, dimensionless: bool = True, frequency_weighted: bool = True, interpolate_points: bool = True, scaling: str = 'spectrum') -> tuple:
222    def calculate_power_spectrum(
223        self,
224        col: str,
225        dimensionless: bool = True,
226        frequency_weighted: bool = True,
227        interpolate_points: bool = True,
228        scaling: str = "spectrum",
229    ) -> tuple:
230        """
231        指定されたcolに基づいてDataFrameからパワースペクトルと周波数軸を計算します。
232        scipy.signal.welchを使用してパワースペクトルを計算します。
233
234        Parameters:
235        ------
236            col : str
237                データの列名
238            dimensionless : bool, optional
239                Trueの場合、分散で割って無次元化を行います。デフォルトはTrueです。
240            frequency_weighted : bool, optional
241                周波数の重みづけを適用するかどうか。デフォルトはTrueです。
242            interpolate_points : bool, optional
243                等間隔なデータ点を生成するかどうか(対数軸上で等間隔)。
244            scaling : str, optional
245                "density"でスペクトル密度、"spectrum"でスペクトル。デフォルトは"spectrum"です。
246
247        Returns:
248        ------
249            tuple
250                - freqs (np.ndarray): 周波数軸(対数スケールの場合は対数変換済み)
251                - power_spectrum (np.ndarray): パワースペクトル(対数スケールの場合は対数変換済み)
252        """
253        # バリデーション
254        valid_scaling_options = ["density", "spectrum"]
255        if scaling not in valid_scaling_options:
256            raise ValueError(
257                f"'scaling'は次のパラメータから選択してください: {valid_scaling_options}"
258            )
259
260        # データの取得とトレンド除去
261        data: np.ndarray = np.array(self._df[col].values)
262        data = SpectrumCalculator._detrend(data)
263
264        # welchメソッドでパワースペクトル計算
265        freqs, power_spectrum = signal.welch(
266            data, fs=self._fs, window=self._window_type, nperseg=1024, scaling=scaling
267        )
268
269        # 周波数の重みづけ(0Hz除外の前に実施)
270        if frequency_weighted:
271            power_spectrum = freqs * power_spectrum
272
273        # 無次元化(0Hz除外の前に実施)
274        if dimensionless:
275            variance = np.var(data)
276            power_spectrum /= variance
277
278        if interpolate_points:
279            # 補間処理(0Hz除外の前に実施)
280            log_freq_min = np.log10(0.001)
281            log_freq_max = np.log10(freqs[-1])
282            log_freq_resampled = np.logspace(log_freq_min, log_freq_max, self._plots)
283
284            power_spectrum_resampled = np.interp(
285                log_freq_resampled, freqs, power_spectrum, left=np.nan, right=np.nan
286            )
287
288            # NaNを除外
289            valid_mask = ~np.isnan(power_spectrum_resampled)
290            freqs = log_freq_resampled[valid_mask]
291            power_spectrum = power_spectrum_resampled[valid_mask]
292
293        # 0Hz成分を最後に除外
294        nonzero_mask = freqs != 0
295        freqs = freqs[nonzero_mask]
296        power_spectrum = power_spectrum[nonzero_mask]
297
298        return freqs, power_spectrum

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

Parameters:

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

Returns:

tuple
    - freqs (np.ndarray): 周波数軸(対数スケールの場合は対数変換済み)
    - power_spectrum (np.ndarray): パワースペクトル(対数スケールの場合は対数変換済み)
class FigureUtils:
 5class FigureUtils:
 6    @staticmethod
 7    def setup_plot_params(
 8        font_family: list[str] = ["Arial", "MS Gothic", "Dejavu Sans"],
 9        font_size: float = 20,
10        legend_size: float = 20,
11        tick_size: float = 20,
12        title_size: float = 20,
13        plot_params: dict[str, any] | None = None,
14    ) -> None:
15        """
16        matplotlibのプロットパラメータを設定します。
17
18        Parameters:
19        ------
20            font_family : list[str]
21                使用するフォントファミリーのリスト。
22            font_size : float
23                軸ラベルのフォントサイズ。
24            legend_size : float
25                凡例のフォントサイズ。
26            tick_size : float
27                軸目盛りのフォントサイズ。
28            title_size : float
29                タイトルのフォントサイズ。
30            plot_params : dict[str, any] | None
31                matplotlibのプロットパラメータの辞書。
32        """
33        # デフォルトのプロットパラメータ
34        default_params = {
35            "axes.linewidth": 1.0,
36            "axes.titlesize": title_size,  # タイトル
37            "grid.color": "gray",
38            "grid.linewidth": 1.0,
39            "font.family": font_family,
40            "font.size": font_size,  # 軸ラベル
41            "legend.fontsize": legend_size,  # 凡例
42            "text.color": "black",
43            "xtick.color": "black",
44            "ytick.color": "black",
45            "xtick.labelsize": tick_size,  # 軸目盛
46            "ytick.labelsize": tick_size,  # 軸目盛
47            "xtick.major.size": 0,
48            "ytick.major.size": 0,
49            "ytick.direction": "out",
50            "ytick.major.width": 1.0,
51        }
52
53        # plot_paramsが定義されている場合、デフォルトに追記
54        if plot_params:
55            default_params.update(plot_params)
56
57        plt.rcParams.update(default_params)  # プロットパラメータを更新
@staticmethod
def setup_plot_params( font_family: list[str] = ['Arial', 'MS Gothic', 'Dejavu Sans'], font_size: float = 20, legend_size: float = 20, tick_size: float = 20, title_size: float = 20, plot_params: dict[str, any] | None = None) -> None:
 6    @staticmethod
 7    def setup_plot_params(
 8        font_family: list[str] = ["Arial", "MS Gothic", "Dejavu Sans"],
 9        font_size: float = 20,
10        legend_size: float = 20,
11        tick_size: float = 20,
12        title_size: float = 20,
13        plot_params: dict[str, any] | None = None,
14    ) -> None:
15        """
16        matplotlibのプロットパラメータを設定します。
17
18        Parameters:
19        ------
20            font_family : list[str]
21                使用するフォントファミリーのリスト。
22            font_size : float
23                軸ラベルのフォントサイズ。
24            legend_size : float
25                凡例のフォントサイズ。
26            tick_size : float
27                軸目盛りのフォントサイズ。
28            title_size : float
29                タイトルのフォントサイズ。
30            plot_params : dict[str, any] | None
31                matplotlibのプロットパラメータの辞書。
32        """
33        # デフォルトのプロットパラメータ
34        default_params = {
35            "axes.linewidth": 1.0,
36            "axes.titlesize": title_size,  # タイトル
37            "grid.color": "gray",
38            "grid.linewidth": 1.0,
39            "font.family": font_family,
40            "font.size": font_size,  # 軸ラベル
41            "legend.fontsize": legend_size,  # 凡例
42            "text.color": "black",
43            "xtick.color": "black",
44            "ytick.color": "black",
45            "xtick.labelsize": tick_size,  # 軸目盛
46            "ytick.labelsize": tick_size,  # 軸目盛
47            "xtick.major.size": 0,
48            "ytick.major.size": 0,
49            "ytick.direction": "out",
50            "ytick.major.width": 1.0,
51        }
52
53        # plot_paramsが定義されている場合、デフォルトに追記
54        if plot_params:
55            default_params.update(plot_params)
56
57        plt.rcParams.update(default_params)  # プロットパラメータを更新

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

Parameters:

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

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

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

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

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

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

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

Parameters:

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

ロガーを設定します。

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

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

Parameters:

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

Returns:

Logger
    設定されたロガーオブジェクト。
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]]:
126    def calculate_flux_footprint(
127        self,
128        df: pd.DataFrame,
129        col_flux: str,
130        plot_count: int = 10000,
131        start_time: str = "10:00",
132        end_time: str = "16:00",
133    ) -> tuple[list[float], list[float], list[float]]:
134        """
135        フラックスフットプリントを計算し、指定された時間帯のデータを基に可視化します。
136
137        Parameters:
138        ------
139            df : pd.DataFrame
140                分析対象のデータフレーム。フラックスデータを含む。
141            col_flux : str
142                フラックスデータの列名。計算に使用される。
143            plot_count : int, optional
144                生成するプロットの数。デフォルトは10000。
145            start_time : str, optional
146                フットプリント計算に使用する開始時間。デフォルトは"10:00"。
147            end_time : str, optional
148                フットプリント計算に使用する終了時間。デフォルトは"16:00"。
149
150        Returns:
151        ------
152            tuple[list[float], list[float], list[float]]:
153                x座標 (メートル): タワーを原点とした東西方向の距離
154                y座標 (メートル): タワーを原点とした南北方向の距離
155                対象スカラー量の値: 各地点でのフラックス値
156
157        Notes:
158        ------
159            - 返却される座標は測定タワーを原点(0,0)とした相対位置です
160            - すべての距離はメートル単位で表されます
161            - 正のx値は東方向、正のy値は北方向を示します
162        """
163        df: pd.DataFrame = df.copy()
164
165        # インデックスがdatetimeであることを確認し、必要に応じて変換
166        if not isinstance(df.index, pd.DatetimeIndex):
167            df.index = pd.to_datetime(df.index)
168
169        # DatetimeIndexから直接dateプロパティにアクセス
170        datelist: np.ndarray = np.array(df.index.date)
171
172        # 各日付が平日かどうかを判定し、リストに格納
173        numbers: list[int] = [
174            FluxFootprintAnalyzer.is_weekday(date) for date in datelist
175        ]
176
177        # col_weekdayに基づいてデータフレームに平日情報を追加
178        df.loc[:, self._col_weekday] = numbers  # .locを使用して値を設定
179
180        # 値が1のもの(平日)をコピーする
181        data_weekday: pd.DataFrame = df[df[self._col_weekday] == 1].copy()
182        # 特定の時間帯を抽出
183        data_weekday = data_weekday.between_time(
184            start_time, end_time
185        )  # 引数を使用して時間帯を抽出
186        data_weekday = data_weekday.dropna(subset=[col_flux])
187
188        directions: list[float] = [
189            wind_direction if wind_direction >= 0 else wind_direction + 360
190            for wind_direction in data_weekday["Wind direction"]
191        ]
192
193        data_weekday.loc[:, "Wind direction_360"] = directions
194        data_weekday.loc[:, "radian"] = data_weekday["Wind direction_360"] / 180 * np.pi
195
196        # 風向が欠測なら除去
197        data_weekday = data_weekday.dropna(subset=["Wind direction", col_flux])
198
199        # 数値型への変換を確実に行う
200        numeric_columns: list[str] = ["u*", "WS vector", "sigmaV", "z/L"]
201        for col in numeric_columns:
202            data_weekday[col] = pd.to_numeric(data_weekday[col], errors="coerce")
203
204        # 地面修正量dの計算
205        z_m: float = self._z_m
206        Z_d: float = FluxFootprintAnalyzer._calculate_ground_correction(
207            z_m=z_m,
208            wind_speed=data_weekday["WS vector"].values,
209            friction_velocity=data_weekday["u*"].values,
210            stability_parameter=data_weekday["z/L"].values,
211        )
212
213        x_list: list[float] = []
214        y_list: list[float] = []
215        c_list: list[float] | None = []
216
217        # tqdmを使用してプログレスバーを表示
218        for i in tqdm(range(len(data_weekday)), desc="Calculating footprint"):
219            dUstar: float = data_weekday["u*"].iloc[i]
220            dU: float = data_weekday["WS vector"].iloc[i]
221            sigmaV: float = data_weekday["sigmaV"].iloc[i]
222            dzL: float = data_weekday["z/L"].iloc[i]
223
224            if pd.isna(dUstar) or pd.isna(dU) or pd.isna(sigmaV) or pd.isna(dzL):
225                self.logger.warning(f"#N/A fields are exist.: i = {i}")
226                continue
227            elif dUstar < 5.0 and dUstar != 0.0 and dU > 0.1:
228                phi_m, phi_c, n = FluxFootprintAnalyzer._calculate_stability_parameters(
229                    dzL=dzL
230                )
231                m, U, r, mu, ksi = (
232                    FluxFootprintAnalyzer._calculate_footprint_parameters(
233                        dUstar=dUstar, dU=dU, Z_d=Z_d, phi_m=phi_m, phi_c=phi_c, n=n
234                    )
235                )
236
237                # 80%ソースエリアの計算
238                x80: float = FluxFootprintAnalyzer._source_area_KM2001(
239                    ksi=ksi, mu=mu, dU=dU, sigmaV=sigmaV, Z_d=Z_d, max_ratio=0.8
240                )
241
242                if not np.isnan(x80):
243                    x1, y1, flux1 = FluxFootprintAnalyzer._prepare_plot_data(
244                        x80,
245                        ksi,
246                        mu,
247                        r,
248                        U,
249                        m,
250                        sigmaV,
251                        data_weekday[col_flux].iloc[i],
252                        plot_count=plot_count,
253                    )
254                    x1_, y1_ = FluxFootprintAnalyzer._rotate_coordinates(
255                        x=x1, y=y1, radian=data_weekday["radian"].iloc[i]
256                    )
257
258                    x_list.extend(x1_)
259                    y_list.extend(y1_)
260                    c_list.extend(flux1)
261
262        return (
263            x_list,
264            y_list,
265            c_list,
266        )

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

Parameters:

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

Returns:

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

Notes:

- 返却される座標は測定タワーを原点(0,0)とした相対位置です
- すべての距離はメートル単位で表されます
- 正のx値は東方向、正のy値は北方向を示します
def combine_all_data( self, data_source: str | pandas.core.frame.DataFrame, source_type: str = 'csv', **kwargs) -> pandas.core.frame.DataFrame:
268    def combine_all_data(
269        self, data_source: str | pd.DataFrame, source_type: str = "csv", **kwargs
270    ) -> pd.DataFrame:
271        """
272        CSVファイルまたはMonthlyConverterからのデータを統合します
273
274        Parameters:
275        ------
276            data_source : str | pd.DataFrame
277                CSVディレクトリパスまたはDataFrame
278            source_type : str
279                "csv" または "monthly"
280            **kwargs :
281                追加パラメータ
282                - sheet_names : list[str]
283                    Monthlyの場合のシート名
284                - start_date : str
285                    開始日
286                - end_date : str
287                    終了日
288
289        Returns:
290        ------
291            pd.DataFrame
292                処理済みのデータフレーム
293        """
294        if source_type == "csv":
295            # 既存のCSV処理ロジック
296            return self._combine_all_csv(data_source)
297        elif source_type == "monthly":
298            # MonthlyConverterからのデータを処理
299            if not isinstance(data_source, pd.DataFrame):
300                raise ValueError("monthly形式の場合、DataFrameを直接渡す必要があります")
301
302            df = data_source.copy()
303
304            # required_columnsからDateを除外して欠損値チェックを行う
305            check_columns = [col for col in self._required_columns if col != "Date"]
306
307            # インデックスがdatetimeであることを確認
308            if not isinstance(df.index, pd.DatetimeIndex) and "Date" not in df.columns:
309                raise ValueError("DatetimeIndexまたはDateカラムが必要です")
310
311            if "Date" in df.columns:
312                df.set_index("Date", inplace=True)
313
314            # 必要なカラムの存在確認
315            missing_columns = [
316                col for col in check_columns if col not in df.columns.tolist()
317            ]
318            if missing_columns:
319                missing_cols = "','".join(missing_columns)
320                current_cols = "','".join(df.columns.tolist())
321                raise ValueError(
322                    f"必要なカラムが不足しています: '{missing_cols}'\n"
323                    f"現在のカラム: '{current_cols}'"
324                )
325
326            # 平日/休日の判定用カラムを追加
327            df[self._col_weekday] = df.index.map(FluxFootprintAnalyzer.is_weekday)
328
329            # Dateを除外したカラムで欠損値の処理
330            df = df.dropna(subset=check_columns)
331
332            # インデックスの重複を除去
333            df = df.loc[~df.index.duplicated(), :]
334
335            return df
336        else:
337            raise ValueError("source_typeは'csv'または'monthly'である必要があります")

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

Parameters:

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

Returns:

pd.DataFrame
    処理済みのデータフレーム
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:
339    def get_satellite_image_from_api(
340        self,
341        api_key: str,
342        center_lat: float,
343        center_lon: float,
344        output_path: str,
345        scale: int = 1,
346        size: tuple[int, int] = (2160, 2160),
347        zoom: int = 13,
348    ) -> ImageFile:
349        """
350        Google Maps Static APIを使用して衛星画像を取得します。
351
352        Parameters:
353        ------
354            api_key : str
355                Google Maps Static APIのキー。
356            center_lat : float
357                中心の緯度。
358            center_lon : float
359                中心の経度。
360            output_path : str
361                画像の保存先パス。拡張子は'.png'のみ許可される。
362            scale : int, optional
363                画像の解像度スケール(1か2)。デフォルトは1。
364            size : tuple[int, int], optional
365                画像サイズ (幅, 高さ)。デフォルトは(2160, 2160)。
366            zoom : int, optional
367                ズームレベル(0-21)。デフォルトは13。
368
369        Returns:
370        ------
371            ImageFile
372                取得した衛星画像
373
374        Raises:
375        ------
376            requests.RequestException
377                API呼び出しに失敗した場合
378        """
379        # バリデーション
380        if not output_path.endswith(".png"):
381            raise ValueError("出力ファイル名は'.png'で終わる必要があります。")
382
383        # HTTPリクエストの定義
384        base_url = "https://maps.googleapis.com/maps/api/staticmap"
385        params = {
386            "center": f"{center_lat},{center_lon}",
387            "zoom": zoom,
388            "size": f"{size[0]}x{size[1]}",
389            "maptype": "satellite",
390            "scale": scale,
391            "key": api_key,
392        }
393
394        try:
395            response = requests.get(base_url, params=params)
396            response.raise_for_status()
397            # 画像ファイルに変換
398            image = Image.open(io.BytesIO(response.content))
399            image.save(output_path)
400            self._got_satellite_image = True
401            self.logger.info(f"リモート画像を取得し、保存しました: {output_path}")
402            return image
403        except requests.RequestException as e:
404            self.logger.error(f"衛星画像の取得に失敗しました: {str(e)}")
405            raise

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

Parameters:

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

Returns:

ImageFile
    取得した衛星画像

Raises:

requests.RequestException
    API呼び出しに失敗した場合
def get_satellite_image_from_local( self, local_image_path: str, alpha: float = 1.0) -> PIL.ImageFile.ImageFile:
407    def get_satellite_image_from_local(
408        self,
409        local_image_path: str,
410        alpha: float = 1.0,
411    ) -> ImageFile:
412        """
413        ローカルファイルから衛星画像を読み込みます。
414
415        Parameters:
416        ------
417            local_image_path : str
418                ローカル画像のパス
419            alpha : float, optional
420                画像の透過率(0.0~1.0)。デフォルトは1.0。
421
422        Returns:
423        ------
424            ImageFile
425                読み込んだ衛星画像(透過設定済み)
426
427        Raises:
428        ------
429            FileNotFoundError
430                指定されたパスにファイルが存在しない場合
431        """
432        if not os.path.exists(local_image_path):
433            raise FileNotFoundError(
434                f"指定されたローカル画像が存在しません: {local_image_path}"
435            )
436        # 画像を読み込む
437        image = Image.open(local_image_path)
438
439        # RGBAモードに変換して透過率を設定
440        if image.mode != "RGBA":
441            image = image.convert("RGBA")
442
443        # 透過率を設定
444        data = image.getdata()
445        new_data = [(r, g, b, int(255 * alpha)) for r, g, b, a in data]
446        image.putdata(new_data)
447
448        self._got_satellite_image = True
449        self.logger.info(
450            f"ローカル画像を使用しました(透過率: {alpha}): {local_image_path}"
451        )
452        return image

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

Parameters:

local_image_path : str
    ローカル画像のパス
alpha : float, optional
    画像の透過率(0.0~1.0)。デフォルトは1.0。

Returns:

ImageFile
    読み込んだ衛星画像(透過設定済み)

Raises:

FileNotFoundError
    指定されたパスにファイルが存在しない場合
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:
454    def plot_flux_footprint(
455        self,
456        x_list: list[float],
457        y_list: list[float],
458        c_list: list[float] | None,
459        center_lat: float,
460        center_lon: float,
461        vmin: float,
462        vmax: float,
463        add_cbar: bool = True,
464        add_legend: bool = True,
465        cbar_label: str | None = None,
466        cbar_labelpad: int = 20,
467        cmap: str = "jet",
468        reduce_c_function: callable = np.mean,
469        lat_correction: float = 1,
470        lon_correction: float = 1,
471        output_dir: str | None = None,
472        output_filename: str = "footprint.png",
473        save_fig: bool = True,
474        show_fig: bool = True,
475        satellite_image: ImageFile | None = None,
476        xy_max: float = 5000,
477    ) -> None:
478        """
479        フットプリントデータをプロットします。
480
481        このメソッドは、指定されたフットプリントデータのみを可視化します。
482
483        Parameters:
484        ------
485            x_list : list[float]
486                フットプリントのx座標リスト(メートル単位)。
487            y_list : list[float]
488                フットプリントのy座標リスト(メートル単位)。
489            c_list : list[float] | None
490                フットプリントの強度を示す値のリスト。
491            center_lat : float
492                プロットの中心となる緯度。
493            center_lon : float
494                プロットの中心となる経度。
495            cmap : str
496                使用するカラーマップの名前。
497            vmin : float
498                カラーバーの最小値。
499            vmax : float
500                カラーバーの最大値。
501            reduce_c_function : callable, optional
502                フットプリントの集約関数(デフォルトはnp.mean)。
503            cbar_label : str | None, optional
504                カラーバーのラベル。
505            cbar_labelpad : int, optional
506                カラーバーラベルのパディング。
507            lon_correction : float, optional
508                経度方向の補正係数(デフォルトは1)。
509            lat_correction : float, optional
510                緯度方向の補正係数(デフォルトは1)。
511            output_dir : str | None, optional
512                プロット画像の保存先パス。
513            output_filename : str
514                プロット画像の保存ファイル名(拡張子を含む)。デフォルトは'footprint.png'。
515            save_fig : bool
516                図の保存を許可するフラグ。デフォルトはTrue。
517            show_fig : bool
518                図の表示を許可するフラグ。デフォルトはTrue。
519            satellite_image : ImageFile | None, optional
520                使用する衛星画像。指定がない場合はデフォルトの画像が生成されます。
521            xy_max : float, optional
522                表示範囲の最大値(デフォルトは4000)。
523        """
524        self.plot_flux_footprint_with_hotspots(
525            x_list=x_list,
526            y_list=y_list,
527            c_list=c_list,
528            center_lat=center_lat,
529            center_lon=center_lon,
530            vmin=vmin,
531            vmax=vmax,
532            add_cbar=add_cbar,
533            add_legend=add_legend,
534            cbar_label=cbar_label,
535            cbar_labelpad=cbar_labelpad,
536            cmap=cmap,
537            reduce_c_function=reduce_c_function,
538            hotspots=None,  # hotspotsをNoneに設定
539            hotspot_colors=None,
540            lat_correction=lat_correction,
541            lon_correction=lon_correction,
542            output_dir=output_dir,
543            output_filename=output_filename,
544            save_fig=save_fig,
545            show_fig=show_fig,
546            satellite_image=satellite_image,
547            xy_max=xy_max,
548        )

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

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

Parameters:

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

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

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

Parameters:

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

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

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

Parameters:

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

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

Parameters:

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

Returns:

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

Raises:

ValueError
    インデックスがDatetimeIndexでない場合、または日付の形式が不正な場合
@staticmethod
def is_weekday(date: datetime.datetime) -> int:
1322    @staticmethod
1323    def is_weekday(date: datetime) -> int:
1324        """
1325        指定された日付が平日であるかどうかを判定します。
1326
1327        Parameters:
1328        ------
1329            date : datetime
1330                判定する日付。
1331
1332        Returns:
1333        ------
1334            int
1335                平日であれば1、そうでなければ0。
1336        """
1337        return 1 if not jpholiday.is_holiday(date) and date.weekday() < 5 else 0

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

Parameters:

date : datetime
    判定する日付。

Returns:

int
    平日であれば1、そうでなければ0。
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)):
105            raise ValueError("Delta C2H6 must be a int or float")
106
107        # ratioのバリデーション
108        if not isinstance(self.ratio, (int, float)) or self.ratio < 0:
109            raise ValueError("Ratio must be a non-negative number")
110
111        # emission_rateのバリデーション
112        if not isinstance(self.emission_rate, (int, float)) or self.emission_rate < 0:
113            raise ValueError("Emission rate must be a non-negative number")
114
115        # daily_emissionのバリデーション
116        expected_daily = self.emission_rate * 60 * 24
117        if not math.isclose(self.daily_emission, expected_daily, rel_tol=1e-10):
118            raise ValueError(
119                f"Daily emission ({self.daily_emission}) does not match "
120                f"calculated value from emission rate ({expected_daily})"
121            )
122
123        # annual_emissionのバリデーション
124        expected_annual = self.daily_emission * 365
125        if not math.isclose(self.annual_emission, expected_annual, rel_tol=1e-10):
126            raise ValueError(
127                f"Annual emission ({self.annual_emission}) does not match "
128                f"calculated value from daily emission ({expected_annual})"
129            )
130
131        # NaN値のチェック
132        numeric_fields = [
133            self.latitude,
134            self.longitude,
135            self.delta_ch4,
136            self.delta_c2h6,
137            self.ratio,
138            self.emission_rate,
139            self.daily_emission,
140            self.annual_emission,
141        ]
142        if any(math.isnan(x) for x in numeric_fields):
143            raise ValueError("Numeric fields cannot contain NaN values")
144
145    def to_dict(self) -> dict:
146        """
147        データクラスの内容を辞書形式に変換します。
148
149        Returns:
150        ------
151            dict: データクラスの属性と値を含む辞書
152        """
153        return {
154            "source": self.source,
155            "type": self.type,
156            "section": self.section,
157            "latitude": self.latitude,
158            "longitude": self.longitude,
159            "delta_ch4": self.delta_ch4,
160            "delta_c2h6": self.delta_c2h6,
161            "ratio": self.ratio,
162            "emission_rate": self.emission_rate,
163            "daily_emission": self.daily_emission,
164            "annual_emission": self.annual_emission,
165        }

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

Parameters:

source : str
    データソース(日時)
type : HotspotType
    ホットスポットの種類(`HotspotType`を参照)
section : str | int | float
    セクション情報
latitude : float
    緯度
longitude : float
    経度
delta_ch4 : float
    CH4の増加量 (ppm)
delta_c2h6 : float
    C2H6の増加量 (ppb)
ratio : float
    C2H6/CH4比
emission_rate : float
    排出量 (L/min)
daily_emission : float
    日排出量 (L/day)
annual_emission : float
    年間排出量 (L/year)
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    @staticmethod
 716    def extract_source_name_from_path(path: str | Path) -> str:
 717        """
 718        ファイルパスからソース名(拡張子なしのファイル名)を抽出します。
 719
 720        Parameters:
 721        ------
 722            path : str | Path
 723                ソース名を抽出するファイルパス
 724                例: "/path/to/Pico100121_241017_092120+.txt"
 725
 726        Returns:
 727        ------
 728            str
 729                抽出されたソース名
 730                例: "Pico100121_241017_092120+"
 731
 732        Examples:
 733        ------
 734            >>> path = "/path/to/data/Pico100121_241017_092120+.txt"
 735            >>> MobileSpatialAnalyzer.extract_source_from_path(path)
 736            'Pico100121_241017_092120+'
 737        """
 738        # Pathオブジェクトに変換
 739        path_obj = Path(path)
 740        # stem属性で拡張子なしのファイル名を取得
 741        source_name = path_obj.stem
 742        return source_name
 743
 744    def get_preprocessed_data(
 745        self,
 746    ) -> pd.DataFrame:
 747        """
 748        データ前処理を行い、CH4とC2H6の相関解析に必要な形式に整えます。
 749        コンストラクタで読み込んだすべてのデータを前処理し、結合したDataFrameを返します。
 750
 751        Returns:
 752        ------
 753            pd.DataFrame
 754                前処理済みの結合されたDataFrame
 755        """
 756        processed_dfs: list[pd.DataFrame] = []
 757
 758        # 各データソースに対して解析を実行
 759        for source_name, df in self._data.items():
 760            # パラメータの計算
 761            processed_df = MobileSpatialAnalyzer._calculate_hotspots_parameters(
 762                df, self._window_size
 763            )
 764            # ソース名を列として追加
 765            processed_df["source"] = source_name
 766            processed_dfs.append(processed_df)
 767
 768        # すべてのDataFrameを結合
 769        if not processed_dfs:
 770            raise ValueError("処理対象のデータが存在しません。")
 771
 772        combined_df = pd.concat(processed_dfs, axis=0)
 773        return combined_df
 774
 775    def get_section_size(self) -> float:
 776        """
 777        セクションのサイズを取得するメソッド。
 778        このメソッドは、解析対象のデータを区画に分割する際の
 779        各区画の角度範囲を示すサイズを返します。
 780
 781        Returns:
 782        ------
 783            float
 784                1セクションのサイズ(度単位)
 785        """
 786        return self._section_size
 787
 788    def get_source_names(self, print_all: bool = False) -> list[str]:
 789        """
 790        データソースの名前を取得します。
 791
 792        Parameters
 793        ----------
 794        print_all : bool, optional
 795            すべてのデータソース名を表示するかどうかを指定します。デフォルトはFalseです。
 796
 797        Returns
 798        -------
 799        list[str]
 800            データソース名のリスト
 801
 802        Raises
 803        ------
 804        ValueError
 805            データが読み込まれていない場合に発生します。
 806        """
 807        dfs_dict: dict[str, pd.DataFrame] = self._data
 808        # データソースの選択
 809        if not dfs_dict:
 810            raise ValueError("データが読み込まれていません。")
 811        source_name_list: list[str] = list(dfs_dict.keys())
 812        if print_all:
 813            print(source_name_list)
 814        return source_name_list
 815
 816    def plot_ch4_delta_histogram(
 817        self,
 818        hotspots: list[HotspotData],
 819        output_dir: str | Path | None,
 820        output_filename: str = "ch4_delta_histogram.png",
 821        dpi: int = 200,
 822        figsize: tuple[int, int] = (8, 6),
 823        fontsize: float = 20,
 824        xlim: tuple[float, float] | None = None,
 825        ylim: tuple[float, float] | None = None,
 826        save_fig: bool = True,
 827        show_fig: bool = True,
 828        yscale_log: bool = True,
 829        print_bins_analysis: bool = False,
 830    ) -> None:
 831        """
 832        CH4の増加量(ΔCH4)の積み上げヒストグラムをプロットします。
 833
 834        Parameters:
 835        ------
 836            hotspots : list[HotspotData]
 837                プロットするホットスポットのリスト
 838            output_dir : str | Path | None
 839                保存先のディレクトリパス
 840            output_filename : str
 841                保存するファイル名。デフォルトは"ch4_delta_histogram.png"。
 842            dpi : int
 843                解像度。デフォルトは200。
 844            figsize : tuple[int, int]
 845                図のサイズ。デフォルトは(8, 6)。
 846            fontsize : float
 847                フォントサイズ。デフォルトは20。
 848            xlim : tuple[float, float] | None
 849                x軸の範囲。Noneの場合は自動設定。
 850            ylim : tuple[float, float] | None
 851                y軸の範囲。Noneの場合は自動設定。
 852            save_fig : bool
 853                図の保存を許可するフラグ。デフォルトはTrue。
 854            show_fig : bool
 855                図の表示を許可するフラグ。デフォルトはTrue。
 856            yscale_log : bool
 857                y軸をlogにするかどうか。デフォルトはTrue。
 858            print_bins_analysis : bool
 859                ビンごとの内訳を表示するオプション。
 860        """
 861        plt.rcParams["font.size"] = fontsize
 862        fig = plt.figure(figsize=figsize, dpi=dpi)
 863
 864        # ホットスポットからデータを抽出
 865        all_ch4_deltas = []
 866        all_types = []
 867        for spot in hotspots:
 868            all_ch4_deltas.append(spot.delta_ch4)
 869            all_types.append(spot.type)
 870
 871        # データをNumPy配列に変換
 872        all_ch4_deltas = np.array(all_ch4_deltas)
 873        all_types = np.array(all_types)
 874
 875        # 0.1刻みのビンを作成
 876        if xlim is not None:
 877            bins = np.arange(xlim[0], xlim[1] + 0.1, 0.1)
 878        else:
 879            max_val = np.ceil(np.max(all_ch4_deltas) * 10) / 10
 880            bins = np.arange(0, max_val + 0.1, 0.1)
 881
 882        # タイプごとのヒストグラムデータを計算
 883        hist_data = {}
 884        # HotspotTypeのリテラル値を使用してイテレーション
 885        for type_name in get_args(HotspotType):  # typing.get_argsをインポート
 886            mask = all_types == type_name
 887            if np.any(mask):
 888                counts, _ = np.histogram(all_ch4_deltas[mask], bins=bins)
 889                hist_data[type_name] = counts
 890
 891        # ビンごとの内訳を表示
 892        if print_bins_analysis:
 893            self.logger.info("各ビンの内訳:")
 894            print(f"{'Bin Range':15} {'bio':>8} {'gas':>8} {'comb':>8} {'Total':>8}")
 895            print("-" * 50)
 896
 897            for i in range(len(bins) - 1):
 898                bin_start = bins[i]
 899                bin_end = bins[i + 1]
 900                bio_count = hist_data.get("bio", np.zeros(len(bins) - 1))[i]
 901                gas_count = hist_data.get("gas", np.zeros(len(bins) - 1))[i]
 902                comb_count = hist_data.get("comb", np.zeros(len(bins) - 1))[i]
 903                total = bio_count + gas_count + comb_count
 904
 905                if total > 0:  # 合計が0のビンは表示しない
 906                    print(
 907                        f"{bin_start:4.1f}-{bin_end:<8.1f}"
 908                        f"{int(bio_count):8d}"
 909                        f"{int(gas_count):8d}"
 910                        f"{int(comb_count):8d}"
 911                        f"{int(total):8d}"
 912                    )
 913
 914        # 積み上げヒストグラムを作成
 915        bottom = np.zeros_like(hist_data.get("bio", np.zeros(len(bins) - 1)))
 916
 917        # 色の定義をHotspotTypeを使用して型安全に定義
 918        colors: dict[HotspotType, str] = {"bio": "blue", "gas": "red", "comb": "green"}
 919
 920        # HotspotTypeのリテラル値を使用してイテレーション
 921        for type_name in get_args(HotspotType):
 922            if type_name in hist_data:
 923                plt.bar(
 924                    bins[:-1],
 925                    hist_data[type_name],
 926                    width=np.diff(bins)[0],
 927                    bottom=bottom,
 928                    color=colors[type_name],
 929                    label=type_name,
 930                    alpha=0.6,
 931                    align="edge",
 932                )
 933                bottom += hist_data[type_name]
 934
 935        if yscale_log:
 936            plt.yscale("log")
 937        plt.xlabel("Δ$\\mathregular{CH_{4}}$ (ppm)")
 938        plt.ylabel("Frequency")
 939        plt.legend()
 940        plt.grid(True, which="both", ls="-", alpha=0.2)
 941
 942        # 軸の範囲を設定
 943        if xlim is not None:
 944            plt.xlim(xlim)
 945        if ylim is not None:
 946            plt.ylim(ylim)
 947
 948        # グラフの保存または表示
 949        if save_fig:
 950            if output_dir is None:
 951                raise ValueError(
 952                    "save_fig=Trueの場合、output_dirを指定する必要があります。有効なディレクトリパスを指定してください。"
 953                )
 954            os.makedirs(output_dir, exist_ok=True)
 955            output_path: str = os.path.join(output_dir, output_filename)
 956            plt.savefig(output_path, bbox_inches="tight")
 957            self.logger.info(f"ヒストグラムを保存しました: {output_path}")
 958        if show_fig:
 959            plt.show()
 960        else:
 961            plt.close(fig=fig)
 962
 963    def plot_mapbox(
 964        self,
 965        df: pd.DataFrame,
 966        col_conc: str,
 967        mapbox_access_token: str,
 968        sort_conc_column: bool = True,
 969        output_dir: str | Path | None = None,
 970        output_filename: str = "mapbox_plot.html",
 971        col_lat: str = "latitude",
 972        col_lon: str = "longitude",
 973        colorscale: str = "Jet",
 974        center_lat: float | None = None,
 975        center_lon: float | None = None,
 976        zoom: float = 12,
 977        width: int = 700,
 978        height: int = 700,
 979        tick_font_family: str = "Arial",
 980        title_font_family: str = "Arial",
 981        tick_font_size: int = 12,
 982        title_font_size: int = 14,
 983        marker_size: int = 4,
 984        colorbar_title: str | None = None,
 985        value_range: tuple[float, float] | None = None,
 986        save_fig: bool = True,
 987        show_fig: bool = True,
 988    ) -> None:
 989        """
 990        Plotlyを使用してMapbox上にデータをプロットします。
 991
 992        Parameters:
 993        ------
 994            df : pd.DataFrame
 995                プロットするデータを含むDataFrame
 996            col_conc : str
 997                カラーマッピングに使用する列名
 998            mapbox_access_token : str
 999                Mapboxのアクセストークン
1000            sort_conc_column : bool
1001                value_columnをソートするか否か。デフォルトはTrue。
1002            output_dir : str | Path | None
1003                出力ディレクトリのパス
1004            output_filename : str
1005                出力ファイル名。デフォルトは"mapbox_plot.html"
1006            col_lat : str
1007                緯度の列名。デフォルトは"latitude"
1008            col_lon : str
1009                経度の列名。デフォルトは"longitude"
1010            colorscale : str
1011                使用するカラースケール。デフォルトは"Jet"
1012            center_lat : float | None
1013                中心緯度。デフォルトはNoneで、self._center_latを使用
1014            center_lon : float | None
1015                中心経度。デフォルトはNoneで、self._center_lonを使用
1016            zoom : float
1017                マップの初期ズームレベル。デフォルトは12
1018            width : int
1019                プロットの幅(ピクセル)。デフォルトは700
1020            height : int
1021                プロットの高さ(ピクセル)。デフォルトは700
1022            tick_font_family : str
1023                カラーバーの目盛りフォントファミリー。デフォルトは"Arial"
1024            title_font_family : str
1025                カラーバーのラベルフォントファミリー。デフォルトは"Arial"
1026            tick_font_size : int
1027                カラーバーの目盛りフォントサイズ。デフォルトは12
1028            title_font_size : int
1029                カラーバーのラベルフォントサイズ。デフォルトは14
1030            marker_size : int
1031                マーカーのサイズ。デフォルトは4
1032            colorbar_title : str | None
1033                カラーバーのラベル
1034            value_range : tuple[float, float] | None
1035                カラーマッピングの範囲。デフォルトはNoneで、データの最小値と最大値を使用
1036            save_fig : bool
1037                図を保存するかどうか。デフォルトはTrue
1038            show_fig : bool
1039                図を表示するかどうか。デフォルトはTrue
1040        """
1041        df_mapping: pd.DataFrame = df.copy().dropna(subset=[col_conc])
1042        if sort_conc_column:
1043            df_mapping = df_mapping.sort_values(col_conc)
1044        # 中心座標の設定
1045        center_lat = center_lat if center_lat is not None else self._center_lat
1046        center_lon = center_lon if center_lon is not None else self._center_lon
1047
1048        # カラーマッピングの範囲を設定
1049        cmin, cmax = 0, 0
1050        if value_range is None:
1051            cmin = df_mapping[col_conc].min()
1052            cmax = df_mapping[col_conc].max()
1053        else:
1054            cmin, cmax = value_range
1055
1056        # カラーバーのタイトルを設定
1057        title_text = colorbar_title if colorbar_title is not None else col_conc
1058
1059        # Scattermapboxのデータを作成
1060        scatter_data = go.Scattermapbox(
1061            lat=df_mapping[col_lat],
1062            lon=df_mapping[col_lon],
1063            text=df_mapping[col_conc].astype(str),
1064            hoverinfo="text",
1065            mode="markers",
1066            marker=dict(
1067                color=df_mapping[col_conc],
1068                size=marker_size,
1069                reversescale=False,
1070                autocolorscale=False,
1071                colorscale=colorscale,
1072                cmin=cmin,
1073                cmax=cmax,
1074                colorbar=dict(
1075                    tickformat="3.2f",
1076                    outlinecolor="black",
1077                    outlinewidth=1.5,
1078                    ticks="outside",
1079                    ticklen=7,
1080                    tickwidth=1.5,
1081                    tickcolor="black",
1082                    tickfont=dict(
1083                        family=tick_font_family, color="black", size=tick_font_size
1084                    ),
1085                    title=dict(
1086                        text=title_text, side="top"
1087                    ),  # カラーバーのタイトルを設定
1088                    titlefont=dict(
1089                        family=title_font_family,
1090                        color="black",
1091                        size=title_font_size,
1092                    ),
1093                ),
1094            ),
1095        )
1096
1097        # レイアウトの設定
1098        layout = go.Layout(
1099            width=width,
1100            height=height,
1101            showlegend=False,
1102            mapbox=dict(
1103                accesstoken=mapbox_access_token,
1104                center=dict(lat=center_lat, lon=center_lon),
1105                zoom=zoom,
1106            ),
1107        )
1108
1109        # 図の作成
1110        fig = go.Figure(data=[scatter_data], layout=layout)
1111
1112        # 図の保存
1113        if save_fig:
1114            # 保存時の出力ディレクトリチェック
1115            if output_dir is None:
1116                raise ValueError(
1117                    "save_fig=Trueの場合、output_dirを指定する必要があります。"
1118                )
1119            os.makedirs(output_dir, exist_ok=True)
1120            output_path = os.path.join(output_dir, output_filename)
1121            pyo.plot(fig, filename=output_path, auto_open=False)
1122            self.logger.info(f"Mapboxプロットを保存しました: {output_path}")
1123        # 図の表示
1124        if show_fig:
1125            pyo.iplot(fig)
1126
1127    def plot_scatter_c2c1(
1128        self,
1129        hotspots: list[HotspotData],
1130        output_dir: str | Path | None = None,
1131        output_filename: str = "scatter_c2c1.png",
1132        dpi: int = 200,
1133        figsize: tuple[int, int] = (4, 4),
1134        fontsize: float = 12,
1135        save_fig: bool = True,
1136        show_fig: bool = True,
1137        ratio_labels: dict[float, tuple[float, float, str]] | None = None,
1138    ) -> None:
1139        """
1140        検出されたホットスポットのΔC2H6とΔCH4の散布図をプロットします。
1141
1142        Parameters:
1143        ------
1144            hotspots : list[HotspotData]
1145                プロットするホットスポットのリスト
1146            output_dir : str | Path | None
1147                保存先のディレクトリパス
1148            output_filename : str
1149                保存するファイル名。デフォルトは"scatter_c2c1.png"。
1150            dpi : int
1151                解像度。デフォルトは200。
1152            figsize : tuple[int, int]
1153                図のサイズ。デフォルトは(4, 4)。
1154            fontsize : float
1155                フォントサイズ。デフォルトは12。
1156            save_fig : bool
1157                図の保存を許可するフラグ。デフォルトはTrue。
1158            show_fig : bool
1159                図の表示を許可するフラグ。デフォルトはTrue。
1160            ratio_labels : dict[float, tuple[float, float, str]] | None
1161                比率線とラベルの設定。
1162                キーは比率値、値は (x位置, y位置, ラベルテキスト) のタプル。
1163                Noneの場合はデフォルト設定を使用。デフォルト値:
1164                {
1165                    0.001: (1.25, 2, "0.001"),
1166                    0.005: (1.25, 8, "0.005"),
1167                    0.010: (1.25, 15, "0.01"),
1168                    0.020: (1.25, 30, "0.02"),
1169                    0.030: (1.0, 40, "0.03"),
1170                    0.076: (0.20, 42, "0.076 (Osaka)")
1171                }
1172        """
1173        plt.rcParams["font.size"] = fontsize
1174        fig = plt.figure(figsize=figsize, dpi=dpi)
1175
1176        # タイプごとのデータを収集
1177        type_data: dict[HotspotType, list[tuple[float, float]]] = {
1178            "bio": [],
1179            "gas": [],
1180            "comb": [],
1181        }
1182        for spot in hotspots:
1183            type_data[spot.type].append((spot.delta_ch4, spot.delta_c2h6))
1184
1185        # 色とラベルの定義
1186        colors: dict[HotspotType, str] = {"bio": "blue", "gas": "red", "comb": "green"}
1187        labels: dict[HotspotType, str] = {"bio": "bio", "gas": "gas", "comb": "comb"}
1188
1189        # タイプごとにプロット(データが存在する場合のみ)
1190        for spot_type, data in type_data.items():
1191            if data:  # データが存在する場合のみプロット
1192                ch4_values, c2h6_values = zip(*data)
1193                plt.plot(
1194                    ch4_values,
1195                    c2h6_values,
1196                    "o",
1197                    c=colors[spot_type],
1198                    alpha=0.5,
1199                    ms=2,
1200                    label=labels[spot_type],
1201                )
1202
1203        # デフォルトの比率とラベル設定
1204        default_ratio_labels = {
1205            0.001: (1.25, 2, "0.001"),
1206            0.005: (1.25, 8, "0.005"),
1207            0.010: (1.25, 15, "0.01"),
1208            0.020: (1.25, 30, "0.02"),
1209            0.030: (1.0, 40, "0.03"),
1210            0.076: (0.20, 42, "0.076 (Osaka)"),
1211        }
1212
1213        ratio_labels = ratio_labels or default_ratio_labels
1214
1215        # プロット後、軸の設定前に比率の線を追加
1216        x = np.array([0, 5])
1217        base_ch4 = 0.0
1218        base = 0.0
1219
1220        # 各比率に対して線を引く
1221        for ratio, (x_pos, y_pos, label) in ratio_labels.items():
1222            y = (x - base_ch4) * 1000 * ratio + base
1223            plt.plot(x, y, "-", c="black", alpha=0.5)
1224            plt.text(x_pos, y_pos, label)
1225
1226        plt.ylim(0, 50)
1227        plt.xlim(0, 2.0)
1228        plt.ylabel("Δ$\\mathregular{C_{2}H_{6}}$ (ppb)")
1229        plt.xlabel("Δ$\\mathregular{CH_{4}}$ (ppm)")
1230        plt.legend()
1231
1232        # グラフの保存または表示
1233        if save_fig:
1234            if output_dir is None:
1235                raise ValueError(
1236                    "save_fig=Trueの場合、output_dirを指定する必要があります。有効なディレクトリパスを指定してください。"
1237                )
1238            output_path: str = os.path.join(output_dir, output_filename)
1239            plt.savefig(output_path, bbox_inches="tight")
1240            self.logger.info(f"散布図を保存しました: {output_path}")
1241        if show_fig:
1242            plt.show()
1243        else:
1244            plt.close(fig=fig)
1245
1246    def plot_conc_timeseries(
1247        self,
1248        source_name: str | None = None,
1249        output_dir: str | Path | None = None,
1250        output_filename: str = "timeseries.png",
1251        dpi: int = 200,
1252        figsize: tuple[float, float] = (8, 4),
1253        save_fig: bool = True,
1254        show_fig: bool = True,
1255        col_ch4: str = "ch4_ppm",
1256        col_c2h6: str = "c2h6_ppb",
1257        col_h2o: str = "h2o_ppm",
1258        ylim_ch4: tuple[float, float] | None = None,
1259        ylim_c2h6: tuple[float, float] | None = None,
1260        ylim_h2o: tuple[float, float] | None = None,
1261        font_size: float = 12,
1262        label_pad: float = 10,
1263    ) -> None:
1264        """
1265        時系列データをプロットします。
1266
1267        Parameters:
1268        ------
1269            dpi : int
1270                図の解像度を指定します。デフォルトは200です。
1271            source_name : str | None
1272                プロットするデータソースの名前。Noneの場合は最初のデータソースを使用します。
1273            figsize : tuple[float, float]
1274                図のサイズを指定します。デフォルトは(8, 4)です。
1275            output_dir : str | Path | None
1276                保存先のディレクトリを指定します。save_fig=Trueの場合は必須です。
1277            output_filename : str
1278                保存するファイル名を指定します。デフォルトは"time_series.png"です。
1279            save_fig : bool
1280                図を保存するかどうかを指定します。デフォルトはFalseです。
1281            show_fig : bool
1282                図を表示するかどうかを指定します。デフォルトはTrueです。
1283            col_ch4 : str
1284                CH4データのキーを指定します。デフォルトは"ch4_ppm"です。
1285            col_c2h6 : str
1286                C2H6データのキーを指定します。デフォルトは"c2h6_ppb"です。
1287            col_h2o : str
1288                H2Oデータのキーを指定します。デフォルトは"h2o_ppm"です。
1289            ylim_ch4 : tuple[float, float] | None
1290                CH4プロットのy軸範囲を指定します。デフォルトはNoneです。
1291            ylim_c2h6 : tuple[float, float] | None
1292                C2H6プロットのy軸範囲を指定します。デフォルトはNoneです。
1293            ylim_h2o : tuple[float, float] | None
1294                H2Oプロットのy軸範囲を指定します。デフォルトはNoneです。
1295            font_size : float
1296                基本フォントサイズ。デフォルトは12。
1297            label_pad : float
1298                y軸ラベルのパディング。デフォルトは10。
1299        """
1300        # プロットパラメータの設定
1301        plt.rcParams.update(
1302            {
1303                "font.size": font_size,
1304                "axes.labelsize": font_size,
1305                "axes.titlesize": font_size,
1306                "xtick.labelsize": font_size,
1307                "ytick.labelsize": font_size,
1308            }
1309        )
1310        dfs_dict: dict[str, pd.DataFrame] = self._data.copy()
1311        # データソースの選択
1312        if not dfs_dict:
1313            raise ValueError("データが読み込まれていません。")
1314
1315        if source_name not in dfs_dict:
1316            raise ValueError(
1317                f"指定されたデータソース '{source_name}' が見つかりません。"
1318            )
1319
1320        df = dfs_dict[source_name]
1321
1322        # プロットの作成
1323        fig = plt.figure(figsize=figsize, dpi=dpi)
1324
1325        # CH4プロット
1326        ax1 = fig.add_subplot(3, 1, 1)
1327        ax1.plot(df.index, df[col_ch4], c="red")
1328        if ylim_ch4:
1329            ax1.set_ylim(ylim_ch4)
1330        ax1.set_ylabel("$\\mathregular{CH_{4}}$ (ppm)", labelpad=label_pad)
1331        ax1.grid(True, alpha=0.3)
1332
1333        # C2H6プロット
1334        ax2 = fig.add_subplot(3, 1, 2)
1335        ax2.plot(df.index, df[col_c2h6], c="red")
1336        if ylim_c2h6:
1337            ax2.set_ylim(ylim_c2h6)
1338        ax2.set_ylabel("$\\mathregular{C_{2}H_{6}}$ (ppb)", labelpad=label_pad)
1339        ax2.grid(True, alpha=0.3)
1340
1341        # H2Oプロット
1342        ax3 = fig.add_subplot(3, 1, 3)
1343        ax3.plot(df.index, df[col_h2o], c="red")
1344        if ylim_h2o:
1345            ax3.set_ylim(ylim_h2o)
1346        ax3.set_ylabel("$\\mathregular{H_{2}O}$ (ppm)", labelpad=label_pad)
1347        ax3.grid(True, alpha=0.3)
1348
1349        # x軸のフォーマット調整
1350        for ax in [ax1, ax2, ax3]:
1351            ax.xaxis.set_major_formatter(mdates.DateFormatter("%H:%M"))
1352            # 軸のラベルとグリッド線の調整
1353            ax.tick_params(axis="both", which="major", labelsize=font_size)
1354            ax.grid(True, alpha=0.3)
1355
1356        # サブプロット間の間隔調整
1357        plt.subplots_adjust(wspace=0.38, hspace=0.38)
1358
1359        # 図の保存
1360        if save_fig:
1361            if output_dir is None:
1362                raise ValueError(
1363                    "save_fig=Trueの場合、output_dirを指定する必要があります。有効なディレクトリパスを指定してください。"
1364                )
1365            os.makedirs(output_dir, exist_ok=True)
1366            output_path = os.path.join(output_dir, output_filename)
1367            plt.savefig(output_path, bbox_inches="tight")
1368
1369        if show_fig:
1370            plt.show()
1371        else:
1372            plt.close(fig=fig)
1373
1374    def _detect_hotspots(
1375        self,
1376        df: pd.DataFrame,
1377        ch4_enhance_threshold: float,
1378    ) -> list[HotspotData]:
1379        """
1380        シンプル化したホットスポット検出
1381
1382        Parameters:
1383        ------
1384            df : pd.DataFrame
1385                入力データフレーム
1386            ch4_enhance_threshold : float
1387                CH4増加の閾値
1388
1389        Returns:
1390        ------
1391            list[HotspotData]
1392                検出されたホットスポットのリスト
1393        """
1394        hotspots: list[HotspotData] = []
1395
1396        # CH4増加量が閾値を超えるデータポイントを抽出
1397        enhanced_mask = df["ch4_ppm_delta"] >= ch4_enhance_threshold
1398
1399        if enhanced_mask.any():
1400            lat = df["latitude"][enhanced_mask]
1401            lon = df["longitude"][enhanced_mask]
1402            ratios = df["c2c1_ratio_delta"][enhanced_mask]
1403            delta_ch4 = df["ch4_ppm_delta"][enhanced_mask]
1404            delta_c2h6 = df["c2h6_ppb_delta"][enhanced_mask]
1405
1406            # 各ポイントに対してホットスポットを作成
1407            for i in range(len(lat)):
1408                if pd.notna(ratios.iloc[i]):
1409                    current_lat = lat.iloc[i]
1410                    current_lon = lon.iloc[i]
1411                    correlation = df["ch4_c2h6_correlation"].iloc[i]
1412
1413                    # 比率に基づいてタイプを決定
1414                    spot_type: HotspotType = "bio"
1415                    if ratios.iloc[i] >= 100:
1416                        spot_type = "comb"
1417                    elif ratios.iloc[i] >= 5:
1418                        spot_type = "gas"
1419
1420                    angle: float = MobileSpatialAnalyzer._calculate_angle(
1421                        lat=current_lat,
1422                        lon=current_lon,
1423                        center_lat=self._center_lat,
1424                        center_lon=self._center_lon,
1425                    )
1426                    section: int = self._determine_section(angle)
1427
1428                    hotspots.append(
1429                        HotspotData(
1430                            source=ratios.index[i].strftime("%Y-%m-%d %H:%M:%S"),
1431                            angle=angle,
1432                            avg_lat=current_lat,
1433                            avg_lon=current_lon,
1434                            delta_ch4=delta_ch4.iloc[i],
1435                            delta_c2h6=delta_c2h6.iloc[i],
1436                            correlation=max(-1, min(1, correlation)),
1437                            ratio=ratios.iloc[i],
1438                            section=section,
1439                            type=spot_type,
1440                        )
1441                    )
1442
1443        return hotspots
1444
1445    def _determine_section(self, angle: float) -> int:
1446        """
1447        角度に基づいて所属する区画を特定します。
1448
1449        Parameters:
1450        ------
1451            angle : float
1452                計算された角度
1453
1454        Returns:
1455        ------
1456            int
1457                区画番号(0-based-index)
1458        """
1459        for section_num, (start, end) in self._sections.items():
1460            if start <= angle < end:
1461                return section_num
1462        # -180度の場合は最後の区画に含める
1463        return self._num_sections - 1
1464
1465    def _load_all_data(
1466        self, input_configs: list[MSAInputConfig]
1467    ) -> dict[str, pd.DataFrame]:
1468        """
1469        全入力ファイルのデータを読み込み、データフレームの辞書を返します。
1470
1471        このメソッドは、指定された入力設定に基づいてすべてのデータファイルを読み込み、
1472        各ファイルのデータをデータフレームとして格納した辞書を生成します。
1473
1474        Parameters:
1475        ------
1476            input_configs : list[MSAInputConfig]
1477                読み込むファイルの設定リスト。
1478
1479        Returns:
1480        ------
1481            dict[str, pd.DataFrame]
1482                読み込まれたデータフレームの辞書。キーはファイル名、値はデータフレーム。
1483        """
1484        all_data: dict[str, pd.DataFrame] = {}
1485        for config in input_configs:
1486            df, source_name = self._load_data(config)
1487            all_data[source_name] = df
1488        return all_data
1489
1490    def _load_data(
1491        self,
1492        config: MSAInputConfig,
1493        columns_to_shift: list[str] = ["ch4_ppm", "c2h6_ppb", "h2o_ppm"],
1494    ) -> tuple[pd.DataFrame, str]:
1495        """
1496        測定データを読み込み、前処理を行うメソッド。
1497
1498        Parameters:
1499        ------
1500            config : MSAInputConfig
1501                入力ファイルの設定を含むオブジェクト。ファイルパス、遅れ時間、サンプリング周波数、補正タイプなどの情報を持つ。
1502            columns_to_shift : list[str], optional
1503                シフトを適用するカラム名のリスト。デフォルトは["ch4_ppm", "c2h6_ppb", "h2o_ppm"]で、これらのカラムに対して遅れ時間の補正が行われる。
1504
1505        Returns:
1506        ------
1507            tuple[pd.DataFrame, str]
1508                読み込まれたデータフレームとそのソース名を含むタプル。データフレームは前処理が施されており、ソース名はファイル名から抽出されたもの。
1509        """
1510        source_name: str = MobileSpatialAnalyzer.extract_source_name_from_path(
1511            config.path
1512        )
1513        df: pd.DataFrame = pd.read_csv(config.path, na_values=["No Data", "nan"])
1514
1515        # カラム名の標準化(測器に依存しない汎用的な名前に変更)
1516        df = df.rename(columns=self._column_mapping)
1517        df["timestamp"] = pd.to_datetime(df["timestamp"])
1518        # インデックスを設定(元のtimestampカラムは保持)
1519        df = df.set_index("timestamp", drop=False)
1520
1521        # 緯度経度のnanを削除
1522        df = df.dropna(subset=["latitude", "longitude"])
1523
1524        if config.lag < 0:
1525            raise ValueError(
1526                f"Invalid lag value: {config.lag}. Must be a non-negative float."
1527            )
1528
1529        # サンプリング周波数に応じてシフト量を調整
1530        shift_periods: float = -config.lag * config.fs  # fsを掛けて補正
1531
1532        # 遅れ時間の補正
1533        for col in columns_to_shift:
1534            df[col] = df[col].shift(shift_periods)
1535
1536        df = df.dropna(subset=columns_to_shift)
1537
1538        # 水蒸気干渉などの補正式を適用
1539        if config.correction_type is not None:
1540            df = CorrectingUtils.correct_df_by_type(df, config.correction_type)
1541        else:
1542            self.logger.warn(
1543                f"'correction_type' is None, so no correction functions will be applied. Source: {source_name}"
1544            )
1545
1546        return df, source_name
1547
1548    @staticmethod
1549    def _calculate_angle(
1550        lat: float, lon: float, center_lat: float, center_lon: float
1551    ) -> float:
1552        """
1553        中心からの角度を計算
1554
1555        Parameters:
1556        ------
1557            lat : float
1558                対象地点の緯度
1559            lon : float
1560                対象地点の経度
1561            center_lat : float
1562                中心の緯度
1563            center_lon : float
1564                中心の経度
1565
1566        Returns:
1567        ------
1568            float
1569                真北を0°として時計回りの角度(-180°から180°)
1570        """
1571        d_lat: float = lat - center_lat
1572        d_lon: float = lon - center_lon
1573        # arctanを使用して角度を計算(ラジアン)
1574        angle_rad: float = math.atan2(d_lon, d_lat)
1575        # ラジアンから度に変換(-180から180の範囲)
1576        angle_deg: float = math.degrees(angle_rad)
1577        return angle_deg
1578
1579    @classmethod
1580    def _calculate_distance(
1581        cls, lat1: float, lon1: float, lat2: float, lon2: float
1582    ) -> float:
1583        """
1584        2点間の距離をメートル単位で計算(Haversine formula)
1585
1586        Parameters:
1587        ------
1588            lat1 : float
1589                地点1の緯度
1590            lon1 : float
1591                地点1の経度
1592            lat2 : float
1593                地点2の緯度
1594            lon2 : float
1595                地点2の経度
1596
1597        Returns:
1598        ------
1599            float
1600                2地点間の距離(メートル)
1601        """
1602        R = cls.EARTH_RADIUS_METERS
1603
1604        # 緯度経度をラジアンに変換
1605        lat1_rad: float = math.radians(lat1)
1606        lon1_rad: float = math.radians(lon1)
1607        lat2_rad: float = math.radians(lat2)
1608        lon2_rad: float = math.radians(lon2)
1609
1610        # 緯度と経度の差分
1611        dlat: float = lat2_rad - lat1_rad
1612        dlon: float = lon2_rad - lon1_rad
1613
1614        # Haversine formula
1615        a: float = (
1616            math.sin(dlat / 2) ** 2
1617            + math.cos(lat1_rad) * math.cos(lat2_rad) * math.sin(dlon / 2) ** 2
1618        )
1619        c: float = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
1620
1621        return R * c  # メートル単位での距離
1622
1623    @staticmethod
1624    def _calculate_hotspots_parameters(
1625        df: pd.DataFrame,
1626        window_size: int,
1627        col_ch4_ppm: str = "ch4_ppm",
1628        col_c2h6_ppb: str = "c2h6_ppb",
1629        col_h2o_ppm: str = "h2o_ppm",
1630        ch4_threshold: float = 0.05,
1631        c2h6_threshold: float = 0.0,
1632        use_quantile: bool = True,
1633    ) -> pd.DataFrame:
1634        """
1635        ホットスポットのパラメータを計算します。
1636        このメソッドは、指定されたデータフレームに対して移動平均(または5パーセンタイル)や相関を計算し、
1637        各種のデルタ値や比率を追加します。
1638
1639        Parameters:
1640        ------
1641            df : pd.DataFrame
1642                入力データフレーム
1643            window_size : int
1644                移動窓のサイズ
1645            col_ch4_ppm : str
1646                CH4濃度を示すカラム名
1647            col_c2h6_ppb : str
1648                C2H6濃度を示すカラム名
1649            col_h2o_ppm : str
1650                H2O濃度を示すカラム名
1651            ch4_threshold : float
1652                CH4の閾値
1653            c2h6_threshold : float
1654                C2H6の閾値
1655            use_quantile : bool
1656                Trueの場合は5パーセンタイルを使用、Falseの場合は移動平均を使用
1657
1658        Returns:
1659        ------
1660            pd.DataFrame
1661                計算されたパラメータを含むデータフレーム
1662        """
1663        # データのコピーを作成
1664        df = df.copy()
1665
1666        # 移動相関の計算
1667        df["ch4_c2h6_correlation"] = (
1668            df[col_ch4_ppm].rolling(window=window_size).corr(df[col_c2h6_ppb])
1669        )
1670
1671        # バックグラウンド値の計算(5パーセンタイルまたは移動平均)
1672        if use_quantile:
1673            df["ch4_ppm_mv"] = (
1674                df[col_ch4_ppm]
1675                .rolling(window=window_size, center=True, min_periods=1)
1676                .quantile(0.05)
1677            )
1678            df["c2h6_ppb_mv"] = (
1679                df[col_c2h6_ppb]
1680                .rolling(window=window_size, center=True, min_periods=1)
1681                .quantile(0.05)
1682            )
1683        else:
1684            df["ch4_ppm_mv"] = (
1685                df[col_ch4_ppm]
1686                .rolling(window=window_size, center=True, min_periods=1)
1687                .mean()
1688            )
1689            df["c2h6_ppb_mv"] = (
1690                df[col_c2h6_ppb]
1691                .rolling(window=window_size, center=True, min_periods=1)
1692                .mean()
1693            )
1694
1695        # デルタ値の計算
1696        df["ch4_ppm_delta"] = df[col_ch4_ppm] - df["ch4_ppm_mv"]
1697        df["c2h6_ppb_delta"] = df[col_c2h6_ppb] - df["c2h6_ppb_mv"]
1698
1699        # C2H6/CH4の比率計算
1700        df["c2c1_ratio"] = df[col_c2h6_ppb] / df[col_ch4_ppm]
1701
1702        # デルタ値に基づく比の計算とフィルタリング
1703        df["c2c1_ratio_delta"] = df["c2h6_ppb_delta"] / df["ch4_ppm_delta"]
1704
1705        # フィルタリング条件の適用
1706        df.loc[df["ch4_ppm_delta"] < ch4_threshold, "c2c1_ratio_delta"] = np.nan
1707        df.loc[df["c2h6_ppb_delta"] < -10.0, "c2h6_ppb_delta"] = np.nan
1708        df.loc[df["c2h6_ppb_delta"] > 1000.0, "c2h6_ppb_delta"] = np.nan
1709        df.loc[df["c2h6_ppb_delta"] < c2h6_threshold, "c2c1_ratio_delta"] = 0.0
1710
1711        # 水蒸気濃度によるフィルタリング
1712        df.loc[df[col_h2o_ppm] < 2000, [col_ch4_ppm, col_c2h6_ppb]] = np.nan
1713
1714        # 欠損値の除去
1715        df = df.dropna(subset=[col_ch4_ppm])
1716
1717        return df
1718
1719    @staticmethod
1720    def _calculate_window_size(window_minutes: float) -> int:
1721        """
1722        時間窓からデータポイント数を計算
1723
1724        Parameters:
1725        ------
1726            window_minutes : float
1727                時間窓の大きさ(分)
1728
1729        Returns:
1730        ------
1731            int
1732                データポイント数
1733        """
1734        return int(60 * window_minutes)
1735
1736    @staticmethod
1737    def _initialize_sections(
1738        num_sections: int, section_size: float
1739    ) -> dict[int, tuple[float, float]]:
1740        """
1741        指定された区画数と区画サイズに基づいて、区画の範囲を初期化します。
1742
1743        Parameters:
1744        ------
1745            num_sections : int
1746                初期化する区画の数。
1747            section_size : float
1748                各区画の角度範囲のサイズ。
1749
1750        Returns:
1751        ------
1752            dict[int, tuple[float, float]]
1753                区画番号(0-based-index)とその範囲の辞書。各区画は-180度から180度の範囲に分割されます。
1754        """
1755        sections: dict[int, tuple[float, float]] = {}
1756        for i in range(num_sections):
1757            # -180から180の範囲で区画を設定
1758            start_angle = -180 + i * section_size
1759            end_angle = -180 + (i + 1) * section_size
1760            sections[i] = (start_angle, end_angle)
1761        return sections
1762
1763    @staticmethod
1764    def _is_duplicate_spot(
1765        current_lat: float,
1766        current_lon: float,
1767        current_time: str,
1768        used_positions: list[tuple[float, float, str, float]],
1769        check_time_all: bool,
1770        min_time_threshold_seconds: float,
1771        max_time_threshold_hours: float,
1772        hotspot_area_meter: float,
1773    ) -> bool:
1774        """
1775        与えられた地点が既存の地点と重複しているかを判定します。
1776
1777        Parameters:
1778        ------
1779            current_lat : float
1780                判定する地点の緯度
1781            current_lon : float
1782                判定する地点の経度
1783            current_time : str
1784                判定する地点の時刻
1785            used_positions : list[tuple[float, float, str, float]]
1786                既存の地点情報のリスト (lat, lon, time, value)
1787            check_time_all : bool
1788                時間に関係なく重複チェックを行うかどうか
1789            min_time_threshold_seconds : float
1790                重複とみなす最小時間の閾値(秒)
1791            max_time_threshold_hours : float
1792                重複チェックを一時的に無視する最大時間の閾値(時間)
1793            hotspot_area_meter : float
1794                重複とみなす距離の閾値(m)
1795
1796        Returns:
1797        ------
1798            bool
1799                重複している場合はTrue、そうでない場合はFalse
1800        """
1801        for used_lat, used_lon, used_time, _ in used_positions:
1802            # 距離チェック
1803            distance = MobileSpatialAnalyzer._calculate_distance(
1804                lat1=current_lat, lon1=current_lon, lat2=used_lat, lon2=used_lon
1805            )
1806
1807            if distance < hotspot_area_meter:
1808                # 時間差の計算(秒単位)
1809                time_diff = pd.Timedelta(
1810                    pd.to_datetime(current_time) - pd.to_datetime(used_time)
1811                ).total_seconds()
1812                time_diff_abs = abs(time_diff)
1813
1814                if check_time_all:
1815                    # 時間に関係なく、距離が近ければ重複とみなす
1816                    return True
1817                else:
1818                    # 時間窓による判定を行う
1819                    if time_diff_abs <= min_time_threshold_seconds:
1820                        # Case 1: 最小時間閾値以内は重複とみなす
1821                        return True
1822                    elif time_diff_abs > max_time_threshold_hours * 3600:
1823                        # Case 2: 最大時間閾値を超えた場合は重複チェックをスキップ
1824                        continue
1825                    # Case 3: その間の時間差の場合は、距離が近ければ重複とみなす
1826                    return True
1827
1828        return False
1829
1830    @staticmethod
1831    def _normalize_inputs(
1832        inputs: list[MSAInputConfig] | list[tuple[float, float, str | Path]],
1833    ) -> list[MSAInputConfig]:
1834        """
1835        入力設定を標準化
1836
1837        Parameters:
1838        ------
1839            inputs : list[MSAInputConfig] | list[tuple[float, float, str | Path]]
1840                入力設定のリスト
1841
1842        Returns:
1843        ------
1844            list[MSAInputConfig]
1845                標準化された入力設定のリスト
1846        """
1847        normalized: list[MSAInputConfig] = []
1848        for inp in inputs:
1849            if isinstance(inp, MSAInputConfig):
1850                normalized.append(inp)  # すでに検証済みのため、そのまま追加
1851            else:
1852                fs, lag, path = inp
1853                normalized.append(
1854                    MSAInputConfig.validate_and_create(fs=fs, lag=lag, path=path)
1855                )
1856        return normalized
1857
1858    def remove_c2c1_ratio_duplicates(
1859        self,
1860        df: pd.DataFrame,
1861        min_time_threshold_seconds: float = 300,  # 5分以内は重複とみなす
1862        max_time_threshold_hours: float = 12.0,  # 12時間以上離れている場合は別のポイントとして扱う
1863        check_time_all: bool = True,  # 時間閾値を超えた場合の重複チェックを継続するかどうか
1864        hotspot_area_meter: float = 50.0,  # 重複とみなす距離の閾値(メートル)
1865        col_ch4_ppm: str = "ch4_ppm",
1866        col_ch4_ppm_mv: str = "ch4_ppm_mv",
1867        col_ch4_ppm_delta: str = "ch4_ppm_delta",
1868    ):
1869        """
1870        メタン濃度の増加が閾値を超えた地点から、重複を除外してユニークなホットスポットを抽出する関数。
1871
1872        Parameters:
1873        ------
1874            df : pandas.DataFrame
1875                入力データフレーム。必須カラム:
1876                - ch4_ppm: メタン濃度(ppm)
1877                - ch4_ppm_mv: メタン濃度の移動平均(ppm)
1878                - ch4_ppm_delta: メタン濃度の増加量(ppm)
1879                - latitude: 緯度
1880                - longitude: 経度
1881            min_time_threshold_seconds : float, optional
1882                重複とみなす最小時間差(秒)。デフォルトは300秒(5分)。
1883            max_time_threshold_hours : float, optional
1884                別ポイントとして扱う最大時間差(時間)。デフォルトは12時間。
1885            check_time_all : bool, optional
1886                時間閾値を超えた場合の重複チェックを継続するかどうか。デフォルトはTrue。
1887            hotspot_area_meter : float, optional
1888                重複とみなす距離の閾値(メートル)。デフォルトは50メートル。
1889
1890        Returns:
1891        ------
1892            pandas.DataFrame
1893                ユニークなホットスポットのデータフレーム。
1894        """
1895        df_data: pd.DataFrame = df.copy()
1896        # メタン濃度の増加が閾値を超えた点を抽出
1897        mask = (
1898            df_data[col_ch4_ppm] - df_data[col_ch4_ppm_mv] > self._ch4_enhance_threshold
1899        )
1900        hotspot_candidates = df_data[mask].copy()
1901
1902        # ΔCH4の降順でソート
1903        sorted_hotspots = hotspot_candidates.sort_values(
1904            by=col_ch4_ppm_delta, ascending=False
1905        )
1906        used_positions = []
1907        unique_hotspots = pd.DataFrame()
1908
1909        for _, spot in sorted_hotspots.iterrows():
1910            should_add = True
1911            for used_lat, used_lon, used_time in used_positions:
1912                # 距離チェック
1913                distance = geodesic(
1914                    (spot.latitude, spot.longitude), (used_lat, used_lon)
1915                ).meters
1916
1917                if distance < hotspot_area_meter:
1918                    # 時間差の計算(秒単位)
1919                    time_diff = pd.Timedelta(
1920                        spot.name - pd.to_datetime(used_time)
1921                    ).total_seconds()
1922                    time_diff_abs = abs(time_diff)
1923
1924                    # 時間差に基づく判定
1925                    if check_time_all:
1926                        # 時間に関係なく、距離が近ければ重複とみなす
1927                        # ΔCH4が大きい方を残す(現在のスポットは必ず小さい)
1928                        should_add = False
1929                        break
1930                    else:
1931                        # 時間窓による判定を行う
1932                        if time_diff_abs <= min_time_threshold_seconds:
1933                            # Case 1: 最小時間閾値以内は重複とみなす
1934                            should_add = False
1935                            break
1936                        elif time_diff_abs > max_time_threshold_hours * 3600:
1937                            # Case 2: 最大時間閾値を超えた場合は重複チェックをスキップ
1938                            continue
1939                        # Case 3: その間の時間差の場合は、距離が近ければ重複とみなす
1940                        should_add = False
1941                        break
1942
1943            if should_add:
1944                unique_hotspots = pd.concat([unique_hotspots, pd.DataFrame([spot])])
1945                used_positions.append((spot.latitude, spot.longitude, spot.name))
1946
1947        return unique_hotspots
1948
1949    @staticmethod
1950    def remove_hotspots_duplicates(
1951        hotspots: list[HotspotData],
1952        check_time_all: bool,
1953        min_time_threshold_seconds: float = 300,
1954        max_time_threshold_hours: float = 12,
1955        hotspot_area_meter: float = 50,
1956    ) -> list[HotspotData]:
1957        """
1958        重複するホットスポットを除外します。
1959
1960        このメソッドは、与えられたホットスポットのリストから重複を検出し、
1961        一意のホットスポットのみを返します。重複の判定は、指定された
1962        時間および距離の閾値に基づいて行われます。
1963
1964        Parameters:
1965        ------
1966            hotspots : list[HotspotData]
1967                重複を除外する対象のホットスポットのリスト。
1968            check_time_all : bool
1969                時間に関係なく重複チェックを行うかどうか。
1970            min_time_threshold_seconds : float
1971                重複とみなす最小時間の閾値(秒)。
1972            max_time_threshold_hours : float
1973                重複チェックを一時的に無視する最大時間の閾値(時間)。
1974            hotspot_area_meter : float
1975                重複とみなす距離の閾値(メートル)。
1976
1977        Returns:
1978        ------
1979            list[HotspotData]
1980                重複を除去したホットスポットのリスト。
1981        """
1982        # ΔCH4の降順でソート
1983        sorted_hotspots: list[HotspotData] = sorted(
1984            hotspots, key=lambda x: x.delta_ch4, reverse=True
1985        )
1986        used_positions_by_type: dict[
1987            HotspotType, list[tuple[float, float, str, float]]
1988        ] = {
1989            "bio": [],
1990            "gas": [],
1991            "comb": [],
1992        }
1993        unique_hotspots: list[HotspotData] = []
1994
1995        for spot in sorted_hotspots:
1996            is_duplicate = MobileSpatialAnalyzer._is_duplicate_spot(
1997                current_lat=spot.avg_lat,
1998                current_lon=spot.avg_lon,
1999                current_time=spot.source,
2000                used_positions=used_positions_by_type[spot.type],
2001                check_time_all=check_time_all,
2002                min_time_threshold_seconds=min_time_threshold_seconds,
2003                max_time_threshold_hours=max_time_threshold_hours,
2004                hotspot_area_meter=hotspot_area_meter,
2005            )
2006
2007            if not is_duplicate:
2008                unique_hotspots.append(spot)
2009                used_positions_by_type[spot.type].append(
2010                    (spot.avg_lat, spot.avg_lon, spot.source, spot.delta_ch4)
2011                )
2012
2013        return unique_hotspots
2014
2015    @staticmethod
2016    def setup_logger(logger: Logger | None, log_level: int = INFO) -> Logger:
2017        """
2018        ロガーを設定します。
2019
2020        このメソッドは、ロギングの設定を行い、ログメッセージのフォーマットを指定します。
2021        ログメッセージには、日付、ログレベル、メッセージが含まれます。
2022
2023        渡されたロガーがNoneまたは不正な場合は、新たにロガーを作成し、標準出力に
2024        ログメッセージが表示されるようにStreamHandlerを追加します。ロガーのレベルは
2025        引数で指定されたlog_levelに基づいて設定されます。
2026
2027        Parameters:
2028        ------
2029            logger : Logger | None
2030                使用するロガー。Noneの場合は新しいロガーを作成します。
2031            log_level : int
2032                ロガーのログレベル。デフォルトはINFO。
2033
2034        Returns:
2035        ------
2036            Logger
2037                設定されたロガーオブジェクト。
2038        """
2039        if logger is not None and isinstance(logger, Logger):
2040            return logger
2041        # 渡されたロガーがNoneまたは正しいものでない場合は独自に設定
2042        new_logger: Logger = getLogger()
2043        # 既存のハンドラーをすべて削除
2044        for handler in new_logger.handlers[:]:
2045            new_logger.removeHandler(handler)
2046        new_logger.setLevel(log_level)  # ロガーのレベルを設定
2047        ch = StreamHandler()
2048        ch_formatter = Formatter("%(asctime)s - %(levelname)s - %(message)s")
2049        ch.setFormatter(ch_formatter)  # フォーマッターをハンドラーに設定
2050        new_logger.addHandler(ch)  # StreamHandlerの追加
2051        return new_logger
2052
2053    @staticmethod
2054    def calculate_emission_rates(
2055        hotspots: list[HotspotData],
2056        method: Literal["weller", "weitzel", "joo", "umezawa"] = "weller",
2057        print_summary: bool = True,
2058        custom_formulas: dict[str, dict[str, float]] | None = None,
2059    ) -> tuple[list[EmissionData], dict[str, dict[str, float]]]:
2060        """
2061        検出されたホットスポットのCH4漏出量を計算・解析し、統計情報を生成します。
2062
2063        Parameters:
2064        ------
2065            hotspots : list[HotspotData]
2066                分析対象のホットスポットのリスト
2067            method : Literal["weller", "weitzel", "joo", "umezawa"]
2068                使用する計算式。デフォルトは"weller"。
2069            print_summary : bool
2070                統計情報を表示するかどうか。デフォルトはTrue。
2071            custom_formulas : dict[str, dict[str, float]] | None
2072                カスタム計算式の係数。
2073                例: {"custom_method": {"a": 1.0, "b": 1.0}}
2074                Noneの場合はデフォルトの計算式を使用。
2075
2076        Returns:
2077        ------
2078            tuple[list[EmissionData], dict[str, dict[str, float]]]
2079                - 各ホットスポットの排出量データを含むリスト
2080                - タイプ別の統計情報を含む辞書
2081        """
2082        # デフォルトの経験式係数
2083        default_formulas = {
2084            "weller": {"a": 0.988, "b": 0.817},
2085            "weitzel": {"a": 0.521, "b": 0.795},
2086            "joo": {"a": 2.738, "b": 1.329},
2087            "umezawa": {"a": 2.716, "b": 0.741},
2088        }
2089
2090        # カスタム計算式がある場合は追加
2091        emission_formulas = default_formulas.copy()
2092        if custom_formulas:
2093            emission_formulas.update(custom_formulas)
2094
2095        if method not in emission_formulas:
2096            raise ValueError(f"Unknown method: {method}")
2097
2098        # 係数の取得
2099        a = emission_formulas[method]["a"]
2100        b = emission_formulas[method]["b"]
2101
2102        # 排出量の計算
2103        emission_data_list = []
2104        for spot in hotspots:
2105            # 漏出量の計算 (L/min)
2106            emission_rate = np.exp((np.log(spot.delta_ch4) + a) / b)
2107            # 日排出量 (L/day)
2108            daily_emission = emission_rate * 60 * 24
2109            # 年間排出量 (L/year)
2110            annual_emission = daily_emission * 365
2111
2112            emission_data = EmissionData(
2113                source=spot.source,
2114                type=spot.type,
2115                section=spot.section,
2116                latitude=spot.avg_lat,
2117                longitude=spot.avg_lon,
2118                delta_ch4=spot.delta_ch4,
2119                delta_c2h6=spot.delta_c2h6,
2120                ratio=spot.ratio,
2121                emission_rate=emission_rate,
2122                daily_emission=daily_emission,
2123                annual_emission=annual_emission,
2124            )
2125            emission_data_list.append(emission_data)
2126
2127        # 統計計算用にDataFrameを作成
2128        emission_df = pd.DataFrame([e.to_dict() for e in emission_data_list])
2129
2130        # タイプ別の統計情報を計算
2131        stats = {}
2132        # emission_formulas の定義の後に、排出量カテゴリーの閾値を定義
2133        emission_categories = {
2134            "low": {"min": 0, "max": 6},  # < 6 L/min
2135            "medium": {"min": 6, "max": 40},  # 6-40 L/min
2136            "high": {"min": 40, "max": float("inf")},  # > 40 L/min
2137        }
2138        # get_args(HotspotType)を使用して型安全なリストを作成
2139        types = list(get_args(HotspotType))
2140        for spot_type in types:
2141            df_type = emission_df[emission_df["type"] == spot_type]
2142            if len(df_type) > 0:
2143                # 既存の統計情報を計算
2144                type_stats = {
2145                    "count": len(df_type),
2146                    "emission_rate_min": df_type["emission_rate"].min(),
2147                    "emission_rate_max": df_type["emission_rate"].max(),
2148                    "emission_rate_mean": df_type["emission_rate"].mean(),
2149                    "emission_rate_median": df_type["emission_rate"].median(),
2150                    "total_annual_emission": df_type["annual_emission"].sum(),
2151                    "mean_annual_emission": df_type["annual_emission"].mean(),
2152                }
2153
2154                # 排出量カテゴリー別の統計を追加
2155                category_counts = {
2156                    "low": len(
2157                        df_type[
2158                            df_type["emission_rate"] < emission_categories["low"]["max"]
2159                        ]
2160                    ),
2161                    "medium": len(
2162                        df_type[
2163                            (
2164                                df_type["emission_rate"]
2165                                >= emission_categories["medium"]["min"]
2166                            )
2167                            & (
2168                                df_type["emission_rate"]
2169                                < emission_categories["medium"]["max"]
2170                            )
2171                        ]
2172                    ),
2173                    "high": len(
2174                        df_type[
2175                            df_type["emission_rate"]
2176                            >= emission_categories["high"]["min"]
2177                        ]
2178                    ),
2179                }
2180                type_stats["emission_categories"] = category_counts
2181
2182                stats[spot_type] = type_stats
2183
2184                if print_summary:
2185                    print(f"\n{spot_type}タイプの統計情報:")
2186                    print(f"  検出数: {type_stats['count']}")
2187                    print("  排出量 (L/min):")
2188                    print(f"    最小値: {type_stats['emission_rate_min']:.2f}")
2189                    print(f"    最大値: {type_stats['emission_rate_max']:.2f}")
2190                    print(f"    平均値: {type_stats['emission_rate_mean']:.2f}")
2191                    print(f"    中央値: {type_stats['emission_rate_median']:.2f}")
2192                    print("  排出量カテゴリー別の検出数:")
2193                    print(f"    低放出 (< 6 L/min): {category_counts['low']}")
2194                    print(f"    中放出 (6-40 L/min): {category_counts['medium']}")
2195                    print(f"    高放出 (> 40 L/min): {category_counts['high']}")
2196                    print("  年間排出量 (L/year):")
2197                    print(f"    合計: {type_stats['total_annual_emission']:.2f}")
2198                    print(f"    平均: {type_stats['mean_annual_emission']:.2f}")
2199
2200        return emission_data_list, stats
2201
2202    @staticmethod
2203    def plot_emission_analysis(
2204        emission_data_list: list[EmissionData],
2205        dpi: int = 300,
2206        output_dir: str | Path | None = None,
2207        output_filename: str = "emission_analysis.png",
2208        figsize: tuple[float, float] = (12, 5),
2209        add_legend: bool = True,
2210        hist_log_y: bool = False,
2211        hist_xlim: tuple[float, float] | None = None,
2212        hist_ylim: tuple[float, float] | None = None,
2213        scatter_xlim: tuple[float, float] | None = None,
2214        scatter_ylim: tuple[float, float] | None = None,
2215        hist_bin_width: float = 0.5,
2216        print_summary: bool = True,
2217        save_fig: bool = False,
2218        show_fig: bool = True,
2219        show_scatter: bool = True,  # 散布図の表示を制御するオプションを追加
2220    ) -> None:
2221        """
2222        排出量分析のプロットを作成する静的メソッド。
2223
2224        Parameters:
2225        ------
2226            emission_data_list : list[EmissionData]
2227                EmissionDataオブジェクトのリスト。
2228            output_dir : str | Path | None
2229                出力先ディレクトリのパス。
2230            output_filename : str
2231                保存するファイル名。デフォルトは"emission_analysis.png"。
2232            dpi : int
2233                プロットの解像度。デフォルトは300。
2234            figsize : tuple[float, float]
2235                プロットのサイズ。デフォルトは(12, 5)。
2236            add_legend : bool
2237                凡例を追加するかどうか。デフォルトはTrue。
2238            hist_log_y : bool
2239                ヒストグラムのy軸を対数スケールにするかどうか。デフォルトはFalse。
2240            hist_xlim : tuple[float, float] | None
2241                ヒストグラムのx軸の範囲。デフォルトはNone。
2242            hist_ylim : tuple[float, float] | None
2243                ヒストグラムのy軸の範囲。デフォルトはNone。
2244            scatter_xlim : tuple[float, float] | None
2245                散布図のx軸の範囲。デフォルトはNone。
2246            scatter_ylim : tuple[float, float] | None
2247                散布図のy軸の範囲。デフォルトはNone。
2248            hist_bin_width : float
2249                ヒストグラムのビンの幅。デフォルトは0.5。
2250            print_summary : bool
2251                集計結果を表示するかどうか。デフォルトはFalse。
2252            save_fig : bool
2253                図をファイルに保存するかどうか。デフォルトはFalse。
2254            show_fig : bool
2255                図を表示するかどうか。デフォルトはTrue。
2256            show_scatter : bool
2257                散布図(右図)を表示するかどうか。デフォルトはTrue。
2258        """
2259        # データをDataFrameに変換
2260        df = pd.DataFrame([e.to_dict() for e in emission_data_list])
2261
2262        # プロットの作成(散布図の有無に応じてサブプロット数を調整)
2263        if show_scatter:
2264            fig, (ax1, ax2) = plt.subplots(1, 2, figsize=figsize)
2265            axes = [ax1, ax2]
2266        else:
2267            fig, ax1 = plt.subplots(1, 1, figsize=(figsize[0] // 2, figsize[1]))
2268            axes = [ax1]
2269
2270        # カラーマップの定義
2271        colors: dict[HotspotType, str] = {"bio": "blue", "gas": "red", "comb": "green"}
2272
2273        # 存在するタイプを確認
2274        # HotspotTypeの定義順を基準にソート
2275        hotspot_types = list(get_args(HotspotType))
2276        existing_types = sorted(
2277            df["type"].unique(), key=lambda x: hotspot_types.index(x)
2278        )
2279
2280        # 左側: ヒストグラム
2281        # ビンの範囲を設定
2282        start = 0  # 必ず0から開始
2283        if hist_xlim is not None:
2284            end = hist_xlim[1]
2285        else:
2286            end = np.ceil(df["emission_rate"].max() * 1.05)
2287
2288        # ビン数を計算(end値をbin_widthで割り切れるように調整)
2289        n_bins = int(np.ceil(end / hist_bin_width))
2290        end = n_bins * hist_bin_width
2291
2292        # ビンの生成(0から開始し、bin_widthの倍数で区切る)
2293        bins = np.linspace(start, end, n_bins + 1)
2294
2295        # タイプごとにヒストグラムを積み上げ
2296        bottom = np.zeros(len(bins) - 1)
2297        for spot_type in existing_types:
2298            data = df[df["type"] == spot_type]["emission_rate"]
2299            if len(data) > 0:
2300                counts, _ = np.histogram(data, bins=bins)
2301                ax1.bar(
2302                    bins[:-1],
2303                    counts,
2304                    width=hist_bin_width,
2305                    bottom=bottom,
2306                    alpha=0.6,
2307                    label=spot_type,
2308                    color=colors[spot_type],
2309                )
2310                bottom += counts
2311
2312        ax1.set_xlabel("CH$_4$ Emission (L min$^{-1}$)")
2313        ax1.set_ylabel("Frequency")
2314        if hist_log_y:
2315            # ax1.set_yscale("log")
2316            # 非線形スケールを設定(linthreshで線形から対数への遷移点を指定)
2317            ax1.set_yscale("symlog", linthresh=1.0)
2318        if hist_xlim is not None:
2319            ax1.set_xlim(hist_xlim)
2320        else:
2321            ax1.set_xlim(0, np.ceil(df["emission_rate"].max() * 1.05))
2322
2323        if hist_ylim is not None:
2324            ax1.set_ylim(hist_ylim)
2325        else:
2326            ax1.set_ylim(0, ax1.get_ylim()[1])  # 下限を0に設定
2327
2328        if show_scatter:
2329            # 右側: 散布図
2330            for spot_type in existing_types:
2331                mask = df["type"] == spot_type
2332                ax2.scatter(
2333                    df[mask]["emission_rate"],
2334                    df[mask]["delta_ch4"],
2335                    alpha=0.6,
2336                    label=spot_type,
2337                    color=colors[spot_type],
2338                )
2339
2340            ax2.set_xlabel("Emission Rate (L min$^{-1}$)")
2341            ax2.set_ylabel("ΔCH$_4$ (ppm)")
2342            if scatter_xlim is not None:
2343                ax2.set_xlim(scatter_xlim)
2344            else:
2345                ax2.set_xlim(0, np.ceil(df["emission_rate"].max() * 1.05))
2346
2347            if scatter_ylim is not None:
2348                ax2.set_ylim(scatter_ylim)
2349            else:
2350                ax2.set_ylim(0, np.ceil(df["delta_ch4"].max() * 1.05))
2351
2352        # 凡例の表示
2353        if add_legend:
2354            for ax in axes:
2355                ax.legend(
2356                    bbox_to_anchor=(0.5, -0.30),
2357                    loc="upper center",
2358                    ncol=len(existing_types),
2359                )
2360
2361        plt.tight_layout()
2362
2363        # 図の保存
2364        if save_fig:
2365            if output_dir is None:
2366                raise ValueError(
2367                    "save_fig=Trueの場合、output_dirを指定する必要があります。有効なディレクトリパスを指定してください。"
2368                )
2369            os.makedirs(output_dir, exist_ok=True)
2370            output_path = os.path.join(output_dir, output_filename)
2371            plt.savefig(output_path, bbox_inches="tight", dpi=dpi)
2372        # 図の表示
2373        if show_fig:
2374            plt.show()
2375        else:
2376            plt.close(fig=fig)
2377
2378        if print_summary:
2379            # デバッグ用の出力
2380            print("\nビンごとの集計:")
2381            print(f"{'Range':>12} | {'bio':>8} | {'gas':>8} | {'total':>8}")
2382            print("-" * 50)
2383
2384            for i in range(len(bins) - 1):
2385                bin_start = bins[i]
2386                bin_end = bins[i + 1]
2387
2388                # 各タイプのカウントを計算
2389                counts_by_type: dict[HotspotType, int] = {"bio": 0, "gas": 0, "comb": 0}
2390                total = 0
2391                for spot_type in existing_types:
2392                    mask = (
2393                        (df["type"] == spot_type)
2394                        & (df["emission_rate"] >= bin_start)
2395                        & (df["emission_rate"] < bin_end)
2396                    )
2397                    count = len(df[mask])
2398                    counts_by_type[spot_type] = count
2399                    total += count
2400
2401                # カウントが0の場合はスキップ
2402                if total > 0:
2403                    range_str = f"{bin_start:5.1f}-{bin_end:<5.1f}"
2404                    bio_count = counts_by_type.get("bio", 0)
2405                    gas_count = counts_by_type.get("gas", 0)
2406                    print(
2407                        f"{range_str:>12} | {bio_count:8d} | {gas_count:8d} | {total:8d}"
2408                    )

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

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
    出力ファイル名
@staticmethod
def extract_source_name_from_path(path: str | pathlib.Path) -> str:
715    @staticmethod
716    def extract_source_name_from_path(path: str | Path) -> str:
717        """
718        ファイルパスからソース名(拡張子なしのファイル名)を抽出します。
719
720        Parameters:
721        ------
722            path : str | Path
723                ソース名を抽出するファイルパス
724                例: "/path/to/Pico100121_241017_092120+.txt"
725
726        Returns:
727        ------
728            str
729                抽出されたソース名
730                例: "Pico100121_241017_092120+"
731
732        Examples:
733        ------
734            >>> path = "/path/to/data/Pico100121_241017_092120+.txt"
735            >>> MobileSpatialAnalyzer.extract_source_from_path(path)
736            'Pico100121_241017_092120+'
737        """
738        # Pathオブジェクトに変換
739        path_obj = Path(path)
740        # stem属性で拡張子なしのファイル名を取得
741        source_name = path_obj.stem
742        return source_name

ファイルパスからソース名(拡張子なしのファイル名)を抽出します。

Parameters:

path : str | Path
    ソース名を抽出するファイルパス
    例: "/path/to/Pico100121_241017_092120+.txt"

Returns:

str
    抽出されたソース名
    例: "Pico100121_241017_092120+"

Examples:

>>> path = "/path/to/data/Pico100121_241017_092120+.txt"
>>> MobileSpatialAnalyzer.extract_source_from_path(path)
'Pico100121_241017_092120+'
def get_preprocessed_data(self) -> pandas.core.frame.DataFrame:
744    def get_preprocessed_data(
745        self,
746    ) -> pd.DataFrame:
747        """
748        データ前処理を行い、CH4とC2H6の相関解析に必要な形式に整えます。
749        コンストラクタで読み込んだすべてのデータを前処理し、結合したDataFrameを返します。
750
751        Returns:
752        ------
753            pd.DataFrame
754                前処理済みの結合されたDataFrame
755        """
756        processed_dfs: list[pd.DataFrame] = []
757
758        # 各データソースに対して解析を実行
759        for source_name, df in self._data.items():
760            # パラメータの計算
761            processed_df = MobileSpatialAnalyzer._calculate_hotspots_parameters(
762                df, self._window_size
763            )
764            # ソース名を列として追加
765            processed_df["source"] = source_name
766            processed_dfs.append(processed_df)
767
768        # すべてのDataFrameを結合
769        if not processed_dfs:
770            raise ValueError("処理対象のデータが存在しません。")
771
772        combined_df = pd.concat(processed_dfs, axis=0)
773        return combined_df

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

Returns:

pd.DataFrame
    前処理済みの結合されたDataFrame
def get_section_size(self) -> float:
775    def get_section_size(self) -> float:
776        """
777        セクションのサイズを取得するメソッド。
778        このメソッドは、解析対象のデータを区画に分割する際の
779        各区画の角度範囲を示すサイズを返します。
780
781        Returns:
782        ------
783            float
784                1セクションのサイズ(度単位)
785        """
786        return self._section_size

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

Returns:

float
    1セクションのサイズ(度単位)
def get_source_names(self, print_all: bool = False) -> list[str]:
788    def get_source_names(self, print_all: bool = False) -> list[str]:
789        """
790        データソースの名前を取得します。
791
792        Parameters
793        ----------
794        print_all : bool, optional
795            すべてのデータソース名を表示するかどうかを指定します。デフォルトはFalseです。
796
797        Returns
798        -------
799        list[str]
800            データソース名のリスト
801
802        Raises
803        ------
804        ValueError
805            データが読み込まれていない場合に発生します。
806        """
807        dfs_dict: dict[str, pd.DataFrame] = self._data
808        # データソースの選択
809        if not dfs_dict:
810            raise ValueError("データが読み込まれていません。")
811        source_name_list: list[str] = list(dfs_dict.keys())
812        if print_all:
813            print(source_name_list)
814        return source_name_list

データソースの名前を取得します。

Parameters

print_all : bool, optional すべてのデータソース名を表示するかどうかを指定します。デフォルトはFalseです。

Returns

list[str] データソース名のリスト

Raises

ValueError データが読み込まれていない場合に発生します。

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:
816    def plot_ch4_delta_histogram(
817        self,
818        hotspots: list[HotspotData],
819        output_dir: str | Path | None,
820        output_filename: str = "ch4_delta_histogram.png",
821        dpi: int = 200,
822        figsize: tuple[int, int] = (8, 6),
823        fontsize: float = 20,
824        xlim: tuple[float, float] | None = None,
825        ylim: tuple[float, float] | None = None,
826        save_fig: bool = True,
827        show_fig: bool = True,
828        yscale_log: bool = True,
829        print_bins_analysis: bool = False,
830    ) -> None:
831        """
832        CH4の増加量(ΔCH4)の積み上げヒストグラムをプロットします。
833
834        Parameters:
835        ------
836            hotspots : list[HotspotData]
837                プロットするホットスポットのリスト
838            output_dir : str | Path | None
839                保存先のディレクトリパス
840            output_filename : str
841                保存するファイル名。デフォルトは"ch4_delta_histogram.png"。
842            dpi : int
843                解像度。デフォルトは200。
844            figsize : tuple[int, int]
845                図のサイズ。デフォルトは(8, 6)。
846            fontsize : float
847                フォントサイズ。デフォルトは20。
848            xlim : tuple[float, float] | None
849                x軸の範囲。Noneの場合は自動設定。
850            ylim : tuple[float, float] | None
851                y軸の範囲。Noneの場合は自動設定。
852            save_fig : bool
853                図の保存を許可するフラグ。デフォルトはTrue。
854            show_fig : bool
855                図の表示を許可するフラグ。デフォルトはTrue。
856            yscale_log : bool
857                y軸をlogにするかどうか。デフォルトはTrue。
858            print_bins_analysis : bool
859                ビンごとの内訳を表示するオプション。
860        """
861        plt.rcParams["font.size"] = fontsize
862        fig = plt.figure(figsize=figsize, dpi=dpi)
863
864        # ホットスポットからデータを抽出
865        all_ch4_deltas = []
866        all_types = []
867        for spot in hotspots:
868            all_ch4_deltas.append(spot.delta_ch4)
869            all_types.append(spot.type)
870
871        # データをNumPy配列に変換
872        all_ch4_deltas = np.array(all_ch4_deltas)
873        all_types = np.array(all_types)
874
875        # 0.1刻みのビンを作成
876        if xlim is not None:
877            bins = np.arange(xlim[0], xlim[1] + 0.1, 0.1)
878        else:
879            max_val = np.ceil(np.max(all_ch4_deltas) * 10) / 10
880            bins = np.arange(0, max_val + 0.1, 0.1)
881
882        # タイプごとのヒストグラムデータを計算
883        hist_data = {}
884        # HotspotTypeのリテラル値を使用してイテレーション
885        for type_name in get_args(HotspotType):  # typing.get_argsをインポート
886            mask = all_types == type_name
887            if np.any(mask):
888                counts, _ = np.histogram(all_ch4_deltas[mask], bins=bins)
889                hist_data[type_name] = counts
890
891        # ビンごとの内訳を表示
892        if print_bins_analysis:
893            self.logger.info("各ビンの内訳:")
894            print(f"{'Bin Range':15} {'bio':>8} {'gas':>8} {'comb':>8} {'Total':>8}")
895            print("-" * 50)
896
897            for i in range(len(bins) - 1):
898                bin_start = bins[i]
899                bin_end = bins[i + 1]
900                bio_count = hist_data.get("bio", np.zeros(len(bins) - 1))[i]
901                gas_count = hist_data.get("gas", np.zeros(len(bins) - 1))[i]
902                comb_count = hist_data.get("comb", np.zeros(len(bins) - 1))[i]
903                total = bio_count + gas_count + comb_count
904
905                if total > 0:  # 合計が0のビンは表示しない
906                    print(
907                        f"{bin_start:4.1f}-{bin_end:<8.1f}"
908                        f"{int(bio_count):8d}"
909                        f"{int(gas_count):8d}"
910                        f"{int(comb_count):8d}"
911                        f"{int(total):8d}"
912                    )
913
914        # 積み上げヒストグラムを作成
915        bottom = np.zeros_like(hist_data.get("bio", np.zeros(len(bins) - 1)))
916
917        # 色の定義をHotspotTypeを使用して型安全に定義
918        colors: dict[HotspotType, str] = {"bio": "blue", "gas": "red", "comb": "green"}
919
920        # HotspotTypeのリテラル値を使用してイテレーション
921        for type_name in get_args(HotspotType):
922            if type_name in hist_data:
923                plt.bar(
924                    bins[:-1],
925                    hist_data[type_name],
926                    width=np.diff(bins)[0],
927                    bottom=bottom,
928                    color=colors[type_name],
929                    label=type_name,
930                    alpha=0.6,
931                    align="edge",
932                )
933                bottom += hist_data[type_name]
934
935        if yscale_log:
936            plt.yscale("log")
937        plt.xlabel("Δ$\\mathregular{CH_{4}}$ (ppm)")
938        plt.ylabel("Frequency")
939        plt.legend()
940        plt.grid(True, which="both", ls="-", alpha=0.2)
941
942        # 軸の範囲を設定
943        if xlim is not None:
944            plt.xlim(xlim)
945        if ylim is not None:
946            plt.ylim(ylim)
947
948        # グラフの保存または表示
949        if save_fig:
950            if output_dir is None:
951                raise ValueError(
952                    "save_fig=Trueの場合、output_dirを指定する必要があります。有効なディレクトリパスを指定してください。"
953                )
954            os.makedirs(output_dir, exist_ok=True)
955            output_path: str = os.path.join(output_dir, output_filename)
956            plt.savefig(output_path, bbox_inches="tight")
957            self.logger.info(f"ヒストグラムを保存しました: {output_path}")
958        if show_fig:
959            plt.show()
960        else:
961            plt.close(fig=fig)

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

Parameters:

hotspots : list[HotspotData]
    プロットするホットスポットのリスト
output_dir : str | Path | None
    保存先のディレクトリパス
output_filename : str
    保存するファイル名。デフォルトは"ch4_delta_histogram.png"。
dpi : int
    解像度。デフォルトは200。
figsize : tuple[int, int]
    図のサイズ。デフォルトは(8, 6)。
fontsize : float
    フォントサイズ。デフォルトは20。
xlim : tuple[float, float] | None
    x軸の範囲。Noneの場合は自動設定。
ylim : tuple[float, float] | None
    y軸の範囲。Noneの場合は自動設定。
save_fig : bool
    図の保存を許可するフラグ。デフォルトはTrue。
show_fig : bool
    図の表示を許可するフラグ。デフォルトはTrue。
yscale_log : bool
    y軸をlogにするかどうか。デフォルトはTrue。
print_bins_analysis : bool
    ビンごとの内訳を表示するオプション。
def plot_mapbox( self, df: pandas.core.frame.DataFrame, col_conc: str, mapbox_access_token: str, sort_conc_column: bool = True, output_dir: str | pathlib.Path | None = None, output_filename: str = 'mapbox_plot.html', col_lat: str = 'latitude', col_lon: 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:
 963    def plot_mapbox(
 964        self,
 965        df: pd.DataFrame,
 966        col_conc: str,
 967        mapbox_access_token: str,
 968        sort_conc_column: bool = True,
 969        output_dir: str | Path | None = None,
 970        output_filename: str = "mapbox_plot.html",
 971        col_lat: str = "latitude",
 972        col_lon: str = "longitude",
 973        colorscale: str = "Jet",
 974        center_lat: float | None = None,
 975        center_lon: float | None = None,
 976        zoom: float = 12,
 977        width: int = 700,
 978        height: int = 700,
 979        tick_font_family: str = "Arial",
 980        title_font_family: str = "Arial",
 981        tick_font_size: int = 12,
 982        title_font_size: int = 14,
 983        marker_size: int = 4,
 984        colorbar_title: str | None = None,
 985        value_range: tuple[float, float] | None = None,
 986        save_fig: bool = True,
 987        show_fig: bool = True,
 988    ) -> None:
 989        """
 990        Plotlyを使用してMapbox上にデータをプロットします。
 991
 992        Parameters:
 993        ------
 994            df : pd.DataFrame
 995                プロットするデータを含むDataFrame
 996            col_conc : str
 997                カラーマッピングに使用する列名
 998            mapbox_access_token : str
 999                Mapboxのアクセストークン
1000            sort_conc_column : bool
1001                value_columnをソートするか否か。デフォルトはTrue。
1002            output_dir : str | Path | None
1003                出力ディレクトリのパス
1004            output_filename : str
1005                出力ファイル名。デフォルトは"mapbox_plot.html"
1006            col_lat : str
1007                緯度の列名。デフォルトは"latitude"
1008            col_lon : str
1009                経度の列名。デフォルトは"longitude"
1010            colorscale : str
1011                使用するカラースケール。デフォルトは"Jet"
1012            center_lat : float | None
1013                中心緯度。デフォルトはNoneで、self._center_latを使用
1014            center_lon : float | None
1015                中心経度。デフォルトはNoneで、self._center_lonを使用
1016            zoom : float
1017                マップの初期ズームレベル。デフォルトは12
1018            width : int
1019                プロットの幅(ピクセル)。デフォルトは700
1020            height : int
1021                プロットの高さ(ピクセル)。デフォルトは700
1022            tick_font_family : str
1023                カラーバーの目盛りフォントファミリー。デフォルトは"Arial"
1024            title_font_family : str
1025                カラーバーのラベルフォントファミリー。デフォルトは"Arial"
1026            tick_font_size : int
1027                カラーバーの目盛りフォントサイズ。デフォルトは12
1028            title_font_size : int
1029                カラーバーのラベルフォントサイズ。デフォルトは14
1030            marker_size : int
1031                マーカーのサイズ。デフォルトは4
1032            colorbar_title : str | None
1033                カラーバーのラベル
1034            value_range : tuple[float, float] | None
1035                カラーマッピングの範囲。デフォルトはNoneで、データの最小値と最大値を使用
1036            save_fig : bool
1037                図を保存するかどうか。デフォルトはTrue
1038            show_fig : bool
1039                図を表示するかどうか。デフォルトはTrue
1040        """
1041        df_mapping: pd.DataFrame = df.copy().dropna(subset=[col_conc])
1042        if sort_conc_column:
1043            df_mapping = df_mapping.sort_values(col_conc)
1044        # 中心座標の設定
1045        center_lat = center_lat if center_lat is not None else self._center_lat
1046        center_lon = center_lon if center_lon is not None else self._center_lon
1047
1048        # カラーマッピングの範囲を設定
1049        cmin, cmax = 0, 0
1050        if value_range is None:
1051            cmin = df_mapping[col_conc].min()
1052            cmax = df_mapping[col_conc].max()
1053        else:
1054            cmin, cmax = value_range
1055
1056        # カラーバーのタイトルを設定
1057        title_text = colorbar_title if colorbar_title is not None else col_conc
1058
1059        # Scattermapboxのデータを作成
1060        scatter_data = go.Scattermapbox(
1061            lat=df_mapping[col_lat],
1062            lon=df_mapping[col_lon],
1063            text=df_mapping[col_conc].astype(str),
1064            hoverinfo="text",
1065            mode="markers",
1066            marker=dict(
1067                color=df_mapping[col_conc],
1068                size=marker_size,
1069                reversescale=False,
1070                autocolorscale=False,
1071                colorscale=colorscale,
1072                cmin=cmin,
1073                cmax=cmax,
1074                colorbar=dict(
1075                    tickformat="3.2f",
1076                    outlinecolor="black",
1077                    outlinewidth=1.5,
1078                    ticks="outside",
1079                    ticklen=7,
1080                    tickwidth=1.5,
1081                    tickcolor="black",
1082                    tickfont=dict(
1083                        family=tick_font_family, color="black", size=tick_font_size
1084                    ),
1085                    title=dict(
1086                        text=title_text, side="top"
1087                    ),  # カラーバーのタイトルを設定
1088                    titlefont=dict(
1089                        family=title_font_family,
1090                        color="black",
1091                        size=title_font_size,
1092                    ),
1093                ),
1094            ),
1095        )
1096
1097        # レイアウトの設定
1098        layout = go.Layout(
1099            width=width,
1100            height=height,
1101            showlegend=False,
1102            mapbox=dict(
1103                accesstoken=mapbox_access_token,
1104                center=dict(lat=center_lat, lon=center_lon),
1105                zoom=zoom,
1106            ),
1107        )
1108
1109        # 図の作成
1110        fig = go.Figure(data=[scatter_data], layout=layout)
1111
1112        # 図の保存
1113        if save_fig:
1114            # 保存時の出力ディレクトリチェック
1115            if output_dir is None:
1116                raise ValueError(
1117                    "save_fig=Trueの場合、output_dirを指定する必要があります。"
1118                )
1119            os.makedirs(output_dir, exist_ok=True)
1120            output_path = os.path.join(output_dir, output_filename)
1121            pyo.plot(fig, filename=output_path, auto_open=False)
1122            self.logger.info(f"Mapboxプロットを保存しました: {output_path}")
1123        # 図の表示
1124        if show_fig:
1125            pyo.iplot(fig)

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

Parameters:

df : pd.DataFrame
    プロットするデータを含むDataFrame
col_conc : str
    カラーマッピングに使用する列名
mapbox_access_token : str
    Mapboxのアクセストークン
sort_conc_column : bool
    value_columnをソートするか否か。デフォルトはTrue。
output_dir : str | Path | None
    出力ディレクトリのパス
output_filename : str
    出力ファイル名。デフォルトは"mapbox_plot.html"
col_lat : str
    緯度の列名。デフォルトは"latitude"
col_lon : str
    経度の列名。デフォルトは"longitude"
colorscale : str
    使用するカラースケール。デフォルトは"Jet"
center_lat : float | None
    中心緯度。デフォルトはNoneで、self._center_latを使用
center_lon : float | None
    中心経度。デフォルトはNoneで、self._center_lonを使用
zoom : float
    マップの初期ズームレベル。デフォルトは12
width : int
    プロットの幅(ピクセル)。デフォルトは700
height : int
    プロットの高さ(ピクセル)。デフォルトは700
tick_font_family : str
    カラーバーの目盛りフォントファミリー。デフォルトは"Arial"
title_font_family : str
    カラーバーのラベルフォントファミリー。デフォルトは"Arial"
tick_font_size : int
    カラーバーの目盛りフォントサイズ。デフォルトは12
title_font_size : int
    カラーバーのラベルフォントサイズ。デフォルトは14
marker_size : int
    マーカーのサイズ。デフォルトは4
colorbar_title : str | None
    カラーバーのラベル
value_range : tuple[float, float] | None
    カラーマッピングの範囲。デフォルトはNoneで、データの最小値と最大値を使用
save_fig : bool
    図を保存するかどうか。デフォルトはTrue
show_fig : bool
    図を表示するかどうか。デフォルトはTrue
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:
1127    def plot_scatter_c2c1(
1128        self,
1129        hotspots: list[HotspotData],
1130        output_dir: str | Path | None = None,
1131        output_filename: str = "scatter_c2c1.png",
1132        dpi: int = 200,
1133        figsize: tuple[int, int] = (4, 4),
1134        fontsize: float = 12,
1135        save_fig: bool = True,
1136        show_fig: bool = True,
1137        ratio_labels: dict[float, tuple[float, float, str]] | None = None,
1138    ) -> None:
1139        """
1140        検出されたホットスポットのΔC2H6とΔCH4の散布図をプロットします。
1141
1142        Parameters:
1143        ------
1144            hotspots : list[HotspotData]
1145                プロットするホットスポットのリスト
1146            output_dir : str | Path | None
1147                保存先のディレクトリパス
1148            output_filename : str
1149                保存するファイル名。デフォルトは"scatter_c2c1.png"。
1150            dpi : int
1151                解像度。デフォルトは200。
1152            figsize : tuple[int, int]
1153                図のサイズ。デフォルトは(4, 4)。
1154            fontsize : float
1155                フォントサイズ。デフォルトは12。
1156            save_fig : bool
1157                図の保存を許可するフラグ。デフォルトはTrue。
1158            show_fig : bool
1159                図の表示を許可するフラグ。デフォルトはTrue。
1160            ratio_labels : dict[float, tuple[float, float, str]] | None
1161                比率線とラベルの設定。
1162                キーは比率値、値は (x位置, y位置, ラベルテキスト) のタプル。
1163                Noneの場合はデフォルト設定を使用。デフォルト値:
1164                {
1165                    0.001: (1.25, 2, "0.001"),
1166                    0.005: (1.25, 8, "0.005"),
1167                    0.010: (1.25, 15, "0.01"),
1168                    0.020: (1.25, 30, "0.02"),
1169                    0.030: (1.0, 40, "0.03"),
1170                    0.076: (0.20, 42, "0.076 (Osaka)")
1171                }
1172        """
1173        plt.rcParams["font.size"] = fontsize
1174        fig = plt.figure(figsize=figsize, dpi=dpi)
1175
1176        # タイプごとのデータを収集
1177        type_data: dict[HotspotType, list[tuple[float, float]]] = {
1178            "bio": [],
1179            "gas": [],
1180            "comb": [],
1181        }
1182        for spot in hotspots:
1183            type_data[spot.type].append((spot.delta_ch4, spot.delta_c2h6))
1184
1185        # 色とラベルの定義
1186        colors: dict[HotspotType, str] = {"bio": "blue", "gas": "red", "comb": "green"}
1187        labels: dict[HotspotType, str] = {"bio": "bio", "gas": "gas", "comb": "comb"}
1188
1189        # タイプごとにプロット(データが存在する場合のみ)
1190        for spot_type, data in type_data.items():
1191            if data:  # データが存在する場合のみプロット
1192                ch4_values, c2h6_values = zip(*data)
1193                plt.plot(
1194                    ch4_values,
1195                    c2h6_values,
1196                    "o",
1197                    c=colors[spot_type],
1198                    alpha=0.5,
1199                    ms=2,
1200                    label=labels[spot_type],
1201                )
1202
1203        # デフォルトの比率とラベル設定
1204        default_ratio_labels = {
1205            0.001: (1.25, 2, "0.001"),
1206            0.005: (1.25, 8, "0.005"),
1207            0.010: (1.25, 15, "0.01"),
1208            0.020: (1.25, 30, "0.02"),
1209            0.030: (1.0, 40, "0.03"),
1210            0.076: (0.20, 42, "0.076 (Osaka)"),
1211        }
1212
1213        ratio_labels = ratio_labels or default_ratio_labels
1214
1215        # プロット後、軸の設定前に比率の線を追加
1216        x = np.array([0, 5])
1217        base_ch4 = 0.0
1218        base = 0.0
1219
1220        # 各比率に対して線を引く
1221        for ratio, (x_pos, y_pos, label) in ratio_labels.items():
1222            y = (x - base_ch4) * 1000 * ratio + base
1223            plt.plot(x, y, "-", c="black", alpha=0.5)
1224            plt.text(x_pos, y_pos, label)
1225
1226        plt.ylim(0, 50)
1227        plt.xlim(0, 2.0)
1228        plt.ylabel("Δ$\\mathregular{C_{2}H_{6}}$ (ppb)")
1229        plt.xlabel("Δ$\\mathregular{CH_{4}}$ (ppm)")
1230        plt.legend()
1231
1232        # グラフの保存または表示
1233        if save_fig:
1234            if output_dir is None:
1235                raise ValueError(
1236                    "save_fig=Trueの場合、output_dirを指定する必要があります。有効なディレクトリパスを指定してください。"
1237                )
1238            output_path: str = os.path.join(output_dir, output_filename)
1239            plt.savefig(output_path, bbox_inches="tight")
1240            self.logger.info(f"散布図を保存しました: {output_path}")
1241        if show_fig:
1242            plt.show()
1243        else:
1244            plt.close(fig=fig)

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

Parameters:

hotspots : list[HotspotData]
    プロットするホットスポットのリスト
output_dir : str | Path | None
    保存先のディレクトリパス
output_filename : str
    保存するファイル名。デフォルトは"scatter_c2c1.png"。
dpi : int
    解像度。デフォルトは200。
figsize : tuple[int, int]
    図のサイズ。デフォルトは(4, 4)。
fontsize : float
    フォントサイズ。デフォルトは12。
save_fig : bool
    図の保存を許可するフラグ。デフォルトはTrue。
show_fig : bool
    図の表示を許可するフラグ。デフォルトはTrue。
ratio_labels : dict[float, tuple[float, float, str]] | None
    比率線とラベルの設定。
    キーは比率値、値は (x位置, y位置, ラベルテキスト) のタプル。
    Noneの場合はデフォルト設定を使用。デフォルト値:
    {
        0.001: (1.25, 2, "0.001"),
        0.005: (1.25, 8, "0.005"),
        0.010: (1.25, 15, "0.01"),
        0.020: (1.25, 30, "0.02"),
        0.030: (1.0, 40, "0.03"),
        0.076: (0.20, 42, "0.076 (Osaka)")
    }
def plot_conc_timeseries( self, source_name: str | None = None, output_dir: str | pathlib.Path | None = None, output_filename: str = 'timeseries.png', dpi: int = 200, figsize: tuple[float, float] = (8, 4), save_fig: bool = True, 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, font_size: float = 12, label_pad: float = 10) -> None:
1246    def plot_conc_timeseries(
1247        self,
1248        source_name: str | None = None,
1249        output_dir: str | Path | None = None,
1250        output_filename: str = "timeseries.png",
1251        dpi: int = 200,
1252        figsize: tuple[float, float] = (8, 4),
1253        save_fig: bool = True,
1254        show_fig: bool = True,
1255        col_ch4: str = "ch4_ppm",
1256        col_c2h6: str = "c2h6_ppb",
1257        col_h2o: str = "h2o_ppm",
1258        ylim_ch4: tuple[float, float] | None = None,
1259        ylim_c2h6: tuple[float, float] | None = None,
1260        ylim_h2o: tuple[float, float] | None = None,
1261        font_size: float = 12,
1262        label_pad: float = 10,
1263    ) -> None:
1264        """
1265        時系列データをプロットします。
1266
1267        Parameters:
1268        ------
1269            dpi : int
1270                図の解像度を指定します。デフォルトは200です。
1271            source_name : str | None
1272                プロットするデータソースの名前。Noneの場合は最初のデータソースを使用します。
1273            figsize : tuple[float, float]
1274                図のサイズを指定します。デフォルトは(8, 4)です。
1275            output_dir : str | Path | None
1276                保存先のディレクトリを指定します。save_fig=Trueの場合は必須です。
1277            output_filename : str
1278                保存するファイル名を指定します。デフォルトは"time_series.png"です。
1279            save_fig : bool
1280                図を保存するかどうかを指定します。デフォルトはFalseです。
1281            show_fig : bool
1282                図を表示するかどうかを指定します。デフォルトはTrueです。
1283            col_ch4 : str
1284                CH4データのキーを指定します。デフォルトは"ch4_ppm"です。
1285            col_c2h6 : str
1286                C2H6データのキーを指定します。デフォルトは"c2h6_ppb"です。
1287            col_h2o : str
1288                H2Oデータのキーを指定します。デフォルトは"h2o_ppm"です。
1289            ylim_ch4 : tuple[float, float] | None
1290                CH4プロットのy軸範囲を指定します。デフォルトはNoneです。
1291            ylim_c2h6 : tuple[float, float] | None
1292                C2H6プロットのy軸範囲を指定します。デフォルトはNoneです。
1293            ylim_h2o : tuple[float, float] | None
1294                H2Oプロットのy軸範囲を指定します。デフォルトはNoneです。
1295            font_size : float
1296                基本フォントサイズ。デフォルトは12。
1297            label_pad : float
1298                y軸ラベルのパディング。デフォルトは10。
1299        """
1300        # プロットパラメータの設定
1301        plt.rcParams.update(
1302            {
1303                "font.size": font_size,
1304                "axes.labelsize": font_size,
1305                "axes.titlesize": font_size,
1306                "xtick.labelsize": font_size,
1307                "ytick.labelsize": font_size,
1308            }
1309        )
1310        dfs_dict: dict[str, pd.DataFrame] = self._data.copy()
1311        # データソースの選択
1312        if not dfs_dict:
1313            raise ValueError("データが読み込まれていません。")
1314
1315        if source_name not in dfs_dict:
1316            raise ValueError(
1317                f"指定されたデータソース '{source_name}' が見つかりません。"
1318            )
1319
1320        df = dfs_dict[source_name]
1321
1322        # プロットの作成
1323        fig = plt.figure(figsize=figsize, dpi=dpi)
1324
1325        # CH4プロット
1326        ax1 = fig.add_subplot(3, 1, 1)
1327        ax1.plot(df.index, df[col_ch4], c="red")
1328        if ylim_ch4:
1329            ax1.set_ylim(ylim_ch4)
1330        ax1.set_ylabel("$\\mathregular{CH_{4}}$ (ppm)", labelpad=label_pad)
1331        ax1.grid(True, alpha=0.3)
1332
1333        # C2H6プロット
1334        ax2 = fig.add_subplot(3, 1, 2)
1335        ax2.plot(df.index, df[col_c2h6], c="red")
1336        if ylim_c2h6:
1337            ax2.set_ylim(ylim_c2h6)
1338        ax2.set_ylabel("$\\mathregular{C_{2}H_{6}}$ (ppb)", labelpad=label_pad)
1339        ax2.grid(True, alpha=0.3)
1340
1341        # H2Oプロット
1342        ax3 = fig.add_subplot(3, 1, 3)
1343        ax3.plot(df.index, df[col_h2o], c="red")
1344        if ylim_h2o:
1345            ax3.set_ylim(ylim_h2o)
1346        ax3.set_ylabel("$\\mathregular{H_{2}O}$ (ppm)", labelpad=label_pad)
1347        ax3.grid(True, alpha=0.3)
1348
1349        # x軸のフォーマット調整
1350        for ax in [ax1, ax2, ax3]:
1351            ax.xaxis.set_major_formatter(mdates.DateFormatter("%H:%M"))
1352            # 軸のラベルとグリッド線の調整
1353            ax.tick_params(axis="both", which="major", labelsize=font_size)
1354            ax.grid(True, alpha=0.3)
1355
1356        # サブプロット間の間隔調整
1357        plt.subplots_adjust(wspace=0.38, hspace=0.38)
1358
1359        # 図の保存
1360        if save_fig:
1361            if output_dir is None:
1362                raise ValueError(
1363                    "save_fig=Trueの場合、output_dirを指定する必要があります。有効なディレクトリパスを指定してください。"
1364                )
1365            os.makedirs(output_dir, exist_ok=True)
1366            output_path = os.path.join(output_dir, output_filename)
1367            plt.savefig(output_path, bbox_inches="tight")
1368
1369        if show_fig:
1370            plt.show()
1371        else:
1372            plt.close(fig=fig)

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

Parameters:

dpi : int
    図の解像度を指定します。デフォルトは200です。
source_name : str | None
    プロットするデータソースの名前。Noneの場合は最初のデータソースを使用します。
figsize : tuple[float, float]
    図のサイズを指定します。デフォルトは(8, 4)です。
output_dir : str | Path | None
    保存先のディレクトリを指定します。save_fig=Trueの場合は必須です。
output_filename : str
    保存するファイル名を指定します。デフォルトは"time_series.png"です。
save_fig : bool
    図を保存するかどうかを指定します。デフォルトはFalseです。
show_fig : bool
    図を表示するかどうかを指定します。デフォルトはTrueです。
col_ch4 : str
    CH4データのキーを指定します。デフォルトは"ch4_ppm"です。
col_c2h6 : str
    C2H6データのキーを指定します。デフォルトは"c2h6_ppb"です。
col_h2o : str
    H2Oデータのキーを指定します。デフォルトは"h2o_ppm"です。
ylim_ch4 : tuple[float, float] | None
    CH4プロットのy軸範囲を指定します。デフォルトはNoneです。
ylim_c2h6 : tuple[float, float] | None
    C2H6プロットのy軸範囲を指定します。デフォルトはNoneです。
ylim_h2o : tuple[float, float] | None
    H2Oプロットのy軸範囲を指定します。デフォルトはNoneです。
font_size : float
    基本フォントサイズ。デフォルトは12。
label_pad : float
    y軸ラベルのパディング。デフォルトは10。
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'):
1858    def remove_c2c1_ratio_duplicates(
1859        self,
1860        df: pd.DataFrame,
1861        min_time_threshold_seconds: float = 300,  # 5分以内は重複とみなす
1862        max_time_threshold_hours: float = 12.0,  # 12時間以上離れている場合は別のポイントとして扱う
1863        check_time_all: bool = True,  # 時間閾値を超えた場合の重複チェックを継続するかどうか
1864        hotspot_area_meter: float = 50.0,  # 重複とみなす距離の閾値(メートル)
1865        col_ch4_ppm: str = "ch4_ppm",
1866        col_ch4_ppm_mv: str = "ch4_ppm_mv",
1867        col_ch4_ppm_delta: str = "ch4_ppm_delta",
1868    ):
1869        """
1870        メタン濃度の増加が閾値を超えた地点から、重複を除外してユニークなホットスポットを抽出する関数。
1871
1872        Parameters:
1873        ------
1874            df : pandas.DataFrame
1875                入力データフレーム。必須カラム:
1876                - ch4_ppm: メタン濃度(ppm)
1877                - ch4_ppm_mv: メタン濃度の移動平均(ppm)
1878                - ch4_ppm_delta: メタン濃度の増加量(ppm)
1879                - latitude: 緯度
1880                - longitude: 経度
1881            min_time_threshold_seconds : float, optional
1882                重複とみなす最小時間差(秒)。デフォルトは300秒(5分)。
1883            max_time_threshold_hours : float, optional
1884                別ポイントとして扱う最大時間差(時間)。デフォルトは12時間。
1885            check_time_all : bool, optional
1886                時間閾値を超えた場合の重複チェックを継続するかどうか。デフォルトはTrue。
1887            hotspot_area_meter : float, optional
1888                重複とみなす距離の閾値(メートル)。デフォルトは50メートル。
1889
1890        Returns:
1891        ------
1892            pandas.DataFrame
1893                ユニークなホットスポットのデータフレーム。
1894        """
1895        df_data: pd.DataFrame = df.copy()
1896        # メタン濃度の増加が閾値を超えた点を抽出
1897        mask = (
1898            df_data[col_ch4_ppm] - df_data[col_ch4_ppm_mv] > self._ch4_enhance_threshold
1899        )
1900        hotspot_candidates = df_data[mask].copy()
1901
1902        # ΔCH4の降順でソート
1903        sorted_hotspots = hotspot_candidates.sort_values(
1904            by=col_ch4_ppm_delta, ascending=False
1905        )
1906        used_positions = []
1907        unique_hotspots = pd.DataFrame()
1908
1909        for _, spot in sorted_hotspots.iterrows():
1910            should_add = True
1911            for used_lat, used_lon, used_time in used_positions:
1912                # 距離チェック
1913                distance = geodesic(
1914                    (spot.latitude, spot.longitude), (used_lat, used_lon)
1915                ).meters
1916
1917                if distance < hotspot_area_meter:
1918                    # 時間差の計算(秒単位)
1919                    time_diff = pd.Timedelta(
1920                        spot.name - pd.to_datetime(used_time)
1921                    ).total_seconds()
1922                    time_diff_abs = abs(time_diff)
1923
1924                    # 時間差に基づく判定
1925                    if check_time_all:
1926                        # 時間に関係なく、距離が近ければ重複とみなす
1927                        # ΔCH4が大きい方を残す(現在のスポットは必ず小さい)
1928                        should_add = False
1929                        break
1930                    else:
1931                        # 時間窓による判定を行う
1932                        if time_diff_abs <= min_time_threshold_seconds:
1933                            # Case 1: 最小時間閾値以内は重複とみなす
1934                            should_add = False
1935                            break
1936                        elif time_diff_abs > max_time_threshold_hours * 3600:
1937                            # Case 2: 最大時間閾値を超えた場合は重複チェックをスキップ
1938                            continue
1939                        # Case 3: その間の時間差の場合は、距離が近ければ重複とみなす
1940                        should_add = False
1941                        break
1942
1943            if should_add:
1944                unique_hotspots = pd.concat([unique_hotspots, pd.DataFrame([spot])])
1945                used_positions.append((spot.latitude, spot.longitude, spot.name))
1946
1947        return unique_hotspots

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

Parameters:

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

Returns:

pandas.DataFrame
    ユニークなホットスポットのデータフレーム。
@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]:
1949    @staticmethod
1950    def remove_hotspots_duplicates(
1951        hotspots: list[HotspotData],
1952        check_time_all: bool,
1953        min_time_threshold_seconds: float = 300,
1954        max_time_threshold_hours: float = 12,
1955        hotspot_area_meter: float = 50,
1956    ) -> list[HotspotData]:
1957        """
1958        重複するホットスポットを除外します。
1959
1960        このメソッドは、与えられたホットスポットのリストから重複を検出し、
1961        一意のホットスポットのみを返します。重複の判定は、指定された
1962        時間および距離の閾値に基づいて行われます。
1963
1964        Parameters:
1965        ------
1966            hotspots : list[HotspotData]
1967                重複を除外する対象のホットスポットのリスト。
1968            check_time_all : bool
1969                時間に関係なく重複チェックを行うかどうか。
1970            min_time_threshold_seconds : float
1971                重複とみなす最小時間の閾値(秒)。
1972            max_time_threshold_hours : float
1973                重複チェックを一時的に無視する最大時間の閾値(時間)。
1974            hotspot_area_meter : float
1975                重複とみなす距離の閾値(メートル)。
1976
1977        Returns:
1978        ------
1979            list[HotspotData]
1980                重複を除去したホットスポットのリスト。
1981        """
1982        # ΔCH4の降順でソート
1983        sorted_hotspots: list[HotspotData] = sorted(
1984            hotspots, key=lambda x: x.delta_ch4, reverse=True
1985        )
1986        used_positions_by_type: dict[
1987            HotspotType, list[tuple[float, float, str, float]]
1988        ] = {
1989            "bio": [],
1990            "gas": [],
1991            "comb": [],
1992        }
1993        unique_hotspots: list[HotspotData] = []
1994
1995        for spot in sorted_hotspots:
1996            is_duplicate = MobileSpatialAnalyzer._is_duplicate_spot(
1997                current_lat=spot.avg_lat,
1998                current_lon=spot.avg_lon,
1999                current_time=spot.source,
2000                used_positions=used_positions_by_type[spot.type],
2001                check_time_all=check_time_all,
2002                min_time_threshold_seconds=min_time_threshold_seconds,
2003                max_time_threshold_hours=max_time_threshold_hours,
2004                hotspot_area_meter=hotspot_area_meter,
2005            )
2006
2007            if not is_duplicate:
2008                unique_hotspots.append(spot)
2009                used_positions_by_type[spot.type].append(
2010                    (spot.avg_lat, spot.avg_lon, spot.source, spot.delta_ch4)
2011                )
2012
2013        return unique_hotspots

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

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

Parameters:

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

Returns:

list[HotspotData]
    重複を除去したホットスポットのリスト。
@staticmethod
def setup_logger(logger: logging.Logger | None, log_level: int = 20) -> logging.Logger:
2015    @staticmethod
2016    def setup_logger(logger: Logger | None, log_level: int = INFO) -> Logger:
2017        """
2018        ロガーを設定します。
2019
2020        このメソッドは、ロギングの設定を行い、ログメッセージのフォーマットを指定します。
2021        ログメッセージには、日付、ログレベル、メッセージが含まれます。
2022
2023        渡されたロガーがNoneまたは不正な場合は、新たにロガーを作成し、標準出力に
2024        ログメッセージが表示されるようにStreamHandlerを追加します。ロガーのレベルは
2025        引数で指定されたlog_levelに基づいて設定されます。
2026
2027        Parameters:
2028        ------
2029            logger : Logger | None
2030                使用するロガー。Noneの場合は新しいロガーを作成します。
2031            log_level : int
2032                ロガーのログレベル。デフォルトはINFO。
2033
2034        Returns:
2035        ------
2036            Logger
2037                設定されたロガーオブジェクト。
2038        """
2039        if logger is not None and isinstance(logger, Logger):
2040            return logger
2041        # 渡されたロガーがNoneまたは正しいものでない場合は独自に設定
2042        new_logger: Logger = getLogger()
2043        # 既存のハンドラーをすべて削除
2044        for handler in new_logger.handlers[:]:
2045            new_logger.removeHandler(handler)
2046        new_logger.setLevel(log_level)  # ロガーのレベルを設定
2047        ch = StreamHandler()
2048        ch_formatter = Formatter("%(asctime)s - %(levelname)s - %(message)s")
2049        ch.setFormatter(ch_formatter)  # フォーマッターをハンドラーに設定
2050        new_logger.addHandler(ch)  # StreamHandlerの追加
2051        return new_logger

ロガーを設定します。

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

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

Parameters:

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

Returns:

Logger
    設定されたロガーオブジェクト。
@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]]]:
2053    @staticmethod
2054    def calculate_emission_rates(
2055        hotspots: list[HotspotData],
2056        method: Literal["weller", "weitzel", "joo", "umezawa"] = "weller",
2057        print_summary: bool = True,
2058        custom_formulas: dict[str, dict[str, float]] | None = None,
2059    ) -> tuple[list[EmissionData], dict[str, dict[str, float]]]:
2060        """
2061        検出されたホットスポットのCH4漏出量を計算・解析し、統計情報を生成します。
2062
2063        Parameters:
2064        ------
2065            hotspots : list[HotspotData]
2066                分析対象のホットスポットのリスト
2067            method : Literal["weller", "weitzel", "joo", "umezawa"]
2068                使用する計算式。デフォルトは"weller"。
2069            print_summary : bool
2070                統計情報を表示するかどうか。デフォルトはTrue。
2071            custom_formulas : dict[str, dict[str, float]] | None
2072                カスタム計算式の係数。
2073                例: {"custom_method": {"a": 1.0, "b": 1.0}}
2074                Noneの場合はデフォルトの計算式を使用。
2075
2076        Returns:
2077        ------
2078            tuple[list[EmissionData], dict[str, dict[str, float]]]
2079                - 各ホットスポットの排出量データを含むリスト
2080                - タイプ別の統計情報を含む辞書
2081        """
2082        # デフォルトの経験式係数
2083        default_formulas = {
2084            "weller": {"a": 0.988, "b": 0.817},
2085            "weitzel": {"a": 0.521, "b": 0.795},
2086            "joo": {"a": 2.738, "b": 1.329},
2087            "umezawa": {"a": 2.716, "b": 0.741},
2088        }
2089
2090        # カスタム計算式がある場合は追加
2091        emission_formulas = default_formulas.copy()
2092        if custom_formulas:
2093            emission_formulas.update(custom_formulas)
2094
2095        if method not in emission_formulas:
2096            raise ValueError(f"Unknown method: {method}")
2097
2098        # 係数の取得
2099        a = emission_formulas[method]["a"]
2100        b = emission_formulas[method]["b"]
2101
2102        # 排出量の計算
2103        emission_data_list = []
2104        for spot in hotspots:
2105            # 漏出量の計算 (L/min)
2106            emission_rate = np.exp((np.log(spot.delta_ch4) + a) / b)
2107            # 日排出量 (L/day)
2108            daily_emission = emission_rate * 60 * 24
2109            # 年間排出量 (L/year)
2110            annual_emission = daily_emission * 365
2111
2112            emission_data = EmissionData(
2113                source=spot.source,
2114                type=spot.type,
2115                section=spot.section,
2116                latitude=spot.avg_lat,
2117                longitude=spot.avg_lon,
2118                delta_ch4=spot.delta_ch4,
2119                delta_c2h6=spot.delta_c2h6,
2120                ratio=spot.ratio,
2121                emission_rate=emission_rate,
2122                daily_emission=daily_emission,
2123                annual_emission=annual_emission,
2124            )
2125            emission_data_list.append(emission_data)
2126
2127        # 統計計算用にDataFrameを作成
2128        emission_df = pd.DataFrame([e.to_dict() for e in emission_data_list])
2129
2130        # タイプ別の統計情報を計算
2131        stats = {}
2132        # emission_formulas の定義の後に、排出量カテゴリーの閾値を定義
2133        emission_categories = {
2134            "low": {"min": 0, "max": 6},  # < 6 L/min
2135            "medium": {"min": 6, "max": 40},  # 6-40 L/min
2136            "high": {"min": 40, "max": float("inf")},  # > 40 L/min
2137        }
2138        # get_args(HotspotType)を使用して型安全なリストを作成
2139        types = list(get_args(HotspotType))
2140        for spot_type in types:
2141            df_type = emission_df[emission_df["type"] == spot_type]
2142            if len(df_type) > 0:
2143                # 既存の統計情報を計算
2144                type_stats = {
2145                    "count": len(df_type),
2146                    "emission_rate_min": df_type["emission_rate"].min(),
2147                    "emission_rate_max": df_type["emission_rate"].max(),
2148                    "emission_rate_mean": df_type["emission_rate"].mean(),
2149                    "emission_rate_median": df_type["emission_rate"].median(),
2150                    "total_annual_emission": df_type["annual_emission"].sum(),
2151                    "mean_annual_emission": df_type["annual_emission"].mean(),
2152                }
2153
2154                # 排出量カテゴリー別の統計を追加
2155                category_counts = {
2156                    "low": len(
2157                        df_type[
2158                            df_type["emission_rate"] < emission_categories["low"]["max"]
2159                        ]
2160                    ),
2161                    "medium": len(
2162                        df_type[
2163                            (
2164                                df_type["emission_rate"]
2165                                >= emission_categories["medium"]["min"]
2166                            )
2167                            & (
2168                                df_type["emission_rate"]
2169                                < emission_categories["medium"]["max"]
2170                            )
2171                        ]
2172                    ),
2173                    "high": len(
2174                        df_type[
2175                            df_type["emission_rate"]
2176                            >= emission_categories["high"]["min"]
2177                        ]
2178                    ),
2179                }
2180                type_stats["emission_categories"] = category_counts
2181
2182                stats[spot_type] = type_stats
2183
2184                if print_summary:
2185                    print(f"\n{spot_type}タイプの統計情報:")
2186                    print(f"  検出数: {type_stats['count']}")
2187                    print("  排出量 (L/min):")
2188                    print(f"    最小値: {type_stats['emission_rate_min']:.2f}")
2189                    print(f"    最大値: {type_stats['emission_rate_max']:.2f}")
2190                    print(f"    平均値: {type_stats['emission_rate_mean']:.2f}")
2191                    print(f"    中央値: {type_stats['emission_rate_median']:.2f}")
2192                    print("  排出量カテゴリー別の検出数:")
2193                    print(f"    低放出 (< 6 L/min): {category_counts['low']}")
2194                    print(f"    中放出 (6-40 L/min): {category_counts['medium']}")
2195                    print(f"    高放出 (> 40 L/min): {category_counts['high']}")
2196                    print("  年間排出量 (L/year):")
2197                    print(f"    合計: {type_stats['total_annual_emission']:.2f}")
2198                    print(f"    平均: {type_stats['mean_annual_emission']:.2f}")
2199
2200        return emission_data_list, stats

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

Parameters:

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

Returns:

tuple[list[EmissionData], dict[str, dict[str, float]]]
    - 各ホットスポットの排出量データを含むリスト
    - タイプ別の統計情報を含む辞書
@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:
2202    @staticmethod
2203    def plot_emission_analysis(
2204        emission_data_list: list[EmissionData],
2205        dpi: int = 300,
2206        output_dir: str | Path | None = None,
2207        output_filename: str = "emission_analysis.png",
2208        figsize: tuple[float, float] = (12, 5),
2209        add_legend: bool = True,
2210        hist_log_y: bool = False,
2211        hist_xlim: tuple[float, float] | None = None,
2212        hist_ylim: tuple[float, float] | None = None,
2213        scatter_xlim: tuple[float, float] | None = None,
2214        scatter_ylim: tuple[float, float] | None = None,
2215        hist_bin_width: float = 0.5,
2216        print_summary: bool = True,
2217        save_fig: bool = False,
2218        show_fig: bool = True,
2219        show_scatter: bool = True,  # 散布図の表示を制御するオプションを追加
2220    ) -> None:
2221        """
2222        排出量分析のプロットを作成する静的メソッド。
2223
2224        Parameters:
2225        ------
2226            emission_data_list : list[EmissionData]
2227                EmissionDataオブジェクトのリスト。
2228            output_dir : str | Path | None
2229                出力先ディレクトリのパス。
2230            output_filename : str
2231                保存するファイル名。デフォルトは"emission_analysis.png"。
2232            dpi : int
2233                プロットの解像度。デフォルトは300。
2234            figsize : tuple[float, float]
2235                プロットのサイズ。デフォルトは(12, 5)。
2236            add_legend : bool
2237                凡例を追加するかどうか。デフォルトはTrue。
2238            hist_log_y : bool
2239                ヒストグラムのy軸を対数スケールにするかどうか。デフォルトはFalse。
2240            hist_xlim : tuple[float, float] | None
2241                ヒストグラムのx軸の範囲。デフォルトはNone。
2242            hist_ylim : tuple[float, float] | None
2243                ヒストグラムのy軸の範囲。デフォルトはNone。
2244            scatter_xlim : tuple[float, float] | None
2245                散布図のx軸の範囲。デフォルトはNone。
2246            scatter_ylim : tuple[float, float] | None
2247                散布図のy軸の範囲。デフォルトはNone。
2248            hist_bin_width : float
2249                ヒストグラムのビンの幅。デフォルトは0.5。
2250            print_summary : bool
2251                集計結果を表示するかどうか。デフォルトはFalse。
2252            save_fig : bool
2253                図をファイルに保存するかどうか。デフォルトはFalse。
2254            show_fig : bool
2255                図を表示するかどうか。デフォルトはTrue。
2256            show_scatter : bool
2257                散布図(右図)を表示するかどうか。デフォルトはTrue。
2258        """
2259        # データをDataFrameに変換
2260        df = pd.DataFrame([e.to_dict() for e in emission_data_list])
2261
2262        # プロットの作成(散布図の有無に応じてサブプロット数を調整)
2263        if show_scatter:
2264            fig, (ax1, ax2) = plt.subplots(1, 2, figsize=figsize)
2265            axes = [ax1, ax2]
2266        else:
2267            fig, ax1 = plt.subplots(1, 1, figsize=(figsize[0] // 2, figsize[1]))
2268            axes = [ax1]
2269
2270        # カラーマップの定義
2271        colors: dict[HotspotType, str] = {"bio": "blue", "gas": "red", "comb": "green"}
2272
2273        # 存在するタイプを確認
2274        # HotspotTypeの定義順を基準にソート
2275        hotspot_types = list(get_args(HotspotType))
2276        existing_types = sorted(
2277            df["type"].unique(), key=lambda x: hotspot_types.index(x)
2278        )
2279
2280        # 左側: ヒストグラム
2281        # ビンの範囲を設定
2282        start = 0  # 必ず0から開始
2283        if hist_xlim is not None:
2284            end = hist_xlim[1]
2285        else:
2286            end = np.ceil(df["emission_rate"].max() * 1.05)
2287
2288        # ビン数を計算(end値をbin_widthで割り切れるように調整)
2289        n_bins = int(np.ceil(end / hist_bin_width))
2290        end = n_bins * hist_bin_width
2291
2292        # ビンの生成(0から開始し、bin_widthの倍数で区切る)
2293        bins = np.linspace(start, end, n_bins + 1)
2294
2295        # タイプごとにヒストグラムを積み上げ
2296        bottom = np.zeros(len(bins) - 1)
2297        for spot_type in existing_types:
2298            data = df[df["type"] == spot_type]["emission_rate"]
2299            if len(data) > 0:
2300                counts, _ = np.histogram(data, bins=bins)
2301                ax1.bar(
2302                    bins[:-1],
2303                    counts,
2304                    width=hist_bin_width,
2305                    bottom=bottom,
2306                    alpha=0.6,
2307                    label=spot_type,
2308                    color=colors[spot_type],
2309                )
2310                bottom += counts
2311
2312        ax1.set_xlabel("CH$_4$ Emission (L min$^{-1}$)")
2313        ax1.set_ylabel("Frequency")
2314        if hist_log_y:
2315            # ax1.set_yscale("log")
2316            # 非線形スケールを設定(linthreshで線形から対数への遷移点を指定)
2317            ax1.set_yscale("symlog", linthresh=1.0)
2318        if hist_xlim is not None:
2319            ax1.set_xlim(hist_xlim)
2320        else:
2321            ax1.set_xlim(0, np.ceil(df["emission_rate"].max() * 1.05))
2322
2323        if hist_ylim is not None:
2324            ax1.set_ylim(hist_ylim)
2325        else:
2326            ax1.set_ylim(0, ax1.get_ylim()[1])  # 下限を0に設定
2327
2328        if show_scatter:
2329            # 右側: 散布図
2330            for spot_type in existing_types:
2331                mask = df["type"] == spot_type
2332                ax2.scatter(
2333                    df[mask]["emission_rate"],
2334                    df[mask]["delta_ch4"],
2335                    alpha=0.6,
2336                    label=spot_type,
2337                    color=colors[spot_type],
2338                )
2339
2340            ax2.set_xlabel("Emission Rate (L min$^{-1}$)")
2341            ax2.set_ylabel("ΔCH$_4$ (ppm)")
2342            if scatter_xlim is not None:
2343                ax2.set_xlim(scatter_xlim)
2344            else:
2345                ax2.set_xlim(0, np.ceil(df["emission_rate"].max() * 1.05))
2346
2347            if scatter_ylim is not None:
2348                ax2.set_ylim(scatter_ylim)
2349            else:
2350                ax2.set_ylim(0, np.ceil(df["delta_ch4"].max() * 1.05))
2351
2352        # 凡例の表示
2353        if add_legend:
2354            for ax in axes:
2355                ax.legend(
2356                    bbox_to_anchor=(0.5, -0.30),
2357                    loc="upper center",
2358                    ncol=len(existing_types),
2359                )
2360
2361        plt.tight_layout()
2362
2363        # 図の保存
2364        if save_fig:
2365            if output_dir is None:
2366                raise ValueError(
2367                    "save_fig=Trueの場合、output_dirを指定する必要があります。有効なディレクトリパスを指定してください。"
2368                )
2369            os.makedirs(output_dir, exist_ok=True)
2370            output_path = os.path.join(output_dir, output_filename)
2371            plt.savefig(output_path, bbox_inches="tight", dpi=dpi)
2372        # 図の表示
2373        if show_fig:
2374            plt.show()
2375        else:
2376            plt.close(fig=fig)
2377
2378        if print_summary:
2379            # デバッグ用の出力
2380            print("\nビンごとの集計:")
2381            print(f"{'Range':>12} | {'bio':>8} | {'gas':>8} | {'total':>8}")
2382            print("-" * 50)
2383
2384            for i in range(len(bins) - 1):
2385                bin_start = bins[i]
2386                bin_end = bins[i + 1]
2387
2388                # 各タイプのカウントを計算
2389                counts_by_type: dict[HotspotType, int] = {"bio": 0, "gas": 0, "comb": 0}
2390                total = 0
2391                for spot_type in existing_types:
2392                    mask = (
2393                        (df["type"] == spot_type)
2394                        & (df["emission_rate"] >= bin_start)
2395                        & (df["emission_rate"] < bin_end)
2396                    )
2397                    count = len(df[mask])
2398                    counts_by_type[spot_type] = count
2399                    total += count
2400
2401                # カウントが0の場合はスキップ
2402                if total > 0:
2403                    range_str = f"{bin_start:5.1f}-{bin_end:<5.1f}"
2404                    bio_count = counts_by_type.get("bio", 0)
2405                    gas_count = counts_by_type.get("gas", 0)
2406                    print(
2407                        f"{range_str:>12} | {bio_count:8d} | {gas_count:8d} | {total:8d}"
2408                    )

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

Parameters:

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

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

Parameters:

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

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

Parameters:

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

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

Parameters:

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

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

Parameters:

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

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

Parameters:

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

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

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

Parameters:

fs : float
    サンプリング周波数。
lag_second : float
    ラグ時間(秒)。
input_dir : str | Path | None
    データファイルが格納されているディレクトリ。
output_dir : str | Path | None
    出力先ディレクトリ。
col_ch4 : str, optional
    CH4の濃度データが入ったカラムのキー。デフォルトは"Ultra_CH4_ppm_C"。
col_c2h6 : str, optional
    C2H6の濃度データが入ったカラムのキー。デフォルトは"Ultra_C2H6_ppb"。
col_tv : str, optional
    気温データが入ったカラムのキー。デフォルトは"Tv"。
label_ch4 : str | None, optional
    CH4のラベル。デフォルトはNone。
label_c2h6 : str | None, optional
    C2H6のラベル。デフォルトはNone。
label_tv : str | None, optional
    気温のラベル。デフォルトはNone。
file_pattern : str, optional
    処理対象のファイルパターン。デフォルトは"*.csv"。
markersize : float, optional
    プロットマーカーのサイズ。デフォルトは14。
are_inputs_resampled : bool, optional
    入力データが再サンプリングされているかどうか。デフォルトはTrue。
save_fig : bool, optional
    図を保存するかどうか。デフォルトはTrue。
show_fig : bool, optional
    図を表示するかどうか。デフォルトはTrue。
plot_power : bool, optional
    パワースペクトルをプロットするかどうか。デフォルトはTrue。
plot_co : bool, optional
    COのスペクトルをプロットするかどうか。デフォルトはTrue。
add_tv_in_co : bool, optional
    顕熱フラックスのコスペクトルを表示するかどうか。デフォルトはTrue。
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:
2290    def plot_turbulence(
2291        self,
2292        df: pd.DataFrame,
2293        output_dir: str,
2294        output_filename: str = "turbulence.png",
2295        col_uz: str = "Uz",
2296        col_ch4: str = "Ultra_CH4_ppm_C",
2297        col_c2h6: str = "Ultra_C2H6_ppb",
2298        col_timestamp: str = "TIMESTAMP",
2299        add_serial_labels: bool = True,
2300    ) -> None:
2301        """時系列データのプロットを作成する
2302
2303        Parameters:
2304        ------
2305            df : pd.DataFrame
2306                プロットするデータを含むDataFrame
2307            output_dir : str
2308                出力ディレクトリのパス
2309            output_filename : str
2310                出力ファイル名
2311            col_uz : str
2312                鉛直風速データのカラム名
2313            col_ch4 : str
2314                メタンデータのカラム名
2315            col_c2h6 : str
2316                エタンデータのカラム名
2317            col_timestamp : str
2318                タイムスタンプのカラム名
2319        """
2320        # 出力ディレクトリの作成
2321        os.makedirs(output_dir, exist_ok=True)
2322        output_path: str = os.path.join(output_dir, output_filename)
2323
2324        # データの前処理
2325        df = df.copy()
2326
2327        # タイムスタンプをインデックスに設定(まだ設定されていない場合)
2328        if not isinstance(df.index, pd.DatetimeIndex):
2329            df[col_timestamp] = pd.to_datetime(df[col_timestamp])
2330            df.set_index(col_timestamp, inplace=True)
2331
2332        # 開始時刻と終了時刻を取得
2333        start_time = df.index[0]
2334        end_time = df.index[-1]
2335
2336        # 開始時刻の分を取得
2337        start_minute = start_time.minute
2338
2339        # 時間軸の作成(実際の開始時刻からの経過分数)
2340        minutes_elapsed = (df.index - start_time).total_seconds() / 60
2341
2342        # プロットの作成
2343        _, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=(12, 10), sharex=True)
2344
2345        # 鉛直風速
2346        ax1.plot(minutes_elapsed, df[col_uz], "k-", linewidth=0.5)
2347        ax1.set_ylabel(r"$w$ (m s$^{-1}$)")
2348        if add_serial_labels:
2349            ax1.text(0.02, 0.98, "(a)", transform=ax1.transAxes, va="top")
2350        ax1.grid(True, alpha=0.3)
2351
2352        # CH4濃度
2353        ax2.plot(minutes_elapsed, df[col_ch4], "r-", linewidth=0.5)
2354        ax2.set_ylabel(r"$\mathrm{CH_4}$ (ppm)")
2355        if add_serial_labels:
2356            ax2.text(0.02, 0.98, "(b)", transform=ax2.transAxes, va="top")
2357        ax2.grid(True, alpha=0.3)
2358
2359        # C2H6濃度
2360        ax3.plot(minutes_elapsed, df[col_c2h6], "orange", linewidth=0.5)
2361        ax3.set_ylabel(r"$\mathrm{C_2H_6}$ (ppb)")
2362        if add_serial_labels:
2363            ax3.text(0.02, 0.98, "(c)", transform=ax3.transAxes, va="top")
2364        ax3.grid(True, alpha=0.3)
2365        ax3.set_xlabel("Time (minutes)")
2366
2367        # x軸の範囲を実際の開始時刻から30分後までに設定
2368        total_minutes = (end_time - start_time).total_seconds() / 60
2369        ax3.set_xlim(0, min(30, total_minutes))
2370
2371        # x軸の目盛りを5分間隔で設定
2372        np.arange(start_minute, start_minute + 35, 5)
2373        ax3.xaxis.set_major_locator(MultipleLocator(5))
2374
2375        # レイアウトの調整
2376        plt.tight_layout()
2377
2378        # 図の保存
2379        plt.savefig(output_path, dpi=300, bbox_inches="tight")
2380        plt.close()

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

Parameters:

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

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

Parameters:

df : pd.DataFrame
    風配図を作成するためのデータフレーム
output_dir : str | Path | None
    生成された図を保存するディレクトリのパス
output_filename : str
    保存するファイル名(デフォルトは"edp_wind_rose.png")
col_ch4_flux : str
    CH4フラックスを示すカラム名
col_c2h6_flux : str
    C2H6フラックスを示すカラム名
col_wind_dir : str
    風向を示すカラム名
label_gas : str
    都市ガス起源のフラックスに対するラベル
label_bio : str
    生物起源のフラックスに対するラベル
col_datetime : str
    日時を示すカラム名
num_directions : int
    風向の数(デフォルトは8)
center_on_angles: bool
    Trueの場合、45度刻みの線を境界として扇形を描画します。
    Falseの場合、45度の中間(22.5度)を中心として扇形を描画します。
subplot_label : str
    サブプロットに表示するラベル
print_summary : bool
    統計情報を表示するかどうかのフラグ
flux_unit : str
    フラックスの単位
ymax : float | None
    y軸の上限値(指定しない場合はデータの最大値に基づいて自動設定)
figsize : tuple[float, float]
    図のサイズ
flux_alpha : float
    フラックスの透明度
stack_bars : bool, optional
    Trueの場合、生物起源の上に都市ガス起源を積み上げます。
    Falseの場合、両方を0から積み上げます(デフォルト)。
save_fig : bool
    図を保存するかどうかのフラグ
show_fig : bool
    図を表示するかどうかのフラグ
@staticmethod
def get_valid_data( df: pandas.core.frame.DataFrame, x_col: str, y_col: str) -> pandas.core.frame.DataFrame:
2895    @staticmethod
2896    def get_valid_data(df: pd.DataFrame, x_col: str, y_col: str) -> pd.DataFrame:
2897        """
2898        指定された列の有効なデータ(NaNを除いた)を取得します。
2899
2900        Parameters:
2901        ------
2902            df : pd.DataFrame
2903                データフレーム
2904            x_col : str
2905                X軸の列名
2906            y_col : str
2907                Y軸の列名
2908
2909        Returns:
2910        ------
2911            pd.DataFrame
2912                有効なデータのみを含むDataFrame
2913        """
2914        return df.copy().dropna(subset=[x_col, y_col])

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

Parameters:

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

Returns:

pd.DataFrame
    有効なデータのみを含むDataFrame
@staticmethod
def setup_logger(logger: logging.Logger | None, log_level: int = 20) -> logging.Logger:
2916    @staticmethod
2917    def setup_logger(logger: Logger | None, log_level: int = INFO) -> Logger:
2918        """
2919        ロガーを設定します。
2920
2921        このメソッドは、ロギングの設定を行い、ログメッセージのフォーマットを指定します。
2922        ログメッセージには、日付、ログレベル、メッセージが含まれます。
2923
2924        渡されたロガーがNoneまたは不正な場合は、新たにロガーを作成し、標準出力に
2925        ログメッセージが表示されるようにStreamHandlerを追加します。ロガーのレベルは
2926        引数で指定されたlog_levelに基づいて設定されます。
2927
2928        Parameters:
2929        ------
2930            logger : Logger | None
2931                使用するロガー。Noneの場合は新しいロガーを作成します。
2932            log_level : int
2933                ロガーのログレベル。デフォルトはINFO。
2934
2935        Returns:
2936        ------
2937            Logger
2938                設定されたロガーオブジェクト。
2939        """
2940        if logger is not None and isinstance(logger, Logger):
2941            return logger
2942        # 渡されたロガーがNoneまたは正しいものでない場合は独自に設定
2943        new_logger: Logger = getLogger()
2944        # 既存のハンドラーをすべて削除
2945        for handler in new_logger.handlers[:]:
2946            new_logger.removeHandler(handler)
2947        new_logger.setLevel(log_level)  # ロガーのレベルを設定
2948        ch = StreamHandler()
2949        ch_formatter = Formatter("%(asctime)s - %(levelname)s - %(message)s")
2950        ch.setFormatter(ch_formatter)  # フォーマッターをハンドラーに設定
2951        new_logger.addHandler(ch)  # StreamHandlerの追加
2952        return new_logger

ロガーを設定します。

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

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

Parameters:

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

Returns:

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

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

Parameters:

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

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

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

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

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

Parameters:

input_dir : str
    入力ファイルが格納されているディレクトリのパス
output_dir : str
    出力ファイルを格納するディレクトリのパス
flag_csv_path : str
    フラグ情報が記載されているCSVファイルのパス
filename_patterns : list[str] | None
    ファイル名のパターン(正規表現)のリスト
output_dirs_struct : dict[str, str] | None
    出力ディレクトリの構造を定義する辞書
sort_by_rh : bool
    RHに基づいてサブディレクトリにファイルを分類するかどうか
logger : Logger | None
    使用するロガー
logging_debug : bool
    ログレベルをDEBUGに設定するかどうか
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

このクラスは、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
    伝達関数の値。