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]
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
... )
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を補正する関数をクラス化したものです。
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
... )
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
... )
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分間データファイルから図を作成するクラス
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以上のレベルのメッセージが出力されます。
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"
... )
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)
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
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以上のレベルのメッセージが出力されます。
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_u
、edp_wind_v
、edp_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)
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}
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']
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ファイルを読み込み、前処理を行う
前処理の手順は以下の通りです:
- 不要な行を削除する。デフォルトの場合は、2行目をヘッダーとして残し、1、3、4行目が削除される。
- 数値データを float 型に変換する
- TIMESTAMP列をDateTimeインデックスに設定する
- エラー値をNaNに置き換える
- 指定されたサンプリングレートでリサンプリングする
- 欠損値(NaN)を前後の値から線形補間する
- 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"]
... )
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"
... )
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]
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`を参照)
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: データクラスの属性と値を含む辞書
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_a
とcoef_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)
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)に基づいたサブディレクトリへの分類も可能です。
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() # ファイルの再編成を実行
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() # ファイルの再編成を実行
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'
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)に基づいています。
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
... )
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
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')
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"
... )
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
... )
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
... )
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
... )
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
... )
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
... )
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]
... )
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
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
... )
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
ホットスポットの種類
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 )
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以上のレベルのメッセージが出力されます。
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
... )
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
... )
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)),
... ]
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)
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
車載濃度観測で得られた測定データを解析するクラス
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
... )
436 @property 437 def hotspot_params(self) -> HotspotParams: 438 """ホットスポット解析のパラメータ設定を取得""" 439 return self._hotspot_params
ホットスポット解析のパラメータ設定を取得
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]
分析対象のホットスポットリスト
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
... )
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}")
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
... )
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"
... )
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+'
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
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セクションのサイズ(度単位)
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)
... )
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'
... )
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)
... )
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)
... )
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"}
... )
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
... )
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
... )
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")
... )
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"
... )
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')。
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
... )
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ファイルをクローズする
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']
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']
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"
... )
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
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
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
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 で削除される予定です。
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)
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以上のレベルのメッセージが出力されます。
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
... )
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"
... )
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"
... )
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
... )
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
... )
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"
... )
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
... )
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'
... )
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'
... )
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"
... )
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'
... )
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"
... )
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
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'
... )
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
... )
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オブジェクト
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"
... )
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
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'。
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
... )
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
... )
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
... )
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)
伝達関数曲線のプロット設定を保持するデータクラス
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) の論文に基づいています。
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
... )
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}")
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"
... )
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
... )
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
... )
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"
... )
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"
... )
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
伝達関数の値。
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 - ログメッセージ
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)