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

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

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

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

def analyze_delta_ch4_stats( self, hotspots: list[HotspotData]) -> None:
446    def analyze_delta_ch4_stats(self, hotspots: list[HotspotData]) -> None:
447        """
448        各タイプのホットスポットについてΔCH4の統計情報を計算し、結果を表示します。
449
450        Parameters
451        ----------
452            hotspots: list[HotspotData]
453                分析対象のホットスポットリスト
454        """
455        # タイプごとにホットスポットを分類
456        hotspots_by_type: dict[HotspotType, list[HotspotData]] = {
457            "bio": [h for h in hotspots if h.type == "bio"],
458            "gas": [h for h in hotspots if h.type == "gas"],
459            "comb": [h for h in hotspots if h.type == "comb"],
460        }
461
462        # 統計情報を計算し、表示
463        for spot_type, spots in hotspots_by_type.items():
464            if spots:
465                delta_ch4_values = [spot.delta_ch4 for spot in spots]
466                max_value = max(delta_ch4_values)
467                mean_value = sum(delta_ch4_values) / len(delta_ch4_values)
468                median_value = sorted(delta_ch4_values)[len(delta_ch4_values) // 2]
469                print(f"{spot_type}タイプのホットスポットの統計情報:")
470                print(f"  最大値: {max_value}")
471                print(f"  平均値: {mean_value}")
472                print(f"  中央値: {median_value}")
473            else:
474                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]:
476    def analyze_hotspots(
477        self,
478        duplicate_check_mode: Literal["none", "time_window", "time_all"] = "none",
479        min_time_threshold_seconds: float = 300,
480        max_time_threshold_hours: float = 12,
481    ) -> list[HotspotData]:
482        """
483        ホットスポットを検出して分析します。
484
485        Parameters
486        ----------
487            duplicate_check_mode: Literal["none", "time_window", "time_all"], optional
488                重複チェックのモード。デフォルトは"none"。
489                    - "none": 重複チェックを行わない
490                    - "time_window": 指定された時間窓内の重複のみを除外
491                    - "time_all": すべての時間範囲で重複チェックを行う
492            min_time_threshold_seconds: float, optional
493                重複とみなす最小時間の閾値。デフォルトは300秒。
494            max_time_threshold_hours: float, optional
495                重複チェックを一時的に無視する最大時間の閾値。デフォルトは12時間。
496
497        Returns
498        ----------
499            list[HotspotData]
500                検出されたホットスポットのリスト
501
502        Examples
503        --------
504        >>> analyzer = MobileMeasurementAnalyzer()
505        >>> # 重複チェックなしでホットスポットを検出
506        >>> hotspots = analyzer.analyze_hotspots()
507        >>>
508        >>> # 時間窓内の重複を除外してホットスポットを検出
509        >>> hotspots = analyzer.analyze_hotspots(
510        ...     duplicate_check_mode="time_window",
511        ...     min_time_threshold_seconds=600,
512        ...     max_time_threshold_hours=24
513        ... )
514        """
515        all_hotspots: list[HotspotData] = []
516        params: HotspotParams = self._hotspot_params
517
518        # 各データソースに対して解析を実行
519        # パラメータの計算
520        df_processed: pd.DataFrame = (
521            MobileMeasurementAnalyzer._calculate_hotspots_parameters(
522                df=self.df,
523                window_size=self._window_size,
524                col_ch4_ppm=params.col_ch4_ppm,
525                col_c2h6_ppb=params.col_c2h6_ppb,
526                col_h2o_ppm=params.col_h2o_ppm,
527                ch4_ppm_delta_min=params.ch4_ppm_delta_min,
528                ch4_ppm_delta_max=params.ch4_ppm_delta_max,
529                c2h6_ppb_delta_min=params.c2h6_ppb_delta_min,
530                c2h6_ppb_delta_max=params.c2h6_ppb_delta_max,
531                h2o_ppm_threshold=params.h2o_ppm_min,
532                rolling_method=params.rolling_method,
533                quantile_value=params.quantile_value,
534            )
535        )
536
537        # ホットスポットの検出
538        hotspots: list[HotspotData] = self._detect_hotspots(
539            df=df_processed,
540            ch4_enhance_threshold=self._ch4_enhance_threshold,
541        )
542        all_hotspots.extend(hotspots)
543
544        # 重複チェックモードに応じて処理
545        if duplicate_check_mode != "none":
546            unique_hotspots = MobileMeasurementAnalyzer.remove_hotspots_duplicates(
547                all_hotspots,
548                check_time_all=(duplicate_check_mode == "time_all"),
549                min_time_threshold_seconds=min_time_threshold_seconds,
550                max_time_threshold_hours=max_time_threshold_hours,
551                hotspot_area_meter=self._hotspot_area_meter,
552            )
553            self.logger.info(
554                f"重複除外: {len(all_hotspots)}{len(unique_hotspots)} ホットスポット"
555            )
556            return unique_hotspots
557
558        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]:
560    def calculate_measurement_stats(
561        self,
562        col_latitude: str = "latitude",
563        col_longitude: str = "longitude",
564        print_summary_individual: bool = True,
565        print_summary_total: bool = True,
566    ) -> tuple[float, timedelta]:
567        """
568        各ファイルの測定時間と走行距離を計算し、合計を返します。
569
570        Parameters
571        ----------
572            col_latitude: str, optional
573                緯度情報が格納されているカラム名。デフォルト値は"latitude"です。
574            col_longitude: str, optional
575                経度情報が格納されているカラム名。デフォルト値は"longitude"です。
576            print_summary_individual: bool, optional
577                個別ファイルの統計を表示するかどうか。デフォルト値はTrueです。
578            print_summary_total: bool, optional
579                合計統計を表示するかどうか。デフォルト値はTrueです。
580
581        Returns
582        ----------
583            tuple[float, timedelta]
584                総距離(km)と総時間のタプル
585
586        Examples
587        ----------
588        >>> analyzer = MobileMeasurementAnalyzer(config_list)
589        >>> total_distance, total_time = analyzer.calculate_measurement_stats()
590        >>> print(f"総距離: {total_distance:.2f}km")
591        >>> print(f"総時間: {total_time}")
592        """
593        total_distance: float = 0.0
594        total_time: timedelta = timedelta()
595        individual_stats: list[dict] = []  # 個別の統計情報を保存するリスト
596
597        # プログレスバーを表示しながら計算
598        for config in tqdm(self._configs, desc="Calculating", unit="file"):
599            df, source_name = self._load_data(config=config)
600            # 時間の計算
601            time_spent = df.index[-1] - df.index[0]
602
603            # 距離の計算
604            distance_km = 0.0
605            for i in range(len(df) - 1):
606                lat1, lon1 = df.iloc[i][[col_latitude, col_longitude]]
607                lat2, lon2 = df.iloc[i + 1][[col_latitude, col_longitude]]
608                distance_km += (
609                    MobileMeasurementAnalyzer._calculate_distance(
610                        lat1=lat1, lon1=lon1, lat2=lat2, lon2=lon2
611                    )
612                    / 1000
613                )
614
615            # 合計に加算
616            total_distance += distance_km
617            total_time += time_spent
618
619            # 統計情報を保存
620            if print_summary_individual:
621                average_speed = distance_km / (time_spent.total_seconds() / 3600)
622                individual_stats.append(
623                    {
624                        "source": source_name,
625                        "distance": distance_km,
626                        "time": time_spent,
627                        "speed": average_speed,
628                    }
629                )
630
631        # 計算完了後に統計情報を表示
632        if print_summary_individual:
633            self.logger.info("=== Individual Stats ===")
634            for stat in individual_stats:
635                print(f"File        : {stat['source']}")
636                print(f"  Distance  : {stat['distance']:.2f} km")
637                print(f"  Time      : {stat['time']}")
638                print(f"  Avg. Speed: {stat['speed']:.1f} km/h\n")
639
640        # 合計を表示
641        if print_summary_total:
642            average_speed_total: float = total_distance / (
643                total_time.total_seconds() / 3600
644            )
645            self.logger.info("=== Total Stats ===")
646            print(f"  Distance  : {total_distance:.2f} km")
647            print(f"  Time      : {total_time}")
648            print(f"  Avg. Speed: {average_speed_total:.1f} km/h\n")
649
650        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:
652    def create_hotspots_map(
653        self,
654        hotspots: list[HotspotData],
655        output_dirpath: str | Path | None = None,
656        output_filename: str = "hotspots_map.html",
657        center_marker_color: str = "green",
658        center_marker_label: str = "Center",
659        plot_center_marker: bool = True,
660        radius_meters: float = 3000,
661        save_fig: bool = True,
662    ) -> None:
663        """
664        ホットスポットの分布を地図上にプロットして保存します。
665
666        Parameters
667        ----------
668            hotspots: list[HotspotData]
669                プロットするホットスポットのリスト
670            output_dirpath: str | Path | None, optional
671                保存先のディレクトリパス。未指定の場合はカレントディレクトリに保存されます。
672            output_filename: str, optional
673                保存するファイル名。デフォルト値は"hotspots_map.html"です。
674            center_marker_color: str, optional
675                中心を示すマーカーの色。デフォルト値は"green"です。
676            center_marker_label: str, optional
677                中心を示すマーカーのラベル。デフォルト値は"Center"です。
678            plot_center_marker: bool, optional
679                中心を示すマーカーを表示するかどうか。デフォルト値はTrueです。
680            radius_meters: float, optional
681                区画分けを示す線の長さ(メートル)。デフォルト値は3000です。
682            save_fig: bool, optional
683                図を保存するかどうか。デフォルト値はTrueです。
684
685        Returns
686        -------
687            None
688
689        Examples
690        --------
691        >>> analyzer = MobileMeasurementAnalyzer(center_lat=35.6895, center_lon=139.6917, configs=[])
692        >>> hotspots = [HotspotData(...)]  # ホットスポットのリスト
693        >>> analyzer.create_hotspots_map(
694        ...     hotspots=hotspots,
695        ...     output_dirpath="results",
696        ...     radius_meters=5000
697        ... )
698        """
699        # 地図の作成
700        m = folium.Map(
701            location=[self._center_lat, self._center_lon],
702            zoom_start=15,
703            tiles="OpenStreetMap",
704        )
705
706        # ホットスポットの種類ごとに異なる色でプロット
707        for spot in hotspots:
708            # NaN値チェックを追加
709            if math.isnan(spot.avg_lat) or math.isnan(spot.avg_lon):
710                continue
711
712            # default type
713            color = "black"
714            # タイプに応じて色を設定
715            if spot.type == "comb":
716                color = "green"
717            elif spot.type == "gas":
718                color = "red"
719            elif spot.type == "bio":
720                color = "blue"
721
722            # CSSのgrid layoutを使用してHTMLタグを含むテキストをフォーマット
723            popup_html = f"""
724            <div style='font-family: Arial; font-size: 12px; display: grid; grid-template-columns: auto auto auto; gap: 5px;'>
725                <b>Date</b> <span>:</span> <span>{spot.timestamp}</span>
726                <b>Lat</b> <span>:</span> <span>{spot.avg_lat:.3f}</span>
727                <b>Lon</b> <span>:</span> <span>{spot.avg_lon:.3f}</span>
728                <b>ΔCH<sub>4</sub></b> <span>:</span> <span>{spot.delta_ch4:.3f}</span>
729                <b>ΔC<sub>2</sub>H<sub>6</sub></b> <span>:</span> <span>{spot.delta_c2h6:.3f}</span>
730                <b>Ratio</b> <span>:</span> <span>{spot.delta_ratio:.3f}</span>
731                <b>Type</b> <span>:</span> <span>{spot.type}</span>
732                <b>Section</b> <span>:</span> <span>{spot.section}</span>
733            </div>
734            """
735
736            # ポップアップのサイズを指定
737            popup = folium.Popup(
738                folium.Html(popup_html, script=True),
739                max_width=200,  # 最大幅(ピクセル)
740            )
741
742            folium.CircleMarker(
743                location=[spot.avg_lat, spot.avg_lon],
744                radius=8,
745                color=color,
746                fill=True,
747                popup=popup,
748            ).add_to(m)
749
750        # 中心点のマーカー
751        if plot_center_marker:
752            folium.Marker(
753                [self._center_lat, self._center_lon],
754                popup=center_marker_label,
755                icon=folium.Icon(color=center_marker_color, icon="info-sign"),
756            ).add_to(m)
757
758        # 区画の境界線を描画
759        for section in range(self._num_sections):
760            start_angle = math.radians(-180 + section * self._section_size)
761
762            const_r = self.EARTH_RADIUS_METERS
763
764            # 境界線の座標を計算
765            lat1 = self._center_lat
766            lon1 = self._center_lon
767            lat2 = math.degrees(
768                math.asin(
769                    math.sin(math.radians(lat1)) * math.cos(radius_meters / const_r)
770                    + math.cos(math.radians(lat1))
771                    * math.sin(radius_meters / const_r)
772                    * math.cos(start_angle)
773                )
774            )
775            lon2 = self._center_lon + math.degrees(
776                math.atan2(
777                    math.sin(start_angle)
778                    * math.sin(radius_meters / const_r)
779                    * math.cos(math.radians(lat1)),
780                    math.cos(radius_meters / const_r)
781                    - math.sin(math.radians(lat1)) * math.sin(math.radians(lat2)),
782                )
783            )
784
785            # 境界線を描画
786            folium.PolyLine(
787                locations=[[lat1, lon1], [lat2, lon2]],
788                color="black",
789                weight=1,
790                opacity=0.5,
791            ).add_to(m)
792
793        # 地図を保存
794        if save_fig and output_dirpath is None:
795            raise ValueError(
796                "save_fig = True の場合、 output_dirpath を指定する必要があります。有効なディレクトリパスを指定してください。"
797            )
798            output_filepath: str = os.path.join(output_dirpath, output_filename)
799            m.save(str(output_filepath))
800            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:
802    def export_hotspots_to_csv(
803        self,
804        hotspots: list[HotspotData],
805        output_dirpath: str | Path | None = None,
806        output_filename: str = "hotspots.csv",
807    ) -> None:
808        """
809        ホットスポットの情報をCSVファイルに出力します。
810
811        Parameters
812        ----------
813            hotspots: list[HotspotData]
814                出力するホットスポットのリスト
815            output_dirpath: str | Path | None, optional
816                出力先ディレクトリのパス。未指定の場合はValueErrorが発生します。
817            output_filename: str, optional
818                出力ファイル名。デフォルト値は"hotspots.csv"です。
819
820        Returns
821        -------
822            None
823                戻り値はありません。
824
825        Examples
826        --------
827        >>> analyzer = MobileMeasurementAnalyzer()
828        >>> hotspots = analyzer.analyze_hotspots()
829        >>> analyzer.export_hotspots_to_csv(
830        ...     hotspots=hotspots,
831        ...     output_dirpath="output",
832        ...     output_filename="hotspots_20240101.csv"
833        ... )
834        """
835        # 日時の昇順でソート
836        sorted_hotspots = sorted(hotspots, key=lambda x: x.timestamp)
837
838        # 出力用のデータを作成
839        records = []
840        for spot in sorted_hotspots:
841            record = {
842                "timestamp": spot.timestamp,
843                "type": spot.type,
844                "delta_ch4": spot.delta_ch4,
845                "delta_c2h6": spot.delta_c2h6,
846                "delta_ratio": spot.delta_ratio,
847                "correlation": spot.correlation,
848                "angle": spot.angle,
849                "section": spot.section,
850                "latitude": spot.avg_lat,
851                "longitude": spot.avg_lon,
852            }
853            records.append(record)
854
855        # DataFrameに変換してCSVに出力
856        if output_dirpath is None:
857            raise ValueError(
858                "output_dirpath が指定されていません。有効なディレクトリパスを指定してください。"
859            )
860        os.makedirs(output_dirpath, exist_ok=True)
861        output_filepath: str = os.path.join(output_dirpath, output_filename)
862        df: pd.DataFrame = pd.DataFrame(records)
863        df.to_csv(output_filepath, index=False)
864        self.logger.info(
865            f"ホットスポット情報をCSVファイルに出力しました: {output_filepath}"
866        )

ホットスポットの情報を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:
868    @staticmethod
869    def extract_source_name_from_path(path: str | Path) -> str:
870        """
871        ファイルパスからソース名(拡張子なしのファイル名)を抽出します。
872
873        Parameters
874        ----------
875            path: str | Path
876                ソース名を抽出するファイルパス
877                例: "/path/to/Pico100121_241017_092120+.txt"
878
879        Returns
880        ----------
881            str
882                抽出されたソース名
883                例: "Pico100121_241017_092120+"
884
885        Examples:
886        ----------
887            >>> path = "/path/to/data/Pico100121_241017_092120+.txt"
888            >>> MobileMeasurementAnalyzer.extract_source_from_path(path)
889            'Pico100121_241017_092120+'
890        """
891        # Pathオブジェクトに変換
892        path_obj: Path = Path(path)
893        # stem属性で拡張子なしのファイル名を取得
894        source_name: str = path_obj.stem
895        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:
897    def get_preprocessed_data(
898        self,
899    ) -> pd.DataFrame:
900        """
901        データ前処理を行い、CH4とC2H6の相関解析に必要な形式に整えます。
902        コンストラクタで読み込んだすべてのデータを前処理し、結合したDataFrameを返します。
903
904        Returns
905        ----------
906            pd.DataFrame
907                前処理済みの結合されたDataFrame
908        """
909        return self.df.copy()

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

Returns

pd.DataFrame
    前処理済みの結合されたDataFrame
def get_section_size(self) -> float:
911    def get_section_size(self) -> float:
912        """
913        セクションのサイズを取得するメソッド。
914        このメソッドは、解析対象のデータを区画に分割する際の
915        各区画の角度範囲を示すサイズを返します。
916
917        Returns
918        ----------
919            float
920                1セクションのサイズ(度単位)
921        """
922        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:
 924    def plot_ch4_delta_histogram(
 925        self,
 926        hotspots: list[HotspotData],
 927        output_dirpath: str | Path | None,
 928        output_filename: str = "ch4_delta_histogram.png",
 929        dpi: float | None = 350,
 930        figsize: tuple[float, float] = (8, 6),
 931        fontsize: float = 20,
 932        hotspot_colors: dict[HotspotType, str] | None = None,
 933        xlabel: str = "Δ$\\mathregular{CH_{4}}$ (ppm)",
 934        ylabel: str = "Frequency",
 935        xlim: tuple[float, float] | None = None,
 936        ylim: tuple[float, float] | None = None,
 937        save_fig: bool = True,
 938        show_fig: bool = True,
 939        yscale_log: bool = True,
 940        print_bins_analysis: bool = False,
 941    ) -> None:
 942        """
 943        CH4の増加量(ΔCH4)の積み上げヒストグラムをプロットします。
 944
 945        Parameters
 946        ----------
 947            hotspots: list[HotspotData]
 948                プロットするホットスポットのリスト
 949            output_dirpath: str | Path | None
 950                保存先のディレクトリパス
 951            output_filename: str, optional
 952                保存するファイル名。デフォルト値は"ch4_delta_histogram.png"です。
 953            dpi: float | None, optional
 954                解像度。デフォルト値は350です。
 955            figsize: tuple[float, float], optional
 956                図のサイズ。デフォルト値は(8, 6)です。
 957            fontsize: float, optional
 958                フォントサイズ。デフォルト値は20です。
 959            hotspot_colors: dict[HotspotType, str] | None, optional
 960                ホットスポットの色を定義する辞書。未指定の場合は以下のデフォルト値を使用します:
 961                {
 962                    "bio": "blue",
 963                    "gas": "red",
 964                    "comb": "green",
 965                }
 966            xlabel: str, optional
 967                x軸のラベル。デフォルト値は"Δ$\\mathregular{CH_{4}}$ (ppm)"です。
 968            ylabel: str, optional
 969                y軸のラベル。デフォルト値は"Frequency"です。
 970            xlim: tuple[float, float] | None, optional
 971                x軸の範囲。未指定の場合は自動設定されます。
 972            ylim: tuple[float, float] | None, optional
 973                y軸の範囲。未指定の場合は自動設定されます。
 974            save_fig: bool, optional
 975                図の保存を許可するフラグ。デフォルト値はTrueです。
 976            show_fig: bool, optional
 977                図の表示を許可するフラグ。デフォルト値はTrueです。
 978            yscale_log: bool, optional
 979                y軸をlogスケールにするかどうか。デフォルト値はTrueです。
 980            print_bins_analysis: bool, optional
 981                ビンごとの内訳を表示するかどうか。デフォルト値はFalseです。
 982
 983        Returns
 984        -------
 985            None
 986
 987        Examples
 988        --------
 989        >>> analyzer = MobileMeasurementAnalyzer(...)
 990        >>> hotspots = analyzer.detect_hotspots()
 991        >>> analyzer.plot_ch4_delta_histogram(
 992        ...     hotspots=hotspots,
 993        ...     output_dirpath="results",
 994        ...     xlim=(0, 5),
 995        ...     ylim=(0, 100)
 996        ... )
 997        """
 998        if hotspot_colors is None:
 999            hotspot_colors = {
1000                "bio": "blue",
1001                "gas": "red",
1002                "comb": "green",
1003            }
1004        plt.rcParams["font.size"] = fontsize
1005        fig = plt.figure(figsize=figsize, dpi=dpi)
1006
1007        # ホットスポットからデータを抽出
1008        all_ch4_deltas = []
1009        all_types = []
1010        for spot in hotspots:
1011            all_ch4_deltas.append(spot.delta_ch4)
1012            all_types.append(spot.type)
1013
1014        # データをNumPy配列に変換
1015        all_ch4_deltas = np.array(all_ch4_deltas)
1016        all_types = np.array(all_types)
1017
1018        # 0.1刻みのビンを作成
1019        if xlim is not None:
1020            bins = np.arange(xlim[0], xlim[1] + 0.1, 0.1)
1021        else:
1022            max_val = np.ceil(np.max(all_ch4_deltas) * 10) / 10
1023            bins = np.arange(0, max_val + 0.1, 0.1)
1024
1025        # タイプごとのヒストグラムデータを計算
1026        hist_data = {}
1027        # HotspotTypeのリテラル値を使用してイテレーション
1028        for type_name in get_args(HotspotType):  # typing.get_argsをインポート
1029            mask = all_types == type_name
1030            if np.any(mask):
1031                counts, _ = np.histogram(all_ch4_deltas[mask], bins=bins)
1032                hist_data[type_name] = counts
1033
1034        # ビンごとの内訳を表示
1035        if print_bins_analysis:
1036            self.logger.info("各ビンの内訳:")
1037            print(f"{'Bin Range':15} {'bio':>8} {'gas':>8} {'comb':>8} {'Total':>8}")
1038            print("-" * 50)
1039
1040            for i in range(len(bins) - 1):
1041                bin_start = bins[i]
1042                bin_end = bins[i + 1]
1043                bio_count = hist_data.get("bio", np.zeros(len(bins) - 1))[i]
1044                gas_count = hist_data.get("gas", np.zeros(len(bins) - 1))[i]
1045                comb_count = hist_data.get("comb", np.zeros(len(bins) - 1))[i]
1046                total = bio_count + gas_count + comb_count
1047
1048                if total > 0:  # 合計が0のビンは表示しない
1049                    print(
1050                        f"{bin_start:4.1f}-{bin_end:<8.1f}"
1051                        f"{int(bio_count):8d}"
1052                        f"{int(gas_count):8d}"
1053                        f"{int(comb_count):8d}"
1054                        f"{int(total):8d}"
1055                    )
1056
1057        # 積み上げヒストグラムを作成
1058        bottom = np.zeros_like(hist_data.get("bio", np.zeros(len(bins) - 1)))
1059
1060        # HotspotTypeのリテラル値を使用してイテレーション
1061        for type_name in get_args(HotspotType):
1062            if type_name in hist_data:
1063                plt.bar(
1064                    bins[:-1],
1065                    hist_data[type_name],
1066                    width=np.diff(bins)[0],
1067                    bottom=bottom,
1068                    color=hotspot_colors[type_name],
1069                    label=type_name,
1070                    alpha=0.6,
1071                    align="edge",
1072                )
1073                bottom += hist_data[type_name]
1074
1075        if yscale_log:
1076            plt.yscale("log")
1077        plt.xlabel(xlabel)
1078        plt.ylabel(ylabel)
1079        plt.legend()
1080        plt.grid(True, which="both", ls="-", alpha=0.2)
1081
1082        # 軸の範囲を設定
1083        if xlim is not None:
1084            plt.xlim(xlim)
1085        if ylim is not None:
1086            plt.ylim(ylim)
1087
1088        # グラフの保存または表示
1089        if save_fig:
1090            if output_dirpath is None:
1091                raise ValueError(
1092                    "save_fig = True の場合、 output_dirpath を指定する必要があります。有効なディレクトリパスを指定してください。"
1093                )
1094            os.makedirs(output_dirpath, exist_ok=True)
1095            output_filepath: str = os.path.join(output_dirpath, output_filename)
1096            plt.savefig(output_filepath, bbox_inches="tight")
1097        if show_fig:
1098            plt.show()
1099        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:
1101    def plot_mapbox(
1102        self,
1103        df: pd.DataFrame,
1104        col_conc: str,
1105        mapbox_access_token: str,
1106        sort_conc_column: bool = True,
1107        output_dirpath: str | Path | None = None,
1108        output_filename: str = "mapbox_plot.html",
1109        col_lat: str = "latitude",
1110        col_lon: str = "longitude",
1111        colorscale: str = "Jet",
1112        center_lat: float | None = None,
1113        center_lon: float | None = None,
1114        zoom: float = 12,
1115        width: int = 700,
1116        height: int = 700,
1117        tick_font_family: str = "Arial",
1118        title_font_family: str = "Arial",
1119        tick_font_size: int = 12,
1120        title_font_size: int = 14,
1121        marker_size: int = 4,
1122        colorbar_title: str | None = None,
1123        value_range: tuple[float, float] | None = None,
1124        save_fig: bool = True,
1125        show_fig: bool = True,
1126    ) -> None:
1127        """
1128        Mapbox上にデータをプロットします。
1129
1130        Parameters
1131        ----------
1132            df: pd.DataFrame
1133                プロットするデータを含むDataFrame
1134            col_conc: str
1135                カラーマッピングに使用する列名
1136            mapbox_access_token: str
1137                Mapboxのアクセストークン
1138            sort_conc_column: bool, optional
1139                濃度列をソートするかどうか。デフォルトはTrue
1140            output_dirpath: str | Path | None, optional
1141                出力ディレクトリのパス。デフォルトはNone
1142            output_filename: str, optional
1143                出力ファイル名。デフォルトは"mapbox_plot.html"
1144            col_lat: str, optional
1145                緯度の列名。デフォルトは"latitude"
1146            col_lon: str, optional
1147                経度の列名。デフォルトは"longitude"
1148            colorscale: str, optional
1149                使用するカラースケール。デフォルトは"Jet"
1150            center_lat: float | None, optional
1151                中心緯度。デフォルトはNoneで、self._center_latを使用
1152            center_lon: float | None, optional
1153                中心経度。デフォルトはNoneで、self._center_lonを使用
1154            zoom: float, optional
1155                マップの初期ズームレベル。デフォルトは12
1156            width: int, optional
1157                プロットの幅(ピクセル)。デフォルトは700
1158            height: int, optional
1159                プロットの高さ(ピクセル)。デフォルトは700
1160            tick_font_family: str, optional
1161                カラーバーの目盛りフォントファミリー。デフォルトは"Arial"
1162            title_font_family: str, optional
1163                カラーバーのタイトルフォントファミリー。デフォルトは"Arial"
1164            tick_font_size: int, optional
1165                カラーバーの目盛りフォントサイズ。デフォルトは12
1166            title_font_size: int, optional
1167                カラーバーのタイトルフォントサイズ。デフォルトは14
1168            marker_size: int, optional
1169                マーカーのサイズ。デフォルトは4
1170            colorbar_title: str | None, optional
1171                カラーバーのタイトル。デフォルトはNoneでcol_concを使用
1172            value_range: tuple[float, float] | None, optional
1173                カラーマッピングの範囲。デフォルトはNoneでデータの最小値と最大値を使用
1174            save_fig: bool, optional
1175                図を保存するかどうか。デフォルトはTrue
1176            show_fig: bool, optional
1177                図を表示するかどうか。デフォルトはTrue
1178
1179        Returns
1180        -------
1181            None
1182
1183        Examples
1184        --------
1185        >>> analyzer = MobileMeasurementAnalyzer()
1186        >>> df = pd.DataFrame({
1187        ...     'latitude': [35.681236, 35.681237],
1188        ...     'longitude': [139.767125, 139.767126],
1189        ...     'concentration': [1.2, 1.5]
1190        ... })
1191        >>> analyzer.plot_mapbox(
1192        ...     df=df,
1193        ...     col_conc='concentration',
1194        ...     mapbox_access_token='your_token_here'
1195        ... )
1196        """
1197        df_mapping: pd.DataFrame = df.copy().dropna(subset=[col_conc])
1198        if sort_conc_column:
1199            df_mapping = df_mapping.sort_values(col_conc)
1200        # 中心座標の設定
1201        center_lat = center_lat if center_lat is not None else self._center_lat
1202        center_lon = center_lon if center_lon is not None else self._center_lon
1203
1204        # カラーマッピングの範囲を設定
1205        cmin, cmax = 0, 0
1206        if value_range is None:
1207            cmin = df_mapping[col_conc].min()
1208            cmax = df_mapping[col_conc].max()
1209        else:
1210            cmin, cmax = value_range
1211
1212        # カラーバーのタイトルを設定
1213        title_text = colorbar_title if colorbar_title is not None else col_conc
1214
1215        # Scattermapboxのデータを作成
1216        scatter_data = go.Scattermapbox(
1217            lat=df_mapping[col_lat],
1218            lon=df_mapping[col_lon],
1219            text=df_mapping[col_conc].astype(str),
1220            hoverinfo="text",
1221            mode="markers",
1222            marker={
1223                "color": df_mapping[col_conc],
1224                "size": marker_size,
1225                "reversescale": False,
1226                "autocolorscale": False,
1227                "colorscale": colorscale,
1228                "cmin": cmin,
1229                "cmax": cmax,
1230                "colorbar": {
1231                    "tickformat": "3.2f",
1232                    "outlinecolor": "black",
1233                    "outlinewidth": 1.5,
1234                    "ticks": "outside",
1235                    "ticklen": 7,
1236                    "tickwidth": 1.5,
1237                    "tickcolor": "black",
1238                    "tickfont": {
1239                        "family": tick_font_family,
1240                        "color": "black",
1241                        "size": tick_font_size,
1242                    },
1243                    "title": {
1244                        "text": title_text,
1245                        "side": "top",
1246                    },  # カラーバーのタイトルを設定
1247                    "titlefont": {
1248                        "family": title_font_family,
1249                        "color": "black",
1250                        "size": title_font_size,
1251                    },
1252                },
1253            },
1254        )
1255
1256        # レイアウトの設定
1257        layout = go.Layout(
1258            width=width,
1259            height=height,
1260            showlegend=False,
1261            mapbox={
1262                "accesstoken": mapbox_access_token,
1263                "center": {"lat": center_lat, "lon": center_lon},
1264                "zoom": zoom,
1265            },
1266        )
1267
1268        # 図の作成
1269        fig = go.Figure(data=[scatter_data], layout=layout)
1270
1271        # 図の保存
1272        if save_fig:
1273            # 保存時の出力ディレクトリチェック
1274            if output_dirpath is None:
1275                raise ValueError(
1276                    "save_fig=Trueの場合、output_dirpathを指定する必要があります。"
1277                )
1278            os.makedirs(output_dirpath, exist_ok=True)
1279            output_filepath = os.path.join(output_dirpath, output_filename)
1280            pyo.plot(fig, filename=output_filepath, auto_open=False)
1281            self.logger.info(f"Mapboxプロットを保存しました: {output_filepath}")
1282        # 図の表示
1283        if show_fig:
1284            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:
1286    def plot_scatter_c2c1(
1287        self,
1288        hotspots: list[HotspotData],
1289        output_dirpath: str | Path | None = None,
1290        output_filename: str = "scatter_c2c1.png",
1291        figsize: tuple[float, float] = (4, 4),
1292        dpi: float | None = 350,
1293        hotspot_colors: dict[HotspotType, str] | None = None,
1294        hotspot_labels: dict[HotspotType, str] | None = None,
1295        fontsize: float = 12,
1296        xlim: tuple[float, float] = (0, 2.0),
1297        ylim: tuple[float, float] = (0, 50),
1298        xlabel: str = "Δ$\\mathregular{CH_{4}}$ (ppm)",
1299        ylabel: str = "Δ$\\mathregular{C_{2}H_{6}}$ (ppb)",
1300        xscale_log: bool = False,
1301        yscale_log: bool = False,
1302        add_legend: bool = True,
1303        save_fig: bool = True,
1304        show_fig: bool = True,
1305        add_ratio_labels: bool = True,
1306        ratio_labels: dict[float, tuple[float, float, str]] | None = None,
1307    ) -> None:
1308        """
1309        検出されたホットスポットのΔC2H6とΔCH4の散布図をプロットします。
1310
1311        Parameters
1312        ----------
1313            hotspots: list[HotspotData]
1314                プロットするホットスポットのリスト
1315            output_dirpath: str | Path | None, optional
1316                保存先のディレクトリパス。未指定の場合はNoneとなります。
1317            output_filename: str, optional
1318                保存するファイル名。デフォルト値は"scatter_c2c1.png"です。
1319            figsize: tuple[float, float], optional
1320                図のサイズ。デフォルト値は(4, 4)です。
1321            dpi: float | None, optional
1322                解像度。デフォルト値は350です。
1323            hotspot_colors: dict[HotspotType, str] | None, optional
1324                ホットスポットの色を定義する辞書。未指定の場合は{"bio": "blue", "gas": "red", "comb": "green"}が使用されます。
1325            hotspot_labels: dict[HotspotType, str] | None, optional
1326                ホットスポットのラベルを定義する辞書。未指定の場合は{"bio": "bio", "gas": "gas", "comb": "comb"}が使用されます。
1327            fontsize: float, optional
1328                フォントサイズ。デフォルト値は12です。
1329            xlim: tuple[float, float], optional
1330                x軸の範囲。デフォルト値は(0, 2.0)です。
1331            ylim: tuple[float, float], optional
1332                y軸の範囲。デフォルト値は(0, 50)です。
1333            xlabel: str, optional
1334                x軸のラベル。デフォルト値は"Δ$\\mathregular{CH_{4}}$ (ppm)"です。
1335            ylabel: str, optional
1336                y軸のラベル。デフォルト値は"Δ$\\mathregular{C_{2}H_{6}}$ (ppb)"です。
1337            xscale_log: bool, optional
1338                x軸を対数スケールにするかどうか。デフォルト値はFalseです。
1339            yscale_log: bool, optional
1340                y軸を対数スケールにするかどうか。デフォルト値はFalseです。
1341            add_legend: bool, optional
1342                凡例を追加するかどうか。デフォルト値はTrueです。
1343            save_fig: bool, optional
1344                図の保存を許可するフラグ。デフォルト値はTrueです。
1345            show_fig: bool, optional
1346                図の表示を許可するフラグ。デフォルト値はTrueです。
1347            add_ratio_labels: bool, optional
1348                比率線を表示するかどうか。デフォルト値はTrueです。
1349            ratio_labels: dict[float, tuple[float, float, str]] | None, optional
1350                比率線とラベルの設定。キーは比率値、値は(x位置, y位置, ラベルテキスト)のタプル。
1351                未指定の場合は以下のデフォルト値が使用されます:
1352                {0.001: (1.25, 2, "0.001"), 0.005: (1.25, 8, "0.005"),
1353                0.010: (1.25, 15, "0.01"), 0.020: (1.25, 30, "0.02"),
1354                0.030: (1.0, 40, "0.03"), 0.076: (0.20, 42, "0.076 (Osaka)")}
1355
1356        Returns
1357        -------
1358            None
1359
1360        Examples
1361        --------
1362        >>> analyzer = MobileMeasurementAnalyzer()
1363        >>> hotspots = analyzer.analyze_hotspots()
1364        >>> analyzer.plot_scatter_c2c1(
1365        ...     hotspots=hotspots,
1366        ...     output_dirpath="output",
1367        ...     xlim=(0, 5),
1368        ...     ylim=(0, 100)
1369        ... )
1370        """
1371        # デフォルト値の設定
1372        if hotspot_colors is None:
1373            hotspot_colors = {
1374                "bio": "blue",
1375                "gas": "red",
1376                "comb": "green",
1377            }
1378        if hotspot_labels is None:
1379            hotspot_labels = {
1380                "bio": "bio",
1381                "gas": "gas",
1382                "comb": "comb",
1383            }
1384        if ratio_labels is None:
1385            ratio_labels = {
1386                0.001: (1.25, 2, "0.001"),
1387                0.005: (1.25, 8, "0.005"),
1388                0.010: (1.25, 15, "0.01"),
1389                0.020: (1.25, 30, "0.02"),
1390                0.030: (1.0, 40, "0.03"),
1391                0.076: (0.20, 42, "0.076 (Osaka)"),
1392            }
1393        plt.rcParams["font.size"] = fontsize
1394        fig = plt.figure(figsize=figsize, dpi=dpi)
1395
1396        # タイプごとのデータを収集
1397        type_data: dict[HotspotType, list[tuple[float, float]]] = {
1398            "bio": [],
1399            "gas": [],
1400            "comb": [],
1401        }
1402        for spot in hotspots:
1403            type_data[spot.type].append((spot.delta_ch4, spot.delta_c2h6))
1404
1405        # タイプごとにプロット(データが存在する場合のみ)
1406        for spot_type, data in type_data.items():
1407            if data:  # データが存在する場合のみプロット
1408                ch4_values, c2h6_values = zip(*data, strict=True)
1409                plt.plot(
1410                    ch4_values,
1411                    c2h6_values,
1412                    "o",
1413                    c=hotspot_colors[spot_type],
1414                    alpha=0.5,
1415                    ms=2,
1416                    label=hotspot_labels[spot_type],
1417                )
1418
1419        # プロット後、軸の設定前に比率の線を追加
1420        x = np.array([0, 5])
1421        base_ch4 = 0.0
1422        base = 0.0
1423
1424        # 各比率に対して線を引く
1425        if ratio_labels is not None:
1426            if not add_ratio_labels:
1427                raise ValueError(
1428                    "ratio_labels に基づいて比率線を描画する場合は、 add_ratio_labels = True を指定してください。"
1429                )
1430            for ratio, (x_pos, y_pos, label) in ratio_labels.items():
1431                y = (x - base_ch4) * 1000 * ratio + base
1432                plt.plot(x, y, "-", c="black", alpha=0.5)
1433                plt.text(x_pos, y_pos, label)
1434
1435        # 軸の設定
1436        if xscale_log:
1437            plt.xscale("log")
1438        if yscale_log:
1439            plt.yscale("log")
1440
1441        plt.xlim(xlim)
1442        plt.ylim(ylim)
1443        plt.xlabel(xlabel)
1444        plt.ylabel(ylabel)
1445        if add_legend:
1446            plt.legend()
1447
1448        # グラフの保存または表示
1449        if save_fig:
1450            if output_dirpath is None:
1451                raise ValueError(
1452                    "save_fig = True の場合、 output_dirpath を指定する必要があります。有効なディレクトリパスを指定してください。"
1453                )
1454            output_filepath: str = os.path.join(output_dirpath, output_filename)
1455            plt.savefig(output_filepath, bbox_inches="tight")
1456            self.logger.info(f"散布図を保存しました: {output_filepath}")
1457        if show_fig:
1458            plt.show()
1459        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:
1461    def plot_conc_timeseries(
1462        self,
1463        output_dirpath: str | Path | None = None,
1464        output_filename: str = "timeseries.png",
1465        figsize: tuple[float, float] = (8, 4),
1466        dpi: float | None = 350,
1467        save_fig: bool = True,
1468        show_fig: bool = True,
1469        col_ch4: str = "ch4_ppm",
1470        col_c2h6: str = "c2h6_ppb",
1471        col_h2o: str = "h2o_ppm",
1472        ylim_ch4: tuple[float, float] | None = None,
1473        ylim_c2h6: tuple[float, float] | None = None,
1474        ylim_h2o: tuple[float, float] | None = None,
1475        yscale_log_ch4: bool = False,
1476        yscale_log_c2h6: bool = False,
1477        yscale_log_h2o: bool = False,
1478        font_size: float = 12,
1479        label_pad: float = 10,
1480        line_color: str = "black",
1481    ) -> None:
1482        """
1483        CH4、C2H6、H2Oの時系列データをプロットします。
1484
1485        Parameters
1486        ----------
1487            output_dirpath: str | Path | None, optional
1488                保存先のディレクトリを指定します。save_fig=Trueの場合は必須です。デフォルト値はNoneです。
1489            output_filename: str, optional
1490                保存するファイル名を指定します。デフォルト値は"timeseries.png"です。
1491            figsize: tuple[float, float], optional
1492                図のサイズを指定します。デフォルト値は(8, 4)です。
1493            dpi: float | None, optional
1494                図の解像度を指定します。デフォルト値は350です。
1495            save_fig: bool, optional
1496                図を保存するかどうかを指定します。デフォルト値はTrueです。
1497            show_fig: bool, optional
1498                図を表示するかどうかを指定します。デフォルト値はTrueです。
1499            col_ch4: str, optional
1500                CH4データの列名を指定します。デフォルト値は"ch4_ppm"です。
1501            col_c2h6: str, optional
1502                C2H6データの列名を指定します。デフォルト値は"c2h6_ppb"です。
1503            col_h2o: str, optional
1504                H2Oデータの列名を指定します。デフォルト値は"h2o_ppm"です。
1505            ylim_ch4: tuple[float, float] | None, optional
1506                CH4プロットのy軸範囲を指定します。デフォルト値はNoneです。
1507            ylim_c2h6: tuple[float, float] | None, optional
1508                C2H6プロットのy軸範囲を指定します。デフォルト値はNoneです。
1509            ylim_h2o: tuple[float, float] | None, optional
1510                H2Oプロットのy軸範囲を指定します。デフォルト値はNoneです。
1511            yscale_log_ch4: bool, optional
1512                CH4データのy軸を対数スケールで表示するかどうかを指定します。デフォルト値はFalseです。
1513            yscale_log_c2h6: bool, optional
1514                C2H6データのy軸を対数スケールで表示するかどうかを指定します。デフォルト値はFalseです。
1515            yscale_log_h2o: bool, optional
1516                H2Oデータのy軸を対数スケールで表示するかどうかを指定します。デフォルト値はFalseです。
1517            font_size: float, optional
1518                プロット全体のフォントサイズを指定します。デフォルト値は12です。
1519            label_pad: float, optional
1520                y軸ラベルとプロットの間隔を指定します。デフォルト値は10です。
1521            line_color: str, optional
1522                プロットする線の色を指定します。デフォルト値は"black"です。
1523
1524        Returns
1525        -------
1526            None
1527
1528        Examples
1529        --------
1530        >>> analyzer = MobileMeasurementAnalyzer(df)
1531        >>> analyzer.plot_conc_timeseries(
1532        ...     output_dirpath="output",
1533        ...     ylim_ch4=(1.8, 2.5),
1534        ...     ylim_c2h6=(0, 100),
1535        ...     ylim_h2o=(0, 20000)
1536        ... )
1537        """
1538        # プロットパラメータの設定
1539        plt.rcParams.update(
1540            {
1541                "font.size": font_size,
1542                "axes.labelsize": font_size,
1543                "axes.titlesize": font_size,
1544                "xtick.labelsize": font_size,
1545                "ytick.labelsize": font_size,
1546            }
1547        )
1548        df_internal: pd.DataFrame = self.df.copy()
1549
1550        # プロットの作成
1551        fig = plt.figure(figsize=figsize, dpi=dpi)
1552
1553        # CH4プロット
1554        ax1 = fig.add_subplot(3, 1, 1)
1555        ax1.plot(df_internal.index, df_internal[col_ch4], c=line_color)
1556        if ylim_ch4:
1557            ax1.set_ylim(ylim_ch4)
1558        if yscale_log_ch4:
1559            ax1.set_yscale("log")
1560        ax1.set_ylabel("$\\mathregular{CH_{4}}$ (ppm)", labelpad=label_pad)
1561        ax1.grid(True, alpha=0.3)
1562
1563        # C2H6プロット
1564        ax2 = fig.add_subplot(3, 1, 2)
1565        ax2.plot(df_internal.index, df_internal[col_c2h6], c=line_color)
1566        if ylim_c2h6:
1567            ax2.set_ylim(ylim_c2h6)
1568        if yscale_log_c2h6:
1569            ax2.set_yscale("log")
1570        ax2.set_ylabel("$\\mathregular{C_{2}H_{6}}$ (ppb)", labelpad=label_pad)
1571        ax2.grid(True, alpha=0.3)
1572
1573        # H2Oプロット
1574        ax3 = fig.add_subplot(3, 1, 3)
1575        ax3.plot(df_internal.index, df_internal[col_h2o], c=line_color)
1576        if ylim_h2o:
1577            ax3.set_ylim(ylim_h2o)
1578        if yscale_log_h2o:
1579            ax3.set_yscale("log")
1580        ax3.set_ylabel("$\\mathregular{H_{2}O}$ (ppm)", labelpad=label_pad)
1581        ax3.grid(True, alpha=0.3)
1582
1583        # x軸のフォーマット調整
1584        for ax in [ax1, ax2, ax3]:
1585            ax.xaxis.set_major_formatter(mdates.DateFormatter("%H:%M"))
1586            # 軸のラベルとグリッド線の調整
1587            ax.tick_params(axis="both", which="major", labelsize=font_size)
1588            ax.grid(True, alpha=0.3)
1589
1590        # サブプロット間の間隔調整
1591        plt.subplots_adjust(wspace=0.38, hspace=0.38)
1592
1593        # 図の保存
1594        if save_fig:
1595            if output_dirpath is None:
1596                raise ValueError(
1597                    "save_fig = True の場合、 output_dirpath を指定する必要があります。有効なディレクトリパスを指定してください。"
1598                )
1599            os.makedirs(output_dirpath, exist_ok=True)
1600            output_filepath = os.path.join(output_dirpath, output_filename)
1601            plt.savefig(output_filepath, bbox_inches="tight")
1602
1603        if show_fig:
1604            plt.show()
1605        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:
1607    def plot_conc_timeseries_with_hotspots(
1608        self,
1609        hotspots: list[HotspotData] | None = None,
1610        output_dirpath: str | Path | None = None,
1611        output_filename: str = "timeseries_with_hotspots.png",
1612        figsize: tuple[float, float] = (8, 6),
1613        dpi: float | None = 350,
1614        save_fig: bool = True,
1615        show_fig: bool = True,
1616        col_ch4: str = "ch4_ppm",
1617        col_c2h6: str = "c2h6_ppb",
1618        col_h2o: str = "h2o_ppm",
1619        add_legend: bool = True,
1620        legend_bbox_to_anchor: tuple[float, float] = (0.5, 0.05),
1621        legend_ncol: int | None = None,
1622        font_size: float = 12,
1623        label_pad: float = 10,
1624        line_color: str = "black",
1625        hotspot_colors: dict[HotspotType, str] | None = None,
1626        hotspot_markerscale: float = 1,
1627        hotspot_size: int = 10,
1628        time_margin_minutes: float = 2.0,
1629        ylim_ch4: tuple[float, float] | None = None,
1630        ylim_c2h6: tuple[float, float] | None = None,
1631        ylim_h2o: tuple[float, float] | None = None,
1632        ylim_ratio: tuple[float, float] | None = None,
1633        yscale_log_ch4: bool = False,
1634        yscale_log_c2h6: bool = False,
1635        yscale_log_h2o: bool = False,
1636        yscale_log_ratio: bool = False,
1637        ylabel_ch4: str = "$\\mathregular{CH_{4}}$ (ppm)",
1638        ylabel_c2h6: str = "$\\mathregular{C_{2}H_{6}}$ (ppb)",
1639        ylabel_h2o: str = "$\\mathregular{H_{2}O}$ (ppm)",
1640        ylabel_ratio: str = "ΔC$_2$H$_6$/ΔCH$_4$\n(ppb ppm$^{-1}$)",
1641    ) -> None:
1642        """
1643        時系列データとホットスポットをプロットします。
1644
1645        Parameters
1646        ----------
1647            hotspots: list[HotspotData] | None, optional
1648                表示するホットスポットのリスト。デフォルトはNoneで、この場合ホットスポットは表示されません。
1649            output_dirpath: str | Path | None, optional
1650                出力先ディレクトリのパス。save_fig=Trueの場合は必須です。デフォルトはNoneです。
1651            output_filename: str, optional
1652                保存するファイル名。デフォルトは"timeseries_with_hotspots.png"です。
1653            figsize: tuple[float, float], optional
1654                図のサイズ。デフォルトは(8, 6)です。
1655            dpi: float | None, optional
1656                図の解像度。デフォルトは350です。
1657            save_fig: bool, optional
1658                図を保存するかどうか。デフォルトはTrueです。
1659            show_fig: bool, optional
1660                図を表示するかどうか。デフォルトはTrueです。
1661            col_ch4: str, optional
1662                CH4データのカラム名。デフォルトは"ch4_ppm"です。
1663            col_c2h6: str, optional
1664                C2H6データのカラム名。デフォルトは"c2h6_ppb"です。
1665            col_h2o: str, optional
1666                H2Oデータのカラム名。デフォルトは"h2o_ppm"です。
1667            add_legend: bool, optional
1668                ホットスポットの凡例を表示するかどうか。デフォルトはTrueです。
1669            legend_bbox_to_anchor: tuple[float, float], optional
1670                ホットスポットの凡例の位置。デフォルトは(0.5, 0.05)です。
1671            legend_ncol: int | None, optional
1672                凡例のカラム数。デフォルトはNoneで、この場合ホットスポットの種類数に基づいて一行で表示します。
1673            font_size: float, optional
1674                基本フォントサイズ。デフォルトは12です。
1675            label_pad: float, optional
1676                y軸ラベルのパディング。デフォルトは10です。
1677            line_color: str, optional
1678                線の色。デフォルトは"black"です。
1679            hotspot_colors: dict[HotspotType, str] | None, optional
1680                ホットスポットタイプごとの色指定。デフォルトはNoneで、{"bio": "blue", "gas": "red", "comb": "green"}が使用されます。
1681            hotspot_markerscale: float, optional
1682                ホットスポットの凡例でのサイズ。hotspot_sizeに対する相対値。デフォルトは1です。
1683            hotspot_size: int, optional
1684                ホットスポットの図でのサイズ。デフォルトは10です。
1685            time_margin_minutes: float, optional
1686                プロットの時間軸の余白(分)。デフォルトは2.0分です。
1687            ylim_ch4: tuple[float, float] | None, optional
1688                CH4プロットのy軸範囲。デフォルトはNoneで、自動設定されます。
1689            ylim_c2h6: tuple[float, float] | None, optional
1690                C2H6プロットのy軸範囲。デフォルトはNoneで、自動設定されます。
1691            ylim_h2o: tuple[float, float] | None, optional
1692                H2Oプロットのy軸範囲。デフォルトはNoneで、自動設定されます。
1693            ylim_ratio: tuple[float, float] | None, optional
1694                比率プロットのy軸範囲。デフォルトはNoneで、自動設定されます。
1695            yscale_log_ch4: bool, optional
1696                CH4データのy軸を対数スケールで表示するかどうか。デフォルトはFalseです。
1697            yscale_log_c2h6: bool, optional
1698                C2H6データのy軸を対数スケールで表示するかどうか。デフォルトはFalseです。
1699            yscale_log_h2o: bool, optional
1700                H2Oデータのy軸を対数スケールで表示するかどうか。デフォルトはFalseです。
1701            yscale_log_ratio: bool, optional
1702                比率データのy軸を対数スケールで表示するかどうか。デフォルトはFalseです。
1703            ylabel_ch4: str, optional
1704                CH4プロットのy軸ラベル。デフォルトは"$\\mathregular{CH_{4}}$ (ppm)"です。
1705            ylabel_c2h6: str, optional
1706                C2H6プロットのy軸ラベル。デフォルトは"$\\mathregular{C_{2}H_{6}}$ (ppb)"です。
1707            ylabel_h2o: str, optional
1708                H2Oプロットのy軸ラベル。デフォルトは"$\\mathregular{H_{2}O}$ (ppm)"です。
1709            ylabel_ratio: str, optional
1710                比率プロットのy軸ラベル。デフォルトは"ΔC$_2$H$_6$/ΔCH$_4$\\n(ppb ppm$^{-1}$)"です。
1711
1712        Examples
1713        --------
1714        基本的な使用方法:
1715        >>> analyzer = MobileMeasurementAnalyzer(df)
1716        >>> analyzer.plot_conc_timeseries_with_hotspots()
1717
1718        ホットスポットを指定して保存する:
1719        >>> hotspots = [HotspotData(...), HotspotData(...)]
1720        >>> analyzer.plot_conc_timeseries_with_hotspots(
1721        ...     hotspots=hotspots,
1722        ...     output_dirpath="output",
1723        ...     save_fig=True
1724        ... )
1725
1726        カスタマイズした表示:
1727        >>> analyzer.plot_conc_timeseries_with_hotspots(
1728        ...     figsize=(12, 8),
1729        ...     ylim_ch4=(1.8, 2.5),
1730        ...     yscale_log_c2h6=True,
1731        ...     hotspot_colors={"bio": "purple", "gas": "orange"}
1732        ... )
1733        """
1734        if hotspot_colors is None:
1735            hotspot_colors = {"bio": "blue", "gas": "red", "comb": "green"}
1736        # プロットパラメータの設定
1737        plt.rcParams.update(
1738            {
1739                "font.size": font_size,
1740                "axes.labelsize": font_size,
1741                "axes.titlesize": font_size,
1742                "xtick.labelsize": font_size,
1743                "ytick.labelsize": font_size,
1744            }
1745        )
1746
1747        df_internal: pd.DataFrame = self.df.copy()
1748
1749        # プロットの作成
1750        fig = plt.figure(figsize=figsize, dpi=dpi)
1751
1752        # サブプロットのグリッドを作成 (4行1列)
1753        gs = gridspec.GridSpec(4, 1, height_ratios=[1, 1, 1, 1])
1754
1755        # 時間軸の範囲を設定(余白付き)
1756        time_min = df_internal.index.min()
1757        time_max = df_internal.index.max()
1758        time_margin = pd.Timedelta(minutes=time_margin_minutes)
1759        plot_time_min = time_min - time_margin
1760        plot_time_max = time_max + time_margin
1761
1762        # CH4プロット
1763        ax1 = fig.add_subplot(gs[0])
1764        ax1.plot(df_internal.index, df_internal[col_ch4], c=line_color)
1765        if ylim_ch4:
1766            ax1.set_ylim(ylim_ch4)
1767        if yscale_log_ch4:
1768            ax1.set_yscale("log")
1769        ax1.set_ylabel(ylabel_ch4, labelpad=label_pad)
1770        ax1.grid(True, alpha=0.3)
1771        ax1.set_xlim(plot_time_min, plot_time_max)
1772
1773        # C2H6プロット
1774        ax2 = fig.add_subplot(gs[1])
1775        ax2.plot(df_internal.index, df_internal[col_c2h6], c=line_color)
1776        if ylim_c2h6:
1777            ax2.set_ylim(ylim_c2h6)
1778        if yscale_log_c2h6:
1779            ax2.set_yscale("log")
1780        ax2.set_ylabel(ylabel_c2h6, labelpad=label_pad)
1781        ax2.grid(True, alpha=0.3)
1782        ax2.set_xlim(plot_time_min, plot_time_max)
1783
1784        # H2Oプロット
1785        ax3 = fig.add_subplot(gs[2])
1786        ax3.plot(df_internal.index, df_internal[col_h2o], c=line_color)
1787        if ylim_h2o:
1788            ax3.set_ylim(ylim_h2o)
1789        if yscale_log_h2o:
1790            ax3.set_yscale("log")
1791        ax3.set_ylabel(ylabel_h2o, labelpad=label_pad)
1792        ax3.grid(True, alpha=0.3)
1793        ax3.set_xlim(plot_time_min, plot_time_max)
1794
1795        # ホットスポットの比率プロット
1796        ax4 = fig.add_subplot(gs[3])
1797
1798        if hotspots:
1799            # ホットスポットをDataFrameに変換
1800            hotspot_df = pd.DataFrame(
1801                [
1802                    {
1803                        "timestamp": pd.to_datetime(spot.timestamp),
1804                        "delta_ratio": spot.delta_ratio,
1805                        "type": spot.type,
1806                    }
1807                    for spot in hotspots
1808                ]
1809            )
1810
1811            # タイプごとにプロット
1812            for spot_type in set(hotspot_df["type"]):
1813                type_data = hotspot_df[hotspot_df["type"] == spot_type]
1814
1815                # 点をプロット
1816                ax4.scatter(
1817                    type_data["timestamp"],
1818                    type_data["delta_ratio"],
1819                    c=hotspot_colors.get(spot_type, "black"),
1820                    label=spot_type,
1821                    alpha=0.6,
1822                    s=hotspot_size,
1823                )
1824
1825        ax4.set_ylabel(ylabel_ratio, labelpad=label_pad)
1826        if ylim_ratio:
1827            ax4.set_ylim(ylim_ratio)
1828        if yscale_log_ratio:
1829            ax4.set_yscale("log")
1830        ax4.grid(True, alpha=0.3)
1831        ax4.set_xlim(plot_time_min, plot_time_max)  # 他のプロットと同じ時間範囲を設定
1832
1833        # 凡例を図の下部に配置
1834        if hotspots and add_legend:
1835            ncol = (
1836                legend_ncol if legend_ncol is not None else len(set(hotspot_df["type"]))
1837            )
1838            # markerscaleは元のサイズに対する倍率を指定するため、
1839            # 目的のサイズ(100)をプロットのマーカーサイズで割ることで、適切な倍率を計算しています
1840            fig.legend(
1841                bbox_to_anchor=legend_bbox_to_anchor,
1842                loc="upper center",
1843                ncol=ncol,
1844                columnspacing=1.0,
1845                handletextpad=0.5,
1846                markerscale=hotspot_markerscale,
1847            )
1848
1849        # x軸のフォーマット調整(全てのサブプロットで共通)
1850        for ax in [ax1, ax2, ax3, ax4]:
1851            ax.xaxis.set_major_formatter(mdates.DateFormatter("%H:%M"))
1852            ax.xaxis.set_major_locator(mdates.AutoDateLocator())
1853            ax.tick_params(axis="both", which="major", labelsize=font_size)
1854            ax.grid(True, alpha=0.3)
1855
1856        # サブプロット間の間隔調整と凡例のためのスペース確保
1857        plt.subplots_adjust(hspace=0.38, bottom=0.12)  # bottomを0.15から0.12に変更
1858
1859        # 図の保存
1860        if save_fig:
1861            if output_dirpath is None:
1862                raise ValueError(
1863                    "save_fig = True の場合、 output_dirpath を指定する必要があります。有効なディレクトリパスを指定してください。"
1864                )
1865            os.makedirs(output_dirpath, exist_ok=True)
1866            output_filepath = os.path.join(output_dirpath, output_filename)
1867            plt.savefig(output_filepath, bbox_inches="tight", dpi=dpi)
1868
1869        if show_fig:
1870            plt.show()
1871        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'):
2427    def remove_c2c1_ratio_duplicates(
2428        self,
2429        df: pd.DataFrame,
2430        min_time_threshold_seconds: float = 300,
2431        max_time_threshold_hours: float = 12.0,
2432        check_time_all: bool = True,
2433        hotspot_area_meter: float = 50.0,
2434        col_ch4_ppm: str = "ch4_ppm",
2435        col_ch4_ppm_mv: str = "ch4_ppm_mv",
2436        col_ch4_ppm_delta: str = "ch4_ppm_delta",
2437    ):
2438        """
2439        メタン濃度の増加が閾値を超えた地点から、重複を除外してユニークなホットスポットを抽出する関数。
2440
2441        Parameters
2442        ----------
2443            df: pd.DataFrame
2444                入力データフレーム。必須カラム:
2445                - latitude: 緯度
2446                - longitude: 経度
2447                - ch4_ppm: メタン濃度(ppm)
2448                - ch4_ppm_mv: メタン濃度の移動平均(ppm)
2449                - ch4_ppm_delta: メタン濃度の増加量(ppm)
2450            min_time_threshold_seconds: float, optional
2451                重複とみなす最小時間差(秒)。デフォルト値は300秒(5分)。
2452            max_time_threshold_hours: float, optional
2453                別ポイントとして扱う最大時間差(時間)。デフォルト値は12時間。
2454            check_time_all: bool, optional
2455                時間閾値を超えた場合の重複チェックを継続するかどうか。デフォルト値はTrue。
2456            hotspot_area_meter: float, optional
2457                重複とみなす距離の閾値(メートル)。デフォルト値は50メートル。
2458            col_ch4_ppm: str, optional
2459                メタン濃度のカラム名。デフォルト値は"ch4_ppm"。
2460            col_ch4_ppm_mv: str, optional
2461                メタン濃度移動平均のカラム名。デフォルト値は"ch4_ppm_mv"。
2462            col_ch4_ppm_delta: str, optional
2463                メタン濃度増加量のカラム名。デフォルト値は"ch4_ppm_delta"。
2464
2465        Returns
2466        ----------
2467            pd.DataFrame
2468                ユニークなホットスポットのデータフレーム。
2469
2470        Examples
2471        ----------
2472        >>> analyzer = MobileMeasurementAnalyzer()
2473        >>> df = pd.read_csv("measurement_data.csv")
2474        >>> unique_spots = analyzer.remove_c2c1_ratio_duplicates(
2475        ...     df,
2476        ...     min_time_threshold_seconds=300,
2477        ...     hotspot_area_meter=50.0
2478        ... )
2479        """
2480        df_data: pd.DataFrame = df.copy()
2481        # メタン濃度の増加が閾値を超えた点を抽出
2482        mask = (
2483            df_data[col_ch4_ppm] - df_data[col_ch4_ppm_mv] > self._ch4_enhance_threshold
2484        )
2485        hotspot_candidates = df_data[mask].copy()
2486
2487        # ΔCH4の降順でソート
2488        sorted_hotspots = hotspot_candidates.sort_values(
2489            by=col_ch4_ppm_delta, ascending=False
2490        )
2491        used_positions = []
2492        unique_hotspots = pd.DataFrame()
2493
2494        for _, spot in sorted_hotspots.iterrows():
2495            should_add = True
2496            for used_lat, used_lon, used_time in used_positions:
2497                # 距離チェック
2498                distance = geodesic(
2499                    (spot.latitude, spot.longitude), (used_lat, used_lon)
2500                ).meters
2501
2502                if distance < hotspot_area_meter:
2503                    # 時間差の計算(秒単位)
2504                    time_diff = pd.Timedelta(
2505                        spot.name - pd.to_datetime(used_time)
2506                    ).total_seconds()
2507                    time_diff_abs = abs(time_diff)
2508
2509                    # 時間差に基づく判定
2510                    if check_time_all:
2511                        # 時間に関係なく、距離が近ければ重複とみなす
2512                        # ΔCH4が大きい方を残す(現在のスポットは必ず小さい)
2513                        should_add = False
2514                        break
2515                    else:
2516                        # 時間窓による判定を行う
2517                        if time_diff_abs <= min_time_threshold_seconds:
2518                            # Case 1: 最小時間閾値以内は重複とみなす
2519                            should_add = False
2520                            break
2521                        elif time_diff_abs > max_time_threshold_hours * 3600:
2522                            # Case 2: 最大時間閾値を超えた場合は重複チェックをスキップ
2523                            continue
2524                        # Case 3: その間の時間差の場合は、距離が近ければ重複とみなす
2525                        should_add = False
2526                        break
2527
2528            if should_add:
2529                unique_hotspots = pd.concat([unique_hotspots, pd.DataFrame([spot])])
2530                used_positions.append((spot.latitude, spot.longitude, spot.name))
2531
2532        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]:
2534    @staticmethod
2535    def remove_hotspots_duplicates(
2536        hotspots: list[HotspotData],
2537        check_time_all: bool,
2538        min_time_threshold_seconds: float = 300,
2539        max_time_threshold_hours: float = 12,
2540        hotspot_area_meter: float = 50,
2541    ) -> list[HotspotData]:
2542        """
2543        重複するホットスポットを除外します。
2544
2545        このメソッドは、与えられたホットスポットのリストから重複を検出し、
2546        一意のホットスポットのみを返します。重複の判定は、指定された
2547        時間および距離の閾値に基づいて行われます。
2548
2549        Parameters
2550        ----------
2551            hotspots: list[HotspotData]
2552                重複を除外する対象のホットスポットのリスト
2553            check_time_all: bool
2554                時間に関係なく重複チェックを行うかどうか
2555            min_time_threshold_seconds: float, optional
2556                重複とみなす最小時間の閾値(秒)。デフォルト値は300秒
2557            max_time_threshold_hours: float, optional
2558                重複チェックを一時的に無視する最大時間の閾値(時間)。デフォルト値は12時間
2559            hotspot_area_meter: float, optional
2560                重複とみなす距離の閾値(メートル)。デフォルト値は50メートル
2561
2562        Returns
2563        ----------
2564            list[HotspotData]
2565                重複を除去したホットスポットのリスト
2566
2567        Examples
2568        ----------
2569        >>> hotspots = [HotspotData(...), HotspotData(...)]  # ホットスポットのリスト
2570        >>> analyzer = MobileMeasurementAnalyzer()
2571        >>> unique_spots = analyzer.remove_hotspots_duplicates(
2572        ...     hotspots=hotspots,
2573        ...     check_time_all=True,
2574        ...     min_time_threshold_seconds=300,
2575        ...     max_time_threshold_hours=12,
2576        ...     hotspot_area_meter=50
2577        ... )
2578        """
2579        # ΔCH4の降順でソート
2580        sorted_hotspots: list[HotspotData] = sorted(
2581            hotspots, key=lambda x: x.delta_ch4, reverse=True
2582        )
2583        used_positions_by_type: dict[
2584            HotspotType, list[tuple[float, float, str, float]]
2585        ] = {
2586            "bio": [],
2587            "gas": [],
2588            "comb": [],
2589        }
2590        unique_hotspots: list[HotspotData] = []
2591
2592        for spot in sorted_hotspots:
2593            is_duplicate = MobileMeasurementAnalyzer._is_duplicate_spot(
2594                current_lat=spot.avg_lat,
2595                current_lon=spot.avg_lon,
2596                current_time=spot.timestamp,
2597                used_positions=used_positions_by_type[spot.type],
2598                check_time_all=check_time_all,
2599                min_time_threshold_seconds=min_time_threshold_seconds,
2600                max_time_threshold_hours=max_time_threshold_hours,
2601                hotspot_area_meter=hotspot_area_meter,
2602            )
2603
2604            if not is_duplicate:
2605                unique_hotspots.append(spot)
2606                used_positions_by_type[spot.type].append(
2607                    (spot.avg_lat, spot.avg_lon, spot.timestamp, spot.delta_ch4)
2608                )
2609
2610        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    >>> config = MobileMeasurementConfig(
227    ...     fs=1.0,
228    ...     lag=2.0,
229    ...     path="data.csv",
230    ...     bias_removal=BiasRemovalConfig(method="linear"),
231    ...     h2o_correction=H2OCorrectionConfig(method="default")
232    ... )
233    """
234
235    fs: float
236    lag: float
237    path: Path | str
238    bias_removal: BiasRemovalConfig | None = None
239    h2o_correction: H2OCorrectionConfig | None = None
240
241    def __post_init__(self) -> None:
242        """
243        インスタンス生成後に入力値の検証を行います。
244        """
245        # fsが有効かを確認
246        if not isinstance(self.fs, int | float) or self.fs <= 0:
247            raise ValueError(
248                f"Invalid sampling frequency: {self.fs}. Must be a positive float."
249            )
250        # lagが0以上のfloatかを確認
251        if not isinstance(self.lag, int | float) or self.lag < 0:
252            raise ValueError(
253                f"Invalid lag value: {self.lag}. Must be a non-negative float."
254            )
255        # 拡張子の確認
256        supported_extensions: list[str] = [".txt", ".csv"]
257        extension = Path(self.path).suffix
258        if extension not in supported_extensions:
259            raise ValueError(
260                f"Unsupported file extension: '{extension}'. Supported: {supported_extensions}"
261            )
262        # ファイルの存在確認
263        if not os.path.exists(self.path):
264            raise FileNotFoundError(f"'{self.path}'")
265
266    @classmethod
267    def validate_and_create(
268        cls,
269        fs: float,
270        lag: float,
271        path: Path | str,
272        bias_removal: BiasRemovalConfig | None = None,
273        h2o_correction: H2OCorrectionConfig | None = None,
274    ) -> "MobileMeasurementConfig":
275        """
276        入力値を検証し、MobileMeasurementConfigインスタンスを生成するファクトリメソッドです。
277
278        Parameters
279        ----------
280            fs: float
281                サンプリング周波数(Hz)。正の値である必要があります。
282            lag: float
283                遅延時間(秒)。0以上の値である必要があります。
284            path: Path | str
285                入力ファイルのパス。サポートされている拡張子は.txtと.csvです。
286            bias_removal: BiasRemovalConfig | None, optional
287                バイアス除去の設定。未指定の場合は補正を実施しません。
288            h2o_correction: H2OCorrectionConfig | None, optional
289                水蒸気補正の設定。未指定の場合は補正を実施しません。
290
291        Returns
292        -------
293            MobileMeasurementConfig
294                検証された入力設定を持つMobileMeasurementConfigオブジェクト
295
296        Examples
297        --------
298        >>> config = MobileMeasurementConfig.validate_and_create(
299        ...     fs=1.0,
300        ...     lag=2.0,
301        ...     path="data.csv"
302        ... )
303        """
304        return cls(
305            fs=fs,
306            lag=lag,
307            path=path,
308            bias_removal=bias_removal,
309            h2o_correction=h2o_correction,
310        )

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

Parameters

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

Examples

>>> config = MobileMeasurementConfig(
...     fs=1.0,
...     lag=2.0,
...     path="data.csv",
...     bias_removal=BiasRemovalConfig(method="linear"),
...     h2o_correction=H2OCorrectionConfig(method="default")
... )
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:
266    @classmethod
267    def validate_and_create(
268        cls,
269        fs: float,
270        lag: float,
271        path: Path | str,
272        bias_removal: BiasRemovalConfig | None = None,
273        h2o_correction: H2OCorrectionConfig | None = None,
274    ) -> "MobileMeasurementConfig":
275        """
276        入力値を検証し、MobileMeasurementConfigインスタンスを生成するファクトリメソッドです。
277
278        Parameters
279        ----------
280            fs: float
281                サンプリング周波数(Hz)。正の値である必要があります。
282            lag: float
283                遅延時間(秒)。0以上の値である必要があります。
284            path: Path | str
285                入力ファイルのパス。サポートされている拡張子は.txtと.csvです。
286            bias_removal: BiasRemovalConfig | None, optional
287                バイアス除去の設定。未指定の場合は補正を実施しません。
288            h2o_correction: H2OCorrectionConfig | None, optional
289                水蒸気補正の設定。未指定の場合は補正を実施しません。
290
291        Returns
292        -------
293            MobileMeasurementConfig
294                検証された入力設定を持つMobileMeasurementConfigオブジェクト
295
296        Examples
297        --------
298        >>> config = MobileMeasurementConfig.validate_and_create(
299        ...     fs=1.0,
300        ...     lag=2.0,
301        ...     path="data.csv"
302        ... )
303        """
304        return cls(
305            fs=fs,
306            lag=lag,
307            path=path,
308            bias_removal=bias_removal,
309            h2o_correction=h2o_correction,
310        )

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

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

def get_available_dates(self) -> list[str]:
 90    def get_available_dates(self) -> list[str]:
 91        """
 92        利用可能なファイルの日付一覧を返却します。
 93
 94        Returns
 95        ----------
 96            list[str]
 97                'yyyy.MM'形式の日付リスト
 98
 99        Examples
100        --------
101        >>> converter = MonthlyConverter("path/to/excel/files")
102        >>> dates = converter.get_available_dates()
103        >>> print(dates)
104        ['2023.01', '2023.02', '2023.03']
105        """
106        dates = []
107        for filename in self._directory.glob(self._file_pattern):
108            try:
109                date = self._extract_date(filename.name)
110                dates.append(date.strftime(self.FILE_DATE_FORMAT))
111            except ValueError:
112                continue
113        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]:
115    def get_sheet_names(self, filename: str) -> list[str]:
116        """
117        指定されたファイルで利用可能なシート名の一覧を返却します。
118
119        Parameters
120        ----------
121            filename: str
122                対象のExcelファイル名を指定します。ファイル名のみを指定し、パスは含めません。
123
124        Returns
125        ----------
126            list[str]
127                シート名のリスト
128
129        Examples
130        --------
131        >>> converter = MonthlyConverter("path/to/excel/files")
132        >>> sheets = converter.get_sheet_names("2023.01.xlsx")
133        >>> print(sheets)
134        ['Sheet1', 'Sheet2', 'Sheet3']
135        """
136        if filename not in self._excel_files:
137            filepath = self._directory / filename
138            if not filepath.exists():
139                raise FileNotFoundError(f"File not found: {filepath}")
140            self._excel_files[filename] = pd.ExcelFile(filepath)
141        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:
143    def read_sheets(
144        self,
145        sheet_names: str | list[str],
146        columns: list[str] | None = None,
147        col_datetime: str = "Date",
148        header: int = 0,
149        skiprows: int | list[int] | None = None,
150        start_date: str | None = None,
151        end_date: str | None = None,
152        include_end_date: bool = True,
153        sort_by_date: bool = True,
154    ) -> pd.DataFrame:
155        """
156        指定されたシートを読み込み、DataFrameとして返却します。
157        デフォルトでは2行目(単位の行)はスキップされます。
158        重複するカラム名がある場合は、より先に指定されたシートに存在するカラムの値を保持します。
159
160        Parameters
161        ----------
162            sheet_names: str | list[str]
163                読み込むシート名を指定します。文字列または文字列のリストを指定できます。
164            columns: list[str] | None, optional
165                残すカラム名のリストを指定します。Noneの場合は全てのカラムを保持します。
166            col_datetime: str, optional
167                日付と時刻の情報が含まれるカラム名を指定します。デフォルト値は'Date'です。
168            header: int, optional
169                データのヘッダー行を指定します。デフォルト値は0です。
170            skiprows: int | list[int] | None, optional
171                スキップする行数を指定します。Noneの場合のデフォルトでは2行目をスキップします。
172            start_date: str | None, optional
173                開始日を'yyyy-MM-dd'形式で指定します。この日付の'00:00:00'のデータが開始行となります。
174            end_date: str | None, optional
175                終了日を'yyyy-MM-dd'形式で指定します。この日付をデータに含めるかはinclude_end_dateフラグによって変わります。
176            include_end_date: bool, optional
177                終了日を含めるかどうかを指定します。デフォルト値はTrueです。
178            sort_by_date: bool, optional
179                ファイルの日付でソートするかどうかを指定します。デフォルト値はTrueです。
180
181        Returns
182        ----------
183            pd.DataFrame
184                読み込まれたデータを結合したDataFrameを返します。
185
186        Examples
187        --------
188        >>> converter = MonthlyConverter("path/to/excel/files")
189        >>> # 単一シートの読み込み
190        >>> df = converter.read_sheets("Sheet1")
191        >>> # 複数シートの読み込み
192        >>> df = converter.read_sheets(["Sheet1", "Sheet2"])
193        >>> # 特定の期間のデータ読み込み
194        >>> df = converter.read_sheets(
195        ...     "Sheet1",
196        ...     start_date="2023-01-01",
197        ...     end_date="2023-12-31"
198        ... )
199        """
200        if skiprows is None:
201            skiprows = [1]
202        if isinstance(sheet_names, str):
203            sheet_names = [sheet_names]
204
205        self._load_excel_files(start_date, end_date)
206
207        if not self._excel_files:
208            raise ValueError("No Excel files found matching the criteria")
209
210        # ファイルを日付順にソート
211        sorted_files = (
212            sorted(self._excel_files.items(), key=lambda x: self._extract_date(x[0]))
213            if sort_by_date
214            else self._excel_files.items()
215        )
216
217        # 各シートのデータを格納するリスト
218        sheet_dfs = {sheet_name: [] for sheet_name in sheet_names}
219
220        # 各ファイルからデータを読み込む
221        for filename, excel_file in sorted_files:
222            file_date = self._extract_date(filename)
223
224            for sheet_name in sheet_names:
225                if sheet_name in excel_file.sheet_names:
226                    df = pd.read_excel(
227                        excel_file,
228                        sheet_name=sheet_name,
229                        header=header,
230                        skiprows=skiprows,
231                        na_values=self._na_values,
232                    )
233                    # 年と月を追加
234                    df["year"] = file_date.year
235                    df["month"] = file_date.month
236                    sheet_dfs[sheet_name].append(df)
237
238        if not any(sheet_dfs.values()):
239            raise ValueError(f"No sheets found matching: {sheet_names}")
240
241        # 各シートのデータを結合
242        combined_sheets = {}
243        for sheet_name, dfs in sheet_dfs.items():
244            if dfs:  # シートにデータがある場合のみ結合
245                combined_sheets[sheet_name] = pd.concat(dfs, ignore_index=True)
246
247        # 最初のシートをベースにする
248        base_df = combined_sheets[sheet_names[0]]
249
250        # 2つ目以降のシートを結合
251        for sheet_name in sheet_names[1:]:
252            if sheet_name in combined_sheets:
253                base_df = self.merge_dataframes(
254                    base_df, combined_sheets[sheet_name], date_column=col_datetime
255                )
256
257        # 日付でフィルタリング
258        if start_date:
259            start_dt = pd.to_datetime(start_date)
260            base_df = base_df[base_df[col_datetime] >= start_dt]
261
262        if end_date:
263            end_dt = pd.to_datetime(end_date)
264            if include_end_date:
265                end_dt += pd.Timedelta(days=1)
266            base_df = base_df[base_df[col_datetime] < end_dt]
267
268        # カラムの選択
269        if columns is not None:
270            required_columns = [col_datetime, "year", "month"]
271            available_columns = base_df.columns.tolist()  # 利用可能なカラムを取得
272            if not all(col in available_columns for col in columns):
273                raise ValueError(
274                    f"指定されたカラムが見つかりません: {columns}. 利用可能なカラム: {available_columns}"
275                )
276            selected_columns = list(set(columns + required_columns))
277            base_df = base_df[selected_columns]
278
279        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:
287    @staticmethod
288    def get_last_day_of_month(year: int, month: int) -> int:
289        """
290        指定された年月の最終日を返します。
291
292        Parameters
293        ----------
294            year: int
295                年を指定します。
296            month: int
297                月を指定します。1から12の整数を指定してください。
298
299        Returns
300        ----------
301            int
302                指定された年月の最終日の日付。1から31の整数で返されます。
303
304        Examples
305        ----------
306        >>> MonthlyConverter.get_last_day_of_month(2023, 2)
307        28
308        >>> MonthlyConverter.get_last_day_of_month(2024, 2)  # 閏年の場合
309        29
310        """
311        next_month = (
312            datetime(year, month % 12 + 1, 1)
313            if month < 12
314            else datetime(year + 1, 1, 1)
315        )
316        last_day = (next_month - timedelta(days=1)).day
317        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:
319    @staticmethod
320    def extract_period_data(
321        df: pd.DataFrame,
322        start_date: str | pd.Timestamp,
323        end_date: str | pd.Timestamp,
324        include_end_date: bool = True,
325        datetime_column: str = "Date",
326    ) -> pd.DataFrame:
327        """
328        指定された期間のデータを抽出します。
329
330        Parameters
331        ----------
332            df: pd.DataFrame
333                入力データフレーム。
334            start_date: str | pd.Timestamp
335                開始日。YYYY-MM-DD形式の文字列またはTimestampで指定します。
336            end_date: str | pd.Timestamp
337                終了日。YYYY-MM-DD形式の文字列またはTimestampで指定します。
338            include_end_date: bool, optional
339                終了日を含めるかどうかを指定します。デフォルトはTrueです。
340            datetime_column: str, optional
341                日付を含む列の名前を指定します。デフォルトは"Date"です。
342
343        Returns
344        ----------
345            pd.DataFrame
346                指定された期間のデータのみを含むデータフレーム。
347
348        Examples
349        ----------
350        >>> df = pd.DataFrame({
351        ...     'Date': ['2023-01-01', '2023-01-02', '2023-01-03'],
352        ...     'Value': [1, 2, 3]
353        ... })
354        >>> MonthlyConverter.extract_period_data(
355        ...     df, 
356        ...     '2023-01-01', 
357        ...     '2023-01-02'
358        ... )
359           Date  Value
360        0  2023-01-01  1
361        1  2023-01-02  2
362        """
363        # データフレームのコピーを作成
364        df_internal = df.copy()
365        df_internal[datetime_column] = pd.to_datetime(df_internal[datetime_column])
366        start_dt = pd.to_datetime(start_date)
367        end_dt = pd.to_datetime(end_date)
368
369        # 開始日と終了日の順序チェック
370        if start_dt > end_dt:
371            raise ValueError("start_date は end_date より前である必要があります")
372
373        # 期間でフィルタリング
374        period_data = df_internal[
375            (df_internal[datetime_column] >= start_dt)
376            & (
377                df_internal[datetime_column]
378                < (end_dt + pd.Timedelta(days=1) if include_end_date else end_dt)
379            )
380        ]
381
382        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:
384    @staticmethod
385    def merge_dataframes(
386        df1: pd.DataFrame, df2: pd.DataFrame, date_column: str = "Date"
387    ) -> pd.DataFrame:
388        """
389        2つのDataFrameを結合します。重複するカラムは元の名前とサフィックス付きの両方を保持します。
390
391        Parameters
392        ----------
393            df1: pd.DataFrame
394                ベースとなるデータフレームを指定します。
395            df2: pd.DataFrame
396                結合するデータフレームを指定します。
397            date_column: str, optional
398                日付カラムの名前を指定します。デフォルトは"Date"です。
399
400        Returns
401        ----------
402            pd.DataFrame
403                結合されたデータフレームを返します。重複するカラムには_xと_yのサフィックスが付与されます。
404
405        Examples
406        ----------
407        >>> df1 = pd.DataFrame({
408        ...     'Date': ['2023-01-01', '2023-01-02'],
409        ...     'Value': [1, 2]
410        ... })
411        >>> df2 = pd.DataFrame({
412        ...     'Date': ['2023-01-01', '2023-01-02'],
413        ...     'Value': [10, 20]
414        ... })
415        >>> MonthlyConverter.merge_dataframes(df1, df2)
416               Date  Value  Value_x  Value_y
417        0  2023-01-01   1       1       10
418        1  2023-01-02   2       2       20
419        """
420        # インデックスをリセット
421        df1 = df1.reset_index(drop=True)
422        df2 = df2.reset_index(drop=True)
423
424        # 日付カラムを統一
425        df2[date_column] = df1[date_column]
426
427        # 重複しないカラムと重複するカラムを分離
428        duplicate_cols = [date_column, "year", "month"]  # 常に除外するカラム
429        overlapping_cols = [
430            col
431            for col in df2.columns
432            if col in df1.columns and col not in duplicate_cols
433        ]
434        unique_cols = [
435            col
436            for col in df2.columns
437            if col not in df1.columns and col not in duplicate_cols
438        ]
439
440        # 結果のDataFrameを作成
441        result = df1.copy()
442
443        # 重複しないカラムを追加
444        for col in unique_cols:
445            result[col] = df2[col]
446
447        # 重複するカラムを処理
448        for col in overlapping_cols:
449            # 元のカラムはdf1の値を保持(既に result に含まれている)
450            # _x サフィックスでdf1の値を追加
451            result[f"{col}_x"] = df1[col]
452            # _y サフィックスでdf2の値を追加
453            result[f"{col}_y"] = df2[col]
454
455        return result

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

Parameters

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

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

Parameters

flux_data: dict[str, pd.Series]
    各測器のフラックスデータを格納した辞書を指定します。キーは測器名、値はフラックスデータです。
month: int
    測定月を指定します。
output_dirpath: str | Path | None, optional
    出力ディレクトリを指定します。デフォルト値はNoneです。
output_filename: str, optional
    出力ファイル名を指定します。デフォルト値は"flux_distribution.png"です。
colors: dict[str, str] | None, optional
    各測器の色を指定する辞書を指定します。指定がない場合は自動で色を割り当てます。デフォルト値はNoneです。
xlim: tuple[float, float], optional
    x軸の範囲を指定します。デフォルト値は(-50, 200)です。
bandwidth: float, optional
    カーネル密度推定のバンド幅調整係数を指定します。デフォルト値は1.0です。
figsize: tuple[float, float], optional
    プロットのサイズを指定します。デフォルト値は(10, 6)です。
dpi: float | None, optional
    プロットの解像度を指定します。デフォルト値は350です。
save_fig: bool, optional
    プロットを保存するかどうかを指定します。デフォルト値はTrueです。
show_fig: bool, optional
    プロットを表示するかどうかを指定します。デフォルト値はTrueです。

Examples

>>> flux_data = {
...     '測器1': pd.Series([1, 2, 3, 4, 5]),
...     '測器2': pd.Series([2, 3, 4, 5, 6])
... }
>>> MonthlyFiguresGenerator.plot_fluxes_distributions(
...     flux_data=flux_data,
...     month=1,
...     output_dirpath='output'
... )
@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        psd_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        ...     psd_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    psd_ylabel: str
96    co_ylabel: str
97    color: str
98    label: str | None = None

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

Parameters

psd_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(
...     psd_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( psd_ylabel: str, co_ylabel: str, color: str, label: str | None = None)
psd_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']
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_paths: 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_paths: 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_paths: 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_paths:
109        for path in font_paths:
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_paths: 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)