py_flux_tracer

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

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

Parameters

fs (float): サンプリング周波数。
logger (Logger | None): 使用するロガー。Noneの場合は新しいロガーを作成します。
logging_debug (bool): ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。
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, column_mapping: dict[str, str] = {'u_m': 'Ux', 'v_m': 'Uy', 'w_m': 'Uz'}) -> pandas.core.frame.DataFrame:
 47    def add_uvw_columns(
 48        self, 
 49        df: pd.DataFrame,
 50        column_mapping: dict[str, str] = {
 51            "u_m": "Ux",
 52            "v_m": "Uy",
 53            "w_m": "Uz"
 54        },
 55    ) -> pd.DataFrame:
 56        """
 57        DataFrameに水平風速u、v、鉛直風速wの列を追加する関数。
 58        各成分のキーは`edp_wind_u`、`edp_wind_v`、`edp_wind_w`である。
 59
 60        Parameters
 61        ----------
 62            df : pd.DataFrame
 63                風速データを含むDataFrame
 64            column_mapping : dict[str, str]
 65                入力データのカラム名マッピング。
 66                キーは"u_m", "v_m", "w_m"で、値は対応する入力データのカラム名。
 67                デフォルトは{"u_m": "Ux", "v_m": "Uy", "w_m": "Uz"}。
 68
 69        Returns
 70        ----------
 71            pd.DataFrame
 72                水平風速u、v、鉛直風速wの列を追加したDataFrame
 73
 74        Raises
 75        ----------
 76            ValueError
 77                必要なカラムが存在しない場合、またはマッピングに必要なキーが不足している場合
 78        """
 79        required_keys = ["u_m", "v_m", "w_m"]
 80        # マッピングに必要なキーが存在するか確認
 81        for key in required_keys:
 82            if key not in column_mapping:
 83                raise ValueError(f"column_mappingに必要なキー '{key}' が存在しません。")
 84
 85        # 必要な列がDataFrameに存在するか確認
 86        for key, column in column_mapping.items():
 87            if column not in df.columns:
 88                raise ValueError(f"必要な列 '{column}' (mapped from '{key}') がDataFrameに存在しません。")
 89
 90        df_copied: pd.DataFrame = df.copy()
 91        # pandasの.valuesを使用してnumpy配列を取得し、その型をnp.ndarrayに明示的にキャストする
 92        wind_x_array: np.ndarray = np.array(df_copied[column_mapping["u_m"]].values)
 93        wind_y_array: np.ndarray = np.array(df_copied[column_mapping["v_m"]].values)
 94        wind_z_array: np.ndarray = np.array(df_copied[column_mapping["w_m"]].values)
 95
 96        # 平均風向を計算
 97        wind_direction: float = EddyDataPreprocessor._wind_direction(
 98            wind_x_array, wind_y_array
 99        )
100
101        # 水平方向に座標回転を行u, v成分を求める
102        wind_u_array, wind_v_array = EddyDataPreprocessor._horizontal_wind_speed(
103            wind_x_array, wind_y_array, wind_direction
104        )
105        wind_w_array: np.ndarray = wind_z_array  # wはz成分そのまま
106
107        # u, wから風の迎角を計算
108        wind_inclination: float = EddyDataPreprocessor._wind_inclination(
109            wind_u_array, wind_w_array
110        )
111
112        # 2回座標回転を行い、u, wを求める
113        wind_u_array_rotated, wind_w_array_rotated = (
114            EddyDataPreprocessor._vertical_rotation(
115                wind_u_array, wind_w_array, wind_inclination
116            )
117        )
118
119        df_copied[self.WIND_U] = wind_u_array_rotated
120        df_copied[self.WIND_V] = wind_v_array
121        df_copied[self.WIND_W] = wind_w_array_rotated
122        df_copied[self.RAD_WIND_DIR] = wind_direction
123        df_copied[self.RAD_WIND_INC] = wind_inclination
124        df_copied[self.DEGREE_WIND_DIR] = np.degrees(wind_direction)
125        df_copied[self.DEGREE_WIND_INC] = np.degrees(wind_inclination)
126
127        return df_copied

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

Parameters

df : pd.DataFrame
    風速データを含むDataFrame
column_mapping : dict[str, str]
    入力データのカラム名マッピング。
    キーは"u_m", "v_m", "w_m"で、値は対応する入力データのカラム名。
    デフォルトは{"u_m": "Ux", "v_m": "Uy", "w_m": "Uz"}。

Returns

pd.DataFrame
    水平風速u、v、鉛直風速wの列を追加したDataFrame

Raises

ValueError
    必要なカラムが存在しない場合、またはマッピングに必要なキーが不足している場合
def analyze_lag_times( self, input_dir: str | pathlib.Path, 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, output_dir: str | pathlib.Path | None = None, output_tag: str = '', plot_range_tuple: tuple = (-50, 200), add_title: bool = True, xlabel: str | None = 'Seconds', ylabel: str | None = 'Frequency', print_results: bool = True, index_column: str = 'TIMESTAMP', index_format: str = '%Y-%m-%d %H:%M:%S.%f', resample_in_processing: bool = False, 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], add_uvw_columns: bool = True, uvw_column_mapping: dict[str, str] = {'u_m': 'Ux', 'v_m': 'Uy', 'w_m': 'Uz'}) -> dict[str, float]:
129    def analyze_lag_times(
130        self,
131        input_dir: str | Path,
132        figsize: tuple[float, float] = (10, 8),
133        input_files_pattern: str = r"Eddy_(\d+)",
134        input_files_suffix: str = ".dat",
135        col1: str = "edp_wind_w",
136        col2_list: list[str] = ["Tv"],
137        median_range: float = 20,
138        output_dir: str | Path | None = None,
139        output_tag: str = "",
140        plot_range_tuple: tuple = (-50, 200),
141        add_title: bool = True,
142        xlabel: str | None = "Seconds",
143        ylabel: str | None = "Frequency",
144        print_results: bool = True,
145        index_column: str = "TIMESTAMP",
146        index_format: str = "%Y-%m-%d %H:%M:%S.%f",
147        resample_in_processing: bool = False,
148        interpolate: bool = True,
149        numeric_columns: list[str] = [
150            "Ux",
151            "Uy",
152            "Uz",
153            "Tv",
154            "diag_sonic",
155            "CO2_new",
156            "H2O",
157            "diag_irga",
158            "cell_tmpr",
159            "cell_press",
160            "Ultra_CH4_ppm",
161            "Ultra_C2H6_ppb",
162            "Ultra_H2O_ppm",
163            "Ultra_CH4_ppm_C",
164            "Ultra_C2H6_ppb_C",
165        ],
166        metadata_rows: int = 4,
167        skiprows: list[int] = [0, 2, 3],
168        add_uvw_columns: bool = True,
169        uvw_column_mapping: dict[str, str] = {
170            "u_m": "Ux",
171            "v_m": "Uy",
172            "w_m": "Uz"
173        },
174    ) -> dict[str, float]:
175        """
176        遅れ時間(ラグ)の統計分析を行い、指定されたディレクトリ内のデータファイルを処理します。
177        解析結果とメタデータはCSVファイルとして出力されます。
178
179        Parameters
180        ----------
181            input_dir : str | Path
182                入力データファイルが格納されているディレクトリのパス。
183            figsize : tuple[float, float]
184                プロットのサイズ(幅、高さ)。
185            input_files_pattern : str
186                入力ファイル名のパターン(正規表現)。
187            input_files_suffix : str
188                入力ファイルの拡張子。
189            col1 : str
190                基準変数の列名。
191            col2_list : list[str]
192                比較変数の列名のリスト。
193            median_range : float
194                中央値を中心とした範囲。
195            output_dir : str | Path | None
196                出力ディレクトリのパス。Noneの場合は保存しない。
197            output_tag : str
198                出力ファイルに付与するタグ。デフォルトは空文字で、何も付与されない。
199            plot_range_tuple : tuple
200                ヒストグラムの表示範囲。
201            add_title : bool
202                プロットにタイトルを追加するかどうか。デフォルトはTrue。
203            xlabel : str | None
204                x軸のラベル。デフォルトは"Seconds"。
205            ylabel : str | None
206                y軸のラベル。デフォルトは"Frequency"。
207            print_results : bool
208                結果をコンソールに表示するかどうか。
209            resample_in_processing : bool
210                データを遅れ時間の計算中にリサンプリングするかどうか。
211                inputするファイルが既にリサンプリング済みの場合はFalseでよい。
212                デフォルトはFalse。
213            interpolate : bool
214                欠損値の補完を適用するフラグ。デフォルトはTrue。
215            numeric_columns : list[str]
216                数値型に変換する列名のリスト。デフォルトは特定の列名のリスト。
217            metadata_rows : int
218                メタデータの行数。
219            skiprows : list[int]
220                スキップする行番号のリスト。
221            add_uvw_columns : bool
222                u, v, wの列を追加するかどうか。デフォルトはTrue。
223            uvw_column_mapping : dict[str, str]
224                u, v, wの列名をマッピングする辞書。デフォルトは以下の通り。
225                {
226                    "u_m": "Ux",
227                    "v_m": "Uy",
228                    "w_m": "Uz"
229                }
230
231        Returns
232        ----------
233            dict[str, float]
234                各変数の遅れ時間(平均値を採用)を含む辞書。
235        """
236        if output_dir is None:
237            self.logger.warn(
238                "output_dirが指定されていません。解析結果を保存する場合は、有効なディレクトリを指定してください。"
239            )
240        all_lags_indices: list[list[int]] = []
241        results: dict[str, float] = {}
242
243        # メイン処理
244        # ファイル名に含まれる数字に基づいてソート
245        csv_files = EddyDataPreprocessor._get_sorted_files(
246            input_dir, input_files_pattern, input_files_suffix
247        )
248        if not csv_files:
249            raise FileNotFoundError(
250                f"There is no '{input_files_suffix}' file to process; input_dir: '{input_dir}', input_files_suffix: '{input_files_suffix}'"
251            )
252
253        for file in tqdm(csv_files, desc="Calculating"):
254            path: str = os.path.join(input_dir, file)
255            df: pd.DataFrame = {} # 未定義エラーを防止
256            if resample_in_processing:
257                df, _ = self.get_resampled_df(
258                    filepath=path,
259                    metadata_rows=metadata_rows,
260                    skiprows=skiprows,
261                    index_column=index_column,
262                    index_format=index_format,
263                    numeric_columns=numeric_columns,
264                    interpolate=interpolate,
265                    resample=resample_in_processing,
266                )
267            else:
268                df = pd.read_csv(path, skiprows=skiprows)
269            if add_uvw_columns:
270                df = self.add_uvw_columns(df=df, column_mapping=uvw_column_mapping)
271            lags_list = EddyDataPreprocessor._calculate_lag_time(
272                df=df,
273                col1=col1,
274                col2_list=col2_list,
275            )
276            all_lags_indices.append(lags_list)
277        self.logger.info("すべてのCSVファイルにおける遅れ時間が計算されました。")
278
279        # Convert all_lags_indices to a DataFrame
280        lags_indices_df: pd.DataFrame = pd.DataFrame(
281            all_lags_indices, columns=col2_list
282        )
283
284        # フォーマット用のキーの最大の長さ
285        max_col_name_length: int = max(
286            len(column) for column in lags_indices_df.columns
287        )
288
289        if print_results:
290            self.logger.info(f"カラム`{col1}`に対する遅れ時間を表示します。")
291
292        # 結果を格納するためのリスト
293        output_data = []
294
295        for column in lags_indices_df.columns:
296            data: pd.Series = lags_indices_df[column]
297
298            # ヒストグラムの作成
299            fig = plt.figure(figsize=figsize)
300            plt.hist(data, bins=20, range=plot_range_tuple)
301            if add_title:
302                plt.title(f"Delays of {column}")
303            plt.xlabel(xlabel)
304            plt.ylabel(ylabel)
305            plt.xlim(plot_range_tuple)
306
307            # ファイルとして保存するか
308            if output_dir is not None:
309                os.makedirs(output_dir, exist_ok=True)
310                filename: str = f"lags_histogram-{column}{output_tag}.png"
311                filepath: str = os.path.join(output_dir, filename)
312                plt.savefig(filepath, dpi=300, bbox_inches="tight")
313                plt.close(fig=fig)
314
315            # 中央値を計算し、その周辺のデータのみを使用
316            median_value = np.median(data)
317            filtered_data: pd.Series = data[
318                (data >= median_value - median_range)
319                & (data <= median_value + median_range)
320            ]
321
322            # 平均値を計算
323            mean_value = np.mean(filtered_data)
324            mean_seconds: float = float(mean_value / self.fs)  # 統計値を秒に変換
325            results[column] = mean_seconds
326
327            # 結果とメタデータを出力データに追加
328            output_data.append(
329                {
330                    "col1": col1,
331                    "col2": column,
332                    "col2_lag": round(mean_seconds, 2),  # 数値として小数点2桁を保持
333                    "lag_unit": "s",
334                    "median_range": median_range,
335                }
336            )
337
338            if print_results:
339                print(f"{column:<{max_col_name_length}} : {mean_seconds:.2f} s")
340
341        # 結果をCSVファイルとして出力
342        if output_dir is not None:
343            output_df: pd.DataFrame = pd.DataFrame(output_data)
344            csv_filepath: str = os.path.join(
345                output_dir, f"lags_results{output_tag}.csv"
346            )
347            output_df.to_csv(csv_filepath, index=False, encoding="utf-8")
348            self.logger.info(f"解析結果をCSVファイルに保存しました: {csv_filepath}")
349
350        return results

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

Parameters

input_dir : str | Path
    入力データファイルが格納されているディレクトリのパス。
figsize : tuple[float, float]
    プロットのサイズ(幅、高さ)。
input_files_pattern : str
    入力ファイル名のパターン(正規表現)。
input_files_suffix : str
    入力ファイルの拡張子。
col1 : str
    基準変数の列名。
col2_list : list[str]
    比較変数の列名のリスト。
median_range : float
    中央値を中心とした範囲。
output_dir : str | Path | None
    出力ディレクトリのパス。Noneの場合は保存しない。
output_tag : str
    出力ファイルに付与するタグ。デフォルトは空文字で、何も付与されない。
plot_range_tuple : tuple
    ヒストグラムの表示範囲。
add_title : bool
    プロットにタイトルを追加するかどうか。デフォルトはTrue。
xlabel : str | None
    x軸のラベル。デフォルトは"Seconds"。
ylabel : str | None
    y軸のラベル。デフォルトは"Frequency"。
print_results : bool
    結果をコンソールに表示するかどうか。
resample_in_processing : bool
    データを遅れ時間の計算中にリサンプリングするかどうか。
    inputするファイルが既にリサンプリング済みの場合はFalseでよい。
    デフォルトはFalse。
interpolate : bool
    欠損値の補完を適用するフラグ。デフォルトはTrue。
numeric_columns : list[str]
    数値型に変換する列名のリスト。デフォルトは特定の列名のリスト。
metadata_rows : int
    メタデータの行数。
skiprows : list[int]
    スキップする行番号のリスト。
add_uvw_columns : bool
    u, v, wの列を追加するかどうか。デフォルトはTrue。
uvw_column_mapping : dict[str, str]
    u, v, wの列名をマッピングする辞書。デフォルトは以下の通り。
    {
        "u_m": "Ux",
        "v_m": "Uy",
        "w_m": "Uz"
    }

Returns

dict[str, float]
    各変数の遅れ時間(平均値を採用)を含む辞書。
def get_generated_columns_names(self, print: bool = True) -> list[str]:
352    def get_generated_columns_names(self, print: bool = True) -> list[str]:
353        """
354        クラス内部で生成されるカラム名を取得する。
355
356        Parameters
357        ----------
358            print : bool
359                print()で表示するか。デフォルトはTrue。
360
361        Returns
362        ----------
363            list[str]
364                生成されるカラム名のリスト。
365        """
366        list_cols: list[str] = [
367            self.WIND_U,
368            self.WIND_V,
369            self.WIND_W,
370            self.RAD_WIND_DIR,
371            self.RAD_WIND_INC,
372            self.DEGREE_WIND_DIR,
373            self.DEGREE_WIND_INC,
374        ]
375        if print:
376            print(list_cols)
377        return list_cols

クラス内部で生成されるカラム名を取得する。

Parameters

print : bool
    print()で表示するか。デフォルトはTrue。

Returns

list[str]
    生成されるカラム名のリスト。
def get_resampled_df( self, filepath: str, index_column: str = 'TIMESTAMP', index_format: str = '%Y-%m-%d %H:%M:%S.%f', 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], resample: bool = True, interpolate: bool = True) -> tuple[pandas.core.frame.DataFrame, list[str]]:
379    def get_resampled_df(
380        self,
381        filepath: str,
382        index_column: str = "TIMESTAMP",
383        index_format: str = "%Y-%m-%d %H:%M:%S.%f",
384        numeric_columns: list[str] = [
385            "Ux",
386            "Uy",
387            "Uz",
388            "Tv",
389            "diag_sonic",
390            "CO2_new",
391            "H2O",
392            "diag_irga",
393            "cell_tmpr",
394            "cell_press",
395            "Ultra_CH4_ppm",
396            "Ultra_C2H6_ppb",
397            "Ultra_H2O_ppm",
398            "Ultra_CH4_ppm_C",
399            "Ultra_C2H6_ppb_C",
400        ],
401        metadata_rows: int = 4,
402        skiprows: list[int] = [0, 2, 3],
403        resample: bool = True,
404        interpolate: bool = True,
405    ) -> tuple[pd.DataFrame, list[str]]:
406        """
407        CSVファイルを読み込み、前処理を行う
408
409        前処理の手順は以下の通りです:
410        1. 不要な行を削除する。デフォルト(`skiprows=[0, 2, 3]`)の場合は、2行目をヘッダーとして残し、1、3、4行目が削除される。
411        2. 数値データを float 型に変換する
412        3. TIMESTAMP列をDateTimeインデックスに設定する
413        4. エラー値をNaNに置き換える
414        5. 指定されたサンプリングレートでリサンプリングする
415        6. 欠損値(NaN)を前後の値から線形補間する
416        7. DateTimeインデックスを削除する
417
418        Parameters
419        ----------
420            filepath : str
421                読み込むCSVファイルのパス
422            index_column : str, optional
423                インデックスに使用する列名。デフォルトは'TIMESTAMP'。
424            index_format : str, optional
425                インデックスの日付形式。デフォルトは'%Y-%m-%d %H:%M:%S.%f'。
426            numeric_columns : list[str], optional
427                数値型に変換する列名のリスト。
428                デフォルトは["Ux", "Uy", "Uz", "Tv", "diag_sonic", "CO2_new", "H2O", "diag_irga", "cell_tmpr", "cell_press", "Ultra_CH4_ppm", "Ultra_C2H6_ppb", "Ultra_H2O_ppm", "Ultra_CH4_ppm_C", "Ultra_C2H6_ppb_C"]。
429            metadata_rows : int, optional
430                メタデータとして読み込む行数。デフォルトは4。
431            skiprows : list[int], optional
432                スキップする行インデックスのリスト。デフォルトは[0, 2, 3]のため、1, 3, 4行目がスキップされる。
433            resample : bool
434                メソッド内でリサンプリング&欠損補間をするか。Falseの場合はfloat変換などの処理のみ適用する。
435            interpolate : bool, optional
436                欠損値の補完を適用するフラグ。デフォルトはTrue。
437
438        Returns
439        ----------
440            tuple[pd.DataFrame, list[str]]
441                前処理済みのデータフレームとメタデータのリスト。
442        """
443        # メタデータを読み込む
444        metadata: list[str] = []
445        with open(filepath, "r") as f:
446            for _ in range(metadata_rows):
447                line = f.readline().strip()
448                metadata.append(line.replace('"', ""))
449
450        # CSVファイルを読み込む
451        df: pd.DataFrame = pd.read_csv(filepath, skiprows=skiprows)
452
453        # 数値データをfloat型に変換する
454        for col in numeric_columns:
455            if col in df.columns:
456                df[col] = pd.to_numeric(df[col], errors="coerce")
457
458        if not resample:
459            # μ秒がない場合は".0"を追加する
460            df[index_column] = df[index_column].apply(
461                lambda x: f"{x}.0" if "." not in x else x
462            )
463            # TIMESTAMPをDateTimeインデックスに設定する
464            df[index_column] = pd.to_datetime(df[index_column], format=index_format)
465            df = df.set_index(index_column)
466
467            # リサンプリング前の有効数字を取得
468            decimal_places = {}
469            for col in numeric_columns:
470                if col in df.columns:
471                    max_decimals = (
472                        df[col].astype(str).str.extract(r"\.(\d+)")[0].str.len().max()
473                    )
474                    decimal_places[col] = (
475                        int(max_decimals) if pd.notna(max_decimals) else 0
476                    )
477
478            # リサンプリングを実行
479            resampling_period: int = int(1000 / self.fs)
480            df_resampled: pd.DataFrame = df.resample(f"{resampling_period}ms").mean(
481                numeric_only=True
482            )
483
484            if interpolate:
485                # 補間を実行
486                df_resampled = df_resampled.interpolate()
487                # 有効数字を調整
488                for col, decimals in decimal_places.items():
489                    if col in df_resampled.columns:
490                        df_resampled[col] = df_resampled[col].round(decimals)
491
492            # DateTimeインデックスを削除する
493            df = df_resampled.reset_index()
494            # ミリ秒を1桁にフォーマット
495            df[index_column] = (
496                df[index_column].dt.strftime("%Y-%m-%d %H:%M:%S.%f").str[:-5]
497            )
498
499        return df, metadata

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

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

  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'。
numeric_columns : list[str], optional
    数値型に変換する列名のリスト。
    デフォルトは["Ux", "Uy", "Uz", "Tv", "diag_sonic", "CO2_new", "H2O", "diag_irga", "cell_tmpr", "cell_press", "Ultra_CH4_ppm", "Ultra_C2H6_ppb", "Ultra_H2O_ppm", "Ultra_CH4_ppm_C", "Ultra_C2H6_ppb_C"]。
metadata_rows : int, optional
    メタデータとして読み込む行数。デフォルトは4。
skiprows : list[int], optional
    スキップする行インデックスのリスト。デフォルトは[0, 2, 3]のため、1, 3, 4行目がスキップされる。
resample : bool
    メソッド内でリサンプリング&欠損補間をするか。Falseの場合はfloat変換などの処理のみ適用する。
interpolate : bool, optional
    欠損値の補完を適用するフラグ。デフォルトはTrue。

Returns

tuple[pd.DataFrame, list[str]]
    前処理済みのデータフレームとメタデータのリスト。
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', resample: bool = True, 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:
501    def output_resampled_data(
502        self,
503        input_dir: str,
504        resampled_dir: str,
505        c2c1_ratio_dir: str,
506        input_file_pattern: str = r"Eddy_(\d+)",
507        input_files_suffix: str = ".dat",
508        col_c1: str = "Ultra_CH4_ppm_C",
509        col_c2: str = "Ultra_C2H6_ppb",
510        output_c2c1_ratio: bool = True,
511        output_resampled: bool = True,
512        c2c1_ratio_csv_prefix: str = "SAC.Ultra",
513        index_column: str = "TIMESTAMP",
514        index_format: str = "%Y-%m-%d %H:%M:%S.%f",
515        resample: bool = True,
516        interpolate: bool = True,
517        numeric_columns: list[str] = [
518            "Ux",
519            "Uy",
520            "Uz",
521            "Tv",
522            "diag_sonic",
523            "CO2_new",
524            "H2O",
525            "diag_irga",
526            "cell_tmpr",
527            "cell_press",
528            "Ultra_CH4_ppm",
529            "Ultra_C2H6_ppb",
530            "Ultra_H2O_ppm",
531            "Ultra_CH4_ppm_C",
532            "Ultra_C2H6_ppb_C",
533        ],
534        metadata_rows: int = 4,
535        skiprows: list[int] = [0, 2, 3],
536    ) -> None:
537        """
538        指定されたディレクトリ内のCSVファイルを処理し、リサンプリングと欠損値補間を行います。
539
540        このメソッドは、指定されたディレクトリ内のCSVファイルを読み込み、リサンプリングを行い、
541        欠損値を補完します。処理結果として、リサンプリングされたCSVファイルを出力し、
542        相関係数やC2H6/CH4比を計算してDataFrameに保存します。
543        リサンプリングと欠損値補完は`get_resampled_df`と同様のロジックを使用します。
544
545        Parameters
546        ----------
547            input_dir : str
548                入力CSVファイルが格納されているディレクトリのパス。
549            resampled_dir : str
550                リサンプリングされたCSVファイルを出力するディレクトリのパス。
551            c2c1_ratio_dir : str
552                計算結果を保存するディレクトリのパス。
553            input_file_pattern : str
554                ファイル名からソートキーを抽出する正規表現パターン。デフォルトでは、最初の数字グループでソートします。
555            input_files_suffix : str
556                入力ファイルの拡張子(.datや.csvなど)。デフォルトは".dat"。
557            col_c1 : str
558                CH4濃度を含む列名。デフォルトは'Ultra_CH4_ppm_C'。
559            col_c2 : str
560                C2H6濃度を含む列名。デフォルトは'Ultra_C2H6_ppb'。
561            output_c2c1_ratio : bool, optional
562                線形回帰を行うかどうか。デフォルトはTrue。
563            output_resampled : bool, optional
564                リサンプリングされたCSVファイルを出力するかどうか。デフォルトはTrue。
565            c2c1_ratio_csv_prefix : str
566                出力ファイルの接頭辞。デフォルトは'SAC.Ultra'で、出力時は'SAC.Ultra.2024.09.21.ratio.csv'のような形式となる。
567            index_column : str
568                日時情報を含む列名。デフォルトは'TIMESTAMP'。
569            index_format : str, optional
570                インデックスの日付形式。デフォルトは'%Y-%m-%d %H:%M:%S.%f'。
571            resample : bool
572                リサンプリングを行うかどうか。デフォルトはTrue。
573            interpolate : bool
574                欠損値補間を行うかどうか。デフォルトはTrue。
575            numeric_columns : list[str]
576                数値データを含む列名のリスト。デフォルトは指定された列名のリスト。
577            metadata_rows : int
578                メタデータとして読み込む行数。デフォルトは4。
579            skiprows : list[int]
580                読み飛ばす行のインデックスリスト。デフォルトは[0, 2, 3]。
581
582        Raises
583        ----------
584            OSError
585                ディレクトリの作成に失敗した場合。
586            FileNotFoundError
587                入力ファイルが見つからない場合。
588            ValueError
589                出力ディレクトリが指定されていない、またはデータの処理中にエラーが発生した場合。
590        """
591        # 出力オプションとディレクトリの検証
592        if output_resampled and resampled_dir is None:
593            raise ValueError(
594                "output_resampled が True の場合、resampled_dir を指定する必要があります"
595            )
596        if output_c2c1_ratio and c2c1_ratio_dir is None:
597            raise ValueError(
598                "output_c2c1_ratio が True の場合、c2c1_ratio_dir を指定する必要があります"
599            )
600
601        # ディレクトリの作成(必要な場合のみ)
602        if output_resampled:
603            os.makedirs(resampled_dir, exist_ok=True)
604        if output_c2c1_ratio:
605            os.makedirs(c2c1_ratio_dir, exist_ok=True)
606
607        ratio_data: list[dict[str, str | float]] = []
608        latest_date: datetime = datetime.min
609
610        # csvファイル名のリスト
611        csv_files: list[str] = EddyDataPreprocessor._get_sorted_files(
612            input_dir, input_file_pattern, input_files_suffix
613        )
614
615        for filename in tqdm(csv_files, desc="Processing files"):
616            input_filepath: str = os.path.join(input_dir, filename)
617            # リサンプリング&欠損値補間
618            df, metadata = self.get_resampled_df(
619                filepath=input_filepath,
620                index_column=index_column,
621                index_format=index_format,
622                interpolate=interpolate,
623                resample=resample,
624                numeric_columns=numeric_columns,
625                metadata_rows=metadata_rows,
626                skiprows=skiprows,
627            )
628
629            # 開始時間を取得
630            start_time: datetime = pd.to_datetime(df[index_column].iloc[0])
631            # 処理したファイルの中で最も最新の日付
632            latest_date = max(latest_date, start_time)
633
634            # リサンプリング&欠損値補間したCSVを出力
635            if output_resampled:
636                base_filename: str = re.sub(rf"\{input_files_suffix}$", "", filename)
637                output_csv_path: str = os.path.join(
638                    resampled_dir, f"{base_filename}-resampled.csv"
639                )
640                # メタデータを先に書き込む
641                with open(output_csv_path, "w") as f:
642                    for line in metadata:
643                        f.write(f"{line}\n")
644                # データフレームを追記モードで書き込む
645                df.to_csv(
646                    output_csv_path, index=False, mode="a", quoting=3, header=False
647                )
648
649            # 相関係数とC2H6/CH4比を計算
650            if output_c2c1_ratio:
651                ch4_data: pd.Series = df[col_c1]
652                c2h6_data: pd.Series = df[col_c2]
653
654                ratio_row: dict[str, str | float] = {
655                    "Date": start_time.strftime("%Y-%m-%d %H:%M:%S.%f"),
656                    "slope": f"{np.nan}",
657                    "intercept": f"{np.nan}",
658                    "r_value": f"{np.nan}",
659                    "p_value": f"{np.nan}",
660                    "stderr": f"{np.nan}",
661                }
662                # 近似直線の傾き、切片、相関係数を計算
663                try:
664                    slope, intercept, r_value, p_value, stderr = stats.linregress(
665                        ch4_data, c2h6_data
666                    )
667                    ratio_row: dict[str, str | float] = {
668                        "Date": start_time.strftime("%Y-%m-%d %H:%M:%S.%f"),
669                        "slope": f"{slope:.6f}",
670                        "intercept": f"{intercept:.6f}",
671                        "r_value": f"{r_value:.6f}",
672                        "p_value": f"{p_value:.6f}",
673                        "stderr": f"{stderr:.6f}",
674                    }
675                except Exception:
676                    # 何もせず、デフォルトの ratio_row を使用する
677                    pass
678
679                # 結果をリストに追加
680                ratio_data.append(ratio_row)
681
682        if output_c2c1_ratio:
683            # DataFrameを作成し、Dateカラムで昇順ソート
684            ratio_df: pd.DataFrame = pd.DataFrame(ratio_data)
685            ratio_df["Date"] = pd.to_datetime(
686                ratio_df["Date"]
687            )  # Dateカラムをdatetime型に変換
688            ratio_df = ratio_df.sort_values("Date")  # Dateカラムで昇順ソート
689
690            # CSVとして保存
691            ratio_filename: str = (
692                f"{c2c1_ratio_csv_prefix}.{latest_date.strftime('%Y.%m.%d')}.ratio.csv"
693            )
694            ratio_path: str = os.path.join(c2c1_ratio_dir, ratio_filename)
695            ratio_df.to_csv(ratio_path, index=False)

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

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

Parameters

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

Raises

OSError
    ディレクトリの作成に失敗した場合。
FileNotFoundError
    入力ファイルが見つからない場合。
ValueError
    出力ディレクトリが指定されていない、またはデータの処理中にエラーが発生した場合。
@staticmethod
def setup_logger(logger: logging.Logger | None, log_level: int = 20) -> logging.Logger:
807    @staticmethod
808    def setup_logger(logger: Logger | None, log_level: int = INFO) -> Logger:
809        """
810        ロガーを設定します。
811
812        このメソッドは、ロギングの設定を行い、ログメッセージのフォーマットを指定します。
813        ログメッセージには、日付、ログレベル、メッセージが含まれます。
814
815        渡されたロガーがNoneまたは不正な場合は、新たにロガーを作成し、標準出力に
816        ログメッセージが表示されるようにStreamHandlerを追加します。ロガーのレベルは
817        引数で指定されたlog_levelに基づいて設定されます。
818
819        Parameters
820        ----------
821            logger : Logger | None
822                使用するロガー。Noneの場合は新しいロガーを作成します。
823            log_level : int
824                ロガーのログレベル。デフォルトはINFO。
825
826        Returns
827        ----------
828            Logger
829                設定されたロガーオブジェクト。
830        """
831        if logger is not None and isinstance(logger, Logger):
832            return logger
833        # 渡されたロガーがNoneまたは正しいものでない場合は独自に設定
834        new_logger: Logger = getLogger()
835        # 既存のハンドラーをすべて削除
836        for handler in new_logger.handlers[:]:
837            new_logger.removeHandler(handler)
838        new_logger.setLevel(log_level)  # ロガーのレベルを設定
839        ch = StreamHandler()
840        ch_formatter = Formatter("%(asctime)s - %(levelname)s - %(message)s")
841        ch.setFormatter(ch_formatter)  # フォーマッターをハンドラーに設定
842        new_logger.addHandler(ch)  # StreamHandlerの追加
843        return new_logger

ロガーを設定します。

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

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

Parameters

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

Returns

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

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

Parameters

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

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

Parameters

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

Returns

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

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

Parameters

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

Returns

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

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

Parameters

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

Returns

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

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

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

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

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

FluxFootprintAnalyzer( z_m: float, na_values: list[str] = ['#DIV/0!', '#VALUE!', '#REF!', '#N/A', '#NAME?', 'NAN', 'nan'], column_mapping: Optional[Mapping[str, str]] = None, labelsize: float = 20, ticksize: float = 16, plot_params: dict[str, any] | None = None, logger: logging.Logger | None = None, logging_debug: bool = False)
 56    def __init__(
 57        self,
 58        z_m: float,
 59        na_values: list[str] = [
 60            "#DIV/0!",
 61            "#VALUE!",
 62            "#REF!",
 63            "#N/A",
 64            "#NAME?",
 65            "NAN",
 66            "nan",
 67        ],
 68        column_mapping: Mapping[str, str] | None = None,
 69        labelsize: float = 20,
 70        ticksize: float = 16,
 71        plot_params: dict[str, any] | None = None,
 72        logger: Logger | None = None,
 73        logging_debug: bool = False,
 74    ):
 75        """
 76        衛星画像を用いて FluxFootprintAnalyzer を初期化します。
 77
 78        Parameters
 79        ----------
 80            z_m : float
 81                測定の高さ(メートル単位)。
 82            na_values : list[str]
 83                NaNと判定する値のパターン。
 84            column_mapping : Mapping[str, str] | None, optional
 85                入力データのカラム名とデフォルトカラム名のマッピング
 86                例: {
 87                    "wind_dir": "WIND_DIRECTION",
 88                    "ws": "WIND_SPEED",
 89                    "ustar": "FRICTION_VELOCITY",
 90                    "sigma_v": "SIGMA_V",
 91                    "stability": "STABILITY",
 92                    "timestamp": "DATETIME",
 93                }
 94            labelsize : float
 95                軸ラベルのフォントサイズ。デフォルトは20。
 96            ticksize : float
 97                軸目盛りのフォントサイズ。デフォルトは16。
 98            plot_params : dict[str, any] | None
 99                matplotlibのプロットパラメータを指定する辞書。
100            logger : Logger | None
101                使用するロガー。Noneの場合は新しいロガーを生成します。
102            logging_debug : bool
103                ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。
104        """
105        # デフォルトのカラム名を設定
106        self._default_cols = DefaultColumnNames()
107        # カラム名マッピングの作成
108        self._cols = self._create_column_mapping(column_mapping)
109        # 必須カラムのリストを作成
110        self._required_columns = [
111            self._cols[self._default_cols.WIND_DIRECTION],
112            self._cols[self._default_cols.WIND_SPEED],
113            self._cols[self._default_cols.FRICTION_VELOCITY],
114            self._cols[self._default_cols.SIGMA_V],
115            self._cols[self._default_cols.STABILITY],
116        ]
117        self._z_m: float = z_m  # 測定高度
118        self._na_values: list[str] = na_values
119        # 状態を管理するフラグ
120        self._got_satellite_image: bool = False
121
122        # 図表の初期設定
123        FigureUtils.setup_plot_params(
124            font_size=labelsize, tick_size=ticksize, plot_params=plot_params
125        )
126        # ロガー
127        log_level: int = INFO
128        if logging_debug:
129            log_level = DEBUG
130        self.logger: Logger = FluxFootprintAnalyzer.setup_logger(logger, log_level)

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

Parameters

z_m : float
    測定の高さ(メートル単位)。
na_values : list[str]
    NaNと判定する値のパターン。
column_mapping : Mapping[str, str] | None, optional
    入力データのカラム名とデフォルトカラム名のマッピング
    例: {
        "wind_dir": "WIND_DIRECTION",
        "ws": "WIND_SPEED",
        "ustar": "FRICTION_VELOCITY",
        "sigma_v": "SIGMA_V",
        "stability": "STABILITY",
        "timestamp": "DATETIME",
    }
labelsize : float
    軸ラベルのフォントサイズ。デフォルトは20。
ticksize : float
    軸目盛りのフォントサイズ。デフォルトは16。
plot_params : dict[str, any] | None
    matplotlibのプロットパラメータを指定する辞書。
logger : Logger | None
    使用するロガー。Noneの場合は新しいロガーを生成します。
logging_debug : bool
    ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。
EARTH_RADIUS_METER: int = 6371000
COL_FFA_IS_WEEKDAY = 'ffa_is_weekday'
COL_FFA_RADIAN = 'ffa_radian'
COL_FFA_WIND_DIR_360 = 'ffa_wind_direction_360'
logger: logging.Logger
def check_required_columns( self, df: pandas.core.frame.DataFrame, col_datetime: str | None = None) -> bool:
166    def check_required_columns(
167        self,
168        df: pd.DataFrame,
169        col_datetime: str | None = None,
170    ) -> bool:
171        """
172        必須カラムの存在チェック
173
174        Parameters
175        ----------
176            df : pd.DataFrame
177                チェック対象のデータフレーム
178            col_datetime : str | None
179                日時カラム名(指定された場合はチェックから除外)
180
181        Returns
182        ----------
183            bool
184                すべての必須カラムが存在する場合True
185        """
186        check_columns: list[str] = [
187            col for col in self._required_columns if col != col_datetime
188        ]
189
190        missing_columns = [col for col in check_columns if col not in df.columns]
191
192        if missing_columns:
193            self.logger.error(
194                f"Required columns are missing: {missing_columns}"
195                f"\nAvailable columns: {df.columns.tolist()}"
196            )
197            return False
198
199        return True

必須カラムの存在チェック

Parameters

df : pd.DataFrame
    チェック対象のデータフレーム
col_datetime : str | None
    日時カラム名(指定された場合はチェックから除外)

Returns

bool
    すべての必須カラムが存在する場合True
@staticmethod
def setup_logger(logger: logging.Logger | None, log_level: int = 20) -> logging.Logger:
201    @staticmethod
202    def setup_logger(logger: Logger | None, log_level: int = INFO) -> Logger:
203        """
204        ロガーを設定します。
205
206        このメソッドは、ロギングの設定を行い、ログメッセージのフォーマットを指定します。
207        ログメッセージには、日付、ログレベル、メッセージが含まれます。
208
209        渡されたロガーがNoneまたは不正な場合は、新たにロガーを作成し、標準出力に
210        ログメッセージが表示されるようにStreamHandlerを追加します。ロガーのレベルは
211        引数で指定されたlog_levelに基づいて設定されます。
212
213        Parameters
214        ----------
215            logger : Logger | None
216                使用するロガー。Noneの場合は新しいロガーを作成します。
217            log_level : int
218                ロガーのログレベル。デフォルトはINFO。
219
220        Returns
221        ----------
222            Logger
223                設定されたロガーオブジェクト。
224        """
225        if logger is not None and isinstance(logger, Logger):
226            return logger
227        # 渡されたロガーがNoneまたは正しいものでない場合は独自に設定
228        new_logger: Logger = getLogger()
229        # 既存のハンドラーをすべて削除
230        for handler in new_logger.handlers[:]:
231            new_logger.removeHandler(handler)
232        new_logger.setLevel(log_level)  # ロガーのレベルを設定
233        ch = StreamHandler()
234        ch_formatter = Formatter("%(asctime)s - %(levelname)s - %(message)s")
235        ch.setFormatter(ch_formatter)  # フォーマッターをハンドラーに設定
236        new_logger.addHandler(ch)  # StreamHandlerの追加
237        return new_logger

ロガーを設定します。

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

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

Parameters

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

Returns

Logger
    設定されたロガーオブジェクト。
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]]:
239    def calculate_flux_footprint(
240        self,
241        df: pd.DataFrame,
242        col_flux: str,
243        plot_count: int = 10000,
244        start_time: str = "10:00",
245        end_time: str = "16:00",
246    ) -> tuple[list[float], list[float], list[float]]:
247        """
248        フラックスフットプリントを計算し、指定された時間帯のデータを基に可視化します。
249
250        Parameters
251        ----------
252            df : pd.DataFrame
253                分析対象のデータフレーム。フラックスデータを含む。
254            col_flux : str
255                フラックスデータの列名。計算に使用される。
256            plot_count : int, optional
257                生成するプロットの数。デフォルトは10000。
258            start_time : str, optional
259                フットプリント計算に使用する開始時間。デフォルトは"10:00"。
260            end_time : str, optional
261                フットプリント計算に使用する終了時間。デフォルトは"16:00"。
262
263        Returns
264        ----------
265            tuple[list[float], list[float], list[float]]:
266                x座標 (メートル): タワーを原点とした東西方向の距離
267                y座標 (メートル): タワーを原点とした南北方向の距離
268                対象スカラー量の値: 各地点でのフラックス値
269
270        Notes
271        ----------
272            - 返却される座標は測定タワーを原点(0,0)とした相対位置です
273            - すべての距離はメートル単位で表されます
274            - 正のx値は東方向、正のy値は北方向を示します
275            Required columns (default names):
276                - Wind direction: 風向 (度)
277                - WS vector: 風速 (m/s)
278                - u*: 摩擦速度 (m/s)
279                - sigmaV: 風速の標準偏差 (m/s)
280                - z/L: 安定度パラメータ (無次元)
281        """
282        col_weekday: str = self.COL_FFA_IS_WEEKDAY
283        df_copied: pd.DataFrame = df.copy()
284
285        # インデックスがdatetimeであることを確認し、必要に応じて変換
286        if not isinstance(df_copied.index, pd.DatetimeIndex):
287            df_copied.index = pd.to_datetime(df_copied.index)
288
289        # DatetimeIndexから直接dateプロパティにアクセス
290        datelist: np.ndarray = np.array(df_copied.index.date)
291
292        # 各日付が平日かどうかを判定し、リストに格納
293        numbers: list[int] = [
294            FluxFootprintAnalyzer.is_weekday(date) for date in datelist
295        ]
296
297        # col_weekdayに基づいてデータフレームに平日情報を追加
298        df_copied.loc[:, col_weekday] = numbers  # .locを使用して値を設定
299
300        # 値が1のもの(平日)をコピーする
301        data_weekday: pd.DataFrame = df_copied[df_copied[col_weekday] == 1].copy()
302        # 特定の時間帯を抽出
303        data_weekday = data_weekday.between_time(
304            start_time, end_time
305        )  # 引数を使用して時間帯を抽出
306        data_weekday = data_weekday.dropna(subset=[col_flux])
307
308        directions: list[float] = [
309            wind_direction if wind_direction >= 0 else wind_direction + 360
310            for wind_direction in data_weekday[
311                self._cols[self._default_cols.WIND_DIRECTION]
312            ]
313        ]
314
315        data_weekday.loc[:, self.COL_FFA_WIND_DIR_360] = directions
316        data_weekday.loc[:, self.COL_FFA_RADIAN] = (
317            data_weekday[self.COL_FFA_WIND_DIR_360] / 180 * np.pi
318        )
319
320        # 風向が欠測なら除去
321        data_weekday = data_weekday.dropna(
322            subset=[self._cols[self._default_cols.WIND_DIRECTION], col_flux]
323        )
324
325        # 数値型への変換を確実に行う
326        numeric_columns: list[str] = [
327            self._cols[self._default_cols.FRICTION_VELOCITY],
328            self._cols[self._default_cols.WIND_SPEED],
329            self._cols[self._default_cols.SIGMA_V],
330            self._cols[self._default_cols.STABILITY],
331        ]
332        for col in numeric_columns:
333            data_weekday[col] = pd.to_numeric(data_weekday[col], errors="coerce")
334
335        # 地面修正量dの計算
336        z_m: float = self._z_m
337        z_d: float = FluxFootprintAnalyzer._calculate_ground_correction(
338            z_m=z_m,
339            wind_speed=data_weekday[self._cols[self._default_cols.WIND_SPEED]].values,
340            friction_velocity=data_weekday[
341                self._cols[self._default_cols.FRICTION_VELOCITY]
342            ].values,
343            stability_parameter=data_weekday[
344                self._cols[self._default_cols.STABILITY]
345            ].values,
346        )
347
348        x_list: list[float] = []
349        y_list: list[float] = []
350        c_list: list[float] | None = []
351
352        # tqdmを使用してプログレスバーを表示
353        for i in tqdm(range(len(data_weekday)), desc="Calculating footprint"):
354            dUstar: float = data_weekday[self._cols[self._default_cols.FRICTION_VELOCITY]].iloc[i]
355            dU: float = data_weekday[self._cols[self._default_cols.WIND_SPEED]].iloc[i]
356            sigmaV: float = data_weekday[self._cols[self._default_cols.SIGMA_V]].iloc[i]
357            dzL: float = data_weekday[self._cols[self._default_cols.STABILITY]].iloc[i]
358
359
360            if pd.isna(dUstar) or pd.isna(dU) or pd.isna(sigmaV) or pd.isna(dzL):
361                self.logger.warning(f"NaN fields are exist.: i = {i}")
362                continue
363            elif dUstar < 5.0 and dUstar != 0.0 and dU > 0.1:
364                phi_m, phi_c, n = FluxFootprintAnalyzer._calculate_stability_parameters(
365                    dzL=dzL
366                )
367                m, U, r, mu, ksi = (
368                    FluxFootprintAnalyzer._calculate_footprint_parameters(
369                        dUstar=dUstar, dU=dU, z_d=z_d, phi_m=phi_m, phi_c=phi_c, n=n
370                    )
371                )
372
373                # 80%ソースエリアの計算
374                x80: float = FluxFootprintAnalyzer._source_area_KM2001(
375                    ksi=ksi, mu=mu, dU=dU, sigmaV=sigmaV, z_d=z_d, max_ratio=0.8
376                )
377
378                if not np.isnan(x80):
379                    x1, y1, flux1 = FluxFootprintAnalyzer._prepare_plot_data(
380                        x80,
381                        ksi,
382                        mu,
383                        r,
384                        U,
385                        m,
386                        sigmaV,
387                        data_weekday[col_flux].iloc[i],
388                        plot_count=plot_count,
389                    )
390                    x1_, y1_ = FluxFootprintAnalyzer._rotate_coordinates(
391                        x=x1, y=y1, radian=data_weekday[self.COL_FFA_RADIAN].iloc[i]
392                    )
393
394                    x_list.extend(x1_)
395                    y_list.extend(y1_)
396                    c_list.extend(flux1)
397
398        return (
399            x_list,
400            y_list,
401            c_list,
402        )

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

Parameters

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

Returns

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

Notes

- 返却される座標は測定タワーを原点(0,0)とした相対位置です
- すべての距離はメートル単位で表されます
- 正のx値は東方向、正のy値は北方向を示します
Required columns (default names):
    - Wind direction: 風向 (度)
    - WS vector: 風速 (m/s)
    - u*: 摩擦速度 (m/s)
    - sigmaV: 風速の標準偏差 (m/s)
    - z/L: 安定度パラメータ (無次元)
def combine_all_data( self, data_source: str | pandas.core.frame.DataFrame, col_datetime: str = 'Date', source_type: Literal['csv', 'monthly'] = 'csv') -> pandas.core.frame.DataFrame:
404    def combine_all_data(
405        self,
406        data_source: str | pd.DataFrame,
407        col_datetime: str = "Date",
408        source_type: Literal["csv", "monthly"] = "csv",
409    ) -> pd.DataFrame:
410        """
411        CSVファイルまたはMonthlyConverterからのデータを統合します
412
413        Parameters
414        ----------
415            data_source : str | pd.DataFrame
416                CSVディレクトリパスまたはDataFrame
417            col_datetime :str
418                datetimeカラムのカラム名。デフォルトは"Date"。
419            source_type : str
420                "csv" または "monthly"
421
422        Returns
423        ----------
424            pd.DataFrame
425                処理済みのデータフレーム
426        """
427        col_weekday: str = self.COL_FFA_IS_WEEKDAY
428        if source_type == "csv":
429            # 既存のCSV処理ロジック
430            return self._combine_all_csv(
431                csv_dir_path=data_source, col_datetime=col_datetime
432            )
433        elif source_type == "monthly":
434            # MonthlyConverterからのデータを処理
435            if not isinstance(data_source, pd.DataFrame):
436                raise ValueError("monthly形式の場合、DataFrameを直接渡す必要があります")
437
438            df: pd.DataFrame = data_source.copy()
439
440            # required_columnsからDateを除外して欠損値チェックを行う
441            check_columns: list[str] = [
442                col for col in self._required_columns if col != col_datetime
443            ]
444
445            # インデックスがdatetimeであることを確認
446            if (
447                not isinstance(df.index, pd.DatetimeIndex)
448                and col_datetime not in df.columns
449            ):
450                raise ValueError(f"DatetimeIndexまたは{col_datetime}カラムが必要です")
451
452            if col_datetime in df.columns:
453                df.set_index(col_datetime, inplace=True)
454
455            # 必要なカラムの存在確認
456            missing_columns = [
457                col for col in check_columns if col not in df.columns.tolist()
458            ]
459            if missing_columns:
460                missing_cols = "','".join(missing_columns)
461                current_cols = "','".join(df.columns.tolist())
462                raise ValueError(
463                    f"必要なカラムが不足しています: '{missing_cols}'\n"
464                    f"現在のカラム: '{current_cols}'"
465                )
466
467            # 平日/休日の判定用カラムを追加
468            df[col_weekday] = df.index.map(FluxFootprintAnalyzer.is_weekday)
469
470            # Dateを除外したカラムで欠損値の処理
471            df = df.dropna(subset=check_columns)
472
473            # インデックスの重複を除去
474            df = df.loc[~df.index.duplicated(), :]
475
476            return df

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

Parameters

data_source : str | pd.DataFrame
    CSVディレクトリパスまたはDataFrame
col_datetime :str
    datetimeカラムのカラム名。デフォルトは"Date"。
source_type : str
    "csv" または "monthly"

Returns

pd.DataFrame
    処理済みのデータフレーム
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:
478    def get_satellite_image_from_api(
479        self,
480        api_key: str,
481        center_lat: float,
482        center_lon: float,
483        output_path: str,
484        scale: int = 1,
485        size: tuple[int, int] = (2160, 2160),
486        zoom: int = 13,
487    ) -> ImageFile:
488        """
489        Google Maps Static APIを使用して衛星画像を取得します。
490
491        Parameters
492        ----------
493            api_key : str
494                Google Maps Static APIのキー。
495            center_lat : float
496                中心の緯度。
497            center_lon : float
498                中心の経度。
499            output_path : str
500                画像の保存先パス。拡張子は'.png'のみ許可される。
501            scale : int, optional
502                画像の解像度スケール(1か2)。デフォルトは1。
503            size : tuple[int, int], optional
504                画像サイズ (幅, 高さ)。デフォルトは(2160, 2160)。
505            zoom : int, optional
506                ズームレベル(0-21)。デフォルトは13。
507
508        Returns
509        ----------
510            ImageFile
511                取得した衛星画像
512
513        Raises
514        ----------
515            requests.RequestException
516                API呼び出しに失敗した場合
517        """
518        # バリデーション
519        if not output_path.endswith(".png"):
520            raise ValueError("出力ファイル名は'.png'で終わる必要があります。")
521
522        # HTTPリクエストの定義
523        base_url = "https://maps.googleapis.com/maps/api/staticmap"
524        params = {
525            "center": f"{center_lat},{center_lon}",
526            "zoom": zoom,
527            "size": f"{size[0]}x{size[1]}",
528            "maptype": "satellite",
529            "scale": scale,
530            "key": api_key,
531        }
532
533        try:
534            response = requests.get(base_url, params=params)
535            response.raise_for_status()
536            # 画像ファイルに変換
537            image = Image.open(io.BytesIO(response.content))
538            image.save(output_path)
539            self._got_satellite_image = True
540            self.logger.info(f"リモート画像を取得し、保存しました: {output_path}")
541            return image
542        except requests.RequestException as e:
543            self.logger.error(f"衛星画像の取得に失敗しました: {str(e)}")
544            raise e

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

Parameters

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

Returns

ImageFile
    取得した衛星画像

Raises

requests.RequestException
    API呼び出しに失敗した場合
def get_satellite_image_from_local( self, local_image_path: str, alpha: float = 1.0, grayscale: bool = False) -> PIL.ImageFile.ImageFile:
546    def get_satellite_image_from_local(
547        self,
548        local_image_path: str,
549        alpha: float = 1.0,
550        grayscale: bool = False,
551    ) -> ImageFile:
552        """
553        ローカルファイルから衛星画像を読み込みます。
554
555        Parameters
556        ----------
557            local_image_path : str
558                ローカル画像のパス
559            alpha : float, optional
560                画像の透過率(0.0~1.0)。デフォルトは1.0。
561            grayscale : bool, optional
562                Trueの場合、画像を白黒に変換します。デフォルトはFalse。
563
564        Returns
565        ----------
566            ImageFile
567                読み込んだ衛星画像(透過設定済み)
568
569        Raises
570        ----------
571            FileNotFoundError
572                指定されたパスにファイルが存在しない場合
573        """
574        if not os.path.exists(local_image_path):
575            raise FileNotFoundError(
576                f"指定されたローカル画像が存在しません: {local_image_path}"
577            )
578
579        # 画像を読み込む
580        image: ImageFile = Image.open(local_image_path)
581
582        # 白黒変換が指定されている場合
583        if grayscale:
584            image = image.convert("L")  # グレースケールに変換
585
586        # RGBAモードに変換
587        image = image.convert("RGBA")
588
589        # 透過率を設定
590        data = image.getdata()
591        new_data = [(r, g, b, int(255 * alpha)) for r, g, b, a in data]
592        image.putdata(new_data)
593
594        self._got_satellite_image = True
595        self.logger.info(
596            f"ローカル画像を使用しました(透過率: {alpha}, 白黒: {grayscale}): {local_image_path}"
597        )
598        return image

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

Parameters

local_image_path : str
    ローカル画像のパス
alpha : float, optional
    画像の透過率(0.0~1.0)。デフォルトは1.0。
grayscale : bool, optional
    Trueの場合、画像を白黒に変換します。デフォルトはFalse。

Returns

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

Raises

FileNotFoundError
    指定されたパスにファイルが存在しない場合
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 | pathlib.Path | 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:
600    def plot_flux_footprint(
601        self,
602        x_list: list[float],
603        y_list: list[float],
604        c_list: list[float] | None,
605        center_lat: float,
606        center_lon: float,
607        vmin: float,
608        vmax: float,
609        add_cbar: bool = True,
610        add_legend: bool = True,
611        cbar_label: str | None = None,
612        cbar_labelpad: int = 20,
613        cmap: str = "jet",
614        reduce_c_function: callable = np.mean,
615        lat_correction: float = 1,
616        lon_correction: float = 1,
617        output_dir: str | Path | None = None,
618        output_filename: str = "footprint.png",
619        save_fig: bool = True,
620        show_fig: bool = True,
621        satellite_image: ImageFile | None = None,
622        xy_max: float = 5000,
623    ) -> None:
624        """
625        フットプリントデータをプロットします。
626
627        このメソッドは、指定されたフットプリントデータのみを可視化します。
628
629        Parameters
630        ----------
631            x_list : list[float]
632                フットプリントのx座標リスト(メートル単位)。
633            y_list : list[float]
634                フットプリントのy座標リスト(メートル単位)。
635            c_list : list[float] | None
636                フットプリントの強度を示す値のリスト。
637            center_lat : float
638                プロットの中心となる緯度。
639            center_lon : float
640                プロットの中心となる経度。
641            cmap : str
642                使用するカラーマップの名前。
643            vmin : float
644                カラーバーの最小値。
645            vmax : float
646                カラーバーの最大値。
647            reduce_c_function : callable, optional
648                フットプリントの集約関数(デフォルトはnp.mean)。
649            cbar_label : str | None, optional
650                カラーバーのラベル。
651            cbar_labelpad : int, optional
652                カラーバーラベルのパディング。
653            lon_correction : float, optional
654                経度方向の補正係数(デフォルトは1)。
655            lat_correction : float, optional
656                緯度方向の補正係数(デフォルトは1)。
657            output_dir : str | Path | None, optional
658                プロット画像の保存先パス。
659            output_filename : str
660                プロット画像の保存ファイル名(拡張子を含む)。デフォルトは'footprint.png'。
661            save_fig : bool
662                図の保存を許可するフラグ。デフォルトはTrue。
663            show_fig : bool
664                図の表示を許可するフラグ。デフォルトはTrue。
665            satellite_image : ImageFile | None, optional
666                使用する衛星画像。指定がない場合はデフォルトの画像が生成されます。
667            xy_max : float, optional
668                表示範囲の最大値(デフォルトは4000)。
669        """
670        self.plot_flux_footprint_with_hotspots(
671            x_list=x_list,
672            y_list=y_list,
673            c_list=c_list,
674            center_lat=center_lat,
675            center_lon=center_lon,
676            vmin=vmin,
677            vmax=vmax,
678            add_cbar=add_cbar,
679            add_legend=add_legend,
680            cbar_label=cbar_label,
681            cbar_labelpad=cbar_labelpad,
682            cmap=cmap,
683            reduce_c_function=reduce_c_function,
684            hotspots=None,  # hotspotsをNoneに設定
685            hotspot_colors=None,
686            lat_correction=lat_correction,
687            lon_correction=lon_correction,
688            output_dir=output_dir,
689            output_filename=output_filename,
690            save_fig=save_fig,
691            show_fig=show_fig,
692            satellite_image=satellite_image,
693            xy_max=xy_max,
694        )

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

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

Parameters

x_list : list[float]
    フットプリントのx座標リスト(メートル単位)。
y_list : list[float]
    フットプリントのy座標リスト(メートル単位)。
c_list : list[float] | None
    フットプリントの強度を示す値のリスト。
center_lat : float
    プロットの中心となる緯度。
center_lon : float
    プロットの中心となる経度。
cmap : str
    使用するカラーマップの名前。
vmin : float
    カラーバーの最小値。
vmax : float
    カラーバーの最大値。
reduce_c_function : callable, optional
    フットプリントの集約関数(デフォルトはnp.mean)。
cbar_label : str | None, optional
    カラーバーのラベル。
cbar_labelpad : int, optional
    カラーバーラベルのパディング。
lon_correction : float, optional
    経度方向の補正係数(デフォルトは1)。
lat_correction : float, optional
    緯度方向の補正係数(デフォルトは1)。
output_dir : str | Path | None, optional
    プロット画像の保存先パス。
output_filename : str
    プロット画像の保存ファイル名(拡張子を含む)。デフォルトは'footprint.png'。
save_fig : bool
    図の保存を許可するフラグ。デフォルトはTrue。
show_fig : bool
    図の表示を許可するフラグ。デフォルトはTrue。
satellite_image : ImageFile | None, optional
    使用する衛星画像。指定がない場合はデフォルトの画像が生成されます。
xy_max : float, optional
    表示範囲の最大値(デフォルトは4000)。
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, hotspots_alpha: float = 0.7, 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, legend_bbox_to_anchor: tuple[float, float] = (0.55, -0.01), lat_correction: float = 1, lon_correction: float = 1, output_dir: str | pathlib.Path | 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:
 696    def plot_flux_footprint_with_hotspots(
 697        self,
 698        x_list: list[float],
 699        y_list: list[float],
 700        c_list: list[float] | None,
 701        center_lat: float,
 702        center_lon: float,
 703        vmin: float,
 704        vmax: float,
 705        add_cbar: bool = True,
 706        add_legend: bool = True,
 707        cbar_label: str | None = None,
 708        cbar_labelpad: int = 20,
 709        cmap: str = "jet",
 710        reduce_c_function: callable = np.mean,
 711        hotspots: list[HotspotData] | None = None,
 712        hotspots_alpha: float = 0.7,
 713        hotspot_colors: dict[HotspotType, str] | None = None,
 714        hotspot_labels: dict[HotspotType, str] | None = None,
 715        hotspot_markers: dict[HotspotType, str] | None = None,
 716        legend_bbox_to_anchor: tuple[float, float] = (0.55, -0.01),
 717        lat_correction: float = 1,
 718        lon_correction: float = 1,
 719        output_dir: str | Path | None = None,
 720        output_filename: str = "footprint.png",
 721        save_fig: bool = True,
 722        show_fig: bool = True,
 723        satellite_image: ImageFile | None = None,
 724        xy_max: float = 5000,
 725    ) -> None:
 726        """
 727        Staticな衛星画像上にフットプリントデータとホットスポットをプロットします。
 728
 729        このメソッドは、指定されたフットプリントデータとホットスポットを可視化します。
 730        ホットスポットが指定されない場合は、フットプリントのみ作図します。
 731
 732        Parameters
 733        ----------
 734            x_list : list[float]
 735                フットプリントのx座標リスト(メートル単位)。
 736            y_list : list[float]
 737                フットプリントのy座標リスト(メートル単位)。
 738            c_list : list[float] | None
 739                フットプリントの強度を示す値のリスト。
 740            center_lat : float
 741                プロットの中心となる緯度。
 742            center_lon : float
 743                プロットの中心となる経度。
 744            vmin : float
 745                カラーバーの最小値。
 746            vmax : float
 747                カラーバーの最大値。
 748            add_cbar : bool, optional
 749                カラーバーを追加するかどうか(デフォルトはTrue)。
 750            add_legend : bool, optional
 751                凡例を追加するかどうか(デフォルトはTrue)。
 752            cbar_label : str | None, optional
 753                カラーバーのラベル。
 754            cbar_labelpad : int, optional
 755                カラーバーラベルのパディング。
 756            cmap : str
 757                使用するカラーマップの名前。
 758            reduce_c_function : callable
 759                フットプリントの集約関数(デフォルトはnp.mean)。
 760            hotspots : list[HotspotData] | None, optional
 761                ホットスポットデータのリスト。デフォルトはNone。
 762            hotspots_alpha : float, optional
 763                ホットスポットの透明度。デフォルトは0.7。
 764            hotspot_colors : dict[HotspotType, str] | None, optional
 765                ホットスポットの色を指定する辞書。
 766                例: {'bio': 'blue', 'gas': 'red', 'comb': 'green'}
 767            hotspot_labels : dict[HotspotType, str] | None, optional
 768                ホットスポットの表示ラベルを指定する辞書。
 769                例: {'bio': '生物', 'gas': 'ガス', 'comb': '燃焼'}
 770            hotspot_markers : dict[HotspotType, str] | None, optional
 771                ホットスポットの形状を指定する辞書。
 772                例: {'bio': '^', 'gas': 'o', 'comb': 's'}
 773            legend_bbox_to_anchor : tuple[float, flaot], optional
 774                ホットスポットの凡例の位置。デフォルトは (0.55, -0.01) 。
 775            lat_correction : float, optional
 776                緯度方向の補正係数(デフォルトは1)。
 777            lon_correction : float, optional
 778                経度方向の補正係数(デフォルトは1)。
 779            output_dir : str | Path | None, optional
 780                プロット画像の保存先パス。
 781            output_filename : str
 782                プロット画像の保存ファイル名(拡張子を含む)。デフォルトは'footprint.png'。
 783            save_fig : bool
 784                図の保存を許可するフラグ。デフォルトはTrue。
 785            show_fig : bool
 786                図の表示を許可するフラグ。デフォルトはTrue。
 787            satellite_image : ImageFile | None, optional
 788                使用する衛星画像。指定がない場合はデフォルトの画像が生成されます。
 789            xy_max : float, optional
 790                表示範囲の最大値(デフォルトは5000)。
 791        """
 792        # 1. 引数のバリデーション
 793        valid_extensions: list[str] = [".png", ".jpg", ".jpeg", ".pdf", ".svg"]
 794        _, file_extension = os.path.splitext(output_filename)
 795        if file_extension.lower() not in valid_extensions:
 796            quoted_extensions: list[str] = [f'"{ext}"' for ext in valid_extensions]
 797            self.logger.error(
 798                f"`output_filename`は有効な拡張子ではありません。プロットを保存するには、次のいずれかを指定してください: {','.join(quoted_extensions)}"
 799            )
 800            return
 801
 802        # 2. フラグチェック
 803        if not self._got_satellite_image:
 804            raise ValueError(
 805                "`get_satellite_image_from_api`または`get_satellite_image_from_local`が実行されていません。"
 806            )
 807
 808        # 3. 衛星画像の取得
 809        if satellite_image is None:
 810            satellite_image = Image.new("RGB", (2160, 2160), "lightgray")
 811
 812        self.logger.info("プロットを作成中...")
 813
 814        # 4. 座標変換のための定数計算(1回だけ)
 815        meters_per_lat: float = self.EARTH_RADIUS_METER * (
 816            math.pi / 180
 817        )  # 緯度1度あたりのメートル
 818        meters_per_lon: float = meters_per_lat * math.cos(
 819            math.radians(center_lat)
 820        )  # 経度1度あたりのメートル
 821
 822        # 5. フットプリントデータの座標変換(まとめて1回で実行)
 823        x_deg = (
 824            np.array(x_list) / meters_per_lon * lon_correction
 825        )  # 補正係数も同時に適用
 826        y_deg = (
 827            np.array(y_list) / meters_per_lat * lat_correction
 828        )  # 補正係数も同時に適用
 829
 830        # 6. 中心点からの相対座標を実際の緯度経度に変換
 831        lons = center_lon + x_deg
 832        lats = center_lat + y_deg
 833
 834        # 7. 表示範囲の計算(変更なし)
 835        x_range: float = xy_max / meters_per_lon
 836        y_range: float = xy_max / meters_per_lat
 837        map_boundaries: tuple[float, float, float, float] = (
 838            center_lon - x_range,  # left_lon
 839            center_lon + x_range,  # right_lon
 840            center_lat - y_range,  # bottom_lat
 841            center_lat + y_range,  # top_lat
 842        )
 843        left_lon, right_lon, bottom_lat, top_lat = map_boundaries
 844
 845        # 8. プロットの作成
 846        plt.rcParams["axes.edgecolor"] = "None"
 847        fig: plt.Figure = plt.figure(figsize=(10, 8), dpi=300)
 848        ax_data: plt.Axes = fig.add_axes([0.05, 0.1, 0.8, 0.8])
 849
 850        # 9. フットプリントの描画
 851        # フットプリントの描画とカラーバー用の2つのhexbinを作成
 852        if c_list is not None:
 853            ax_data.hexbin(
 854                lons,
 855                lats,
 856                C=c_list,
 857                cmap=cmap,
 858                vmin=vmin,
 859                vmax=vmax,
 860                alpha=0.3,  # 実際のプロット用
 861                gridsize=100,
 862                linewidths=0,
 863                mincnt=100,
 864                extent=[left_lon, right_lon, bottom_lat, top_lat],
 865                reduce_C_function=reduce_c_function,
 866            )
 867
 868        # カラーバー用の非表示hexbin(alpha=1.0)
 869        hidden_hexbin = ax_data.hexbin(
 870            lons,
 871            lats,
 872            C=c_list,
 873            cmap=cmap,
 874            vmin=vmin,
 875            vmax=vmax,
 876            alpha=1.0,  # カラーバー用
 877            gridsize=100,
 878            linewidths=0,
 879            mincnt=100,
 880            extent=[left_lon, right_lon, bottom_lat, top_lat],
 881            reduce_C_function=reduce_c_function,
 882            visible=False,  # プロットには表示しない
 883        )
 884
 885        # 10. ホットスポットの描画
 886        spot_handles = []
 887        if hotspots is not None:
 888            default_colors: dict[HotspotType, str] = {
 889                "bio": "blue",
 890                "gas": "red",
 891                "comb": "green",
 892            }
 893
 894            # デフォルトのマーカー形状を定義
 895            default_markers: dict[HotspotType, str] = {
 896                "bio": "^",  # 三角
 897                "gas": "o",  # 丸
 898                "comb": "s",  # 四角
 899            }
 900
 901            # デフォルトのラベルを定義
 902            default_labels: dict[HotspotType, str] = {
 903                "bio": "bio",
 904                "gas": "gas",
 905                "comb": "comb",
 906            }
 907
 908            # 座標変換のための定数
 909            meters_per_lat: float = self.EARTH_RADIUS_METER * (math.pi / 180)
 910            meters_per_lon: float = meters_per_lat * math.cos(math.radians(center_lat))
 911
 912            for spot_type, color in (hotspot_colors or default_colors).items():
 913                spots_lon = []
 914                spots_lat = []
 915
 916                # 使用するマーカーを決定
 917                marker = (hotspot_markers or default_markers).get(spot_type, "o")
 918
 919                for spot in hotspots:
 920                    if spot.type == spot_type:
 921                        # 変換前の緯度経度をログ出力
 922                        self.logger.debug(
 923                            f"Before - Type: {spot_type}, Lat: {spot.avg_lat:.6f}, Lon: {spot.avg_lon:.6f}"
 924                        )
 925
 926                        # 中心からの相対距離を計算
 927                        dx: float = (spot.avg_lon - center_lon) * meters_per_lon
 928                        dy: float = (spot.avg_lat - center_lat) * meters_per_lat
 929
 930                        # 補正前の相対座標をログ出力
 931                        self.logger.debug(
 932                            f"Relative - Type: {spot_type}, X: {dx:.2f}m, Y: {dy:.2f}m"
 933                        )
 934
 935                        # 補正を適用
 936                        corrected_dx: float = dx * lon_correction
 937                        corrected_dy: float = dy * lat_correction
 938
 939                        # 補正後の緯度経度を計算
 940                        adjusted_lon: float = center_lon + corrected_dx / meters_per_lon
 941                        adjusted_lat: float = center_lat + corrected_dy / meters_per_lat
 942
 943                        # 変換後の緯度経度をログ出力
 944                        self.logger.debug(
 945                            f"After - Type: {spot_type}, Lat: {adjusted_lat:.6f}, Lon: {adjusted_lon:.6f}\n"
 946                        )
 947
 948                        if (
 949                            left_lon <= adjusted_lon <= right_lon
 950                            and bottom_lat <= adjusted_lat <= top_lat
 951                        ):
 952                            spots_lon.append(adjusted_lon)
 953                            spots_lat.append(adjusted_lat)
 954
 955                if spots_lon:
 956                    # 使用するラベルを決定
 957                    label = (hotspot_labels or default_labels).get(spot_type, spot_type)
 958
 959                    handle = ax_data.scatter(
 960                        spots_lon,
 961                        spots_lat,
 962                        c=color,
 963                        marker=marker,  # マーカー形状を指定
 964                        s=100,
 965                        alpha=hotspots_alpha,
 966                        label=label,
 967                        edgecolor="black",
 968                        linewidth=1,
 969                    )
 970                    spot_handles.append(handle)
 971
 972        # 11. 背景画像の設定
 973        ax_img = ax_data.twiny().twinx()
 974        ax_img.imshow(
 975            satellite_image,
 976            extent=[left_lon, right_lon, bottom_lat, top_lat],
 977            aspect="equal",
 978        )
 979
 980        # 12. 軸の設定
 981        for ax in [ax_data, ax_img]:
 982            ax.set_xlim(left_lon, right_lon)
 983            ax.set_ylim(bottom_lat, top_lat)
 984            ax.set_xticks([])
 985            ax.set_yticks([])
 986
 987        ax_data.set_zorder(2)
 988        ax_data.patch.set_alpha(0)
 989        ax_img.set_zorder(1)
 990
 991        # 13. カラーバーの追加
 992        if add_cbar:
 993            cbar_ax: plt.Axes = fig.add_axes([0.88, 0.1, 0.03, 0.8])
 994            cbar = fig.colorbar(hidden_hexbin, cax=cbar_ax)  # hidden_hexbinを使用
 995            # cbar_labelが指定されている場合のみラベルを設定
 996            if cbar_label:
 997                cbar.set_label(cbar_label, rotation=270, labelpad=cbar_labelpad)
 998
 999        # 14. ホットスポットの凡例追加
1000        if add_legend and hotspots and spot_handles:
1001            ax_data.legend(
1002                handles=spot_handles,
1003                loc="upper center",
1004                bbox_to_anchor=legend_bbox_to_anchor,  # 図の下に配置
1005                ncol=len(spot_handles),  # ハンドルの数に応じて列数を設定
1006            )
1007
1008        # 15. 画像の保存
1009        if save_fig:
1010            if output_dir is None:
1011                raise ValueError(
1012                    "save_fig=Trueの場合、output_dirを指定する必要があります。有効なディレクトリパスを指定してください。"
1013                )
1014            output_path: str = os.path.join(output_dir, output_filename)
1015            self.logger.info("プロットを保存中...")
1016            try:
1017                fig.savefig(output_path, bbox_inches="tight")
1018                self.logger.info(f"プロットが正常に保存されました: {output_path}")
1019            except Exception as e:
1020                self.logger.error(f"プロットの保存中にエラーが発生しました: {str(e)}")
1021        # 16. 画像の表示
1022        if show_fig:
1023            plt.show()
1024        else:
1025            plt.close(fig=fig)

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

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

Parameters

x_list : list[float]
    フットプリントのx座標リスト(メートル単位)。
y_list : list[float]
    フットプリントのy座標リスト(メートル単位)。
c_list : list[float] | None
    フットプリントの強度を示す値のリスト。
center_lat : float
    プロットの中心となる緯度。
center_lon : float
    プロットの中心となる経度。
vmin : float
    カラーバーの最小値。
vmax : float
    カラーバーの最大値。
add_cbar : bool, optional
    カラーバーを追加するかどうか(デフォルトはTrue)。
add_legend : bool, optional
    凡例を追加するかどうか(デフォルトはTrue)。
cbar_label : str | None, optional
    カラーバーのラベル。
cbar_labelpad : int, optional
    カラーバーラベルのパディング。
cmap : str
    使用するカラーマップの名前。
reduce_c_function : callable
    フットプリントの集約関数(デフォルトはnp.mean)。
hotspots : list[HotspotData] | None, optional
    ホットスポットデータのリスト。デフォルトはNone。
hotspots_alpha : float, optional
    ホットスポットの透明度。デフォルトは0.7。
hotspot_colors : dict[HotspotType, str] | None, optional
    ホットスポットの色を指定する辞書。
    例: {'bio': 'blue', 'gas': 'red', 'comb': 'green'}
hotspot_labels : dict[HotspotType, str] | None, optional
    ホットスポットの表示ラベルを指定する辞書。
    例: {'bio': '生物', 'gas': 'ガス', 'comb': '燃焼'}
hotspot_markers : dict[HotspotType, str] | None, optional
    ホットスポットの形状を指定する辞書。
    例: {'bio': '^', 'gas': 'o', 'comb': 's'}
legend_bbox_to_anchor : tuple[float, flaot], optional
    ホットスポットの凡例の位置。デフォルトは (0.55, -0.01) 。
lat_correction : float, optional
    緯度方向の補正係数(デフォルトは1)。
lon_correction : float, optional
    経度方向の補正係数(デフォルトは1)。
output_dir : str | Path | None, optional
    プロット画像の保存先パス。
output_filename : str
    プロット画像の保存ファイル名(拡張子を含む)。デフォルトは'footprint.png'。
save_fig : bool
    図の保存を許可するフラグ。デフォルトはTrue。
show_fig : bool
    図の表示を許可するフラグ。デフォルトはTrue。
satellite_image : ImageFile | None, optional
    使用する衛星画像。指定がない場合はデフォルトの画像が生成されます。
xy_max : float, optional
    表示範囲の最大値(デフォルトは5000)。
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 | pathlib.Path | 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:
1027    def plot_flux_footprint_with_scale_checker(
1028        self,
1029        x_list: list[float],
1030        y_list: list[float],
1031        c_list: list[float] | None,
1032        center_lat: float,
1033        center_lon: float,
1034        check_points: list[tuple[float, float, str]] | None = None,
1035        vmin: float = 0,
1036        vmax: float = 100,
1037        add_cbar: bool = True,
1038        cbar_label: str | None = None,
1039        cbar_labelpad: int = 20,
1040        cmap: str = "jet",
1041        reduce_c_function: callable = np.mean,
1042        lat_correction: float = 1,
1043        lon_correction: float = 1,
1044        output_dir: str | Path | None = None,
1045        output_filename: str = "footprint-scale_checker.png",
1046        save_fig: bool = True,
1047        show_fig: bool = True,
1048        satellite_image: ImageFile | None = None,
1049        xy_max: float = 5000,
1050    ) -> None:
1051        """
1052        Staticな衛星画像上にフットプリントデータとホットスポットをプロットします。
1053
1054        このメソッドは、指定されたフットプリントデータとホットスポットを可視化します。
1055        ホットスポットが指定されない場合は、フットプリントのみ作図します。
1056
1057        Parameters
1058        ----------
1059            x_list : list[float]
1060                フットプリントのx座標リスト(メートル単位)。
1061            y_list : list[float]
1062                フットプリントのy座標リスト(メートル単位)。
1063            c_list : list[float] | None
1064                フットプリントの強度を示す値のリスト。
1065            center_lat : float
1066                プロットの中心となる緯度。
1067            center_lon : float
1068                プロットの中心となる経度。
1069            check_points : list[tuple[float, float, str]] | None
1070                確認用の地点リスト。各要素は (緯度, 経度, ラベル) のタプル。
1071                Noneの場合は中心から500m、1000m、2000m、3000mの位置に仮想的な点を配置。
1072            cmap : str
1073                使用するカラーマップの名前。
1074            vmin : float
1075                カラーバーの最小値。
1076            vmax : float
1077                カラーバーの最大値。
1078            reduce_c_function : callable, optional
1079                フットプリントの集約関数(デフォルトはnp.mean)。
1080            cbar_label : str, optional
1081                カラーバーのラベル。
1082            cbar_labelpad : int, optional
1083                カラーバーラベルのパディング。
1084            hotspots : list[HotspotData] | None
1085                ホットスポットデータのリスト。デフォルトはNone。
1086            hotspot_colors : dict[str, str] | None, optional
1087                ホットスポットの色を指定する辞書。
1088            lon_correction : float, optional
1089                経度方向の補正係数(デフォルトは1)。
1090            lat_correction : float, optional
1091                緯度方向の補正係数(デフォルトは1)。
1092            output_dir : str | Path | None, optional
1093                プロット画像の保存先パス。
1094            output_filename : str
1095                プロット画像の保存ファイル名(拡張子を含む)。デフォルトは'footprint.png'。
1096            save_fig : bool
1097                図の保存を許可するフラグ。デフォルトはTrue。
1098            show_fig : bool
1099                図の表示を許可するフラグ。デフォルトはTrue。
1100            satellite_image : ImageFile | None, optional
1101                使用する衛星画像。指定がない場合はデフォルトの画像が生成されます。
1102            xy_max : float, optional
1103                表示範囲の最大値(デフォルトは5000)。
1104        """
1105        if check_points is None:
1106            # デフォルトの確認ポイントを生成(従来の方式)
1107            default_points = [
1108                (500, "North", 90),  # 北 500m
1109                (1000, "East", 0),  # 東 1000m
1110                (2000, "South", 270),  # 南 2000m
1111                (3000, "West", 180),  # 西 3000m
1112            ]
1113
1114            dummy_hotspots = []
1115            for distance, direction, angle in default_points:
1116                rad = math.radians(angle)
1117                meters_per_lat = self.EARTH_RADIUS_METER * (math.pi / 180)
1118                meters_per_lon = meters_per_lat * math.cos(math.radians(center_lat))
1119
1120                dx = distance * math.cos(rad)
1121                dy = distance * math.sin(rad)
1122
1123                delta_lon = dx / meters_per_lon
1124                delta_lat = dy / meters_per_lat
1125
1126                hotspot = HotspotData(
1127                    avg_lat=center_lat + delta_lat,
1128                    avg_lon=center_lon + delta_lon,
1129                    delta_ch4=0.0,
1130                    delta_c2h6=0.0,
1131                    ratio=0.0,
1132                    type=f"{direction}_{distance}m",
1133                    section=0,
1134                    source="scale_check",
1135                    angle=0,
1136                    correlation=0,
1137                )
1138                dummy_hotspots.append(hotspot)
1139        else:
1140            # 指定された緯度経度を使用
1141            dummy_hotspots = []
1142            for lat, lon, label in check_points:
1143                hotspot = HotspotData(
1144                    avg_lat=lat,
1145                    avg_lon=lon,
1146                    delta_ch4=0.0,
1147                    delta_c2h6=0.0,
1148                    ratio=0.0,
1149                    type=label,
1150                    section=0,
1151                    source="scale_check",
1152                    angle=0,
1153                    correlation=0,
1154                )
1155                dummy_hotspots.append(hotspot)
1156
1157        # カスタムカラーマップの作成
1158        hotspot_colors = {
1159            spot.type: plt.cm.tab10(i % 10) for i, spot in enumerate(dummy_hotspots)
1160        }
1161
1162        # 既存のメソッドを呼び出してプロット
1163        self.plot_flux_footprint_with_hotspots(
1164            x_list=x_list,
1165            y_list=y_list,
1166            c_list=c_list,
1167            center_lat=center_lat,
1168            center_lon=center_lon,
1169            vmin=vmin,
1170            vmax=vmax,
1171            add_cbar=add_cbar,
1172            add_legend=True,
1173            cbar_label=cbar_label,
1174            cbar_labelpad=cbar_labelpad,
1175            cmap=cmap,
1176            reduce_c_function=reduce_c_function,
1177            hotspots=dummy_hotspots,
1178            hotspot_colors=hotspot_colors,
1179            lat_correction=lat_correction,
1180            lon_correction=lon_correction,
1181            output_dir=output_dir,
1182            output_filename=output_filename,
1183            save_fig=save_fig,
1184            show_fig=show_fig,
1185            satellite_image=satellite_image,
1186            xy_max=xy_max,
1187        )

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

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

Parameters

x_list : list[float]
    フットプリントのx座標リスト(メートル単位)。
y_list : list[float]
    フットプリントのy座標リスト(メートル単位)。
c_list : list[float] | None
    フットプリントの強度を示す値のリスト。
center_lat : float
    プロットの中心となる緯度。
center_lon : float
    プロットの中心となる経度。
check_points : list[tuple[float, float, str]] | None
    確認用の地点リスト。各要素は (緯度, 経度, ラベル) のタプル。
    Noneの場合は中心から500m、1000m、2000m、3000mの位置に仮想的な点を配置。
cmap : str
    使用するカラーマップの名前。
vmin : float
    カラーバーの最小値。
vmax : float
    カラーバーの最大値。
reduce_c_function : callable, optional
    フットプリントの集約関数(デフォルトはnp.mean)。
cbar_label : str, optional
    カラーバーのラベル。
cbar_labelpad : int, optional
    カラーバーラベルのパディング。
hotspots : list[HotspotData] | None
    ホットスポットデータのリスト。デフォルトはNone。
hotspot_colors : dict[str, str] | None, optional
    ホットスポットの色を指定する辞書。
lon_correction : float, optional
    経度方向の補正係数(デフォルトは1)。
lat_correction : float, optional
    緯度方向の補正係数(デフォルトは1)。
output_dir : str | Path | None, optional
    プロット画像の保存先パス。
output_filename : str
    プロット画像の保存ファイル名(拡張子を含む)。デフォルトは'footprint.png'。
save_fig : bool
    図の保存を許可するフラグ。デフォルトはTrue。
show_fig : bool
    図の表示を許可するフラグ。デフォルトはTrue。
satellite_image : ImageFile | None, optional
    使用する衛星画像。指定がない場合はデフォルトの画像が生成されます。
xy_max : float, optional
    表示範囲の最大値(デフォルトは5000)。
@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:
1416    @staticmethod
1417    def filter_data(
1418        df: pd.DataFrame,
1419        start_date: str | None = None,
1420        end_date: str | None = None,
1421        months: list[int] | None = None,
1422    ) -> pd.DataFrame:
1423        """
1424        指定された期間や月でデータをフィルタリングするメソッド。
1425
1426        Parameters
1427        ----------
1428            df : pd.DataFrame
1429                フィルタリングするデータフレーム
1430            start_date : str | None
1431                フィルタリングの開始日('YYYY-MM-DD'形式)。デフォルトはNone。
1432            end_date : str | None
1433                フィルタリングの終了日('YYYY-MM-DD'形式)。デフォルトはNone。
1434            months : list[int] | None
1435                フィルタリングする月のリスト(例:[1, 2, 12])。デフォルトはNone。
1436
1437        Returns
1438        ----------
1439            pd.DataFrame
1440                フィルタリングされたデータフレーム
1441
1442        Raises
1443        ----------
1444            ValueError
1445                インデックスがDatetimeIndexでない場合、または日付の形式が不正な場合
1446        """
1447        # インデックスの検証
1448        if not isinstance(df.index, pd.DatetimeIndex):
1449            raise ValueError(
1450                "DataFrameのインデックスはDatetimeIndexである必要があります"
1451            )
1452
1453        df_copied: pd.DataFrame = df.copy()
1454
1455        # 日付形式の検証と変換
1456        try:
1457            if start_date is not None:
1458                start_date = pd.to_datetime(start_date)
1459            if end_date is not None:
1460                end_date = pd.to_datetime(end_date)
1461        except ValueError as e:
1462            raise ValueError(
1463                "日付の形式が不正です。'YYYY-MM-DD'形式で指定してください"
1464            ) from e
1465
1466        # 期間でフィルタリング
1467        if start_date is not None or end_date is not None:
1468            df_copied = df_copied.loc[start_date:end_date]
1469
1470        # 月のバリデーション
1471        if months is not None:
1472            if not all(isinstance(m, int) and 1 <= m <= 12 for m in months):
1473                raise ValueError(
1474                    "monthsは1から12までの整数のリストである必要があります"
1475                )
1476            df_copied = df_copied[df_copied.index.month.isin(months)]
1477
1478        # フィルタリング後のデータが空でないことを確認
1479        if df_copied.empty:
1480            raise ValueError("フィルタリング後のデータが空になりました")
1481
1482        return df_copied

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

Parameters

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

Returns

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

Raises

ValueError
    インデックスがDatetimeIndexでない場合、または日付の形式が不正な場合
@staticmethod
def is_weekday(date: datetime.datetime) -> int:
1484    @staticmethod
1485    def is_weekday(date: datetime) -> int:
1486        """
1487        指定された日付が平日であるかどうかを判定します。
1488
1489        Parameters
1490        ----------
1491            date : datetime
1492                判定する日付。
1493
1494        Returns
1495        ----------
1496            int
1497                平日であれば1、そうでなければ0。
1498        """
1499        return 1 if not jpholiday.is_holiday(date) and date.weekday() < 5 else 0

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

Parameters

date : datetime
    判定する日付。

Returns

int
    平日であれば1、そうでなければ0。
class CorrectingUtils:
 47class CorrectingUtils:
 48    @staticmethod
 49    def correct_h2o_interference(
 50        df: pd.DataFrame,
 51        coef_a: float,
 52        coef_b: float,
 53        coef_c: float,
 54        col_ch4_ppm: str = "ch4_ppm",
 55        col_h2o_ppm: str = "h2o_ppm",
 56        h2o_ppm_threshold: float | None = 2000,
 57    ) -> pd.DataFrame:
 58        """
 59        水蒸気干渉を補正するためのメソッドです。
 60        CH4濃度に対する水蒸気の影響を2次関数を用いて補正します。
 61
 62        References
 63        ----------
 64            - Commane et al. (2023): Intercomparison of commercial analyzers for atmospheric ethane and methane observations
 65                https://amt.copernicus.org/articles/16/1431/2023/,
 66                https://amt.copernicus.org/articles/16/1431/2023/amt-16-1431-2023.pdf
 67
 68        Parameters
 69        ----------
 70            df : pd.DataFrame
 71                補正対象のデータフレーム
 72            coef_a : float
 73                補正曲線の切片
 74            coef_b : float
 75                補正曲線の1次係数
 76            coef_c : float
 77                補正曲線の2次係数
 78            col_ch4_ppm : str
 79                CH4濃度を示すカラム名
 80            col_h2o_ppm : str
 81                水蒸気濃度を示すカラム名
 82            h2o_ppm_threshold : float | None
 83                水蒸気濃度の下限値(この値未満のデータは除外)
 84
 85        Returns
 86        ----------
 87            pd.DataFrame
 88                水蒸気干渉が補正されたデータフレーム
 89        """
 90        # 元のデータを保護するためコピーを作成
 91        df_h2o_corrected = df.copy()
 92        # 水蒸気濃度の配列を取得
 93        h2o = np.array(df_h2o_corrected[col_h2o_ppm])
 94
 95        # 補正項の計算
 96        correction_curve = coef_a + coef_b * h2o + coef_c * pow(h2o, 2)
 97        max_correction = np.max(correction_curve)
 98        correction_term = -(correction_curve - max_correction)
 99
100        # CH4濃度の補正
101        df_h2o_corrected[col_ch4_ppm] = df_h2o_corrected[col_ch4_ppm] + correction_term
102
103        # 極端に低い水蒸気濃度のデータは信頼性が低いため除外
104        if h2o_ppm_threshold is not None:
105            df_h2o_corrected.loc[df[col_h2o_ppm] < h2o_ppm_threshold, col_ch4_ppm] = np.nan
106            df_h2o_corrected = df_h2o_corrected.dropna(subset=[col_ch4_ppm])
107
108        return df_h2o_corrected
109
110    @staticmethod
111    def remove_bias(
112        df: pd.DataFrame,
113        col_ch4_ppm: str = "ch4_ppm",
114        col_c2h6_ppb: str = "c2h6_ppb",
115        base_ch4_ppm: float = 2.0,
116        base_c2h6_ppb: float = 0,
117        percentile: float = 5,
118    ) -> pd.DataFrame:
119        """
120        データフレームからバイアスを除去します。
121
122        Parameters
123        ----------
124            df : pd.DataFrame
125                バイアスを除去する対象のデータフレーム。
126            col_ch4_ppm : str
127                CH4濃度を示すカラム名。デフォルトは"ch4_ppm"。
128            col_c2h6_ppb : str
129                C2H6濃度を示すカラム名。デフォルトは"c2h6_ppb"。
130            base_ch4_ppm : float
131                補正前の値から最小値を引いた後に足すCH4濃度。
132            base_c2h6_ppb : float
133                補正前の値から最小値を引いた後に足すC2H6濃度。
134            percentile : float
135                下位何パーセンタイルの値を最小値として補正を行うか。
136                
137        Returns
138        ----------
139            pd.DataFrame
140                バイアスが除去されたデータフレーム。
141        """
142        df_copied: pd.DataFrame = df.copy()
143        c2h6_min = np.percentile(df_copied[col_c2h6_ppb], percentile)
144        df_copied[col_c2h6_ppb] = df_copied[col_c2h6_ppb] - c2h6_min + base_c2h6_ppb
145        ch4_min = np.percentile(df_copied[col_ch4_ppm], percentile)
146        df_copied[col_ch4_ppm] = df_copied[col_ch4_ppm] - ch4_min + base_ch4_ppm
147        return df_copied
@staticmethod
def correct_h2o_interference( df: pandas.core.frame.DataFrame, coef_a: float, coef_b: float, coef_c: float, col_ch4_ppm: str = 'ch4_ppm', col_h2o_ppm: str = 'h2o_ppm', h2o_ppm_threshold: float | None = 2000) -> pandas.core.frame.DataFrame:
 48    @staticmethod
 49    def correct_h2o_interference(
 50        df: pd.DataFrame,
 51        coef_a: float,
 52        coef_b: float,
 53        coef_c: float,
 54        col_ch4_ppm: str = "ch4_ppm",
 55        col_h2o_ppm: str = "h2o_ppm",
 56        h2o_ppm_threshold: float | None = 2000,
 57    ) -> pd.DataFrame:
 58        """
 59        水蒸気干渉を補正するためのメソッドです。
 60        CH4濃度に対する水蒸気の影響を2次関数を用いて補正します。
 61
 62        References
 63        ----------
 64            - Commane et al. (2023): Intercomparison of commercial analyzers for atmospheric ethane and methane observations
 65                https://amt.copernicus.org/articles/16/1431/2023/,
 66                https://amt.copernicus.org/articles/16/1431/2023/amt-16-1431-2023.pdf
 67
 68        Parameters
 69        ----------
 70            df : pd.DataFrame
 71                補正対象のデータフレーム
 72            coef_a : float
 73                補正曲線の切片
 74            coef_b : float
 75                補正曲線の1次係数
 76            coef_c : float
 77                補正曲線の2次係数
 78            col_ch4_ppm : str
 79                CH4濃度を示すカラム名
 80            col_h2o_ppm : str
 81                水蒸気濃度を示すカラム名
 82            h2o_ppm_threshold : float | None
 83                水蒸気濃度の下限値(この値未満のデータは除外)
 84
 85        Returns
 86        ----------
 87            pd.DataFrame
 88                水蒸気干渉が補正されたデータフレーム
 89        """
 90        # 元のデータを保護するためコピーを作成
 91        df_h2o_corrected = df.copy()
 92        # 水蒸気濃度の配列を取得
 93        h2o = np.array(df_h2o_corrected[col_h2o_ppm])
 94
 95        # 補正項の計算
 96        correction_curve = coef_a + coef_b * h2o + coef_c * pow(h2o, 2)
 97        max_correction = np.max(correction_curve)
 98        correction_term = -(correction_curve - max_correction)
 99
100        # CH4濃度の補正
101        df_h2o_corrected[col_ch4_ppm] = df_h2o_corrected[col_ch4_ppm] + correction_term
102
103        # 極端に低い水蒸気濃度のデータは信頼性が低いため除外
104        if h2o_ppm_threshold is not None:
105            df_h2o_corrected.loc[df[col_h2o_ppm] < h2o_ppm_threshold, col_ch4_ppm] = np.nan
106            df_h2o_corrected = df_h2o_corrected.dropna(subset=[col_ch4_ppm])
107
108        return df_h2o_corrected

水蒸気干渉を補正するためのメソッドです。 CH4濃度に対する水蒸気の影響を2次関数を用いて補正します。

References

- Commane et al. (2023): Intercomparison of commercial analyzers for atmospheric ethane and methane observations
    https://amt.copernicus.org/articles/16/1431/2023/,
    https://amt.copernicus.org/articles/16/1431/2023/amt-16-1431-2023.pdf

Parameters

df : pd.DataFrame
    補正対象のデータフレーム
coef_a : float
    補正曲線の切片
coef_b : float
    補正曲線の1次係数
coef_c : float
    補正曲線の2次係数
col_ch4_ppm : str
    CH4濃度を示すカラム名
col_h2o_ppm : str
    水蒸気濃度を示すカラム名
h2o_ppm_threshold : float | None
    水蒸気濃度の下限値(この値未満のデータは除外)

Returns

pd.DataFrame
    水蒸気干渉が補正されたデータフレーム
@staticmethod
def remove_bias( df: pandas.core.frame.DataFrame, col_ch4_ppm: str = 'ch4_ppm', col_c2h6_ppb: str = 'c2h6_ppb', base_ch4_ppm: float = 2.0, base_c2h6_ppb: float = 0, percentile: float = 5) -> pandas.core.frame.DataFrame:
110    @staticmethod
111    def remove_bias(
112        df: pd.DataFrame,
113        col_ch4_ppm: str = "ch4_ppm",
114        col_c2h6_ppb: str = "c2h6_ppb",
115        base_ch4_ppm: float = 2.0,
116        base_c2h6_ppb: float = 0,
117        percentile: float = 5,
118    ) -> pd.DataFrame:
119        """
120        データフレームからバイアスを除去します。
121
122        Parameters
123        ----------
124            df : pd.DataFrame
125                バイアスを除去する対象のデータフレーム。
126            col_ch4_ppm : str
127                CH4濃度を示すカラム名。デフォルトは"ch4_ppm"。
128            col_c2h6_ppb : str
129                C2H6濃度を示すカラム名。デフォルトは"c2h6_ppb"。
130            base_ch4_ppm : float
131                補正前の値から最小値を引いた後に足すCH4濃度。
132            base_c2h6_ppb : float
133                補正前の値から最小値を引いた後に足すC2H6濃度。
134            percentile : float
135                下位何パーセンタイルの値を最小値として補正を行うか。
136                
137        Returns
138        ----------
139            pd.DataFrame
140                バイアスが除去されたデータフレーム。
141        """
142        df_copied: pd.DataFrame = df.copy()
143        c2h6_min = np.percentile(df_copied[col_c2h6_ppb], percentile)
144        df_copied[col_c2h6_ppb] = df_copied[col_c2h6_ppb] - c2h6_min + base_c2h6_ppb
145        ch4_min = np.percentile(df_copied[col_ch4_ppm], percentile)
146        df_copied[col_ch4_ppm] = df_copied[col_ch4_ppm] - ch4_min + base_ch4_ppm
147        return df_copied

データフレームからバイアスを除去します。

Parameters

df : pd.DataFrame
    バイアスを除去する対象のデータフレーム。
col_ch4_ppm : str
    CH4濃度を示すカラム名。デフォルトは"ch4_ppm"。
col_c2h6_ppb : str
    C2H6濃度を示すカラム名。デフォルトは"c2h6_ppb"。
base_ch4_ppm : float
    補正前の値から最小値を引いた後に足すCH4濃度。
base_c2h6_ppb : float
    補正前の値から最小値を引いた後に足すC2H6濃度。
percentile : float
    下位何パーセンタイルの値を最小値として補正を行うか。

Returns

pd.DataFrame
    バイアスが除去されたデータフレーム。
@dataclass
class H2OCorrectionConfig:
 7@dataclass
 8class H2OCorrectionConfig:
 9    """水蒸気補正の設定を保持するデータクラス
10
11    Parameters
12    ----------
13    coef_a : float | None
14        補正曲線の切片
15    coef_b : float | None
16        補正曲線の1次係数
17    coef_c : float | None
18        補正曲線の2次係数
19    h2o_ppm_threshold : float | None
20        水蒸気濃度の下限値(この値未満のデータは除外)
21    """
22
23    coef_a: float | None = None
24    coef_b: float | None = None
25    coef_c: float | None = None
26    h2o_ppm_threshold: float | None = 2000

水蒸気補正の設定を保持するデータクラス

Parameters

coef_a : float | None 補正曲線の切片 coef_b : float | None 補正曲線の1次係数 coef_c : float | None 補正曲線の2次係数 h2o_ppm_threshold : float | None 水蒸気濃度の下限値(この値未満のデータは除外)

H2OCorrectionConfig( coef_a: float | None = None, coef_b: float | None = None, coef_c: float | None = None, h2o_ppm_threshold: float | None = 2000)
coef_a: float | None = None
coef_b: float | None = None
coef_c: float | None = None
h2o_ppm_threshold: float | None = 2000
@dataclass
class BiasRemovalConfig:
28@dataclass
29class BiasRemovalConfig:
30    """バイアス除去の設定を保持するデータクラス
31
32    Parameters
33    ----------
34    percentile : float
35        バイアス除去に使用するパーセンタイル値
36    base_ch4_ppm : float
37        補正前の値から最小値を引いた後に足すCH4濃度の基準値。
38    base_c2h6_ppb : float
39        補正前の値から最小値を引いた後に足すC2H6濃度の基準値。
40    """
41
42    percentile: float = 5.0
43    base_ch4_ppm: float = 2.0
44    base_c2h6_ppb: float = 0

バイアス除去の設定を保持するデータクラス

Parameters

percentile : float バイアス除去に使用するパーセンタイル値 base_ch4_ppm : float 補正前の値から最小値を引いた後に足すCH4濃度の基準値。 base_c2h6_ppb : float 補正前の値から最小値を引いた後に足すC2H6濃度の基準値。

BiasRemovalConfig( percentile: float = 5.0, base_ch4_ppm: float = 2.0, base_c2h6_ppb: float = 0)
percentile: float = 5.0
base_ch4_ppm: float = 2.0
base_c2h6_ppb: float = 0
@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: データクラスの属性と値を含む辞書
@dataclass
class HotspotParams:
169@dataclass
170class HotspotParams:
171    """ホットスポット解析のパラメータ設定
172
173    Parameters
174    ----------
175    CH4_PPM : str
176        CH4濃度を示すカラム名
177    C2H6_PPB : str
178        C2H6濃度を示すカラム名
179    H2O_PPM : str
180        H2O濃度を示すカラム名
181    CH4_PPM_DELTA_THRESHOLD : float
182        CH4の閾値
183    C2H6_PPB_DELTA_THRESHOLD : float
184        C2H6の閾値
185    USE_QUANTILE : bool
186        5パーセンタイルを使用するかどうかのフラグ
187    ROLLING_METHOD : RollingMethod
188        移動計算の方法
189        - "quantile"は下位{QUANTILE_VALUE}%の値を使用する。
190        - "mean"は移動平均を行う。
191    QUANTILE_VALUE : float
192        下位何パーセントの値を使用するか。デフォルトは5。
193    """
194
195    CH4_PPM: str = "ch4_ppm"
196    C2H6_PPB: str = "c2h6_ppb"
197    H2O_PPM: str = "h2o_ppm"
198    CH4_PPM_DELTA_THRESHOLD: float = 0.05
199    C2H6_PPB_DELTA_THRESHOLD: float = 0.0
200    H2O_PPM_THRESHOLD: float = 2000
201    ROLLING_METHOD: RollingMethod = "quantile"
202    QUANTILE_VALUE: float = 5
203
204    def __post_init__(self) -> None:
205        """パラメータの検証を行います。
206
207        Raises
208        ----------
209            ValueError: QUANTILE_VALUEが0以上100以下でない場合
210        """
211        # QUANTILE_VALUEの値域チェック
212        if not 0 <= self.QUANTILE_VALUE <= 100:
213            raise ValueError(
214                f"QUANTILE_VALUE must be between 0 and 100, got {self.QUANTILE_VALUE}"
215            )

ホットスポット解析のパラメータ設定

Parameters

CH4_PPM : str CH4濃度を示すカラム名 C2H6_PPB : str C2H6濃度を示すカラム名 H2O_PPM : str H2O濃度を示すカラム名 CH4_PPM_DELTA_THRESHOLD : float CH4の閾値 C2H6_PPB_DELTA_THRESHOLD : float C2H6の閾値 USE_QUANTILE : bool 5パーセンタイルを使用するかどうかのフラグ ROLLING_METHOD : RollingMethod 移動計算の方法 - "quantile"は下位{QUANTILE_VALUE}%の値を使用する。 - "mean"は移動平均を行う。 QUANTILE_VALUE : float 下位何パーセントの値を使用するか。デフォルトは5。

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

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

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, hotspot_params: HotspotParams | None = None, 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'}, na_values: list[str] = ['No Data', 'nan'], logger: logging.Logger | None = None, logging_debug: bool = False)
312    def __init__(
313        self,
314        center_lat: float,
315        center_lon: float,
316        inputs: list[MSAInputConfig] | list[tuple[float, float, str | Path]],
317        num_sections: int = 4,
318        ch4_enhance_threshold: float = 0.1,
319        correlation_threshold: float = 0.7,
320        hotspot_area_meter: float = 50,
321        hotspot_params: HotspotParams | None = None,
322        window_minutes: float = 5,
323        column_mapping: dict[str, str] = {
324            "Time Stamp": "timestamp",
325            "CH4 (ppm)": "ch4_ppm",
326            "C2H6 (ppb)": "c2h6_ppb",
327            "H2O (ppm)": "h2o_ppm",
328            "Latitude": "latitude",
329            "Longitude": "longitude",
330        },
331        na_values: list[str] = ["No Data", "nan"],
332        logger: Logger | None = None,
333        logging_debug: bool = False,
334    ):
335        """
336        測定データ解析クラスの初期化
337
338        Parameters
339        ----------
340            center_lat : float
341                中心緯度
342            center_lon : float
343                中心経度
344            inputs : list[MSAInputConfig] | list[tuple[float, float, str | Path]]
345                入力ファイルのリスト
346            num_sections : int
347                分割する区画数。デフォルトは4。
348            ch4_enhance_threshold : float
349                CH4増加の閾値(ppm)。デフォルトは0.1。
350            correlation_threshold : float
351                相関係数の閾値。デフォルトは0.7。
352            hotspot_area_meter : float
353                ホットスポットの検出に使用するエリアの半径(メートル)。デフォルトは50メートル。
354            hotspot_params : HotspotParams | None, optional
355                ホットスポット解析のパラメータ設定
356            window_minutes : float
357                移動窓の大きさ(分)。デフォルトは5分。
358            column_mapping : dict[str, str]
359                元のデータファイルのヘッダーを汎用的な単語に変換するための辞書型データ。
360                - timestamp,ch4_ppm,c2h6_ppm,h2o_ppm,latitude,longitudeをvalueに、それぞれに対応するカラム名をcolに指定してください。
361                - デフォルト: {
362                    "Time Stamp": "timestamp",
363                    "CH4 (ppm)": "ch4_ppm",
364                    "C2H6 (ppb)": "c2h6_ppb",
365                    "H2O (ppm)": "h2o_ppm",
366                    "Latitude": "latitude",
367                    "Longitude": "longitude",
368                }
369            na_values : list[str]
370                NaNと判定する値のパターン。
371            logger : Logger | None
372                使用するロガー。Noneの場合は新しいロガーを作成します。
373            logging_debug : bool
374                ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。
375
376        Returns
377        ----------
378            None
379                初期化処理が完了したことを示します。
380        """
381        # ロガー
382        log_level: int = INFO
383        if logging_debug:
384            log_level = DEBUG
385        self.logger: Logger = MobileSpatialAnalyzer.setup_logger(logger, log_level)
386        # プライベートなプロパティ
387        self._center_lat: float = center_lat
388        self._center_lon: float = center_lon
389        self._ch4_enhance_threshold: float = ch4_enhance_threshold
390        self._correlation_threshold: float = correlation_threshold
391        self._hotspot_area_meter: float = hotspot_area_meter
392        self._column_mapping: dict[str, str] = column_mapping
393        self._na_values: list[str] = na_values
394        self._hotspot_params = hotspot_params or HotspotParams()
395        self._num_sections: int = num_sections
396        # セクションの範囲
397        section_size: float = 360 / num_sections
398        self._section_size: float = section_size
399        self._sections = MobileSpatialAnalyzer._initialize_sections(
400            num_sections, section_size
401        )
402        # window_sizeをデータポイント数に変換(分→秒→データポイント数)
403        self._window_size: int = MobileSpatialAnalyzer._calculate_window_size(
404            window_minutes
405        )
406        # 入力設定の標準化
407        normalized_input_configs: list[MSAInputConfig] = (
408            MobileSpatialAnalyzer._normalize_inputs(inputs)
409        )
410        # 複数ファイルのデータを読み込み
411        self._data: dict[str, pd.DataFrame] = self._load_all_data(
412            normalized_input_configs
413        )

測定データ解析クラスの初期化

Parameters

center_lat : float
    中心緯度
center_lon : float
    中心経度
inputs : list[MSAInputConfig] | list[tuple[float, float, str | Path]]
    入力ファイルのリスト
num_sections : int
    分割する区画数。デフォルトは4。
ch4_enhance_threshold : float
    CH4増加の閾値(ppm)。デフォルトは0.1。
correlation_threshold : float
    相関係数の閾値。デフォルトは0.7。
hotspot_area_meter : float
    ホットスポットの検出に使用するエリアの半径(メートル)。デフォルトは50メートル。
hotspot_params : HotspotParams | None, optional
    ホットスポット解析のパラメータ設定
window_minutes : float
    移動窓の大きさ(分)。デフォルトは5分。
column_mapping : dict[str, str]
    元のデータファイルのヘッダーを汎用的な単語に変換するための辞書型データ。
    - timestamp,ch4_ppm,c2h6_ppm,h2o_ppm,latitude,longitudeをvalueに、それぞれに対応するカラム名をcolに指定してください。
    - デフォルト: {
        "Time Stamp": "timestamp",
        "CH4 (ppm)": "ch4_ppm",
        "C2H6 (ppb)": "c2h6_ppb",
        "H2O (ppm)": "h2o_ppm",
        "Latitude": "latitude",
        "Longitude": "longitude",
    }
na_values : list[str]
    NaNと判定する値のパターン。
logger : Logger | None
    使用するロガー。Noneの場合は新しいロガーを作成します。
logging_debug : bool
    ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。

Returns

None
    初期化処理が完了したことを示します。
EARTH_RADIUS_METERS: float = 6371000
logger: logging.Logger
hotspot_params: HotspotParams
415    @property
416    def hotspot_params(self) -> HotspotParams:
417        """ホットスポット解析のパラメータ設定を取得"""
418        return self._hotspot_params

ホットスポット解析のパラメータ設定を取得

def analyze_delta_ch4_stats( self, hotspots: list[HotspotData]) -> None:
425    def analyze_delta_ch4_stats(self, hotspots: list[HotspotData]) -> None:
426        """
427        各タイプのホットスポットについてΔCH4の統計情報を計算し、結果を表示します。
428
429        Parameters
430        ----------
431            hotspots : list[HotspotData]
432                分析対象のホットスポットリスト
433
434        Returns
435        ----------
436            None
437                統計情報の表示が完了したことを示します。
438        """
439        # タイプごとにホットスポットを分類
440        hotspots_by_type: dict[HotspotType, list[HotspotData]] = {
441            "bio": [h for h in hotspots if h.type == "bio"],
442            "gas": [h for h in hotspots if h.type == "gas"],
443            "comb": [h for h in hotspots if h.type == "comb"],
444        }
445
446        # 統計情報を計算し、表示
447        for spot_type, spots in hotspots_by_type.items():
448            if spots:
449                delta_ch4_values = [spot.delta_ch4 for spot in spots]
450                max_value = max(delta_ch4_values)
451                mean_value = sum(delta_ch4_values) / len(delta_ch4_values)
452                median_value = sorted(delta_ch4_values)[len(delta_ch4_values) // 2]
453                print(f"{spot_type}タイプのホットスポットの統計情報:")
454                print(f"  最大値: {max_value}")
455                print(f"  平均値: {mean_value}")
456                print(f"  中央値: {median_value}")
457            else:
458                print(f"{spot_type}タイプのホットスポットは存在しません。")

各タイプのホットスポットについてΔCH4の統計情報を計算し、結果を表示します。

Parameters

hotspots : list[HotspotData]
    分析対象のホットスポットリスト

Returns

None
    統計情報の表示が完了したことを示します。
def analyze_hotspots( self, duplicate_check_mode: Literal['none', 'time_window', 'time_all'] = 'none', min_time_threshold_seconds: float = 300, max_time_threshold_hours: float = 12) -> list[HotspotData]:
460    def analyze_hotspots(
461        self,
462        duplicate_check_mode: Literal["none", "time_window", "time_all"] = "none",
463        min_time_threshold_seconds: float = 300,
464        max_time_threshold_hours: float = 12,
465    ) -> list[HotspotData]:
466        """
467        ホットスポットを検出して分析します。
468
469        Parameters
470        ----------
471            duplicate_check_mode : Literal["none", "time_window", "time_all"]
472                重複チェックのモード。
473                - "none": 重複チェックを行わない。
474                - "time_window": 指定された時間窓内の重複のみを除外。
475                - "time_all": すべての時間範囲で重複チェックを行う。
476            min_time_threshold_seconds : float
477                重複とみなす最小時間の閾値(秒)。デフォルトは300秒。
478            max_time_threshold_hours : float
479                重複チェックを一時的に無視する最大時間の閾値(時間)。デフォルトは12時間。
480
481        Returns
482        ----------
483            list[HotspotData]
484                検出されたホットスポットのリスト。
485        """
486        all_hotspots: list[HotspotData] = []
487        params: HotspotParams = self._hotspot_params
488
489        # 各データソースに対して解析を実行
490        for _, df in self._data.items():
491            # パラメータの計算
492            df = MobileSpatialAnalyzer._calculate_hotspots_parameters(
493                df=df,
494                window_size=self._window_size,
495                col_ch4_ppm=params.CH4_PPM,
496                col_c2h6_ppb=params.C2H6_PPB,
497                col_h2o_ppm=params.H2O_PPM,
498                ch4_ppm_delta_threshold=params.CH4_PPM_DELTA_THRESHOLD,
499                c2h6_ppb_delta_threshold=params.C2H6_PPB_DELTA_THRESHOLD,
500                h2o_ppm_threshold=params.H2O_PPM_THRESHOLD,
501                rolling_method=params.ROLLING_METHOD,
502                quantile_value=params.QUANTILE_VALUE,
503            )
504
505            # ホットスポットの検出
506            hotspots: list[HotspotData] = self._detect_hotspots(
507                df=df,
508                ch4_enhance_threshold=self._ch4_enhance_threshold,
509            )
510            all_hotspots.extend(hotspots)
511
512        # 重複チェックモードに応じて処理
513        if duplicate_check_mode != "none":
514            unique_hotspots = MobileSpatialAnalyzer.remove_hotspots_duplicates(
515                all_hotspots,
516                check_time_all=(duplicate_check_mode == "time_all"),
517                min_time_threshold_seconds=min_time_threshold_seconds,
518                max_time_threshold_hours=max_time_threshold_hours,
519                hotspot_area_meter=self._hotspot_area_meter,
520            )
521            self.logger.info(
522                f"重複除外: {len(all_hotspots)}{len(unique_hotspots)} ホットスポット"
523            )
524            return unique_hotspots
525
526        return all_hotspots

ホットスポットを検出して分析します。

Parameters

duplicate_check_mode : Literal["none", "time_window", "time_all"]
    重複チェックのモード。
    - "none": 重複チェックを行わない。
    - "time_window": 指定された時間窓内の重複のみを除外。
    - "time_all": すべての時間範囲で重複チェックを行う。
min_time_threshold_seconds : float
    重複とみなす最小時間の閾値(秒)。デフォルトは300秒。
max_time_threshold_hours : float
    重複チェックを一時的に無視する最大時間の閾値(時間)。デフォルトは12時間。

Returns

list[HotspotData]
    検出されたホットスポットのリスト。
def calculate_measurement_stats( self, print_individual_stats: bool = True, print_total_stats: bool = True, col_latitude: str = 'latitude', col_longitude: str = 'longitude') -> tuple[float, datetime.timedelta]:
528    def calculate_measurement_stats(
529        self,
530        print_individual_stats: bool = True,
531        print_total_stats: bool = True,
532        col_latitude: str = "latitude",
533        col_longitude: str = "longitude",
534    ) -> tuple[float, timedelta]:
535        """
536        各ファイルの測定時間と走行距離を計算し、合計を返します。
537
538        Parameters
539        ----------
540            print_individual_stats : bool
541                個別ファイルの統計を表示するかどうか。デフォルトはTrue。
542            print_total_stats : bool
543                合計統計を表示するかどうか。デフォルトはTrue。
544            col_latitude : str
545                緯度情報が格納されているカラム名。デフォルトは"latitude"。
546            col_longitude : str
547                経度情報が格納されているカラム名。デフォルトは"longitude"。
548
549        Returns
550        ----------
551            tuple[float, timedelta]
552                総距離(km)と総時間のタプル
553        """
554        total_distance: float = 0.0
555        total_time: timedelta = timedelta()
556        individual_stats: list[dict] = []  # 個別の統計情報を保存するリスト
557
558        # プログレスバーを表示しながら計算
559        for source_name, df in tqdm(
560            self._data.items(), desc="Calculating", unit="file"
561        ):
562            # 時間の計算
563            time_spent = df.index[-1] - df.index[0]
564
565            # 距離の計算
566            distance_km = 0.0
567            for i in range(len(df) - 1):
568                lat1, lon1 = df.iloc[i][[col_latitude, col_longitude]]
569                lat2, lon2 = df.iloc[i + 1][[col_latitude, col_longitude]]
570                distance_km += (
571                    MobileSpatialAnalyzer._calculate_distance(
572                        lat1=lat1, lon1=lon1, lat2=lat2, lon2=lon2
573                    )
574                    / 1000
575                )
576
577            # 合計に加算
578            total_distance += distance_km
579            total_time += time_spent
580
581            # 統計情報を保存
582            if print_individual_stats:
583                average_speed = distance_km / (time_spent.total_seconds() / 3600)
584                individual_stats.append(
585                    {
586                        "source": source_name,
587                        "distance": distance_km,
588                        "time": time_spent,
589                        "speed": average_speed,
590                    }
591                )
592
593        # 計算完了後に統計情報を表示
594        if print_individual_stats:
595            self.logger.info("=== Individual Stats ===")
596            for stat in individual_stats:
597                print(f"File         : {stat['source']}")
598                print(f"  Distance   : {stat['distance']:.2f} km")
599                print(f"  Time       : {stat['time']}")
600                print(f"  Avg. Speed : {stat['speed']:.1f} km/h\n")
601
602        # 合計を表示
603        if print_total_stats:
604            average_speed_total: float = total_distance / (
605                total_time.total_seconds() / 3600
606            )
607            self.logger.info("=== Total Stats ===")
608            print(f"  Distance   : {total_distance:.2f} km")
609            print(f"  Time       : {total_time}")
610            print(f"  Avg. Speed : {average_speed_total:.1f} km/h\n")
611
612        return total_distance, total_time

各ファイルの測定時間と走行距離を計算し、合計を返します。

Parameters

print_individual_stats : bool
    個別ファイルの統計を表示するかどうか。デフォルトはTrue。
print_total_stats : bool
    合計統計を表示するかどうか。デフォルトはTrue。
col_latitude : str
    緯度情報が格納されているカラム名。デフォルトは"latitude"。
col_longitude : str
    経度情報が格納されているカラム名。デフォルトは"longitude"。

Returns

tuple[float, timedelta]
    総距離(km)と総時間のタプル
def create_hotspots_map( self, hotspots: list[HotspotData], output_dir: str | pathlib.Path | None = None, output_filename: str = 'hotspots_map.html', center_marker_color: str = 'green', center_marker_label: str = 'Center', plot_center_marker: bool = True, radius_meters: float = 3000, save_fig: bool = True) -> None:
614    def create_hotspots_map(
615        self,
616        hotspots: list[HotspotData],
617        output_dir: str | Path | None = None,
618        output_filename: str = "hotspots_map.html",
619        center_marker_color: str = "green",
620        center_marker_label: str = "Center",
621        plot_center_marker: bool = True,
622        radius_meters: float = 3000,
623        save_fig: bool = True,
624    ) -> None:
625        """
626        ホットスポットの分布を地図上にプロットして保存
627
628        Parameters
629        ----------
630            hotspots : list[HotspotData]
631                プロットするホットスポットのリスト
632            output_dir : str | Path
633                保存先のディレクトリパス
634            output_filename : str
635                保存するファイル名。デフォルトは"hotspots_map"。
636            center_marker_color : str
637                中心を示すマーカーのラベルカラー。デフォルトは"green"。
638            center_marker_label : str
639                中心を示すマーカーのラベルテキスト。デフォルトは"Center"。
640            plot_center_marker : bool
641                中心を示すマーカーの有無。デフォルトはTrue。
642            radius_meters : float
643                区画分けを示す線の長さ。デフォルトは3000。
644            save_fig : bool
645                図の保存を許可するフラグ。デフォルトはTrue。
646        """
647        # 地図の作成
648        m = folium.Map(
649            location=[self._center_lat, self._center_lon],
650            zoom_start=15,
651            tiles="OpenStreetMap",
652        )
653
654        # ホットスポットの種類ごとに異なる色でプロット
655        for spot in hotspots:
656            # NaN値チェックを追加
657            if math.isnan(spot.avg_lat) or math.isnan(spot.avg_lon):
658                continue
659
660            # default type
661            color = "black"
662            # タイプに応じて色を設定
663            if spot.type == "comb":
664                color = "green"
665            elif spot.type == "gas":
666                color = "red"
667            elif spot.type == "bio":
668                color = "blue"
669
670            # CSSのgrid layoutを使用してHTMLタグを含むテキストをフォーマット
671            popup_html = f"""
672            <div style='font-family: Arial; font-size: 12px; display: grid; grid-template-columns: auto auto auto; gap: 5px;'>
673                <b>Date</b> <span>:</span> <span>{spot.source}</span>
674                <b>Lat</b> <span>:</span> <span>{spot.avg_lat:.3f}</span>
675                <b>Lon</b> <span>:</span> <span>{spot.avg_lon:.3f}</span>
676                <b>ΔCH<sub>4</sub></b> <span>:</span> <span>{spot.delta_ch4:.3f}</span>
677                <b>ΔC<sub>2</sub>H<sub>6</sub></b> <span>:</span> <span>{spot.delta_c2h6:.3f}</span>
678                <b>Ratio</b> <span>:</span> <span>{spot.ratio:.3f}</span>
679                <b>Type</b> <span>:</span> <span>{spot.type}</span>
680                <b>Section</b> <span>:</span> <span>{spot.section}</span>
681            </div>
682            """
683
684            # ポップアップのサイズを指定
685            popup = folium.Popup(
686                folium.Html(popup_html, script=True),
687                max_width=200,  # 最大幅(ピクセル)
688            )
689
690            folium.CircleMarker(
691                location=[spot.avg_lat, spot.avg_lon],
692                radius=8,
693                color=color,
694                fill=True,
695                popup=popup,
696            ).add_to(m)
697
698        # 中心点のマーカー
699        if plot_center_marker:
700            folium.Marker(
701                [self._center_lat, self._center_lon],
702                popup=center_marker_label,
703                icon=folium.Icon(color=center_marker_color, icon="info-sign"),
704            ).add_to(m)
705
706        # 区画の境界線を描画
707        for section in range(self._num_sections):
708            start_angle = math.radians(-180 + section * self._section_size)
709
710            R = self.EARTH_RADIUS_METERS
711
712            # 境界線の座標を計算
713            lat1 = self._center_lat
714            lon1 = self._center_lon
715            lat2 = math.degrees(
716                math.asin(
717                    math.sin(math.radians(lat1)) * math.cos(radius_meters / R)
718                    + math.cos(math.radians(lat1))
719                    * math.sin(radius_meters / R)
720                    * math.cos(start_angle)
721                )
722            )
723            lon2 = self._center_lon + math.degrees(
724                math.atan2(
725                    math.sin(start_angle)
726                    * math.sin(radius_meters / R)
727                    * math.cos(math.radians(lat1)),
728                    math.cos(radius_meters / R)
729                    - math.sin(math.radians(lat1)) * math.sin(math.radians(lat2)),
730                )
731            )
732
733            # 境界線を描画
734            folium.PolyLine(
735                locations=[[lat1, lon1], [lat2, lon2]],
736                color="black",
737                weight=1,
738                opacity=0.5,
739            ).add_to(m)
740
741        # 地図を保存
742        if save_fig and output_dir is None:
743            raise ValueError(
744                "save_fig=Trueの場合、output_dirを指定する必要があります。有効なディレクトリパスを指定してください。"
745            )
746            output_path: str = os.path.join(output_dir, output_filename)
747            m.save(str(output_path))
748            self.logger.info(f"地図を保存しました: {output_path}")

ホットスポットの分布を地図上にプロットして保存

Parameters

hotspots : list[HotspotData]
    プロットするホットスポットのリスト
output_dir : str | Path
    保存先のディレクトリパス
output_filename : str
    保存するファイル名。デフォルトは"hotspots_map"。
center_marker_color : str
    中心を示すマーカーのラベルカラー。デフォルトは"green"。
center_marker_label : str
    中心を示すマーカーのラベルテキスト。デフォルトは"Center"。
plot_center_marker : bool
    中心を示すマーカーの有無。デフォルトはTrue。
radius_meters : float
    区画分けを示す線の長さ。デフォルトは3000。
save_fig : bool
    図の保存を許可するフラグ。デフォルトはTrue。
def export_hotspots_to_csv( self, hotspots: list[HotspotData], output_dir: str | pathlib.Path | None = None, output_filename: str = 'hotspots.csv') -> None:
750    def export_hotspots_to_csv(
751        self,
752        hotspots: list[HotspotData],
753        output_dir: str | Path | None = None,
754        output_filename: str = "hotspots.csv",
755    ) -> None:
756        """
757        ホットスポットの情報をCSVファイルに出力します。
758
759        Parameters
760        ----------
761            hotspots : list[HotspotData]
762                出力するホットスポットのリスト
763            output_dir : str | Path | None
764                出力先ディレクトリ
765            output_filename : str
766                出力ファイル名
767        """
768        # 日時の昇順でソート
769        sorted_hotspots = sorted(hotspots, key=lambda x: x.source)
770
771        # 出力用のデータを作成
772        records = []
773        for spot in sorted_hotspots:
774            record = {
775                "source": spot.source,
776                "type": spot.type,
777                "delta_ch4": spot.delta_ch4,
778                "delta_c2h6": spot.delta_c2h6,
779                "ratio": spot.ratio,
780                "correlation": spot.correlation,
781                "angle": spot.angle,
782                "section": spot.section,
783                "latitude": spot.avg_lat,
784                "longitude": spot.avg_lon,
785            }
786            records.append(record)
787
788        # DataFrameに変換してCSVに出力
789        if output_dir is None:
790            raise ValueError(
791                "output_dirが指定されていません。有効なディレクトリパスを指定してください。"
792            )
793        os.makedirs(output_dir, exist_ok=True)
794        output_path: str = os.path.join(output_dir, output_filename)
795        df: pd.DataFrame = pd.DataFrame(records)
796        df.to_csv(output_path, index=False)
797        self.logger.info(
798            f"ホットスポット情報をCSVファイルに出力しました: {output_path}"
799        )

ホットスポットの情報をCSVファイルに出力します。

Parameters

hotspots : list[HotspotData]
    出力するホットスポットのリスト
output_dir : str | Path | None
    出力先ディレクトリ
output_filename : str
    出力ファイル名
@staticmethod
def extract_source_name_from_path(path: str | pathlib.Path) -> str:
801    @staticmethod
802    def extract_source_name_from_path(path: str | Path) -> str:
803        """
804        ファイルパスからソース名(拡張子なしのファイル名)を抽出します。
805
806        Parameters
807        ----------
808            path : str | Path
809                ソース名を抽出するファイルパス
810                例: "/path/to/Pico100121_241017_092120+.txt"
811
812        Returns
813        ----------
814            str
815                抽出されたソース名
816                例: "Pico100121_241017_092120+"
817
818        Examples:
819        ----------
820            >>> path = "/path/to/data/Pico100121_241017_092120+.txt"
821            >>> MobileSpatialAnalyzer.extract_source_from_path(path)
822            'Pico100121_241017_092120+'
823        """
824        # Pathオブジェクトに変換
825        path_obj: Path = Path(path)
826        # stem属性で拡張子なしのファイル名を取得
827        source_name: str = path_obj.stem
828        return source_name

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

Parameters

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

Returns

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

Examples:

>>> path = "/path/to/data/Pico100121_241017_092120+.txt"
>>> MobileSpatialAnalyzer.extract_source_from_path(path)
'Pico100121_241017_092120+'
def get_preprocessed_data(self) -> pandas.core.frame.DataFrame:
830    def get_preprocessed_data(
831        self,
832    ) -> pd.DataFrame:
833        """
834        データ前処理を行い、CH4とC2H6の相関解析に必要な形式に整えます。
835        コンストラクタで読み込んだすべてのデータを前処理し、結合したDataFrameを返します。
836
837        Returns
838        ----------
839            pd.DataFrame
840                前処理済みの結合されたDataFrame
841        """
842        processed_dfs: list[pd.DataFrame] = []
843        params: HotspotParams = self._hotspot_params
844
845        # 各データソースに対して解析を実行
846        for source_name, df in self._data.items():
847            # パラメータの計算
848            processed_df = MobileSpatialAnalyzer._calculate_hotspots_parameters(
849                df=df,
850                window_size=self._window_size,
851                col_ch4_ppm=params.CH4_PPM,
852                col_c2h6_ppb=params.C2H6_PPB,
853                col_h2o_ppm=params.H2O_PPM,
854                ch4_ppm_delta_threshold=params.CH4_PPM_DELTA_THRESHOLD,
855                c2h6_ppb_delta_threshold=params.C2H6_PPB_DELTA_THRESHOLD,
856                h2o_ppm_threshold=params.H2O_PPM_THRESHOLD,
857                rolling_method=params.ROLLING_METHOD,
858                quantile_value=params.QUANTILE_VALUE,
859            )
860            # ソース名を列として追加
861            processed_df["source"] = source_name
862            processed_dfs.append(processed_df)
863
864        # すべてのDataFrameを結合
865        if not processed_dfs:
866            raise ValueError("処理対象のデータが存在しません。")
867
868        combined_df: pd.DataFrame = pd.concat(processed_dfs, axis=0)
869        return combined_df

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

Returns

pd.DataFrame
    前処理済みの結合されたDataFrame
def get_section_size(self) -> float:
871    def get_section_size(self) -> float:
872        """
873        セクションのサイズを取得するメソッド。
874        このメソッドは、解析対象のデータを区画に分割する際の
875        各区画の角度範囲を示すサイズを返します。
876
877        Returns
878        ----------
879            float
880                1セクションのサイズ(度単位)
881        """
882        return self._section_size

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

Returns

float
    1セクションのサイズ(度単位)
def get_source_names(self, print_all: bool = False) -> list[str]:
884    def get_source_names(self, print_all: bool = False) -> list[str]:
885        """
886        データソースの名前を取得します。
887
888        Parameters
889        ----------
890        print_all : bool, optional
891            すべてのデータソース名を表示するかどうかを指定します。デフォルトはFalseです。
892
893        Returns
894        ----------
895        list[str]
896            データソース名のリスト
897
898        Raises
899        ----------
900        ValueError
901            データが読み込まれていない場合に発生します。
902        """
903        dfs_dict: dict[str, pd.DataFrame] = self._data
904        # データソースの選択
905        if not dfs_dict:
906            raise ValueError("データが読み込まれていません。")
907        source_name_list: list[str] = list(dfs_dict.keys())
908        if print_all:
909            print(source_name_list)
910        return source_name_list

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

Parameters

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

Returns

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

Raises

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

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, hotspot_colors: dict[typing.Literal['bio', 'gas', 'comb'], str] = {'bio': 'blue', 'gas': 'red', 'comb': 'green'}, xlabel: str = 'Δ$\\mathregular{CH_{4}}$ (ppm)', ylabel: str = 'Frequency', 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:
 912    def plot_ch4_delta_histogram(
 913        self,
 914        hotspots: list[HotspotData],
 915        output_dir: str | Path | None,
 916        output_filename: str = "ch4_delta_histogram.png",
 917        dpi: int = 200,
 918        figsize: tuple[int, int] = (8, 6),
 919        fontsize: float = 20,
 920        hotspot_colors: dict[HotspotType, str] = {
 921            "bio": "blue",
 922            "gas": "red",
 923            "comb": "green",
 924        },
 925        xlabel: str = "Δ$\\mathregular{CH_{4}}$ (ppm)",
 926        ylabel: str = "Frequency",
 927        xlim: tuple[float, float] | None = None,
 928        ylim: tuple[float, float] | None = None,
 929        save_fig: bool = True,
 930        show_fig: bool = True,
 931        yscale_log: bool = True,
 932        print_bins_analysis: bool = False,
 933    ) -> None:
 934        """
 935        CH4の増加量(ΔCH4)の積み上げヒストグラムをプロットします。
 936
 937        Parameters
 938        ----------
 939            hotspots : list[HotspotData]
 940                プロットするホットスポットのリスト
 941            output_dir : str | Path | None
 942                保存先のディレクトリパス
 943            output_filename : str
 944                保存するファイル名。デフォルトは"ch4_delta_histogram.png"。
 945            dpi : int
 946                解像度。デフォルトは200。
 947            figsize : tuple[int, int]
 948                図のサイズ。デフォルトは(8, 6)。
 949            fontsize : float
 950                フォントサイズ。デフォルトは20。
 951            hotspot_colors : dict[HotspotType, str]
 952                ホットスポットの色を定義する辞書。
 953            xlabel : str
 954                x軸のラベル。
 955            ylabel : str
 956                y軸のラベル。
 957            xlim : tuple[float, float] | None
 958                x軸の範囲。Noneの場合は自動設定。
 959            ylim : tuple[float, float] | None
 960                y軸の範囲。Noneの場合は自動設定。
 961            save_fig : bool
 962                図の保存を許可するフラグ。デフォルトはTrue。
 963            show_fig : bool
 964                図の表示を許可するフラグ。デフォルトはTrue。
 965            yscale_log : bool
 966                y軸をlogにするかどうか。デフォルトはTrue。
 967            print_bins_analysis : bool
 968                ビンごとの内訳を表示するオプション。
 969        """
 970        plt.rcParams["font.size"] = fontsize
 971        fig = plt.figure(figsize=figsize, dpi=dpi)
 972
 973        # ホットスポットからデータを抽出
 974        all_ch4_deltas = []
 975        all_types = []
 976        for spot in hotspots:
 977            all_ch4_deltas.append(spot.delta_ch4)
 978            all_types.append(spot.type)
 979
 980        # データをNumPy配列に変換
 981        all_ch4_deltas = np.array(all_ch4_deltas)
 982        all_types = np.array(all_types)
 983
 984        # 0.1刻みのビンを作成
 985        if xlim is not None:
 986            bins = np.arange(xlim[0], xlim[1] + 0.1, 0.1)
 987        else:
 988            max_val = np.ceil(np.max(all_ch4_deltas) * 10) / 10
 989            bins = np.arange(0, max_val + 0.1, 0.1)
 990
 991        # タイプごとのヒストグラムデータを計算
 992        hist_data = {}
 993        # HotspotTypeのリテラル値を使用してイテレーション
 994        for type_name in get_args(HotspotType):  # typing.get_argsをインポート
 995            mask = all_types == type_name
 996            if np.any(mask):
 997                counts, _ = np.histogram(all_ch4_deltas[mask], bins=bins)
 998                hist_data[type_name] = counts
 999
1000        # ビンごとの内訳を表示
1001        if print_bins_analysis:
1002            self.logger.info("各ビンの内訳:")
1003            print(f"{'Bin Range':15} {'bio':>8} {'gas':>8} {'comb':>8} {'Total':>8}")
1004            print("-" * 50)
1005
1006            for i in range(len(bins) - 1):
1007                bin_start = bins[i]
1008                bin_end = bins[i + 1]
1009                bio_count = hist_data.get("bio", np.zeros(len(bins) - 1))[i]
1010                gas_count = hist_data.get("gas", np.zeros(len(bins) - 1))[i]
1011                comb_count = hist_data.get("comb", np.zeros(len(bins) - 1))[i]
1012                total = bio_count + gas_count + comb_count
1013
1014                if total > 0:  # 合計が0のビンは表示しない
1015                    print(
1016                        f"{bin_start:4.1f}-{bin_end:<8.1f}"
1017                        f"{int(bio_count):8d}"
1018                        f"{int(gas_count):8d}"
1019                        f"{int(comb_count):8d}"
1020                        f"{int(total):8d}"
1021                    )
1022
1023        # 積み上げヒストグラムを作成
1024        bottom = np.zeros_like(hist_data.get("bio", np.zeros(len(bins) - 1)))
1025
1026        # HotspotTypeのリテラル値を使用してイテレーション
1027        for type_name in get_args(HotspotType):
1028            if type_name in hist_data:
1029                plt.bar(
1030                    bins[:-1],
1031                    hist_data[type_name],
1032                    width=np.diff(bins)[0],
1033                    bottom=bottom,
1034                    color=hotspot_colors[type_name],
1035                    label=type_name,
1036                    alpha=0.6,
1037                    align="edge",
1038                )
1039                bottom += hist_data[type_name]
1040
1041        if yscale_log:
1042            plt.yscale("log")
1043        plt.xlabel(xlabel)
1044        plt.ylabel(ylabel)
1045        plt.legend()
1046        plt.grid(True, which="both", ls="-", alpha=0.2)
1047
1048        # 軸の範囲を設定
1049        if xlim is not None:
1050            plt.xlim(xlim)
1051        if ylim is not None:
1052            plt.ylim(ylim)
1053
1054        # グラフの保存または表示
1055        if save_fig:
1056            if output_dir is None:
1057                raise ValueError(
1058                    "save_fig=Trueの場合、output_dirを指定する必要があります。有効なディレクトリパスを指定してください。"
1059                )
1060            os.makedirs(output_dir, exist_ok=True)
1061            output_path: str = os.path.join(output_dir, output_filename)
1062            plt.savefig(output_path, bbox_inches="tight")
1063            self.logger.info(f"ヒストグラムを保存しました: {output_path}")
1064        if show_fig:
1065            plt.show()
1066        else:
1067            plt.close(fig=fig)

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

Parameters

hotspots : list[HotspotData]
    プロットするホットスポットのリスト
output_dir : str | Path | None
    保存先のディレクトリパス
output_filename : str
    保存するファイル名。デフォルトは"ch4_delta_histogram.png"。
dpi : int
    解像度。デフォルトは200。
figsize : tuple[int, int]
    図のサイズ。デフォルトは(8, 6)。
fontsize : float
    フォントサイズ。デフォルトは20。
hotspot_colors : dict[HotspotType, str]
    ホットスポットの色を定義する辞書。
xlabel : str
    x軸のラベル。
ylabel : str
    y軸のラベル。
xlim : tuple[float, float] | None
    x軸の範囲。Noneの場合は自動設定。
ylim : tuple[float, float] | None
    y軸の範囲。Noneの場合は自動設定。
save_fig : bool
    図の保存を許可するフラグ。デフォルトはTrue。
show_fig : bool
    図の表示を許可するフラグ。デフォルトはTrue。
yscale_log : bool
    y軸をlogにするかどうか。デフォルトはTrue。
print_bins_analysis : bool
    ビンごとの内訳を表示するオプション。
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:
1069    def plot_mapbox(
1070        self,
1071        df: pd.DataFrame,
1072        col_conc: str,
1073        mapbox_access_token: str,
1074        sort_conc_column: bool = True,
1075        output_dir: str | Path | None = None,
1076        output_filename: str = "mapbox_plot.html",
1077        col_lat: str = "latitude",
1078        col_lon: str = "longitude",
1079        colorscale: str = "Jet",
1080        center_lat: float | None = None,
1081        center_lon: float | None = None,
1082        zoom: float = 12,
1083        width: int = 700,
1084        height: int = 700,
1085        tick_font_family: str = "Arial",
1086        title_font_family: str = "Arial",
1087        tick_font_size: int = 12,
1088        title_font_size: int = 14,
1089        marker_size: int = 4,
1090        colorbar_title: str | None = None,
1091        value_range: tuple[float, float] | None = None,
1092        save_fig: bool = True,
1093        show_fig: bool = True,
1094    ) -> None:
1095        """
1096        Plotlyを使用してMapbox上にデータをプロットします。
1097
1098        Parameters
1099        ----------
1100            df : pd.DataFrame
1101                プロットするデータを含むDataFrame
1102            col_conc : str
1103                カラーマッピングに使用する列名
1104            mapbox_access_token : str
1105                Mapboxのアクセストークン
1106            sort_conc_column : bool
1107                value_columnをソートするか否か。デフォルトはTrue。
1108            output_dir : str | Path | None
1109                出力ディレクトリのパス
1110            output_filename : str
1111                出力ファイル名。デフォルトは"mapbox_plot.html"
1112            col_lat : str
1113                緯度の列名。デフォルトは"latitude"
1114            col_lon : str
1115                経度の列名。デフォルトは"longitude"
1116            colorscale : str
1117                使用するカラースケール。デフォルトは"Jet"
1118            center_lat : float | None
1119                中心緯度。デフォルトはNoneで、self._center_latを使用
1120            center_lon : float | None
1121                中心経度。デフォルトはNoneで、self._center_lonを使用
1122            zoom : float
1123                マップの初期ズームレベル。デフォルトは12
1124            width : int
1125                プロットの幅(ピクセル)。デフォルトは700
1126            height : int
1127                プロットの高さ(ピクセル)。デフォルトは700
1128            tick_font_family : str
1129                カラーバーの目盛りフォントファミリー。デフォルトは"Arial"
1130            title_font_family : str
1131                カラーバーのラベルフォントファミリー。デフォルトは"Arial"
1132            tick_font_size : int
1133                カラーバーの目盛りフォントサイズ。デフォルトは12
1134            title_font_size : int
1135                カラーバーのラベルフォントサイズ。デフォルトは14
1136            marker_size : int
1137                マーカーのサイズ。デフォルトは4
1138            colorbar_title : str | None
1139                カラーバーのラベル
1140            value_range : tuple[float, float] | None
1141                カラーマッピングの範囲。デフォルトはNoneで、データの最小値と最大値を使用
1142            save_fig : bool
1143                図を保存するかどうか。デフォルトはTrue
1144            show_fig : bool
1145                図を表示するかどうか。デフォルトはTrue
1146        """
1147        df_mapping: pd.DataFrame = df.copy().dropna(subset=[col_conc])
1148        if sort_conc_column:
1149            df_mapping = df_mapping.sort_values(col_conc)
1150        # 中心座標の設定
1151        center_lat = center_lat if center_lat is not None else self._center_lat
1152        center_lon = center_lon if center_lon is not None else self._center_lon
1153
1154        # カラーマッピングの範囲を設定
1155        cmin, cmax = 0, 0
1156        if value_range is None:
1157            cmin = df_mapping[col_conc].min()
1158            cmax = df_mapping[col_conc].max()
1159        else:
1160            cmin, cmax = value_range
1161
1162        # カラーバーのタイトルを設定
1163        title_text = colorbar_title if colorbar_title is not None else col_conc
1164
1165        # Scattermapboxのデータを作成
1166        scatter_data = go.Scattermapbox(
1167            lat=df_mapping[col_lat],
1168            lon=df_mapping[col_lon],
1169            text=df_mapping[col_conc].astype(str),
1170            hoverinfo="text",
1171            mode="markers",
1172            marker=dict(
1173                color=df_mapping[col_conc],
1174                size=marker_size,
1175                reversescale=False,
1176                autocolorscale=False,
1177                colorscale=colorscale,
1178                cmin=cmin,
1179                cmax=cmax,
1180                colorbar=dict(
1181                    tickformat="3.2f",
1182                    outlinecolor="black",
1183                    outlinewidth=1.5,
1184                    ticks="outside",
1185                    ticklen=7,
1186                    tickwidth=1.5,
1187                    tickcolor="black",
1188                    tickfont=dict(
1189                        family=tick_font_family, color="black", size=tick_font_size
1190                    ),
1191                    title=dict(
1192                        text=title_text, side="top"
1193                    ),  # カラーバーのタイトルを設定
1194                    titlefont=dict(
1195                        family=title_font_family,
1196                        color="black",
1197                        size=title_font_size,
1198                    ),
1199                ),
1200            ),
1201        )
1202
1203        # レイアウトの設定
1204        layout = go.Layout(
1205            width=width,
1206            height=height,
1207            showlegend=False,
1208            mapbox=dict(
1209                accesstoken=mapbox_access_token,
1210                center=dict(lat=center_lat, lon=center_lon),
1211                zoom=zoom,
1212            ),
1213        )
1214
1215        # 図の作成
1216        fig = go.Figure(data=[scatter_data], layout=layout)
1217
1218        # 図の保存
1219        if save_fig:
1220            # 保存時の出力ディレクトリチェック
1221            if output_dir is None:
1222                raise ValueError(
1223                    "save_fig=Trueの場合、output_dirを指定する必要があります。"
1224                )
1225            os.makedirs(output_dir, exist_ok=True)
1226            output_path = os.path.join(output_dir, output_filename)
1227            pyo.plot(fig, filename=output_path, auto_open=False)
1228            self.logger.info(f"Mapboxプロットを保存しました: {output_path}")
1229        # 図の表示
1230        if show_fig:
1231            pyo.iplot(fig)

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

Parameters

df : pd.DataFrame
    プロットするデータを含むDataFrame
col_conc : str
    カラーマッピングに使用する列名
mapbox_access_token : str
    Mapboxのアクセストークン
sort_conc_column : bool
    value_columnをソートするか否か。デフォルトはTrue。
output_dir : str | Path | None
    出力ディレクトリのパス
output_filename : str
    出力ファイル名。デフォルトは"mapbox_plot.html"
col_lat : str
    緯度の列名。デフォルトは"latitude"
col_lon : str
    経度の列名。デフォルトは"longitude"
colorscale : str
    使用するカラースケール。デフォルトは"Jet"
center_lat : float | None
    中心緯度。デフォルトはNoneで、self._center_latを使用
center_lon : float | None
    中心経度。デフォルトはNoneで、self._center_lonを使用
zoom : float
    マップの初期ズームレベル。デフォルトは12
width : int
    プロットの幅(ピクセル)。デフォルトは700
height : int
    プロットの高さ(ピクセル)。デフォルトは700
tick_font_family : str
    カラーバーの目盛りフォントファミリー。デフォルトは"Arial"
title_font_family : str
    カラーバーのラベルフォントファミリー。デフォルトは"Arial"
tick_font_size : int
    カラーバーの目盛りフォントサイズ。デフォルトは12
title_font_size : int
    カラーバーのラベルフォントサイズ。デフォルトは14
marker_size : int
    マーカーのサイズ。デフォルトは4
colorbar_title : str | None
    カラーバーのラベル
value_range : tuple[float, float] | None
    カラーマッピングの範囲。デフォルトはNoneで、データの最小値と最大値を使用
save_fig : bool
    図を保存するかどうか。デフォルトはTrue
show_fig : bool
    図を表示するかどうか。デフォルトはTrue
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), hotspot_colors: dict[typing.Literal['bio', 'gas', 'comb'], str] = {'bio': 'blue', 'gas': 'red', 'comb': 'green'}, hotspot_labels: dict[typing.Literal['bio', 'gas', 'comb'], str] = {'bio': 'bio', 'gas': 'gas', 'comb': 'comb'}, fontsize: float = 12, xlim: tuple[float, float] = (0, 2.0), ylim: tuple[float, float] = (0, 50), xlabel: str = 'Δ$\\mathregular{CH_{4}}$ (ppm)', ylabel: str = 'Δ$\\mathregular{C_{2}H_{6}}$ (ppb)', add_legend: bool = True, save_fig: bool = True, show_fig: bool = True, ratio_labels: dict[float, tuple[float, float, str]] | None = None) -> None:
1233    def plot_scatter_c2c1(
1234        self,
1235        hotspots: list[HotspotData],
1236        output_dir: str | Path | None = None,
1237        output_filename: str = "scatter_c2c1.png",
1238        dpi: int = 200,
1239        figsize: tuple[int, int] = (4, 4),
1240        hotspot_colors: dict[HotspotType, str] = {
1241            "bio": "blue",
1242            "gas": "red",
1243            "comb": "green",
1244        },
1245        hotspot_labels: dict[HotspotType, str] = {
1246            "bio": "bio",
1247            "gas": "gas",
1248            "comb": "comb",
1249        },
1250        fontsize: float = 12,
1251        xlim: tuple[float, float] = (0, 2.0),
1252        ylim: tuple[float, float] = (0, 50),
1253        xlabel: str = "Δ$\\mathregular{CH_{4}}$ (ppm)",
1254        ylabel: str = "Δ$\\mathregular{C_{2}H_{6}}$ (ppb)",
1255        add_legend: bool = True,
1256        save_fig: bool = True,
1257        show_fig: bool = True,
1258        ratio_labels: dict[float, tuple[float, float, str]] | None = None,
1259    ) -> None:
1260        """
1261        検出されたホットスポットのΔC2H6とΔCH4の散布図をプロットします。
1262
1263        Parameters
1264        ----------
1265            hotspots : list[HotspotData]
1266                プロットするホットスポットのリスト
1267            output_dir : str | Path | None
1268                保存先のディレクトリパス
1269            output_filename : str
1270                保存するファイル名。デフォルトは"scatter_c2c1.png"。
1271            dpi : int
1272                解像度。デフォルトは200。
1273            figsize : tuple[int, int]
1274                図のサイズ。デフォルトは(4, 4)。
1275            fontsize : float
1276                フォントサイズ。デフォルトは12。
1277            hotspot_colors : dict[HotspotType, str]
1278                ホットスポットの色を定義する辞書。
1279            hotspot_labels : dict[HotspotType, str]
1280                ホットスポットのラベルを定義する辞書。
1281            save_fig : bool
1282                図の保存を許可するフラグ。デフォルトはTrue。
1283            show_fig : bool
1284                図の表示を許可するフラグ。デフォルトはTrue。
1285            ratio_labels : dict[float, tuple[float, float, str]] | None
1286                比率線とラベルの設定。
1287                キーは比率値、値は (x位置, y位置, ラベルテキスト) のタプル。
1288                Noneの場合はデフォルト設定を使用。デフォルト値:
1289                {
1290                    0.001: (1.25, 2, "0.001"),
1291                    0.005: (1.25, 8, "0.005"),
1292                    0.010: (1.25, 15, "0.01"),
1293                    0.020: (1.25, 30, "0.02"),
1294                    0.030: (1.0, 40, "0.03"),
1295                    0.076: (0.20, 42, "0.076 (Osaka)")
1296                }
1297            xlim : tuple[float, float]
1298                x軸の範囲を指定します。デフォルトは(0, 2.0)です。
1299            ylim : tuple[float, float]
1300                y軸の範囲を指定します。デフォルトは(0, 50)です。
1301            xlabel : str
1302                x軸のラベルを指定します。デフォルトは"Δ$\\mathregular{CH_{4}}$ (ppm)"です。
1303            ylabel : str
1304                y軸のラベルを指定します。デフォルトは"Δ$\\mathregular{C_{2}H_{6}}$ (ppb)"です。
1305            add_legend : bool
1306                凡例を追加するかどうか。
1307        """
1308        plt.rcParams["font.size"] = fontsize
1309        fig = plt.figure(figsize=figsize, dpi=dpi)
1310
1311        # タイプごとのデータを収集
1312        type_data: dict[HotspotType, list[tuple[float, float]]] = {
1313            "bio": [],
1314            "gas": [],
1315            "comb": [],
1316        }
1317        for spot in hotspots:
1318            type_data[spot.type].append((spot.delta_ch4, spot.delta_c2h6))
1319
1320        # タイプごとにプロット(データが存在する場合のみ)
1321        for spot_type, data in type_data.items():
1322            if data:  # データが存在する場合のみプロット
1323                ch4_values, c2h6_values = zip(*data)
1324                plt.plot(
1325                    ch4_values,
1326                    c2h6_values,
1327                    "o",
1328                    c=hotspot_colors[spot_type],
1329                    alpha=0.5,
1330                    ms=2,
1331                    label=hotspot_labels[spot_type],
1332                )
1333
1334        # デフォルトの比率とラベル設定
1335        default_ratio_labels = {
1336            0.001: (1.25, 2, "0.001"),
1337            0.005: (1.25, 8, "0.005"),
1338            0.010: (1.25, 15, "0.01"),
1339            0.020: (1.25, 30, "0.02"),
1340            0.030: (1.0, 40, "0.03"),
1341            0.076: (0.20, 42, "0.076 (Osaka)"),
1342        }
1343
1344        ratio_labels = ratio_labels or default_ratio_labels
1345
1346        # プロット後、軸の設定前に比率の線を追加
1347        x = np.array([0, 5])
1348        base_ch4 = 0.0
1349        base = 0.0
1350
1351        # 各比率に対して線を引く
1352        for ratio, (x_pos, y_pos, label) in ratio_labels.items():
1353            y = (x - base_ch4) * 1000 * ratio + base
1354            plt.plot(x, y, "-", c="black", alpha=0.5)
1355            plt.text(x_pos, y_pos, label)
1356
1357        plt.xlim(xlim)
1358        plt.ylim(ylim)
1359        plt.xlabel(xlabel)
1360        plt.ylabel(ylabel)
1361        if add_legend:
1362            plt.legend()
1363
1364        # グラフの保存または表示
1365        if save_fig:
1366            if output_dir is None:
1367                raise ValueError(
1368                    "save_fig=Trueの場合、output_dirを指定する必要があります。有効なディレクトリパスを指定してください。"
1369                )
1370            output_path: str = os.path.join(output_dir, output_filename)
1371            plt.savefig(output_path, bbox_inches="tight")
1372            self.logger.info(f"散布図を保存しました: {output_path}")
1373        if show_fig:
1374            plt.show()
1375        else:
1376            plt.close(fig=fig)

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

Parameters

hotspots : list[HotspotData]
    プロットするホットスポットのリスト
output_dir : str | Path | None
    保存先のディレクトリパス
output_filename : str
    保存するファイル名。デフォルトは"scatter_c2c1.png"。
dpi : int
    解像度。デフォルトは200。
figsize : tuple[int, int]
    図のサイズ。デフォルトは(4, 4)。
fontsize : float
    フォントサイズ。デフォルトは12。
hotspot_colors : dict[HotspotType, str]
    ホットスポットの色を定義する辞書。
hotspot_labels : dict[HotspotType, str]
    ホットスポットのラベルを定義する辞書。
save_fig : bool
    図の保存を許可するフラグ。デフォルトはTrue。
show_fig : bool
    図の表示を許可するフラグ。デフォルトはTrue。
ratio_labels : dict[float, tuple[float, float, str]] | None
    比率線とラベルの設定。
    キーは比率値、値は (x位置, y位置, ラベルテキスト) のタプル。
    Noneの場合はデフォルト設定を使用。デフォルト値:
    {
        0.001: (1.25, 2, "0.001"),
        0.005: (1.25, 8, "0.005"),
        0.010: (1.25, 15, "0.01"),
        0.020: (1.25, 30, "0.02"),
        0.030: (1.0, 40, "0.03"),
        0.076: (0.20, 42, "0.076 (Osaka)")
    }
xlim : tuple[float, float]
    x軸の範囲を指定します。デフォルトは(0, 2.0)です。
ylim : tuple[float, float]
    y軸の範囲を指定します。デフォルトは(0, 50)です。
xlabel : str
    x軸のラベルを指定します。デフォルトは"Δ$\mathregular{CH_{4}}$ (ppm)"です。
ylabel : str
    y軸のラベルを指定します。デフォルトは"Δ$\mathregular{C_{2}H_{6}}$ (ppb)"です。
add_legend : bool
    凡例を追加するかどうか。
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:
1378    def plot_conc_timeseries(
1379        self,
1380        source_name: str | None = None,
1381        output_dir: str | Path | None = None,
1382        output_filename: str = "timeseries.png",
1383        dpi: int = 200,
1384        figsize: tuple[float, float] = (8, 4),
1385        save_fig: bool = True,
1386        show_fig: bool = True,
1387        col_ch4: str = "ch4_ppm",
1388        col_c2h6: str = "c2h6_ppb",
1389        col_h2o: str = "h2o_ppm",
1390        ylim_ch4: tuple[float, float] | None = None,
1391        ylim_c2h6: tuple[float, float] | None = None,
1392        ylim_h2o: tuple[float, float] | None = None,
1393        font_size: float = 12,
1394        label_pad: float = 10,
1395    ) -> None:
1396        """
1397        時系列データをプロットします。
1398
1399        Parameters
1400        ----------
1401            dpi : int
1402                図の解像度を指定します。デフォルトは200です。
1403            source_name : str | None
1404                プロットするデータソースの名前。Noneの場合は最初のデータソースを使用します。
1405            figsize : tuple[float, float]
1406                図のサイズを指定します。デフォルトは(8, 4)です。
1407            output_dir : str | Path | None
1408                保存先のディレクトリを指定します。save_fig=Trueの場合は必須です。
1409            output_filename : str
1410                保存するファイル名を指定します。デフォルトは"time_series.png"です。
1411            save_fig : bool
1412                図を保存するかどうかを指定します。デフォルトはFalseです。
1413            show_fig : bool
1414                図を表示するかどうかを指定します。デフォルトはTrueです。
1415            col_ch4 : str
1416                CH4データのキーを指定します。デフォルトは"ch4_ppm"です。
1417            col_c2h6 : str
1418                C2H6データのキーを指定します。デフォルトは"c2h6_ppb"です。
1419            col_h2o : str
1420                H2Oデータのキーを指定します。デフォルトは"h2o_ppm"です。
1421            ylim_ch4 : tuple[float, float] | None
1422                CH4プロットのy軸範囲を指定します。デフォルトはNoneです。
1423            ylim_c2h6 : tuple[float, float] | None
1424                C2H6プロットのy軸範囲を指定します。デフォルトはNoneです。
1425            ylim_h2o : tuple[float, float] | None
1426                H2Oプロットのy軸範囲を指定します。デフォルトはNoneです。
1427            font_size : float
1428                基本フォントサイズ。デフォルトは12。
1429            label_pad : float
1430                y軸ラベルのパディング。デフォルトは10。
1431        """
1432        # プロットパラメータの設定
1433        plt.rcParams.update(
1434            {
1435                "font.size": font_size,
1436                "axes.labelsize": font_size,
1437                "axes.titlesize": font_size,
1438                "xtick.labelsize": font_size,
1439                "ytick.labelsize": font_size,
1440            }
1441        )
1442        dfs_dict: dict[str, pd.DataFrame] = self._data.copy()
1443        # データソースの選択
1444        if not dfs_dict:
1445            raise ValueError("データが読み込まれていません。")
1446
1447        if source_name not in dfs_dict:
1448            raise ValueError(
1449                f"指定されたデータソース '{source_name}' が見つかりません。"
1450            )
1451
1452        df = dfs_dict[source_name]
1453
1454        # プロットの作成
1455        fig = plt.figure(figsize=figsize, dpi=dpi)
1456
1457        # CH4プロット
1458        ax1 = fig.add_subplot(3, 1, 1)
1459        ax1.plot(df.index, df[col_ch4], c="red")
1460        if ylim_ch4:
1461            ax1.set_ylim(ylim_ch4)
1462        ax1.set_ylabel("$\\mathregular{CH_{4}}$ (ppm)", labelpad=label_pad)
1463        ax1.grid(True, alpha=0.3)
1464
1465        # C2H6プロット
1466        ax2 = fig.add_subplot(3, 1, 2)
1467        ax2.plot(df.index, df[col_c2h6], c="red")
1468        if ylim_c2h6:
1469            ax2.set_ylim(ylim_c2h6)
1470        ax2.set_ylabel("$\\mathregular{C_{2}H_{6}}$ (ppb)", labelpad=label_pad)
1471        ax2.grid(True, alpha=0.3)
1472
1473        # H2Oプロット
1474        ax3 = fig.add_subplot(3, 1, 3)
1475        ax3.plot(df.index, df[col_h2o], c="red")
1476        if ylim_h2o:
1477            ax3.set_ylim(ylim_h2o)
1478        ax3.set_ylabel("$\\mathregular{H_{2}O}$ (ppm)", labelpad=label_pad)
1479        ax3.grid(True, alpha=0.3)
1480
1481        # x軸のフォーマット調整
1482        for ax in [ax1, ax2, ax3]:
1483            ax.xaxis.set_major_formatter(mdates.DateFormatter("%H:%M"))
1484            # 軸のラベルとグリッド線の調整
1485            ax.tick_params(axis="both", which="major", labelsize=font_size)
1486            ax.grid(True, alpha=0.3)
1487
1488        # サブプロット間の間隔調整
1489        plt.subplots_adjust(wspace=0.38, hspace=0.38)
1490
1491        # 図の保存
1492        if save_fig:
1493            if output_dir is None:
1494                raise ValueError(
1495                    "save_fig=Trueの場合、output_dirを指定する必要があります。有効なディレクトリパスを指定してください。"
1496                )
1497            os.makedirs(output_dir, exist_ok=True)
1498            output_path = os.path.join(output_dir, output_filename)
1499            plt.savefig(output_path, bbox_inches="tight")
1500
1501        if show_fig:
1502            plt.show()
1503        else:
1504            plt.close(fig=fig)

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

Parameters

dpi : int
    図の解像度を指定します。デフォルトは200です。
source_name : str | None
    プロットするデータソースの名前。Noneの場合は最初のデータソースを使用します。
figsize : tuple[float, float]
    図のサイズを指定します。デフォルトは(8, 4)です。
output_dir : str | Path | None
    保存先のディレクトリを指定します。save_fig=Trueの場合は必須です。
output_filename : str
    保存するファイル名を指定します。デフォルトは"time_series.png"です。
save_fig : bool
    図を保存するかどうかを指定します。デフォルトはFalseです。
show_fig : bool
    図を表示するかどうかを指定します。デフォルトはTrueです。
col_ch4 : str
    CH4データのキーを指定します。デフォルトは"ch4_ppm"です。
col_c2h6 : str
    C2H6データのキーを指定します。デフォルトは"c2h6_ppb"です。
col_h2o : str
    H2Oデータのキーを指定します。デフォルトは"h2o_ppm"です。
ylim_ch4 : tuple[float, float] | None
    CH4プロットのy軸範囲を指定します。デフォルトはNoneです。
ylim_c2h6 : tuple[float, float] | None
    C2H6プロットのy軸範囲を指定します。デフォルトはNoneです。
ylim_h2o : tuple[float, float] | None
    H2Oプロットのy軸範囲を指定します。デフォルトはNoneです。
font_size : float
    基本フォントサイズ。デフォルトは12。
label_pad : float
    y軸ラベルのパディング。デフォルトは10。
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_bg: str = 'ch4_ppm_bg', col_ch4_ppm_delta: str = 'ch4_ppm_delta'):
2035    def remove_c2c1_ratio_duplicates(
2036        self,
2037        df: pd.DataFrame,
2038        min_time_threshold_seconds: float = 300,  # 5分以内は重複とみなす
2039        max_time_threshold_hours: float = 12.0,  # 12時間以上離れている場合は別のポイントとして扱う
2040        check_time_all: bool = True,  # 時間閾値を超えた場合の重複チェックを継続するかどうか
2041        hotspot_area_meter: float = 50.0,  # 重複とみなす距離の閾値(メートル)
2042        col_ch4_ppm: str = "ch4_ppm",
2043        col_ch4_ppm_bg: str = "ch4_ppm_bg",
2044        col_ch4_ppm_delta: str = "ch4_ppm_delta",
2045    ):
2046        """
2047        メタン濃度の増加が閾値を超えた地点から、重複を除外してユニークなホットスポットを抽出する関数。
2048
2049        Parameters
2050        ----------
2051            df : pandas.DataFrame
2052                入力データフレーム。必須カラム:
2053                - ch4_ppm: メタン濃度(ppm)
2054                - ch4_ppm_bg: メタン濃度の移動平均(ppm)
2055                - ch4_ppm_delta: メタン濃度の増加量(ppm)
2056                - latitude: 緯度
2057                - longitude: 経度
2058            min_time_threshold_seconds : float, optional
2059                重複とみなす最小時間差(秒)。デフォルトは300秒(5分)。
2060            max_time_threshold_hours : float, optional
2061                別ポイントとして扱う最大時間差(時間)。デフォルトは12時間。
2062            check_time_all : bool, optional
2063                時間閾値を超えた場合の重複チェックを継続するかどうか。デフォルトはTrue。
2064            hotspot_area_meter : float, optional
2065                重複とみなす距離の閾値(メートル)。デフォルトは50メートル。
2066
2067        Returns
2068        ----------
2069            pandas.DataFrame
2070                ユニークなホットスポットのデータフレーム。
2071        """
2072        df_data: pd.DataFrame = df.copy()
2073        # メタン濃度の増加が閾値を超えた点を抽出
2074        mask = (
2075            df_data[col_ch4_ppm] - df_data[col_ch4_ppm_bg] > self._ch4_enhance_threshold
2076        )
2077        hotspot_candidates = df_data[mask].copy()
2078
2079        # ΔCH4の降順でソート
2080        sorted_hotspots = hotspot_candidates.sort_values(
2081            by=col_ch4_ppm_delta, ascending=False
2082        )
2083        used_positions = []
2084        unique_hotspots = pd.DataFrame()
2085
2086        for _, spot in sorted_hotspots.iterrows():
2087            should_add = True
2088            for used_lat, used_lon, used_time in used_positions:
2089                # 距離チェック
2090                distance = geodesic(
2091                    (spot.latitude, spot.longitude), (used_lat, used_lon)
2092                ).meters
2093
2094                if distance < hotspot_area_meter:
2095                    # 時間差の計算(秒単位)
2096                    time_diff = pd.Timedelta(
2097                        spot.name - pd.to_datetime(used_time)
2098                    ).total_seconds()
2099                    time_diff_abs = abs(time_diff)
2100
2101                    # 時間差に基づく判定
2102                    if check_time_all:
2103                        # 時間に関係なく、距離が近ければ重複とみなす
2104                        # ΔCH4が大きい方を残す(現在のスポットは必ず小さい)
2105                        should_add = False
2106                        break
2107                    else:
2108                        # 時間窓による判定を行う
2109                        if time_diff_abs <= min_time_threshold_seconds:
2110                            # Case 1: 最小時間閾値以内は重複とみなす
2111                            should_add = False
2112                            break
2113                        elif time_diff_abs > max_time_threshold_hours * 3600:
2114                            # Case 2: 最大時間閾値を超えた場合は重複チェックをスキップ
2115                            continue
2116                        # Case 3: その間の時間差の場合は、距離が近ければ重複とみなす
2117                        should_add = False
2118                        break
2119
2120            if should_add:
2121                unique_hotspots = pd.concat([unique_hotspots, pd.DataFrame([spot])])
2122                used_positions.append((spot.latitude, spot.longitude, spot.name))
2123
2124        return unique_hotspots

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

Parameters

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

Returns

pandas.DataFrame
    ユニークなホットスポットのデータフレーム。
@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]:
2126    @staticmethod
2127    def remove_hotspots_duplicates(
2128        hotspots: list[HotspotData],
2129        check_time_all: bool,
2130        min_time_threshold_seconds: float = 300,
2131        max_time_threshold_hours: float = 12,
2132        hotspot_area_meter: float = 50,
2133    ) -> list[HotspotData]:
2134        """
2135        重複するホットスポットを除外します。
2136
2137        このメソッドは、与えられたホットスポットのリストから重複を検出し、
2138        一意のホットスポットのみを返します。重複の判定は、指定された
2139        時間および距離の閾値に基づいて行われます。
2140
2141        Parameters
2142        ----------
2143            hotspots : list[HotspotData]
2144                重複を除外する対象のホットスポットのリスト。
2145            check_time_all : bool
2146                時間に関係なく重複チェックを行うかどうか。
2147            min_time_threshold_seconds : float
2148                重複とみなす最小時間の閾値(秒)。
2149            max_time_threshold_hours : float
2150                重複チェックを一時的に無視する最大時間の閾値(時間)。
2151            hotspot_area_meter : float
2152                重複とみなす距離の閾値(メートル)。
2153
2154        Returns
2155        ----------
2156            list[HotspotData]
2157                重複を除去したホットスポットのリスト。
2158        """
2159        # ΔCH4の降順でソート
2160        sorted_hotspots: list[HotspotData] = sorted(
2161            hotspots, key=lambda x: x.delta_ch4, reverse=True
2162        )
2163        used_positions_by_type: dict[
2164            HotspotType, list[tuple[float, float, str, float]]
2165        ] = {
2166            "bio": [],
2167            "gas": [],
2168            "comb": [],
2169        }
2170        unique_hotspots: list[HotspotData] = []
2171
2172        for spot in sorted_hotspots:
2173            is_duplicate = MobileSpatialAnalyzer._is_duplicate_spot(
2174                current_lat=spot.avg_lat,
2175                current_lon=spot.avg_lon,
2176                current_time=spot.source,
2177                used_positions=used_positions_by_type[spot.type],
2178                check_time_all=check_time_all,
2179                min_time_threshold_seconds=min_time_threshold_seconds,
2180                max_time_threshold_hours=max_time_threshold_hours,
2181                hotspot_area_meter=hotspot_area_meter,
2182            )
2183
2184            if not is_duplicate:
2185                unique_hotspots.append(spot)
2186                used_positions_by_type[spot.type].append(
2187                    (spot.avg_lat, spot.avg_lon, spot.source, spot.delta_ch4)
2188                )
2189
2190        return unique_hotspots

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

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

Parameters

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

Returns

list[HotspotData]
    重複を除去したホットスポットのリスト。
@staticmethod
def setup_logger(logger: logging.Logger | None, log_level: int = 20) -> logging.Logger:
2192    @staticmethod
2193    def setup_logger(logger: Logger | None, log_level: int = INFO) -> Logger:
2194        """
2195        ロガーを設定します。
2196
2197        このメソッドは、ロギングの設定を行い、ログメッセージのフォーマットを指定します。
2198        ログメッセージには、日付、ログレベル、メッセージが含まれます。
2199
2200        渡されたロガーがNoneまたは不正な場合は、新たにロガーを作成し、標準出力に
2201        ログメッセージが表示されるようにStreamHandlerを追加します。ロガーのレベルは
2202        引数で指定されたlog_levelに基づいて設定されます。
2203
2204        Parameters
2205        ----------
2206            logger : Logger | None
2207                使用するロガー。Noneの場合は新しいロガーを作成します。
2208            log_level : int
2209                ロガーのログレベル。デフォルトはINFO。
2210
2211        Returns
2212        ----------
2213            Logger
2214                設定されたロガーオブジェクト。
2215        """
2216        if logger is not None and isinstance(logger, Logger):
2217            return logger
2218        # 渡されたロガーがNoneまたは正しいものでない場合は独自に設定
2219        new_logger: Logger = getLogger()
2220        # 既存のハンドラーをすべて削除
2221        for handler in new_logger.handlers[:]:
2222            new_logger.removeHandler(handler)
2223        new_logger.setLevel(log_level)  # ロガーのレベルを設定
2224        ch = StreamHandler()
2225        ch_formatter = Formatter("%(asctime)s - %(levelname)s - %(message)s")
2226        ch.setFormatter(ch_formatter)  # フォーマッターをハンドラーに設定
2227        new_logger.addHandler(ch)  # StreamHandlerの追加
2228        return new_logger

ロガーを設定します。

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

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

Parameters

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

Returns

Logger
    設定されたロガーオブジェクト。
@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]]]:
2230    @staticmethod
2231    def calculate_emission_rates(
2232        hotspots: list[HotspotData],
2233        method: Literal["weller", "weitzel", "joo", "umezawa"] = "weller",
2234        print_summary: bool = True,
2235        custom_formulas: dict[str, dict[str, float]] | None = None,
2236    ) -> tuple[list[EmissionData], dict[str, dict[str, float]]]:
2237        """
2238        検出されたホットスポットのCH4漏出量を計算・解析し、統計情報を生成します。
2239
2240        Parameters
2241        ----------
2242            hotspots : list[HotspotData]
2243                分析対象のホットスポットのリスト
2244            method : Literal["weller", "weitzel", "joo", "umezawa"]
2245                使用する計算式。デフォルトは"weller"。
2246            print_summary : bool
2247                統計情報を表示するかどうか。デフォルトはTrue。
2248            custom_formulas : dict[str, dict[str, float]] | None
2249                カスタム計算式の係数。
2250                例: {"custom_method": {"a": 1.0, "b": 1.0}}
2251                Noneの場合はデフォルトの計算式を使用。
2252
2253        Returns
2254        ----------
2255            tuple[list[EmissionData], dict[str, dict[str, float]]]
2256                - 各ホットスポットの排出量データを含むリスト
2257                - タイプ別の統計情報を含む辞書
2258        """
2259        # デフォルトの経験式係数
2260        default_formulas = {
2261            "weller": {"a": 0.988, "b": 0.817},
2262            "weitzel": {"a": 0.521, "b": 0.795},
2263            "joo": {"a": 2.738, "b": 1.329},
2264            "umezawa": {"a": 2.716, "b": 0.741},
2265        }
2266
2267        # カスタム計算式がある場合は追加
2268        emission_formulas = default_formulas.copy()
2269        if custom_formulas:
2270            emission_formulas.update(custom_formulas)
2271
2272        if method not in emission_formulas:
2273            raise ValueError(f"Unknown method: {method}")
2274
2275        # 係数の取得
2276        a = emission_formulas[method]["a"]
2277        b = emission_formulas[method]["b"]
2278
2279        # 排出量の計算
2280        emission_data_list = []
2281        for spot in hotspots:
2282            # 漏出量の計算 (L/min)
2283            emission_rate = np.exp((np.log(spot.delta_ch4) + a) / b)
2284            # 日排出量 (L/day)
2285            daily_emission = emission_rate * 60 * 24
2286            # 年間排出量 (L/year)
2287            annual_emission = daily_emission * 365
2288
2289            emission_data = EmissionData(
2290                source=spot.source,
2291                type=spot.type,
2292                section=spot.section,
2293                latitude=spot.avg_lat,
2294                longitude=spot.avg_lon,
2295                delta_ch4=spot.delta_ch4,
2296                delta_c2h6=spot.delta_c2h6,
2297                ratio=spot.ratio,
2298                emission_rate=emission_rate,
2299                daily_emission=daily_emission,
2300                annual_emission=annual_emission,
2301            )
2302            emission_data_list.append(emission_data)
2303
2304        # 統計計算用にDataFrameを作成
2305        emission_df = pd.DataFrame([e.to_dict() for e in emission_data_list])
2306
2307        # タイプ別の統計情報を計算
2308        stats = {}
2309        # emission_formulas の定義の後に、排出量カテゴリーの閾値を定義
2310        emission_categories = {
2311            "low": {"min": 0, "max": 6},  # < 6 L/min
2312            "medium": {"min": 6, "max": 40},  # 6-40 L/min
2313            "high": {"min": 40, "max": float("inf")},  # > 40 L/min
2314        }
2315        # get_args(HotspotType)を使用して型安全なリストを作成
2316        types = list(get_args(HotspotType))
2317        for spot_type in types:
2318            df_type = emission_df[emission_df["type"] == spot_type]
2319            if len(df_type) > 0:
2320                # 既存の統計情報を計算
2321                type_stats = {
2322                    "count": len(df_type),
2323                    "emission_rate_min": df_type["emission_rate"].min(),
2324                    "emission_rate_max": df_type["emission_rate"].max(),
2325                    "emission_rate_mean": df_type["emission_rate"].mean(),
2326                    "emission_rate_median": df_type["emission_rate"].median(),
2327                    "total_annual_emission": df_type["annual_emission"].sum(),
2328                    "mean_annual_emission": df_type["annual_emission"].mean(),
2329                }
2330
2331                # 排出量カテゴリー別の統計を追加
2332                category_counts = {
2333                    "low": len(
2334                        df_type[
2335                            df_type["emission_rate"] < emission_categories["low"]["max"]
2336                        ]
2337                    ),
2338                    "medium": len(
2339                        df_type[
2340                            (
2341                                df_type["emission_rate"]
2342                                >= emission_categories["medium"]["min"]
2343                            )
2344                            & (
2345                                df_type["emission_rate"]
2346                                < emission_categories["medium"]["max"]
2347                            )
2348                        ]
2349                    ),
2350                    "high": len(
2351                        df_type[
2352                            df_type["emission_rate"]
2353                            >= emission_categories["high"]["min"]
2354                        ]
2355                    ),
2356                }
2357                type_stats["emission_categories"] = category_counts
2358
2359                stats[spot_type] = type_stats
2360
2361                if print_summary:
2362                    print(f"\n{spot_type}タイプの統計情報:")
2363                    print(f"  検出数: {type_stats['count']}")
2364                    print("  排出量 (L/min):")
2365                    print(f"    最小値: {type_stats['emission_rate_min']:.2f}")
2366                    print(f"    最大値: {type_stats['emission_rate_max']:.2f}")
2367                    print(f"    平均値: {type_stats['emission_rate_mean']:.2f}")
2368                    print(f"    中央値: {type_stats['emission_rate_median']:.2f}")
2369                    print("  排出量カテゴリー別の検出数:")
2370                    print(f"    低放出 (< 6 L/min): {category_counts['low']}")
2371                    print(f"    中放出 (6-40 L/min): {category_counts['medium']}")
2372                    print(f"    高放出 (> 40 L/min): {category_counts['high']}")
2373                    print("  年間排出量 (L/year):")
2374                    print(f"    合計: {type_stats['total_annual_emission']:.2f}")
2375                    print(f"    平均: {type_stats['mean_annual_emission']:.2f}")
2376
2377        return emission_data_list, stats

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

Parameters

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

Returns

tuple[list[EmissionData], dict[str, dict[str, float]]]
    - 各ホットスポットの排出量データを含むリスト
    - タイプ別の統計情報を含む辞書
@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), hotspot_colors: dict[typing.Literal['bio', 'gas', 'comb'], str] = {'bio': 'blue', 'gas': 'red', 'comb': 'green'}, 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:
2379    @staticmethod
2380    def plot_emission_analysis(
2381        emission_data_list: list[EmissionData],
2382        dpi: int = 300,
2383        output_dir: str | Path | None = None,
2384        output_filename: str = "emission_analysis.png",
2385        figsize: tuple[float, float] = (12, 5),
2386        hotspot_colors: dict[HotspotType, str] = {
2387            "bio": "blue",
2388            "gas": "red",
2389            "comb": "green",
2390        },
2391        add_legend: bool = True,
2392        hist_log_y: bool = False,
2393        hist_xlim: tuple[float, float] | None = None,
2394        hist_ylim: tuple[float, float] | None = None,
2395        scatter_xlim: tuple[float, float] | None = None,
2396        scatter_ylim: tuple[float, float] | None = None,
2397        hist_bin_width: float = 0.5,
2398        print_summary: bool = True,
2399        save_fig: bool = False,
2400        show_fig: bool = True,
2401        show_scatter: bool = True,  # 散布図の表示を制御するオプションを追加
2402    ) -> None:
2403        """
2404        排出量分析のプロットを作成する静的メソッド。
2405
2406        Parameters
2407        ----------
2408            emission_data_list : list[EmissionData]
2409                EmissionDataオブジェクトのリスト。
2410            output_dir : str | Path | None
2411                出力先ディレクトリのパス。
2412            output_filename : str
2413                保存するファイル名。デフォルトは"emission_analysis.png"。
2414            dpi : int
2415                プロットの解像度。デフォルトは300。
2416            figsize : tuple[float, float]
2417                プロットのサイズ。デフォルトは(12, 5)。
2418            hotspot_colors : dict[HotspotType, str]
2419                ホットスポットの色を定義する辞書。
2420            add_legend : bool
2421                凡例を追加するかどうか。デフォルトはTrue。
2422            hist_log_y : bool
2423                ヒストグラムのy軸を対数スケールにするかどうか。デフォルトはFalse。
2424            hist_xlim : tuple[float, float] | None
2425                ヒストグラムのx軸の範囲。デフォルトはNone。
2426            hist_ylim : tuple[float, float] | None
2427                ヒストグラムのy軸の範囲。デフォルトはNone。
2428            scatter_xlim : tuple[float, float] | None
2429                散布図のx軸の範囲。デフォルトはNone。
2430            scatter_ylim : tuple[float, float] | None
2431                散布図のy軸の範囲。デフォルトはNone。
2432            hist_bin_width : float
2433                ヒストグラムのビンの幅。デフォルトは0.5。
2434            print_summary : bool
2435                集計結果を表示するかどうか。デフォルトはFalse。
2436            save_fig : bool
2437                図をファイルに保存するかどうか。デフォルトはFalse。
2438            show_fig : bool
2439                図を表示するかどうか。デフォルトはTrue。
2440            show_scatter : bool
2441                散布図(右図)を表示するかどうか。デフォルトはTrue。
2442        """
2443        # データをDataFrameに変換
2444        df = pd.DataFrame([e.to_dict() for e in emission_data_list])
2445
2446        # プロットの作成(散布図の有無に応じてサブプロット数を調整)
2447        if show_scatter:
2448            fig, (ax1, ax2) = plt.subplots(1, 2, figsize=figsize)
2449            axes = [ax1, ax2]
2450        else:
2451            fig, ax1 = plt.subplots(1, 1, figsize=(figsize[0] // 2, figsize[1]))
2452            axes = [ax1]
2453
2454        # 存在するタイプを確認
2455        # HotspotTypeの定義順を基準にソート
2456        hotspot_types = list(get_args(HotspotType))
2457        existing_types = sorted(
2458            df["type"].unique(), key=lambda x: hotspot_types.index(x)
2459        )
2460
2461        # 左側: ヒストグラム
2462        # ビンの範囲を設定
2463        start = 0  # 必ず0から開始
2464        if hist_xlim is not None:
2465            end = hist_xlim[1]
2466        else:
2467            end = np.ceil(df["emission_rate"].max() * 1.05)
2468
2469        # ビン数を計算(end値をbin_widthで割り切れるように調整)
2470        n_bins = int(np.ceil(end / hist_bin_width))
2471        end = n_bins * hist_bin_width
2472
2473        # ビンの生成(0から開始し、bin_widthの倍数で区切る)
2474        bins = np.linspace(start, end, n_bins + 1)
2475
2476        # タイプごとにヒストグラムを積み上げ
2477        bottom = np.zeros(len(bins) - 1)
2478        for spot_type in existing_types:
2479            data = df[df["type"] == spot_type]["emission_rate"]
2480            if len(data) > 0:
2481                counts, _ = np.histogram(data, bins=bins)
2482                ax1.bar(
2483                    bins[:-1],
2484                    counts,
2485                    width=hist_bin_width,
2486                    bottom=bottom,
2487                    alpha=0.6,
2488                    label=spot_type,
2489                    color=hotspot_colors[spot_type],
2490                )
2491                bottom += counts
2492
2493        ax1.set_xlabel("CH$_4$ Emission (L min$^{-1}$)")
2494        ax1.set_ylabel("Frequency")
2495        if hist_log_y:
2496            # ax1.set_yscale("log")
2497            # 非線形スケールを設定(linthreshで線形から対数への遷移点を指定)
2498            ax1.set_yscale("symlog", linthresh=1.0)
2499        if hist_xlim is not None:
2500            ax1.set_xlim(hist_xlim)
2501        else:
2502            ax1.set_xlim(0, np.ceil(df["emission_rate"].max() * 1.05))
2503
2504        if hist_ylim is not None:
2505            ax1.set_ylim(hist_ylim)
2506        else:
2507            ax1.set_ylim(0, ax1.get_ylim()[1])  # 下限を0に設定
2508
2509        if show_scatter:
2510            # 右側: 散布図
2511            for spot_type in existing_types:
2512                mask = df["type"] == spot_type
2513                ax2.scatter(
2514                    df[mask]["emission_rate"],
2515                    df[mask]["delta_ch4"],
2516                    alpha=0.6,
2517                    label=spot_type,
2518                    color=hotspot_colors[spot_type],
2519                )
2520
2521            ax2.set_xlabel("Emission Rate (L min$^{-1}$)")
2522            ax2.set_ylabel("ΔCH$_4$ (ppm)")
2523            if scatter_xlim is not None:
2524                ax2.set_xlim(scatter_xlim)
2525            else:
2526                ax2.set_xlim(0, np.ceil(df["emission_rate"].max() * 1.05))
2527
2528            if scatter_ylim is not None:
2529                ax2.set_ylim(scatter_ylim)
2530            else:
2531                ax2.set_ylim(0, np.ceil(df["delta_ch4"].max() * 1.05))
2532
2533        # 凡例の表示
2534        if add_legend:
2535            for ax in axes:
2536                ax.legend(
2537                    bbox_to_anchor=(0.5, -0.30),
2538                    loc="upper center",
2539                    ncol=len(existing_types),
2540                )
2541
2542        plt.tight_layout()
2543
2544        # 図の保存
2545        if save_fig:
2546            if output_dir is None:
2547                raise ValueError(
2548                    "save_fig=Trueの場合、output_dirを指定する必要があります。有効なディレクトリパスを指定してください。"
2549                )
2550            os.makedirs(output_dir, exist_ok=True)
2551            output_path = os.path.join(output_dir, output_filename)
2552            plt.savefig(output_path, bbox_inches="tight", dpi=dpi)
2553        # 図の表示
2554        if show_fig:
2555            plt.show()
2556        else:
2557            plt.close(fig=fig)
2558
2559        if print_summary:
2560            # デバッグ用の出力
2561            print("\nビンごとの集計:")
2562            print(f"{'Range':>12} | {'bio':>8} | {'gas':>8} | {'total':>8}")
2563            print("-" * 50)
2564
2565            for i in range(len(bins) - 1):
2566                bin_start = bins[i]
2567                bin_end = bins[i + 1]
2568
2569                # 各タイプのカウントを計算
2570                counts_by_type: dict[HotspotType, int] = {"bio": 0, "gas": 0, "comb": 0}
2571                total = 0
2572                for spot_type in existing_types:
2573                    mask = (
2574                        (df["type"] == spot_type)
2575                        & (df["emission_rate"] >= bin_start)
2576                        & (df["emission_rate"] < bin_end)
2577                    )
2578                    count = len(df[mask])
2579                    counts_by_type[spot_type] = count
2580                    total += count
2581
2582                # カウントが0の場合はスキップ
2583                if total > 0:
2584                    range_str = f"{bin_start:5.1f}-{bin_end:<5.1f}"
2585                    bio_count = counts_by_type.get("bio", 0)
2586                    gas_count = counts_by_type.get("gas", 0)
2587                    print(
2588                        f"{range_str:>12} | {bio_count:8d} | {gas_count:8d} | {total:8d}"
2589                    )

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

Parameters

emission_data_list : list[EmissionData]
    EmissionDataオブジェクトのリスト。
output_dir : str | Path | None
    出力先ディレクトリのパス。
output_filename : str
    保存するファイル名。デフォルトは"emission_analysis.png"。
dpi : int
    プロットの解像度。デフォルトは300。
figsize : tuple[float, float]
    プロットのサイズ。デフォルトは(12, 5)。
hotspot_colors : dict[HotspotType, str]
    ホットスポットの色を定義する辞書。
add_legend : bool
    凡例を追加するかどうか。デフォルトはTrue。
hist_log_y : bool
    ヒストグラムのy軸を対数スケールにするかどうか。デフォルトはFalse。
hist_xlim : tuple[float, float] | None
    ヒストグラムのx軸の範囲。デフォルトはNone。
hist_ylim : tuple[float, float] | None
    ヒストグラムのy軸の範囲。デフォルトはNone。
scatter_xlim : tuple[float, float] | None
    散布図のx軸の範囲。デフォルトはNone。
scatter_ylim : tuple[float, float] | None
    散布図のy軸の範囲。デフォルトはNone。
hist_bin_width : float
    ヒストグラムのビンの幅。デフォルトは0.5。
print_summary : bool
    集計結果を表示するかどうか。デフォルトはFalse。
save_fig : bool
    図をファイルに保存するかどうか。デフォルトはFalse。
show_fig : bool
    図を表示するかどうか。デフォルトはTrue。
show_scatter : bool
    散布図(右図)を表示するかどうか。デフォルトはTrue。
@dataclass
class MSAInputConfig:
217@dataclass
218class MSAInputConfig:
219    """入力ファイルの設定を保持するデータクラス
220
221    Parameters
222    ----------
223        fs : float
224            サンプリング周波数(Hz)
225        lag : float
226            測器の遅れ時間(秒)
227        path : Path | str
228            ファイルパス
229        bias_removal : BiasRemovalConfig | None
230            バイアス除去の設定。None(または未定義)の場合は補正を実施しない。
231        h2o_correction : H2OCorrectionConfig | None
232            水蒸気補正の設定。None(または未定義)の場合は補正を実施しない。
233    """
234
235    fs: float
236    lag: float
237    path: Path | str
238    bias_removal: BiasRemovalConfig | None = None
239    h2o_correction: H2OCorrectionConfig | None = None
240
241    def __post_init__(self) -> None:
242        """
243        インスタンス生成後に入力値の検証を行います。
244        """
245        # fsが有効かを確認
246        if not isinstance(self.fs, (int, float)) or self.fs <= 0:
247            raise ValueError(
248                f"Invalid sampling frequency: {self.fs}. Must be a positive float."
249            )
250        # lagが0以上のfloatかを確認
251        if not isinstance(self.lag, (int, float)) or self.lag < 0:
252            raise ValueError(
253                f"Invalid lag value: {self.lag}. Must be a non-negative float."
254            )
255        # 拡張子の確認
256        supported_extensions: list[str] = [".txt", ".csv"]
257        extension = Path(self.path).suffix
258        if extension not in supported_extensions:
259            raise ValueError(
260                f"Unsupported file extension: '{extension}'. Supported: {supported_extensions}"
261            )
262
263    @classmethod
264    def validate_and_create(
265        cls,
266        fs: float,
267        lag: float,
268        path: Path | str,
269        h2o_correction: H2OCorrectionConfig | None = None,
270        bias_removal: BiasRemovalConfig | None = None,
271    ) -> "MSAInputConfig":
272        """
273        入力値を検証し、MSAInputConfigインスタンスを生成するファクトリメソッドです。
274
275        指定された遅延時間、サンプリング周波数、およびファイルパスが有効であることを確認し、
276        有効な場合に新しいMSAInputConfigオブジェクトを返します。
277
278        Parameters
279        ----------
280            fs : float
281                サンプリング周波数。正のfloatである必要があります。
282            lag : float
283                遅延時間。0以上のfloatである必要があります。
284            path : Path | str
285                入力ファイルのパス。サポートされている拡張子は.txtと.csvです。
286            bias_removal : BiasRemovalConfig | None
287                バイアス除去の設定。None(または未定義)の場合は補正を実施しない。
288            h2o_correction : H2OCorrectionConfig | None
289                水蒸気補正の設定。None(または未定義)の場合は補正を実施しない。
290
291        Returns
292        ----------
293            MSAInputConfig
294                検証された入力設定を持つMSAInputConfigオブジェクト。
295        """
296        return cls(
297            fs=fs,
298            lag=lag,
299            path=path,
300            bias_removal=bias_removal,
301            h2o_correction=h2o_correction,
302        )

入力ファイルの設定を保持するデータクラス

Parameters

fs : float
    サンプリング周波数(Hz)
lag : float
    測器の遅れ時間(秒)
path : Path | str
    ファイルパス
bias_removal : BiasRemovalConfig | None
    バイアス除去の設定。None(または未定義)の場合は補正を実施しない。
h2o_correction : H2OCorrectionConfig | None
    水蒸気補正の設定。None(または未定義)の場合は補正を実施しない。
MSAInputConfig( fs: float, lag: float, path: pathlib.Path | str, bias_removal: BiasRemovalConfig | None = None, h2o_correction: H2OCorrectionConfig | None = None)
fs: float
lag: float
path: pathlib.Path | str
bias_removal: BiasRemovalConfig | None = None
h2o_correction: H2OCorrectionConfig | None = None
@classmethod
def validate_and_create( cls, fs: float, lag: float, path: pathlib.Path | str, h2o_correction: H2OCorrectionConfig | None = None, bias_removal: BiasRemovalConfig | None = None) -> MSAInputConfig:
263    @classmethod
264    def validate_and_create(
265        cls,
266        fs: float,
267        lag: float,
268        path: Path | str,
269        h2o_correction: H2OCorrectionConfig | None = None,
270        bias_removal: BiasRemovalConfig | None = None,
271    ) -> "MSAInputConfig":
272        """
273        入力値を検証し、MSAInputConfigインスタンスを生成するファクトリメソッドです。
274
275        指定された遅延時間、サンプリング周波数、およびファイルパスが有効であることを確認し、
276        有効な場合に新しいMSAInputConfigオブジェクトを返します。
277
278        Parameters
279        ----------
280            fs : float
281                サンプリング周波数。正のfloatである必要があります。
282            lag : float
283                遅延時間。0以上のfloatである必要があります。
284            path : Path | str
285                入力ファイルのパス。サポートされている拡張子は.txtと.csvです。
286            bias_removal : BiasRemovalConfig | None
287                バイアス除去の設定。None(または未定義)の場合は補正を実施しない。
288            h2o_correction : H2OCorrectionConfig | None
289                水蒸気補正の設定。None(または未定義)の場合は補正を実施しない。
290
291        Returns
292        ----------
293            MSAInputConfig
294                検証された入力設定を持つMSAInputConfigオブジェクト。
295        """
296        return cls(
297            fs=fs,
298            lag=lag,
299            path=path,
300            bias_removal=bias_removal,
301            h2o_correction=h2o_correction,
302        )

入力値を検証し、MSAInputConfigインスタンスを生成するファクトリメソッドです。

指定された遅延時間、サンプリング周波数、およびファイルパスが有効であることを確認し、 有効な場合に新しいMSAInputConfigオブジェクトを返します。

Parameters

fs : float
    サンプリング周波数。正のfloatである必要があります。
lag : float
    遅延時間。0以上のfloatである必要があります。
path : Path | str
    入力ファイルのパス。サポートされている拡張子は.txtと.csvです。
bias_removal : BiasRemovalConfig | None
    バイアス除去の設定。None(または未定義)の場合は補正を実施しない。
h2o_correction : H2OCorrectionConfig | None
    水蒸気補正の設定。None(または未定義)の場合は補正を実施しない。

Returns

MSAInputConfig
    検証された入力設定を持つMSAInputConfigオブジェクト。
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        na_values: list[str] = [
 23            "#DIV/0!",
 24            "#VALUE!",
 25            "#REF!",
 26            "#N/A",
 27            "#NAME?",
 28            "NAN",
 29            "nan",
 30        ],
 31        logger: Logger | None = None,
 32        logging_debug: bool = False,
 33    ):
 34        """
 35        MonthlyConverterクラスのコンストラクタ
 36
 37        Parameters
 38        ----------
 39            directory : str | Path
 40                Excelファイルが格納されているディレクトリのパス
 41            file_pattern : str
 42                ファイル名のパターン。デフォルトは'SA.Ultra.*.xlsx'。
 43            na_values : list[str]
 44                NaNと判定する値のパターン。
 45            logger : Logger | None
 46                使用するロガー。Noneの場合は新しいロガーを作成します。
 47            logging_debug : bool
 48                ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。
 49        """
 50        # ロガー
 51        log_level: int = INFO
 52        if logging_debug:
 53            log_level = DEBUG
 54        self.logger: Logger = MonthlyConverter.setup_logger(logger, log_level)
 55
 56        self._na_values: list[str] = na_values
 57        self._directory = Path(directory)
 58        if not self._directory.exists():
 59            raise NotADirectoryError(f"Directory not found: {self._directory}")
 60
 61        # Excelファイルのパスを保持
 62        self._excel_files: dict[str, pd.ExcelFile] = {}
 63        self._file_pattern: str = file_pattern
 64
 65    @staticmethod
 66    def setup_logger(logger: Logger | None, log_level: int = INFO) -> Logger:
 67        """
 68        ロガーを設定します。
 69
 70        このメソッドは、ロギングの設定を行い、ログメッセージのフォーマットを指定します。
 71        ログメッセージには、日付、ログレベル、メッセージが含まれます。
 72
 73        渡されたロガーがNoneまたは不正な場合は、新たにロガーを作成し、標準出力に
 74        ログメッセージが表示されるようにStreamHandlerを追加します。ロガーのレベルは
 75        引数で指定されたlog_levelに基づいて設定されます。
 76
 77        Parameters
 78        ----------
 79            logger : Logger | None
 80                使用するロガー。Noneの場合は新しいロガーを作成します。
 81            log_level : int
 82                ロガーのログレベル。デフォルトはINFO。
 83
 84        Returns
 85        ----------
 86            Logger
 87                設定されたロガーオブジェクト。
 88        """
 89        if logger is not None and isinstance(logger, Logger):
 90            return logger
 91        # 渡されたロガーがNoneまたは正しいものでない場合は独自に設定
 92        new_logger: Logger = getLogger()
 93        # 既存のハンドラーをすべて削除
 94        for handler in new_logger.handlers[:]:
 95            new_logger.removeHandler(handler)
 96        new_logger.setLevel(log_level)  # ロガーのレベルを設定
 97        ch = StreamHandler()
 98        ch_formatter = Formatter("%(asctime)s - %(levelname)s - %(message)s")
 99        ch.setFormatter(ch_formatter)  # フォーマッターをハンドラーに設定
100        new_logger.addHandler(ch)  # StreamHandlerの追加
101        return new_logger
102
103    def close(self) -> None:
104        """
105        すべてのExcelファイルをクローズする
106        """
107        for excel_file in self._excel_files.values():
108            excel_file.close()
109        self._excel_files.clear()
110
111    def get_available_dates(self) -> list[str]:
112        """
113        利用可能なファイルの日付一覧を返却します。
114
115        Returns
116        ----------
117            list[str]
118                'yyyy.MM'形式の日付リスト
119        """
120        dates = []
121        for file_name in self._directory.glob(self._file_pattern):
122            try:
123                date = self._extract_date(file_name.name)
124                dates.append(date.strftime(self.FILE_DATE_FORMAT))
125            except ValueError:
126                continue
127        return sorted(dates)
128
129    def get_sheet_names(self, file_name: str) -> list[str]:
130        """
131        指定されたファイルで利用可能なシート名の一覧を返却する
132
133        Parameters
134        ----------
135            file_name : str
136                Excelファイル名
137
138        Returns
139        ----------
140            list[str]
141                シート名のリスト
142        """
143        if file_name not in self._excel_files:
144            file_path = self._directory / file_name
145            if not file_path.exists():
146                raise FileNotFoundError(f"File not found: {file_path}")
147            self._excel_files[file_name] = pd.ExcelFile(file_path)
148        return self._excel_files[file_name].sheet_names
149
150    def read_sheets(
151        self,
152        sheet_names: str | list[str],
153        columns: list[str] | None = None,  # 新しいパラメータを追加
154        col_datetime: str = "Date",
155        header: int = 0,
156        skiprows: int | list[int] = [1],
157        start_date: str | None = None,
158        end_date: str | None = None,
159        include_end_date: bool = True,
160        sort_by_date: bool = True,
161    ) -> pd.DataFrame:
162        """
163        指定されたシートを読み込み、DataFrameとして返却します。
164        デフォルトでは2行目(単位の行)はスキップされます。
165        重複するカラム名がある場合は、より先に指定されたシートに存在するカラムの値を保持します。
166
167        Parameters
168        ----------
169            sheet_names : str | list[str]
170                読み込むシート名。文字列または文字列のリストを指定できます。
171            columns : list[str] | None
172                残すカラム名のリスト。Noneの場合は全てのカラムを保持します。
173            col_datetime : str
174                日付と時刻の情報が含まれるカラム名。デフォルトは'Date'。
175            header : int
176                データのヘッダー行を指定します。デフォルトは0。
177            skiprows : int | list[int]
178                スキップする行数。デフォルトでは1行目をスキップします。
179            start_date : str | None
180                開始日 ('yyyy-MM-dd')。この日付の'00:00:00'のデータが開始行となります。
181            end_date : str | None
182                終了日 ('yyyy-MM-dd')。この日付をデータに含めるかはinclude_end_dateフラグによって変わります。
183            include_end_date : bool
184                終了日を含めるかどうか。デフォルトはTrueです。
185            sort_by_date : bool
186                ファイルの日付でソートするかどうか。デフォルトはTrueです。
187
188        Returns
189        ----------
190            pd.DataFrame
191                読み込まれたデータを結合したDataFrameを返します。
192        """
193        if isinstance(sheet_names, str):
194            sheet_names = [sheet_names]
195
196        self._load_excel_files(start_date, end_date)
197
198        if not self._excel_files:
199            raise ValueError("No Excel files found matching the criteria")
200
201        # ファイルを日付順にソート
202        sorted_files = (
203            sorted(self._excel_files.items(), key=lambda x: self._extract_date(x[0]))
204            if sort_by_date
205            else self._excel_files.items()
206        )
207
208        # 各シートのデータを格納するリスト
209        sheet_dfs = {sheet_name: [] for sheet_name in sheet_names}
210
211        # 各ファイルからデータを読み込む
212        for file_name, excel_file in sorted_files:
213            file_date = self._extract_date(file_name)
214
215            for sheet_name in sheet_names:
216                if sheet_name in excel_file.sheet_names:
217                    df = pd.read_excel(
218                        excel_file,
219                        sheet_name=sheet_name,
220                        header=header,
221                        skiprows=skiprows,
222                        na_values=self._na_values,
223                    )
224                    # 年と月を追加
225                    df["year"] = file_date.year
226                    df["month"] = file_date.month
227                    sheet_dfs[sheet_name].append(df)
228
229        if not any(sheet_dfs.values()):
230            raise ValueError(f"No sheets found matching: {sheet_names}")
231
232        # 各シートのデータを結合
233        combined_sheets = {}
234        for sheet_name, dfs in sheet_dfs.items():
235            if dfs:  # シートにデータがある場合のみ結合
236                combined_sheets[sheet_name] = pd.concat(dfs, ignore_index=True)
237
238        # 最初のシートをベースにする
239        base_df = combined_sheets[sheet_names[0]]
240
241        # 2つ目以降のシートを結合
242        for sheet_name in sheet_names[1:]:
243            if sheet_name in combined_sheets:
244                base_df = self.merge_dataframes(
245                    base_df, combined_sheets[sheet_name], date_column=col_datetime
246                )
247
248        # 日付でフィルタリング
249        if start_date:
250            start_dt = pd.to_datetime(start_date)
251            base_df = base_df[base_df[col_datetime] >= start_dt]
252
253        if end_date:
254            end_dt = pd.to_datetime(end_date)
255            if include_end_date:
256                end_dt += pd.Timedelta(days=1)
257            base_df = base_df[base_df[col_datetime] < end_dt]
258
259        # カラムの選択
260        if columns is not None:
261            required_columns = [col_datetime, "year", "month"]
262            available_columns = base_df.columns.tolist()  # 利用可能なカラムを取得
263            if not all(col in available_columns for col in columns):
264                raise ValueError(
265                    f"指定されたカラムが見つかりません: {columns}. 利用可能なカラム: {available_columns}"
266                )
267            selected_columns = list(set(columns + required_columns))
268            base_df = base_df[selected_columns]
269
270        return base_df
271
272    def __enter__(self) -> "MonthlyConverter":
273        return self
274
275    def __exit__(self, exc_type, exc_val, exc_tb) -> None:
276        self.close()
277
278    def _extract_date(self, file_name: str) -> datetime:
279        """
280        ファイル名から日付を抽出する
281
282        Parameters
283        ----------
284            file_name : str
285                "SA.Ultra.yyyy.MM.xlsx"または"SA.Picaro.yyyy.MM.xlsx"形式のファイル名
286
287        Returns
288        ----------
289            datetime
290                抽出された日付
291        """
292        # ファイル名から日付部分を抽出
293        date_str = ".".join(file_name.split(".")[-3:-1])  # "yyyy.MM"の部分を取得
294        return datetime.strptime(date_str, self.FILE_DATE_FORMAT)
295
296    def _load_excel_files(
297        self, start_date: str | None = None, end_date: str | None = None
298    ) -> None:
299        """
300        指定された日付範囲のExcelファイルを読み込む
301
302        Parameters
303        ----------
304            start_date : str | None
305                開始日 ('yyyy-MM-dd'形式)
306            end_date : str | None
307                終了日 ('yyyy-MM-dd'形式)
308        """
309        # 期間指定がある場合は、yyyy-MM-dd形式から年月のみを抽出
310        start_dt = None
311        end_dt = None
312        if start_date:
313            temp_dt = datetime.strptime(start_date, self.PERIOD_DATE_FORMAT)
314            start_dt = datetime(temp_dt.year, temp_dt.month, 1)
315        if end_date:
316            temp_dt = datetime.strptime(end_date, self.PERIOD_DATE_FORMAT)
317            end_dt = datetime(temp_dt.year, temp_dt.month, 1)
318
319        # 既存のファイルをクリア
320        self.close()
321
322        for excel_path in self._directory.glob(self._file_pattern):
323            try:
324                file_date = self._extract_date(excel_path.name)
325
326                # 日付範囲チェック
327                if start_dt and file_date < start_dt:
328                    continue
329                if end_dt and file_date > end_dt:
330                    continue
331
332                if excel_path.name not in self._excel_files:
333                    self._excel_files[excel_path.name] = pd.ExcelFile(excel_path)
334
335            except ValueError as e:
336                self.logger.warning(
337                    f"Could not parse date from file {excel_path.name}: {e}"
338                )
339
340    @staticmethod
341    def extract_monthly_data(
342        df: pd.DataFrame,
343        target_months: list[int],
344        start_day: int | None = None,
345        end_day: int | None = None,
346        datetime_column: str = "Date",
347    ) -> pd.DataFrame:
348        """
349        指定された月と期間のデータを抽出します。
350
351        Parameters
352        ----------
353            df : pd.DataFrame
354                入力データフレーム。
355            target_months : list[int]
356                抽出したい月のリスト(1から12の整数)。
357            start_day : int | None
358                開始日(1から31の整数)。Noneの場合は月初め。
359            end_day : int | None
360                終了日(1から31の整数)。Noneの場合は月末。
361            datetime_column : str, optional
362                日付を含む列の名前。デフォルトは"Date"。
363
364        Returns
365        ----------
366            pd.DataFrame
367                指定された期間のデータのみを含むデータフレーム。
368        """
369        # 入力チェック
370        if not all(1 <= month <= 12 for month in target_months):
371            raise ValueError("target_monthsは1から12の間である必要があります")
372
373        if start_day is not None and not 1 <= start_day <= 31:
374            raise ValueError("start_dayは1から31の間である必要があります")
375
376        if end_day is not None and not 1 <= end_day <= 31:
377            raise ValueError("end_dayは1から31の間である必要があります")
378
379        if start_day is not None and end_day is not None and start_day > end_day:
380            raise ValueError("start_dayはend_day以下である必要があります")
381
382        # datetime_column をDatetime型に変換
383        df_copied = df.copy()
384        df_copied[datetime_column] = pd.to_datetime(df_copied[datetime_column])
385
386        # 月でフィルタリング
387        monthly_data = df_copied[df_copied[datetime_column].dt.month.isin(target_months)]
388
389        # 日付範囲でフィルタリング
390        if start_day is not None:
391            monthly_data = monthly_data[
392                monthly_data[datetime_column].dt.day >= start_day
393            ]
394        if end_day is not None:
395            monthly_data = monthly_data[monthly_data[datetime_column].dt.day <= end_day]
396
397        return monthly_data
398
399    @staticmethod
400    def merge_dataframes(
401        df1: pd.DataFrame, df2: pd.DataFrame, date_column: str = "Date"
402    ) -> pd.DataFrame:
403        """
404        2つのDataFrameを結合します。重複するカラムは元の名前とサフィックス付きの両方を保持します。
405
406        Parameters
407        ----------
408            df1 : pd.DataFrame
409                ベースとなるDataFrame
410            df2 : pd.DataFrame
411                結合するDataFrame
412            date_column : str
413                日付カラムの名前。デフォルトは"Date"
414
415        Returns
416        ----------
417            pd.DataFrame
418                結合されたDataFrame
419        """
420        # インデックスをリセット
421        df1 = df1.reset_index(drop=True)
422        df2 = df2.reset_index(drop=True)
423
424        # 日付カラムを統一
425        df2[date_column] = df1[date_column]
426
427        # 重複しないカラムと重複するカラムを分離
428        duplicate_cols = [date_column, "year", "month"]  # 常に除外するカラム
429        overlapping_cols = [
430            col
431            for col in df2.columns
432            if col in df1.columns and col not in duplicate_cols
433        ]
434        unique_cols = [
435            col
436            for col in df2.columns
437            if col not in df1.columns and col not in duplicate_cols
438        ]
439
440        # 結果のDataFrameを作成
441        result = df1.copy()
442
443        # 重複しないカラムを追加
444        for col in unique_cols:
445            result[col] = df2[col]
446
447        # 重複するカラムを処理
448        for col in overlapping_cols:
449            # 元のカラムはdf1の値を保持(既に result に含まれている)
450            # _x サフィックスでdf1の値を追加
451            result[f"{col}_x"] = df1[col]
452            # _y サフィックスでdf2の値を追加
453            result[f"{col}_y"] = df2[col]
454
455        return result

Monthlyシート(Excel)を一括で読み込み、DataFrameに変換するクラス。 デフォルトは'SA.Ultra..xlsx'に対応していますが、コンストラクタのfile_patternを 変更すると別のシートにも対応可能です(例: 'SA.Picaro..xlsx')。

MonthlyConverter( directory: str | pathlib.Path, file_pattern: str = 'SA.Ultra.*.xlsx', na_values: list[str] = ['#DIV/0!', '#VALUE!', '#REF!', '#N/A', '#NAME?', 'NAN', 'nan'], 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        na_values: list[str] = [
23            "#DIV/0!",
24            "#VALUE!",
25            "#REF!",
26            "#N/A",
27            "#NAME?",
28            "NAN",
29            "nan",
30        ],
31        logger: Logger | None = None,
32        logging_debug: bool = False,
33    ):
34        """
35        MonthlyConverterクラスのコンストラクタ
36
37        Parameters
38        ----------
39            directory : str | Path
40                Excelファイルが格納されているディレクトリのパス
41            file_pattern : str
42                ファイル名のパターン。デフォルトは'SA.Ultra.*.xlsx'。
43            na_values : list[str]
44                NaNと判定する値のパターン。
45            logger : Logger | None
46                使用するロガー。Noneの場合は新しいロガーを作成します。
47            logging_debug : bool
48                ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。
49        """
50        # ロガー
51        log_level: int = INFO
52        if logging_debug:
53            log_level = DEBUG
54        self.logger: Logger = MonthlyConverter.setup_logger(logger, log_level)
55
56        self._na_values: list[str] = na_values
57        self._directory = Path(directory)
58        if not self._directory.exists():
59            raise NotADirectoryError(f"Directory not found: {self._directory}")
60
61        # Excelファイルのパスを保持
62        self._excel_files: dict[str, pd.ExcelFile] = {}
63        self._file_pattern: str = file_pattern

MonthlyConverterクラスのコンストラクタ

Parameters

directory : str | Path
    Excelファイルが格納されているディレクトリのパス
file_pattern : str
    ファイル名のパターン。デフォルトは'SA.Ultra.*.xlsx'。
na_values : list[str]
    NaNと判定する値のパターン。
logger : Logger | None
    使用するロガー。Noneの場合は新しいロガーを作成します。
logging_debug : bool
    ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。
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:
 65    @staticmethod
 66    def setup_logger(logger: Logger | None, log_level: int = INFO) -> Logger:
 67        """
 68        ロガーを設定します。
 69
 70        このメソッドは、ロギングの設定を行い、ログメッセージのフォーマットを指定します。
 71        ログメッセージには、日付、ログレベル、メッセージが含まれます。
 72
 73        渡されたロガーがNoneまたは不正な場合は、新たにロガーを作成し、標準出力に
 74        ログメッセージが表示されるようにStreamHandlerを追加します。ロガーのレベルは
 75        引数で指定されたlog_levelに基づいて設定されます。
 76
 77        Parameters
 78        ----------
 79            logger : Logger | None
 80                使用するロガー。Noneの場合は新しいロガーを作成します。
 81            log_level : int
 82                ロガーのログレベル。デフォルトはINFO。
 83
 84        Returns
 85        ----------
 86            Logger
 87                設定されたロガーオブジェクト。
 88        """
 89        if logger is not None and isinstance(logger, Logger):
 90            return logger
 91        # 渡されたロガーがNoneまたは正しいものでない場合は独自に設定
 92        new_logger: Logger = getLogger()
 93        # 既存のハンドラーをすべて削除
 94        for handler in new_logger.handlers[:]:
 95            new_logger.removeHandler(handler)
 96        new_logger.setLevel(log_level)  # ロガーのレベルを設定
 97        ch = StreamHandler()
 98        ch_formatter = Formatter("%(asctime)s - %(levelname)s - %(message)s")
 99        ch.setFormatter(ch_formatter)  # フォーマッターをハンドラーに設定
100        new_logger.addHandler(ch)  # StreamHandlerの追加
101        return new_logger

ロガーを設定します。

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

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

Parameters

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

Returns

Logger
    設定されたロガーオブジェクト。
def close(self) -> None:
103    def close(self) -> None:
104        """
105        すべてのExcelファイルをクローズする
106        """
107        for excel_file in self._excel_files.values():
108            excel_file.close()
109        self._excel_files.clear()

すべてのExcelファイルをクローズする

def get_available_dates(self) -> list[str]:
111    def get_available_dates(self) -> list[str]:
112        """
113        利用可能なファイルの日付一覧を返却します。
114
115        Returns
116        ----------
117            list[str]
118                'yyyy.MM'形式の日付リスト
119        """
120        dates = []
121        for file_name in self._directory.glob(self._file_pattern):
122            try:
123                date = self._extract_date(file_name.name)
124                dates.append(date.strftime(self.FILE_DATE_FORMAT))
125            except ValueError:
126                continue
127        return sorted(dates)

利用可能なファイルの日付一覧を返却します。

Returns

list[str]
    'yyyy.MM'形式の日付リスト
def get_sheet_names(self, file_name: str) -> list[str]:
129    def get_sheet_names(self, file_name: str) -> list[str]:
130        """
131        指定されたファイルで利用可能なシート名の一覧を返却する
132
133        Parameters
134        ----------
135            file_name : str
136                Excelファイル名
137
138        Returns
139        ----------
140            list[str]
141                シート名のリスト
142        """
143        if file_name not in self._excel_files:
144            file_path = self._directory / file_name
145            if not file_path.exists():
146                raise FileNotFoundError(f"File not found: {file_path}")
147            self._excel_files[file_name] = pd.ExcelFile(file_path)
148        return self._excel_files[file_name].sheet_names

指定されたファイルで利用可能なシート名の一覧を返却する

Parameters

file_name : str
    Excelファイル名

Returns

list[str]
    シート名のリスト
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:
150    def read_sheets(
151        self,
152        sheet_names: str | list[str],
153        columns: list[str] | None = None,  # 新しいパラメータを追加
154        col_datetime: str = "Date",
155        header: int = 0,
156        skiprows: int | list[int] = [1],
157        start_date: str | None = None,
158        end_date: str | None = None,
159        include_end_date: bool = True,
160        sort_by_date: bool = True,
161    ) -> pd.DataFrame:
162        """
163        指定されたシートを読み込み、DataFrameとして返却します。
164        デフォルトでは2行目(単位の行)はスキップされます。
165        重複するカラム名がある場合は、より先に指定されたシートに存在するカラムの値を保持します。
166
167        Parameters
168        ----------
169            sheet_names : str | list[str]
170                読み込むシート名。文字列または文字列のリストを指定できます。
171            columns : list[str] | None
172                残すカラム名のリスト。Noneの場合は全てのカラムを保持します。
173            col_datetime : str
174                日付と時刻の情報が含まれるカラム名。デフォルトは'Date'。
175            header : int
176                データのヘッダー行を指定します。デフォルトは0。
177            skiprows : int | list[int]
178                スキップする行数。デフォルトでは1行目をスキップします。
179            start_date : str | None
180                開始日 ('yyyy-MM-dd')。この日付の'00:00:00'のデータが開始行となります。
181            end_date : str | None
182                終了日 ('yyyy-MM-dd')。この日付をデータに含めるかはinclude_end_dateフラグによって変わります。
183            include_end_date : bool
184                終了日を含めるかどうか。デフォルトはTrueです。
185            sort_by_date : bool
186                ファイルの日付でソートするかどうか。デフォルトはTrueです。
187
188        Returns
189        ----------
190            pd.DataFrame
191                読み込まれたデータを結合したDataFrameを返します。
192        """
193        if isinstance(sheet_names, str):
194            sheet_names = [sheet_names]
195
196        self._load_excel_files(start_date, end_date)
197
198        if not self._excel_files:
199            raise ValueError("No Excel files found matching the criteria")
200
201        # ファイルを日付順にソート
202        sorted_files = (
203            sorted(self._excel_files.items(), key=lambda x: self._extract_date(x[0]))
204            if sort_by_date
205            else self._excel_files.items()
206        )
207
208        # 各シートのデータを格納するリスト
209        sheet_dfs = {sheet_name: [] for sheet_name in sheet_names}
210
211        # 各ファイルからデータを読み込む
212        for file_name, excel_file in sorted_files:
213            file_date = self._extract_date(file_name)
214
215            for sheet_name in sheet_names:
216                if sheet_name in excel_file.sheet_names:
217                    df = pd.read_excel(
218                        excel_file,
219                        sheet_name=sheet_name,
220                        header=header,
221                        skiprows=skiprows,
222                        na_values=self._na_values,
223                    )
224                    # 年と月を追加
225                    df["year"] = file_date.year
226                    df["month"] = file_date.month
227                    sheet_dfs[sheet_name].append(df)
228
229        if not any(sheet_dfs.values()):
230            raise ValueError(f"No sheets found matching: {sheet_names}")
231
232        # 各シートのデータを結合
233        combined_sheets = {}
234        for sheet_name, dfs in sheet_dfs.items():
235            if dfs:  # シートにデータがある場合のみ結合
236                combined_sheets[sheet_name] = pd.concat(dfs, ignore_index=True)
237
238        # 最初のシートをベースにする
239        base_df = combined_sheets[sheet_names[0]]
240
241        # 2つ目以降のシートを結合
242        for sheet_name in sheet_names[1:]:
243            if sheet_name in combined_sheets:
244                base_df = self.merge_dataframes(
245                    base_df, combined_sheets[sheet_name], date_column=col_datetime
246                )
247
248        # 日付でフィルタリング
249        if start_date:
250            start_dt = pd.to_datetime(start_date)
251            base_df = base_df[base_df[col_datetime] >= start_dt]
252
253        if end_date:
254            end_dt = pd.to_datetime(end_date)
255            if include_end_date:
256                end_dt += pd.Timedelta(days=1)
257            base_df = base_df[base_df[col_datetime] < end_dt]
258
259        # カラムの選択
260        if columns is not None:
261            required_columns = [col_datetime, "year", "month"]
262            available_columns = base_df.columns.tolist()  # 利用可能なカラムを取得
263            if not all(col in available_columns for col in columns):
264                raise ValueError(
265                    f"指定されたカラムが見つかりません: {columns}. 利用可能なカラム: {available_columns}"
266                )
267            selected_columns = list(set(columns + required_columns))
268            base_df = base_df[selected_columns]
269
270        return base_df

指定されたシートを読み込み、DataFrameとして返却します。 デフォルトでは2行目(単位の行)はスキップされます。 重複するカラム名がある場合は、より先に指定されたシートに存在するカラムの値を保持します。

Parameters

sheet_names : str | list[str]
    読み込むシート名。文字列または文字列のリストを指定できます。
columns : list[str] | None
    残すカラム名のリスト。Noneの場合は全てのカラムを保持します。
col_datetime : str
    日付と時刻の情報が含まれるカラム名。デフォルトは'Date'。
header : int
    データのヘッダー行を指定します。デフォルトは0。
skiprows : int | list[int]
    スキップする行数。デフォルトでは1行目をスキップします。
start_date : str | None
    開始日 ('yyyy-MM-dd')。この日付の'00:00:00'のデータが開始行となります。
end_date : str | None
    終了日 ('yyyy-MM-dd')。この日付をデータに含めるかはinclude_end_dateフラグによって変わります。
include_end_date : bool
    終了日を含めるかどうか。デフォルトはTrueです。
sort_by_date : bool
    ファイルの日付でソートするかどうか。デフォルトはTrueです。

Returns

pd.DataFrame
    読み込まれたデータを結合したDataFrameを返します。
@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:
340    @staticmethod
341    def extract_monthly_data(
342        df: pd.DataFrame,
343        target_months: list[int],
344        start_day: int | None = None,
345        end_day: int | None = None,
346        datetime_column: str = "Date",
347    ) -> pd.DataFrame:
348        """
349        指定された月と期間のデータを抽出します。
350
351        Parameters
352        ----------
353            df : pd.DataFrame
354                入力データフレーム。
355            target_months : list[int]
356                抽出したい月のリスト(1から12の整数)。
357            start_day : int | None
358                開始日(1から31の整数)。Noneの場合は月初め。
359            end_day : int | None
360                終了日(1から31の整数)。Noneの場合は月末。
361            datetime_column : str, optional
362                日付を含む列の名前。デフォルトは"Date"。
363
364        Returns
365        ----------
366            pd.DataFrame
367                指定された期間のデータのみを含むデータフレーム。
368        """
369        # 入力チェック
370        if not all(1 <= month <= 12 for month in target_months):
371            raise ValueError("target_monthsは1から12の間である必要があります")
372
373        if start_day is not None and not 1 <= start_day <= 31:
374            raise ValueError("start_dayは1から31の間である必要があります")
375
376        if end_day is not None and not 1 <= end_day <= 31:
377            raise ValueError("end_dayは1から31の間である必要があります")
378
379        if start_day is not None and end_day is not None and start_day > end_day:
380            raise ValueError("start_dayはend_day以下である必要があります")
381
382        # datetime_column をDatetime型に変換
383        df_copied = df.copy()
384        df_copied[datetime_column] = pd.to_datetime(df_copied[datetime_column])
385
386        # 月でフィルタリング
387        monthly_data = df_copied[df_copied[datetime_column].dt.month.isin(target_months)]
388
389        # 日付範囲でフィルタリング
390        if start_day is not None:
391            monthly_data = monthly_data[
392                monthly_data[datetime_column].dt.day >= start_day
393            ]
394        if end_day is not None:
395            monthly_data = monthly_data[monthly_data[datetime_column].dt.day <= end_day]
396
397        return monthly_data

指定された月と期間のデータを抽出します。

Parameters

df : pd.DataFrame
    入力データフレーム。
target_months : list[int]
    抽出したい月のリスト(1から12の整数)。
start_day : int | None
    開始日(1から31の整数)。Noneの場合は月初め。
end_day : int | None
    終了日(1から31の整数)。Noneの場合は月末。
datetime_column : str, optional
    日付を含む列の名前。デフォルトは"Date"。

Returns

pd.DataFrame
    指定された期間のデータのみを含むデータフレーム。
@staticmethod
def merge_dataframes( df1: pandas.core.frame.DataFrame, df2: pandas.core.frame.DataFrame, date_column: str = 'Date') -> pandas.core.frame.DataFrame:
399    @staticmethod
400    def merge_dataframes(
401        df1: pd.DataFrame, df2: pd.DataFrame, date_column: str = "Date"
402    ) -> pd.DataFrame:
403        """
404        2つのDataFrameを結合します。重複するカラムは元の名前とサフィックス付きの両方を保持します。
405
406        Parameters
407        ----------
408            df1 : pd.DataFrame
409                ベースとなるDataFrame
410            df2 : pd.DataFrame
411                結合するDataFrame
412            date_column : str
413                日付カラムの名前。デフォルトは"Date"
414
415        Returns
416        ----------
417            pd.DataFrame
418                結合されたDataFrame
419        """
420        # インデックスをリセット
421        df1 = df1.reset_index(drop=True)
422        df2 = df2.reset_index(drop=True)
423
424        # 日付カラムを統一
425        df2[date_column] = df1[date_column]
426
427        # 重複しないカラムと重複するカラムを分離
428        duplicate_cols = [date_column, "year", "month"]  # 常に除外するカラム
429        overlapping_cols = [
430            col
431            for col in df2.columns
432            if col in df1.columns and col not in duplicate_cols
433        ]
434        unique_cols = [
435            col
436            for col in df2.columns
437            if col not in df1.columns and col not in duplicate_cols
438        ]
439
440        # 結果のDataFrameを作成
441        result = df1.copy()
442
443        # 重複しないカラムを追加
444        for col in unique_cols:
445            result[col] = df2[col]
446
447        # 重複するカラムを処理
448        for col in overlapping_cols:
449            # 元のカラムはdf1の値を保持(既に result に含まれている)
450            # _x サフィックスでdf1の値を追加
451            result[f"{col}_x"] = df1[col]
452            # _y サフィックスでdf2の値を追加
453            result[f"{col}_y"] = df2[col]
454
455        return result

2つのDataFrameを結合します。重複するカラムは元の名前とサフィックス付きの両方を保持します。

Parameters

df1 : pd.DataFrame
    ベースとなるDataFrame
df2 : pd.DataFrame
    結合するDataFrame
date_column : str
    日付カラムの名前。デフォルトは"Date"

Returns

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

クラスのコンストラクタ

Parameters

logger : Logger | None
    使用するロガー。Noneの場合は新しいロガーを作成します。
logging_debug : bool
    ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。
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'):
 86    def plot_c1c2_fluxes_timeseries(
 87        self,
 88        df,
 89        output_dir: str,
 90        output_filename: str = "timeseries.png",
 91        col_datetime: str = "Date",
 92        col_c1_flux: str = "Fch4_ultra",
 93        col_c2_flux: str = "Fc2h6_ultra",
 94    ):
 95        """
 96        月別のフラックスデータを時系列プロットとして出力する
 97
 98        Parameters
 99        ------
100            df : pd.DataFrame
101                月別データを含むDataFrame
102            output_dir : str
103                出力ファイルを保存するディレクトリのパス
104            output_filename : str
105                出力ファイルの名前
106            col_datetime : str
107                日付を含む列の名前。デフォルトは"Date"。
108            col_c1_flux : str
109                CH4フラックスを含む列の名前。デフォルトは"Fch4_ultra"。
110            col_c2_flux : str
111                C2H6フラックスを含む列の名前。デフォルトは"Fc2h6_ultra"。
112        """
113        os.makedirs(output_dir, exist_ok=True)
114        output_path: str = os.path.join(output_dir, output_filename)
115
116        # 図の作成
117        _, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 8), sharex=True)
118
119        # CH4フラックスのプロット
120        ax1.scatter(df[col_datetime], df[col_c1_flux], color="red", alpha=0.5, s=20)
121        ax1.set_ylabel(r"CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)")
122        ax1.set_ylim(-100, 600)
123        ax1.text(0.02, 0.98, "(a)", transform=ax1.transAxes, va="top", fontsize=20)
124        ax1.grid(True, alpha=0.3)
125
126        # C2H6フラックスのプロット
127        ax2.scatter(
128            df[col_datetime],
129            df[col_c2_flux],
130            color="orange",
131            alpha=0.5,
132            s=20,
133        )
134        ax2.set_ylabel(r"C$_2$H$_6$ flux (nmol m$^{-2}$ s$^{-1}$)")
135        ax2.set_ylim(-20, 60)
136        ax2.text(0.02, 0.98, "(b)", transform=ax2.transAxes, va="top", fontsize=20)
137        ax2.grid(True, alpha=0.3)
138
139        # x軸の設定
140        ax2.xaxis.set_major_locator(mdates.MonthLocator(interval=1))
141        ax2.xaxis.set_major_formatter(mdates.DateFormatter("%m"))
142        plt.setp(ax2.get_xticklabels(), rotation=0, ha="right")
143        ax2.set_xlabel("Month")
144
145        # 図の保存
146        plt.savefig(output_path, dpi=300, bbox_inches="tight")
147        plt.close()

月別のフラックスデータを時系列プロットとして出力する

Parameters

df : pd.DataFrame
    月別データを含むDataFrame
output_dir : str
    出力ファイルを保存するディレクトリのパス
output_filename : str
    出力ファイルの名前
col_datetime : str
    日付を含む列の名前。デフォルトは"Date"。
col_c1_flux : str
    CH4フラックスを含む列の名前。デフォルトは"Fch4_ultra"。
col_c2_flux : str
    C2H6フラックスを含む列の名前。デフォルトは"Fc2h6_ultra"。
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:
149    def plot_c1c2_concentrations_and_fluxes_timeseries(
150        self,
151        df: pd.DataFrame,
152        output_dir: str,
153        output_filename: str = "conc_flux_timeseries.png",
154        col_datetime: str = "Date",
155        col_ch4_conc: str = "CH4_ultra",
156        col_ch4_flux: str = "Fch4_ultra",
157        col_c2h6_conc: str = "C2H6_ultra",
158        col_c2h6_flux: str = "Fc2h6_ultra",
159        print_summary: bool = True,
160    ) -> None:
161        """
162        CH4とC2H6の濃度とフラックスの時系列プロットを作成する
163
164        Parameters
165        ------
166            df : pd.DataFrame
167                月別データを含むDataFrame
168            output_dir : str
169                出力ディレクトリのパス
170            output_filename : str
171                出力ファイル名
172            col_datetime : str
173                日付列の名前
174            col_ch4_conc : str
175                CH4濃度列の名前
176            col_ch4_flux : str
177                CH4フラックス列の名前
178            col_c2h6_conc : str
179                C2H6濃度列の名前
180            col_c2h6_flux : str
181                C2H6フラックス列の名前
182            print_summary : bool
183                解析情報をprintするかどうか
184        """
185        # 出力ディレクトリの作成
186        os.makedirs(output_dir, exist_ok=True)
187        output_path: str = os.path.join(output_dir, output_filename)
188
189        if print_summary:
190            # 統計情報の計算と表示
191            for name, col in [
192                ("CH4 concentration", col_ch4_conc),
193                ("CH4 flux", col_ch4_flux),
194                ("C2H6 concentration", col_c2h6_conc),
195                ("C2H6 flux", col_c2h6_flux),
196            ]:
197                # NaNを除外してから統計量を計算
198                valid_data = df[col].dropna()
199
200                if len(valid_data) > 0:
201                    percentile_5 = np.nanpercentile(valid_data, 5)
202                    percentile_95 = np.nanpercentile(valid_data, 95)
203                    mean_value = np.nanmean(valid_data)
204                    positive_ratio = (valid_data > 0).mean() * 100
205
206                    print(f"\n{name}:")
207                    print(
208                        f"90パーセンタイルレンジ: {percentile_5:.2f} - {percentile_95:.2f}"
209                    )
210                    print(f"平均値: {mean_value:.2f}")
211                    print(f"正の値の割合: {positive_ratio:.1f}%")
212                else:
213                    print(f"\n{name}: データが存在しません")
214
215        # プロットの作成
216        _, (ax1, ax2, ax3, ax4) = plt.subplots(4, 1, figsize=(12, 16), sharex=True)
217
218        # CH4濃度のプロット
219        ax1.scatter(df[col_datetime], df[col_ch4_conc], color="red", alpha=0.5, s=20)
220        ax1.set_ylabel("CH$_4$ Concentration\n(ppm)")
221        ax1.set_ylim(1.8, 2.6)
222        ax1.text(0.02, 0.98, "(a)", transform=ax1.transAxes, va="top", fontsize=20)
223        ax1.grid(True, alpha=0.3)
224
225        # CH4フラックスのプロット
226        ax2.scatter(df[col_datetime], df[col_ch4_flux], color="red", alpha=0.5, s=20)
227        ax2.set_ylabel("CH$_4$ flux\n(nmol m$^{-2}$ s$^{-1}$)")
228        ax2.set_ylim(-100, 600)
229        # ax2.set_yticks([-100, 0, 200, 400, 600])
230        ax2.text(0.02, 0.98, "(b)", transform=ax2.transAxes, va="top", fontsize=20)
231        ax2.grid(True, alpha=0.3)
232
233        # C2H6濃度のプロット
234        ax3.scatter(
235            df[col_datetime], df[col_c2h6_conc], color="orange", alpha=0.5, s=20
236        )
237        ax3.set_ylabel("C$_2$H$_6$ Concentration\n(ppb)")
238        ax3.text(0.02, 0.98, "(c)", transform=ax3.transAxes, va="top", fontsize=20)
239        ax3.grid(True, alpha=0.3)
240
241        # C2H6フラックスのプロット
242        ax4.scatter(
243            df[col_datetime], df[col_c2h6_flux], color="orange", alpha=0.5, s=20
244        )
245        ax4.set_ylabel("C$_2$H$_6$ flux\n(nmol m$^{-2}$ s$^{-1}$)")
246        ax4.set_ylim(-20, 40)
247        ax4.text(0.02, 0.98, "(d)", transform=ax4.transAxes, va="top", fontsize=20)
248        ax4.grid(True, alpha=0.3)
249
250        # x軸の設定
251        ax4.xaxis.set_major_locator(mdates.MonthLocator(interval=1))
252        ax4.xaxis.set_major_formatter(mdates.DateFormatter("%m"))
253        plt.setp(ax4.get_xticklabels(), rotation=0, ha="right")
254        ax4.set_xlabel("Month")
255
256        # レイアウトの調整と保存
257        plt.tight_layout()
258        plt.savefig(output_path, dpi=300, bbox_inches="tight")
259        plt.close()
260
261        if print_summary:
262
263            def analyze_top_values(df, column_name, top_percent=20):
264                print(f"\n{column_name}の上位{top_percent}%の分析:")
265
266                # DataFrameのコピーを作成し、日時関連の列を追加
267                df_analysis = df.copy()
268                df_analysis["hour"] = pd.to_datetime(df_analysis[col_datetime]).dt.hour
269                df_analysis["month"] = pd.to_datetime(
270                    df_analysis[col_datetime]
271                ).dt.month
272                df_analysis["weekday"] = pd.to_datetime(
273                    df_analysis[col_datetime]
274                ).dt.dayofweek
275
276                # 上位20%のしきい値を計算
277                threshold = df[column_name].quantile(1 - top_percent / 100)
278                high_values = df_analysis[df_analysis[column_name] > threshold]
279
280                # 月ごとの分析
281                print("\n月別分布:")
282                monthly_counts = high_values.groupby("month").size()
283                total_counts = df_analysis.groupby("month").size()
284                monthly_percentages = (monthly_counts / total_counts * 100).round(1)
285
286                # 月ごとのデータを安全に表示
287                available_months = set(monthly_counts.index) & set(total_counts.index)
288                for month in sorted(available_months):
289                    print(
290                        f"月{month}: {monthly_percentages[month]}% ({monthly_counts[month]}件/{total_counts[month]}件)"
291                    )
292
293                # 時間帯ごとの分析(3時間区切り)
294                print("\n時間帯別分布:")
295                # copyを作成して新しい列を追加
296                high_values = high_values.copy()
297                high_values["time_block"] = high_values["hour"] // 3 * 3
298                time_blocks = high_values.groupby("time_block").size()
299                total_time_blocks = df_analysis.groupby(
300                    df_analysis["hour"] // 3 * 3
301                ).size()
302                time_percentages = (time_blocks / total_time_blocks * 100).round(1)
303
304                # 時間帯ごとのデータを安全に表示
305                available_blocks = set(time_blocks.index) & set(total_time_blocks.index)
306                for block in sorted(available_blocks):
307                    print(
308                        f"{block:02d}:00-{block + 3:02d}:00: {time_percentages[block]}% ({time_blocks[block]}件/{total_time_blocks[block]}件)"
309                    )
310
311                # 曜日ごとの分析
312                print("\n曜日別分布:")
313                weekday_names = ["月曜", "火曜", "水曜", "木曜", "金曜", "土曜", "日曜"]
314                weekday_counts = high_values.groupby("weekday").size()
315                total_weekdays = df_analysis.groupby("weekday").size()
316                weekday_percentages = (weekday_counts / total_weekdays * 100).round(1)
317
318                # 曜日ごとのデータを安全に表示
319                available_days = set(weekday_counts.index) & set(total_weekdays.index)
320                for day in sorted(available_days):
321                    if 0 <= day <= 6:  # 有効な曜日インデックスのチェック
322                        print(
323                            f"{weekday_names[day]}: {weekday_percentages[day]}% ({weekday_counts[day]}件/{total_weekdays[day]}件)"
324                        )
325
326            # 濃度とフラックスそれぞれの分析を実行
327            print("\n=== 上位値の時間帯・曜日分析 ===")
328            analyze_top_values(df, col_ch4_conc)
329            analyze_top_values(df, col_ch4_flux)
330            analyze_top_values(df, col_c2h6_conc)
331            analyze_top_values(df, col_c2h6_flux)

CH4とC2H6の濃度とフラックスの時系列プロットを作成する

Parameters

df : pd.DataFrame
    月別データを含むDataFrame
output_dir : str
    出力ディレクトリのパス
output_filename : str
    出力ファイル名
col_datetime : str
    日付列の名前
col_ch4_conc : str
    CH4濃度列の名前
col_ch4_flux : str
    CH4フラックス列の名前
col_c2h6_conc : str
    C2H6濃度列の名前
col_c2h6_flux : str
    C2H6フラックス列の名前
print_summary : bool
    解析情報をprintするかどうか
def plot_c1c2_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:
333    def plot_c1c2_timeseries(
334        self,
335        df: pd.DataFrame,
336        output_dir: str,
337        col_ch4_flux: str,
338        col_c2h6_flux: str,
339        output_filename: str = "timeseries_year.png",
340        col_datetime: str = "Date",
341        window_size: int = 24 * 7,  # 1週間の移動平均のデフォルト値
342        confidence_interval: float = 0.95,  # 95%信頼区間
343        subplot_label_ch4: str | None = "(a)",
344        subplot_label_c2h6: str | None = "(b)",
345        subplot_fontsize: int = 20,
346        show_ci: bool = True,
347        ch4_ylim: tuple[float, float] | None = None,
348        c2h6_ylim: tuple[float, float] | None = None,
349        start_date: str | None = None,  # 追加:"YYYY-MM-DD"形式
350        end_date: str | None = None,  # 追加:"YYYY-MM-DD"形式
351        figsize: tuple[float, float] = (16, 6),
352    ) -> None:
353        """CH4とC2H6フラックスの時系列変動をプロット
354
355        Parameters
356        ------
357            df : pd.DataFrame
358                データフレーム
359            output_dir : str
360                出力ディレクトリのパス
361            col_ch4_flux : str
362                CH4フラックスのカラム名
363            col_c2h6_flux : str
364                C2H6フラックスのカラム名
365            output_filename : str
366                出力ファイル名
367            col_datetime : str
368                日時カラムの名前
369            window_size : int
370                移動平均の窓サイズ
371            confidence_interval : float
372                信頼区間(0-1)
373            subplot_label_ch4 : str | None
374                CH4プロットのラベル
375            subplot_label_c2h6 : str | None
376                C2H6プロットのラベル
377            subplot_fontsize : int
378                サブプロットのフォントサイズ
379            show_ci : bool
380                信頼区間を表示するか
381            ch4_ylim : tuple[float, float] | None
382                CH4のy軸範囲
383            c2h6_ylim : tuple[float, float] | None
384                C2H6のy軸範囲
385            start_date : str | None
386                開始日(YYYY-MM-DD形式)
387            end_date : str | None
388                終了日(YYYY-MM-DD形式)
389            figsize : tuple[float, float]
390                図のサイズ。デフォルトは(16, 6)。
391        """
392        # 出力ディレクトリの作成
393        os.makedirs(output_dir, exist_ok=True)
394        output_path: str = os.path.join(output_dir, output_filename)
395
396        # データの準備
397        df_copied = df.copy()
398        if not isinstance(df_copied.index, pd.DatetimeIndex):
399            df_copied[col_datetime] = pd.to_datetime(df_copied[col_datetime])
400            df_copied.set_index(col_datetime, inplace=True)
401
402        # 日付範囲の処理
403        if start_date is not None:
404            start_dt = pd.to_datetime(start_date).normalize()  # 時刻を00:00:00に設定
405            df_min_date = (
406                df_copied.index.normalize().min().normalize()
407            )  # 日付のみの比較のため正規化
408
409            # データの最小日付が指定開始日より後の場合にのみ警告
410            if df_min_date.date() > start_dt.date():
411                self.logger.warning(
412                    f"指定された開始日{start_date}がデータの開始日{df_min_date.strftime('%Y-%m-%d')}より前です。"
413                    f"データの開始日を使用します。"
414                )
415                start_dt = df_min_date
416        else:
417            start_dt = df_copied.index.normalize().min()
418
419        if end_date is not None:
420            end_dt = (
421                pd.to_datetime(end_date).normalize()
422                + pd.Timedelta(days=1)
423                - pd.Timedelta(seconds=1)
424            )
425            df_max_date = (
426                df_copied.index.normalize().max().normalize()
427            )  # 日付のみの比較のため正規化
428
429            # データの最大日付が指定終了日より前の場合にのみ警告
430            if df_max_date.date() < pd.to_datetime(end_date).date():
431                self.logger.warning(
432                    f"指定された終了日{end_date}がデータの終了日{df_max_date.strftime('%Y-%m-%d')}より後です。"
433                    f"データの終了日を使用します。"
434                )
435                end_dt = df_copied.index.max()
436        else:
437            end_dt = df_copied.index.max()
438
439        # 指定された期間のデータを抽出
440        mask = (df_copied.index >= start_dt) & (df_copied.index <= end_dt)
441        df_copied = df_copied[mask]
442
443        # CH4とC2H6の移動平均と信頼区間を計算
444        ch4_mean, ch4_lower, ch4_upper = calculate_rolling_stats(
445            df_copied[col_ch4_flux], window_size, confidence_interval
446        )
447        c2h6_mean, c2h6_lower, c2h6_upper = calculate_rolling_stats(
448            df_copied[col_c2h6_flux], window_size, confidence_interval
449        )
450
451        # プロットの作成
452        fig, (ax1, ax2) = plt.subplots(1, 2, figsize=figsize)
453
454        # CH4プロット
455        ax1.plot(df_copied.index, ch4_mean, "red", label="CH$_4$")
456        if show_ci:
457            ax1.fill_between(df_copied.index, ch4_lower, ch4_upper, color="red", alpha=0.2)
458        if subplot_label_ch4:
459            ax1.text(
460                0.02,
461                0.98,
462                subplot_label_ch4,
463                transform=ax1.transAxes,
464                va="top",
465                fontsize=subplot_fontsize,
466            )
467        ax1.set_ylabel("CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)")
468        if ch4_ylim is not None:
469            ax1.set_ylim(ch4_ylim)
470        ax1.grid(True, alpha=0.3)
471
472        # C2H6プロット
473        ax2.plot(df_copied.index, c2h6_mean, "orange", label="C$_2$H$_6$")
474        if show_ci:
475            ax2.fill_between(
476                df_copied.index, c2h6_lower, c2h6_upper, color="orange", alpha=0.2
477            )
478        if subplot_label_c2h6:
479            ax2.text(
480                0.02,
481                0.98,
482                subplot_label_c2h6,
483                transform=ax2.transAxes,
484                va="top",
485                fontsize=subplot_fontsize,
486            )
487        ax2.set_ylabel("C$_2$H$_6$ flux (nmol m$^{-2}$ s$^{-1}$)")
488        if c2h6_ylim is not None:
489            ax2.set_ylim(c2h6_ylim)
490        ax2.grid(True, alpha=0.3)
491
492        # x軸の設定
493        for ax in [ax1, ax2]:
494            ax.set_xlabel("Month")
495            # x軸の範囲を設定
496            ax.set_xlim(start_dt, end_dt)
497
498            # 1ヶ月ごとの主目盛り
499            ax.xaxis.set_major_locator(mdates.MonthLocator(interval=1))
500
501            # カスタムフォーマッタの作成(数字を通常フォントで表示)
502            def date_formatter(x, p):
503                date = mdates.num2date(x)
504                return f"{date.strftime('%m')}"
505
506            ax.xaxis.set_major_formatter(plt.FuncFormatter(date_formatter))
507
508            # 補助目盛りの設定
509            ax.xaxis.set_minor_locator(mdates.MonthLocator())
510            # ティックラベルの回転と位置調整
511            plt.setp(ax.xaxis.get_majorticklabels(), ha="right")
512
513        plt.tight_layout()
514        plt.savefig(output_path, dpi=300, bbox_inches="tight")
515        plt.close(fig)

CH4とC2H6フラックスの時系列変動をプロット

Parameters

df : pd.DataFrame
    データフレーム
output_dir : str
    出力ディレクトリのパス
col_ch4_flux : str
    CH4フラックスのカラム名
col_c2h6_flux : str
    C2H6フラックスのカラム名
output_filename : str
    出力ファイル名
col_datetime : str
    日時カラムの名前
window_size : int
    移動平均の窓サイズ
confidence_interval : float
    信頼区間(0-1)
subplot_label_ch4 : str | None
    CH4プロットのラベル
subplot_label_c2h6 : str | None
    C2H6プロットのラベル
subplot_fontsize : int
    サブプロットのフォントサイズ
show_ci : bool
    信頼区間を表示するか
ch4_ylim : tuple[float, float] | None
    CH4のy軸範囲
c2h6_ylim : tuple[float, float] | None
    C2H6のy軸範囲
start_date : str | None
    開始日(YYYY-MM-DD形式)
end_date : str | None
    終了日(YYYY-MM-DD形式)
figsize : tuple[float, float]
    図のサイズ。デフォルトは(16, 6)。
def plot_fluxes_comparison( self, df: pandas.core.frame.DataFrame, output_dir: str, cols_flux: list[str], labels: list[str], colors: list[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, include_end_date: bool = True, figsize: tuple[float, float] = (12, 6), legend_loc: str = 'upper right', apply_ma: bool = True, hourly_mean: bool = False, x_interval: Literal['month', '10days'] = 'month', xlabel: str = 'Month', ylabel: str = 'CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)', save_fig: bool = True, show_fig: bool = False) -> None:
517    def plot_fluxes_comparison(
518        self,
519        df: pd.DataFrame,
520        output_dir: str,
521        cols_flux: list[str],
522        labels: list[str],
523        colors: list[str],
524        output_filename: str = "ch4_flux_comparison.png",
525        col_datetime: str = "Date",
526        window_size: int = 24 * 7,  # 1週間の移動平均のデフォルト値
527        confidence_interval: float = 0.95,  # 95%信頼区間
528        subplot_label: str | None = None,
529        subplot_fontsize: int = 20,
530        show_ci: bool = True,
531        y_lim: tuple[float, float] | None = None,
532        start_date: str | None = None,
533        end_date: str | None = None,
534        include_end_date: bool = True,
535        figsize: tuple[float, float] = (12, 6),
536        legend_loc: str = "upper right",
537        apply_ma: bool = True,  # 移動平均を適用するかどうか
538        hourly_mean: bool = False,  # 1時間平均を適用するかどうか
539        x_interval: Literal["month", "10days"] = "month",  # "month" または "10days"
540        xlabel: str = "Month",
541        ylabel: str = "CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)",
542        save_fig: bool = True,
543        show_fig: bool = False,
544    ) -> None:
545        """複数のCH4フラックスの時系列比較プロット
546
547        Parameters
548        ------
549            df : pd.DataFrame
550                データフレーム
551            output_dir : str
552                出力ディレクトリのパス
553            cols_flux : list[str]
554                比較するフラックスのカラム名リスト
555            labels : list[str]
556                凡例に表示する各フラックスのラベルリスト
557            colors : list[str]
558                各フラックスの色リスト
559            output_filename : str
560                出力ファイル名
561            col_datetime : str
562                日時カラムの名前
563            window_size : int
564                移動平均の窓サイズ
565            confidence_interval : float
566                信頼区間(0-1)
567            subplot_label : str | None
568                プロットのラベル
569            subplot_fontsize : int
570                サブプロットのフォントサイズ
571            show_ci : bool
572                信頼区間を表示するか
573            y_lim : tuple[float, float] | None
574                y軸の範囲
575            start_date : str | None
576                開始日(YYYY-MM-DD形式)
577            end_date : str | None
578                終了日(YYYY-MM-DD形式)
579            include_end_date : bool
580                終了日を含めるかどうか。Falseの場合、終了日の前日までを表示
581            figsize : tuple[float, float]
582                図のサイズ
583            legend_loc : str
584                凡例の位置
585            apply_ma : bool
586                移動平均を適用するかどうか
587            hourly_mean : bool
588                1時間平均を適用するかどうか
589            x_interval : Literal['month', '10days']
590                x軸の目盛り間隔。"month"(月初めのみ)または"10days"(10日刻み)
591            xlabel : str
592                x軸のラベル(通常は"Month")
593            ylabel : str
594                y軸のラベル(通常は"CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)")
595            save_fig : bool
596                図を保存するかどうか
597            show_fig : bool
598                図を表示するかどうか
599        """
600        # 出力ディレクトリの作成
601        os.makedirs(output_dir, exist_ok=True)
602        output_path: str = os.path.join(output_dir, output_filename)
603
604        # データの準備
605        df = df.copy()
606        if not isinstance(df.index, pd.DatetimeIndex):
607            df[col_datetime] = pd.to_datetime(df[col_datetime])
608            df.set_index(col_datetime, inplace=True)
609
610        # 1時間平均の適用
611        if hourly_mean:
612            # 時間情報のみを使用してグループ化
613            df = df.groupby([df.index.date, df.index.hour]).mean()
614            # マルチインデックスを日時インデックスに変換
615            df.index = pd.to_datetime(
616                [f"{date} {hour:02d}:00:00" for date, hour in df.index]
617            )
618
619        # 日付範囲の処理
620        if start_date is not None:
621            start_dt = pd.to_datetime(start_date).normalize()  # 時刻を00:00:00に設定
622            df_min_date = (
623                df.index.normalize().min().normalize()
624            )  # 日付のみの比較のため正規化
625
626            # データの最小日付が指定開始日より後の場合にのみ警告
627            if df_min_date.date() > start_dt.date():
628                self.logger.warning(
629                    f"指定された開始日{start_date}がデータの開始日{df_min_date.strftime('%Y-%m-%d')}より前です。"
630                    f"データの開始日を使用します。"
631                )
632                start_dt = df_min_date
633        else:
634            start_dt = df.index.normalize().min()
635
636        if end_date is not None:
637            if include_end_date:
638                end_dt = (
639                    pd.to_datetime(end_date).normalize()
640                    + pd.Timedelta(days=1)
641                    - pd.Timedelta(seconds=1)
642                )
643            else:
644                # 終了日を含まない場合、終了日の前日の23:59:59まで
645                end_dt = pd.to_datetime(end_date).normalize() - pd.Timedelta(seconds=1)
646
647            df_max_date = (
648                df.index.normalize().max().normalize()
649            )  # 日付のみの比較のため正規化
650
651            # データの最大日付が指定終了日より前の場合にのみ警告
652            compare_date = pd.to_datetime(end_date).date()
653            if not include_end_date:
654                compare_date = compare_date - pd.Timedelta(days=1)
655
656            if df_max_date.date() < compare_date:
657                self.logger.warning(
658                    f"指定された終了日{end_date}がデータの終了日{df_max_date.strftime('%Y-%m-%d')}より後です。"
659                    f"データの終了日を使用します。"
660                )
661                end_dt = df.index.max()
662        else:
663            end_dt = df.index.max()
664
665        # 指定された期間のデータを抽出
666        mask = (df.index >= start_dt) & (df.index <= end_dt)
667        df = df[mask]
668
669        # プロットの作成
670        fig, ax = plt.subplots(figsize=figsize)
671
672        # 各フラックスのプロット
673        for flux_col, label, color in zip(cols_flux, labels, colors):
674            if apply_ma:
675                # 移動平均の計算
676                mean, lower, upper = calculate_rolling_stats(
677                    df[flux_col], window_size, confidence_interval
678                )
679                ax.plot(df.index, mean, color, label=label, alpha=0.7)
680                if show_ci:
681                    ax.fill_between(df.index, lower, upper, color=color, alpha=0.2)
682            else:
683                # 生データのプロット
684                ax.plot(df.index, df[flux_col], color, label=label, alpha=0.7)
685
686        # プロットの設定
687        if subplot_label:
688            ax.text(
689                0.02,
690                0.98,
691                subplot_label,
692                transform=ax.transAxes,
693                va="top",
694                fontsize=subplot_fontsize,
695            )
696
697        ax.set_xlabel(xlabel)
698        ax.set_ylabel(ylabel)
699
700        if y_lim is not None:
701            ax.set_ylim(y_lim)
702
703        ax.grid(True, alpha=0.3)
704        ax.legend(loc=legend_loc)
705
706        # x軸の設定
707        ax.set_xlim(start_dt, end_dt)
708
709        if x_interval == "month":
710            # 月初めにメジャー線のみ表示
711            ax.xaxis.set_major_locator(mdates.MonthLocator())
712            ax.xaxis.set_minor_locator(plt.NullLocator())  # マイナー線を非表示
713        elif x_interval == "10days":
714            # 10日刻みでメジャー線、日毎にマイナー線を表示
715            ax.xaxis.set_major_locator(mdates.DayLocator(bymonthday=[1, 11, 21]))
716            ax.xaxis.set_minor_locator(mdates.DayLocator())
717            ax.grid(True, which="minor", alpha=0.1)  # マイナー線の表示設定
718
719        # カスタムフォーマッタの作成(月初めの1日のみMMを表示)
720        def date_formatter(x, p):
721            date = mdates.num2date(x)
722            # 月初めの1日の場合のみ月を表示
723            if date.day == 1:
724                return f"{date.strftime('%m')}"
725            return ""
726
727        ax.xaxis.set_major_formatter(plt.FuncFormatter(date_formatter))
728        plt.setp(ax.xaxis.get_majorticklabels(), ha="right", rotation=0)
729
730        plt.tight_layout()
731
732        if save_fig:
733            plt.savefig(output_path, dpi=300, bbox_inches="tight")
734        if show_fig:
735            plt.show()
736        plt.close(fig)

複数のCH4フラックスの時系列比較プロット

Parameters

df : pd.DataFrame
    データフレーム
output_dir : str
    出力ディレクトリのパス
cols_flux : list[str]
    比較するフラックスのカラム名リスト
labels : list[str]
    凡例に表示する各フラックスのラベルリスト
colors : list[str]
    各フラックスの色リスト
output_filename : str
    出力ファイル名
col_datetime : str
    日時カラムの名前
window_size : int
    移動平均の窓サイズ
confidence_interval : float
    信頼区間(0-1)
subplot_label : str | None
    プロットのラベル
subplot_fontsize : int
    サブプロットのフォントサイズ
show_ci : bool
    信頼区間を表示するか
y_lim : tuple[float, float] | None
    y軸の範囲
start_date : str | None
    開始日(YYYY-MM-DD形式)
end_date : str | None
    終了日(YYYY-MM-DD形式)
include_end_date : bool
    終了日を含めるかどうか。Falseの場合、終了日の前日までを表示
figsize : tuple[float, float]
    図のサイズ
legend_loc : str
    凡例の位置
apply_ma : bool
    移動平均を適用するかどうか
hourly_mean : bool
    1時間平均を適用するかどうか
x_interval : Literal['month', '10days']
    x軸の目盛り間隔。"month"(月初めのみ)または"10days"(10日刻み)
xlabel : str
    x軸のラベル(通常は"Month")
ylabel : str
    y軸のラベル(通常は"CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)")
save_fig : bool
    図を保存するかどうか
show_fig : bool
    図を表示するかどうか
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:
738    def plot_c1c2_fluxes_diurnal_patterns(
739        self,
740        df: pd.DataFrame,
741        y_cols_ch4: list[str],
742        y_cols_c2h6: list[str],
743        labels_ch4: list[str],
744        labels_c2h6: list[str],
745        colors_ch4: list[str],
746        colors_c2h6: list[str],
747        output_dir: str,
748        output_filename: str = "diurnal.png",
749        legend_only_ch4: bool = False,
750        add_label: bool = True,
751        add_legend: bool = True,
752        show_std: bool = False,  # 標準偏差表示のオプションを追加
753        std_alpha: float = 0.2,  # 標準偏差の透明度
754        subplot_fontsize: int = 20,
755        subplot_label_ch4: str | None = "(a)",
756        subplot_label_c2h6: str | None = "(b)",
757        ax1_ylim: tuple[float, float] | None = None,
758        ax2_ylim: tuple[float, float] | None = None,
759    ) -> None:
760        """CH4とC2H6の日変化パターンを1つの図に並べてプロットする
761
762        Parameters
763        ------
764            df : pd.DataFrame
765                入力データフレーム。
766            y_cols_ch4 : list[str]
767                CH4のプロットに使用するカラム名のリスト。
768            y_cols_c2h6 : list[str]
769                C2H6のプロットに使用するカラム名のリスト。
770            labels_ch4 : list[str]
771                CH4の各ラインに対応するラベルのリスト。
772            labels_c2h6 : list[str]
773                C2H6の各ラインに対応するラベルのリスト。
774            colors_ch4 : list[str]
775                CH4の各ラインに使用する色のリスト。
776            colors_c2h6 : list[str]
777                C2H6の各ラインに使用する色のリスト。
778            output_dir : str
779                出力先ディレクトリのパス。
780            output_filename : str, optional
781                出力ファイル名。デフォルトは"diurnal.png"。
782            legend_only_ch4 : bool, optional
783                CH4の凡例のみを表示するかどうか。デフォルトはFalse。
784            add_label : bool, optional
785                サブプロットラベルを表示するかどうか。デフォルトはTrue。
786            add_legend : bool, optional
787                凡例を表示するかどうか。デフォルトはTrue。
788            show_std : bool, optional
789                標準偏差を表示するかどうか。デフォルトはFalse。
790            std_alpha : float, optional
791                標準偏差の透明度。デフォルトは0.2。
792            subplot_fontsize : int, optional
793                サブプロットのフォントサイズ。デフォルトは20。
794            subplot_label_ch4 : str | None, optional
795                CH4プロットのラベル。デフォルトは"(a)"。
796            subplot_label_c2h6 : str | None, optional
797                C2H6プロットのラベル。デフォルトは"(b)"。
798            ax1_ylim : tuple[float, float] | None, optional
799                CH4プロットのy軸の範囲。デフォルトはNone。
800            ax2_ylim : tuple[float, float] | None, optional
801                C2H6プロットのy軸の範囲。デフォルトはNone。
802        """
803        os.makedirs(output_dir, exist_ok=True)
804        output_path: str = os.path.join(output_dir, output_filename)
805
806        # データの準備
807        target_columns = y_cols_ch4 + y_cols_c2h6
808        hourly_means, time_points = self._prepare_diurnal_data(df, target_columns)
809
810        # 標準偏差の計算を追加
811        hourly_stds = {}
812        if show_std:
813            hourly_stds = df.groupby(df.index.hour)[target_columns].std()
814            # 24時間目のデータ点を追加
815            last_hour = hourly_stds.iloc[0:1].copy()
816            last_hour.index = [24]
817            hourly_stds = pd.concat([hourly_stds, last_hour])
818
819        # プロットの作成
820        fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
821
822        # CH4のプロット (左側)
823        ch4_lines = []
824        for y_col, label, color in zip(y_cols_ch4, labels_ch4, colors_ch4):
825            mean_values = hourly_means["all"][y_col]
826            line = ax1.plot(
827                time_points,
828                mean_values,
829                "-o",
830                label=label,
831                color=color,
832            )
833            ch4_lines.extend(line)
834
835            # 標準偏差の表示
836            if show_std:
837                std_values = hourly_stds[y_col]
838                ax1.fill_between(
839                    time_points,
840                    mean_values - std_values,
841                    mean_values + std_values,
842                    color=color,
843                    alpha=std_alpha,
844                )
845
846        # C2H6のプロット (右側)
847        c2h6_lines = []
848        for y_col, label, color in zip(y_cols_c2h6, labels_c2h6, colors_c2h6):
849            mean_values = hourly_means["all"][y_col]
850            line = ax2.plot(
851                time_points,
852                mean_values,
853                "o-",
854                label=label,
855                color=color,
856            )
857            c2h6_lines.extend(line)
858
859            # 標準偏差の表示
860            if show_std:
861                std_values = hourly_stds[y_col]
862                ax2.fill_between(
863                    time_points,
864                    mean_values - std_values,
865                    mean_values + std_values,
866                    color=color,
867                    alpha=std_alpha,
868                )
869
870        # 軸の設定
871        for ax, ylabel, subplot_label in [
872            (ax1, r"CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)", subplot_label_ch4),
873            (ax2, r"C$_2$H$_6$ flux (nmol m$^{-2}$ s$^{-1}$)", subplot_label_c2h6),
874        ]:
875            self._setup_diurnal_axes(
876                ax=ax,
877                time_points=time_points,
878                ylabel=ylabel,
879                subplot_label=subplot_label,
880                add_label=add_label,
881                add_legend=False,  # 個別の凡例は表示しない
882                subplot_fontsize=subplot_fontsize,
883            )
884
885        if ax1_ylim is not None:
886            ax1.set_ylim(ax1_ylim)
887        ax1.yaxis.set_major_locator(MultipleLocator(20))
888        ax1.yaxis.set_major_formatter(FuncFormatter(lambda x, p: f"{x:.0f}"))
889
890        if ax2_ylim is not None:
891            ax2.set_ylim(ax2_ylim)
892        ax2.yaxis.set_major_locator(MultipleLocator(1))
893        ax2.yaxis.set_major_formatter(FuncFormatter(lambda x, p: f"{x:.1f}"))
894
895        plt.tight_layout()
896
897        # 共通の凡例
898        if add_legend:
899            all_lines = ch4_lines
900            all_labels = [line.get_label() for line in ch4_lines]
901            if not legend_only_ch4:
902                all_lines += c2h6_lines
903                all_labels += [line.get_label() for line in c2h6_lines]
904            fig.legend(
905                all_lines,
906                all_labels,
907                loc="center",
908                bbox_to_anchor=(0.5, 0.02),
909                ncol=len(all_lines),
910            )
911            plt.subplots_adjust(bottom=0.25)  # 下部に凡例用のスペースを確保
912
913        fig.savefig(output_path, dpi=300, bbox_inches="tight")
914        plt.close(fig)

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

Parameters

df : pd.DataFrame
    入力データフレーム。
y_cols_ch4 : list[str]
    CH4のプロットに使用するカラム名のリスト。
y_cols_c2h6 : list[str]
    C2H6のプロットに使用するカラム名のリスト。
labels_ch4 : list[str]
    CH4の各ラインに対応するラベルのリスト。
labels_c2h6 : list[str]
    C2H6の各ラインに対応するラベルのリスト。
colors_ch4 : list[str]
    CH4の各ラインに使用する色のリスト。
colors_c2h6 : list[str]
    C2H6の各ラインに使用する色のリスト。
output_dir : str
    出力先ディレクトリのパス。
output_filename : str, optional
    出力ファイル名。デフォルトは"diurnal.png"。
legend_only_ch4 : bool, optional
    CH4の凡例のみを表示するかどうか。デフォルトはFalse。
add_label : bool, optional
    サブプロットラベルを表示するかどうか。デフォルトはTrue。
add_legend : bool, optional
    凡例を表示するかどうか。デフォルトはTrue。
show_std : bool, optional
    標準偏差を表示するかどうか。デフォルトはFalse。
std_alpha : float, optional
    標準偏差の透明度。デフォルトは0.2。
subplot_fontsize : int, optional
    サブプロットのフォントサイズ。デフォルトは20。
subplot_label_ch4 : str | None, optional
    CH4プロットのラベル。デフォルトは"(a)"。
subplot_label_c2h6 : str | None, optional
    C2H6プロットのラベル。デフォルトは"(b)"。
ax1_ylim : tuple[float, float] | None, optional
    CH4プロットのy軸の範囲。デフォルトはNone。
ax2_ylim : tuple[float, float] | None, optional
    C2H6プロットのy軸の範囲。デフォルトはNone。
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:
 916    def plot_c1c2_fluxes_diurnal_patterns_by_date(
 917        self,
 918        df: pd.DataFrame,
 919        y_col_ch4: str,
 920        y_col_c2h6: str,
 921        output_dir: str,
 922        output_filename: str = "diurnal_by_date.png",
 923        plot_all: bool = True,
 924        plot_weekday: bool = True,
 925        plot_weekend: bool = True,
 926        plot_holiday: bool = True,
 927        add_label: bool = True,
 928        add_legend: bool = True,
 929        show_std: bool = False,  # 標準偏差表示のオプションを追加
 930        std_alpha: float = 0.2,  # 標準偏差の透明度
 931        legend_only_ch4: bool = False,
 932        subplot_fontsize: int = 20,
 933        subplot_label_ch4: str | None = "(a)",
 934        subplot_label_c2h6: str | None = "(b)",
 935        ax1_ylim: tuple[float, float] | None = None,
 936        ax2_ylim: tuple[float, float] | None = None,
 937        print_summary: bool = True,  # 追加: 統計情報を表示するかどうか
 938    ) -> None:
 939        """CH4とC2H6の日変化パターンを日付分類して1つの図に並べてプロットする
 940
 941        Parameters
 942        ------
 943            df : pd.DataFrame
 944                入力データフレーム。
 945            y_col_ch4 : str
 946                CH4フラックスを含むカラム名。
 947            y_col_c2h6 : str
 948                C2H6フラックスを含むカラム名。
 949            output_dir : str
 950                出力先ディレクトリのパス。
 951            output_filename : str, optional
 952                出力ファイル名。デフォルトは"diurnal_by_date.png"。
 953            plot_all : bool, optional
 954                すべての日をプロットするかどうか。デフォルトはTrue。
 955            plot_weekday : bool, optional
 956                平日をプロットするかどうか。デフォルトはTrue。
 957            plot_weekend : bool, optional
 958                週末をプロットするかどうか。デフォルトはTrue。
 959            plot_holiday : bool, optional
 960                祝日をプロットするかどうか。デフォルトはTrue。
 961            add_label : bool, optional
 962                サブプロットラベルを表示するかどうか。デフォルトはTrue。
 963            add_legend : bool, optional
 964                凡例を表示するかどうか。デフォルトはTrue。
 965            show_std : bool, optional
 966                標準偏差を表示するかどうか。デフォルトはFalse。
 967            std_alpha : float, optional
 968                標準偏差の透明度。デフォルトは0.2。
 969            legend_only_ch4 : bool, optional
 970                CH4の凡例のみを表示するかどうか。デフォルトはFalse。
 971            subplot_fontsize : int, optional
 972                サブプロットのフォントサイズ。デフォルトは20。
 973            subplot_label_ch4 : str | None, optional
 974                CH4プロットのラベル。デフォルトは"(a)"。
 975            subplot_label_c2h6 : str | None, optional
 976                C2H6プロットのラベル。デフォルトは"(b)"。
 977            ax1_ylim : tuple[float, float] | None, optional
 978                CH4プロットのy軸の範囲。デフォルトはNone。
 979            ax2_ylim : tuple[float, float] | None, optional
 980                C2H6プロットのy軸の範囲。デフォルトはNone。
 981            print_summary : bool, optional
 982                統計情報を表示するかどうか。デフォルトはTrue。
 983        """
 984        os.makedirs(output_dir, exist_ok=True)
 985        output_path: str = os.path.join(output_dir, output_filename)
 986
 987        # データの準備
 988        target_columns = [y_col_ch4, y_col_c2h6]
 989        hourly_means, time_points = self._prepare_diurnal_data(
 990            df, target_columns, include_date_types=True
 991        )
 992
 993        # 標準偏差の計算を追加
 994        hourly_stds = {}
 995        if show_std:
 996            for condition in ["all", "weekday", "weekend", "holiday"]:
 997                if condition == "all":
 998                    condition_data = df
 999                elif condition == "weekday":
1000                    condition_data = df[
1001                        ~(
1002                            df.index.dayofweek.isin([5, 6])
1003                            | df.index.map(lambda x: jpholiday.is_holiday(x.date()))
1004                        )
1005                    ]
1006                elif condition == "weekend":
1007                    condition_data = df[df.index.dayofweek.isin([5, 6])]
1008                else:  # holiday
1009                    condition_data = df[
1010                        df.index.map(lambda x: jpholiday.is_holiday(x.date()))
1011                    ]
1012
1013                hourly_stds[condition] = condition_data.groupby(
1014                    condition_data.index.hour
1015                )[target_columns].std()
1016                # 24時間目のデータ点を追加
1017                last_hour = hourly_stds[condition].iloc[0:1].copy()
1018                last_hour.index = [24]
1019                hourly_stds[condition] = pd.concat([hourly_stds[condition], last_hour])
1020
1021        # プロットスタイルの設定
1022        styles = {
1023            "all": {
1024                "color": "black",
1025                "linestyle": "-",
1026                "alpha": 1.0,
1027                "label": "All days",
1028            },
1029            "weekday": {
1030                "color": "blue",
1031                "linestyle": "-",
1032                "alpha": 0.8,
1033                "label": "Weekdays",
1034            },
1035            "weekend": {
1036                "color": "red",
1037                "linestyle": "-",
1038                "alpha": 0.8,
1039                "label": "Weekends",
1040            },
1041            "holiday": {
1042                "color": "green",
1043                "linestyle": "-",
1044                "alpha": 0.8,
1045                "label": "Weekends & Holidays",
1046            },
1047        }
1048
1049        # プロット対象の条件を選択
1050        plot_conditions = {
1051            "all": plot_all,
1052            "weekday": plot_weekday,
1053            "weekend": plot_weekend,
1054            "holiday": plot_holiday,
1055        }
1056        selected_conditions = {
1057            col: means
1058            for col, means in hourly_means.items()
1059            if col in plot_conditions and plot_conditions[col]
1060        }
1061
1062        # プロットの作成
1063        fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
1064
1065        # CH4とC2H6のプロット用のラインオブジェクトを保存
1066        ch4_lines = []
1067        c2h6_lines = []
1068
1069        # CH4とC2H6のプロット
1070        for condition, means in selected_conditions.items():
1071            style = styles[condition].copy()
1072
1073            # CH4プロット
1074            mean_values_ch4 = means[y_col_ch4]
1075            line_ch4 = ax1.plot(time_points, mean_values_ch4, marker="o", **style)
1076            ch4_lines.extend(line_ch4)
1077
1078            if show_std and condition in hourly_stds:
1079                std_values = hourly_stds[condition][y_col_ch4]
1080                ax1.fill_between(
1081                    time_points,
1082                    mean_values_ch4 - std_values,
1083                    mean_values_ch4 + std_values,
1084                    color=style["color"],
1085                    alpha=std_alpha,
1086                )
1087
1088            # C2H6プロット
1089            style["linestyle"] = "--"
1090            mean_values_c2h6 = means[y_col_c2h6]
1091            line_c2h6 = ax2.plot(time_points, mean_values_c2h6, marker="o", **style)
1092            c2h6_lines.extend(line_c2h6)
1093
1094            if show_std and condition in hourly_stds:
1095                std_values = hourly_stds[condition][y_col_c2h6]
1096                ax2.fill_between(
1097                    time_points,
1098                    mean_values_c2h6 - std_values,
1099                    mean_values_c2h6 + std_values,
1100                    color=style["color"],
1101                    alpha=std_alpha,
1102                )
1103
1104        # 軸の設定
1105        for ax, ylabel, subplot_label in [
1106            (ax1, r"CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)", subplot_label_ch4),
1107            (ax2, r"C$_2$H$_6$ flux (nmol m$^{-2}$ s$^{-1}$)", subplot_label_c2h6),
1108        ]:
1109            self._setup_diurnal_axes(
1110                ax=ax,
1111                time_points=time_points,
1112                ylabel=ylabel,
1113                subplot_label=subplot_label,
1114                add_label=add_label,
1115                add_legend=False,
1116                subplot_fontsize=subplot_fontsize,
1117            )
1118
1119        if ax1_ylim is not None:
1120            ax1.set_ylim(ax1_ylim)
1121        ax1.yaxis.set_major_locator(MultipleLocator(20))
1122        ax1.yaxis.set_major_formatter(FuncFormatter(lambda x, p: f"{x:.0f}"))
1123
1124        if ax2_ylim is not None:
1125            ax2.set_ylim(ax2_ylim)
1126        ax2.yaxis.set_major_locator(MultipleLocator(1))
1127        ax2.yaxis.set_major_formatter(FuncFormatter(lambda x, p: f"{x:.1f}"))
1128
1129        plt.tight_layout()
1130
1131        # 共通の凡例を図の下部に配置
1132        if add_legend:
1133            lines_to_show = (
1134                ch4_lines if legend_only_ch4 else ch4_lines[: len(selected_conditions)]
1135            )
1136            fig.legend(
1137                lines_to_show,
1138                [
1139                    style["label"]
1140                    for style in list(styles.values())[: len(lines_to_show)]
1141                ],
1142                loc="center",
1143                bbox_to_anchor=(0.5, 0.02),
1144                ncol=len(lines_to_show),
1145            )
1146            plt.subplots_adjust(bottom=0.25)  # 下部に凡例用のスペースを確保
1147
1148        fig.savefig(output_path, dpi=300, bbox_inches="tight")
1149        plt.close(fig)
1150
1151        # 日変化パターンの統計分析を追加
1152        if print_summary:
1153            # 平日と休日のデータを準備
1154            dates = pd.to_datetime(df.index)
1155            is_weekend = dates.dayofweek.isin([5, 6])
1156            is_holiday = dates.map(lambda x: jpholiday.is_holiday(x.date()))
1157            is_weekday = ~(is_weekend | is_holiday)
1158
1159            weekday_data = df[is_weekday]
1160            holiday_data = df[is_weekend | is_holiday]
1161
1162            def get_diurnal_stats(data, column):
1163                # 時間ごとの平均値を計算
1164                hourly_means = data.groupby(data.index.hour)[column].mean()
1165
1166                # 8-16時の時間帯の統計
1167                daytime_means = hourly_means[
1168                    (hourly_means.index >= 8) & (hourly_means.index <= 16)
1169                ]
1170
1171                if len(daytime_means) == 0:
1172                    return None
1173
1174                return {
1175                    "mean": daytime_means.mean(),
1176                    "max": daytime_means.max(),
1177                    "max_hour": daytime_means.idxmax(),
1178                    "min": daytime_means.min(),
1179                    "min_hour": daytime_means.idxmin(),
1180                    "hours_count": len(daytime_means),
1181                }
1182
1183            # CH4とC2H6それぞれの統計を計算
1184            for col, gas_name in [(y_col_ch4, "CH4"), (y_col_c2h6, "C2H6")]:
1185                print(f"\n=== {gas_name} フラックス 8-16時の統計分析 ===")
1186
1187                weekday_stats = get_diurnal_stats(weekday_data, col)
1188                holiday_stats = get_diurnal_stats(holiday_data, col)
1189
1190                if weekday_stats and holiday_stats:
1191                    print("\n平日:")
1192                    print(f"  平均値: {weekday_stats['mean']:.2f}")
1193                    print(
1194                        f"  最大値: {weekday_stats['max']:.2f} ({weekday_stats['max_hour']}時)"
1195                    )
1196                    print(
1197                        f"  最小値: {weekday_stats['min']:.2f} ({weekday_stats['min_hour']}時)"
1198                    )
1199                    print(f"  集計時間数: {weekday_stats['hours_count']}")
1200
1201                    print("\n休日:")
1202                    print(f"  平均値: {holiday_stats['mean']:.2f}")
1203                    print(
1204                        f"  最大値: {holiday_stats['max']:.2f} ({holiday_stats['max_hour']}時)"
1205                    )
1206                    print(
1207                        f"  最小値: {holiday_stats['min']:.2f} ({holiday_stats['min_hour']}時)"
1208                    )
1209                    print(f"  集計時間数: {holiday_stats['hours_count']}")
1210
1211                    # 平日/休日の比率を計算
1212                    print("\n平日/休日の比率:")
1213                    print(
1214                        f"  平均値比: {weekday_stats['mean'] / holiday_stats['mean']:.2f}"
1215                    )
1216                    print(
1217                        f"  最大値比: {weekday_stats['max'] / holiday_stats['max']:.2f}"
1218                    )
1219                    print(
1220                        f"  最小値比: {weekday_stats['min'] / holiday_stats['min']:.2f}"
1221                    )
1222                else:
1223                    print("十分なデータがありません")

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

Parameters

df : pd.DataFrame
    入力データフレーム。
y_col_ch4 : str
    CH4フラックスを含むカラム名。
y_col_c2h6 : str
    C2H6フラックスを含むカラム名。
output_dir : str
    出力先ディレクトリのパス。
output_filename : str, optional
    出力ファイル名。デフォルトは"diurnal_by_date.png"。
plot_all : bool, optional
    すべての日をプロットするかどうか。デフォルトはTrue。
plot_weekday : bool, optional
    平日をプロットするかどうか。デフォルトはTrue。
plot_weekend : bool, optional
    週末をプロットするかどうか。デフォルトはTrue。
plot_holiday : bool, optional
    祝日をプロットするかどうか。デフォルトはTrue。
add_label : bool, optional
    サブプロットラベルを表示するかどうか。デフォルトはTrue。
add_legend : bool, optional
    凡例を表示するかどうか。デフォルトはTrue。
show_std : bool, optional
    標準偏差を表示するかどうか。デフォルトはFalse。
std_alpha : float, optional
    標準偏差の透明度。デフォルトは0.2。
legend_only_ch4 : bool, optional
    CH4の凡例のみを表示するかどうか。デフォルトはFalse。
subplot_fontsize : int, optional
    サブプロットのフォントサイズ。デフォルトは20。
subplot_label_ch4 : str | None, optional
    CH4プロットのラベル。デフォルトは"(a)"。
subplot_label_c2h6 : str | None, optional
    C2H6プロットのラベル。デフォルトは"(b)"。
ax1_ylim : tuple[float, float] | None, optional
    CH4プロットのy軸の範囲。デフォルトはNone。
ax2_ylim : tuple[float, float] | None, optional
    C2H6プロットのy軸の範囲。デフォルトはNone。
print_summary : bool, optional
    統計情報を表示するかどうか。デフォルトはTrue。
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:
1225    def plot_diurnal_concentrations(
1226        self,
1227        df: pd.DataFrame,
1228        output_dir: str,
1229        col_ch4_conc: str = "CH4_ultra_cal",
1230        col_c2h6_conc: str = "C2H6_ultra_cal",
1231        col_datetime: str = "Date",
1232        output_filename: str = "diurnal_concentrations.png",
1233        show_std: bool = True,
1234        alpha_std: float = 0.2,
1235        add_legend: bool = True,  # 凡例表示のオプションを追加
1236        print_summary: bool = True,
1237        subplot_label_ch4: str | None = None,
1238        subplot_label_c2h6: str | None = None,
1239        subplot_fontsize: int = 24,
1240        ch4_ylim: tuple[float, float] | None = None,
1241        c2h6_ylim: tuple[float, float] | None = None,
1242        interval: str = "1H",  # "30min" または "1H" を指定
1243    ) -> None:
1244        """CH4とC2H6の濃度の日内変動を描画する
1245
1246        Parameters
1247        ------
1248            df : pd.DataFrame
1249                濃度データを含むDataFrame
1250            output_dir : str
1251                出力ディレクトリのパス
1252            col_ch4_conc : str
1253                CH4濃度のカラム名
1254            col_c2h6_conc : str
1255                C2H6濃度のカラム名
1256            col_datetime : str
1257                日時カラム名
1258            output_filename : str
1259                出力ファイル名
1260            show_std : bool
1261                標準偏差を表示するかどうか
1262            alpha_std : float
1263                標準偏差の透明度
1264            add_legend : bool
1265                凡例を追加するかどうか
1266            print_summary : bool
1267                統計情報を表示するかどうか
1268            subplot_label_ch4 : str | None
1269                CH4プロットのラベル
1270            subplot_label_c2h6 : str | None
1271                C2H6プロットのラベル
1272            subplot_fontsize : int
1273                サブプロットのフォントサイズ
1274            ch4_ylim : tuple[float, float] | None
1275                CH4のy軸範囲
1276            c2h6_ylim : tuple[float, float] | None
1277                C2H6のy軸範囲
1278            interval : str
1279                時間間隔。"30min"または"1H"を指定
1280        """
1281        # 出力ディレクトリの作成
1282        os.makedirs(output_dir, exist_ok=True)
1283        output_path: str = os.path.join(output_dir, output_filename)
1284
1285        # データの準備
1286        df = df.copy()
1287        if interval == "30min":
1288            # 30分間隔の場合、時間と30分を別々に取得
1289            df["hour"] = pd.to_datetime(df[col_datetime]).dt.hour
1290            df["minute"] = pd.to_datetime(df[col_datetime]).dt.minute
1291            df["time_bin"] = df["hour"] + df["minute"].map({0: 0, 30: 0.5})
1292        else:
1293            # 1時間間隔の場合
1294            df["time_bin"] = pd.to_datetime(df[col_datetime]).dt.hour
1295
1296        # 時間ごとの平均値と標準偏差を計算
1297        hourly_stats = df.groupby("time_bin")[[col_ch4_conc, col_c2h6_conc]].agg(
1298            ["mean", "std"]
1299        )
1300
1301        # 最後のデータポイントを追加(最初のデータを使用)
1302        last_point = hourly_stats.iloc[0:1].copy()
1303        last_point.index = [
1304            hourly_stats.index[-1] + (0.5 if interval == "30min" else 1)
1305        ]
1306        hourly_stats = pd.concat([hourly_stats, last_point])
1307
1308        # 時間軸の作成
1309        if interval == "30min":
1310            time_points = pd.date_range("2024-01-01", periods=49, freq="30min")
1311            x_ticks = [0, 6, 12, 18, 24]  # 主要な時間のティック
1312        else:
1313            time_points = pd.date_range("2024-01-01", periods=25, freq="1H")
1314            x_ticks = [0, 6, 12, 18, 24]
1315
1316        # プロットの作成
1317        fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
1318
1319        # CH4濃度プロット
1320        mean_ch4 = hourly_stats[col_ch4_conc]["mean"]
1321        if show_std:
1322            std_ch4 = hourly_stats[col_ch4_conc]["std"]
1323            ax1.fill_between(
1324                time_points,
1325                mean_ch4 - std_ch4,
1326                mean_ch4 + std_ch4,
1327                color="red",
1328                alpha=alpha_std,
1329            )
1330        ch4_line = ax1.plot(time_points, mean_ch4, "red", label="CH$_4$")[0]
1331
1332        ax1.set_ylabel("CH$_4$ (ppm)")
1333        if ch4_ylim is not None:
1334            ax1.set_ylim(ch4_ylim)
1335        if subplot_label_ch4:
1336            ax1.text(
1337                0.02,
1338                0.98,
1339                subplot_label_ch4,
1340                transform=ax1.transAxes,
1341                va="top",
1342                fontsize=subplot_fontsize,
1343            )
1344
1345        # C2H6濃度プロット
1346        mean_c2h6 = hourly_stats[col_c2h6_conc]["mean"]
1347        if show_std:
1348            std_c2h6 = hourly_stats[col_c2h6_conc]["std"]
1349            ax2.fill_between(
1350                time_points,
1351                mean_c2h6 - std_c2h6,
1352                mean_c2h6 + std_c2h6,
1353                color="orange",
1354                alpha=alpha_std,
1355            )
1356        c2h6_line = ax2.plot(time_points, mean_c2h6, "orange", label="C$_2$H$_6$")[0]
1357
1358        ax2.set_ylabel("C$_2$H$_6$ (ppb)")
1359        if c2h6_ylim is not None:
1360            ax2.set_ylim(c2h6_ylim)
1361        if subplot_label_c2h6:
1362            ax2.text(
1363                0.02,
1364                0.98,
1365                subplot_label_c2h6,
1366                transform=ax2.transAxes,
1367                va="top",
1368                fontsize=subplot_fontsize,
1369            )
1370
1371        # 両プロットの共通設定
1372        for ax in [ax1, ax2]:
1373            ax.set_xlabel("Time (hour)")
1374            ax.xaxis.set_major_formatter(mdates.DateFormatter("%-H"))
1375            ax.xaxis.set_major_locator(mdates.HourLocator(byhour=x_ticks))
1376            ax.set_xlim(time_points[0], time_points[-1])
1377            # 1時間ごとの縦線を表示
1378            ax.grid(True, which="major", alpha=0.3)
1379            # 補助目盛りは表示するが、グリッド線は表示しない
1380            # if interval == "30min":
1381            #     ax.xaxis.set_minor_locator(mdates.MinuteLocator(byminute=[30]))
1382            #     ax.tick_params(which='minor', length=4)
1383
1384        # 共通の凡例を図の下部に配置
1385        if add_legend:
1386            fig.legend(
1387                [ch4_line, c2h6_line],
1388                ["CH$_4$", "C$_2$H$_6$"],
1389                loc="center",
1390                bbox_to_anchor=(0.5, 0.02),
1391                ncol=2,
1392            )
1393        plt.subplots_adjust(bottom=0.2)
1394
1395        plt.tight_layout()
1396        plt.savefig(output_path, dpi=300, bbox_inches="tight")
1397        plt.close(fig)
1398
1399        if print_summary:
1400            # 統計情報の表示
1401            for name, col in [("CH4", col_ch4_conc), ("C2H6", col_c2h6_conc)]:
1402                stats = hourly_stats[col]
1403                mean_vals = stats["mean"]
1404
1405                print(f"\n{name}濃度の日内変動統計:")
1406                print(f"最小値: {mean_vals.min():.3f} (Hour: {mean_vals.idxmin()})")
1407                print(f"最大値: {mean_vals.max():.3f} (Hour: {mean_vals.idxmax()})")
1408                print(f"平均値: {mean_vals.mean():.3f}")
1409                print(f"日内変動幅: {mean_vals.max() - mean_vals.min():.3f}")
1410                print(f"最大/最小比: {mean_vals.max() / mean_vals.min():.3f}")

CH4とC2H6の濃度の日内変動を描画する

Parameters

df : pd.DataFrame
    濃度データを含むDataFrame
output_dir : str
    出力ディレクトリのパス
col_ch4_conc : str
    CH4濃度のカラム名
col_c2h6_conc : str
    C2H6濃度のカラム名
col_datetime : str
    日時カラム名
output_filename : str
    出力ファイル名
show_std : bool
    標準偏差を表示するかどうか
alpha_std : float
    標準偏差の透明度
add_legend : bool
    凡例を追加するかどうか
print_summary : bool
    統計情報を表示するかどうか
subplot_label_ch4 : str | None
    CH4プロットのラベル
subplot_label_c2h6 : str | None
    C2H6プロットのラベル
subplot_fontsize : int
    サブプロットのフォントサイズ
ch4_ylim : tuple[float, float] | None
    CH4のy軸範囲
c2h6_ylim : tuple[float, float] | None
    C2H6のy軸範囲
interval : str
    時間間隔。"30min"または"1H"を指定
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:
1412    def plot_flux_diurnal_patterns_with_std(
1413        self,
1414        df: pd.DataFrame,
1415        output_dir: str,
1416        col_ch4_flux: str = "Fch4",
1417        col_c2h6_flux: str = "Fc2h6",
1418        ch4_label: str = r"$\mathregular{CH_{4}}$フラックス",
1419        c2h6_label: str = r"$\mathregular{C_{2}H_{6}}$フラックス",
1420        col_datetime: str = "Date",
1421        output_filename: str = "diurnal_patterns.png",
1422        window_size: int = 6,  # 移動平均の窓サイズ
1423        show_std: bool = True,  # 標準偏差の表示有無
1424        alpha_std: float = 0.1,  # 標準偏差の透明度
1425    ) -> None:
1426        """CH4とC2H6フラックスの日変化パターンをプロットする
1427
1428        Parameters
1429        ------
1430            df : pd.DataFrame
1431                データフレーム
1432            output_dir : str
1433                出力ディレクトリのパス
1434            col_ch4_flux : str
1435                CH4フラックスのカラム名
1436            col_c2h6_flux : str
1437                C2H6フラックスのカラム名
1438            ch4_label : str
1439                CH4フラックスのラベル
1440            c2h6_label : str
1441                C2H6フラックスのラベル
1442            col_datetime : str
1443                日時カラムの名前
1444            output_filename : str
1445                出力ファイル名
1446            window_size : int
1447                移動平均の窓サイズ(デフォルト6)
1448            show_std : bool
1449                標準偏差を表示するかどうか
1450            alpha_std : float
1451                標準偏差の透明度(0-1)
1452        """
1453        # 出力ディレクトリの作成
1454        os.makedirs(output_dir, exist_ok=True)
1455        output_path: str = os.path.join(output_dir, output_filename)
1456
1457        # # プロットのスタイル設定
1458        # plt.rcParams.update({
1459        #     'font.size': 20,
1460        #     'axes.labelsize': 20,
1461        #     'axes.titlesize': 20,
1462        #     'xtick.labelsize': 20,
1463        #     'ytick.labelsize': 20,
1464        #     'legend.fontsize': 20,
1465        # })
1466
1467        # 日時インデックスの処理
1468        df = df.copy()
1469        if not isinstance(df.index, pd.DatetimeIndex):
1470            df[col_datetime] = pd.to_datetime(df[col_datetime])
1471            df.set_index(col_datetime, inplace=True)
1472
1473        # 時刻データの抽出とグループ化
1474        df["hour"] = df.index.hour
1475        hourly_means = df.groupby("hour")[[col_ch4_flux, col_c2h6_flux]].agg(
1476            ["mean", "std"]
1477        )
1478
1479        # 24時間目のデータ点を追加(0時のデータを使用)
1480        last_hour = hourly_means.iloc[0:1].copy()
1481        last_hour.index = [24]
1482        hourly_means = pd.concat([hourly_means, last_hour])
1483
1484        # 24時間分のデータポイントを作成
1485        time_points = pd.date_range("2024-01-01", periods=25, freq="h")
1486
1487        # プロットの作成
1488        fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
1489
1490        # 移動平均の計算と描画
1491        ch4_mean = (
1492            hourly_means[(col_ch4_flux, "mean")]
1493            .rolling(window=window_size, center=True, min_periods=1)
1494            .mean()
1495        )
1496        c2h6_mean = (
1497            hourly_means[(col_c2h6_flux, "mean")]
1498            .rolling(window=window_size, center=True, min_periods=1)
1499            .mean()
1500        )
1501
1502        if show_std:
1503            ch4_std = (
1504                hourly_means[(col_ch4_flux, "std")]
1505                .rolling(window=window_size, center=True, min_periods=1)
1506                .mean()
1507            )
1508            c2h6_std = (
1509                hourly_means[(col_c2h6_flux, "std")]
1510                .rolling(window=window_size, center=True, min_periods=1)
1511                .mean()
1512            )
1513
1514            ax1.fill_between(
1515                time_points,
1516                ch4_mean - ch4_std,
1517                ch4_mean + ch4_std,
1518                color="blue",
1519                alpha=alpha_std,
1520            )
1521            ax2.fill_between(
1522                time_points,
1523                c2h6_mean - c2h6_std,
1524                c2h6_mean + c2h6_std,
1525                color="red",
1526                alpha=alpha_std,
1527            )
1528
1529        # メインのラインプロット
1530        ax1.plot(time_points, ch4_mean, "blue", label=ch4_label)
1531        ax2.plot(time_points, c2h6_mean, "red", label=c2h6_label)
1532
1533        # 軸の設定
1534        for ax, ylabel in [
1535            (ax1, r"CH$_4$ (nmol m$^{-2}$ s$^{-1}$)"),
1536            (ax2, r"C$_2$H$_6$ (nmol m$^{-2}$ s$^{-1}$)"),
1537        ]:
1538            ax.set_xlabel("Time")
1539            ax.set_ylabel(ylabel)
1540            ax.xaxis.set_major_formatter(mdates.DateFormatter("%-H"))
1541            ax.xaxis.set_major_locator(mdates.HourLocator(byhour=[0, 6, 12, 18, 24]))
1542            ax.set_xlim(time_points[0], time_points[-1])
1543            ax.grid(True, alpha=0.3)
1544            ax.legend()
1545
1546        # グラフの保存
1547        plt.tight_layout()
1548        plt.savefig(output_path, dpi=300, bbox_inches="tight")
1549        plt.close()
1550
1551        # 統計情報の表示(オプション)
1552        for col, name in [(col_ch4_flux, "CH4"), (col_c2h6_flux, "C2H6")]:
1553            mean_val = hourly_means[(col, "mean")].mean()
1554            min_val = hourly_means[(col, "mean")].min()
1555            max_val = hourly_means[(col, "mean")].max()
1556            min_time = hourly_means[(col, "mean")].idxmin()
1557            max_time = hourly_means[(col, "mean")].idxmax()
1558
1559            self.logger.info(f"{name} Statistics:")
1560            self.logger.info(f"Mean: {mean_val:.2f}")
1561            self.logger.info(f"Min: {min_val:.2f} (Hour: {min_time})")
1562            self.logger.info(f"Max: {max_val:.2f} (Hour: {max_time})")
1563            self.logger.info(f"Max/Min ratio: {max_val / min_val:.2f}\n")

CH4とC2H6フラックスの日変化パターンをプロットする

Parameters

df : pd.DataFrame
    データフレーム
output_dir : str
    出力ディレクトリのパス
col_ch4_flux : str
    CH4フラックスのカラム名
col_c2h6_flux : str
    C2H6フラックスのカラム名
ch4_label : str
    CH4フラックスのラベル
c2h6_label : str
    C2H6フラックスのラベル
col_datetime : str
    日時カラムの名前
output_filename : str
    出力ファイル名
window_size : int
    移動平均の窓サイズ(デフォルト6)
show_std : bool
    標準偏差を表示するかどうか
alpha_std : float
    標準偏差の透明度(0-1)
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:
1565    def plot_scatter(
1566        self,
1567        df: pd.DataFrame,
1568        x_col: str,
1569        y_col: str,
1570        output_dir: str,
1571        output_filename: str = "scatter.png",
1572        xlabel: str | None = None,
1573        ylabel: str | None = None,
1574        add_label: bool = True,
1575        x_axis_range: tuple | None = None,
1576        y_axis_range: tuple | None = None,
1577        fixed_slope: float = 0.076,
1578        show_fixed_slope: bool = False,
1579        x_scientific: bool = False,  # 追加:x軸を指数表記にするかどうか
1580        y_scientific: bool = False,  # 追加:y軸を指数表記にするかどうか
1581    ) -> None:
1582        """散布図を作成し、TLS回帰直線を描画します。
1583
1584        Parameters
1585        ------
1586            df : pd.DataFrame
1587                プロットに使用するデータフレーム
1588            x_col : str
1589                x軸に使用する列名
1590            y_col : str
1591                y軸に使用する列名
1592            xlabel : str
1593                x軸のラベル
1594            ylabel : str
1595                y軸のラベル
1596            output_dir : str
1597                出力先ディレクトリ
1598            output_filename : str, optional
1599                出力ファイル名。デフォルトは"scatter.png"
1600            add_label : bool, optional
1601                軸ラベルを表示するかどうか。デフォルトはTrue
1602            x_axis_range : tuple, optional
1603                x軸の範囲。デフォルトはNone。
1604            y_axis_range : tuple, optional
1605                y軸の範囲。デフォルトはNone。
1606            fixed_slope : float, optional
1607                固定傾きを指定するための値。デフォルトは0.076
1608            show_fixed_slope : bool, optional
1609                固定傾きの線を表示するかどうか。デフォルトはFalse
1610        """
1611        os.makedirs(output_dir, exist_ok=True)
1612        output_path: str = os.path.join(output_dir, output_filename)
1613
1614        # 有効なデータの抽出
1615        df = MonthlyFiguresGenerator.get_valid_data(df, x_col, y_col)
1616
1617        # データの準備
1618        x = df[x_col].values
1619        y = df[y_col].values
1620
1621        # データの中心化
1622        x_mean = np.mean(x)
1623        y_mean = np.mean(y)
1624        x_c = x - x_mean
1625        y_c = y - y_mean
1626
1627        # TLS回帰の計算
1628        data_matrix = np.vstack((x_c, y_c))
1629        cov_matrix = np.cov(data_matrix)
1630        _, eigenvecs = linalg.eigh(cov_matrix)
1631        largest_eigenvec = eigenvecs[:, -1]
1632
1633        slope = largest_eigenvec[1] / largest_eigenvec[0]
1634        intercept = y_mean - slope * x_mean
1635
1636        # R²とRMSEの計算
1637        y_pred = slope * x + intercept
1638        r_squared = 1 - np.sum((y - y_pred) ** 2) / np.sum((y - np.mean(y)) ** 2)
1639        rmse = np.sqrt(np.mean((y - y_pred) ** 2))
1640
1641        # プロットの作成
1642        fig, ax = plt.subplots(figsize=(6, 6))
1643
1644        # データ点のプロット
1645        ax.scatter(x, y, color="black")
1646
1647        # データの範囲を取得
1648        if x_axis_range is None:
1649            x_axis_range = (df[x_col].min(), df[x_col].max())
1650        if y_axis_range is None:
1651            y_axis_range = (df[y_col].min(), df[y_col].max())
1652
1653        # 回帰直線のプロット
1654        x_range = np.linspace(x_axis_range[0], x_axis_range[1], 150)
1655        y_range = slope * x_range + intercept
1656        ax.plot(x_range, y_range, "r", label="TLS regression")
1657
1658        # 傾き固定の線を追加(フラグがTrueの場合)
1659        if show_fixed_slope:
1660            fixed_intercept = (
1661                y_mean - fixed_slope * x_mean
1662            )  # 中心点を通るように切片を計算
1663            y_fixed = fixed_slope * x_range + fixed_intercept
1664            ax.plot(x_range, y_fixed, "b--", label=f"Slope = {fixed_slope}", alpha=0.7)
1665
1666        # 軸の設定
1667        ax.set_xlim(x_axis_range)
1668        ax.set_ylim(y_axis_range)
1669
1670        # 指数表記の設定
1671        if x_scientific:
1672            ax.ticklabel_format(style="sci", axis="x", scilimits=(0, 0))
1673            ax.xaxis.get_offset_text().set_position((1.1, 0))  # 指数の位置調整
1674        if y_scientific:
1675            ax.ticklabel_format(style="sci", axis="y", scilimits=(0, 0))
1676            ax.yaxis.get_offset_text().set_position((0, 1.1))  # 指数の位置調整
1677
1678        if add_label:
1679            if xlabel is not None:
1680                ax.set_xlabel(xlabel)
1681            if ylabel is not None:
1682                ax.set_ylabel(ylabel)
1683
1684        # 1:1の関係を示す点線(軸の範囲が同じ場合のみ表示)
1685        if (
1686            x_axis_range is not None
1687            and y_axis_range is not None
1688            and x_axis_range == y_axis_range
1689        ):
1690            ax.plot(
1691                [x_axis_range[0], x_axis_range[1]],
1692                [x_axis_range[0], x_axis_range[1]],
1693                "k--",
1694                alpha=0.5,
1695            )
1696
1697        # 回帰情報の表示
1698        equation = (
1699            f"y = {slope:.2f}x {'+' if intercept >= 0 else '-'} {abs(intercept):.2f}"
1700        )
1701        position_x = 0.05
1702        fig_ha: str = "left"
1703        ax.text(
1704            position_x,
1705            0.95,
1706            equation,
1707            transform=ax.transAxes,
1708            va="top",
1709            ha=fig_ha,
1710            color="red",
1711        )
1712        ax.text(
1713            position_x,
1714            0.88,
1715            f"R² = {r_squared:.2f}",
1716            transform=ax.transAxes,
1717            va="top",
1718            ha=fig_ha,
1719            color="red",
1720        )
1721        ax.text(
1722            position_x,
1723            0.81,  # RMSEのための新しい位置
1724            f"RMSE = {rmse:.2f}",
1725            transform=ax.transAxes,
1726            va="top",
1727            ha=fig_ha,
1728            color="red",
1729        )
1730        # 目盛り線の設定
1731        ax.grid(True, alpha=0.3)
1732
1733        fig.savefig(output_path, dpi=300, bbox_inches="tight")
1734        plt.close(fig)

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

Parameters

df : pd.DataFrame
    プロットに使用するデータフレーム
x_col : str
    x軸に使用する列名
y_col : str
    y軸に使用する列名
xlabel : str
    x軸のラベル
ylabel : str
    y軸のラベル
output_dir : str
    出力先ディレクトリ
output_filename : str, optional
    出力ファイル名。デフォルトは"scatter.png"
add_label : bool, optional
    軸ラベルを表示するかどうか。デフォルトはTrue
x_axis_range : tuple, optional
    x軸の範囲。デフォルトはNone。
y_axis_range : tuple, optional
    y軸の範囲。デフォルトはNone。
fixed_slope : float, optional
    固定傾きを指定するための値。デフォルトは0.076
show_fixed_slope : bool, optional
    固定傾きの線を表示するかどうか。デフォルトはFalse
def plot_source_contributions_diurnal( self, df: pandas.core.frame.DataFrame, output_dir: str, col_ch4_flux: str, col_c2h6_flux: str, color_bio: str = 'blue', color_gas: str = 'red', label_gas: str = 'gas', label_bio: str = 'bio', flux_alpha: float = 0.6, col_datetime: str = 'Date', output_filename: str = 'source_contributions.png', window_size: int = 6, print_summary: bool = True, add_legend: bool = True, smooth: bool = False, y_max: float = 100, subplot_label: str | None = None, subplot_fontsize: int = 20) -> None:
1736    def plot_source_contributions_diurnal(
1737        self,
1738        df: pd.DataFrame,
1739        output_dir: str,
1740        col_ch4_flux: str,
1741        col_c2h6_flux: str,
1742        color_bio: str = "blue",
1743        color_gas: str = "red",
1744        label_gas: str = "gas",
1745        label_bio: str = "bio",
1746        flux_alpha: float = 0.6,
1747        col_datetime: str = "Date",
1748        output_filename: str = "source_contributions.png",
1749        window_size: int = 6,  # 移動平均の窓サイズ
1750        print_summary: bool = True,  # 統計情報を表示するかどうか,
1751        add_legend: bool = True,
1752        smooth: bool = False,
1753        y_max: float = 100,  # y軸の上限値を追加
1754        subplot_label: str | None = None,
1755        subplot_fontsize: int = 20,
1756    ) -> None:
1757        """CH4フラックスの都市ガス起源と生物起源の日変化を積み上げグラフとして表示
1758
1759        Parameters
1760        ------
1761            df : pd.DataFrame
1762                データフレーム
1763            output_dir : str
1764                出力ディレクトリのパス
1765            col_ch4_flux : str
1766                CH4フラックスのカラム名
1767            col_c2h6_flux : str
1768                C2H6フラックスのカラム名
1769            label_gas : str
1770                都市ガス起源のラベル
1771            label_bio : str
1772                生物起源のラベル
1773            col_datetime : str
1774                日時カラムの名前
1775            output_filename : str
1776                出力ファイル名
1777            window_size : int
1778                移動平均の窓サイズ
1779            print_summary : bool
1780                統計情報を表示するかどうか
1781            smooth : bool
1782                移動平均を適用するかどうか
1783            y_max : float
1784                y軸の上限値(デフォルト: 100)
1785        """
1786        # 出力ディレクトリの作成
1787        os.makedirs(output_dir, exist_ok=True)
1788        output_path: str = os.path.join(output_dir, output_filename)
1789
1790        # 起源の計算
1791        df_with_sources = self._calculate_source_contributions(
1792            df=df,
1793            col_ch4_flux=col_ch4_flux,
1794            col_c2h6_flux=col_c2h6_flux,
1795            col_datetime=col_datetime,
1796        )
1797
1798        # 時刻データの抽出とグループ化
1799        df_with_sources["hour"] = df_with_sources.index.hour
1800        hourly_means = df_with_sources.groupby("hour")[["ch4_gas", "ch4_bio"]].mean()
1801
1802        # 24時間目のデータ点を追加(0時のデータを使用)
1803        last_hour = hourly_means.iloc[0:1].copy()
1804        last_hour.index = [24]
1805        hourly_means = pd.concat([hourly_means, last_hour])
1806
1807        # 移動平均の適用
1808        hourly_means_smoothed = hourly_means
1809        if smooth:
1810            hourly_means_smoothed = hourly_means.rolling(
1811                window=window_size, center=True, min_periods=1
1812            ).mean()
1813
1814        # 24時間分のデータポイントを作成
1815        time_points = pd.date_range("2024-01-01", periods=25, freq="h")
1816
1817        # プロットの作成
1818        plt.figure(figsize=(10, 6))
1819        ax = plt.gca()
1820
1821        # サブプロットラベルの追加(subplot_labelが指定されている場合)
1822        if subplot_label:
1823            ax.text(
1824                0.02,  # x位置
1825                0.98,  # y位置
1826                subplot_label,
1827                transform=ax.transAxes,
1828                va="top",
1829                fontsize=subplot_fontsize,
1830            )
1831
1832        # 積み上げプロット
1833        ax.fill_between(
1834            time_points,
1835            0,
1836            hourly_means_smoothed["ch4_bio"],
1837            color=color_bio,
1838            alpha=flux_alpha,
1839            label=label_bio,
1840        )
1841        ax.fill_between(
1842            time_points,
1843            hourly_means_smoothed["ch4_bio"],
1844            hourly_means_smoothed["ch4_bio"] + hourly_means_smoothed["ch4_gas"],
1845            color=color_gas,
1846            alpha=flux_alpha,
1847            label=label_gas,
1848        )
1849
1850        # 合計値のライン
1851        total_flux = hourly_means_smoothed["ch4_bio"] + hourly_means_smoothed["ch4_gas"]
1852        ax.plot(time_points, total_flux, "-", color="black", alpha=0.5)
1853
1854        # 軸の設定
1855        ax.set_xlabel("Time (hour)")
1856        ax.set_ylabel(r"CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)")
1857        ax.xaxis.set_major_formatter(mdates.DateFormatter("%-H"))
1858        ax.xaxis.set_major_locator(mdates.HourLocator(byhour=[0, 6, 12, 18, 24]))
1859        ax.set_xlim(time_points[0], time_points[-1])
1860        ax.set_ylim(0, y_max)  # y軸の範囲を設定
1861        ax.grid(True, alpha=0.3)
1862
1863        # 凡例を図の下部に配置
1864        if add_legend:
1865            handles, labels = ax.get_legend_handles_labels()
1866            fig = plt.gcf()  # 現在の図を取得
1867            fig.legend(
1868                handles,
1869                labels,
1870                loc="center",
1871                bbox_to_anchor=(0.5, 0.01),
1872                ncol=len(handles),
1873            )
1874            plt.subplots_adjust(bottom=0.2)  # 下部に凡例用のスペースを確保
1875
1876        # グラフの保存
1877        plt.tight_layout()
1878        plt.savefig(output_path, dpi=300, bbox_inches="tight")
1879        plt.close()
1880
1881        # 統計情報の表示
1882        if print_summary:
1883            stats = {
1884                "都市ガス起源": hourly_means["ch4_gas"],
1885                "生物起源": hourly_means["ch4_bio"],
1886                "合計": hourly_means["ch4_gas"] + hourly_means["ch4_bio"],
1887            }
1888
1889            for source, data in stats.items():
1890                mean_val = data.mean()
1891                min_val = data.min()
1892                max_val = data.max()
1893                min_time = data.idxmin()
1894                max_time = data.idxmax()
1895
1896                self.logger.info(f"{source}の統計:")
1897                print(f"  平均値: {mean_val:.2f}")
1898                print(f"  最小値: {min_val:.2f} (Hour: {min_time})")
1899                print(f"  最大値: {max_val:.2f} (Hour: {max_time})")
1900                if min_val != 0:
1901                    print(f"  最大/最小比: {max_val / min_val:.2f}")

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

Parameters

df : pd.DataFrame
    データフレーム
output_dir : str
    出力ディレクトリのパス
col_ch4_flux : str
    CH4フラックスのカラム名
col_c2h6_flux : str
    C2H6フラックスのカラム名
label_gas : str
    都市ガス起源のラベル
label_bio : str
    生物起源のラベル
col_datetime : str
    日時カラムの名前
output_filename : str
    出力ファイル名
window_size : int
    移動平均の窓サイズ
print_summary : bool
    統計情報を表示するかどうか
smooth : bool
    移動平均を適用するかどうか
y_max : float
    y軸の上限値(デフォルト: 100)
def plot_source_contributions_diurnal_by_date( self, df: pandas.core.frame.DataFrame, output_dir: str, col_ch4_flux: str, col_c2h6_flux: str, color_bio: str = 'blue', color_gas: str = 'red', label_bio: str = 'bio', label_gas: str = 'gas', flux_alpha: float = 0.6, col_datetime: str = 'Date', output_filename: str = 'source_contributions_by_date.png', add_label: bool = True, add_legend: bool = True, print_summary: bool = True, subplot_fontsize: int = 20, subplot_label_weekday: str | None = None, subplot_label_weekend: str | None = None, y_max: float | None = None) -> None:
1903    def plot_source_contributions_diurnal_by_date(
1904        self,
1905        df: pd.DataFrame,
1906        output_dir: str,
1907        col_ch4_flux: str,
1908        col_c2h6_flux: str,
1909        color_bio: str = "blue",
1910        color_gas: str = "red",
1911        label_bio: str = "bio",
1912        label_gas: str = "gas",
1913        flux_alpha: float = 0.6,
1914        col_datetime: str = "Date",
1915        output_filename: str = "source_contributions_by_date.png",
1916        add_label: bool = True,
1917        add_legend: bool = True,
1918        print_summary: bool = True,  # 統計情報を表示するかどうか,
1919        subplot_fontsize: int = 20,
1920        subplot_label_weekday: str | None = None,
1921        subplot_label_weekend: str | None = None,
1922        y_max: float | None = None,  # y軸の上限値
1923    ) -> None:
1924        """CH4フラックスの都市ガス起源と生物起源の日変化を平日・休日別に表示
1925
1926        Parameters
1927        ------
1928            df : pd.DataFrame
1929                データフレーム
1930            output_dir : str
1931                出力ディレクトリのパス
1932            col_ch4_flux : str
1933                CH4フラックスのカラム名
1934            col_c2h6_flux : str
1935                C2H6フラックスのカラム名
1936            label_bio : str
1937                生物起源のラベル
1938            label_gas : str
1939                都市ガス起源のラベル
1940            col_datetime : str
1941                日時カラムの名前
1942            output_filename : str
1943                出力ファイル名
1944            add_label : bool
1945                ラベルを表示するか
1946            add_legend : bool
1947                凡例を表示するか
1948            subplot_fontsize : int
1949                サブプロットのフォントサイズ
1950            subplot_label_weekday : str | None
1951                平日グラフのラベル
1952            subplot_label_weekend : str | None
1953                休日グラフのラベル
1954            y_max : float | None
1955                y軸の上限値
1956        """
1957        # 出力ディレクトリの作成
1958        os.makedirs(output_dir, exist_ok=True)
1959        output_path: str = os.path.join(output_dir, output_filename)
1960
1961        # 起源の計算
1962        df_with_sources = self._calculate_source_contributions(
1963            df=df,
1964            col_ch4_flux=col_ch4_flux,
1965            col_c2h6_flux=col_c2h6_flux,
1966            col_datetime=col_datetime,
1967        )
1968
1969        # 日付タイプの分類
1970        dates = pd.to_datetime(df_with_sources.index)
1971        is_weekend = dates.dayofweek.isin([5, 6])
1972        is_holiday = dates.map(lambda x: jpholiday.is_holiday(x.date()))
1973        is_weekday = ~(is_weekend | is_holiday)
1974
1975        # データの分類
1976        data_weekday = df_with_sources[is_weekday]
1977        data_holiday = df_with_sources[is_weekend | is_holiday]
1978
1979        # プロットの作成
1980        fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
1981
1982        # 平日と休日それぞれのプロット
1983        for ax, data, label in [
1984            (ax1, data_weekday, "Weekdays"),
1985            (ax2, data_holiday, "Weekends & Holidays"),
1986        ]:
1987            # 時間ごとの平均値を計算
1988            hourly_means = data.groupby(data.index.hour)[["ch4_gas", "ch4_bio"]].mean()
1989
1990            # 24時間目のデータ点を追加
1991            last_hour = hourly_means.iloc[0:1].copy()
1992            last_hour.index = [24]
1993            hourly_means = pd.concat([hourly_means, last_hour])
1994
1995            # 24時間分のデータポイントを作成
1996            time_points = pd.date_range("2024-01-01", periods=25, freq="h")
1997
1998            # 積み上げプロット
1999            ax.fill_between(
2000                time_points,
2001                0,
2002                hourly_means["ch4_bio"],
2003                color=color_bio,
2004                alpha=flux_alpha,
2005                label=label_bio,
2006            )
2007            ax.fill_between(
2008                time_points,
2009                hourly_means["ch4_bio"],
2010                hourly_means["ch4_bio"] + hourly_means["ch4_gas"],
2011                color=color_gas,
2012                alpha=flux_alpha,
2013                label=label_gas,
2014            )
2015
2016            # 合計値のライン
2017            total_flux = hourly_means["ch4_bio"] + hourly_means["ch4_gas"]
2018            ax.plot(time_points, total_flux, "-", color="black", alpha=0.5)
2019
2020            # 軸の設定
2021            if add_label:
2022                ax.set_xlabel("Time (hour)")
2023                if ax == ax1:  # 左側のプロットのラベル
2024                    ax.set_ylabel("Weekdays CH$_4$ flux\n" r"(nmol m$^{-2}$ s$^{-1}$)")
2025                else:  # 右側のプロットのラベル
2026                    ax.set_ylabel("Weekends CH$_4$ flux\n" r"(nmol m$^{-2}$ s$^{-1}$)")
2027
2028            ax.xaxis.set_major_formatter(mdates.DateFormatter("%-H"))
2029            ax.xaxis.set_major_locator(mdates.HourLocator(byhour=[0, 6, 12, 18, 24]))
2030            ax.set_xlim(time_points[0], time_points[-1])
2031            if y_max is not None:
2032                ax.set_ylim(0, y_max)
2033            ax.grid(True, alpha=0.3)
2034
2035        # サブプロットラベルの追加
2036        if subplot_label_weekday:
2037            ax1.text(
2038                0.02,
2039                0.98,
2040                subplot_label_weekday,
2041                transform=ax1.transAxes,
2042                va="top",
2043                fontsize=subplot_fontsize,
2044            )
2045        if subplot_label_weekend:
2046            ax2.text(
2047                0.02,
2048                0.98,
2049                subplot_label_weekend,
2050                transform=ax2.transAxes,
2051                va="top",
2052                fontsize=subplot_fontsize,
2053            )
2054
2055        # 凡例を図の下部に配置
2056        if add_legend:
2057            # 最初のプロットから凡例のハンドルとラベルを取得
2058            handles, labels = ax1.get_legend_handles_labels()
2059            # 図の下部に凡例を配置
2060            fig.legend(
2061                handles,
2062                labels,
2063                loc="center",
2064                bbox_to_anchor=(0.5, 0.01),  # x=0.5で中央、y=0.01で下部に配置
2065                ncol=len(handles),  # ハンドルの数だけ列を作成(一行に表示)
2066            )
2067            # 凡例用のスペースを確保
2068            plt.subplots_adjust(bottom=0.2)  # 下部に30%のスペースを確保
2069
2070        plt.tight_layout()
2071        plt.savefig(output_path, dpi=300, bbox_inches="tight")
2072        plt.close(fig=fig)
2073
2074        # 統計情報の表示
2075        if print_summary:
2076            for data, label in [
2077                (data_weekday, "Weekdays"),
2078                (data_holiday, "Weekends & Holidays"),
2079            ]:
2080                hourly_means = data.groupby(data.index.hour)[
2081                    ["ch4_gas", "ch4_bio"]
2082                ].mean()
2083
2084                print(f"\n{label}の統計:")
2085
2086                # 都市ガス起源の統計
2087                gas_flux = hourly_means["ch4_gas"]
2088                bio_flux = hourly_means["ch4_bio"]
2089
2090                # 昼夜の時間帯を定義
2091                daytime_range: list[int] = [6, 19]  # m~n時の場合、[m ,(n+1)]と定義
2092                daytime_hours = range(daytime_range[0], daytime_range[1])
2093                nighttime_hours = list(range(0, daytime_range[0])) + list(
2094                    range(daytime_range[1], 24)
2095                )
2096
2097                # 昼間の統計
2098                daytime_gas = gas_flux[daytime_hours]
2099                daytime_bio = bio_flux[daytime_hours]
2100                daytime_total = daytime_gas + daytime_bio
2101                daytime_ratio = (daytime_gas.sum() / daytime_total.sum()) * 100
2102
2103                # 夜間の統計
2104                nighttime_gas = gas_flux[nighttime_hours]
2105                nighttime_bio = bio_flux[nighttime_hours]
2106                nighttime_total = nighttime_gas + nighttime_bio
2107                nighttime_ratio = (nighttime_gas.sum() / nighttime_total.sum()) * 100
2108
2109                print("\n都市ガス起源:")
2110                print(f"  平均値: {gas_flux.mean():.2f}")
2111                print(f"  最小値: {gas_flux.min():.2f} (Hour: {gas_flux.idxmin()})")
2112                print(f"  最大値: {gas_flux.max():.2f} (Hour: {gas_flux.idxmax()})")
2113                if gas_flux.min() != 0:
2114                    print(f"  最大/最小比: {gas_flux.max() / gas_flux.min():.2f}")
2115                print(
2116                    f"  全体に占める割合: {(gas_flux.sum() / (gas_flux.sum() + hourly_means['ch4_bio'].sum()) * 100):.1f}%"
2117                )
2118                print(
2119                    f"  昼間({daytime_range[0]}~{daytime_range[1] - 1}時)の割合: {daytime_ratio:.1f}%"
2120                )
2121                print(
2122                    f"  夜間({daytime_range[1] - 1}~{daytime_range[0]}時)の割合: {nighttime_ratio:.1f}%"
2123                )
2124
2125                # 生物起源の統計
2126                bio_flux = hourly_means["ch4_bio"]
2127                print("\n生物起源:")
2128                print(f"  平均値: {bio_flux.mean():.2f}")
2129                print(f"  最小値: {bio_flux.min():.2f} (Hour: {bio_flux.idxmin()})")
2130                print(f"  最大値: {bio_flux.max():.2f} (Hour: {bio_flux.idxmax()})")
2131                if bio_flux.min() != 0:
2132                    print(f"  最大/最小比: {bio_flux.max() / bio_flux.min():.2f}")
2133                print(
2134                    f"  全体に占める割合: {(bio_flux.sum() / (gas_flux.sum() + bio_flux.sum()) * 100):.1f}%"
2135                )
2136
2137                # 合計フラックスの統計
2138                total_flux = gas_flux + bio_flux
2139                print("\n合計:")
2140                print(f"  平均値: {total_flux.mean():.2f}")
2141                print(f"  最小値: {total_flux.min():.2f} (Hour: {total_flux.idxmin()})")
2142                print(f"  最大値: {total_flux.max():.2f} (Hour: {total_flux.idxmax()})")
2143                if total_flux.min() != 0:
2144                    print(f"  最大/最小比: {total_flux.max() / total_flux.min():.2f}")

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

Parameters

df : pd.DataFrame
    データフレーム
output_dir : str
    出力ディレクトリのパス
col_ch4_flux : str
    CH4フラックスのカラム名
col_c2h6_flux : str
    C2H6フラックスのカラム名
label_bio : str
    生物起源のラベル
label_gas : str
    都市ガス起源のラベル
col_datetime : str
    日時カラムの名前
output_filename : str
    出力ファイル名
add_label : bool
    ラベルを表示するか
add_legend : bool
    凡例を表示するか
subplot_fontsize : int
    サブプロットのフォントサイズ
subplot_label_weekday : str | None
    平日グラフのラベル
subplot_label_weekend : str | None
    休日グラフのラベル
y_max : float | None
    y軸の上限値
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:
2146    def plot_spectra(
2147        self,
2148        fs: float,
2149        lag_second: float,
2150        input_dir: str | Path | None,
2151        output_dir: str | Path | None,
2152        output_basename: str = "spectrum",
2153        col_ch4: str = "Ultra_CH4_ppm_C",
2154        col_c2h6: str = "Ultra_C2H6_ppb",
2155        col_tv: str = "Tv",
2156        label_ch4: str | None = None,
2157        label_c2h6: str | None = None,
2158        label_tv: str | None = None,
2159        file_pattern: str = "*.csv",
2160        markersize: float = 14,
2161        are_inputs_resampled: bool = True,
2162        save_fig: bool = True,
2163        show_fig: bool = True,
2164        plot_power: bool = True,
2165        plot_co: bool = True,
2166        add_tv_in_co: bool = True,
2167    ) -> None:
2168        """
2169        月間の平均パワースペクトル密度を計算してプロットする。
2170
2171        データファイルを指定されたディレクトリから読み込み、パワースペクトル密度を計算し、
2172        結果を指定された出力ディレクトリにプロットして保存します。
2173
2174        Parameters
2175        ------
2176            fs : float
2177                サンプリング周波数。
2178            lag_second : float
2179                ラグ時間(秒)。
2180            input_dir : str | Path | None
2181                データファイルが格納されているディレクトリ。
2182            output_dir : str | Path | None
2183                出力先ディレクトリ。
2184            col_ch4 : str, optional
2185                CH4の濃度データが入ったカラムのキー。デフォルトは"Ultra_CH4_ppm_C"。
2186            col_c2h6 : str, optional
2187                C2H6の濃度データが入ったカラムのキー。デフォルトは"Ultra_C2H6_ppb"。
2188            col_tv : str, optional
2189                気温データが入ったカラムのキー。デフォルトは"Tv"。
2190            label_ch4 : str | None, optional
2191                CH4のラベル。デフォルトはNone。
2192            label_c2h6 : str | None, optional
2193                C2H6のラベル。デフォルトはNone。
2194            label_tv : str | None, optional
2195                気温のラベル。デフォルトはNone。
2196            file_pattern : str, optional
2197                処理対象のファイルパターン。デフォルトは"*.csv"。
2198            markersize : float, optional
2199                プロットマーカーのサイズ。デフォルトは14。
2200            are_inputs_resampled : bool, optional
2201                入力データが再サンプリングされているかどうか。デフォルトはTrue。
2202            save_fig : bool, optional
2203                図を保存するかどうか。デフォルトはTrue。
2204            show_fig : bool, optional
2205                図を表示するかどうか。デフォルトはTrue。
2206            plot_power : bool, optional
2207                パワースペクトルをプロットするかどうか。デフォルトはTrue。
2208            plot_co : bool, optional
2209                COのスペクトルをプロットするかどうか。デフォルトはTrue。
2210            add_tv_in_co : bool, optional
2211                顕熱フラックスのコスペクトルを表示するかどうか。デフォルトはTrue。
2212        """
2213        # 出力ディレクトリの作成
2214        if save_fig:
2215            if output_dir is None:
2216                raise ValueError(
2217                    "save_fig=Trueのとき、output_dirに有効なディレクトリパスを指定する必要があります。"
2218                )
2219            os.makedirs(output_dir, exist_ok=True)
2220
2221        # データの読み込みと結合
2222        edp = EddyDataPreprocessor(fs=fs)
2223        col_wind_w: str = EddyDataPreprocessor.WIND_W
2224
2225        # 各変数のパワースペクトルを格納する辞書
2226        power_spectra = {col_ch4: [], col_c2h6: []}
2227        co_spectra = {col_ch4: [], col_c2h6: [], col_tv: []}
2228        freqs = None
2229
2230        # プログレスバーを表示しながらファイルを処理
2231        file_list = glob.glob(os.path.join(input_dir, file_pattern))
2232        for filepath in tqdm(file_list, desc="Processing files"):
2233            df, _ = edp.get_resampled_df(
2234                filepath=filepath, resample_in_processing=are_inputs_resampled
2235            )
2236
2237            # 風速成分の計算を追加
2238            df = edp.add_uvw_columns(df)
2239
2240            # NaNや無限大を含む行を削除
2241            df = df.replace([np.inf, -np.inf], np.nan).dropna(
2242                subset=[col_ch4, col_c2h6, col_tv, col_wind_w]
2243            )
2244
2245            # データが十分な行数を持っているか確認
2246            if len(df) < 100:
2247                continue
2248
2249            # 各ファイルごとにスペクトル計算
2250            calculator = SpectrumCalculator(
2251                df=df,
2252                fs=fs,
2253            )
2254
2255            for col in power_spectra.keys():
2256                # 各変数のパワースペクトルを計算して保存
2257                if plot_power:
2258                    f, ps = calculator.calculate_power_spectrum(
2259                        col=col,
2260                        dimensionless=True,
2261                        frequency_weighted=True,
2262                        interpolate_points=True,
2263                        scaling="density",
2264                    )
2265                    # 最初のファイル処理時にfreqsを初期化
2266                    if freqs is None:
2267                        freqs = f
2268                        power_spectra[col].append(ps)
2269                    # 以降は周波数配列の長さが一致する場合のみ追加
2270                    elif len(f) == len(freqs):
2271                        power_spectra[col].append(ps)
2272
2273                # コスペクトル
2274                if plot_co:
2275                    _, cs, _ = calculator.calculate_co_spectrum(
2276                        col1=col_wind_w,
2277                        col2=col,
2278                        dimensionless=True,
2279                        frequency_weighted=True,
2280                        interpolate_points=True,
2281                        scaling="spectrum",
2282                        apply_lag_correction_to_col2=True,
2283                        lag_second=lag_second,
2284                    )
2285                    if freqs is not None and len(cs) == len(freqs):
2286                        co_spectra[col].append(cs)
2287
2288            # 顕熱フラックスのコスペクトル計算を追加
2289            if plot_co and add_tv_in_co:
2290                _, cs_heat, _ = calculator.calculate_co_spectrum(
2291                    col1=col_wind_w,
2292                    col2=col_tv,
2293                    dimensionless=True,
2294                    frequency_weighted=True,
2295                    interpolate_points=True,
2296                    scaling="spectrum",
2297                )
2298                if freqs is not None and len(cs_heat) == len(freqs):
2299                    co_spectra[col_tv].append(cs_heat)
2300
2301        # 各変数のスペクトルを平均化
2302        if plot_power:
2303            averaged_power_spectra = {
2304                col: np.mean(spectra, axis=0) for col, spectra in power_spectra.items()
2305            }
2306        if plot_co:
2307            averaged_co_spectra = {
2308                col: np.mean(spectra, axis=0) for col, spectra in co_spectra.items()
2309            }
2310        # 顕熱フラックスの平均コスペクトル計算
2311        if plot_co and add_tv_in_co and co_spectra[col_tv]:
2312            averaged_heat_co_spectra = np.mean(co_spectra[col_tv], axis=0)
2313
2314        # プロット設定を修正
2315        plot_configs = [
2316            {
2317                "col": col_ch4,
2318                "psd_ylabel": r"$fS_{\mathrm{CH_4}} / s_{\mathrm{CH_4}}^2$",
2319                "co_ylabel": r"$fC_{w\mathrm{CH_4}} / \overline{w'\mathrm{CH_4}'}$",
2320                "color": "red",
2321                "label": label_ch4,
2322            },
2323            {
2324                "col": col_c2h6,
2325                "psd_ylabel": r"$fS_{\mathrm{C_2H_6}} / s_{\mathrm{C_2H_6}}^2$",
2326                "co_ylabel": r"$fC_{w\mathrm{C_2H_6}} / \overline{w'\mathrm{C_2H_6}'}$",
2327                "color": "orange",
2328                "label": label_c2h6,
2329            },
2330        ]
2331        plot_tv_config = {
2332            "col": col_tv,
2333            "psd_ylabel": r"$fS_{T_v} / s_{T_v}^2$",
2334            "co_ylabel": r"$fC_{wT_v} / \overline{w'T_v'}$",
2335            "color": "blue",
2336            "label": label_tv,
2337        }
2338
2339        # パワースペクトルの図を作成
2340        if plot_power:
2341            fig_power, axes_psd = plt.subplots(1, 2, figsize=(12, 5), sharex=True)
2342            for ax, config in zip(axes_psd, plot_configs):
2343                ax.plot(
2344                    freqs,
2345                    averaged_power_spectra[config["col"]],
2346                    "o",  # マーカーを丸に設定
2347                    color=config["color"],
2348                    markersize=markersize,
2349                )
2350                ax.set_xscale("log")
2351                ax.set_yscale("log")
2352                ax.set_xlim(0.001, 10)
2353                ax.plot([0.01, 10], [1, 0.01], "-", color="black", alpha=0.5)
2354                ax.text(0.1, 0.06, "-2/3", fontsize=18)
2355                ax.set_ylabel(config["psd_ylabel"])
2356                if config["label"] is not None:
2357                    ax.text(
2358                        0.02, 0.98, config["label"], transform=ax.transAxes, va="top"
2359                    )
2360                ax.grid(True, alpha=0.3)
2361                ax.set_xlabel("f (Hz)")
2362
2363            plt.tight_layout()
2364
2365            if save_fig:
2366                output_path_psd: str = os.path.join(
2367                    output_dir, f"power_{output_basename}.png"
2368                )
2369                plt.savefig(
2370                    output_path_psd,
2371                    dpi=300,
2372                    bbox_inches="tight",
2373                )
2374            if show_fig:
2375                plt.show()
2376            else:
2377                plt.close(fig=fig_power)
2378
2379        # コスペクトルの図を作成
2380        if plot_co:
2381            fig_co, axes_cosp = plt.subplots(1, 2, figsize=(12, 5), sharex=True)
2382            for ax, config in zip(axes_cosp, plot_configs):
2383                # 顕熱フラックスのコスペクトルを先に描画(背景として)
2384                if add_tv_in_co and len(co_spectra[col_tv]) > 0:
2385                    ax.plot(
2386                        freqs,
2387                        averaged_heat_co_spectra,
2388                        "o",
2389                        color="gray",
2390                        alpha=0.3,
2391                        markersize=markersize,
2392                        label=plot_tv_config["label"]
2393                        if plot_tv_config["label"]
2394                        else None,
2395                    )
2396
2397                # CH4またはC2H6のコスペクトルを描画
2398                ax.plot(
2399                    freqs,
2400                    averaged_co_spectra[config["col"]],
2401                    "o",
2402                    color=config["color"],
2403                    markersize=markersize,
2404                    label=config["label"] if config["label"] else None,
2405                )
2406                ax.set_xscale("log")
2407                ax.set_yscale("log")
2408                ax.set_xlim(0.001, 10)
2409                # ax.plot([0.01, 10], [1, 0.01], "-", color="black", alpha=0.5)
2410                # ax.text(0.1, 0.1, "-4/3", fontsize=18)
2411                ax.set_ylabel(config["co_ylabel"])
2412                if config["label"] is not None:
2413                    ax.text(
2414                        0.02, 0.98, config["label"], transform=ax.transAxes, va="top"
2415                    )
2416                ax.grid(True, alpha=0.3)
2417                ax.set_xlabel("f (Hz)")
2418                # 凡例を追加(顕熱フラックスが含まれる場合)
2419                if add_tv_in_co and label_tv:
2420                    ax.legend(loc="lower left")
2421
2422            plt.tight_layout()
2423            if save_fig:
2424                output_path_csd: str = os.path.join(
2425                    output_dir, f"co_{output_basename}.png"
2426                )
2427                plt.savefig(
2428                    output_path_csd,
2429                    dpi=300,
2430                    bbox_inches="tight",
2431                )
2432            if show_fig:
2433                plt.show()
2434            else:
2435                plt.close(fig=fig_co)

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

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

Parameters

fs : float
    サンプリング周波数。
lag_second : float
    ラグ時間(秒)。
input_dir : str | Path | None
    データファイルが格納されているディレクトリ。
output_dir : str | Path | None
    出力先ディレクトリ。
col_ch4 : str, optional
    CH4の濃度データが入ったカラムのキー。デフォルトは"Ultra_CH4_ppm_C"。
col_c2h6 : str, optional
    C2H6の濃度データが入ったカラムのキー。デフォルトは"Ultra_C2H6_ppb"。
col_tv : str, optional
    気温データが入ったカラムのキー。デフォルトは"Tv"。
label_ch4 : str | None, optional
    CH4のラベル。デフォルトはNone。
label_c2h6 : str | None, optional
    C2H6のラベル。デフォルトはNone。
label_tv : str | None, optional
    気温のラベル。デフォルトはNone。
file_pattern : str, optional
    処理対象のファイルパターン。デフォルトは"*.csv"。
markersize : float, optional
    プロットマーカーのサイズ。デフォルトは14。
are_inputs_resampled : bool, optional
    入力データが再サンプリングされているかどうか。デフォルトはTrue。
save_fig : bool, optional
    図を保存するかどうか。デフォルトはTrue。
show_fig : bool, optional
    図を表示するかどうか。デフォルトはTrue。
plot_power : bool, optional
    パワースペクトルをプロットするかどうか。デフォルトはTrue。
plot_co : bool, optional
    COのスペクトルをプロットするかどうか。デフォルトはTrue。
add_tv_in_co : bool, optional
    顕熱フラックスのコスペクトルを表示するかどうか。デフォルトはTrue。
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:
2437    def plot_turbulence(
2438        self,
2439        df: pd.DataFrame,
2440        output_dir: str,
2441        output_filename: str = "turbulence.png",
2442        col_uz: str = "Uz",
2443        col_ch4: str = "Ultra_CH4_ppm_C",
2444        col_c2h6: str = "Ultra_C2H6_ppb",
2445        col_timestamp: str = "TIMESTAMP",
2446        add_serial_labels: bool = True,
2447    ) -> None:
2448        """時系列データのプロットを作成する
2449
2450        Parameters
2451        ------
2452            df : pd.DataFrame
2453                プロットするデータを含むDataFrame
2454            output_dir : str
2455                出力ディレクトリのパス
2456            output_filename : str
2457                出力ファイル名
2458            col_uz : str
2459                鉛直風速データのカラム名
2460            col_ch4 : str
2461                メタンデータのカラム名
2462            col_c2h6 : str
2463                エタンデータのカラム名
2464            col_timestamp : str
2465                タイムスタンプのカラム名
2466        """
2467        # 出力ディレクトリの作成
2468        os.makedirs(output_dir, exist_ok=True)
2469        output_path: str = os.path.join(output_dir, output_filename)
2470
2471        # データの前処理
2472        df = df.copy()
2473
2474        # タイムスタンプをインデックスに設定(まだ設定されていない場合)
2475        if not isinstance(df.index, pd.DatetimeIndex):
2476            df[col_timestamp] = pd.to_datetime(df[col_timestamp])
2477            df.set_index(col_timestamp, inplace=True)
2478
2479        # 開始時刻と終了時刻を取得
2480        start_time = df.index[0]
2481        end_time = df.index[-1]
2482
2483        # 開始時刻の分を取得
2484        start_minute = start_time.minute
2485
2486        # 時間軸の作成(実際の開始時刻からの経過分数)
2487        minutes_elapsed = (df.index - start_time).total_seconds() / 60
2488
2489        # プロットの作成
2490        _, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=(12, 10), sharex=True)
2491
2492        # 鉛直風速
2493        ax1.plot(minutes_elapsed, df[col_uz], "k-", linewidth=0.5)
2494        ax1.set_ylabel(r"$w$ (m s$^{-1}$)")
2495        if add_serial_labels:
2496            ax1.text(0.02, 0.98, "(a)", transform=ax1.transAxes, va="top")
2497        ax1.grid(True, alpha=0.3)
2498
2499        # CH4濃度
2500        ax2.plot(minutes_elapsed, df[col_ch4], "r-", linewidth=0.5)
2501        ax2.set_ylabel(r"$\mathrm{CH_4}$ (ppm)")
2502        if add_serial_labels:
2503            ax2.text(0.02, 0.98, "(b)", transform=ax2.transAxes, va="top")
2504        ax2.grid(True, alpha=0.3)
2505
2506        # C2H6濃度
2507        ax3.plot(minutes_elapsed, df[col_c2h6], "orange", linewidth=0.5)
2508        ax3.set_ylabel(r"$\mathrm{C_2H_6}$ (ppb)")
2509        if add_serial_labels:
2510            ax3.text(0.02, 0.98, "(c)", transform=ax3.transAxes, va="top")
2511        ax3.grid(True, alpha=0.3)
2512        ax3.set_xlabel("Time (minutes)")
2513
2514        # x軸の範囲を実際の開始時刻から30分後までに設定
2515        total_minutes = (end_time - start_time).total_seconds() / 60
2516        ax3.set_xlim(0, min(30, total_minutes))
2517
2518        # x軸の目盛りを5分間隔で設定
2519        np.arange(start_minute, start_minute + 35, 5)
2520        ax3.xaxis.set_major_locator(MultipleLocator(5))
2521
2522        # レイアウトの調整
2523        plt.tight_layout()
2524
2525        # 図の保存
2526        plt.savefig(output_path, dpi=300, bbox_inches="tight")
2527        plt.close()

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

Parameters

df : pd.DataFrame
    プロットするデータを含むDataFrame
output_dir : str
    出力ディレクトリのパス
output_filename : str
    出力ファイル名
col_uz : str
    鉛直風速データのカラム名
col_ch4 : str
    メタンデータのカラム名
col_c2h6 : str
    エタンデータのカラム名
col_timestamp : str
    タイムスタンプのカラム名
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, color_bio: str = 'blue', color_gas: str = 'red', label_bio: str = '生物起源', label_gas: str = '都市ガス起源', figsize: tuple[float, float] = (8, 8), flux_alpha: float = 0.4, num_directions: int = 8, gap_degrees: float = 0.0, center_on_angles: bool = True, subplot_label: str | None = None, add_legend: bool = True, stack_bars: bool = True, print_summary: bool = True, save_fig: bool = True, show_fig: bool = True) -> None:
2529    def plot_wind_rose_sources(
2530        self,
2531        df: pd.DataFrame,
2532        output_dir: str | Path | None = None,
2533        output_filename: str = "edp_wind_rose.png",
2534        col_datetime: str = "Date",
2535        col_ch4_flux: str = "Fch4",
2536        col_c2h6_flux: str = "Fc2h6",
2537        col_wind_dir: str = "Wind direction",
2538        flux_unit: str = r"(nmol m$^{-2}$ s$^{-1}$)",
2539        ymax: float | None = None,  # フラックスの上限値
2540        color_bio: str = "blue",
2541        color_gas: str = "red",
2542        label_bio: str = "生物起源",
2543        label_gas: str = "都市ガス起源",
2544        figsize: tuple[float, float] = (8, 8),
2545        flux_alpha: float = 0.4,
2546        num_directions: int = 8,  # 方位の数(8方位)
2547        gap_degrees: float = 0.0,  # セクター間の隙間(度数)
2548        center_on_angles: bool = True,  # 追加:45度刻みの線を境界にするかどうか
2549        subplot_label: str | None = None,
2550        add_legend: bool = True,
2551        stack_bars: bool = True,  # 追加:積み上げ方式を選択するパラメータ
2552        print_summary: bool = True,  # 統計情報を表示するかどうか
2553        save_fig: bool = True,
2554        show_fig: bool = True,
2555    ) -> None:
2556        """CH4フラックスの都市ガス起源と生物起源の風配図を作成する関数
2557
2558        Parameters
2559        ------
2560            df : pd.DataFrame
2561                風配図を作成するためのデータフレーム
2562            output_dir : str | Path | None
2563                生成された図を保存するディレクトリのパス
2564            output_filename : str
2565                保存するファイル名(デフォルトは"edp_wind_rose.png")
2566            col_ch4_flux : str
2567                CH4フラックスを示すカラム名
2568            col_c2h6_flux : str
2569                C2H6フラックスを示すカラム名
2570            col_wind_dir : str
2571                風向を示すカラム名
2572            color_bio : str
2573                生物起源のフラックスに対する色
2574            color_gas : str
2575                都市ガス起源のフラックスに対する色
2576                風向を示すカラム名
2577            label_bio : str
2578                生物起源のフラックスに対するラベル
2579            label_gas : str
2580                都市ガス起源のフラックスに対するラベル
2581            col_datetime : str
2582                日時を示すカラム名
2583            num_directions : int
2584                風向の数(デフォルトは8)
2585            gap_degrees : float
2586                セクター間の隙間の大きさ(度数)。0の場合は隙間なし。
2587            center_on_angles: bool
2588                Trueの場合、45度刻みの線を境界として扇形を描画します。
2589                Falseの場合、45度の中間(22.5度)を中心として扇形を描画します。
2590            subplot_label : str
2591                サブプロットに表示するラベル
2592            print_summary : bool
2593                統計情報を表示するかどうかのフラグ
2594            flux_unit : str
2595                フラックスの単位
2596            ymax : float | None
2597                y軸の上限値(指定しない場合はデータの最大値に基づいて自動設定)
2598            figsize : tuple[float, float]
2599                図のサイズ
2600            flux_alpha : float
2601                フラックスの透明度
2602            stack_bars : bool, optional
2603                Trueの場合、生物起源の上に都市ガス起源を積み上げます(デフォルト)。
2604                Falseの場合、両方を0から積み上げます。
2605            save_fig : bool
2606                図を保存するかどうかのフラグ
2607            show_fig : bool
2608                図を表示するかどうかのフラグ
2609        """
2610        # 起源の計算
2611        df_with_sources = self._calculate_source_contributions(
2612            df=df,
2613            col_ch4_flux=col_ch4_flux,
2614            col_c2h6_flux=col_c2h6_flux,
2615            col_datetime=col_datetime,
2616        )
2617
2618        # 方位の定義
2619        direction_ranges = self._define_direction_ranges(
2620            num_directions, center_on_angles
2621        )
2622
2623        # 方位ごとのデータを集計
2624        direction_data = self._aggregate_direction_data(
2625            df_with_sources, col_wind_dir, direction_ranges
2626        )
2627
2628        # プロットの作成
2629        fig = plt.figure(figsize=figsize)
2630        ax = fig.add_subplot(111, projection="polar")
2631
2632        # 方位の角度(ラジアン)を計算
2633        theta = np.array(
2634            [np.radians(angle) for angle in direction_data["center_angle"]]
2635        )
2636
2637        # セクターの幅を計算(隙間を考慮)
2638        sector_width = np.radians((360.0 / num_directions) - gap_degrees)
2639
2640        # 積み上げ方式に応じてプロット
2641        if stack_bars:
2642            # 生物起源を基準として描画
2643            ax.bar(
2644                theta,
2645                direction_data["bio_flux"],
2646                width=sector_width,  # 隙間を考慮した幅
2647                bottom=0.0,
2648                color=color_bio,
2649                alpha=flux_alpha,
2650                label=label_bio,
2651            )
2652            # 都市ガス起源を生物起源の上に積み上げ
2653            ax.bar(
2654                theta,
2655                direction_data["gas_flux"],
2656                width=sector_width,  # 隙間を考慮した幅
2657                bottom=direction_data["bio_flux"],
2658                color=color_gas,
2659                alpha=flux_alpha,
2660                label=label_gas,
2661            )
2662        else:
2663            # 両方を0から積み上げ
2664            ax.bar(
2665                theta,
2666                direction_data["bio_flux"],
2667                width=sector_width,  # 隙間を考慮した幅
2668                bottom=0.0,
2669                color=color_bio,
2670                alpha=flux_alpha,
2671                label=label_bio,
2672            )
2673            ax.bar(
2674                theta,
2675                direction_data["gas_flux"],
2676                width=sector_width,  # 隙間を考慮した幅
2677                bottom=0.0,
2678                color=color_gas,
2679                alpha=flux_alpha,
2680                label=label_gas,
2681            )
2682
2683        # y軸の範囲を設定
2684        if ymax is not None:
2685            ax.set_ylim(0, ymax)
2686        else:
2687            # データの最大値に基づいて自動設定
2688            max_value = max(
2689                direction_data["bio_flux"].max(), direction_data["gas_flux"].max()
2690            )
2691            ax.set_ylim(0, max_value * 1.1)  # 最大値の1.1倍を上限に設定
2692
2693        # 方位ラベルの設定
2694        ax.set_theta_zero_location("N")  # 北を上に設定
2695        ax.set_theta_direction(-1)  # 時計回りに設定
2696
2697        # 方位ラベルの表示
2698        labels = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"]
2699        angles = np.radians(np.linspace(0, 360, len(labels), endpoint=False))
2700        ax.set_xticks(angles)
2701        ax.set_xticklabels(labels)
2702
2703        # プロット領域の調整(上部と下部にスペースを確保)
2704        plt.subplots_adjust(
2705            top=0.8,  # 上部に20%のスペースを確保
2706            bottom=0.2,  # 下部に20%のスペースを確保(凡例用)
2707        )
2708
2709        # サブプロットラベルの追加(デフォルトは左上)
2710        if subplot_label:
2711            ax.text(
2712                0.01,
2713                0.99,
2714                subplot_label,
2715                transform=ax.transAxes,
2716            )
2717
2718        # 単位の追加(図の下部中央に配置)
2719        plt.figtext(
2720            0.5,  # x位置(中央)
2721            0.1,  # y位置(下部)
2722            flux_unit,
2723            ha="center",  # 水平方向の位置揃え
2724            va="bottom",  # 垂直方向の位置揃え
2725        )
2726
2727        # 凡例の追加(単位の下に配置)
2728        if add_legend:
2729            # 最初のプロットから凡例のハンドルとラベルを取得
2730            handles, labels = ax.get_legend_handles_labels()
2731            # 図の下部に凡例を配置
2732            fig.legend(
2733                handles,
2734                labels,
2735                loc="center",
2736                bbox_to_anchor=(0.5, 0.05),  # x=0.5で中央、y=0.05で下部に配置
2737                ncol=len(handles),  # ハンドルの数だけ列を作成(一行に表示)
2738            )
2739
2740        # グラフの保存
2741        if save_fig:
2742            if output_dir is None:
2743                raise ValueError(
2744                    "save_fig=Trueのとき、output_dirに有効なパスを指定する必要があります。"
2745                )
2746            # 出力ディレクトリの作成
2747            os.makedirs(output_dir, exist_ok=True)
2748            output_path: str = os.path.join(output_dir, output_filename)
2749            plt.savefig(output_path, dpi=300, bbox_inches="tight")
2750
2751        # グラフの表示
2752        if show_fig:
2753            plt.show()
2754        else:
2755            plt.close(fig=fig)
2756
2757        # 統計情報の表示
2758        if print_summary:
2759            for source in ["gas", "bio"]:
2760                flux_data = direction_data[f"{source}_flux"]
2761                mean_val = flux_data.mean()
2762                max_val = flux_data.max()
2763                max_dir = direction_data.loc[flux_data.idxmax(), "name"]
2764
2765                self.logger.info(
2766                    f"{label_gas if source == 'gas' else label_bio}の統計:"
2767                )
2768                print(f"  平均フラックス: {mean_val:.2f}")
2769                print(f"  最大フラックス: {max_val:.2f}")
2770                print(f"  最大フラックスの方位: {max_dir}")

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

Parameters

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

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

Parameters

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

Returns

pd.DataFrame
    有効なデータのみを含むDataFrame
@staticmethod
def setup_logger(logger: logging.Logger | None, log_level: int = 20) -> logging.Logger:
3076    @staticmethod
3077    def setup_logger(logger: Logger | None, log_level: int = INFO) -> Logger:
3078        """
3079        ロガーを設定します。
3080
3081        このメソッドは、ロギングの設定を行い、ログメッセージのフォーマットを指定します。
3082        ログメッセージには、日付、ログレベル、メッセージが含まれます。
3083
3084        渡されたロガーがNoneまたは不正な場合は、新たにロガーを作成し、標準出力に
3085        ログメッセージが表示されるようにStreamHandlerを追加します。ロガーのレベルは
3086        引数で指定されたlog_levelに基づいて設定されます。
3087
3088        Parameters
3089        ------
3090            logger : Logger | None
3091                使用するロガー。Noneの場合は新しいロガーを作成します。
3092            log_level : int
3093                ロガーのログレベル。デフォルトはINFO。
3094
3095        Returns
3096        ------
3097            Logger
3098                設定されたロガーオブジェクト。
3099        """
3100        if logger is not None and isinstance(logger, Logger):
3101            return logger
3102        # 渡されたロガーがNoneまたは正しいものでない場合は独自に設定
3103        new_logger: Logger = getLogger()
3104        # 既存のハンドラーをすべて削除
3105        for handler in new_logger.handlers[:]:
3106            new_logger.removeHandler(handler)
3107        new_logger.setLevel(log_level)  # ロガーのレベルを設定
3108        ch = StreamHandler()
3109        ch_formatter = Formatter("%(asctime)s - %(levelname)s - %(message)s")
3110        ch.setFormatter(ch_formatter)  # フォーマッターをハンドラーに設定
3111        new_logger.addHandler(ch)  # StreamHandlerの追加
3112        return new_logger

ロガーを設定します。

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

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

Parameters

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

Returns

Logger
    設定されたロガーオブジェクト。
@staticmethod
def plot_fluxes_distributions( flux_data: dict[str, pandas.core.series.Series], month: int, output_dir: str | pathlib.Path | None = None, output_filename: str = 'flux_distribution.png', colors: dict[str, str] | None = None, xlim: tuple[float, float] = (-50, 200), bandwidth: float = 1.0, save_fig: bool = True, show_fig: bool = True) -> None:
3114    @staticmethod
3115    def plot_fluxes_distributions(
3116        flux_data: dict[str, pd.Series],
3117        month: int,
3118        output_dir: str | Path | None = None,
3119        output_filename: str = "flux_distribution.png",
3120        colors: dict[str, str] | None = None,
3121        xlim: tuple[float, float] = (-50, 200),
3122        bandwidth: float = 1.0,
3123        save_fig: bool = True,
3124        show_fig: bool = True,
3125    ) -> None:
3126        """複数のフラックスデータの分布を可視化
3127
3128        Parameters
3129        ------
3130            flux_data : dict[str, pd.Series]
3131                各測器のフラックスデータを格納した辞書
3132                キー: 測器名, 値: フラックスデータ
3133            month : int
3134                測定月
3135            output_dir : str | Path | None
3136                出力ディレクトリ。指定しない場合はデフォルトのディレクトリに保存されます。
3137            output_filename : str
3138                出力ファイル名。デフォルトは"flux_distribution.png"です。
3139            colors : dict[str, str] | None
3140                各測器の色を指定する辞書。指定がない場合は自動で色を割り当てます。
3141            xlim : tuple[float, float]
3142                x軸の範囲。デフォルトは(-50, 200)です。
3143            bandwidth : float
3144                カーネル密度推定のバンド幅調整係数。デフォルトは1.0です。
3145            save_fig : bool
3146                図を保存するかどうか。デフォルトはTrueです。
3147            show_fig : bool
3148                図を表示するかどうか。デフォルトはTrueです。
3149        """
3150        # デフォルトの色を設定
3151        default_colors = plt.rcParams["axes.prop_cycle"].by_key()["color"]
3152        if colors is None:
3153            colors = {
3154                name: default_colors[i % len(default_colors)]
3155                for i, name in enumerate(flux_data.keys())
3156            }
3157
3158        fig = plt.figure(figsize=(10, 6))
3159
3160        # 統計情報を格納する辞書
3161        stats_info = {}
3162
3163        # 各測器のデータをプロット
3164        for i, (name, flux) in enumerate(flux_data.items()):
3165            # nanを除去
3166            flux = flux.dropna()
3167            color = colors.get(name, default_colors[i % len(default_colors)])
3168
3169            # KDEプロット
3170            sns.kdeplot(
3171                data=flux,
3172                label=name,
3173                color=color,
3174                alpha=0.5,
3175                bw_adjust=bandwidth,
3176            )
3177
3178            # 平均値と中央値のマーカー
3179            mean_val = flux.mean()
3180            median_val = np.median(flux)
3181            plt.axvline(
3182                mean_val,
3183                color=color,
3184                linestyle="--",
3185                alpha=0.5,
3186                label=f"{name} mean",
3187            )
3188            plt.axvline(
3189                median_val,
3190                color=color,
3191                linestyle=":",
3192                alpha=0.5,
3193                label=f"{name} median",
3194            )
3195
3196            # 統計情報を保存
3197            stats_info[name] = {
3198                "mean": mean_val,
3199                "median": median_val,
3200                "std": flux.std(),
3201            }
3202
3203        # 軸ラベルとタイトル
3204        plt.xlabel(r"CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)")
3205        plt.ylabel("Probability Density")
3206        plt.title(f"Distribution of CH$_4$ fluxes - Month {month}")
3207
3208        # x軸の範囲設定
3209        plt.xlim(xlim)
3210
3211        # グリッド表示
3212        plt.grid(True, alpha=0.3)
3213
3214        # 統計情報のテキスト作成
3215        stats_text = ""
3216        for name, stats_item in stats_info.items():
3217            stats_text += (
3218                f"{name}:\n"
3219                f"  Mean: {stats_item['mean']:.2f}\n"
3220                f"  Median: {stats_item['median']:.2f}\n"
3221                f"  Std: {stats_item['std']:.2f}\n"
3222            )
3223
3224        # 統計情報の表示
3225        plt.text(
3226            0.02,
3227            0.98,
3228            stats_text.rstrip(),  # 最後の改行を削除
3229            transform=plt.gca().transAxes,
3230            verticalalignment="top",
3231            fontsize=10,
3232            bbox=dict(boxstyle="round", facecolor="white", alpha=0.8),
3233        )
3234
3235        # 凡例の表示
3236        plt.legend(loc="upper right")
3237        plt.tight_layout()
3238
3239        # グラフの保存
3240        if save_fig:
3241            if output_dir is None:
3242                raise ValueError(
3243                    "save_fig=Trueのとき、output_dirに有効なディレクトリパスを指定する必要があります。"
3244                )
3245            os.makedirs(output_dir, exist_ok=True)
3246            plt.savefig(
3247                os.path.join(output_dir, f"{output_filename.format(month=month)}"),
3248                dpi=300,
3249                bbox_inches="tight",
3250            )
3251        if show_fig:
3252            plt.show()
3253        else:
3254            plt.close(fig=fig)

複数のフラックスデータの分布を可視化

Parameters

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

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

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

TransferFunctionCalculator( file_path: str, col_freq: str, cutoff_freq_low: float = 0.01, cutoff_freq_high: float = 1)
18    def __init__(
19        self,
20        file_path: str,
21        col_freq: str,
22        cutoff_freq_low: float = 0.01,
23        cutoff_freq_high: float = 1,
24    ):
25        """
26        TransferFunctionCalculatorクラスのコンストラクタ。
27
28        Parameters
29        ----------
30            file_path : str
31                分析対象のCSVファイルのパス。
32            col_freq : str
33                周波数のキー。
34            cutoff_freq_low : float
35                カットオフ周波数の最低値。
36            cutoff_freq_high : float
37                カットオフ周波数の最高値。
38        """
39        self._col_freq: str = col_freq
40        self._cutoff_freq_low: float = cutoff_freq_low
41        self._cutoff_freq_high: float = cutoff_freq_high
42        self._df: pd.DataFrame = TransferFunctionCalculator._load_data(file_path)

TransferFunctionCalculatorクラスのコンストラクタ。

Parameters

file_path : str
    分析対象のCSVファイルのパス。
col_freq : str
    周波数のキー。
cutoff_freq_low : float
    カットオフ周波数の最低値。
cutoff_freq_high : float
    カットオフ周波数の最高値。
def calculate_transfer_function( self, col_reference: str, col_target: str) -> tuple[float, float, pandas.core.frame.DataFrame]:
44    def calculate_transfer_function(
45        self, col_reference: str, col_target: str
46    ) -> tuple[float, float, pd.DataFrame]:
47        """
48        伝達関数の係数を計算する。
49
50        Parameters
51        ----------
52            col_reference : str
53                参照データのカラム名。
54            col_target : str
55                ターゲットデータのカラム名。
56
57        Returns
58        ----------
59            tuple[float, float, pandas.DataFrame]
60                伝達関数の係数aとその標準誤差、および計算に用いたDataFrame。
61        """
62        df_processed: pd.DataFrame = self.process_data(
63            col_reference=col_reference, col_target=col_target
64        )
65        df_cutoff: pd.DataFrame = self._cutoff_df(df_processed)
66
67        array_x = np.array(df_cutoff.index)
68        array_y = np.array(df_cutoff["target"] / df_cutoff["reference"])
69
70        # フィッティングパラメータと共分散行列を取得
71        popt, pcov = curve_fit(
72            TransferFunctionCalculator.transfer_function, array_x, array_y
73        )
74
75        # 標準誤差を計算(共分散行列の対角成分の平方根)
76        perr = np.sqrt(np.diag(pcov))
77
78        # 係数aとその標準誤差、および計算に用いたDataFrameを返す
79        return popt[0], perr[0], df_processed

伝達関数の係数を計算する。

Parameters

col_reference : str
    参照データのカラム名。
col_target : str
    ターゲットデータのカラム名。

Returns

tuple[float, float, pandas.DataFrame]
    伝達関数の係数aとその標準誤差、および計算に用いたDataFrame。
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 | pathlib.Path | 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:
 81    def create_plot_co_spectra(
 82        self,
 83        col1: str,
 84        col2: str,
 85        color1: str = "gray",
 86        color2: str = "red",
 87        figsize: tuple[int, int] = (10, 8),
 88        label1: str | None = None,
 89        label2: str | None = None,
 90        output_dir: str | Path | None = None,
 91        output_basename: str = "co",
 92        add_legend: bool = True,
 93        add_xy_labels: bool = True,
 94        show_fig: bool = True,
 95        subplot_label: str | None = "(a)",
 96        window_size: int = 5,  # 移動平均の窓サイズ
 97        markersize: float = 14,
 98    ) -> None:
 99        """
100        2種類のコスペクトルをプロットする。
101
102        Parameters
103        ----------
104            col1 : str
105                1つ目のコスペクトルデータのカラム名。
106            col2 : str
107                2つ目のコスペクトルデータのカラム名。
108            color1 : str, optional
109                1つ目のデータの色。デフォルトは'gray'。
110            color2 : str, optional
111                2つ目のデータの色。デフォルトは'red'。
112            figsize : tuple[int, int], optional
113                プロットのサイズ。デフォルトは(10, 8)。
114            label1 : str, optional
115                1つ目のデータのラベル名。デフォルトはNone。
116            label2 : str, optional
117                2つ目のデータのラベル名。デフォルトはNone。
118            output_dir : str | Path | None, optional
119                プロットを保存するディレクトリ。デフォルトはNoneで、保存しない。
120            output_basename : str, optional
121                保存するファイル名のベース。デフォルトは"co"。
122            show_fig : bool, optional
123                プロットを表示するかどうか。デフォルトはTrue。
124            subplot_label : str | None, optional
125                左上に表示するサブプロットラベル。デフォルトは"(a)"。
126            window_size : int, optional
127                移動平均の窓サイズ。デフォルトは5。
128        """
129        df_copied: pd.DataFrame = self._df.copy()
130        # データの取得と移動平均の適用
131        data1 = df_copied[df_copied[col1] > 0].groupby(self._col_freq)[col1].median()
132        data2 = df_copied[df_copied[col2] > 0].groupby(self._col_freq)[col2].median()
133
134        data1 = data1.rolling(window=window_size, center=True, min_periods=1).mean()
135        data2 = data2.rolling(window=window_size, center=True, min_periods=1).mean()
136
137        fig = plt.figure(figsize=figsize)
138        ax = fig.add_subplot(111)
139
140        # マーカーサイズを設定して見やすくする
141        ax.plot(
142            data1.index, data1, "o", color=color1, label=label1, markersize=markersize
143        )
144        ax.plot(
145            data2.index, data2, "o", color=color2, label=label2, markersize=markersize
146        )
147        ax.plot([0.01, 10], [10, 0.001], "-", color="black")
148        ax.text(0.25, 0.4, "-4/3")
149
150        ax.grid(True, alpha=0.3)
151        ax.set_xscale("log")
152        ax.set_yscale("log")
153        ax.set_xlim(0.0001, 10)
154        ax.set_ylim(0.0001, 10)
155        if add_xy_labels:
156            ax.set_xlabel("f (Hz)")
157            ax.set_ylabel("無次元コスペクトル")
158
159        if add_legend:
160            ax.legend(
161                bbox_to_anchor=(0.05, 1),
162                loc="lower left",
163                fontsize=16,
164                ncol=3,
165                frameon=False,
166            )
167        if subplot_label is not None:
168            ax.text(0.00015, 3, subplot_label)
169        fig.tight_layout()
170
171        if output_dir is not None:
172            os.makedirs(output_dir, exist_ok=True)
173            # プロットをPNG形式で保存
174            filename: str = f"{output_basename}.png"
175            fig.savefig(os.path.join(output_dir, filename), dpi=300)
176        if show_fig:
177            plt.show()
178        else:
179            plt.close(fig=fig)

2種類のコスペクトルをプロットする。

Parameters

col1 : str
    1つ目のコスペクトルデータのカラム名。
col2 : str
    2つ目のコスペクトルデータのカラム名。
color1 : str, optional
    1つ目のデータの色。デフォルトは'gray'。
color2 : str, optional
    2つ目のデータの色。デフォルトは'red'。
figsize : tuple[int, int], optional
    プロットのサイズ。デフォルトは(10, 8)。
label1 : str, optional
    1つ目のデータのラベル名。デフォルトはNone。
label2 : str, optional
    2つ目のデータのラベル名。デフォルトはNone。
output_dir : str | Path | None, optional
    プロットを保存するディレクトリ。デフォルトはNoneで、保存しない。
output_basename : str, optional
    保存するファイル名のベース。デフォルトは"co"。
show_fig : bool, optional
    プロットを表示するかどうか。デフォルトはTrue。
subplot_label : str | None, optional
    左上に表示するサブプロットラベル。デフォルトは"(a)"。
window_size : int, optional
    移動平均の窓サイズ。デフォルトは5。
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 | pathlib.Path | None = None, output_basename: str = 'ratio', show_fig: bool = True) -> None:
181    def create_plot_ratio(
182        self,
183        df_processed: pd.DataFrame,
184        reference_name: str,
185        target_name: str,
186        figsize: tuple[int, int] = (10, 6),
187        output_dir: str | Path | None = None,
188        output_basename: str = "ratio",
189        show_fig: bool = True,
190    ) -> None:
191        """
192        ターゲットと参照の比率をプロットする。
193
194        Parameters
195        ----------
196            df_processed : pd.DataFrame
197                処理されたデータフレーム。
198            reference_name : str
199                参照の名前。
200            target_name : str
201                ターゲットの名前。
202            figsize : tuple[int, int], optional
203                プロットのサイズ。デフォルトは(10, 6)。
204            output_dir : str | Path | None, optional
205                プロットを保存するディレクトリ。デフォルトはNoneで、保存しない。
206            output_basename : str, optional
207                保存するファイル名のベース。デフォルトは"ratio"。
208            show_fig : bool, optional
209                プロットを表示するかどうか。デフォルトはTrue。
210        """
211        fig = plt.figure(figsize=figsize)
212        ax = fig.add_subplot(111)
213
214        ax.plot(
215            df_processed.index, df_processed["target"] / df_processed["reference"], "o"
216        )
217        ax.set_xscale("log")
218        ax.set_yscale("log")
219        ax.set_xlabel("f (Hz)")
220        ax.set_ylabel(f"{target_name} / {reference_name}")
221        ax.set_title(f"{target_name}{reference_name}の比")
222
223        if output_dir is not None:
224            # プロットをPNG形式で保存
225            filename: str = f"{output_basename}-{reference_name}_{target_name}.png"
226            fig.savefig(os.path.join(output_dir, filename), dpi=300)
227        if show_fig:
228            plt.show()
229        else:
230            plt.close(fig=fig)

ターゲットと参照の比率をプロットする。

Parameters

df_processed : pd.DataFrame
    処理されたデータフレーム。
reference_name : str
    参照の名前。
target_name : str
    ターゲットの名前。
figsize : tuple[int, int], optional
    プロットのサイズ。デフォルトは(10, 6)。
output_dir : str | Path | None, optional
    プロットを保存するディレクトリ。デフォルトはNoneで、保存しない。
output_basename : str, optional
    保存するファイル名のベース。デフォルトは"ratio"。
show_fig : bool, optional
    プロットを表示するかどうか。デフォルトはTrue。
@classmethod
def create_plot_tf_curves_from_csv( cls, file_path: str, gas_configs: list[tuple[str, str, str, str]], output_dir: str | pathlib.Path | 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:
232    @classmethod
233    def create_plot_tf_curves_from_csv(
234        cls,
235        file_path: str,
236        gas_configs: list[tuple[str, str, str, str]],
237        output_dir: str | Path | None = None,
238        output_basename: str = "all_tf_curves",
239        col_datetime: str = "Date",
240        add_xlabel: bool = True,
241        label_x: str = "f (Hz)",
242        label_y: str = "無次元コスペクトル比",
243        label_avg: str = "Avg.",
244        label_co_ref: str = "Tv",
245        line_colors: list[str] | None = None,
246        font_family: list[str] = ["Arial", "MS Gothic"],
247        font_size: float = 20,
248        save_fig: bool = True,
249        show_fig: bool = True,
250    ) -> None:
251        """
252        複数の伝達関数の係数をプロットし、各ガスの平均値を表示します。
253        各ガスのデータをCSVファイルから読み込み、指定された設定に基づいてプロットを生成します。
254        プロットはオプションで保存することも可能です。
255
256        Parameters
257        ----------
258            file_path : str
259                伝達関数の係数が格納されたCSVファイルのパス。
260            gas_configs : list[tuple[str, str, str, str]]
261                ガスごとの設定のリスト。各タプルは以下の要素を含む:
262                (係数のカラム名, ガスの表示ラベル, 平均線の色, 出力ファイル用のガス名)
263                例: [("a_ch4-used", "CH$_4$", "red", "ch4")]
264            output_dir : str | Path | None, optional
265                出力ディレクトリ。Noneの場合は保存しない。
266            output_basename : str, optional
267                出力ファイル名のベース。デフォルトは"all_tf_curves"。
268            col_datetime : str, optional
269                日付情報が格納されているカラム名。デフォルトは"Date"。
270            add_xlabel : bool, optional
271                x軸ラベルを追加するかどうか。デフォルトはTrue。
272            label_x : str, optional
273                x軸のラベル。デフォルトは"f (Hz)"。
274            label_y : str, optional
275                y軸のラベル。デフォルトは"無次元コスペクトル比"。
276            label_avg : str, optional
277                平均値のラベル。デフォルトは"Avg."。
278            line_colors : list[str] | None, optional
279                各日付のデータに使用する色のリスト。
280            font_family : list[str], optional
281                使用するフォントファミリーのリスト。
282            font_size : float, optional
283                フォントサイズ。
284            save_fig : bool, optional
285                プロットを保存するかどうか。デフォルトはTrue。
286            show_fig : bool, optional
287                プロットを表示するかどうか。デフォルトはTrue。
288        """
289        # プロットパラメータの設定
290        plt.rcParams.update(
291            {
292                "font.family": font_family,
293                "font.size": font_size,
294                "axes.labelsize": font_size,
295                "axes.titlesize": font_size,
296                "xtick.labelsize": font_size,
297                "ytick.labelsize": font_size,
298                "legend.fontsize": font_size,
299            }
300        )
301
302        # CSVファイルを読み込む
303        df = pd.read_csv(file_path)
304
305        # 各ガスについてプロット
306        for col_coef_a, label_gas, base_color, gas_name in gas_configs:
307            fig = plt.figure(figsize=(10, 6))
308
309            # データ数に応じたデフォルトの色リストを作成
310            if line_colors is None:
311                default_colors = [
312                    "#1f77b4",
313                    "#ff7f0e",
314                    "#2ca02c",
315                    "#d62728",
316                    "#9467bd",
317                    "#8c564b",
318                    "#e377c2",
319                    "#7f7f7f",
320                    "#bcbd22",
321                    "#17becf",
322                ]
323                n_dates = len(df)
324                plot_colors = (default_colors * (n_dates // len(default_colors) + 1))[
325                    :n_dates
326                ]
327            else:
328                plot_colors = line_colors
329
330            # 全てのa値を用いて伝達関数をプロット
331            for i, row in enumerate(df.iterrows()):
332                a = row[1][col_coef_a]
333                date = row[1][col_datetime]
334                x_fit = np.logspace(-3, 1, 1000)
335                y_fit = cls.transfer_function(x_fit, a)
336                plt.plot(
337                    x_fit,
338                    y_fit,
339                    "-",
340                    color=plot_colors[i],
341                    alpha=0.7,
342                    label=f"{date} (a = {a:.3f})",
343                )
344
345            # 平均のa値を用いた伝達関数をプロット
346            a_mean = df[col_coef_a].mean()
347            x_fit = np.logspace(-3, 1, 1000)
348            y_fit = cls.transfer_function(x_fit, a_mean)
349            plt.plot(
350                x_fit,
351                y_fit,
352                "-",
353                color=base_color,
354                linewidth=3,
355                label=f"{label_avg} (a = {a_mean:.3f})",
356            )
357
358            # グラフの設定
359            label_y_formatted: str = f"{label_y}\n({label_gas} / {label_co_ref})"
360            plt.xscale("log")
361            if add_xlabel:
362                plt.xlabel(label_x)
363            plt.ylabel(label_y_formatted)
364            plt.legend(loc="lower left", fontsize=font_size - 6)
365            plt.grid(True, which="both", ls="-", alpha=0.2)
366            plt.tight_layout()
367
368            if save_fig:
369                if output_dir is None:
370                    raise ValueError(
371                        "save_fig=Trueのとき、output_dirに有効なディレクトリパスを指定する必要があります。"
372                    )
373                os.makedirs(output_dir, exist_ok=True)
374                output_path: str = os.path.join(
375                    output_dir, f"{output_basename}-{gas_name}.png"
376                )
377                plt.savefig(output_path, dpi=300, bbox_inches="tight")
378            if show_fig:
379                plt.show()
380            else:
381                plt.close(fig=fig)

複数の伝達関数の係数をプロットし、各ガスの平均値を表示します。 各ガスのデータをCSVファイルから読み込み、指定された設定に基づいてプロットを生成します。 プロットはオプションで保存することも可能です。

Parameters

file_path : str
    伝達関数の係数が格納されたCSVファイルのパス。
gas_configs : list[tuple[str, str, str, str]]
    ガスごとの設定のリスト。各タプルは以下の要素を含む:
    (係数のカラム名, ガスの表示ラベル, 平均線の色, 出力ファイル用のガス名)
    例: [("a_ch4-used", "CH$_4$", "red", "ch4")]
output_dir : str | Path | None, optional
    出力ディレクトリ。Noneの場合は保存しない。
output_basename : str, optional
    出力ファイル名のベース。デフォルトは"all_tf_curves"。
col_datetime : str, optional
    日付情報が格納されているカラム名。デフォルトは"Date"。
add_xlabel : bool, optional
    x軸ラベルを追加するかどうか。デフォルトはTrue。
label_x : str, optional
    x軸のラベル。デフォルトは"f (Hz)"。
label_y : str, optional
    y軸のラベル。デフォルトは"無次元コスペクトル比"。
label_avg : str, optional
    平均値のラベル。デフォルトは"Avg."。
line_colors : list[str] | None, optional
    各日付のデータに使用する色のリスト。
font_family : list[str], optional
    使用するフォントファミリーのリスト。
font_size : float, optional
    フォントサイズ。
save_fig : bool, optional
    プロットを保存するかどうか。デフォルトはTrue。
show_fig : bool, optional
    プロットを表示するかどうか。デフォルトはTrue。
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 | pathlib.Path | 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:
383    def create_plot_transfer_function(
384        self,
385        a: float,
386        df_processed: pd.DataFrame,
387        reference_name: str,
388        target_name: str,
389        figsize: tuple[int, int] = (10, 6),
390        output_dir: str | Path | None = None,
391        output_basename: str = "tf",
392        show_fig: bool = True,
393        add_xlabel: bool = True,
394        label_x: str = "f (Hz)",
395        label_y: str = "コスペクトル比",
396        label_gas: str | None = None,
397    ) -> None:
398        """
399        伝達関数とそのフィットをプロットする。
400
401        Parameters
402        ----------
403            a : float
404                伝達関数の係数。
405            df_processed : pd.DataFrame
406                処理されたデータフレーム。
407            reference_name : str
408                参照の名前。
409            target_name : str
410                ターゲットの名前。
411            figsize : tuple[int, int], optional
412                プロットのサイズ。デフォルトは(10, 6)。
413            output_dir : str | Path | None, optional
414                プロットを保存するディレクトリ。デフォルトはNoneで、保存しない。
415            output_basename : str, optional
416                保存するファイル名のベース。デフォルトは"tf"。
417            show_fig : bool, optional
418                プロットを表示するかどうか。デフォルトはTrue。
419        """
420        df_cutoff: pd.DataFrame = self._cutoff_df(df_processed)
421
422        fig = plt.figure(figsize=figsize)
423        ax = fig.add_subplot(111)
424
425        ax.plot(
426            df_cutoff.index,
427            df_cutoff["target"] / df_cutoff["reference"],
428            "o",
429            label=f"{target_name} / {reference_name}",
430        )
431
432        x_fit = np.logspace(
433            np.log10(self._cutoff_freq_low), np.log10(self._cutoff_freq_high), 1000
434        )
435        y_fit = self.transfer_function(x_fit, a)
436        ax.plot(x_fit, y_fit, "-", label=f"フィット (a = {a:.4f})")
437
438        ax.set_xscale("log")
439        # グラフの設定
440        label_y_formatted: str = f"{label_y}\n({label_gas} / 顕熱)"
441        plt.xscale("log")
442        if add_xlabel:
443            plt.xlabel(label_x)
444        plt.ylabel(label_y_formatted)
445        ax.legend()
446
447        if output_dir is not None:
448            # プロットをPNG形式で保存
449            filename: str = f"{output_basename}-{reference_name}_{target_name}.png"
450            fig.savefig(os.path.join(output_dir, filename), dpi=300)
451        if show_fig:
452            plt.show()
453        else:
454            plt.close(fig=fig)

伝達関数とそのフィットをプロットする。

Parameters

a : float
    伝達関数の係数。
df_processed : pd.DataFrame
    処理されたデータフレーム。
reference_name : str
    参照の名前。
target_name : str
    ターゲットの名前。
figsize : tuple[int, int], optional
    プロットのサイズ。デフォルトは(10, 6)。
output_dir : str | Path | None, optional
    プロットを保存するディレクトリ。デフォルトはNoneで、保存しない。
output_basename : str, optional
    保存するファイル名のベース。デフォルトは"tf"。
show_fig : bool, optional
    プロットを表示するかどうか。デフォルトはTrue。
def process_data(self, col_reference: str, col_target: str) -> pandas.core.frame.DataFrame:
456    def process_data(self, col_reference: str, col_target: str) -> pd.DataFrame:
457        """
458        指定されたキーに基づいてデータを処理する。
459
460        Parameters
461        ----------
462            col_reference : str
463                参照データのカラム名。
464            col_target : str
465                ターゲットデータのカラム名。
466
467        Returns
468        ----------
469            pd.DataFrame
470                処理されたデータフレーム。
471        """
472        df_copied: pd.DataFrame = self._df.copy()
473        col_freq: str = self._col_freq
474
475        # データ型の確認と変換
476        df_copied[col_freq] = pd.to_numeric(df_copied[col_freq], errors="coerce")
477        df_copied[col_reference] = pd.to_numeric(df_copied[col_reference], errors="coerce")
478        df_copied[col_target] = pd.to_numeric(df_copied[col_target], errors="coerce")
479
480        # NaNを含む行を削除
481        df_copied = df_copied.dropna(subset=[col_freq, col_reference, col_target])
482
483        # グループ化と中央値の計算
484        grouped = df_copied.groupby(col_freq)
485        reference_data = grouped[col_reference].median()
486        target_data = grouped[col_target].median()
487
488        df_processed = pd.DataFrame(
489            {"reference": reference_data, "target": target_data}
490        )
491
492        # 異常な比率を除去
493        df_processed.loc[
494            (
495                (df_processed["target"] / df_processed["reference"] > 1)
496                | (df_processed["target"] / df_processed["reference"] < 0)
497            )
498        ] = np.nan
499        df_processed = df_processed.dropna()
500
501        return df_processed

指定されたキーに基づいてデータを処理する。

Parameters

col_reference : str
    参照データのカラム名。
col_target : str
    ターゲットデータのカラム名。

Returns

pd.DataFrame
    処理されたデータフレーム。
@classmethod
def transfer_function(cls, x: numpy.ndarray, a: float) -> numpy.ndarray:
522    @classmethod
523    def transfer_function(cls, x: np.ndarray, a: float) -> np.ndarray:
524        """
525        伝達関数を計算する。
526
527        Parameters
528        ----------
529            x : np.ndarray
530                周波数の配列。
531            a : float
532                伝達関数の係数。
533
534        Returns
535        ----------
536            np.ndarray
537                伝達関数の値。
538        """
539        return np.exp(-np.log(np.sqrt(2)) * np.power(x / a, 2))

伝達関数を計算する。

Parameters

x : np.ndarray
    周波数の配列。
a : float
    伝達関数の係数。

Returns

np.ndarray
    伝達関数の値。