py_flux_tracer

 1from .campbell.eddy_data_figures_generator import (
 2    EddyDataFiguresGenerator,
 3    SlopeLine,
 4    SpectralPlotConfig,
 5)
 6from .campbell.eddy_data_preprocessor import EddyDataPreprocessor, MeasuredWindKeyType
 7from .campbell.spectrum_calculator import SpectrumCalculator, WindowFunctionType
 8from .commons.utilities import setup_logger, setup_plot_params
 9from .footprint.flux_footprint_analyzer import FluxFootprintAnalyzer
10from .mobile.correcting_utils import (
11    BiasRemovalConfig,
12    CorrectingUtils,
13    H2OCorrectionConfig,
14)
15from .mobile.hotspot_emission_analyzer import (
16    EmissionData,
17    EmissionFormula,
18    HotspotEmissionAnalyzer,
19    HotspotEmissionConfig,
20)
21from .mobile.mobile_measurement_analyzer import (
22    HotspotData,
23    HotspotParams,
24    HotspotType,
25    MobileMeasurementAnalyzer,
26    MobileMeasurementConfig,
27)
28from .monthly.monthly_converter import MonthlyConverter
29from .monthly.monthly_figures_generator import MonthlyFiguresGenerator
30from .transfer_function.fft_files_reorganizer import FftFileReorganizer
31from .transfer_function.transfer_function_calculator import (
32    TfCurvesFromCsvConfig,
33    TransferFunctionCalculator,
34)
35
36"""
37versionを動的に設定する。
38`./_version.py`がない場合はsetuptools_scmを用いてGitからバージョン取得を試行
39それも失敗した場合にデフォルトバージョン(0.0.0)を設定
40"""
41try:
42    from ._version import __version__  # type:ignore
43except ImportError:
44    try:
45        from setuptools_scm import get_version
46
47        __version__ = get_version(root="..", relative_to=__file__)
48    except Exception:
49        __version__ = "0.0.0"
50
51# モジュールを __all__ にセット
52__all__ = [
53    "BiasRemovalConfig",
54    "CorrectingUtils",
55    "EddyDataFiguresGenerator",
56    "EddyDataPreprocessor",
57    "EmissionData",
58    "EmissionFormula",
59    "FftFileReorganizer",
60    "FluxFootprintAnalyzer",
61    "H2OCorrectionConfig",
62    "HotspotData",
63    "HotspotEmissionAnalyzer",
64    "HotspotEmissionConfig",
65    "HotspotParams",
66    "HotspotType",
67    "MeasuredWindKeyType",
68    "MobileMeasurementAnalyzer",
69    "MobileMeasurementConfig",
70    "MonthlyConverter",
71    "MonthlyFiguresGenerator",
72    "SlopeLine",
73    "SpectralPlotConfig",
74    "SpectrumCalculator",
75    "TfCurvesFromCsvConfig",
76    "TransferFunctionCalculator",
77    "WindowFunctionType",
78    "__version__",
79    "setup_logger",
80    "setup_plot_params",
81]
@dataclass
class BiasRemovalConfig:
39@dataclass
40class BiasRemovalConfig:
41    """バイアス除去の設定を保持するデータクラス
42
43    Parameters
44    ----------
45        quantile_value: float, optional
46            バイアス除去に使用するクォンタイル値。デフォルト値は0.05。
47        base_ch4_ppm: float, optional
48            補正前の値から最小値を引いた後に足すCH4濃度の基準値。デフォルト値は2.0。
49        base_c2h6_ppb: float, optional
50            補正前の値から最小値を引いた後に足すC2H6濃度の基準値。デフォルト値は0。
51
52    Examples
53    --------
54    >>> config = BiasRemovalConfig(
55    ...     quantile_value=0.05,
56    ...     base_ch4_ppm=2.0,
57    ...     base_c2h6_ppb=0
58    ... )
59    """
60
61    quantile_value: float = 0.05
62    base_ch4_ppm: float = 2.0
63    base_c2h6_ppb: float = 0
64
65    def __post_init__(self) -> None:
66        """パラメータの検証を行います。
67
68        Raises
69        ----------
70            ValueError: quantile_valueが0以上1未満でない場合
71        """
72        if not 0 <= self.quantile_value < 1:
73            raise ValueError(
74                f"quantile_value must be between 0 and 1, got {self.quantile_value}"
75            )

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

Parameters

quantile_value: float, optional
    バイアス除去に使用するクォンタイル値。デフォルト値は0.05。
base_ch4_ppm: float, optional
    補正前の値から最小値を引いた後に足すCH4濃度の基準値。デフォルト値は2.0。
base_c2h6_ppb: float, optional
    補正前の値から最小値を引いた後に足すC2H6濃度の基準値。デフォルト値は0。

Examples

>>> config = BiasRemovalConfig(
...     quantile_value=0.05,
...     base_ch4_ppm=2.0,
...     base_c2h6_ppb=0
... )
BiasRemovalConfig( quantile_value: float = 0.05, base_ch4_ppm: float = 2.0, base_c2h6_ppb: float = 0)
quantile_value: float = 0.05
base_ch4_ppm: float = 2.0
base_c2h6_ppb: float = 0
class CorrectingUtils:
 78class CorrectingUtils:
 79    """
 80    車載濃度観測で得たファイルのDataFrameを補正する関数をクラス化したものです。
 81    """
 82
 83    @staticmethod
 84    def correct_h2o_interference(
 85        df: pd.DataFrame,
 86        coef_b: float,
 87        coef_c: float,
 88        col_ch4_ppm: str = "ch4_ppm",
 89        col_h2o_ppm: str = "h2o_ppm",
 90        h2o_ppm_threshold: float | None = 2000,
 91        target_h2o_ppm: float = 10000,
 92    ) -> pd.DataFrame:
 93        """
 94        水蒸気干渉を補正するためのメソッドです。
 95        CH4濃度を特定の水蒸気濃度下での値に換算します。
 96
 97        Parameters
 98        ----------
 99            df: pd.DataFrame
100                補正対象のデータフレーム
101            coef_b: float
102                補正曲線の1次係数
103            coef_c: float
104                補正曲線の2次係数
105            col_ch4_ppm: str, optional
106                CH4濃度を示すカラム名。デフォルト値は"ch4_ppm"
107            col_h2o_ppm: str, optional
108                水蒸気濃度を示すカラム名。デフォルト値は"h2o_ppm"
109            h2o_ppm_threshold: float | None, optional
110                水蒸気濃度の下限値(この値未満のデータは除外)。デフォルト値は2000 ppm
111            target_h2o_ppm: float, optional
112                換算先の水蒸気濃度。デフォルト値は10000 ppm
113
114        Returns
115        ----------
116            pd.DataFrame
117                水蒸気干渉が補正されたデータフレーム
118
119        Examples
120        --------
121        >>> import pandas as pd
122        >>> import numpy as np
123        >>> # サンプルデータの作成
124        >>> df = pd.DataFrame({
125        ...     'ch4_ppm': [2.0, 2.1, 2.2],
126        ...     'h2o_ppm': [5000, 6000, 7000]
127        ... })
128        >>> # 水蒸気干渉の補正
129        >>> df_corrected = CorrectingUtils.correct_h2o_interference(
130        ...     df=df,
131        ...     coef_b=0.0001,
132        ...     coef_c=0.00001
133        ... )
134        """
135        # 元のデータを保護するためコピーを作成
136        df_h2o_corrected: pd.DataFrame = df.copy()
137        # 水蒸気濃度の配列を取得
138        h2o: np.ndarray = np.array(df_h2o_corrected[col_h2o_ppm])
139
140        # 現在の水蒸気濃度での補正項
141        term_ch4 = coef_b * h2o + coef_c * np.power(h2o, 2)
142
143        # 目標水蒸気濃度での補正項
144        target_term = coef_b * target_h2o_ppm + coef_c * np.power(target_h2o_ppm, 2)
145
146        # CH4濃度の補正(現在の補正項を引いて、目標水蒸気濃度での補正項を足す)
147        df_h2o_corrected[col_ch4_ppm] = (
148            df_h2o_corrected[col_ch4_ppm] - term_ch4 + target_term
149        )
150
151        # 極端に低い水蒸気濃度のデータは信頼性が低いため除外
152        if h2o_ppm_threshold is not None:
153            df_h2o_corrected.loc[df[col_h2o_ppm] < h2o_ppm_threshold, col_ch4_ppm] = (
154                np.nan
155            )
156            df_h2o_corrected = df_h2o_corrected.dropna(subset=[col_ch4_ppm])
157
158        return df_h2o_corrected
159
160    @staticmethod
161    def remove_bias(
162        df: pd.DataFrame,
163        col_ch4_ppm: str = "ch4_ppm",
164        col_c2h6_ppb: str = "c2h6_ppb",
165        base_ch4_ppm: float = 2.0,
166        base_c2h6_ppb: float = 0,
167        quantile_value: float = 0.05,
168    ) -> pd.DataFrame:
169        """
170        データフレームからバイアスを除去します。
171
172        Parameters
173        ----------
174            df: pd.DataFrame
175                バイアスを除去する対象のデータフレーム
176            col_ch4_ppm: str, optional
177                CH4濃度を示すカラム名。デフォルト値は"ch4_ppm"
178            col_c2h6_ppb: str, optional
179                C2H6濃度を示すカラム名。デフォルト値は"c2h6_ppb"
180            base_ch4_ppm: float, optional
181                補正前の値から最小値を引いた後に足すCH4濃度。デフォルト値は2.0
182            base_c2h6_ppb: float, optional
183                補正前の値から最小値を引いた後に足すC2H6濃度。デフォルト値は0
184            quantile_value: float, optional
185                下位何クォンタイルの値を最小値として補正を行うか。デフォルト値は0.05
186
187        Returns
188        ----------
189            pd.DataFrame
190                バイアスが除去されたデータフレーム
191
192        Examples
193        --------
194        >>> import pandas as pd
195        >>> # サンプルデータの作成
196        >>> df = pd.DataFrame({
197        ...     'ch4_ppm': [2.1, 2.2, 2.3],
198        ...     'c2h6_ppb': [1.1, 1.2, 1.3]
199        ... })
200        >>> # バイアスの除去
201        >>> df_unbiased = CorrectingUtils.remove_bias(
202        ...     df=df,
203        ...     base_ch4_ppm=2.0,
204        ...     base_c2h6_ppb=0
205        ... )
206        """
207        df_internal: pd.DataFrame = df.copy()
208        # CH4
209        ch4_min: float = df_internal[col_ch4_ppm].quantile(quantile_value)
210        df_internal[col_ch4_ppm] = df_internal[col_ch4_ppm] - ch4_min + base_ch4_ppm
211        # C2H6
212        c2h6_min: float = df_internal[col_c2h6_ppb].quantile(quantile_value)
213        df_internal[col_c2h6_ppb] = df_internal[col_c2h6_ppb] - c2h6_min + base_c2h6_ppb
214        return df_internal

車載濃度観測で得たファイルのDataFrameを補正する関数をクラス化したものです。

@staticmethod
def correct_h2o_interference( df: pandas.core.frame.DataFrame, coef_b: float, coef_c: float, col_ch4_ppm: str = 'ch4_ppm', col_h2o_ppm: str = 'h2o_ppm', h2o_ppm_threshold: float | None = 2000, target_h2o_ppm: float = 10000) -> pandas.core.frame.DataFrame:
 83    @staticmethod
 84    def correct_h2o_interference(
 85        df: pd.DataFrame,
 86        coef_b: float,
 87        coef_c: float,
 88        col_ch4_ppm: str = "ch4_ppm",
 89        col_h2o_ppm: str = "h2o_ppm",
 90        h2o_ppm_threshold: float | None = 2000,
 91        target_h2o_ppm: float = 10000,
 92    ) -> pd.DataFrame:
 93        """
 94        水蒸気干渉を補正するためのメソッドです。
 95        CH4濃度を特定の水蒸気濃度下での値に換算します。
 96
 97        Parameters
 98        ----------
 99            df: pd.DataFrame
100                補正対象のデータフレーム
101            coef_b: float
102                補正曲線の1次係数
103            coef_c: float
104                補正曲線の2次係数
105            col_ch4_ppm: str, optional
106                CH4濃度を示すカラム名。デフォルト値は"ch4_ppm"
107            col_h2o_ppm: str, optional
108                水蒸気濃度を示すカラム名。デフォルト値は"h2o_ppm"
109            h2o_ppm_threshold: float | None, optional
110                水蒸気濃度の下限値(この値未満のデータは除外)。デフォルト値は2000 ppm
111            target_h2o_ppm: float, optional
112                換算先の水蒸気濃度。デフォルト値は10000 ppm
113
114        Returns
115        ----------
116            pd.DataFrame
117                水蒸気干渉が補正されたデータフレーム
118
119        Examples
120        --------
121        >>> import pandas as pd
122        >>> import numpy as np
123        >>> # サンプルデータの作成
124        >>> df = pd.DataFrame({
125        ...     'ch4_ppm': [2.0, 2.1, 2.2],
126        ...     'h2o_ppm': [5000, 6000, 7000]
127        ... })
128        >>> # 水蒸気干渉の補正
129        >>> df_corrected = CorrectingUtils.correct_h2o_interference(
130        ...     df=df,
131        ...     coef_b=0.0001,
132        ...     coef_c=0.00001
133        ... )
134        """
135        # 元のデータを保護するためコピーを作成
136        df_h2o_corrected: pd.DataFrame = df.copy()
137        # 水蒸気濃度の配列を取得
138        h2o: np.ndarray = np.array(df_h2o_corrected[col_h2o_ppm])
139
140        # 現在の水蒸気濃度での補正項
141        term_ch4 = coef_b * h2o + coef_c * np.power(h2o, 2)
142
143        # 目標水蒸気濃度での補正項
144        target_term = coef_b * target_h2o_ppm + coef_c * np.power(target_h2o_ppm, 2)
145
146        # CH4濃度の補正(現在の補正項を引いて、目標水蒸気濃度での補正項を足す)
147        df_h2o_corrected[col_ch4_ppm] = (
148            df_h2o_corrected[col_ch4_ppm] - term_ch4 + target_term
149        )
150
151        # 極端に低い水蒸気濃度のデータは信頼性が低いため除外
152        if h2o_ppm_threshold is not None:
153            df_h2o_corrected.loc[df[col_h2o_ppm] < h2o_ppm_threshold, col_ch4_ppm] = (
154                np.nan
155            )
156            df_h2o_corrected = df_h2o_corrected.dropna(subset=[col_ch4_ppm])
157
158        return df_h2o_corrected

水蒸気干渉を補正するためのメソッドです。 CH4濃度を特定の水蒸気濃度下での値に換算します。

Parameters

df: pd.DataFrame
    補正対象のデータフレーム
coef_b: float
    補正曲線の1次係数
coef_c: float
    補正曲線の2次係数
col_ch4_ppm: str, optional
    CH4濃度を示すカラム名。デフォルト値は"ch4_ppm"
col_h2o_ppm: str, optional
    水蒸気濃度を示すカラム名。デフォルト値は"h2o_ppm"
h2o_ppm_threshold: float | None, optional
    水蒸気濃度の下限値(この値未満のデータは除外)。デフォルト値は2000 ppm
target_h2o_ppm: float, optional
    換算先の水蒸気濃度。デフォルト値は10000 ppm

Returns

pd.DataFrame
    水蒸気干渉が補正されたデータフレーム

Examples

>>> import pandas as pd
>>> import numpy as np
>>> # サンプルデータの作成
>>> df = pd.DataFrame({
...     'ch4_ppm': [2.0, 2.1, 2.2],
...     'h2o_ppm': [5000, 6000, 7000]
... })
>>> # 水蒸気干渉の補正
>>> df_corrected = CorrectingUtils.correct_h2o_interference(
...     df=df,
...     coef_b=0.0001,
...     coef_c=0.00001
... )
@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, quantile_value: float = 0.05) -> pandas.core.frame.DataFrame:
160    @staticmethod
161    def remove_bias(
162        df: pd.DataFrame,
163        col_ch4_ppm: str = "ch4_ppm",
164        col_c2h6_ppb: str = "c2h6_ppb",
165        base_ch4_ppm: float = 2.0,
166        base_c2h6_ppb: float = 0,
167        quantile_value: float = 0.05,
168    ) -> pd.DataFrame:
169        """
170        データフレームからバイアスを除去します。
171
172        Parameters
173        ----------
174            df: pd.DataFrame
175                バイアスを除去する対象のデータフレーム
176            col_ch4_ppm: str, optional
177                CH4濃度を示すカラム名。デフォルト値は"ch4_ppm"
178            col_c2h6_ppb: str, optional
179                C2H6濃度を示すカラム名。デフォルト値は"c2h6_ppb"
180            base_ch4_ppm: float, optional
181                補正前の値から最小値を引いた後に足すCH4濃度。デフォルト値は2.0
182            base_c2h6_ppb: float, optional
183                補正前の値から最小値を引いた後に足すC2H6濃度。デフォルト値は0
184            quantile_value: float, optional
185                下位何クォンタイルの値を最小値として補正を行うか。デフォルト値は0.05
186
187        Returns
188        ----------
189            pd.DataFrame
190                バイアスが除去されたデータフレーム
191
192        Examples
193        --------
194        >>> import pandas as pd
195        >>> # サンプルデータの作成
196        >>> df = pd.DataFrame({
197        ...     'ch4_ppm': [2.1, 2.2, 2.3],
198        ...     'c2h6_ppb': [1.1, 1.2, 1.3]
199        ... })
200        >>> # バイアスの除去
201        >>> df_unbiased = CorrectingUtils.remove_bias(
202        ...     df=df,
203        ...     base_ch4_ppm=2.0,
204        ...     base_c2h6_ppb=0
205        ... )
206        """
207        df_internal: pd.DataFrame = df.copy()
208        # CH4
209        ch4_min: float = df_internal[col_ch4_ppm].quantile(quantile_value)
210        df_internal[col_ch4_ppm] = df_internal[col_ch4_ppm] - ch4_min + base_ch4_ppm
211        # C2H6
212        c2h6_min: float = df_internal[col_c2h6_ppb].quantile(quantile_value)
213        df_internal[col_c2h6_ppb] = df_internal[col_c2h6_ppb] - c2h6_min + base_c2h6_ppb
214        return df_internal

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

Parameters

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

Returns

pd.DataFrame
    バイアスが除去されたデータフレーム

Examples

>>> import pandas as pd
>>> # サンプルデータの作成
>>> df = pd.DataFrame({
...     'ch4_ppm': [2.1, 2.2, 2.3],
...     'c2h6_ppb': [1.1, 1.2, 1.3]
... })
>>> # バイアスの除去
>>> df_unbiased = CorrectingUtils.remove_bias(
...     df=df,
...     base_ch4_ppm=2.0,
...     base_c2h6_ppb=0
... )
class EddyDataFiguresGenerator:
101class EddyDataFiguresGenerator:
102    """
103    データロガーの30分間データファイルから図を作成するクラス
104    """
105
106    def __init__(
107        self,
108        fs: float,
109        logger: Logger | None = None,
110        logging_debug: bool = False,
111    ):
112        """
113        Parameters
114        ----------
115            fs: float
116                サンプリング周波数
117            logger: Logger | None, optional
118                ロガーオブジェクト。デフォルトはNone。
119            logging_debug: bool, optional
120                ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。
121        """
122        self._fs: float = fs
123        log_level: int = INFO
124        if logging_debug:
125            log_level = DEBUG
126        self.logger: Logger = setup_logger(logger=logger, log_level=log_level)
127
128    def plot_c1c2_spectra(
129        self,
130        input_dirpath: str | Path,
131        output_dirpath: str | Path,
132        output_filename_power: str = "power_spectrum.png",
133        output_filename_co: str = "co_spectrum.png",
134        col_ch4: str = "Ultra_CH4_ppm_C",
135        col_c2h6: str = "Ultra_C2H6_ppb",
136        col_tv: str = "Tv",
137        ch4_config: SpectralPlotConfig | None = None,
138        c2h6_config: SpectralPlotConfig | None = None,
139        tv_config: SpectralPlotConfig | None = None,
140        lag_second: float | None = None,
141        file_pattern: str = r"Eddy_(\d+)",
142        file_suffix: str = ".dat",
143        figsize: tuple[float, float] = (20, 6),
144        dpi: float | None = 350,
145        markersize: float = 14,
146        xlim_power: tuple[float, float] | None = None,
147        ylim_power: tuple[float, float] | None = None,
148        xlim_co: tuple[float, float] | None = (0.001, 10),
149        ylim_co: tuple[float, float] | None = (0.0001, 10),
150        scaling_power: str = "spectrum",
151        scaling_co: str = "spectrum",
152        power_slope: SlopeLine | None = None,
153        co_slope: SlopeLine | None = None,
154        are_configs_resampled: bool = True,
155        save_fig: bool = True,
156        show_fig: bool = True,
157        plot_power: bool = True,
158        plot_co: bool = True,
159        add_tv_in_co: bool = True,
160        xlabel: str = "f (Hz)",
161    ) -> None:
162        """月間平均のスペクトルを計算してプロットする。
163
164        データファイルを指定されたディレクトリから読み込み、スペクトルを計算し、
165        結果を指定された出力ディレクトリにプロットして保存します。
166
167        Parameters
168        ----------
169            input_dirpath: str | Path
170                データファイルが格納されているディレクトリ。
171            output_dirpath: str | Path
172                出力先ディレクトリ。
173            output_filename_power: str, optional
174                出力するパワースペクトルのファイル名。デフォルトは`"power_spectrum.png"`。
175            output_filename_co: str, optional
176                出力するコスペクトルのファイル名。デフォルトは`"co_spectrum.png"`。
177            col_ch4: str, optional
178                CH4の濃度データが入ったカラムのキー。デフォルトは`"Ultra_CH4_ppm_C"`。
179            col_c2h6: str, optional
180                C2H6の濃度データが入ったカラムのキー。デフォルトは`"Ultra_C2H6_ppb"`。
181            col_tv: str, optional
182                気温データが入ったカラムのキー。デフォルトは`"Tv"`。
183            ch4_config: SpectralPlotConfig | None, optional
184                CH4のプロット設定。Noneの場合はデフォルト設定を使用。
185            c2h6_config: SpectralPlotConfig | None, optional
186                C2H6のプロット設定。Noneの場合はデフォルト設定を使用。
187            tv_config: SpectralPlotConfig | None, optional
188                気温のプロット設定。Noneの場合はデフォルト設定を使用。
189            lag_second: float | None, optional
190                ラグ時間(秒)。デフォルトはNone。
191            file_pattern: str, optional
192                入力ファイルのパターン。デフォルトは`r"Eddy_(\\d+)"`。
193            file_suffix: str, optional
194                入力ファイルの拡張子。デフォルトは`".dat"`。
195            figsize: tuple[float, float], optional
196                プロットのサイズ。デフォルトは`(20, 6)`。
197            dpi: float | None, optional
198                プロットのdpi。デフォルトは`350`。
199            markersize: float, optional
200                プロットマーカーのサイズ。デフォルトは`14`。
201            xlim_power: tuple[float, float] | None, optional
202                パワースペクトルのx軸の範囲。デフォルトはNone。
203            ylim_power: tuple[float, float] | None, optional
204                パワースペクトルのy軸の範囲。デフォルトはNone。
205            xlim_co: tuple[float, float] | None, optional
206                コスペクトルのx軸の範囲。デフォルトは`(0.001, 10)`。
207            ylim_co: tuple[float, float] | None, optional
208                コスペクトルのy軸の範囲。デフォルトは`(0.0001, 10)`。
209            scaling_power: str, optional
210                パワースペクトルのスケーリング方法。`'spectrum'`または`'density'`などが指定可能。
211                signal.welchのパラメータに渡すものと同様の値を取ることが可能。デフォルトは`"spectrum"`。
212            scaling_co: str, optional
213                コスペクトルのスケーリング方法。`'spectrum'`または`'density'`などが指定可能。
214                signal.welchのパラメータに渡すものと同様の値を取ることが可能。デフォルトは`"spectrum"`。
215            power_slope: SlopeLine | None, optional
216                パワースペクトルの傾き線設定。Noneの場合はデフォルト設定を使用。
217            co_slope: SlopeLine | None, optional
218                コスペクトルの傾き線設定。Noneの場合はデフォルト設定を使用。
219            are_configs_resampled: bool, optional
220                入力データが再サンプリングされているかどうか。デフォルトは`True`。
221            save_fig: bool, optional
222                図を保存するかどうか。デフォルトは`True`。
223            show_fig: bool, optional
224                図を表示するかどうか。デフォルトは`True`。
225            plot_power: bool, optional
226                パワースペクトルをプロットするかどうか。デフォルトは`True`。
227            plot_co: bool, optional
228                コスペクトルをプロットするかどうか。デフォルトは`True`。
229            add_tv_in_co: bool, optional
230                顕熱フラックスのコスペクトルを表示するかどうか。デフォルトは`True`。
231            xlabel: str, optional
232                x軸のラベル。デフォルトは`"f (Hz)"`。
233
234        Examples
235        --------
236        >>> edfg = EddyDataFiguresGenerator(fs=10)
237        >>> edfg.plot_c1c2_spectra(
238        ...     input_dirpath="data/eddy",
239        ...     output_dirpath="outputs",
240        ...     output_filename_power="power.png",
241        ...     output_filename_co="co.png"
242        ... )
243        """
244        # 出力ディレクトリの作成
245        if save_fig:
246            os.makedirs(output_dirpath, exist_ok=True)
247
248        # デフォルトのconfig設定
249        if ch4_config is None:
250            ch4_config = SpectralPlotConfig(
251                power_ylabel=r"$fS_{\mathrm{CH_4}} / s_{\mathrm{CH_4}}^2$",
252                co_ylabel=r"$fC_{w\mathrm{CH_4}} / \overline{w'\mathrm{CH_4}'}$",
253                color="red",
254                label="CH4",
255            )
256
257        if c2h6_config is None:
258            c2h6_config = SpectralPlotConfig(
259                power_ylabel=r"$fS_{\mathrm{C_2H_6}} / s_{\mathrm{C_2H_6}}^2$",
260                co_ylabel=r"$fC_{w\mathrm{C_2H_6}} / \overline{w'\mathrm{C_2H_6}'}$",
261                color="orange",
262                label="C2H6",
263            )
264
265        if tv_config is None:
266            tv_config = SpectralPlotConfig(
267                power_ylabel=r"$fS_{T_v} / s_{T_v}^2$",
268                co_ylabel=r"$fC_{wT_v} / \overline{w'T_v'}$",
269                color="blue",
270                label="Tv",
271            )
272
273        # データの読み込みと結合
274        edp = EddyDataPreprocessor(fs=self._fs)
275        col_wind_w: str = EddyDataPreprocessor.WIND_W
276
277        # 各変数のパワースペクトルを格納する辞書
278        power_spectra = {col_ch4: [], col_c2h6: []}
279        co_spectra = {col_ch4: [], col_c2h6: [], col_tv: []}
280        freqs = None
281
282        # ファイルリストの取得
283        csv_files = edp._get_sorted_files(input_dirpath, file_pattern, file_suffix)
284        if not csv_files:
285            raise FileNotFoundError(
286                f"file_suffix:'{file_suffix}'に一致するファイルが見つかりませんでした。"
287            )
288
289        for filename in tqdm(csv_files, desc="Processing files"):
290            df, _ = edp.get_resampled_df(
291                filepath=os.path.join(input_dirpath, filename),
292                resample=are_configs_resampled,
293            )
294
295            # 風速成分の計算を追加
296            df = edp.add_uvw_columns(df)
297
298            # NaNや無限大を含む行を削除
299            df = df.replace([np.inf, -np.inf], np.nan).dropna(
300                subset=[col_ch4, col_c2h6, col_tv, col_wind_w]
301            )
302
303            # データが十分な行数を持っているか確認
304            if len(df) < 100:
305                continue
306
307            # 各ファイルごとにスペクトル計算
308            calculator = SpectrumCalculator(
309                df=df,
310                fs=self._fs,
311            )
312
313            for col in power_spectra.keys():
314                # 各変数のパワースペクトルを計算して保存
315                if plot_power:
316                    f, ps = calculator.calculate_power_spectrum(
317                        col=col,
318                        dimensionless=True,
319                        frequency_weighted=True,
320                        interpolate_points=True,
321                        scaling=scaling_power,
322                    )
323                    # 最初のファイル処理時にfreqsを初期化
324                    if freqs is None:
325                        freqs = f
326                        power_spectra[col].append(ps)
327                    # 以降は周波数配列の長さが一致する場合のみ追加
328                    elif len(f) == len(freqs):
329                        power_spectra[col].append(ps)
330
331                # コスペクトル
332                if plot_co:
333                    _, cs, _ = calculator.calculate_co_spectrum(
334                        col1=col_wind_w,
335                        col2=col,
336                        dimensionless=True,
337                        frequency_weighted=True,
338                        interpolate_points=True,
339                        scaling=scaling_co,
340                        apply_lag_correction_to_col2=True,
341                        lag_second=lag_second,
342                    )
343                    if freqs is not None and len(cs) == len(freqs):
344                        co_spectra[col].append(cs)
345
346            # 顕熱フラックスのコスペクトル計算を追加
347            if plot_co and add_tv_in_co:
348                _, cs_heat, _ = calculator.calculate_co_spectrum(
349                    col1=col_wind_w,
350                    col2=col_tv,
351                    dimensionless=True,
352                    frequency_weighted=True,
353                    interpolate_points=True,
354                    scaling=scaling_co,
355                )
356                if freqs is not None and len(cs_heat) == len(freqs):
357                    co_spectra[col_tv].append(cs_heat)
358
359        # 各変数のスペクトルを平均化
360        if plot_power:
361            averaged_power_spectra = {
362                col: np.mean(spectra, axis=0) for col, spectra in power_spectra.items()
363            }
364        if plot_co:
365            averaged_co_spectra = {
366                col: np.mean(spectra, axis=0) for col, spectra in co_spectra.items()
367            }
368        # 顕熱フラックスの平均コスペクトル計算
369        if plot_co and add_tv_in_co and co_spectra[col_tv]:
370            averaged_heat_co_spectra = np.mean(co_spectra[col_tv], axis=0)
371
372        # パワースペクトルの図を作成
373        if plot_power:
374            fig_power, axes_power = plt.subplots(1, 2, figsize=figsize, sharex=True)
375            configs = [(col_ch4, ch4_config), (col_c2h6, c2h6_config)]
376
377            for ax, (col, config) in zip(axes_power, configs, strict=True):
378                ax.plot(
379                    freqs,
380                    averaged_power_spectra[col],
381                    "o",
382                    color=config.color,
383                    markersize=markersize,
384                )
385                ax.set_xscale("log")
386                ax.set_yscale("log")
387                if xlim_power:
388                    ax.set_xlim(*xlim_power)
389                if ylim_power:
390                    ax.set_ylim(*ylim_power)
391
392                # 傾き線とテキストの追加
393                if power_slope:
394                    power_slope.plot(ax)
395
396                ax.set_ylabel(config.power_ylabel)
397                if config.label is not None:
398                    ax.text(0.02, 0.98, config.label, transform=ax.transAxes, va="top")
399                ax.grid(True, alpha=0.3)
400                ax.set_xlabel(xlabel)
401
402            plt.tight_layout()
403
404            if save_fig:
405                output_filepath_power: str = os.path.join(
406                    output_dirpath, output_filename_power
407                )
408                plt.savefig(
409                    output_filepath_power,
410                    dpi=dpi,
411                    bbox_inches="tight",
412                )
413            if show_fig:
414                plt.show()
415            plt.close(fig=fig_power)
416
417        # コスペクトルの図を作成
418        if plot_co:
419            fig_co, axes_cosp = plt.subplots(1, 2, figsize=figsize, sharex=True)
420            configs = [(col_ch4, ch4_config), (col_c2h6, c2h6_config)]
421
422            for ax, (col, config) in zip(axes_cosp, configs, strict=True):
423                if add_tv_in_co:
424                    ax.plot(
425                        freqs,
426                        averaged_heat_co_spectra,
427                        "o",
428                        color=tv_config.color,
429                        alpha=0.3,
430                        markersize=markersize,
431                        label=tv_config.label,
432                    )
433
434                ax.plot(
435                    freqs,
436                    averaged_co_spectra[col],
437                    "o",
438                    color=config.color,
439                    markersize=markersize,
440                    label=config.label,
441                )
442
443                ax.set_xscale("log")
444                ax.set_yscale("log")
445                if xlim_co:
446                    ax.set_xlim(*xlim_co)
447                if ylim_co:
448                    ax.set_ylim(*ylim_co)
449
450                # 傾き線とテキストの追加
451                if co_slope:
452                    co_slope.plot(ax)
453
454                ax.set_ylabel(config.co_ylabel)
455                if config.label is not None:
456                    ax.text(0.02, 0.98, config.label, transform=ax.transAxes, va="top")
457                ax.grid(True, alpha=0.3)
458                ax.set_xlabel(xlabel)
459
460                if add_tv_in_co and tv_config.label:
461                    ax.legend(loc="lower left")
462
463            plt.tight_layout()
464            if save_fig:
465                output_filepath_csd: str = os.path.join(
466                    output_dirpath, output_filename_co
467                )
468                plt.savefig(
469                    output_filepath_csd,
470                    dpi=dpi,
471                    bbox_inches="tight",
472                )
473            if show_fig:
474                plt.show()
475            plt.close(fig=fig_co)
476
477    def plot_turbulence(
478        self,
479        df: pd.DataFrame,
480        output_dirpath: str | Path | None = None,
481        output_filename: str = "turbulence.png",
482        col_uz: str = "Uz",
483        col_ch4: str = "Ultra_CH4_ppm_C",
484        col_c2h6: str = "Ultra_C2H6_ppb",
485        col_timestamp: str = "TIMESTAMP",
486        add_serial_labels: bool = True,
487        figsize: tuple[float, float] = (12, 10),
488        dpi: float | None = 350,
489        save_fig: bool = True,
490        show_fig: bool = True,
491    ) -> None:
492        """時系列データのプロットを作成する
493
494        Parameters
495        ------
496            df: pd.DataFrame
497                プロットするデータを含むDataFrame
498            output_dirpath: str | Path | None, optional
499                出力ディレクトリのパス。デフォルトはNone。
500            output_filename: str, optional
501                出力ファイル名。デフォルトは"turbulence.png"。
502            col_uz: str, optional
503                鉛直風速データのカラム名。デフォルトは"Uz"。
504            col_ch4: str, optional
505                メタンデータのカラム名。デフォルトは"Ultra_CH4_ppm_C"。
506            col_c2h6: str, optional
507                エタンデータのカラム名。デフォルトは"Ultra_C2H6_ppb"。
508            col_timestamp: str, optional
509                タイムスタンプのカラム名。デフォルトは"TIMESTAMP"。
510            add_serial_labels: bool, optional
511                シリアルラベルを追加するかどうかのフラグ。デフォルトはTrue。
512            figsize: tuple[float, float], optional
513                プロットのサイズ。デフォルトは(12, 10)。
514            dpi: float | None, optional
515                プロットのdpi。デフォルトは350。
516            save_fig: bool, optional
517                プロットを保存するかどうか。デフォルトはTrue。
518            show_fig: bool, optional
519                プロットを表示するかどうか。デフォルトはTrue。
520
521        Examples
522        --------
523        >>> edfg = EddyDataFiguresGenerator(fs=10)
524        >>> edfg.plot_turbulence(df=data_frame)
525        """
526        # データの前処理
527        df_internal = df.copy()
528        df_internal.index = pd.to_datetime(df_internal.index)
529
530        # タイムスタンプをインデックスに設定(まだ設定されていない場合)
531        if not isinstance(df_internal.index, pd.DatetimeIndex):
532            df_internal[col_timestamp] = pd.to_datetime(df_internal[col_timestamp])
533            df_internal.set_index(col_timestamp, inplace=True)
534
535        # 開始時刻と終了時刻を取得
536        start_time = df_internal.index[0]
537        end_time = df_internal.index[-1]
538
539        # 開始時刻の分を取得
540        start_minute = start_time.minute
541
542        # 時間軸の作成(実際の開始時刻からの経過分数)
543        minutes_elapsed = (df_internal.index - start_time).total_seconds() / 60
544
545        # プロットの作成
546        fig, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=figsize, sharex=True)
547
548        # 鉛直風速
549        ax1.plot(minutes_elapsed, df_internal[col_uz], "k-", linewidth=0.5)
550        ax1.set_ylabel(r"$w$ (m s$^{-1}$)")
551        if add_serial_labels:
552            ax1.text(0.02, 0.98, "(a)", transform=ax1.transAxes, va="top")
553        ax1.grid(True, alpha=0.3)
554
555        # CH4濃度
556        ax2.plot(minutes_elapsed, df_internal[col_ch4], "r-", linewidth=0.5)
557        ax2.set_ylabel(r"$\mathrm{CH_4}$ (ppm)")
558        if add_serial_labels:
559            ax2.text(0.02, 0.98, "(b)", transform=ax2.transAxes, va="top")
560        ax2.grid(True, alpha=0.3)
561
562        # C2H6濃度
563        ax3.plot(minutes_elapsed, df_internal[col_c2h6], "orange", linewidth=0.5)
564        ax3.set_ylabel(r"$\mathrm{C_2H_6}$ (ppb)")
565        if add_serial_labels:
566            ax3.text(0.02, 0.98, "(c)", transform=ax3.transAxes, va="top")
567        ax3.grid(True, alpha=0.3)
568        ax3.set_xlabel("Time (minutes)")
569
570        # x軸の範囲を実際の開始時刻から30分後までに設定
571        total_minutes = (end_time - start_time).total_seconds() / 60
572        ax3.set_xlim(0, min(30, total_minutes))
573
574        # x軸の目盛りを5分間隔で設定
575        np.arange(start_minute, start_minute + 35, 5)
576        ax3.xaxis.set_major_locator(MultipleLocator(5))
577
578        # レイアウトの調整
579        plt.tight_layout()
580
581        # グラフの保存または表示
582        if save_fig:
583            if output_dirpath is None:
584                raise ValueError(
585                    "save_fig = True の場合、 output_dirpath を指定する必要があります。有効なディレクトリパスを指定してください。"
586                )
587            os.makedirs(output_dirpath, exist_ok=True)
588            output_filepath: str = os.path.join(output_dirpath, output_filename)
589            plt.savefig(output_filepath, bbox_inches="tight")
590        if show_fig:
591            plt.show()
592        plt.close(fig=fig)

データロガーの30分間データファイルから図を作成するクラス

EddyDataFiguresGenerator( fs: float, logger: logging.Logger | None = None, logging_debug: bool = False)
106    def __init__(
107        self,
108        fs: float,
109        logger: Logger | None = None,
110        logging_debug: bool = False,
111    ):
112        """
113        Parameters
114        ----------
115            fs: float
116                サンプリング周波数
117            logger: Logger | None, optional
118                ロガーオブジェクト。デフォルトはNone。
119            logging_debug: bool, optional
120                ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。
121        """
122        self._fs: float = fs
123        log_level: int = INFO
124        if logging_debug:
125            log_level = DEBUG
126        self.logger: Logger = setup_logger(logger=logger, log_level=log_level)

Parameters

fs: float
    サンプリング周波数
logger: Logger | None, optional
    ロガーオブジェクト。デフォルトはNone。
logging_debug: bool, optional
    ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。
logger: logging.Logger
def plot_c1c2_spectra( self, input_dirpath: str | pathlib.Path, output_dirpath: str | pathlib.Path, output_filename_power: str = 'power_spectrum.png', output_filename_co: str = 'co_spectrum.png', col_ch4: str = 'Ultra_CH4_ppm_C', col_c2h6: str = 'Ultra_C2H6_ppb', col_tv: str = 'Tv', ch4_config: SpectralPlotConfig | None = None, c2h6_config: SpectralPlotConfig | None = None, tv_config: SpectralPlotConfig | None = None, lag_second: float | None = None, file_pattern: str = 'Eddy_(\\d+)', file_suffix: str = '.dat', figsize: tuple[float, float] = (20, 6), dpi: float | None = 350, markersize: float = 14, xlim_power: tuple[float, float] | None = None, ylim_power: tuple[float, float] | None = None, xlim_co: tuple[float, float] | None = (0.001, 10), ylim_co: tuple[float, float] | None = (0.0001, 10), scaling_power: str = 'spectrum', scaling_co: str = 'spectrum', power_slope: SlopeLine | None = None, co_slope: SlopeLine | None = None, are_configs_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, xlabel: str = 'f (Hz)') -> None:
128    def plot_c1c2_spectra(
129        self,
130        input_dirpath: str | Path,
131        output_dirpath: str | Path,
132        output_filename_power: str = "power_spectrum.png",
133        output_filename_co: str = "co_spectrum.png",
134        col_ch4: str = "Ultra_CH4_ppm_C",
135        col_c2h6: str = "Ultra_C2H6_ppb",
136        col_tv: str = "Tv",
137        ch4_config: SpectralPlotConfig | None = None,
138        c2h6_config: SpectralPlotConfig | None = None,
139        tv_config: SpectralPlotConfig | None = None,
140        lag_second: float | None = None,
141        file_pattern: str = r"Eddy_(\d+)",
142        file_suffix: str = ".dat",
143        figsize: tuple[float, float] = (20, 6),
144        dpi: float | None = 350,
145        markersize: float = 14,
146        xlim_power: tuple[float, float] | None = None,
147        ylim_power: tuple[float, float] | None = None,
148        xlim_co: tuple[float, float] | None = (0.001, 10),
149        ylim_co: tuple[float, float] | None = (0.0001, 10),
150        scaling_power: str = "spectrum",
151        scaling_co: str = "spectrum",
152        power_slope: SlopeLine | None = None,
153        co_slope: SlopeLine | None = None,
154        are_configs_resampled: bool = True,
155        save_fig: bool = True,
156        show_fig: bool = True,
157        plot_power: bool = True,
158        plot_co: bool = True,
159        add_tv_in_co: bool = True,
160        xlabel: str = "f (Hz)",
161    ) -> None:
162        """月間平均のスペクトルを計算してプロットする。
163
164        データファイルを指定されたディレクトリから読み込み、スペクトルを計算し、
165        結果を指定された出力ディレクトリにプロットして保存します。
166
167        Parameters
168        ----------
169            input_dirpath: str | Path
170                データファイルが格納されているディレクトリ。
171            output_dirpath: str | Path
172                出力先ディレクトリ。
173            output_filename_power: str, optional
174                出力するパワースペクトルのファイル名。デフォルトは`"power_spectrum.png"`。
175            output_filename_co: str, optional
176                出力するコスペクトルのファイル名。デフォルトは`"co_spectrum.png"`。
177            col_ch4: str, optional
178                CH4の濃度データが入ったカラムのキー。デフォルトは`"Ultra_CH4_ppm_C"`。
179            col_c2h6: str, optional
180                C2H6の濃度データが入ったカラムのキー。デフォルトは`"Ultra_C2H6_ppb"`。
181            col_tv: str, optional
182                気温データが入ったカラムのキー。デフォルトは`"Tv"`。
183            ch4_config: SpectralPlotConfig | None, optional
184                CH4のプロット設定。Noneの場合はデフォルト設定を使用。
185            c2h6_config: SpectralPlotConfig | None, optional
186                C2H6のプロット設定。Noneの場合はデフォルト設定を使用。
187            tv_config: SpectralPlotConfig | None, optional
188                気温のプロット設定。Noneの場合はデフォルト設定を使用。
189            lag_second: float | None, optional
190                ラグ時間(秒)。デフォルトはNone。
191            file_pattern: str, optional
192                入力ファイルのパターン。デフォルトは`r"Eddy_(\\d+)"`。
193            file_suffix: str, optional
194                入力ファイルの拡張子。デフォルトは`".dat"`。
195            figsize: tuple[float, float], optional
196                プロットのサイズ。デフォルトは`(20, 6)`。
197            dpi: float | None, optional
198                プロットのdpi。デフォルトは`350`。
199            markersize: float, optional
200                プロットマーカーのサイズ。デフォルトは`14`。
201            xlim_power: tuple[float, float] | None, optional
202                パワースペクトルのx軸の範囲。デフォルトはNone。
203            ylim_power: tuple[float, float] | None, optional
204                パワースペクトルのy軸の範囲。デフォルトはNone。
205            xlim_co: tuple[float, float] | None, optional
206                コスペクトルのx軸の範囲。デフォルトは`(0.001, 10)`。
207            ylim_co: tuple[float, float] | None, optional
208                コスペクトルのy軸の範囲。デフォルトは`(0.0001, 10)`。
209            scaling_power: str, optional
210                パワースペクトルのスケーリング方法。`'spectrum'`または`'density'`などが指定可能。
211                signal.welchのパラメータに渡すものと同様の値を取ることが可能。デフォルトは`"spectrum"`。
212            scaling_co: str, optional
213                コスペクトルのスケーリング方法。`'spectrum'`または`'density'`などが指定可能。
214                signal.welchのパラメータに渡すものと同様の値を取ることが可能。デフォルトは`"spectrum"`。
215            power_slope: SlopeLine | None, optional
216                パワースペクトルの傾き線設定。Noneの場合はデフォルト設定を使用。
217            co_slope: SlopeLine | None, optional
218                コスペクトルの傾き線設定。Noneの場合はデフォルト設定を使用。
219            are_configs_resampled: bool, optional
220                入力データが再サンプリングされているかどうか。デフォルトは`True`。
221            save_fig: bool, optional
222                図を保存するかどうか。デフォルトは`True`。
223            show_fig: bool, optional
224                図を表示するかどうか。デフォルトは`True`。
225            plot_power: bool, optional
226                パワースペクトルをプロットするかどうか。デフォルトは`True`。
227            plot_co: bool, optional
228                コスペクトルをプロットするかどうか。デフォルトは`True`。
229            add_tv_in_co: bool, optional
230                顕熱フラックスのコスペクトルを表示するかどうか。デフォルトは`True`。
231            xlabel: str, optional
232                x軸のラベル。デフォルトは`"f (Hz)"`。
233
234        Examples
235        --------
236        >>> edfg = EddyDataFiguresGenerator(fs=10)
237        >>> edfg.plot_c1c2_spectra(
238        ...     input_dirpath="data/eddy",
239        ...     output_dirpath="outputs",
240        ...     output_filename_power="power.png",
241        ...     output_filename_co="co.png"
242        ... )
243        """
244        # 出力ディレクトリの作成
245        if save_fig:
246            os.makedirs(output_dirpath, exist_ok=True)
247
248        # デフォルトのconfig設定
249        if ch4_config is None:
250            ch4_config = SpectralPlotConfig(
251                power_ylabel=r"$fS_{\mathrm{CH_4}} / s_{\mathrm{CH_4}}^2$",
252                co_ylabel=r"$fC_{w\mathrm{CH_4}} / \overline{w'\mathrm{CH_4}'}$",
253                color="red",
254                label="CH4",
255            )
256
257        if c2h6_config is None:
258            c2h6_config = SpectralPlotConfig(
259                power_ylabel=r"$fS_{\mathrm{C_2H_6}} / s_{\mathrm{C_2H_6}}^2$",
260                co_ylabel=r"$fC_{w\mathrm{C_2H_6}} / \overline{w'\mathrm{C_2H_6}'}$",
261                color="orange",
262                label="C2H6",
263            )
264
265        if tv_config is None:
266            tv_config = SpectralPlotConfig(
267                power_ylabel=r"$fS_{T_v} / s_{T_v}^2$",
268                co_ylabel=r"$fC_{wT_v} / \overline{w'T_v'}$",
269                color="blue",
270                label="Tv",
271            )
272
273        # データの読み込みと結合
274        edp = EddyDataPreprocessor(fs=self._fs)
275        col_wind_w: str = EddyDataPreprocessor.WIND_W
276
277        # 各変数のパワースペクトルを格納する辞書
278        power_spectra = {col_ch4: [], col_c2h6: []}
279        co_spectra = {col_ch4: [], col_c2h6: [], col_tv: []}
280        freqs = None
281
282        # ファイルリストの取得
283        csv_files = edp._get_sorted_files(input_dirpath, file_pattern, file_suffix)
284        if not csv_files:
285            raise FileNotFoundError(
286                f"file_suffix:'{file_suffix}'に一致するファイルが見つかりませんでした。"
287            )
288
289        for filename in tqdm(csv_files, desc="Processing files"):
290            df, _ = edp.get_resampled_df(
291                filepath=os.path.join(input_dirpath, filename),
292                resample=are_configs_resampled,
293            )
294
295            # 風速成分の計算を追加
296            df = edp.add_uvw_columns(df)
297
298            # NaNや無限大を含む行を削除
299            df = df.replace([np.inf, -np.inf], np.nan).dropna(
300                subset=[col_ch4, col_c2h6, col_tv, col_wind_w]
301            )
302
303            # データが十分な行数を持っているか確認
304            if len(df) < 100:
305                continue
306
307            # 各ファイルごとにスペクトル計算
308            calculator = SpectrumCalculator(
309                df=df,
310                fs=self._fs,
311            )
312
313            for col in power_spectra.keys():
314                # 各変数のパワースペクトルを計算して保存
315                if plot_power:
316                    f, ps = calculator.calculate_power_spectrum(
317                        col=col,
318                        dimensionless=True,
319                        frequency_weighted=True,
320                        interpolate_points=True,
321                        scaling=scaling_power,
322                    )
323                    # 最初のファイル処理時にfreqsを初期化
324                    if freqs is None:
325                        freqs = f
326                        power_spectra[col].append(ps)
327                    # 以降は周波数配列の長さが一致する場合のみ追加
328                    elif len(f) == len(freqs):
329                        power_spectra[col].append(ps)
330
331                # コスペクトル
332                if plot_co:
333                    _, cs, _ = calculator.calculate_co_spectrum(
334                        col1=col_wind_w,
335                        col2=col,
336                        dimensionless=True,
337                        frequency_weighted=True,
338                        interpolate_points=True,
339                        scaling=scaling_co,
340                        apply_lag_correction_to_col2=True,
341                        lag_second=lag_second,
342                    )
343                    if freqs is not None and len(cs) == len(freqs):
344                        co_spectra[col].append(cs)
345
346            # 顕熱フラックスのコスペクトル計算を追加
347            if plot_co and add_tv_in_co:
348                _, cs_heat, _ = calculator.calculate_co_spectrum(
349                    col1=col_wind_w,
350                    col2=col_tv,
351                    dimensionless=True,
352                    frequency_weighted=True,
353                    interpolate_points=True,
354                    scaling=scaling_co,
355                )
356                if freqs is not None and len(cs_heat) == len(freqs):
357                    co_spectra[col_tv].append(cs_heat)
358
359        # 各変数のスペクトルを平均化
360        if plot_power:
361            averaged_power_spectra = {
362                col: np.mean(spectra, axis=0) for col, spectra in power_spectra.items()
363            }
364        if plot_co:
365            averaged_co_spectra = {
366                col: np.mean(spectra, axis=0) for col, spectra in co_spectra.items()
367            }
368        # 顕熱フラックスの平均コスペクトル計算
369        if plot_co and add_tv_in_co and co_spectra[col_tv]:
370            averaged_heat_co_spectra = np.mean(co_spectra[col_tv], axis=0)
371
372        # パワースペクトルの図を作成
373        if plot_power:
374            fig_power, axes_power = plt.subplots(1, 2, figsize=figsize, sharex=True)
375            configs = [(col_ch4, ch4_config), (col_c2h6, c2h6_config)]
376
377            for ax, (col, config) in zip(axes_power, configs, strict=True):
378                ax.plot(
379                    freqs,
380                    averaged_power_spectra[col],
381                    "o",
382                    color=config.color,
383                    markersize=markersize,
384                )
385                ax.set_xscale("log")
386                ax.set_yscale("log")
387                if xlim_power:
388                    ax.set_xlim(*xlim_power)
389                if ylim_power:
390                    ax.set_ylim(*ylim_power)
391
392                # 傾き線とテキストの追加
393                if power_slope:
394                    power_slope.plot(ax)
395
396                ax.set_ylabel(config.power_ylabel)
397                if config.label is not None:
398                    ax.text(0.02, 0.98, config.label, transform=ax.transAxes, va="top")
399                ax.grid(True, alpha=0.3)
400                ax.set_xlabel(xlabel)
401
402            plt.tight_layout()
403
404            if save_fig:
405                output_filepath_power: str = os.path.join(
406                    output_dirpath, output_filename_power
407                )
408                plt.savefig(
409                    output_filepath_power,
410                    dpi=dpi,
411                    bbox_inches="tight",
412                )
413            if show_fig:
414                plt.show()
415            plt.close(fig=fig_power)
416
417        # コスペクトルの図を作成
418        if plot_co:
419            fig_co, axes_cosp = plt.subplots(1, 2, figsize=figsize, sharex=True)
420            configs = [(col_ch4, ch4_config), (col_c2h6, c2h6_config)]
421
422            for ax, (col, config) in zip(axes_cosp, configs, strict=True):
423                if add_tv_in_co:
424                    ax.plot(
425                        freqs,
426                        averaged_heat_co_spectra,
427                        "o",
428                        color=tv_config.color,
429                        alpha=0.3,
430                        markersize=markersize,
431                        label=tv_config.label,
432                    )
433
434                ax.plot(
435                    freqs,
436                    averaged_co_spectra[col],
437                    "o",
438                    color=config.color,
439                    markersize=markersize,
440                    label=config.label,
441                )
442
443                ax.set_xscale("log")
444                ax.set_yscale("log")
445                if xlim_co:
446                    ax.set_xlim(*xlim_co)
447                if ylim_co:
448                    ax.set_ylim(*ylim_co)
449
450                # 傾き線とテキストの追加
451                if co_slope:
452                    co_slope.plot(ax)
453
454                ax.set_ylabel(config.co_ylabel)
455                if config.label is not None:
456                    ax.text(0.02, 0.98, config.label, transform=ax.transAxes, va="top")
457                ax.grid(True, alpha=0.3)
458                ax.set_xlabel(xlabel)
459
460                if add_tv_in_co and tv_config.label:
461                    ax.legend(loc="lower left")
462
463            plt.tight_layout()
464            if save_fig:
465                output_filepath_csd: str = os.path.join(
466                    output_dirpath, output_filename_co
467                )
468                plt.savefig(
469                    output_filepath_csd,
470                    dpi=dpi,
471                    bbox_inches="tight",
472                )
473            if show_fig:
474                plt.show()
475            plt.close(fig=fig_co)

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

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

Parameters

input_dirpath: str | Path
    データファイルが格納されているディレクトリ。
output_dirpath: str | Path
    出力先ディレクトリ。
output_filename_power: str, optional
    出力するパワースペクトルのファイル名。デフォルトは`"power_spectrum.png"`。
output_filename_co: str, optional
    出力するコスペクトルのファイル名。デフォルトは`"co_spectrum.png"`。
col_ch4: str, optional
    CH4の濃度データが入ったカラムのキー。デフォルトは`"Ultra_CH4_ppm_C"`。
col_c2h6: str, optional
    C2H6の濃度データが入ったカラムのキー。デフォルトは`"Ultra_C2H6_ppb"`。
col_tv: str, optional
    気温データが入ったカラムのキー。デフォルトは`"Tv"`。
ch4_config: SpectralPlotConfig | None, optional
    CH4のプロット設定。Noneの場合はデフォルト設定を使用。
c2h6_config: SpectralPlotConfig | None, optional
    C2H6のプロット設定。Noneの場合はデフォルト設定を使用。
tv_config: SpectralPlotConfig | None, optional
    気温のプロット設定。Noneの場合はデフォルト設定を使用。
lag_second: float | None, optional
    ラグ時間(秒)。デフォルトはNone。
file_pattern: str, optional
    入力ファイルのパターン。デフォルトは`r"Eddy_(\d+)"`。
file_suffix: str, optional
    入力ファイルの拡張子。デフォルトは`".dat"`。
figsize: tuple[float, float], optional
    プロットのサイズ。デフォルトは`(20, 6)`。
dpi: float | None, optional
    プロットのdpi。デフォルトは`350`。
markersize: float, optional
    プロットマーカーのサイズ。デフォルトは`14`。
xlim_power: tuple[float, float] | None, optional
    パワースペクトルのx軸の範囲。デフォルトはNone。
ylim_power: tuple[float, float] | None, optional
    パワースペクトルのy軸の範囲。デフォルトはNone。
xlim_co: tuple[float, float] | None, optional
    コスペクトルのx軸の範囲。デフォルトは`(0.001, 10)`。
ylim_co: tuple[float, float] | None, optional
    コスペクトルのy軸の範囲。デフォルトは`(0.0001, 10)`。
scaling_power: str, optional
    パワースペクトルのスケーリング方法。`'spectrum'`または`'density'`などが指定可能。
    signal.welchのパラメータに渡すものと同様の値を取ることが可能。デフォルトは`"spectrum"`。
scaling_co: str, optional
    コスペクトルのスケーリング方法。`'spectrum'`または`'density'`などが指定可能。
    signal.welchのパラメータに渡すものと同様の値を取ることが可能。デフォルトは`"spectrum"`。
power_slope: SlopeLine | None, optional
    パワースペクトルの傾き線設定。Noneの場合はデフォルト設定を使用。
co_slope: SlopeLine | None, optional
    コスペクトルの傾き線設定。Noneの場合はデフォルト設定を使用。
are_configs_resampled: bool, optional
    入力データが再サンプリングされているかどうか。デフォルトは`True`。
save_fig: bool, optional
    図を保存するかどうか。デフォルトは`True`。
show_fig: bool, optional
    図を表示するかどうか。デフォルトは`True`。
plot_power: bool, optional
    パワースペクトルをプロットするかどうか。デフォルトは`True`。
plot_co: bool, optional
    コスペクトルをプロットするかどうか。デフォルトは`True`。
add_tv_in_co: bool, optional
    顕熱フラックスのコスペクトルを表示するかどうか。デフォルトは`True`。
xlabel: str, optional
    x軸のラベル。デフォルトは`"f (Hz)"`。

Examples

>>> edfg = EddyDataFiguresGenerator(fs=10)
>>> edfg.plot_c1c2_spectra(
...     input_dirpath="data/eddy",
...     output_dirpath="outputs",
...     output_filename_power="power.png",
...     output_filename_co="co.png"
... )
def plot_turbulence( self, df: pandas.core.frame.DataFrame, output_dirpath: str | pathlib.Path | None = None, 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, figsize: tuple[float, float] = (12, 10), dpi: float | None = 350, save_fig: bool = True, show_fig: bool = True) -> None:
477    def plot_turbulence(
478        self,
479        df: pd.DataFrame,
480        output_dirpath: str | Path | None = None,
481        output_filename: str = "turbulence.png",
482        col_uz: str = "Uz",
483        col_ch4: str = "Ultra_CH4_ppm_C",
484        col_c2h6: str = "Ultra_C2H6_ppb",
485        col_timestamp: str = "TIMESTAMP",
486        add_serial_labels: bool = True,
487        figsize: tuple[float, float] = (12, 10),
488        dpi: float | None = 350,
489        save_fig: bool = True,
490        show_fig: bool = True,
491    ) -> None:
492        """時系列データのプロットを作成する
493
494        Parameters
495        ------
496            df: pd.DataFrame
497                プロットするデータを含むDataFrame
498            output_dirpath: str | Path | None, optional
499                出力ディレクトリのパス。デフォルトはNone。
500            output_filename: str, optional
501                出力ファイル名。デフォルトは"turbulence.png"。
502            col_uz: str, optional
503                鉛直風速データのカラム名。デフォルトは"Uz"。
504            col_ch4: str, optional
505                メタンデータのカラム名。デフォルトは"Ultra_CH4_ppm_C"。
506            col_c2h6: str, optional
507                エタンデータのカラム名。デフォルトは"Ultra_C2H6_ppb"。
508            col_timestamp: str, optional
509                タイムスタンプのカラム名。デフォルトは"TIMESTAMP"。
510            add_serial_labels: bool, optional
511                シリアルラベルを追加するかどうかのフラグ。デフォルトはTrue。
512            figsize: tuple[float, float], optional
513                プロットのサイズ。デフォルトは(12, 10)。
514            dpi: float | None, optional
515                プロットのdpi。デフォルトは350。
516            save_fig: bool, optional
517                プロットを保存するかどうか。デフォルトはTrue。
518            show_fig: bool, optional
519                プロットを表示するかどうか。デフォルトはTrue。
520
521        Examples
522        --------
523        >>> edfg = EddyDataFiguresGenerator(fs=10)
524        >>> edfg.plot_turbulence(df=data_frame)
525        """
526        # データの前処理
527        df_internal = df.copy()
528        df_internal.index = pd.to_datetime(df_internal.index)
529
530        # タイムスタンプをインデックスに設定(まだ設定されていない場合)
531        if not isinstance(df_internal.index, pd.DatetimeIndex):
532            df_internal[col_timestamp] = pd.to_datetime(df_internal[col_timestamp])
533            df_internal.set_index(col_timestamp, inplace=True)
534
535        # 開始時刻と終了時刻を取得
536        start_time = df_internal.index[0]
537        end_time = df_internal.index[-1]
538
539        # 開始時刻の分を取得
540        start_minute = start_time.minute
541
542        # 時間軸の作成(実際の開始時刻からの経過分数)
543        minutes_elapsed = (df_internal.index - start_time).total_seconds() / 60
544
545        # プロットの作成
546        fig, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=figsize, sharex=True)
547
548        # 鉛直風速
549        ax1.plot(minutes_elapsed, df_internal[col_uz], "k-", linewidth=0.5)
550        ax1.set_ylabel(r"$w$ (m s$^{-1}$)")
551        if add_serial_labels:
552            ax1.text(0.02, 0.98, "(a)", transform=ax1.transAxes, va="top")
553        ax1.grid(True, alpha=0.3)
554
555        # CH4濃度
556        ax2.plot(minutes_elapsed, df_internal[col_ch4], "r-", linewidth=0.5)
557        ax2.set_ylabel(r"$\mathrm{CH_4}$ (ppm)")
558        if add_serial_labels:
559            ax2.text(0.02, 0.98, "(b)", transform=ax2.transAxes, va="top")
560        ax2.grid(True, alpha=0.3)
561
562        # C2H6濃度
563        ax3.plot(minutes_elapsed, df_internal[col_c2h6], "orange", linewidth=0.5)
564        ax3.set_ylabel(r"$\mathrm{C_2H_6}$ (ppb)")
565        if add_serial_labels:
566            ax3.text(0.02, 0.98, "(c)", transform=ax3.transAxes, va="top")
567        ax3.grid(True, alpha=0.3)
568        ax3.set_xlabel("Time (minutes)")
569
570        # x軸の範囲を実際の開始時刻から30分後までに設定
571        total_minutes = (end_time - start_time).total_seconds() / 60
572        ax3.set_xlim(0, min(30, total_minutes))
573
574        # x軸の目盛りを5分間隔で設定
575        np.arange(start_minute, start_minute + 35, 5)
576        ax3.xaxis.set_major_locator(MultipleLocator(5))
577
578        # レイアウトの調整
579        plt.tight_layout()
580
581        # グラフの保存または表示
582        if save_fig:
583            if output_dirpath is None:
584                raise ValueError(
585                    "save_fig = True の場合、 output_dirpath を指定する必要があります。有効なディレクトリパスを指定してください。"
586                )
587            os.makedirs(output_dirpath, exist_ok=True)
588            output_filepath: str = os.path.join(output_dirpath, output_filename)
589            plt.savefig(output_filepath, bbox_inches="tight")
590        if show_fig:
591            plt.show()
592        plt.close(fig=fig)

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

Parameters

df: pd.DataFrame
    プロットするデータを含むDataFrame
output_dirpath: str | Path | None, optional
    出力ディレクトリのパス。デフォルトはNone。
output_filename: str, optional
    出力ファイル名。デフォルトは"turbulence.png"。
col_uz: str, optional
    鉛直風速データのカラム名。デフォルトは"Uz"。
col_ch4: str, optional
    メタンデータのカラム名。デフォルトは"Ultra_CH4_ppm_C"。
col_c2h6: str, optional
    エタンデータのカラム名。デフォルトは"Ultra_C2H6_ppb"。
col_timestamp: str, optional
    タイムスタンプのカラム名。デフォルトは"TIMESTAMP"。
add_serial_labels: bool, optional
    シリアルラベルを追加するかどうかのフラグ。デフォルトはTrue。
figsize: tuple[float, float], optional
    プロットのサイズ。デフォルトは(12, 10)。
dpi: float | None, optional
    プロットのdpi。デフォルトは350。
save_fig: bool, optional
    プロットを保存するかどうか。デフォルトはTrue。
show_fig: bool, optional
    プロットを表示するかどうか。デフォルトはTrue。

Examples

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

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

Parameters

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

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

Parameters

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

Returns

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

Raises

ValueError
    必要なカラムが存在しない場合、またはマッピングに必要なキーが不足している場合

Examples

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

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

Parameters

input_dirpath: str | Path
    入力データファイルが格納されているディレクトリのパス
input_files_pattern: str, optional
    入力ファイル名のパターン(正規表現)。デフォルトは"Eddy_(\d+)"
input_files_suffix: str, optional
    入力ファイルの拡張子。デフォルトは".dat"
col1: str, optional
    基準変数の列名。デフォルトは"edp_wind_w"
col2_list: list[str] | None, optional
    比較変数の列名のリスト。デフォルトはNoneで、その場合は["Tv"]を使用
median_range: float, optional
    中央値を中心とした範囲。デフォルトは20
output_dirpath: str | Path | None, optional
    出力ディレクトリのパス。デフォルトはNoneで、その場合は結果を保存しない
output_tag: str, optional
    出力ファイルに付与するタグ。デフォルトは空文字
add_title: bool, optional
    プロットにタイトルを追加するかどうか。デフォルトはTrue
figsize: tuple[float, float], optional
    プロットのサイズ。デフォルトは(10, 8)
dpi: float | None, optional
    プロットのdpi。デフォルトは350
plot_range_tuple: tuple, optional
    ヒストグラムの表示範囲。デフォルトは(-50, 200)
print_results: bool, optional
    結果をコンソールに表示するかどうか。デフォルトはTrue
xlabel: str | None, optional
    x軸のラベル。デフォルトは"Seconds"
ylabel: str | None, optional
    y軸のラベル。デフォルトは"Frequency"
index_column: str, optional
    タイムスタンプが格納された列名。デフォルトは"TIMESTAMP"
index_format: str, optional
    タイムスタンプのフォーマット。デフォルトは"%Y-%m-%d %H:%M:%S.%f"
resample_in_processing: bool, optional
    データを遅れ時間の計算中にリサンプリングするかどうか。デフォルトはFalse
interpolate: bool, optional
    欠損値の補完を適用するかどうか。デフォルトはTrue
numeric_columns: list[str] | None, optional
    数値型に変換する列名のリスト。デフォルトはNoneで、その場合は標準の列名リストを使用
metadata_rows: int, optional
    メタデータの行数。デフォルトは4
skiprows: list[int] | None, optional
    スキップする行番号のリスト。デフォルトはNoneで、その場合は[0, 2, 3]を使用
add_uvw_columns: bool, optional
    u, v, wの列を追加するかどうか。デフォルトはTrue
uvw_column_mapping: dict[MeasuredWindKeyType, str] | None, optional
    u, v, wの列名をマッピングする辞書。デフォルトはNoneで、その場合は標準のマッピングを使用

Returns

dict[str, float]
    各変数の遅れ時間(平均値を採用)を含む辞書

Examples

>>> edp = EddyDataPreprocessor(fs=10)
>>> result = edp.analyze_lag_times(
...     input_dirpath="data/eddy",
...     col2_list=["Ultra_CH4_ppm", "Ultra_C2H6_ppb"],
...     output_dirpath="outputs"
... )
>>> print(result)
{'Ultra_CH4_ppm': 0.2, 'Ultra_C2H6_ppb': 0.3}
def get_generated_columns_names(self, print_summary: bool = True) -> list[str]:
400    def get_generated_columns_names(self, print_summary: bool = True) -> list[str]:
401        """
402        クラス内部で生成されるカラム名を取得する。
403
404        Parameters
405        ----------
406            print_summary: bool, optional
407                結果をprint()で表示するかどうかを指定するフラグ。デフォルトはTrueで表示する。
408
409        Returns
410        ----------
411            list[str]
412                生成されるカラム名のリスト。以下のカラムが含まれる:
413                - edp_wind_u: 水平風速u成分
414                - edp_wind_v: 水平風速v成分 
415                - edp_wind_w: 鉛直風速w成分
416                - edp_rad_wind_dir: 風向(ラジアン)
417                - edp_rad_wind_inc: 迎角(ラジアン)
418                - edp_degree_wind_dir: 風向(度)
419                - edp_degree_wind_inc: 迎角(度)
420
421        Examples
422        --------
423        >>> edp = EddyDataPreprocessor()
424        >>> cols = edp.get_generated_columns_names(print_summary=False)
425        >>> print(cols)
426        ['edp_wind_u', 'edp_wind_v', 'edp_wind_w', 'edp_rad_wind_dir', 
427         'edp_rad_wind_inc', 'edp_degree_wind_dir', 'edp_degree_wind_inc']
428        """
429        list_cols: list[str] = [
430            self.WIND_U,
431            self.WIND_V,
432            self.WIND_W,
433            self.RAD_WIND_DIR,
434            self.RAD_WIND_INC,
435            self.DEGREE_WIND_DIR,
436            self.DEGREE_WIND_INC,
437        ]
438        if print_summary:
439            print(list_cols)
440        return list_cols

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

Parameters

print_summary: bool, optional
    結果をprint()で表示するかどうかを指定するフラグ。デフォルトはTrueで表示する。

Returns

list[str]
    生成されるカラム名のリスト。以下のカラムが含まれる:
    - edp_wind_u: 水平風速u成分
    - edp_wind_v: 水平風速v成分 
    - edp_wind_w: 鉛直風速w成分
    - edp_rad_wind_dir: 風向(ラジアン)
    - edp_rad_wind_inc: 迎角(ラジアン)
    - edp_degree_wind_dir: 風向(度)
    - edp_degree_wind_inc: 迎角(度)

Examples

>>> edp = EddyDataPreprocessor()
>>> cols = edp.get_generated_columns_names(print_summary=False)
>>> print(cols)
['edp_wind_u', 'edp_wind_v', 'edp_wind_w', 'edp_rad_wind_dir', 
 'edp_rad_wind_inc', 'edp_degree_wind_dir', 'edp_degree_wind_inc']
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] | None = None, metadata_rows: int = 4, skiprows: list[int] | None = None, resample: bool = True, interpolate: bool = True) -> tuple[pandas.core.frame.DataFrame, list[str]]:
442    def get_resampled_df(
443        self,
444        filepath: str,
445        index_column: str = "TIMESTAMP",
446        index_format: str = "%Y-%m-%d %H:%M:%S.%f",
447        numeric_columns: list[str] | None = None,
448        metadata_rows: int = 4,
449        skiprows: list[int] | None = None,
450        resample: bool = True,
451        interpolate: bool = True,
452    ) -> tuple[pd.DataFrame, list[str]]:
453        """CSVファイルを読み込み、前処理を行う
454
455        前処理の手順は以下の通りです:
456        1. 不要な行を削除する。デフォルトの場合は、2行目をヘッダーとして残し、1、3、4行目が削除される。
457        2. 数値データを float 型に変換する
458        3. TIMESTAMP列をDateTimeインデックスに設定する
459        4. エラー値をNaNに置き換える
460        5. 指定されたサンプリングレートでリサンプリングする
461        6. 欠損値(NaN)を前後の値から線形補間する
462        7. DateTimeインデックスを削除する
463
464        Parameters
465        ----------
466            filepath: str
467                読み込むCSVファイルのパス
468            index_column: str, optional
469                インデックスに使用する列名。デフォルトは"TIMESTAMP"。
470            index_format: str, optional
471                インデックスの日付形式。デフォルトは"%Y-%m-%d %H:%M:%S.%f"。
472            numeric_columns: list[str] | None, optional
473                数値型に変換する列名のリスト。デフォルトはNoneで、その場合は標準の列名リストを使用。
474            metadata_rows: int, optional
475                メタデータとして読み込む行数。デフォルトは4。
476            skiprows: list[int] | None, optional
477                スキップする行インデックスのリスト。デフォルトはNoneで、その場合は[0, 2, 3]を使用。
478            resample: bool, optional
479                メソッド内でリサンプリング&欠損補間をするかのフラグ。デフォルトはTrue。
480            interpolate: bool, optional
481                欠損値の補完を適用するかのフラグ。デフォルトはTrue。
482
483        Returns
484        ----------
485            tuple[pd.DataFrame, list[str]]
486                前処理済みのデータフレームとメタデータのリストを含むタプル。
487
488        Examples
489        --------
490        >>> edp = EddyDataPreprocessor()
491        >>> df, metadata = edp.get_resampled_df(
492        ...     filepath="data/eddy_data.csv",
493        ...     numeric_columns=["Ux", "Uy", "Uz", "Tv"]
494        ... )
495        """
496        # デフォルト値の設定
497        if numeric_columns is None:
498            numeric_columns = [
499                "Ux",
500                "Uy",
501                "Uz",
502                "Tv",
503                "diag_sonic",
504                "CO2_new",
505                "H2O",
506                "diag_irga",
507                "cell_tmpr",
508                "cell_press",
509                "Ultra_CH4_ppm",
510                "Ultra_C2H6_ppb",
511                "Ultra_H2O_ppm",
512                "Ultra_CH4_ppm_C",
513                "Ultra_C2H6_ppb_C",
514            ]
515        if skiprows is None:
516            skiprows = [0, 2, 3]
517
518        # メタデータを読み込む
519        metadata: list[str] = []
520        with open(filepath) as f:
521            for _ in range(metadata_rows):
522                line = f.readline().strip()
523                metadata.append(line.replace('"', ""))
524
525        # CSVファイルを読み込む
526        df: pd.DataFrame = pd.read_csv(filepath, skiprows=skiprows)
527
528        # 数値データをfloat型に変換する
529        for col in numeric_columns:
530            if col in df.columns:
531                df[col] = pd.to_numeric(df[col], errors="coerce")
532
533        if not resample:
534            # μ秒がない場合は".0"を追加する
535            df[index_column] = df[index_column].apply(
536                lambda x: f"{x}.0" if "." not in x else x
537            )
538            # TIMESTAMPをDateTimeインデックスに設定する
539            df[index_column] = pd.to_datetime(df[index_column], format=index_format)
540            df = df.set_index(index_column)
541
542            # リサンプリング前の有効数字を取得
543            decimal_places = {}
544            for col in numeric_columns:
545                if col in df.columns:
546                    max_decimals = (
547                        df[col].astype(str).str.extract(r"\.(\d+)")[0].str.len().max()
548                    )
549                    decimal_places[col] = (
550                        int(max_decimals) if pd.notna(max_decimals) else 0
551                    )
552
553            # リサンプリングを実行
554            resampling_period: int = int(1000 / self.fs)
555            df_resampled: pd.DataFrame = df.resample(f"{resampling_period}ms").mean(
556                numeric_only=True
557            )
558
559            if interpolate:
560                # 補間を実行
561                df_resampled = df_resampled.interpolate()
562                # 有効数字を調整
563                for col, decimals in decimal_places.items():
564                    if col in df_resampled.columns:
565                        df_resampled[col] = df_resampled[col].round(decimals)
566
567            # DateTimeインデックスを削除する
568            df = df_resampled.reset_index()
569            # ミリ秒を1桁にフォーマット
570            df[index_column] = (
571                df[index_column].dt.strftime("%Y-%m-%d %H:%M:%S.%f").str[:-5]
572            )
573
574        return df, metadata

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

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

  1. 不要な行を削除する。デフォルトの場合は、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] | None, optional
    数値型に変換する列名のリスト。デフォルトはNoneで、その場合は標準の列名リストを使用。
metadata_rows: int, optional
    メタデータとして読み込む行数。デフォルトは4。
skiprows: list[int] | None, optional
    スキップする行インデックスのリスト。デフォルトはNoneで、その場合は[0, 2, 3]を使用。
resample: bool, optional
    メソッド内でリサンプリング&欠損補間をするかのフラグ。デフォルトはTrue。
interpolate: bool, optional
    欠損値の補完を適用するかのフラグ。デフォルトはTrue。

Returns

tuple[pd.DataFrame, list[str]]
    前処理済みのデータフレームとメタデータのリストを含むタプル。

Examples

>>> edp = EddyDataPreprocessor()
>>> df, metadata = edp.get_resampled_df(
...     filepath="data/eddy_data.csv",
...     numeric_columns=["Ux", "Uy", "Uz", "Tv"]
... )
def output_resampled_data( self, input_dirpath: str, resampled_dirpath: str, c2c1_ratio_dirpath: 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] | None = None, metadata_rows: int = 4, skiprows: list[int] | None = None) -> None:
576    def output_resampled_data(
577        self,
578        input_dirpath: str,
579        resampled_dirpath: str,
580        c2c1_ratio_dirpath: str,
581        input_file_pattern: str = r"Eddy_(\d+)",
582        input_files_suffix: str = ".dat",
583        col_c1: str = "Ultra_CH4_ppm_C",
584        col_c2: str = "Ultra_C2H6_ppb",
585        output_c2c1_ratio: bool = True,
586        output_resampled: bool = True,
587        c2c1_ratio_csv_prefix: str = "SAC.Ultra",
588        index_column: str = "TIMESTAMP",
589        index_format: str = "%Y-%m-%d %H:%M:%S.%f",
590        resample: bool = True,
591        interpolate: bool = True,
592        numeric_columns: list[str] | None = None,
593        metadata_rows: int = 4,
594        skiprows: list[int] | None = None,
595    ) -> None:
596        """
597        指定されたディレクトリ内のCSVファイルを処理し、リサンプリングと欠損値補間を行います。
598
599        このメソッドは、指定されたディレクトリ内のCSVファイルを読み込み、リサンプリングを行い、
600        欠損値を補完します。処理結果として、リサンプリングされたCSVファイルを出力し、
601        相関係数やC2H6/CH4比を計算してDataFrameに保存します。
602        リサンプリングと欠損値補完は`get_resampled_df`と同様のロジックを使用します。
603
604        Parameters
605        ----------
606            input_dirpath: str
607                入力CSVファイルが格納されているディレクトリのパス
608            resampled_dirpath: str
609                リサンプリングされたCSVファイルを出力するディレクトリのパス
610            c2c1_ratio_dirpath: str
611                計算結果を保存するディレクトリのパス
612            input_file_pattern: str, optional
613                ファイル名からソートキーを抽出する正規表現パターン。デフォルトは"Eddy_(\\d+)"で、最初の数字グループでソートします
614            input_files_suffix: str, optional
615                入力ファイルの拡張子。デフォルトは".dat"
616            col_c1: str, optional
617                CH4濃度を含む列名。デフォルトは"Ultra_CH4_ppm_C"
618            col_c2: str, optional
619                C2H6濃度を含む列名。デフォルトは"Ultra_C2H6_ppb"
620            output_c2c1_ratio: bool, optional
621                線形回帰を行うかどうか。デフォルトはTrue
622            output_resampled: bool, optional
623                リサンプリングされたCSVファイルを出力するかどうか。デフォルトはTrue
624            c2c1_ratio_csv_prefix: str, optional
625                出力ファイルの接頭辞。デフォルトは"SAC.Ultra"で、出力時は"SAC.Ultra.2024.09.21.ratio.csv"のような形式となる
626            index_column: str, optional
627                日時情報を含む列名。デフォルトは"TIMESTAMP"
628            index_format: str, optional
629                インデックスの日付形式。デフォルトは"%Y-%m-%d %H:%M:%S.%f"
630            resample: bool, optional
631                リサンプリングを行うかどうか。デフォルトはTrue
632            interpolate: bool, optional
633                欠損値補間を行うかどうか。デフォルトはTrue
634            numeric_columns: list[str] | None, optional
635                数値型に変換する列名のリスト。デフォルトはNoneで、その場合は標準の列名リストを使用
636            metadata_rows: int, optional
637                メタデータとして読み込む行数。デフォルトは4
638            skiprows: list[int] | None, optional
639                スキップする行インデックスのリスト。デフォルトはNoneで、その場合は[0, 2, 3]を使用
640
641        Raises
642        ----------
643            OSError
644                ディレクトリの作成に失敗した場合
645            FileNotFoundError
646                入力ファイルが見つからない場合
647            ValueError
648                出力ディレクトリが指定されていない、またはデータの処理中にエラーが発生した場合
649
650        Examples
651        --------
652        >>> edp = EddyDataPreprocessor(fs=10)
653        >>> edp.output_resampled_data(
654        ...     input_dirpath="data/raw",
655        ...     resampled_dirpath="data/resampled",
656        ...     c2c1_ratio_dirpath="data/ratio",
657        ...     col_c1="Ultra_CH4_ppm_C",
658        ...     col_c2="Ultra_C2H6_ppb"
659        ... )
660        """
661        # 出力オプションとディレクトリの検証
662        if output_resampled and resampled_dirpath is None:
663            raise ValueError(
664                "output_resampled が True の場合、resampled_dirpath を指定する必要があります"
665            )
666        if output_c2c1_ratio and c2c1_ratio_dirpath is None:
667            raise ValueError(
668                "output_c2c1_ratio が True の場合、c2c1_ratio_dirpath を指定する必要があります"
669            )
670
671        # ディレクトリの作成(必要な場合のみ)
672        if output_resampled:
673            os.makedirs(resampled_dirpath, exist_ok=True)
674        if output_c2c1_ratio:
675            os.makedirs(c2c1_ratio_dirpath, exist_ok=True)
676
677        # デフォルト値の設定
678        if numeric_columns is None:
679            numeric_columns = [
680                "Ux",
681                "Uy",
682                "Uz",
683                "Tv",
684                "diag_sonic",
685                "CO2_new",
686                "H2O",
687                "diag_irga",
688                "cell_tmpr",
689                "cell_press",
690                "Ultra_CH4_ppm",
691                "Ultra_C2H6_ppb",
692                "Ultra_H2O_ppm",
693                "Ultra_CH4_ppm_C",
694                "Ultra_C2H6_ppb_C",
695            ]
696        if skiprows is None:
697            skiprows = [0, 2, 3]
698
699        ratio_data: list[dict[str, str | float]] = []
700        latest_date: datetime = datetime.min
701
702        # csvファイル名のリスト
703        csv_files: list[str] = EddyDataPreprocessor._get_sorted_files(
704            input_dirpath, input_file_pattern, input_files_suffix
705        )
706
707        for filename in tqdm(csv_files, desc="Processing files"):
708            input_filepath: str = os.path.join(input_dirpath, filename)
709            # リサンプリング&欠損値補間
710            df, metadata = self.get_resampled_df(
711                filepath=input_filepath,
712                index_column=index_column,
713                index_format=index_format,
714                interpolate=interpolate,
715                resample=resample,
716                numeric_columns=numeric_columns,
717                metadata_rows=metadata_rows,
718                skiprows=skiprows,
719            )
720
721            # 開始時間を取得
722            start_time: datetime = pd.to_datetime(df[index_column].iloc[0])
723            # 処理したファイルの中で最も最新の日付
724            latest_date = max(latest_date, start_time)
725
726            # リサンプリング&欠損値補間したCSVを出力
727            if output_resampled:
728                base_filename: str = re.sub(rf"\{input_files_suffix}$", "", filename)
729                output_csv_path: str = os.path.join(
730                    resampled_dirpath, f"{base_filename}-resampled.csv"
731                )
732                # メタデータを先に書き込む
733                with open(output_csv_path, "w") as f:
734                    for line in metadata:
735                        f.write(f"{line}\n")
736                # データフレームを追記モードで書き込む
737                df.to_csv(
738                    output_csv_path, index=False, mode="a", quoting=3, header=False
739                )
740
741            # 相関係数とC2H6/CH4比を計算
742            if output_c2c1_ratio:
743                ch4_data: pd.Series = df[col_c1]
744                c2h6_data: pd.Series = df[col_c2]
745
746                ratio_row: dict[str, str | float] = {
747                    "Date": start_time.strftime("%Y-%m-%d %H:%M:%S.%f"),
748                    "slope": f"{np.nan}",
749                    "intercept": f"{np.nan}",
750                    "r_value": f"{np.nan}",
751                    "p_value": f"{np.nan}",
752                    "stderr": f"{np.nan}",
753                }
754                # 近似直線の傾き、切片、相関係数を計算
755                try:
756                    slope, intercept, r_value, p_value, stderr = stats.linregress(
757                        ch4_data, c2h6_data
758                    )
759                    ratio_row: dict[str, str | float] = {
760                        "Date": start_time.strftime("%Y-%m-%d %H:%M:%S.%f"),
761                        "slope": f"{slope:.6f}",
762                        "intercept": f"{intercept:.6f}",
763                        "r_value": f"{r_value:.6f}",
764                        "p_value": f"{p_value:.6f}",
765                        "stderr": f"{stderr:.6f}",
766                    }
767                except Exception:
768                    # 何もせず、デフォルトの ratio_row を使用する
769                    pass
770
771                # 結果をリストに追加
772                ratio_data.append(ratio_row)
773
774        if output_c2c1_ratio:
775            # DataFrameを作成し、Dateカラムで昇順ソート
776            ratio_df: pd.DataFrame = pd.DataFrame(ratio_data)
777            ratio_df["Date"] = pd.to_datetime(
778                ratio_df["Date"]
779            )  # Dateカラムをdatetime型に変換
780            ratio_df = ratio_df.sort_values("Date")  # Dateカラムで昇順ソート
781
782            # CSVとして保存
783            ratio_filename: str = (
784                f"{c2c1_ratio_csv_prefix}.{latest_date.strftime('%Y.%m.%d')}.ratio.csv"
785            )
786            ratio_path: str = os.path.join(c2c1_ratio_dirpath, ratio_filename)
787            ratio_df.to_csv(ratio_path, index=False)

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

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

Parameters

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

Raises

OSError
    ディレクトリの作成に失敗した場合
FileNotFoundError
    入力ファイルが見つからない場合
ValueError
    出力ディレクトリが指定されていない、またはデータの処理中にエラーが発生した場合

Examples

>>> edp = EddyDataPreprocessor(fs=10)
>>> edp.output_resampled_data(
...     input_dirpath="data/raw",
...     resampled_dirpath="data/resampled",
...     c2c1_ratio_dirpath="data/ratio",
...     col_c1="Ultra_CH4_ppm_C",
...     col_c2="Ultra_C2H6_ppb"
... )
@staticmethod
def calculate_lag_time( df: pandas.core.frame.DataFrame, col1: str, col2_list: list[str]) -> list[int]:
789    @staticmethod
790    def calculate_lag_time(
791        df: pd.DataFrame,
792        col1: str,
793        col2_list: list[str],
794    ) -> list[int]:
795        """指定された基準変数と比較変数の間の遅れ時間(ディレイ)を計算します。
796        周波数が10Hzでcol1がcol2より10.0秒遅れている場合は、+100がインデックスとして取得されます。
797
798        Parameters
799        ----------
800            df: pd.DataFrame
801                遅れ時間の計算に使用するデータフレーム
802            col1: str
803                基準変数の列名
804            col2_list: list[str]
805                比較変数の列名のリスト
806
807        Returns
808        -------
809            list[int]
810                各比較変数に対する遅れ時間(ディレイ)のリスト。正の値は基準変数が比較変数より遅れていることを示し、
811                負の値は基準変数が比較変数より進んでいることを示します。
812
813        Examples
814        --------
815        >>> df = pd.DataFrame({
816        ...     'var1': [1, 2, 3, 4, 5],
817        ...     'var2': [2, 3, 4, 5, 6],
818        ...     'var3': [3, 4, 5, 6, 7]
819        ... })
820        >>> EddyDataPreprocessor.calculate_lag_time(df, 'var1', ['var2', 'var3'])
821        [1, 2]
822        """
823        lags_list: list[int] = []
824        for col2 in col2_list:
825            data1: np.ndarray = np.array(df[col1].values)
826            data2: np.ndarray = np.array(df[col2].values)
827
828            # 平均を0に調整
829            data1 = data1 - data1.mean()
830            data2 = data2 - data2.mean()
831
832            data_length: int = len(data1)
833
834            # 相互相関の計算
835            correlation: np.ndarray = np.correlate(
836                data1, data2, mode="full"
837            )  # data2とdata1の順序を入れ替え
838
839            # 相互相関のピークのインデックスを取得
840            lag: int = int((data_length - 1) - correlation.argmax())  # 符号を反転
841
842            lags_list.append(lag)
843        return lags_list

指定された基準変数と比較変数の間の遅れ時間(ディレイ)を計算します。 周波数が10Hzでcol1がcol2より10.0秒遅れている場合は、+100がインデックスとして取得されます。

Parameters

df: pd.DataFrame
    遅れ時間の計算に使用するデータフレーム
col1: str
    基準変数の列名
col2_list: list[str]
    比較変数の列名のリスト

Returns

list[int]
    各比較変数に対する遅れ時間(ディレイ)のリスト。正の値は基準変数が比較変数より遅れていることを示し、
    負の値は基準変数が比較変数より進んでいることを示します。

Examples

>>> df = pd.DataFrame({
...     'var1': [1, 2, 3, 4, 5],
...     'var2': [2, 3, 4, 5, 6],
...     'var3': [3, 4, 5, 6, 7]
... })
>>> EddyDataPreprocessor.calculate_lag_time(df, 'var1', ['var2', 'var3'])
[1, 2]
@dataclass
class EmissionData:
 17@dataclass
 18class EmissionData:
 19    """
 20    ホットスポットの排出量データを格納するクラス。
 21
 22    Parameters
 23    ----------
 24        timestamp: str
 25            タイムスタンプ
 26        avg_lat: float
 27            平均緯度
 28        avg_lon: float
 29            平均経度
 30        delta_ch4: float
 31            CH4の増加量 (ppm)
 32        delta_c2h6: float
 33            C2H6の増加量 (ppb)
 34        delta_ratio: float
 35            C2H6/CH4比
 36        emission_per_min: float
 37            排出量 (L/min)
 38        emission_per_day: float
 39            日排出量 (L/day)
 40        emission_per_year: float
 41            年間排出量 (L/year)
 42        section: str | int | float
 43            セクション情報
 44        type: HotspotType
 45            ホットスポットの種類(`HotspotType`を参照)
 46    """
 47
 48    timestamp: str
 49    avg_lat: float
 50    avg_lon: float
 51    delta_ch4: float
 52    delta_c2h6: float
 53    delta_ratio: float
 54    emission_per_min: float
 55    emission_per_day: float
 56    emission_per_year: float
 57    section: str | int | float
 58    type: HotspotType
 59
 60    def __post_init__(self) -> None:
 61        """
 62        Initialize時のバリデーションを行います。
 63
 64        Raises
 65        ----------
 66            ValueError: 入力値が不正な場合
 67        """
 68        # timestamp のバリデーション
 69        if not isinstance(self.timestamp, str) or not self.timestamp.strip():
 70            raise ValueError("'timestamp' must be a non-empty string")
 71
 72        # typeのバリデーションは型システムによって保証されるため削除
 73        # HotspotTypeはLiteral["bio", "gas", "comb"]として定義されているため、
 74        # 不正な値は型チェック時に検出されます
 75
 76        # section のバリデーション(Noneは許可)
 77        if self.section is not None and not isinstance(self.section, str | int | float):
 78            raise ValueError("'section' must be a string, int, float, or None")
 79
 80        # avg_lat のバリデーション
 81        if not isinstance(self.avg_lat, int | float) or not -90 <= self.avg_lat <= 90:
 82            raise ValueError("'avg_lat' must be a number between -90 and 90")
 83
 84        # avg_lon のバリデーション
 85        if not isinstance(self.avg_lon, int | float) or not -180 <= self.avg_lon <= 180:
 86            raise ValueError("'avg_lon' must be a number between -180 and 180")
 87
 88        # delta_ch4 のバリデーション
 89        if not isinstance(self.delta_ch4, int | float) or self.delta_ch4 < 0:
 90            raise ValueError("'delta_ch4' must be a non-negative number")
 91
 92        # delta_c2h6 のバリデーション
 93        if not isinstance(self.delta_c2h6, int | float):
 94            raise ValueError("'delta_c2h6' must be a int or float")
 95
 96        # delta_ratio のバリデーション
 97        if not isinstance(self.delta_ratio, int | float):
 98            raise ValueError("'delta_ratio' must be a int or float")
 99
100        # emission_per_min のバリデーション
101        if (
102            not isinstance(self.emission_per_min, int | float)
103            or self.emission_per_min < 0
104        ):
105            raise ValueError("'emission_per_min' must be a non-negative number")
106
107        # emission_per_day のバリデーション
108        expected_daily = self.emission_per_min * 60 * 24
109        if not math.isclose(self.emission_per_day, expected_daily, rel_tol=1e-10):
110            raise ValueError(
111                f"'emission_per_day' ({self.emission_per_day}) does not match "
112                f"calculated value from emission rate ({expected_daily})"
113            )
114
115        # emission_per_year のバリデーション
116        expected_annual = self.emission_per_day * 365
117        if not math.isclose(self.emission_per_year, expected_annual, rel_tol=1e-10):
118            raise ValueError(
119                f"'emission_per_year' ({self.emission_per_year}) does not match "
120                f"calculated value from daily emission ({expected_annual})"
121            )
122
123    def to_dict(self) -> dict:
124        """
125        データクラスの内容を辞書形式に変換します。
126
127        Returns
128        ----------
129            dict: データクラスの属性と値を含む辞書
130        """
131        return {
132            "timestamp": self.timestamp,
133            "delta_ch4": self.delta_ch4,
134            "delta_c2h6": self.delta_c2h6,
135            "delta_ratio": self.delta_ratio,
136            "emission_per_min": self.emission_per_min,
137            "emission_per_day": self.emission_per_day,
138            "emission_per_year": self.emission_per_year,
139            "avg_lat": self.avg_lat,
140            "avg_lon": self.avg_lon,
141            "section": self.section,
142            "type": self.type,
143        }

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

Parameters

timestamp: str
    タイムスタンプ
avg_lat: float
    平均緯度
avg_lon: float
    平均経度
delta_ch4: float
    CH4の増加量 (ppm)
delta_c2h6: float
    C2H6の増加量 (ppb)
delta_ratio: float
    C2H6/CH4比
emission_per_min: float
    排出量 (L/min)
emission_per_day: float
    日排出量 (L/day)
emission_per_year: float
    年間排出量 (L/year)
section: str | int | float
    セクション情報
type: HotspotType
    ホットスポットの種類(`HotspotType`を参照)
EmissionData( timestamp: str, avg_lat: float, avg_lon: float, delta_ch4: float, delta_c2h6: float, delta_ratio: float, emission_per_min: float, emission_per_day: float, emission_per_year: float, section: str | int | float, type: Literal['bio', 'gas', 'comb', 'scale_check'])
timestamp: str
avg_lat: float
avg_lon: float
delta_ch4: float
delta_c2h6: float
delta_ratio: float
emission_per_min: float
emission_per_day: float
emission_per_year: float
section: str | int | float
type: Literal['bio', 'gas', 'comb', 'scale_check']
def to_dict(self) -> dict:
123    def to_dict(self) -> dict:
124        """
125        データクラスの内容を辞書形式に変換します。
126
127        Returns
128        ----------
129            dict: データクラスの属性と値を含む辞書
130        """
131        return {
132            "timestamp": self.timestamp,
133            "delta_ch4": self.delta_ch4,
134            "delta_c2h6": self.delta_c2h6,
135            "delta_ratio": self.delta_ratio,
136            "emission_per_min": self.emission_per_min,
137            "emission_per_day": self.emission_per_day,
138            "emission_per_year": self.emission_per_year,
139            "avg_lat": self.avg_lat,
140            "avg_lon": self.avg_lon,
141            "section": self.section,
142            "type": self.type,
143        }

データクラスの内容を辞書形式に変換します。

Returns

dict: データクラスの属性と値を含む辞書
@dataclass
class EmissionFormula:
146@dataclass
147class EmissionFormula:
148    """
149    排出量計算式の係数セットを保持するデータクラス
150    設定した`coef_a`と`coef_b`は以下のように使用される。
151
152    ```python
153    emission_per_min = np.exp((np.log(spot.delta_ch4) + coef_a) / coef_b)
154    ```
155
156    Parameters
157    ----------
158        name: str
159            計算式の名前(例: "weller", "weitzel", "joo", "umezawa"など)
160        coef_a: float
161            計算式の係数a
162        coef_b: float
163            計算式の係数b
164
165    Examples
166    ----------
167    >>> # Weller et al. (2022)の係数を使用する場合
168    >>> formula = EmissionFormula(name="weller", coef_a=0.988, coef_b=0.817)
169    >>>
170    >>> # Weitzel et al. (2019)の係数を使用する場合
171    >>> formula = EmissionFormula(name="weitzel", coef_a=0.521, coef_b=0.795)
172    >>>
173    >>> # カスタム係数を使用する場合
174    >>> formula = EmissionFormula(name="custom", coef_a=1.0, coef_b=1.0)
175    """
176
177    name: str
178    coef_a: float
179    coef_b: float
180
181    def __post_init__(self) -> None:
182        """
183        パラメータの検証を行います。
184        """
185        if not isinstance(self.name, str) or not self.name.strip():
186            raise ValueError("'name' must be a non-empty string")
187        if not isinstance(self.coef_a, (int | float)):
188            raise ValueError("'coef_a' must be a number")
189        if not isinstance(self.coef_b, (int | float)):
190            raise ValueError("'coef_b' must be a number")

排出量計算式の係数セットを保持するデータクラス 設定したcoef_acoef_bは以下のように使用される。

emission_per_min = np.exp((np.log(spot.delta_ch4) + coef_a) / coef_b)

Parameters

name: str
    計算式の名前(例: "weller", "weitzel", "joo", "umezawa"など)
coef_a: float
    計算式の係数a
coef_b: float
    計算式の係数b

Examples

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

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

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

FftFileReorganizer( input_dirpath: str, output_dirpath: str, flag_csv_path: str, filename_patterns: list[str] | None = None, output_dirpaths_struct: dict[str, str] | None = None, sort_by_rh: bool = True, logger: logging.Logger | None = None, logging_debug: bool = False)
 35    def __init__(
 36        self,
 37        input_dirpath: str,
 38        output_dirpath: str,
 39        flag_csv_path: str,
 40        filename_patterns: list[str] | None = None,
 41        output_dirpaths_struct: dict[str, str] | None = None,
 42        sort_by_rh: bool = True,
 43        logger: Logger | None = None,
 44        logging_debug: bool = False,
 45    ):
 46        """
 47        FftFileReorganizerクラスを初期化します。
 48
 49        Parameters
 50        ----------
 51            input_dirpath: str
 52                入力ファイルが格納されているディレクトリのパス
 53            output_dirpath: str
 54                出力ファイルを格納するディレクトリのパス
 55            flag_csv_path: str
 56                フラグ情報が記載されているCSVファイルのパス
 57            filename_patterns: list[str] | None, optional
 58                ファイル名のパターン(正規表現)のリスト。指定しない場合はデフォルトのパターンが使用されます。
 59            output_dirpaths_struct: dict[str, str] | None, optional
 60                出力ディレクトリの構造を定義する辞書。指定しない場合はデフォルトの構造が使用されます。
 61            sort_by_rh: bool, optional
 62                RHに基づいてサブディレクトリにファイルを分類するかどうか。デフォルトはTrueです。
 63            logger: Logger | None, optional
 64                使用するロガー。指定しない場合は新規のロガーが作成されます。
 65            logging_debug: bool, optional
 66                ログレベルをDEBUGに設定するかどうか。デフォルトはFalseです。
 67
 68        Examples
 69        -------
 70        >>> reorganizer = FftFileReorganizer(
 71        ...     input_dirpath="./raw_data",
 72        ...     output_dirpath="./processed_data",
 73        ...     flag_csv_path="./flags.csv"
 74        ... )
 75        >>> reorganizer.reorganize()  # ファイルの再編成を実行
 76        """
 77        self._fft_path: str = input_dirpath
 78        self._sorted_path: str = output_dirpath
 79        self._output_dirpaths_struct = (
 80            output_dirpaths_struct or self.DEFAULT_OUTPUT_DIRS
 81        )
 82        self._good_data_path: str = os.path.join(
 83            output_dirpath, self._output_dirpaths_struct["GOOD_DATA"]
 84        )
 85        self._bad_data_path: str = os.path.join(
 86            output_dirpath, self._output_dirpaths_struct["BAD_DATA"]
 87        )
 88        self._filename_patterns: list[str] = (
 89            self.DEFAULT_FILENAME_PATTERNS.copy()
 90            if filename_patterns is None
 91            else filename_patterns
 92        )
 93        self._flag_filepath: str = flag_csv_path
 94        self._sort_by_rh: bool = sort_by_rh
 95        self._flags = {}
 96        self._warnings = []
 97        # ロガー
 98        log_level: int = INFO
 99        if logging_debug:
100            log_level = DEBUG
101        self.logger: Logger = setup_logger(logger=logger, log_level=log_level)

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

Parameters

input_dirpath: str
    入力ファイルが格納されているディレクトリのパス
output_dirpath: str
    出力ファイルを格納するディレクトリのパス
flag_csv_path: str
    フラグ情報が記載されているCSVファイルのパス
filename_patterns: list[str] | None, optional
    ファイル名のパターン(正規表現)のリスト。指定しない場合はデフォルトのパターンが使用されます。
output_dirpaths_struct: dict[str, str] | None, optional
    出力ディレクトリの構造を定義する辞書。指定しない場合はデフォルトの構造が使用されます。
sort_by_rh: bool, optional
    RHに基づいてサブディレクトリにファイルを分類するかどうか。デフォルトはTrueです。
logger: Logger | None, optional
    使用するロガー。指定しない場合は新規のロガーが作成されます。
logging_debug: bool, optional
    ログレベルをDEBUGに設定するかどうか。デフォルトはFalseです。

Examples

>>> reorganizer = FftFileReorganizer(
...     input_dirpath="./raw_data",
...     output_dirpath="./processed_data",
...     flag_csv_path="./flags.csv"
... )
>>> reorganizer.reorganize()  # ファイルの再編成を実行
DEFAULT_FILENAME_PATTERNS: ClassVar[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: ClassVar[dict[str, str]] = {'GOOD_DATA': 'good_data_all', 'BAD_DATA': 'bad_data'}
logger: logging.Logger
def reorganize(self):
103    def reorganize(self):
104        """
105        ファイルの再編成プロセス全体を実行します。
106        
107        ディレクトリの準備、フラグファイルの読み込み、有効なファイルの取得、ファイルのコピーを順に行います。
108        処理後、警告メッセージがあれば出力します。
109
110        Returns
111        ----------
112            None
113                戻り値はありません。
114
115        Examples
116        -------
117        >>> reorganizer = FftFileReorganizer(
118        ...     input_dirpath="./raw_data",
119        ...     output_dirpath="./processed_data", 
120        ...     flag_csv_path="./flags.csv"
121        ... )
122        >>> reorganizer.reorganize()  # ファイルの再編成を実行
123        """
124        self._prepare_directories()
125        self._read_flag_file()
126        valid_files = self._get_valid_files()
127        self._copy_files(valid_files)
128
129        if self._warnings:
130            self.logger.warning("Warnings:")
131            for warning in self._warnings:
132                self.logger.warning(warning)

ファイルの再編成プロセス全体を実行します。

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

Returns

None
    戻り値はありません。

Examples

>>> reorganizer = FftFileReorganizer(
...     input_dirpath="./raw_data",
...     output_dirpath="./processed_data", 
...     flag_csv_path="./flags.csv"
... )
>>> reorganizer.reorganize()  # ファイルの再編成を実行
@staticmethod
def get_rh_directory(rh: float):
256    @staticmethod
257    def get_rh_directory(rh: float):
258        """
259        相対湿度の値に基づいて、保存先のディレクトリ名を決定します。
260        値は10刻みで切り上げられます。
261
262        Parameters
263        ----------
264            rh: float
265                相対湿度の値(0-100の範囲)
266
267        Returns
268        ----------
269            str
270                ディレクトリ名(例: "RH90")。不正な値の場合は"bad_data"
271
272        Examples
273        ----------
274        >>> FFTFilesReorganizer.get_rh_directory(80.1)
275        'RH90'
276        >>> FFTFilesReorganizer.get_rh_directory(86.0) 
277        'RH90'
278        >>> FFTFilesReorganizer.get_rh_directory(91.2)
279        'RH100'
280        >>> FFTFilesReorganizer.get_rh_directory(-1)
281        'bad_data'
282        """
283        if rh < 0 or rh > 100:  # 相対湿度として不正な値を除外
284            return "bad_data"
285        elif rh == 0:  # 0の場合はRH0に入れる
286            return "RH0"
287        else:  # 10刻みで切り上げ
288            return f"RH{min(int((rh + 9.99) // 10 * 10), 100)}"

相対湿度の値に基づいて、保存先のディレクトリ名を決定します。 値は10刻みで切り上げられます。

Parameters

rh: float
    相対湿度の値(0-100の範囲)

Returns

str
    ディレクトリ名(例: "RH90")。不正な値の場合は"bad_data"

Examples

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

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

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

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

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

FluxFootprintAnalyzer( z_m: float, na_values: list[str] | None = None, column_mapping: Mapping[typing.Literal['datetime', 'wind_direction', 'wind_speed', 'friction_velocity', 'sigma_v', 'stability'], str] | None = None, logger: logging.Logger | None = None, logging_debug: bool = False)
 93    def __init__(
 94        self,
 95        z_m: float,
 96        na_values: list[str] | None = None,
 97        column_mapping: Mapping[DefaultColumnsNames, str] | None = None,
 98        logger: Logger | None = None,
 99        logging_debug: bool = False,
100    ):
101        """
102        衛星画像を用いて FluxFootprintAnalyzer を初期化します。
103
104        Parameters
105        ----------
106            z_m: float
107                測定の高さ(メートル単位)
108            na_values: list[str] | None, optional
109                NaNと判定する値のパターン。デフォルト値は以下の通り:
110                ["#DIV/0!", "#VALUE!", "#REF!", "#N/A", "#NAME?", "NAN", "nan"]
111            column_mapping: Mapping[DefaultColumnsNames, str] | None, optional
112                入力データのカラム名とデフォルトカラム名のマッピング。
113                キーにスネークケースの識別子、値に実際のカラム名を指定します。
114                デフォルト値はNoneで、その場合は以下のデフォルトマッピングを使用:
115                ```python
116                {
117                    "datetime": "Date",                 # 日時カラム
118                    "wind_direction": "Wind direction", # 風向 [度]
119                    "wind_speed": "WS vector",          # 風速 [m/s]
120                    "friction_velocity": "u*",          # 摩擦速度 [m/s]
121                    "sigma_v": "sigmaV",                # 風速の標準偏差 [m/s]
122                    "stability": "z/L"                  # 安定度パラメータ [-]
123                }
124                ```
125                例えば、入力データのカラム名が異なる場合は以下のように指定:
126                ```python
127                {
128                    "wind_direction": "WD",      # 風向カラム名が"WD"の場合
129                    "wind_speed": "WS",          # 風速カラム名が"WS"の場合
130                    "friction_velocity": "USTAR" # 摩擦速度カラム名が"USTAR"の場合
131                }
132                ```
133                指定されなかったキーはデフォルト値が使用されます。
134            logger: Logger | None, optional
135                使用するロガー。デフォルト値はNoneで、その場合は新しいロガーを生成
136            logging_debug: bool, optional
137                ログレベルを"DEBUG"に設定するかどうか。デフォルト値はFalseで、その場合はINFO以上のレベルのメッセージを出力
138
139        Returns
140        ----------
141            None
142
143        Examples
144        --------
145        >>> # 基本的な初期化(デフォルトのカラム名を使用)
146        >>> analyzer = FluxFootprintAnalyzer(z_m=2.5)
147
148        >>> # カスタムのカラム名マッピングを指定
149        >>> custom_mapping = {
150        ...     "wind_direction": "WD",      # 風向カラムが"WD"
151        ...     "wind_speed": "WS",          # 風速カラムが"WS"
152        ...     "friction_velocity": "USTAR"  # 摩擦速度カラムが"USTAR"
153        ... }
154        >>> analyzer = FluxFootprintAnalyzer(
155        ...     z_m=3.0,
156        ...     column_mapping=custom_mapping
157        ... )
158        """
159        # デフォルトのカラム名を設定
160        self._default_cols = DefaultColumns()
161        # カラム名マッピングの作成
162        self._cols: Mapping[DefaultColumnsNames, str] = self._create_column_mapping(
163            column_mapping
164        )
165        # 必須カラムのリストを作成
166        self._required_columns = [
167            self._cols[self._default_cols.WIND_DIRECTION],
168            self._cols[self._default_cols.WIND_SPEED],
169            self._cols[self._default_cols.FRICTION_VELOCITY],
170            self._cols[self._default_cols.SIGMA_V],
171            self._cols[self._default_cols.STABILITY],
172        ]
173        self._z_m: float = z_m  # 測定高度
174        if na_values is None:
175            na_values = [
176                "#DIV/0!",
177                "#VALUE!",
178                "#REF!",
179                "#N/A",
180                "#NAME?",
181                "NAN",
182                "nan",
183            ]
184        self._na_values: list[str] = na_values
185        # 状態を管理するフラグ
186        self._got_satellite_image: bool = False
187
188        # ロガー
189        log_level: int = INFO
190        if logging_debug:
191            log_level = DEBUG
192        self.logger: Logger = setup_logger(logger=logger, log_level=log_level)

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

Parameters

z_m: float
    測定の高さ(メートル単位)
na_values: list[str] | None, optional
    NaNと判定する値のパターン。デフォルト値は以下の通り:
    ["#DIV/0!", "#VALUE!", "#REF!", "#N/A", "#NAME?", "NAN", "nan"]
column_mapping: Mapping[DefaultColumnsNames, str] | None, optional
    入力データのカラム名とデフォルトカラム名のマッピング。
    キーにスネークケースの識別子、値に実際のカラム名を指定します。
    デフォルト値はNoneで、その場合は以下のデフォルトマッピングを使用:


    
{
    "datetime": "Date",                 # 日時カラム
    "wind_direction": "Wind direction", # 風向 [度]
    "wind_speed": "WS vector",          # 風速 [m/s]
    "friction_velocity": "u*",          # 摩擦速度 [m/s]
    "sigma_v": "sigmaV",                # 風速の標準偏差 [m/s]
    "stability": "z/L"                  # 安定度パラメータ [-]
}
例えば、入力データのカラム名が異なる場合は以下のように指定:
{
    "wind_direction": "WD",      # 風向カラム名が"WD"の場合
    "wind_speed": "WS",          # 風速カラム名が"WS"の場合
    "friction_velocity": "USTAR" # 摩擦速度カラム名が"USTAR"の場合
}
指定されなかったキーはデフォルト値が使用されます。 logger: Logger | None, optional 使用するロガー。デフォルト値はNoneで、その場合は新しいロガーを生成 logging_debug: bool, optional ログレベルを"DEBUG"に設定するかどうか。デフォルト値はFalseで、その場合はINFO以上のレベルのメッセージを出力

Returns

None

Examples

>>> # 基本的な初期化(デフォルトのカラム名を使用)
>>> analyzer = FluxFootprintAnalyzer(z_m=2.5)
>>> # カスタムのカラム名マッピングを指定
>>> custom_mapping = {
...     "wind_direction": "WD",      # 風向カラムが"WD"
...     "wind_speed": "WS",          # 風速カラムが"WS"
...     "friction_velocity": "USTAR"  # 摩擦速度カラムが"USTAR"
... }
>>> analyzer = FluxFootprintAnalyzer(
...     z_m=3.0,
...     column_mapping=custom_mapping
... )
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:
228    def check_required_columns(
229        self,
230        df: pd.DataFrame,
231        col_datetime: str | None = None,
232    ) -> bool:
233        """
234        データフレームに必要なカラムが存在するかチェックします。
235
236        Parameters
237        ----------
238            df: pd.DataFrame
239                チェック対象のデータフレーム
240            col_datetime: str | None, optional
241                日時カラム名。指定された場合はチェック対象から除外されます。デフォルト値はNoneです。
242
243        Returns
244        -------
245            bool
246                すべての必須カラムが存在する場合はTrue、存在しない場合はFalseを返します。
247
248        Examples
249        --------
250        >>> import pandas as pd
251        >>> # カスタムのカラム名を持つデータフレームを作成
252        >>> df = pd.DataFrame({
253        ...     'TIMESTAMP': ['2024-01-01'],
254        ...     'WD': [180.0],
255        ...     'WS': [2.5],
256        ...     'USTAR': [0.3],
257        ...     'SIGMAV': [0.5],
258        ...     'ZL': [0.1]
259        ... })
260        >>> # カスタムのカラム名マッピングを定義
261        >>> custom_mapping = {
262        ...     "datetime": "TIMESTAMP",      # 日時カラム
263        ...     "wind_direction": "WD",       # 風向カラム
264        ...     "wind_speed": "WS",           # 風速カラム
265        ...     "friction_velocity": "USTAR",  # 摩擦速度カラム
266        ...     "sigma_v": "SIGMAV",          # 風速の標準偏差カラム
267        ...     "stability": "ZL"             # 安定度パラメータカラム
268        ... }
269        >>> # カスタムマッピングを使用してアナライザーを初期化
270        >>> analyzer = FluxFootprintAnalyzer(
271        ...     z_m=2.5,
272        ...     column_mapping=custom_mapping
273        ... )
274        >>> analyzer.check_required_columns(df)
275        True
276        """
277        check_columns = [
278            self._cols[self._default_cols.WIND_DIRECTION],
279            self._cols[self._default_cols.WIND_SPEED],
280            self._cols[self._default_cols.FRICTION_VELOCITY],
281            self._cols[self._default_cols.SIGMA_V],
282            self._cols[self._default_cols.STABILITY],
283        ]
284
285        missing_columns = [col for col in check_columns if col not in df.columns]
286        if missing_columns:
287            self.logger.error(
288                f"Required columns are missing: {missing_columns}."
289                f"Available columns: {df.columns.tolist()}"
290            )
291            return False
292
293        return True

データフレームに必要なカラムが存在するかチェックします。

Parameters

df: pd.DataFrame
    チェック対象のデータフレーム
col_datetime: str | None, optional
    日時カラム名。指定された場合はチェック対象から除外されます。デフォルト値はNoneです。

Returns

bool
    すべての必須カラムが存在する場合はTrue、存在しない場合はFalseを返します。

Examples

>>> import pandas as pd
>>> # カスタムのカラム名を持つデータフレームを作成
>>> df = pd.DataFrame({
...     'TIMESTAMP': ['2024-01-01'],
...     'WD': [180.0],
...     'WS': [2.5],
...     'USTAR': [0.3],
...     'SIGMAV': [0.5],
...     'ZL': [0.1]
... })
>>> # カスタムのカラム名マッピングを定義
>>> custom_mapping = {
...     "datetime": "TIMESTAMP",      # 日時カラム
...     "wind_direction": "WD",       # 風向カラム
...     "wind_speed": "WS",           # 風速カラム
...     "friction_velocity": "USTAR",  # 摩擦速度カラム
...     "sigma_v": "SIGMAV",          # 風速の標準偏差カラム
...     "stability": "ZL"             # 安定度パラメータカラム
... }
>>> # カスタムマッピングを使用してアナライザーを初期化
>>> analyzer = FluxFootprintAnalyzer(
...     z_m=2.5,
...     column_mapping=custom_mapping
... )
>>> analyzer.check_required_columns(df)
True
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]]:
295    def calculate_flux_footprint(
296        self,
297        df: pd.DataFrame,
298        col_flux: str,
299        plot_count: int = 10000,
300        start_time: str = "10:00",
301        end_time: str = "16:00",
302    ) -> tuple[list[float], list[float], list[float]]:
303        """
304        フラックスフットプリントを計算し、指定された時間帯のデータを基に可視化します。
305
306        Parameters
307        ----------
308            df: pd.DataFrame
309                分析対象のデータフレーム。フラックスデータを含む。
310            col_flux: str
311                フラックスデータの列名。計算に使用される。
312            plot_count: int, optional
313                生成するプロットの数。デフォルト値は10000。
314            start_time: str, optional
315                フットプリント計算に使用する開始時間。デフォルト値は"10:00"。
316            end_time: str, optional
317                フットプリント計算に使用する終了時間。デフォルト値は"16:00"。
318
319        Examples
320        --------
321        >>> import pandas as pd
322        >>> # カスタムのカラム名を持つデータフレームを作成
323        >>> df = pd.DataFrame({
324        ...     'TIMESTAMP': pd.date_range('2024-01-01', periods=24, freq='H'),
325        ...     'WD': [180.0] * 24,
326        ...     'WS': [2.5] * 24,
327        ...     'USTAR': [0.3] * 24,
328        ...     'SIGMAV': [0.5] * 24,
329        ...     'ZL': [0.1] * 24,
330        ...     'FCO2': [-2.0] * 24
331        ... })
332        >>> # カスタムのカラム名マッピングを定義
333        >>> custom_mapping = {
334        ...     "datetime": "TIMESTAMP",
335        ...     "wind_direction": "WD",
336        ...     "wind_speed": "WS",
337        ...     "friction_velocity": "USTAR",
338        ...     "sigma_v": "SIGMAV",
339        ...     "stability": "ZL"
340        ... }
341        >>> analyzer = FluxFootprintAnalyzer(
342        ...     z_m=2.5,
343        ...     column_mapping=custom_mapping
344        ... )
345        >>> x, y, flux = analyzer.calculate_flux_footprint(df, 'FCO2')
346        """
347        # インデックスがdatetimeであることを確認し、必要に応じて変換
348        df_internal: pd.DataFrame = df.copy()
349        if not isinstance(df_internal.index, pd.DatetimeIndex):
350            datetime_col = self._cols[self._default_cols.DATETIME]
351            df_internal.set_index(datetime_col, inplace=True)
352            df_internal.index = pd.to_datetime(df_internal.index)
353
354        # 平日/休日の判定を追加
355        df_internal[self.COL_FFA_IS_WEEKDAY] = df_internal.index.map(self.is_weekday)
356
357        # 平日データの抽出と時間帯フィルタリング
358        data_weekday = df_internal[df_internal[self.COL_FFA_IS_WEEKDAY] == 1].copy()
359        data_weekday = data_weekday.between_time(start_time, end_time)
360        data_weekday = data_weekday.dropna(subset=[col_flux])
361
362        # 風向の360度系への変換
363        wind_dir_col = self._cols[self._default_cols.WIND_DIRECTION]
364        directions = [
365            wind_direction if wind_direction >= 0 else wind_direction + 360
366            for wind_direction in data_weekday[wind_dir_col]
367        ]
368        data_weekday[self.COL_FFA_WIND_DIR_360] = directions
369        data_weekday[self.COL_FFA_RADIAN] = (
370            data_weekday[self.COL_FFA_WIND_DIR_360] * np.pi / 180
371        )
372
373        # 欠測値の除去
374        data_weekday = data_weekday.dropna(subset=[wind_dir_col, col_flux])
375
376        # 数値型への変換
377        numeric_columns: set[DefaultColumnsNames] = {
378            self._default_cols.FRICTION_VELOCITY,
379            self._default_cols.WIND_SPEED,
380            self._default_cols.SIGMA_V,
381            self._default_cols.STABILITY,
382        }
383        for col in numeric_columns:
384            data_weekday[self._cols[col]] = pd.to_numeric(
385                data_weekday[self._cols[col]], errors="coerce"
386            )
387
388        # 地面修正量dの計算
389        z_m: float = self._z_m
390        z_d: float = FluxFootprintAnalyzer._calculate_ground_correction(
391            z_m=z_m,
392            wind_speed=data_weekday[
393                self._cols[self._default_cols.WIND_SPEED]
394            ].to_numpy(),
395            friction_velocity=data_weekday[
396                self._cols[self._default_cols.FRICTION_VELOCITY]
397            ].to_numpy(),
398            stability_parameter=data_weekday[
399                self._cols[self._default_cols.STABILITY]
400            ].to_numpy(),
401        )
402
403        x_list: list[float] = []
404        y_list: list[float] = []
405        c_list: list[float] | None = []
406
407        # tqdmを使用してプログレスバーを表示
408        for i in tqdm(range(len(data_weekday)), desc="Calculating footprint"):
409            d_u_star: float = data_weekday[
410                self._cols[self._default_cols.FRICTION_VELOCITY]
411            ].iloc[i]
412            d_u: float = data_weekday[self._cols[self._default_cols.WIND_SPEED]].iloc[i]
413            sigma_v: float = data_weekday[self._cols[self._default_cols.SIGMA_V]].iloc[
414                i
415            ]
416            d_z_l: float = data_weekday[self._cols[self._default_cols.STABILITY]].iloc[
417                i
418            ]
419
420            if pd.isna(d_u_star) or pd.isna(d_u) or pd.isna(sigma_v) or pd.isna(d_z_l):
421                self.logger.warning(f"NaN fields are exist.: i = {i}")
422                continue
423            elif d_u_star < 5.0 and d_u_star != 0.0 and d_u > 0.1:
424                phi_m, phi_c, n = FluxFootprintAnalyzer._calculate_stability_parameters(
425                    d_z_l=d_z_l
426                )
427                m, u, r, mu, ksi = (
428                    FluxFootprintAnalyzer._calculate_footprint_parameters(
429                        d_u_star=d_u_star,
430                        d_u=d_u,
431                        z_d=z_d,
432                        phi_m=phi_m,
433                        phi_c=phi_c,
434                        n=n,
435                    )
436                )
437
438                # 80%ソースエリアの計算
439                x80: float = FluxFootprintAnalyzer._source_area_kormann2001(
440                    ksi=ksi, mu=mu, d_u=d_u, sigma_v=sigma_v, z_d=z_d, max_ratio=0.8
441                )
442
443                if not np.isnan(x80):
444                    x1, y1, flux1 = FluxFootprintAnalyzer._prepare_plot_data(
445                        x80=x80,
446                        ksi=ksi,
447                        mu=mu,
448                        r=r,
449                        u=u,
450                        m=m,
451                        sigma_v=sigma_v,
452                        flux_value=data_weekday[col_flux].iloc[i],
453                        plot_count=plot_count,
454                    )
455                    x1_rotated, y1__rotated = FluxFootprintAnalyzer._rotate_coordinates(
456                        x=x1, y=y1, radian=data_weekday[self.COL_FFA_RADIAN].iloc[i]
457                    )
458
459                    x_list.extend(x1_rotated)
460                    y_list.extend(y1__rotated)
461                    c_list.extend(flux1)
462
463        return (
464            x_list,
465            y_list,
466            c_list,
467        )

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

Parameters

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

Examples

>>> import pandas as pd
>>> # カスタムのカラム名を持つデータフレームを作成
>>> df = pd.DataFrame({
...     'TIMESTAMP': pd.date_range('2024-01-01', periods=24, freq='H'),
...     'WD': [180.0] * 24,
...     'WS': [2.5] * 24,
...     'USTAR': [0.3] * 24,
...     'SIGMAV': [0.5] * 24,
...     'ZL': [0.1] * 24,
...     'FCO2': [-2.0] * 24
... })
>>> # カスタムのカラム名マッピングを定義
>>> custom_mapping = {
...     "datetime": "TIMESTAMP",
...     "wind_direction": "WD",
...     "wind_speed": "WS",
...     "friction_velocity": "USTAR",
...     "sigma_v": "SIGMAV",
...     "stability": "ZL"
... }
>>> analyzer = FluxFootprintAnalyzer(
...     z_m=2.5,
...     column_mapping=custom_mapping
... )
>>> x, y, flux = analyzer.calculate_flux_footprint(df, 'FCO2')
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:
469    def combine_all_data(
470        self,
471        data_source: str | pd.DataFrame,
472        col_datetime: str = "Date",
473        source_type: Literal["csv", "monthly"] = "csv",
474    ) -> pd.DataFrame:
475        """
476        CSVファイルまたはMonthlyConverterからのデータを統合します。
477
478        Parameters
479        ----------
480            data_source: str | pd.DataFrame
481                CSVディレクトリパスまたはDataFrame形式のデータソース
482            col_datetime: str, optional
483                datetime型のカラム名。デフォルト値は"Date"
484            source_type: Literal["csv", "monthly"], optional
485                データソースの種類。"csv"または"monthly"を指定。デフォルト値は"csv"
486
487        Returns
488        ----------
489            pd.DataFrame
490                処理済みのデータフレーム。平日/休日の判定結果と欠損値を除去したデータ
491
492        Examples
493        --------
494        >>> # CSVファイルからデータを読み込む場合
495        >>> analyzer = FluxFootprintAnalyzer(z_m=2.5)
496        >>> df = analyzer.combine_all_data(
497        ...     data_source="path/to/csv/dir",
498        ...     source_type="csv"
499        ... )
500
501        >>> # DataFrameから直接データを読み込む場合
502        >>> analyzer = FluxFootprintAnalyzer(z_m=2.5)
503        >>> input_df = pd.DataFrame({
504        ...     "Date": pd.date_range("2024-01-01", periods=3),
505        ...     "Wind direction": [180, 270, 90],
506        ...     "WS vector": [2.5, 3.0, 1.5]
507        ... })
508        >>> df = analyzer.combine_all_data(
509        ...     data_source=input_df,
510        ...     source_type="monthly"
511        ... )
512        """
513        col_weekday: str = self.COL_FFA_IS_WEEKDAY
514        if source_type == "csv":
515            # 既存のCSV処理ロジック
516            if not isinstance(data_source, str):
517                raise ValueError(
518                    "source_type='csv'の場合、data_sourceはstr型である必要があります"
519                )
520            return self._combine_all_csv(
521                csv_dir_path=data_source, col_datetime=col_datetime
522            )
523        elif source_type == "monthly":
524            # MonthlyConverterからのデータを処理
525            if not isinstance(data_source, pd.DataFrame):
526                raise ValueError("monthly形式の場合、DataFrameを直接渡す必要があります")
527
528            df: pd.DataFrame = data_source.copy()
529
530            # required_columnsからDateを除外して欠損値チェックを行う
531            check_columns: list[str] = [
532                col for col in self._required_columns if col != col_datetime
533            ]
534
535            # インデックスがdatetimeであることを確認
536            if (
537                not isinstance(df.index, pd.DatetimeIndex)
538                and col_datetime not in df.columns
539            ):
540                raise ValueError(f"DatetimeIndexまたは{col_datetime}カラムが必要です")
541
542            if col_datetime in df.columns:
543                df.set_index(col_datetime, inplace=True)
544
545            # 必要なカラムの存在確認
546            missing_columns = [
547                col for col in check_columns if col not in df.columns.tolist()
548            ]
549            if missing_columns:
550                missing_cols = "','".join(missing_columns)
551                current_cols = "','".join(df.columns.tolist())
552                raise ValueError(
553                    f"必要なカラムが不足しています: '{missing_cols}'\n"
554                    f"現在のカラム: '{current_cols}'"
555                )
556
557            # 平日/休日の判定用カラムを追加
558            df[col_weekday] = df.index.map(FluxFootprintAnalyzer.is_weekday)
559
560            # Dateを除外したカラムで欠損値の処理
561            df = df.dropna(subset=check_columns)
562
563            # インデックスの重複を除去
564            df = df.loc[~df.index.duplicated(), :]
565
566            return df

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

Parameters

data_source: str | pd.DataFrame
    CSVディレクトリパスまたはDataFrame形式のデータソース
col_datetime: str, optional
    datetime型のカラム名。デフォルト値は"Date"
source_type: Literal["csv", "monthly"], optional
    データソースの種類。"csv"または"monthly"を指定。デフォルト値は"csv"

Returns

pd.DataFrame
    処理済みのデータフレーム。平日/休日の判定結果と欠損値を除去したデータ

Examples

>>> # CSVファイルからデータを読み込む場合
>>> analyzer = FluxFootprintAnalyzer(z_m=2.5)
>>> df = analyzer.combine_all_data(
...     data_source="path/to/csv/dir",
...     source_type="csv"
... )
>>> # DataFrameから直接データを読み込む場合
>>> analyzer = FluxFootprintAnalyzer(z_m=2.5)
>>> input_df = pd.DataFrame({
...     "Date": pd.date_range("2024-01-01", periods=3),
...     "Wind direction": [180, 270, 90],
...     "WS vector": [2.5, 3.0, 1.5]
... })
>>> df = analyzer.combine_all_data(
...     data_source=input_df,
...     source_type="monthly"
... )
def get_satellite_image_from_api( self, api_key: str, center_lat: float, center_lon: float, output_filepath: str, scale: int = 1, size: tuple[int, int] = (2160, 2160), zoom: int = 13) -> PIL.Image.Image:
568    def get_satellite_image_from_api(
569        self,
570        api_key: str,
571        center_lat: float,
572        center_lon: float,
573        output_filepath: str,
574        scale: int = 1,
575        size: tuple[int, int] = (2160, 2160),
576        zoom: int = 13,
577    ) -> Image.Image:
578        """
579        Google Maps Static APIを使用して衛星画像を取得します。
580
581        Parameters
582        ----------
583            api_key: str
584                Google Maps Static APIのキー
585            center_lat: float
586                中心の緯度
587            center_lon: float
588                中心の経度
589            output_filepath: str
590                画像の保存先パス。拡張子は'.png'のみ許可される
591            scale: int, optional
592                画像の解像度スケール。1または2を指定可能。デフォルトは1
593            size: tuple[int, int], optional
594                画像サイズ。(幅, 高さ)の形式で指定。デフォルトは(2160, 2160)
595            zoom: int, optional
596                ズームレベル。0から21の整数を指定可能。デフォルトは13
597
598        Returns
599        ----------
600            Image.Image
601                取得した衛星画像
602
603        Raises
604        ----------
605            requests.RequestException
606                API呼び出しに失敗した場合
607
608        Example
609        ----------
610        >>> analyzer = FluxFootprintAnalyzer()
611        >>> image = analyzer.get_satellite_image_from_api(
612        ...     api_key="your_api_key",
613        ...     center_lat=35.6895,
614        ...     center_lon=139.6917,
615        ...     output_filepath="satellite.png",
616        ...     zoom=15
617        ... )
618        """
619        # バリデーション
620        if not output_filepath.endswith(".png"):
621            raise ValueError("出力ファイル名は'.png'で終わる必要があります。")
622
623        # HTTPリクエストの定義
624        base_url = "https://maps.googleapis.com/maps/api/staticmap"
625        params = {
626            "center": f"{center_lat},{center_lon}",
627            "zoom": zoom,
628            "size": f"{size[0]}x{size[1]}",
629            "maptype": "satellite",
630            "scale": scale,
631            "key": api_key,
632        }
633
634        try:
635            response = requests.get(base_url, params=params)
636            response.raise_for_status()
637            # 画像ファイルに変換
638            image: Image.Image = Image.open(io.BytesIO(response.content))
639            image.save(output_filepath)
640            self._got_satellite_image = True
641            self.logger.info(f"リモート画像を取得し、保存しました: {output_filepath}")
642            return image
643        except requests.RequestException as e:
644            self.logger.error(f"衛星画像の取得に失敗しました: {e!s}")
645            raise e

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

Parameters

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

Returns

Image.Image
    取得した衛星画像

Raises

requests.RequestException
    API呼び出しに失敗した場合

Example

>>> analyzer = FluxFootprintAnalyzer()
>>> image = analyzer.get_satellite_image_from_api(
...     api_key="your_api_key",
...     center_lat=35.6895,
...     center_lon=139.6917,
...     output_filepath="satellite.png",
...     zoom=15
... )
def get_satellite_image_from_local( self, local_image_path: str, alpha: float = 1.0, grayscale: bool = False) -> PIL.Image.Image:
647    def get_satellite_image_from_local(
648        self,
649        local_image_path: str,
650        alpha: float = 1.0,
651        grayscale: bool = False,
652    ) -> Image.Image:
653        """
654        ローカルファイルから衛星画像を読み込みます。
655
656        Parameters
657        ----------
658            local_image_path: str
659                ローカル画像のパス
660            alpha: float, optional
661                画像の透過率。0.0から1.0の範囲で指定します。デフォルト値は1.0です。
662            grayscale: bool, optional
663                画像を白黒に変換するかどうかを指定します。デフォルト値はFalseです。
664
665        Returns
666        ----------
667            Image.Image
668                読み込んだ衛星画像(透過設定済み)
669
670        Raises
671        ----------
672            FileNotFoundError
673                指定されたパスにファイルが存在しない場合
674
675        Example
676        ----------
677        >>> analyzer = FluxFootprintAnalyzer()
678        >>> image = analyzer.get_satellite_image_from_local(
679        ...     local_image_path="satellite.png",
680        ...     alpha=0.7,
681        ...     grayscale=True
682        ... )
683        """
684        if not os.path.exists(local_image_path):
685            raise FileNotFoundError(
686                f"指定されたローカル画像が存在しません: {local_image_path}"
687            )
688
689        # 画像を読み込む
690        image: Image.Image = Image.open(local_image_path)
691
692        # 白黒変換が指定されている場合
693        if grayscale:
694            image = image.convert("L")  # グレースケールに変換
695
696        # RGBAモードに変換
697        image = image.convert("RGBA")
698
699        # 透過率を設定
700        data = image.getdata()
701        new_data = [(r, g, b, int(255 * alpha)) for r, g, b, a in data]
702        image.putdata(new_data)
703
704        self._got_satellite_image = True
705        self.logger.info(
706            f"ローカル画像を使用しました(透過率: {alpha}, 白黒: {grayscale}): {local_image_path}"
707        )
708        return image

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

Parameters

local_image_path: str
    ローカル画像のパス
alpha: float, optional
    画像の透過率。0.0から1.0の範囲で指定します。デフォルト値は1.0です。
grayscale: bool, optional
    画像を白黒に変換するかどうかを指定します。デフォルト値はFalseです。

Returns

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

Raises

FileNotFoundError
    指定されたパスにファイルが存在しない場合

Example

>>> analyzer = FluxFootprintAnalyzer()
>>> image = analyzer.get_satellite_image_from_local(
...     local_image_path="satellite.png",
...     alpha=0.7,
...     grayscale=True
... )
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: Callable = <function mean>, lat_correction: float = 1, lon_correction: float = 1, output_dirpath: str | pathlib.Path | None = None, output_filename: str = 'footprint.png', save_fig: bool = True, show_fig: bool = True, satellite_image: PIL.Image.Image | None = None, xy_max: float = 5000) -> None:
710    def plot_flux_footprint(
711        self,
712        x_list: list[float],
713        y_list: list[float],
714        c_list: list[float] | None,
715        center_lat: float,
716        center_lon: float,
717        vmin: float,
718        vmax: float,
719        add_cbar: bool = True,
720        add_legend: bool = True,
721        cbar_label: str | None = None,
722        cbar_labelpad: int = 20,
723        cmap: str = "jet",
724        reduce_c_function: Callable = np.mean,
725        lat_correction: float = 1,
726        lon_correction: float = 1,
727        output_dirpath: str | Path | None = None,
728        output_filename: str = "footprint.png",
729        save_fig: bool = True,
730        show_fig: bool = True,
731        satellite_image: Image.Image | None = None,
732        xy_max: float = 5000,
733    ) -> None:
734        """
735        フットプリントデータをプロットします。
736
737        このメソッドは、指定されたフットプリントデータのみを可視化します。
738
739        Parameters
740        ----------
741            x_list: list[float]
742                フットプリントのx座標リスト(メートル単位)
743            y_list: list[float]
744                フットプリントのy座標リスト(メートル単位)
745            c_list: list[float] | None
746                フットプリントの強度を示す値のリスト
747            center_lat: float
748                プロットの中心となる緯度
749            center_lon: float
750                プロットの中心となる経度
751            vmin: float
752                カラーバーの最小値
753            vmax: float
754                カラーバーの最大値
755            add_cbar: bool, optional
756                カラーバーを追加するかどうか。デフォルト値はTrueです
757            add_legend: bool, optional
758                凡例を追加するかどうか。デフォルト値はTrueです
759            cbar_label: str | None, optional
760                カラーバーのラベル。デフォルト値はNoneです
761            cbar_labelpad: int, optional
762                カラーバーラベルのパディング。デフォルト値は20です
763            cmap: str, optional
764                使用するカラーマップの名前。デフォルト値は"jet"です
765            reduce_c_function: Callable, optional
766                フットプリントの集約関数。デフォルト値はnp.meanです
767            lon_correction: float, optional
768                経度方向の補正係数。デフォルト値は1です
769            lat_correction: float, optional
770                緯度方向の補正係数。デフォルト値は1です
771            output_dirpath: str | Path | None, optional
772                プロット画像の保存先パス。デフォルト値はNoneです
773            output_filename: str, optional
774                プロット画像の保存ファイル名(拡張子を含む)。デフォルト値は'footprint.png'です
775            save_fig: bool, optional
776                図の保存を許可するフラグ。デフォルト値はTrueです
777            show_fig: bool, optional
778                図の表示を許可するフラグ。デフォルト値はTrueです
779            satellite_image: Image.Image | None, optional
780                使用する衛星画像。指定がない場合はデフォルトの画像が生成されます
781            xy_max: float, optional
782                表示範囲の最大値。デフォルト値は5000です
783
784        Returns
785        ----------
786            None
787                戻り値はありません
788
789        Example
790        ----------
791        >>> analyzer = FluxFootprintAnalyzer()
792        >>> analyzer.plot_flux_footprint(
793        ...     x_list=[0, 100, 200],
794        ...     y_list=[0, 150, 250],
795        ...     c_list=[1.0, 0.8, 0.6],
796        ...     center_lat=35.0,
797        ...     center_lon=135.0,
798        ...     vmin=0.0,
799        ...     vmax=1.0,
800        ...     cmap="jet",
801        ...     xy_max=1000
802        ... )
803        """
804        self.plot_flux_footprint_with_hotspots(
805            x_list=x_list,
806            y_list=y_list,
807            c_list=c_list,
808            center_lat=center_lat,
809            center_lon=center_lon,
810            vmin=vmin,
811            vmax=vmax,
812            add_cbar=add_cbar,
813            add_legend=add_legend,
814            cbar_label=cbar_label,
815            cbar_labelpad=cbar_labelpad,
816            cmap=cmap,
817            reduce_c_function=reduce_c_function,
818            hotspots=None,  # hotspotsをNoneに設定
819            hotspot_colors=None,
820            lat_correction=lat_correction,
821            lon_correction=lon_correction,
822            output_dirpath=output_dirpath,
823            output_filename=output_filename,
824            save_fig=save_fig,
825            show_fig=show_fig,
826            satellite_image=satellite_image,
827            xy_max=xy_max,
828        )

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

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

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
    カラーバーのラベル。デフォルト値はNoneです
cbar_labelpad: int, optional
    カラーバーラベルのパディング。デフォルト値は20です
cmap: str, optional
    使用するカラーマップの名前。デフォルト値は"jet"です
reduce_c_function: Callable, optional
    フットプリントの集約関数。デフォルト値はnp.meanです
lon_correction: float, optional
    経度方向の補正係数。デフォルト値は1です
lat_correction: float, optional
    緯度方向の補正係数。デフォルト値は1です
output_dirpath: str | Path | None, optional
    プロット画像の保存先パス。デフォルト値はNoneです
output_filename: str, optional
    プロット画像の保存ファイル名(拡張子を含む)。デフォルト値は'footprint.png'です
save_fig: bool, optional
    図の保存を許可するフラグ。デフォルト値はTrueです
show_fig: bool, optional
    図の表示を許可するフラグ。デフォルト値はTrueです
satellite_image: Image.Image | None, optional
    使用する衛星画像。指定がない場合はデフォルトの画像が生成されます
xy_max: float, optional
    表示範囲の最大値。デフォルト値は5000です

Returns

None
    戻り値はありません

Example

>>> analyzer = FluxFootprintAnalyzer()
>>> analyzer.plot_flux_footprint(
...     x_list=[0, 100, 200],
...     y_list=[0, 150, 250],
...     c_list=[1.0, 0.8, 0.6],
...     center_lat=35.0,
...     center_lon=135.0,
...     vmin=0.0,
...     vmax=1.0,
...     cmap="jet",
...     xy_max=1000
... )
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: Callable = <function mean>, dpi: float = 300, figsize: tuple[float, float] = (8, 8), constrained_layout: bool = False, hotspots: list[HotspotData] | None = None, hotspots_alpha: float = 0.7, hotspot_colors: dict[typing.Literal['bio', 'gas', 'comb', 'scale_check'], str] | None = None, hotspot_labels: dict[typing.Literal['bio', 'gas', 'comb', 'scale_check'], str] | None = None, hotspot_markers: dict[typing.Literal['bio', 'gas', 'comb', 'scale_check'], str] | None = None, hotspot_sizes: dict[str, tuple[tuple[float, float], float]] | None = None, hotspot_sorting_by_delta_ch4: bool = True, legend_alpha: float = 1.0, legend_bbox_to_anchor: tuple[float, float] = (0.55, -0.01), legend_loc: str = 'upper center', legend_ncol: int | None = None, lat_correction: float = 1, lon_correction: float = 1, output_dirpath: str | pathlib.Path | None = None, output_filename: str = 'footprint.png', save_fig: bool = True, show_fig: bool = True, satellite_image: PIL.Image.Image | None = None, satellite_image_aspect: Literal['auto', 'equal'] = 'auto', xy_max: float = 5000) -> None:
 830    def plot_flux_footprint_with_hotspots(
 831        self,
 832        x_list: list[float],
 833        y_list: list[float],
 834        c_list: list[float] | None,
 835        center_lat: float,
 836        center_lon: float,
 837        vmin: float,
 838        vmax: float,
 839        add_cbar: bool = True,
 840        add_legend: bool = True,
 841        cbar_label: str | None = None,
 842        cbar_labelpad: int = 20,
 843        cmap: str = "jet",
 844        reduce_c_function: Callable = np.mean,
 845        dpi: float = 300,
 846        figsize: tuple[float, float] = (8, 8),
 847        constrained_layout: bool = False,
 848        hotspots: list[HotspotData] | None = None,
 849        hotspots_alpha: float = 0.7,
 850        hotspot_colors: dict[HotspotType, str] | None = None,
 851        hotspot_labels: dict[HotspotType, str] | None = None,
 852        hotspot_markers: dict[HotspotType, str] | None = None,
 853        hotspot_sizes: dict[str, tuple[tuple[float, float], float]] | None = None,
 854        hotspot_sorting_by_delta_ch4: bool = True,
 855        legend_alpha: float = 1.0,
 856        legend_bbox_to_anchor: tuple[float, float] = (0.55, -0.01),
 857        legend_loc: str = "upper center",
 858        legend_ncol: int | None = None,
 859        lat_correction: float = 1,
 860        lon_correction: float = 1,
 861        output_dirpath: str | Path | None = None,
 862        output_filename: str = "footprint.png",
 863        save_fig: bool = True,
 864        show_fig: bool = True,
 865        satellite_image: Image.Image | None = None,
 866        satellite_image_aspect: Literal["auto", "equal"] = "auto",
 867        xy_max: float = 5000,
 868    ) -> None:
 869        """
 870        静的な衛星画像上にフットプリントデータとホットスポットをプロットします。
 871
 872        このメソッドは、指定されたフットプリントデータとホットスポットを可視化します。
 873        ホットスポットが指定されない場合は、フットプリントのみ作図します。
 874
 875        Parameters
 876        ----------
 877            x_list: list[float]
 878                フットプリントのx座標リスト(メートル単位)
 879            y_list: list[float]
 880                フットプリントのy座標リスト(メートル単位)
 881            c_list: list[float] | None
 882                フットプリントの強度を示す値のリスト
 883            center_lat: float
 884                プロットの中心となる緯度
 885            center_lon: float
 886                プロットの中心となる経度
 887            vmin: float
 888                カラーバーの最小値
 889            vmax: float
 890                カラーバーの最大値
 891            add_cbar: bool, optional
 892                カラーバーを追加するかどうか。デフォルトはTrue
 893            add_legend: bool, optional
 894                凡例を追加するかどうか。デフォルトはTrue
 895            cbar_label: str | None, optional
 896                カラーバーのラベル。デフォルトはNone
 897            cbar_labelpad: int, optional
 898                カラーバーラベルのパディング。デフォルトは20
 899            cmap: str, optional
 900                使用するカラーマップの名前。デフォルトは"jet"
 901            reduce_c_function: Callable, optional
 902                フットプリントの集約関数。デフォルトはnp.mean
 903            dpi: float, optional
 904                出力画像の解像度。デフォルトは300
 905            figsize: tuple[float, float], optional
 906                出力画像のサイズ。デフォルトは(8, 8)
 907            constrained_layout: bool, optional
 908                図のレイアウトを自動調整するかどうか。デフォルトはFalse
 909            hotspots: list[HotspotData] | None, optional
 910                ホットスポットデータのリスト。デフォルトはNone
 911            hotspots_alpha: float, optional
 912                ホットスポットの透明度。デフォルトは0.7
 913            hotspot_colors: dict[HotspotType, str] | None, optional
 914                ホットスポットの色を指定する辞書。デフォルトはNone
 915            hotspot_labels: dict[HotspotType, str] | None, optional
 916                ホットスポットの表示ラベルを指定する辞書。デフォルトはNone
 917            hotspot_markers: dict[HotspotType, str] | None, optional
 918                ホットスポットの形状を指定する辞書。デフォルトはNone
 919            hotspot_sizes: dict[str, tuple[tuple[float, float], float]] | None, optional
 920                ホットスポットのサイズ範囲とマーカーサイズを指定する辞書。デフォルトはNone
 921            hotspot_sorting_by_delta_ch4: bool, optional
 922                ホットスポットをΔCH4で昇順ソートするか。デフォルトはTrue
 923            legend_alpha: float, optional
 924                凡例の透過率。デフォルトは1.0
 925            legend_bbox_to_anchor: tuple[float, float], optional
 926                凡例の位置を微調整するためのアンカーポイント。デフォルトは(0.55, -0.01)
 927            legend_loc: str, optional
 928                凡例の基準位置。デフォルトは"upper center"
 929            legend_ncol: int | None, optional
 930                凡例の列数。デフォルトはNone
 931            lat_correction: float, optional
 932                緯度方向の補正係数。デフォルトは1
 933            lon_correction: float, optional
 934                経度方向の補正係数。デフォルトは1
 935            output_dirpath: str | Path | None, optional
 936                プロット画像の保存先パス。デフォルトはNone
 937            output_filename: str, optional
 938                プロット画像の保存ファイル名。デフォルトは"footprint.png"
 939            save_fig: bool, optional
 940                図の保存を許可するフラグ。デフォルトはTrue
 941            show_fig: bool, optional
 942                図の表示を許可するフラグ。デフォルトはTrue
 943            satellite_image: Image.Image | None, optional
 944                使用する衛星画像。デフォルトはNone
 945            satellite_image_aspect: Literal["auto", "equal"], optional
 946                衛星画像のアスペクト比。デフォルトは"auto"
 947            xy_max: float, optional
 948                表示範囲の最大値。デフォルトは5000
 949
 950        Returns
 951        -------
 952            None
 953                戻り値はありません
 954
 955        Example
 956        -------
 957        >>> analyzer = FluxFootprintAnalyzer()
 958        >>> analyzer.plot_flux_footprint_with_hotspots(
 959        ...     x_list=[0, 100, 200],
 960        ...     y_list=[0, 150, 250],
 961        ...     c_list=[1.0, 0.8, 0.6],
 962        ...     center_lat=35.0,
 963        ...     center_lon=135.0,
 964        ...     vmin=0.0,
 965        ...     vmax=1.0,
 966        ...     hotspots=[HotspotData(lat=35.001, lon=135.001, type="gas", delta_ch4=0.5)],
 967        ...     xy_max=1000
 968        ... )
 969        """
 970        # 1. 引数のバリデーション
 971        valid_extensions: list[str] = [".png", ".jpg", ".jpeg", ".pdf", ".svg"]
 972        _, file_extension = os.path.splitext(output_filename)
 973        if file_extension.lower() not in valid_extensions:
 974            quoted_extensions: list[str] = [f'"{ext}"' for ext in valid_extensions]
 975            self.logger.error(
 976                f"`output_filename`は有効な拡張子ではありません。プロットを保存するには、次のいずれかを指定してください: {','.join(quoted_extensions)}"
 977            )
 978            return
 979
 980        # 2. フラグチェック
 981        if not self._got_satellite_image:
 982            raise ValueError(
 983                "`get_satellite_image_from_api`または`get_satellite_image_from_local`が実行されていません。"
 984            )
 985
 986        # 3. 衛星画像の取得
 987        if satellite_image is None:
 988            satellite_image = Image.new("RGB", (2160, 2160), "lightgray")
 989
 990        self.logger.info("プロットを作成中...")
 991
 992        # 4. 座標変換のための定数計算(1回だけ)
 993        meters_per_lat: float = self.EARTH_RADIUS_METER * (
 994            math.pi / 180
 995        )  # 緯度1度あたりのメートル
 996        meters_per_lon: float = meters_per_lat * math.cos(
 997            math.radians(center_lat)
 998        )  # 経度1度あたりのメートル
 999
1000        # 5. フットプリントデータの座標変換(まとめて1回で実行)
1001        x_deg = (
1002            np.array(x_list) / meters_per_lon * lon_correction
1003        )  # 補正係数も同時に適用
1004        y_deg = (
1005            np.array(y_list) / meters_per_lat * lat_correction
1006        )  # 補正係数も同時に適用
1007
1008        # 6. 中心点からの相対座標を実際の緯度経度に変換
1009        lons = center_lon + x_deg
1010        lats = center_lat + y_deg
1011
1012        # 7. 表示範囲の計算(変更なし)
1013        x_range: float = xy_max / meters_per_lon
1014        y_range: float = xy_max / meters_per_lat
1015        map_boundaries: tuple[float, float, float, float] = (
1016            center_lon - x_range,  # left_lon
1017            center_lon + x_range,  # right_lon
1018            center_lat - y_range,  # bottom_lat
1019            center_lat + y_range,  # top_lat
1020        )
1021        left_lon, right_lon, bottom_lat, top_lat = map_boundaries
1022
1023        # 8. プロットの作成
1024        plt.rcParams["axes.edgecolor"] = "None"
1025
1026        # 従来のロジック
1027        fig: Figure = plt.figure(
1028            figsize=figsize, dpi=dpi, constrained_layout=constrained_layout
1029        )
1030        ax_data: Axes = fig.add_axes((0.05, 0.1, 0.8, 0.8))
1031
1032        # 9. フットプリントの描画
1033        # フットプリントの描画とカラーバー用の2つのhexbinを作成
1034        if c_list is not None:
1035            ax_data.hexbin(
1036                lons,
1037                lats,
1038                C=c_list,
1039                cmap=cmap,
1040                vmin=vmin,
1041                vmax=vmax,
1042                alpha=0.3,  # 実際のプロット用
1043                gridsize=100,
1044                linewidths=0,
1045                mincnt=100,
1046                extent=(left_lon, right_lon, bottom_lat, top_lat),
1047                reduce_C_function=reduce_c_function,
1048            )
1049
1050        # カラーバー用の非表示hexbin(alpha=1.0)
1051        hidden_hexbin = ax_data.hexbin(
1052            lons,
1053            lats,
1054            C=c_list,
1055            cmap=cmap,
1056            vmin=vmin,
1057            vmax=vmax,
1058            alpha=1.0,  # カラーバー用
1059            gridsize=100,
1060            linewidths=0,
1061            mincnt=100,
1062            extent=(left_lon, right_lon, bottom_lat, top_lat),
1063            reduce_C_function=reduce_c_function,
1064            visible=False,  # プロットには表示しない
1065        )
1066
1067        # 10. ホットスポットの描画
1068        spot_handles = []
1069        if hotspots is not None:
1070            default_colors: dict[HotspotType, str] = {
1071                "bio": "blue",
1072                "gas": "red",
1073                "comb": "green",
1074            }
1075
1076            # デフォルトのマーカー形状を定義
1077            default_markers: dict[HotspotType, str] = {
1078                "bio": "^",  # 三角
1079                "gas": "o",  # 丸
1080                "comb": "s",  # 四角
1081            }
1082
1083            # デフォルトのラベルを定義
1084            default_labels: dict[HotspotType, str] = {
1085                "bio": "bio",
1086                "gas": "gas",
1087                "comb": "comb",
1088            }
1089
1090            # デフォルトのサイズ設定を定義
1091            default_sizes = {
1092                "small": ((0, 0.5), 50),
1093                "medium": ((0.5, 1.0), 100),
1094                "large": ((1.0, float("inf")), 200),
1095            }
1096
1097            # ユーザー指定のサイズ設定を使用(指定がない場合はデフォルト値を使用)
1098            sizes = hotspot_sizes or default_sizes
1099
1100            # 座標変換のための定数
1101            meters_per_lat: float = self.EARTH_RADIUS_METER * (math.pi / 180)
1102            meters_per_lon: float = meters_per_lat * math.cos(math.radians(center_lat))
1103
1104            # ΔCH4で昇順ソート
1105            if hotspot_sorting_by_delta_ch4:
1106                hotspots = sorted(hotspots, key=lambda x: x.delta_ch4)
1107
1108            for spot_type, color in (hotspot_colors or default_colors).items():
1109                spots_lon = []
1110                spots_lat = []
1111
1112                # 使用するマーカーを決定
1113                marker = (hotspot_markers or default_markers).get(spot_type, "o")
1114
1115                for spot in hotspots:
1116                    if spot.type == spot_type:
1117                        # 変換前の緯度経度をログ出力
1118                        self.logger.debug(
1119                            f"Before - Type: {spot_type}, Lat: {spot.avg_lat:.6f}, Lon: {spot.avg_lon:.6f}"
1120                        )
1121
1122                        # 中心からの相対距離を計算
1123                        dx: float = (spot.avg_lon - center_lon) * meters_per_lon
1124                        dy: float = (spot.avg_lat - center_lat) * meters_per_lat
1125
1126                        # 補正前の相対座標をログ出力
1127                        self.logger.debug(
1128                            f"Relative - Type: {spot_type}, X: {dx:.2f}m, Y: {dy:.2f}m"
1129                        )
1130
1131                        # 補正を適用
1132                        corrected_dx: float = dx * lon_correction
1133                        corrected_dy: float = dy * lat_correction
1134
1135                        # 補正後の緯度経度を計算
1136                        adjusted_lon: float = center_lon + corrected_dx / meters_per_lon
1137                        adjusted_lat: float = center_lat + corrected_dy / meters_per_lat
1138
1139                        # 変換後の緯度経度をログ出力
1140                        self.logger.debug(
1141                            f"After - Type: {spot_type}, Lat: {adjusted_lat:.6f}, Lon: {adjusted_lon:.6f}\n"
1142                        )
1143
1144                        if (
1145                            left_lon <= adjusted_lon <= right_lon
1146                            and bottom_lat <= adjusted_lat <= top_lat
1147                        ):
1148                            spots_lon.append(adjusted_lon)
1149                            spots_lat.append(adjusted_lat)
1150
1151                if spots_lon:
1152                    # 使用するラベルを決定
1153                    label = (hotspot_labels or default_labels).get(spot_type, spot_type)
1154
1155                    spot_sizes = [
1156                        sizes[self._get_size_category(spot.delta_ch4, sizes)][1]
1157                        for spot in hotspots
1158                        if spot.type == spot_type
1159                        and left_lon
1160                        <= (center_lon + (spot.avg_lon - center_lon) * lon_correction)
1161                        <= right_lon
1162                        and bottom_lat
1163                        <= (center_lat + (spot.avg_lat - center_lat) * lat_correction)
1164                        <= top_lat
1165                    ]
1166
1167                    handle = ax_data.scatter(
1168                        spots_lon,
1169                        spots_lat,
1170                        c=color,
1171                        marker=marker,  # マーカー形状を指定
1172                        # s=100,
1173                        s=spot_sizes,  # 修正したサイズリストを使用
1174                        alpha=hotspots_alpha,
1175                        label=label,
1176                        edgecolor="black",
1177                        linewidth=1,
1178                    )
1179                    spot_handles.append(handle)
1180
1181        # 11. 背景画像の設定
1182        ax_img = ax_data.twiny().twinx()
1183        ax_img.imshow(
1184            satellite_image,
1185            extent=(left_lon, right_lon, bottom_lat, top_lat),
1186            aspect=satellite_image_aspect,
1187        )
1188
1189        # 12. 軸の設定
1190        for ax in [ax_data, ax_img]:
1191            ax.set_xlim(left_lon, right_lon)
1192            ax.set_ylim(bottom_lat, top_lat)
1193            ax.set_xticks([])
1194            ax.set_yticks([])
1195
1196        ax_data.set_zorder(2)
1197        ax_data.patch.set_alpha(0)
1198        ax_img.set_zorder(1)
1199
1200        # 13. カラーバーの追加
1201        if add_cbar:
1202            cbar_ax: Axes = fig.add_axes((0.88, 0.1, 0.03, 0.8))
1203            cbar = fig.colorbar(hidden_hexbin, cax=cbar_ax)  # hidden_hexbinを使用
1204            # cbar_labelが指定されている場合のみラベルを設定
1205            if cbar_label:
1206                cbar.set_label(cbar_label, rotation=270, labelpad=cbar_labelpad)
1207
1208        # 14. ホットスポットの凡例追加
1209        if add_legend and hotspots and spot_handles:
1210            # 列数を決定(指定がない場合はホットスポットの数を使用)
1211            ncol = legend_ncol if legend_ncol is not None else len(spot_handles)
1212            ax_data.legend(
1213                handles=spot_handles,
1214                loc=legend_loc,  # 凡例の基準位置を指定
1215                bbox_to_anchor=legend_bbox_to_anchor,  # アンカーポイントを指定
1216                ncol=ncol,  # 列数を指定
1217                framealpha=legend_alpha,
1218            )
1219
1220        # 15. 画像の保存
1221        if save_fig:
1222            if output_dirpath is None:
1223                raise ValueError(
1224                    "save_fig = True の場合、 output_dirpath を指定する必要があります。有効なディレクトリパスを指定してください。"
1225                )
1226            output_filepath: str = os.path.join(output_dirpath, output_filename)
1227            self.logger.info("プロットを保存中...")
1228            try:
1229                fig.savefig(output_filepath, bbox_inches="tight")
1230                self.logger.info(f"プロットが正常に保存されました: {output_filepath}")
1231            except Exception as e:
1232                self.logger.error(f"プロットの保存中にエラーが発生しました: {e!s}")
1233        # 16. 画像の表示
1234        if show_fig:
1235            plt.show()
1236        plt.close(fig=fig)

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

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

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
    カラーバーのラベル。デフォルトはNone
cbar_labelpad: int, optional
    カラーバーラベルのパディング。デフォルトは20
cmap: str, optional
    使用するカラーマップの名前。デフォルトは"jet"
reduce_c_function: Callable, optional
    フットプリントの集約関数。デフォルトはnp.mean
dpi: float, optional
    出力画像の解像度。デフォルトは300
figsize: tuple[float, float], optional
    出力画像のサイズ。デフォルトは(8, 8)
constrained_layout: bool, optional
    図のレイアウトを自動調整するかどうか。デフォルトはFalse
hotspots: list[HotspotData] | None, optional
    ホットスポットデータのリスト。デフォルトはNone
hotspots_alpha: float, optional
    ホットスポットの透明度。デフォルトは0.7
hotspot_colors: dict[HotspotType, str] | None, optional
    ホットスポットの色を指定する辞書。デフォルトはNone
hotspot_labels: dict[HotspotType, str] | None, optional
    ホットスポットの表示ラベルを指定する辞書。デフォルトはNone
hotspot_markers: dict[HotspotType, str] | None, optional
    ホットスポットの形状を指定する辞書。デフォルトはNone
hotspot_sizes: dict[str, tuple[tuple[float, float], float]] | None, optional
    ホットスポットのサイズ範囲とマーカーサイズを指定する辞書。デフォルトはNone
hotspot_sorting_by_delta_ch4: bool, optional
    ホットスポットをΔCH4で昇順ソートするか。デフォルトはTrue
legend_alpha: float, optional
    凡例の透過率。デフォルトは1.0
legend_bbox_to_anchor: tuple[float, float], optional
    凡例の位置を微調整するためのアンカーポイント。デフォルトは(0.55, -0.01)
legend_loc: str, optional
    凡例の基準位置。デフォルトは"upper center"
legend_ncol: int | None, optional
    凡例の列数。デフォルトはNone
lat_correction: float, optional
    緯度方向の補正係数。デフォルトは1
lon_correction: float, optional
    経度方向の補正係数。デフォルトは1
output_dirpath: str | Path | None, optional
    プロット画像の保存先パス。デフォルトはNone
output_filename: str, optional
    プロット画像の保存ファイル名。デフォルトは"footprint.png"
save_fig: bool, optional
    図の保存を許可するフラグ。デフォルトはTrue
show_fig: bool, optional
    図の表示を許可するフラグ。デフォルトはTrue
satellite_image: Image.Image | None, optional
    使用する衛星画像。デフォルトはNone
satellite_image_aspect: Literal["auto", "equal"], optional
    衛星画像のアスペクト比。デフォルトは"auto"
xy_max: float, optional
    表示範囲の最大値。デフォルトは5000

Returns

None
    戻り値はありません

Example

>>> analyzer = FluxFootprintAnalyzer()
>>> analyzer.plot_flux_footprint_with_hotspots(
...     x_list=[0, 100, 200],
...     y_list=[0, 150, 250],
...     c_list=[1.0, 0.8, 0.6],
...     center_lat=35.0,
...     center_lon=135.0,
...     vmin=0.0,
...     vmax=1.0,
...     hotspots=[HotspotData(lat=35.001, lon=135.001, type="gas", delta_ch4=0.5)],
...     xy_max=1000
... )
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: Callable = <function mean>, lat_correction: float = 1, lon_correction: float = 1, output_dirpath: str | pathlib.Path | None = None, output_filename: str = 'footprint-scale_checker.png', save_fig: bool = True, show_fig: bool = True, satellite_image: PIL.Image.Image | None = None, xy_max: float = 5000) -> None:
1238    def plot_flux_footprint_with_scale_checker(
1239        self,
1240        x_list: list[float],
1241        y_list: list[float],
1242        c_list: list[float] | None,
1243        center_lat: float,
1244        center_lon: float,
1245        check_points: list[tuple[float, float, str]] | None = None,
1246        vmin: float = 0,
1247        vmax: float = 100,
1248        add_cbar: bool = True,
1249        cbar_label: str | None = None,
1250        cbar_labelpad: int = 20,
1251        cmap: str = "jet",
1252        reduce_c_function: Callable = np.mean,
1253        lat_correction: float = 1,
1254        lon_correction: float = 1,
1255        output_dirpath: str | Path | None = None,
1256        output_filename: str = "footprint-scale_checker.png",
1257        save_fig: bool = True,
1258        show_fig: bool = True,
1259        satellite_image: Image.Image | None = None,
1260        xy_max: float = 5000,
1261    ) -> None:
1262        """
1263        衛星画像上にフットプリントデータとスケールチェック用のポイントをプロットします。
1264
1265        Parameters
1266        ----------
1267            x_list: list[float]
1268                フットプリントのx座標リスト(メートル単位)
1269            y_list: list[float]
1270                フットプリントのy座標リスト(メートル単位)
1271            c_list: list[float] | None
1272                フットプリントの強度を示す値のリスト
1273            center_lat: float
1274                プロットの中心となる緯度
1275            center_lon: float
1276                プロットの中心となる経度
1277            check_points: list[tuple[float, float, str]] | None, optional
1278                確認用の地点リスト。各要素は(緯度、経度、ラベル)のタプル。デフォルト値はNoneで、その場合は中心から500m、1000m、2000m、3000mの位置に仮想的な点を配置
1279            vmin: float, optional
1280                カラーバーの最小値。デフォルト値は0
1281            vmax: float, optional
1282                カラーバーの最大値。デフォルト値は100
1283            add_cbar: bool, optional
1284                カラーバーを追加するかどうか。デフォルト値はTrue
1285            cbar_label: str | None, optional
1286                カラーバーのラベル。デフォルト値はNone
1287            cbar_labelpad: int, optional
1288                カラーバーラベルのパディング。デフォルト値は20
1289            cmap: str, optional
1290                使用するカラーマップの名前。デフォルト値は"jet"
1291            reduce_c_function: Callable, optional
1292                フットプリントの集約関数。デフォルト値はnp.mean
1293            lat_correction: float, optional
1294                緯度方向の補正係数。デフォルト値は1
1295            lon_correction: float, optional
1296                経度方向の補正係数。デフォルト値は1
1297            output_dirpath: str | Path | None, optional
1298                プロット画像の保存先パス。デフォルト値はNone
1299            output_filename: str, optional
1300                プロット画像の保存ファイル名。デフォルト値は"footprint-scale_checker.png"
1301            save_fig: bool, optional
1302                図の保存を許可するフラグ。デフォルト値はTrue
1303            show_fig: bool, optional
1304                図の表示を許可するフラグ。デフォルト値はTrue
1305            satellite_image: Image.Image | None, optional
1306                使用する衛星画像。デフォルト値はNoneで、その場合はデフォルトの画像が生成されます
1307            xy_max: float, optional
1308                表示範囲の最大値。デフォルト値は5000
1309
1310        Returns
1311        ----------
1312            None
1313                戻り値はありません
1314
1315        Example
1316        ----------
1317        >>> analyzer = FluxFootprintAnalyzer(z_m=2.5)
1318        >>> analyzer.plot_flux_footprint_with_scale_checker(
1319        ...     x_list=[0, 100, 200],
1320        ...     y_list=[0, 150, 250],
1321        ...     c_list=[1.0, 0.8, 0.6],
1322        ...     center_lat=35.0,
1323        ...     center_lon=135.0,
1324        ...     check_points=[(35.001, 135.001, "Point A")],
1325        ...     vmin=0.0,
1326        ...     vmax=1.0,
1327        ...     cmap="jet",
1328        ...     xy_max=1000
1329        ... )
1330        """
1331        if check_points is None:
1332            # デフォルトの確認ポイントを生成(従来の方式)
1333            default_points = [
1334                (500, "North", 90),  # 北 500m
1335                (1000, "East", 0),  # 東 1000m
1336                (2000, "South", 270),  # 南 2000m
1337                (3000, "West", 180),  # 西 3000m
1338            ]
1339
1340            dummy_hotspots = []
1341            for distance, _, angle in default_points:
1342                rad = math.radians(angle)
1343                meters_per_lat = self.EARTH_RADIUS_METER * (math.pi / 180)
1344                meters_per_lon = meters_per_lat * math.cos(math.radians(center_lat))
1345
1346                dx = distance * math.cos(rad)
1347                dy = distance * math.sin(rad)
1348
1349                delta_lon = dx / meters_per_lon
1350                delta_lat = dy / meters_per_lat
1351
1352                hotspot = HotspotData(
1353                    avg_lat=center_lat + delta_lat,
1354                    avg_lon=center_lon + delta_lon,
1355                    delta_ch4=0.0,
1356                    delta_c2h6=0.0,
1357                    delta_ratio=0.0,
1358                    type="scale_check",
1359                    section=0,
1360                    timestamp="scale_check",
1361                    angle=0,
1362                    correlation=0,
1363                )
1364                dummy_hotspots.append(hotspot)
1365        else:
1366            # 指定された緯度経度を使用
1367            dummy_hotspots = []
1368            for lat, lon, _ in check_points:
1369                hotspot = HotspotData(
1370                    avg_lat=lat,
1371                    avg_lon=lon,
1372                    delta_ch4=0.0,
1373                    delta_c2h6=0.0,
1374                    delta_ratio=0.0,
1375                    type="scale_check",
1376                    section=0,
1377                    timestamp="scale_check",
1378                    angle=0,
1379                    correlation=0,
1380                )
1381                dummy_hotspots.append(hotspot)
1382
1383        # カスタムカラーマップの作成
1384        hotspot_colors = {
1385            spot.type: f"C{i % 10}" for i, spot in enumerate(dummy_hotspots)
1386        }
1387
1388        # 既存のメソッドを呼び出してプロット
1389        self.plot_flux_footprint_with_hotspots(
1390            x_list=x_list,
1391            y_list=y_list,
1392            c_list=c_list,
1393            center_lat=center_lat,
1394            center_lon=center_lon,
1395            vmin=vmin,
1396            vmax=vmax,
1397            add_cbar=add_cbar,
1398            add_legend=True,
1399            cbar_label=cbar_label,
1400            cbar_labelpad=cbar_labelpad,
1401            cmap=cmap,
1402            reduce_c_function=reduce_c_function,
1403            hotspots=dummy_hotspots,
1404            hotspot_colors=hotspot_colors,
1405            lat_correction=lat_correction,
1406            lon_correction=lon_correction,
1407            output_dirpath=output_dirpath,
1408            output_filename=output_filename,
1409            save_fig=save_fig,
1410            show_fig=show_fig,
1411            satellite_image=satellite_image,
1412            xy_max=xy_max,
1413        )

衛星画像上にフットプリントデータとスケールチェック用のポイントをプロットします。

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, optional
    確認用の地点リスト。各要素は(緯度、経度、ラベル)のタプル。デフォルト値はNoneで、その場合は中心から500m、1000m、2000m、3000mの位置に仮想的な点を配置
vmin: float, optional
    カラーバーの最小値。デフォルト値は0
vmax: float, optional
    カラーバーの最大値。デフォルト値は100
add_cbar: bool, optional
    カラーバーを追加するかどうか。デフォルト値はTrue
cbar_label: str | None, optional
    カラーバーのラベル。デフォルト値はNone
cbar_labelpad: int, optional
    カラーバーラベルのパディング。デフォルト値は20
cmap: str, optional
    使用するカラーマップの名前。デフォルト値は"jet"
reduce_c_function: Callable, optional
    フットプリントの集約関数。デフォルト値はnp.mean
lat_correction: float, optional
    緯度方向の補正係数。デフォルト値は1
lon_correction: float, optional
    経度方向の補正係数。デフォルト値は1
output_dirpath: str | Path | None, optional
    プロット画像の保存先パス。デフォルト値はNone
output_filename: str, optional
    プロット画像の保存ファイル名。デフォルト値は"footprint-scale_checker.png"
save_fig: bool, optional
    図の保存を許可するフラグ。デフォルト値はTrue
show_fig: bool, optional
    図の表示を許可するフラグ。デフォルト値はTrue
satellite_image: Image.Image | None, optional
    使用する衛星画像。デフォルト値はNoneで、その場合はデフォルトの画像が生成されます
xy_max: float, optional
    表示範囲の最大値。デフォルト値は5000

Returns

None
    戻り値はありません

Example

>>> analyzer = FluxFootprintAnalyzer(z_m=2.5)
>>> analyzer.plot_flux_footprint_with_scale_checker(
...     x_list=[0, 100, 200],
...     y_list=[0, 150, 250],
...     c_list=[1.0, 0.8, 0.6],
...     center_lat=35.0,
...     center_lon=135.0,
...     check_points=[(35.001, 135.001, "Point A")],
...     vmin=0.0,
...     vmax=1.0,
...     cmap="jet",
...     xy_max=1000
... )
@staticmethod
def filter_data( df: pandas.core.frame.DataFrame, start_date: str | datetime.datetime | None = None, end_date: str | datetime.datetime | None = None, months: list[int] | None = None) -> pandas.core.frame.DataFrame:
1642    @staticmethod
1643    def filter_data(
1644        df: pd.DataFrame,
1645        start_date: str | datetime | None = None,
1646        end_date: str | datetime | None = None,
1647        months: list[int] | None = None,
1648    ) -> pd.DataFrame:
1649        """
1650        指定された期間や月でデータをフィルタリングするメソッド。
1651
1652        Parameters
1653        ----------
1654            df: pd.DataFrame
1655                フィルタリングするデータフレーム
1656            start_date: str | datetime | None, optional
1657                フィルタリングの開始日。'YYYY-MM-DD'形式の文字列またはdatetimeオブジェクト。指定しない場合は最初のデータから開始。
1658            end_date: str | datetime | None, optional
1659                フィルタリングの終了日。'YYYY-MM-DD'形式の文字列またはdatetimeオブジェクト。指定しない場合は最後のデータまで。
1660            months: list[int] | None, optional
1661                フィルタリングする月のリスト。1から12までの整数を含むリスト。指定しない場合は全ての月を対象。
1662
1663        Returns
1664        ----------
1665            pd.DataFrame
1666                フィルタリングされたデータフレーム
1667
1668        Raises
1669        ----------
1670            ValueError
1671                インデックスがDatetimeIndexでない場合、または日付の形式が不正な場合
1672
1673        Examples
1674        ----------
1675        >>> import pandas as pd
1676        >>> df = pd.DataFrame(index=pd.date_range('2020-01-01', '2020-12-31'))
1677        >>> # 2020年1月から3月までのデータを抽出
1678        >>> filtered_df = FluxFootprintAnalyzer.filter_data(
1679        ...     df,
1680        ...     start_date='2020-01-01',
1681        ...     end_date='2020-03-31'
1682        ... )
1683        >>> # 冬季(12月、1月、2月)のデータのみを抽出
1684        >>> winter_df = FluxFootprintAnalyzer.filter_data(
1685        ...     df,
1686        ...     months=[12, 1, 2]
1687        ... )
1688        """
1689        # インデックスの検証
1690        if not isinstance(df.index, pd.DatetimeIndex):
1691            raise ValueError(
1692                "DataFrameのインデックスはDatetimeIndexである必要があります"
1693            )
1694
1695        df_internal: pd.DataFrame = df.copy()
1696
1697        # 日付形式の検証と変換
1698        try:
1699            if start_date is not None:
1700                start_date = pd.to_datetime(start_date)
1701            if end_date is not None:
1702                end_date = pd.to_datetime(end_date)
1703        except ValueError as e:
1704            raise ValueError(
1705                "日付の形式が不正です。'YYYY-MM-DD'形式で指定してください"
1706            ) from e
1707
1708        # 期間でフィルタリング
1709        if start_date is not None or end_date is not None:
1710            df_internal = df_internal.loc[start_date:end_date]
1711
1712        # 月のバリデーション
1713        if months is not None:
1714            if not all(isinstance(m, int) and 1 <= m <= 12 for m in months):
1715                raise ValueError(
1716                    "monthsは1から12までの整数のリストである必要があります"
1717                )
1718            df_internal = df_internal[
1719                pd.to_datetime(df_internal.index).month.isin(months)
1720            ]
1721
1722        # フィルタリング後のデータが空でないことを確認
1723        if df_internal.empty:
1724            raise ValueError("フィルタリング後のデータが空になりました")
1725
1726        return df_internal

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

Parameters

df: pd.DataFrame
    フィルタリングするデータフレーム
start_date: str | datetime | None, optional
    フィルタリングの開始日。'YYYY-MM-DD'形式の文字列またはdatetimeオブジェクト。指定しない場合は最初のデータから開始。
end_date: str | datetime | None, optional
    フィルタリングの終了日。'YYYY-MM-DD'形式の文字列またはdatetimeオブジェクト。指定しない場合は最後のデータまで。
months: list[int] | None, optional
    フィルタリングする月のリスト。1から12までの整数を含むリスト。指定しない場合は全ての月を対象。

Returns

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

Raises

ValueError
    インデックスがDatetimeIndexでない場合、または日付の形式が不正な場合

Examples

>>> import pandas as pd
>>> df = pd.DataFrame(index=pd.date_range('2020-01-01', '2020-12-31'))
>>> # 2020年1月から3月までのデータを抽出
>>> filtered_df = FluxFootprintAnalyzer.filter_data(
...     df,
...     start_date='2020-01-01',
...     end_date='2020-03-31'
... )
>>> # 冬季(12月、1月、2月)のデータのみを抽出
>>> winter_df = FluxFootprintAnalyzer.filter_data(
...     df,
...     months=[12, 1, 2]
... )
@staticmethod
def is_weekday(date: datetime.datetime) -> int:
1728    @staticmethod
1729    def is_weekday(date: datetime) -> int:
1730        """
1731        指定された日付が平日であるかどうかを判定します。
1732
1733        Parameters
1734        ----------
1735            date: datetime
1736                判定対象の日付。土日祝日以外の日付を平日として判定します。
1737
1738        Returns
1739        ----------
1740            int
1741                平日の場合は1、土日祝日の場合は0を返します。
1742
1743        Examples
1744        --------
1745        >>> from datetime import datetime
1746        >>> date = datetime(2024, 1, 1)  # 2024年1月1日(祝日)
1747        >>> FluxFootprintAnalyzer.is_weekday(date)
1748        0
1749        >>> date = datetime(2024, 1, 4)  # 2024年1月4日(木曜)
1750        >>> FluxFootprintAnalyzer.is_weekday(date)
1751        1
1752        """
1753        return 1 if not jpholiday.is_holiday(date) and date.weekday() < 5 else 0

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

Parameters

date: datetime
    判定対象の日付。土日祝日以外の日付を平日として判定します。

Returns

int
    平日の場合は1、土日祝日の場合は0を返します。

Examples

>>> from datetime import datetime
>>> date = datetime(2024, 1, 1)  # 2024年1月1日(祝日)
>>> FluxFootprintAnalyzer.is_weekday(date)
0
>>> date = datetime(2024, 1, 4)  # 2024年1月4日(木曜)
>>> FluxFootprintAnalyzer.is_weekday(date)
1
@dataclass
class H2OCorrectionConfig:
 8@dataclass
 9class H2OCorrectionConfig:
10    """水蒸気補正の設定を保持するデータクラス
11
12    Parameters
13    ----------
14        coef_b: float | None, optional
15            補正曲線の1次係数。デフォルト値はNone。
16        coef_c: float | None, optional
17            補正曲線の2次係数。デフォルト値はNone。
18        h2o_ppm_threshold: float | None, optional
19            水蒸気濃度の下限値(この値未満のデータは除外)。デフォルト値は2000。
20        target_h2o_ppm: float, optional
21            換算先の水蒸気濃度。デフォルト値は10000 ppm。
22
23    Examples
24    --------
25    >>> config = H2OCorrectionConfig(
26    ...     coef_b=0.001,
27    ...     coef_c=0.0001,
28    ...     h2o_ppm_threshold=2000,
29    ...     target_h2o_ppm=10000
30    ... )
31    """
32
33    coef_b: float | None = None
34    coef_c: float | None = None
35    h2o_ppm_threshold: float | None = 2000
36    target_h2o_ppm: float = 10000

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

Parameters

coef_b: float | None, optional
    補正曲線の1次係数。デフォルト値はNone。
coef_c: float | None, optional
    補正曲線の2次係数。デフォルト値はNone。
h2o_ppm_threshold: float | None, optional
    水蒸気濃度の下限値(この値未満のデータは除外)。デフォルト値は2000。
target_h2o_ppm: float, optional
    換算先の水蒸気濃度。デフォルト値は10000 ppm。

Examples

>>> config = H2OCorrectionConfig(
...     coef_b=0.001,
...     coef_c=0.0001,
...     h2o_ppm_threshold=2000,
...     target_h2o_ppm=10000
... )
H2OCorrectionConfig( coef_b: float | None = None, coef_c: float | None = None, h2o_ppm_threshold: float | None = 2000, target_h2o_ppm: float = 10000)
coef_b: float | None = None
coef_c: float | None = None
h2o_ppm_threshold: float | None = 2000
target_h2o_ppm: float = 10000
@dataclass
class HotspotData:
 28@dataclass
 29class HotspotData:
 30    """
 31    ホットスポットの情報を保持するデータクラス
 32
 33    Parameters
 34    ----------
 35        timestamp: str
 36            タイムスタンプ
 37        angle: float
 38            中心からの角度
 39        avg_lat: float
 40            平均緯度
 41        avg_lon: float
 42            平均経度
 43        correlation: float
 44            ΔC2H6/ΔCH4相関係数
 45        delta_ch4: float
 46            CH4の増加量
 47        delta_c2h6: float
 48            C2H6の増加量
 49        delta_ratio: float
 50            ΔC2H6/ΔCH4の比率
 51        section: int
 52            所属する区画番号
 53        type: HotspotType
 54            ホットスポットの種類
 55    """
 56
 57    timestamp: str
 58    angle: float
 59    avg_lat: float
 60    avg_lon: float
 61    correlation: float
 62    delta_ch4: float
 63    delta_c2h6: float
 64    delta_ratio: float
 65    section: int
 66    type: HotspotType
 67
 68    def __post_init__(self):
 69        """
 70        __post_init__で各プロパティをバリデーション
 71        """
 72        # タイムスタンプが空でないことを確認
 73        if not self.timestamp.strip():
 74            raise ValueError(f"'timestamp' must not be empty: {self.timestamp}")
 75
 76        # 角度は-180~180度の範囲内であることを確認
 77        if not -180 <= self.angle <= 180:
 78            raise ValueError(
 79                f"'angle' must be between -180 and 180 degrees: {self.angle}"
 80            )
 81
 82        # 緯度は-90から90度の範囲内であることを確認
 83        if not -90 <= self.avg_lat <= 90:
 84            raise ValueError(
 85                f"'avg_lat' must be between -90 and 90 degrees: {self.avg_lat}"
 86            )
 87
 88        # 経度は-180から180度の範囲内であることを確認
 89        if not -180 <= self.avg_lon <= 180:
 90            raise ValueError(
 91                f"'avg_lon' must be between -180 and 180 degrees: {self.avg_lon}"
 92            )
 93
 94        # ΔCH4はfloat型であり、0以上を許可
 95        if not isinstance(self.delta_c2h6, float) or self.delta_ch4 < 0:
 96            raise ValueError(
 97                f"'delta_ch4' must be a non-negative value and at least 0: {self.delta_ch4}"
 98            )
 99
100        # ΔC2H6はfloat型のみを許可
101        if not isinstance(self.delta_c2h6, float):
102            raise ValueError(f"'delta_c2h6' must be a float value: {self.delta_c2h6}")
103
104        # 比率は0または正の値であることを確認
105        # if self.delta_ratio < 0:
106        #     raise ValueError(
107        #         f"'delta_ratio' must be 0 or a positive value: {self.delta_ratio}"
108        #     )
109        # エラーが出たため暫定的にfloat型の確認のみに変更
110        if not isinstance(self.delta_ratio, float):
111            raise ValueError(f"'delta_ratio' must be a float value: {self.delta_ratio}")
112
113        # 相関係数は-1から1の範囲内であることを確認
114        if not -1 <= self.correlation <= 1 and str(self.correlation) != "nan":
115            raise ValueError(
116                f"'correlation' must be between -1 and 1: {self.correlation}"
117            )
118
119        # セクション番号は0または正の整数であることを確認
120        if not isinstance(self.section, int) or self.section < 0:
121            raise ValueError(
122                f"'section' must be a non-negative integer: {self.section}"
123            )

ホットスポットの情報を保持するデータクラス

Parameters

timestamp: str
    タイムスタンプ
angle: float
    中心からの角度
avg_lat: float
    平均緯度
avg_lon: float
    平均経度
correlation: float
    ΔC2H6/ΔCH4相関係数
delta_ch4: float
    CH4の増加量
delta_c2h6: float
    C2H6の増加量
delta_ratio: float
    ΔC2H6/ΔCH4の比率
section: int
    所属する区画番号
type: HotspotType
    ホットスポットの種類
HotspotData( timestamp: str, angle: float, avg_lat: float, avg_lon: float, correlation: float, delta_ch4: float, delta_c2h6: float, delta_ratio: float, section: int, type: Literal['bio', 'gas', 'comb', 'scale_check'])
timestamp: str
angle: float
avg_lat: float
avg_lon: float
correlation: float
delta_ch4: float
delta_c2h6: float
delta_ratio: float
section: int
type: Literal['bio', 'gas', 'comb', 'scale_check']
class HotspotEmissionAnalyzer:
256class HotspotEmissionAnalyzer:
257    def __init__(
258        self,
259        logger: Logger | None = None,
260        logging_debug: bool = False,
261    ):
262        """
263        渦相関法によって記録されたデータファイルを処理するクラス。
264
265        Parameters
266        ----------
267            fs: float
268                サンプリング周波数。
269            logger: Logger | None, optional
270                使用するロガー。Noneの場合は新しいロガーを作成します。
271            logging_debug: bool, optional
272                ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。
273        """
274        # ロガー
275        log_level: int = INFO
276        if logging_debug:
277            log_level = DEBUG
278        self.logger: Logger = setup_logger(logger=logger, log_level=log_level)
279
280    def calculate_emission_rates(
281        self,
282        hotspots: list[HotspotData],
283        config: HotspotEmissionConfig,
284        print_summary: bool = False,
285    ) -> list[EmissionData]:
286        """
287        検出されたホットスポットのCH4漏出量を計算・解析し、統計情報を生成します。
288
289        Parameters
290        ----------
291            hotspots: list[HotspotData]
292                分析対象のホットスポットのリスト
293            config: HotspotEmissionConfig
294                排出量計算の設定
295            print_summary: bool
296                統計情報を表示するかどうか。デフォルトはFalse。
297
298        Returns
299        ----------
300            list[EmissionData]
301                - 各ホットスポットの排出量データを含むリスト
302
303        Examples
304        ----------
305        >>> # Weller et al. (2022)の係数を使用する例
306        >>> config = HotspotEmissionConfig(
307        ...     formula=EmissionFormula(name="weller", coef_a=0.988, coef_b=0.817),
308        ...     emission_categories={
309        ...         "low": {"min": 0, "max": 6},
310        ...         "medium": {"min": 6, "max": 40},
311        ...         "high": {"min": 40, "max": float("inf")},
312        ...     }
313        ... )
314        >>> emissions_list = HotspotEmissionAnalyzer.calculate_emission_rates(
315        ...     hotspots=hotspots,
316        ...     config=config,
317        ...     print_summary=True
318        ... )
319        """
320        # 係数の取得
321        coef_a: float = config.formula.coef_a
322        coef_b: float = config.formula.coef_b
323
324        # 排出量の計算
325        emission_data_list = []
326        for spot in hotspots:
327            # 漏出量の計算 (L/min)
328            emission_per_min = np.exp((np.log(spot.delta_ch4) + coef_a) / coef_b)
329            # 日排出量 (L/day)
330            emission_per_day = emission_per_min * 60 * 24
331            # 年間排出量 (L/year)
332            emission_per_year = emission_per_day * 365
333
334            emission_data = EmissionData(
335                timestamp=spot.timestamp,
336                delta_ch4=spot.delta_ch4,
337                delta_c2h6=spot.delta_c2h6,
338                delta_ratio=spot.delta_ratio,
339                emission_per_min=emission_per_min,
340                emission_per_day=emission_per_day,
341                emission_per_year=emission_per_year,
342                avg_lat=spot.avg_lat,
343                avg_lon=spot.avg_lon,
344                section=spot.section,
345                type=spot.type,
346            )
347            emission_data_list.append(emission_data)
348
349        # 統計計算用にDataFrameを作成
350        emission_df = pd.DataFrame([e.to_dict() for e in emission_data_list])
351
352        # タイプ別の統計情報を計算
353        # get_args(HotspotType)を使用して型安全なリストを作成
354        types = list(get_args(HotspotType))
355        for spot_type in types:
356            df_type = emission_df[emission_df["type"] == spot_type]
357            if len(df_type) > 0:
358                # 既存の統計情報を計算
359                type_stats = {
360                    "count": len(df_type),
361                    "emission_per_min_min": df_type["emission_per_min"].min(),
362                    "emission_per_min_max": df_type["emission_per_min"].max(),
363                    "emission_per_min_mean": df_type["emission_per_min"].mean(),
364                    "emission_per_min_median": df_type["emission_per_min"].median(),
365                    "total_annual_emission": df_type["emission_per_year"].sum(),
366                    "mean_annual_emission": df_type["emission_per_year"].mean(),
367                }
368
369                # 排出量カテゴリー別の統計を追加
370                category_counts = {}
371                for category, limits in config.emission_categories.items():
372                    mask = (df_type["emission_per_min"] >= limits["min"]) & (
373                        df_type["emission_per_min"] < limits["max"]
374                    )
375                    category_counts[category] = len(df_type[mask])
376                type_stats["emission_categories"] = category_counts
377
378                if print_summary:
379                    self.logger.info(f"{spot_type}タイプの統計情報:")
380                    print(f"  検出数: {type_stats['count']}")
381                    print("  排出量 (L/min):")
382                    print(f"    最小値: {type_stats['emission_per_min_min']:.2f}")
383                    print(f"    最大値: {type_stats['emission_per_min_max']:.2f}")
384                    print(f"    平均値: {type_stats['emission_per_min_mean']:.2f}")
385                    print(f"    中央値: {type_stats['emission_per_min_median']:.2f}")
386                    print("  排出量カテゴリー別の検出数:")
387                    for category, count in category_counts.items():
388                        print(f"    {category}: {count}")
389                    print("  年間排出量 (L/year):")
390                    print(f"    合計: {type_stats['total_annual_emission']:.2f}")
391                    print(f"    平均: {type_stats['mean_annual_emission']:.2f}")
392
393        return emission_data_list
394
395    def plot_emission_analysis(
396        self,
397        emissions: list[EmissionData],
398        output_dirpath: str | Path | None = None,
399        output_filename: str = "emission_analysis.png",
400        figsize: tuple[float, float] = (12, 5),
401        dpi: float | None = 350,
402        hotspot_colors: dict[HotspotType, str] | None = None,
403        add_legend: bool = True,
404        hist_log_y: bool = False,
405        hist_xlim: tuple[float, float] | None = None,
406        hist_ylim: tuple[float, float] | None = None,
407        scatter_xlim: tuple[float, float] | None = None,
408        scatter_ylim: tuple[float, float] | None = None,
409        hist_bin_width: float = 0.5,
410        print_summary: bool = False,
411        stack_bars: bool = True,
412        save_fig: bool = False,
413        show_fig: bool = True,
414        show_scatter: bool = True,
415    ) -> None:
416        """
417        排出量分析のプロットを作成する静的メソッド。
418
419        Parameters
420        ----------
421            emissions: list[EmissionData]
422                calculate_emission_ratesで生成された分析結果
423            output_dirpath: str | Path | None, optional
424                出力先ディレクトリのパス。指定しない場合はカレントディレクトリに保存。
425            output_filename: str, optional
426                保存するファイル名。デフォルトは"emission_analysis.png"。
427            figsize: tuple[float, float], optional
428                プロットのサイズ。デフォルトは(12, 5)。
429            dpi: float | None, optional
430                プロットの解像度。デフォルトは350。
431            hotspot_colors: dict[HotspotType, str] | None, optional
432                ホットスポットの色を定義する辞書。指定しない場合はデフォルトの色を使用。
433            add_legend: bool, optional
434                凡例を追加するかどうか。デフォルトはTrue。
435            hist_log_y: bool, optional
436                ヒストグラムのy軸を対数スケールにするかどうか。デフォルトはFalse。
437            hist_xlim: tuple[float, float] | None, optional
438                ヒストグラムのx軸の範囲。指定しない場合は自動で設定。
439            hist_ylim: tuple[float, float] | None, optional
440                ヒストグラムのy軸の範囲。指定しない場合は自動で設定。
441            scatter_xlim: tuple[float, float] | None, optional
442                散布図のx軸の範囲。指定しない場合は自動で設定。
443            scatter_ylim: tuple[float, float] | None, optional
444                散布図のy軸の範囲。指定しない場合は自動で設定。
445            hist_bin_width: float, optional
446                ヒストグラムのビンの幅。デフォルトは0.5。
447            print_summary: bool, optional
448                集計結果を表示するかどうか。デフォルトはFalse。
449            stack_bars: bool, optional
450                ヒストグラムを積み上げ方式で表示するかどうか。デフォルトはTrue。
451            save_fig: bool, optional
452                図をファイルに保存するかどうか。デフォルトはFalse。
453            show_fig: bool, optional
454                図を表示するかどうか。デフォルトはTrue。
455            show_scatter: bool, optional
456                散布図(右図)を表示するかどうか。デフォルトはTrue。
457
458        Returns
459        -------
460            None
461
462        Examples
463        --------
464        >>> analyzer = HotspotEmissionAnalyzer()
465        >>> emissions = analyzer.calculate_emission_rates(hotspots)
466        >>> analyzer.plot_emission_analysis(
467        ...     emissions,
468        ...     output_dirpath="results",
469        ...     save_fig=True,
470        ...     hist_bin_width=1.0
471        ... )
472        """
473        if hotspot_colors is None:
474            hotspot_colors = {
475                "bio": "blue",
476                "gas": "red",
477                "comb": "green",
478            }
479        # データをDataFrameに変換
480        df = pd.DataFrame([e.to_dict() for e in emissions])
481
482        # プロットの作成(散布図の有無に応じてサブプロット数を調整)
483        if show_scatter:
484            fig, (ax1, ax2) = plt.subplots(1, 2, figsize=figsize)
485            axes = [ax1, ax2]
486        else:
487            fig, ax1 = plt.subplots(1, 1, figsize=(figsize[0] // 2, figsize[1]))
488            axes = [ax1]
489
490        # 存在するタイプを確認
491        # HotspotTypeの定義順を基準にソート
492        hotspot_types = list(get_args(HotspotType))
493        existing_types = sorted(
494            df["type"].unique(), key=lambda x: hotspot_types.index(x)
495        )
496
497        # 左側: ヒストグラム
498        # ビンの範囲を設定
499        start = 0  # 必ず0から開始
500        if hist_xlim is not None:
501            end = hist_xlim[1]
502        else:
503            end = np.ceil(df["emission_per_min"].max() * 1.05)
504
505        # ビン数を計算(end値をbin_widthで割り切れるように調整)
506        n_bins = int(np.ceil(end / hist_bin_width))
507        end = n_bins * hist_bin_width
508
509        # ビンの生成(0から開始し、bin_widthの倍数で区切る)
510        bins = np.linspace(start, end, n_bins + 1)
511
512        # タイプごとにヒストグラムを積み上げ
513        if stack_bars:
514            # 積み上げ方式
515            bottom = np.zeros(len(bins) - 1)
516            for spot_type in existing_types:
517                data = df[df["type"] == spot_type]["emission_per_min"]
518                if len(data) > 0:
519                    counts, _ = np.histogram(data, bins=bins)
520                    ax1.bar(
521                        bins[:-1],
522                        counts,
523                        width=hist_bin_width,
524                        bottom=bottom,
525                        alpha=0.6,
526                        label=spot_type,
527                        color=hotspot_colors[spot_type],
528                    )
529                    bottom += counts
530        else:
531            # 重ね合わせ方式
532            for spot_type in existing_types:
533                data = df[df["type"] == spot_type]["emission_per_min"]
534                if len(data) > 0:
535                    counts, _ = np.histogram(data, bins=bins)
536                    ax1.bar(
537                        bins[:-1],
538                        counts,
539                        width=hist_bin_width,
540                        alpha=0.4,  # 透明度を上げて重なりを見やすく
541                        label=spot_type,
542                        color=hotspot_colors[spot_type],
543                    )
544
545        ax1.set_xlabel("CH$_4$ Emission (L min$^{-1}$)")
546        ax1.set_ylabel("Frequency")
547        if hist_log_y:
548            # ax1.set_yscale("log")
549            # 非線形スケールを設定(linthreshで線形から対数への遷移点を指定)
550            ax1.set_yscale("symlog", linthresh=1.0)
551        if hist_xlim is not None:
552            ax1.set_xlim(hist_xlim)
553        else:
554            ax1.set_xlim(0, np.ceil(df["emission_per_min"].max() * 1.05))
555
556        if hist_ylim is not None:
557            ax1.set_ylim(hist_ylim)
558        else:
559            ax1.set_ylim(0, ax1.get_ylim()[1])  # 下限を0に設定
560
561        if show_scatter:
562            # 右側: 散布図
563            for spot_type in existing_types:
564                mask = df["type"] == spot_type
565                ax2.scatter(
566                    df[mask]["emission_per_min"],
567                    df[mask]["delta_ch4"],
568                    alpha=0.6,
569                    label=spot_type,
570                    color=hotspot_colors[spot_type],
571                )
572
573            ax2.set_xlabel("Emission Rate (L min$^{-1}$)")
574            ax2.set_ylabel("ΔCH$_4$ (ppm)")
575            if scatter_xlim is not None:
576                ax2.set_xlim(scatter_xlim)
577            else:
578                ax2.set_xlim(0, np.ceil(df["emission_per_min"].max() * 1.05))
579
580            if scatter_ylim is not None:
581                ax2.set_ylim(scatter_ylim)
582            else:
583                ax2.set_ylim(0, np.ceil(df["delta_ch4"].max() * 1.05))
584
585        # 凡例の表示
586        if add_legend:
587            for ax in axes:
588                ax.legend(
589                    bbox_to_anchor=(0.5, -0.30),
590                    loc="upper center",
591                    ncol=len(existing_types),
592                )
593
594        plt.tight_layout()
595
596        # 図の保存
597        if save_fig:
598            if output_dirpath is None:
599                raise ValueError(
600                    "save_fig = True の場合、 output_dirpath を指定する必要があります。有効なディレクトリパスを指定してください。"
601                )
602            os.makedirs(output_dirpath, exist_ok=True)
603            output_filepath = os.path.join(output_dirpath, output_filename)
604            plt.savefig(output_filepath, bbox_inches="tight", dpi=dpi)
605        # 図の表示
606        if show_fig:
607            plt.show()
608        plt.close(fig=fig)
609
610        if print_summary:
611            # デバッグ用の出力
612            self.logger.info("ビンごとの集計:")
613            print(f"{'Range':>12} | {'bio':>8} | {'gas':>8} | {'total':>8}")
614            print("-" * 50)
615
616            for i in range(len(bins) - 1):
617                bin_start = bins[i]
618                bin_end = bins[i + 1]
619
620                # 各タイプのカウントを計算
621                counts_by_type: dict[HotspotType, int] = {"bio": 0, "gas": 0, "comb": 0}
622                total = 0
623                for spot_type in existing_types:
624                    mask = (
625                        (df["type"] == spot_type)
626                        & (df["emission_per_min"] >= bin_start)
627                        & (df["emission_per_min"] < bin_end)
628                    )
629                    count = len(df[mask])
630                    counts_by_type[spot_type] = count
631                    total += count
632
633                # カウントが0の場合はスキップ
634                if total > 0:
635                    range_str = f"{bin_start:5.1f}-{bin_end:<5.1f}"
636                    bio_count = counts_by_type.get("bio", 0)
637                    gas_count = counts_by_type.get("gas", 0)
638                    print(
639                        f"{range_str:>12} | {bio_count:8d} | {gas_count:8d} | {total:8d}"
640                    )
HotspotEmissionAnalyzer(logger: logging.Logger | None = None, logging_debug: bool = False)
257    def __init__(
258        self,
259        logger: Logger | None = None,
260        logging_debug: bool = False,
261    ):
262        """
263        渦相関法によって記録されたデータファイルを処理するクラス。
264
265        Parameters
266        ----------
267            fs: float
268                サンプリング周波数。
269            logger: Logger | None, optional
270                使用するロガー。Noneの場合は新しいロガーを作成します。
271            logging_debug: bool, optional
272                ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。
273        """
274        # ロガー
275        log_level: int = INFO
276        if logging_debug:
277            log_level = DEBUG
278        self.logger: Logger = setup_logger(logger=logger, log_level=log_level)

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

Parameters

fs: float
    サンプリング周波数。
logger: Logger | None, optional
    使用するロガー。Noneの場合は新しいロガーを作成します。
logging_debug: bool, optional
    ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。
logger: logging.Logger
def calculate_emission_rates( self, hotspots: list[HotspotData], config: HotspotEmissionConfig, print_summary: bool = False) -> list[EmissionData]:
280    def calculate_emission_rates(
281        self,
282        hotspots: list[HotspotData],
283        config: HotspotEmissionConfig,
284        print_summary: bool = False,
285    ) -> list[EmissionData]:
286        """
287        検出されたホットスポットのCH4漏出量を計算・解析し、統計情報を生成します。
288
289        Parameters
290        ----------
291            hotspots: list[HotspotData]
292                分析対象のホットスポットのリスト
293            config: HotspotEmissionConfig
294                排出量計算の設定
295            print_summary: bool
296                統計情報を表示するかどうか。デフォルトはFalse。
297
298        Returns
299        ----------
300            list[EmissionData]
301                - 各ホットスポットの排出量データを含むリスト
302
303        Examples
304        ----------
305        >>> # Weller et al. (2022)の係数を使用する例
306        >>> config = HotspotEmissionConfig(
307        ...     formula=EmissionFormula(name="weller", coef_a=0.988, coef_b=0.817),
308        ...     emission_categories={
309        ...         "low": {"min": 0, "max": 6},
310        ...         "medium": {"min": 6, "max": 40},
311        ...         "high": {"min": 40, "max": float("inf")},
312        ...     }
313        ... )
314        >>> emissions_list = HotspotEmissionAnalyzer.calculate_emission_rates(
315        ...     hotspots=hotspots,
316        ...     config=config,
317        ...     print_summary=True
318        ... )
319        """
320        # 係数の取得
321        coef_a: float = config.formula.coef_a
322        coef_b: float = config.formula.coef_b
323
324        # 排出量の計算
325        emission_data_list = []
326        for spot in hotspots:
327            # 漏出量の計算 (L/min)
328            emission_per_min = np.exp((np.log(spot.delta_ch4) + coef_a) / coef_b)
329            # 日排出量 (L/day)
330            emission_per_day = emission_per_min * 60 * 24
331            # 年間排出量 (L/year)
332            emission_per_year = emission_per_day * 365
333
334            emission_data = EmissionData(
335                timestamp=spot.timestamp,
336                delta_ch4=spot.delta_ch4,
337                delta_c2h6=spot.delta_c2h6,
338                delta_ratio=spot.delta_ratio,
339                emission_per_min=emission_per_min,
340                emission_per_day=emission_per_day,
341                emission_per_year=emission_per_year,
342                avg_lat=spot.avg_lat,
343                avg_lon=spot.avg_lon,
344                section=spot.section,
345                type=spot.type,
346            )
347            emission_data_list.append(emission_data)
348
349        # 統計計算用にDataFrameを作成
350        emission_df = pd.DataFrame([e.to_dict() for e in emission_data_list])
351
352        # タイプ別の統計情報を計算
353        # get_args(HotspotType)を使用して型安全なリストを作成
354        types = list(get_args(HotspotType))
355        for spot_type in types:
356            df_type = emission_df[emission_df["type"] == spot_type]
357            if len(df_type) > 0:
358                # 既存の統計情報を計算
359                type_stats = {
360                    "count": len(df_type),
361                    "emission_per_min_min": df_type["emission_per_min"].min(),
362                    "emission_per_min_max": df_type["emission_per_min"].max(),
363                    "emission_per_min_mean": df_type["emission_per_min"].mean(),
364                    "emission_per_min_median": df_type["emission_per_min"].median(),
365                    "total_annual_emission": df_type["emission_per_year"].sum(),
366                    "mean_annual_emission": df_type["emission_per_year"].mean(),
367                }
368
369                # 排出量カテゴリー別の統計を追加
370                category_counts = {}
371                for category, limits in config.emission_categories.items():
372                    mask = (df_type["emission_per_min"] >= limits["min"]) & (
373                        df_type["emission_per_min"] < limits["max"]
374                    )
375                    category_counts[category] = len(df_type[mask])
376                type_stats["emission_categories"] = category_counts
377
378                if print_summary:
379                    self.logger.info(f"{spot_type}タイプの統計情報:")
380                    print(f"  検出数: {type_stats['count']}")
381                    print("  排出量 (L/min):")
382                    print(f"    最小値: {type_stats['emission_per_min_min']:.2f}")
383                    print(f"    最大値: {type_stats['emission_per_min_max']:.2f}")
384                    print(f"    平均値: {type_stats['emission_per_min_mean']:.2f}")
385                    print(f"    中央値: {type_stats['emission_per_min_median']:.2f}")
386                    print("  排出量カテゴリー別の検出数:")
387                    for category, count in category_counts.items():
388                        print(f"    {category}: {count}")
389                    print("  年間排出量 (L/year):")
390                    print(f"    合計: {type_stats['total_annual_emission']:.2f}")
391                    print(f"    平均: {type_stats['mean_annual_emission']:.2f}")
392
393        return emission_data_list

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

Parameters

hotspots: list[HotspotData]
    分析対象のホットスポットのリスト
config: HotspotEmissionConfig
    排出量計算の設定
print_summary: bool
    統計情報を表示するかどうか。デフォルトはFalse。

Returns

list[EmissionData]
    - 各ホットスポットの排出量データを含むリスト

Examples

>>> # Weller et al. (2022)の係数を使用する例
>>> config = HotspotEmissionConfig(
...     formula=EmissionFormula(name="weller", coef_a=0.988, coef_b=0.817),
...     emission_categories={
...         "low": {"min": 0, "max": 6},
...         "medium": {"min": 6, "max": 40},
...         "high": {"min": 40, "max": float("inf")},
...     }
... )
>>> emissions_list = HotspotEmissionAnalyzer.calculate_emission_rates(
...     hotspots=hotspots,
...     config=config,
...     print_summary=True
... )
def plot_emission_analysis( self, emissions: list[EmissionData], output_dirpath: str | pathlib.Path | None = None, output_filename: str = 'emission_analysis.png', figsize: tuple[float, float] = (12, 5), dpi: float | None = 350, hotspot_colors: dict[typing.Literal['bio', 'gas', 'comb', 'scale_check'], str] | None = None, 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 = False, stack_bars: bool = True, save_fig: bool = False, show_fig: bool = True, show_scatter: bool = True) -> None:
395    def plot_emission_analysis(
396        self,
397        emissions: list[EmissionData],
398        output_dirpath: str | Path | None = None,
399        output_filename: str = "emission_analysis.png",
400        figsize: tuple[float, float] = (12, 5),
401        dpi: float | None = 350,
402        hotspot_colors: dict[HotspotType, str] | None = None,
403        add_legend: bool = True,
404        hist_log_y: bool = False,
405        hist_xlim: tuple[float, float] | None = None,
406        hist_ylim: tuple[float, float] | None = None,
407        scatter_xlim: tuple[float, float] | None = None,
408        scatter_ylim: tuple[float, float] | None = None,
409        hist_bin_width: float = 0.5,
410        print_summary: bool = False,
411        stack_bars: bool = True,
412        save_fig: bool = False,
413        show_fig: bool = True,
414        show_scatter: bool = True,
415    ) -> None:
416        """
417        排出量分析のプロットを作成する静的メソッド。
418
419        Parameters
420        ----------
421            emissions: list[EmissionData]
422                calculate_emission_ratesで生成された分析結果
423            output_dirpath: str | Path | None, optional
424                出力先ディレクトリのパス。指定しない場合はカレントディレクトリに保存。
425            output_filename: str, optional
426                保存するファイル名。デフォルトは"emission_analysis.png"。
427            figsize: tuple[float, float], optional
428                プロットのサイズ。デフォルトは(12, 5)。
429            dpi: float | None, optional
430                プロットの解像度。デフォルトは350。
431            hotspot_colors: dict[HotspotType, str] | None, optional
432                ホットスポットの色を定義する辞書。指定しない場合はデフォルトの色を使用。
433            add_legend: bool, optional
434                凡例を追加するかどうか。デフォルトはTrue。
435            hist_log_y: bool, optional
436                ヒストグラムのy軸を対数スケールにするかどうか。デフォルトはFalse。
437            hist_xlim: tuple[float, float] | None, optional
438                ヒストグラムのx軸の範囲。指定しない場合は自動で設定。
439            hist_ylim: tuple[float, float] | None, optional
440                ヒストグラムのy軸の範囲。指定しない場合は自動で設定。
441            scatter_xlim: tuple[float, float] | None, optional
442                散布図のx軸の範囲。指定しない場合は自動で設定。
443            scatter_ylim: tuple[float, float] | None, optional
444                散布図のy軸の範囲。指定しない場合は自動で設定。
445            hist_bin_width: float, optional
446                ヒストグラムのビンの幅。デフォルトは0.5。
447            print_summary: bool, optional
448                集計結果を表示するかどうか。デフォルトはFalse。
449            stack_bars: bool, optional
450                ヒストグラムを積み上げ方式で表示するかどうか。デフォルトはTrue。
451            save_fig: bool, optional
452                図をファイルに保存するかどうか。デフォルトはFalse。
453            show_fig: bool, optional
454                図を表示するかどうか。デフォルトはTrue。
455            show_scatter: bool, optional
456                散布図(右図)を表示するかどうか。デフォルトはTrue。
457
458        Returns
459        -------
460            None
461
462        Examples
463        --------
464        >>> analyzer = HotspotEmissionAnalyzer()
465        >>> emissions = analyzer.calculate_emission_rates(hotspots)
466        >>> analyzer.plot_emission_analysis(
467        ...     emissions,
468        ...     output_dirpath="results",
469        ...     save_fig=True,
470        ...     hist_bin_width=1.0
471        ... )
472        """
473        if hotspot_colors is None:
474            hotspot_colors = {
475                "bio": "blue",
476                "gas": "red",
477                "comb": "green",
478            }
479        # データをDataFrameに変換
480        df = pd.DataFrame([e.to_dict() for e in emissions])
481
482        # プロットの作成(散布図の有無に応じてサブプロット数を調整)
483        if show_scatter:
484            fig, (ax1, ax2) = plt.subplots(1, 2, figsize=figsize)
485            axes = [ax1, ax2]
486        else:
487            fig, ax1 = plt.subplots(1, 1, figsize=(figsize[0] // 2, figsize[1]))
488            axes = [ax1]
489
490        # 存在するタイプを確認
491        # HotspotTypeの定義順を基準にソート
492        hotspot_types = list(get_args(HotspotType))
493        existing_types = sorted(
494            df["type"].unique(), key=lambda x: hotspot_types.index(x)
495        )
496
497        # 左側: ヒストグラム
498        # ビンの範囲を設定
499        start = 0  # 必ず0から開始
500        if hist_xlim is not None:
501            end = hist_xlim[1]
502        else:
503            end = np.ceil(df["emission_per_min"].max() * 1.05)
504
505        # ビン数を計算(end値をbin_widthで割り切れるように調整)
506        n_bins = int(np.ceil(end / hist_bin_width))
507        end = n_bins * hist_bin_width
508
509        # ビンの生成(0から開始し、bin_widthの倍数で区切る)
510        bins = np.linspace(start, end, n_bins + 1)
511
512        # タイプごとにヒストグラムを積み上げ
513        if stack_bars:
514            # 積み上げ方式
515            bottom = np.zeros(len(bins) - 1)
516            for spot_type in existing_types:
517                data = df[df["type"] == spot_type]["emission_per_min"]
518                if len(data) > 0:
519                    counts, _ = np.histogram(data, bins=bins)
520                    ax1.bar(
521                        bins[:-1],
522                        counts,
523                        width=hist_bin_width,
524                        bottom=bottom,
525                        alpha=0.6,
526                        label=spot_type,
527                        color=hotspot_colors[spot_type],
528                    )
529                    bottom += counts
530        else:
531            # 重ね合わせ方式
532            for spot_type in existing_types:
533                data = df[df["type"] == spot_type]["emission_per_min"]
534                if len(data) > 0:
535                    counts, _ = np.histogram(data, bins=bins)
536                    ax1.bar(
537                        bins[:-1],
538                        counts,
539                        width=hist_bin_width,
540                        alpha=0.4,  # 透明度を上げて重なりを見やすく
541                        label=spot_type,
542                        color=hotspot_colors[spot_type],
543                    )
544
545        ax1.set_xlabel("CH$_4$ Emission (L min$^{-1}$)")
546        ax1.set_ylabel("Frequency")
547        if hist_log_y:
548            # ax1.set_yscale("log")
549            # 非線形スケールを設定(linthreshで線形から対数への遷移点を指定)
550            ax1.set_yscale("symlog", linthresh=1.0)
551        if hist_xlim is not None:
552            ax1.set_xlim(hist_xlim)
553        else:
554            ax1.set_xlim(0, np.ceil(df["emission_per_min"].max() * 1.05))
555
556        if hist_ylim is not None:
557            ax1.set_ylim(hist_ylim)
558        else:
559            ax1.set_ylim(0, ax1.get_ylim()[1])  # 下限を0に設定
560
561        if show_scatter:
562            # 右側: 散布図
563            for spot_type in existing_types:
564                mask = df["type"] == spot_type
565                ax2.scatter(
566                    df[mask]["emission_per_min"],
567                    df[mask]["delta_ch4"],
568                    alpha=0.6,
569                    label=spot_type,
570                    color=hotspot_colors[spot_type],
571                )
572
573            ax2.set_xlabel("Emission Rate (L min$^{-1}$)")
574            ax2.set_ylabel("ΔCH$_4$ (ppm)")
575            if scatter_xlim is not None:
576                ax2.set_xlim(scatter_xlim)
577            else:
578                ax2.set_xlim(0, np.ceil(df["emission_per_min"].max() * 1.05))
579
580            if scatter_ylim is not None:
581                ax2.set_ylim(scatter_ylim)
582            else:
583                ax2.set_ylim(0, np.ceil(df["delta_ch4"].max() * 1.05))
584
585        # 凡例の表示
586        if add_legend:
587            for ax in axes:
588                ax.legend(
589                    bbox_to_anchor=(0.5, -0.30),
590                    loc="upper center",
591                    ncol=len(existing_types),
592                )
593
594        plt.tight_layout()
595
596        # 図の保存
597        if save_fig:
598            if output_dirpath is None:
599                raise ValueError(
600                    "save_fig = True の場合、 output_dirpath を指定する必要があります。有効なディレクトリパスを指定してください。"
601                )
602            os.makedirs(output_dirpath, exist_ok=True)
603            output_filepath = os.path.join(output_dirpath, output_filename)
604            plt.savefig(output_filepath, bbox_inches="tight", dpi=dpi)
605        # 図の表示
606        if show_fig:
607            plt.show()
608        plt.close(fig=fig)
609
610        if print_summary:
611            # デバッグ用の出力
612            self.logger.info("ビンごとの集計:")
613            print(f"{'Range':>12} | {'bio':>8} | {'gas':>8} | {'total':>8}")
614            print("-" * 50)
615
616            for i in range(len(bins) - 1):
617                bin_start = bins[i]
618                bin_end = bins[i + 1]
619
620                # 各タイプのカウントを計算
621                counts_by_type: dict[HotspotType, int] = {"bio": 0, "gas": 0, "comb": 0}
622                total = 0
623                for spot_type in existing_types:
624                    mask = (
625                        (df["type"] == spot_type)
626                        & (df["emission_per_min"] >= bin_start)
627                        & (df["emission_per_min"] < bin_end)
628                    )
629                    count = len(df[mask])
630                    counts_by_type[spot_type] = count
631                    total += count
632
633                # カウントが0の場合はスキップ
634                if total > 0:
635                    range_str = f"{bin_start:5.1f}-{bin_end:<5.1f}"
636                    bio_count = counts_by_type.get("bio", 0)
637                    gas_count = counts_by_type.get("gas", 0)
638                    print(
639                        f"{range_str:>12} | {bio_count:8d} | {gas_count:8d} | {total:8d}"
640                    )

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

Parameters

emissions: list[EmissionData]
    calculate_emission_ratesで生成された分析結果
output_dirpath: str | Path | None, optional
    出力先ディレクトリのパス。指定しない場合はカレントディレクトリに保存。
output_filename: str, optional
    保存するファイル名。デフォルトは"emission_analysis.png"。
figsize: tuple[float, float], optional
    プロットのサイズ。デフォルトは(12, 5)。
dpi: float | None, optional
    プロットの解像度。デフォルトは350。
hotspot_colors: dict[HotspotType, str] | None, optional
    ホットスポットの色を定義する辞書。指定しない場合はデフォルトの色を使用。
add_legend: bool, optional
    凡例を追加するかどうか。デフォルトはTrue。
hist_log_y: bool, optional
    ヒストグラムのy軸を対数スケールにするかどうか。デフォルトはFalse。
hist_xlim: tuple[float, float] | None, optional
    ヒストグラムのx軸の範囲。指定しない場合は自動で設定。
hist_ylim: tuple[float, float] | None, optional
    ヒストグラムのy軸の範囲。指定しない場合は自動で設定。
scatter_xlim: tuple[float, float] | None, optional
    散布図のx軸の範囲。指定しない場合は自動で設定。
scatter_ylim: tuple[float, float] | None, optional
    散布図のy軸の範囲。指定しない場合は自動で設定。
hist_bin_width: float, optional
    ヒストグラムのビンの幅。デフォルトは0.5。
print_summary: bool, optional
    集計結果を表示するかどうか。デフォルトはFalse。
stack_bars: bool, optional
    ヒストグラムを積み上げ方式で表示するかどうか。デフォルトはTrue。
save_fig: bool, optional
    図をファイルに保存するかどうか。デフォルトはFalse。
show_fig: bool, optional
    図を表示するかどうか。デフォルトはTrue。
show_scatter: bool, optional
    散布図(右図)を表示するかどうか。デフォルトはTrue。

Returns

None

Examples

>>> analyzer = HotspotEmissionAnalyzer()
>>> emissions = analyzer.calculate_emission_rates(hotspots)
>>> analyzer.plot_emission_analysis(
...     emissions,
...     output_dirpath="results",
...     save_fig=True,
...     hist_bin_width=1.0
... )
@dataclass
class HotspotEmissionConfig:
193@dataclass
194class HotspotEmissionConfig:
195    """
196    排出量計算の設定を保持するデータクラス
197
198    Parameters
199    ----------
200        formula: EmissionFormula
201            使用する計算式の設定
202        emission_categories: dict[str, dict[str, float]]
203            排出量カテゴリーの閾値設定
204            デフォルト値:
205            ```python
206            {
207                "low": {"min": 0, "max": 6},               # < 6 L/min
208                "medium": {"min": 6, "max": 40},           # 6-40 L/min
209                "high": {"min": 40, "max": float("inf")}  # > 40 L/min
210            }
211            ```
212
213    Examples
214    ----------
215    >>> # Weller et al. (2022)の係数を使用する場合
216    >>> config = HotspotEmissionConfig(
217    ...     formula=EmissionFormula(name="weller", coef_a=0.988, coef_b=0.817),
218    ...     emission_categories={
219    ...         "low": {"min": 0, "max": 6},  # < 6 L/min
220    ...         "medium": {"min": 6, "max": 40},  # 6-40 L/min
221    ...         "high": {"min": 40, "max": float("inf")},  # > 40 L/min
222    ...     }
223    ... )
224    >>> # 複数のconfigをリスト形式で定義する場合
225    >>> emission_configs: list[HotspotEmissionConfig] = [
226    ...     HotspotEmissionConfig(formula=EmissionFormula(name="weller", coef_a=0.988, coef_b=0.817)),
227    ...     HotspotEmissionConfig(formula=EmissionFormula(name="weitzel", coef_a=0.521, coef_b=0.795)),
228    ...     HotspotEmissionConfig(formula=EmissionFormula(name="joo", coef_a=2.738, coef_b=1.329)),
229    ...     HotspotEmissionConfig(formula=EmissionFormula(name="umezawa", coef_a=2.716, coef_b=0.741)),
230    ... ]
231    """
232
233    formula: EmissionFormula
234    emission_categories: dict[str, dict[str, float]] = field(
235        default_factory=lambda: {
236            "low": {"min": 0, "max": 6},  # < 6 L/min
237            "medium": {"min": 6, "max": 40},  # 6-40 L/min
238            "high": {"min": 40, "max": float("inf")},  # > 40 L/min
239        }
240    )
241
242    def __post_init__(self) -> None:
243        """
244        パラメータの検証を行います。
245        """
246        # カテゴリーの閾値の整合性チェック
247        for category, limits in self.emission_categories.items():
248            if "min" not in limits or "max" not in limits:
249                raise ValueError(
250                    f"Category {category} must have 'min' and 'max' values"
251                )
252            if limits["min"] > limits["max"]:
253                raise ValueError(f"Category {category} has invalid range: min > max")

排出量計算の設定を保持するデータクラス

Parameters

formula: EmissionFormula
    使用する計算式の設定
emission_categories: dict[str, dict[str, float]]
    排出量カテゴリーの閾値設定
    デフォルト値:


    
{
    "low": {"min": 0, "max": 6},               # < 6 L/min
    "medium": {"min": 6, "max": 40},           # 6-40 L/min
    "high": {"min": 40, "max": float("inf")}  # > 40 L/min
}

Examples

>>> # Weller et al. (2022)の係数を使用する場合
>>> config = HotspotEmissionConfig(
...     formula=EmissionFormula(name="weller", coef_a=0.988, coef_b=0.817),
...     emission_categories={
...         "low": {"min": 0, "max": 6},  # < 6 L/min
...         "medium": {"min": 6, "max": 40},  # 6-40 L/min
...         "high": {"min": 40, "max": float("inf")},  # > 40 L/min
...     }
... )
>>> # 複数のconfigをリスト形式で定義する場合
>>> emission_configs: list[HotspotEmissionConfig] = [
...     HotspotEmissionConfig(formula=EmissionFormula(name="weller", coef_a=0.988, coef_b=0.817)),
...     HotspotEmissionConfig(formula=EmissionFormula(name="weitzel", coef_a=0.521, coef_b=0.795)),
...     HotspotEmissionConfig(formula=EmissionFormula(name="joo", coef_a=2.738, coef_b=1.329)),
...     HotspotEmissionConfig(formula=EmissionFormula(name="umezawa", coef_a=2.716, coef_b=0.741)),
... ]
HotspotEmissionConfig( formula: EmissionFormula, emission_categories: dict[str, dict[str, float]] = <factory>)
formula: EmissionFormula
emission_categories: dict[str, dict[str, float]]
@dataclass
class HotspotParams:
129@dataclass
130class HotspotParams:
131    """ホットスポット解析のパラメータ設定
132
133    Parameters
134    ----------
135        col_ch4_ppm: str, optional
136            CH4濃度のカラム名。デフォルトは"ch4_ppm"。
137        col_c2h6_ppb: str, optional
138            C2H6濃度のカラム名。デフォルトは"c2h6_ppb"。
139        col_h2o_ppm: str, optional
140            H2O濃度のカラム名。デフォルトは"h2o_ppm"。
141        ch4_ppm_delta_min: float, optional
142            CH4濃度変化量の下限閾値。この値未満のデータは除外される。デフォルトは0.05。
143        ch4_ppm_delta_max: float, optional
144            CH4濃度変化量の上限閾値。この値を超えるデータは除外される。デフォルトは無限大。
145        c2h6_ppb_delta_min: float, optional
146            C2H6濃度変化量の下限閾値。この値未満のデータは除外される。デフォルトは0.0。
147        c2h6_ppb_delta_max: float, optional
148            C2H6濃度変化量の上限閾値。この値を超えるデータは除外される。デフォルトは1000.0。
149        h2o_ppm_min: float, optional
150            H2O濃度の下限閾値。この値未満のデータは除外される。デフォルトは2000。
151        rolling_method: RollingMethod, optional
152            移動計算の方法。"quantile"は下位パーセンタイル値を使用し、"mean"は移動平均を使用する。デフォルトは"quantile"。
153        quantile_value: float, optional
154            quantileメソッド使用時の下位パーセンタイル値(0-1)。デフォルトは0.05。
155
156    Examples
157    --------
158    >>> params = HotspotParams(
159    ...     col_ch4_ppm="ch4_ppm",
160    ...     ch4_ppm_delta_min=0.1,
161    ...     rolling_method="mean"
162    ... )
163    >>> analyzer = MobileMeasurementAnalyzer(hotspot_params=params)
164    """
165
166    col_ch4_ppm: str = "ch4_ppm"
167    col_c2h6_ppb: str = "c2h6_ppb"
168    col_h2o_ppm: str = "h2o_ppm"
169    ch4_ppm_delta_min: float = 0.05
170    ch4_ppm_delta_max: float = float("inf")
171    c2h6_ppb_delta_min: float = 0.0
172    c2h6_ppb_delta_max: float = 1000.0
173    h2o_ppm_min: float = 2000
174    rolling_method: RollingMethod = "quantile"
175    quantile_value: float = 0.05
176
177    def __post_init__(self) -> None:
178        """パラメータの検証を行います。
179
180        Raises
181        ----------
182            ValueError: quantile_value が0以上1以下でない場合
183            ValueError: 下限値が上限値を超える場合
184        """
185        if not 0 <= self.quantile_value <= 1:
186            raise ValueError(
187                f"'quantile_value' must be between 0 and 1, got {self.quantile_value}"
188            )
189
190        if math.isinf(self.ch4_ppm_delta_min) or math.isinf(self.c2h6_ppb_delta_min):
191            raise ValueError(
192                "Lower threshold values cannot be set to infinity: 'ch4_ppm_delta_min', 'c2h6_ppb_delta_min'."
193            )
194
195        if self.ch4_ppm_delta_min > self.ch4_ppm_delta_max:
196            raise ValueError(
197                "'ch4_ppm_delta_min' must be less than or equal to 'ch4_ppm_delta_max'"
198            )
199
200        if self.c2h6_ppb_delta_min > self.c2h6_ppb_delta_max:
201            raise ValueError(
202                "'c2h6_ppb_delta_min' must be less than or equal to 'c2h6_ppb_delta_max'"
203            )

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

Parameters

col_ch4_ppm: str, optional
    CH4濃度のカラム名。デフォルトは"ch4_ppm"。
col_c2h6_ppb: str, optional
    C2H6濃度のカラム名。デフォルトは"c2h6_ppb"。
col_h2o_ppm: str, optional
    H2O濃度のカラム名。デフォルトは"h2o_ppm"。
ch4_ppm_delta_min: float, optional
    CH4濃度変化量の下限閾値。この値未満のデータは除外される。デフォルトは0.05。
ch4_ppm_delta_max: float, optional
    CH4濃度変化量の上限閾値。この値を超えるデータは除外される。デフォルトは無限大。
c2h6_ppb_delta_min: float, optional
    C2H6濃度変化量の下限閾値。この値未満のデータは除外される。デフォルトは0.0。
c2h6_ppb_delta_max: float, optional
    C2H6濃度変化量の上限閾値。この値を超えるデータは除外される。デフォルトは1000.0。
h2o_ppm_min: float, optional
    H2O濃度の下限閾値。この値未満のデータは除外される。デフォルトは2000。
rolling_method: RollingMethod, optional
    移動計算の方法。"quantile"は下位パーセンタイル値を使用し、"mean"は移動平均を使用する。デフォルトは"quantile"。
quantile_value: float, optional
    quantileメソッド使用時の下位パーセンタイル値(0-1)。デフォルトは0.05。

Examples

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

車載濃度観測で得られた測定データを解析するクラス

MobileMeasurementAnalyzer( center_lat: float, center_lon: float, configs: list[MobileMeasurementConfig] | 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, columns_rename_dict: dict[str, str] | None = None, na_values: list[str] | None = None, logger: logging.Logger | None = None, logging_debug: bool = False)
334    def __init__(
335        self,
336        center_lat: float,
337        center_lon: float,
338        configs: list[MobileMeasurementConfig] | list[tuple[float, float, str | Path]],
339        num_sections: int = 4,
340        ch4_enhance_threshold: float = 0.1,
341        correlation_threshold: float = 0.7,
342        hotspot_area_meter: float = 50,
343        hotspot_params: HotspotParams | None = None,
344        window_minutes: float = 5,
345        columns_rename_dict: dict[str, str] | None = None,
346        na_values: list[str] | None = None,
347        logger: Logger | None = None,
348        logging_debug: bool = False,
349    ):
350        """
351        測定データ解析クラスを初期化します。
352
353        Parameters
354        ----------
355            center_lat: float
356                中心緯度
357            center_lon: float
358                中心経度
359            configs: list[MobileMeasurementConfig] | list[tuple[float, float, str | Path]]
360                入力ファイルのリスト
361            num_sections: int, optional
362                分割する区画数。デフォルト値は4です。
363            ch4_enhance_threshold: float, optional
364                CH4増加の閾値(ppm)。デフォルト値は0.1です。
365            correlation_threshold: float, optional
366                相関係数の閾値。デフォルト値は0.7です。
367            hotspot_area_meter: float, optional
368                ホットスポットの検出に使用するエリアの半径(メートル)。デフォルト値は50メートルです。
369            hotspot_params: HotspotParams | None, optional
370                ホットスポット解析のパラメータ設定。未指定の場合はデフォルト設定を使用します。
371            window_minutes: float, optional
372                移動窓の大きさ(分)。デフォルト値は5分です。
373            columns_rename_dict: dict[str, str] | None, optional
374                元のデータファイルのヘッダーを汎用的な単語に変換するための辞書型データ。未指定の場合は以下のデフォルト値を使用します:
375                ```python
376                {
377                    "Time Stamp": "timestamp",
378                    "CH4 (ppm)": "ch4_ppm",
379                    "C2H6 (ppb)": "c2h6_ppb",
380                    "H2O (ppm)": "h2o_ppm",
381                    "Latitude": "latitude",
382                    "Longitude": "longitude"
383                }
384                ```
385            na_values: list[str] | None, optional
386                NaNと判定する値のパターン。未指定の場合は["No Data", "nan"]を使用します。
387            logger: Logger | None, optional
388                使用するロガー。未指定の場合は新しいロガーを作成します。
389            logging_debug: bool, optional
390                ログレベルを"DEBUG"に設定するかどうか。デフォルト値はFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。
391
392        Examples
393        --------
394        >>> analyzer = MobileMeasurementAnalyzer(
395        ...     center_lat=35.6895,
396        ...     center_lon=139.6917,
397        ...     configs=[
398        ...         MobileMeasurementConfig(fs=1.0, lag=0.0, path="data1.csv"),
399        ...         MobileMeasurementConfig(fs=1.0, lag=0.0, path="data2.csv")
400        ...     ],
401        ...     num_sections=6,
402        ...     ch4_enhance_threshold=0.2
403        ... )
404        """
405        # ロガー
406        log_level: int = INFO
407        if logging_debug:
408            log_level = DEBUG
409        self.logger: Logger = setup_logger(logger=logger, log_level=log_level)
410        # デフォルト値を使用
411        if columns_rename_dict is None:
412            columns_rename_dict = {
413                "Time Stamp": "timestamp",
414                "CH4 (ppm)": "ch4_ppm",
415                "C2H6 (ppb)": "c2h6_ppb",
416                "H2O (ppm)": "h2o_ppm",
417                "Latitude": "latitude",
418                "Longitude": "longitude",
419            }
420        if na_values is None:
421            na_values = ["No Data", "nan"]
422        # プライベートなプロパティ
423        self._center_lat: float = center_lat
424        self._center_lon: float = center_lon
425        self._ch4_enhance_threshold: float = ch4_enhance_threshold
426        self._correlation_threshold: float = correlation_threshold
427        self._hotspot_area_meter: float = hotspot_area_meter
428        self._column_mapping: dict[str, str] = columns_rename_dict
429        self._na_values: list[str] = na_values
430        self._hotspot_params = hotspot_params or HotspotParams()
431        self._num_sections: int = num_sections
432        # セクションの範囲
433        section_size: float = 360 / num_sections
434        self._section_size: float = section_size
435        self._sections = MobileMeasurementAnalyzer._initialize_sections(
436            num_sections, section_size
437        )
438        # window_sizeをデータポイント数に変換(分→秒→データポイント数)
439        self._window_size: int = MobileMeasurementAnalyzer._calculate_window_size(
440            window_minutes
441        )
442        # 入力設定の標準化
443        normalized_input_configs: list[MobileMeasurementConfig] = (
444            MobileMeasurementAnalyzer._normalize_configs(configs)
445        )
446        self._configs: list[MobileMeasurementConfig] = normalized_input_configs
447        # 複数ファイルのデータを読み込み結合
448        self.df: pd.DataFrame = self._load_all_combined_data(normalized_input_configs)

測定データ解析クラスを初期化します。

Parameters

center_lat: float
    中心緯度
center_lon: float
    中心経度
configs: list[MobileMeasurementConfig] | list[tuple[float, float, str | Path]]
    入力ファイルのリスト
num_sections: int, optional
    分割する区画数。デフォルト値は4です。
ch4_enhance_threshold: float, optional
    CH4増加の閾値(ppm)。デフォルト値は0.1です。
correlation_threshold: float, optional
    相関係数の閾値。デフォルト値は0.7です。
hotspot_area_meter: float, optional
    ホットスポットの検出に使用するエリアの半径(メートル)。デフォルト値は50メートルです。
hotspot_params: HotspotParams | None, optional
    ホットスポット解析のパラメータ設定。未指定の場合はデフォルト設定を使用します。
window_minutes: float, optional
    移動窓の大きさ(分)。デフォルト値は5分です。
columns_rename_dict: dict[str, str] | None, optional
    元のデータファイルのヘッダーを汎用的な単語に変換するための辞書型データ。未指定の場合は以下のデフォルト値を使用します:


    
{
    "Time Stamp": "timestamp",
    "CH4 (ppm)": "ch4_ppm",
    "C2H6 (ppb)": "c2h6_ppb",
    "H2O (ppm)": "h2o_ppm",
    "Latitude": "latitude",
    "Longitude": "longitude"
}
na_values: list[str] | None, optional NaNと判定する値のパターン。未指定の場合は["No Data", "nan"]を使用します。 logger: Logger | None, optional 使用するロガー。未指定の場合は新しいロガーを作成します。 logging_debug: bool, optional ログレベルを"DEBUG"に設定するかどうか。デフォルト値はFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。

Examples

>>> analyzer = MobileMeasurementAnalyzer(
...     center_lat=35.6895,
...     center_lon=139.6917,
...     configs=[
...         MobileMeasurementConfig(fs=1.0, lag=0.0, path="data1.csv"),
...         MobileMeasurementConfig(fs=1.0, lag=0.0, path="data2.csv")
...     ],
...     num_sections=6,
...     ch4_enhance_threshold=0.2
... )
EARTH_RADIUS_METERS: float = 6371000
logger: logging.Logger
df: pandas.core.frame.DataFrame
hotspot_params: HotspotParams
450    @property
451    def hotspot_params(self) -> HotspotParams:
452        """ホットスポット解析のパラメータ設定を取得"""
453        return self._hotspot_params

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

def analyze_delta_ch4_stats( self, hotspots: list[HotspotData]) -> None:
460    def analyze_delta_ch4_stats(self, hotspots: list[HotspotData]) -> None:
461        """
462        各タイプのホットスポットについてΔCH4の統計情報を計算し、結果を表示します。
463
464        Parameters
465        ----------
466            hotspots: list[HotspotData]
467                分析対象のホットスポットリスト
468        """
469        # タイプごとにホットスポットを分類
470        hotspots_by_type: dict[HotspotType, list[HotspotData]] = {
471            "bio": [h for h in hotspots if h.type == "bio"],
472            "gas": [h for h in hotspots if h.type == "gas"],
473            "comb": [h for h in hotspots if h.type == "comb"],
474        }
475
476        # 統計情報を計算し、表示
477        for spot_type, spots in hotspots_by_type.items():
478            if spots:
479                delta_ch4_values = [spot.delta_ch4 for spot in spots]
480                max_value = max(delta_ch4_values)
481                mean_value = sum(delta_ch4_values) / len(delta_ch4_values)
482                median_value = sorted(delta_ch4_values)[len(delta_ch4_values) // 2]
483                print(f"{spot_type}タイプのホットスポットの統計情報:")
484                print(f"  最大値: {max_value}")
485                print(f"  平均値: {mean_value}")
486                print(f"  中央値: {median_value}")
487            else:
488                print(f"{spot_type}タイプのホットスポットは存在しません。")

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

Parameters

hotspots: list[HotspotData]
    分析対象のホットスポットリスト
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]:
490    def analyze_hotspots(
491        self,
492        duplicate_check_mode: Literal["none", "time_window", "time_all"] = "none",
493        min_time_threshold_seconds: float = 300,
494        max_time_threshold_hours: float = 12,
495    ) -> list[HotspotData]:
496        """
497        ホットスポットを検出して分析します。
498
499        Parameters
500        ----------
501            duplicate_check_mode: Literal["none", "time_window", "time_all"], optional
502                重複チェックのモード。デフォルトは"none"。
503                    - "none": 重複チェックを行わない
504                    - "time_window": 指定された時間窓内の重複のみを除外
505                    - "time_all": すべての時間範囲で重複チェックを行う
506            min_time_threshold_seconds: float, optional
507                重複とみなす最小時間の閾値。デフォルトは300秒。
508            max_time_threshold_hours: float, optional
509                重複チェックを一時的に無視する最大時間の閾値。デフォルトは12時間。
510
511        Returns
512        ----------
513            list[HotspotData]
514                検出されたホットスポットのリスト
515
516        Examples
517        --------
518        >>> analyzer = MobileMeasurementAnalyzer()
519        >>> # 重複チェックなしでホットスポットを検出
520        >>> hotspots = analyzer.analyze_hotspots()
521        >>>
522        >>> # 時間窓内の重複を除外してホットスポットを検出
523        >>> hotspots = analyzer.analyze_hotspots(
524        ...     duplicate_check_mode="time_window",
525        ...     min_time_threshold_seconds=600,
526        ...     max_time_threshold_hours=24
527        ... )
528        """
529        all_hotspots: list[HotspotData] = []
530        df_processed: pd.DataFrame = self.get_preprocessed_data()
531
532        # ホットスポットの検出
533        hotspots: list[HotspotData] = self._detect_hotspots(
534            df=df_processed,
535            ch4_enhance_threshold=self._ch4_enhance_threshold,
536        )
537        all_hotspots.extend(hotspots)
538
539        # 重複チェックモードに応じて処理
540        if duplicate_check_mode != "none":
541            unique_hotspots = MobileMeasurementAnalyzer.remove_hotspots_duplicates(
542                all_hotspots,
543                check_time_all=(duplicate_check_mode == "time_all"),
544                min_time_threshold_seconds=min_time_threshold_seconds,
545                max_time_threshold_hours=max_time_threshold_hours,
546                hotspot_area_meter=self._hotspot_area_meter,
547            )
548            self.logger.info(
549                f"重複除外: {len(all_hotspots)}{len(unique_hotspots)} ホットスポット"
550            )
551            return unique_hotspots
552
553        return all_hotspots

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

Parameters

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

Returns

list[HotspotData]
    検出されたホットスポットのリスト

Examples

>>> analyzer = MobileMeasurementAnalyzer()
>>> # 重複チェックなしでホットスポットを検出
>>> hotspots = analyzer.analyze_hotspots()
>>>
>>> # 時間窓内の重複を除外してホットスポットを検出
>>> hotspots = analyzer.analyze_hotspots(
...     duplicate_check_mode="time_window",
...     min_time_threshold_seconds=600,
...     max_time_threshold_hours=24
... )
def calculate_measurement_stats( self, col_latitude: str = 'latitude', col_longitude: str = 'longitude', print_summary_individual: bool = True, print_summary_total: bool = True) -> tuple[float, datetime.timedelta]:
555    def calculate_measurement_stats(
556        self,
557        col_latitude: str = "latitude",
558        col_longitude: str = "longitude",
559        print_summary_individual: bool = True,
560        print_summary_total: bool = True,
561    ) -> tuple[float, timedelta]:
562        """
563        各ファイルの測定時間と走行距離を計算し、合計を返します。
564
565        Parameters
566        ----------
567            col_latitude: str, optional
568                緯度情報が格納されているカラム名。デフォルト値は"latitude"です。
569            col_longitude: str, optional
570                経度情報が格納されているカラム名。デフォルト値は"longitude"です。
571            print_summary_individual: bool, optional
572                個別ファイルの統計を表示するかどうか。デフォルト値はTrueです。
573            print_summary_total: bool, optional
574                合計統計を表示するかどうか。デフォルト値はTrueです。
575
576        Returns
577        ----------
578            tuple[float, timedelta]
579                総距離(km)と総時間のタプル
580
581        Examples
582        ----------
583        >>> analyzer = MobileMeasurementAnalyzer(config_list)
584        >>> total_distance, total_time = analyzer.calculate_measurement_stats()
585        >>> print(f"総距離: {total_distance:.2f}km")
586        >>> print(f"総時間: {total_time}")
587        """
588        total_distance: float = 0.0
589        total_time: timedelta = timedelta()
590        individual_stats: list[dict] = []
591
592        # 必要な列のみを読み込むように指定
593        columns_to_read = [col_latitude, col_longitude, "timestamp"]
594
595        for config in tqdm(self._configs, desc="Calculating", unit="file"):
596            # 必要な列のみを読み込み、メモリ使用を最適化
597            df = pd.read_csv(config.path, usecols=columns_to_read)
598            df["timestamp"] = pd.to_datetime(df["timestamp"])
599            source_name = self.extract_source_name_from_path(config.path)
600
601            # 時間の計算
602            time_spent = df["timestamp"].max() - df["timestamp"].min()
603
604            # ベクトル化した距離計算
605            lat_shift = df[col_latitude].shift(-1)
606            lon_shift = df[col_longitude].shift(-1)
607
608            # nanを除外して距離計算
609            mask = ~(lat_shift.isna() | lon_shift.isna())
610            if mask.any():
611                # ラジアンに変換(一度に計算)
612                lat1_rad = np.radians(df[col_latitude][mask])
613                lon1_rad = np.radians(df[col_longitude][mask])
614                lat2_rad = np.radians(lat_shift[mask])
615                lon2_rad = np.radians(lon_shift[mask])
616
617                # Haversine formulaをベクトル化
618                dlat = lat2_rad - lat1_rad
619                dlon = lon2_rad - lon1_rad
620
621                # 距離計算を一度に実行
622                a = (
623                    np.sin(dlat / 2) ** 2
624                    + np.cos(lat1_rad) * np.cos(lat2_rad) * np.sin(dlon / 2) ** 2
625                )
626                c = 2 * np.arctan2(np.sqrt(a), np.sqrt(1 - a))
627                distance_km = (self.EARTH_RADIUS_METERS * c).sum() / 1000
628
629                # 合計に加算
630                total_distance += distance_km
631                total_time += time_spent
632
633                # 統計情報を保存
634                if print_summary_individual:
635                    average_speed = distance_km / (time_spent.total_seconds() / 3600)
636                    individual_stats.append(
637                        {
638                            "source": source_name,
639                            "distance": distance_km,
640                            "time": time_spent,
641                            "speed": average_speed,
642                        }
643                    )
644
645        # 統計情報の表示(変更なし)
646        if print_summary_individual:
647            self.logger.info("=== Individual Stats ===")
648            for stat in individual_stats:
649                print(f"File        : {stat['source']}")
650                print(f"  Distance  : {stat['distance']:.2f} km")
651                print(f"  Time      : {stat['time']}")
652                print(f"  Avg. Speed: {stat['speed']:.1f} km/h\n")
653
654        if print_summary_total:
655            average_speed_total = total_distance / (total_time.total_seconds() / 3600)
656            self.logger.info("=== Total Stats ===")
657            print(f"  Distance  : {total_distance:.2f} km")
658            print(f"  Time      : {total_time}")
659            print(f"  Avg. Speed: {average_speed_total:.1f} km/h\n")
660
661        return total_distance, total_time

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

Parameters

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

Returns

tuple[float, timedelta]
    総距離(km)と総時間のタプル

Examples

>>> analyzer = MobileMeasurementAnalyzer(config_list)
>>> total_distance, total_time = analyzer.calculate_measurement_stats()
>>> print(f"総距離: {total_distance:.2f}km")
>>> print(f"総時間: {total_time}")
def create_hotspots_map( self, hotspots: list[HotspotData], output_dirpath: 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:
663    def create_hotspots_map(
664        self,
665        hotspots: list[HotspotData],
666        output_dirpath: str | Path | None = None,
667        output_filename: str = "hotspots_map.html",
668        center_marker_color: str = "green",
669        center_marker_label: str = "Center",
670        plot_center_marker: bool = True,
671        radius_meters: float = 3000,
672        save_fig: bool = True,
673    ) -> None:
674        """
675        ホットスポットの分布を地図上にプロットして保存します。
676
677        Parameters
678        ----------
679            hotspots: list[HotspotData]
680                プロットするホットスポットのリスト
681            output_dirpath: str | Path | None, optional
682                保存先のディレクトリパス。未指定の場合はカレントディレクトリに保存されます。
683            output_filename: str, optional
684                保存するファイル名。デフォルト値は"hotspots_map.html"です。
685            center_marker_color: str, optional
686                中心を示すマーカーの色。デフォルト値は"green"です。
687            center_marker_label: str, optional
688                中心を示すマーカーのラベル。デフォルト値は"Center"です。
689            plot_center_marker: bool, optional
690                中心を示すマーカーを表示するかどうか。デフォルト値はTrueです。
691            radius_meters: float, optional
692                区画分けを示す線の長さ(メートル)。デフォルト値は3000です。
693            save_fig: bool, optional
694                図を保存するかどうか。デフォルト値はTrueです。
695
696        Returns
697        -------
698            None
699
700        Examples
701        --------
702        >>> analyzer = MobileMeasurementAnalyzer(center_lat=35.6895, center_lon=139.6917, configs=[])
703        >>> hotspots = [HotspotData(...)]  # ホットスポットのリスト
704        >>> analyzer.create_hotspots_map(
705        ...     hotspots=hotspots,
706        ...     output_dirpath="results",
707        ...     radius_meters=5000
708        ... )
709        """
710        # 地図の作成
711        m = folium.Map(
712            location=[self._center_lat, self._center_lon],
713            zoom_start=15,
714            tiles="OpenStreetMap",
715        )
716
717        # ホットスポットの種類ごとに異なる色でプロット
718        for spot in hotspots:
719            # NaN値チェックを追加
720            if math.isnan(spot.avg_lat) or math.isnan(spot.avg_lon):
721                continue
722
723            # default type
724            color = "black"
725            # タイプに応じて色を設定
726            if spot.type == "comb":
727                color = "green"
728            elif spot.type == "gas":
729                color = "red"
730            elif spot.type == "bio":
731                color = "blue"
732
733            # CSSのgrid layoutを使用してHTMLタグを含むテキストをフォーマット
734            popup_html = f"""
735            <div style='font-family: Arial; font-size: 12px; display: grid; grid-template-columns: auto auto auto; gap: 5px;'>
736                <b>Date</b> <span>:</span> <span>{spot.timestamp}</span>
737                <b>Lat</b> <span>:</span> <span>{spot.avg_lat:.3f}</span>
738                <b>Lon</b> <span>:</span> <span>{spot.avg_lon:.3f}</span>
739                <b>ΔCH<sub>4</sub></b> <span>:</span> <span>{spot.delta_ch4:.3f}</span>
740                <b>ΔC<sub>2</sub>H<sub>6</sub></b> <span>:</span> <span>{spot.delta_c2h6:.3f}</span>
741                <b>Ratio</b> <span>:</span> <span>{spot.delta_ratio:.3f}</span>
742                <b>Type</b> <span>:</span> <span>{spot.type}</span>
743                <b>Section</b> <span>:</span> <span>{spot.section}</span>
744            </div>
745            """
746
747            # ポップアップのサイズを指定
748            popup = folium.Popup(
749                folium.Html(popup_html, script=True),
750                max_width=200,  # 最大幅(ピクセル)
751            )
752
753            folium.CircleMarker(
754                location=[spot.avg_lat, spot.avg_lon],
755                radius=8,
756                color=color,
757                fill=True,
758                popup=popup,
759            ).add_to(m)
760
761        # 中心点のマーカー
762        if plot_center_marker:
763            folium.Marker(
764                [self._center_lat, self._center_lon],
765                popup=center_marker_label,
766                icon=folium.Icon(color=center_marker_color, icon="info-sign"),
767            ).add_to(m)
768
769        # 区画の境界線を描画
770        for section in range(self._num_sections):
771            start_angle = math.radians(-180 + section * self._section_size)
772
773            const_r = self.EARTH_RADIUS_METERS
774
775            # 境界線の座標を計算
776            lat1 = self._center_lat
777            lon1 = self._center_lon
778            lat2 = math.degrees(
779                math.asin(
780                    math.sin(math.radians(lat1)) * math.cos(radius_meters / const_r)
781                    + math.cos(math.radians(lat1))
782                    * math.sin(radius_meters / const_r)
783                    * math.cos(start_angle)
784                )
785            )
786            lon2 = self._center_lon + math.degrees(
787                math.atan2(
788                    math.sin(start_angle)
789                    * math.sin(radius_meters / const_r)
790                    * math.cos(math.radians(lat1)),
791                    math.cos(radius_meters / const_r)
792                    - math.sin(math.radians(lat1)) * math.sin(math.radians(lat2)),
793                )
794            )
795
796            # 境界線を描画
797            folium.PolyLine(
798                locations=[[lat1, lon1], [lat2, lon2]],
799                color="black",
800                weight=1,
801                opacity=0.5,
802            ).add_to(m)
803
804        # 地図を保存
805        if save_fig and output_dirpath is None:
806            raise ValueError(
807                "save_fig = True の場合、 output_dirpath を指定する必要があります。有効なディレクトリパスを指定してください。"
808            )
809            output_filepath: str = os.path.join(output_dirpath, output_filename)
810            m.save(str(output_filepath))
811            self.logger.info(f"地図を保存しました: {output_filepath}")

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

Parameters

hotspots: list[HotspotData]
    プロットするホットスポットのリスト
output_dirpath: str | Path | None, optional
    保存先のディレクトリパス。未指定の場合はカレントディレクトリに保存されます。
output_filename: str, optional
    保存するファイル名。デフォルト値は"hotspots_map.html"です。
center_marker_color: str, optional
    中心を示すマーカーの色。デフォルト値は"green"です。
center_marker_label: str, optional
    中心を示すマーカーのラベル。デフォルト値は"Center"です。
plot_center_marker: bool, optional
    中心を示すマーカーを表示するかどうか。デフォルト値はTrueです。
radius_meters: float, optional
    区画分けを示す線の長さ(メートル)。デフォルト値は3000です。
save_fig: bool, optional
    図を保存するかどうか。デフォルト値はTrueです。

Returns

None

Examples

>>> analyzer = MobileMeasurementAnalyzer(center_lat=35.6895, center_lon=139.6917, configs=[])
>>> hotspots = [HotspotData(...)]  # ホットスポットのリスト
>>> analyzer.create_hotspots_map(
...     hotspots=hotspots,
...     output_dirpath="results",
...     radius_meters=5000
... )
def export_hotspots_to_csv( self, hotspots: list[HotspotData], output_dirpath: str | pathlib.Path | None = None, output_filename: str = 'hotspots.csv') -> None:
813    def export_hotspots_to_csv(
814        self,
815        hotspots: list[HotspotData],
816        output_dirpath: str | Path | None = None,
817        output_filename: str = "hotspots.csv",
818    ) -> None:
819        """
820        ホットスポットの情報をCSVファイルに出力します。
821
822        Parameters
823        ----------
824            hotspots: list[HotspotData]
825                出力するホットスポットのリスト
826            output_dirpath: str | Path | None, optional
827                出力先ディレクトリのパス。未指定の場合はValueErrorが発生します。
828            output_filename: str, optional
829                出力ファイル名。デフォルト値は"hotspots.csv"です。
830
831        Returns
832        -------
833            None
834                戻り値はありません。
835
836        Examples
837        --------
838        >>> analyzer = MobileMeasurementAnalyzer()
839        >>> hotspots = analyzer.analyze_hotspots()
840        >>> analyzer.export_hotspots_to_csv(
841        ...     hotspots=hotspots,
842        ...     output_dirpath="output",
843        ...     output_filename="hotspots_20240101.csv"
844        ... )
845        """
846        # 日時の昇順でソート
847        sorted_hotspots = sorted(hotspots, key=lambda x: x.timestamp)
848
849        # 出力用のデータを作成
850        records = []
851        for spot in sorted_hotspots:
852            record = {
853                "timestamp": spot.timestamp,
854                "type": spot.type,
855                "delta_ch4": spot.delta_ch4,
856                "delta_c2h6": spot.delta_c2h6,
857                "delta_ratio": spot.delta_ratio,
858                "correlation": spot.correlation,
859                "angle": spot.angle,
860                "section": spot.section,
861                "latitude": spot.avg_lat,
862                "longitude": spot.avg_lon,
863            }
864            records.append(record)
865
866        # DataFrameに変換してCSVに出力
867        if output_dirpath is None:
868            raise ValueError(
869                "output_dirpath が指定されていません。有効なディレクトリパスを指定してください。"
870            )
871        os.makedirs(output_dirpath, exist_ok=True)
872        output_filepath: str = os.path.join(output_dirpath, output_filename)
873        df: pd.DataFrame = pd.DataFrame(records)
874        df.to_csv(output_filepath, index=False)
875        self.logger.info(
876            f"ホットスポット情報をCSVファイルに出力しました: {output_filepath}"
877        )

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

Parameters

hotspots: list[HotspotData]
    出力するホットスポットのリスト
output_dirpath: str | Path | None, optional
    出力先ディレクトリのパス。未指定の場合はValueErrorが発生します。
output_filename: str, optional
    出力ファイル名。デフォルト値は"hotspots.csv"です。

Returns

None
    戻り値はありません。

Examples

>>> analyzer = MobileMeasurementAnalyzer()
>>> hotspots = analyzer.analyze_hotspots()
>>> analyzer.export_hotspots_to_csv(
...     hotspots=hotspots,
...     output_dirpath="output",
...     output_filename="hotspots_20240101.csv"
... )
@staticmethod
def extract_source_name_from_path(path: str | pathlib.Path) -> str:
879    @staticmethod
880    def extract_source_name_from_path(path: str | Path) -> str:
881        """
882        ファイルパスからソース名(拡張子なしのファイル名)を抽出します。
883
884        Parameters
885        ----------
886            path: str | Path
887                ソース名を抽出するファイルパス
888                例: "/path/to/Pico100121_241017_092120+.txt"
889
890        Returns
891        ----------
892            str
893                抽出されたソース名
894                例: "Pico100121_241017_092120+"
895
896        Examples:
897        ----------
898            >>> path = "/path/to/data/Pico100121_241017_092120+.txt"
899            >>> MobileMeasurementAnalyzer.extract_source_from_path(path)
900            'Pico100121_241017_092120+'
901        """
902        # Pathオブジェクトに変換
903        path_obj: Path = Path(path)
904        # stem属性で拡張子なしのファイル名を取得
905        source_name: str = path_obj.stem
906        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"
>>> MobileMeasurementAnalyzer.extract_source_from_path(path)
'Pico100121_241017_092120+'
def get_preprocessed_data(self) -> pandas.core.frame.DataFrame:
908    def get_preprocessed_data(
909        self,
910    ) -> pd.DataFrame:
911        """
912        データ前処理を行い、CH4とC2H6の相関解析に必要な形式に整えます。
913        コンストラクタで読み込んだすべてのデータを前処理し、結合したDataFrameを返します。
914
915        内部で`MobileMeasurementAnalyzer._calculate_hotspots_parameters()`を適用し、
916        `ch4_ppm_mv`などのパラメータが追加されたDataFrameが戻り値として取得できます。
917
918        Returns
919        ----------
920            pd.DataFrame
921                前処理済みの結合されたDataFrame
922        """
923        params: HotspotParams = self._hotspot_params
924        # ホットスポットのパラメータを計算
925        df_processed: pd.DataFrame = (
926            MobileMeasurementAnalyzer._calculate_hotspots_parameters(
927                df=self.df,
928                window_size=self._window_size,
929                col_ch4_ppm=params.col_ch4_ppm,
930                col_c2h6_ppb=params.col_c2h6_ppb,
931                col_h2o_ppm=params.col_h2o_ppm,
932                ch4_ppm_delta_min=params.ch4_ppm_delta_min,
933                ch4_ppm_delta_max=params.ch4_ppm_delta_max,
934                c2h6_ppb_delta_min=params.c2h6_ppb_delta_min,
935                c2h6_ppb_delta_max=params.c2h6_ppb_delta_max,
936                h2o_ppm_threshold=params.h2o_ppm_min,
937                rolling_method=params.rolling_method,
938                quantile_value=params.quantile_value,
939            )
940        )
941        return df_processed

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

内部でMobileMeasurementAnalyzer._calculate_hotspots_parameters()を適用し、 ch4_ppm_mvなどのパラメータが追加されたDataFrameが戻り値として取得できます。

Returns

pd.DataFrame
    前処理済みの結合されたDataFrame
def get_section_size(self) -> float:
943    def get_section_size(self) -> float:
944        """
945        セクションのサイズを取得するメソッド。
946        このメソッドは、解析対象のデータを区画に分割する際の
947        各区画の角度範囲を示すサイズを返します。
948
949        Returns
950        ----------
951            float
952                1セクションのサイズ(度単位)
953        """
954        return self._section_size

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

Returns

float
    1セクションのサイズ(度単位)
def plot_ch4_delta_histogram( self, hotspots: list[HotspotData], output_dirpath: str | pathlib.Path | None, output_filename: str = 'ch4_delta_histogram.png', dpi: float | None = 350, figsize: tuple[float, float] = (8, 6), fontsize: float = 20, hotspot_colors: dict[typing.Literal['bio', 'gas', 'comb', 'scale_check'], str] | None = None, 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:
 956    def plot_ch4_delta_histogram(
 957        self,
 958        hotspots: list[HotspotData],
 959        output_dirpath: str | Path | None,
 960        output_filename: str = "ch4_delta_histogram.png",
 961        dpi: float | None = 350,
 962        figsize: tuple[float, float] = (8, 6),
 963        fontsize: float = 20,
 964        hotspot_colors: dict[HotspotType, str] | None = None,
 965        xlabel: str = "Δ$\\mathregular{CH_{4}}$ (ppm)",
 966        ylabel: str = "Frequency",
 967        xlim: tuple[float, float] | None = None,
 968        ylim: tuple[float, float] | None = None,
 969        save_fig: bool = True,
 970        show_fig: bool = True,
 971        yscale_log: bool = True,
 972        print_bins_analysis: bool = False,
 973    ) -> None:
 974        """
 975        CH4の増加量(ΔCH4)の積み上げヒストグラムをプロットします。
 976
 977        Parameters
 978        ----------
 979            hotspots: list[HotspotData]
 980                プロットするホットスポットのリスト
 981            output_dirpath: str | Path | None
 982                保存先のディレクトリパス
 983            output_filename: str, optional
 984                保存するファイル名。デフォルト値は"ch4_delta_histogram.png"です。
 985            dpi: float | None, optional
 986                解像度。デフォルト値は350です。
 987            figsize: tuple[float, float], optional
 988                図のサイズ。デフォルト値は(8, 6)です。
 989            fontsize: float, optional
 990                フォントサイズ。デフォルト値は20です。
 991            hotspot_colors: dict[HotspotType, str] | None, optional
 992                ホットスポットの色を定義する辞書。未指定の場合は以下のデフォルト値を使用します:
 993                ```python
 994                {"bio": "blue", "gas": "red", "comb": "green"}
 995                ```
 996            xlabel: str, optional
 997                x軸のラベル。デフォルト値は"Δ$\\mathregular{CH_{4}}$ (ppm)"です。
 998            ylabel: str, optional
 999                y軸のラベル。デフォルト値は"Frequency"です。
1000            xlim: tuple[float, float] | None, optional
1001                x軸の範囲。未指定の場合は自動設定されます。
1002            ylim: tuple[float, float] | None, optional
1003                y軸の範囲。未指定の場合は自動設定されます。
1004            save_fig: bool, optional
1005                図の保存を許可するフラグ。デフォルト値はTrueです。
1006            show_fig: bool, optional
1007                図の表示を許可するフラグ。デフォルト値はTrueです。
1008            yscale_log: bool, optional
1009                y軸をlogスケールにするかどうか。デフォルト値はTrueです。
1010            print_bins_analysis: bool, optional
1011                ビンごとの内訳を表示するかどうか。デフォルト値はFalseです。
1012
1013        Returns
1014        -------
1015            None
1016
1017        Examples
1018        --------
1019        >>> analyzer = MobileMeasurementAnalyzer(...)
1020        >>> hotspots = analyzer.detect_hotspots()
1021        >>> analyzer.plot_ch4_delta_histogram(
1022        ...     hotspots=hotspots,
1023        ...     output_dirpath="results",
1024        ...     xlim=(0, 5),
1025        ...     ylim=(0, 100)
1026        ... )
1027        """
1028        if hotspot_colors is None:
1029            hotspot_colors = {
1030                "bio": "blue",
1031                "gas": "red",
1032                "comb": "green",
1033            }
1034        plt.rcParams["font.size"] = fontsize
1035        fig = plt.figure(figsize=figsize, dpi=dpi)
1036
1037        # ホットスポットからデータを抽出
1038        all_ch4_deltas = []
1039        all_types = []
1040        for spot in hotspots:
1041            all_ch4_deltas.append(spot.delta_ch4)
1042            all_types.append(spot.type)
1043
1044        # データをNumPy配列に変換
1045        all_ch4_deltas = np.array(all_ch4_deltas)
1046        all_types = np.array(all_types)
1047
1048        # 0.1刻みのビンを作成
1049        if xlim is not None:
1050            bins = np.arange(xlim[0], xlim[1] + 0.1, 0.1)
1051        else:
1052            max_val = np.ceil(np.max(all_ch4_deltas) * 10) / 10
1053            bins = np.arange(0, max_val + 0.1, 0.1)
1054
1055        # タイプごとのヒストグラムデータを計算
1056        hist_data = {}
1057        # HotspotTypeのリテラル値を使用してイテレーション
1058        for type_name in get_args(HotspotType):  # typing.get_argsをインポート
1059            mask = all_types == type_name
1060            if np.any(mask):
1061                counts, _ = np.histogram(all_ch4_deltas[mask], bins=bins)
1062                hist_data[type_name] = counts
1063
1064        # ビンごとの内訳を表示
1065        if print_bins_analysis:
1066            self.logger.info("各ビンの内訳:")
1067            print(f"{'Bin Range':15} {'bio':>8} {'gas':>8} {'comb':>8} {'Total':>8}")
1068            print("-" * 50)
1069
1070            for i in range(len(bins) - 1):
1071                bin_start = bins[i]
1072                bin_end = bins[i + 1]
1073                bio_count = hist_data.get("bio", np.zeros(len(bins) - 1))[i]
1074                gas_count = hist_data.get("gas", np.zeros(len(bins) - 1))[i]
1075                comb_count = hist_data.get("comb", np.zeros(len(bins) - 1))[i]
1076                total = bio_count + gas_count + comb_count
1077
1078                if total > 0:  # 合計が0のビンは表示しない
1079                    print(
1080                        f"{bin_start:4.1f}-{bin_end:<8.1f}"
1081                        f"{int(bio_count):8d}"
1082                        f"{int(gas_count):8d}"
1083                        f"{int(comb_count):8d}"
1084                        f"{int(total):8d}"
1085                    )
1086
1087        # 積み上げヒストグラムを作成
1088        bottom = np.zeros_like(hist_data.get("bio", np.zeros(len(bins) - 1)))
1089
1090        # HotspotTypeのリテラル値を使用してイテレーション
1091        for type_name in get_args(HotspotType):
1092            if type_name in hist_data:
1093                plt.bar(
1094                    bins[:-1],
1095                    hist_data[type_name],
1096                    width=np.diff(bins)[0],
1097                    bottom=bottom,
1098                    color=hotspot_colors[type_name],
1099                    label=type_name,
1100                    alpha=0.6,
1101                    align="edge",
1102                )
1103                bottom += hist_data[type_name]
1104
1105        if yscale_log:
1106            plt.yscale("log")
1107        plt.xlabel(xlabel)
1108        plt.ylabel(ylabel)
1109        plt.legend()
1110        plt.grid(True, which="both", ls="-", alpha=0.2)
1111
1112        # 軸の範囲を設定
1113        if xlim is not None:
1114            plt.xlim(xlim)
1115        if ylim is not None:
1116            plt.ylim(ylim)
1117
1118        # グラフの保存または表示
1119        if save_fig:
1120            if output_dirpath is None:
1121                raise ValueError(
1122                    "save_fig = True の場合、 output_dirpath を指定する必要があります。有効なディレクトリパスを指定してください。"
1123                )
1124            os.makedirs(output_dirpath, exist_ok=True)
1125            output_filepath: str = os.path.join(output_dirpath, output_filename)
1126            plt.savefig(output_filepath, bbox_inches="tight")
1127        if show_fig:
1128            plt.show()
1129        plt.close(fig=fig)

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

Parameters

hotspots: list[HotspotData]
    プロットするホットスポットのリスト
output_dirpath: str | Path | None
    保存先のディレクトリパス
output_filename: str, optional
    保存するファイル名。デフォルト値は"ch4_delta_histogram.png"です。
dpi: float | None, optional
    解像度。デフォルト値は350です。
figsize: tuple[float, float], optional
    図のサイズ。デフォルト値は(8, 6)です。
fontsize: float, optional
    フォントサイズ。デフォルト値は20です。
hotspot_colors: dict[HotspotType, str] | None, optional
    ホットスポットの色を定義する辞書。未指定の場合は以下のデフォルト値を使用します:


    
{"bio": "blue", "gas": "red", "comb": "green"}
xlabel: str, optional x軸のラベル。デフォルト値は"Δ$\mathregular{CH_{4}}$ (ppm)"です。 ylabel: str, optional y軸のラベル。デフォルト値は"Frequency"です。 xlim: tuple[float, float] | None, optional x軸の範囲。未指定の場合は自動設定されます。 ylim: tuple[float, float] | None, optional y軸の範囲。未指定の場合は自動設定されます。 save_fig: bool, optional 図の保存を許可するフラグ。デフォルト値はTrueです。 show_fig: bool, optional 図の表示を許可するフラグ。デフォルト値はTrueです。 yscale_log: bool, optional y軸をlogスケールにするかどうか。デフォルト値はTrueです。 print_bins_analysis: bool, optional ビンごとの内訳を表示するかどうか。デフォルト値はFalseです。

Returns

None

Examples

>>> analyzer = MobileMeasurementAnalyzer(...)
>>> hotspots = analyzer.detect_hotspots()
>>> analyzer.plot_ch4_delta_histogram(
...     hotspots=hotspots,
...     output_dirpath="results",
...     xlim=(0, 5),
...     ylim=(0, 100)
... )
def plot_mapbox( self, df: pandas.core.frame.DataFrame, col_conc: str, mapbox_access_token: str, sort_conc_column: bool = True, output_dirpath: 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:
1131    def plot_mapbox(
1132        self,
1133        df: pd.DataFrame,
1134        col_conc: str,
1135        mapbox_access_token: str,
1136        sort_conc_column: bool = True,
1137        output_dirpath: str | Path | None = None,
1138        output_filename: str = "mapbox_plot.html",
1139        col_lat: str = "latitude",
1140        col_lon: str = "longitude",
1141        colorscale: str = "Jet",
1142        center_lat: float | None = None,
1143        center_lon: float | None = None,
1144        zoom: float = 12,
1145        width: int = 700,
1146        height: int = 700,
1147        tick_font_family: str = "Arial",
1148        title_font_family: str = "Arial",
1149        tick_font_size: int = 12,
1150        title_font_size: int = 14,
1151        marker_size: int = 4,
1152        colorbar_title: str | None = None,
1153        value_range: tuple[float, float] | None = None,
1154        save_fig: bool = True,
1155        show_fig: bool = True,
1156    ) -> None:
1157        """
1158        Mapbox上にデータをプロットします。
1159
1160        Parameters
1161        ----------
1162            df: pd.DataFrame
1163                プロットするデータを含むDataFrame
1164            col_conc: str
1165                カラーマッピングに使用する列名
1166            mapbox_access_token: str
1167                Mapboxのアクセストークン
1168            sort_conc_column: bool, optional
1169                濃度列をソートするかどうか。デフォルトはTrue
1170            output_dirpath: str | Path | None, optional
1171                出力ディレクトリのパス。デフォルトはNone
1172            output_filename: str, optional
1173                出力ファイル名。デフォルトは"mapbox_plot.html"
1174            col_lat: str, optional
1175                緯度の列名。デフォルトは"latitude"
1176            col_lon: str, optional
1177                経度の列名。デフォルトは"longitude"
1178            colorscale: str, optional
1179                使用するカラースケール。デフォルトは"Jet"
1180            center_lat: float | None, optional
1181                中心緯度。デフォルトはNoneで、self._center_latを使用
1182            center_lon: float | None, optional
1183                中心経度。デフォルトはNoneで、self._center_lonを使用
1184            zoom: float, optional
1185                マップの初期ズームレベル。デフォルトは12
1186            width: int, optional
1187                プロットの幅(ピクセル)。デフォルトは700
1188            height: int, optional
1189                プロットの高さ(ピクセル)。デフォルトは700
1190            tick_font_family: str, optional
1191                カラーバーの目盛りフォントファミリー。デフォルトは"Arial"
1192            title_font_family: str, optional
1193                カラーバーのタイトルフォントファミリー。デフォルトは"Arial"
1194            tick_font_size: int, optional
1195                カラーバーの目盛りフォントサイズ。デフォルトは12
1196            title_font_size: int, optional
1197                カラーバーのタイトルフォントサイズ。デフォルトは14
1198            marker_size: int, optional
1199                マーカーのサイズ。デフォルトは4
1200            colorbar_title: str | None, optional
1201                カラーバーのタイトル。デフォルトはNoneでcol_concを使用
1202            value_range: tuple[float, float] | None, optional
1203                カラーマッピングの範囲。デフォルトはNoneでデータの最小値と最大値を使用
1204            save_fig: bool, optional
1205                図を保存するかどうか。デフォルトはTrue
1206            show_fig: bool, optional
1207                図を表示するかどうか。デフォルトはTrue
1208
1209        Returns
1210        -------
1211            None
1212
1213        Examples
1214        --------
1215        >>> analyzer = MobileMeasurementAnalyzer()
1216        >>> df = pd.DataFrame({
1217        ...     'latitude': [35.681236, 35.681237],
1218        ...     'longitude': [139.767125, 139.767126],
1219        ...     'concentration': [1.2, 1.5]
1220        ... })
1221        >>> analyzer.plot_mapbox(
1222        ...     df=df,
1223        ...     col_conc='concentration',
1224        ...     mapbox_access_token='your_token_here'
1225        ... )
1226        """
1227        df_mapping: pd.DataFrame = df.copy().dropna(subset=[col_conc])
1228        if sort_conc_column:
1229            df_mapping = df_mapping.sort_values(col_conc)
1230        # 中心座標の設定
1231        center_lat = center_lat if center_lat is not None else self._center_lat
1232        center_lon = center_lon if center_lon is not None else self._center_lon
1233
1234        # カラーマッピングの範囲を設定
1235        cmin, cmax = 0, 0
1236        if value_range is None:
1237            cmin = df_mapping[col_conc].min()
1238            cmax = df_mapping[col_conc].max()
1239        else:
1240            cmin, cmax = value_range
1241
1242        # カラーバーのタイトルを設定
1243        title_text = colorbar_title if colorbar_title is not None else col_conc
1244
1245        # Scattermapboxのデータを作成
1246        scatter_data = go.Scattermapbox(
1247            lat=df_mapping[col_lat],
1248            lon=df_mapping[col_lon],
1249            text=df_mapping[col_conc].astype(str),
1250            hoverinfo="text",
1251            mode="markers",
1252            marker={
1253                "color": df_mapping[col_conc],
1254                "size": marker_size,
1255                "reversescale": False,
1256                "autocolorscale": False,
1257                "colorscale": colorscale,
1258                "cmin": cmin,
1259                "cmax": cmax,
1260                "colorbar": {
1261                    "tickformat": "3.2f",
1262                    "outlinecolor": "black",
1263                    "outlinewidth": 1.5,
1264                    "ticks": "outside",
1265                    "ticklen": 7,
1266                    "tickwidth": 1.5,
1267                    "tickcolor": "black",
1268                    "tickfont": {
1269                        "family": tick_font_family,
1270                        "color": "black",
1271                        "size": tick_font_size,
1272                    },
1273                    "title": {
1274                        "text": title_text,
1275                        "side": "top",
1276                    },  # カラーバーのタイトルを設定
1277                    "titlefont": {
1278                        "family": title_font_family,
1279                        "color": "black",
1280                        "size": title_font_size,
1281                    },
1282                },
1283            },
1284        )
1285
1286        # レイアウトの設定
1287        layout = go.Layout(
1288            width=width,
1289            height=height,
1290            showlegend=False,
1291            mapbox={
1292                "accesstoken": mapbox_access_token,
1293                "center": {"lat": center_lat, "lon": center_lon},
1294                "zoom": zoom,
1295            },
1296        )
1297
1298        # 図の作成
1299        fig = go.Figure(data=[scatter_data], layout=layout)
1300
1301        # 図の保存
1302        if save_fig:
1303            # 保存時の出力ディレクトリチェック
1304            if output_dirpath is None:
1305                raise ValueError(
1306                    "save_fig=Trueの場合、output_dirpathを指定する必要があります。"
1307                )
1308            os.makedirs(output_dirpath, exist_ok=True)
1309            output_filepath = os.path.join(output_dirpath, output_filename)
1310            pyo.plot(fig, filename=output_filepath, auto_open=False)
1311            self.logger.info(f"Mapboxプロットを保存しました: {output_filepath}")
1312        # 図の表示
1313        if show_fig:
1314            pyo.iplot(fig)

Mapbox上にデータをプロットします。

Parameters

df: pd.DataFrame
    プロットするデータを含むDataFrame
col_conc: str
    カラーマッピングに使用する列名
mapbox_access_token: str
    Mapboxのアクセストークン
sort_conc_column: bool, optional
    濃度列をソートするかどうか。デフォルトはTrue
output_dirpath: str | Path | None, optional
    出力ディレクトリのパス。デフォルトはNone
output_filename: str, optional
    出力ファイル名。デフォルトは"mapbox_plot.html"
col_lat: str, optional
    緯度の列名。デフォルトは"latitude"
col_lon: str, optional
    経度の列名。デフォルトは"longitude"
colorscale: str, optional
    使用するカラースケール。デフォルトは"Jet"
center_lat: float | None, optional
    中心緯度。デフォルトはNoneで、self._center_latを使用
center_lon: float | None, optional
    中心経度。デフォルトはNoneで、self._center_lonを使用
zoom: float, optional
    マップの初期ズームレベル。デフォルトは12
width: int, optional
    プロットの幅(ピクセル)。デフォルトは700
height: int, optional
    プロットの高さ(ピクセル)。デフォルトは700
tick_font_family: str, optional
    カラーバーの目盛りフォントファミリー。デフォルトは"Arial"
title_font_family: str, optional
    カラーバーのタイトルフォントファミリー。デフォルトは"Arial"
tick_font_size: int, optional
    カラーバーの目盛りフォントサイズ。デフォルトは12
title_font_size: int, optional
    カラーバーのタイトルフォントサイズ。デフォルトは14
marker_size: int, optional
    マーカーのサイズ。デフォルトは4
colorbar_title: str | None, optional
    カラーバーのタイトル。デフォルトはNoneでcol_concを使用
value_range: tuple[float, float] | None, optional
    カラーマッピングの範囲。デフォルトはNoneでデータの最小値と最大値を使用
save_fig: bool, optional
    図を保存するかどうか。デフォルトはTrue
show_fig: bool, optional
    図を表示するかどうか。デフォルトはTrue

Returns

None

Examples

>>> analyzer = MobileMeasurementAnalyzer()
>>> df = pd.DataFrame({
...     'latitude': [35.681236, 35.681237],
...     'longitude': [139.767125, 139.767126],
...     'concentration': [1.2, 1.5]
... })
>>> analyzer.plot_mapbox(
...     df=df,
...     col_conc='concentration',
...     mapbox_access_token='your_token_here'
... )
def plot_scatter_c2c1( self, hotspots: list[HotspotData], output_dirpath: str | pathlib.Path | None = None, output_filename: str = 'scatter_c2c1.png', figsize: tuple[float, float] = (4, 4), dpi: float | None = 350, hotspot_colors: dict[typing.Literal['bio', 'gas', 'comb', 'scale_check'], str] | None = None, hotspot_labels: dict[typing.Literal['bio', 'gas', 'comb', 'scale_check'], str] | None = None, 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)', xscale_log: bool = False, yscale_log: bool = False, add_legend: bool = True, save_fig: bool = True, show_fig: bool = True, add_ratio_labels: bool = True, ratio_labels: dict[float, tuple[float, float, str]] | None = None) -> None:
1316    def plot_scatter_c2c1(
1317        self,
1318        hotspots: list[HotspotData],
1319        output_dirpath: str | Path | None = None,
1320        output_filename: str = "scatter_c2c1.png",
1321        figsize: tuple[float, float] = (4, 4),
1322        dpi: float | None = 350,
1323        hotspot_colors: dict[HotspotType, str] | None = None,
1324        hotspot_labels: dict[HotspotType, str] | None = None,
1325        fontsize: float = 12,
1326        xlim: tuple[float, float] = (0, 2.0),
1327        ylim: tuple[float, float] = (0, 50),
1328        xlabel: str = "Δ$\\mathregular{CH_{4}}$ (ppm)",
1329        ylabel: str = "Δ$\\mathregular{C_{2}H_{6}}$ (ppb)",
1330        xscale_log: bool = False,
1331        yscale_log: bool = False,
1332        add_legend: bool = True,
1333        save_fig: bool = True,
1334        show_fig: bool = True,
1335        add_ratio_labels: bool = True,
1336        ratio_labels: dict[float, tuple[float, float, str]] | None = None,
1337    ) -> None:
1338        """
1339        検出されたホットスポットのΔC2H6とΔCH4の散布図をプロットします。
1340
1341        Parameters
1342        ----------
1343            hotspots: list[HotspotData]
1344                プロットするホットスポットのリスト
1345            output_dirpath: str | Path | None, optional
1346                保存先のディレクトリパス。未指定の場合はNoneとなります。
1347            output_filename: str, optional
1348                保存するファイル名。デフォルト値は"scatter_c2c1.png"です。
1349            figsize: tuple[float, float], optional
1350                図のサイズ。デフォルト値は(4, 4)です。
1351            dpi: float | None, optional
1352                解像度。デフォルト値は350です。
1353            hotspot_colors: dict[HotspotType, str] | None, optional
1354                ホットスポットの色を定義する辞書。未指定の場合は{"bio": "blue", "gas": "red", "comb": "green"}が使用されます。
1355            hotspot_labels: dict[HotspotType, str] | None, optional
1356                ホットスポットのラベルを定義する辞書。未指定の場合は{"bio": "bio", "gas": "gas", "comb": "comb"}が使用されます。
1357            fontsize: float, optional
1358                フォントサイズ。デフォルト値は12です。
1359            xlim: tuple[float, float], optional
1360                x軸の範囲。デフォルト値は(0, 2.0)です。
1361            ylim: tuple[float, float], optional
1362                y軸の範囲。デフォルト値は(0, 50)です。
1363            xlabel: str, optional
1364                x軸のラベル。デフォルト値は"Δ$\\mathregular{CH_{4}}$ (ppm)"です。
1365            ylabel: str, optional
1366                y軸のラベル。デフォルト値は"Δ$\\mathregular{C_{2}H_{6}}$ (ppb)"です。
1367            xscale_log: bool, optional
1368                x軸を対数スケールにするかどうか。デフォルト値はFalseです。
1369            yscale_log: bool, optional
1370                y軸を対数スケールにするかどうか。デフォルト値はFalseです。
1371            add_legend: bool, optional
1372                凡例を追加するかどうか。デフォルト値はTrueです。
1373            save_fig: bool, optional
1374                図の保存を許可するフラグ。デフォルト値はTrueです。
1375            show_fig: bool, optional
1376                図の表示を許可するフラグ。デフォルト値はTrueです。
1377            add_ratio_labels: bool, optional
1378                比率線を表示するかどうか。デフォルト値はTrueです。
1379            ratio_labels: dict[float, tuple[float, float, str]] | None, optional
1380                比率線とラベルの設定。キーは比率値、値は(x位置, y位置, ラベルテキスト)のタプル。
1381                未指定の場合は以下のデフォルト値が使用されます:
1382                ```python
1383                {
1384                    0.001: (1.25, 2, "0.001"),
1385                    0.005: (1.25, 8, "0.005"),
1386                    0.010: (1.25, 15, "0.01"),
1387                    0.020: (1.25, 30, "0.02"),
1388                    0.030: (1.0, 40, "0.03"),
1389                    0.076: (0.20, 42, "0.076 (Osaka)")
1390                }
1391                ```
1392
1393        Returns
1394        -------
1395            None
1396
1397        Examples
1398        --------
1399        >>> analyzer = MobileMeasurementAnalyzer()
1400        >>> hotspots = analyzer.analyze_hotspots()
1401        >>> analyzer.plot_scatter_c2c1(
1402        ...     hotspots=hotspots,
1403        ...     output_dirpath="output",
1404        ...     xlim=(0, 5),
1405        ...     ylim=(0, 100)
1406        ... )
1407        """
1408        # デフォルト値の設定
1409        if hotspot_colors is None:
1410            hotspot_colors = {
1411                "bio": "blue",
1412                "gas": "red",
1413                "comb": "green",
1414            }
1415        if hotspot_labels is None:
1416            hotspot_labels = {
1417                "bio": "bio",
1418                "gas": "gas",
1419                "comb": "comb",
1420            }
1421        if ratio_labels is None:
1422            ratio_labels = {
1423                0.001: (1.25, 2, "0.001"),
1424                0.005: (1.25, 8, "0.005"),
1425                0.010: (1.25, 15, "0.01"),
1426                0.020: (1.25, 30, "0.02"),
1427                0.030: (1.0, 40, "0.03"),
1428                0.076: (0.20, 42, "0.076 (Osaka)"),
1429            }
1430        plt.rcParams["font.size"] = fontsize
1431        fig = plt.figure(figsize=figsize, dpi=dpi)
1432
1433        # タイプごとのデータを収集
1434        type_data: dict[HotspotType, list[tuple[float, float]]] = {
1435            "bio": [],
1436            "gas": [],
1437            "comb": [],
1438        }
1439        for spot in hotspots:
1440            type_data[spot.type].append((spot.delta_ch4, spot.delta_c2h6))
1441
1442        # タイプごとにプロット(データが存在する場合のみ)
1443        for spot_type, data in type_data.items():
1444            if data:  # データが存在する場合のみプロット
1445                ch4_values, c2h6_values = zip(*data, strict=True)
1446                plt.plot(
1447                    ch4_values,
1448                    c2h6_values,
1449                    "o",
1450                    c=hotspot_colors[spot_type],
1451                    alpha=0.5,
1452                    ms=2,
1453                    label=hotspot_labels[spot_type],
1454                )
1455
1456        # プロット後、軸の設定前に比率の線を追加
1457        x = np.array([0, 5])
1458        base_ch4 = 0.0
1459        base = 0.0
1460
1461        # 各比率に対して線を引く
1462        if ratio_labels is not None:
1463            if not add_ratio_labels:
1464                raise ValueError(
1465                    "ratio_labels に基づいて比率線を描画する場合は、 add_ratio_labels = True を指定してください。"
1466                )
1467            for ratio, (x_pos, y_pos, label) in ratio_labels.items():
1468                y = (x - base_ch4) * 1000 * ratio + base
1469                plt.plot(x, y, "-", c="black", alpha=0.5)
1470                plt.text(x_pos, y_pos, label)
1471
1472        # 軸の設定
1473        if xscale_log:
1474            plt.xscale("log")
1475        if yscale_log:
1476            plt.yscale("log")
1477
1478        plt.xlim(xlim)
1479        plt.ylim(ylim)
1480        plt.xlabel(xlabel)
1481        plt.ylabel(ylabel)
1482        if add_legend:
1483            plt.legend()
1484
1485        # グラフの保存または表示
1486        if save_fig:
1487            if output_dirpath is None:
1488                raise ValueError(
1489                    "save_fig = True の場合、 output_dirpath を指定する必要があります。有効なディレクトリパスを指定してください。"
1490                )
1491            output_filepath: str = os.path.join(output_dirpath, output_filename)
1492            plt.savefig(output_filepath, bbox_inches="tight")
1493            self.logger.info(f"散布図を保存しました: {output_filepath}")
1494        if show_fig:
1495            plt.show()
1496        plt.close(fig=fig)

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

Parameters

hotspots: list[HotspotData]
    プロットするホットスポットのリスト
output_dirpath: str | Path | None, optional
    保存先のディレクトリパス。未指定の場合はNoneとなります。
output_filename: str, optional
    保存するファイル名。デフォルト値は"scatter_c2c1.png"です。
figsize: tuple[float, float], optional
    図のサイズ。デフォルト値は(4, 4)です。
dpi: float | None, optional
    解像度。デフォルト値は350です。
hotspot_colors: dict[HotspotType, str] | None, optional
    ホットスポットの色を定義する辞書。未指定の場合は{"bio": "blue", "gas": "red", "comb": "green"}が使用されます。
hotspot_labels: dict[HotspotType, str] | None, optional
    ホットスポットのラベルを定義する辞書。未指定の場合は{"bio": "bio", "gas": "gas", "comb": "comb"}が使用されます。
fontsize: float, optional
    フォントサイズ。デフォルト値は12です。
xlim: tuple[float, float], optional
    x軸の範囲。デフォルト値は(0, 2.0)です。
ylim: tuple[float, float], optional
    y軸の範囲。デフォルト値は(0, 50)です。
xlabel: str, optional
    x軸のラベル。デフォルト値は"Δ$\mathregular{CH_{4}}$ (ppm)"です。
ylabel: str, optional
    y軸のラベル。デフォルト値は"Δ$\mathregular{C_{2}H_{6}}$ (ppb)"です。
xscale_log: bool, optional
    x軸を対数スケールにするかどうか。デフォルト値はFalseです。
yscale_log: bool, optional
    y軸を対数スケールにするかどうか。デフォルト値はFalseです。
add_legend: bool, optional
    凡例を追加するかどうか。デフォルト値はTrueです。
save_fig: bool, optional
    図の保存を許可するフラグ。デフォルト値はTrueです。
show_fig: bool, optional
    図の表示を許可するフラグ。デフォルト値はTrueです。
add_ratio_labels: bool, optional
    比率線を表示するかどうか。デフォルト値はTrueです。
ratio_labels: dict[float, tuple[float, float, str]] | None, optional
    比率線とラベルの設定。キーは比率値、値は(x位置, y位置, ラベルテキスト)のタプル。
    未指定の場合は以下のデフォルト値が使用されます:


    
{
    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)")
}

Returns

None

Examples

>>> analyzer = MobileMeasurementAnalyzer()
>>> hotspots = analyzer.analyze_hotspots()
>>> analyzer.plot_scatter_c2c1(
...     hotspots=hotspots,
...     output_dirpath="output",
...     xlim=(0, 5),
...     ylim=(0, 100)
... )
def plot_conc_timeseries( self, output_dirpath: str | pathlib.Path | None = None, output_filename: str = 'timeseries.png', figsize: tuple[float, float] = (8, 4), dpi: float | None = 350, 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, yscale_log_ch4: bool = False, yscale_log_c2h6: bool = False, yscale_log_h2o: bool = False, font_size: float = 12, label_pad: float = 10, line_color: str = 'black') -> None:
1498    def plot_conc_timeseries(
1499        self,
1500        output_dirpath: str | Path | None = None,
1501        output_filename: str = "timeseries.png",
1502        figsize: tuple[float, float] = (8, 4),
1503        dpi: float | None = 350,
1504        save_fig: bool = True,
1505        show_fig: bool = True,
1506        col_ch4: str = "ch4_ppm",
1507        col_c2h6: str = "c2h6_ppb",
1508        col_h2o: str = "h2o_ppm",
1509        ylim_ch4: tuple[float, float] | None = None,
1510        ylim_c2h6: tuple[float, float] | None = None,
1511        ylim_h2o: tuple[float, float] | None = None,
1512        yscale_log_ch4: bool = False,
1513        yscale_log_c2h6: bool = False,
1514        yscale_log_h2o: bool = False,
1515        font_size: float = 12,
1516        label_pad: float = 10,
1517        line_color: str = "black",
1518    ) -> None:
1519        """
1520        CH4、C2H6、H2Oの時系列データをプロットします。
1521
1522        Parameters
1523        ----------
1524            output_dirpath: str | Path | None, optional
1525                保存先のディレクトリを指定します。save_fig=Trueの場合は必須です。デフォルト値はNoneです。
1526            output_filename: str, optional
1527                保存するファイル名を指定します。デフォルト値は"timeseries.png"です。
1528            figsize: tuple[float, float], optional
1529                図のサイズを指定します。デフォルト値は(8, 4)です。
1530            dpi: float | None, optional
1531                図の解像度を指定します。デフォルト値は350です。
1532            save_fig: bool, optional
1533                図を保存するかどうかを指定します。デフォルト値はTrueです。
1534            show_fig: bool, optional
1535                図を表示するかどうかを指定します。デフォルト値はTrueです。
1536            col_ch4: str, optional
1537                CH4データの列名を指定します。デフォルト値は"ch4_ppm"です。
1538            col_c2h6: str, optional
1539                C2H6データの列名を指定します。デフォルト値は"c2h6_ppb"です。
1540            col_h2o: str, optional
1541                H2Oデータの列名を指定します。デフォルト値は"h2o_ppm"です。
1542            ylim_ch4: tuple[float, float] | None, optional
1543                CH4プロットのy軸範囲を指定します。デフォルト値はNoneです。
1544            ylim_c2h6: tuple[float, float] | None, optional
1545                C2H6プロットのy軸範囲を指定します。デフォルト値はNoneです。
1546            ylim_h2o: tuple[float, float] | None, optional
1547                H2Oプロットのy軸範囲を指定します。デフォルト値はNoneです。
1548            yscale_log_ch4: bool, optional
1549                CH4データのy軸を対数スケールで表示するかどうかを指定します。デフォルト値はFalseです。
1550            yscale_log_c2h6: bool, optional
1551                C2H6データのy軸を対数スケールで表示するかどうかを指定します。デフォルト値はFalseです。
1552            yscale_log_h2o: bool, optional
1553                H2Oデータのy軸を対数スケールで表示するかどうかを指定します。デフォルト値はFalseです。
1554            font_size: float, optional
1555                プロット全体のフォントサイズを指定します。デフォルト値は12です。
1556            label_pad: float, optional
1557                y軸ラベルとプロットの間隔を指定します。デフォルト値は10です。
1558            line_color: str, optional
1559                プロットする線の色を指定します。デフォルト値は"black"です。
1560
1561        Returns
1562        -------
1563            None
1564
1565        Examples
1566        --------
1567        >>> analyzer = MobileMeasurementAnalyzer(df)
1568        >>> analyzer.plot_conc_timeseries(
1569        ...     output_dirpath="output",
1570        ...     ylim_ch4=(1.8, 2.5),
1571        ...     ylim_c2h6=(0, 100),
1572        ...     ylim_h2o=(0, 20000)
1573        ... )
1574        """
1575        # プロットパラメータの設定
1576        plt.rcParams.update(
1577            {
1578                "font.size": font_size,
1579                "axes.labelsize": font_size,
1580                "axes.titlesize": font_size,
1581                "xtick.labelsize": font_size,
1582                "ytick.labelsize": font_size,
1583            }
1584        )
1585        # timestampをインデックスとして設定
1586        df_internal = self.df.copy()
1587        df_internal.set_index("timestamp", inplace=True)
1588
1589        # プロットの作成
1590        fig = plt.figure(figsize=figsize, dpi=dpi)
1591
1592        # CH4プロット
1593        ax1 = fig.add_subplot(3, 1, 1)
1594        ax1.plot(df_internal.index, df_internal[col_ch4], c=line_color)
1595        if ylim_ch4:
1596            ax1.set_ylim(ylim_ch4)
1597        if yscale_log_ch4:
1598            ax1.set_yscale("log")
1599        ax1.set_ylabel("$\\mathregular{CH_{4}}$ (ppm)", labelpad=label_pad)
1600        ax1.grid(True, alpha=0.3)
1601
1602        # C2H6プロット
1603        ax2 = fig.add_subplot(3, 1, 2)
1604        ax2.plot(df_internal.index, df_internal[col_c2h6], c=line_color)
1605        if ylim_c2h6:
1606            ax2.set_ylim(ylim_c2h6)
1607        if yscale_log_c2h6:
1608            ax2.set_yscale("log")
1609        ax2.set_ylabel("$\\mathregular{C_{2}H_{6}}$ (ppb)", labelpad=label_pad)
1610        ax2.grid(True, alpha=0.3)
1611
1612        # H2Oプロット
1613        ax3 = fig.add_subplot(3, 1, 3)
1614        ax3.plot(df_internal.index, df_internal[col_h2o], c=line_color)
1615        if ylim_h2o:
1616            ax3.set_ylim(ylim_h2o)
1617        if yscale_log_h2o:
1618            ax3.set_yscale("log")
1619        ax3.set_ylabel("$\\mathregular{H_{2}O}$ (ppm)", labelpad=label_pad)
1620        ax3.grid(True, alpha=0.3)
1621
1622        # x軸のフォーマット調整
1623        for ax in [ax1, ax2, ax3]:
1624            ax.xaxis.set_major_formatter(mdates.DateFormatter("%H:%M"))
1625            # 軸のラベルとグリッド線の調整
1626            ax.tick_params(axis="both", which="major", labelsize=font_size)
1627            ax.grid(True, alpha=0.3)
1628
1629        # サブプロット間の間隔調整
1630        plt.subplots_adjust(wspace=0.38, hspace=0.38)
1631
1632        # 図の保存
1633        if save_fig:
1634            if output_dirpath is None:
1635                raise ValueError(
1636                    "save_fig = True の場合、 output_dirpath を指定する必要があります。有効なディレクトリパスを指定してください。"
1637                )
1638            os.makedirs(output_dirpath, exist_ok=True)
1639            output_filepath = os.path.join(output_dirpath, output_filename)
1640            plt.savefig(output_filepath, bbox_inches="tight")
1641
1642        if show_fig:
1643            plt.show()
1644        plt.close(fig=fig)

CH4、C2H6、H2Oの時系列データをプロットします。

Parameters

output_dirpath: str | Path | None, optional
    保存先のディレクトリを指定します。save_fig=Trueの場合は必須です。デフォルト値はNoneです。
output_filename: str, optional
    保存するファイル名を指定します。デフォルト値は"timeseries.png"です。
figsize: tuple[float, float], optional
    図のサイズを指定します。デフォルト値は(8, 4)です。
dpi: float | None, optional
    図の解像度を指定します。デフォルト値は350です。
save_fig: bool, optional
    図を保存するかどうかを指定します。デフォルト値はTrueです。
show_fig: bool, optional
    図を表示するかどうかを指定します。デフォルト値はTrueです。
col_ch4: str, optional
    CH4データの列名を指定します。デフォルト値は"ch4_ppm"です。
col_c2h6: str, optional
    C2H6データの列名を指定します。デフォルト値は"c2h6_ppb"です。
col_h2o: str, optional
    H2Oデータの列名を指定します。デフォルト値は"h2o_ppm"です。
ylim_ch4: tuple[float, float] | None, optional
    CH4プロットのy軸範囲を指定します。デフォルト値はNoneです。
ylim_c2h6: tuple[float, float] | None, optional
    C2H6プロットのy軸範囲を指定します。デフォルト値はNoneです。
ylim_h2o: tuple[float, float] | None, optional
    H2Oプロットのy軸範囲を指定します。デフォルト値はNoneです。
yscale_log_ch4: bool, optional
    CH4データのy軸を対数スケールで表示するかどうかを指定します。デフォルト値はFalseです。
yscale_log_c2h6: bool, optional
    C2H6データのy軸を対数スケールで表示するかどうかを指定します。デフォルト値はFalseです。
yscale_log_h2o: bool, optional
    H2Oデータのy軸を対数スケールで表示するかどうかを指定します。デフォルト値はFalseです。
font_size: float, optional
    プロット全体のフォントサイズを指定します。デフォルト値は12です。
label_pad: float, optional
    y軸ラベルとプロットの間隔を指定します。デフォルト値は10です。
line_color: str, optional
    プロットする線の色を指定します。デフォルト値は"black"です。

Returns

None

Examples

>>> analyzer = MobileMeasurementAnalyzer(df)
>>> analyzer.plot_conc_timeseries(
...     output_dirpath="output",
...     ylim_ch4=(1.8, 2.5),
...     ylim_c2h6=(0, 100),
...     ylim_h2o=(0, 20000)
... )
def plot_conc_timeseries_with_hotspots( self, hotspots: list[HotspotData] | None = None, output_dirpath: str | pathlib.Path | None = None, output_filename: str = 'timeseries_with_hotspots.png', figsize: tuple[float, float] = (8, 6), dpi: float | None = 350, save_fig: bool = True, show_fig: bool = True, col_ch4: str = 'ch4_ppm', col_c2h6: str = 'c2h6_ppb', col_h2o: str = 'h2o_ppm', add_legend: bool = True, legend_bbox_to_anchor: tuple[float, float] = (0.5, 0.05), legend_ncol: int | None = None, font_size: float = 12, label_pad: float = 10, line_color: str = 'black', hotspot_colors: dict[typing.Literal['bio', 'gas', 'comb', 'scale_check'], str] | None = None, hotspot_markerscale: float = 1, hotspot_size: int = 10, time_margin_minutes: float = 2.0, ylim_ch4: tuple[float, float] | None = None, ylim_c2h6: tuple[float, float] | None = None, ylim_h2o: tuple[float, float] | None = None, ylim_ratio: tuple[float, float] | None = None, yscale_log_ch4: bool = False, yscale_log_c2h6: bool = False, yscale_log_h2o: bool = False, yscale_log_ratio: bool = False, ylabel_ch4: str = '$\\mathregular{CH_{4}}$ (ppm)', ylabel_c2h6: str = '$\\mathregular{C_{2}H_{6}}$ (ppb)', ylabel_h2o: str = '$\\mathregular{H_{2}O}$ (ppm)', ylabel_ratio: str = 'ΔC$_2$H$_6$/ΔCH$_4$\n(ppb ppm$^{-1}$)') -> None:
1646    def plot_conc_timeseries_with_hotspots(
1647        self,
1648        hotspots: list[HotspotData] | None = None,
1649        output_dirpath: str | Path | None = None,
1650        output_filename: str = "timeseries_with_hotspots.png",
1651        figsize: tuple[float, float] = (8, 6),
1652        dpi: float | None = 350,
1653        save_fig: bool = True,
1654        show_fig: bool = True,
1655        col_ch4: str = "ch4_ppm",
1656        col_c2h6: str = "c2h6_ppb",
1657        col_h2o: str = "h2o_ppm",
1658        add_legend: bool = True,
1659        legend_bbox_to_anchor: tuple[float, float] = (0.5, 0.05),
1660        legend_ncol: int | None = None,
1661        font_size: float = 12,
1662        label_pad: float = 10,
1663        line_color: str = "black",
1664        hotspot_colors: dict[HotspotType, str] | None = None,
1665        hotspot_markerscale: float = 1,
1666        hotspot_size: int = 10,
1667        time_margin_minutes: float = 2.0,
1668        ylim_ch4: tuple[float, float] | None = None,
1669        ylim_c2h6: tuple[float, float] | None = None,
1670        ylim_h2o: tuple[float, float] | None = None,
1671        ylim_ratio: tuple[float, float] | None = None,
1672        yscale_log_ch4: bool = False,
1673        yscale_log_c2h6: bool = False,
1674        yscale_log_h2o: bool = False,
1675        yscale_log_ratio: bool = False,
1676        ylabel_ch4: str = "$\\mathregular{CH_{4}}$ (ppm)",
1677        ylabel_c2h6: str = "$\\mathregular{C_{2}H_{6}}$ (ppb)",
1678        ylabel_h2o: str = "$\\mathregular{H_{2}O}$ (ppm)",
1679        ylabel_ratio: str = "ΔC$_2$H$_6$/ΔCH$_4$\n(ppb ppm$^{-1}$)",
1680    ) -> None:
1681        """
1682        時系列データとホットスポットをプロットします。
1683
1684        Parameters
1685        ----------
1686            hotspots: list[HotspotData] | None, optional
1687                表示するホットスポットのリスト。デフォルトはNoneで、この場合ホットスポットは表示されません。
1688            output_dirpath: str | Path | None, optional
1689                出力先ディレクトリのパス。save_fig=Trueの場合は必須です。デフォルトはNoneです。
1690            output_filename: str, optional
1691                保存するファイル名。デフォルトは"timeseries_with_hotspots.png"です。
1692            figsize: tuple[float, float], optional
1693                図のサイズ。デフォルトは(8, 6)です。
1694            dpi: float | None, optional
1695                図の解像度。デフォルトは350です。
1696            save_fig: bool, optional
1697                図を保存するかどうか。デフォルトはTrueです。
1698            show_fig: bool, optional
1699                図を表示するかどうか。デフォルトはTrueです。
1700            col_ch4: str, optional
1701                CH4データのカラム名。デフォルトは"ch4_ppm"です。
1702            col_c2h6: str, optional
1703                C2H6データのカラム名。デフォルトは"c2h6_ppb"です。
1704            col_h2o: str, optional
1705                H2Oデータのカラム名。デフォルトは"h2o_ppm"です。
1706            add_legend: bool, optional
1707                ホットスポットの凡例を表示するかどうか。デフォルトはTrueです。
1708            legend_bbox_to_anchor: tuple[float, float], optional
1709                ホットスポットの凡例の位置。デフォルトは(0.5, 0.05)です。
1710            legend_ncol: int | None, optional
1711                凡例のカラム数。デフォルトはNoneで、この場合ホットスポットの種類数に基づいて一行で表示します。
1712            font_size: float, optional
1713                基本フォントサイズ。デフォルトは12です。
1714            label_pad: float, optional
1715                y軸ラベルのパディング。デフォルトは10です。
1716            line_color: str, optional
1717                線の色。デフォルトは"black"です。
1718            hotspot_colors: dict[HotspotType, str] | None, optional
1719                ホットスポットタイプごとの色指定。デフォルトはNoneで、{"bio": "blue", "gas": "red", "comb": "green"}が使用されます。
1720            hotspot_markerscale: float, optional
1721                ホットスポットの凡例でのサイズ。hotspot_sizeに対する相対値。デフォルトは1です。
1722            hotspot_size: int, optional
1723                ホットスポットの図でのサイズ。デフォルトは10です。
1724            time_margin_minutes: float, optional
1725                プロットの時間軸の余白(分)。デフォルトは2.0分です。
1726            ylim_ch4: tuple[float, float] | None, optional
1727                CH4プロットのy軸範囲。デフォルトはNoneで、自動設定されます。
1728            ylim_c2h6: tuple[float, float] | None, optional
1729                C2H6プロットのy軸範囲。デフォルトはNoneで、自動設定されます。
1730            ylim_h2o: tuple[float, float] | None, optional
1731                H2Oプロットのy軸範囲。デフォルトはNoneで、自動設定されます。
1732            ylim_ratio: tuple[float, float] | None, optional
1733                比率プロットのy軸範囲。デフォルトはNoneで、自動設定されます。
1734            yscale_log_ch4: bool, optional
1735                CH4データのy軸を対数スケールで表示するかどうか。デフォルトはFalseです。
1736            yscale_log_c2h6: bool, optional
1737                C2H6データのy軸を対数スケールで表示するかどうか。デフォルトはFalseです。
1738            yscale_log_h2o: bool, optional
1739                H2Oデータのy軸を対数スケールで表示するかどうか。デフォルトはFalseです。
1740            yscale_log_ratio: bool, optional
1741                比率データのy軸を対数スケールで表示するかどうか。デフォルトはFalseです。
1742            ylabel_ch4: str, optional
1743                CH4プロットのy軸ラベル。デフォルトは"$\\mathregular{CH_{4}}$ (ppm)"です。
1744            ylabel_c2h6: str, optional
1745                C2H6プロットのy軸ラベル。デフォルトは"$\\mathregular{C_{2}H_{6}}$ (ppb)"です。
1746            ylabel_h2o: str, optional
1747                H2Oプロットのy軸ラベル。デフォルトは"$\\mathregular{H_{2}O}$ (ppm)"です。
1748            ylabel_ratio: str, optional
1749                比率プロットのy軸ラベル。デフォルトは"ΔC$_2$H$_6$/ΔCH$_4$\\n(ppb ppm$^{-1}$)"です。
1750
1751        Examples
1752        --------
1753        基本的な使用方法:
1754        >>> analyzer = MobileMeasurementAnalyzer(df)
1755        >>> analyzer.plot_conc_timeseries_with_hotspots()
1756
1757        ホットスポットを指定して保存する:
1758        >>> hotspots = [HotspotData(...), HotspotData(...)]
1759        >>> analyzer.plot_conc_timeseries_with_hotspots(
1760        ...     hotspots=hotspots,
1761        ...     output_dirpath="output",
1762        ...     save_fig=True
1763        ... )
1764
1765        カスタマイズした表示:
1766        >>> analyzer.plot_conc_timeseries_with_hotspots(
1767        ...     figsize=(12, 8),
1768        ...     ylim_ch4=(1.8, 2.5),
1769        ...     yscale_log_c2h6=True,
1770        ...     hotspot_colors={"bio": "purple", "gas": "orange"}
1771        ... )
1772        """
1773        if hotspot_colors is None:
1774            hotspot_colors = {"bio": "blue", "gas": "red", "comb": "green"}
1775        # プロットパラメータの設定
1776        plt.rcParams.update(
1777            {
1778                "font.size": font_size,
1779                "axes.labelsize": font_size,
1780                "axes.titlesize": font_size,
1781                "xtick.labelsize": font_size,
1782                "ytick.labelsize": font_size,
1783            }
1784        )
1785
1786        # timestampをインデックスとして設定
1787        df_internal = self.df.copy()
1788        df_internal.set_index("timestamp", inplace=True)
1789
1790        # プロットの作成
1791        fig = plt.figure(figsize=figsize, dpi=dpi)
1792
1793        # サブプロットのグリッドを作成 (4行1列)
1794        gs = gridspec.GridSpec(4, 1, height_ratios=[1, 1, 1, 1])
1795
1796        # 時間軸の範囲を設定(余白付き)
1797        time_min = df_internal.index.min()
1798        time_max = df_internal.index.max()
1799        time_margin = pd.Timedelta(minutes=time_margin_minutes)
1800        plot_time_min = time_min - time_margin
1801        plot_time_max = time_max + time_margin
1802
1803        # CH4プロット
1804        ax1 = fig.add_subplot(gs[0])
1805        ax1.plot(df_internal.index, df_internal[col_ch4], c=line_color)
1806        if ylim_ch4:
1807            ax1.set_ylim(ylim_ch4)
1808        if yscale_log_ch4:
1809            ax1.set_yscale("log")
1810        ax1.set_ylabel(ylabel_ch4, labelpad=label_pad)
1811        ax1.grid(True, alpha=0.3)
1812        ax1.set_xlim(plot_time_min, plot_time_max)
1813
1814        # C2H6プロット
1815        ax2 = fig.add_subplot(gs[1])
1816        ax2.plot(df_internal.index, df_internal[col_c2h6], c=line_color)
1817        if ylim_c2h6:
1818            ax2.set_ylim(ylim_c2h6)
1819        if yscale_log_c2h6:
1820            ax2.set_yscale("log")
1821        ax2.set_ylabel(ylabel_c2h6, labelpad=label_pad)
1822        ax2.grid(True, alpha=0.3)
1823        ax2.set_xlim(plot_time_min, plot_time_max)
1824
1825        # H2Oプロット
1826        ax3 = fig.add_subplot(gs[2])
1827        ax3.plot(df_internal.index, df_internal[col_h2o], c=line_color)
1828        if ylim_h2o:
1829            ax3.set_ylim(ylim_h2o)
1830        if yscale_log_h2o:
1831            ax3.set_yscale("log")
1832        ax3.set_ylabel(ylabel_h2o, labelpad=label_pad)
1833        ax3.grid(True, alpha=0.3)
1834        ax3.set_xlim(plot_time_min, plot_time_max)
1835
1836        # ホットスポットの比率プロット
1837        ax4 = fig.add_subplot(gs[3])
1838
1839        if hotspots:
1840            # ホットスポットをDataFrameに変換
1841            hotspot_df = pd.DataFrame(
1842                [
1843                    {
1844                        "timestamp": pd.to_datetime(spot.timestamp),
1845                        "delta_ratio": spot.delta_ratio,
1846                        "type": spot.type,
1847                    }
1848                    for spot in hotspots
1849                ]
1850            )
1851
1852            # タイプごとにプロット
1853            for spot_type in set(hotspot_df["type"]):
1854                type_data = hotspot_df[hotspot_df["type"] == spot_type]
1855
1856                # 点をプロット
1857                ax4.scatter(
1858                    type_data["timestamp"],
1859                    type_data["delta_ratio"],
1860                    c=hotspot_colors.get(spot_type, "black"),
1861                    label=spot_type,
1862                    alpha=0.6,
1863                    s=hotspot_size,
1864                )
1865
1866        ax4.set_ylabel(ylabel_ratio, labelpad=label_pad)
1867        if ylim_ratio:
1868            ax4.set_ylim(ylim_ratio)
1869        if yscale_log_ratio:
1870            ax4.set_yscale("log")
1871        ax4.grid(True, alpha=0.3)
1872        ax4.set_xlim(plot_time_min, plot_time_max)  # 他のプロットと同じ時間範囲を設定
1873
1874        # 凡例を図の下部に配置
1875        if hotspots and add_legend:
1876            ncol = (
1877                legend_ncol if legend_ncol is not None else len(set(hotspot_df["type"]))
1878            )
1879            # markerscaleは元のサイズに対する倍率を指定するため、
1880            # 目的のサイズ(100)をプロットのマーカーサイズで割ることで、適切な倍率を計算しています
1881            fig.legend(
1882                bbox_to_anchor=legend_bbox_to_anchor,
1883                loc="upper center",
1884                ncol=ncol,
1885                columnspacing=1.0,
1886                handletextpad=0.5,
1887                markerscale=hotspot_markerscale,
1888            )
1889
1890        # x軸のフォーマット調整(全てのサブプロットで共通)
1891        for ax in [ax1, ax2, ax3, ax4]:
1892            ax.xaxis.set_major_formatter(mdates.DateFormatter("%H:%M"))
1893            ax.xaxis.set_major_locator(mdates.AutoDateLocator())
1894            ax.tick_params(axis="both", which="major", labelsize=font_size)
1895            ax.grid(True, alpha=0.3)
1896
1897        # サブプロット間の間隔調整と凡例のためのスペース確保
1898        plt.subplots_adjust(hspace=0.38, bottom=0.12)  # bottomを0.15から0.12に変更
1899
1900        # 図の保存
1901        if save_fig:
1902            if output_dirpath is None:
1903                raise ValueError(
1904                    "save_fig = True の場合、 output_dirpath を指定する必要があります。有効なディレクトリパスを指定してください。"
1905                )
1906            os.makedirs(output_dirpath, exist_ok=True)
1907            output_filepath = os.path.join(output_dirpath, output_filename)
1908            plt.savefig(output_filepath, bbox_inches="tight", dpi=dpi)
1909
1910        if show_fig:
1911            plt.show()
1912        plt.close(fig=fig)

時系列データとホットスポットをプロットします。

Parameters

hotspots: list[HotspotData] | None, optional
    表示するホットスポットのリスト。デフォルトはNoneで、この場合ホットスポットは表示されません。
output_dirpath: str | Path | None, optional
    出力先ディレクトリのパス。save_fig=Trueの場合は必須です。デフォルトはNoneです。
output_filename: str, optional
    保存するファイル名。デフォルトは"timeseries_with_hotspots.png"です。
figsize: tuple[float, float], optional
    図のサイズ。デフォルトは(8, 6)です。
dpi: float | None, optional
    図の解像度。デフォルトは350です。
save_fig: bool, optional
    図を保存するかどうか。デフォルトはTrueです。
show_fig: bool, optional
    図を表示するかどうか。デフォルトはTrueです。
col_ch4: str, optional
    CH4データのカラム名。デフォルトは"ch4_ppm"です。
col_c2h6: str, optional
    C2H6データのカラム名。デフォルトは"c2h6_ppb"です。
col_h2o: str, optional
    H2Oデータのカラム名。デフォルトは"h2o_ppm"です。
add_legend: bool, optional
    ホットスポットの凡例を表示するかどうか。デフォルトはTrueです。
legend_bbox_to_anchor: tuple[float, float], optional
    ホットスポットの凡例の位置。デフォルトは(0.5, 0.05)です。
legend_ncol: int | None, optional
    凡例のカラム数。デフォルトはNoneで、この場合ホットスポットの種類数に基づいて一行で表示します。
font_size: float, optional
    基本フォントサイズ。デフォルトは12です。
label_pad: float, optional
    y軸ラベルのパディング。デフォルトは10です。
line_color: str, optional
    線の色。デフォルトは"black"です。
hotspot_colors: dict[HotspotType, str] | None, optional
    ホットスポットタイプごとの色指定。デフォルトはNoneで、{"bio": "blue", "gas": "red", "comb": "green"}が使用されます。
hotspot_markerscale: float, optional
    ホットスポットの凡例でのサイズ。hotspot_sizeに対する相対値。デフォルトは1です。
hotspot_size: int, optional
    ホットスポットの図でのサイズ。デフォルトは10です。
time_margin_minutes: float, optional
    プロットの時間軸の余白(分)。デフォルトは2.0分です。
ylim_ch4: tuple[float, float] | None, optional
    CH4プロットのy軸範囲。デフォルトはNoneで、自動設定されます。
ylim_c2h6: tuple[float, float] | None, optional
    C2H6プロットのy軸範囲。デフォルトはNoneで、自動設定されます。
ylim_h2o: tuple[float, float] | None, optional
    H2Oプロットのy軸範囲。デフォルトはNoneで、自動設定されます。
ylim_ratio: tuple[float, float] | None, optional
    比率プロットのy軸範囲。デフォルトはNoneで、自動設定されます。
yscale_log_ch4: bool, optional
    CH4データのy軸を対数スケールで表示するかどうか。デフォルトはFalseです。
yscale_log_c2h6: bool, optional
    C2H6データのy軸を対数スケールで表示するかどうか。デフォルトはFalseです。
yscale_log_h2o: bool, optional
    H2Oデータのy軸を対数スケールで表示するかどうか。デフォルトはFalseです。
yscale_log_ratio: bool, optional
    比率データのy軸を対数スケールで表示するかどうか。デフォルトはFalseです。
ylabel_ch4: str, optional
    CH4プロットのy軸ラベル。デフォルトは"$\mathregular{CH_{4}}$ (ppm)"です。
ylabel_c2h6: str, optional
    C2H6プロットのy軸ラベル。デフォルトは"$\mathregular{C_{2}H_{6}}$ (ppb)"です。
ylabel_h2o: str, optional
    H2Oプロットのy軸ラベル。デフォルトは"$\mathregular{H_{2}O}$ (ppm)"です。
ylabel_ratio: str, optional
    比率プロットのy軸ラベル。デフォルトは"ΔC$_2$H$_6$/ΔCH$_4$\n(ppb ppm$^{-1}$)"です。

Examples

基本的な使用方法:

>>> analyzer = MobileMeasurementAnalyzer(df)
>>> analyzer.plot_conc_timeseries_with_hotspots()

ホットスポットを指定して保存する:

>>> hotspots = [HotspotData(...), HotspotData(...)]
>>> analyzer.plot_conc_timeseries_with_hotspots(
...     hotspots=hotspots,
...     output_dirpath="output",
...     save_fig=True
... )

カスタマイズした表示:

>>> analyzer.plot_conc_timeseries_with_hotspots(
...     figsize=(12, 8),
...     ylim_ch4=(1.8, 2.5),
...     yscale_log_c2h6=True,
...     hotspot_colors={"bio": "purple", "gas": "orange"}
... )
def remove_c2c1_ratio_duplicates( self, df: pandas.core.frame.DataFrame, min_time_threshold_seconds: float = 300, max_time_threshold_hours: float = 12.0, check_time_all: bool = True, hotspot_area_meter: float = 50.0, col_ch4_ppm: str = 'ch4_ppm', col_ch4_ppm_mv: str = 'ch4_ppm_mv', col_ch4_ppm_delta: str = 'ch4_ppm_delta'):
2468    def remove_c2c1_ratio_duplicates(
2469        self,
2470        df: pd.DataFrame,
2471        min_time_threshold_seconds: float = 300,
2472        max_time_threshold_hours: float = 12.0,
2473        check_time_all: bool = True,
2474        hotspot_area_meter: float = 50.0,
2475        col_ch4_ppm: str = "ch4_ppm",
2476        col_ch4_ppm_mv: str = "ch4_ppm_mv",
2477        col_ch4_ppm_delta: str = "ch4_ppm_delta",
2478    ):
2479        """
2480        メタン濃度の増加が閾値を超えた地点から、重複を除外してユニークなホットスポットを抽出する関数。
2481
2482        Parameters
2483        ----------
2484            df: pd.DataFrame
2485                入力データフレーム。必須カラム:
2486                - latitude: 緯度
2487                - longitude: 経度
2488                - ch4_ppm: メタン濃度(ppm)
2489                - ch4_ppm_mv: メタン濃度の移動平均(ppm)
2490                - ch4_ppm_delta: メタン濃度の増加量(ppm)
2491            min_time_threshold_seconds: float, optional
2492                重複とみなす最小時間差(秒)。デフォルト値は300秒(5分)。
2493            max_time_threshold_hours: float, optional
2494                別ポイントとして扱う最大時間差(時間)。デフォルト値は12時間。
2495            check_time_all: bool, optional
2496                時間閾値を超えた場合の重複チェックを継続するかどうか。デフォルト値はTrue。
2497            hotspot_area_meter: float, optional
2498                重複とみなす距離の閾値(メートル)。デフォルト値は50メートル。
2499            col_ch4_ppm: str, optional
2500                メタン濃度のカラム名。デフォルト値は"ch4_ppm"。
2501            col_ch4_ppm_mv: str, optional
2502                メタン濃度移動平均のカラム名。デフォルト値は"ch4_ppm_mv"。
2503            col_ch4_ppm_delta: str, optional
2504                メタン濃度増加量のカラム名。デフォルト値は"ch4_ppm_delta"。
2505
2506        Returns
2507        ----------
2508            pd.DataFrame
2509                ユニークなホットスポットのデータフレーム。
2510
2511        Examples
2512        ----------
2513        >>> analyzer = MobileMeasurementAnalyzer()
2514        >>> df = pd.read_csv("measurement_data.csv")
2515        >>> unique_spots = analyzer.remove_c2c1_ratio_duplicates(
2516        ...     df,
2517        ...     min_time_threshold_seconds=300,
2518        ...     hotspot_area_meter=50.0
2519        ... )
2520        """
2521        df_data: pd.DataFrame = df.copy()
2522        # メタン濃度の増加が閾値を超えた点を抽出
2523        mask = (
2524            df_data[col_ch4_ppm] - df_data[col_ch4_ppm_mv] > self._ch4_enhance_threshold
2525        )
2526        hotspot_candidates = df_data[mask].copy()
2527
2528        # ΔCH4の降順でソート
2529        sorted_hotspots = hotspot_candidates.sort_values(
2530            by=col_ch4_ppm_delta, ascending=False
2531        )
2532        used_positions = []
2533        unique_hotspots = pd.DataFrame()
2534
2535        for _, spot in sorted_hotspots.iterrows():
2536            should_add = True
2537            for used_lat, used_lon, used_time in used_positions:
2538                # 距離チェック
2539                distance = geodesic(
2540                    (spot.latitude, spot.longitude), (used_lat, used_lon)
2541                ).meters
2542
2543                if distance < hotspot_area_meter:
2544                    # 時間差の計算(秒単位)
2545                    time_diff = pd.Timedelta(
2546                        pd.to_datetime(spot.name) - pd.to_datetime(used_time)
2547                    ).total_seconds()
2548                    time_diff_abs = abs(time_diff)
2549
2550                    # 時間差に基づく判定
2551                    if check_time_all:
2552                        # 時間に関係なく、距離が近ければ重複とみなす
2553                        # ΔCH4が大きい方を残す(現在のスポットは必ず小さい)
2554                        should_add = False
2555                        break
2556                    else:
2557                        # 時間窓による判定を行う
2558                        if time_diff_abs <= min_time_threshold_seconds:
2559                            # Case 1: 最小時間閾値以内は重複とみなす
2560                            should_add = False
2561                            break
2562                        elif time_diff_abs > max_time_threshold_hours * 3600:
2563                            # Case 2: 最大時間閾値を超えた場合は重複チェックをスキップ
2564                            continue
2565                        # Case 3: その間の時間差の場合は、距離が近ければ重複とみなす
2566                        should_add = False
2567                        break
2568
2569            if should_add:
2570                unique_hotspots = pd.concat([unique_hotspots, pd.DataFrame([spot])])
2571                used_positions.append((spot.latitude, spot.longitude, spot.name))
2572
2573        return unique_hotspots

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

Parameters

df: pd.DataFrame
    入力データフレーム。必須カラム:
    - latitude: 緯度
    - longitude: 経度
    - ch4_ppm: メタン濃度(ppm)
    - ch4_ppm_mv: メタン濃度の移動平均(ppm)
    - ch4_ppm_delta: メタン濃度の増加量(ppm)
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メートル。
col_ch4_ppm: str, optional
    メタン濃度のカラム名。デフォルト値は"ch4_ppm"。
col_ch4_ppm_mv: str, optional
    メタン濃度移動平均のカラム名。デフォルト値は"ch4_ppm_mv"。
col_ch4_ppm_delta: str, optional
    メタン濃度増加量のカラム名。デフォルト値は"ch4_ppm_delta"。

Returns

pd.DataFrame
    ユニークなホットスポットのデータフレーム。

Examples

>>> analyzer = MobileMeasurementAnalyzer()
>>> df = pd.read_csv("measurement_data.csv")
>>> unique_spots = analyzer.remove_c2c1_ratio_duplicates(
...     df,
...     min_time_threshold_seconds=300,
...     hotspot_area_meter=50.0
... )
@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]:
2575    @staticmethod
2576    def remove_hotspots_duplicates(
2577        hotspots: list[HotspotData],
2578        check_time_all: bool,
2579        min_time_threshold_seconds: float = 300,
2580        max_time_threshold_hours: float = 12,
2581        hotspot_area_meter: float = 50,
2582    ) -> list[HotspotData]:
2583        """
2584        重複するホットスポットを除外します。
2585
2586        このメソッドは、与えられたホットスポットのリストから重複を検出し、
2587        一意のホットスポットのみを返します。重複の判定は、指定された
2588        時間および距離の閾値に基づいて行われます。
2589
2590        Parameters
2591        ----------
2592            hotspots: list[HotspotData]
2593                重複を除外する対象のホットスポットのリスト
2594            check_time_all: bool
2595                時間に関係なく重複チェックを行うかどうか
2596            min_time_threshold_seconds: float, optional
2597                重複とみなす最小時間の閾値(秒)。デフォルト値は300秒
2598            max_time_threshold_hours: float, optional
2599                重複チェックを一時的に無視する最大時間の閾値(時間)。デフォルト値は12時間
2600            hotspot_area_meter: float, optional
2601                重複とみなす距離の閾値(メートル)。デフォルト値は50メートル
2602
2603        Returns
2604        ----------
2605            list[HotspotData]
2606                重複を除去したホットスポットのリスト
2607
2608        Examples
2609        ----------
2610        >>> hotspots = [HotspotData(...), HotspotData(...)]  # ホットスポットのリスト
2611        >>> analyzer = MobileMeasurementAnalyzer()
2612        >>> unique_spots = analyzer.remove_hotspots_duplicates(
2613        ...     hotspots=hotspots,
2614        ...     check_time_all=True,
2615        ...     min_time_threshold_seconds=300,
2616        ...     max_time_threshold_hours=12,
2617        ...     hotspot_area_meter=50
2618        ... )
2619        """
2620        # ΔCH4の降順でソート
2621        sorted_hotspots: list[HotspotData] = sorted(
2622            hotspots, key=lambda x: x.delta_ch4, reverse=True
2623        )
2624        used_positions_by_type: dict[
2625            HotspotType, list[tuple[float, float, str, float]]
2626        ] = {
2627            "bio": [],
2628            "gas": [],
2629            "comb": [],
2630        }
2631        unique_hotspots: list[HotspotData] = []
2632
2633        for spot in sorted_hotspots:
2634            is_duplicate = MobileMeasurementAnalyzer._is_duplicate_spot(
2635                current_lat=spot.avg_lat,
2636                current_lon=spot.avg_lon,
2637                current_time=spot.timestamp,
2638                used_positions=used_positions_by_type[spot.type],
2639                check_time_all=check_time_all,
2640                min_time_threshold_seconds=min_time_threshold_seconds,
2641                max_time_threshold_hours=max_time_threshold_hours,
2642                hotspot_area_meter=hotspot_area_meter,
2643            )
2644
2645            if not is_duplicate:
2646                unique_hotspots.append(spot)
2647                used_positions_by_type[spot.type].append(
2648                    (spot.avg_lat, spot.avg_lon, spot.timestamp, spot.delta_ch4)
2649                )
2650
2651        return unique_hotspots

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

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

Parameters

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

Returns

list[HotspotData]
    重複を除去したホットスポットのリスト

Examples

>>> hotspots = [HotspotData(...), HotspotData(...)]  # ホットスポットのリスト
>>> analyzer = MobileMeasurementAnalyzer()
>>> unique_spots = analyzer.remove_hotspots_duplicates(
...     hotspots=hotspots,
...     check_time_all=True,
...     min_time_threshold_seconds=300,
...     max_time_threshold_hours=12,
...     hotspot_area_meter=50
... )
@dataclass
class MobileMeasurementConfig:
206@dataclass
207class MobileMeasurementConfig:
208    """
209    MobileMeasurementAnalyzerのconfigsに与える設定の値を保持するデータクラス
210
211    Parameters
212    ----------
213        fs: float
214            サンプリング周波数(Hz)
215        lag: float
216            測器の遅れ時間(秒)
217        path: Path | str
218            ファイルパス
219        bias_removal: BiasRemovalConfig | None, optional
220            バイアス除去の設定。未指定の場合は補正を実施しません。
221        h2o_correction: H2OCorrectionConfig | None, optional
222            水蒸気補正の設定。未指定の場合は補正を実施しません。
223
224    Examples
225    --------
226    >>> # 水蒸気補正の設定
227    >>> h2o_config = H2OCorrectionConfig(
228    ...     coef_b=0.001,
229    ...     coef_c=0.0001,
230    ...     h2o_ppm_threshold=2000,
231    ...     target_h2o_ppm=10000
232    ... )
233    >>> # バイアス除去の設定
234    >>> bias_config = BiasRemovalConfig(
235    ...     quantile_value=0.05,
236    ...     base_ch4_ppm=2.0,
237    ...     base_c2h6_ppb=0
238    ... )
239    >>> # 車載観測の設定
240    >>> analyzer_config = MobileMeasurementConfig(
241    ...     fs=1.0,  # サンプリング周波数 1Hz
242    ...     lag=2.0,  # 遅れ時間 2秒
243    ...     path="data.csv",
244    ...     bias_removal=bias_config,
245    ...     h2o_correction=h2o_config
246    ... )
247    """
248
249    fs: float
250    lag: float
251    path: Path | str
252    bias_removal: BiasRemovalConfig | None = None
253    h2o_correction: H2OCorrectionConfig | None = None
254
255    def __post_init__(self) -> None:
256        """
257        インスタンス生成後に入力値の検証を行います。
258        """
259        # fsが有効かを確認
260        if not isinstance(self.fs, int | float) or self.fs <= 0:
261            raise ValueError(
262                f"Invalid sampling frequency: {self.fs}. Must be a positive float."
263            )
264        # lagが0以上のfloatかを確認
265        if not isinstance(self.lag, int | float) or self.lag < 0:
266            raise ValueError(
267                f"Invalid lag value: {self.lag}. Must be a non-negative float."
268            )
269        # 拡張子の確認
270        supported_extensions: list[str] = [".txt", ".csv"]
271        extension = Path(self.path).suffix
272        if extension not in supported_extensions:
273            raise ValueError(
274                f"Unsupported file extension: '{extension}'. Supported: {supported_extensions}"
275            )
276        # ファイルの存在確認
277        if not os.path.exists(self.path):
278            raise FileNotFoundError(f"'{self.path}'")
279
280    @classmethod
281    def validate_and_create(
282        cls,
283        fs: float,
284        lag: float,
285        path: Path | str,
286        bias_removal: BiasRemovalConfig | None = None,
287        h2o_correction: H2OCorrectionConfig | None = None,
288    ) -> "MobileMeasurementConfig":
289        """
290        入力値を検証し、MobileMeasurementConfigインスタンスを生成するファクトリメソッドです。
291
292        Parameters
293        ----------
294            fs: float
295                サンプリング周波数(Hz)。正の値である必要があります。
296            lag: float
297                遅延時間(秒)。0以上の値である必要があります。
298            path: Path | str
299                入力ファイルのパス。サポートされている拡張子は.txtと.csvです。
300            bias_removal: BiasRemovalConfig | None, optional
301                バイアス除去の設定。未指定の場合は補正を実施しません。
302            h2o_correction: H2OCorrectionConfig | None, optional
303                水蒸気補正の設定。未指定の場合は補正を実施しません。
304
305        Returns
306        -------
307            MobileMeasurementConfig
308                検証された入力設定を持つMobileMeasurementConfigオブジェクト
309
310        Examples
311        --------
312        >>> config = MobileMeasurementConfig.validate_and_create(
313        ...     fs=1.0,
314        ...     lag=2.0,
315        ...     path="data.csv"
316        ... )
317        """
318        return cls(
319            fs=fs,
320            lag=lag,
321            path=path,
322            bias_removal=bias_removal,
323            h2o_correction=h2o_correction,
324        )

MobileMeasurementAnalyzerのconfigsに与える設定の値を保持するデータクラス

Parameters

fs: float
    サンプリング周波数(Hz)
lag: float
    測器の遅れ時間(秒)
path: Path | str
    ファイルパス
bias_removal: BiasRemovalConfig | None, optional
    バイアス除去の設定。未指定の場合は補正を実施しません。
h2o_correction: H2OCorrectionConfig | None, optional
    水蒸気補正の設定。未指定の場合は補正を実施しません。

Examples

>>> # 水蒸気補正の設定
>>> h2o_config = H2OCorrectionConfig(
...     coef_b=0.001,
...     coef_c=0.0001,
...     h2o_ppm_threshold=2000,
...     target_h2o_ppm=10000
... )
>>> # バイアス除去の設定
>>> bias_config = BiasRemovalConfig(
...     quantile_value=0.05,
...     base_ch4_ppm=2.0,
...     base_c2h6_ppb=0
... )
>>> # 車載観測の設定
>>> analyzer_config = MobileMeasurementConfig(
...     fs=1.0,  # サンプリング周波数 1Hz
...     lag=2.0,  # 遅れ時間 2秒
...     path="data.csv",
...     bias_removal=bias_config,
...     h2o_correction=h2o_config
... )
MobileMeasurementConfig( 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, bias_removal: BiasRemovalConfig | None = None, h2o_correction: H2OCorrectionConfig | None = None) -> MobileMeasurementConfig:
280    @classmethod
281    def validate_and_create(
282        cls,
283        fs: float,
284        lag: float,
285        path: Path | str,
286        bias_removal: BiasRemovalConfig | None = None,
287        h2o_correction: H2OCorrectionConfig | None = None,
288    ) -> "MobileMeasurementConfig":
289        """
290        入力値を検証し、MobileMeasurementConfigインスタンスを生成するファクトリメソッドです。
291
292        Parameters
293        ----------
294            fs: float
295                サンプリング周波数(Hz)。正の値である必要があります。
296            lag: float
297                遅延時間(秒)。0以上の値である必要があります。
298            path: Path | str
299                入力ファイルのパス。サポートされている拡張子は.txtと.csvです。
300            bias_removal: BiasRemovalConfig | None, optional
301                バイアス除去の設定。未指定の場合は補正を実施しません。
302            h2o_correction: H2OCorrectionConfig | None, optional
303                水蒸気補正の設定。未指定の場合は補正を実施しません。
304
305        Returns
306        -------
307            MobileMeasurementConfig
308                検証された入力設定を持つMobileMeasurementConfigオブジェクト
309
310        Examples
311        --------
312        >>> config = MobileMeasurementConfig.validate_and_create(
313        ...     fs=1.0,
314        ...     lag=2.0,
315        ...     path="data.csv"
316        ... )
317        """
318        return cls(
319            fs=fs,
320            lag=lag,
321            path=path,
322            bias_removal=bias_removal,
323            h2o_correction=h2o_correction,
324        )

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

Parameters

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

Returns

MobileMeasurementConfig
    検証された入力設定を持つMobileMeasurementConfigオブジェクト

Examples

>>> config = MobileMeasurementConfig.validate_and_create(
...     fs=1.0,
...     lag=2.0,
...     path="data.csv"
... )
class MonthlyConverter:
 13class MonthlyConverter:
 14    """
 15    Monthlyシート(Excel)を一括で読み込み、DataFrameに変換するクラス。
 16    デフォルトは'SA.Ultra.*.xlsx'に対応していますが、コンストラクタのfile_patternを
 17    変更すると別のシートにも対応可能です(例: 'SA.Picaro.*.xlsx')。
 18    """
 19
 20    FILE_DATE_FORMAT = "%Y.%m"  # ファイル名用
 21    PERIOD_DATE_FORMAT = "%Y-%m-%d"  # 期間指定用
 22
 23    def __init__(
 24        self,
 25        directory: str | Path,
 26        file_pattern: str = "SA.Ultra.*.xlsx",
 27        na_values: list[str] | None = None,
 28        logger: Logger | None = None,
 29        logging_debug: bool = False,
 30    ):
 31        """
 32        MonthlyConverterクラスのコンストラクタ
 33
 34        Parameters
 35        ----------
 36            directory: str | Path
 37                Excelファイルが格納されているディレクトリのパス
 38            file_pattern: str, optional
 39                ファイル名のパターン。デフォルト値は'SA.Ultra.*.xlsx'です。
 40            na_values: list[str] | None, optional
 41                NaNと判定する値のパターン。デフォルト値はNoneで、その場合は以下の値が使用されます:
 42                ```python
 43                ["#DIV/0!", "#VALUE!", "#REF!", "#N/A", "#NAME?", "NAN", "nan"]
 44                ```
 45            logger: Logger | None, optional
 46                使用するロガー。デフォルト値はNoneで、その場合は新しいロガーが作成されます。
 47            logging_debug: bool, optional
 48                ログレベルを"DEBUG"に設定するかどうか。デフォルト値はFalseで、その場合はINFO以上のレベルのメッセージが出力されます。
 49
 50        Examples
 51        --------
 52        >>> converter = MonthlyConverter("path/to/excel/files")
 53        >>> converter = MonthlyConverter(
 54        ...     "path/to/excel/files",
 55        ...     file_pattern="SA.Picaro.*.xlsx",
 56        ...     logging_debug=True
 57        ... )
 58        """
 59        # ロガー
 60        log_level: int = INFO
 61        if logging_debug:
 62            log_level = DEBUG
 63        self.logger: Logger = setup_logger(logger=logger, log_level=log_level)
 64
 65        if na_values is None:
 66            na_values = ["#DIV/0!", "#VALUE!", "#REF!", "#N/A", "#NAME?", "NAN", "nan"]
 67        self._na_values: list[str] = na_values
 68        self._directory = Path(directory)
 69        if not self._directory.exists():
 70            raise NotADirectoryError(f"Directory not found: {self._directory}")
 71
 72        # Excelファイルのパスを保持
 73        self._excel_files: dict[str, pd.ExcelFile] = {}
 74        self._file_pattern: str = file_pattern
 75
 76    def close(self) -> None:
 77        """
 78        すべてのExcelファイルをクローズする
 79        """
 80        for excel_file in self._excel_files.values():
 81            excel_file.close()
 82        self._excel_files.clear()
 83
 84    def get_available_dates(self) -> list[str]:
 85        """
 86        利用可能なファイルの日付一覧を返却します。
 87
 88        Returns
 89        ----------
 90            list[str]
 91                'yyyy.MM'形式の日付リスト
 92
 93        Examples
 94        --------
 95        >>> converter = MonthlyConverter("path/to/excel/files")
 96        >>> dates = converter.get_available_dates()
 97        >>> print(dates)
 98        ['2023.01', '2023.02', '2023.03']
 99        """
100        dates = []
101        for filename in self._directory.glob(self._file_pattern):
102            try:
103                date = self._extract_date(filename.name)
104                dates.append(date.strftime(self.FILE_DATE_FORMAT))
105            except ValueError:
106                continue
107        return sorted(dates)
108
109    def get_sheet_names(self, filename: str) -> list[str]:
110        """
111        指定されたファイルで利用可能なシート名の一覧を返却します。
112
113        Parameters
114        ----------
115            filename: str
116                対象のExcelファイル名を指定します。ファイル名のみを指定し、パスは含めません。
117
118        Returns
119        ----------
120            list[str]
121                シート名のリスト
122
123        Examples
124        --------
125        >>> converter = MonthlyConverter("path/to/excel/files")
126        >>> sheets = converter.get_sheet_names("2023.01.xlsx")
127        >>> print(sheets)
128        ['Sheet1', 'Sheet2', 'Sheet3']
129        """
130        if filename not in self._excel_files:
131            filepath = self._directory / filename
132            if not filepath.exists():
133                raise FileNotFoundError(f"File not found: {filepath}")
134            self._excel_files[filename] = pd.ExcelFile(filepath)
135        return [str(name) for name in self._excel_files[filename].sheet_names]
136
137    def read_sheets(
138        self,
139        sheet_names: str | list[str],
140        columns: list[str] | None = None,
141        col_datetime: str = "Date",
142        header: int = 0,
143        skiprows: int | list[int] | None = None,
144        start_date: str | None = None,
145        end_date: str | None = None,
146        include_end_date: bool = True,
147        sort_by_date: bool = True,
148    ) -> pd.DataFrame:
149        """
150        指定されたシートを読み込み、DataFrameとして返却します。
151        デフォルトでは2行目(単位の行)はスキップされます。
152        重複するカラム名がある場合は、より先に指定されたシートに存在するカラムの値を保持します。
153
154        Parameters
155        ----------
156            sheet_names: str | list[str]
157                読み込むシート名を指定します。文字列または文字列のリストを指定できます。
158            columns: list[str] | None, optional
159                残すカラム名のリストを指定します。Noneの場合は全てのカラムを保持します。
160            col_datetime: str, optional
161                日付と時刻の情報が含まれるカラム名を指定します。デフォルト値は'Date'です。
162            header: int, optional
163                データのヘッダー行を指定します。デフォルト値は0です。
164            skiprows: int | list[int] | None, optional
165                スキップする行数を指定します。Noneの場合のデフォルトでは2行目をスキップします。
166            start_date: str | None, optional
167                開始日を'yyyy-MM-dd'形式で指定します。この日付の'00:00:00'のデータが開始行となります。
168            end_date: str | None, optional
169                終了日を'yyyy-MM-dd'形式で指定します。この日付をデータに含めるかはinclude_end_dateフラグによって変わります。
170            include_end_date: bool, optional
171                終了日を含めるかどうかを指定します。デフォルト値はTrueです。
172            sort_by_date: bool, optional
173                ファイルの日付でソートするかどうかを指定します。デフォルト値はTrueです。
174
175        Returns
176        ----------
177            pd.DataFrame
178                読み込まれたデータを結合したDataFrameを返します。
179
180        Examples
181        --------
182        >>> converter = MonthlyConverter("path/to/excel/files")
183        >>> # 単一シートの読み込み
184        >>> df = converter.read_sheets("Sheet1")
185        >>> # 複数シートの読み込み
186        >>> df = converter.read_sheets(["Sheet1", "Sheet2"])
187        >>> # 特定の期間のデータ読み込み
188        >>> df = converter.read_sheets(
189        ...     "Sheet1",
190        ...     start_date="2023-01-01",
191        ...     end_date="2023-12-31"
192        ... )
193        """
194        if skiprows is None:
195            skiprows = [1]
196        if isinstance(sheet_names, str):
197            sheet_names = [sheet_names]
198
199        self._load_excel_files(start_date, end_date)
200
201        if not self._excel_files:
202            raise ValueError("No Excel files found matching the criteria")
203
204        # ファイルを日付順にソート
205        sorted_files = (
206            sorted(self._excel_files.items(), key=lambda x: self._extract_date(x[0]))
207            if sort_by_date
208            else self._excel_files.items()
209        )
210
211        # 各シートのデータを格納するリスト
212        sheet_dfs = {sheet_name: [] for sheet_name in sheet_names}
213
214        # 各ファイルからデータを読み込む
215        for filename, excel_file in sorted_files:
216            file_date = self._extract_date(filename)
217
218            for sheet_name in sheet_names:
219                if sheet_name in excel_file.sheet_names:
220                    df = pd.read_excel(
221                        excel_file,
222                        sheet_name=sheet_name,
223                        header=header,
224                        skiprows=skiprows,
225                        na_values=self._na_values,
226                    )
227                    # 年と月を追加
228                    df["year"] = file_date.year
229                    df["month"] = file_date.month
230                    sheet_dfs[sheet_name].append(df)
231
232        if not any(sheet_dfs.values()):
233            raise ValueError(f"No sheets found matching: {sheet_names}")
234
235        # 各シートのデータを結合
236        combined_sheets = {}
237        for sheet_name, dfs in sheet_dfs.items():
238            if dfs:  # シートにデータがある場合のみ結合
239                combined_sheets[sheet_name] = pd.concat(dfs, ignore_index=True)
240
241        # 最初のシートをベースにする
242        base_df = combined_sheets[sheet_names[0]]
243
244        # 2つ目以降のシートを結合
245        for sheet_name in sheet_names[1:]:
246            if sheet_name in combined_sheets:
247                base_df = self.merge_dataframes(
248                    base_df, combined_sheets[sheet_name], date_column=col_datetime
249                )
250
251        # 日付でフィルタリング
252        if start_date:
253            start_dt = pd.to_datetime(start_date)
254            base_df = base_df[base_df[col_datetime] >= start_dt]
255
256        if end_date:
257            end_dt = pd.to_datetime(end_date)
258            if include_end_date:
259                end_dt += pd.Timedelta(days=1)
260            base_df = base_df[base_df[col_datetime] < end_dt]
261
262        # カラムの選択
263        if columns is not None:
264            required_columns = [col_datetime, "year", "month"]
265            available_columns = base_df.columns.tolist()  # 利用可能なカラムを取得
266            if not all(col in available_columns for col in columns):
267                raise ValueError(
268                    f"指定されたカラムが見つかりません: {columns}. 利用可能なカラム: {available_columns}"
269                )
270            selected_columns = list(set(columns + required_columns))
271            base_df = base_df[selected_columns]
272
273        return base_df
274
275    def __enter__(self) -> "MonthlyConverter":
276        return self
277
278    def __exit__(self, exc_type, exc_val, exc_tb) -> None:
279        self.close()
280
281    @staticmethod
282    def get_last_day_of_month(year: int, month: int) -> int:
283        """
284        指定された年月の最終日を返します。
285
286        Parameters
287        ----------
288            year: int
289                年を指定します。
290            month: int
291                月を指定します。1から12の整数を指定してください。
292
293        Returns
294        ----------
295            int
296                指定された年月の最終日の日付。1から31の整数で返されます。
297
298        Examples
299        ----------
300        >>> MonthlyConverter.get_last_day_of_month(2023, 2)
301        28
302        >>> MonthlyConverter.get_last_day_of_month(2024, 2)  # 閏年の場合
303        29
304        """
305        next_month = (
306            datetime(year, month % 12 + 1, 1)
307            if month < 12
308            else datetime(year + 1, 1, 1)
309        )
310        last_day = (next_month - timedelta(days=1)).day
311        return last_day
312
313    @staticmethod
314    def extract_period_data(
315        df: pd.DataFrame,
316        start_date: str | pd.Timestamp,
317        end_date: str | pd.Timestamp,
318        include_end_date: bool = True,
319        datetime_column: str = "Date",
320    ) -> pd.DataFrame:
321        """
322        指定された期間のデータを抽出します。
323
324        Parameters
325        ----------
326            df: pd.DataFrame
327                入力データフレーム。
328            start_date: str | pd.Timestamp
329                開始日。YYYY-MM-DD形式の文字列またはTimestampで指定します。
330            end_date: str | pd.Timestamp
331                終了日。YYYY-MM-DD形式の文字列またはTimestampで指定します。
332            include_end_date: bool, optional
333                終了日を含めるかどうかを指定します。デフォルトはTrueです。
334            datetime_column: str, optional
335                日付を含む列の名前を指定します。デフォルトは"Date"です。
336
337        Returns
338        ----------
339            pd.DataFrame
340                指定された期間のデータのみを含むデータフレーム。
341
342        Examples
343        ----------
344        >>> df = pd.DataFrame({
345        ...     'Date': ['2023-01-01', '2023-01-02', '2023-01-03'],
346        ...     'Value': [1, 2, 3]
347        ... })
348        >>> MonthlyConverter.extract_period_data(
349        ...     df,
350        ...     '2023-01-01',
351        ...     '2023-01-02'
352        ... )
353           Date  Value
354        0  2023-01-01  1
355        1  2023-01-02  2
356        """
357        # データフレームのコピーを作成
358        df_internal = df.copy()
359        df_internal[datetime_column] = pd.to_datetime(df_internal[datetime_column])
360        start_dt = pd.to_datetime(start_date)
361        end_dt = pd.to_datetime(end_date)
362
363        # 開始日と終了日の順序チェック
364        if start_dt > end_dt:
365            raise ValueError("start_date は end_date より前である必要があります")
366
367        # 期間でフィルタリング
368        period_data = df_internal[
369            (df_internal[datetime_column] >= start_dt)
370            & (
371                df_internal[datetime_column]
372                < (end_dt + pd.Timedelta(days=1) if include_end_date else end_dt)
373            )
374        ]
375
376        return period_data
377
378    @staticmethod
379    def merge_dataframes(
380        df1: pd.DataFrame, df2: pd.DataFrame, date_column: str = "Date"
381    ) -> pd.DataFrame:
382        """
383        2つのDataFrameを結合します。重複するカラムは元の名前とサフィックス付きの両方を保持します。
384
385        Parameters
386        ----------
387            df1: pd.DataFrame
388                ベースとなるデータフレームを指定します。
389            df2: pd.DataFrame
390                結合するデータフレームを指定します。
391            date_column: str, optional
392                日付カラムの名前を指定します。デフォルトは"Date"です。
393
394        Returns
395        ----------
396            pd.DataFrame
397                結合されたデータフレームを返します。重複するカラムには_xと_yのサフィックスが付与されます。
398
399        Examples
400        ----------
401        >>> df1 = pd.DataFrame({
402        ...     'Date': ['2023-01-01', '2023-01-02'],
403        ...     'Value': [1, 2]
404        ... })
405        >>> df2 = pd.DataFrame({
406        ...     'Date': ['2023-01-01', '2023-01-02'],
407        ...     'Value': [10, 20]
408        ... })
409        >>> MonthlyConverter.merge_dataframes(df1, df2)
410               Date  Value  Value_x  Value_y
411        0  2023-01-01   1       1       10
412        1  2023-01-02   2       2       20
413        """
414        # インデックスをリセット
415        df1 = df1.reset_index(drop=True)
416        df2 = df2.reset_index(drop=True)
417
418        # 日付カラムを統一
419        df2[date_column] = df1[date_column]
420
421        # 重複しないカラムと重複するカラムを分離
422        duplicate_cols = [date_column, "year", "month"]  # 常に除外するカラム
423        overlapping_cols = [
424            col
425            for col in df2.columns
426            if col in df1.columns and col not in duplicate_cols
427        ]
428        unique_cols = [
429            col
430            for col in df2.columns
431            if col not in df1.columns and col not in duplicate_cols
432        ]
433
434        # 結果のDataFrameを作成
435        result = df1.copy()
436
437        # 重複しないカラムを追加
438        for col in unique_cols:
439            result[col] = df2[col]
440
441        # 重複するカラムを処理
442        for col in overlapping_cols:
443            # 元のカラムはdf1の値を保持(既に result に含まれている)
444            # _x サフィックスでdf1の値を追加
445            result[f"{col}_x"] = df1[col]
446            # _y サフィックスでdf2の値を追加
447            result[f"{col}_y"] = df2[col]
448
449        return result
450
451    def _extract_date(self, filename: str) -> datetime:
452        """
453        ファイル名から日付を抽出する
454
455        Parameters
456        ----------
457            filename: str
458                "SA.Ultra.yyyy.MM.xlsx"または"SA.Picaro.yyyy.MM.xlsx"形式のファイル名
459
460        Returns
461        ----------
462            datetime
463                抽出された日付
464        """
465        # ファイル名から日付部分を抽出
466        date_str = ".".join(filename.split(".")[-3:-1])  # "yyyy.MM"の部分を取得
467        return datetime.strptime(date_str, self.FILE_DATE_FORMAT)
468
469    def _load_excel_files(
470        self, start_date: str | None = None, end_date: str | None = None
471    ) -> None:
472        """
473        指定された日付範囲のExcelファイルを読み込む
474
475        Parameters
476        ----------
477            start_date: str | None
478                開始日 ('yyyy-MM-dd'形式)
479            end_date: str | None
480                終了日 ('yyyy-MM-dd'形式)
481        """
482        # 期間指定がある場合は、yyyy-MM-dd形式から年月のみを抽出
483        start_dt = None
484        end_dt = None
485        if start_date:
486            temp_dt = datetime.strptime(start_date, self.PERIOD_DATE_FORMAT)
487            start_dt = datetime(temp_dt.year, temp_dt.month, 1)
488        if end_date:
489            temp_dt = datetime.strptime(end_date, self.PERIOD_DATE_FORMAT)
490            end_dt = datetime(temp_dt.year, temp_dt.month, 1)
491
492        # 既存のファイルをクリア
493        self.close()
494
495        for excel_path in self._directory.glob(self._file_pattern):
496            try:
497                file_date = self._extract_date(excel_path.name)
498
499                # 日付範囲チェック
500                if start_dt and file_date < start_dt:
501                    continue
502                if end_dt and file_date > end_dt:
503                    continue
504
505                if excel_path.name not in self._excel_files:
506                    self._excel_files[excel_path.name] = pd.ExcelFile(excel_path)
507
508            except ValueError as e:
509                self.logger.warning(
510                    f"Could not parse date from file {excel_path.name}: {e}"
511                )
512
513    @staticmethod
514    def extract_monthly_data(
515        df: pd.DataFrame,
516        target_months: list[int],
517        start_day: int | None = None,
518        end_day: int | None = None,
519        datetime_column: str = "Date",
520    ) -> pd.DataFrame:
521        """
522        指定された月と期間のデータを抽出します。
523
524        Parameters
525        ----------
526            df: pd.DataFrame
527                入力データフレーム。
528            target_months: list[int]
529                抽出したい月のリスト(1から12の整数)。
530            start_day: int | None
531                開始日(1から31の整数)。Noneの場合は月初め。
532            end_day: int | None
533                終了日(1から31の整数)。Noneの場合は月末。
534            datetime_column: str, optional
535                日付を含む列の名前。デフォルトは"Date"。
536
537        Returns
538        ----------
539            pd.DataFrame
540                指定された期間のデータのみを含むデータフレーム。
541
542        .. warning::
543            このメソッドは非推奨です。代わりに `extract_period_data` を使用してください。
544            v1.0.0 で削除される予定です。
545        """
546        try:
547            ver = version("py_flux_tracer")
548            # print(ver)
549            if ver.startswith("0."):
550                warnings.warn(
551                    "`extract_monthly_data` is deprecated. Please use `extract_period_data` instead. This method will be removed in v1.0.0.",
552                    FutureWarning,
553                    stacklevel=2,  # 警告メッセージでライブラリの内部実装ではなく、非推奨のメソッドを使用しているユーザーのコードの行を指し示すことができる
554                )
555        except Exception:
556            pass
557
558        # 入力チェック
559        if not all(1 <= month <= 12 for month in target_months):
560            raise ValueError("target_monthsは1から12の間である必要があります")
561
562        if start_day is not None and not 1 <= start_day <= 31:
563            raise ValueError("start_dayは1から31の間である必要があります")
564
565        if end_day is not None and not 1 <= end_day <= 31:
566            raise ValueError("end_dayは1から31の間である必要があります")
567
568        if start_day is not None and end_day is not None and start_day > end_day:
569            raise ValueError("start_dayはend_day以下である必要があります")
570
571        # datetime_column をDatetime型に変換
572        df_internal = df.copy()
573        df_internal[datetime_column] = pd.to_datetime(df_internal[datetime_column])
574
575        # 月でフィルタリング
576        monthly_data = df_internal[
577            df_internal[datetime_column].dt.month.isin(target_months)
578        ]
579
580        # 日付範囲でフィルタリング
581        if start_day is not None:
582            monthly_data = monthly_data[
583                monthly_data[datetime_column].dt.day >= start_day
584            ]
585        if end_day is not None:
586            monthly_data = monthly_data[monthly_data[datetime_column].dt.day <= end_day]
587
588        return monthly_data

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

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

Parameters

directory: str | Path
    Excelファイルが格納されているディレクトリのパス
file_pattern: str, optional
    ファイル名のパターン。デフォルト値は'SA.Ultra.*.xlsx'です。
na_values: list[str] | None, optional
    NaNと判定する値のパターン。デフォルト値はNoneで、その場合は以下の値が使用されます:


    
["#DIV/0!", "#VALUE!", "#REF!", "#N/A", "#NAME?", "NAN", "nan"]
logger: Logger | None, optional 使用するロガー。デフォルト値はNoneで、その場合は新しいロガーが作成されます。 logging_debug: bool, optional ログレベルを"DEBUG"に設定するかどうか。デフォルト値はFalseで、その場合はINFO以上のレベルのメッセージが出力されます。

Examples

>>> converter = MonthlyConverter("path/to/excel/files")
>>> converter = MonthlyConverter(
...     "path/to/excel/files",
...     file_pattern="SA.Picaro.*.xlsx",
...     logging_debug=True
... )
FILE_DATE_FORMAT = '%Y.%m'
PERIOD_DATE_FORMAT = '%Y-%m-%d'
logger: logging.Logger
def close(self) -> None:
76    def close(self) -> None:
77        """
78        すべてのExcelファイルをクローズする
79        """
80        for excel_file in self._excel_files.values():
81            excel_file.close()
82        self._excel_files.clear()

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

def get_available_dates(self) -> list[str]:
 84    def get_available_dates(self) -> list[str]:
 85        """
 86        利用可能なファイルの日付一覧を返却します。
 87
 88        Returns
 89        ----------
 90            list[str]
 91                'yyyy.MM'形式の日付リスト
 92
 93        Examples
 94        --------
 95        >>> converter = MonthlyConverter("path/to/excel/files")
 96        >>> dates = converter.get_available_dates()
 97        >>> print(dates)
 98        ['2023.01', '2023.02', '2023.03']
 99        """
100        dates = []
101        for filename in self._directory.glob(self._file_pattern):
102            try:
103                date = self._extract_date(filename.name)
104                dates.append(date.strftime(self.FILE_DATE_FORMAT))
105            except ValueError:
106                continue
107        return sorted(dates)

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

Returns

list[str]
    'yyyy.MM'形式の日付リスト

Examples

>>> converter = MonthlyConverter("path/to/excel/files")
>>> dates = converter.get_available_dates()
>>> print(dates)
['2023.01', '2023.02', '2023.03']
def get_sheet_names(self, filename: str) -> list[str]:
109    def get_sheet_names(self, filename: str) -> list[str]:
110        """
111        指定されたファイルで利用可能なシート名の一覧を返却します。
112
113        Parameters
114        ----------
115            filename: str
116                対象のExcelファイル名を指定します。ファイル名のみを指定し、パスは含めません。
117
118        Returns
119        ----------
120            list[str]
121                シート名のリスト
122
123        Examples
124        --------
125        >>> converter = MonthlyConverter("path/to/excel/files")
126        >>> sheets = converter.get_sheet_names("2023.01.xlsx")
127        >>> print(sheets)
128        ['Sheet1', 'Sheet2', 'Sheet3']
129        """
130        if filename not in self._excel_files:
131            filepath = self._directory / filename
132            if not filepath.exists():
133                raise FileNotFoundError(f"File not found: {filepath}")
134            self._excel_files[filename] = pd.ExcelFile(filepath)
135        return [str(name) for name in self._excel_files[filename].sheet_names]

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

Parameters

filename: str
    対象のExcelファイル名を指定します。ファイル名のみを指定し、パスは含めません。

Returns

list[str]
    シート名のリスト

Examples

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

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

Parameters

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

Returns

pd.DataFrame
    読み込まれたデータを結合したDataFrameを返します。

Examples

>>> converter = MonthlyConverter("path/to/excel/files")
>>> # 単一シートの読み込み
>>> df = converter.read_sheets("Sheet1")
>>> # 複数シートの読み込み
>>> df = converter.read_sheets(["Sheet1", "Sheet2"])
>>> # 特定の期間のデータ読み込み
>>> df = converter.read_sheets(
...     "Sheet1",
...     start_date="2023-01-01",
...     end_date="2023-12-31"
... )
@staticmethod
def get_last_day_of_month(year: int, month: int) -> int:
281    @staticmethod
282    def get_last_day_of_month(year: int, month: int) -> int:
283        """
284        指定された年月の最終日を返します。
285
286        Parameters
287        ----------
288            year: int
289                年を指定します。
290            month: int
291                月を指定します。1から12の整数を指定してください。
292
293        Returns
294        ----------
295            int
296                指定された年月の最終日の日付。1から31の整数で返されます。
297
298        Examples
299        ----------
300        >>> MonthlyConverter.get_last_day_of_month(2023, 2)
301        28
302        >>> MonthlyConverter.get_last_day_of_month(2024, 2)  # 閏年の場合
303        29
304        """
305        next_month = (
306            datetime(year, month % 12 + 1, 1)
307            if month < 12
308            else datetime(year + 1, 1, 1)
309        )
310        last_day = (next_month - timedelta(days=1)).day
311        return last_day

指定された年月の最終日を返します。

Parameters

year: int
    年を指定します。
month: int
    月を指定します。1から12の整数を指定してください。

Returns

int
    指定された年月の最終日の日付。1から31の整数で返されます。

Examples

>>> MonthlyConverter.get_last_day_of_month(2023, 2)
28
>>> MonthlyConverter.get_last_day_of_month(2024, 2)  # 閏年の場合
29
@staticmethod
def extract_period_data( df: pandas.core.frame.DataFrame, start_date: str | pandas._libs.tslibs.timestamps.Timestamp, end_date: str | pandas._libs.tslibs.timestamps.Timestamp, include_end_date: bool = True, datetime_column: str = 'Date') -> pandas.core.frame.DataFrame:
313    @staticmethod
314    def extract_period_data(
315        df: pd.DataFrame,
316        start_date: str | pd.Timestamp,
317        end_date: str | pd.Timestamp,
318        include_end_date: bool = True,
319        datetime_column: str = "Date",
320    ) -> pd.DataFrame:
321        """
322        指定された期間のデータを抽出します。
323
324        Parameters
325        ----------
326            df: pd.DataFrame
327                入力データフレーム。
328            start_date: str | pd.Timestamp
329                開始日。YYYY-MM-DD形式の文字列またはTimestampで指定します。
330            end_date: str | pd.Timestamp
331                終了日。YYYY-MM-DD形式の文字列またはTimestampで指定します。
332            include_end_date: bool, optional
333                終了日を含めるかどうかを指定します。デフォルトはTrueです。
334            datetime_column: str, optional
335                日付を含む列の名前を指定します。デフォルトは"Date"です。
336
337        Returns
338        ----------
339            pd.DataFrame
340                指定された期間のデータのみを含むデータフレーム。
341
342        Examples
343        ----------
344        >>> df = pd.DataFrame({
345        ...     'Date': ['2023-01-01', '2023-01-02', '2023-01-03'],
346        ...     'Value': [1, 2, 3]
347        ... })
348        >>> MonthlyConverter.extract_period_data(
349        ...     df,
350        ...     '2023-01-01',
351        ...     '2023-01-02'
352        ... )
353           Date  Value
354        0  2023-01-01  1
355        1  2023-01-02  2
356        """
357        # データフレームのコピーを作成
358        df_internal = df.copy()
359        df_internal[datetime_column] = pd.to_datetime(df_internal[datetime_column])
360        start_dt = pd.to_datetime(start_date)
361        end_dt = pd.to_datetime(end_date)
362
363        # 開始日と終了日の順序チェック
364        if start_dt > end_dt:
365            raise ValueError("start_date は end_date より前である必要があります")
366
367        # 期間でフィルタリング
368        period_data = df_internal[
369            (df_internal[datetime_column] >= start_dt)
370            & (
371                df_internal[datetime_column]
372                < (end_dt + pd.Timedelta(days=1) if include_end_date else end_dt)
373            )
374        ]
375
376        return period_data

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

Parameters

df: pd.DataFrame
    入力データフレーム。
start_date: str | pd.Timestamp
    開始日。YYYY-MM-DD形式の文字列またはTimestampで指定します。
end_date: str | pd.Timestamp
    終了日。YYYY-MM-DD形式の文字列またはTimestampで指定します。
include_end_date: bool, optional
    終了日を含めるかどうかを指定します。デフォルトはTrueです。
datetime_column: str, optional
    日付を含む列の名前を指定します。デフォルトは"Date"です。

Returns

pd.DataFrame
    指定された期間のデータのみを含むデータフレーム。

Examples

>>> df = pd.DataFrame({
...     'Date': ['2023-01-01', '2023-01-02', '2023-01-03'],
...     'Value': [1, 2, 3]
... })
>>> MonthlyConverter.extract_period_data(
...     df,
...     '2023-01-01',
...     '2023-01-02'
... )
   Date  Value
0  2023-01-01  1
1  2023-01-02  2
@staticmethod
def merge_dataframes( df1: pandas.core.frame.DataFrame, df2: pandas.core.frame.DataFrame, date_column: str = 'Date') -> pandas.core.frame.DataFrame:
378    @staticmethod
379    def merge_dataframes(
380        df1: pd.DataFrame, df2: pd.DataFrame, date_column: str = "Date"
381    ) -> pd.DataFrame:
382        """
383        2つのDataFrameを結合します。重複するカラムは元の名前とサフィックス付きの両方を保持します。
384
385        Parameters
386        ----------
387            df1: pd.DataFrame
388                ベースとなるデータフレームを指定します。
389            df2: pd.DataFrame
390                結合するデータフレームを指定します。
391            date_column: str, optional
392                日付カラムの名前を指定します。デフォルトは"Date"です。
393
394        Returns
395        ----------
396            pd.DataFrame
397                結合されたデータフレームを返します。重複するカラムには_xと_yのサフィックスが付与されます。
398
399        Examples
400        ----------
401        >>> df1 = pd.DataFrame({
402        ...     'Date': ['2023-01-01', '2023-01-02'],
403        ...     'Value': [1, 2]
404        ... })
405        >>> df2 = pd.DataFrame({
406        ...     'Date': ['2023-01-01', '2023-01-02'],
407        ...     'Value': [10, 20]
408        ... })
409        >>> MonthlyConverter.merge_dataframes(df1, df2)
410               Date  Value  Value_x  Value_y
411        0  2023-01-01   1       1       10
412        1  2023-01-02   2       2       20
413        """
414        # インデックスをリセット
415        df1 = df1.reset_index(drop=True)
416        df2 = df2.reset_index(drop=True)
417
418        # 日付カラムを統一
419        df2[date_column] = df1[date_column]
420
421        # 重複しないカラムと重複するカラムを分離
422        duplicate_cols = [date_column, "year", "month"]  # 常に除外するカラム
423        overlapping_cols = [
424            col
425            for col in df2.columns
426            if col in df1.columns and col not in duplicate_cols
427        ]
428        unique_cols = [
429            col
430            for col in df2.columns
431            if col not in df1.columns and col not in duplicate_cols
432        ]
433
434        # 結果のDataFrameを作成
435        result = df1.copy()
436
437        # 重複しないカラムを追加
438        for col in unique_cols:
439            result[col] = df2[col]
440
441        # 重複するカラムを処理
442        for col in overlapping_cols:
443            # 元のカラムはdf1の値を保持(既に result に含まれている)
444            # _x サフィックスでdf1の値を追加
445            result[f"{col}_x"] = df1[col]
446            # _y サフィックスでdf2の値を追加
447            result[f"{col}_y"] = df2[col]
448
449        return result

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

Parameters

df1: pd.DataFrame
    ベースとなるデータフレームを指定します。
df2: pd.DataFrame
    結合するデータフレームを指定します。
date_column: str, optional
    日付カラムの名前を指定します。デフォルトは"Date"です。

Returns

pd.DataFrame
    結合されたデータフレームを返します。重複するカラムには_xと_yのサフィックスが付与されます。

Examples

>>> df1 = pd.DataFrame({
...     'Date': ['2023-01-01', '2023-01-02'],
...     'Value': [1, 2]
... })
>>> df2 = pd.DataFrame({
...     'Date': ['2023-01-01', '2023-01-02'],
...     'Value': [10, 20]
... })
>>> MonthlyConverter.merge_dataframes(df1, df2)
       Date  Value  Value_x  Value_y
0  2023-01-01   1       1       10
1  2023-01-02   2       2       20
@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:
513    @staticmethod
514    def extract_monthly_data(
515        df: pd.DataFrame,
516        target_months: list[int],
517        start_day: int | None = None,
518        end_day: int | None = None,
519        datetime_column: str = "Date",
520    ) -> pd.DataFrame:
521        """
522        指定された月と期間のデータを抽出します。
523
524        Parameters
525        ----------
526            df: pd.DataFrame
527                入力データフレーム。
528            target_months: list[int]
529                抽出したい月のリスト(1から12の整数)。
530            start_day: int | None
531                開始日(1から31の整数)。Noneの場合は月初め。
532            end_day: int | None
533                終了日(1から31の整数)。Noneの場合は月末。
534            datetime_column: str, optional
535                日付を含む列の名前。デフォルトは"Date"。
536
537        Returns
538        ----------
539            pd.DataFrame
540                指定された期間のデータのみを含むデータフレーム。
541
542        .. warning::
543            このメソッドは非推奨です。代わりに `extract_period_data` を使用してください。
544            v1.0.0 で削除される予定です。
545        """
546        try:
547            ver = version("py_flux_tracer")
548            # print(ver)
549            if ver.startswith("0."):
550                warnings.warn(
551                    "`extract_monthly_data` is deprecated. Please use `extract_period_data` instead. This method will be removed in v1.0.0.",
552                    FutureWarning,
553                    stacklevel=2,  # 警告メッセージでライブラリの内部実装ではなく、非推奨のメソッドを使用しているユーザーのコードの行を指し示すことができる
554                )
555        except Exception:
556            pass
557
558        # 入力チェック
559        if not all(1 <= month <= 12 for month in target_months):
560            raise ValueError("target_monthsは1から12の間である必要があります")
561
562        if start_day is not None and not 1 <= start_day <= 31:
563            raise ValueError("start_dayは1から31の間である必要があります")
564
565        if end_day is not None and not 1 <= end_day <= 31:
566            raise ValueError("end_dayは1から31の間である必要があります")
567
568        if start_day is not None and end_day is not None and start_day > end_day:
569            raise ValueError("start_dayはend_day以下である必要があります")
570
571        # datetime_column をDatetime型に変換
572        df_internal = df.copy()
573        df_internal[datetime_column] = pd.to_datetime(df_internal[datetime_column])
574
575        # 月でフィルタリング
576        monthly_data = df_internal[
577            df_internal[datetime_column].dt.month.isin(target_months)
578        ]
579
580        # 日付範囲でフィルタリング
581        if start_day is not None:
582            monthly_data = monthly_data[
583                monthly_data[datetime_column].dt.day >= start_day
584            ]
585        if end_day is not None:
586            monthly_data = monthly_data[monthly_data[datetime_column].dt.day <= end_day]
587
588        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
    指定された期間のデータのみを含むデータフレーム。

このメソッドは非推奨です。代わりに extract_period_data を使用してください。 v1.0.0 で削除される予定です。

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

Monthlyシートから作成したDataFrameを解析して図を作成するクラス

Parameters

logger: Logger | None
    使用するロガー。Noneの場合は新しいロガーを作成します。
logging_debug: bool
    ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。
logger: logging.Logger
def plot_c1c2_concs_and_fluxes_timeseries( self, df: pandas.core.frame.DataFrame, output_dirpath: str | pathlib.Path | None = None, 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', ylim_ch4_conc: tuple = (1.8, 2.6), ylim_ch4_flux: tuple = (-100, 600), ylim_c2h6_conc: tuple = (-12, 20), ylim_c2h6_flux: tuple = (-20, 40), figsize: tuple[float, float] = (12, 16), dpi: float | None = 350, save_fig: bool = True, show_fig: bool = True, print_summary: bool = False) -> None:
 85    def plot_c1c2_concs_and_fluxes_timeseries(
 86        self,
 87        df: pd.DataFrame,
 88        output_dirpath: str | Path | None = None,
 89        output_filename: str = "conc_flux_timeseries.png",
 90        col_datetime: str = "Date",
 91        col_ch4_conc: str = "CH4_ultra",
 92        col_ch4_flux: str = "Fch4_ultra",
 93        col_c2h6_conc: str = "C2H6_ultra",
 94        col_c2h6_flux: str = "Fc2h6_ultra",
 95        ylim_ch4_conc: tuple = (1.8, 2.6),
 96        ylim_ch4_flux: tuple = (-100, 600),
 97        ylim_c2h6_conc: tuple = (-12, 20),
 98        ylim_c2h6_flux: tuple = (-20, 40),
 99        figsize: tuple[float, float] = (12, 16),
100        dpi: float | None = 350,
101        save_fig: bool = True,
102        show_fig: bool = True,
103        print_summary: bool = False,
104    ) -> None:
105        """CH4とC2H6の濃度とフラックスの時系列プロットを作成します。
106
107        Parameters
108        ----------
109            df: pd.DataFrame
110                月別データを含むDataFrameを指定します。
111            output_dirpath: str | Path | None, optional
112                出力ディレクトリのパスを指定します。save_fig=Trueの場合は必須です。
113            output_filename: str, optional
114                出力ファイル名を指定します。デフォルト値は"conc_flux_timeseries.png"です。
115            col_datetime: str, optional
116                日付列の名前を指定します。デフォルト値は"Date"です。
117            col_ch4_conc: str, optional
118                CH4濃度列の名前を指定します。デフォルト値は"CH4_ultra"です。
119            col_ch4_flux: str, optional
120                CH4フラックス列の名前を指定します。デフォルト値は"Fch4_ultra"です。
121            col_c2h6_conc: str, optional
122                C2H6濃度列の名前を指定します。デフォルト値は"C2H6_ultra"です。
123            col_c2h6_flux: str, optional
124                C2H6フラックス列の名前を指定します。デフォルト値は"Fc2h6_ultra"です。
125            ylim_ch4_conc: tuple, optional
126                CH4濃度のy軸範囲を指定します。デフォルト値は(1.8, 2.6)です。
127            ylim_ch4_flux: tuple, optional
128                CH4フラックスのy軸範囲を指定します。デフォルト値は(-100, 600)です。
129            ylim_c2h6_conc: tuple, optional
130                C2H6濃度のy軸範囲を指定します。デフォルト値は(-12, 20)です。
131            ylim_c2h6_flux: tuple, optional
132                C2H6フラックスのy軸範囲を指定します。デフォルト値は(-20, 40)です。
133            figsize: tuple[float, float], optional
134                プロットのサイズを指定します。デフォルト値は(12, 16)です。
135            dpi: float | None, optional
136                プロットのdpiを指定します。デフォルト値は350です。
137            save_fig: bool, optional
138                図を保存するかどうかを指定します。デフォルト値はTrueです。
139            show_fig: bool, optional
140                図を表示するかどうかを指定します。デフォルト値はTrueです。
141            print_summary: bool, optional
142                解析情報をprintするかどうかを指定します。デフォルト値はFalseです。
143
144        Examples
145        --------
146        >>> generator = MonthlyFiguresGenerator()
147        >>> generator.plot_c1c2_concs_and_fluxes_timeseries(
148        ...     df=monthly_data,
149        ...     output_dirpath="output",
150        ...     ylim_ch4_conc=(1.5, 3.0),
151        ...     print_summary=True
152        ... )
153        """
154        # dfのコピー
155        df_internal: pd.DataFrame = df.copy()
156        if print_summary:
157            # 統計情報の計算と表示
158            for name, col in [
159                ("CH4 concentration", col_ch4_conc),
160                ("CH4 flux", col_ch4_flux),
161                ("C2H6 concentration", col_c2h6_conc),
162                ("C2H6 flux", col_c2h6_flux),
163            ]:
164                # NaNを除外してから統計量を計算
165                valid_data = df_internal[col].dropna()
166
167                if len(valid_data) > 0:
168                    # quantileで計算(0-1の範囲)
169                    quantile_05 = valid_data.quantile(0.05)
170                    quantile_95 = valid_data.quantile(0.95)
171                    mean_value = np.nanmean(valid_data)
172                    positive_ratio = (valid_data > 0).mean() * 100
173
174                    print(f"\n{name}:")
175                    # パーセンタイルで表示(0-100の範囲)
176                    print(
177                        f"90パーセンタイルレンジ: {quantile_05:.2f} - {quantile_95:.2f}"
178                    )
179                    print(f"平均値: {mean_value:.2f}")
180                    print(f"正の値の割合: {positive_ratio:.1f}%")
181                else:
182                    print(f"\n{name}: データが存在しません")
183
184        # プロットの作成
185        _, (ax1, ax2, ax3, ax4) = plt.subplots(4, 1, figsize=figsize, sharex=True)
186
187        # CH4濃度のプロット
188        ax1.scatter(
189            df_internal[col_datetime],
190            df_internal[col_ch4_conc],
191            color="red",
192            alpha=0.5,
193            s=20,
194        )
195        ax1.set_ylabel("CH$_4$ Concentration\n(ppm)")
196        ax1.set_ylim(*ylim_ch4_conc)  # 引数からy軸範囲を設定
197        ax1.text(0.02, 0.98, "(a)", transform=ax1.transAxes, va="top", fontsize=20)
198        ax1.grid(True, alpha=0.3)
199
200        # CH4フラックスのプロット
201        ax2.scatter(
202            df_internal[col_datetime],
203            df_internal[col_ch4_flux],
204            color="red",
205            alpha=0.5,
206            s=20,
207        )
208        ax2.set_ylabel("CH$_4$ flux\n(nmol m$^{-2}$ s$^{-1}$)")
209        ax2.set_ylim(*ylim_ch4_flux)  # 引数からy軸範囲を設定
210        ax2.text(0.02, 0.98, "(b)", transform=ax2.transAxes, va="top", fontsize=20)
211        ax2.grid(True, alpha=0.3)
212
213        # C2H6濃度のプロット
214        ax3.scatter(
215            df_internal[col_datetime],
216            df_internal[col_c2h6_conc],
217            color="orange",
218            alpha=0.5,
219            s=20,
220        )
221        ax3.set_ylabel("C$_2$H$_6$ Concentration\n(ppb)")
222        ax3.set_ylim(*ylim_c2h6_conc)  # 引数からy軸範囲を設定
223        ax3.text(0.02, 0.98, "(c)", transform=ax3.transAxes, va="top", fontsize=20)
224        ax3.grid(True, alpha=0.3)
225
226        # C2H6フラックスのプロット
227        ax4.scatter(
228            df_internal[col_datetime],
229            df_internal[col_c2h6_flux],
230            color="orange",
231            alpha=0.5,
232            s=20,
233        )
234        ax4.set_ylabel("C$_2$H$_6$ flux\n(nmol m$^{-2}$ s$^{-1}$)")
235        ax4.set_ylim(*ylim_c2h6_flux)  # 引数からy軸範囲を設定
236        ax4.text(0.02, 0.98, "(d)", transform=ax4.transAxes, va="top", fontsize=20)
237        ax4.grid(True, alpha=0.3)
238
239        # x軸の設定
240        ax4.xaxis.set_major_locator(mdates.MonthLocator(interval=1))
241        ax4.xaxis.set_major_formatter(mdates.DateFormatter("%m"))
242        plt.setp(ax4.get_xticklabels(), rotation=0, ha="right")
243        ax4.set_xlabel("Month")
244
245        # レイアウトの調整と保存
246        plt.tight_layout()
247
248        # 図の保存
249        if save_fig:
250            if output_dirpath is None:
251                raise ValueError(
252                    "save_fig = True のとき、 output_dirpath に有効なディレクトリパスを指定する必要があります。"
253                )
254            # 出力ディレクトリの作成
255            os.makedirs(output_dirpath, exist_ok=True)
256            output_filepath: str = os.path.join(output_dirpath, output_filename)
257            plt.savefig(output_filepath, dpi=dpi, bbox_inches="tight")
258        if show_fig:
259            plt.show()
260        plt.close()
261
262        if print_summary:
263
264            def analyze_top_values(df, column_name, top_percent=20):
265                print(f"\n{column_name}の上位{top_percent}%の分析:")
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_analysis[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_internal, col_ch4_conc)
329            analyze_top_values(df_internal, col_ch4_flux)
330            analyze_top_values(df_internal, col_c2h6_conc)
331            analyze_top_values(df_internal, col_c2h6_flux)

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

Parameters

df: pd.DataFrame
    月別データを含むDataFrameを指定します。
output_dirpath: str | Path | None, optional
    出力ディレクトリのパスを指定します。save_fig=Trueの場合は必須です。
output_filename: str, optional
    出力ファイル名を指定します。デフォルト値は"conc_flux_timeseries.png"です。
col_datetime: str, optional
    日付列の名前を指定します。デフォルト値は"Date"です。
col_ch4_conc: str, optional
    CH4濃度列の名前を指定します。デフォルト値は"CH4_ultra"です。
col_ch4_flux: str, optional
    CH4フラックス列の名前を指定します。デフォルト値は"Fch4_ultra"です。
col_c2h6_conc: str, optional
    C2H6濃度列の名前を指定します。デフォルト値は"C2H6_ultra"です。
col_c2h6_flux: str, optional
    C2H6フラックス列の名前を指定します。デフォルト値は"Fc2h6_ultra"です。
ylim_ch4_conc: tuple, optional
    CH4濃度のy軸範囲を指定します。デフォルト値は(1.8, 2.6)です。
ylim_ch4_flux: tuple, optional
    CH4フラックスのy軸範囲を指定します。デフォルト値は(-100, 600)です。
ylim_c2h6_conc: tuple, optional
    C2H6濃度のy軸範囲を指定します。デフォルト値は(-12, 20)です。
ylim_c2h6_flux: tuple, optional
    C2H6フラックスのy軸範囲を指定します。デフォルト値は(-20, 40)です。
figsize: tuple[float, float], optional
    プロットのサイズを指定します。デフォルト値は(12, 16)です。
dpi: float | None, optional
    プロットのdpiを指定します。デフォルト値は350です。
save_fig: bool, optional
    図を保存するかどうかを指定します。デフォルト値はTrueです。
show_fig: bool, optional
    図を表示するかどうかを指定します。デフォルト値はTrueです。
print_summary: bool, optional
    解析情報をprintするかどうかを指定します。デフォルト値はFalseです。

Examples

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

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

Parameters

df: pd.DataFrame
    プロットするデータを含むDataFrameを指定します。
col_ch4_flux: str
    CH4フラックスのカラム名を指定します。
col_c2h6_flux: str
    C2H6フラックスのカラム名を指定します。
output_dirpath: str | Path | None, optional
    出力ディレクトリのパスを指定します。save_fig=Trueの場合は必須です。
output_filename: str, optional
    出力ファイル名を指定します。デフォルト値は"timeseries_year.png"です。
col_datetime: str, optional
    日時カラムの名前を指定します。デフォルト値は"Date"です。
window_size: int, optional
    移動平均の窓サイズを指定します。デフォルト値は24*7(1週間)です。
confidence_interval: float, optional
    信頼区間を指定します。0から1の間の値で、デフォルト値は0.95(95%)です。
subplot_label_ch4: str | None, optional
    CH4プロットのラベルを指定します。デフォルト値は"(a)"です。
subplot_label_c2h6: str | None, optional
    C2H6プロットのラベルを指定します。デフォルト値は"(b)"です。
subplot_fontsize: int, optional
    サブプロットのフォントサイズを指定します。デフォルト値は20です。
show_ci: bool, optional
    信頼区間を表示するかどうかを指定します。デフォルト値はTrueです。
ch4_ylim: tuple[float, float] | None, optional
    CH4のy軸範囲を指定します。未指定の場合は自動で設定されます。
c2h6_ylim: tuple[float, float] | None, optional
    C2H6のy軸範囲を指定します。未指定の場合は自動で設定されます。
start_date: str | None, optional
    開始日を"YYYY-MM-DD"形式で指定します。未指定の場合はデータの最初の日付が使用されます。
end_date: str | None, optional
    終了日を"YYYY-MM-DD"形式で指定します。未指定の場合はデータの最後の日付が使用されます。
figsize: tuple[float, float], optional
    プロットのサイズを指定します。デフォルト値は(16, 6)です。
dpi: float | None, optional
    プロットのdpiを指定します。デフォルト値は350です。
save_fig: bool, optional
    プロットを保存するかどうかを指定します。デフォルト値はTrueです。
show_fig: bool, optional
    プロットを表示するかどうかを指定します。デフォルト値はTrueです。

Examples

>>> generator = MonthlyFiguresGenerator()
>>> generator.plot_c1c2_timeseries(
...     df=monthly_data,
...     col_ch4_flux="Fch4_ultra",
...     col_c2h6_flux="Fc2h6_ultra",
...     output_dirpath="output",
...     start_date="2023-01-01",
...     end_date="2023-12-31"
... )
def plot_fluxes_comparison( self, df: pandas.core.frame.DataFrame, cols_flux: list[str], labels: list[str], colors: list[str], output_dirpath: str | pathlib.Path | None, 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, ylim: tuple[float, float] | None = None, start_date: str | None = None, end_date: str | None = None, include_end_date: bool = True, 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}$)', figsize: tuple[float, float] = (12, 6), dpi: float | None = 350, save_fig: bool = True, show_fig: bool = True) -> None:
550    def plot_fluxes_comparison(
551        self,
552        df: pd.DataFrame,
553        cols_flux: list[str],
554        labels: list[str],
555        colors: list[str],
556        output_dirpath: str | Path | None,
557        output_filename: str = "ch4_flux_comparison.png",
558        col_datetime: str = "Date",
559        window_size: int = 24 * 7,
560        confidence_interval: float = 0.95,
561        subplot_label: str | None = None,
562        subplot_fontsize: int = 20,
563        show_ci: bool = True,
564        ylim: tuple[float, float] | None = None,
565        start_date: str | None = None,
566        end_date: str | None = None,
567        include_end_date: bool = True,
568        legend_loc: str = "upper right",
569        apply_ma: bool = True,
570        hourly_mean: bool = False,
571        x_interval: Literal["month", "10days"] = "month",
572        xlabel: str = "Month",
573        ylabel: str = "CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)",
574        figsize: tuple[float, float] = (12, 6),
575        dpi: float | None = 350,
576        save_fig: bool = True,
577        show_fig: bool = True,
578    ) -> None:
579        """複数のCH4フラックスの時系列変動を比較するプロットを作成します。
580
581        Parameters
582        ----------
583            df: pd.DataFrame
584                プロットするデータを含むDataFrameを指定します。
585            cols_flux: list[str]
586                比較するフラックスのカラム名のリストを指定します。
587            labels: list[str]
588                凡例に表示する各フラックスのラベルのリストを指定します。
589            colors: list[str]
590                各フラックスの色のリストを指定します。
591            output_dirpath: str | Path | None
592                出力ディレクトリのパスを指定します。save_fig=Trueの場合は必須です。
593            output_filename: str, optional
594                出力ファイル名を指定します。デフォルト値は"ch4_flux_comparison.png"です。
595            col_datetime: str, optional
596                日時カラムの名前を指定します。デフォルト値は"Date"です。
597            window_size: int, optional
598                移動平均の窓サイズを指定します。デフォルト値は24*7(1週間)です。
599            confidence_interval: float, optional
600                信頼区間を指定します。0から1の間の値で、デフォルト値は0.95(95%)です。
601            subplot_label: str | None, optional
602                プロットのラベルを指定します。デフォルト値はNoneです。
603            subplot_fontsize: int, optional
604                サブプロットのフォントサイズを指定します。デフォルト値は20です。
605            show_ci: bool, optional
606                信頼区間を表示するかどうかを指定します。デフォルト値はTrueです。
607            ylim: tuple[float, float] | None, optional
608                y軸の範囲を指定します。デフォルト値はNoneです。
609            start_date: str | None, optional
610                開始日をYYYY-MM-DD形式で指定します。デフォルト値はNoneです。
611            end_date: str | None, optional
612                終了日をYYYY-MM-DD形式で指定します。デフォルト値はNoneです。
613            include_end_date: bool, optional
614                終了日を含めるかどうかを指定します。デフォルト値はTrueです。
615            legend_loc: str, optional
616                凡例の位置を指定します。デフォルト値は"upper right"です。
617            apply_ma: bool, optional
618                移動平均を適用するかどうかを指定します。デフォルト値はTrueです。
619            hourly_mean: bool, optional
620                1時間平均を適用するかどうかを指定します。デフォルト値はFalseです。
621            x_interval: Literal["month", "10days"], optional
622                x軸の目盛り間隔を指定します。"month"(月初めのみ)または"10days"(10日刻み)を指定できます。デフォルト値は"month"です。
623            xlabel: str, optional
624                x軸のラベルを指定します。デフォルト値は"Month"です。
625            ylabel: str, optional
626                y軸のラベルを指定します。デフォルト値は"CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)"です。
627            figsize: tuple[float, float], optional
628                プロットのサイズを指定します。デフォルト値は(12, 6)です。
629            dpi: float | None, optional
630                プロットのdpiを指定します。デフォルト値は350です。
631            save_fig: bool, optional
632                図を保存するかどうかを指定します。デフォルト値はTrueです。
633            show_fig: bool, optional
634                図を表示するかどうかを指定します。デフォルト値はTrueです。
635
636        Examples
637        -------
638        >>> generator = MonthlyFiguresGenerator()
639        >>> generator.plot_fluxes_comparison(
640        ...     df=monthly_data,
641        ...     cols_flux=["Fch4_ultra", "Fch4_picarro"],
642        ...     labels=["Ultra", "Picarro"],
643        ...     colors=["red", "blue"],
644        ...     output_dirpath="output",
645        ...     start_date="2023-01-01",
646        ...     end_date="2023-12-31"
647        ... )
648        """
649        # データの準備
650        df_internal = df.copy()
651
652        # インデックスを日時型に変換
653        df_internal.index = pd.to_datetime(df_internal.index)
654
655        # 1時間平均の適用
656        if hourly_mean:
657            # 時間情報のみを使用してグループ化
658            df_internal = df_internal.groupby(
659                [df_internal.index.strftime("%Y-%m-%d"), df_internal.index.hour]
660            ).mean()
661            # マルチインデックスを日時インデックスに変換
662            df_internal.index = pd.to_datetime(
663                [f"{date} {hour:02d}:00:00" for date, hour in df_internal.index]
664            )
665
666        # 日付範囲の処理
667        if start_date is not None:
668            start_dt = pd.to_datetime(start_date).normalize()  # 時刻を00:00:00に設定
669            df_min_date = (
670                df_internal.index.normalize().min().normalize()
671            )  # 日付のみの比較のため正規化
672
673            # データの最小日付が指定開始日より後の場合にのみ警告
674            if df_min_date.date() > start_dt.date():
675                self.logger.warning(
676                    f"指定された開始日{start_date}がデータの開始日{df_min_date.strftime('%Y-%m-%d')}より前です。"
677                    f"データの開始日を使用します。"
678                )
679                start_dt = df_min_date
680        else:
681            start_dt = df_internal.index.normalize().min()
682
683        if end_date is not None:
684            if include_end_date:
685                end_dt = (
686                    pd.to_datetime(end_date).normalize()
687                    + pd.Timedelta(days=1)
688                    - pd.Timedelta(seconds=1)
689                )
690            else:
691                # 終了日を含まない場合、終了日の前日の23:59:59まで
692                end_dt = pd.to_datetime(end_date).normalize() - pd.Timedelta(seconds=1)
693
694            df_max_date = (
695                df_internal.index.normalize().max().normalize()
696            )  # 日付のみの比較のため正規化
697
698            # データの最大日付が指定終了日より前の場合にのみ警告
699            compare_date = pd.to_datetime(end_date).date()
700            if not include_end_date:
701                compare_date = compare_date - pd.Timedelta(days=1)
702
703            if df_max_date.date() < compare_date:
704                self.logger.warning(
705                    f"指定された終了日{end_date}がデータの終了日{df_max_date.strftime('%Y-%m-%d')}より後です。"
706                    f"データの終了日を使用します。"
707                )
708                end_dt = df_internal.index.max()
709        else:
710            end_dt = df_internal.index.max()
711
712        # 指定された期間のデータを抽出
713        mask = (df_internal.index >= start_dt) & (df_internal.index <= end_dt)
714        df_internal = df_internal[mask]
715
716        # プロットの作成
717        fig, ax = plt.subplots(figsize=figsize)
718
719        # 各フラックスのプロット
720        for flux_col, label, color in zip(cols_flux, labels, colors, strict=True):
721            if apply_ma:
722                # 移動平均の計算
723                mean, lower, upper = calculate_rolling_stats(
724                    df_internal[flux_col], window_size, confidence_interval
725                )
726                ax.plot(df_internal.index, mean, color, label=label, alpha=0.7)
727                if show_ci:
728                    ax.fill_between(
729                        df_internal.index, lower, upper, color=color, alpha=0.2
730                    )
731            else:
732                # 生データのプロット
733                ax.plot(
734                    df_internal.index,
735                    df_internal[flux_col],
736                    color,
737                    label=label,
738                    alpha=0.7,
739                )
740
741        # プロットの設定
742        if subplot_label:
743            ax.text(
744                0.02,
745                0.98,
746                subplot_label,
747                transform=ax.transAxes,
748                va="top",
749                fontsize=subplot_fontsize,
750            )
751
752        ax.set_xlabel(xlabel)
753        ax.set_ylabel(ylabel)
754
755        if ylim is not None:
756            ax.set_ylim(ylim)
757
758        ax.grid(True, alpha=0.3)
759        ax.legend(loc=legend_loc)
760
761        # x軸の設定
762        ax.set_xlim(float(mdates.date2num(start_dt)), float(mdates.date2num(end_dt)))
763
764        if x_interval == "month":
765            # 月初めにメジャー線のみ表示
766            ax.xaxis.set_major_locator(mdates.MonthLocator())
767            ax.xaxis.set_minor_locator(NullLocator())  # マイナー線を非表示
768        elif x_interval == "10days":
769            # 月初め(1日)、10日、20日、30日に目盛りを表示
770            class Custom10DayLocator(mdates.DateLocator):
771                def __call__(self):
772                    dmin, dmax = self.viewlim_to_dt()
773                    dates = []
774                    current = pd.to_datetime(dmin).normalize()
775                    end = pd.to_datetime(dmax).normalize()
776
777                    while current <= end:
778                        # その月の1日、10日、20日、30日を追加
779                        for day in [1, 10, 20, 30]:
780                            try:
781                                date = current.replace(day=day)
782                                if dmin <= date <= dmax:
783                                    dates.append(date)
784                            except ValueError:
785                                # 30日が存在しない月(2月など)の場合は
786                                # その月の最終日を使用
787                                if day == 30:
788                                    last_day = (
789                                        current + pd.DateOffset(months=1)
790                                    ).replace(day=1) - pd.Timedelta(days=1)
791                                    if dmin <= last_day <= dmax:
792                                        dates.append(last_day)
793
794                        # 次の月へ
795                        current = (current + pd.DateOffset(months=1)).replace(day=1)
796
797                    return self.raise_if_exceeds(
798                        [float(mdates.date2num(date)) for date in dates]
799                    )
800
801            ax.xaxis.set_major_locator(Custom10DayLocator())
802            ax.xaxis.set_minor_locator(mdates.DayLocator())
803            ax.grid(True, which="minor", alpha=0.1)
804
805        # カスタムフォーマッタの作成
806        def date_formatter(x, p):
807            date = mdates.num2date(x)
808            if x_interval == "month":
809                # 月初めの1日の場合のみ月を表示
810                if date.day == 1:
811                    return f"{date.strftime('%m')}"
812                return ""
813            else:  # "10days"の場合
814                # MM/DD形式で表示し、/を中心に配置
815                month = f"{date.strftime('%m'):>2}"  # 右寄せで2文字
816                day = f"{date.strftime('%d'):<2}"  # 左寄せで2文字
817                return f"{month}/{day}"
818
819        ax.xaxis.set_major_formatter(FuncFormatter(date_formatter))
820        plt.setp(
821            ax.xaxis.get_majorticklabels(), ha="center", rotation=0
822        )  # 中央揃えに変更
823
824        plt.tight_layout()
825
826        if save_fig:
827            if output_dirpath is None:
828                raise ValueError(
829                    "save_fig = True のとき、 output_dirpath に有効なディレクトリパスを指定する必要があります。"
830                )
831            # 出力ディレクトリの作成
832            os.makedirs(output_dirpath, exist_ok=True)
833            output_filepath: str = os.path.join(output_dirpath, output_filename)
834            plt.savefig(output_filepath, dpi=dpi, bbox_inches="tight")
835        if show_fig:
836            plt.show()
837        plt.close(fig=fig)

複数のCH4フラックスの時系列変動を比較するプロットを作成します。

Parameters

df: pd.DataFrame
    プロットするデータを含むDataFrameを指定します。
cols_flux: list[str]
    比較するフラックスのカラム名のリストを指定します。
labels: list[str]
    凡例に表示する各フラックスのラベルのリストを指定します。
colors: list[str]
    各フラックスの色のリストを指定します。
output_dirpath: str | Path | None
    出力ディレクトリのパスを指定します。save_fig=Trueの場合は必須です。
output_filename: str, optional
    出力ファイル名を指定します。デフォルト値は"ch4_flux_comparison.png"です。
col_datetime: str, optional
    日時カラムの名前を指定します。デフォルト値は"Date"です。
window_size: int, optional
    移動平均の窓サイズを指定します。デフォルト値は24*7(1週間)です。
confidence_interval: float, optional
    信頼区間を指定します。0から1の間の値で、デフォルト値は0.95(95%)です。
subplot_label: str | None, optional
    プロットのラベルを指定します。デフォルト値はNoneです。
subplot_fontsize: int, optional
    サブプロットのフォントサイズを指定します。デフォルト値は20です。
show_ci: bool, optional
    信頼区間を表示するかどうかを指定します。デフォルト値はTrueです。
ylim: tuple[float, float] | None, optional
    y軸の範囲を指定します。デフォルト値はNoneです。
start_date: str | None, optional
    開始日をYYYY-MM-DD形式で指定します。デフォルト値はNoneです。
end_date: str | None, optional
    終了日をYYYY-MM-DD形式で指定します。デフォルト値はNoneです。
include_end_date: bool, optional
    終了日を含めるかどうかを指定します。デフォルト値はTrueです。
legend_loc: str, optional
    凡例の位置を指定します。デフォルト値は"upper right"です。
apply_ma: bool, optional
    移動平均を適用するかどうかを指定します。デフォルト値はTrueです。
hourly_mean: bool, optional
    1時間平均を適用するかどうかを指定します。デフォルト値はFalseです。
x_interval: Literal["month", "10days"], optional
    x軸の目盛り間隔を指定します。"month"(月初めのみ)または"10days"(10日刻み)を指定できます。デフォルト値は"month"です。
xlabel: str, optional
    x軸のラベルを指定します。デフォルト値は"Month"です。
ylabel: str, optional
    y軸のラベルを指定します。デフォルト値は"CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)"です。
figsize: tuple[float, float], optional
    プロットのサイズを指定します。デフォルト値は(12, 6)です。
dpi: float | None, optional
    プロットのdpiを指定します。デフォルト値は350です。
save_fig: bool, optional
    図を保存するかどうかを指定します。デフォルト値はTrueです。
show_fig: bool, optional
    図を表示するかどうかを指定します。デフォルト値はTrueです。

Examples

>>> generator = MonthlyFiguresGenerator()
>>> generator.plot_fluxes_comparison(
...     df=monthly_data,
...     cols_flux=["Fch4_ultra", "Fch4_picarro"],
...     labels=["Ultra", "Picarro"],
...     colors=["red", "blue"],
...     output_dirpath="output",
...     start_date="2023-01-01",
...     end_date="2023-12-31"
... )
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_dirpath: str | pathlib.Path | None = None, 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, figsize: tuple[float, float] = (12, 5), dpi: float | None = 350, 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, save_fig: bool = True, show_fig: bool = True) -> None:
 839    def plot_c1c2_fluxes_diurnal_patterns(
 840        self,
 841        df: pd.DataFrame,
 842        y_cols_ch4: list[str],
 843        y_cols_c2h6: list[str],
 844        labels_ch4: list[str],
 845        labels_c2h6: list[str],
 846        colors_ch4: list[str],
 847        colors_c2h6: list[str],
 848        output_dirpath: str | Path | None = None,
 849        output_filename: str = "diurnal.png",
 850        legend_only_ch4: bool = False,
 851        add_label: bool = True,
 852        add_legend: bool = True,
 853        show_std: bool = False,
 854        std_alpha: float = 0.2,
 855        figsize: tuple[float, float] = (12, 5),
 856        dpi: float | None = 350,
 857        subplot_fontsize: int = 20,
 858        subplot_label_ch4: str | None = "(a)",
 859        subplot_label_c2h6: str | None = "(b)",
 860        ax1_ylim: tuple[float, float] | None = None,
 861        ax2_ylim: tuple[float, float] | None = None,
 862        save_fig: bool = True,
 863        show_fig: bool = True,
 864    ) -> None:
 865        """CH4とC2H6の日変化パターンを1つの図に並べてプロットします。
 866
 867        Parameters
 868        ----------
 869            df: pd.DataFrame
 870                入力データフレームを指定します。
 871            y_cols_ch4: list[str]
 872                CH4のプロットに使用するカラム名のリストを指定します。
 873            y_cols_c2h6: list[str]
 874                C2H6のプロットに使用するカラム名のリストを指定します。
 875            labels_ch4: list[str]
 876                CH4の各ラインに対応するラベルのリストを指定します。
 877            labels_c2h6: list[str]
 878                C2H6の各ラインに対応するラベルのリストを指定します。
 879            colors_ch4: list[str]
 880                CH4の各ラインに使用する色のリストを指定します。
 881            colors_c2h6: list[str]
 882                C2H6の各ラインに使用する色のリストを指定します。
 883            output_dirpath: str | Path | None, optional
 884                出力先ディレクトリのパスを指定します。save_fig=Trueの場合は必須です。
 885            output_filename: str, optional
 886                出力ファイル名を指定します。デフォルト値は"diurnal.png"です。
 887            legend_only_ch4: bool, optional
 888                CH4の凡例のみを表示するかどうかを指定します。デフォルト値はFalseです。
 889            add_label: bool, optional
 890                サブプロットラベルを表示するかどうかを指定します。デフォルト値はTrueです。
 891            add_legend: bool, optional
 892                凡例を表示するかどうかを指定します。デフォルト値はTrueです。
 893            show_std: bool, optional
 894                標準偏差を表示するかどうかを指定します。デフォルト値はFalseです。
 895            std_alpha: float, optional
 896                標準偏差の透明度を指定します。デフォルト値は0.2です。
 897            figsize: tuple[float, float], optional
 898                プロットのサイズを指定します。デフォルト値は(12, 5)です。
 899            dpi: float | None, optional
 900                プロットのdpiを指定します。デフォルト値は350です。
 901            subplot_fontsize: int, optional
 902                サブプロットのフォントサイズを指定します。デフォルト値は20です。
 903            subplot_label_ch4: str | None, optional
 904                CH4プロットのラベルを指定します。デフォルト値は"(a)"です。
 905            subplot_label_c2h6: str | None, optional
 906                C2H6プロットのラベルを指定します。デフォルト値は"(b)"です。
 907            ax1_ylim: tuple[float, float] | None, optional
 908                CH4プロットのy軸の範囲を指定します。デフォルト値はNoneです。
 909            ax2_ylim: tuple[float, float] | None, optional
 910                C2H6プロットのy軸の範囲を指定します。デフォルト値はNoneです。
 911            save_fig: bool, optional
 912                プロットを保存するかどうかを指定します。デフォルト値はTrueです。
 913            show_fig: bool, optional
 914                プロットを表示するかどうかを指定します。デフォルト値はTrueです。
 915
 916        Examples
 917        --------
 918        >>> generator = MonthlyFiguresGenerator()
 919        >>> generator.plot_c1c2_fluxes_diurnal_patterns(
 920        ...     df=monthly_data,
 921        ...     y_cols_ch4=["CH4_flux1", "CH4_flux2"],
 922        ...     y_cols_c2h6=["C2H6_flux1", "C2H6_flux2"],
 923        ...     labels_ch4=["CH4 1", "CH4 2"],
 924        ...     labels_c2h6=["C2H6 1", "C2H6 2"],
 925        ...     colors_ch4=["red", "blue"],
 926        ...     colors_c2h6=["green", "orange"],
 927        ...     output_dirpath="output",
 928        ...     show_std=True
 929        ... )
 930        """
 931        # データの準備
 932        df_internal: pd.DataFrame = df.copy()
 933        df_internal.index = pd.to_datetime(df_internal.index)
 934        target_columns = y_cols_ch4 + y_cols_c2h6
 935        hourly_means, time_points = self._prepare_diurnal_data(
 936            df_internal, target_columns
 937        )
 938
 939        # 標準偏差の計算を追加
 940        hourly_stds = {}
 941        if show_std:
 942            hourly_stds = df_internal.groupby(df_internal.index.hour)[
 943                target_columns
 944            ].std()
 945            # 24時間目のデータ点を追加
 946            last_hour = hourly_stds.iloc[0:1].copy()
 947            last_hour.index = pd.Index([24])
 948            hourly_stds = pd.concat([hourly_stds, last_hour])
 949
 950        # プロットの作成
 951        fig, (ax1, ax2) = plt.subplots(1, 2, figsize=figsize)
 952
 953        # CH4のプロット (左側)
 954        ch4_lines = []
 955        for col_y, label, color in zip(y_cols_ch4, labels_ch4, colors_ch4, strict=True):
 956            mean_values = hourly_means["all"][col_y]
 957            line = ax1.plot(
 958                time_points,
 959                mean_values,
 960                "-o",
 961                label=label,
 962                color=color,
 963            )
 964            ch4_lines.extend(line)
 965
 966            # 標準偏差の表示
 967            if show_std:
 968                std_values = hourly_stds[col_y]
 969                ax1.fill_between(
 970                    time_points,
 971                    mean_values - std_values,
 972                    mean_values + std_values,
 973                    color=color,
 974                    alpha=std_alpha,
 975                )
 976
 977        # C2H6のプロット (右側)
 978        c2h6_lines = []
 979        for col_y, label, color in zip(
 980            y_cols_c2h6, labels_c2h6, colors_c2h6, strict=True
 981        ):
 982            mean_values = hourly_means["all"][col_y]
 983            line = ax2.plot(
 984                time_points,
 985                mean_values,
 986                "o-",
 987                label=label,
 988                color=color,
 989            )
 990            c2h6_lines.extend(line)
 991
 992            # 標準偏差の表示
 993            if show_std:
 994                std_values = hourly_stds[col_y]
 995                ax2.fill_between(
 996                    time_points,
 997                    mean_values - std_values,
 998                    mean_values + std_values,
 999                    color=color,
1000                    alpha=std_alpha,
1001                )
1002
1003        # 軸の設定
1004        for ax, ylabel, subplot_label in [
1005            (ax1, r"CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)", subplot_label_ch4),
1006            (ax2, r"C$_2$H$_6$ flux (nmol m$^{-2}$ s$^{-1}$)", subplot_label_c2h6),
1007        ]:
1008            self._setup_diurnal_axes(
1009                ax=ax,
1010                time_points=time_points,
1011                ylabel=ylabel,
1012                subplot_label=subplot_label,
1013                add_label=add_label,
1014                add_legend=False,  # 個別の凡例は表示しない
1015                subplot_fontsize=subplot_fontsize,
1016            )
1017
1018        if ax1_ylim is not None:
1019            ax1.set_ylim(ax1_ylim)
1020        ax1.yaxis.set_major_locator(MultipleLocator(20))
1021        ax1.yaxis.set_major_formatter(FuncFormatter(lambda x, p: f"{x:.0f}"))
1022
1023        if ax2_ylim is not None:
1024            ax2.set_ylim(ax2_ylim)
1025        ax2.yaxis.set_major_locator(MultipleLocator(1))
1026        ax2.yaxis.set_major_formatter(FuncFormatter(lambda x, p: f"{x:.1f}"))
1027
1028        plt.tight_layout()
1029
1030        # 共通の凡例
1031        if add_legend:
1032            all_lines = ch4_lines
1033            all_labels = [line.get_label() for line in ch4_lines]
1034            if not legend_only_ch4:
1035                all_lines += c2h6_lines
1036                all_labels += [line.get_label() for line in c2h6_lines]
1037            fig.legend(
1038                all_lines,
1039                all_labels,
1040                loc="center",
1041                bbox_to_anchor=(0.5, 0.02),
1042                ncol=len(all_lines),
1043            )
1044            plt.subplots_adjust(bottom=0.25)  # 下部に凡例用のスペースを確保
1045
1046        if save_fig:
1047            if output_dirpath is None:
1048                raise ValueError(
1049                    "save_fig = True の場合、 output_dirpath を指定する必要があります。有効なディレクトリパスを指定してください。"
1050                )
1051            os.makedirs(output_dirpath, exist_ok=True)
1052            output_filepath: str = os.path.join(output_dirpath, output_filename)
1053            fig.savefig(output_filepath, dpi=dpi, bbox_inches="tight")
1054        if show_fig:
1055            plt.show()
1056        plt.close(fig=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_dirpath: str | Path | None, optional
    出力先ディレクトリのパスを指定します。save_fig=Trueの場合は必須です。
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です。
figsize: tuple[float, float], optional
    プロットのサイズを指定します。デフォルト値は(12, 5)です。
dpi: float | None, optional
    プロットのdpiを指定します。デフォルト値は350です。
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です。
save_fig: bool, optional
    プロットを保存するかどうかを指定します。デフォルト値はTrueです。
show_fig: bool, optional
    プロットを表示するかどうかを指定します。デフォルト値はTrueです。

Examples

>>> generator = MonthlyFiguresGenerator()
>>> generator.plot_c1c2_fluxes_diurnal_patterns(
...     df=monthly_data,
...     y_cols_ch4=["CH4_flux1", "CH4_flux2"],
...     y_cols_c2h6=["C2H6_flux1", "C2H6_flux2"],
...     labels_ch4=["CH4 1", "CH4 2"],
...     labels_c2h6=["C2H6 1", "C2H6 2"],
...     colors_ch4=["red", "blue"],
...     colors_c2h6=["green", "orange"],
...     output_dirpath="output",
...     show_std=True
... )
def plot_c1c2_fluxes_diurnal_patterns_by_date( self, df: pandas.core.frame.DataFrame, y_col_ch4: str, y_col_c2h6: str, output_dirpath: str | pathlib.Path | None = None, 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, figsize: tuple[float, float] = (12, 5), dpi: float | None = 350, save_fig: bool = True, show_fig: bool = True, print_summary: bool = False) -> None:
1058    def plot_c1c2_fluxes_diurnal_patterns_by_date(
1059        self,
1060        df: pd.DataFrame,
1061        y_col_ch4: str,
1062        y_col_c2h6: str,
1063        output_dirpath: str | Path | None = None,
1064        output_filename: str = "diurnal_by_date.png",
1065        plot_all: bool = True,
1066        plot_weekday: bool = True,
1067        plot_weekend: bool = True,
1068        plot_holiday: bool = True,
1069        add_label: bool = True,
1070        add_legend: bool = True,
1071        show_std: bool = False,
1072        std_alpha: float = 0.2,
1073        legend_only_ch4: bool = False,
1074        subplot_fontsize: int = 20,
1075        subplot_label_ch4: str | None = "(a)",
1076        subplot_label_c2h6: str | None = "(b)",
1077        ax1_ylim: tuple[float, float] | None = None,
1078        ax2_ylim: tuple[float, float] | None = None,
1079        figsize: tuple[float, float] = (12, 5),
1080        dpi: float | None = 350,
1081        save_fig: bool = True,
1082        show_fig: bool = True,
1083        print_summary: bool = False,
1084    ) -> None:
1085        """CH4とC2H6の日変化パターンを日付分類して1つの図に並べてプロットします。
1086
1087        Parameters
1088        ----------
1089            df: pd.DataFrame
1090                入力データフレームを指定します。
1091            y_col_ch4: str
1092                CH4フラックスを含むカラム名を指定します。
1093            y_col_c2h6: str
1094                C2H6フラックスを含むカラム名を指定します。
1095            output_dirpath: str | Path | None, optional
1096                出力先ディレクトリのパスを指定します。save_fig=Trueの場合は必須です。
1097            output_filename: str, optional
1098                出力ファイル名を指定します。デフォルト値は"diurnal_by_date.png"です。
1099            plot_all: bool, optional
1100                すべての日をプロットするかどうかを指定します。デフォルト値はTrueです。
1101            plot_weekday: bool, optional
1102                平日をプロットするかどうかを指定します。デフォルト値はTrueです。
1103            plot_weekend: bool, optional
1104                週末をプロットするかどうかを指定します。デフォルト値はTrueです。
1105            plot_holiday: bool, optional
1106                祝日をプロットするかどうかを指定します。デフォルト値はTrueです。
1107            add_label: bool, optional
1108                サブプロットラベルを表示するかどうかを指定します。デフォルト値はTrueです。
1109            add_legend: bool, optional
1110                凡例を表示するかどうかを指定します。デフォルト値はTrueです。
1111            show_std: bool, optional
1112                標準偏差を表示するかどうかを指定します。デフォルト値はFalseです。
1113            std_alpha: float, optional
1114                標準偏差の透明度を指定します。デフォルト値は0.2です。
1115            legend_only_ch4: bool, optional
1116                CH4の凡例のみを表示するかどうかを指定します。デフォルト値はFalseです。
1117            subplot_fontsize: int, optional
1118                サブプロットのフォントサイズを指定します。デフォルト値は20です。
1119            subplot_label_ch4: str | None, optional
1120                CH4プロットのラベルを指定します。デフォルト値は"(a)"です。
1121            subplot_label_c2h6: str | None, optional
1122                C2H6プロットのラベルを指定します。デフォルト値は"(b)"です。
1123            ax1_ylim: tuple[float, float] | None, optional
1124                CH4プロットのy軸の範囲を指定します。デフォルト値はNoneです。
1125            ax2_ylim: tuple[float, float] | None, optional
1126                C2H6プロットのy軸の範囲を指定します。デフォルト値はNoneです。
1127            figsize: tuple[float, float], optional
1128                プロットのサイズを指定します。デフォルト値は(12, 5)です。
1129            dpi: float | None, optional
1130                プロットのdpiを指定します。デフォルト値は350です。
1131            save_fig: bool, optional
1132                プロットを保存するかどうかを指定します。デフォルト値はTrueです。
1133            show_fig: bool, optional
1134                プロットを表示するかどうかを指定します。デフォルト値はTrueです。
1135            print_summary: bool, optional
1136                統計情報を表示するかどうかを指定します。デフォルト値はFalseです。
1137
1138        Examples
1139        -------
1140        >>> generator = MonthlyFiguresGenerator()
1141        >>> generator.plot_c1c2_fluxes_diurnal_patterns_by_date(
1142        ...     df=monthly_data,
1143        ...     y_col_ch4="CH4_flux",
1144        ...     y_col_c2h6="C2H6_flux",
1145        ...     output_dirpath="output",
1146        ...     show_std=True,
1147        ...     print_summary=True
1148        ... )
1149        """
1150        # データの準備
1151        df_internal: pd.DataFrame = df.copy()
1152        df_internal.index = pd.to_datetime(df_internal.index)
1153        target_columns = [y_col_ch4, y_col_c2h6]
1154        hourly_means, time_points = self._prepare_diurnal_data(
1155            df_internal, target_columns, include_date_types=True
1156        )
1157
1158        # 標準偏差の計算を追加
1159        hourly_stds = {}
1160        if show_std:
1161            for condition in ["all", "weekday", "weekend", "holiday"]:
1162                if condition == "all":
1163                    condition_data = df_internal
1164                elif condition == "weekday":
1165                    condition_data = df_internal[
1166                        ~(
1167                            df_internal.index.dayofweek.isin([5, 6])
1168                            | df_internal.index.map(
1169                                lambda x: jpholiday.is_holiday(x.date())
1170                            )
1171                        )
1172                    ]
1173                elif condition == "weekend":
1174                    condition_data = df_internal[
1175                        df_internal.index.dayofweek.isin([5, 6])
1176                    ]
1177                else:  # holiday
1178                    condition_data = df_internal[
1179                        df_internal.index.map(lambda x: jpholiday.is_holiday(x.date()))
1180                    ]
1181
1182                hourly_stds[condition] = condition_data.groupby(
1183                    pd.to_datetime(condition_data.index).hour
1184                )[target_columns].std()
1185                # 24時間目のデータ点を追加
1186                last_hour = hourly_stds[condition].iloc[0:1].copy()
1187                last_hour.index = [24]
1188                hourly_stds[condition] = pd.concat([hourly_stds[condition], last_hour])
1189
1190        # プロットスタイルの設定
1191        styles = {
1192            "all": {
1193                "color": "black",
1194                "linestyle": "-",
1195                "alpha": 1.0,
1196                "label": "All days",
1197            },
1198            "weekday": {
1199                "color": "blue",
1200                "linestyle": "-",
1201                "alpha": 0.8,
1202                "label": "Weekdays",
1203            },
1204            "weekend": {
1205                "color": "red",
1206                "linestyle": "-",
1207                "alpha": 0.8,
1208                "label": "Weekends",
1209            },
1210            "holiday": {
1211                "color": "green",
1212                "linestyle": "-",
1213                "alpha": 0.8,
1214                "label": "Weekends & Holidays",
1215            },
1216        }
1217
1218        # プロット対象の条件を選択
1219        plot_conditions = {
1220            "all": plot_all,
1221            "weekday": plot_weekday,
1222            "weekend": plot_weekend,
1223            "holiday": plot_holiday,
1224        }
1225        selected_conditions = {
1226            col: means
1227            for col, means in hourly_means.items()
1228            if plot_conditions.get(col)
1229        }
1230
1231        # プロットの作成
1232        fig, (ax1, ax2) = plt.subplots(1, 2, figsize=figsize)
1233
1234        # CH4とC2H6のプロット用のラインオブジェクトを保存
1235        ch4_lines = []
1236        c2h6_lines = []
1237
1238        # CH4とC2H6のプロット
1239        for condition, means in selected_conditions.items():
1240            style = styles[condition].copy()
1241
1242            # CH4プロット
1243            mean_values_ch4 = means[y_col_ch4]
1244            line_ch4 = ax1.plot(time_points, mean_values_ch4, marker="o", **style)
1245            ch4_lines.extend(line_ch4)
1246
1247            if show_std and condition in hourly_stds:
1248                std_values = hourly_stds[condition][y_col_ch4]
1249                ax1.fill_between(
1250                    time_points,
1251                    mean_values_ch4 - std_values,
1252                    mean_values_ch4 + std_values,
1253                    color=style["color"],
1254                    alpha=std_alpha,
1255                )
1256
1257            # C2H6プロット
1258            style["linestyle"] = "--"
1259            mean_values_c2h6 = means[y_col_c2h6]
1260            line_c2h6 = ax2.plot(time_points, mean_values_c2h6, marker="o", **style)
1261            c2h6_lines.extend(line_c2h6)
1262
1263            if show_std and condition in hourly_stds:
1264                std_values = hourly_stds[condition][y_col_c2h6]
1265                ax2.fill_between(
1266                    time_points,
1267                    mean_values_c2h6 - std_values,
1268                    mean_values_c2h6 + std_values,
1269                    color=style["color"],
1270                    alpha=std_alpha,
1271                )
1272
1273        # 軸の設定
1274        for ax, ylabel, subplot_label in [
1275            (ax1, r"CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)", subplot_label_ch4),
1276            (ax2, r"C$_2$H$_6$ flux (nmol m$^{-2}$ s$^{-1}$)", subplot_label_c2h6),
1277        ]:
1278            self._setup_diurnal_axes(
1279                ax=ax,
1280                time_points=time_points,
1281                ylabel=ylabel,
1282                subplot_label=subplot_label,
1283                add_label=add_label,
1284                add_legend=False,
1285                subplot_fontsize=subplot_fontsize,
1286            )
1287
1288        if ax1_ylim is not None:
1289            ax1.set_ylim(ax1_ylim)
1290        ax1.yaxis.set_major_locator(MultipleLocator(20))
1291        ax1.yaxis.set_major_formatter(FuncFormatter(lambda x, p: f"{x:.0f}"))
1292
1293        if ax2_ylim is not None:
1294            ax2.set_ylim(ax2_ylim)
1295        ax2.yaxis.set_major_locator(MultipleLocator(1))
1296        ax2.yaxis.set_major_formatter(FuncFormatter(lambda x, p: f"{x:.1f}"))
1297
1298        plt.tight_layout()
1299
1300        # 共通の凡例を図の下部に配置
1301        if add_legend:
1302            lines_to_show = (
1303                ch4_lines if legend_only_ch4 else ch4_lines[: len(selected_conditions)]
1304            )
1305            fig.legend(
1306                lines_to_show,
1307                [
1308                    style["label"]
1309                    for style in list(styles.values())[: len(lines_to_show)]
1310                ],
1311                loc="center",
1312                bbox_to_anchor=(0.5, 0.02),
1313                ncol=len(lines_to_show),
1314            )
1315            plt.subplots_adjust(bottom=0.25)  # 下部に凡例用のスペースを確保
1316
1317        if save_fig:
1318            if output_dirpath is None:
1319                raise ValueError(
1320                    "save_fig = True の場合、 output_dirpath を指定する必要があります。有効なディレクトリパスを指定してください。"
1321                )
1322            os.makedirs(output_dirpath, exist_ok=True)
1323            output_filepath: str = os.path.join(output_dirpath, output_filename)
1324            fig.savefig(output_filepath, dpi=dpi, bbox_inches="tight")
1325        if show_fig:
1326            plt.show()
1327        plt.close(fig=fig)
1328
1329        # 日変化パターンの統計分析を追加
1330        if print_summary:
1331            # 平日と休日のデータを準備
1332            dates = pd.to_datetime(df_internal.index)
1333            is_weekend = dates.dayofweek.isin([5, 6])
1334            is_holiday = dates.map(lambda x: jpholiday.is_holiday(x.date()))
1335            is_weekday = ~(is_weekend | is_holiday)
1336
1337            weekday_data = df_internal[is_weekday]
1338            holiday_data = df_internal[is_weekend | is_holiday]
1339
1340            def get_diurnal_stats(data, column):
1341                # 時間ごとの平均値を計算
1342                hourly_means = data.groupby(data.index.hour)[column].mean()
1343
1344                # 8-16時の時間帯の統計
1345                daytime_means = hourly_means[
1346                    (hourly_means.index >= 8) & (hourly_means.index <= 16)
1347                ]
1348
1349                if len(daytime_means) == 0:
1350                    return None
1351
1352                return {
1353                    "mean": daytime_means.mean(),
1354                    "max": daytime_means.max(),
1355                    "max_hour": daytime_means.idxmax(),
1356                    "min": daytime_means.min(),
1357                    "min_hour": daytime_means.idxmin(),
1358                    "hours_count": len(daytime_means),
1359                }
1360
1361            # CH4とC2H6それぞれの統計を計算
1362            for col, gas_name in [(y_col_ch4, "CH4"), (y_col_c2h6, "C2H6")]:
1363                print(f"\n=== {gas_name} フラックス 8-16時の統計分析 ===")
1364
1365                weekday_stats = get_diurnal_stats(weekday_data, col)
1366                holiday_stats = get_diurnal_stats(holiday_data, col)
1367
1368                if weekday_stats and holiday_stats:
1369                    print("\n平日:")
1370                    print(f"  平均値: {weekday_stats['mean']:.2f}")
1371                    print(
1372                        f"  最大値: {weekday_stats['max']:.2f} ({weekday_stats['max_hour']}時)"
1373                    )
1374                    print(
1375                        f"  最小値: {weekday_stats['min']:.2f} ({weekday_stats['min_hour']}時)"
1376                    )
1377                    print(f"  集計時間数: {weekday_stats['hours_count']}")
1378
1379                    print("\n休日:")
1380                    print(f"  平均値: {holiday_stats['mean']:.2f}")
1381                    print(
1382                        f"  最大値: {holiday_stats['max']:.2f} ({holiday_stats['max_hour']}時)"
1383                    )
1384                    print(
1385                        f"  最小値: {holiday_stats['min']:.2f} ({holiday_stats['min_hour']}時)"
1386                    )
1387                    print(f"  集計時間数: {holiday_stats['hours_count']}")
1388
1389                    # 平日/休日の比率を計算
1390                    print("\n平日/休日の比率:")
1391                    print(
1392                        f"  平均値比: {weekday_stats['mean'] / holiday_stats['mean']:.2f}"
1393                    )
1394                    print(
1395                        f"  最大値比: {weekday_stats['max'] / holiday_stats['max']:.2f}"
1396                    )
1397                    print(
1398                        f"  最小値比: {weekday_stats['min'] / holiday_stats['min']:.2f}"
1399                    )
1400                else:
1401                    print("十分なデータがありません")

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

Parameters

df: pd.DataFrame
    入力データフレームを指定します。
y_col_ch4: str
    CH4フラックスを含むカラム名を指定します。
y_col_c2h6: str
    C2H6フラックスを含むカラム名を指定します。
output_dirpath: str | Path | None, optional
    出力先ディレクトリのパスを指定します。save_fig=Trueの場合は必須です。
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です。
figsize: tuple[float, float], optional
    プロットのサイズを指定します。デフォルト値は(12, 5)です。
dpi: float | None, optional
    プロットのdpiを指定します。デフォルト値は350です。
save_fig: bool, optional
    プロットを保存するかどうかを指定します。デフォルト値はTrueです。
show_fig: bool, optional
    プロットを表示するかどうかを指定します。デフォルト値はTrueです。
print_summary: bool, optional
    統計情報を表示するかどうかを指定します。デフォルト値はFalseです。

Examples

>>> generator = MonthlyFiguresGenerator()
>>> generator.plot_c1c2_fluxes_diurnal_patterns_by_date(
...     df=monthly_data,
...     y_col_ch4="CH4_flux",
...     y_col_c2h6="C2H6_flux",
...     output_dirpath="output",
...     show_std=True,
...     print_summary=True
... )
def plot_diurnal_concentrations( self, df: pandas.core.frame.DataFrame, col_ch4_conc: str = 'CH4_ultra_cal', col_c2h6_conc: str = 'C2H6_ultra_cal', col_datetime: str = 'Date', output_dirpath: str | pathlib.Path | None = None, output_filename: str = 'diurnal_concentrations.png', show_std: bool = True, alpha_std: float = 0.2, add_legend: bool = True, print_summary: bool = False, 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: Literal['30min', '1H'] = '1H', figsize: tuple[float, float] = (12, 5), dpi: float | None = 350, save_fig: bool = True, show_fig: bool = True) -> None:
1403    def plot_diurnal_concentrations(
1404        self,
1405        df: pd.DataFrame,
1406        col_ch4_conc: str = "CH4_ultra_cal",
1407        col_c2h6_conc: str = "C2H6_ultra_cal",
1408        col_datetime: str = "Date",
1409        output_dirpath: str | Path | None = None,
1410        output_filename: str = "diurnal_concentrations.png",
1411        show_std: bool = True,
1412        alpha_std: float = 0.2,
1413        add_legend: bool = True,
1414        print_summary: bool = False,
1415        subplot_label_ch4: str | None = None,
1416        subplot_label_c2h6: str | None = None,
1417        subplot_fontsize: int = 24,
1418        ch4_ylim: tuple[float, float] | None = None,
1419        c2h6_ylim: tuple[float, float] | None = None,
1420        interval: Literal["30min", "1H"] = "1H",
1421        figsize: tuple[float, float] = (12, 5),
1422        dpi: float | None = 350,
1423        save_fig: bool = True,
1424        show_fig: bool = True,
1425    ) -> None:
1426        """CH4とC2H6の濃度の日内変動を描画します。
1427
1428        Parameters
1429        ----------
1430            df: pd.DataFrame
1431                濃度データを含むDataFrameを指定します。
1432            col_ch4_conc: str, optional
1433                CH4濃度のカラム名を指定します。デフォルト値は"CH4_ultra_cal"です。
1434            col_c2h6_conc: str, optional
1435                C2H6濃度のカラム名を指定します。デフォルト値は"C2H6_ultra_cal"です。
1436            col_datetime: str, optional
1437                日時カラム名を指定します。デフォルト値は"Date"です。
1438            output_dirpath: str | Path | None, optional
1439                出力ディレクトリのパスを指定します。save_fig=Trueの場合は必須です。
1440            output_filename: str, optional
1441                出力ファイル名を指定します。デフォルト値は"diurnal_concentrations.png"です。
1442            show_std: bool, optional
1443                標準偏差を表示するかどうかを指定します。デフォルト値はTrueです。
1444            alpha_std: float, optional
1445                標準偏差の透明度を指定します。デフォルト値は0.2です。
1446            add_legend: bool, optional
1447                凡例を追加するかどうかを指定します。デフォルト値はTrueです。
1448            print_summary: bool, optional
1449                統計情報を表示するかどうかを指定します。デフォルト値はFalseです。
1450            subplot_label_ch4: str | None, optional
1451                CH4プロットのラベルを指定します。デフォルト値はNoneです。
1452            subplot_label_c2h6: str | None, optional
1453                C2H6プロットのラベルを指定します。デフォルト値はNoneです。
1454            subplot_fontsize: int, optional
1455                サブプロットのフォントサイズを指定します。デフォルト値は24です。
1456            ch4_ylim: tuple[float, float] | None, optional
1457                CH4のy軸範囲を指定します。デフォルト値はNoneです。
1458            c2h6_ylim: tuple[float, float] | None, optional
1459                C2H6のy軸範囲を指定します。デフォルト値はNoneです。
1460            interval: Literal["30min", "1H"], optional
1461                時間間隔を指定します。"30min"または"1H"を指定できます。デフォルト値は"1H"です。
1462            figsize: tuple[float, float], optional
1463                プロットのサイズを指定します。デフォルト値は(12, 5)です。
1464            dpi: float | None, optional
1465                プロットのdpiを指定します。デフォルト値は350です。
1466            save_fig: bool, optional
1467                プロットを保存するかどうかを指定します。デフォルト値はTrueです。
1468            show_fig: bool, optional
1469                プロットを表示するかどうかを指定します。デフォルト値はTrueです。
1470
1471        Examples
1472        --------
1473        >>> generator = MonthlyFiguresGenerator()
1474        >>> generator.plot_diurnal_concentrations(
1475        ...     df=monthly_data,
1476        ...     output_dirpath="output",
1477        ...     show_std=True,
1478        ...     interval="30min"
1479        ... )
1480        """
1481        # データの準備
1482        df_internal = df.copy()
1483        df_internal.index = pd.to_datetime(df_internal.index)
1484        if interval == "30min":
1485            # 30分間隔の場合、時間と30分を別々に取得
1486            df_internal["hour"] = pd.to_datetime(df_internal[col_datetime]).dt.hour
1487            df_internal["minute"] = pd.to_datetime(df_internal[col_datetime]).dt.minute
1488            df_internal["time_bin"] = df_internal["hour"] + df_internal["minute"].map(
1489                {0: 0, 30: 0.5}
1490            )
1491        else:
1492            # 1時間間隔の場合
1493            df_internal["time_bin"] = pd.to_datetime(df_internal[col_datetime]).dt.hour
1494
1495        # 時間ごとの平均値と標準偏差を計算
1496        hourly_stats = df_internal.groupby("time_bin")[
1497            [col_ch4_conc, col_c2h6_conc]
1498        ].agg(["mean", "std"])
1499
1500        # 最後のデータポイントを追加(最初のデータを使用)
1501        last_point = hourly_stats.iloc[0:1].copy()
1502        last_point.index = pd.Index(
1503            [hourly_stats.index[-1] + (0.5 if interval == "30min" else 1)]
1504        )
1505        hourly_stats = pd.concat([hourly_stats, last_point])
1506
1507        # 時間軸の作成
1508        if interval == "30min":
1509            time_points = pd.date_range("2024-01-01", periods=49, freq="30min")
1510            x_ticks = [0, 6, 12, 18, 24]  # 主要な時間のティック
1511        else:
1512            time_points = pd.date_range("2024-01-01", periods=25, freq="1H")
1513            x_ticks = [0, 6, 12, 18, 24]
1514
1515        # プロットの作成
1516        fig, (ax1, ax2) = plt.subplots(1, 2, figsize=figsize)
1517
1518        # CH4濃度プロット
1519        mean_ch4 = hourly_stats[col_ch4_conc]["mean"]
1520        if show_std:
1521            std_ch4 = hourly_stats[col_ch4_conc]["std"]
1522            ax1.fill_between(
1523                time_points,
1524                mean_ch4 - std_ch4,
1525                mean_ch4 + std_ch4,
1526                color="red",
1527                alpha=alpha_std,
1528            )
1529        ch4_line = ax1.plot(time_points, mean_ch4, "red", label="CH$_4$")[0]
1530
1531        ax1.set_ylabel("CH$_4$ (ppm)")
1532        if ch4_ylim is not None:
1533            ax1.set_ylim(ch4_ylim)
1534        if subplot_label_ch4:
1535            ax1.text(
1536                0.02,
1537                0.98,
1538                subplot_label_ch4,
1539                transform=ax1.transAxes,
1540                va="top",
1541                fontsize=subplot_fontsize,
1542            )
1543
1544        # C2H6濃度プロット
1545        mean_c2h6 = hourly_stats[col_c2h6_conc]["mean"]
1546        if show_std:
1547            std_c2h6 = hourly_stats[col_c2h6_conc]["std"]
1548            ax2.fill_between(
1549                time_points,
1550                mean_c2h6 - std_c2h6,
1551                mean_c2h6 + std_c2h6,
1552                color="orange",
1553                alpha=alpha_std,
1554            )
1555        c2h6_line = ax2.plot(time_points, mean_c2h6, "orange", label="C$_2$H$_6$")[0]
1556
1557        ax2.set_ylabel("C$_2$H$_6$ (ppb)")
1558        if c2h6_ylim is not None:
1559            ax2.set_ylim(c2h6_ylim)
1560        if subplot_label_c2h6:
1561            ax2.text(
1562                0.02,
1563                0.98,
1564                subplot_label_c2h6,
1565                transform=ax2.transAxes,
1566                va="top",
1567                fontsize=subplot_fontsize,
1568            )
1569
1570        # 両プロットの共通設定
1571        for ax in [ax1, ax2]:
1572            ax.set_xlabel("Time (hour)")
1573            ax.xaxis.set_major_formatter(mdates.DateFormatter("%-H"))
1574            ax.xaxis.set_major_locator(mdates.HourLocator(byhour=x_ticks))
1575            ax.set_xlim(time_points[0], time_points[-1])
1576            # 1時間ごとの縦線を表示
1577            ax.grid(True, which="major", alpha=0.3)
1578
1579        # 共通の凡例を図の下部に配置
1580        if add_legend:
1581            fig.legend(
1582                [ch4_line, c2h6_line],
1583                ["CH$_4$", "C$_2$H$_6$"],
1584                loc="center",
1585                bbox_to_anchor=(0.5, 0.02),
1586                ncol=2,
1587            )
1588        plt.subplots_adjust(bottom=0.2)
1589
1590        plt.tight_layout()
1591        if save_fig:
1592            if output_dirpath is None:
1593                raise ValueError()
1594            # 出力ディレクトリの作成
1595            os.makedirs(output_dirpath, exist_ok=True)
1596            output_filepath: str = os.path.join(output_dirpath, output_filename)
1597            plt.savefig(output_filepath, dpi=dpi, bbox_inches="tight")
1598        if show_fig:
1599            plt.show()
1600        plt.close(fig=fig)
1601
1602        if print_summary:
1603            # 統計情報の表示
1604            for name, col in [("CH4", col_ch4_conc), ("C2H6", col_c2h6_conc)]:
1605                stats = hourly_stats[col]
1606                mean_vals = stats["mean"]
1607
1608                print(f"\n{name}濃度の日内変動統計:")
1609                print(f"最小値: {mean_vals.min():.3f} (Hour: {mean_vals.idxmin()})")
1610                print(f"最大値: {mean_vals.max():.3f} (Hour: {mean_vals.idxmax()})")
1611                print(f"平均値: {mean_vals.mean():.3f}")
1612                print(f"日内変動幅: {mean_vals.max() - mean_vals.min():.3f}")
1613                print(f"最大/最小比: {mean_vals.max() / mean_vals.min():.3f}")

CH4とC2H6の濃度の日内変動を描画します。

Parameters

df: pd.DataFrame
    濃度データを含むDataFrameを指定します。
col_ch4_conc: str, optional
    CH4濃度のカラム名を指定します。デフォルト値は"CH4_ultra_cal"です。
col_c2h6_conc: str, optional
    C2H6濃度のカラム名を指定します。デフォルト値は"C2H6_ultra_cal"です。
col_datetime: str, optional
    日時カラム名を指定します。デフォルト値は"Date"です。
output_dirpath: str | Path | None, optional
    出力ディレクトリのパスを指定します。save_fig=Trueの場合は必須です。
output_filename: str, optional
    出力ファイル名を指定します。デフォルト値は"diurnal_concentrations.png"です。
show_std: bool, optional
    標準偏差を表示するかどうかを指定します。デフォルト値はTrueです。
alpha_std: float, optional
    標準偏差の透明度を指定します。デフォルト値は0.2です。
add_legend: bool, optional
    凡例を追加するかどうかを指定します。デフォルト値はTrueです。
print_summary: bool, optional
    統計情報を表示するかどうかを指定します。デフォルト値はFalseです。
subplot_label_ch4: str | None, optional
    CH4プロットのラベルを指定します。デフォルト値はNoneです。
subplot_label_c2h6: str | None, optional
    C2H6プロットのラベルを指定します。デフォルト値はNoneです。
subplot_fontsize: int, optional
    サブプロットのフォントサイズを指定します。デフォルト値は24です。
ch4_ylim: tuple[float, float] | None, optional
    CH4のy軸範囲を指定します。デフォルト値はNoneです。
c2h6_ylim: tuple[float, float] | None, optional
    C2H6のy軸範囲を指定します。デフォルト値はNoneです。
interval: Literal["30min", "1H"], optional
    時間間隔を指定します。"30min"または"1H"を指定できます。デフォルト値は"1H"です。
figsize: tuple[float, float], optional
    プロットのサイズを指定します。デフォルト値は(12, 5)です。
dpi: float | None, optional
    プロットのdpiを指定します。デフォルト値は350です。
save_fig: bool, optional
    プロットを保存するかどうかを指定します。デフォルト値はTrueです。
show_fig: bool, optional
    プロットを表示するかどうかを指定します。デフォルト値はTrueです。

Examples

>>> generator = MonthlyFiguresGenerator()
>>> generator.plot_diurnal_concentrations(
...     df=monthly_data,
...     output_dirpath="output",
...     show_std=True,
...     interval="30min"
... )
def plot_flux_diurnal_patterns_with_std( self, df: pandas.core.frame.DataFrame, 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_dirpath: str | pathlib.Path | None = None, output_filename: str = 'diurnal_patterns.png', window_size: int = 6, show_std: bool = True, alpha_std: float = 0.1, figsize: tuple[float, float] = (12, 5), dpi: float | None = 350, save_fig: bool = True, show_fig: bool = True, print_summary: bool = False) -> None:
1615    def plot_flux_diurnal_patterns_with_std(
1616        self,
1617        df: pd.DataFrame,
1618        col_ch4_flux: str = "Fch4",
1619        col_c2h6_flux: str = "Fc2h6",
1620        ch4_label: str = r"$\mathregular{CH_{4}}$フラックス",
1621        c2h6_label: str = r"$\mathregular{C_{2}H_{6}}$フラックス",
1622        col_datetime: str = "Date",
1623        output_dirpath: str | Path | None = None,
1624        output_filename: str = "diurnal_patterns.png",
1625        window_size: int = 6,
1626        show_std: bool = True,
1627        alpha_std: float = 0.1,
1628        figsize: tuple[float, float] = (12, 5),
1629        dpi: float | None = 350,
1630        save_fig: bool = True,
1631        show_fig: bool = True,
1632        print_summary: bool = False,
1633    ) -> None:
1634        """CH4とC2H6フラックスの日変化パターンをプロットします。
1635
1636        Parameters
1637        ----------
1638            df: pd.DataFrame
1639                プロットするデータを含むDataFrameを指定します。
1640            col_ch4_flux: str, optional
1641                CH4フラックスのカラム名を指定します。デフォルト値は"Fch4"です。
1642            col_c2h6_flux: str, optional
1643                C2H6フラックスのカラム名を指定します。デフォルト値は"Fc2h6"です。
1644            ch4_label: str, optional
1645                CH4フラックスの凡例ラベルを指定します。デフォルト値は"CH4フラックス"です。
1646            c2h6_label: str, optional
1647                C2H6フラックスの凡例ラベルを指定します。デフォルト値は"C2H6フラックス"です。
1648            col_datetime: str, optional
1649                日時カラムの名前を指定します。デフォルト値は"Date"です。
1650            output_dirpath: str | Path | None, optional
1651                出力ディレクトリのパスを指定します。save_fig=Trueの場合は必須です。
1652            output_filename: str, optional
1653                出力ファイル名を指定します。デフォルト値は"diurnal_patterns.png"です。
1654            window_size: int, optional
1655                移動平均の窓サイズを指定します。デフォルト値は6です。
1656            show_std: bool, optional
1657                標準偏差を表示するかどうかを指定します。デフォルト値はTrueです。
1658            alpha_std: float, optional
1659                標準偏差の透明度を0-1の範囲で指定します。デフォルト値は0.1です。
1660            figsize: tuple[float, float], optional
1661                プロットのサイズを指定します。デフォルト値は(12, 5)です。
1662            dpi: float | None, optional
1663                プロットの解像度を指定します。デフォルト値は350です。
1664            save_fig: bool, optional
1665                プロットを保存するかどうかを指定します。デフォルト値はTrueです。
1666            show_fig: bool, optional
1667                プロットを表示するかどうかを指定します。デフォルト値はTrueです。
1668            print_summary: bool, optional
1669                統計情報を表示するかどうかを指定します。デフォルト値はFalseです。
1670
1671        Examples
1672        --------
1673        >>> generator = MonthlyFiguresGenerator()
1674        >>> df = pd.read_csv("flux_data.csv")
1675        >>> generator.plot_flux_diurnal_patterns_with_std(
1676        ...     df,
1677        ...     col_ch4_flux="CH4_flux",
1678        ...     col_c2h6_flux="C2H6_flux",
1679        ...     output_dirpath="output",
1680        ...     show_std=True
1681        ... )
1682        """
1683        # 日時インデックスの処理
1684        df_internal = df.copy()
1685        if not isinstance(df_internal.index, pd.DatetimeIndex):
1686            df_internal[col_datetime] = pd.to_datetime(df_internal[col_datetime])
1687            df_internal.set_index(col_datetime, inplace=True)
1688        df_internal.index = pd.to_datetime(df_internal.index)
1689        # 時刻データの抽出とグループ化
1690        df_internal["hour"] = df_internal.index.hour
1691        hourly_means = df_internal.groupby("hour")[[col_ch4_flux, col_c2h6_flux]].agg(
1692            ["mean", "std"]
1693        )
1694
1695        # 24時間目のデータ点を追加(0時のデータを使用)
1696        last_hour = hourly_means.iloc[0:1].copy()
1697        last_hour.index = pd.Index([24])
1698        hourly_means = pd.concat([hourly_means, last_hour])
1699
1700        # 24時間分のデータポイントを作成
1701        time_points = pd.date_range("2024-01-01", periods=25, freq="h")
1702
1703        # プロットの作成
1704        fig, (ax1, ax2) = plt.subplots(1, 2, figsize=figsize)
1705
1706        # 移動平均の計算と描画
1707        ch4_mean = (
1708            hourly_means[(col_ch4_flux, "mean")]
1709            .rolling(window=window_size, center=True, min_periods=1)
1710            .mean()
1711        )
1712        c2h6_mean = (
1713            hourly_means[(col_c2h6_flux, "mean")]
1714            .rolling(window=window_size, center=True, min_periods=1)
1715            .mean()
1716        )
1717
1718        if show_std:
1719            ch4_std = (
1720                hourly_means[(col_ch4_flux, "std")]
1721                .rolling(window=window_size, center=True, min_periods=1)
1722                .mean()
1723            )
1724            c2h6_std = (
1725                hourly_means[(col_c2h6_flux, "std")]
1726                .rolling(window=window_size, center=True, min_periods=1)
1727                .mean()
1728            )
1729
1730            ax1.fill_between(
1731                time_points,
1732                ch4_mean - ch4_std,
1733                ch4_mean + ch4_std,
1734                color="blue",
1735                alpha=alpha_std,
1736            )
1737            ax2.fill_between(
1738                time_points,
1739                c2h6_mean - c2h6_std,
1740                c2h6_mean + c2h6_std,
1741                color="red",
1742                alpha=alpha_std,
1743            )
1744
1745        # メインのラインプロット
1746        ax1.plot(time_points, ch4_mean, "blue", label=ch4_label)
1747        ax2.plot(time_points, c2h6_mean, "red", label=c2h6_label)
1748
1749        # 軸の設定
1750        for ax, ylabel in [
1751            (ax1, r"CH$_4$ (nmol m$^{-2}$ s$^{-1}$)"),
1752            (ax2, r"C$_2$H$_6$ (nmol m$^{-2}$ s$^{-1}$)"),
1753        ]:
1754            ax.set_xlabel("Time")
1755            ax.set_ylabel(ylabel)
1756            ax.xaxis.set_major_formatter(mdates.DateFormatter("%-H"))
1757            ax.xaxis.set_major_locator(mdates.HourLocator(byhour=[0, 6, 12, 18, 24]))
1758            ax.set_xlim(time_points[0], time_points[-1])
1759            ax.grid(True, alpha=0.3)
1760            ax.legend()
1761
1762        # グラフの保存
1763        plt.tight_layout()
1764
1765        if save_fig:
1766            if output_dirpath is None:
1767                raise ValueError(
1768                    "save_fig = True の場合、 output_dirpath を指定する必要があります。有効なディレクトリパスを指定してください。"
1769                )
1770            # 出力ディレクトリの作成
1771            os.makedirs(output_dirpath, exist_ok=True)
1772            output_filepath: str = os.path.join(output_dirpath, output_filename)
1773            plt.savefig(output_filepath, dpi=350, bbox_inches="tight")
1774        if show_fig:
1775            plt.show()
1776        plt.close(fig=fig)
1777
1778        # 統計情報の表示(オプション)
1779        if print_summary:
1780            for col, name in [(col_ch4_flux, "CH4"), (col_c2h6_flux, "C2H6")]:
1781                mean_val = hourly_means[(col, "mean")].mean()
1782                min_val = hourly_means[(col, "mean")].min()
1783                max_val = hourly_means[(col, "mean")].max()
1784                min_time = hourly_means[(col, "mean")].idxmin()
1785                max_time = hourly_means[(col, "mean")].idxmax()
1786
1787                self.logger.info(f"{name} Statistics:")
1788                self.logger.info(f"Mean: {mean_val:.2f}")
1789                self.logger.info(f"Min: {min_val:.2f} (Hour: {min_time})")
1790                self.logger.info(f"Max: {max_val:.2f} (Hour: {max_time})")
1791                self.logger.info(f"Max/Min ratio: {max_val / min_val:.2f}\n")

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

Parameters

df: pd.DataFrame
    プロットするデータを含むDataFrameを指定します。
col_ch4_flux: str, optional
    CH4フラックスのカラム名を指定します。デフォルト値は"Fch4"です。
col_c2h6_flux: str, optional
    C2H6フラックスのカラム名を指定します。デフォルト値は"Fc2h6"です。
ch4_label: str, optional
    CH4フラックスの凡例ラベルを指定します。デフォルト値は"CH4フラックス"です。
c2h6_label: str, optional
    C2H6フラックスの凡例ラベルを指定します。デフォルト値は"C2H6フラックス"です。
col_datetime: str, optional
    日時カラムの名前を指定します。デフォルト値は"Date"です。
output_dirpath: str | Path | None, optional
    出力ディレクトリのパスを指定します。save_fig=Trueの場合は必須です。
output_filename: str, optional
    出力ファイル名を指定します。デフォルト値は"diurnal_patterns.png"です。
window_size: int, optional
    移動平均の窓サイズを指定します。デフォルト値は6です。
show_std: bool, optional
    標準偏差を表示するかどうかを指定します。デフォルト値はTrueです。
alpha_std: float, optional
    標準偏差の透明度を0-1の範囲で指定します。デフォルト値は0.1です。
figsize: tuple[float, float], optional
    プロットのサイズを指定します。デフォルト値は(12, 5)です。
dpi: float | None, optional
    プロットの解像度を指定します。デフォルト値は350です。
save_fig: bool, optional
    プロットを保存するかどうかを指定します。デフォルト値はTrueです。
show_fig: bool, optional
    プロットを表示するかどうかを指定します。デフォルト値はTrueです。
print_summary: bool, optional
    統計情報を表示するかどうかを指定します。デフォルト値はFalseです。

Examples

>>> generator = MonthlyFiguresGenerator()
>>> df = pd.read_csv("flux_data.csv")
>>> generator.plot_flux_diurnal_patterns_with_std(
...     df,
...     col_ch4_flux="CH4_flux",
...     col_c2h6_flux="C2H6_flux",
...     output_dirpath="output",
...     show_std=True
... )
def plot_gas_ratio_diurnal( self, df: pandas.core.frame.DataFrame, col_ratio_1: str, col_ratio_2: str, label_1: str, label_2: str, color_1: str, color_2: str, output_dirpath: str | pathlib.Path | None = None, output_filename: str = 'gas_ratio_diurnal.png', add_xlabel: bool = True, add_ylabel: bool = True, add_legend: bool = True, xlabel: str = 'Hour', ylabel: str = '都市ガスが占める排出比率 (%)', subplot_fontsize: int = 20, subplot_label: str | None = None, y_max: float | None = 100, figsize: tuple[float, float] = (12, 5), dpi: float | None = 350, save_fig: bool = True, show_fig: bool = False) -> None:
1793    def plot_gas_ratio_diurnal(
1794        self,
1795        df: pd.DataFrame,
1796        col_ratio_1: str,
1797        col_ratio_2: str,
1798        label_1: str,
1799        label_2: str,
1800        color_1: str,
1801        color_2: str,
1802        output_dirpath: str | Path | None = None,
1803        output_filename: str = "gas_ratio_diurnal.png",
1804        add_xlabel: bool = True,
1805        add_ylabel: bool = True,
1806        add_legend: bool = True,
1807        xlabel: str = "Hour",
1808        ylabel: str = "都市ガスが占める排出比率 (%)",
1809        subplot_fontsize: int = 20,
1810        subplot_label: str | None = None,
1811        y_max: float | None = 100,
1812        figsize: tuple[float, float] = (12, 5),
1813        dpi: float | None = 350,
1814        save_fig: bool = True,
1815        show_fig: bool = False,
1816    ) -> None:
1817        """2つの比率の日変化を比較するプロットを作成します。
1818
1819        Parameters
1820        ----------
1821            df: pd.DataFrame
1822                プロットするデータを含むDataFrameを指定します。
1823            col_ratio_1: str
1824                1つ目の比率データを含むカラム名を指定します。
1825            col_ratio_2: str
1826                2つ目の比率データを含むカラム名を指定します。
1827            label_1: str
1828                1つ目の比率データの凡例ラベルを指定します。
1829            label_2: str
1830                2つ目の比率データの凡例ラベルを指定します。
1831            color_1: str
1832                1つ目の比率データのプロット色を指定します。
1833            color_2: str
1834                2つ目の比率データのプロット色を指定します。
1835            output_dirpath: str | Path | None, optional
1836                出力先ディレクトリのパスを指定します。save_fig=Trueの場合は必須です。
1837            output_filename: str, optional
1838                出力ファイル名を指定します。デフォルト値は"gas_ratio_diurnal.png"です。
1839            add_xlabel: bool, optional
1840                x軸ラベルを表示するかどうかを指定します。デフォルト値はTrueです。
1841            add_ylabel: bool, optional
1842                y軸ラベルを表示するかどうかを指定します。デフォルト値はTrueです。
1843            add_legend: bool, optional
1844                凡例を表示するかどうかを指定します。デフォルト値はTrueです。
1845            xlabel: str, optional
1846                x軸のラベルを指定します。デフォルト値は"Hour"です。
1847            ylabel: str, optional
1848                y軸のラベルを指定します。デフォルト値は"都市ガスが占める排出比率 (%)"です。
1849            subplot_fontsize: int, optional
1850                サブプロットのフォントサイズを指定します。デフォルト値は20です。
1851            subplot_label: str | None, optional
1852                サブプロットのラベルを指定します。デフォルト値はNoneです。
1853            y_max: float | None, optional
1854                y軸の最大値を指定します。デフォルト値は100です。
1855            figsize: tuple[float, float], optional
1856                図のサイズを指定します。デフォルト値は(12, 5)です。
1857            dpi: float | None, optional
1858                図の解像度を指定します。デフォルト値は350です。
1859            save_fig: bool, optional
1860                図を保存するかどうかを指定します。デフォルト値はTrueです。
1861            show_fig: bool, optional
1862                図を表示するかどうかを指定します。デフォルト値はFalseです。
1863
1864        Examples
1865        -------
1866        >>> df = pd.DataFrame({
1867        ...     'ratio1': [80, 85, 90],
1868        ...     'ratio2': [70, 75, 80]
1869        ... })
1870        >>> generator = MonthlyFiguresGenerator()
1871        >>> generator.plot_gas_ratio_diurnal(
1872        ...     df=df,
1873        ...     col_ratio_1='ratio1',
1874        ...     col_ratio_2='ratio2',
1875        ...     label_1='比率1',
1876        ...     label_2='比率2',
1877        ...     color_1='blue',
1878        ...     color_2='red',
1879        ...     output_dirpath='output'
1880        ... )
1881        """
1882        df_internal: pd.DataFrame = df.copy()
1883        df_internal.index = pd.to_datetime(df_internal.index)
1884
1885        # 時刻でグループ化して平均を計算
1886        hourly_means = df_internal.groupby(df_internal.index.hour)[
1887            [col_ratio_1, col_ratio_2]
1888        ].mean()
1889        hourly_stds = df_internal.groupby(df_internal.index.hour)[
1890            [col_ratio_1, col_ratio_2]
1891        ].std()
1892
1893        # 24時間目のデータ点を追加(0時のデータを使用)
1894        last_hour = hourly_means.iloc[0:1].copy()
1895        last_hour.index = pd.Index([24])
1896        hourly_means = pd.concat([hourly_means, last_hour])
1897
1898        last_hour_std = hourly_stds.iloc[0:1].copy()
1899        last_hour_std.index = pd.Index([24])
1900        hourly_stds = pd.concat([hourly_stds, last_hour_std])
1901
1902        # 24時間分の時刻を生成
1903        time_points: pd.DatetimeIndex = pd.date_range(
1904            "2024-01-01", periods=25, freq="h"
1905        )
1906
1907        # プロットの作成
1908        fig, ax = plt.subplots(figsize=figsize)
1909
1910        # 1つ目の比率
1911        ax.plot(
1912            time_points,  # [:-1]を削除
1913            hourly_means[col_ratio_1],
1914            color=color_1,
1915            label=label_1,
1916            alpha=0.7,
1917        )
1918        ax.fill_between(
1919            time_points,  # [:-1]を削除
1920            hourly_means[col_ratio_1] - hourly_stds[col_ratio_1],
1921            hourly_means[col_ratio_1] + hourly_stds[col_ratio_1],
1922            color=color_1,
1923            alpha=0.2,
1924        )
1925
1926        # 2つ目の比率
1927        ax.plot(
1928            time_points,  # [:-1]を削除
1929            hourly_means[col_ratio_2],
1930            color=color_2,
1931            label=label_2,
1932            alpha=0.7,
1933        )
1934        ax.fill_between(
1935            time_points,  # [:-1]を削除
1936            hourly_means[col_ratio_2] - hourly_stds[col_ratio_2],
1937            hourly_means[col_ratio_2] + hourly_stds[col_ratio_2],
1938            color=color_2,
1939            alpha=0.2,
1940        )
1941
1942        # 軸の設定
1943        if add_xlabel:
1944            ax.set_xlabel(xlabel)
1945        if add_ylabel:
1946            ax.set_ylabel(ylabel)
1947
1948        # y軸の範囲設定
1949        if y_max is not None:
1950            ax.set_ylim(0, y_max)
1951
1952        # グリッド線の追加
1953        ax.grid(True, alpha=0.3)
1954        ax.grid(True, which="minor", alpha=0.1)
1955
1956        # x軸の設定
1957        ax.xaxis.set_major_formatter(mdates.DateFormatter("%-H"))
1958        ax.xaxis.set_major_locator(mdates.HourLocator(byhour=[0, 6, 12, 18, 24]))
1959        ax.set_xlim(
1960            float(mdates.date2num(time_points[0])),
1961            float(mdates.date2num(time_points[-1])),
1962        )
1963        ax.set_xticks(time_points[::6])
1964        ax.set_xticklabels(["0", "6", "12", "18", "24"])
1965
1966        # サブプロットラベルの追加
1967        if subplot_label:
1968            ax.text(
1969                0.02,
1970                0.98,
1971                subplot_label,
1972                transform=ax.transAxes,
1973                va="top",
1974                fontsize=subplot_fontsize,
1975            )
1976
1977        # 凡例の追加
1978        if add_legend:
1979            # 凡例を図の下部中央に配置
1980            ax.legend(
1981                loc="center",
1982                bbox_to_anchor=(0.5, -0.25),  # 図の下部に配置
1983                ncol=2,  # 2列で表示
1984                frameon=False,  # 枠を非表示
1985            )
1986            # 凡例のために下部のマージンを調整
1987            plt.subplots_adjust(bottom=0.2)
1988
1989        # プロットの保存と表示
1990        plt.tight_layout()
1991        if save_fig:
1992            if output_dirpath is None:
1993                raise ValueError(
1994                    "save_fig = True の場合、 output_dirpath を指定する必要があります。有効なディレクトリパスを指定してください。"
1995                )
1996            # 出力ディレクトリの作成
1997            os.makedirs(output_dirpath, exist_ok=True)
1998            output_filepath = os.path.join(output_dirpath, output_filename)
1999            plt.savefig(output_filepath, dpi=dpi, bbox_inches="tight")
2000        if show_fig:
2001            plt.show()
2002        plt.close(fig=fig)

2つの比率の日変化を比較するプロットを作成します。

Parameters

df: pd.DataFrame
    プロットするデータを含むDataFrameを指定します。
col_ratio_1: str
    1つ目の比率データを含むカラム名を指定します。
col_ratio_2: str
    2つ目の比率データを含むカラム名を指定します。
label_1: str
    1つ目の比率データの凡例ラベルを指定します。
label_2: str
    2つ目の比率データの凡例ラベルを指定します。
color_1: str
    1つ目の比率データのプロット色を指定します。
color_2: str
    2つ目の比率データのプロット色を指定します。
output_dirpath: str | Path | None, optional
    出力先ディレクトリのパスを指定します。save_fig=Trueの場合は必須です。
output_filename: str, optional
    出力ファイル名を指定します。デフォルト値は"gas_ratio_diurnal.png"です。
add_xlabel: bool, optional
    x軸ラベルを表示するかどうかを指定します。デフォルト値はTrueです。
add_ylabel: bool, optional
    y軸ラベルを表示するかどうかを指定します。デフォルト値はTrueです。
add_legend: bool, optional
    凡例を表示するかどうかを指定します。デフォルト値はTrueです。
xlabel: str, optional
    x軸のラベルを指定します。デフォルト値は"Hour"です。
ylabel: str, optional
    y軸のラベルを指定します。デフォルト値は"都市ガスが占める排出比率 (%)"です。
subplot_fontsize: int, optional
    サブプロットのフォントサイズを指定します。デフォルト値は20です。
subplot_label: str | None, optional
    サブプロットのラベルを指定します。デフォルト値はNoneです。
y_max: float | None, optional
    y軸の最大値を指定します。デフォルト値は100です。
figsize: tuple[float, float], optional
    図のサイズを指定します。デフォルト値は(12, 5)です。
dpi: float | None, optional
    図の解像度を指定します。デフォルト値は350です。
save_fig: bool, optional
    図を保存するかどうかを指定します。デフォルト値はTrueです。
show_fig: bool, optional
    図を表示するかどうかを指定します。デフォルト値はFalseです。

Examples

>>> df = pd.DataFrame({
...     'ratio1': [80, 85, 90],
...     'ratio2': [70, 75, 80]
... })
>>> generator = MonthlyFiguresGenerator()
>>> generator.plot_gas_ratio_diurnal(
...     df=df,
...     col_ratio_1='ratio1',
...     col_ratio_2='ratio2',
...     label_1='比率1',
...     label_2='比率2',
...     color_1='blue',
...     color_2='red',
...     output_dirpath='output'
... )
def plot_scatter( self, df: pandas.core.frame.DataFrame, col_x: str, col_y: str, output_dirpath: str | pathlib.Path | None = None, output_filename: str = 'scatter.png', add_label: bool = True, xlabel: str | None = None, ylabel: str | None = None, x_axis_range: tuple | None = None, y_axis_range: tuple | None = None, x_scientific: bool = False, y_scientific: bool = False, fixed_slope: float = 0.076, show_fixed_slope: bool = False, figsize: tuple[float, float] = (6, 6), dpi: float | None = 350, save_fig: bool = True, show_fig: bool = True) -> None:
2004    def plot_scatter(
2005        self,
2006        df: pd.DataFrame,
2007        col_x: str,
2008        col_y: str,
2009        output_dirpath: str | Path | None = None,
2010        output_filename: str = "scatter.png",
2011        add_label: bool = True,
2012        xlabel: str | None = None,
2013        ylabel: str | None = None,
2014        x_axis_range: tuple | None = None,
2015        y_axis_range: tuple | None = None,
2016        x_scientific: bool = False,
2017        y_scientific: bool = False,
2018        fixed_slope: float = 0.076,
2019        show_fixed_slope: bool = False,
2020        figsize: tuple[float, float] = (6, 6),
2021        dpi: float | None = 350,
2022        save_fig: bool = True,
2023        show_fig: bool = True,
2024    ) -> None:
2025        """散布図を作成し、TLS回帰直線を描画します。
2026
2027        Parameters
2028        ----------
2029            df: pd.DataFrame
2030                プロットに使用するデータフレームを指定します。
2031            col_x: str
2032                x軸に使用する列名を指定します。
2033            col_y: str
2034                y軸に使用する列名を指定します。
2035            output_dirpath: str | Path | None, optional
2036                出力先ディレクトリを指定します。save_fig=Trueの場合は必須です。
2037            output_filename: str, optional
2038                出力ファイル名を指定します。デフォルト値は"scatter.png"です。
2039            add_label: bool, optional
2040                軸ラベルを表示するかどうかを指定します。デフォルト値はTrueです。
2041            xlabel: str | None, optional
2042                x軸のラベルを指定します。デフォルト値はNoneです。
2043            ylabel: str | None, optional
2044                y軸のラベルを指定します。デフォルト値はNoneです。
2045            x_axis_range: tuple | None, optional
2046                x軸の表示範囲を(最小値, 最大値)で指定します。デフォルト値はNoneです。
2047            y_axis_range: tuple | None, optional
2048                y軸の表示範囲を(最小値, 最大値)で指定します。デフォルト値はNoneです。
2049            x_scientific: bool, optional
2050                x軸を科学的記法で表示するかどうかを指定します。デフォルト値はFalseです。
2051            y_scientific: bool, optional
2052                y軸を科学的記法で表示するかどうかを指定します。デフォルト値はFalseです。
2053            fixed_slope: float, optional
2054                固定傾きの値を指定します。デフォルト値は0.076です。
2055            show_fixed_slope: bool, optional
2056                固定傾きの線を表示するかどうかを指定します。デフォルト値はFalseです。
2057            figsize: tuple[float, float], optional
2058                プロットのサイズを(幅, 高さ)で指定します。デフォルト値は(6, 6)です。
2059            dpi: float | None, optional
2060                プロットの解像度を指定します。デフォルト値は350です。
2061            save_fig: bool, optional
2062                プロットを保存するかどうかを指定します。デフォルト値はTrueです。
2063            show_fig: bool, optional
2064                プロットを表示するかどうかを指定します。デフォルト値はTrueです。
2065
2066        Examples
2067        --------
2068        >>> df = pd.DataFrame({
2069        ...     'x': [1, 2, 3, 4, 5],
2070        ...     'y': [2, 4, 6, 8, 10]
2071        ... })
2072        >>> generator = MonthlyFiguresGenerator()
2073        >>> generator.plot_scatter(
2074        ...     df=df,
2075        ...     col_x='x',
2076        ...     col_y='y',
2077        ...     xlabel='X軸',
2078        ...     ylabel='Y軸',
2079        ...     output_dirpath='output'
2080        ... )
2081        """
2082        # 有効なデータの抽出
2083        df_internal = MonthlyFiguresGenerator.get_valid_data(
2084            df=df, col_x=col_x, col_y=col_y
2085        )
2086
2087        # データの準備
2088        x = df_internal[col_x].values
2089        y = df_internal[col_y].values
2090
2091        # データの中心化
2092        x_array = np.array(x)
2093        y_array = np.array(y)
2094        x_mean = np.mean(x_array, axis=0)
2095        y_mean = np.mean(y_array, axis=0)
2096        x_c = x - x_mean
2097        y_c = y - y_mean
2098
2099        # TLS回帰の計算
2100        data_matrix = np.vstack((x_c, y_c))
2101        cov_matrix = np.cov(data_matrix)
2102        _, eigenvecs = linalg.eigh(cov_matrix)
2103        largest_eigenvec = eigenvecs[:, -1]
2104
2105        slope = largest_eigenvec[1] / largest_eigenvec[0]
2106        intercept = y_mean - slope * x_mean
2107
2108        # R²とRMSEの計算
2109        y_pred = slope * x + intercept
2110        r_squared = 1 - np.sum((y - y_pred) ** 2) / np.sum((y - y_mean) ** 2)
2111        rmse = np.sqrt(np.mean((y - y_pred) ** 2))
2112
2113        # プロットの作成
2114        fig, ax = plt.subplots(figsize=figsize)
2115
2116        # データ点のプロット
2117        ax.scatter(x_array, y_array, color="black")
2118
2119        # データの範囲を取得
2120        if x_axis_range is None:
2121            x_axis_range = (df_internal[col_x].min(), df_internal[col_x].max())
2122        if y_axis_range is None:
2123            y_axis_range = (df_internal[col_y].min(), df_internal[col_y].max())
2124
2125        # 回帰直線のプロット
2126        x_range = np.linspace(x_axis_range[0], x_axis_range[1], 150)
2127        y_range = slope * x_range + intercept
2128        ax.plot(x_range, y_range, "r", label="TLS regression")
2129
2130        # 傾き固定の線を追加(フラグがTrueの場合)
2131        if show_fixed_slope:
2132            fixed_intercept = (
2133                y_mean - fixed_slope * x_mean
2134            )  # 中心点を通るように切片を計算
2135            y_fixed = fixed_slope * x_range + fixed_intercept
2136            ax.plot(x_range, y_fixed, "b--", label=f"Slope = {fixed_slope}", alpha=0.7)
2137
2138        # 軸の設定
2139        ax.set_xlim(x_axis_range)
2140        ax.set_ylim(y_axis_range)
2141
2142        # 指数表記の設定
2143        if x_scientific:
2144            ax.ticklabel_format(style="sci", axis="x", scilimits=(0, 0))
2145            ax.xaxis.get_offset_text().set_position((1.1, 0))  # 指数の位置調整
2146        if y_scientific:
2147            ax.ticklabel_format(style="sci", axis="y", scilimits=(0, 0))
2148            ax.yaxis.get_offset_text().set_position((0, 1.1))  # 指数の位置調整
2149
2150        if add_label:
2151            if xlabel is not None:
2152                ax.set_xlabel(xlabel)
2153            if ylabel is not None:
2154                ax.set_ylabel(ylabel)
2155
2156        # 1:1の関係を示す点線(軸の範囲が同じ場合のみ表示)
2157        if (
2158            x_axis_range is not None
2159            and y_axis_range is not None
2160            and x_axis_range == y_axis_range
2161        ):
2162            ax.plot(
2163                [x_axis_range[0], x_axis_range[1]],
2164                [x_axis_range[0], x_axis_range[1]],
2165                "k--",
2166                alpha=0.5,
2167            )
2168
2169        # 回帰情報の表示
2170        equation = (
2171            f"y = {slope:.2f}x {'+' if intercept >= 0 else '-'} {abs(intercept):.2f}"
2172        )
2173        position_x = 0.05
2174        fig_ha: str = "left"
2175        ax.text(
2176            position_x,
2177            0.95,
2178            equation,
2179            transform=ax.transAxes,
2180            va="top",
2181            ha=fig_ha,
2182            color="red",
2183        )
2184        ax.text(
2185            position_x,
2186            0.88,
2187            f"R² = {r_squared:.2f}",
2188            transform=ax.transAxes,
2189            va="top",
2190            ha=fig_ha,
2191            color="red",
2192        )
2193        ax.text(
2194            position_x,
2195            0.81,  # RMSEのための新しい位置
2196            f"RMSE = {rmse:.2f}",
2197            transform=ax.transAxes,
2198            va="top",
2199            ha=fig_ha,
2200            color="red",
2201        )
2202        # 目盛り線の設定
2203        ax.grid(True, alpha=0.3)
2204
2205        if save_fig:
2206            if output_dirpath is None:
2207                raise ValueError(
2208                    "save_fig = True の場合、 output_dirpath を指定する必要があります。有効なディレクトリパスを指定してください。"
2209                )
2210            os.makedirs(output_dirpath, exist_ok=True)
2211            output_filepath: str = os.path.join(output_dirpath, output_filename)
2212            fig.savefig(output_filepath, dpi=dpi, bbox_inches="tight")
2213        if show_fig:
2214            plt.show()
2215        plt.close(fig=fig)

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

Parameters

df: pd.DataFrame
    プロットに使用するデータフレームを指定します。
col_x: str
    x軸に使用する列名を指定します。
col_y: str
    y軸に使用する列名を指定します。
output_dirpath: str | Path | None, optional
    出力先ディレクトリを指定します。save_fig=Trueの場合は必須です。
output_filename: str, optional
    出力ファイル名を指定します。デフォルト値は"scatter.png"です。
add_label: bool, optional
    軸ラベルを表示するかどうかを指定します。デフォルト値はTrueです。
xlabel: str | None, optional
    x軸のラベルを指定します。デフォルト値はNoneです。
ylabel: str | None, optional
    y軸のラベルを指定します。デフォルト値はNoneです。
x_axis_range: tuple | None, optional
    x軸の表示範囲を(最小値, 最大値)で指定します。デフォルト値はNoneです。
y_axis_range: tuple | None, optional
    y軸の表示範囲を(最小値, 最大値)で指定します。デフォルト値はNoneです。
x_scientific: bool, optional
    x軸を科学的記法で表示するかどうかを指定します。デフォルト値はFalseです。
y_scientific: bool, optional
    y軸を科学的記法で表示するかどうかを指定します。デフォルト値はFalseです。
fixed_slope: float, optional
    固定傾きの値を指定します。デフォルト値は0.076です。
show_fixed_slope: bool, optional
    固定傾きの線を表示するかどうかを指定します。デフォルト値はFalseです。
figsize: tuple[float, float], optional
    プロットのサイズを(幅, 高さ)で指定します。デフォルト値は(6, 6)です。
dpi: float | None, optional
    プロットの解像度を指定します。デフォルト値は350です。
save_fig: bool, optional
    プロットを保存するかどうかを指定します。デフォルト値はTrueです。
show_fig: bool, optional
    プロットを表示するかどうかを指定します。デフォルト値はTrueです。

Examples

>>> df = pd.DataFrame({
...     'x': [1, 2, 3, 4, 5],
...     'y': [2, 4, 6, 8, 10]
... })
>>> generator = MonthlyFiguresGenerator()
>>> generator.plot_scatter(
...     df=df,
...     col_x='x',
...     col_y='y',
...     xlabel='X軸',
...     ylabel='Y軸',
...     output_dirpath='output'
... )
def plot_source_contributions_diurnal( self, df: pandas.core.frame.DataFrame, col_ch4_flux: str, col_c2h6_flux: str, col_datetime: str = 'Date', color_bio: str = 'blue', color_gas: str = 'red', label_bio: str = 'bio', label_gas: str = 'gas', flux_alpha: float = 0.6, output_dirpath: str | pathlib.Path | None = None, output_filename: str = 'source_contributions.png', window_size: int = 6, print_summary: bool = False, add_xlabel: bool = True, add_ylabel: bool = True, add_legend: bool = True, smooth: bool = False, y_max: float = 100, subplot_label: str | None = None, subplot_fontsize: int = 20, figsize: tuple[float, float] = (10, 6), dpi: float | None = 350, save_fig: bool = True, show_fig: bool = True) -> None:
2217    def plot_source_contributions_diurnal(
2218        self,
2219        df: pd.DataFrame,
2220        col_ch4_flux: str,
2221        col_c2h6_flux: str,
2222        col_datetime: str = "Date",
2223        color_bio: str = "blue",
2224        color_gas: str = "red",
2225        label_bio: str = "bio",
2226        label_gas: str = "gas",
2227        flux_alpha: float = 0.6,
2228        output_dirpath: str | Path | None = None,
2229        output_filename: str = "source_contributions.png",
2230        window_size: int = 6,
2231        print_summary: bool = False,
2232        add_xlabel: bool = True,
2233        add_ylabel: bool = True,
2234        add_legend: bool = True,
2235        smooth: bool = False,
2236        y_max: float = 100,
2237        subplot_label: str | None = None,
2238        subplot_fontsize: int = 20,
2239        figsize: tuple[float, float] = (10, 6),
2240        dpi: float | None = 350,
2241        save_fig: bool = True,
2242        show_fig: bool = True,
2243    ) -> None:
2244        """CH4フラックスの都市ガス起源と生物起源の日変化を積み上げグラフとして表示します。
2245
2246        Parameters
2247        ----------
2248            df: pd.DataFrame
2249                CH4フラックスデータを含むデータフレームを指定します。
2250            col_ch4_flux: str
2251                CH4フラックスの列名を指定します。
2252            col_c2h6_flux: str
2253                C2H6フラックスの列名を指定します。
2254            col_datetime: str, optional
2255                日時の列名を指定します。デフォルト値は"Date"です。
2256            color_bio: str, optional
2257                生物起源の色を指定します。デフォルト値は"blue"です。
2258            color_gas: str, optional
2259                都市ガス起源の色を指定します。デフォルト値は"red"です。
2260            label_bio: str, optional
2261                生物起源のラベルを指定します。デフォルト値は"bio"です。
2262            label_gas: str, optional
2263                都市ガスのラベルを指定します。デフォルト値は"gas"です。
2264            flux_alpha: float, optional
2265                フラックスの透明度を指定します。デフォルト値は0.6です。
2266            output_dirpath: str | Path | None, optional
2267                出力先のディレクトリを指定します。save_fig=Trueの場合は必須です。
2268            output_filename: str, optional
2269                出力ファイル名を指定します。デフォルト値は"source_contributions.png"です。
2270            window_size: int, optional
2271                移動平均の窓サイズを指定します。デフォルト値は6です。
2272            print_summary: bool, optional
2273                統計情報を表示するかどうかを指定します。デフォルト値はFalseです。
2274            add_xlabel: bool, optional
2275                x軸のラベルを追加するかどうかを指定します。デフォルト値はTrueです。
2276            add_ylabel: bool, optional
2277                y軸のラベルを追加するかどうかを指定します。デフォルト値はTrueです。
2278            add_legend: bool, optional
2279                凡例を追加するかどうかを指定します。デフォルト値はTrueです。
2280            smooth: bool, optional
2281                移動平均を適用するかどうかを指定します。デフォルト値はFalseです。
2282            y_max: float, optional
2283                y軸の上限値を指定します。デフォルト値は100です。
2284            subplot_label: str | None, optional
2285                サブプロットのラベルを指定します。デフォルト値はNoneです。
2286            subplot_fontsize: int, optional
2287                サブプロットラベルのフォントサイズを指定します。デフォルト値は20です。
2288            figsize: tuple[float, float], optional
2289                プロットのサイズを指定します。デフォルト値は(10, 6)です。
2290            dpi: float | None, optional
2291                プロットのdpiを指定します。デフォルト値は350です。
2292            save_fig: bool, optional
2293                プロットを保存するかどうかを指定します。デフォルト値はTrueです。
2294            show_fig: bool, optional
2295                プロットを表示するかどうかを指定します。デフォルト値はTrueです。
2296
2297        Examples
2298        --------
2299        >>> df = pd.read_csv("flux_data.csv")
2300        >>> generator = MonthlyFiguresGenerator()
2301        >>> generator.plot_source_contributions_diurnal(
2302        ...     df=df,
2303        ...     col_ch4_flux="Fch4",
2304        ...     col_c2h6_flux="Fc2h6",
2305        ...     output_dirpath="output",
2306        ...     output_filename="diurnal_sources.png"
2307        ... )
2308        """
2309        # 起源の計算
2310        df_with_sources = self._calculate_source_contributions(
2311            df=df,
2312            col_ch4_flux=col_ch4_flux,
2313            col_c2h6_flux=col_c2h6_flux,
2314            col_datetime=col_datetime,
2315        )
2316        df_with_sources.index = pd.to_datetime(df_with_sources.index)
2317
2318        # 時刻データの抽出とグループ化
2319        df_with_sources["hour"] = df_with_sources.index.hour
2320        hourly_means = df_with_sources.groupby("hour")[["ch4_gas", "ch4_bio"]].mean()
2321
2322        # 24時間目のデータ点を追加(0時のデータを使用)
2323        last_hour = hourly_means.iloc[0:1].copy()
2324        last_hour.index = pd.Index([24])
2325        hourly_means = pd.concat([hourly_means, last_hour])
2326
2327        # 移動平均の適用
2328        hourly_means_smoothed = hourly_means
2329        if smooth:
2330            hourly_means_smoothed = hourly_means.rolling(
2331                window=window_size, center=True, min_periods=1
2332            ).mean()
2333
2334        # 24時間分のデータポイントを作成
2335        time_points = pd.date_range("2024-01-01", periods=25, freq="h")
2336
2337        # プロットの作成
2338        fig = plt.figure(figsize=figsize)
2339        ax = plt.gca()
2340
2341        # サブプロットラベルの追加(subplot_labelが指定されている場合)
2342        if subplot_label:
2343            ax.text(
2344                0.02,  # x位置
2345                0.98,  # y位置
2346                subplot_label,
2347                transform=ax.transAxes,
2348                va="top",
2349                fontsize=subplot_fontsize,
2350            )
2351
2352        # 積み上げプロット
2353        ax.fill_between(
2354            time_points,
2355            0,
2356            hourly_means_smoothed["ch4_bio"],
2357            color=color_bio,
2358            alpha=flux_alpha,
2359            label=label_bio,
2360        )
2361        ax.fill_between(
2362            time_points,
2363            hourly_means_smoothed["ch4_bio"],
2364            hourly_means_smoothed["ch4_bio"] + hourly_means_smoothed["ch4_gas"],
2365            color=color_gas,
2366            alpha=flux_alpha,
2367            label=label_gas,
2368        )
2369
2370        # 合計値のライン
2371        total_flux = hourly_means_smoothed["ch4_bio"] + hourly_means_smoothed["ch4_gas"]
2372        ax.plot(time_points, total_flux, "-", color="black", alpha=0.5)
2373
2374        # 軸の設定
2375        if add_xlabel:
2376            ax.set_xlabel("Time (hour)")
2377        if add_ylabel:
2378            ax.set_ylabel(r"CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)")
2379        ax.xaxis.set_major_formatter(mdates.DateFormatter("%-H"))
2380        ax.xaxis.set_major_locator(mdates.HourLocator(byhour=[0, 6, 12, 18, 24]))
2381        ax.set_xlim(
2382            float(mdates.date2num(time_points[0])),
2383            float(mdates.date2num(time_points[-1])),
2384        )
2385        ax.set_ylim(0, y_max)  # y軸の範囲を設定
2386        ax.grid(True, alpha=0.3)
2387
2388        # 凡例を図の下部に配置
2389        if add_legend:
2390            handles, labels = ax.get_legend_handles_labels()
2391            fig = plt.gcf()  # 現在の図を取得
2392            fig.legend(
2393                handles,
2394                labels,
2395                loc="center",
2396                bbox_to_anchor=(0.5, 0.01),
2397                ncol=len(handles),
2398            )
2399            plt.subplots_adjust(bottom=0.2)  # 下部に凡例用のスペースを確保
2400        plt.tight_layout()
2401
2402        # グラフの保存、表示
2403        if save_fig:
2404            if output_dirpath is None:
2405                raise ValueError(
2406                    "save_fig = True の場合、 output_dirpath を指定する必要があります。有効なディレクトリパスを指定してください。"
2407                )
2408            os.makedirs(output_dirpath, exist_ok=True)
2409            output_filepath: str = os.path.join(output_dirpath, output_filename)
2410            plt.savefig(output_filepath, dpi=dpi, bbox_inches="tight")
2411        if show_fig:
2412            plt.show()
2413        plt.close(fig=fig)
2414
2415        # 統計情報の表示
2416        if print_summary:
2417            # 昼夜の時間帯を定義
2418            daytime_range: list[int] = [6, 19]  # 6~18時
2419            daytime_hours = range(daytime_range[0], daytime_range[1])
2420            nighttime_hours = list(range(0, daytime_range[0])) + list(
2421                range(daytime_range[1], 24)
2422            )
2423
2424            # 都市ガスと生物起源のデータを取得
2425            gas_flux = hourly_means["ch4_gas"]
2426            bio_flux = hourly_means["ch4_bio"]
2427            total_flux = gas_flux + bio_flux
2428
2429            # 都市ガス比率を計算
2430            gas_ratio = (gas_flux / total_flux) * 100
2431            daytime_gas_ratio = (
2432                pd.Series(gas_flux).iloc[np.array(list(daytime_hours))].sum()
2433                / pd.Series(total_flux).iloc[np.array(list(daytime_hours))].sum()
2434            ) * 100
2435            nighttime_gas_ratio = (
2436                pd.Series(gas_flux).iloc[np.array(list(nighttime_hours))].sum()
2437                / pd.Series(total_flux).iloc[np.array(list(nighttime_hours))].sum()
2438            ) * 100
2439
2440            stats = {
2441                "都市ガス起源": gas_flux,
2442                "生物起源": bio_flux,
2443                "合計": total_flux,
2444            }
2445
2446            # 都市ガス比率の統計を出力
2447            self.logger.info("\n都市ガス比率の統計:")
2448            print(f"  全体の都市ガス比率: {gas_ratio.mean():.1f}%")
2449            print(
2450                f"  昼間({daytime_range[0]}~{daytime_range[1] - 1}時)の都市ガス比率: {daytime_gas_ratio:.1f}%"
2451            )
2452            print(
2453                f"  夜間({daytime_range[1] - 1}~{daytime_range[0]}時)の都市ガス比率: {nighttime_gas_ratio:.1f}%"
2454            )
2455            print(f"  最小比率: {gas_ratio.min():.1f}% (Hour: {gas_ratio.idxmin()})")
2456            print(f"  最大比率: {gas_ratio.max():.1f}% (Hour: {gas_ratio.idxmax()})")
2457
2458            # 各フラックスの統計を出力
2459            for source, data in stats.items():
2460                mean_val = data.mean()
2461                min_val = data.min()
2462                max_val = data.max()
2463                min_time = data.idxmin()
2464                max_time = data.idxmax()
2465
2466                # 昼間と夜間のデータを抽出
2467                daytime_data = pd.Series(data).iloc[np.array(list(daytime_hours))]
2468                nighttime_data = pd.Series(data).iloc[np.array(list(nighttime_hours))]
2469
2470                daytime_mean = daytime_data.mean()
2471                nighttime_mean = nighttime_data.mean()
2472
2473                self.logger.info(f"\n{source}の統計:")
2474                print(f"  平均値: {mean_val:.2f}")
2475                print(f"  最小値: {min_val:.2f} (Hour: {min_time})")
2476                print(f"  最大値: {max_val:.2f} (Hour: {max_time})")
2477                if min_val != 0:
2478                    print(f"  最大/最小比: {max_val / min_val:.2f}")
2479                print(
2480                    f"  昼間({daytime_range[0]}~{daytime_range[1] - 1}時)の平均: {daytime_mean:.2f}"
2481                )
2482                print(
2483                    f"  夜間({daytime_range[1] - 1}~{daytime_range[0]}時)の平均: {nighttime_mean:.2f}"
2484                )
2485                if nighttime_mean != 0:
2486                    print(f"  昼/夜比: {daytime_mean / nighttime_mean:.2f}")

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

Parameters

df: pd.DataFrame
    CH4フラックスデータを含むデータフレームを指定します。
col_ch4_flux: str
    CH4フラックスの列名を指定します。
col_c2h6_flux: str
    C2H6フラックスの列名を指定します。
col_datetime: str, optional
    日時の列名を指定します。デフォルト値は"Date"です。
color_bio: str, optional
    生物起源の色を指定します。デフォルト値は"blue"です。
color_gas: str, optional
    都市ガス起源の色を指定します。デフォルト値は"red"です。
label_bio: str, optional
    生物起源のラベルを指定します。デフォルト値は"bio"です。
label_gas: str, optional
    都市ガスのラベルを指定します。デフォルト値は"gas"です。
flux_alpha: float, optional
    フラックスの透明度を指定します。デフォルト値は0.6です。
output_dirpath: str | Path | None, optional
    出力先のディレクトリを指定します。save_fig=Trueの場合は必須です。
output_filename: str, optional
    出力ファイル名を指定します。デフォルト値は"source_contributions.png"です。
window_size: int, optional
    移動平均の窓サイズを指定します。デフォルト値は6です。
print_summary: bool, optional
    統計情報を表示するかどうかを指定します。デフォルト値はFalseです。
add_xlabel: bool, optional
    x軸のラベルを追加するかどうかを指定します。デフォルト値はTrueです。
add_ylabel: bool, optional
    y軸のラベルを追加するかどうかを指定します。デフォルト値はTrueです。
add_legend: bool, optional
    凡例を追加するかどうかを指定します。デフォルト値はTrueです。
smooth: bool, optional
    移動平均を適用するかどうかを指定します。デフォルト値はFalseです。
y_max: float, optional
    y軸の上限値を指定します。デフォルト値は100です。
subplot_label: str | None, optional
    サブプロットのラベルを指定します。デフォルト値はNoneです。
subplot_fontsize: int, optional
    サブプロットラベルのフォントサイズを指定します。デフォルト値は20です。
figsize: tuple[float, float], optional
    プロットのサイズを指定します。デフォルト値は(10, 6)です。
dpi: float | None, optional
    プロットのdpiを指定します。デフォルト値は350です。
save_fig: bool, optional
    プロットを保存するかどうかを指定します。デフォルト値はTrueです。
show_fig: bool, optional
    プロットを表示するかどうかを指定します。デフォルト値はTrueです。

Examples

>>> df = pd.read_csv("flux_data.csv")
>>> generator = MonthlyFiguresGenerator()
>>> generator.plot_source_contributions_diurnal(
...     df=df,
...     col_ch4_flux="Fch4",
...     col_c2h6_flux="Fc2h6",
...     output_dirpath="output",
...     output_filename="diurnal_sources.png"
... )
def plot_source_contributions_diurnal_by_date( self, df: pandas.core.frame.DataFrame, col_ch4_flux: str, col_c2h6_flux: str, col_datetime: str = 'Date', color_bio: str = 'blue', color_gas: str = 'red', label_bio: str = 'bio', label_gas: str = 'gas', flux_alpha: float = 0.6, output_dirpath: str | pathlib.Path | None = None, output_filename: str = 'source_contributions_by_date.png', add_xlabel: bool = True, add_ylabel: bool = True, add_legend: bool = True, print_summary: bool = False, subplot_fontsize: int = 20, subplot_label_weekday: str | None = None, subplot_label_weekend: str | None = None, y_max: float | None = None, figsize: tuple[float, float] = (12, 5), dpi: float | None = 350, save_fig: bool = True, show_fig: bool = True) -> None:
2488    def plot_source_contributions_diurnal_by_date(
2489        self,
2490        df: pd.DataFrame,
2491        col_ch4_flux: str,
2492        col_c2h6_flux: str,
2493        col_datetime: str = "Date",
2494        color_bio: str = "blue",
2495        color_gas: str = "red",
2496        label_bio: str = "bio",
2497        label_gas: str = "gas",
2498        flux_alpha: float = 0.6,
2499        output_dirpath: str | Path | None = None,
2500        output_filename: str = "source_contributions_by_date.png",
2501        add_xlabel: bool = True,
2502        add_ylabel: bool = True,
2503        add_legend: bool = True,
2504        print_summary: bool = False,
2505        subplot_fontsize: int = 20,
2506        subplot_label_weekday: str | None = None,
2507        subplot_label_weekend: str | None = None,
2508        y_max: float | None = None,
2509        figsize: tuple[float, float] = (12, 5),
2510        dpi: float | None = 350,
2511        save_fig: bool = True,
2512        show_fig: bool = True,
2513    ) -> None:
2514        """CH4フラックスの都市ガス起源と生物起源の日変化を平日・休日別に表示します。
2515
2516        Parameters
2517        ----------
2518            df: pd.DataFrame
2519                CH4フラックスとC2H6フラックスのデータを含むデータフレームを指定します。
2520            col_ch4_flux: str
2521                CH4フラックスのカラム名を指定します。
2522            col_c2h6_flux: str
2523                C2H6フラックスのカラム名を指定します。
2524            col_datetime: str, optional
2525                日時カラムの名前を指定します。デフォルト値は"Date"です。
2526            color_bio: str, optional
2527                生物起源のプロット色を指定します。デフォルト値は"blue"です。
2528            color_gas: str, optional
2529                都市ガス起源のプロット色を指定します。デフォルト値は"red"です。
2530            label_bio: str, optional
2531                生物起源の凡例ラベルを指定します。デフォルト値は"bio"です。
2532            label_gas: str, optional
2533                都市ガスの凡例ラベルを指定します。デフォルト値は"gas"です。
2534            flux_alpha: float, optional
2535                フラックスプロットの透明度を指定します。デフォルト値は0.6です。
2536            output_dirpath: str | Path | None, optional
2537                出力ディレクトリのパスを指定します。save_fig=Trueの場合は必須です。
2538            output_filename: str, optional
2539                出力ファイル名を指定します。デフォルト値は"source_contributions_by_date.png"です。
2540            add_xlabel: bool, optional
2541                x軸のラベルを表示するかどうかを指定します。デフォルト値はTrueです。
2542            add_ylabel: bool, optional
2543                y軸のラベルを表示するかどうかを指定します。デフォルト値はTrueです。
2544            add_legend: bool, optional
2545                凡例を表示するかどうかを指定します。デフォルト値はTrueです。
2546            print_summary: bool, optional
2547                統計情報を表示するかどうかを指定します。デフォルト値はFalseです。
2548            subplot_fontsize: int, optional
2549                サブプロットのフォントサイズを指定します。デフォルト値は20です。
2550            subplot_label_weekday: str | None, optional
2551                平日グラフのラベルを指定します。デフォルト値はNoneです。
2552            subplot_label_weekend: str | None, optional
2553                休日グラフのラベルを指定します。デフォルト値はNoneです。
2554            y_max: float | None, optional
2555                y軸の上限値を指定します。デフォルト値はNoneです。
2556            figsize: tuple[float, float], optional
2557                プロットのサイズを(幅, 高さ)で指定します。デフォルト値は(12, 5)です。
2558            dpi: float | None, optional
2559                プロットの解像度を指定します。デフォルト値は350です。
2560            save_fig: bool, optional
2561                プロットを保存するかどうかを指定します。デフォルト値はTrueです。
2562            show_fig: bool, optional
2563                プロットを表示するかどうかを指定します。デフォルト値はTrueです。
2564
2565        Examples
2566        --------
2567        >>> df = pd.DataFrame({
2568        ...     'Date': pd.date_range('2024-01-01', periods=48, freq='H'),
2569        ...     'Fch4': [1.2] * 48,
2570        ...     'Fc2h6': [0.1] * 48
2571        ... })
2572        >>> generator = MonthlyFiguresGenerator()
2573        >>> generator.plot_source_contributions_diurnal_by_date(
2574        ...     df=df,
2575        ...     col_ch4_flux='Fch4',
2576        ...     col_c2h6_flux='Fc2h6',
2577        ...     output_dirpath='output'
2578        ... )
2579        """
2580        # 起源の計算
2581        df_with_sources = self._calculate_source_contributions(
2582            df=df,
2583            col_ch4_flux=col_ch4_flux,
2584            col_c2h6_flux=col_c2h6_flux,
2585            col_datetime=col_datetime,
2586        )
2587        df_with_sources.index = pd.to_datetime(df_with_sources.index)
2588
2589        # 日付タイプの分類
2590        dates = pd.to_datetime(df_with_sources.index)
2591        is_weekend = dates.dayofweek.isin([5, 6])
2592        is_holiday = dates.map(lambda x: jpholiday.is_holiday(x.date()))
2593        is_weekday = ~(is_weekend | is_holiday)
2594
2595        # データの分類
2596        data_weekday = df_with_sources[is_weekday]
2597        data_holiday = df_with_sources[is_weekend | is_holiday]
2598
2599        # プロットの作成
2600        fig, (ax1, ax2) = plt.subplots(1, 2, figsize=figsize)
2601
2602        # 平日と休日それぞれのプロット
2603        for ax, data, _ in [
2604            (ax1, data_weekday, "Weekdays"),
2605            (ax2, data_holiday, "Weekends & Holidays"),
2606        ]:
2607            # 時間ごとの平均値を計算
2608            hourly_means = data.groupby(pd.to_datetime(data.index).hour)[
2609                ["ch4_gas", "ch4_bio"]
2610            ].mean()
2611
2612            # 24時間目のデータ点を追加
2613            last_hour = hourly_means.iloc[0:1].copy()
2614            last_hour.index = pd.Index([24])
2615            hourly_means = pd.concat([hourly_means, last_hour])
2616
2617            # 24時間分のデータポイントを作成
2618            time_points = pd.date_range("2024-01-01", periods=25, freq="h")
2619
2620            # 積み上げプロット
2621            ax.fill_between(
2622                time_points,
2623                0,
2624                hourly_means["ch4_bio"],
2625                color=color_bio,
2626                alpha=flux_alpha,
2627                label=label_bio,
2628            )
2629            ax.fill_between(
2630                time_points,
2631                hourly_means["ch4_bio"],
2632                hourly_means["ch4_bio"] + hourly_means["ch4_gas"],
2633                color=color_gas,
2634                alpha=flux_alpha,
2635                label=label_gas,
2636            )
2637
2638            # 合計値のライン
2639            total_flux = hourly_means["ch4_bio"] + hourly_means["ch4_gas"]
2640            ax.plot(time_points, total_flux, "-", color="black", alpha=0.5)
2641
2642            # 軸の設定
2643            if add_xlabel:
2644                ax.set_xlabel("Time (hour)")
2645            if add_ylabel:
2646                if ax == ax1:  # 左側のプロットのラベル
2647                    ax.set_ylabel("CH$_4$ flux\n" r"(nmol m$^{-2}$ s$^{-1}$)")
2648                else:  # 右側のプロットのラベル
2649                    ax.set_ylabel("CH$_4$ flux\n" r"(nmol m$^{-2}$ s$^{-1}$)")
2650
2651            ax.xaxis.set_major_formatter(mdates.DateFormatter("%-H"))
2652            ax.xaxis.set_major_locator(mdates.HourLocator(byhour=[0, 6, 12, 18, 24]))
2653            ax.set_xlim(
2654                float(mdates.date2num(time_points[0])),
2655                float(mdates.date2num(time_points[-1])),
2656            )
2657            if y_max is not None:
2658                ax.set_ylim(0, y_max)
2659            ax.grid(True, alpha=0.3)
2660
2661        # サブプロットラベルの追加
2662        if subplot_label_weekday:
2663            ax1.text(
2664                0.02,
2665                0.98,
2666                subplot_label_weekday,
2667                transform=ax1.transAxes,
2668                va="top",
2669                fontsize=subplot_fontsize,
2670            )
2671        if subplot_label_weekend:
2672            ax2.text(
2673                0.02,
2674                0.98,
2675                subplot_label_weekend,
2676                transform=ax2.transAxes,
2677                va="top",
2678                fontsize=subplot_fontsize,
2679            )
2680
2681        # 凡例を図の下部に配置
2682        if add_legend:
2683            # 最初のプロットから凡例のハンドルとラベルを取得
2684            handles, labels = ax1.get_legend_handles_labels()
2685            # 図の下部に凡例を配置
2686            fig.legend(
2687                handles,
2688                labels,
2689                loc="center",
2690                bbox_to_anchor=(0.5, 0.01),  # x=0.5で中央、y=0.01で下部に配置
2691                ncol=len(handles),  # ハンドルの数だけ列を作成(一行に表示)
2692            )
2693            # 凡例用のスペースを確保
2694            plt.subplots_adjust(bottom=0.2)  # 下部に30%のスペースを確保
2695
2696        plt.tight_layout()
2697        # グラフの保存または表示
2698        if save_fig:
2699            if output_dirpath is None:
2700                raise ValueError(
2701                    "save_fig = True の場合、 output_dirpath を指定する必要があります。有効なディレクトリパスを指定してください。"
2702                )
2703            os.makedirs(output_dirpath, exist_ok=True)
2704            output_filepath: str = os.path.join(output_dirpath, output_filename)
2705            plt.savefig(output_filepath, dpi=dpi, bbox_inches="tight")
2706        if show_fig:
2707            plt.show()
2708        plt.close(fig=fig)
2709
2710        # 統計情報の表示
2711        if print_summary:
2712            for data, label in [
2713                (data_weekday, "Weekdays"),
2714                (data_holiday, "Weekends & Holidays"),
2715            ]:
2716                hourly_means = data.groupby(pd.to_datetime(data.index).hour)[
2717                    ["ch4_gas", "ch4_bio"]
2718                ].mean()
2719
2720                print(f"\n{label}の統計:")
2721
2722                # 都市ガス起源の統計
2723                gas_flux = hourly_means["ch4_gas"]
2724                bio_flux = hourly_means["ch4_bio"]
2725
2726                # 昼夜の時間帯を定義
2727                daytime_range: list[int] = [6, 19]  # m~n時の場合、[m ,(n+1)]と定義
2728                daytime_hours = range(daytime_range[0], daytime_range[1])
2729                nighttime_hours = list(range(0, daytime_range[0])) + list(
2730                    range(daytime_range[1], 24)
2731                )
2732
2733                # 昼間の統計
2734                daytime_gas = pd.Series(gas_flux).iloc[np.array(list(daytime_hours))]
2735                daytime_bio = pd.Series(bio_flux).iloc[np.array(list(daytime_hours))]
2736                daytime_total = daytime_gas + daytime_bio
2737                daytime_total = daytime_gas + daytime_bio
2738                daytime_ratio = (daytime_gas.sum() / daytime_total.sum()) * 100
2739
2740                # 夜間の統計
2741                nighttime_gas = pd.Series(gas_flux).iloc[
2742                    np.array(list(nighttime_hours))
2743                ]
2744                nighttime_bio = pd.Series(bio_flux).iloc[
2745                    np.array(list(nighttime_hours))
2746                ]
2747                nighttime_total = nighttime_gas + nighttime_bio
2748                nighttime_ratio = (nighttime_gas.sum() / nighttime_total.sum()) * 100
2749
2750                print("\n都市ガス起源:")
2751                print(f"  平均値: {gas_flux.mean():.2f}")
2752                print(f"  最小値: {gas_flux.min():.2f} (Hour: {gas_flux.idxmin()})")
2753                print(f"  最大値: {gas_flux.max():.2f} (Hour: {gas_flux.idxmax()})")
2754                if gas_flux.min() != 0:
2755                    print(f"  最大/最小比: {gas_flux.max() / gas_flux.min():.2f}")
2756                print(
2757                    f"  全体に占める割合: {(gas_flux.sum() / (gas_flux.sum() + hourly_means['ch4_bio'].sum()) * 100):.1f}%"
2758                )
2759                print(
2760                    f"  昼間({daytime_range[0]}~{daytime_range[1] - 1}時)の割合: {daytime_ratio:.1f}%"
2761                )
2762                print(
2763                    f"  夜間({daytime_range[1] - 1}~{daytime_range[0]}時)の割合: {nighttime_ratio:.1f}%"
2764                )
2765
2766                # 生物起源の統計
2767                bio_flux = hourly_means["ch4_bio"]
2768                print("\n生物起源:")
2769                print(f"  平均値: {bio_flux.mean():.2f}")
2770                print(f"  最小値: {bio_flux.min():.2f} (Hour: {bio_flux.idxmin()})")
2771                print(f"  最大値: {bio_flux.max():.2f} (Hour: {bio_flux.idxmax()})")
2772                if bio_flux.min() != 0:
2773                    print(f"  最大/最小比: {bio_flux.max() / bio_flux.min():.2f}")
2774                print(
2775                    f"  全体に占める割合: {(bio_flux.sum() / (gas_flux.sum() + bio_flux.sum()) * 100):.1f}%"
2776                )
2777
2778                # 合計フラックスの統計
2779                total_flux = gas_flux + bio_flux
2780                print("\n合計:")
2781                print(f"  平均値: {total_flux.mean():.2f}")
2782                print(f"  最小値: {total_flux.min():.2f} (Hour: {total_flux.idxmin()})")
2783                print(f"  最大値: {total_flux.max():.2f} (Hour: {total_flux.idxmax()})")
2784                if total_flux.min() != 0:
2785                    print(f"  最大/最小比: {total_flux.max() / total_flux.min():.2f}")

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

Parameters

df: pd.DataFrame
    CH4フラックスとC2H6フラックスのデータを含むデータフレームを指定します。
col_ch4_flux: str
    CH4フラックスのカラム名を指定します。
col_c2h6_flux: str
    C2H6フラックスのカラム名を指定します。
col_datetime: str, optional
    日時カラムの名前を指定します。デフォルト値は"Date"です。
color_bio: str, optional
    生物起源のプロット色を指定します。デフォルト値は"blue"です。
color_gas: str, optional
    都市ガス起源のプロット色を指定します。デフォルト値は"red"です。
label_bio: str, optional
    生物起源の凡例ラベルを指定します。デフォルト値は"bio"です。
label_gas: str, optional
    都市ガスの凡例ラベルを指定します。デフォルト値は"gas"です。
flux_alpha: float, optional
    フラックスプロットの透明度を指定します。デフォルト値は0.6です。
output_dirpath: str | Path | None, optional
    出力ディレクトリのパスを指定します。save_fig=Trueの場合は必須です。
output_filename: str, optional
    出力ファイル名を指定します。デフォルト値は"source_contributions_by_date.png"です。
add_xlabel: bool, optional
    x軸のラベルを表示するかどうかを指定します。デフォルト値はTrueです。
add_ylabel: bool, optional
    y軸のラベルを表示するかどうかを指定します。デフォルト値はTrueです。
add_legend: bool, optional
    凡例を表示するかどうかを指定します。デフォルト値はTrueです。
print_summary: bool, optional
    統計情報を表示するかどうかを指定します。デフォルト値はFalseです。
subplot_fontsize: int, optional
    サブプロットのフォントサイズを指定します。デフォルト値は20です。
subplot_label_weekday: str | None, optional
    平日グラフのラベルを指定します。デフォルト値はNoneです。
subplot_label_weekend: str | None, optional
    休日グラフのラベルを指定します。デフォルト値はNoneです。
y_max: float | None, optional
    y軸の上限値を指定します。デフォルト値はNoneです。
figsize: tuple[float, float], optional
    プロットのサイズを(幅, 高さ)で指定します。デフォルト値は(12, 5)です。
dpi: float | None, optional
    プロットの解像度を指定します。デフォルト値は350です。
save_fig: bool, optional
    プロットを保存するかどうかを指定します。デフォルト値はTrueです。
show_fig: bool, optional
    プロットを表示するかどうかを指定します。デフォルト値はTrueです。

Examples

>>> df = pd.DataFrame({
...     'Date': pd.date_range('2024-01-01', periods=48, freq='H'),
...     'Fch4': [1.2] * 48,
...     'Fc2h6': [0.1] * 48
... })
>>> generator = MonthlyFiguresGenerator()
>>> generator.plot_source_contributions_diurnal_by_date(
...     df=df,
...     col_ch4_flux='Fch4',
...     col_c2h6_flux='Fc2h6',
...     output_dirpath='output'
... )
def plot_wind_rose_sources( self, df: pandas.core.frame.DataFrame, output_dirpath: 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 = '都市ガス起源', 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 = False, figsize: tuple[float, float] = (8, 8), dpi: float | None = 350, save_fig: bool = True, show_fig: bool = True) -> None:
2787    def plot_wind_rose_sources(
2788        self,
2789        df: pd.DataFrame,
2790        output_dirpath: str | Path | None = None,
2791        output_filename: str = "edp_wind_rose.png",
2792        col_datetime: str = "Date",
2793        col_ch4_flux: str = "Fch4",
2794        col_c2h6_flux: str = "Fc2h6",
2795        col_wind_dir: str = "Wind direction",
2796        flux_unit: str = r"(nmol m$^{-2}$ s$^{-1}$)",
2797        ymax: float | None = None,
2798        color_bio: str = "blue",
2799        color_gas: str = "red",
2800        label_bio: str = "生物起源",
2801        label_gas: str = "都市ガス起源",
2802        flux_alpha: float = 0.4,
2803        num_directions: int = 8,
2804        gap_degrees: float = 0.0,
2805        center_on_angles: bool = True,
2806        subplot_label: str | None = None,
2807        add_legend: bool = True,
2808        stack_bars: bool = True,
2809        print_summary: bool = False,
2810        figsize: tuple[float, float] = (8, 8),
2811        dpi: float | None = 350,
2812        save_fig: bool = True,
2813        show_fig: bool = True,
2814    ) -> None:
2815        """CH4フラックスの都市ガス起源と生物起源の風配図を作成します。
2816
2817        Parameters
2818        ----------
2819            df: pd.DataFrame
2820                風配図を作成するためのデータフレーム
2821            output_dirpath: str | Path | None, optional
2822                生成された図を保存するディレクトリのパス。デフォルトはNone
2823            output_filename: str, optional
2824                保存するファイル名。デフォルトは"edp_wind_rose.png"
2825            col_datetime: str, optional
2826                日時を示すカラム名。デフォルトは"Date"
2827            col_ch4_flux: str, optional
2828                CH4フラックスを示すカラム名。デフォルトは"Fch4"
2829            col_c2h6_flux: str, optional
2830                C2H6フラックスを示すカラム名。デフォルトは"Fc2h6"
2831            col_wind_dir: str, optional
2832                風向を示すカラム名。デフォルトは"Wind direction"
2833            flux_unit: str, optional
2834                フラックスの単位。デフォルトは"(nmol m$^{-2}$ s$^{-1}$)"
2835            ymax: float | None, optional
2836                y軸の上限値。指定しない場合はデータの最大値に基づいて自動設定。デフォルトはNone
2837            color_bio: str, optional
2838                生物起源のフラックスに対する色。デフォルトは"blue"
2839            color_gas: str, optional
2840                都市ガス起源のフラックスに対する色。デフォルトは"red"
2841            label_bio: str, optional
2842                生物起源のフラックスに対するラベル。デフォルトは"生物起源"
2843            label_gas: str, optional
2844                都市ガス起源のフラックスに対するラベル。デフォルトは"都市ガス起源"
2845            flux_alpha: float, optional
2846                フラックスの透明度。デフォルトは0.4
2847            num_directions: int, optional
2848                風向の数。デフォルトは8
2849            gap_degrees: float, optional
2850                セクター間の隙間の大きさ(度数)。0の場合は隙間なし。デフォルトは0.0
2851            center_on_angles: bool, optional
2852                45度刻みの線を境界として扇形を描画するかどうか。Trueの場合は境界として、Falseの場合は中間(22.5度)を中心として描画。デフォルトはTrue
2853            subplot_label: str | None, optional
2854                サブプロットに表示するラベル。デフォルトはNone
2855            add_legend: bool, optional
2856                凡例を表示するかどうか。デフォルトはTrue
2857            stack_bars: bool, optional
2858                生物起源の上に都市ガス起源を積み上げるかどうか。Trueの場合は積み上げ、Falseの場合は両方を0から積み上げ。デフォルトはTrue
2859            print_summary: bool, optional
2860                統計情報を表示するかどうか。デフォルトはFalse
2861            figsize: tuple[float, float], optional
2862                プロットのサイズ。デフォルトは(8, 8)
2863            dpi: float | None, optional
2864                プロットのdpi。デフォルトは350
2865            save_fig: bool, optional
2866                図を保存するかどうか。デフォルトはTrue
2867            show_fig: bool, optional
2868                図を表示するかどうか。デフォルトはTrue
2869
2870        Returns
2871        ----------
2872            None
2873
2874        Examples
2875        ----------
2876        >>> # 基本的な使用方法
2877        >>> generator = MonthlyFiguresGenerator()
2878        >>> generator.plot_wind_rose_sources(
2879        ...     df=data,
2880        ...     output_dirpath="output/figures",
2881        ...     output_filename="wind_rose_2023.png"
2882        ... )
2883        
2884        >>> # カスタマイズした例
2885        >>> generator.plot_wind_rose_sources(
2886        ...     df=data,
2887        ...     num_directions=16,  # 16方位で表示
2888        ...     stack_bars=False,   # 積み上げない
2889        ...     color_bio="green",  # 色を変更
2890        ...     color_gas="orange"
2891        ... )
2892        """
2893        # 起源の計算
2894        df_with_sources = self._calculate_source_contributions(
2895            df=df,
2896            col_ch4_flux=col_ch4_flux,
2897            col_c2h6_flux=col_c2h6_flux,
2898            col_datetime=col_datetime,
2899        )
2900
2901        # 方位の定義
2902        direction_ranges = self._define_direction_ranges(
2903            num_directions, center_on_angles
2904        )
2905
2906        # 方位ごとのデータを集計
2907        direction_data = self._aggregate_direction_data(
2908            df_with_sources, col_wind_dir, direction_ranges
2909        )
2910
2911        # プロットの作成
2912        fig = plt.figure(figsize=figsize, dpi=dpi)
2913        ax = fig.add_subplot(111, projection="polar")
2914
2915        # 方位の角度(ラジアン)を計算
2916        theta = np.array(
2917            [np.radians(angle) for angle in direction_data["center_angle"]]
2918        )
2919
2920        # セクターの幅を計算(隙間を考慮)
2921        sector_width = np.radians((360.0 / num_directions) - gap_degrees)
2922
2923        # 積み上げ方式に応じてプロット
2924        if stack_bars:
2925            # 生物起源を基準として描画
2926            ax.bar(
2927                theta,
2928                direction_data["bio_flux"],
2929                width=sector_width,  # 隙間を考慮した幅
2930                bottom=0.0,
2931                color=color_bio,
2932                alpha=flux_alpha,
2933                label=label_bio,
2934            )
2935            # 都市ガス起源を生物起源の上に積み上げ
2936            ax.bar(
2937                theta,
2938                direction_data["gas_flux"],
2939                width=sector_width,  # 隙間を考慮した幅
2940                bottom=direction_data["bio_flux"],
2941                color=color_gas,
2942                alpha=flux_alpha,
2943                label=label_gas,
2944            )
2945        else:
2946            # 両方を0から積み上げ
2947            ax.bar(
2948                theta,
2949                direction_data["bio_flux"],
2950                width=sector_width,  # 隙間を考慮した幅
2951                bottom=0.0,
2952                color=color_bio,
2953                alpha=flux_alpha,
2954                label=label_bio,
2955            )
2956            ax.bar(
2957                theta,
2958                direction_data["gas_flux"],
2959                width=sector_width,  # 隙間を考慮した幅
2960                bottom=0.0,
2961                color=color_gas,
2962                alpha=flux_alpha,
2963                label=label_gas,
2964            )
2965
2966        # y軸の範囲を設定
2967        if ymax is not None:
2968            ax.set_ylim(0, ymax)
2969        else:
2970            # データの最大値に基づいて自動設定
2971            max_value = max(
2972                direction_data["bio_flux"].max(), direction_data["gas_flux"].max()
2973            )
2974            ax.set_ylim(0, max_value * 1.1)  # 最大値の1.1倍を上限に設定
2975
2976        # 方位ラベルの設定
2977        # 北を上に設定
2978        ax.set_theta_zero_location("N")  # type:ignore
2979        # 時計回りに設定
2980        ax.set_theta_direction(-1)  # type:ignore
2981
2982        # 方位ラベルの表示
2983        labels = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"]
2984        angles = np.radians(np.linspace(0, 360, len(labels), endpoint=False))
2985        ax.set_xticks(angles)
2986        ax.set_xticklabels(labels)
2987
2988        # プロット領域の調整(上部と下部にスペースを確保)
2989        plt.subplots_adjust(
2990            top=0.8,  # 上部に20%のスペースを確保
2991            bottom=0.2,  # 下部に20%のスペースを確保(凡例用)
2992        )
2993
2994        # サブプロットラベルの追加(デフォルトは左上)
2995        if subplot_label:
2996            ax.text(
2997                0.01,
2998                0.99,
2999                subplot_label,
3000                transform=ax.transAxes,
3001            )
3002
3003        # 単位の追加(図の下部中央に配置)
3004        plt.figtext(
3005            0.5,  # x位置(中央)
3006            0.1,  # y位置(下部)
3007            flux_unit,
3008            ha="center",  # 水平方向の位置揃え
3009            va="bottom",  # 垂直方向の位置揃え
3010        )
3011
3012        # 凡例の追加(単位の下に配置)
3013        if add_legend:
3014            # 最初のプロットから凡例のハンドルとラベルを取得
3015            handles, labels = ax.get_legend_handles_labels()
3016            # 図の下部に凡例を配置
3017            fig.legend(
3018                handles,
3019                labels,
3020                loc="center",
3021                bbox_to_anchor=(0.5, 0.05),  # x=0.5で中央、y=0.05で下部に配置
3022                ncol=len(handles),  # ハンドルの数だけ列を作成(一行に表示)
3023            )
3024
3025        # グラフの保存または表示
3026        if save_fig:
3027            if output_dirpath is None:
3028                raise ValueError(
3029                    "save_fig = True の場合、 output_dirpath を指定する必要があります。有効なディレクトリパスを指定してください。"
3030                )
3031            os.makedirs(output_dirpath, exist_ok=True)
3032            output_filepath: str = os.path.join(output_dirpath, output_filename)
3033            plt.savefig(output_filepath, dpi=dpi, bbox_inches="tight")
3034        if show_fig:
3035            plt.show()
3036        plt.close(fig=fig)
3037
3038        # 統計情報の表示
3039        if print_summary:
3040            for source in ["gas", "bio"]:
3041                flux_data = direction_data[f"{source}_flux"]
3042                mean_val = flux_data.mean()
3043                max_val = flux_data.max()
3044                max_dir = direction_data.loc[flux_data.idxmax(), "name"]
3045
3046                self.logger.info(
3047                    f"{label_gas if source == 'gas' else label_bio}の統計:"
3048                )
3049                print(f"  平均フラックス: {mean_val:.2f}")
3050                print(f"  最大フラックス: {max_val:.2f}")
3051                print(f"  最大フラックスの方位: {max_dir}")

CH4フラックスの都市ガス起源と生物起源の風配図を作成します。

Parameters

df: pd.DataFrame
    風配図を作成するためのデータフレーム
output_dirpath: str | Path | None, optional
    生成された図を保存するディレクトリのパス。デフォルトはNone
output_filename: str, optional
    保存するファイル名。デフォルトは"edp_wind_rose.png"
col_datetime: str, optional
    日時を示すカラム名。デフォルトは"Date"
col_ch4_flux: str, optional
    CH4フラックスを示すカラム名。デフォルトは"Fch4"
col_c2h6_flux: str, optional
    C2H6フラックスを示すカラム名。デフォルトは"Fc2h6"
col_wind_dir: str, optional
    風向を示すカラム名。デフォルトは"Wind direction"
flux_unit: str, optional
    フラックスの単位。デフォルトは"(nmol m$^{-2}$ s$^{-1}$)"
ymax: float | None, optional
    y軸の上限値。指定しない場合はデータの最大値に基づいて自動設定。デフォルトはNone
color_bio: str, optional
    生物起源のフラックスに対する色。デフォルトは"blue"
color_gas: str, optional
    都市ガス起源のフラックスに対する色。デフォルトは"red"
label_bio: str, optional
    生物起源のフラックスに対するラベル。デフォルトは"生物起源"
label_gas: str, optional
    都市ガス起源のフラックスに対するラベル。デフォルトは"都市ガス起源"
flux_alpha: float, optional
    フラックスの透明度。デフォルトは0.4
num_directions: int, optional
    風向の数。デフォルトは8
gap_degrees: float, optional
    セクター間の隙間の大きさ(度数)。0の場合は隙間なし。デフォルトは0.0
center_on_angles: bool, optional
    45度刻みの線を境界として扇形を描画するかどうか。Trueの場合は境界として、Falseの場合は中間(22.5度)を中心として描画。デフォルトはTrue
subplot_label: str | None, optional
    サブプロットに表示するラベル。デフォルトはNone
add_legend: bool, optional
    凡例を表示するかどうか。デフォルトはTrue
stack_bars: bool, optional
    生物起源の上に都市ガス起源を積み上げるかどうか。Trueの場合は積み上げ、Falseの場合は両方を0から積み上げ。デフォルトはTrue
print_summary: bool, optional
    統計情報を表示するかどうか。デフォルトはFalse
figsize: tuple[float, float], optional
    プロットのサイズ。デフォルトは(8, 8)
dpi: float | None, optional
    プロットのdpi。デフォルトは350
save_fig: bool, optional
    図を保存するかどうか。デフォルトはTrue
show_fig: bool, optional
    図を表示するかどうか。デフォルトはTrue

Returns

None

Examples

>>> # 基本的な使用方法
>>> generator = MonthlyFiguresGenerator()
>>> generator.plot_wind_rose_sources(
...     df=data,
...     output_dirpath="output/figures",
...     output_filename="wind_rose_2023.png"
... )
>>> # カスタマイズした例
>>> generator.plot_wind_rose_sources(
...     df=data,
...     num_directions=16,  # 16方位で表示
...     stack_bars=False,   # 積み上げない
...     color_bio="green",  # 色を変更
...     color_gas="orange"
... )
@staticmethod
def get_valid_data( df: pandas.core.frame.DataFrame, col_x: str, col_y: str) -> pandas.core.frame.DataFrame:
3342    @staticmethod
3343    def get_valid_data(df: pd.DataFrame, col_x: str, col_y: str) -> pd.DataFrame:
3344        """指定された列の有効なデータ(NaNを除いた)を取得します。
3345
3346        Parameters
3347        ----------
3348            df: pd.DataFrame
3349                データフレームを指定します。
3350            col_x: str
3351                X軸の列名を指定します。
3352            col_y: str
3353                Y軸の列名を指定します。
3354
3355        Returns
3356        ----------
3357            pd.DataFrame
3358                有効なデータのみを含むDataFrameを返します。
3359
3360        Examples
3361        --------
3362        >>> df = pd.DataFrame({
3363        ...     'x': [1, 2, np.nan, 4],
3364        ...     'y': [1, np.nan, 3, 4]
3365        ... })
3366        >>> valid_df = MonthlyFiguresGenerator.get_valid_data(df, 'x', 'y')
3367        >>> print(valid_df)
3368           x  y
3369        0  1  1
3370        3  4  4
3371        """
3372        return df.copy().dropna(subset=[col_x, col_y])

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

Parameters

df: pd.DataFrame
    データフレームを指定します。
col_x: str
    X軸の列名を指定します。
col_y: str
    Y軸の列名を指定します。

Returns

pd.DataFrame
    有効なデータのみを含むDataFrameを返します。

Examples

>>> df = pd.DataFrame({
...     'x': [1, 2, np.nan, 4],
...     'y': [1, np.nan, 3, 4]
... })
>>> valid_df = MonthlyFiguresGenerator.get_valid_data(df, 'x', 'y')
>>> print(valid_df)
   x  y
0  1  1
3  4  4
@dataclass
class SlopeLine:
19@dataclass
20class SlopeLine:
21    """傾き線の設定用のデータクラス
22
23    Parameters
24    ----------
25        coordinates: tuple[tuple[float, float], tuple[float, float]]
26            傾き線の始点と終点の座標。((x1, y1), (x2, y2))の形式で指定。
27            x座標は周波数(Hz)、y座標は無次元化されたスペクトル値を表す。
28        text: str
29            傾き線に付随するテキスト(例:"-2/3", "-4/3"など)。
30        text_pos: tuple[float, float] | None
31            テキストを表示する位置の座標(x, y)。
32            x座標は周波数(Hz)、y座標は無次元化されたスペクトル値を表す。
33            Noneの場合、テキストは表示されない。
34        fontsize: float, optional
35            テキストのフォントサイズ。デフォルトは20。
36
37    Examples
38    --------
39        >>> power_slope = SlopeLine(
40        ...     coordinates=((0.01, 10), (10, 0.01)),
41        ...     text="-2/3",
42        ...     text_pos=(0.1, 0.06),
43        ...     fontsize=18
44        ... )
45    """
46
47    coordinates: tuple[tuple[float, float], tuple[float, float]]
48    text: str
49    text_pos: tuple[float, float] | None
50    fontsize: float = 20
51
52    def plot(self, ax: Axes) -> None:
53        """傾き線とテキストを描画する
54
55        Parameters
56        ----------
57            ax: matplotlib.axes.Axes
58                描画対象のAxesオブジェクト
59        """
60        (x1, y1), (x2, y2) = self.coordinates
61        ax.plot([x1, x2], [y1, y2], "-", color="black", alpha=0.5)
62        if self.text_pos:
63            ax.text(
64                self.text_pos[0], self.text_pos[1], self.text, fontsize=self.fontsize
65            )

傾き線の設定用のデータクラス

Parameters

coordinates: tuple[tuple[float, float], tuple[float, float]]
    傾き線の始点と終点の座標。((x1, y1), (x2, y2))の形式で指定。
    x座標は周波数(Hz)、y座標は無次元化されたスペクトル値を表す。
text: str
    傾き線に付随するテキスト(例:"-2/3", "-4/3"など)。
text_pos: tuple[float, float] | None
    テキストを表示する位置の座標(x, y)。
    x座標は周波数(Hz)、y座標は無次元化されたスペクトル値を表す。
    Noneの場合、テキストは表示されない。
fontsize: float, optional
    テキストのフォントサイズ。デフォルトは20。

Examples

>>> power_slope = SlopeLine(
...     coordinates=((0.01, 10), (10, 0.01)),
...     text="-2/3",
...     text_pos=(0.1, 0.06),
...     fontsize=18
... )
SlopeLine( coordinates: tuple[tuple[float, float], tuple[float, float]], text: str, text_pos: tuple[float, float] | None, fontsize: float = 20)
coordinates: tuple[tuple[float, float], tuple[float, float]]
text: str
text_pos: tuple[float, float] | None
fontsize: float = 20
def plot(self, ax: matplotlib.axes._axes.Axes) -> None:
52    def plot(self, ax: Axes) -> None:
53        """傾き線とテキストを描画する
54
55        Parameters
56        ----------
57            ax: matplotlib.axes.Axes
58                描画対象のAxesオブジェクト
59        """
60        (x1, y1), (x2, y2) = self.coordinates
61        ax.plot([x1, x2], [y1, y2], "-", color="black", alpha=0.5)
62        if self.text_pos:
63            ax.text(
64                self.text_pos[0], self.text_pos[1], self.text, fontsize=self.fontsize
65            )

傾き線とテキストを描画する

Parameters

ax: matplotlib.axes.Axes
    描画対象のAxesオブジェクト
@dataclass
class SpectralPlotConfig:
68@dataclass
69class SpectralPlotConfig:
70    """スペクトルプロット設定用のデータクラス
71
72    Parameters
73    ----------
74        power_ylabel: str
75            パワースペクトルのy軸ラベル。
76            LaTeXの数式表記が使用可能(例r"$fS_{\\mathrm{CH_4}} / s_{\\mathrm{CH_4}}^2$")。
77        co_ylabel: str
78            コスペクトルのy軸ラベル。
79            LaTeXの数式表記が使用可能(例:r"$fC_{w\\mathrm{CH_4}} / \\overline{w'\\mathrm{CH_4}'}$")。
80        color: str
81            プロットの色。matplotlib.colorsで定義されている色名または16進数カラーコードを指定。
82        label: str | None, optional
83            凡例に表示するラベル。Noneの場合、凡例は表示されない。デフォルトはNone。
84
85    Examples
86    --------
87        >>> ch4_config = SpectralPlotConfig(
88        ...     power_ylabel=r"$fS_{\\mathrm{CH_4}} / s_{\\mathrm{CH_4}}^2$",
89        ...     co_ylabel=r"$fC_{w\\mathrm{CH_4}} / \\overline{w'\\mathrm{CH_4}'}$",
90        ...     color="red",
91        ...     label="CH4"
92        ... )
93    """
94
95    power_ylabel: str
96    co_ylabel: str
97    color: str
98    label: str | None = None

スペクトルプロット設定用のデータクラス

Parameters

power_ylabel: str
    パワースペクトルのy軸ラベル。
    LaTeXの数式表記が使用可能(例r"$fS_{\mathrm{CH_4}} / s_{\mathrm{CH_4}}^2$")。
co_ylabel: str
    コスペクトルのy軸ラベル。
    LaTeXの数式表記が使用可能(例:r"$fC_{w\mathrm{CH_4}} / \overline{w'\mathrm{CH_4}'}$")。
color: str
    プロットの色。matplotlib.colorsで定義されている色名または16進数カラーコードを指定。
label: str | None, optional
    凡例に表示するラベル。Noneの場合、凡例は表示されない。デフォルトはNone。

Examples

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

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

Parameters

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

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

Parameters

col1: str
    データの列名1
col2: str  
    データの列名2
dimensionless: bool, optional
    分散で割って無次元化を行うかのフラグ。デフォルトはTrueで無次元化を行う
frequency_weighted: bool, optional
    周波数の重みづけを適用するかのフラグ。デフォルトはTrueで重みづけを行う
interpolate_points: bool, optional
    対数軸上で等間隔なデータ点を生成するかのフラグ。デフォルトはTrueで等間隔点を生成する
scaling: str, optional
    スペクトルのスケーリング方法。"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
    以下の3つの要素を含むタプル:
    - freqs: np.ndarray
        周波数軸(対数スケールの場合は対数変換済み)
    - co_spectrum: np.ndarray  
        コスペクトル(対数スケールの場合は対数変換済み)
    - corr_coef: float
        変数の相関係数

Examples

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

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

Parameters

col1: str
    データの列名1
col2: str
    データの列名2
dimensionless: bool, optional
    分散で割って無次元化を行うかのフラグ。デフォルトはTrueで無次元化を行う
frequency_weighted: bool, optional
    周波数の重みづけを適用するかのフラグ。デフォルトはTrueで重みづけを行う
interpolate_points: bool, optional
    対数軸上で等間隔なデータ点を生成するかのフラグ。デフォルトはTrueで等間隔点を生成する
scaling: str, optional
    スペクトルのスケーリング方法。"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[np.ndarray, np.ndarray, np.ndarray, float]
    以下の4つの要素を含むタプル:
    - freqs: 周波数軸(対数スケールの場合は対数変換済み)
    - co_spectrum: コスペクトル(対数スケールの場合は対数変換済み)
    - quad_spectrum: クアドラチャスペクトル
    - corr_coef: 変数の相関係数

Examples

>>> sc = SpectrumCalculator(df=data, fs=10)
>>> freqs, co_spec, quad_spec, corr = sc.calculate_cross_spectrum(
...     col1="Uz",
...     col2="Ultra_CH4_ppm_C",
...     lag_second=0.1
... )
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:
260    def calculate_power_spectrum(
261        self,
262        col: str,
263        dimensionless: bool = True,
264        frequency_weighted: bool = True,
265        interpolate_points: bool = True,
266        scaling: str = "spectrum",
267        detrend_1st: bool = True,
268        detrend_2nd: bool = False,
269    ) -> tuple:
270        """指定されたcolに基づいてDataFrameからパワースペクトルと周波数軸を計算します。
271        scipy.signal.welchを使用してパワースペクトルを計算します。
272
273        Parameters
274        ----------
275            col: str
276                パワースペクトルを計算するデータの列名
277            dimensionless: bool, optional
278                分散で割って無次元化を行うかどうか。デフォルトはTrueで無次元化を行います。
279            frequency_weighted: bool, optional
280                周波数の重みづけを適用するかどうか。デフォルトはTrueで重みづけを行います。
281            interpolate_points: bool, optional
282                対数軸上で等間隔なデータ点を生成するかどうか。デフォルトはTrueで等間隔化を行います。
283            scaling: str, optional
284                スペクトルの計算方法。"density"でスペクトル密度、"spectrum"でスペクトル。デフォルトは"spectrum"です。
285            detrend_1st: bool, optional
286                1次トレンドを除去するかどうか。デフォルトはTrueで除去を行います。
287            detrend_2nd: bool, optional
288                2次トレンドを除去するかどうか。デフォルトはFalseで除去を行いません。
289
290        Returns
291        ----------
292            tuple
293                以下の要素を含むタプル:
294                - freqs (np.ndarray): 周波数軸(対数スケールの場合は対数変換済み)
295                - power_spectrum (np.ndarray): パワースペクトル(対数スケールの場合は対数変換済み)
296
297        Examples
298        --------
299        >>> sc = SpectrumCalculator(df=data_frame, fs=10)
300        >>> freqs, power = sc.calculate_power_spectrum(
301        ...     col="Uz",
302        ...     dimensionless=True,
303        ...     frequency_weighted=True
304        ... )
305        """
306        # バリデーション
307        valid_scaling_options = ["density", "spectrum"]
308        if scaling not in valid_scaling_options:
309            raise ValueError(
310                f"'scaling'は次のパラメータから選択してください: {valid_scaling_options}"
311            )
312
313        # データの取得とトレンド除去
314        df_internal: pd.DataFrame = self._df.copy()
315        data: np.ndarray = np.array(df_internal[col].values)
316        # どちらか一方でもTrueの場合は適用
317        if detrend_1st or detrend_2nd:
318            data = SpectrumCalculator._detrend(
319                data=data, first=detrend_1st, second=detrend_2nd
320            )
321
322        # welchメソッドでパワースペクトル計算
323        freqs, power_spectrum = signal.welch(
324            data, fs=self._fs, window=self._window_type, nperseg=1024, scaling=scaling
325        )
326
327        # 周波数の重みづけ(0Hz除外の前に実施)
328        if frequency_weighted:
329            power_spectrum = freqs * power_spectrum
330
331        # 無次元化(0Hz除外の前に実施)
332        if dimensionless:
333            variance = np.var(data)
334            power_spectrum /= variance
335
336        if interpolate_points:
337            # 補間処理(0Hz除外の前に実施)
338            log_freq_min = np.log10(0.001)
339            log_freq_max = np.log10(freqs[-1])
340            log_freq_resampled = np.logspace(log_freq_min, log_freq_max, self._plots)
341
342            power_spectrum_resampled = np.interp(
343                log_freq_resampled, freqs, power_spectrum, left=np.nan, right=np.nan
344            )
345
346            # NaNを除外
347            valid_mask = ~np.isnan(power_spectrum_resampled)
348            freqs = log_freq_resampled[valid_mask]
349            power_spectrum = power_spectrum_resampled[valid_mask]
350
351        # 0Hz成分を最後に除外
352        nonzero_mask = freqs != 0
353        freqs = freqs[nonzero_mask]
354        power_spectrum = power_spectrum[nonzero_mask]
355
356        return freqs, power_spectrum

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

Parameters

col: str
    パワースペクトルを計算するデータの列名
dimensionless: bool, optional
    分散で割って無次元化を行うかどうか。デフォルトはTrueで無次元化を行います。
frequency_weighted: bool, optional
    周波数の重みづけを適用するかどうか。デフォルトはTrueで重みづけを行います。
interpolate_points: bool, optional
    対数軸上で等間隔なデータ点を生成するかどうか。デフォルトはTrueで等間隔化を行います。
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): パワースペクトル(対数スケールの場合は対数変換済み)

Examples

>>> sc = SpectrumCalculator(df=data_frame, fs=10)
>>> freqs, power = sc.calculate_power_spectrum(
...     col="Uz",
...     dimensionless=True,
...     frequency_weighted=True
... )
@dataclass
class TfCurvesFromCsvConfig:
12@dataclass
13class TfCurvesFromCsvConfig:
14    """伝達関数曲線のプロット設定を保持するデータクラス"""
15
16    col_coef_a: str  # 係数のカラム名
17    label_gas: str  # ガスの表示ラベル
18    base_color: str  # 平均線の色
19    gas_name: str  # 出力ファイル用のガス名
20
21    @classmethod
22    def create_default_configs(cls) -> list["TfCurvesFromCsvConfig"]:
23        """デフォルトの設定リストを生成"""
24        return [
25            cls("a_ch4-used", "CH$_4$", "red", "ch4"),
26            cls("a_c2h6-used", "C$_2$H$_6$", "orange", "c2h6"),
27        ]
28
29    @classmethod
30    def from_tuple(
31        cls, config_tuple: tuple[str, str, str, str]
32    ) -> "TfCurvesFromCsvConfig":
33        """タプルから設定オブジェクトを生成"""
34        return cls(*config_tuple)

伝達関数曲線のプロット設定を保持するデータクラス

TfCurvesFromCsvConfig(col_coef_a: str, label_gas: str, base_color: str, gas_name: str)
col_coef_a: str
label_gas: str
base_color: str
gas_name: str
@classmethod
def create_default_configs( cls) -> list[TfCurvesFromCsvConfig]:
21    @classmethod
22    def create_default_configs(cls) -> list["TfCurvesFromCsvConfig"]:
23        """デフォルトの設定リストを生成"""
24        return [
25            cls("a_ch4-used", "CH$_4$", "red", "ch4"),
26            cls("a_c2h6-used", "C$_2$H$_6$", "orange", "c2h6"),
27        ]

デフォルトの設定リストを生成

@classmethod
def from_tuple( cls, config_tuple: tuple[str, str, str, str]) -> TfCurvesFromCsvConfig:
29    @classmethod
30    def from_tuple(
31        cls, config_tuple: tuple[str, str, str, str]
32    ) -> "TfCurvesFromCsvConfig":
33        """タプルから設定オブジェクトを生成"""
34        return cls(*config_tuple)

タプルから設定オブジェクトを生成

class TransferFunctionCalculator:
 37class TransferFunctionCalculator:
 38    """
 39    このクラスは、CSVファイルからデータを読み込み、処理し、
 40    伝達関数を計算してプロットするための機能を提供します。
 41
 42    この実装は Moore (1986) の論文に基づいています。
 43    """
 44
 45    def __init__(
 46        self,
 47        filepath: str | Path,
 48        col_freq: str,
 49        cutoff_freq_low: float = 0.01,
 50        cutoff_freq_high: float = 1,
 51    ):
 52        """
 53        伝達関数計算のためのクラスを初期化します。
 54
 55        Parameters
 56        ----------
 57            filepath: str | Path
 58                分析対象のCSVファイルのパス
 59            col_freq: str
 60                周波数データが格納されている列名
 61            cutoff_freq_low: float, optional
 62                カットオフ周波数の最低値。デフォルトは0.01Hz
 63            cutoff_freq_high: float, optional
 64                カットオフ周波数の最高値。デフォルトは1Hz
 65
 66        Examples
 67        -------
 68        >>> calculator = TransferFunctionCalculator(
 69        ...     filepath="data.csv",
 70        ...     col_freq="frequency",
 71        ...     cutoff_freq_low=0.02,
 72        ...     cutoff_freq_high=0.5
 73        ... )
 74        """
 75        self._col_freq: str = col_freq
 76        self._cutoff_freq_low: float = cutoff_freq_low
 77        self._cutoff_freq_high: float = cutoff_freq_high
 78        self._df: pd.DataFrame = TransferFunctionCalculator._load_data(filepath)
 79
 80    def calculate_transfer_function(
 81        self, col_reference: str, col_target: str
 82    ) -> tuple[float, float, pd.DataFrame]:
 83        """
 84        伝達関数の係数を計算します。
 85
 86        Parameters
 87        ----------
 88            col_reference: str
 89                参照データが格納されている列名を指定します。
 90            col_target: str
 91                ターゲットデータが格納されている列名を指定します。
 92
 93        Returns
 94        ----------
 95            tuple[float, float, pandas.DataFrame]
 96                伝達関数の係数a、係数aの標準誤差、および計算に使用したDataFrameを返します。
 97
 98        Examples
 99        -------
100        >>> calculator = TransferFunctionCalculator("data.csv", "frequency")
101        >>> a, a_err, df = calculator.calculate_transfer_function(
102        ...     col_reference="ref_data",
103        ...     col_target="target_data"
104        ... )
105        >>> print(f"伝達関数の係数: {a:.3f} ± {a_err:.3f}")
106        """
107        df_processed: pd.DataFrame = self.process_data(
108            col_reference=col_reference, col_target=col_target
109        )
110        df_cutoff: pd.DataFrame = self._cutoff_df(df_processed)
111
112        array_x = np.array(df_cutoff.index)
113        array_y = np.array(df_cutoff["target"] / df_cutoff["reference"])
114
115        # フィッティングパラメータと共分散行列を取得
116        popt, pcov = curve_fit(
117            TransferFunctionCalculator.transfer_function, array_x, array_y
118        )
119
120        # 標準誤差を計算(共分散行列の対角成分の平方根)
121        perr = np.sqrt(np.diag(pcov))
122
123        # 係数aとその標準誤差、および計算に用いたDataFrameを返す
124        return popt[0], perr[0], df_processed
125
126    def create_plot_co_spectra(
127        self,
128        col1: str,
129        col2: str,
130        color1: str = "gray",
131        color2: str = "red",
132        figsize: tuple[float, float] = (10, 6),
133        dpi: float | None = 350,
134        label1: str | None = None,
135        label2: str | None = None,
136        output_dirpath: str | Path | None = None,
137        output_filename: str = "co.png",
138        add_legend: bool = True,
139        add_xy_labels: bool = True,
140        legend_font_size: float = 16,
141        save_fig: bool = True,
142        show_fig: bool = True,
143        subplot_label: str | None = None,
144        window_size: int = 5,
145        markersize: float = 14,
146        xlim: tuple[float, float] = (0.0001, 10),
147        ylim: tuple[float, float] = (0.0001, 10),
148        slope_line: tuple[tuple[float, float], tuple[float, float]] = (
149            (0.01, 10),
150            (10, 0.001),
151        ),
152        slope_text: tuple[str, tuple[float, float]] = ("-4/3", (0.25, 0.4)),
153        subplot_label_pos: tuple[float, float] = (0.00015, 3),
154    ) -> None:
155        """
156        2種類のコスペクトルをプロットします。
157
158        Parameters
159        ----------
160            col1: str
161                1つ目のコスペクトルデータのカラム名を指定します。
162            col2: str
163                2つ目のコスペクトルデータのカラム名を指定します。
164            color1: str, optional
165                1つ目のデータの色を指定します。デフォルトは'gray'です。
166            color2: str, optional
167                2つ目のデータの色を指定します。デフォルトは'red'です。
168            figsize: tuple[float, float], optional
169                プロットのサイズを指定します。デフォルトは(10, 6)です。
170            dpi: float | None, optional
171                プロットの解像度を指定します。デフォルトは350です。
172            label1: str | None, optional
173                1つ目のデータの凡例ラベルを指定します。デフォルトはNoneです。
174            label2: str | None, optional
175                2つ目のデータの凡例ラベルを指定します。デフォルトはNoneです。
176            output_dirpath: str | Path | None, optional
177                プロットを保存するディレクトリを指定します。save_fig=Trueの場合は必須です。デフォルトはNoneです。
178            output_filename: str, optional
179                保存するファイル名を指定します。デフォルトは"co.png"です。
180            add_legend: bool, optional
181                凡例を追加するかどうかを指定します。デフォルトはTrueです。
182            add_xy_labels: bool, optional
183                x軸とy軸のラベルを追加するかどうかを指定します。デフォルトはTrueです。
184            legend_font_size: float, optional
185                凡例のフォントサイズを指定します。デフォルトは16です。
186            save_fig: bool, optional
187                プロットを保存するかどうかを指定します。デフォルトはTrueです。
188            show_fig: bool, optional
189                プロットを表示するかどうかを指定します。デフォルトはTrueです。
190            subplot_label: str | None, optional
191                左上に表示するサブプロットラベルを指定します。デフォルトはNoneです。
192            window_size: int, optional
193                移動平均の窓サイズを指定します。デフォルトは5です。
194            markersize: float, optional
195                プロットのマーカーサイズを指定します。デフォルトは14です。
196            xlim: tuple[float, float], optional
197                x軸の表示範囲を指定します。デフォルトは(0.0001, 10)です。
198            ylim: tuple[float, float], optional
199                y軸の表示範囲を指定します。デフォルトは(0.0001, 10)です。
200            slope_line: tuple[tuple[float, float], tuple[float, float]], optional
201                傾きを示す直線の始点と終点の座標を指定します。デフォルトは((0.01, 10), (10, 0.001))です。
202            slope_text: tuple[str, tuple[float, float]], optional
203                傾きを示すテキストとその位置を指定します。デフォルトは("-4/3", (0.25, 0.4))です。
204            subplot_label_pos: tuple[float, float], optional
205                サブプロットラベルの位置を指定します。デフォルトは(0.00015, 3)です。
206
207        Returns
208        ----------
209            None
210                戻り値はありません。
211
212        Examples
213        -------
214        >>> calculator = TransferFunctionCalculator("data.csv", "frequency")
215        >>> calculator.create_plot_co_spectra(
216        ...     col1="co_spectra1",
217        ...     col2="co_spectra2",
218        ...     label1="データ1",
219        ...     label2="データ2",
220        ...     output_dirpath="output"
221        ... )
222        """
223        df_internal: pd.DataFrame = self._df.copy()
224        # データの取得と移動平均の適用
225        data1 = df_internal[df_internal[col1] > 0].groupby(self._col_freq)[col1].median()
226        data2 = df_internal[df_internal[col2] > 0].groupby(self._col_freq)[col2].median()
227
228        data1 = data1.rolling(window=window_size, center=True, min_periods=1).mean()
229        data2 = data2.rolling(window=window_size, center=True, min_periods=1).mean()
230
231        fig = plt.figure(figsize=figsize, dpi=dpi)
232        ax = fig.add_subplot(111)
233
234        # マーカーサイズを設定して見やすくする
235        ax.plot(
236            data1.index, data1, "o", color=color1, label=label1, markersize=markersize
237        )
238        ax.plot(
239            data2.index, data2, "o", color=color2, label=label2, markersize=markersize
240        )
241
242        # 傾きを示す直線とテキストを追加
243        (x1, y1), (x2, y2) = slope_line
244        ax.plot([x1, x2], [y1, y2], "-", color="black")
245        text, (text_x, text_y) = slope_text
246        ax.text(text_x, text_y, text)
247
248        ax.grid(True, alpha=0.3)
249        ax.set_xscale("log")
250        ax.set_yscale("log")
251        ax.set_xlim(*xlim)
252        ax.set_ylim(*ylim)
253        if add_xy_labels:
254            ax.set_xlabel("f (Hz)")
255            ax.set_ylabel("無次元コスペクトル")
256
257        if add_legend:
258            ax.legend(
259                bbox_to_anchor=(0.05, 1),
260                loc="lower left",
261                fontsize=legend_font_size,
262                ncol=3,
263                frameon=False,
264            )
265        if subplot_label is not None:
266            ax.text(*subplot_label_pos, subplot_label)
267        fig.tight_layout()
268
269        if save_fig and output_dirpath is not None:
270            os.makedirs(output_dirpath, exist_ok=True)
271            # プロットをPNG形式で保存
272            fig.savefig(os.path.join(output_dirpath, output_filename), dpi=dpi)
273        if show_fig:
274            plt.show()
275        plt.close(fig=fig)
276
277    def create_plot_ratio(
278        self,
279        df_processed: pd.DataFrame,
280        reference_name: str,
281        target_name: str,
282        output_dirpath: str | Path | None = None,
283        output_filename: str = "ratio.png",
284        figsize: tuple[float, float] = (10, 6),
285        dpi: float | None = 350,
286        save_fig: bool = True,
287        show_fig: bool = True,
288    ) -> None:
289        """
290        ターゲットと参照の比率をプロットします。
291
292        Parameters
293        ----------
294            df_processed: pd.DataFrame
295                処理されたデータフレーム
296            reference_name: str
297                参照の名前
298            target_name: str
299                ターゲットの名前
300            output_dirpath: str | Path | None, optional
301                プロットを保存するディレクトリパス。デフォルト値はNoneで、save_fig=Trueの場合は必須
302            output_filename: str, optional
303                保存するファイル名。デフォルト値は"ratio.png"
304            figsize: tuple[float, float], optional
305                プロットのサイズ。デフォルト値は(10, 6)
306            dpi: float | None, optional
307                プロットの解像度。デフォルト値は350
308            save_fig: bool, optional
309                プロットを保存するかどうか。デフォルト値はTrue
310            show_fig: bool, optional
311                プロットを表示するかどうか。デフォルト値はTrue
312
313        Returns
314        -------
315            None
316                戻り値はありません
317
318        Examples
319        -------
320        >>> df = pd.DataFrame({"target": [1, 2, 3], "reference": [1, 1, 1]})
321        >>> calculator = TransferFunctionCalculator()
322        >>> calculator.create_plot_ratio(
323        ...     df_processed=df,
324        ...     reference_name="参照データ",
325        ...     target_name="ターゲットデータ",
326        ...     output_dirpath="output",
327        ...     show_fig=False
328        ... )
329        """
330        fig = plt.figure(figsize=figsize, dpi=dpi)
331        ax = fig.add_subplot(111)
332
333        ax.plot(
334            df_processed.index, df_processed["target"] / df_processed["reference"], "o"
335        )
336        ax.set_xscale("log")
337        ax.set_yscale("log")
338        ax.set_xlabel("f (Hz)")
339        ax.set_ylabel(f"{target_name} / {reference_name}")
340        ax.set_title(f"{target_name}{reference_name}の比")
341
342        if save_fig:
343            if output_dirpath is None:
344                raise ValueError(
345                    "save_fig = True のとき、 output_dirpath に有効なディレクトリパスを指定する必要があります。"
346                )
347            fig.savefig(os.path.join(output_dirpath, output_filename), dpi=dpi)
348        if show_fig:
349            plt.show()
350        plt.close(fig=fig)
351
352    @classmethod
353    def create_plot_tf_curves_from_csv(
354        cls,
355        filepath: str,
356        config: TfCurvesFromCsvConfig,
357        csv_encoding: str | None = "utf-8-sig",
358        output_dirpath: str | Path | None = None,
359        output_filename: str = "all_tf_curves.png",
360        col_datetime: str = "Date",
361        figsize: tuple[float, float] = (10, 6),
362        dpi: float | None = 350,
363        add_legend: bool = True,
364        add_xlabel: bool = True,
365        label_x: str = "f (Hz)",
366        label_y: str = "無次元コスペクトル比",
367        label_avg: str = "Avg.",
368        label_co_ref: str = "Tv",
369        legend_font_size: float = 16,
370        line_colors: list[str] | None = None,
371        save_fig: bool = True,
372        show_fig: bool = True,
373    ) -> None:
374        """
375        伝達関数の係数をプロットし、平均値を表示します。
376        各ガスのデータをCSVファイルから読み込み、指定された設定に基づいてプロットを生成します。
377        プロットはオプションで保存することも可能です。
378
379        Parameters
380        ----------
381            filepath: str
382                伝達関数の係数が格納されたCSVファイルのパスを指定します。
383            config: TfCurvesFromCsvConfig
384                プロット設定を指定します。
385            csv_encoding: str | None, optional
386                CSVファイルのエンコーディングを指定します。デフォルト値は"utf-8-sig"です。
387            output_dirpath: str | Path | None, optional
388                出力ディレクトリを指定します。save_fig=Trueの場合は必須です。デフォルト値はNoneです。
389            output_filename: str, optional
390                出力ファイル名を指定します。デフォルト値は"all_tf_curves.png"です。
391            col_datetime: str, optional
392                日付情報が格納されているカラム名を指定します。デフォルト値は"Date"です。
393            figsize: tuple[float, float], optional
394                プロットのサイズを指定します。デフォルト値は(10, 6)です。
395            dpi: float | None, optional
396                プロットの解像度を指定します。デフォルト値は350です。
397            add_legend: bool, optional
398                凡例を追加するかどうかを指定します。デフォルト値はTrueです。
399            add_xlabel: bool, optional
400                x軸ラベルを追加するかどうかを指定します。デフォルト値はTrueです。
401            label_x: str, optional
402                x軸のラベルを指定します。デフォルト値は"f (Hz)"です。
403            label_y: str, optional
404                y軸のラベルを指定します。デフォルト値は"無次元コスペクトル比"です。
405            label_avg: str, optional
406                平均値のラベルを指定します。デフォルト値は"Avg."です。
407            label_co_ref: str, optional
408                参照ガスのラベルを指定します。デフォルト値は"Tv"です。
409            legend_font_size: float, optional
410                凡例のフォントサイズを指定します。デフォルト値は16です。
411            line_colors: list[str] | None, optional
412                各日付のデータに使用する色のリストを指定します。デフォルト値はNoneです。
413            save_fig: bool, optional
414                プロットを保存するかどうかを指定します。デフォルト値はTrueです。
415            show_fig: bool, optional
416                プロットを表示するかどうかを指定します。デフォルト値はTrueです。
417
418        Returns
419        -------
420            None
421                戻り値はありません。
422
423        Examples
424        -------
425        >>> config = TfCurvesFromCsvConfig(col_coef_a="a", base_color="red")
426        >>> calculator = TransferFunctionCalculator()
427        >>> calculator.create_plot_tf_curves_from_csv(
428        ...     filepath="transfer_functions.csv",
429        ...     config=config,
430        ...     output_dirpath="output",
431        ...     show_fig=False
432        ... )
433        """
434        # CSVファイルを読み込む
435        df = pd.read_csv(filepath, encoding=csv_encoding)
436
437        fig = plt.figure(figsize=figsize, dpi=dpi)
438
439        # データ数に応じたデフォルトの色リストを作成
440        if line_colors is None:
441            default_colors = [
442                "#1f77b4",
443                "#ff7f0e",
444                "#2ca02c",
445                "#d62728",
446                "#9467bd",
447                "#8c564b",
448                "#e377c2",
449                "#7f7f7f",
450                "#bcbd22",
451                "#17becf",
452            ]
453            n_dates = len(df)
454            plot_colors = (default_colors * (n_dates // len(default_colors) + 1))[
455                :n_dates
456            ]
457        else:
458            plot_colors = line_colors
459
460        # 全てのa値を用いて伝達関数をプロット
461        for i, row in enumerate(df.iterrows()):
462            a = row[1][config.col_coef_a]
463            date = row[1][col_datetime]
464            x_fit = np.logspace(-3, 1, 1000)
465            y_fit = cls.transfer_function(x_fit, a)
466            plt.plot(
467                x_fit,
468                y_fit,
469                "-",
470                color=plot_colors[i],
471                alpha=0.7,
472                label=f"{date} (a = {a:.3f})",
473            )
474
475        # 平均のa値を用いた伝達関数をプロット
476        a_mean = df[config.col_coef_a].mean()
477        x_fit = np.logspace(-3, 1, 1000)
478        y_fit = cls.transfer_function(x_fit, a_mean)
479        plt.plot(
480            x_fit,
481            y_fit,
482            "-",
483            color=config.base_color,
484            linewidth=3,
485            label=f"{label_avg} (a = {a_mean:.3f})",
486        )
487
488        # グラフの設定
489        label_y_formatted: str = f"{label_y}\n({config.label_gas} / {label_co_ref})"
490        plt.xscale("log")
491        if add_xlabel:
492            plt.xlabel(label_x)
493        plt.ylabel(label_y_formatted)
494        if add_legend:
495            plt.legend(loc="lower left", fontsize=legend_font_size)
496        plt.grid(True, which="both", ls="-", alpha=0.2)
497        plt.tight_layout()
498
499        if save_fig:
500            if output_dirpath is None:
501                raise ValueError(
502                    "save_fig = True のとき、 output_dirpath に有効なディレクトリパスを指定する必要があります。"
503                )
504            os.makedirs(output_dirpath, exist_ok=True)
505            # 出力ファイル名が指定されていない場合、gas_nameを使用
506            output_filepath: str = os.path.join(output_dirpath, output_filename)
507            plt.savefig(output_filepath, dpi=dpi, bbox_inches="tight")
508        if show_fig:
509            plt.show()
510        plt.close(fig=fig)
511
512    def create_plot_transfer_function(
513        self,
514        a: float,
515        df_processed: pd.DataFrame,
516        reference_name: str,
517        target_name: str,
518        figsize: tuple[float, float] = (10, 6),
519        dpi: float | None = 350,
520        output_dirpath: str | Path | None = None,
521        output_filename: str = "tf.png",
522        save_fig: bool = True,
523        show_fig: bool = True,
524        add_xlabel: bool = True,
525        label_x: str = "f (Hz)",
526        label_y: str = "コスペクトル比",
527        label_target: str | None = None,
528        label_ref: str = "Tv",
529    ) -> None:
530        """
531        伝達関数とそのフィットをプロットします。
532
533        Parameters
534        ----------
535            a: float
536                伝達関数の係数です。
537            df_processed: pd.DataFrame
538                処理されたデータフレームです。
539            reference_name: str
540                参照データの名前です。
541            target_name: str
542                ターゲットデータの名前です。
543            figsize: tuple[float, float], optional
544                プロットのサイズを指定します。デフォルトは(10, 6)です。
545            dpi: float | None, optional
546                プロットの解像度を指定します。デフォルトは350です。
547            output_dirpath: str | Path | None, optional
548                プロットを保存するディレクトリを指定します。save_fig=Trueの場合は必須です。
549            output_filename: str, optional
550                保存するファイル名を指定します。デフォルトは"tf.png"です。
551            save_fig: bool, optional
552                プロットを保存するかどうかを指定します。デフォルトはTrueです。
553            show_fig: bool, optional
554                プロットを表示するかどうかを指定します。デフォルトはTrueです。
555            add_xlabel: bool, optional
556                x軸のラベルを追加するかどうかを指定します。デフォルトはTrueです。
557            label_x: str, optional
558                x軸のラベル名を指定します。デフォルトは"f (Hz)"です。
559            label_y: str, optional
560                y軸のラベル名を指定します。デフォルトは"コスペクトル比"です。
561            label_target: str | None, optional
562                比較先のラベル名を指定します。デフォルトはNoneです。
563            label_ref: str, optional
564                比較元のラベル名を指定します。デフォルトは"Tv"です。
565
566        Returns
567        -------
568            None
569                戻り値はありません。
570
571        Examples
572        -------
573        >>> # 伝達関数のプロットを作成し、保存する
574        >>> calculator = TransferFunctionCalculator()
575        >>> calculator.create_plot_transfer_function(
576        ...     a=0.5,
577        ...     df_processed=processed_df,
578        ...     reference_name="温度",
579        ...     target_name="CO2",
580        ...     output_dirpath="./output",
581        ...     output_filename="transfer_function.png"
582        ... )
583        """
584        df_cutoff: pd.DataFrame = self._cutoff_df(df_processed)
585
586        fig = plt.figure(figsize=figsize, dpi=dpi)
587        ax = fig.add_subplot(111)
588
589        ax.plot(
590            df_cutoff.index,
591            df_cutoff["target"] / df_cutoff["reference"],
592            "o",
593            label=f"{target_name} / {reference_name}",
594        )
595
596        x_fit = np.logspace(
597            np.log10(self._cutoff_freq_low), np.log10(self._cutoff_freq_high), 1000
598        )
599        y_fit = self.transfer_function(x_fit, a)
600        ax.plot(x_fit, y_fit, "-", label=f"フィット (a = {a:.4f})")
601
602        ax.set_xscale("log")
603        # グラフの設定
604        label_y_formatted: str = f"{label_y}\n({label_target} / {label_ref})"
605        plt.xscale("log")
606        if add_xlabel:
607            plt.xlabel(label_x)
608        plt.ylabel(label_y_formatted)
609        ax.legend()
610
611        if save_fig:
612            if output_dirpath is None:
613                raise ValueError(
614                    "save_fig = True のとき、 output_dirpath に有効なディレクトリパスを指定する必要があります。"
615                )
616            os.makedirs(output_dirpath, exist_ok=True)
617            # プロットをPNG形式で保存
618            fig.savefig(os.path.join(output_dirpath, output_filename), dpi=dpi)
619        if show_fig:
620            plt.show()
621        plt.close(fig=fig)
622
623    def process_data(self, col_reference: str, col_target: str) -> pd.DataFrame:
624        """
625        指定されたキーに基づいてデータを処理します。
626        周波数ごとにデータをグループ化し、中央値を計算します。
627        また、異常な比率のデータを除去します。
628
629        Parameters
630        ----------
631            col_reference: str
632                参照データのカラム名を指定します。
633            col_target: str
634                ターゲットデータのカラム名を指定します。
635
636        Returns
637        ----------
638            pd.DataFrame
639                処理されたデータフレーム。
640                indexは周波数、columnsは'reference'と'target'です。
641
642        Examples
643        -------
644        >>> calculator = TransferFunctionCalculator()
645        >>> processed_df = calculator.process_data(
646        ...     col_reference="温度",
647        ...     col_target="CO2"
648        ... )
649        """
650        df_internal: pd.DataFrame = self._df.copy()
651        col_freq: str = self._col_freq
652
653        # データ型の確認と変換
654        df_internal[col_freq] = pd.to_numeric(df_internal[col_freq], errors="coerce")
655        df_internal[col_reference] = pd.to_numeric(
656            df_internal[col_reference], errors="coerce"
657        )
658        df_internal[col_target] = pd.to_numeric(df_internal[col_target], errors="coerce")
659
660        # NaNを含む行を削除
661        df_internal = df_internal.dropna(subset=[col_freq, col_reference, col_target])
662
663        # グループ化と中央値の計算
664        grouped = df_internal.groupby(col_freq)
665        reference_data = grouped[col_reference].median()
666        target_data = grouped[col_target].median()
667
668        df_processed = pd.DataFrame(
669            {"reference": reference_data, "target": target_data}
670        )
671
672        # 異常な比率を除去
673        df_processed.loc[
674            (
675                (df_processed["target"] / df_processed["reference"] > 1)
676                | (df_processed["target"] / df_processed["reference"] < 0)
677            )
678        ] = np.nan
679        df_processed = df_processed.dropna()
680
681        return df_processed
682
683    def _cutoff_df(self, df: pd.DataFrame) -> pd.DataFrame:
684        """
685        カットオフ周波数に基づいてDataFrameを加工するメソッド
686
687        Parameters
688        ----------
689            df: pd.DataFrame
690                加工対象のデータフレーム。
691
692        Returns
693        ----------
694            pd.DataFrame
695                カットオフ周波数に基づいて加工されたデータフレーム。
696        """
697        df_cutoff: pd.DataFrame = df.loc[
698            (self._cutoff_freq_low <= df.index) & (df.index <= self._cutoff_freq_high)
699        ]
700        return df_cutoff
701
702    @classmethod
703    def transfer_function(cls, x: np.ndarray, a: float) -> np.ndarray:
704        """
705        伝達関数を計算する。
706
707        Parameters
708        ----------
709            x: np.ndarray
710                周波数の配列。
711            a: float
712                伝達関数の係数。
713
714        Returns
715        ----------
716            np.ndarray
717                伝達関数の値。
718        """
719        return np.exp(-np.log(np.sqrt(2)) * np.power(x / a, 2))
720
721    @staticmethod
722    def _load_data(filepath: str | Path) -> pd.DataFrame:
723        """
724        CSVファイルからデータを読み込む。
725
726        Parameters
727        ----------
728            filepath: str | Path
729                csvファイルのパス。
730
731        Returns
732        ----------
733            pd.DataFrame
734                読み込まれたデータフレーム。
735        """
736        tmp = pd.read_csv(filepath, header=None, nrows=1, skiprows=0)
737        header = tmp.loc[tmp.index[0]]
738        df = pd.read_csv(filepath, header=None, skiprows=1)
739        df.columns = header
740        return df

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

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

TransferFunctionCalculator( filepath: str | pathlib.Path, col_freq: str, cutoff_freq_low: float = 0.01, cutoff_freq_high: float = 1)
45    def __init__(
46        self,
47        filepath: str | Path,
48        col_freq: str,
49        cutoff_freq_low: float = 0.01,
50        cutoff_freq_high: float = 1,
51    ):
52        """
53        伝達関数計算のためのクラスを初期化します。
54
55        Parameters
56        ----------
57            filepath: str | Path
58                分析対象のCSVファイルのパス
59            col_freq: str
60                周波数データが格納されている列名
61            cutoff_freq_low: float, optional
62                カットオフ周波数の最低値。デフォルトは0.01Hz
63            cutoff_freq_high: float, optional
64                カットオフ周波数の最高値。デフォルトは1Hz
65
66        Examples
67        -------
68        >>> calculator = TransferFunctionCalculator(
69        ...     filepath="data.csv",
70        ...     col_freq="frequency",
71        ...     cutoff_freq_low=0.02,
72        ...     cutoff_freq_high=0.5
73        ... )
74        """
75        self._col_freq: str = col_freq
76        self._cutoff_freq_low: float = cutoff_freq_low
77        self._cutoff_freq_high: float = cutoff_freq_high
78        self._df: pd.DataFrame = TransferFunctionCalculator._load_data(filepath)

伝達関数計算のためのクラスを初期化します。

Parameters

filepath: str | Path
    分析対象のCSVファイルのパス
col_freq: str
    周波数データが格納されている列名
cutoff_freq_low: float, optional
    カットオフ周波数の最低値。デフォルトは0.01Hz
cutoff_freq_high: float, optional
    カットオフ周波数の最高値。デフォルトは1Hz

Examples

>>> calculator = TransferFunctionCalculator(
...     filepath="data.csv",
...     col_freq="frequency",
...     cutoff_freq_low=0.02,
...     cutoff_freq_high=0.5
... )
def calculate_transfer_function( self, col_reference: str, col_target: str) -> tuple[float, float, pandas.core.frame.DataFrame]:
 80    def calculate_transfer_function(
 81        self, col_reference: str, col_target: str
 82    ) -> tuple[float, float, pd.DataFrame]:
 83        """
 84        伝達関数の係数を計算します。
 85
 86        Parameters
 87        ----------
 88            col_reference: str
 89                参照データが格納されている列名を指定します。
 90            col_target: str
 91                ターゲットデータが格納されている列名を指定します。
 92
 93        Returns
 94        ----------
 95            tuple[float, float, pandas.DataFrame]
 96                伝達関数の係数a、係数aの標準誤差、および計算に使用したDataFrameを返します。
 97
 98        Examples
 99        -------
100        >>> calculator = TransferFunctionCalculator("data.csv", "frequency")
101        >>> a, a_err, df = calculator.calculate_transfer_function(
102        ...     col_reference="ref_data",
103        ...     col_target="target_data"
104        ... )
105        >>> print(f"伝達関数の係数: {a:.3f} ± {a_err:.3f}")
106        """
107        df_processed: pd.DataFrame = self.process_data(
108            col_reference=col_reference, col_target=col_target
109        )
110        df_cutoff: pd.DataFrame = self._cutoff_df(df_processed)
111
112        array_x = np.array(df_cutoff.index)
113        array_y = np.array(df_cutoff["target"] / df_cutoff["reference"])
114
115        # フィッティングパラメータと共分散行列を取得
116        popt, pcov = curve_fit(
117            TransferFunctionCalculator.transfer_function, array_x, array_y
118        )
119
120        # 標準誤差を計算(共分散行列の対角成分の平方根)
121        perr = np.sqrt(np.diag(pcov))
122
123        # 係数aとその標準誤差、および計算に用いたDataFrameを返す
124        return popt[0], perr[0], df_processed

伝達関数の係数を計算します。

Parameters

col_reference: str
    参照データが格納されている列名を指定します。
col_target: str
    ターゲットデータが格納されている列名を指定します。

Returns

tuple[float, float, pandas.DataFrame]
    伝達関数の係数a、係数aの標準誤差、および計算に使用したDataFrameを返します。

Examples

>>> calculator = TransferFunctionCalculator("data.csv", "frequency")
>>> a, a_err, df = calculator.calculate_transfer_function(
...     col_reference="ref_data",
...     col_target="target_data"
... )
>>> print(f"伝達関数の係数: {a:.3f} ± {a_err:.3f}")
def create_plot_co_spectra( self, col1: str, col2: str, color1: str = 'gray', color2: str = 'red', figsize: tuple[float, float] = (10, 6), dpi: float | None = 350, label1: str | None = None, label2: str | None = None, output_dirpath: str | pathlib.Path | None = None, output_filename: str = 'co.png', add_legend: bool = True, add_xy_labels: bool = True, legend_font_size: float = 16, save_fig: bool = True, show_fig: bool = True, subplot_label: str | None = None, window_size: int = 5, markersize: float = 14, xlim: tuple[float, float] = (0.0001, 10), ylim: tuple[float, float] = (0.0001, 10), slope_line: tuple[tuple[float, float], tuple[float, float]] = ((0.01, 10), (10, 0.001)), slope_text: tuple[str, tuple[float, float]] = ('-4/3', (0.25, 0.4)), subplot_label_pos: tuple[float, float] = (0.00015, 3)) -> None:
126    def create_plot_co_spectra(
127        self,
128        col1: str,
129        col2: str,
130        color1: str = "gray",
131        color2: str = "red",
132        figsize: tuple[float, float] = (10, 6),
133        dpi: float | None = 350,
134        label1: str | None = None,
135        label2: str | None = None,
136        output_dirpath: str | Path | None = None,
137        output_filename: str = "co.png",
138        add_legend: bool = True,
139        add_xy_labels: bool = True,
140        legend_font_size: float = 16,
141        save_fig: bool = True,
142        show_fig: bool = True,
143        subplot_label: str | None = None,
144        window_size: int = 5,
145        markersize: float = 14,
146        xlim: tuple[float, float] = (0.0001, 10),
147        ylim: tuple[float, float] = (0.0001, 10),
148        slope_line: tuple[tuple[float, float], tuple[float, float]] = (
149            (0.01, 10),
150            (10, 0.001),
151        ),
152        slope_text: tuple[str, tuple[float, float]] = ("-4/3", (0.25, 0.4)),
153        subplot_label_pos: tuple[float, float] = (0.00015, 3),
154    ) -> None:
155        """
156        2種類のコスペクトルをプロットします。
157
158        Parameters
159        ----------
160            col1: str
161                1つ目のコスペクトルデータのカラム名を指定します。
162            col2: str
163                2つ目のコスペクトルデータのカラム名を指定します。
164            color1: str, optional
165                1つ目のデータの色を指定します。デフォルトは'gray'です。
166            color2: str, optional
167                2つ目のデータの色を指定します。デフォルトは'red'です。
168            figsize: tuple[float, float], optional
169                プロットのサイズを指定します。デフォルトは(10, 6)です。
170            dpi: float | None, optional
171                プロットの解像度を指定します。デフォルトは350です。
172            label1: str | None, optional
173                1つ目のデータの凡例ラベルを指定します。デフォルトはNoneです。
174            label2: str | None, optional
175                2つ目のデータの凡例ラベルを指定します。デフォルトはNoneです。
176            output_dirpath: str | Path | None, optional
177                プロットを保存するディレクトリを指定します。save_fig=Trueの場合は必須です。デフォルトはNoneです。
178            output_filename: str, optional
179                保存するファイル名を指定します。デフォルトは"co.png"です。
180            add_legend: bool, optional
181                凡例を追加するかどうかを指定します。デフォルトはTrueです。
182            add_xy_labels: bool, optional
183                x軸とy軸のラベルを追加するかどうかを指定します。デフォルトはTrueです。
184            legend_font_size: float, optional
185                凡例のフォントサイズを指定します。デフォルトは16です。
186            save_fig: bool, optional
187                プロットを保存するかどうかを指定します。デフォルトはTrueです。
188            show_fig: bool, optional
189                プロットを表示するかどうかを指定します。デフォルトはTrueです。
190            subplot_label: str | None, optional
191                左上に表示するサブプロットラベルを指定します。デフォルトはNoneです。
192            window_size: int, optional
193                移動平均の窓サイズを指定します。デフォルトは5です。
194            markersize: float, optional
195                プロットのマーカーサイズを指定します。デフォルトは14です。
196            xlim: tuple[float, float], optional
197                x軸の表示範囲を指定します。デフォルトは(0.0001, 10)です。
198            ylim: tuple[float, float], optional
199                y軸の表示範囲を指定します。デフォルトは(0.0001, 10)です。
200            slope_line: tuple[tuple[float, float], tuple[float, float]], optional
201                傾きを示す直線の始点と終点の座標を指定します。デフォルトは((0.01, 10), (10, 0.001))です。
202            slope_text: tuple[str, tuple[float, float]], optional
203                傾きを示すテキストとその位置を指定します。デフォルトは("-4/3", (0.25, 0.4))です。
204            subplot_label_pos: tuple[float, float], optional
205                サブプロットラベルの位置を指定します。デフォルトは(0.00015, 3)です。
206
207        Returns
208        ----------
209            None
210                戻り値はありません。
211
212        Examples
213        -------
214        >>> calculator = TransferFunctionCalculator("data.csv", "frequency")
215        >>> calculator.create_plot_co_spectra(
216        ...     col1="co_spectra1",
217        ...     col2="co_spectra2",
218        ...     label1="データ1",
219        ...     label2="データ2",
220        ...     output_dirpath="output"
221        ... )
222        """
223        df_internal: pd.DataFrame = self._df.copy()
224        # データの取得と移動平均の適用
225        data1 = df_internal[df_internal[col1] > 0].groupby(self._col_freq)[col1].median()
226        data2 = df_internal[df_internal[col2] > 0].groupby(self._col_freq)[col2].median()
227
228        data1 = data1.rolling(window=window_size, center=True, min_periods=1).mean()
229        data2 = data2.rolling(window=window_size, center=True, min_periods=1).mean()
230
231        fig = plt.figure(figsize=figsize, dpi=dpi)
232        ax = fig.add_subplot(111)
233
234        # マーカーサイズを設定して見やすくする
235        ax.plot(
236            data1.index, data1, "o", color=color1, label=label1, markersize=markersize
237        )
238        ax.plot(
239            data2.index, data2, "o", color=color2, label=label2, markersize=markersize
240        )
241
242        # 傾きを示す直線とテキストを追加
243        (x1, y1), (x2, y2) = slope_line
244        ax.plot([x1, x2], [y1, y2], "-", color="black")
245        text, (text_x, text_y) = slope_text
246        ax.text(text_x, text_y, text)
247
248        ax.grid(True, alpha=0.3)
249        ax.set_xscale("log")
250        ax.set_yscale("log")
251        ax.set_xlim(*xlim)
252        ax.set_ylim(*ylim)
253        if add_xy_labels:
254            ax.set_xlabel("f (Hz)")
255            ax.set_ylabel("無次元コスペクトル")
256
257        if add_legend:
258            ax.legend(
259                bbox_to_anchor=(0.05, 1),
260                loc="lower left",
261                fontsize=legend_font_size,
262                ncol=3,
263                frameon=False,
264            )
265        if subplot_label is not None:
266            ax.text(*subplot_label_pos, subplot_label)
267        fig.tight_layout()
268
269        if save_fig and output_dirpath is not None:
270            os.makedirs(output_dirpath, exist_ok=True)
271            # プロットをPNG形式で保存
272            fig.savefig(os.path.join(output_dirpath, output_filename), dpi=dpi)
273        if show_fig:
274            plt.show()
275        plt.close(fig=fig)

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

Parameters

col1: str
    1つ目のコスペクトルデータのカラム名を指定します。
col2: str
    2つ目のコスペクトルデータのカラム名を指定します。
color1: str, optional
    1つ目のデータの色を指定します。デフォルトは'gray'です。
color2: str, optional
    2つ目のデータの色を指定します。デフォルトは'red'です。
figsize: tuple[float, float], optional
    プロットのサイズを指定します。デフォルトは(10, 6)です。
dpi: float | None, optional
    プロットの解像度を指定します。デフォルトは350です。
label1: str | None, optional
    1つ目のデータの凡例ラベルを指定します。デフォルトはNoneです。
label2: str | None, optional
    2つ目のデータの凡例ラベルを指定します。デフォルトはNoneです。
output_dirpath: str | Path | None, optional
    プロットを保存するディレクトリを指定します。save_fig=Trueの場合は必須です。デフォルトはNoneです。
output_filename: str, optional
    保存するファイル名を指定します。デフォルトは"co.png"です。
add_legend: bool, optional
    凡例を追加するかどうかを指定します。デフォルトはTrueです。
add_xy_labels: bool, optional
    x軸とy軸のラベルを追加するかどうかを指定します。デフォルトはTrueです。
legend_font_size: float, optional
    凡例のフォントサイズを指定します。デフォルトは16です。
save_fig: bool, optional
    プロットを保存するかどうかを指定します。デフォルトはTrueです。
show_fig: bool, optional
    プロットを表示するかどうかを指定します。デフォルトはTrueです。
subplot_label: str | None, optional
    左上に表示するサブプロットラベルを指定します。デフォルトはNoneです。
window_size: int, optional
    移動平均の窓サイズを指定します。デフォルトは5です。
markersize: float, optional
    プロットのマーカーサイズを指定します。デフォルトは14です。
xlim: tuple[float, float], optional
    x軸の表示範囲を指定します。デフォルトは(0.0001, 10)です。
ylim: tuple[float, float], optional
    y軸の表示範囲を指定します。デフォルトは(0.0001, 10)です。
slope_line: tuple[tuple[float, float], tuple[float, float]], optional
    傾きを示す直線の始点と終点の座標を指定します。デフォルトは((0.01, 10), (10, 0.001))です。
slope_text: tuple[str, tuple[float, float]], optional
    傾きを示すテキストとその位置を指定します。デフォルトは("-4/3", (0.25, 0.4))です。
subplot_label_pos: tuple[float, float], optional
    サブプロットラベルの位置を指定します。デフォルトは(0.00015, 3)です。

Returns

None
    戻り値はありません。

Examples

>>> calculator = TransferFunctionCalculator("data.csv", "frequency")
>>> calculator.create_plot_co_spectra(
...     col1="co_spectra1",
...     col2="co_spectra2",
...     label1="データ1",
...     label2="データ2",
...     output_dirpath="output"
... )
def create_plot_ratio( self, df_processed: pandas.core.frame.DataFrame, reference_name: str, target_name: str, output_dirpath: str | pathlib.Path | None = None, output_filename: str = 'ratio.png', figsize: tuple[float, float] = (10, 6), dpi: float | None = 350, save_fig: bool = True, show_fig: bool = True) -> None:
277    def create_plot_ratio(
278        self,
279        df_processed: pd.DataFrame,
280        reference_name: str,
281        target_name: str,
282        output_dirpath: str | Path | None = None,
283        output_filename: str = "ratio.png",
284        figsize: tuple[float, float] = (10, 6),
285        dpi: float | None = 350,
286        save_fig: bool = True,
287        show_fig: bool = True,
288    ) -> None:
289        """
290        ターゲットと参照の比率をプロットします。
291
292        Parameters
293        ----------
294            df_processed: pd.DataFrame
295                処理されたデータフレーム
296            reference_name: str
297                参照の名前
298            target_name: str
299                ターゲットの名前
300            output_dirpath: str | Path | None, optional
301                プロットを保存するディレクトリパス。デフォルト値はNoneで、save_fig=Trueの場合は必須
302            output_filename: str, optional
303                保存するファイル名。デフォルト値は"ratio.png"
304            figsize: tuple[float, float], optional
305                プロットのサイズ。デフォルト値は(10, 6)
306            dpi: float | None, optional
307                プロットの解像度。デフォルト値は350
308            save_fig: bool, optional
309                プロットを保存するかどうか。デフォルト値はTrue
310            show_fig: bool, optional
311                プロットを表示するかどうか。デフォルト値はTrue
312
313        Returns
314        -------
315            None
316                戻り値はありません
317
318        Examples
319        -------
320        >>> df = pd.DataFrame({"target": [1, 2, 3], "reference": [1, 1, 1]})
321        >>> calculator = TransferFunctionCalculator()
322        >>> calculator.create_plot_ratio(
323        ...     df_processed=df,
324        ...     reference_name="参照データ",
325        ...     target_name="ターゲットデータ",
326        ...     output_dirpath="output",
327        ...     show_fig=False
328        ... )
329        """
330        fig = plt.figure(figsize=figsize, dpi=dpi)
331        ax = fig.add_subplot(111)
332
333        ax.plot(
334            df_processed.index, df_processed["target"] / df_processed["reference"], "o"
335        )
336        ax.set_xscale("log")
337        ax.set_yscale("log")
338        ax.set_xlabel("f (Hz)")
339        ax.set_ylabel(f"{target_name} / {reference_name}")
340        ax.set_title(f"{target_name}{reference_name}の比")
341
342        if save_fig:
343            if output_dirpath is None:
344                raise ValueError(
345                    "save_fig = True のとき、 output_dirpath に有効なディレクトリパスを指定する必要があります。"
346                )
347            fig.savefig(os.path.join(output_dirpath, output_filename), dpi=dpi)
348        if show_fig:
349            plt.show()
350        plt.close(fig=fig)

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

Parameters

df_processed: pd.DataFrame
    処理されたデータフレーム
reference_name: str
    参照の名前
target_name: str
    ターゲットの名前
output_dirpath: str | Path | None, optional
    プロットを保存するディレクトリパス。デフォルト値はNoneで、save_fig=Trueの場合は必須
output_filename: str, optional
    保存するファイル名。デフォルト値は"ratio.png"
figsize: tuple[float, float], optional
    プロットのサイズ。デフォルト値は(10, 6)
dpi: float | None, optional
    プロットの解像度。デフォルト値は350
save_fig: bool, optional
    プロットを保存するかどうか。デフォルト値はTrue
show_fig: bool, optional
    プロットを表示するかどうか。デフォルト値はTrue

Returns

None
    戻り値はありません

Examples

>>> df = pd.DataFrame({"target": [1, 2, 3], "reference": [1, 1, 1]})
>>> calculator = TransferFunctionCalculator()
>>> calculator.create_plot_ratio(
...     df_processed=df,
...     reference_name="参照データ",
...     target_name="ターゲットデータ",
...     output_dirpath="output",
...     show_fig=False
... )
@classmethod
def create_plot_tf_curves_from_csv( cls, filepath: str, config: TfCurvesFromCsvConfig, csv_encoding: str | None = 'utf-8-sig', output_dirpath: str | pathlib.Path | None = None, output_filename: str = 'all_tf_curves.png', col_datetime: str = 'Date', figsize: tuple[float, float] = (10, 6), dpi: float | None = 350, add_legend: bool = True, add_xlabel: bool = True, label_x: str = 'f (Hz)', label_y: str = '無次元コスペクトル比', label_avg: str = 'Avg.', label_co_ref: str = 'Tv', legend_font_size: float = 16, line_colors: list[str] | None = None, save_fig: bool = True, show_fig: bool = True) -> None:
352    @classmethod
353    def create_plot_tf_curves_from_csv(
354        cls,
355        filepath: str,
356        config: TfCurvesFromCsvConfig,
357        csv_encoding: str | None = "utf-8-sig",
358        output_dirpath: str | Path | None = None,
359        output_filename: str = "all_tf_curves.png",
360        col_datetime: str = "Date",
361        figsize: tuple[float, float] = (10, 6),
362        dpi: float | None = 350,
363        add_legend: bool = True,
364        add_xlabel: bool = True,
365        label_x: str = "f (Hz)",
366        label_y: str = "無次元コスペクトル比",
367        label_avg: str = "Avg.",
368        label_co_ref: str = "Tv",
369        legend_font_size: float = 16,
370        line_colors: list[str] | None = None,
371        save_fig: bool = True,
372        show_fig: bool = True,
373    ) -> None:
374        """
375        伝達関数の係数をプロットし、平均値を表示します。
376        各ガスのデータをCSVファイルから読み込み、指定された設定に基づいてプロットを生成します。
377        プロットはオプションで保存することも可能です。
378
379        Parameters
380        ----------
381            filepath: str
382                伝達関数の係数が格納されたCSVファイルのパスを指定します。
383            config: TfCurvesFromCsvConfig
384                プロット設定を指定します。
385            csv_encoding: str | None, optional
386                CSVファイルのエンコーディングを指定します。デフォルト値は"utf-8-sig"です。
387            output_dirpath: str | Path | None, optional
388                出力ディレクトリを指定します。save_fig=Trueの場合は必須です。デフォルト値はNoneです。
389            output_filename: str, optional
390                出力ファイル名を指定します。デフォルト値は"all_tf_curves.png"です。
391            col_datetime: str, optional
392                日付情報が格納されているカラム名を指定します。デフォルト値は"Date"です。
393            figsize: tuple[float, float], optional
394                プロットのサイズを指定します。デフォルト値は(10, 6)です。
395            dpi: float | None, optional
396                プロットの解像度を指定します。デフォルト値は350です。
397            add_legend: bool, optional
398                凡例を追加するかどうかを指定します。デフォルト値はTrueです。
399            add_xlabel: bool, optional
400                x軸ラベルを追加するかどうかを指定します。デフォルト値はTrueです。
401            label_x: str, optional
402                x軸のラベルを指定します。デフォルト値は"f (Hz)"です。
403            label_y: str, optional
404                y軸のラベルを指定します。デフォルト値は"無次元コスペクトル比"です。
405            label_avg: str, optional
406                平均値のラベルを指定します。デフォルト値は"Avg."です。
407            label_co_ref: str, optional
408                参照ガスのラベルを指定します。デフォルト値は"Tv"です。
409            legend_font_size: float, optional
410                凡例のフォントサイズを指定します。デフォルト値は16です。
411            line_colors: list[str] | None, optional
412                各日付のデータに使用する色のリストを指定します。デフォルト値はNoneです。
413            save_fig: bool, optional
414                プロットを保存するかどうかを指定します。デフォルト値はTrueです。
415            show_fig: bool, optional
416                プロットを表示するかどうかを指定します。デフォルト値はTrueです。
417
418        Returns
419        -------
420            None
421                戻り値はありません。
422
423        Examples
424        -------
425        >>> config = TfCurvesFromCsvConfig(col_coef_a="a", base_color="red")
426        >>> calculator = TransferFunctionCalculator()
427        >>> calculator.create_plot_tf_curves_from_csv(
428        ...     filepath="transfer_functions.csv",
429        ...     config=config,
430        ...     output_dirpath="output",
431        ...     show_fig=False
432        ... )
433        """
434        # CSVファイルを読み込む
435        df = pd.read_csv(filepath, encoding=csv_encoding)
436
437        fig = plt.figure(figsize=figsize, dpi=dpi)
438
439        # データ数に応じたデフォルトの色リストを作成
440        if line_colors is None:
441            default_colors = [
442                "#1f77b4",
443                "#ff7f0e",
444                "#2ca02c",
445                "#d62728",
446                "#9467bd",
447                "#8c564b",
448                "#e377c2",
449                "#7f7f7f",
450                "#bcbd22",
451                "#17becf",
452            ]
453            n_dates = len(df)
454            plot_colors = (default_colors * (n_dates // len(default_colors) + 1))[
455                :n_dates
456            ]
457        else:
458            plot_colors = line_colors
459
460        # 全てのa値を用いて伝達関数をプロット
461        for i, row in enumerate(df.iterrows()):
462            a = row[1][config.col_coef_a]
463            date = row[1][col_datetime]
464            x_fit = np.logspace(-3, 1, 1000)
465            y_fit = cls.transfer_function(x_fit, a)
466            plt.plot(
467                x_fit,
468                y_fit,
469                "-",
470                color=plot_colors[i],
471                alpha=0.7,
472                label=f"{date} (a = {a:.3f})",
473            )
474
475        # 平均のa値を用いた伝達関数をプロット
476        a_mean = df[config.col_coef_a].mean()
477        x_fit = np.logspace(-3, 1, 1000)
478        y_fit = cls.transfer_function(x_fit, a_mean)
479        plt.plot(
480            x_fit,
481            y_fit,
482            "-",
483            color=config.base_color,
484            linewidth=3,
485            label=f"{label_avg} (a = {a_mean:.3f})",
486        )
487
488        # グラフの設定
489        label_y_formatted: str = f"{label_y}\n({config.label_gas} / {label_co_ref})"
490        plt.xscale("log")
491        if add_xlabel:
492            plt.xlabel(label_x)
493        plt.ylabel(label_y_formatted)
494        if add_legend:
495            plt.legend(loc="lower left", fontsize=legend_font_size)
496        plt.grid(True, which="both", ls="-", alpha=0.2)
497        plt.tight_layout()
498
499        if save_fig:
500            if output_dirpath is None:
501                raise ValueError(
502                    "save_fig = True のとき、 output_dirpath に有効なディレクトリパスを指定する必要があります。"
503                )
504            os.makedirs(output_dirpath, exist_ok=True)
505            # 出力ファイル名が指定されていない場合、gas_nameを使用
506            output_filepath: str = os.path.join(output_dirpath, output_filename)
507            plt.savefig(output_filepath, dpi=dpi, bbox_inches="tight")
508        if show_fig:
509            plt.show()
510        plt.close(fig=fig)

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

Parameters

filepath: str
    伝達関数の係数が格納されたCSVファイルのパスを指定します。
config: TfCurvesFromCsvConfig
    プロット設定を指定します。
csv_encoding: str | None, optional
    CSVファイルのエンコーディングを指定します。デフォルト値は"utf-8-sig"です。
output_dirpath: str | Path | None, optional
    出力ディレクトリを指定します。save_fig=Trueの場合は必須です。デフォルト値はNoneです。
output_filename: str, optional
    出力ファイル名を指定します。デフォルト値は"all_tf_curves.png"です。
col_datetime: str, optional
    日付情報が格納されているカラム名を指定します。デフォルト値は"Date"です。
figsize: tuple[float, float], optional
    プロットのサイズを指定します。デフォルト値は(10, 6)です。
dpi: float | None, optional
    プロットの解像度を指定します。デフォルト値は350です。
add_legend: bool, optional
    凡例を追加するかどうかを指定します。デフォルト値はTrueです。
add_xlabel: bool, optional
    x軸ラベルを追加するかどうかを指定します。デフォルト値はTrueです。
label_x: str, optional
    x軸のラベルを指定します。デフォルト値は"f (Hz)"です。
label_y: str, optional
    y軸のラベルを指定します。デフォルト値は"無次元コスペクトル比"です。
label_avg: str, optional
    平均値のラベルを指定します。デフォルト値は"Avg."です。
label_co_ref: str, optional
    参照ガスのラベルを指定します。デフォルト値は"Tv"です。
legend_font_size: float, optional
    凡例のフォントサイズを指定します。デフォルト値は16です。
line_colors: list[str] | None, optional
    各日付のデータに使用する色のリストを指定します。デフォルト値はNoneです。
save_fig: bool, optional
    プロットを保存するかどうかを指定します。デフォルト値はTrueです。
show_fig: bool, optional
    プロットを表示するかどうかを指定します。デフォルト値はTrueです。

Returns

None
    戻り値はありません。

Examples

>>> config = TfCurvesFromCsvConfig(col_coef_a="a", base_color="red")
>>> calculator = TransferFunctionCalculator()
>>> calculator.create_plot_tf_curves_from_csv(
...     filepath="transfer_functions.csv",
...     config=config,
...     output_dirpath="output",
...     show_fig=False
... )
def create_plot_transfer_function( self, a: float, df_processed: pandas.core.frame.DataFrame, reference_name: str, target_name: str, figsize: tuple[float, float] = (10, 6), dpi: float | None = 350, output_dirpath: str | pathlib.Path | None = None, output_filename: str = 'tf.png', save_fig: bool = True, show_fig: bool = True, add_xlabel: bool = True, label_x: str = 'f (Hz)', label_y: str = 'コスペクトル比', label_target: str | None = None, label_ref: str = 'Tv') -> None:
512    def create_plot_transfer_function(
513        self,
514        a: float,
515        df_processed: pd.DataFrame,
516        reference_name: str,
517        target_name: str,
518        figsize: tuple[float, float] = (10, 6),
519        dpi: float | None = 350,
520        output_dirpath: str | Path | None = None,
521        output_filename: str = "tf.png",
522        save_fig: bool = True,
523        show_fig: bool = True,
524        add_xlabel: bool = True,
525        label_x: str = "f (Hz)",
526        label_y: str = "コスペクトル比",
527        label_target: str | None = None,
528        label_ref: str = "Tv",
529    ) -> None:
530        """
531        伝達関数とそのフィットをプロットします。
532
533        Parameters
534        ----------
535            a: float
536                伝達関数の係数です。
537            df_processed: pd.DataFrame
538                処理されたデータフレームです。
539            reference_name: str
540                参照データの名前です。
541            target_name: str
542                ターゲットデータの名前です。
543            figsize: tuple[float, float], optional
544                プロットのサイズを指定します。デフォルトは(10, 6)です。
545            dpi: float | None, optional
546                プロットの解像度を指定します。デフォルトは350です。
547            output_dirpath: str | Path | None, optional
548                プロットを保存するディレクトリを指定します。save_fig=Trueの場合は必須です。
549            output_filename: str, optional
550                保存するファイル名を指定します。デフォルトは"tf.png"です。
551            save_fig: bool, optional
552                プロットを保存するかどうかを指定します。デフォルトはTrueです。
553            show_fig: bool, optional
554                プロットを表示するかどうかを指定します。デフォルトはTrueです。
555            add_xlabel: bool, optional
556                x軸のラベルを追加するかどうかを指定します。デフォルトはTrueです。
557            label_x: str, optional
558                x軸のラベル名を指定します。デフォルトは"f (Hz)"です。
559            label_y: str, optional
560                y軸のラベル名を指定します。デフォルトは"コスペクトル比"です。
561            label_target: str | None, optional
562                比較先のラベル名を指定します。デフォルトはNoneです。
563            label_ref: str, optional
564                比較元のラベル名を指定します。デフォルトは"Tv"です。
565
566        Returns
567        -------
568            None
569                戻り値はありません。
570
571        Examples
572        -------
573        >>> # 伝達関数のプロットを作成し、保存する
574        >>> calculator = TransferFunctionCalculator()
575        >>> calculator.create_plot_transfer_function(
576        ...     a=0.5,
577        ...     df_processed=processed_df,
578        ...     reference_name="温度",
579        ...     target_name="CO2",
580        ...     output_dirpath="./output",
581        ...     output_filename="transfer_function.png"
582        ... )
583        """
584        df_cutoff: pd.DataFrame = self._cutoff_df(df_processed)
585
586        fig = plt.figure(figsize=figsize, dpi=dpi)
587        ax = fig.add_subplot(111)
588
589        ax.plot(
590            df_cutoff.index,
591            df_cutoff["target"] / df_cutoff["reference"],
592            "o",
593            label=f"{target_name} / {reference_name}",
594        )
595
596        x_fit = np.logspace(
597            np.log10(self._cutoff_freq_low), np.log10(self._cutoff_freq_high), 1000
598        )
599        y_fit = self.transfer_function(x_fit, a)
600        ax.plot(x_fit, y_fit, "-", label=f"フィット (a = {a:.4f})")
601
602        ax.set_xscale("log")
603        # グラフの設定
604        label_y_formatted: str = f"{label_y}\n({label_target} / {label_ref})"
605        plt.xscale("log")
606        if add_xlabel:
607            plt.xlabel(label_x)
608        plt.ylabel(label_y_formatted)
609        ax.legend()
610
611        if save_fig:
612            if output_dirpath is None:
613                raise ValueError(
614                    "save_fig = True のとき、 output_dirpath に有効なディレクトリパスを指定する必要があります。"
615                )
616            os.makedirs(output_dirpath, exist_ok=True)
617            # プロットをPNG形式で保存
618            fig.savefig(os.path.join(output_dirpath, output_filename), dpi=dpi)
619        if show_fig:
620            plt.show()
621        plt.close(fig=fig)

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

Parameters

a: float
    伝達関数の係数です。
df_processed: pd.DataFrame
    処理されたデータフレームです。
reference_name: str
    参照データの名前です。
target_name: str
    ターゲットデータの名前です。
figsize: tuple[float, float], optional
    プロットのサイズを指定します。デフォルトは(10, 6)です。
dpi: float | None, optional
    プロットの解像度を指定します。デフォルトは350です。
output_dirpath: str | Path | None, optional
    プロットを保存するディレクトリを指定します。save_fig=Trueの場合は必須です。
output_filename: str, optional
    保存するファイル名を指定します。デフォルトは"tf.png"です。
save_fig: bool, optional
    プロットを保存するかどうかを指定します。デフォルトはTrueです。
show_fig: bool, optional
    プロットを表示するかどうかを指定します。デフォルトはTrueです。
add_xlabel: bool, optional
    x軸のラベルを追加するかどうかを指定します。デフォルトはTrueです。
label_x: str, optional
    x軸のラベル名を指定します。デフォルトは"f (Hz)"です。
label_y: str, optional
    y軸のラベル名を指定します。デフォルトは"コスペクトル比"です。
label_target: str | None, optional
    比較先のラベル名を指定します。デフォルトはNoneです。
label_ref: str, optional
    比較元のラベル名を指定します。デフォルトは"Tv"です。

Returns

None
    戻り値はありません。

Examples

>>> # 伝達関数のプロットを作成し、保存する
>>> calculator = TransferFunctionCalculator()
>>> calculator.create_plot_transfer_function(
...     a=0.5,
...     df_processed=processed_df,
...     reference_name="温度",
...     target_name="CO2",
...     output_dirpath="./output",
...     output_filename="transfer_function.png"
... )
def process_data(self, col_reference: str, col_target: str) -> pandas.core.frame.DataFrame:
623    def process_data(self, col_reference: str, col_target: str) -> pd.DataFrame:
624        """
625        指定されたキーに基づいてデータを処理します。
626        周波数ごとにデータをグループ化し、中央値を計算します。
627        また、異常な比率のデータを除去します。
628
629        Parameters
630        ----------
631            col_reference: str
632                参照データのカラム名を指定します。
633            col_target: str
634                ターゲットデータのカラム名を指定します。
635
636        Returns
637        ----------
638            pd.DataFrame
639                処理されたデータフレーム。
640                indexは周波数、columnsは'reference'と'target'です。
641
642        Examples
643        -------
644        >>> calculator = TransferFunctionCalculator()
645        >>> processed_df = calculator.process_data(
646        ...     col_reference="温度",
647        ...     col_target="CO2"
648        ... )
649        """
650        df_internal: pd.DataFrame = self._df.copy()
651        col_freq: str = self._col_freq
652
653        # データ型の確認と変換
654        df_internal[col_freq] = pd.to_numeric(df_internal[col_freq], errors="coerce")
655        df_internal[col_reference] = pd.to_numeric(
656            df_internal[col_reference], errors="coerce"
657        )
658        df_internal[col_target] = pd.to_numeric(df_internal[col_target], errors="coerce")
659
660        # NaNを含む行を削除
661        df_internal = df_internal.dropna(subset=[col_freq, col_reference, col_target])
662
663        # グループ化と中央値の計算
664        grouped = df_internal.groupby(col_freq)
665        reference_data = grouped[col_reference].median()
666        target_data = grouped[col_target].median()
667
668        df_processed = pd.DataFrame(
669            {"reference": reference_data, "target": target_data}
670        )
671
672        # 異常な比率を除去
673        df_processed.loc[
674            (
675                (df_processed["target"] / df_processed["reference"] > 1)
676                | (df_processed["target"] / df_processed["reference"] < 0)
677            )
678        ] = np.nan
679        df_processed = df_processed.dropna()
680
681        return df_processed

指定されたキーに基づいてデータを処理します。 周波数ごとにデータをグループ化し、中央値を計算します。 また、異常な比率のデータを除去します。

Parameters

col_reference: str
    参照データのカラム名を指定します。
col_target: str
    ターゲットデータのカラム名を指定します。

Returns

pd.DataFrame
    処理されたデータフレーム。
    indexは周波数、columnsは'reference'と'target'です。

Examples

>>> calculator = TransferFunctionCalculator()
>>> processed_df = calculator.process_data(
...     col_reference="温度",
...     col_target="CO2"
... )
@classmethod
def transfer_function(cls, x: numpy.ndarray, a: float) -> numpy.ndarray:
702    @classmethod
703    def transfer_function(cls, x: np.ndarray, a: float) -> np.ndarray:
704        """
705        伝達関数を計算する。
706
707        Parameters
708        ----------
709            x: np.ndarray
710                周波数の配列。
711            a: float
712                伝達関数の係数。
713
714        Returns
715        ----------
716            np.ndarray
717                伝達関数の値。
718        """
719        return np.exp(-np.log(np.sqrt(2)) * np.power(x / a, 2))

伝達関数を計算する。

Parameters

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

Returns

np.ndarray
    伝達関数の値。
WindowFunctionType = typing.Literal['hanning', 'hamming', 'blackman']
__version__ = '0.0.0'
def setup_logger(logger: logging.Logger | None, log_level: int = 20) -> logging.Logger:
11def setup_logger(logger: Logger | None, log_level: int = INFO) -> Logger:
12    """
13    ロガーを設定します。
14
15    このメソッドは、ロギングの設定を行い、ログメッセージのフォーマットを指定します。
16    ログメッセージには、日付、ログレベル、メッセージが含まれます。
17
18    渡されたロガーが None または不正な場合は、新たにロガーを作成し、標準出力に
19    ログメッセージが表示されるように StreamHandler を追加します。ロガーのレベルは
20    引数で指定された log_level に基づいて設定されます。
21
22    Parameters
23    ----------
24        logger: Logger | None
25            使用するロガー。Noneの場合は新しいロガーを作成します。
26        log_level: int, optional
27            ロガーのログレベル。デフォルトはINFOで、ログの詳細度を制御します。
28
29    Returns
30    ----------
31        Logger
32            設定されたロガーオブジェクト。
33
34    Examples
35    --------
36    >>> from logging import getLogger, INFO
37    >>> logger = getLogger("my_logger")
38    >>> configured_logger = setup_logger(logger, log_level=INFO)
39    >>> configured_logger.info("ログメッセージ")
40    2024-01-01 00:00:00,000 - INFO - ログメッセージ
41    """
42    if logger is not None and isinstance(logger, Logger):
43        return logger
44    # 渡されたロガーがNoneまたは正しいものでない場合は独自に設定
45    new_logger: Logger = getLogger()
46    # 既存のハンドラーをすべて削除
47    for handler in new_logger.handlers[:]:
48        new_logger.removeHandler(handler)
49    new_logger.setLevel(log_level)  # ロガーのレベルを設定
50    ch = StreamHandler()
51    ch_formatter = Formatter("%(asctime)s - %(levelname)s - %(message)s")
52    ch.setFormatter(ch_formatter)  # フォーマッターをハンドラーに設定
53    new_logger.addHandler(ch)  # StreamHandlerの追加
54    return new_logger

ロガーを設定します。

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

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

Parameters

logger: Logger | None
    使用するロガー。Noneの場合は新しいロガーを作成します。
log_level: int, optional
    ロガーのログレベル。デフォルトはINFOで、ログの詳細度を制御します。

Returns

Logger
    設定されたロガーオブジェクト。

Examples

>>> from logging import getLogger, INFO
>>> logger = getLogger("my_logger")
>>> configured_logger = setup_logger(logger, log_level=INFO)
>>> configured_logger.info("ログメッセージ")
2024-01-01 00:00:00,000 - INFO - ログメッセージ
def setup_plot_params( font_family: list[str] | None = None, font_filepaths: list[str | pathlib.Path] | None = None, font_size: float = 20, legend_size: float = 20, tick_size: float = 20, title_size: float = 20, plot_params: dict[str, typing.Any] | None = None) -> None:
 57def setup_plot_params(
 58    font_family: list[str] | None = None,
 59    font_filepaths: list[str | Path] | None = None,
 60    font_size: float = 20,
 61    legend_size: float = 20,
 62    tick_size: float = 20,
 63    title_size: float = 20,
 64    plot_params: dict[str, Any] | None = None,
 65) -> None:
 66    """
 67    matplotlibのプロットパラメータを設定します。
 68
 69    Parameters
 70    ----------
 71        font_family: list[str] | None, optional
 72            使用するフォントファミリーのリスト。デフォルト値は["Arial", "MS Gothic", "sans-serif"]です。
 73        font_filepaths: list[str | Path] | None, optional
 74            フォントファイルのパスのリスト。デフォルト値はNoneです。指定された場合、fontManagerでフォントを登録します。
 75        font_size: float, optional
 76            軸ラベルのフォントサイズ。デフォルト値は20です。
 77        legend_size: float, optional
 78            凡例のフォントサイズ。デフォルト値は20です。
 79        tick_size: float, optional
 80            軸目盛りのフォントサイズ。デフォルト値は20です。
 81        title_size: float, optional
 82            タイトルのフォントサイズ。デフォルト値は20です。
 83        plot_params: dict[str, Any] | None, optional
 84            matplotlibのプロットパラメータの辞書。デフォルト値はNoneです。指定された場合、デフォルトのパラメータに上書きされます。
 85
 86    Returns
 87    -------
 88    None
 89        戻り値はありません。
 90
 91    Examples
 92    --------
 93    >>> # デフォルト設定でプロットパラメータを設定
 94    >>> setup_plot_params()
 95    
 96    >>> # カスタムフォントとサイズを指定
 97    >>> setup_plot_params(
 98    ...     font_family=["Helvetica", "sans-serif"],
 99    ...     font_size=16,
100    ...     legend_size=14
101    ... )
102    
103    >>> # カスタムプロットパラメータを追加
104    >>> custom_params = {"figure.figsize": (10, 6), "lines.linewidth": 2}
105    >>> setup_plot_params(plot_params=custom_params)
106    """
107    # フォントファイルの登録
108    if font_filepaths:
109        for path in font_filepaths:
110            if not os.path.exists(path):
111                raise FileNotFoundError(f"The font file at {path} does not exist.")
112            fm.fontManager.addfont(path)
113
114    # デフォルト値の設定
115    if font_family is None:
116        font_family = ["Arial", "MS Gothic", "sans-serif"]
117
118    # デフォルトのプロットパラメータ
119    default_params = {
120        "axes.linewidth": 1.0,
121        "axes.titlesize": title_size,  # タイトル
122        "grid.color": "gray",
123        "grid.linewidth": 1.0,
124        "font.family": font_family,
125        "font.size": font_size,  # 軸ラベル
126        "legend.fontsize": legend_size,  # 凡例
127        "text.color": "black",
128        "xtick.color": "black",
129        "ytick.color": "black",
130        "xtick.labelsize": tick_size,  # 軸目盛
131        "ytick.labelsize": tick_size,  # 軸目盛
132        "xtick.major.size": 0,
133        "ytick.major.size": 0,
134        "ytick.direction": "out",
135        "ytick.major.width": 1.0,
136    }
137
138    # plot_paramsが定義されている場合、デフォルトに追記
139    if plot_params:
140        default_params.update(plot_params)
141
142    plt.rcParams.update(default_params)  # プロットパラメータを更新

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

Parameters

font_family: list[str] | None, optional
    使用するフォントファミリーのリスト。デフォルト値は["Arial", "MS Gothic", "sans-serif"]です。
font_filepaths: list[str | Path] | None, optional
    フォントファイルのパスのリスト。デフォルト値はNoneです。指定された場合、fontManagerでフォントを登録します。
font_size: float, optional
    軸ラベルのフォントサイズ。デフォルト値は20です。
legend_size: float, optional
    凡例のフォントサイズ。デフォルト値は20です。
tick_size: float, optional
    軸目盛りのフォントサイズ。デフォルト値は20です。
title_size: float, optional
    タイトルのフォントサイズ。デフォルト値は20です。
plot_params: dict[str, Any] | None, optional
    matplotlibのプロットパラメータの辞書。デフォルト値はNoneです。指定された場合、デフォルトのパラメータに上書きされます。

Returns

None 戻り値はありません。

Examples

>>> # デフォルト設定でプロットパラメータを設定
>>> setup_plot_params()
>>> # カスタムフォントとサイズを指定
>>> setup_plot_params(
...     font_family=["Helvetica", "sans-serif"],
...     font_size=16,
...     legend_size=14
... )
>>> # カスタムプロットパラメータを追加
>>> custom_params = {"figure.figsize": (10, 6), "lines.linewidth": 2}
>>> setup_plot_params(plot_params=custom_params)