py_flux_tracer
1from .campbell.eddy_data_figures_generator import ( 2 EddyDataFiguresGenerator, 3 SlopeLine, 4 SpectralPlotConfig, 5) 6from .campbell.eddy_data_preprocessor import EddyDataPreprocessor, MeasuredWindKeyType 7from .campbell.spectrum_calculator import SpectrumCalculator, WindowFunctionType 8from .commons.utilities import setup_logger, setup_plot_params 9from .footprint.flux_footprint_analyzer import FluxFootprintAnalyzer 10from .mobile.correcting_utils import ( 11 BiasRemovalConfig, 12 CorrectingUtils, 13 H2OCorrectionConfig, 14) 15from .mobile.hotspot_emission_analyzer import ( 16 EmissionData, 17 EmissionFormula, 18 HotspotEmissionAnalyzer, 19 HotspotEmissionConfig, 20) 21from .mobile.mobile_measurement_analyzer import ( 22 HotspotData, 23 HotspotParams, 24 HotspotType, 25 MobileMeasurementAnalyzer, 26 MobileMeasurementConfig, 27) 28from .monthly.monthly_converter import MonthlyConverter 29from .monthly.monthly_figures_generator import MonthlyFiguresGenerator 30from .transfer_function.fft_files_reorganizer import FftFileReorganizer 31from .transfer_function.transfer_function_calculator import ( 32 TfCurvesFromCsvConfig, 33 TransferFunctionCalculator, 34) 35 36""" 37versionを動的に設定する。 38`./_version.py`がない場合はsetuptools_scmを用いてGitからバージョン取得を試行 39それも失敗した場合にデフォルトバージョン(0.0.0)を設定 40""" 41try: 42 from ._version import __version__ # type:ignore 43except ImportError: 44 try: 45 from setuptools_scm import get_version 46 47 __version__ = get_version(root="..", relative_to=__file__) 48 except Exception: 49 __version__ = "0.0.0" 50 51# モジュールを __all__ にセット 52__all__ = [ 53 "BiasRemovalConfig", 54 "CorrectingUtils", 55 "EddyDataFiguresGenerator", 56 "EddyDataPreprocessor", 57 "EmissionData", 58 "EmissionFormula", 59 "FftFileReorganizer", 60 "FluxFootprintAnalyzer", 61 "H2OCorrectionConfig", 62 "HotspotData", 63 "HotspotEmissionAnalyzer", 64 "HotspotEmissionConfig", 65 "HotspotParams", 66 "HotspotType", 67 "MeasuredWindKeyType", 68 "MobileMeasurementAnalyzer", 69 "MobileMeasurementConfig", 70 "MonthlyConverter", 71 "MonthlyFiguresGenerator", 72 "SlopeLine", 73 "SpectralPlotConfig", 74 "SpectrumCalculator", 75 "TfCurvesFromCsvConfig", 76 "TransferFunctionCalculator", 77 "WindowFunctionType", 78 "__version__", 79 "setup_logger", 80 "setup_plot_params", 81]
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 = "spectrum", 151 scaling_co: str = "spectrum", 152 power_slope: SlopeLine | None = None, 153 co_slope: SlopeLine | None = None, 154 are_configs_resampled: bool = True, 155 save_fig: bool = True, 156 show_fig: bool = True, 157 plot_power: bool = True, 158 plot_co: bool = True, 159 add_tv_in_co: bool = True, 160 xlabel: str = "f (Hz)", 161 ) -> None: 162 """月間平均のスペクトルを計算してプロットする。 163 164 データファイルを指定されたディレクトリから読み込み、スペクトルを計算し、 165 結果を指定された出力ディレクトリにプロットして保存します。 166 167 Parameters 168 ---------- 169 input_dirpath: str | Path 170 データファイルが格納されているディレクトリ。 171 output_dirpath: str | Path 172 出力先ディレクトリ。 173 output_filename_power: str, optional 174 出力するパワースペクトルのファイル名。デフォルトは`"power_spectrum.png"`。 175 output_filename_co: str, optional 176 出力するコスペクトルのファイル名。デフォルトは`"co_spectrum.png"`。 177 col_ch4: str, optional 178 CH4の濃度データが入ったカラムのキー。デフォルトは`"Ultra_CH4_ppm_C"`。 179 col_c2h6: str, optional 180 C2H6の濃度データが入ったカラムのキー。デフォルトは`"Ultra_C2H6_ppb"`。 181 col_tv: str, optional 182 気温データが入ったカラムのキー。デフォルトは`"Tv"`。 183 ch4_config: SpectralPlotConfig | None, optional 184 CH4のプロット設定。Noneの場合はデフォルト設定を使用。 185 c2h6_config: SpectralPlotConfig | None, optional 186 C2H6のプロット設定。Noneの場合はデフォルト設定を使用。 187 tv_config: SpectralPlotConfig | None, optional 188 気温のプロット設定。Noneの場合はデフォルト設定を使用。 189 lag_second: float | None, optional 190 ラグ時間(秒)。デフォルトはNone。 191 file_pattern: str, optional 192 入力ファイルのパターン。デフォルトは`r"Eddy_(\\d+)"`。 193 file_suffix: str, optional 194 入力ファイルの拡張子。デフォルトは`".dat"`。 195 figsize: tuple[float, float], optional 196 プロットのサイズ。デフォルトは`(20, 6)`。 197 dpi: float | None, optional 198 プロットのdpi。デフォルトは`350`。 199 markersize: float, optional 200 プロットマーカーのサイズ。デフォルトは`14`。 201 xlim_power: tuple[float, float] | None, optional 202 パワースペクトルのx軸の範囲。デフォルトはNone。 203 ylim_power: tuple[float, float] | None, optional 204 パワースペクトルのy軸の範囲。デフォルトはNone。 205 xlim_co: tuple[float, float] | None, optional 206 コスペクトルのx軸の範囲。デフォルトは`(0.001, 10)`。 207 ylim_co: tuple[float, float] | None, optional 208 コスペクトルのy軸の範囲。デフォルトは`(0.0001, 10)`。 209 scaling_power: str, optional 210 パワースペクトルのスケーリング方法。`'spectrum'`または`'density'`などが指定可能。 211 signal.welchのパラメータに渡すものと同様の値を取ることが可能。デフォルトは`"spectrum"`。 212 scaling_co: str, optional 213 コスペクトルのスケーリング方法。`'spectrum'`または`'density'`などが指定可能。 214 signal.welchのパラメータに渡すものと同様の値を取ることが可能。デフォルトは`"spectrum"`。 215 power_slope: SlopeLine | None, optional 216 パワースペクトルの傾き線設定。Noneの場合はデフォルト設定を使用。 217 co_slope: SlopeLine | None, optional 218 コスペクトルの傾き線設定。Noneの場合はデフォルト設定を使用。 219 are_configs_resampled: bool, optional 220 入力データが再サンプリングされているかどうか。デフォルトは`True`。 221 save_fig: bool, optional 222 図を保存するかどうか。デフォルトは`True`。 223 show_fig: bool, optional 224 図を表示するかどうか。デフォルトは`True`。 225 plot_power: bool, optional 226 パワースペクトルをプロットするかどうか。デフォルトは`True`。 227 plot_co: bool, optional 228 コスペクトルをプロットするかどうか。デフォルトは`True`。 229 add_tv_in_co: bool, optional 230 顕熱フラックスのコスペクトルを表示するかどうか。デフォルトは`True`。 231 xlabel: str, optional 232 x軸のラベル。デフォルトは`"f (Hz)"`。 233 234 Examples 235 -------- 236 >>> edfg = EddyDataFiguresGenerator(fs=10) 237 >>> edfg.plot_c1c2_spectra( 238 ... input_dirpath="data/eddy", 239 ... output_dirpath="outputs", 240 ... output_filename_power="power.png", 241 ... output_filename_co="co.png" 242 ... ) 243 """ 244 # 出力ディレクトリの作成 245 if save_fig: 246 os.makedirs(output_dirpath, exist_ok=True) 247 248 # デフォルトのconfig設定 249 if ch4_config is None: 250 ch4_config = SpectralPlotConfig( 251 power_ylabel=r"$fS_{\mathrm{CH_4}} / s_{\mathrm{CH_4}}^2$", 252 co_ylabel=r"$fC_{w\mathrm{CH_4}} / \overline{w'\mathrm{CH_4}'}$", 253 color="red", 254 label="CH4", 255 ) 256 257 if c2h6_config is None: 258 c2h6_config = SpectralPlotConfig( 259 power_ylabel=r"$fS_{\mathrm{C_2H_6}} / s_{\mathrm{C_2H_6}}^2$", 260 co_ylabel=r"$fC_{w\mathrm{C_2H_6}} / \overline{w'\mathrm{C_2H_6}'}$", 261 color="orange", 262 label="C2H6", 263 ) 264 265 if tv_config is None: 266 tv_config = SpectralPlotConfig( 267 power_ylabel=r"$fS_{T_v} / s_{T_v}^2$", 268 co_ylabel=r"$fC_{wT_v} / \overline{w'T_v'}$", 269 color="blue", 270 label="Tv", 271 ) 272 273 # データの読み込みと結合 274 edp = EddyDataPreprocessor(fs=self._fs) 275 col_wind_w: str = EddyDataPreprocessor.WIND_W 276 277 # 各変数のパワースペクトルを格納する辞書 278 power_spectra = {col_ch4: [], col_c2h6: []} 279 co_spectra = {col_ch4: [], col_c2h6: [], col_tv: []} 280 freqs = None 281 282 # ファイルリストの取得 283 csv_files = edp._get_sorted_files(input_dirpath, file_pattern, file_suffix) 284 if not csv_files: 285 raise FileNotFoundError( 286 f"file_suffix:'{file_suffix}'に一致するファイルが見つかりませんでした。" 287 ) 288 289 for filename in tqdm(csv_files, desc="Processing files"): 290 df, _ = edp.get_resampled_df( 291 filepath=os.path.join(input_dirpath, filename), 292 resample=are_configs_resampled, 293 ) 294 295 # 風速成分の計算を追加 296 df = edp.add_uvw_columns(df) 297 298 # NaNや無限大を含む行を削除 299 df = df.replace([np.inf, -np.inf], np.nan).dropna( 300 subset=[col_ch4, col_c2h6, col_tv, col_wind_w] 301 ) 302 303 # データが十分な行数を持っているか確認 304 if len(df) < 100: 305 continue 306 307 # 各ファイルごとにスペクトル計算 308 calculator = SpectrumCalculator( 309 df=df, 310 fs=self._fs, 311 ) 312 313 for col in power_spectra.keys(): 314 # 各変数のパワースペクトルを計算して保存 315 if plot_power: 316 f, ps = calculator.calculate_power_spectrum( 317 col=col, 318 dimensionless=True, 319 frequency_weighted=True, 320 interpolate_points=True, 321 scaling=scaling_power, 322 ) 323 # 最初のファイル処理時にfreqsを初期化 324 if freqs is None: 325 freqs = f 326 power_spectra[col].append(ps) 327 # 以降は周波数配列の長さが一致する場合のみ追加 328 elif len(f) == len(freqs): 329 power_spectra[col].append(ps) 330 331 # コスペクトル 332 if plot_co: 333 _, cs, _ = calculator.calculate_co_spectrum( 334 col1=col_wind_w, 335 col2=col, 336 dimensionless=True, 337 frequency_weighted=True, 338 interpolate_points=True, 339 scaling=scaling_co, 340 apply_lag_correction_to_col2=True, 341 lag_second=lag_second, 342 ) 343 if freqs is not None and len(cs) == len(freqs): 344 co_spectra[col].append(cs) 345 346 # 顕熱フラックスのコスペクトル計算を追加 347 if plot_co and add_tv_in_co: 348 _, cs_heat, _ = calculator.calculate_co_spectrum( 349 col1=col_wind_w, 350 col2=col_tv, 351 dimensionless=True, 352 frequency_weighted=True, 353 interpolate_points=True, 354 scaling=scaling_co, 355 ) 356 if freqs is not None and len(cs_heat) == len(freqs): 357 co_spectra[col_tv].append(cs_heat) 358 359 # 各変数のスペクトルを平均化 360 if plot_power: 361 averaged_power_spectra = { 362 col: np.mean(spectra, axis=0) for col, spectra in power_spectra.items() 363 } 364 if plot_co: 365 averaged_co_spectra = { 366 col: np.mean(spectra, axis=0) for col, spectra in co_spectra.items() 367 } 368 # 顕熱フラックスの平均コスペクトル計算 369 if plot_co and add_tv_in_co and co_spectra[col_tv]: 370 averaged_heat_co_spectra = np.mean(co_spectra[col_tv], axis=0) 371 372 # パワースペクトルの図を作成 373 if plot_power: 374 fig_power, axes_power = plt.subplots(1, 2, figsize=figsize, sharex=True) 375 configs = [(col_ch4, ch4_config), (col_c2h6, c2h6_config)] 376 377 for ax, (col, config) in zip(axes_power, configs, strict=True): 378 ax.plot( 379 freqs, 380 averaged_power_spectra[col], 381 "o", 382 color=config.color, 383 markersize=markersize, 384 ) 385 ax.set_xscale("log") 386 ax.set_yscale("log") 387 if xlim_power: 388 ax.set_xlim(*xlim_power) 389 if ylim_power: 390 ax.set_ylim(*ylim_power) 391 392 # 傾き線とテキストの追加 393 if power_slope: 394 power_slope.plot(ax) 395 396 ax.set_ylabel(config.power_ylabel) 397 if config.label is not None: 398 ax.text(0.02, 0.98, config.label, transform=ax.transAxes, va="top") 399 ax.grid(True, alpha=0.3) 400 ax.set_xlabel(xlabel) 401 402 plt.tight_layout() 403 404 if save_fig: 405 output_filepath_power: str = os.path.join( 406 output_dirpath, output_filename_power 407 ) 408 plt.savefig( 409 output_filepath_power, 410 dpi=dpi, 411 bbox_inches="tight", 412 ) 413 if show_fig: 414 plt.show() 415 plt.close(fig=fig_power) 416 417 # コスペクトルの図を作成 418 if plot_co: 419 fig_co, axes_cosp = plt.subplots(1, 2, figsize=figsize, sharex=True) 420 configs = [(col_ch4, ch4_config), (col_c2h6, c2h6_config)] 421 422 for ax, (col, config) in zip(axes_cosp, configs, strict=True): 423 if add_tv_in_co: 424 ax.plot( 425 freqs, 426 averaged_heat_co_spectra, 427 "o", 428 color=tv_config.color, 429 alpha=0.3, 430 markersize=markersize, 431 label=tv_config.label, 432 ) 433 434 ax.plot( 435 freqs, 436 averaged_co_spectra[col], 437 "o", 438 color=config.color, 439 markersize=markersize, 440 label=config.label, 441 ) 442 443 ax.set_xscale("log") 444 ax.set_yscale("log") 445 if xlim_co: 446 ax.set_xlim(*xlim_co) 447 if ylim_co: 448 ax.set_ylim(*ylim_co) 449 450 # 傾き線とテキストの追加 451 if co_slope: 452 co_slope.plot(ax) 453 454 ax.set_ylabel(config.co_ylabel) 455 if config.label is not None: 456 ax.text(0.02, 0.98, config.label, transform=ax.transAxes, va="top") 457 ax.grid(True, alpha=0.3) 458 ax.set_xlabel(xlabel) 459 460 if add_tv_in_co and tv_config.label: 461 ax.legend(loc="lower left") 462 463 plt.tight_layout() 464 if save_fig: 465 output_filepath_csd: str = os.path.join( 466 output_dirpath, output_filename_co 467 ) 468 plt.savefig( 469 output_filepath_csd, 470 dpi=dpi, 471 bbox_inches="tight", 472 ) 473 if show_fig: 474 plt.show() 475 plt.close(fig=fig_co) 476 477 def plot_turbulence( 478 self, 479 df: pd.DataFrame, 480 output_dirpath: str | Path | None = None, 481 output_filename: str = "turbulence.png", 482 col_uz: str = "Uz", 483 col_ch4: str = "Ultra_CH4_ppm_C", 484 col_c2h6: str = "Ultra_C2H6_ppb", 485 col_timestamp: str = "TIMESTAMP", 486 add_serial_labels: bool = True, 487 figsize: tuple[float, float] = (12, 10), 488 dpi: float | None = 350, 489 save_fig: bool = True, 490 show_fig: bool = True, 491 ) -> None: 492 """時系列データのプロットを作成する 493 494 Parameters 495 ------ 496 df: pd.DataFrame 497 プロットするデータを含むDataFrame 498 output_dirpath: str | Path | None, optional 499 出力ディレクトリのパス。デフォルトはNone。 500 output_filename: str, optional 501 出力ファイル名。デフォルトは"turbulence.png"。 502 col_uz: str, optional 503 鉛直風速データのカラム名。デフォルトは"Uz"。 504 col_ch4: str, optional 505 メタンデータのカラム名。デフォルトは"Ultra_CH4_ppm_C"。 506 col_c2h6: str, optional 507 エタンデータのカラム名。デフォルトは"Ultra_C2H6_ppb"。 508 col_timestamp: str, optional 509 タイムスタンプのカラム名。デフォルトは"TIMESTAMP"。 510 add_serial_labels: bool, optional 511 シリアルラベルを追加するかどうかのフラグ。デフォルトはTrue。 512 figsize: tuple[float, float], optional 513 プロットのサイズ。デフォルトは(12, 10)。 514 dpi: float | None, optional 515 プロットのdpi。デフォルトは350。 516 save_fig: bool, optional 517 プロットを保存するかどうか。デフォルトはTrue。 518 show_fig: bool, optional 519 プロットを表示するかどうか。デフォルトはTrue。 520 521 Examples 522 -------- 523 >>> edfg = EddyDataFiguresGenerator(fs=10) 524 >>> edfg.plot_turbulence(df=data_frame) 525 """ 526 # データの前処理 527 df_internal = df.copy() 528 df_internal.index = pd.to_datetime(df_internal.index) 529 530 # タイムスタンプをインデックスに設定(まだ設定されていない場合) 531 if not isinstance(df_internal.index, pd.DatetimeIndex): 532 df_internal[col_timestamp] = pd.to_datetime(df_internal[col_timestamp]) 533 df_internal.set_index(col_timestamp, inplace=True) 534 535 # 開始時刻と終了時刻を取得 536 start_time = df_internal.index[0] 537 end_time = df_internal.index[-1] 538 539 # 開始時刻の分を取得 540 start_minute = start_time.minute 541 542 # 時間軸の作成(実際の開始時刻からの経過分数) 543 minutes_elapsed = (df_internal.index - start_time).total_seconds() / 60 544 545 # プロットの作成 546 fig, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=figsize, sharex=True) 547 548 # 鉛直風速 549 ax1.plot(minutes_elapsed, df_internal[col_uz], "k-", linewidth=0.5) 550 ax1.set_ylabel(r"$w$ (m s$^{-1}$)") 551 if add_serial_labels: 552 ax1.text(0.02, 0.98, "(a)", transform=ax1.transAxes, va="top") 553 ax1.grid(True, alpha=0.3) 554 555 # CH4濃度 556 ax2.plot(minutes_elapsed, df_internal[col_ch4], "r-", linewidth=0.5) 557 ax2.set_ylabel(r"$\mathrm{CH_4}$ (ppm)") 558 if add_serial_labels: 559 ax2.text(0.02, 0.98, "(b)", transform=ax2.transAxes, va="top") 560 ax2.grid(True, alpha=0.3) 561 562 # C2H6濃度 563 ax3.plot(minutes_elapsed, df_internal[col_c2h6], "orange", linewidth=0.5) 564 ax3.set_ylabel(r"$\mathrm{C_2H_6}$ (ppb)") 565 if add_serial_labels: 566 ax3.text(0.02, 0.98, "(c)", transform=ax3.transAxes, va="top") 567 ax3.grid(True, alpha=0.3) 568 ax3.set_xlabel("Time (minutes)") 569 570 # x軸の範囲を実際の開始時刻から30分後までに設定 571 total_minutes = (end_time - start_time).total_seconds() / 60 572 ax3.set_xlim(0, min(30, total_minutes)) 573 574 # x軸の目盛りを5分間隔で設定 575 np.arange(start_minute, start_minute + 35, 5) 576 ax3.xaxis.set_major_locator(MultipleLocator(5)) 577 578 # レイアウトの調整 579 plt.tight_layout() 580 581 # グラフの保存または表示 582 if save_fig: 583 if output_dirpath is None: 584 raise ValueError( 585 "save_fig = True の場合、 output_dirpath を指定する必要があります。有効なディレクトリパスを指定してください。" 586 ) 587 os.makedirs(output_dirpath, exist_ok=True) 588 output_filepath: str = os.path.join(output_dirpath, output_filename) 589 plt.savefig(output_filepath, bbox_inches="tight") 590 if show_fig: 591 plt.show() 592 plt.close(fig=fig)
データロガーの30分間データファイルから図を作成するクラス
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 = "spectrum", 151 scaling_co: str = "spectrum", 152 power_slope: SlopeLine | None = None, 153 co_slope: SlopeLine | None = None, 154 are_configs_resampled: bool = True, 155 save_fig: bool = True, 156 show_fig: bool = True, 157 plot_power: bool = True, 158 plot_co: bool = True, 159 add_tv_in_co: bool = True, 160 xlabel: str = "f (Hz)", 161 ) -> None: 162 """月間平均のスペクトルを計算してプロットする。 163 164 データファイルを指定されたディレクトリから読み込み、スペクトルを計算し、 165 結果を指定された出力ディレクトリにプロットして保存します。 166 167 Parameters 168 ---------- 169 input_dirpath: str | Path 170 データファイルが格納されているディレクトリ。 171 output_dirpath: str | Path 172 出力先ディレクトリ。 173 output_filename_power: str, optional 174 出力するパワースペクトルのファイル名。デフォルトは`"power_spectrum.png"`。 175 output_filename_co: str, optional 176 出力するコスペクトルのファイル名。デフォルトは`"co_spectrum.png"`。 177 col_ch4: str, optional 178 CH4の濃度データが入ったカラムのキー。デフォルトは`"Ultra_CH4_ppm_C"`。 179 col_c2h6: str, optional 180 C2H6の濃度データが入ったカラムのキー。デフォルトは`"Ultra_C2H6_ppb"`。 181 col_tv: str, optional 182 気温データが入ったカラムのキー。デフォルトは`"Tv"`。 183 ch4_config: SpectralPlotConfig | None, optional 184 CH4のプロット設定。Noneの場合はデフォルト設定を使用。 185 c2h6_config: SpectralPlotConfig | None, optional 186 C2H6のプロット設定。Noneの場合はデフォルト設定を使用。 187 tv_config: SpectralPlotConfig | None, optional 188 気温のプロット設定。Noneの場合はデフォルト設定を使用。 189 lag_second: float | None, optional 190 ラグ時間(秒)。デフォルトはNone。 191 file_pattern: str, optional 192 入力ファイルのパターン。デフォルトは`r"Eddy_(\\d+)"`。 193 file_suffix: str, optional 194 入力ファイルの拡張子。デフォルトは`".dat"`。 195 figsize: tuple[float, float], optional 196 プロットのサイズ。デフォルトは`(20, 6)`。 197 dpi: float | None, optional 198 プロットのdpi。デフォルトは`350`。 199 markersize: float, optional 200 プロットマーカーのサイズ。デフォルトは`14`。 201 xlim_power: tuple[float, float] | None, optional 202 パワースペクトルのx軸の範囲。デフォルトはNone。 203 ylim_power: tuple[float, float] | None, optional 204 パワースペクトルのy軸の範囲。デフォルトはNone。 205 xlim_co: tuple[float, float] | None, optional 206 コスペクトルのx軸の範囲。デフォルトは`(0.001, 10)`。 207 ylim_co: tuple[float, float] | None, optional 208 コスペクトルのy軸の範囲。デフォルトは`(0.0001, 10)`。 209 scaling_power: str, optional 210 パワースペクトルのスケーリング方法。`'spectrum'`または`'density'`などが指定可能。 211 signal.welchのパラメータに渡すものと同様の値を取ることが可能。デフォルトは`"spectrum"`。 212 scaling_co: str, optional 213 コスペクトルのスケーリング方法。`'spectrum'`または`'density'`などが指定可能。 214 signal.welchのパラメータに渡すものと同様の値を取ることが可能。デフォルトは`"spectrum"`。 215 power_slope: SlopeLine | None, optional 216 パワースペクトルの傾き線設定。Noneの場合はデフォルト設定を使用。 217 co_slope: SlopeLine | None, optional 218 コスペクトルの傾き線設定。Noneの場合はデフォルト設定を使用。 219 are_configs_resampled: bool, optional 220 入力データが再サンプリングされているかどうか。デフォルトは`True`。 221 save_fig: bool, optional 222 図を保存するかどうか。デフォルトは`True`。 223 show_fig: bool, optional 224 図を表示するかどうか。デフォルトは`True`。 225 plot_power: bool, optional 226 パワースペクトルをプロットするかどうか。デフォルトは`True`。 227 plot_co: bool, optional 228 コスペクトルをプロットするかどうか。デフォルトは`True`。 229 add_tv_in_co: bool, optional 230 顕熱フラックスのコスペクトルを表示するかどうか。デフォルトは`True`。 231 xlabel: str, optional 232 x軸のラベル。デフォルトは`"f (Hz)"`。 233 234 Examples 235 -------- 236 >>> edfg = EddyDataFiguresGenerator(fs=10) 237 >>> edfg.plot_c1c2_spectra( 238 ... input_dirpath="data/eddy", 239 ... output_dirpath="outputs", 240 ... output_filename_power="power.png", 241 ... output_filename_co="co.png" 242 ... ) 243 """ 244 # 出力ディレクトリの作成 245 if save_fig: 246 os.makedirs(output_dirpath, exist_ok=True) 247 248 # デフォルトのconfig設定 249 if ch4_config is None: 250 ch4_config = SpectralPlotConfig( 251 power_ylabel=r"$fS_{\mathrm{CH_4}} / s_{\mathrm{CH_4}}^2$", 252 co_ylabel=r"$fC_{w\mathrm{CH_4}} / \overline{w'\mathrm{CH_4}'}$", 253 color="red", 254 label="CH4", 255 ) 256 257 if c2h6_config is None: 258 c2h6_config = SpectralPlotConfig( 259 power_ylabel=r"$fS_{\mathrm{C_2H_6}} / s_{\mathrm{C_2H_6}}^2$", 260 co_ylabel=r"$fC_{w\mathrm{C_2H_6}} / \overline{w'\mathrm{C_2H_6}'}$", 261 color="orange", 262 label="C2H6", 263 ) 264 265 if tv_config is None: 266 tv_config = SpectralPlotConfig( 267 power_ylabel=r"$fS_{T_v} / s_{T_v}^2$", 268 co_ylabel=r"$fC_{wT_v} / \overline{w'T_v'}$", 269 color="blue", 270 label="Tv", 271 ) 272 273 # データの読み込みと結合 274 edp = EddyDataPreprocessor(fs=self._fs) 275 col_wind_w: str = EddyDataPreprocessor.WIND_W 276 277 # 各変数のパワースペクトルを格納する辞書 278 power_spectra = {col_ch4: [], col_c2h6: []} 279 co_spectra = {col_ch4: [], col_c2h6: [], col_tv: []} 280 freqs = None 281 282 # ファイルリストの取得 283 csv_files = edp._get_sorted_files(input_dirpath, file_pattern, file_suffix) 284 if not csv_files: 285 raise FileNotFoundError( 286 f"file_suffix:'{file_suffix}'に一致するファイルが見つかりませんでした。" 287 ) 288 289 for filename in tqdm(csv_files, desc="Processing files"): 290 df, _ = edp.get_resampled_df( 291 filepath=os.path.join(input_dirpath, filename), 292 resample=are_configs_resampled, 293 ) 294 295 # 風速成分の計算を追加 296 df = edp.add_uvw_columns(df) 297 298 # NaNや無限大を含む行を削除 299 df = df.replace([np.inf, -np.inf], np.nan).dropna( 300 subset=[col_ch4, col_c2h6, col_tv, col_wind_w] 301 ) 302 303 # データが十分な行数を持っているか確認 304 if len(df) < 100: 305 continue 306 307 # 各ファイルごとにスペクトル計算 308 calculator = SpectrumCalculator( 309 df=df, 310 fs=self._fs, 311 ) 312 313 for col in power_spectra.keys(): 314 # 各変数のパワースペクトルを計算して保存 315 if plot_power: 316 f, ps = calculator.calculate_power_spectrum( 317 col=col, 318 dimensionless=True, 319 frequency_weighted=True, 320 interpolate_points=True, 321 scaling=scaling_power, 322 ) 323 # 最初のファイル処理時にfreqsを初期化 324 if freqs is None: 325 freqs = f 326 power_spectra[col].append(ps) 327 # 以降は周波数配列の長さが一致する場合のみ追加 328 elif len(f) == len(freqs): 329 power_spectra[col].append(ps) 330 331 # コスペクトル 332 if plot_co: 333 _, cs, _ = calculator.calculate_co_spectrum( 334 col1=col_wind_w, 335 col2=col, 336 dimensionless=True, 337 frequency_weighted=True, 338 interpolate_points=True, 339 scaling=scaling_co, 340 apply_lag_correction_to_col2=True, 341 lag_second=lag_second, 342 ) 343 if freqs is not None and len(cs) == len(freqs): 344 co_spectra[col].append(cs) 345 346 # 顕熱フラックスのコスペクトル計算を追加 347 if plot_co and add_tv_in_co: 348 _, cs_heat, _ = calculator.calculate_co_spectrum( 349 col1=col_wind_w, 350 col2=col_tv, 351 dimensionless=True, 352 frequency_weighted=True, 353 interpolate_points=True, 354 scaling=scaling_co, 355 ) 356 if freqs is not None and len(cs_heat) == len(freqs): 357 co_spectra[col_tv].append(cs_heat) 358 359 # 各変数のスペクトルを平均化 360 if plot_power: 361 averaged_power_spectra = { 362 col: np.mean(spectra, axis=0) for col, spectra in power_spectra.items() 363 } 364 if plot_co: 365 averaged_co_spectra = { 366 col: np.mean(spectra, axis=0) for col, spectra in co_spectra.items() 367 } 368 # 顕熱フラックスの平均コスペクトル計算 369 if plot_co and add_tv_in_co and co_spectra[col_tv]: 370 averaged_heat_co_spectra = np.mean(co_spectra[col_tv], axis=0) 371 372 # パワースペクトルの図を作成 373 if plot_power: 374 fig_power, axes_power = plt.subplots(1, 2, figsize=figsize, sharex=True) 375 configs = [(col_ch4, ch4_config), (col_c2h6, c2h6_config)] 376 377 for ax, (col, config) in zip(axes_power, configs, strict=True): 378 ax.plot( 379 freqs, 380 averaged_power_spectra[col], 381 "o", 382 color=config.color, 383 markersize=markersize, 384 ) 385 ax.set_xscale("log") 386 ax.set_yscale("log") 387 if xlim_power: 388 ax.set_xlim(*xlim_power) 389 if ylim_power: 390 ax.set_ylim(*ylim_power) 391 392 # 傾き線とテキストの追加 393 if power_slope: 394 power_slope.plot(ax) 395 396 ax.set_ylabel(config.power_ylabel) 397 if config.label is not None: 398 ax.text(0.02, 0.98, config.label, transform=ax.transAxes, va="top") 399 ax.grid(True, alpha=0.3) 400 ax.set_xlabel(xlabel) 401 402 plt.tight_layout() 403 404 if save_fig: 405 output_filepath_power: str = os.path.join( 406 output_dirpath, output_filename_power 407 ) 408 plt.savefig( 409 output_filepath_power, 410 dpi=dpi, 411 bbox_inches="tight", 412 ) 413 if show_fig: 414 plt.show() 415 plt.close(fig=fig_power) 416 417 # コスペクトルの図を作成 418 if plot_co: 419 fig_co, axes_cosp = plt.subplots(1, 2, figsize=figsize, sharex=True) 420 configs = [(col_ch4, ch4_config), (col_c2h6, c2h6_config)] 421 422 for ax, (col, config) in zip(axes_cosp, configs, strict=True): 423 if add_tv_in_co: 424 ax.plot( 425 freqs, 426 averaged_heat_co_spectra, 427 "o", 428 color=tv_config.color, 429 alpha=0.3, 430 markersize=markersize, 431 label=tv_config.label, 432 ) 433 434 ax.plot( 435 freqs, 436 averaged_co_spectra[col], 437 "o", 438 color=config.color, 439 markersize=markersize, 440 label=config.label, 441 ) 442 443 ax.set_xscale("log") 444 ax.set_yscale("log") 445 if xlim_co: 446 ax.set_xlim(*xlim_co) 447 if ylim_co: 448 ax.set_ylim(*ylim_co) 449 450 # 傾き線とテキストの追加 451 if co_slope: 452 co_slope.plot(ax) 453 454 ax.set_ylabel(config.co_ylabel) 455 if config.label is not None: 456 ax.text(0.02, 0.98, config.label, transform=ax.transAxes, va="top") 457 ax.grid(True, alpha=0.3) 458 ax.set_xlabel(xlabel) 459 460 if add_tv_in_co and tv_config.label: 461 ax.legend(loc="lower left") 462 463 plt.tight_layout() 464 if save_fig: 465 output_filepath_csd: str = os.path.join( 466 output_dirpath, output_filename_co 467 ) 468 plt.savefig( 469 output_filepath_csd, 470 dpi=dpi, 471 bbox_inches="tight", 472 ) 473 if show_fig: 474 plt.show() 475 plt.close(fig=fig_co)
月間平均のスペクトルを計算してプロットする。
データファイルを指定されたディレクトリから読み込み、スペクトルを計算し、 結果を指定された出力ディレクトリにプロットして保存します。
Parameters
input_dirpath: str | Path
データファイルが格納されているディレクトリ。
output_dirpath: str | Path
出力先ディレクトリ。
output_filename_power: str, optional
出力するパワースペクトルのファイル名。デフォルトは`"power_spectrum.png"`。
output_filename_co: str, optional
出力するコスペクトルのファイル名。デフォルトは`"co_spectrum.png"`。
col_ch4: str, optional
CH4の濃度データが入ったカラムのキー。デフォルトは`"Ultra_CH4_ppm_C"`。
col_c2h6: str, optional
C2H6の濃度データが入ったカラムのキー。デフォルトは`"Ultra_C2H6_ppb"`。
col_tv: str, optional
気温データが入ったカラムのキー。デフォルトは`"Tv"`。
ch4_config: SpectralPlotConfig | None, optional
CH4のプロット設定。Noneの場合はデフォルト設定を使用。
c2h6_config: SpectralPlotConfig | None, optional
C2H6のプロット設定。Noneの場合はデフォルト設定を使用。
tv_config: SpectralPlotConfig | None, optional
気温のプロット設定。Noneの場合はデフォルト設定を使用。
lag_second: float | None, optional
ラグ時間(秒)。デフォルトはNone。
file_pattern: str, optional
入力ファイルのパターン。デフォルトは`r"Eddy_(\d+)"`。
file_suffix: str, optional
入力ファイルの拡張子。デフォルトは`".dat"`。
figsize: tuple[float, float], optional
プロットのサイズ。デフォルトは`(20, 6)`。
dpi: float | None, optional
プロットのdpi。デフォルトは`350`。
markersize: float, optional
プロットマーカーのサイズ。デフォルトは`14`。
xlim_power: tuple[float, float] | None, optional
パワースペクトルのx軸の範囲。デフォルトはNone。
ylim_power: tuple[float, float] | None, optional
パワースペクトルのy軸の範囲。デフォルトはNone。
xlim_co: tuple[float, float] | None, optional
コスペクトルのx軸の範囲。デフォルトは`(0.001, 10)`。
ylim_co: tuple[float, float] | None, optional
コスペクトルのy軸の範囲。デフォルトは`(0.0001, 10)`。
scaling_power: str, optional
パワースペクトルのスケーリング方法。`'spectrum'`または`'density'`などが指定可能。
signal.welchのパラメータに渡すものと同様の値を取ることが可能。デフォルトは`"spectrum"`。
scaling_co: str, optional
コスペクトルのスケーリング方法。`'spectrum'`または`'density'`などが指定可能。
signal.welchのパラメータに渡すものと同様の値を取ることが可能。デフォルトは`"spectrum"`。
power_slope: SlopeLine | None, optional
パワースペクトルの傾き線設定。Noneの場合はデフォルト設定を使用。
co_slope: SlopeLine | None, optional
コスペクトルの傾き線設定。Noneの場合はデフォルト設定を使用。
are_configs_resampled: bool, optional
入力データが再サンプリングされているかどうか。デフォルトは`True`。
save_fig: bool, optional
図を保存するかどうか。デフォルトは`True`。
show_fig: bool, optional
図を表示するかどうか。デフォルトは`True`。
plot_power: bool, optional
パワースペクトルをプロットするかどうか。デフォルトは`True`。
plot_co: bool, optional
コスペクトルをプロットするかどうか。デフォルトは`True`。
add_tv_in_co: bool, optional
顕熱フラックスのコスペクトルを表示するかどうか。デフォルトは`True`。
xlabel: str, optional
x軸のラベル。デフォルトは`"f (Hz)"`。
Examples
>>> edfg = EddyDataFiguresGenerator(fs=10)
>>> edfg.plot_c1c2_spectra(
... input_dirpath="data/eddy",
... output_dirpath="outputs",
... output_filename_power="power.png",
... output_filename_co="co.png"
... )
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 ```python 153 emission_per_min = np.exp((np.log(spot.delta_ch4) + coef_a) / coef_b) 154 ``` 155 156 Parameters 157 ---------- 158 name: str 159 計算式の名前(例: "weller", "weitzel", "joo", "umezawa"など) 160 coef_a: float 161 計算式の係数a 162 coef_b: float 163 計算式の係数b 164 165 Examples 166 ---------- 167 >>> # Weller et al. (2022)の係数を使用する場合 168 >>> formula = EmissionFormula(name="weller", coef_a=0.988, coef_b=0.817) 169 >>> 170 >>> # Weitzel et al. (2019)の係数を使用する場合 171 >>> formula = EmissionFormula(name="weitzel", coef_a=0.521, coef_b=0.795) 172 >>> 173 >>> # カスタム係数を使用する場合 174 >>> formula = EmissionFormula(name="custom", coef_a=1.0, coef_b=1.0) 175 """ 176 177 name: str 178 coef_a: float 179 coef_b: float 180 181 def __post_init__(self) -> None: 182 """ 183 パラメータの検証を行います。 184 """ 185 if not isinstance(self.name, str) or not self.name.strip(): 186 raise ValueError("'name' must be a non-empty string") 187 if not isinstance(self.coef_a, (int | float)): 188 raise ValueError("'coef_a' must be a number") 189 if not isinstance(self.coef_b, (int | float)): 190 raise ValueError("'coef_b' must be a number")
排出量計算式の係数セットを保持するデータクラス
設定したcoef_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
ホットスポットの種類
256class HotspotEmissionAnalyzer: 257 def __init__( 258 self, 259 logger: Logger | None = None, 260 logging_debug: bool = False, 261 ): 262 """ 263 渦相関法によって記録されたデータファイルを処理するクラス。 264 265 Parameters 266 ---------- 267 fs: float 268 サンプリング周波数。 269 logger: Logger | None, optional 270 使用するロガー。Noneの場合は新しいロガーを作成します。 271 logging_debug: bool, optional 272 ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。 273 """ 274 # ロガー 275 log_level: int = INFO 276 if logging_debug: 277 log_level = DEBUG 278 self.logger: Logger = setup_logger(logger=logger, log_level=log_level) 279 280 def calculate_emission_rates( 281 self, 282 hotspots: list[HotspotData], 283 config: HotspotEmissionConfig, 284 print_summary: bool = False, 285 ) -> list[EmissionData]: 286 """ 287 検出されたホットスポットのCH4漏出量を計算・解析し、統計情報を生成します。 288 289 Parameters 290 ---------- 291 hotspots: list[HotspotData] 292 分析対象のホットスポットのリスト 293 config: HotspotEmissionConfig 294 排出量計算の設定 295 print_summary: bool 296 統計情報を表示するかどうか。デフォルトはFalse。 297 298 Returns 299 ---------- 300 list[EmissionData] 301 - 各ホットスポットの排出量データを含むリスト 302 303 Examples 304 ---------- 305 >>> # Weller et al. (2022)の係数を使用する例 306 >>> config = HotspotEmissionConfig( 307 ... formula=EmissionFormula(name="weller", coef_a=0.988, coef_b=0.817), 308 ... emission_categories={ 309 ... "low": {"min": 0, "max": 6}, 310 ... "medium": {"min": 6, "max": 40}, 311 ... "high": {"min": 40, "max": float("inf")}, 312 ... } 313 ... ) 314 >>> emissions_list = HotspotEmissionAnalyzer.calculate_emission_rates( 315 ... hotspots=hotspots, 316 ... config=config, 317 ... print_summary=True 318 ... ) 319 """ 320 # 係数の取得 321 coef_a: float = config.formula.coef_a 322 coef_b: float = config.formula.coef_b 323 324 # 排出量の計算 325 emission_data_list = [] 326 for spot in hotspots: 327 # 漏出量の計算 (L/min) 328 emission_per_min = np.exp((np.log(spot.delta_ch4) + coef_a) / coef_b) 329 # 日排出量 (L/day) 330 emission_per_day = emission_per_min * 60 * 24 331 # 年間排出量 (L/year) 332 emission_per_year = emission_per_day * 365 333 334 emission_data = EmissionData( 335 timestamp=spot.timestamp, 336 delta_ch4=spot.delta_ch4, 337 delta_c2h6=spot.delta_c2h6, 338 delta_ratio=spot.delta_ratio, 339 emission_per_min=emission_per_min, 340 emission_per_day=emission_per_day, 341 emission_per_year=emission_per_year, 342 avg_lat=spot.avg_lat, 343 avg_lon=spot.avg_lon, 344 section=spot.section, 345 type=spot.type, 346 ) 347 emission_data_list.append(emission_data) 348 349 # 統計計算用にDataFrameを作成 350 emission_df = pd.DataFrame([e.to_dict() for e in emission_data_list]) 351 352 # タイプ別の統計情報を計算 353 # get_args(HotspotType)を使用して型安全なリストを作成 354 types = list(get_args(HotspotType)) 355 for spot_type in types: 356 df_type = emission_df[emission_df["type"] == spot_type] 357 if len(df_type) > 0: 358 # 既存の統計情報を計算 359 type_stats = { 360 "count": len(df_type), 361 "emission_per_min_min": df_type["emission_per_min"].min(), 362 "emission_per_min_max": df_type["emission_per_min"].max(), 363 "emission_per_min_mean": df_type["emission_per_min"].mean(), 364 "emission_per_min_median": df_type["emission_per_min"].median(), 365 "total_annual_emission": df_type["emission_per_year"].sum(), 366 "mean_annual_emission": df_type["emission_per_year"].mean(), 367 } 368 369 # 排出量カテゴリー別の統計を追加 370 category_counts = {} 371 for category, limits in config.emission_categories.items(): 372 mask = (df_type["emission_per_min"] >= limits["min"]) & ( 373 df_type["emission_per_min"] < limits["max"] 374 ) 375 category_counts[category] = len(df_type[mask]) 376 type_stats["emission_categories"] = category_counts 377 378 if print_summary: 379 self.logger.info(f"{spot_type}タイプの統計情報:") 380 print(f" 検出数: {type_stats['count']}") 381 print(" 排出量 (L/min):") 382 print(f" 最小値: {type_stats['emission_per_min_min']:.2f}") 383 print(f" 最大値: {type_stats['emission_per_min_max']:.2f}") 384 print(f" 平均値: {type_stats['emission_per_min_mean']:.2f}") 385 print(f" 中央値: {type_stats['emission_per_min_median']:.2f}") 386 print(" 排出量カテゴリー別の検出数:") 387 for category, count in category_counts.items(): 388 print(f" {category}: {count}") 389 print(" 年間排出量 (L/year):") 390 print(f" 合計: {type_stats['total_annual_emission']:.2f}") 391 print(f" 平均: {type_stats['mean_annual_emission']:.2f}") 392 393 return emission_data_list 394 395 def plot_emission_analysis( 396 self, 397 emissions: list[EmissionData], 398 output_dirpath: str | Path | None = None, 399 output_filename: str = "emission_analysis.png", 400 figsize: tuple[float, float] = (12, 5), 401 dpi: float | None = 350, 402 hotspot_colors: dict[HotspotType, str] | None = None, 403 add_legend: bool = True, 404 hist_log_y: bool = False, 405 hist_xlim: tuple[float, float] | None = None, 406 hist_ylim: tuple[float, float] | None = None, 407 scatter_xlim: tuple[float, float] | None = None, 408 scatter_ylim: tuple[float, float] | None = None, 409 hist_bin_width: float = 0.5, 410 print_summary: bool = False, 411 stack_bars: bool = True, 412 save_fig: bool = False, 413 show_fig: bool = True, 414 show_scatter: bool = True, 415 ) -> None: 416 """ 417 排出量分析のプロットを作成する静的メソッド。 418 419 Parameters 420 ---------- 421 emissions: list[EmissionData] 422 calculate_emission_ratesで生成された分析結果 423 output_dirpath: str | Path | None, optional 424 出力先ディレクトリのパス。指定しない場合はカレントディレクトリに保存。 425 output_filename: str, optional 426 保存するファイル名。デフォルトは"emission_analysis.png"。 427 figsize: tuple[float, float], optional 428 プロットのサイズ。デフォルトは(12, 5)。 429 dpi: float | None, optional 430 プロットの解像度。デフォルトは350。 431 hotspot_colors: dict[HotspotType, str] | None, optional 432 ホットスポットの色を定義する辞書。指定しない場合はデフォルトの色を使用。 433 add_legend: bool, optional 434 凡例を追加するかどうか。デフォルトはTrue。 435 hist_log_y: bool, optional 436 ヒストグラムのy軸を対数スケールにするかどうか。デフォルトはFalse。 437 hist_xlim: tuple[float, float] | None, optional 438 ヒストグラムのx軸の範囲。指定しない場合は自動で設定。 439 hist_ylim: tuple[float, float] | None, optional 440 ヒストグラムのy軸の範囲。指定しない場合は自動で設定。 441 scatter_xlim: tuple[float, float] | None, optional 442 散布図のx軸の範囲。指定しない場合は自動で設定。 443 scatter_ylim: tuple[float, float] | None, optional 444 散布図のy軸の範囲。指定しない場合は自動で設定。 445 hist_bin_width: float, optional 446 ヒストグラムのビンの幅。デフォルトは0.5。 447 print_summary: bool, optional 448 集計結果を表示するかどうか。デフォルトはFalse。 449 stack_bars: bool, optional 450 ヒストグラムを積み上げ方式で表示するかどうか。デフォルトはTrue。 451 save_fig: bool, optional 452 図をファイルに保存するかどうか。デフォルトはFalse。 453 show_fig: bool, optional 454 図を表示するかどうか。デフォルトはTrue。 455 show_scatter: bool, optional 456 散布図(右図)を表示するかどうか。デフォルトはTrue。 457 458 Returns 459 ------- 460 None 461 462 Examples 463 -------- 464 >>> analyzer = HotspotEmissionAnalyzer() 465 >>> emissions = analyzer.calculate_emission_rates(hotspots) 466 >>> analyzer.plot_emission_analysis( 467 ... emissions, 468 ... output_dirpath="results", 469 ... save_fig=True, 470 ... hist_bin_width=1.0 471 ... ) 472 """ 473 if hotspot_colors is None: 474 hotspot_colors = { 475 "bio": "blue", 476 "gas": "red", 477 "comb": "green", 478 } 479 # データをDataFrameに変換 480 df = pd.DataFrame([e.to_dict() for e in emissions]) 481 482 # プロットの作成(散布図の有無に応じてサブプロット数を調整) 483 if show_scatter: 484 fig, (ax1, ax2) = plt.subplots(1, 2, figsize=figsize) 485 axes = [ax1, ax2] 486 else: 487 fig, ax1 = plt.subplots(1, 1, figsize=(figsize[0] // 2, figsize[1])) 488 axes = [ax1] 489 490 # 存在するタイプを確認 491 # HotspotTypeの定義順を基準にソート 492 hotspot_types = list(get_args(HotspotType)) 493 existing_types = sorted( 494 df["type"].unique(), key=lambda x: hotspot_types.index(x) 495 ) 496 497 # 左側: ヒストグラム 498 # ビンの範囲を設定 499 start = 0 # 必ず0から開始 500 if hist_xlim is not None: 501 end = hist_xlim[1] 502 else: 503 end = np.ceil(df["emission_per_min"].max() * 1.05) 504 505 # ビン数を計算(end値をbin_widthで割り切れるように調整) 506 n_bins = int(np.ceil(end / hist_bin_width)) 507 end = n_bins * hist_bin_width 508 509 # ビンの生成(0から開始し、bin_widthの倍数で区切る) 510 bins = np.linspace(start, end, n_bins + 1) 511 512 # タイプごとにヒストグラムを積み上げ 513 if stack_bars: 514 # 積み上げ方式 515 bottom = np.zeros(len(bins) - 1) 516 for spot_type in existing_types: 517 data = df[df["type"] == spot_type]["emission_per_min"] 518 if len(data) > 0: 519 counts, _ = np.histogram(data, bins=bins) 520 ax1.bar( 521 bins[:-1], 522 counts, 523 width=hist_bin_width, 524 bottom=bottom, 525 alpha=0.6, 526 label=spot_type, 527 color=hotspot_colors[spot_type], 528 ) 529 bottom += counts 530 else: 531 # 重ね合わせ方式 532 for spot_type in existing_types: 533 data = df[df["type"] == spot_type]["emission_per_min"] 534 if len(data) > 0: 535 counts, _ = np.histogram(data, bins=bins) 536 ax1.bar( 537 bins[:-1], 538 counts, 539 width=hist_bin_width, 540 alpha=0.4, # 透明度を上げて重なりを見やすく 541 label=spot_type, 542 color=hotspot_colors[spot_type], 543 ) 544 545 ax1.set_xlabel("CH$_4$ Emission (L min$^{-1}$)") 546 ax1.set_ylabel("Frequency") 547 if hist_log_y: 548 # ax1.set_yscale("log") 549 # 非線形スケールを設定(linthreshで線形から対数への遷移点を指定) 550 ax1.set_yscale("symlog", linthresh=1.0) 551 if hist_xlim is not None: 552 ax1.set_xlim(hist_xlim) 553 else: 554 ax1.set_xlim(0, np.ceil(df["emission_per_min"].max() * 1.05)) 555 556 if hist_ylim is not None: 557 ax1.set_ylim(hist_ylim) 558 else: 559 ax1.set_ylim(0, ax1.get_ylim()[1]) # 下限を0に設定 560 561 if show_scatter: 562 # 右側: 散布図 563 for spot_type in existing_types: 564 mask = df["type"] == spot_type 565 ax2.scatter( 566 df[mask]["emission_per_min"], 567 df[mask]["delta_ch4"], 568 alpha=0.6, 569 label=spot_type, 570 color=hotspot_colors[spot_type], 571 ) 572 573 ax2.set_xlabel("Emission Rate (L min$^{-1}$)") 574 ax2.set_ylabel("ΔCH$_4$ (ppm)") 575 if scatter_xlim is not None: 576 ax2.set_xlim(scatter_xlim) 577 else: 578 ax2.set_xlim(0, np.ceil(df["emission_per_min"].max() * 1.05)) 579 580 if scatter_ylim is not None: 581 ax2.set_ylim(scatter_ylim) 582 else: 583 ax2.set_ylim(0, np.ceil(df["delta_ch4"].max() * 1.05)) 584 585 # 凡例の表示 586 if add_legend: 587 for ax in axes: 588 ax.legend( 589 bbox_to_anchor=(0.5, -0.30), 590 loc="upper center", 591 ncol=len(existing_types), 592 ) 593 594 plt.tight_layout() 595 596 # 図の保存 597 if save_fig: 598 if output_dirpath is None: 599 raise ValueError( 600 "save_fig = True の場合、 output_dirpath を指定する必要があります。有効なディレクトリパスを指定してください。" 601 ) 602 os.makedirs(output_dirpath, exist_ok=True) 603 output_filepath = os.path.join(output_dirpath, output_filename) 604 plt.savefig(output_filepath, bbox_inches="tight", dpi=dpi) 605 # 図の表示 606 if show_fig: 607 plt.show() 608 plt.close(fig=fig) 609 610 if print_summary: 611 # デバッグ用の出力 612 self.logger.info("ビンごとの集計:") 613 print(f"{'Range':>12} | {'bio':>8} | {'gas':>8} | {'total':>8}") 614 print("-" * 50) 615 616 for i in range(len(bins) - 1): 617 bin_start = bins[i] 618 bin_end = bins[i + 1] 619 620 # 各タイプのカウントを計算 621 counts_by_type: dict[HotspotType, int] = {"bio": 0, "gas": 0, "comb": 0} 622 total = 0 623 for spot_type in existing_types: 624 mask = ( 625 (df["type"] == spot_type) 626 & (df["emission_per_min"] >= bin_start) 627 & (df["emission_per_min"] < bin_end) 628 ) 629 count = len(df[mask]) 630 counts_by_type[spot_type] = count 631 total += count 632 633 # カウントが0の場合はスキップ 634 if total > 0: 635 range_str = f"{bin_start:5.1f}-{bin_end:<5.1f}" 636 bio_count = counts_by_type.get("bio", 0) 637 gas_count = counts_by_type.get("gas", 0) 638 print( 639 f"{range_str:>12} | {bio_count:8d} | {gas_count:8d} | {total:8d}" 640 )
257 def __init__( 258 self, 259 logger: Logger | None = None, 260 logging_debug: bool = False, 261 ): 262 """ 263 渦相関法によって記録されたデータファイルを処理するクラス。 264 265 Parameters 266 ---------- 267 fs: float 268 サンプリング周波数。 269 logger: Logger | None, optional 270 使用するロガー。Noneの場合は新しいロガーを作成します。 271 logging_debug: bool, optional 272 ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。 273 """ 274 # ロガー 275 log_level: int = INFO 276 if logging_debug: 277 log_level = DEBUG 278 self.logger: Logger = setup_logger(logger=logger, log_level=log_level)
渦相関法によって記録されたデータファイルを処理するクラス。
Parameters
fs: float
サンプリング周波数。
logger: Logger | None, optional
使用するロガー。Noneの場合は新しいロガーを作成します。
logging_debug: bool, optional
ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。
280 def calculate_emission_rates( 281 self, 282 hotspots: list[HotspotData], 283 config: HotspotEmissionConfig, 284 print_summary: bool = False, 285 ) -> list[EmissionData]: 286 """ 287 検出されたホットスポットのCH4漏出量を計算・解析し、統計情報を生成します。 288 289 Parameters 290 ---------- 291 hotspots: list[HotspotData] 292 分析対象のホットスポットのリスト 293 config: HotspotEmissionConfig 294 排出量計算の設定 295 print_summary: bool 296 統計情報を表示するかどうか。デフォルトはFalse。 297 298 Returns 299 ---------- 300 list[EmissionData] 301 - 各ホットスポットの排出量データを含むリスト 302 303 Examples 304 ---------- 305 >>> # Weller et al. (2022)の係数を使用する例 306 >>> config = HotspotEmissionConfig( 307 ... formula=EmissionFormula(name="weller", coef_a=0.988, coef_b=0.817), 308 ... emission_categories={ 309 ... "low": {"min": 0, "max": 6}, 310 ... "medium": {"min": 6, "max": 40}, 311 ... "high": {"min": 40, "max": float("inf")}, 312 ... } 313 ... ) 314 >>> emissions_list = HotspotEmissionAnalyzer.calculate_emission_rates( 315 ... hotspots=hotspots, 316 ... config=config, 317 ... print_summary=True 318 ... ) 319 """ 320 # 係数の取得 321 coef_a: float = config.formula.coef_a 322 coef_b: float = config.formula.coef_b 323 324 # 排出量の計算 325 emission_data_list = [] 326 for spot in hotspots: 327 # 漏出量の計算 (L/min) 328 emission_per_min = np.exp((np.log(spot.delta_ch4) + coef_a) / coef_b) 329 # 日排出量 (L/day) 330 emission_per_day = emission_per_min * 60 * 24 331 # 年間排出量 (L/year) 332 emission_per_year = emission_per_day * 365 333 334 emission_data = EmissionData( 335 timestamp=spot.timestamp, 336 delta_ch4=spot.delta_ch4, 337 delta_c2h6=spot.delta_c2h6, 338 delta_ratio=spot.delta_ratio, 339 emission_per_min=emission_per_min, 340 emission_per_day=emission_per_day, 341 emission_per_year=emission_per_year, 342 avg_lat=spot.avg_lat, 343 avg_lon=spot.avg_lon, 344 section=spot.section, 345 type=spot.type, 346 ) 347 emission_data_list.append(emission_data) 348 349 # 統計計算用にDataFrameを作成 350 emission_df = pd.DataFrame([e.to_dict() for e in emission_data_list]) 351 352 # タイプ別の統計情報を計算 353 # get_args(HotspotType)を使用して型安全なリストを作成 354 types = list(get_args(HotspotType)) 355 for spot_type in types: 356 df_type = emission_df[emission_df["type"] == spot_type] 357 if len(df_type) > 0: 358 # 既存の統計情報を計算 359 type_stats = { 360 "count": len(df_type), 361 "emission_per_min_min": df_type["emission_per_min"].min(), 362 "emission_per_min_max": df_type["emission_per_min"].max(), 363 "emission_per_min_mean": df_type["emission_per_min"].mean(), 364 "emission_per_min_median": df_type["emission_per_min"].median(), 365 "total_annual_emission": df_type["emission_per_year"].sum(), 366 "mean_annual_emission": df_type["emission_per_year"].mean(), 367 } 368 369 # 排出量カテゴリー別の統計を追加 370 category_counts = {} 371 for category, limits in config.emission_categories.items(): 372 mask = (df_type["emission_per_min"] >= limits["min"]) & ( 373 df_type["emission_per_min"] < limits["max"] 374 ) 375 category_counts[category] = len(df_type[mask]) 376 type_stats["emission_categories"] = category_counts 377 378 if print_summary: 379 self.logger.info(f"{spot_type}タイプの統計情報:") 380 print(f" 検出数: {type_stats['count']}") 381 print(" 排出量 (L/min):") 382 print(f" 最小値: {type_stats['emission_per_min_min']:.2f}") 383 print(f" 最大値: {type_stats['emission_per_min_max']:.2f}") 384 print(f" 平均値: {type_stats['emission_per_min_mean']:.2f}") 385 print(f" 中央値: {type_stats['emission_per_min_median']:.2f}") 386 print(" 排出量カテゴリー別の検出数:") 387 for category, count in category_counts.items(): 388 print(f" {category}: {count}") 389 print(" 年間排出量 (L/year):") 390 print(f" 合計: {type_stats['total_annual_emission']:.2f}") 391 print(f" 平均: {type_stats['mean_annual_emission']:.2f}") 392 393 return emission_data_list
検出されたホットスポットのCH4漏出量を計算・解析し、統計情報を生成します。
Parameters
hotspots: list[HotspotData]
分析対象のホットスポットのリスト
config: HotspotEmissionConfig
排出量計算の設定
print_summary: bool
統計情報を表示するかどうか。デフォルトはFalse。
Returns
list[EmissionData]
- 各ホットスポットの排出量データを含むリスト
Examples
>>> # Weller et al. (2022)の係数を使用する例
>>> config = HotspotEmissionConfig(
... formula=EmissionFormula(name="weller", coef_a=0.988, coef_b=0.817),
... emission_categories={
... "low": {"min": 0, "max": 6},
... "medium": {"min": 6, "max": 40},
... "high": {"min": 40, "max": float("inf")},
... }
... )
>>> emissions_list = HotspotEmissionAnalyzer.calculate_emission_rates(
... hotspots=hotspots,
... config=config,
... print_summary=True
... )
395 def plot_emission_analysis( 396 self, 397 emissions: list[EmissionData], 398 output_dirpath: str | Path | None = None, 399 output_filename: str = "emission_analysis.png", 400 figsize: tuple[float, float] = (12, 5), 401 dpi: float | None = 350, 402 hotspot_colors: dict[HotspotType, str] | None = None, 403 add_legend: bool = True, 404 hist_log_y: bool = False, 405 hist_xlim: tuple[float, float] | None = None, 406 hist_ylim: tuple[float, float] | None = None, 407 scatter_xlim: tuple[float, float] | None = None, 408 scatter_ylim: tuple[float, float] | None = None, 409 hist_bin_width: float = 0.5, 410 print_summary: bool = False, 411 stack_bars: bool = True, 412 save_fig: bool = False, 413 show_fig: bool = True, 414 show_scatter: bool = True, 415 ) -> None: 416 """ 417 排出量分析のプロットを作成する静的メソッド。 418 419 Parameters 420 ---------- 421 emissions: list[EmissionData] 422 calculate_emission_ratesで生成された分析結果 423 output_dirpath: str | Path | None, optional 424 出力先ディレクトリのパス。指定しない場合はカレントディレクトリに保存。 425 output_filename: str, optional 426 保存するファイル名。デフォルトは"emission_analysis.png"。 427 figsize: tuple[float, float], optional 428 プロットのサイズ。デフォルトは(12, 5)。 429 dpi: float | None, optional 430 プロットの解像度。デフォルトは350。 431 hotspot_colors: dict[HotspotType, str] | None, optional 432 ホットスポットの色を定義する辞書。指定しない場合はデフォルトの色を使用。 433 add_legend: bool, optional 434 凡例を追加するかどうか。デフォルトはTrue。 435 hist_log_y: bool, optional 436 ヒストグラムのy軸を対数スケールにするかどうか。デフォルトはFalse。 437 hist_xlim: tuple[float, float] | None, optional 438 ヒストグラムのx軸の範囲。指定しない場合は自動で設定。 439 hist_ylim: tuple[float, float] | None, optional 440 ヒストグラムのy軸の範囲。指定しない場合は自動で設定。 441 scatter_xlim: tuple[float, float] | None, optional 442 散布図のx軸の範囲。指定しない場合は自動で設定。 443 scatter_ylim: tuple[float, float] | None, optional 444 散布図のy軸の範囲。指定しない場合は自動で設定。 445 hist_bin_width: float, optional 446 ヒストグラムのビンの幅。デフォルトは0.5。 447 print_summary: bool, optional 448 集計結果を表示するかどうか。デフォルトはFalse。 449 stack_bars: bool, optional 450 ヒストグラムを積み上げ方式で表示するかどうか。デフォルトはTrue。 451 save_fig: bool, optional 452 図をファイルに保存するかどうか。デフォルトはFalse。 453 show_fig: bool, optional 454 図を表示するかどうか。デフォルトはTrue。 455 show_scatter: bool, optional 456 散布図(右図)を表示するかどうか。デフォルトはTrue。 457 458 Returns 459 ------- 460 None 461 462 Examples 463 -------- 464 >>> analyzer = HotspotEmissionAnalyzer() 465 >>> emissions = analyzer.calculate_emission_rates(hotspots) 466 >>> analyzer.plot_emission_analysis( 467 ... emissions, 468 ... output_dirpath="results", 469 ... save_fig=True, 470 ... hist_bin_width=1.0 471 ... ) 472 """ 473 if hotspot_colors is None: 474 hotspot_colors = { 475 "bio": "blue", 476 "gas": "red", 477 "comb": "green", 478 } 479 # データをDataFrameに変換 480 df = pd.DataFrame([e.to_dict() for e in emissions]) 481 482 # プロットの作成(散布図の有無に応じてサブプロット数を調整) 483 if show_scatter: 484 fig, (ax1, ax2) = plt.subplots(1, 2, figsize=figsize) 485 axes = [ax1, ax2] 486 else: 487 fig, ax1 = plt.subplots(1, 1, figsize=(figsize[0] // 2, figsize[1])) 488 axes = [ax1] 489 490 # 存在するタイプを確認 491 # HotspotTypeの定義順を基準にソート 492 hotspot_types = list(get_args(HotspotType)) 493 existing_types = sorted( 494 df["type"].unique(), key=lambda x: hotspot_types.index(x) 495 ) 496 497 # 左側: ヒストグラム 498 # ビンの範囲を設定 499 start = 0 # 必ず0から開始 500 if hist_xlim is not None: 501 end = hist_xlim[1] 502 else: 503 end = np.ceil(df["emission_per_min"].max() * 1.05) 504 505 # ビン数を計算(end値をbin_widthで割り切れるように調整) 506 n_bins = int(np.ceil(end / hist_bin_width)) 507 end = n_bins * hist_bin_width 508 509 # ビンの生成(0から開始し、bin_widthの倍数で区切る) 510 bins = np.linspace(start, end, n_bins + 1) 511 512 # タイプごとにヒストグラムを積み上げ 513 if stack_bars: 514 # 積み上げ方式 515 bottom = np.zeros(len(bins) - 1) 516 for spot_type in existing_types: 517 data = df[df["type"] == spot_type]["emission_per_min"] 518 if len(data) > 0: 519 counts, _ = np.histogram(data, bins=bins) 520 ax1.bar( 521 bins[:-1], 522 counts, 523 width=hist_bin_width, 524 bottom=bottom, 525 alpha=0.6, 526 label=spot_type, 527 color=hotspot_colors[spot_type], 528 ) 529 bottom += counts 530 else: 531 # 重ね合わせ方式 532 for spot_type in existing_types: 533 data = df[df["type"] == spot_type]["emission_per_min"] 534 if len(data) > 0: 535 counts, _ = np.histogram(data, bins=bins) 536 ax1.bar( 537 bins[:-1], 538 counts, 539 width=hist_bin_width, 540 alpha=0.4, # 透明度を上げて重なりを見やすく 541 label=spot_type, 542 color=hotspot_colors[spot_type], 543 ) 544 545 ax1.set_xlabel("CH$_4$ Emission (L min$^{-1}$)") 546 ax1.set_ylabel("Frequency") 547 if hist_log_y: 548 # ax1.set_yscale("log") 549 # 非線形スケールを設定(linthreshで線形から対数への遷移点を指定) 550 ax1.set_yscale("symlog", linthresh=1.0) 551 if hist_xlim is not None: 552 ax1.set_xlim(hist_xlim) 553 else: 554 ax1.set_xlim(0, np.ceil(df["emission_per_min"].max() * 1.05)) 555 556 if hist_ylim is not None: 557 ax1.set_ylim(hist_ylim) 558 else: 559 ax1.set_ylim(0, ax1.get_ylim()[1]) # 下限を0に設定 560 561 if show_scatter: 562 # 右側: 散布図 563 for spot_type in existing_types: 564 mask = df["type"] == spot_type 565 ax2.scatter( 566 df[mask]["emission_per_min"], 567 df[mask]["delta_ch4"], 568 alpha=0.6, 569 label=spot_type, 570 color=hotspot_colors[spot_type], 571 ) 572 573 ax2.set_xlabel("Emission Rate (L min$^{-1}$)") 574 ax2.set_ylabel("ΔCH$_4$ (ppm)") 575 if scatter_xlim is not None: 576 ax2.set_xlim(scatter_xlim) 577 else: 578 ax2.set_xlim(0, np.ceil(df["emission_per_min"].max() * 1.05)) 579 580 if scatter_ylim is not None: 581 ax2.set_ylim(scatter_ylim) 582 else: 583 ax2.set_ylim(0, np.ceil(df["delta_ch4"].max() * 1.05)) 584 585 # 凡例の表示 586 if add_legend: 587 for ax in axes: 588 ax.legend( 589 bbox_to_anchor=(0.5, -0.30), 590 loc="upper center", 591 ncol=len(existing_types), 592 ) 593 594 plt.tight_layout() 595 596 # 図の保存 597 if save_fig: 598 if output_dirpath is None: 599 raise ValueError( 600 "save_fig = True の場合、 output_dirpath を指定する必要があります。有効なディレクトリパスを指定してください。" 601 ) 602 os.makedirs(output_dirpath, exist_ok=True) 603 output_filepath = os.path.join(output_dirpath, output_filename) 604 plt.savefig(output_filepath, bbox_inches="tight", dpi=dpi) 605 # 図の表示 606 if show_fig: 607 plt.show() 608 plt.close(fig=fig) 609 610 if print_summary: 611 # デバッグ用の出力 612 self.logger.info("ビンごとの集計:") 613 print(f"{'Range':>12} | {'bio':>8} | {'gas':>8} | {'total':>8}") 614 print("-" * 50) 615 616 for i in range(len(bins) - 1): 617 bin_start = bins[i] 618 bin_end = bins[i + 1] 619 620 # 各タイプのカウントを計算 621 counts_by_type: dict[HotspotType, int] = {"bio": 0, "gas": 0, "comb": 0} 622 total = 0 623 for spot_type in existing_types: 624 mask = ( 625 (df["type"] == spot_type) 626 & (df["emission_per_min"] >= bin_start) 627 & (df["emission_per_min"] < bin_end) 628 ) 629 count = len(df[mask]) 630 counts_by_type[spot_type] = count 631 total += count 632 633 # カウントが0の場合はスキップ 634 if total > 0: 635 range_str = f"{bin_start:5.1f}-{bin_end:<5.1f}" 636 bio_count = counts_by_type.get("bio", 0) 637 gas_count = counts_by_type.get("gas", 0) 638 print( 639 f"{range_str:>12} | {bio_count:8d} | {gas_count:8d} | {total:8d}" 640 )
排出量分析のプロットを作成する静的メソッド。
Parameters
emissions: list[EmissionData]
calculate_emission_ratesで生成された分析結果
output_dirpath: str | Path | None, optional
出力先ディレクトリのパス。指定しない場合はカレントディレクトリに保存。
output_filename: str, optional
保存するファイル名。デフォルトは"emission_analysis.png"。
figsize: tuple[float, float], optional
プロットのサイズ。デフォルトは(12, 5)。
dpi: float | None, optional
プロットの解像度。デフォルトは350。
hotspot_colors: dict[HotspotType, str] | None, optional
ホットスポットの色を定義する辞書。指定しない場合はデフォルトの色を使用。
add_legend: bool, optional
凡例を追加するかどうか。デフォルトはTrue。
hist_log_y: bool, optional
ヒストグラムのy軸を対数スケールにするかどうか。デフォルトはFalse。
hist_xlim: tuple[float, float] | None, optional
ヒストグラムのx軸の範囲。指定しない場合は自動で設定。
hist_ylim: tuple[float, float] | None, optional
ヒストグラムのy軸の範囲。指定しない場合は自動で設定。
scatter_xlim: tuple[float, float] | None, optional
散布図のx軸の範囲。指定しない場合は自動で設定。
scatter_ylim: tuple[float, float] | None, optional
散布図のy軸の範囲。指定しない場合は自動で設定。
hist_bin_width: float, optional
ヒストグラムのビンの幅。デフォルトは0.5。
print_summary: bool, optional
集計結果を表示するかどうか。デフォルトはFalse。
stack_bars: bool, optional
ヒストグラムを積み上げ方式で表示するかどうか。デフォルトはTrue。
save_fig: bool, optional
図をファイルに保存するかどうか。デフォルトはFalse。
show_fig: bool, optional
図を表示するかどうか。デフォルトはTrue。
show_scatter: bool, optional
散布図(右図)を表示するかどうか。デフォルトはTrue。
Returns
None
Examples
>>> analyzer = HotspotEmissionAnalyzer()
>>> emissions = analyzer.calculate_emission_rates(hotspots)
>>> analyzer.plot_emission_analysis(
... emissions,
... output_dirpath="results",
... save_fig=True,
... hist_bin_width=1.0
... )
193@dataclass 194class HotspotEmissionConfig: 195 """ 196 排出量計算の設定を保持するデータクラス 197 198 Parameters 199 ---------- 200 formula: EmissionFormula 201 使用する計算式の設定 202 emission_categories: dict[str, dict[str, float]] 203 排出量カテゴリーの閾値設定 204 デフォルト値: 205 ```python 206 { 207 "low": {"min": 0, "max": 6}, # < 6 L/min 208 "medium": {"min": 6, "max": 40}, # 6-40 L/min 209 "high": {"min": 40, "max": float("inf")} # > 40 L/min 210 } 211 ``` 212 213 Examples 214 ---------- 215 >>> # Weller et al. (2022)の係数を使用する場合 216 >>> config = HotspotEmissionConfig( 217 ... formula=EmissionFormula(name="weller", coef_a=0.988, coef_b=0.817), 218 ... emission_categories={ 219 ... "low": {"min": 0, "max": 6}, # < 6 L/min 220 ... "medium": {"min": 6, "max": 40}, # 6-40 L/min 221 ... "high": {"min": 40, "max": float("inf")}, # > 40 L/min 222 ... } 223 ... ) 224 >>> # 複数のconfigをリスト形式で定義する場合 225 >>> emission_configs: list[HotspotEmissionConfig] = [ 226 ... HotspotEmissionConfig(formula=EmissionFormula(name="weller", coef_a=0.988, coef_b=0.817)), 227 ... HotspotEmissionConfig(formula=EmissionFormula(name="weitzel", coef_a=0.521, coef_b=0.795)), 228 ... HotspotEmissionConfig(formula=EmissionFormula(name="joo", coef_a=2.738, coef_b=1.329)), 229 ... HotspotEmissionConfig(formula=EmissionFormula(name="umezawa", coef_a=2.716, coef_b=0.741)), 230 ... ] 231 """ 232 233 formula: EmissionFormula 234 emission_categories: dict[str, dict[str, float]] = field( 235 default_factory=lambda: { 236 "low": {"min": 0, "max": 6}, # < 6 L/min 237 "medium": {"min": 6, "max": 40}, # 6-40 L/min 238 "high": {"min": 40, "max": float("inf")}, # > 40 L/min 239 } 240 ) 241 242 def __post_init__(self) -> None: 243 """ 244 パラメータの検証を行います。 245 """ 246 # カテゴリーの閾値の整合性チェック 247 for category, limits in self.emission_categories.items(): 248 if "min" not in limits or "max" not in limits: 249 raise ValueError( 250 f"Category {category} must have 'min' and 'max' values" 251 ) 252 if limits["min"] > limits["max"]: 253 raise ValueError(f"Category {category} has invalid range: min > max")
排出量計算の設定を保持するデータクラス
Parameters
formula: EmissionFormula
使用する計算式の設定
emission_categories: dict[str, dict[str, float]]
排出量カテゴリーの閾値設定
デフォルト値:
{
"low": {"min": 0, "max": 6}, # < 6 L/min
"medium": {"min": 6, "max": 40}, # 6-40 L/min
"high": {"min": 40, "max": float("inf")} # > 40 L/min
}
Examples
>>> # Weller et al. (2022)の係数を使用する場合
>>> config = HotspotEmissionConfig(
... formula=EmissionFormula(name="weller", coef_a=0.988, coef_b=0.817),
... emission_categories={
... "low": {"min": 0, "max": 6}, # < 6 L/min
... "medium": {"min": 6, "max": 40}, # 6-40 L/min
... "high": {"min": 40, "max": float("inf")}, # > 40 L/min
... }
... )
>>> # 複数のconfigをリスト形式で定義する場合
>>> emission_configs: list[HotspotEmissionConfig] = [
... HotspotEmissionConfig(formula=EmissionFormula(name="weller", coef_a=0.988, coef_b=0.817)),
... HotspotEmissionConfig(formula=EmissionFormula(name="weitzel", coef_a=0.521, coef_b=0.795)),
... HotspotEmissionConfig(formula=EmissionFormula(name="joo", coef_a=2.738, coef_b=1.329)),
... HotspotEmissionConfig(formula=EmissionFormula(name="umezawa", coef_a=2.716, coef_b=0.741)),
... ]
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)
327class MobileMeasurementAnalyzer: 328 """ 329 車載濃度観測で得られた測定データを解析するクラス 330 """ 331 332 EARTH_RADIUS_METERS: float = 6371000 # 地球の半径(メートル) 333 334 def __init__( 335 self, 336 center_lat: float, 337 center_lon: float, 338 configs: list[MobileMeasurementConfig] | list[tuple[float, float, str | Path]], 339 num_sections: int = 4, 340 ch4_enhance_threshold: float = 0.1, 341 correlation_threshold: float = 0.7, 342 hotspot_area_meter: float = 50, 343 hotspot_params: HotspotParams | None = None, 344 window_minutes: float = 5, 345 columns_rename_dict: dict[str, str] | None = None, 346 na_values: list[str] | None = None, 347 logger: Logger | None = None, 348 logging_debug: bool = False, 349 ): 350 """ 351 測定データ解析クラスを初期化します。 352 353 Parameters 354 ---------- 355 center_lat: float 356 中心緯度 357 center_lon: float 358 中心経度 359 configs: list[MobileMeasurementConfig] | list[tuple[float, float, str | Path]] 360 入力ファイルのリスト 361 num_sections: int, optional 362 分割する区画数。デフォルト値は4です。 363 ch4_enhance_threshold: float, optional 364 CH4増加の閾値(ppm)。デフォルト値は0.1です。 365 correlation_threshold: float, optional 366 相関係数の閾値。デフォルト値は0.7です。 367 hotspot_area_meter: float, optional 368 ホットスポットの検出に使用するエリアの半径(メートル)。デフォルト値は50メートルです。 369 hotspot_params: HotspotParams | None, optional 370 ホットスポット解析のパラメータ設定。未指定の場合はデフォルト設定を使用します。 371 window_minutes: float, optional 372 移動窓の大きさ(分)。デフォルト値は5分です。 373 columns_rename_dict: dict[str, str] | None, optional 374 元のデータファイルのヘッダーを汎用的な単語に変換するための辞書型データ。未指定の場合は以下のデフォルト値を使用します: 375 ```python 376 { 377 "Time Stamp": "timestamp", 378 "CH4 (ppm)": "ch4_ppm", 379 "C2H6 (ppb)": "c2h6_ppb", 380 "H2O (ppm)": "h2o_ppm", 381 "Latitude": "latitude", 382 "Longitude": "longitude" 383 } 384 ``` 385 na_values: list[str] | None, optional 386 NaNと判定する値のパターン。未指定の場合は["No Data", "nan"]を使用します。 387 logger: Logger | None, optional 388 使用するロガー。未指定の場合は新しいロガーを作成します。 389 logging_debug: bool, optional 390 ログレベルを"DEBUG"に設定するかどうか。デフォルト値はFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。 391 392 Examples 393 -------- 394 >>> analyzer = MobileMeasurementAnalyzer( 395 ... center_lat=35.6895, 396 ... center_lon=139.6917, 397 ... configs=[ 398 ... MobileMeasurementConfig(fs=1.0, lag=0.0, path="data1.csv"), 399 ... MobileMeasurementConfig(fs=1.0, lag=0.0, path="data2.csv") 400 ... ], 401 ... num_sections=6, 402 ... ch4_enhance_threshold=0.2 403 ... ) 404 """ 405 # ロガー 406 log_level: int = INFO 407 if logging_debug: 408 log_level = DEBUG 409 self.logger: Logger = setup_logger(logger=logger, log_level=log_level) 410 # デフォルト値を使用 411 if columns_rename_dict is None: 412 columns_rename_dict = { 413 "Time Stamp": "timestamp", 414 "CH4 (ppm)": "ch4_ppm", 415 "C2H6 (ppb)": "c2h6_ppb", 416 "H2O (ppm)": "h2o_ppm", 417 "Latitude": "latitude", 418 "Longitude": "longitude", 419 } 420 if na_values is None: 421 na_values = ["No Data", "nan"] 422 # プライベートなプロパティ 423 self._center_lat: float = center_lat 424 self._center_lon: float = center_lon 425 self._ch4_enhance_threshold: float = ch4_enhance_threshold 426 self._correlation_threshold: float = correlation_threshold 427 self._hotspot_area_meter: float = hotspot_area_meter 428 self._column_mapping: dict[str, str] = columns_rename_dict 429 self._na_values: list[str] = na_values 430 self._hotspot_params = hotspot_params or HotspotParams() 431 self._num_sections: int = num_sections 432 # セクションの範囲 433 section_size: float = 360 / num_sections 434 self._section_size: float = section_size 435 self._sections = MobileMeasurementAnalyzer._initialize_sections( 436 num_sections, section_size 437 ) 438 # window_sizeをデータポイント数に変換(分→秒→データポイント数) 439 self._window_size: int = MobileMeasurementAnalyzer._calculate_window_size( 440 window_minutes 441 ) 442 # 入力設定の標準化 443 normalized_input_configs: list[MobileMeasurementConfig] = ( 444 MobileMeasurementAnalyzer._normalize_configs(configs) 445 ) 446 self._configs: list[MobileMeasurementConfig] = normalized_input_configs 447 # 複数ファイルのデータを読み込み結合 448 self.df: pd.DataFrame = self._load_all_combined_data(normalized_input_configs) 449 450 @property 451 def hotspot_params(self) -> HotspotParams: 452 """ホットスポット解析のパラメータ設定を取得""" 453 return self._hotspot_params 454 455 @hotspot_params.setter 456 def hotspot_params(self, params: HotspotParams) -> None: 457 """ホットスポット解析のパラメータ設定を更新""" 458 self._hotspot_params = params 459 460 def analyze_delta_ch4_stats(self, hotspots: list[HotspotData]) -> None: 461 """ 462 各タイプのホットスポットについてΔCH4の統計情報を計算し、結果を表示します。 463 464 Parameters 465 ---------- 466 hotspots: list[HotspotData] 467 分析対象のホットスポットリスト 468 """ 469 # タイプごとにホットスポットを分類 470 hotspots_by_type: dict[HotspotType, list[HotspotData]] = { 471 "bio": [h for h in hotspots if h.type == "bio"], 472 "gas": [h for h in hotspots if h.type == "gas"], 473 "comb": [h for h in hotspots if h.type == "comb"], 474 } 475 476 # 統計情報を計算し、表示 477 for spot_type, spots in hotspots_by_type.items(): 478 if spots: 479 delta_ch4_values = [spot.delta_ch4 for spot in spots] 480 max_value = max(delta_ch4_values) 481 mean_value = sum(delta_ch4_values) / len(delta_ch4_values) 482 median_value = sorted(delta_ch4_values)[len(delta_ch4_values) // 2] 483 print(f"{spot_type}タイプのホットスポットの統計情報:") 484 print(f" 最大値: {max_value}") 485 print(f" 平均値: {mean_value}") 486 print(f" 中央値: {median_value}") 487 else: 488 print(f"{spot_type}タイプのホットスポットは存在しません。") 489 490 def analyze_hotspots( 491 self, 492 duplicate_check_mode: Literal["none", "time_window", "time_all"] = "none", 493 min_time_threshold_seconds: float = 300, 494 max_time_threshold_hours: float = 12, 495 ) -> list[HotspotData]: 496 """ 497 ホットスポットを検出して分析します。 498 499 Parameters 500 ---------- 501 duplicate_check_mode: Literal["none", "time_window", "time_all"], optional 502 重複チェックのモード。デフォルトは"none"。 503 - "none": 重複チェックを行わない 504 - "time_window": 指定された時間窓内の重複のみを除外 505 - "time_all": すべての時間範囲で重複チェックを行う 506 min_time_threshold_seconds: float, optional 507 重複とみなす最小時間の閾値。デフォルトは300秒。 508 max_time_threshold_hours: float, optional 509 重複チェックを一時的に無視する最大時間の閾値。デフォルトは12時間。 510 511 Returns 512 ---------- 513 list[HotspotData] 514 検出されたホットスポットのリスト 515 516 Examples 517 -------- 518 >>> analyzer = MobileMeasurementAnalyzer() 519 >>> # 重複チェックなしでホットスポットを検出 520 >>> hotspots = analyzer.analyze_hotspots() 521 >>> 522 >>> # 時間窓内の重複を除外してホットスポットを検出 523 >>> hotspots = analyzer.analyze_hotspots( 524 ... duplicate_check_mode="time_window", 525 ... min_time_threshold_seconds=600, 526 ... max_time_threshold_hours=24 527 ... ) 528 """ 529 all_hotspots: list[HotspotData] = [] 530 df_processed: pd.DataFrame = self.get_preprocessed_data() 531 532 # ホットスポットの検出 533 hotspots: list[HotspotData] = self._detect_hotspots( 534 df=df_processed, 535 ch4_enhance_threshold=self._ch4_enhance_threshold, 536 ) 537 all_hotspots.extend(hotspots) 538 539 # 重複チェックモードに応じて処理 540 if duplicate_check_mode != "none": 541 unique_hotspots = MobileMeasurementAnalyzer.remove_hotspots_duplicates( 542 all_hotspots, 543 check_time_all=(duplicate_check_mode == "time_all"), 544 min_time_threshold_seconds=min_time_threshold_seconds, 545 max_time_threshold_hours=max_time_threshold_hours, 546 hotspot_area_meter=self._hotspot_area_meter, 547 ) 548 self.logger.info( 549 f"重複除外: {len(all_hotspots)} → {len(unique_hotspots)} ホットスポット" 550 ) 551 return unique_hotspots 552 553 return all_hotspots 554 555 def calculate_measurement_stats( 556 self, 557 col_latitude: str = "latitude", 558 col_longitude: str = "longitude", 559 print_summary_individual: bool = True, 560 print_summary_total: bool = True, 561 ) -> tuple[float, timedelta]: 562 """ 563 各ファイルの測定時間と走行距離を計算し、合計を返します。 564 565 Parameters 566 ---------- 567 col_latitude: str, optional 568 緯度情報が格納されているカラム名。デフォルト値は"latitude"です。 569 col_longitude: str, optional 570 経度情報が格納されているカラム名。デフォルト値は"longitude"です。 571 print_summary_individual: bool, optional 572 個別ファイルの統計を表示するかどうか。デフォルト値はTrueです。 573 print_summary_total: bool, optional 574 合計統計を表示するかどうか。デフォルト値はTrueです。 575 576 Returns 577 ---------- 578 tuple[float, timedelta] 579 総距離(km)と総時間のタプル 580 581 Examples 582 ---------- 583 >>> analyzer = MobileMeasurementAnalyzer(config_list) 584 >>> total_distance, total_time = analyzer.calculate_measurement_stats() 585 >>> print(f"総距離: {total_distance:.2f}km") 586 >>> print(f"総時間: {total_time}") 587 """ 588 total_distance: float = 0.0 589 total_time: timedelta = timedelta() 590 individual_stats: list[dict] = [] 591 592 # 必要な列のみを読み込むように指定 593 columns_to_read = [col_latitude, col_longitude, "timestamp"] 594 595 for config in tqdm(self._configs, desc="Calculating", unit="file"): 596 # 必要な列のみを読み込み、メモリ使用を最適化 597 df = pd.read_csv(config.path, usecols=columns_to_read) 598 df["timestamp"] = pd.to_datetime(df["timestamp"]) 599 source_name = self.extract_source_name_from_path(config.path) 600 601 # 時間の計算 602 time_spent = df["timestamp"].max() - df["timestamp"].min() 603 604 # ベクトル化した距離計算 605 lat_shift = df[col_latitude].shift(-1) 606 lon_shift = df[col_longitude].shift(-1) 607 608 # nanを除外して距離計算 609 mask = ~(lat_shift.isna() | lon_shift.isna()) 610 if mask.any(): 611 # ラジアンに変換(一度に計算) 612 lat1_rad = np.radians(df[col_latitude][mask]) 613 lon1_rad = np.radians(df[col_longitude][mask]) 614 lat2_rad = np.radians(lat_shift[mask]) 615 lon2_rad = np.radians(lon_shift[mask]) 616 617 # Haversine formulaをベクトル化 618 dlat = lat2_rad - lat1_rad 619 dlon = lon2_rad - lon1_rad 620 621 # 距離計算を一度に実行 622 a = ( 623 np.sin(dlat / 2) ** 2 624 + np.cos(lat1_rad) * np.cos(lat2_rad) * np.sin(dlon / 2) ** 2 625 ) 626 c = 2 * np.arctan2(np.sqrt(a), np.sqrt(1 - a)) 627 distance_km = (self.EARTH_RADIUS_METERS * c).sum() / 1000 628 629 # 合計に加算 630 total_distance += distance_km 631 total_time += time_spent 632 633 # 統計情報を保存 634 if print_summary_individual: 635 average_speed = distance_km / (time_spent.total_seconds() / 3600) 636 individual_stats.append( 637 { 638 "source": source_name, 639 "distance": distance_km, 640 "time": time_spent, 641 "speed": average_speed, 642 } 643 ) 644 645 # 統計情報の表示(変更なし) 646 if print_summary_individual: 647 self.logger.info("=== Individual Stats ===") 648 for stat in individual_stats: 649 print(f"File : {stat['source']}") 650 print(f" Distance : {stat['distance']:.2f} km") 651 print(f" Time : {stat['time']}") 652 print(f" Avg. Speed: {stat['speed']:.1f} km/h\n") 653 654 if print_summary_total: 655 average_speed_total = total_distance / (total_time.total_seconds() / 3600) 656 self.logger.info("=== Total Stats ===") 657 print(f" Distance : {total_distance:.2f} km") 658 print(f" Time : {total_time}") 659 print(f" Avg. Speed: {average_speed_total:.1f} km/h\n") 660 661 return total_distance, total_time 662 663 def create_hotspots_map( 664 self, 665 hotspots: list[HotspotData], 666 output_dirpath: str | Path | None = None, 667 output_filename: str = "hotspots_map.html", 668 center_marker_color: str = "green", 669 center_marker_label: str = "Center", 670 plot_center_marker: bool = True, 671 radius_meters: float = 3000, 672 save_fig: bool = True, 673 ) -> None: 674 """ 675 ホットスポットの分布を地図上にプロットして保存します。 676 677 Parameters 678 ---------- 679 hotspots: list[HotspotData] 680 プロットするホットスポットのリスト 681 output_dirpath: str | Path | None, optional 682 保存先のディレクトリパス。未指定の場合はカレントディレクトリに保存されます。 683 output_filename: str, optional 684 保存するファイル名。デフォルト値は"hotspots_map.html"です。 685 center_marker_color: str, optional 686 中心を示すマーカーの色。デフォルト値は"green"です。 687 center_marker_label: str, optional 688 中心を示すマーカーのラベル。デフォルト値は"Center"です。 689 plot_center_marker: bool, optional 690 中心を示すマーカーを表示するかどうか。デフォルト値はTrueです。 691 radius_meters: float, optional 692 区画分けを示す線の長さ(メートル)。デフォルト値は3000です。 693 save_fig: bool, optional 694 図を保存するかどうか。デフォルト値はTrueです。 695 696 Returns 697 ------- 698 None 699 700 Examples 701 -------- 702 >>> analyzer = MobileMeasurementAnalyzer(center_lat=35.6895, center_lon=139.6917, configs=[]) 703 >>> hotspots = [HotspotData(...)] # ホットスポットのリスト 704 >>> analyzer.create_hotspots_map( 705 ... hotspots=hotspots, 706 ... output_dirpath="results", 707 ... radius_meters=5000 708 ... ) 709 """ 710 # 地図の作成 711 m = folium.Map( 712 location=[self._center_lat, self._center_lon], 713 zoom_start=15, 714 tiles="OpenStreetMap", 715 ) 716 717 # ホットスポットの種類ごとに異なる色でプロット 718 for spot in hotspots: 719 # NaN値チェックを追加 720 if math.isnan(spot.avg_lat) or math.isnan(spot.avg_lon): 721 continue 722 723 # default type 724 color = "black" 725 # タイプに応じて色を設定 726 if spot.type == "comb": 727 color = "green" 728 elif spot.type == "gas": 729 color = "red" 730 elif spot.type == "bio": 731 color = "blue" 732 733 # CSSのgrid layoutを使用してHTMLタグを含むテキストをフォーマット 734 popup_html = f""" 735 <div style='font-family: Arial; font-size: 12px; display: grid; grid-template-columns: auto auto auto; gap: 5px;'> 736 <b>Date</b> <span>:</span> <span>{spot.timestamp}</span> 737 <b>Lat</b> <span>:</span> <span>{spot.avg_lat:.3f}</span> 738 <b>Lon</b> <span>:</span> <span>{spot.avg_lon:.3f}</span> 739 <b>ΔCH<sub>4</sub></b> <span>:</span> <span>{spot.delta_ch4:.3f}</span> 740 <b>ΔC<sub>2</sub>H<sub>6</sub></b> <span>:</span> <span>{spot.delta_c2h6:.3f}</span> 741 <b>Ratio</b> <span>:</span> <span>{spot.delta_ratio:.3f}</span> 742 <b>Type</b> <span>:</span> <span>{spot.type}</span> 743 <b>Section</b> <span>:</span> <span>{spot.section}</span> 744 </div> 745 """ 746 747 # ポップアップのサイズを指定 748 popup = folium.Popup( 749 folium.Html(popup_html, script=True), 750 max_width=200, # 最大幅(ピクセル) 751 ) 752 753 folium.CircleMarker( 754 location=[spot.avg_lat, spot.avg_lon], 755 radius=8, 756 color=color, 757 fill=True, 758 popup=popup, 759 ).add_to(m) 760 761 # 中心点のマーカー 762 if plot_center_marker: 763 folium.Marker( 764 [self._center_lat, self._center_lon], 765 popup=center_marker_label, 766 icon=folium.Icon(color=center_marker_color, icon="info-sign"), 767 ).add_to(m) 768 769 # 区画の境界線を描画 770 for section in range(self._num_sections): 771 start_angle = math.radians(-180 + section * self._section_size) 772 773 const_r = self.EARTH_RADIUS_METERS 774 775 # 境界線の座標を計算 776 lat1 = self._center_lat 777 lon1 = self._center_lon 778 lat2 = math.degrees( 779 math.asin( 780 math.sin(math.radians(lat1)) * math.cos(radius_meters / const_r) 781 + math.cos(math.radians(lat1)) 782 * math.sin(radius_meters / const_r) 783 * math.cos(start_angle) 784 ) 785 ) 786 lon2 = self._center_lon + math.degrees( 787 math.atan2( 788 math.sin(start_angle) 789 * math.sin(radius_meters / const_r) 790 * math.cos(math.radians(lat1)), 791 math.cos(radius_meters / const_r) 792 - math.sin(math.radians(lat1)) * math.sin(math.radians(lat2)), 793 ) 794 ) 795 796 # 境界線を描画 797 folium.PolyLine( 798 locations=[[lat1, lon1], [lat2, lon2]], 799 color="black", 800 weight=1, 801 opacity=0.5, 802 ).add_to(m) 803 804 # 地図を保存 805 if save_fig and output_dirpath is None: 806 raise ValueError( 807 "save_fig = True の場合、 output_dirpath を指定する必要があります。有効なディレクトリパスを指定してください。" 808 ) 809 output_filepath: str = os.path.join(output_dirpath, output_filename) 810 m.save(str(output_filepath)) 811 self.logger.info(f"地図を保存しました: {output_filepath}") 812 813 def export_hotspots_to_csv( 814 self, 815 hotspots: list[HotspotData], 816 output_dirpath: str | Path | None = None, 817 output_filename: str = "hotspots.csv", 818 ) -> None: 819 """ 820 ホットスポットの情報をCSVファイルに出力します。 821 822 Parameters 823 ---------- 824 hotspots: list[HotspotData] 825 出力するホットスポットのリスト 826 output_dirpath: str | Path | None, optional 827 出力先ディレクトリのパス。未指定の場合はValueErrorが発生します。 828 output_filename: str, optional 829 出力ファイル名。デフォルト値は"hotspots.csv"です。 830 831 Returns 832 ------- 833 None 834 戻り値はありません。 835 836 Examples 837 -------- 838 >>> analyzer = MobileMeasurementAnalyzer() 839 >>> hotspots = analyzer.analyze_hotspots() 840 >>> analyzer.export_hotspots_to_csv( 841 ... hotspots=hotspots, 842 ... output_dirpath="output", 843 ... output_filename="hotspots_20240101.csv" 844 ... ) 845 """ 846 # 日時の昇順でソート 847 sorted_hotspots = sorted(hotspots, key=lambda x: x.timestamp) 848 849 # 出力用のデータを作成 850 records = [] 851 for spot in sorted_hotspots: 852 record = { 853 "timestamp": spot.timestamp, 854 "type": spot.type, 855 "delta_ch4": spot.delta_ch4, 856 "delta_c2h6": spot.delta_c2h6, 857 "delta_ratio": spot.delta_ratio, 858 "correlation": spot.correlation, 859 "angle": spot.angle, 860 "section": spot.section, 861 "latitude": spot.avg_lat, 862 "longitude": spot.avg_lon, 863 } 864 records.append(record) 865 866 # DataFrameに変換してCSVに出力 867 if output_dirpath is None: 868 raise ValueError( 869 "output_dirpath が指定されていません。有効なディレクトリパスを指定してください。" 870 ) 871 os.makedirs(output_dirpath, exist_ok=True) 872 output_filepath: str = os.path.join(output_dirpath, output_filename) 873 df: pd.DataFrame = pd.DataFrame(records) 874 df.to_csv(output_filepath, index=False) 875 self.logger.info( 876 f"ホットスポット情報をCSVファイルに出力しました: {output_filepath}" 877 ) 878 879 @staticmethod 880 def extract_source_name_from_path(path: str | Path) -> str: 881 """ 882 ファイルパスからソース名(拡張子なしのファイル名)を抽出します。 883 884 Parameters 885 ---------- 886 path: str | Path 887 ソース名を抽出するファイルパス 888 例: "/path/to/Pico100121_241017_092120+.txt" 889 890 Returns 891 ---------- 892 str 893 抽出されたソース名 894 例: "Pico100121_241017_092120+" 895 896 Examples: 897 ---------- 898 >>> path = "/path/to/data/Pico100121_241017_092120+.txt" 899 >>> MobileMeasurementAnalyzer.extract_source_from_path(path) 900 'Pico100121_241017_092120+' 901 """ 902 # Pathオブジェクトに変換 903 path_obj: Path = Path(path) 904 # stem属性で拡張子なしのファイル名を取得 905 source_name: str = path_obj.stem 906 return source_name 907 908 def get_preprocessed_data( 909 self, 910 ) -> pd.DataFrame: 911 """ 912 データ前処理を行い、CH4とC2H6の相関解析に必要な形式に整えます。 913 コンストラクタで読み込んだすべてのデータを前処理し、結合したDataFrameを返します。 914 915 内部で`MobileMeasurementAnalyzer._calculate_hotspots_parameters()`を適用し、 916 `ch4_ppm_mv`などのパラメータが追加されたDataFrameが戻り値として取得できます。 917 918 Returns 919 ---------- 920 pd.DataFrame 921 前処理済みの結合されたDataFrame 922 """ 923 params: HotspotParams = self._hotspot_params 924 # ホットスポットのパラメータを計算 925 df_processed: pd.DataFrame = ( 926 MobileMeasurementAnalyzer._calculate_hotspots_parameters( 927 df=self.df, 928 window_size=self._window_size, 929 col_ch4_ppm=params.col_ch4_ppm, 930 col_c2h6_ppb=params.col_c2h6_ppb, 931 col_h2o_ppm=params.col_h2o_ppm, 932 ch4_ppm_delta_min=params.ch4_ppm_delta_min, 933 ch4_ppm_delta_max=params.ch4_ppm_delta_max, 934 c2h6_ppb_delta_min=params.c2h6_ppb_delta_min, 935 c2h6_ppb_delta_max=params.c2h6_ppb_delta_max, 936 h2o_ppm_threshold=params.h2o_ppm_min, 937 rolling_method=params.rolling_method, 938 quantile_value=params.quantile_value, 939 ) 940 ) 941 return df_processed 942 943 def get_section_size(self) -> float: 944 """ 945 セクションのサイズを取得するメソッド。 946 このメソッドは、解析対象のデータを区画に分割する際の 947 各区画の角度範囲を示すサイズを返します。 948 949 Returns 950 ---------- 951 float 952 1セクションのサイズ(度単位) 953 """ 954 return self._section_size 955 956 def plot_ch4_delta_histogram( 957 self, 958 hotspots: list[HotspotData], 959 output_dirpath: str | Path | None, 960 output_filename: str = "ch4_delta_histogram.png", 961 dpi: float | None = 350, 962 figsize: tuple[float, float] = (8, 6), 963 fontsize: float = 20, 964 hotspot_colors: dict[HotspotType, str] | None = None, 965 xlabel: str = "Δ$\\mathregular{CH_{4}}$ (ppm)", 966 ylabel: str = "Frequency", 967 xlim: tuple[float, float] | None = None, 968 ylim: tuple[float, float] | None = None, 969 save_fig: bool = True, 970 show_fig: bool = True, 971 yscale_log: bool = True, 972 print_bins_analysis: bool = False, 973 ) -> None: 974 """ 975 CH4の増加量(ΔCH4)の積み上げヒストグラムをプロットします。 976 977 Parameters 978 ---------- 979 hotspots: list[HotspotData] 980 プロットするホットスポットのリスト 981 output_dirpath: str | Path | None 982 保存先のディレクトリパス 983 output_filename: str, optional 984 保存するファイル名。デフォルト値は"ch4_delta_histogram.png"です。 985 dpi: float | None, optional 986 解像度。デフォルト値は350です。 987 figsize: tuple[float, float], optional 988 図のサイズ。デフォルト値は(8, 6)です。 989 fontsize: float, optional 990 フォントサイズ。デフォルト値は20です。 991 hotspot_colors: dict[HotspotType, str] | None, optional 992 ホットスポットの色を定義する辞書。未指定の場合は以下のデフォルト値を使用します: 993 ```python 994 {"bio": "blue", "gas": "red", "comb": "green"} 995 ``` 996 xlabel: str, optional 997 x軸のラベル。デフォルト値は"Δ$\\mathregular{CH_{4}}$ (ppm)"です。 998 ylabel: str, optional 999 y軸のラベル。デフォルト値は"Frequency"です。 1000 xlim: tuple[float, float] | None, optional 1001 x軸の範囲。未指定の場合は自動設定されます。 1002 ylim: tuple[float, float] | None, optional 1003 y軸の範囲。未指定の場合は自動設定されます。 1004 save_fig: bool, optional 1005 図の保存を許可するフラグ。デフォルト値はTrueです。 1006 show_fig: bool, optional 1007 図の表示を許可するフラグ。デフォルト値はTrueです。 1008 yscale_log: bool, optional 1009 y軸をlogスケールにするかどうか。デフォルト値はTrueです。 1010 print_bins_analysis: bool, optional 1011 ビンごとの内訳を表示するかどうか。デフォルト値はFalseです。 1012 1013 Returns 1014 ------- 1015 None 1016 1017 Examples 1018 -------- 1019 >>> analyzer = MobileMeasurementAnalyzer(...) 1020 >>> hotspots = analyzer.detect_hotspots() 1021 >>> analyzer.plot_ch4_delta_histogram( 1022 ... hotspots=hotspots, 1023 ... output_dirpath="results", 1024 ... xlim=(0, 5), 1025 ... ylim=(0, 100) 1026 ... ) 1027 """ 1028 if hotspot_colors is None: 1029 hotspot_colors = { 1030 "bio": "blue", 1031 "gas": "red", 1032 "comb": "green", 1033 } 1034 plt.rcParams["font.size"] = fontsize 1035 fig = plt.figure(figsize=figsize, dpi=dpi) 1036 1037 # ホットスポットからデータを抽出 1038 all_ch4_deltas = [] 1039 all_types = [] 1040 for spot in hotspots: 1041 all_ch4_deltas.append(spot.delta_ch4) 1042 all_types.append(spot.type) 1043 1044 # データをNumPy配列に変換 1045 all_ch4_deltas = np.array(all_ch4_deltas) 1046 all_types = np.array(all_types) 1047 1048 # 0.1刻みのビンを作成 1049 if xlim is not None: 1050 bins = np.arange(xlim[0], xlim[1] + 0.1, 0.1) 1051 else: 1052 max_val = np.ceil(np.max(all_ch4_deltas) * 10) / 10 1053 bins = np.arange(0, max_val + 0.1, 0.1) 1054 1055 # タイプごとのヒストグラムデータを計算 1056 hist_data = {} 1057 # HotspotTypeのリテラル値を使用してイテレーション 1058 for type_name in get_args(HotspotType): # typing.get_argsをインポート 1059 mask = all_types == type_name 1060 if np.any(mask): 1061 counts, _ = np.histogram(all_ch4_deltas[mask], bins=bins) 1062 hist_data[type_name] = counts 1063 1064 # ビンごとの内訳を表示 1065 if print_bins_analysis: 1066 self.logger.info("各ビンの内訳:") 1067 print(f"{'Bin Range':15} {'bio':>8} {'gas':>8} {'comb':>8} {'Total':>8}") 1068 print("-" * 50) 1069 1070 for i in range(len(bins) - 1): 1071 bin_start = bins[i] 1072 bin_end = bins[i + 1] 1073 bio_count = hist_data.get("bio", np.zeros(len(bins) - 1))[i] 1074 gas_count = hist_data.get("gas", np.zeros(len(bins) - 1))[i] 1075 comb_count = hist_data.get("comb", np.zeros(len(bins) - 1))[i] 1076 total = bio_count + gas_count + comb_count 1077 1078 if total > 0: # 合計が0のビンは表示しない 1079 print( 1080 f"{bin_start:4.1f}-{bin_end:<8.1f}" 1081 f"{int(bio_count):8d}" 1082 f"{int(gas_count):8d}" 1083 f"{int(comb_count):8d}" 1084 f"{int(total):8d}" 1085 ) 1086 1087 # 積み上げヒストグラムを作成 1088 bottom = np.zeros_like(hist_data.get("bio", np.zeros(len(bins) - 1))) 1089 1090 # HotspotTypeのリテラル値を使用してイテレーション 1091 for type_name in get_args(HotspotType): 1092 if type_name in hist_data: 1093 plt.bar( 1094 bins[:-1], 1095 hist_data[type_name], 1096 width=np.diff(bins)[0], 1097 bottom=bottom, 1098 color=hotspot_colors[type_name], 1099 label=type_name, 1100 alpha=0.6, 1101 align="edge", 1102 ) 1103 bottom += hist_data[type_name] 1104 1105 if yscale_log: 1106 plt.yscale("log") 1107 plt.xlabel(xlabel) 1108 plt.ylabel(ylabel) 1109 plt.legend() 1110 plt.grid(True, which="both", ls="-", alpha=0.2) 1111 1112 # 軸の範囲を設定 1113 if xlim is not None: 1114 plt.xlim(xlim) 1115 if ylim is not None: 1116 plt.ylim(ylim) 1117 1118 # グラフの保存または表示 1119 if save_fig: 1120 if output_dirpath is None: 1121 raise ValueError( 1122 "save_fig = True の場合、 output_dirpath を指定する必要があります。有効なディレクトリパスを指定してください。" 1123 ) 1124 os.makedirs(output_dirpath, exist_ok=True) 1125 output_filepath: str = os.path.join(output_dirpath, output_filename) 1126 plt.savefig(output_filepath, bbox_inches="tight") 1127 if show_fig: 1128 plt.show() 1129 plt.close(fig=fig) 1130 1131 def plot_mapbox( 1132 self, 1133 df: pd.DataFrame, 1134 col_conc: str, 1135 mapbox_access_token: str, 1136 sort_conc_column: bool = True, 1137 output_dirpath: str | Path | None = None, 1138 output_filename: str = "mapbox_plot.html", 1139 col_lat: str = "latitude", 1140 col_lon: str = "longitude", 1141 colorscale: str = "Jet", 1142 center_lat: float | None = None, 1143 center_lon: float | None = None, 1144 zoom: float = 12, 1145 width: int = 700, 1146 height: int = 700, 1147 tick_font_family: str = "Arial", 1148 title_font_family: str = "Arial", 1149 tick_font_size: int = 12, 1150 title_font_size: int = 14, 1151 marker_size: int = 4, 1152 colorbar_title: str | None = None, 1153 value_range: tuple[float, float] | None = None, 1154 save_fig: bool = True, 1155 show_fig: bool = True, 1156 ) -> None: 1157 """ 1158 Mapbox上にデータをプロットします。 1159 1160 Parameters 1161 ---------- 1162 df: pd.DataFrame 1163 プロットするデータを含むDataFrame 1164 col_conc: str 1165 カラーマッピングに使用する列名 1166 mapbox_access_token: str 1167 Mapboxのアクセストークン 1168 sort_conc_column: bool, optional 1169 濃度列をソートするかどうか。デフォルトはTrue 1170 output_dirpath: str | Path | None, optional 1171 出力ディレクトリのパス。デフォルトはNone 1172 output_filename: str, optional 1173 出力ファイル名。デフォルトは"mapbox_plot.html" 1174 col_lat: str, optional 1175 緯度の列名。デフォルトは"latitude" 1176 col_lon: str, optional 1177 経度の列名。デフォルトは"longitude" 1178 colorscale: str, optional 1179 使用するカラースケール。デフォルトは"Jet" 1180 center_lat: float | None, optional 1181 中心緯度。デフォルトはNoneで、self._center_latを使用 1182 center_lon: float | None, optional 1183 中心経度。デフォルトはNoneで、self._center_lonを使用 1184 zoom: float, optional 1185 マップの初期ズームレベル。デフォルトは12 1186 width: int, optional 1187 プロットの幅(ピクセル)。デフォルトは700 1188 height: int, optional 1189 プロットの高さ(ピクセル)。デフォルトは700 1190 tick_font_family: str, optional 1191 カラーバーの目盛りフォントファミリー。デフォルトは"Arial" 1192 title_font_family: str, optional 1193 カラーバーのタイトルフォントファミリー。デフォルトは"Arial" 1194 tick_font_size: int, optional 1195 カラーバーの目盛りフォントサイズ。デフォルトは12 1196 title_font_size: int, optional 1197 カラーバーのタイトルフォントサイズ。デフォルトは14 1198 marker_size: int, optional 1199 マーカーのサイズ。デフォルトは4 1200 colorbar_title: str | None, optional 1201 カラーバーのタイトル。デフォルトはNoneでcol_concを使用 1202 value_range: tuple[float, float] | None, optional 1203 カラーマッピングの範囲。デフォルトはNoneでデータの最小値と最大値を使用 1204 save_fig: bool, optional 1205 図を保存するかどうか。デフォルトはTrue 1206 show_fig: bool, optional 1207 図を表示するかどうか。デフォルトはTrue 1208 1209 Returns 1210 ------- 1211 None 1212 1213 Examples 1214 -------- 1215 >>> analyzer = MobileMeasurementAnalyzer() 1216 >>> df = pd.DataFrame({ 1217 ... 'latitude': [35.681236, 35.681237], 1218 ... 'longitude': [139.767125, 139.767126], 1219 ... 'concentration': [1.2, 1.5] 1220 ... }) 1221 >>> analyzer.plot_mapbox( 1222 ... df=df, 1223 ... col_conc='concentration', 1224 ... mapbox_access_token='your_token_here' 1225 ... ) 1226 """ 1227 df_mapping: pd.DataFrame = df.copy().dropna(subset=[col_conc]) 1228 if sort_conc_column: 1229 df_mapping = df_mapping.sort_values(col_conc) 1230 # 中心座標の設定 1231 center_lat = center_lat if center_lat is not None else self._center_lat 1232 center_lon = center_lon if center_lon is not None else self._center_lon 1233 1234 # カラーマッピングの範囲を設定 1235 cmin, cmax = 0, 0 1236 if value_range is None: 1237 cmin = df_mapping[col_conc].min() 1238 cmax = df_mapping[col_conc].max() 1239 else: 1240 cmin, cmax = value_range 1241 1242 # カラーバーのタイトルを設定 1243 title_text = colorbar_title if colorbar_title is not None else col_conc 1244 1245 # Scattermapboxのデータを作成 1246 scatter_data = go.Scattermapbox( 1247 lat=df_mapping[col_lat], 1248 lon=df_mapping[col_lon], 1249 text=df_mapping[col_conc].astype(str), 1250 hoverinfo="text", 1251 mode="markers", 1252 marker={ 1253 "color": df_mapping[col_conc], 1254 "size": marker_size, 1255 "reversescale": False, 1256 "autocolorscale": False, 1257 "colorscale": colorscale, 1258 "cmin": cmin, 1259 "cmax": cmax, 1260 "colorbar": { 1261 "tickformat": "3.2f", 1262 "outlinecolor": "black", 1263 "outlinewidth": 1.5, 1264 "ticks": "outside", 1265 "ticklen": 7, 1266 "tickwidth": 1.5, 1267 "tickcolor": "black", 1268 "tickfont": { 1269 "family": tick_font_family, 1270 "color": "black", 1271 "size": tick_font_size, 1272 }, 1273 "title": { 1274 "text": title_text, 1275 "side": "top", 1276 }, # カラーバーのタイトルを設定 1277 "titlefont": { 1278 "family": title_font_family, 1279 "color": "black", 1280 "size": title_font_size, 1281 }, 1282 }, 1283 }, 1284 ) 1285 1286 # レイアウトの設定 1287 layout = go.Layout( 1288 width=width, 1289 height=height, 1290 showlegend=False, 1291 mapbox={ 1292 "accesstoken": mapbox_access_token, 1293 "center": {"lat": center_lat, "lon": center_lon}, 1294 "zoom": zoom, 1295 }, 1296 ) 1297 1298 # 図の作成 1299 fig = go.Figure(data=[scatter_data], layout=layout) 1300 1301 # 図の保存 1302 if save_fig: 1303 # 保存時の出力ディレクトリチェック 1304 if output_dirpath is None: 1305 raise ValueError( 1306 "save_fig=Trueの場合、output_dirpathを指定する必要があります。" 1307 ) 1308 os.makedirs(output_dirpath, exist_ok=True) 1309 output_filepath = os.path.join(output_dirpath, output_filename) 1310 pyo.plot(fig, filename=output_filepath, auto_open=False) 1311 self.logger.info(f"Mapboxプロットを保存しました: {output_filepath}") 1312 # 図の表示 1313 if show_fig: 1314 pyo.iplot(fig) 1315 1316 def plot_scatter_c2c1( 1317 self, 1318 hotspots: list[HotspotData], 1319 output_dirpath: str | Path | None = None, 1320 output_filename: str = "scatter_c2c1.png", 1321 figsize: tuple[float, float] = (4, 4), 1322 dpi: float | None = 350, 1323 hotspot_colors: dict[HotspotType, str] | None = None, 1324 hotspot_labels: dict[HotspotType, str] | None = None, 1325 fontsize: float = 12, 1326 xlim: tuple[float, float] = (0, 2.0), 1327 ylim: tuple[float, float] = (0, 50), 1328 xlabel: str = "Δ$\\mathregular{CH_{4}}$ (ppm)", 1329 ylabel: str = "Δ$\\mathregular{C_{2}H_{6}}$ (ppb)", 1330 xscale_log: bool = False, 1331 yscale_log: bool = False, 1332 add_legend: bool = True, 1333 save_fig: bool = True, 1334 show_fig: bool = True, 1335 add_ratio_labels: bool = True, 1336 ratio_labels: dict[float, tuple[float, float, str]] | None = None, 1337 ) -> None: 1338 """ 1339 検出されたホットスポットのΔC2H6とΔCH4の散布図をプロットします。 1340 1341 Parameters 1342 ---------- 1343 hotspots: list[HotspotData] 1344 プロットするホットスポットのリスト 1345 output_dirpath: str | Path | None, optional 1346 保存先のディレクトリパス。未指定の場合はNoneとなります。 1347 output_filename: str, optional 1348 保存するファイル名。デフォルト値は"scatter_c2c1.png"です。 1349 figsize: tuple[float, float], optional 1350 図のサイズ。デフォルト値は(4, 4)です。 1351 dpi: float | None, optional 1352 解像度。デフォルト値は350です。 1353 hotspot_colors: dict[HotspotType, str] | None, optional 1354 ホットスポットの色を定義する辞書。未指定の場合は{"bio": "blue", "gas": "red", "comb": "green"}が使用されます。 1355 hotspot_labels: dict[HotspotType, str] | None, optional 1356 ホットスポットのラベルを定義する辞書。未指定の場合は{"bio": "bio", "gas": "gas", "comb": "comb"}が使用されます。 1357 fontsize: float, optional 1358 フォントサイズ。デフォルト値は12です。 1359 xlim: tuple[float, float], optional 1360 x軸の範囲。デフォルト値は(0, 2.0)です。 1361 ylim: tuple[float, float], optional 1362 y軸の範囲。デフォルト値は(0, 50)です。 1363 xlabel: str, optional 1364 x軸のラベル。デフォルト値は"Δ$\\mathregular{CH_{4}}$ (ppm)"です。 1365 ylabel: str, optional 1366 y軸のラベル。デフォルト値は"Δ$\\mathregular{C_{2}H_{6}}$ (ppb)"です。 1367 xscale_log: bool, optional 1368 x軸を対数スケールにするかどうか。デフォルト値はFalseです。 1369 yscale_log: bool, optional 1370 y軸を対数スケールにするかどうか。デフォルト値はFalseです。 1371 add_legend: bool, optional 1372 凡例を追加するかどうか。デフォルト値はTrueです。 1373 save_fig: bool, optional 1374 図の保存を許可するフラグ。デフォルト値はTrueです。 1375 show_fig: bool, optional 1376 図の表示を許可するフラグ。デフォルト値はTrueです。 1377 add_ratio_labels: bool, optional 1378 比率線を表示するかどうか。デフォルト値はTrueです。 1379 ratio_labels: dict[float, tuple[float, float, str]] | None, optional 1380 比率線とラベルの設定。キーは比率値、値は(x位置, y位置, ラベルテキスト)のタプル。 1381 未指定の場合は以下のデフォルト値が使用されます: 1382 ```python 1383 { 1384 0.001: (1.25, 2, "0.001"), 1385 0.005: (1.25, 8, "0.005"), 1386 0.010: (1.25, 15, "0.01"), 1387 0.020: (1.25, 30, "0.02"), 1388 0.030: (1.0, 40, "0.03"), 1389 0.076: (0.20, 42, "0.076 (Osaka)") 1390 } 1391 ``` 1392 1393 Returns 1394 ------- 1395 None 1396 1397 Examples 1398 -------- 1399 >>> analyzer = MobileMeasurementAnalyzer() 1400 >>> hotspots = analyzer.analyze_hotspots() 1401 >>> analyzer.plot_scatter_c2c1( 1402 ... hotspots=hotspots, 1403 ... output_dirpath="output", 1404 ... xlim=(0, 5), 1405 ... ylim=(0, 100) 1406 ... ) 1407 """ 1408 # デフォルト値の設定 1409 if hotspot_colors is None: 1410 hotspot_colors = { 1411 "bio": "blue", 1412 "gas": "red", 1413 "comb": "green", 1414 } 1415 if hotspot_labels is None: 1416 hotspot_labels = { 1417 "bio": "bio", 1418 "gas": "gas", 1419 "comb": "comb", 1420 } 1421 if ratio_labels is None: 1422 ratio_labels = { 1423 0.001: (1.25, 2, "0.001"), 1424 0.005: (1.25, 8, "0.005"), 1425 0.010: (1.25, 15, "0.01"), 1426 0.020: (1.25, 30, "0.02"), 1427 0.030: (1.0, 40, "0.03"), 1428 0.076: (0.20, 42, "0.076 (Osaka)"), 1429 } 1430 plt.rcParams["font.size"] = fontsize 1431 fig = plt.figure(figsize=figsize, dpi=dpi) 1432 1433 # タイプごとのデータを収集 1434 type_data: dict[HotspotType, list[tuple[float, float]]] = { 1435 "bio": [], 1436 "gas": [], 1437 "comb": [], 1438 } 1439 for spot in hotspots: 1440 type_data[spot.type].append((spot.delta_ch4, spot.delta_c2h6)) 1441 1442 # タイプごとにプロット(データが存在する場合のみ) 1443 for spot_type, data in type_data.items(): 1444 if data: # データが存在する場合のみプロット 1445 ch4_values, c2h6_values = zip(*data, strict=True) 1446 plt.plot( 1447 ch4_values, 1448 c2h6_values, 1449 "o", 1450 c=hotspot_colors[spot_type], 1451 alpha=0.5, 1452 ms=2, 1453 label=hotspot_labels[spot_type], 1454 ) 1455 1456 # プロット後、軸の設定前に比率の線を追加 1457 x = np.array([0, 5]) 1458 base_ch4 = 0.0 1459 base = 0.0 1460 1461 # 各比率に対して線を引く 1462 if ratio_labels is not None: 1463 if not add_ratio_labels: 1464 raise ValueError( 1465 "ratio_labels に基づいて比率線を描画する場合は、 add_ratio_labels = True を指定してください。" 1466 ) 1467 for ratio, (x_pos, y_pos, label) in ratio_labels.items(): 1468 y = (x - base_ch4) * 1000 * ratio + base 1469 plt.plot(x, y, "-", c="black", alpha=0.5) 1470 plt.text(x_pos, y_pos, label) 1471 1472 # 軸の設定 1473 if xscale_log: 1474 plt.xscale("log") 1475 if yscale_log: 1476 plt.yscale("log") 1477 1478 plt.xlim(xlim) 1479 plt.ylim(ylim) 1480 plt.xlabel(xlabel) 1481 plt.ylabel(ylabel) 1482 if add_legend: 1483 plt.legend() 1484 1485 # グラフの保存または表示 1486 if save_fig: 1487 if output_dirpath is None: 1488 raise ValueError( 1489 "save_fig = True の場合、 output_dirpath を指定する必要があります。有効なディレクトリパスを指定してください。" 1490 ) 1491 output_filepath: str = os.path.join(output_dirpath, output_filename) 1492 plt.savefig(output_filepath, bbox_inches="tight") 1493 self.logger.info(f"散布図を保存しました: {output_filepath}") 1494 if show_fig: 1495 plt.show() 1496 plt.close(fig=fig) 1497 1498 def plot_conc_timeseries( 1499 self, 1500 output_dirpath: str | Path | None = None, 1501 output_filename: str = "timeseries.png", 1502 figsize: tuple[float, float] = (8, 4), 1503 dpi: float | None = 350, 1504 save_fig: bool = True, 1505 show_fig: bool = True, 1506 col_ch4: str = "ch4_ppm", 1507 col_c2h6: str = "c2h6_ppb", 1508 col_h2o: str = "h2o_ppm", 1509 ylim_ch4: tuple[float, float] | None = None, 1510 ylim_c2h6: tuple[float, float] | None = None, 1511 ylim_h2o: tuple[float, float] | None = None, 1512 yscale_log_ch4: bool = False, 1513 yscale_log_c2h6: bool = False, 1514 yscale_log_h2o: bool = False, 1515 font_size: float = 12, 1516 label_pad: float = 10, 1517 line_color: str = "black", 1518 ) -> None: 1519 """ 1520 CH4、C2H6、H2Oの時系列データをプロットします。 1521 1522 Parameters 1523 ---------- 1524 output_dirpath: str | Path | None, optional 1525 保存先のディレクトリを指定します。save_fig=Trueの場合は必須です。デフォルト値はNoneです。 1526 output_filename: str, optional 1527 保存するファイル名を指定します。デフォルト値は"timeseries.png"です。 1528 figsize: tuple[float, float], optional 1529 図のサイズを指定します。デフォルト値は(8, 4)です。 1530 dpi: float | None, optional 1531 図の解像度を指定します。デフォルト値は350です。 1532 save_fig: bool, optional 1533 図を保存するかどうかを指定します。デフォルト値はTrueです。 1534 show_fig: bool, optional 1535 図を表示するかどうかを指定します。デフォルト値はTrueです。 1536 col_ch4: str, optional 1537 CH4データの列名を指定します。デフォルト値は"ch4_ppm"です。 1538 col_c2h6: str, optional 1539 C2H6データの列名を指定します。デフォルト値は"c2h6_ppb"です。 1540 col_h2o: str, optional 1541 H2Oデータの列名を指定します。デフォルト値は"h2o_ppm"です。 1542 ylim_ch4: tuple[float, float] | None, optional 1543 CH4プロットのy軸範囲を指定します。デフォルト値はNoneです。 1544 ylim_c2h6: tuple[float, float] | None, optional 1545 C2H6プロットのy軸範囲を指定します。デフォルト値はNoneです。 1546 ylim_h2o: tuple[float, float] | None, optional 1547 H2Oプロットのy軸範囲を指定します。デフォルト値はNoneです。 1548 yscale_log_ch4: bool, optional 1549 CH4データのy軸を対数スケールで表示するかどうかを指定します。デフォルト値はFalseです。 1550 yscale_log_c2h6: bool, optional 1551 C2H6データのy軸を対数スケールで表示するかどうかを指定します。デフォルト値はFalseです。 1552 yscale_log_h2o: bool, optional 1553 H2Oデータのy軸を対数スケールで表示するかどうかを指定します。デフォルト値はFalseです。 1554 font_size: float, optional 1555 プロット全体のフォントサイズを指定します。デフォルト値は12です。 1556 label_pad: float, optional 1557 y軸ラベルとプロットの間隔を指定します。デフォルト値は10です。 1558 line_color: str, optional 1559 プロットする線の色を指定します。デフォルト値は"black"です。 1560 1561 Returns 1562 ------- 1563 None 1564 1565 Examples 1566 -------- 1567 >>> analyzer = MobileMeasurementAnalyzer(df) 1568 >>> analyzer.plot_conc_timeseries( 1569 ... output_dirpath="output", 1570 ... ylim_ch4=(1.8, 2.5), 1571 ... ylim_c2h6=(0, 100), 1572 ... ylim_h2o=(0, 20000) 1573 ... ) 1574 """ 1575 # プロットパラメータの設定 1576 plt.rcParams.update( 1577 { 1578 "font.size": font_size, 1579 "axes.labelsize": font_size, 1580 "axes.titlesize": font_size, 1581 "xtick.labelsize": font_size, 1582 "ytick.labelsize": font_size, 1583 } 1584 ) 1585 # timestampをインデックスとして設定 1586 df_internal = self.df.copy() 1587 df_internal.set_index("timestamp", inplace=True) 1588 1589 # プロットの作成 1590 fig = plt.figure(figsize=figsize, dpi=dpi) 1591 1592 # CH4プロット 1593 ax1 = fig.add_subplot(3, 1, 1) 1594 ax1.plot(df_internal.index, df_internal[col_ch4], c=line_color) 1595 if ylim_ch4: 1596 ax1.set_ylim(ylim_ch4) 1597 if yscale_log_ch4: 1598 ax1.set_yscale("log") 1599 ax1.set_ylabel("$\\mathregular{CH_{4}}$ (ppm)", labelpad=label_pad) 1600 ax1.grid(True, alpha=0.3) 1601 1602 # C2H6プロット 1603 ax2 = fig.add_subplot(3, 1, 2) 1604 ax2.plot(df_internal.index, df_internal[col_c2h6], c=line_color) 1605 if ylim_c2h6: 1606 ax2.set_ylim(ylim_c2h6) 1607 if yscale_log_c2h6: 1608 ax2.set_yscale("log") 1609 ax2.set_ylabel("$\\mathregular{C_{2}H_{6}}$ (ppb)", labelpad=label_pad) 1610 ax2.grid(True, alpha=0.3) 1611 1612 # H2Oプロット 1613 ax3 = fig.add_subplot(3, 1, 3) 1614 ax3.plot(df_internal.index, df_internal[col_h2o], c=line_color) 1615 if ylim_h2o: 1616 ax3.set_ylim(ylim_h2o) 1617 if yscale_log_h2o: 1618 ax3.set_yscale("log") 1619 ax3.set_ylabel("$\\mathregular{H_{2}O}$ (ppm)", labelpad=label_pad) 1620 ax3.grid(True, alpha=0.3) 1621 1622 # x軸のフォーマット調整 1623 for ax in [ax1, ax2, ax3]: 1624 ax.xaxis.set_major_formatter(mdates.DateFormatter("%H:%M")) 1625 # 軸のラベルとグリッド線の調整 1626 ax.tick_params(axis="both", which="major", labelsize=font_size) 1627 ax.grid(True, alpha=0.3) 1628 1629 # サブプロット間の間隔調整 1630 plt.subplots_adjust(wspace=0.38, hspace=0.38) 1631 1632 # 図の保存 1633 if save_fig: 1634 if output_dirpath is None: 1635 raise ValueError( 1636 "save_fig = True の場合、 output_dirpath を指定する必要があります。有効なディレクトリパスを指定してください。" 1637 ) 1638 os.makedirs(output_dirpath, exist_ok=True) 1639 output_filepath = os.path.join(output_dirpath, output_filename) 1640 plt.savefig(output_filepath, bbox_inches="tight") 1641 1642 if show_fig: 1643 plt.show() 1644 plt.close(fig=fig) 1645 1646 def plot_conc_timeseries_with_hotspots( 1647 self, 1648 hotspots: list[HotspotData] | None = None, 1649 output_dirpath: str | Path | None = None, 1650 output_filename: str = "timeseries_with_hotspots.png", 1651 figsize: tuple[float, float] = (8, 6), 1652 dpi: float | None = 350, 1653 save_fig: bool = True, 1654 show_fig: bool = True, 1655 col_ch4: str = "ch4_ppm", 1656 col_c2h6: str = "c2h6_ppb", 1657 col_h2o: str = "h2o_ppm", 1658 add_legend: bool = True, 1659 legend_bbox_to_anchor: tuple[float, float] = (0.5, 0.05), 1660 legend_ncol: int | None = None, 1661 font_size: float = 12, 1662 label_pad: float = 10, 1663 line_color: str = "black", 1664 hotspot_colors: dict[HotspotType, str] | None = None, 1665 hotspot_markerscale: float = 1, 1666 hotspot_size: int = 10, 1667 time_margin_minutes: float = 2.0, 1668 ylim_ch4: tuple[float, float] | None = None, 1669 ylim_c2h6: tuple[float, float] | None = None, 1670 ylim_h2o: tuple[float, float] | None = None, 1671 ylim_ratio: tuple[float, float] | None = None, 1672 yscale_log_ch4: bool = False, 1673 yscale_log_c2h6: bool = False, 1674 yscale_log_h2o: bool = False, 1675 yscale_log_ratio: bool = False, 1676 ylabel_ch4: str = "$\\mathregular{CH_{4}}$ (ppm)", 1677 ylabel_c2h6: str = "$\\mathregular{C_{2}H_{6}}$ (ppb)", 1678 ylabel_h2o: str = "$\\mathregular{H_{2}O}$ (ppm)", 1679 ylabel_ratio: str = "ΔC$_2$H$_6$/ΔCH$_4$\n(ppb ppm$^{-1}$)", 1680 ) -> None: 1681 """ 1682 時系列データとホットスポットをプロットします。 1683 1684 Parameters 1685 ---------- 1686 hotspots: list[HotspotData] | None, optional 1687 表示するホットスポットのリスト。デフォルトはNoneで、この場合ホットスポットは表示されません。 1688 output_dirpath: str | Path | None, optional 1689 出力先ディレクトリのパス。save_fig=Trueの場合は必須です。デフォルトはNoneです。 1690 output_filename: str, optional 1691 保存するファイル名。デフォルトは"timeseries_with_hotspots.png"です。 1692 figsize: tuple[float, float], optional 1693 図のサイズ。デフォルトは(8, 6)です。 1694 dpi: float | None, optional 1695 図の解像度。デフォルトは350です。 1696 save_fig: bool, optional 1697 図を保存するかどうか。デフォルトはTrueです。 1698 show_fig: bool, optional 1699 図を表示するかどうか。デフォルトはTrueです。 1700 col_ch4: str, optional 1701 CH4データのカラム名。デフォルトは"ch4_ppm"です。 1702 col_c2h6: str, optional 1703 C2H6データのカラム名。デフォルトは"c2h6_ppb"です。 1704 col_h2o: str, optional 1705 H2Oデータのカラム名。デフォルトは"h2o_ppm"です。 1706 add_legend: bool, optional 1707 ホットスポットの凡例を表示するかどうか。デフォルトはTrueです。 1708 legend_bbox_to_anchor: tuple[float, float], optional 1709 ホットスポットの凡例の位置。デフォルトは(0.5, 0.05)です。 1710 legend_ncol: int | None, optional 1711 凡例のカラム数。デフォルトはNoneで、この場合ホットスポットの種類数に基づいて一行で表示します。 1712 font_size: float, optional 1713 基本フォントサイズ。デフォルトは12です。 1714 label_pad: float, optional 1715 y軸ラベルのパディング。デフォルトは10です。 1716 line_color: str, optional 1717 線の色。デフォルトは"black"です。 1718 hotspot_colors: dict[HotspotType, str] | None, optional 1719 ホットスポットタイプごとの色指定。デフォルトはNoneで、{"bio": "blue", "gas": "red", "comb": "green"}が使用されます。 1720 hotspot_markerscale: float, optional 1721 ホットスポットの凡例でのサイズ。hotspot_sizeに対する相対値。デフォルトは1です。 1722 hotspot_size: int, optional 1723 ホットスポットの図でのサイズ。デフォルトは10です。 1724 time_margin_minutes: float, optional 1725 プロットの時間軸の余白(分)。デフォルトは2.0分です。 1726 ylim_ch4: tuple[float, float] | None, optional 1727 CH4プロットのy軸範囲。デフォルトはNoneで、自動設定されます。 1728 ylim_c2h6: tuple[float, float] | None, optional 1729 C2H6プロットのy軸範囲。デフォルトはNoneで、自動設定されます。 1730 ylim_h2o: tuple[float, float] | None, optional 1731 H2Oプロットのy軸範囲。デフォルトはNoneで、自動設定されます。 1732 ylim_ratio: tuple[float, float] | None, optional 1733 比率プロットのy軸範囲。デフォルトはNoneで、自動設定されます。 1734 yscale_log_ch4: bool, optional 1735 CH4データのy軸を対数スケールで表示するかどうか。デフォルトはFalseです。 1736 yscale_log_c2h6: bool, optional 1737 C2H6データのy軸を対数スケールで表示するかどうか。デフォルトはFalseです。 1738 yscale_log_h2o: bool, optional 1739 H2Oデータのy軸を対数スケールで表示するかどうか。デフォルトはFalseです。 1740 yscale_log_ratio: bool, optional 1741 比率データのy軸を対数スケールで表示するかどうか。デフォルトはFalseです。 1742 ylabel_ch4: str, optional 1743 CH4プロットのy軸ラベル。デフォルトは"$\\mathregular{CH_{4}}$ (ppm)"です。 1744 ylabel_c2h6: str, optional 1745 C2H6プロットのy軸ラベル。デフォルトは"$\\mathregular{C_{2}H_{6}}$ (ppb)"です。 1746 ylabel_h2o: str, optional 1747 H2Oプロットのy軸ラベル。デフォルトは"$\\mathregular{H_{2}O}$ (ppm)"です。 1748 ylabel_ratio: str, optional 1749 比率プロットのy軸ラベル。デフォルトは"ΔC$_2$H$_6$/ΔCH$_4$\\n(ppb ppm$^{-1}$)"です。 1750 1751 Examples 1752 -------- 1753 基本的な使用方法: 1754 >>> analyzer = MobileMeasurementAnalyzer(df) 1755 >>> analyzer.plot_conc_timeseries_with_hotspots() 1756 1757 ホットスポットを指定して保存する: 1758 >>> hotspots = [HotspotData(...), HotspotData(...)] 1759 >>> analyzer.plot_conc_timeseries_with_hotspots( 1760 ... hotspots=hotspots, 1761 ... output_dirpath="output", 1762 ... save_fig=True 1763 ... ) 1764 1765 カスタマイズした表示: 1766 >>> analyzer.plot_conc_timeseries_with_hotspots( 1767 ... figsize=(12, 8), 1768 ... ylim_ch4=(1.8, 2.5), 1769 ... yscale_log_c2h6=True, 1770 ... hotspot_colors={"bio": "purple", "gas": "orange"} 1771 ... ) 1772 """ 1773 if hotspot_colors is None: 1774 hotspot_colors = {"bio": "blue", "gas": "red", "comb": "green"} 1775 # プロットパラメータの設定 1776 plt.rcParams.update( 1777 { 1778 "font.size": font_size, 1779 "axes.labelsize": font_size, 1780 "axes.titlesize": font_size, 1781 "xtick.labelsize": font_size, 1782 "ytick.labelsize": font_size, 1783 } 1784 ) 1785 1786 # timestampをインデックスとして設定 1787 df_internal = self.df.copy() 1788 df_internal.set_index("timestamp", inplace=True) 1789 1790 # プロットの作成 1791 fig = plt.figure(figsize=figsize, dpi=dpi) 1792 1793 # サブプロットのグリッドを作成 (4行1列) 1794 gs = gridspec.GridSpec(4, 1, height_ratios=[1, 1, 1, 1]) 1795 1796 # 時間軸の範囲を設定(余白付き) 1797 time_min = df_internal.index.min() 1798 time_max = df_internal.index.max() 1799 time_margin = pd.Timedelta(minutes=time_margin_minutes) 1800 plot_time_min = time_min - time_margin 1801 plot_time_max = time_max + time_margin 1802 1803 # CH4プロット 1804 ax1 = fig.add_subplot(gs[0]) 1805 ax1.plot(df_internal.index, df_internal[col_ch4], c=line_color) 1806 if ylim_ch4: 1807 ax1.set_ylim(ylim_ch4) 1808 if yscale_log_ch4: 1809 ax1.set_yscale("log") 1810 ax1.set_ylabel(ylabel_ch4, labelpad=label_pad) 1811 ax1.grid(True, alpha=0.3) 1812 ax1.set_xlim(plot_time_min, plot_time_max) 1813 1814 # C2H6プロット 1815 ax2 = fig.add_subplot(gs[1]) 1816 ax2.plot(df_internal.index, df_internal[col_c2h6], c=line_color) 1817 if ylim_c2h6: 1818 ax2.set_ylim(ylim_c2h6) 1819 if yscale_log_c2h6: 1820 ax2.set_yscale("log") 1821 ax2.set_ylabel(ylabel_c2h6, labelpad=label_pad) 1822 ax2.grid(True, alpha=0.3) 1823 ax2.set_xlim(plot_time_min, plot_time_max) 1824 1825 # H2Oプロット 1826 ax3 = fig.add_subplot(gs[2]) 1827 ax3.plot(df_internal.index, df_internal[col_h2o], c=line_color) 1828 if ylim_h2o: 1829 ax3.set_ylim(ylim_h2o) 1830 if yscale_log_h2o: 1831 ax3.set_yscale("log") 1832 ax3.set_ylabel(ylabel_h2o, labelpad=label_pad) 1833 ax3.grid(True, alpha=0.3) 1834 ax3.set_xlim(plot_time_min, plot_time_max) 1835 1836 # ホットスポットの比率プロット 1837 ax4 = fig.add_subplot(gs[3]) 1838 1839 if hotspots: 1840 # ホットスポットをDataFrameに変換 1841 hotspot_df = pd.DataFrame( 1842 [ 1843 { 1844 "timestamp": pd.to_datetime(spot.timestamp), 1845 "delta_ratio": spot.delta_ratio, 1846 "type": spot.type, 1847 } 1848 for spot in hotspots 1849 ] 1850 ) 1851 1852 # タイプごとにプロット 1853 for spot_type in set(hotspot_df["type"]): 1854 type_data = hotspot_df[hotspot_df["type"] == spot_type] 1855 1856 # 点をプロット 1857 ax4.scatter( 1858 type_data["timestamp"], 1859 type_data["delta_ratio"], 1860 c=hotspot_colors.get(spot_type, "black"), 1861 label=spot_type, 1862 alpha=0.6, 1863 s=hotspot_size, 1864 ) 1865 1866 ax4.set_ylabel(ylabel_ratio, labelpad=label_pad) 1867 if ylim_ratio: 1868 ax4.set_ylim(ylim_ratio) 1869 if yscale_log_ratio: 1870 ax4.set_yscale("log") 1871 ax4.grid(True, alpha=0.3) 1872 ax4.set_xlim(plot_time_min, plot_time_max) # 他のプロットと同じ時間範囲を設定 1873 1874 # 凡例を図の下部に配置 1875 if hotspots and add_legend: 1876 ncol = ( 1877 legend_ncol if legend_ncol is not None else len(set(hotspot_df["type"])) 1878 ) 1879 # markerscaleは元のサイズに対する倍率を指定するため、 1880 # 目的のサイズ(100)をプロットのマーカーサイズで割ることで、適切な倍率を計算しています 1881 fig.legend( 1882 bbox_to_anchor=legend_bbox_to_anchor, 1883 loc="upper center", 1884 ncol=ncol, 1885 columnspacing=1.0, 1886 handletextpad=0.5, 1887 markerscale=hotspot_markerscale, 1888 ) 1889 1890 # x軸のフォーマット調整(全てのサブプロットで共通) 1891 for ax in [ax1, ax2, ax3, ax4]: 1892 ax.xaxis.set_major_formatter(mdates.DateFormatter("%H:%M")) 1893 ax.xaxis.set_major_locator(mdates.AutoDateLocator()) 1894 ax.tick_params(axis="both", which="major", labelsize=font_size) 1895 ax.grid(True, alpha=0.3) 1896 1897 # サブプロット間の間隔調整と凡例のためのスペース確保 1898 plt.subplots_adjust(hspace=0.38, bottom=0.12) # bottomを0.15から0.12に変更 1899 1900 # 図の保存 1901 if save_fig: 1902 if output_dirpath is None: 1903 raise ValueError( 1904 "save_fig = True の場合、 output_dirpath を指定する必要があります。有効なディレクトリパスを指定してください。" 1905 ) 1906 os.makedirs(output_dirpath, exist_ok=True) 1907 output_filepath = os.path.join(output_dirpath, output_filename) 1908 plt.savefig(output_filepath, bbox_inches="tight", dpi=dpi) 1909 1910 if show_fig: 1911 plt.show() 1912 plt.close(fig=fig) 1913 1914 def _detect_hotspots( 1915 self, 1916 df: pd.DataFrame, 1917 ch4_enhance_threshold: float, 1918 ) -> list[HotspotData]: 1919 """ 1920 シンプル化したホットスポット検出 1921 1922 Parameters 1923 ---------- 1924 df: pd.DataFrame 1925 入力データフレーム 1926 ch4_enhance_threshold: float 1927 CH4増加の閾値 1928 1929 Returns 1930 ---------- 1931 list[HotspotData] 1932 検出されたホットスポットのリスト 1933 """ 1934 hotspots: list[HotspotData] = [] 1935 1936 # CH4増加量が閾値を超えるデータポイントを抽出 1937 enhanced_mask = df["ch4_ppm_delta"] >= ch4_enhance_threshold 1938 1939 if enhanced_mask.any(): 1940 timestamp = df["timestamp"][enhanced_mask] 1941 lat = df["latitude"][enhanced_mask] 1942 lon = df["longitude"][enhanced_mask] 1943 delta_ratio = df["c2c1_ratio_delta"][enhanced_mask] 1944 delta_ch4 = df["ch4_ppm_delta"][enhanced_mask] 1945 delta_c2h6 = df["c2h6_ppb_delta"][enhanced_mask] 1946 1947 # 各ポイントに対してホットスポットを作成 1948 for i in range(len(lat)): 1949 if pd.notna(delta_ratio.iloc[i]): 1950 current_lat = lat.iloc[i] 1951 current_lon = lon.iloc[i] 1952 correlation = df["c1c2_correlation"].iloc[i] 1953 1954 # 比率に基づいてタイプを決定 1955 spot_type: HotspotType = "bio" 1956 if delta_ratio.iloc[i] >= 100: 1957 spot_type = "comb" 1958 elif delta_ratio.iloc[i] >= 5: 1959 spot_type = "gas" 1960 1961 angle: float = MobileMeasurementAnalyzer._calculate_angle( 1962 lat=current_lat, 1963 lon=current_lon, 1964 center_lat=self._center_lat, 1965 center_lon=self._center_lon, 1966 ) 1967 section: int = self._determine_section(angle) 1968 # 値を取得してpd.Timestampに変換 1969 timestamp_raw = pd.Timestamp(str(timestamp.values[i])) 1970 1971 hotspots.append( 1972 HotspotData( 1973 timestamp=timestamp_raw.strftime("%Y-%m-%d %H:%M:%S"), 1974 angle=angle, 1975 avg_lat=current_lat, 1976 avg_lon=current_lon, 1977 delta_ch4=delta_ch4.iloc[i], 1978 delta_c2h6=delta_c2h6.iloc[i], 1979 correlation=max(-1, min(1, correlation)), 1980 delta_ratio=delta_ratio.iloc[i], 1981 section=section, 1982 type=spot_type, 1983 ) 1984 ) 1985 1986 return hotspots 1987 1988 def _determine_section(self, angle: float) -> int: 1989 """ 1990 角度に基づいて所属する区画を特定します。 1991 1992 Parameters 1993 ---------- 1994 angle: float 1995 計算された角度 1996 1997 Returns 1998 ---------- 1999 int 2000 区画番号(0-based-index) 2001 """ 2002 for section_num, (start, end) in self._sections.items(): 2003 if start <= angle < end: 2004 return section_num 2005 # -180度の場合は最後の区画に含める 2006 return self._num_sections - 1 2007 2008 def _load_all_combined_data( 2009 self, input_configs: list[MobileMeasurementConfig] 2010 ) -> pd.DataFrame: 2011 """ 2012 全入力ファイルのデータを読み込み、結合したデータフレームを返します。 2013 2014 Parameters 2015 ---------- 2016 input_configs: list[MobileMeasurementConfig] 2017 読み込むファイルの設定リスト。 2018 2019 Returns 2020 ---------- 2021 pd.DataFrame 2022 読み込まれたすべてのデータを結合したデータフレーム。 2023 """ 2024 dfs: list[pd.DataFrame] = [] 2025 for config in input_configs: 2026 df, _ = self._load_data(config) 2027 dfs.append(df) 2028 return pd.concat(dfs, ignore_index=True) 2029 2030 def _load_data( 2031 self, 2032 config: MobileMeasurementConfig, 2033 columns_to_shift: list[str] | None = None, 2034 col_timestamp: str = "timestamp", 2035 col_latitude: str = "latitude", 2036 col_longitude: str = "longitude", 2037 ) -> tuple[pd.DataFrame, str]: 2038 """ 2039 測定データを読み込み、前処理を行うメソッド。 2040 2041 Parameters 2042 ---------- 2043 config: MobileMeasurementConfig 2044 入力ファイルの設定を含むオブジェクト。ファイルパス、遅れ時間、サンプリング周波数、補正タイプなどの情報を持つ。 2045 columns_to_shift: list[str] | None, optional 2046 シフトを適用するカラム名のリスト。Noneの場合のデフォルトは["ch4_ppm", "c2h6_ppb", "h2o_ppm"]で、これらのカラムに対して遅れ時間の補正が行われる。 2047 col_timestamp: str, optional 2048 タイムスタンプのカラム名。デフォルトは"timestamp"。 2049 col_latitude: str, optional 2050 緯度のカラム名。デフォルトは"latitude"。 2051 col_longitude: str, optional 2052 経度のカラム名。デフォルトは"longitude"。 2053 2054 Returns 2055 ---------- 2056 tuple[pd.DataFrame, str] 2057 読み込まれたデータフレームとそのソース名を含むタプル。データフレームは前処理が施されており、ソース名はファイル名から抽出されたもの。 2058 """ 2059 if columns_to_shift is None: 2060 columns_to_shift = ["ch4_ppm", "c2h6_ppb", "h2o_ppm"] 2061 source_name: str = MobileMeasurementAnalyzer.extract_source_name_from_path( 2062 config.path 2063 ) 2064 df: pd.DataFrame = pd.read_csv(config.path, na_values=self._na_values) 2065 2066 # カラム名の標準化(測器に依存しない汎用的な名前に変更) 2067 df = df.rename(columns=self._column_mapping) 2068 df[col_timestamp] = pd.to_datetime(df[col_timestamp]) 2069 # インデックスを設定(元のtimestampカラムは保持) 2070 df = df.set_index(col_timestamp, drop=False) 2071 2072 if config.lag < 0: 2073 raise ValueError( 2074 f"Invalid lag value: {config.lag}. Must be a non-negative float." 2075 ) 2076 2077 # サンプリング周波数に応じてシフト量を調整 2078 shift_periods: int = -int(config.lag * config.fs) # fsを掛けて補正 2079 2080 # 遅れ時間の補正 2081 for col in columns_to_shift: 2082 df[col] = df[col].shift(shift_periods) 2083 2084 # 緯度経度とシフト対象カラムのnanを一度に削除 2085 df = df.dropna(subset=[col_latitude, col_longitude, *columns_to_shift]) 2086 2087 # 水蒸気補正の適用 2088 if config.h2o_correction is not None and all( 2089 x is not None 2090 for x in [ 2091 config.h2o_correction.coef_b, 2092 config.h2o_correction.coef_c, 2093 ] 2094 ): 2095 h2o_correction: H2OCorrectionConfig = config.h2o_correction 2096 df = CorrectingUtils.correct_h2o_interference( 2097 df=df, 2098 coef_b=float(h2o_correction.coef_b), # type: ignore 2099 coef_c=float(h2o_correction.coef_c), # type: ignore 2100 h2o_ppm_threshold=h2o_correction.h2o_ppm_threshold, 2101 target_h2o_ppm=h2o_correction.target_h2o_ppm, 2102 ) 2103 2104 # バイアス除去の適用 2105 if config.bias_removal is not None: 2106 bias_removal: BiasRemovalConfig = config.bias_removal 2107 df = CorrectingUtils.remove_bias( 2108 df=df, 2109 quantile_value=bias_removal.quantile_value, 2110 base_ch4_ppm=bias_removal.base_ch4_ppm, 2111 base_c2h6_ppb=bias_removal.base_c2h6_ppb, 2112 ) 2113 2114 return df, source_name 2115 2116 @staticmethod 2117 def _calculate_angle( 2118 lat: float, lon: float, center_lat: float, center_lon: float 2119 ) -> float: 2120 """ 2121 中心からの角度を計算 2122 2123 Parameters 2124 ---------- 2125 lat: float 2126 対象地点の緯度 2127 lon: float 2128 対象地点の経度 2129 center_lat: float 2130 中心の緯度 2131 center_lon: float 2132 中心の経度 2133 2134 Returns 2135 ---------- 2136 float 2137 真北を0°として時計回りの角度(-180°から180°) 2138 """ 2139 d_lat: float = lat - center_lat 2140 d_lon: float = lon - center_lon 2141 # arctanを使用して角度を計算(ラジアン) 2142 angle_rad: float = math.atan2(d_lon, d_lat) 2143 # ラジアンから度に変換(-180から180の範囲) 2144 angle_deg: float = math.degrees(angle_rad) 2145 return angle_deg 2146 2147 @classmethod 2148 def _calculate_distance( 2149 cls, lat1: float, lon1: float, lat2: float, lon2: float 2150 ) -> float: 2151 """ 2152 2点間の距離をメートル単位で計算(Haversine formula) 2153 2154 Parameters 2155 ---------- 2156 lat1: float 2157 地点1の緯度 2158 lon1: float 2159 地点1の経度 2160 lat2: float 2161 地点2の緯度 2162 lon2: float 2163 地点2の経度 2164 2165 Returns 2166 ---------- 2167 float 2168 2地点間の距離(メートル) 2169 """ 2170 const_r = cls.EARTH_RADIUS_METERS 2171 2172 # 緯度経度をラジアンに変換 2173 lat1_rad: float = math.radians(lat1) 2174 lon1_rad: float = math.radians(lon1) 2175 lat2_rad: float = math.radians(lat2) 2176 lon2_rad: float = math.radians(lon2) 2177 2178 # 緯度と経度の差分 2179 dlat: float = lat2_rad - lat1_rad 2180 dlon: float = lon2_rad - lon1_rad 2181 2182 # Haversine formula 2183 a: float = ( 2184 math.sin(dlat / 2) ** 2 2185 + math.cos(lat1_rad) * math.cos(lat2_rad) * math.sin(dlon / 2) ** 2 2186 ) 2187 c: float = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a)) 2188 2189 return const_r * c # メートル単位での距離 2190 2191 @staticmethod 2192 def _calculate_hotspots_parameters( 2193 df: pd.DataFrame, 2194 window_size: int, 2195 col_ch4_ppm: str, 2196 col_c2h6_ppb: str, 2197 col_h2o_ppm: str, 2198 ch4_ppm_delta_min: float = 0.05, 2199 ch4_ppm_delta_max: float = float("inf"), 2200 c2h6_ppb_delta_min: float = 0.0, 2201 c2h6_ppb_delta_max: float = 1000.0, 2202 h2o_ppm_threshold: float = 2000, 2203 rolling_method: RollingMethod = "quantile", 2204 quantile_value: float = 0.05, 2205 ) -> pd.DataFrame: 2206 """ 2207 ホットスポットのパラメータを計算します。 2208 このメソッドは、指定されたデータフレームに対して移動平均(または指定されたquantile)や相関を計算し、 2209 各種のデルタ値や比率を追加します。 2210 2211 Parameters 2212 ---------- 2213 df: pd.DataFrame 2214 入力データフレーム 2215 window_size: int 2216 移動窓のサイズ 2217 col_ch4_ppm: str 2218 CH4濃度のカラム名 2219 col_c2h6_ppb: str 2220 C2H6濃度のカラム名 2221 col_h2o_ppm: str 2222 H2O濃度のカラム名 2223 ch4_ppm_delta_min: float, optional 2224 CH4濃度の下限閾値。この値未満のデータは除外されます。デフォルト値は0.05です。 2225 ch4_ppm_delta_max: float, optional 2226 CH4濃度の上限閾値。この値を超えるデータは除外されます。デフォルト値は無限大です。 2227 c2h6_ppb_delta_min: float, optional 2228 C2H6濃度の下限閾値。この値未満のデータは除外されます。デフォルト値は0.0です。 2229 c2h6_ppb_delta_max: float, optional 2230 C2H6濃度の上限閾値。この値を超えるデータは除外されます。デフォルト値は1000.0です。 2231 h2o_ppm_threshold: float, optional 2232 H2Oの閾値。デフォルト値は2000です。 2233 rolling_method: RollingMethod, optional 2234 バックグラウンド値の移動計算に使用する方法を指定します。'quantile'または'mean'を指定できます。デフォルト値は'quantile'です。 2235 quantile_value: float, optional 2236 使用するquantileの値。デフォルト値は0.05です。 2237 2238 Returns 2239 ---------- 2240 pd.DataFrame 2241 計算されたパラメータを含むデータフレーム 2242 2243 Raises 2244 ---------- 2245 ValueError 2246 quantile_value が0未満または1を超える場合に発生します。 2247 """ 2248 # 引数のバリデーション 2249 if quantile_value < 0 or quantile_value > 1: 2250 raise ValueError( 2251 "quantile_value は0以上1以下の float で指定する必要があります。" 2252 ) 2253 2254 # データのコピーを作成 2255 df_internal: pd.DataFrame = df.copy() 2256 2257 # 移動相関の計算 2258 df_internal["c1c2_correlation"] = ( 2259 df_internal[col_ch4_ppm] 2260 .rolling(window=window_size) 2261 .corr(df_internal[col_c2h6_ppb]) 2262 ) 2263 2264 # バックグラウンド値の計算(指定されたパーセンタイルまたは移動平均) 2265 if rolling_method == "quantile": 2266 df_internal["ch4_ppm_mv"] = ( 2267 df_internal[col_ch4_ppm] 2268 .rolling(window=window_size, center=True, min_periods=1) 2269 .quantile(quantile_value) 2270 ) 2271 df_internal["c2h6_ppb_mv"] = ( 2272 df_internal[col_c2h6_ppb] 2273 .rolling(window=window_size, center=True, min_periods=1) 2274 .quantile(quantile_value) 2275 ) 2276 elif rolling_method == "mean": 2277 df_internal["ch4_ppm_mv"] = ( 2278 df_internal[col_ch4_ppm] 2279 .rolling(window=window_size, center=True, min_periods=1) 2280 .mean() 2281 ) 2282 df_internal["c2h6_ppb_mv"] = ( 2283 df_internal[col_c2h6_ppb] 2284 .rolling(window=window_size, center=True, min_periods=1) 2285 .mean() 2286 ) 2287 2288 # デルタ値の計算 2289 df_internal["ch4_ppm_delta"] = ( 2290 df_internal[col_ch4_ppm] - df_internal["ch4_ppm_mv"] 2291 ) 2292 df_internal["c2h6_ppb_delta"] = ( 2293 df_internal[col_c2h6_ppb] - df_internal["c2h6_ppb_mv"] 2294 ) 2295 2296 # C2H6/CH4の比率計算 2297 df_internal["c2c1_ratio"] = df_internal[col_c2h6_ppb] / df_internal[col_ch4_ppm] 2298 # デルタ値に基づく比の計算とフィルタリング 2299 df_internal["c2c1_ratio_delta"] = ( 2300 df_internal["c2h6_ppb_delta"] / df_internal["ch4_ppm_delta"] 2301 ) 2302 2303 # フィルタリング条件の適用 2304 df_internal.loc[ 2305 (df_internal["ch4_ppm_delta"] < ch4_ppm_delta_min) 2306 | (df_internal["ch4_ppm_delta"] > ch4_ppm_delta_max), 2307 "c2c1_ratio_delta", 2308 ] = np.nan 2309 df_internal.loc[ 2310 (df_internal["c2h6_ppb_delta"] < c2h6_ppb_delta_min) 2311 | (df_internal["c2h6_ppb_delta"] > c2h6_ppb_delta_max), 2312 "c2h6_ppb_delta", 2313 ] = np.nan 2314 # c2h6_ppb_delta は0未満のものを一律0とする 2315 df_internal.loc[df_internal["c2h6_ppb_delta"] < 0, "c2c1_ratio_delta"] = 0.0 2316 2317 # 水蒸気濃度によるフィルタリング 2318 df_internal.loc[ 2319 df_internal[col_h2o_ppm] < h2o_ppm_threshold, [col_ch4_ppm, col_c2h6_ppb] 2320 ] = np.nan 2321 2322 # 欠損値の除去 2323 df_internal = df_internal.dropna(subset=[col_ch4_ppm, col_c2h6_ppb]) 2324 2325 return df_internal 2326 2327 @staticmethod 2328 def _calculate_window_size(window_minutes: float) -> int: 2329 """ 2330 時間窓からデータポイント数を計算 2331 2332 Parameters 2333 ---------- 2334 window_minutes: float 2335 時間窓の大きさ(分) 2336 2337 Returns 2338 ---------- 2339 int 2340 データポイント数 2341 """ 2342 return int(60 * window_minutes) 2343 2344 @staticmethod 2345 def _initialize_sections( 2346 num_sections: int, section_size: float 2347 ) -> dict[int, tuple[float, float]]: 2348 """ 2349 指定された区画数と区画サイズに基づいて、区画の範囲を初期化します。 2350 2351 Parameters 2352 ---------- 2353 num_sections: int 2354 初期化する区画の数。 2355 section_size: float 2356 各区画の角度範囲のサイズ。 2357 2358 Returns 2359 ---------- 2360 dict[int, tuple[float, float]] 2361 区画番号(0-based-index)とその範囲の辞書。各区画は-180度から180度の範囲に分割されます。 2362 """ 2363 sections: dict[int, tuple[float, float]] = {} 2364 for i in range(num_sections): 2365 # -180から180の範囲で区画を設定 2366 start_angle = -180 + i * section_size 2367 end_angle = -180 + (i + 1) * section_size 2368 sections[i] = (start_angle, end_angle) 2369 return sections 2370 2371 @staticmethod 2372 def _is_duplicate_spot( 2373 current_lat: float, 2374 current_lon: float, 2375 current_time: str, 2376 used_positions: list[tuple[float, float, str, float]], 2377 check_time_all: bool, 2378 min_time_threshold_seconds: float, 2379 max_time_threshold_hours: float, 2380 hotspot_area_meter: float, 2381 ) -> bool: 2382 """ 2383 与えられた地点が既存の地点と重複しているかを判定します。 2384 2385 Parameters 2386 ---------- 2387 current_lat: float 2388 判定する地点の緯度 2389 current_lon: float 2390 判定する地点の経度 2391 current_time: str 2392 判定する地点の時刻 2393 used_positions: list[tuple[float, float, str, float]] 2394 既存の地点情報のリスト (lat, lon, time, value) 2395 check_time_all: bool 2396 時間に関係なく重複チェックを行うかどうか 2397 min_time_threshold_seconds: float 2398 重複とみなす最小時間の閾値(秒) 2399 max_time_threshold_hours: float 2400 重複チェックを一時的に無視する最大時間の閾値(時間) 2401 hotspot_area_meter: float 2402 重複とみなす距離の閾値(m) 2403 2404 Returns 2405 ---------- 2406 bool 2407 重複している場合はTrue、そうでない場合はFalse 2408 """ 2409 for used_lat, used_lon, used_time, _ in used_positions: 2410 # 距離チェック 2411 distance = MobileMeasurementAnalyzer._calculate_distance( 2412 lat1=current_lat, lon1=current_lon, lat2=used_lat, lon2=used_lon 2413 ) 2414 2415 if distance < hotspot_area_meter: 2416 # 時間差の計算(秒単位) 2417 time_diff = pd.Timedelta( 2418 pd.to_datetime(current_time) - pd.to_datetime(used_time) 2419 ).total_seconds() 2420 time_diff_abs = abs(time_diff) 2421 2422 if check_time_all: 2423 # 時間に関係なく、距離が近ければ重複とみなす 2424 return True 2425 else: 2426 # 時間窓による判定を行う 2427 if time_diff_abs <= min_time_threshold_seconds: 2428 # Case 1: 最小時間閾値以内は重複とみなす 2429 return True 2430 elif time_diff_abs > max_time_threshold_hours * 3600: 2431 # Case 2: 最大時間閾値を超えた場合は重複チェックをスキップ 2432 continue 2433 # Case 3: その間の時間差の場合は、距離が近ければ重複とみなす 2434 return True 2435 2436 return False 2437 2438 @staticmethod 2439 def _normalize_configs( 2440 configs: list[MobileMeasurementConfig] | list[tuple[float, float, str | Path]], 2441 ) -> list[MobileMeasurementConfig]: 2442 """ 2443 入力設定を標準化 2444 2445 Parameters 2446 ---------- 2447 configs: list[MobileMeasurementConfig] | list[tuple[float, float, str | Path]] 2448 入力設定のリスト 2449 2450 Returns 2451 ---------- 2452 list[MobileMeasurementConfig] 2453 標準化された入力設定のリスト 2454 """ 2455 normalized: list[MobileMeasurementConfig] = [] 2456 for inp in configs: 2457 if isinstance(inp, MobileMeasurementConfig): 2458 normalized.append(inp) # すでに検証済みのため、そのまま追加 2459 else: 2460 fs, lag, path = inp 2461 normalized.append( 2462 MobileMeasurementConfig.validate_and_create( 2463 fs=fs, lag=lag, path=path 2464 ) 2465 ) 2466 return normalized 2467 2468 def remove_c2c1_ratio_duplicates( 2469 self, 2470 df: pd.DataFrame, 2471 min_time_threshold_seconds: float = 300, 2472 max_time_threshold_hours: float = 12.0, 2473 check_time_all: bool = True, 2474 hotspot_area_meter: float = 50.0, 2475 col_ch4_ppm: str = "ch4_ppm", 2476 col_ch4_ppm_mv: str = "ch4_ppm_mv", 2477 col_ch4_ppm_delta: str = "ch4_ppm_delta", 2478 ): 2479 """ 2480 メタン濃度の増加が閾値を超えた地点から、重複を除外してユニークなホットスポットを抽出する関数。 2481 2482 Parameters 2483 ---------- 2484 df: pd.DataFrame 2485 入力データフレーム。必須カラム: 2486 - latitude: 緯度 2487 - longitude: 経度 2488 - ch4_ppm: メタン濃度(ppm) 2489 - ch4_ppm_mv: メタン濃度の移動平均(ppm) 2490 - ch4_ppm_delta: メタン濃度の増加量(ppm) 2491 min_time_threshold_seconds: float, optional 2492 重複とみなす最小時間差(秒)。デフォルト値は300秒(5分)。 2493 max_time_threshold_hours: float, optional 2494 別ポイントとして扱う最大時間差(時間)。デフォルト値は12時間。 2495 check_time_all: bool, optional 2496 時間閾値を超えた場合の重複チェックを継続するかどうか。デフォルト値はTrue。 2497 hotspot_area_meter: float, optional 2498 重複とみなす距離の閾値(メートル)。デフォルト値は50メートル。 2499 col_ch4_ppm: str, optional 2500 メタン濃度のカラム名。デフォルト値は"ch4_ppm"。 2501 col_ch4_ppm_mv: str, optional 2502 メタン濃度移動平均のカラム名。デフォルト値は"ch4_ppm_mv"。 2503 col_ch4_ppm_delta: str, optional 2504 メタン濃度増加量のカラム名。デフォルト値は"ch4_ppm_delta"。 2505 2506 Returns 2507 ---------- 2508 pd.DataFrame 2509 ユニークなホットスポットのデータフレーム。 2510 2511 Examples 2512 ---------- 2513 >>> analyzer = MobileMeasurementAnalyzer() 2514 >>> df = pd.read_csv("measurement_data.csv") 2515 >>> unique_spots = analyzer.remove_c2c1_ratio_duplicates( 2516 ... df, 2517 ... min_time_threshold_seconds=300, 2518 ... hotspot_area_meter=50.0 2519 ... ) 2520 """ 2521 df_data: pd.DataFrame = df.copy() 2522 # メタン濃度の増加が閾値を超えた点を抽出 2523 mask = ( 2524 df_data[col_ch4_ppm] - df_data[col_ch4_ppm_mv] > self._ch4_enhance_threshold 2525 ) 2526 hotspot_candidates = df_data[mask].copy() 2527 2528 # ΔCH4の降順でソート 2529 sorted_hotspots = hotspot_candidates.sort_values( 2530 by=col_ch4_ppm_delta, ascending=False 2531 ) 2532 used_positions = [] 2533 unique_hotspots = pd.DataFrame() 2534 2535 for _, spot in sorted_hotspots.iterrows(): 2536 should_add = True 2537 for used_lat, used_lon, used_time in used_positions: 2538 # 距離チェック 2539 distance = geodesic( 2540 (spot.latitude, spot.longitude), (used_lat, used_lon) 2541 ).meters 2542 2543 if distance < hotspot_area_meter: 2544 # 時間差の計算(秒単位) 2545 time_diff = pd.Timedelta( 2546 pd.to_datetime(spot.name) - pd.to_datetime(used_time) 2547 ).total_seconds() 2548 time_diff_abs = abs(time_diff) 2549 2550 # 時間差に基づく判定 2551 if check_time_all: 2552 # 時間に関係なく、距離が近ければ重複とみなす 2553 # ΔCH4が大きい方を残す(現在のスポットは必ず小さい) 2554 should_add = False 2555 break 2556 else: 2557 # 時間窓による判定を行う 2558 if time_diff_abs <= min_time_threshold_seconds: 2559 # Case 1: 最小時間閾値以内は重複とみなす 2560 should_add = False 2561 break 2562 elif time_diff_abs > max_time_threshold_hours * 3600: 2563 # Case 2: 最大時間閾値を超えた場合は重複チェックをスキップ 2564 continue 2565 # Case 3: その間の時間差の場合は、距離が近ければ重複とみなす 2566 should_add = False 2567 break 2568 2569 if should_add: 2570 unique_hotspots = pd.concat([unique_hotspots, pd.DataFrame([spot])]) 2571 used_positions.append((spot.latitude, spot.longitude, spot.name)) 2572 2573 return unique_hotspots 2574 2575 @staticmethod 2576 def remove_hotspots_duplicates( 2577 hotspots: list[HotspotData], 2578 check_time_all: bool, 2579 min_time_threshold_seconds: float = 300, 2580 max_time_threshold_hours: float = 12, 2581 hotspot_area_meter: float = 50, 2582 ) -> list[HotspotData]: 2583 """ 2584 重複するホットスポットを除外します。 2585 2586 このメソッドは、与えられたホットスポットのリストから重複を検出し、 2587 一意のホットスポットのみを返します。重複の判定は、指定された 2588 時間および距離の閾値に基づいて行われます。 2589 2590 Parameters 2591 ---------- 2592 hotspots: list[HotspotData] 2593 重複を除外する対象のホットスポットのリスト 2594 check_time_all: bool 2595 時間に関係なく重複チェックを行うかどうか 2596 min_time_threshold_seconds: float, optional 2597 重複とみなす最小時間の閾値(秒)。デフォルト値は300秒 2598 max_time_threshold_hours: float, optional 2599 重複チェックを一時的に無視する最大時間の閾値(時間)。デフォルト値は12時間 2600 hotspot_area_meter: float, optional 2601 重複とみなす距離の閾値(メートル)。デフォルト値は50メートル 2602 2603 Returns 2604 ---------- 2605 list[HotspotData] 2606 重複を除去したホットスポットのリスト 2607 2608 Examples 2609 ---------- 2610 >>> hotspots = [HotspotData(...), HotspotData(...)] # ホットスポットのリスト 2611 >>> analyzer = MobileMeasurementAnalyzer() 2612 >>> unique_spots = analyzer.remove_hotspots_duplicates( 2613 ... hotspots=hotspots, 2614 ... check_time_all=True, 2615 ... min_time_threshold_seconds=300, 2616 ... max_time_threshold_hours=12, 2617 ... hotspot_area_meter=50 2618 ... ) 2619 """ 2620 # ΔCH4の降順でソート 2621 sorted_hotspots: list[HotspotData] = sorted( 2622 hotspots, key=lambda x: x.delta_ch4, reverse=True 2623 ) 2624 used_positions_by_type: dict[ 2625 HotspotType, list[tuple[float, float, str, float]] 2626 ] = { 2627 "bio": [], 2628 "gas": [], 2629 "comb": [], 2630 } 2631 unique_hotspots: list[HotspotData] = [] 2632 2633 for spot in sorted_hotspots: 2634 is_duplicate = MobileMeasurementAnalyzer._is_duplicate_spot( 2635 current_lat=spot.avg_lat, 2636 current_lon=spot.avg_lon, 2637 current_time=spot.timestamp, 2638 used_positions=used_positions_by_type[spot.type], 2639 check_time_all=check_time_all, 2640 min_time_threshold_seconds=min_time_threshold_seconds, 2641 max_time_threshold_hours=max_time_threshold_hours, 2642 hotspot_area_meter=hotspot_area_meter, 2643 ) 2644 2645 if not is_duplicate: 2646 unique_hotspots.append(spot) 2647 used_positions_by_type[spot.type].append( 2648 (spot.avg_lat, spot.avg_lon, spot.timestamp, spot.delta_ch4) 2649 ) 2650 2651 return unique_hotspots
車載濃度観測で得られた測定データを解析するクラス
334 def __init__( 335 self, 336 center_lat: float, 337 center_lon: float, 338 configs: list[MobileMeasurementConfig] | list[tuple[float, float, str | Path]], 339 num_sections: int = 4, 340 ch4_enhance_threshold: float = 0.1, 341 correlation_threshold: float = 0.7, 342 hotspot_area_meter: float = 50, 343 hotspot_params: HotspotParams | None = None, 344 window_minutes: float = 5, 345 columns_rename_dict: dict[str, str] | None = None, 346 na_values: list[str] | None = None, 347 logger: Logger | None = None, 348 logging_debug: bool = False, 349 ): 350 """ 351 測定データ解析クラスを初期化します。 352 353 Parameters 354 ---------- 355 center_lat: float 356 中心緯度 357 center_lon: float 358 中心経度 359 configs: list[MobileMeasurementConfig] | list[tuple[float, float, str | Path]] 360 入力ファイルのリスト 361 num_sections: int, optional 362 分割する区画数。デフォルト値は4です。 363 ch4_enhance_threshold: float, optional 364 CH4増加の閾値(ppm)。デフォルト値は0.1です。 365 correlation_threshold: float, optional 366 相関係数の閾値。デフォルト値は0.7です。 367 hotspot_area_meter: float, optional 368 ホットスポットの検出に使用するエリアの半径(メートル)。デフォルト値は50メートルです。 369 hotspot_params: HotspotParams | None, optional 370 ホットスポット解析のパラメータ設定。未指定の場合はデフォルト設定を使用します。 371 window_minutes: float, optional 372 移動窓の大きさ(分)。デフォルト値は5分です。 373 columns_rename_dict: dict[str, str] | None, optional 374 元のデータファイルのヘッダーを汎用的な単語に変換するための辞書型データ。未指定の場合は以下のデフォルト値を使用します: 375 ```python 376 { 377 "Time Stamp": "timestamp", 378 "CH4 (ppm)": "ch4_ppm", 379 "C2H6 (ppb)": "c2h6_ppb", 380 "H2O (ppm)": "h2o_ppm", 381 "Latitude": "latitude", 382 "Longitude": "longitude" 383 } 384 ``` 385 na_values: list[str] | None, optional 386 NaNと判定する値のパターン。未指定の場合は["No Data", "nan"]を使用します。 387 logger: Logger | None, optional 388 使用するロガー。未指定の場合は新しいロガーを作成します。 389 logging_debug: bool, optional 390 ログレベルを"DEBUG"に設定するかどうか。デフォルト値はFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。 391 392 Examples 393 -------- 394 >>> analyzer = MobileMeasurementAnalyzer( 395 ... center_lat=35.6895, 396 ... center_lon=139.6917, 397 ... configs=[ 398 ... MobileMeasurementConfig(fs=1.0, lag=0.0, path="data1.csv"), 399 ... MobileMeasurementConfig(fs=1.0, lag=0.0, path="data2.csv") 400 ... ], 401 ... num_sections=6, 402 ... ch4_enhance_threshold=0.2 403 ... ) 404 """ 405 # ロガー 406 log_level: int = INFO 407 if logging_debug: 408 log_level = DEBUG 409 self.logger: Logger = setup_logger(logger=logger, log_level=log_level) 410 # デフォルト値を使用 411 if columns_rename_dict is None: 412 columns_rename_dict = { 413 "Time Stamp": "timestamp", 414 "CH4 (ppm)": "ch4_ppm", 415 "C2H6 (ppb)": "c2h6_ppb", 416 "H2O (ppm)": "h2o_ppm", 417 "Latitude": "latitude", 418 "Longitude": "longitude", 419 } 420 if na_values is None: 421 na_values = ["No Data", "nan"] 422 # プライベートなプロパティ 423 self._center_lat: float = center_lat 424 self._center_lon: float = center_lon 425 self._ch4_enhance_threshold: float = ch4_enhance_threshold 426 self._correlation_threshold: float = correlation_threshold 427 self._hotspot_area_meter: float = hotspot_area_meter 428 self._column_mapping: dict[str, str] = columns_rename_dict 429 self._na_values: list[str] = na_values 430 self._hotspot_params = hotspot_params or HotspotParams() 431 self._num_sections: int = num_sections 432 # セクションの範囲 433 section_size: float = 360 / num_sections 434 self._section_size: float = section_size 435 self._sections = MobileMeasurementAnalyzer._initialize_sections( 436 num_sections, section_size 437 ) 438 # window_sizeをデータポイント数に変換(分→秒→データポイント数) 439 self._window_size: int = MobileMeasurementAnalyzer._calculate_window_size( 440 window_minutes 441 ) 442 # 入力設定の標準化 443 normalized_input_configs: list[MobileMeasurementConfig] = ( 444 MobileMeasurementAnalyzer._normalize_configs(configs) 445 ) 446 self._configs: list[MobileMeasurementConfig] = normalized_input_configs 447 # 複数ファイルのデータを読み込み結合 448 self.df: pd.DataFrame = self._load_all_combined_data(normalized_input_configs)
測定データ解析クラスを初期化します。
Parameters
center_lat: float
中心緯度
center_lon: float
中心経度
configs: list[MobileMeasurementConfig] | list[tuple[float, float, str | Path]]
入力ファイルのリスト
num_sections: int, optional
分割する区画数。デフォルト値は4です。
ch4_enhance_threshold: float, optional
CH4増加の閾値(ppm)。デフォルト値は0.1です。
correlation_threshold: float, optional
相関係数の閾値。デフォルト値は0.7です。
hotspot_area_meter: float, optional
ホットスポットの検出に使用するエリアの半径(メートル)。デフォルト値は50メートルです。
hotspot_params: HotspotParams | None, optional
ホットスポット解析のパラメータ設定。未指定の場合はデフォルト設定を使用します。
window_minutes: float, optional
移動窓の大きさ(分)。デフォルト値は5分です。
columns_rename_dict: dict[str, str] | None, optional
元のデータファイルのヘッダーを汎用的な単語に変換するための辞書型データ。未指定の場合は以下のデフォルト値を使用します:
{
"Time Stamp": "timestamp",
"CH4 (ppm)": "ch4_ppm",
"C2H6 (ppb)": "c2h6_ppb",
"H2O (ppm)": "h2o_ppm",
"Latitude": "latitude",
"Longitude": "longitude"
}
na_values: list[str] | None, optional
NaNと判定する値のパターン。未指定の場合は["No Data", "nan"]を使用します。
logger: Logger | None, optional
使用するロガー。未指定の場合は新しいロガーを作成します。
logging_debug: bool, optional
ログレベルを"DEBUG"に設定するかどうか。デフォルト値はFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。
Examples
>>> analyzer = MobileMeasurementAnalyzer(
... center_lat=35.6895,
... center_lon=139.6917,
... configs=[
... MobileMeasurementConfig(fs=1.0, lag=0.0, path="data1.csv"),
... MobileMeasurementConfig(fs=1.0, lag=0.0, path="data2.csv")
... ],
... num_sections=6,
... ch4_enhance_threshold=0.2
... )
450 @property 451 def hotspot_params(self) -> HotspotParams: 452 """ホットスポット解析のパラメータ設定を取得""" 453 return self._hotspot_params
ホットスポット解析のパラメータ設定を取得
460 def analyze_delta_ch4_stats(self, hotspots: list[HotspotData]) -> None: 461 """ 462 各タイプのホットスポットについてΔCH4の統計情報を計算し、結果を表示します。 463 464 Parameters 465 ---------- 466 hotspots: list[HotspotData] 467 分析対象のホットスポットリスト 468 """ 469 # タイプごとにホットスポットを分類 470 hotspots_by_type: dict[HotspotType, list[HotspotData]] = { 471 "bio": [h for h in hotspots if h.type == "bio"], 472 "gas": [h for h in hotspots if h.type == "gas"], 473 "comb": [h for h in hotspots if h.type == "comb"], 474 } 475 476 # 統計情報を計算し、表示 477 for spot_type, spots in hotspots_by_type.items(): 478 if spots: 479 delta_ch4_values = [spot.delta_ch4 for spot in spots] 480 max_value = max(delta_ch4_values) 481 mean_value = sum(delta_ch4_values) / len(delta_ch4_values) 482 median_value = sorted(delta_ch4_values)[len(delta_ch4_values) // 2] 483 print(f"{spot_type}タイプのホットスポットの統計情報:") 484 print(f" 最大値: {max_value}") 485 print(f" 平均値: {mean_value}") 486 print(f" 中央値: {median_value}") 487 else: 488 print(f"{spot_type}タイプのホットスポットは存在しません。")
各タイプのホットスポットについてΔCH4の統計情報を計算し、結果を表示します。
Parameters
hotspots: list[HotspotData]
分析対象のホットスポットリスト
490 def analyze_hotspots( 491 self, 492 duplicate_check_mode: Literal["none", "time_window", "time_all"] = "none", 493 min_time_threshold_seconds: float = 300, 494 max_time_threshold_hours: float = 12, 495 ) -> list[HotspotData]: 496 """ 497 ホットスポットを検出して分析します。 498 499 Parameters 500 ---------- 501 duplicate_check_mode: Literal["none", "time_window", "time_all"], optional 502 重複チェックのモード。デフォルトは"none"。 503 - "none": 重複チェックを行わない 504 - "time_window": 指定された時間窓内の重複のみを除外 505 - "time_all": すべての時間範囲で重複チェックを行う 506 min_time_threshold_seconds: float, optional 507 重複とみなす最小時間の閾値。デフォルトは300秒。 508 max_time_threshold_hours: float, optional 509 重複チェックを一時的に無視する最大時間の閾値。デフォルトは12時間。 510 511 Returns 512 ---------- 513 list[HotspotData] 514 検出されたホットスポットのリスト 515 516 Examples 517 -------- 518 >>> analyzer = MobileMeasurementAnalyzer() 519 >>> # 重複チェックなしでホットスポットを検出 520 >>> hotspots = analyzer.analyze_hotspots() 521 >>> 522 >>> # 時間窓内の重複を除外してホットスポットを検出 523 >>> hotspots = analyzer.analyze_hotspots( 524 ... duplicate_check_mode="time_window", 525 ... min_time_threshold_seconds=600, 526 ... max_time_threshold_hours=24 527 ... ) 528 """ 529 all_hotspots: list[HotspotData] = [] 530 df_processed: pd.DataFrame = self.get_preprocessed_data() 531 532 # ホットスポットの検出 533 hotspots: list[HotspotData] = self._detect_hotspots( 534 df=df_processed, 535 ch4_enhance_threshold=self._ch4_enhance_threshold, 536 ) 537 all_hotspots.extend(hotspots) 538 539 # 重複チェックモードに応じて処理 540 if duplicate_check_mode != "none": 541 unique_hotspots = MobileMeasurementAnalyzer.remove_hotspots_duplicates( 542 all_hotspots, 543 check_time_all=(duplicate_check_mode == "time_all"), 544 min_time_threshold_seconds=min_time_threshold_seconds, 545 max_time_threshold_hours=max_time_threshold_hours, 546 hotspot_area_meter=self._hotspot_area_meter, 547 ) 548 self.logger.info( 549 f"重複除外: {len(all_hotspots)} → {len(unique_hotspots)} ホットスポット" 550 ) 551 return unique_hotspots 552 553 return all_hotspots
ホットスポットを検出して分析します。
Parameters
duplicate_check_mode: Literal["none", "time_window", "time_all"], optional
重複チェックのモード。デフォルトは"none"。
- "none": 重複チェックを行わない
- "time_window": 指定された時間窓内の重複のみを除外
- "time_all": すべての時間範囲で重複チェックを行う
min_time_threshold_seconds: float, optional
重複とみなす最小時間の閾値。デフォルトは300秒。
max_time_threshold_hours: float, optional
重複チェックを一時的に無視する最大時間の閾値。デフォルトは12時間。
Returns
list[HotspotData]
検出されたホットスポットのリスト
Examples
>>> analyzer = MobileMeasurementAnalyzer()
>>> # 重複チェックなしでホットスポットを検出
>>> hotspots = analyzer.analyze_hotspots()
>>>
>>> # 時間窓内の重複を除外してホットスポットを検出
>>> hotspots = analyzer.analyze_hotspots(
... duplicate_check_mode="time_window",
... min_time_threshold_seconds=600,
... max_time_threshold_hours=24
... )
555 def calculate_measurement_stats( 556 self, 557 col_latitude: str = "latitude", 558 col_longitude: str = "longitude", 559 print_summary_individual: bool = True, 560 print_summary_total: bool = True, 561 ) -> tuple[float, timedelta]: 562 """ 563 各ファイルの測定時間と走行距離を計算し、合計を返します。 564 565 Parameters 566 ---------- 567 col_latitude: str, optional 568 緯度情報が格納されているカラム名。デフォルト値は"latitude"です。 569 col_longitude: str, optional 570 経度情報が格納されているカラム名。デフォルト値は"longitude"です。 571 print_summary_individual: bool, optional 572 個別ファイルの統計を表示するかどうか。デフォルト値はTrueです。 573 print_summary_total: bool, optional 574 合計統計を表示するかどうか。デフォルト値はTrueです。 575 576 Returns 577 ---------- 578 tuple[float, timedelta] 579 総距離(km)と総時間のタプル 580 581 Examples 582 ---------- 583 >>> analyzer = MobileMeasurementAnalyzer(config_list) 584 >>> total_distance, total_time = analyzer.calculate_measurement_stats() 585 >>> print(f"総距離: {total_distance:.2f}km") 586 >>> print(f"総時間: {total_time}") 587 """ 588 total_distance: float = 0.0 589 total_time: timedelta = timedelta() 590 individual_stats: list[dict] = [] 591 592 # 必要な列のみを読み込むように指定 593 columns_to_read = [col_latitude, col_longitude, "timestamp"] 594 595 for config in tqdm(self._configs, desc="Calculating", unit="file"): 596 # 必要な列のみを読み込み、メモリ使用を最適化 597 df = pd.read_csv(config.path, usecols=columns_to_read) 598 df["timestamp"] = pd.to_datetime(df["timestamp"]) 599 source_name = self.extract_source_name_from_path(config.path) 600 601 # 時間の計算 602 time_spent = df["timestamp"].max() - df["timestamp"].min() 603 604 # ベクトル化した距離計算 605 lat_shift = df[col_latitude].shift(-1) 606 lon_shift = df[col_longitude].shift(-1) 607 608 # nanを除外して距離計算 609 mask = ~(lat_shift.isna() | lon_shift.isna()) 610 if mask.any(): 611 # ラジアンに変換(一度に計算) 612 lat1_rad = np.radians(df[col_latitude][mask]) 613 lon1_rad = np.radians(df[col_longitude][mask]) 614 lat2_rad = np.radians(lat_shift[mask]) 615 lon2_rad = np.radians(lon_shift[mask]) 616 617 # Haversine formulaをベクトル化 618 dlat = lat2_rad - lat1_rad 619 dlon = lon2_rad - lon1_rad 620 621 # 距離計算を一度に実行 622 a = ( 623 np.sin(dlat / 2) ** 2 624 + np.cos(lat1_rad) * np.cos(lat2_rad) * np.sin(dlon / 2) ** 2 625 ) 626 c = 2 * np.arctan2(np.sqrt(a), np.sqrt(1 - a)) 627 distance_km = (self.EARTH_RADIUS_METERS * c).sum() / 1000 628 629 # 合計に加算 630 total_distance += distance_km 631 total_time += time_spent 632 633 # 統計情報を保存 634 if print_summary_individual: 635 average_speed = distance_km / (time_spent.total_seconds() / 3600) 636 individual_stats.append( 637 { 638 "source": source_name, 639 "distance": distance_km, 640 "time": time_spent, 641 "speed": average_speed, 642 } 643 ) 644 645 # 統計情報の表示(変更なし) 646 if print_summary_individual: 647 self.logger.info("=== Individual Stats ===") 648 for stat in individual_stats: 649 print(f"File : {stat['source']}") 650 print(f" Distance : {stat['distance']:.2f} km") 651 print(f" Time : {stat['time']}") 652 print(f" Avg. Speed: {stat['speed']:.1f} km/h\n") 653 654 if print_summary_total: 655 average_speed_total = total_distance / (total_time.total_seconds() / 3600) 656 self.logger.info("=== Total Stats ===") 657 print(f" Distance : {total_distance:.2f} km") 658 print(f" Time : {total_time}") 659 print(f" Avg. Speed: {average_speed_total:.1f} km/h\n") 660 661 return total_distance, total_time
各ファイルの測定時間と走行距離を計算し、合計を返します。
Parameters
col_latitude: str, optional
緯度情報が格納されているカラム名。デフォルト値は"latitude"です。
col_longitude: str, optional
経度情報が格納されているカラム名。デフォルト値は"longitude"です。
print_summary_individual: bool, optional
個別ファイルの統計を表示するかどうか。デフォルト値はTrueです。
print_summary_total: bool, optional
合計統計を表示するかどうか。デフォルト値はTrueです。
Returns
tuple[float, timedelta]
総距離(km)と総時間のタプル
Examples
>>> analyzer = MobileMeasurementAnalyzer(config_list)
>>> total_distance, total_time = analyzer.calculate_measurement_stats()
>>> print(f"総距離: {total_distance:.2f}km")
>>> print(f"総時間: {total_time}")
663 def create_hotspots_map( 664 self, 665 hotspots: list[HotspotData], 666 output_dirpath: str | Path | None = None, 667 output_filename: str = "hotspots_map.html", 668 center_marker_color: str = "green", 669 center_marker_label: str = "Center", 670 plot_center_marker: bool = True, 671 radius_meters: float = 3000, 672 save_fig: bool = True, 673 ) -> None: 674 """ 675 ホットスポットの分布を地図上にプロットして保存します。 676 677 Parameters 678 ---------- 679 hotspots: list[HotspotData] 680 プロットするホットスポットのリスト 681 output_dirpath: str | Path | None, optional 682 保存先のディレクトリパス。未指定の場合はカレントディレクトリに保存されます。 683 output_filename: str, optional 684 保存するファイル名。デフォルト値は"hotspots_map.html"です。 685 center_marker_color: str, optional 686 中心を示すマーカーの色。デフォルト値は"green"です。 687 center_marker_label: str, optional 688 中心を示すマーカーのラベル。デフォルト値は"Center"です。 689 plot_center_marker: bool, optional 690 中心を示すマーカーを表示するかどうか。デフォルト値はTrueです。 691 radius_meters: float, optional 692 区画分けを示す線の長さ(メートル)。デフォルト値は3000です。 693 save_fig: bool, optional 694 図を保存するかどうか。デフォルト値はTrueです。 695 696 Returns 697 ------- 698 None 699 700 Examples 701 -------- 702 >>> analyzer = MobileMeasurementAnalyzer(center_lat=35.6895, center_lon=139.6917, configs=[]) 703 >>> hotspots = [HotspotData(...)] # ホットスポットのリスト 704 >>> analyzer.create_hotspots_map( 705 ... hotspots=hotspots, 706 ... output_dirpath="results", 707 ... radius_meters=5000 708 ... ) 709 """ 710 # 地図の作成 711 m = folium.Map( 712 location=[self._center_lat, self._center_lon], 713 zoom_start=15, 714 tiles="OpenStreetMap", 715 ) 716 717 # ホットスポットの種類ごとに異なる色でプロット 718 for spot in hotspots: 719 # NaN値チェックを追加 720 if math.isnan(spot.avg_lat) or math.isnan(spot.avg_lon): 721 continue 722 723 # default type 724 color = "black" 725 # タイプに応じて色を設定 726 if spot.type == "comb": 727 color = "green" 728 elif spot.type == "gas": 729 color = "red" 730 elif spot.type == "bio": 731 color = "blue" 732 733 # CSSのgrid layoutを使用してHTMLタグを含むテキストをフォーマット 734 popup_html = f""" 735 <div style='font-family: Arial; font-size: 12px; display: grid; grid-template-columns: auto auto auto; gap: 5px;'> 736 <b>Date</b> <span>:</span> <span>{spot.timestamp}</span> 737 <b>Lat</b> <span>:</span> <span>{spot.avg_lat:.3f}</span> 738 <b>Lon</b> <span>:</span> <span>{spot.avg_lon:.3f}</span> 739 <b>ΔCH<sub>4</sub></b> <span>:</span> <span>{spot.delta_ch4:.3f}</span> 740 <b>ΔC<sub>2</sub>H<sub>6</sub></b> <span>:</span> <span>{spot.delta_c2h6:.3f}</span> 741 <b>Ratio</b> <span>:</span> <span>{spot.delta_ratio:.3f}</span> 742 <b>Type</b> <span>:</span> <span>{spot.type}</span> 743 <b>Section</b> <span>:</span> <span>{spot.section}</span> 744 </div> 745 """ 746 747 # ポップアップのサイズを指定 748 popup = folium.Popup( 749 folium.Html(popup_html, script=True), 750 max_width=200, # 最大幅(ピクセル) 751 ) 752 753 folium.CircleMarker( 754 location=[spot.avg_lat, spot.avg_lon], 755 radius=8, 756 color=color, 757 fill=True, 758 popup=popup, 759 ).add_to(m) 760 761 # 中心点のマーカー 762 if plot_center_marker: 763 folium.Marker( 764 [self._center_lat, self._center_lon], 765 popup=center_marker_label, 766 icon=folium.Icon(color=center_marker_color, icon="info-sign"), 767 ).add_to(m) 768 769 # 区画の境界線を描画 770 for section in range(self._num_sections): 771 start_angle = math.radians(-180 + section * self._section_size) 772 773 const_r = self.EARTH_RADIUS_METERS 774 775 # 境界線の座標を計算 776 lat1 = self._center_lat 777 lon1 = self._center_lon 778 lat2 = math.degrees( 779 math.asin( 780 math.sin(math.radians(lat1)) * math.cos(radius_meters / const_r) 781 + math.cos(math.radians(lat1)) 782 * math.sin(radius_meters / const_r) 783 * math.cos(start_angle) 784 ) 785 ) 786 lon2 = self._center_lon + math.degrees( 787 math.atan2( 788 math.sin(start_angle) 789 * math.sin(radius_meters / const_r) 790 * math.cos(math.radians(lat1)), 791 math.cos(radius_meters / const_r) 792 - math.sin(math.radians(lat1)) * math.sin(math.radians(lat2)), 793 ) 794 ) 795 796 # 境界線を描画 797 folium.PolyLine( 798 locations=[[lat1, lon1], [lat2, lon2]], 799 color="black", 800 weight=1, 801 opacity=0.5, 802 ).add_to(m) 803 804 # 地図を保存 805 if save_fig and output_dirpath is None: 806 raise ValueError( 807 "save_fig = True の場合、 output_dirpath を指定する必要があります。有効なディレクトリパスを指定してください。" 808 ) 809 output_filepath: str = os.path.join(output_dirpath, output_filename) 810 m.save(str(output_filepath)) 811 self.logger.info(f"地図を保存しました: {output_filepath}")
ホットスポットの分布を地図上にプロットして保存します。
Parameters
hotspots: list[HotspotData]
プロットするホットスポットのリスト
output_dirpath: str | Path | None, optional
保存先のディレクトリパス。未指定の場合はカレントディレクトリに保存されます。
output_filename: str, optional
保存するファイル名。デフォルト値は"hotspots_map.html"です。
center_marker_color: str, optional
中心を示すマーカーの色。デフォルト値は"green"です。
center_marker_label: str, optional
中心を示すマーカーのラベル。デフォルト値は"Center"です。
plot_center_marker: bool, optional
中心を示すマーカーを表示するかどうか。デフォルト値はTrueです。
radius_meters: float, optional
区画分けを示す線の長さ(メートル)。デフォルト値は3000です。
save_fig: bool, optional
図を保存するかどうか。デフォルト値はTrueです。
Returns
None
Examples
>>> analyzer = MobileMeasurementAnalyzer(center_lat=35.6895, center_lon=139.6917, configs=[])
>>> hotspots = [HotspotData(...)] # ホットスポットのリスト
>>> analyzer.create_hotspots_map(
... hotspots=hotspots,
... output_dirpath="results",
... radius_meters=5000
... )
813 def export_hotspots_to_csv( 814 self, 815 hotspots: list[HotspotData], 816 output_dirpath: str | Path | None = None, 817 output_filename: str = "hotspots.csv", 818 ) -> None: 819 """ 820 ホットスポットの情報をCSVファイルに出力します。 821 822 Parameters 823 ---------- 824 hotspots: list[HotspotData] 825 出力するホットスポットのリスト 826 output_dirpath: str | Path | None, optional 827 出力先ディレクトリのパス。未指定の場合はValueErrorが発生します。 828 output_filename: str, optional 829 出力ファイル名。デフォルト値は"hotspots.csv"です。 830 831 Returns 832 ------- 833 None 834 戻り値はありません。 835 836 Examples 837 -------- 838 >>> analyzer = MobileMeasurementAnalyzer() 839 >>> hotspots = analyzer.analyze_hotspots() 840 >>> analyzer.export_hotspots_to_csv( 841 ... hotspots=hotspots, 842 ... output_dirpath="output", 843 ... output_filename="hotspots_20240101.csv" 844 ... ) 845 """ 846 # 日時の昇順でソート 847 sorted_hotspots = sorted(hotspots, key=lambda x: x.timestamp) 848 849 # 出力用のデータを作成 850 records = [] 851 for spot in sorted_hotspots: 852 record = { 853 "timestamp": spot.timestamp, 854 "type": spot.type, 855 "delta_ch4": spot.delta_ch4, 856 "delta_c2h6": spot.delta_c2h6, 857 "delta_ratio": spot.delta_ratio, 858 "correlation": spot.correlation, 859 "angle": spot.angle, 860 "section": spot.section, 861 "latitude": spot.avg_lat, 862 "longitude": spot.avg_lon, 863 } 864 records.append(record) 865 866 # DataFrameに変換してCSVに出力 867 if output_dirpath is None: 868 raise ValueError( 869 "output_dirpath が指定されていません。有効なディレクトリパスを指定してください。" 870 ) 871 os.makedirs(output_dirpath, exist_ok=True) 872 output_filepath: str = os.path.join(output_dirpath, output_filename) 873 df: pd.DataFrame = pd.DataFrame(records) 874 df.to_csv(output_filepath, index=False) 875 self.logger.info( 876 f"ホットスポット情報をCSVファイルに出力しました: {output_filepath}" 877 )
ホットスポットの情報をCSVファイルに出力します。
Parameters
hotspots: list[HotspotData]
出力するホットスポットのリスト
output_dirpath: str | Path | None, optional
出力先ディレクトリのパス。未指定の場合はValueErrorが発生します。
output_filename: str, optional
出力ファイル名。デフォルト値は"hotspots.csv"です。
Returns
None
戻り値はありません。
Examples
>>> analyzer = MobileMeasurementAnalyzer()
>>> hotspots = analyzer.analyze_hotspots()
>>> analyzer.export_hotspots_to_csv(
... hotspots=hotspots,
... output_dirpath="output",
... output_filename="hotspots_20240101.csv"
... )
879 @staticmethod 880 def extract_source_name_from_path(path: str | Path) -> str: 881 """ 882 ファイルパスからソース名(拡張子なしのファイル名)を抽出します。 883 884 Parameters 885 ---------- 886 path: str | Path 887 ソース名を抽出するファイルパス 888 例: "/path/to/Pico100121_241017_092120+.txt" 889 890 Returns 891 ---------- 892 str 893 抽出されたソース名 894 例: "Pico100121_241017_092120+" 895 896 Examples: 897 ---------- 898 >>> path = "/path/to/data/Pico100121_241017_092120+.txt" 899 >>> MobileMeasurementAnalyzer.extract_source_from_path(path) 900 'Pico100121_241017_092120+' 901 """ 902 # Pathオブジェクトに変換 903 path_obj: Path = Path(path) 904 # stem属性で拡張子なしのファイル名を取得 905 source_name: str = path_obj.stem 906 return source_name
ファイルパスからソース名(拡張子なしのファイル名)を抽出します。
Parameters
path: str | Path
ソース名を抽出するファイルパス
例: "/path/to/Pico100121_241017_092120+.txt"
Returns
str
抽出されたソース名
例: "Pico100121_241017_092120+"
Examples:
>>> path = "/path/to/data/Pico100121_241017_092120+.txt"
>>> MobileMeasurementAnalyzer.extract_source_from_path(path)
'Pico100121_241017_092120+'
908 def get_preprocessed_data( 909 self, 910 ) -> pd.DataFrame: 911 """ 912 データ前処理を行い、CH4とC2H6の相関解析に必要な形式に整えます。 913 コンストラクタで読み込んだすべてのデータを前処理し、結合したDataFrameを返します。 914 915 内部で`MobileMeasurementAnalyzer._calculate_hotspots_parameters()`を適用し、 916 `ch4_ppm_mv`などのパラメータが追加されたDataFrameが戻り値として取得できます。 917 918 Returns 919 ---------- 920 pd.DataFrame 921 前処理済みの結合されたDataFrame 922 """ 923 params: HotspotParams = self._hotspot_params 924 # ホットスポットのパラメータを計算 925 df_processed: pd.DataFrame = ( 926 MobileMeasurementAnalyzer._calculate_hotspots_parameters( 927 df=self.df, 928 window_size=self._window_size, 929 col_ch4_ppm=params.col_ch4_ppm, 930 col_c2h6_ppb=params.col_c2h6_ppb, 931 col_h2o_ppm=params.col_h2o_ppm, 932 ch4_ppm_delta_min=params.ch4_ppm_delta_min, 933 ch4_ppm_delta_max=params.ch4_ppm_delta_max, 934 c2h6_ppb_delta_min=params.c2h6_ppb_delta_min, 935 c2h6_ppb_delta_max=params.c2h6_ppb_delta_max, 936 h2o_ppm_threshold=params.h2o_ppm_min, 937 rolling_method=params.rolling_method, 938 quantile_value=params.quantile_value, 939 ) 940 ) 941 return df_processed
データ前処理を行い、CH4とC2H6の相関解析に必要な形式に整えます。 コンストラクタで読み込んだすべてのデータを前処理し、結合したDataFrameを返します。
内部でMobileMeasurementAnalyzer._calculate_hotspots_parameters()
を適用し、
ch4_ppm_mv
などのパラメータが追加されたDataFrameが戻り値として取得できます。
Returns
pd.DataFrame
前処理済みの結合されたDataFrame
943 def get_section_size(self) -> float: 944 """ 945 セクションのサイズを取得するメソッド。 946 このメソッドは、解析対象のデータを区画に分割する際の 947 各区画の角度範囲を示すサイズを返します。 948 949 Returns 950 ---------- 951 float 952 1セクションのサイズ(度単位) 953 """ 954 return self._section_size
セクションのサイズを取得するメソッド。 このメソッドは、解析対象のデータを区画に分割する際の 各区画の角度範囲を示すサイズを返します。
Returns
float
1セクションのサイズ(度単位)
956 def plot_ch4_delta_histogram( 957 self, 958 hotspots: list[HotspotData], 959 output_dirpath: str | Path | None, 960 output_filename: str = "ch4_delta_histogram.png", 961 dpi: float | None = 350, 962 figsize: tuple[float, float] = (8, 6), 963 fontsize: float = 20, 964 hotspot_colors: dict[HotspotType, str] | None = None, 965 xlabel: str = "Δ$\\mathregular{CH_{4}}$ (ppm)", 966 ylabel: str = "Frequency", 967 xlim: tuple[float, float] | None = None, 968 ylim: tuple[float, float] | None = None, 969 save_fig: bool = True, 970 show_fig: bool = True, 971 yscale_log: bool = True, 972 print_bins_analysis: bool = False, 973 ) -> None: 974 """ 975 CH4の増加量(ΔCH4)の積み上げヒストグラムをプロットします。 976 977 Parameters 978 ---------- 979 hotspots: list[HotspotData] 980 プロットするホットスポットのリスト 981 output_dirpath: str | Path | None 982 保存先のディレクトリパス 983 output_filename: str, optional 984 保存するファイル名。デフォルト値は"ch4_delta_histogram.png"です。 985 dpi: float | None, optional 986 解像度。デフォルト値は350です。 987 figsize: tuple[float, float], optional 988 図のサイズ。デフォルト値は(8, 6)です。 989 fontsize: float, optional 990 フォントサイズ。デフォルト値は20です。 991 hotspot_colors: dict[HotspotType, str] | None, optional 992 ホットスポットの色を定義する辞書。未指定の場合は以下のデフォルト値を使用します: 993 ```python 994 {"bio": "blue", "gas": "red", "comb": "green"} 995 ``` 996 xlabel: str, optional 997 x軸のラベル。デフォルト値は"Δ$\\mathregular{CH_{4}}$ (ppm)"です。 998 ylabel: str, optional 999 y軸のラベル。デフォルト値は"Frequency"です。 1000 xlim: tuple[float, float] | None, optional 1001 x軸の範囲。未指定の場合は自動設定されます。 1002 ylim: tuple[float, float] | None, optional 1003 y軸の範囲。未指定の場合は自動設定されます。 1004 save_fig: bool, optional 1005 図の保存を許可するフラグ。デフォルト値はTrueです。 1006 show_fig: bool, optional 1007 図の表示を許可するフラグ。デフォルト値はTrueです。 1008 yscale_log: bool, optional 1009 y軸をlogスケールにするかどうか。デフォルト値はTrueです。 1010 print_bins_analysis: bool, optional 1011 ビンごとの内訳を表示するかどうか。デフォルト値はFalseです。 1012 1013 Returns 1014 ------- 1015 None 1016 1017 Examples 1018 -------- 1019 >>> analyzer = MobileMeasurementAnalyzer(...) 1020 >>> hotspots = analyzer.detect_hotspots() 1021 >>> analyzer.plot_ch4_delta_histogram( 1022 ... hotspots=hotspots, 1023 ... output_dirpath="results", 1024 ... xlim=(0, 5), 1025 ... ylim=(0, 100) 1026 ... ) 1027 """ 1028 if hotspot_colors is None: 1029 hotspot_colors = { 1030 "bio": "blue", 1031 "gas": "red", 1032 "comb": "green", 1033 } 1034 plt.rcParams["font.size"] = fontsize 1035 fig = plt.figure(figsize=figsize, dpi=dpi) 1036 1037 # ホットスポットからデータを抽出 1038 all_ch4_deltas = [] 1039 all_types = [] 1040 for spot in hotspots: 1041 all_ch4_deltas.append(spot.delta_ch4) 1042 all_types.append(spot.type) 1043 1044 # データをNumPy配列に変換 1045 all_ch4_deltas = np.array(all_ch4_deltas) 1046 all_types = np.array(all_types) 1047 1048 # 0.1刻みのビンを作成 1049 if xlim is not None: 1050 bins = np.arange(xlim[0], xlim[1] + 0.1, 0.1) 1051 else: 1052 max_val = np.ceil(np.max(all_ch4_deltas) * 10) / 10 1053 bins = np.arange(0, max_val + 0.1, 0.1) 1054 1055 # タイプごとのヒストグラムデータを計算 1056 hist_data = {} 1057 # HotspotTypeのリテラル値を使用してイテレーション 1058 for type_name in get_args(HotspotType): # typing.get_argsをインポート 1059 mask = all_types == type_name 1060 if np.any(mask): 1061 counts, _ = np.histogram(all_ch4_deltas[mask], bins=bins) 1062 hist_data[type_name] = counts 1063 1064 # ビンごとの内訳を表示 1065 if print_bins_analysis: 1066 self.logger.info("各ビンの内訳:") 1067 print(f"{'Bin Range':15} {'bio':>8} {'gas':>8} {'comb':>8} {'Total':>8}") 1068 print("-" * 50) 1069 1070 for i in range(len(bins) - 1): 1071 bin_start = bins[i] 1072 bin_end = bins[i + 1] 1073 bio_count = hist_data.get("bio", np.zeros(len(bins) - 1))[i] 1074 gas_count = hist_data.get("gas", np.zeros(len(bins) - 1))[i] 1075 comb_count = hist_data.get("comb", np.zeros(len(bins) - 1))[i] 1076 total = bio_count + gas_count + comb_count 1077 1078 if total > 0: # 合計が0のビンは表示しない 1079 print( 1080 f"{bin_start:4.1f}-{bin_end:<8.1f}" 1081 f"{int(bio_count):8d}" 1082 f"{int(gas_count):8d}" 1083 f"{int(comb_count):8d}" 1084 f"{int(total):8d}" 1085 ) 1086 1087 # 積み上げヒストグラムを作成 1088 bottom = np.zeros_like(hist_data.get("bio", np.zeros(len(bins) - 1))) 1089 1090 # HotspotTypeのリテラル値を使用してイテレーション 1091 for type_name in get_args(HotspotType): 1092 if type_name in hist_data: 1093 plt.bar( 1094 bins[:-1], 1095 hist_data[type_name], 1096 width=np.diff(bins)[0], 1097 bottom=bottom, 1098 color=hotspot_colors[type_name], 1099 label=type_name, 1100 alpha=0.6, 1101 align="edge", 1102 ) 1103 bottom += hist_data[type_name] 1104 1105 if yscale_log: 1106 plt.yscale("log") 1107 plt.xlabel(xlabel) 1108 plt.ylabel(ylabel) 1109 plt.legend() 1110 plt.grid(True, which="both", ls="-", alpha=0.2) 1111 1112 # 軸の範囲を設定 1113 if xlim is not None: 1114 plt.xlim(xlim) 1115 if ylim is not None: 1116 plt.ylim(ylim) 1117 1118 # グラフの保存または表示 1119 if save_fig: 1120 if output_dirpath is None: 1121 raise ValueError( 1122 "save_fig = True の場合、 output_dirpath を指定する必要があります。有効なディレクトリパスを指定してください。" 1123 ) 1124 os.makedirs(output_dirpath, exist_ok=True) 1125 output_filepath: str = os.path.join(output_dirpath, output_filename) 1126 plt.savefig(output_filepath, bbox_inches="tight") 1127 if show_fig: 1128 plt.show() 1129 plt.close(fig=fig)
CH4の増加量(ΔCH4)の積み上げヒストグラムをプロットします。
Parameters
hotspots: list[HotspotData]
プロットするホットスポットのリスト
output_dirpath: str | Path | None
保存先のディレクトリパス
output_filename: str, optional
保存するファイル名。デフォルト値は"ch4_delta_histogram.png"です。
dpi: float | None, optional
解像度。デフォルト値は350です。
figsize: tuple[float, float], optional
図のサイズ。デフォルト値は(8, 6)です。
fontsize: float, optional
フォントサイズ。デフォルト値は20です。
hotspot_colors: dict[HotspotType, str] | None, optional
ホットスポットの色を定義する辞書。未指定の場合は以下のデフォルト値を使用します:
{"bio": "blue", "gas": "red", "comb": "green"}
xlabel: str, optional
x軸のラベル。デフォルト値は"Δ$\mathregular{CH_{4}}$ (ppm)"です。
ylabel: str, optional
y軸のラベル。デフォルト値は"Frequency"です。
xlim: tuple[float, float] | None, optional
x軸の範囲。未指定の場合は自動設定されます。
ylim: tuple[float, float] | None, optional
y軸の範囲。未指定の場合は自動設定されます。
save_fig: bool, optional
図の保存を許可するフラグ。デフォルト値はTrueです。
show_fig: bool, optional
図の表示を許可するフラグ。デフォルト値はTrueです。
yscale_log: bool, optional
y軸をlogスケールにするかどうか。デフォルト値はTrueです。
print_bins_analysis: bool, optional
ビンごとの内訳を表示するかどうか。デフォルト値はFalseです。
Returns
None
Examples
>>> analyzer = MobileMeasurementAnalyzer(...)
>>> hotspots = analyzer.detect_hotspots()
>>> analyzer.plot_ch4_delta_histogram(
... hotspots=hotspots,
... output_dirpath="results",
... xlim=(0, 5),
... ylim=(0, 100)
... )
1131 def plot_mapbox( 1132 self, 1133 df: pd.DataFrame, 1134 col_conc: str, 1135 mapbox_access_token: str, 1136 sort_conc_column: bool = True, 1137 output_dirpath: str | Path | None = None, 1138 output_filename: str = "mapbox_plot.html", 1139 col_lat: str = "latitude", 1140 col_lon: str = "longitude", 1141 colorscale: str = "Jet", 1142 center_lat: float | None = None, 1143 center_lon: float | None = None, 1144 zoom: float = 12, 1145 width: int = 700, 1146 height: int = 700, 1147 tick_font_family: str = "Arial", 1148 title_font_family: str = "Arial", 1149 tick_font_size: int = 12, 1150 title_font_size: int = 14, 1151 marker_size: int = 4, 1152 colorbar_title: str | None = None, 1153 value_range: tuple[float, float] | None = None, 1154 save_fig: bool = True, 1155 show_fig: bool = True, 1156 ) -> None: 1157 """ 1158 Mapbox上にデータをプロットします。 1159 1160 Parameters 1161 ---------- 1162 df: pd.DataFrame 1163 プロットするデータを含むDataFrame 1164 col_conc: str 1165 カラーマッピングに使用する列名 1166 mapbox_access_token: str 1167 Mapboxのアクセストークン 1168 sort_conc_column: bool, optional 1169 濃度列をソートするかどうか。デフォルトはTrue 1170 output_dirpath: str | Path | None, optional 1171 出力ディレクトリのパス。デフォルトはNone 1172 output_filename: str, optional 1173 出力ファイル名。デフォルトは"mapbox_plot.html" 1174 col_lat: str, optional 1175 緯度の列名。デフォルトは"latitude" 1176 col_lon: str, optional 1177 経度の列名。デフォルトは"longitude" 1178 colorscale: str, optional 1179 使用するカラースケール。デフォルトは"Jet" 1180 center_lat: float | None, optional 1181 中心緯度。デフォルトはNoneで、self._center_latを使用 1182 center_lon: float | None, optional 1183 中心経度。デフォルトはNoneで、self._center_lonを使用 1184 zoom: float, optional 1185 マップの初期ズームレベル。デフォルトは12 1186 width: int, optional 1187 プロットの幅(ピクセル)。デフォルトは700 1188 height: int, optional 1189 プロットの高さ(ピクセル)。デフォルトは700 1190 tick_font_family: str, optional 1191 カラーバーの目盛りフォントファミリー。デフォルトは"Arial" 1192 title_font_family: str, optional 1193 カラーバーのタイトルフォントファミリー。デフォルトは"Arial" 1194 tick_font_size: int, optional 1195 カラーバーの目盛りフォントサイズ。デフォルトは12 1196 title_font_size: int, optional 1197 カラーバーのタイトルフォントサイズ。デフォルトは14 1198 marker_size: int, optional 1199 マーカーのサイズ。デフォルトは4 1200 colorbar_title: str | None, optional 1201 カラーバーのタイトル。デフォルトはNoneでcol_concを使用 1202 value_range: tuple[float, float] | None, optional 1203 カラーマッピングの範囲。デフォルトはNoneでデータの最小値と最大値を使用 1204 save_fig: bool, optional 1205 図を保存するかどうか。デフォルトはTrue 1206 show_fig: bool, optional 1207 図を表示するかどうか。デフォルトはTrue 1208 1209 Returns 1210 ------- 1211 None 1212 1213 Examples 1214 -------- 1215 >>> analyzer = MobileMeasurementAnalyzer() 1216 >>> df = pd.DataFrame({ 1217 ... 'latitude': [35.681236, 35.681237], 1218 ... 'longitude': [139.767125, 139.767126], 1219 ... 'concentration': [1.2, 1.5] 1220 ... }) 1221 >>> analyzer.plot_mapbox( 1222 ... df=df, 1223 ... col_conc='concentration', 1224 ... mapbox_access_token='your_token_here' 1225 ... ) 1226 """ 1227 df_mapping: pd.DataFrame = df.copy().dropna(subset=[col_conc]) 1228 if sort_conc_column: 1229 df_mapping = df_mapping.sort_values(col_conc) 1230 # 中心座標の設定 1231 center_lat = center_lat if center_lat is not None else self._center_lat 1232 center_lon = center_lon if center_lon is not None else self._center_lon 1233 1234 # カラーマッピングの範囲を設定 1235 cmin, cmax = 0, 0 1236 if value_range is None: 1237 cmin = df_mapping[col_conc].min() 1238 cmax = df_mapping[col_conc].max() 1239 else: 1240 cmin, cmax = value_range 1241 1242 # カラーバーのタイトルを設定 1243 title_text = colorbar_title if colorbar_title is not None else col_conc 1244 1245 # Scattermapboxのデータを作成 1246 scatter_data = go.Scattermapbox( 1247 lat=df_mapping[col_lat], 1248 lon=df_mapping[col_lon], 1249 text=df_mapping[col_conc].astype(str), 1250 hoverinfo="text", 1251 mode="markers", 1252 marker={ 1253 "color": df_mapping[col_conc], 1254 "size": marker_size, 1255 "reversescale": False, 1256 "autocolorscale": False, 1257 "colorscale": colorscale, 1258 "cmin": cmin, 1259 "cmax": cmax, 1260 "colorbar": { 1261 "tickformat": "3.2f", 1262 "outlinecolor": "black", 1263 "outlinewidth": 1.5, 1264 "ticks": "outside", 1265 "ticklen": 7, 1266 "tickwidth": 1.5, 1267 "tickcolor": "black", 1268 "tickfont": { 1269 "family": tick_font_family, 1270 "color": "black", 1271 "size": tick_font_size, 1272 }, 1273 "title": { 1274 "text": title_text, 1275 "side": "top", 1276 }, # カラーバーのタイトルを設定 1277 "titlefont": { 1278 "family": title_font_family, 1279 "color": "black", 1280 "size": title_font_size, 1281 }, 1282 }, 1283 }, 1284 ) 1285 1286 # レイアウトの設定 1287 layout = go.Layout( 1288 width=width, 1289 height=height, 1290 showlegend=False, 1291 mapbox={ 1292 "accesstoken": mapbox_access_token, 1293 "center": {"lat": center_lat, "lon": center_lon}, 1294 "zoom": zoom, 1295 }, 1296 ) 1297 1298 # 図の作成 1299 fig = go.Figure(data=[scatter_data], layout=layout) 1300 1301 # 図の保存 1302 if save_fig: 1303 # 保存時の出力ディレクトリチェック 1304 if output_dirpath is None: 1305 raise ValueError( 1306 "save_fig=Trueの場合、output_dirpathを指定する必要があります。" 1307 ) 1308 os.makedirs(output_dirpath, exist_ok=True) 1309 output_filepath = os.path.join(output_dirpath, output_filename) 1310 pyo.plot(fig, filename=output_filepath, auto_open=False) 1311 self.logger.info(f"Mapboxプロットを保存しました: {output_filepath}") 1312 # 図の表示 1313 if show_fig: 1314 pyo.iplot(fig)
Mapbox上にデータをプロットします。
Parameters
df: pd.DataFrame
プロットするデータを含むDataFrame
col_conc: str
カラーマッピングに使用する列名
mapbox_access_token: str
Mapboxのアクセストークン
sort_conc_column: bool, optional
濃度列をソートするかどうか。デフォルトはTrue
output_dirpath: str | Path | None, optional
出力ディレクトリのパス。デフォルトはNone
output_filename: str, optional
出力ファイル名。デフォルトは"mapbox_plot.html"
col_lat: str, optional
緯度の列名。デフォルトは"latitude"
col_lon: str, optional
経度の列名。デフォルトは"longitude"
colorscale: str, optional
使用するカラースケール。デフォルトは"Jet"
center_lat: float | None, optional
中心緯度。デフォルトはNoneで、self._center_latを使用
center_lon: float | None, optional
中心経度。デフォルトはNoneで、self._center_lonを使用
zoom: float, optional
マップの初期ズームレベル。デフォルトは12
width: int, optional
プロットの幅(ピクセル)。デフォルトは700
height: int, optional
プロットの高さ(ピクセル)。デフォルトは700
tick_font_family: str, optional
カラーバーの目盛りフォントファミリー。デフォルトは"Arial"
title_font_family: str, optional
カラーバーのタイトルフォントファミリー。デフォルトは"Arial"
tick_font_size: int, optional
カラーバーの目盛りフォントサイズ。デフォルトは12
title_font_size: int, optional
カラーバーのタイトルフォントサイズ。デフォルトは14
marker_size: int, optional
マーカーのサイズ。デフォルトは4
colorbar_title: str | None, optional
カラーバーのタイトル。デフォルトはNoneでcol_concを使用
value_range: tuple[float, float] | None, optional
カラーマッピングの範囲。デフォルトはNoneでデータの最小値と最大値を使用
save_fig: bool, optional
図を保存するかどうか。デフォルトはTrue
show_fig: bool, optional
図を表示するかどうか。デフォルトはTrue
Returns
None
Examples
>>> analyzer = MobileMeasurementAnalyzer()
>>> df = pd.DataFrame({
... 'latitude': [35.681236, 35.681237],
... 'longitude': [139.767125, 139.767126],
... 'concentration': [1.2, 1.5]
... })
>>> analyzer.plot_mapbox(
... df=df,
... col_conc='concentration',
... mapbox_access_token='your_token_here'
... )
1316 def plot_scatter_c2c1( 1317 self, 1318 hotspots: list[HotspotData], 1319 output_dirpath: str | Path | None = None, 1320 output_filename: str = "scatter_c2c1.png", 1321 figsize: tuple[float, float] = (4, 4), 1322 dpi: float | None = 350, 1323 hotspot_colors: dict[HotspotType, str] | None = None, 1324 hotspot_labels: dict[HotspotType, str] | None = None, 1325 fontsize: float = 12, 1326 xlim: tuple[float, float] = (0, 2.0), 1327 ylim: tuple[float, float] = (0, 50), 1328 xlabel: str = "Δ$\\mathregular{CH_{4}}$ (ppm)", 1329 ylabel: str = "Δ$\\mathregular{C_{2}H_{6}}$ (ppb)", 1330 xscale_log: bool = False, 1331 yscale_log: bool = False, 1332 add_legend: bool = True, 1333 save_fig: bool = True, 1334 show_fig: bool = True, 1335 add_ratio_labels: bool = True, 1336 ratio_labels: dict[float, tuple[float, float, str]] | None = None, 1337 ) -> None: 1338 """ 1339 検出されたホットスポットのΔC2H6とΔCH4の散布図をプロットします。 1340 1341 Parameters 1342 ---------- 1343 hotspots: list[HotspotData] 1344 プロットするホットスポットのリスト 1345 output_dirpath: str | Path | None, optional 1346 保存先のディレクトリパス。未指定の場合はNoneとなります。 1347 output_filename: str, optional 1348 保存するファイル名。デフォルト値は"scatter_c2c1.png"です。 1349 figsize: tuple[float, float], optional 1350 図のサイズ。デフォルト値は(4, 4)です。 1351 dpi: float | None, optional 1352 解像度。デフォルト値は350です。 1353 hotspot_colors: dict[HotspotType, str] | None, optional 1354 ホットスポットの色を定義する辞書。未指定の場合は{"bio": "blue", "gas": "red", "comb": "green"}が使用されます。 1355 hotspot_labels: dict[HotspotType, str] | None, optional 1356 ホットスポットのラベルを定義する辞書。未指定の場合は{"bio": "bio", "gas": "gas", "comb": "comb"}が使用されます。 1357 fontsize: float, optional 1358 フォントサイズ。デフォルト値は12です。 1359 xlim: tuple[float, float], optional 1360 x軸の範囲。デフォルト値は(0, 2.0)です。 1361 ylim: tuple[float, float], optional 1362 y軸の範囲。デフォルト値は(0, 50)です。 1363 xlabel: str, optional 1364 x軸のラベル。デフォルト値は"Δ$\\mathregular{CH_{4}}$ (ppm)"です。 1365 ylabel: str, optional 1366 y軸のラベル。デフォルト値は"Δ$\\mathregular{C_{2}H_{6}}$ (ppb)"です。 1367 xscale_log: bool, optional 1368 x軸を対数スケールにするかどうか。デフォルト値はFalseです。 1369 yscale_log: bool, optional 1370 y軸を対数スケールにするかどうか。デフォルト値はFalseです。 1371 add_legend: bool, optional 1372 凡例を追加するかどうか。デフォルト値はTrueです。 1373 save_fig: bool, optional 1374 図の保存を許可するフラグ。デフォルト値はTrueです。 1375 show_fig: bool, optional 1376 図の表示を許可するフラグ。デフォルト値はTrueです。 1377 add_ratio_labels: bool, optional 1378 比率線を表示するかどうか。デフォルト値はTrueです。 1379 ratio_labels: dict[float, tuple[float, float, str]] | None, optional 1380 比率線とラベルの設定。キーは比率値、値は(x位置, y位置, ラベルテキスト)のタプル。 1381 未指定の場合は以下のデフォルト値が使用されます: 1382 ```python 1383 { 1384 0.001: (1.25, 2, "0.001"), 1385 0.005: (1.25, 8, "0.005"), 1386 0.010: (1.25, 15, "0.01"), 1387 0.020: (1.25, 30, "0.02"), 1388 0.030: (1.0, 40, "0.03"), 1389 0.076: (0.20, 42, "0.076 (Osaka)") 1390 } 1391 ``` 1392 1393 Returns 1394 ------- 1395 None 1396 1397 Examples 1398 -------- 1399 >>> analyzer = MobileMeasurementAnalyzer() 1400 >>> hotspots = analyzer.analyze_hotspots() 1401 >>> analyzer.plot_scatter_c2c1( 1402 ... hotspots=hotspots, 1403 ... output_dirpath="output", 1404 ... xlim=(0, 5), 1405 ... ylim=(0, 100) 1406 ... ) 1407 """ 1408 # デフォルト値の設定 1409 if hotspot_colors is None: 1410 hotspot_colors = { 1411 "bio": "blue", 1412 "gas": "red", 1413 "comb": "green", 1414 } 1415 if hotspot_labels is None: 1416 hotspot_labels = { 1417 "bio": "bio", 1418 "gas": "gas", 1419 "comb": "comb", 1420 } 1421 if ratio_labels is None: 1422 ratio_labels = { 1423 0.001: (1.25, 2, "0.001"), 1424 0.005: (1.25, 8, "0.005"), 1425 0.010: (1.25, 15, "0.01"), 1426 0.020: (1.25, 30, "0.02"), 1427 0.030: (1.0, 40, "0.03"), 1428 0.076: (0.20, 42, "0.076 (Osaka)"), 1429 } 1430 plt.rcParams["font.size"] = fontsize 1431 fig = plt.figure(figsize=figsize, dpi=dpi) 1432 1433 # タイプごとのデータを収集 1434 type_data: dict[HotspotType, list[tuple[float, float]]] = { 1435 "bio": [], 1436 "gas": [], 1437 "comb": [], 1438 } 1439 for spot in hotspots: 1440 type_data[spot.type].append((spot.delta_ch4, spot.delta_c2h6)) 1441 1442 # タイプごとにプロット(データが存在する場合のみ) 1443 for spot_type, data in type_data.items(): 1444 if data: # データが存在する場合のみプロット 1445 ch4_values, c2h6_values = zip(*data, strict=True) 1446 plt.plot( 1447 ch4_values, 1448 c2h6_values, 1449 "o", 1450 c=hotspot_colors[spot_type], 1451 alpha=0.5, 1452 ms=2, 1453 label=hotspot_labels[spot_type], 1454 ) 1455 1456 # プロット後、軸の設定前に比率の線を追加 1457 x = np.array([0, 5]) 1458 base_ch4 = 0.0 1459 base = 0.0 1460 1461 # 各比率に対して線を引く 1462 if ratio_labels is not None: 1463 if not add_ratio_labels: 1464 raise ValueError( 1465 "ratio_labels に基づいて比率線を描画する場合は、 add_ratio_labels = True を指定してください。" 1466 ) 1467 for ratio, (x_pos, y_pos, label) in ratio_labels.items(): 1468 y = (x - base_ch4) * 1000 * ratio + base 1469 plt.plot(x, y, "-", c="black", alpha=0.5) 1470 plt.text(x_pos, y_pos, label) 1471 1472 # 軸の設定 1473 if xscale_log: 1474 plt.xscale("log") 1475 if yscale_log: 1476 plt.yscale("log") 1477 1478 plt.xlim(xlim) 1479 plt.ylim(ylim) 1480 plt.xlabel(xlabel) 1481 plt.ylabel(ylabel) 1482 if add_legend: 1483 plt.legend() 1484 1485 # グラフの保存または表示 1486 if save_fig: 1487 if output_dirpath is None: 1488 raise ValueError( 1489 "save_fig = True の場合、 output_dirpath を指定する必要があります。有効なディレクトリパスを指定してください。" 1490 ) 1491 output_filepath: str = os.path.join(output_dirpath, output_filename) 1492 plt.savefig(output_filepath, bbox_inches="tight") 1493 self.logger.info(f"散布図を保存しました: {output_filepath}") 1494 if show_fig: 1495 plt.show() 1496 plt.close(fig=fig)
検出されたホットスポットのΔC2H6とΔCH4の散布図をプロットします。
Parameters
hotspots: list[HotspotData]
プロットするホットスポットのリスト
output_dirpath: str | Path | None, optional
保存先のディレクトリパス。未指定の場合はNoneとなります。
output_filename: str, optional
保存するファイル名。デフォルト値は"scatter_c2c1.png"です。
figsize: tuple[float, float], optional
図のサイズ。デフォルト値は(4, 4)です。
dpi: float | None, optional
解像度。デフォルト値は350です。
hotspot_colors: dict[HotspotType, str] | None, optional
ホットスポットの色を定義する辞書。未指定の場合は{"bio": "blue", "gas": "red", "comb": "green"}が使用されます。
hotspot_labels: dict[HotspotType, str] | None, optional
ホットスポットのラベルを定義する辞書。未指定の場合は{"bio": "bio", "gas": "gas", "comb": "comb"}が使用されます。
fontsize: float, optional
フォントサイズ。デフォルト値は12です。
xlim: tuple[float, float], optional
x軸の範囲。デフォルト値は(0, 2.0)です。
ylim: tuple[float, float], optional
y軸の範囲。デフォルト値は(0, 50)です。
xlabel: str, optional
x軸のラベル。デフォルト値は"Δ$\mathregular{CH_{4}}$ (ppm)"です。
ylabel: str, optional
y軸のラベル。デフォルト値は"Δ$\mathregular{C_{2}H_{6}}$ (ppb)"です。
xscale_log: bool, optional
x軸を対数スケールにするかどうか。デフォルト値はFalseです。
yscale_log: bool, optional
y軸を対数スケールにするかどうか。デフォルト値はFalseです。
add_legend: bool, optional
凡例を追加するかどうか。デフォルト値はTrueです。
save_fig: bool, optional
図の保存を許可するフラグ。デフォルト値はTrueです。
show_fig: bool, optional
図の表示を許可するフラグ。デフォルト値はTrueです。
add_ratio_labels: bool, optional
比率線を表示するかどうか。デフォルト値はTrueです。
ratio_labels: dict[float, tuple[float, float, str]] | None, optional
比率線とラベルの設定。キーは比率値、値は(x位置, y位置, ラベルテキスト)のタプル。
未指定の場合は以下のデフォルト値が使用されます:
{
0.001: (1.25, 2, "0.001"),
0.005: (1.25, 8, "0.005"),
0.010: (1.25, 15, "0.01"),
0.020: (1.25, 30, "0.02"),
0.030: (1.0, 40, "0.03"),
0.076: (0.20, 42, "0.076 (Osaka)")
}
Returns
None
Examples
>>> analyzer = MobileMeasurementAnalyzer()
>>> hotspots = analyzer.analyze_hotspots()
>>> analyzer.plot_scatter_c2c1(
... hotspots=hotspots,
... output_dirpath="output",
... xlim=(0, 5),
... ylim=(0, 100)
... )
1498 def plot_conc_timeseries( 1499 self, 1500 output_dirpath: str | Path | None = None, 1501 output_filename: str = "timeseries.png", 1502 figsize: tuple[float, float] = (8, 4), 1503 dpi: float | None = 350, 1504 save_fig: bool = True, 1505 show_fig: bool = True, 1506 col_ch4: str = "ch4_ppm", 1507 col_c2h6: str = "c2h6_ppb", 1508 col_h2o: str = "h2o_ppm", 1509 ylim_ch4: tuple[float, float] | None = None, 1510 ylim_c2h6: tuple[float, float] | None = None, 1511 ylim_h2o: tuple[float, float] | None = None, 1512 yscale_log_ch4: bool = False, 1513 yscale_log_c2h6: bool = False, 1514 yscale_log_h2o: bool = False, 1515 font_size: float = 12, 1516 label_pad: float = 10, 1517 line_color: str = "black", 1518 ) -> None: 1519 """ 1520 CH4、C2H6、H2Oの時系列データをプロットします。 1521 1522 Parameters 1523 ---------- 1524 output_dirpath: str | Path | None, optional 1525 保存先のディレクトリを指定します。save_fig=Trueの場合は必須です。デフォルト値はNoneです。 1526 output_filename: str, optional 1527 保存するファイル名を指定します。デフォルト値は"timeseries.png"です。 1528 figsize: tuple[float, float], optional 1529 図のサイズを指定します。デフォルト値は(8, 4)です。 1530 dpi: float | None, optional 1531 図の解像度を指定します。デフォルト値は350です。 1532 save_fig: bool, optional 1533 図を保存するかどうかを指定します。デフォルト値はTrueです。 1534 show_fig: bool, optional 1535 図を表示するかどうかを指定します。デフォルト値はTrueです。 1536 col_ch4: str, optional 1537 CH4データの列名を指定します。デフォルト値は"ch4_ppm"です。 1538 col_c2h6: str, optional 1539 C2H6データの列名を指定します。デフォルト値は"c2h6_ppb"です。 1540 col_h2o: str, optional 1541 H2Oデータの列名を指定します。デフォルト値は"h2o_ppm"です。 1542 ylim_ch4: tuple[float, float] | None, optional 1543 CH4プロットのy軸範囲を指定します。デフォルト値はNoneです。 1544 ylim_c2h6: tuple[float, float] | None, optional 1545 C2H6プロットのy軸範囲を指定します。デフォルト値はNoneです。 1546 ylim_h2o: tuple[float, float] | None, optional 1547 H2Oプロットのy軸範囲を指定します。デフォルト値はNoneです。 1548 yscale_log_ch4: bool, optional 1549 CH4データのy軸を対数スケールで表示するかどうかを指定します。デフォルト値はFalseです。 1550 yscale_log_c2h6: bool, optional 1551 C2H6データのy軸を対数スケールで表示するかどうかを指定します。デフォルト値はFalseです。 1552 yscale_log_h2o: bool, optional 1553 H2Oデータのy軸を対数スケールで表示するかどうかを指定します。デフォルト値はFalseです。 1554 font_size: float, optional 1555 プロット全体のフォントサイズを指定します。デフォルト値は12です。 1556 label_pad: float, optional 1557 y軸ラベルとプロットの間隔を指定します。デフォルト値は10です。 1558 line_color: str, optional 1559 プロットする線の色を指定します。デフォルト値は"black"です。 1560 1561 Returns 1562 ------- 1563 None 1564 1565 Examples 1566 -------- 1567 >>> analyzer = MobileMeasurementAnalyzer(df) 1568 >>> analyzer.plot_conc_timeseries( 1569 ... output_dirpath="output", 1570 ... ylim_ch4=(1.8, 2.5), 1571 ... ylim_c2h6=(0, 100), 1572 ... ylim_h2o=(0, 20000) 1573 ... ) 1574 """ 1575 # プロットパラメータの設定 1576 plt.rcParams.update( 1577 { 1578 "font.size": font_size, 1579 "axes.labelsize": font_size, 1580 "axes.titlesize": font_size, 1581 "xtick.labelsize": font_size, 1582 "ytick.labelsize": font_size, 1583 } 1584 ) 1585 # timestampをインデックスとして設定 1586 df_internal = self.df.copy() 1587 df_internal.set_index("timestamp", inplace=True) 1588 1589 # プロットの作成 1590 fig = plt.figure(figsize=figsize, dpi=dpi) 1591 1592 # CH4プロット 1593 ax1 = fig.add_subplot(3, 1, 1) 1594 ax1.plot(df_internal.index, df_internal[col_ch4], c=line_color) 1595 if ylim_ch4: 1596 ax1.set_ylim(ylim_ch4) 1597 if yscale_log_ch4: 1598 ax1.set_yscale("log") 1599 ax1.set_ylabel("$\\mathregular{CH_{4}}$ (ppm)", labelpad=label_pad) 1600 ax1.grid(True, alpha=0.3) 1601 1602 # C2H6プロット 1603 ax2 = fig.add_subplot(3, 1, 2) 1604 ax2.plot(df_internal.index, df_internal[col_c2h6], c=line_color) 1605 if ylim_c2h6: 1606 ax2.set_ylim(ylim_c2h6) 1607 if yscale_log_c2h6: 1608 ax2.set_yscale("log") 1609 ax2.set_ylabel("$\\mathregular{C_{2}H_{6}}$ (ppb)", labelpad=label_pad) 1610 ax2.grid(True, alpha=0.3) 1611 1612 # H2Oプロット 1613 ax3 = fig.add_subplot(3, 1, 3) 1614 ax3.plot(df_internal.index, df_internal[col_h2o], c=line_color) 1615 if ylim_h2o: 1616 ax3.set_ylim(ylim_h2o) 1617 if yscale_log_h2o: 1618 ax3.set_yscale("log") 1619 ax3.set_ylabel("$\\mathregular{H_{2}O}$ (ppm)", labelpad=label_pad) 1620 ax3.grid(True, alpha=0.3) 1621 1622 # x軸のフォーマット調整 1623 for ax in [ax1, ax2, ax3]: 1624 ax.xaxis.set_major_formatter(mdates.DateFormatter("%H:%M")) 1625 # 軸のラベルとグリッド線の調整 1626 ax.tick_params(axis="both", which="major", labelsize=font_size) 1627 ax.grid(True, alpha=0.3) 1628 1629 # サブプロット間の間隔調整 1630 plt.subplots_adjust(wspace=0.38, hspace=0.38) 1631 1632 # 図の保存 1633 if save_fig: 1634 if output_dirpath is None: 1635 raise ValueError( 1636 "save_fig = True の場合、 output_dirpath を指定する必要があります。有効なディレクトリパスを指定してください。" 1637 ) 1638 os.makedirs(output_dirpath, exist_ok=True) 1639 output_filepath = os.path.join(output_dirpath, output_filename) 1640 plt.savefig(output_filepath, bbox_inches="tight") 1641 1642 if show_fig: 1643 plt.show() 1644 plt.close(fig=fig)
CH4、C2H6、H2Oの時系列データをプロットします。
Parameters
output_dirpath: str | Path | None, optional
保存先のディレクトリを指定します。save_fig=Trueの場合は必須です。デフォルト値はNoneです。
output_filename: str, optional
保存するファイル名を指定します。デフォルト値は"timeseries.png"です。
figsize: tuple[float, float], optional
図のサイズを指定します。デフォルト値は(8, 4)です。
dpi: float | None, optional
図の解像度を指定します。デフォルト値は350です。
save_fig: bool, optional
図を保存するかどうかを指定します。デフォルト値はTrueです。
show_fig: bool, optional
図を表示するかどうかを指定します。デフォルト値はTrueです。
col_ch4: str, optional
CH4データの列名を指定します。デフォルト値は"ch4_ppm"です。
col_c2h6: str, optional
C2H6データの列名を指定します。デフォルト値は"c2h6_ppb"です。
col_h2o: str, optional
H2Oデータの列名を指定します。デフォルト値は"h2o_ppm"です。
ylim_ch4: tuple[float, float] | None, optional
CH4プロットのy軸範囲を指定します。デフォルト値はNoneです。
ylim_c2h6: tuple[float, float] | None, optional
C2H6プロットのy軸範囲を指定します。デフォルト値はNoneです。
ylim_h2o: tuple[float, float] | None, optional
H2Oプロットのy軸範囲を指定します。デフォルト値はNoneです。
yscale_log_ch4: bool, optional
CH4データのy軸を対数スケールで表示するかどうかを指定します。デフォルト値はFalseです。
yscale_log_c2h6: bool, optional
C2H6データのy軸を対数スケールで表示するかどうかを指定します。デフォルト値はFalseです。
yscale_log_h2o: bool, optional
H2Oデータのy軸を対数スケールで表示するかどうかを指定します。デフォルト値はFalseです。
font_size: float, optional
プロット全体のフォントサイズを指定します。デフォルト値は12です。
label_pad: float, optional
y軸ラベルとプロットの間隔を指定します。デフォルト値は10です。
line_color: str, optional
プロットする線の色を指定します。デフォルト値は"black"です。
Returns
None
Examples
>>> analyzer = MobileMeasurementAnalyzer(df)
>>> analyzer.plot_conc_timeseries(
... output_dirpath="output",
... ylim_ch4=(1.8, 2.5),
... ylim_c2h6=(0, 100),
... ylim_h2o=(0, 20000)
... )
1646 def plot_conc_timeseries_with_hotspots( 1647 self, 1648 hotspots: list[HotspotData] | None = None, 1649 output_dirpath: str | Path | None = None, 1650 output_filename: str = "timeseries_with_hotspots.png", 1651 figsize: tuple[float, float] = (8, 6), 1652 dpi: float | None = 350, 1653 save_fig: bool = True, 1654 show_fig: bool = True, 1655 col_ch4: str = "ch4_ppm", 1656 col_c2h6: str = "c2h6_ppb", 1657 col_h2o: str = "h2o_ppm", 1658 add_legend: bool = True, 1659 legend_bbox_to_anchor: tuple[float, float] = (0.5, 0.05), 1660 legend_ncol: int | None = None, 1661 font_size: float = 12, 1662 label_pad: float = 10, 1663 line_color: str = "black", 1664 hotspot_colors: dict[HotspotType, str] | None = None, 1665 hotspot_markerscale: float = 1, 1666 hotspot_size: int = 10, 1667 time_margin_minutes: float = 2.0, 1668 ylim_ch4: tuple[float, float] | None = None, 1669 ylim_c2h6: tuple[float, float] | None = None, 1670 ylim_h2o: tuple[float, float] | None = None, 1671 ylim_ratio: tuple[float, float] | None = None, 1672 yscale_log_ch4: bool = False, 1673 yscale_log_c2h6: bool = False, 1674 yscale_log_h2o: bool = False, 1675 yscale_log_ratio: bool = False, 1676 ylabel_ch4: str = "$\\mathregular{CH_{4}}$ (ppm)", 1677 ylabel_c2h6: str = "$\\mathregular{C_{2}H_{6}}$ (ppb)", 1678 ylabel_h2o: str = "$\\mathregular{H_{2}O}$ (ppm)", 1679 ylabel_ratio: str = "ΔC$_2$H$_6$/ΔCH$_4$\n(ppb ppm$^{-1}$)", 1680 ) -> None: 1681 """ 1682 時系列データとホットスポットをプロットします。 1683 1684 Parameters 1685 ---------- 1686 hotspots: list[HotspotData] | None, optional 1687 表示するホットスポットのリスト。デフォルトはNoneで、この場合ホットスポットは表示されません。 1688 output_dirpath: str | Path | None, optional 1689 出力先ディレクトリのパス。save_fig=Trueの場合は必須です。デフォルトはNoneです。 1690 output_filename: str, optional 1691 保存するファイル名。デフォルトは"timeseries_with_hotspots.png"です。 1692 figsize: tuple[float, float], optional 1693 図のサイズ。デフォルトは(8, 6)です。 1694 dpi: float | None, optional 1695 図の解像度。デフォルトは350です。 1696 save_fig: bool, optional 1697 図を保存するかどうか。デフォルトはTrueです。 1698 show_fig: bool, optional 1699 図を表示するかどうか。デフォルトはTrueです。 1700 col_ch4: str, optional 1701 CH4データのカラム名。デフォルトは"ch4_ppm"です。 1702 col_c2h6: str, optional 1703 C2H6データのカラム名。デフォルトは"c2h6_ppb"です。 1704 col_h2o: str, optional 1705 H2Oデータのカラム名。デフォルトは"h2o_ppm"です。 1706 add_legend: bool, optional 1707 ホットスポットの凡例を表示するかどうか。デフォルトはTrueです。 1708 legend_bbox_to_anchor: tuple[float, float], optional 1709 ホットスポットの凡例の位置。デフォルトは(0.5, 0.05)です。 1710 legend_ncol: int | None, optional 1711 凡例のカラム数。デフォルトはNoneで、この場合ホットスポットの種類数に基づいて一行で表示します。 1712 font_size: float, optional 1713 基本フォントサイズ。デフォルトは12です。 1714 label_pad: float, optional 1715 y軸ラベルのパディング。デフォルトは10です。 1716 line_color: str, optional 1717 線の色。デフォルトは"black"です。 1718 hotspot_colors: dict[HotspotType, str] | None, optional 1719 ホットスポットタイプごとの色指定。デフォルトはNoneで、{"bio": "blue", "gas": "red", "comb": "green"}が使用されます。 1720 hotspot_markerscale: float, optional 1721 ホットスポットの凡例でのサイズ。hotspot_sizeに対する相対値。デフォルトは1です。 1722 hotspot_size: int, optional 1723 ホットスポットの図でのサイズ。デフォルトは10です。 1724 time_margin_minutes: float, optional 1725 プロットの時間軸の余白(分)。デフォルトは2.0分です。 1726 ylim_ch4: tuple[float, float] | None, optional 1727 CH4プロットのy軸範囲。デフォルトはNoneで、自動設定されます。 1728 ylim_c2h6: tuple[float, float] | None, optional 1729 C2H6プロットのy軸範囲。デフォルトはNoneで、自動設定されます。 1730 ylim_h2o: tuple[float, float] | None, optional 1731 H2Oプロットのy軸範囲。デフォルトはNoneで、自動設定されます。 1732 ylim_ratio: tuple[float, float] | None, optional 1733 比率プロットのy軸範囲。デフォルトはNoneで、自動設定されます。 1734 yscale_log_ch4: bool, optional 1735 CH4データのy軸を対数スケールで表示するかどうか。デフォルトはFalseです。 1736 yscale_log_c2h6: bool, optional 1737 C2H6データのy軸を対数スケールで表示するかどうか。デフォルトはFalseです。 1738 yscale_log_h2o: bool, optional 1739 H2Oデータのy軸を対数スケールで表示するかどうか。デフォルトはFalseです。 1740 yscale_log_ratio: bool, optional 1741 比率データのy軸を対数スケールで表示するかどうか。デフォルトはFalseです。 1742 ylabel_ch4: str, optional 1743 CH4プロットのy軸ラベル。デフォルトは"$\\mathregular{CH_{4}}$ (ppm)"です。 1744 ylabel_c2h6: str, optional 1745 C2H6プロットのy軸ラベル。デフォルトは"$\\mathregular{C_{2}H_{6}}$ (ppb)"です。 1746 ylabel_h2o: str, optional 1747 H2Oプロットのy軸ラベル。デフォルトは"$\\mathregular{H_{2}O}$ (ppm)"です。 1748 ylabel_ratio: str, optional 1749 比率プロットのy軸ラベル。デフォルトは"ΔC$_2$H$_6$/ΔCH$_4$\\n(ppb ppm$^{-1}$)"です。 1750 1751 Examples 1752 -------- 1753 基本的な使用方法: 1754 >>> analyzer = MobileMeasurementAnalyzer(df) 1755 >>> analyzer.plot_conc_timeseries_with_hotspots() 1756 1757 ホットスポットを指定して保存する: 1758 >>> hotspots = [HotspotData(...), HotspotData(...)] 1759 >>> analyzer.plot_conc_timeseries_with_hotspots( 1760 ... hotspots=hotspots, 1761 ... output_dirpath="output", 1762 ... save_fig=True 1763 ... ) 1764 1765 カスタマイズした表示: 1766 >>> analyzer.plot_conc_timeseries_with_hotspots( 1767 ... figsize=(12, 8), 1768 ... ylim_ch4=(1.8, 2.5), 1769 ... yscale_log_c2h6=True, 1770 ... hotspot_colors={"bio": "purple", "gas": "orange"} 1771 ... ) 1772 """ 1773 if hotspot_colors is None: 1774 hotspot_colors = {"bio": "blue", "gas": "red", "comb": "green"} 1775 # プロットパラメータの設定 1776 plt.rcParams.update( 1777 { 1778 "font.size": font_size, 1779 "axes.labelsize": font_size, 1780 "axes.titlesize": font_size, 1781 "xtick.labelsize": font_size, 1782 "ytick.labelsize": font_size, 1783 } 1784 ) 1785 1786 # timestampをインデックスとして設定 1787 df_internal = self.df.copy() 1788 df_internal.set_index("timestamp", inplace=True) 1789 1790 # プロットの作成 1791 fig = plt.figure(figsize=figsize, dpi=dpi) 1792 1793 # サブプロットのグリッドを作成 (4行1列) 1794 gs = gridspec.GridSpec(4, 1, height_ratios=[1, 1, 1, 1]) 1795 1796 # 時間軸の範囲を設定(余白付き) 1797 time_min = df_internal.index.min() 1798 time_max = df_internal.index.max() 1799 time_margin = pd.Timedelta(minutes=time_margin_minutes) 1800 plot_time_min = time_min - time_margin 1801 plot_time_max = time_max + time_margin 1802 1803 # CH4プロット 1804 ax1 = fig.add_subplot(gs[0]) 1805 ax1.plot(df_internal.index, df_internal[col_ch4], c=line_color) 1806 if ylim_ch4: 1807 ax1.set_ylim(ylim_ch4) 1808 if yscale_log_ch4: 1809 ax1.set_yscale("log") 1810 ax1.set_ylabel(ylabel_ch4, labelpad=label_pad) 1811 ax1.grid(True, alpha=0.3) 1812 ax1.set_xlim(plot_time_min, plot_time_max) 1813 1814 # C2H6プロット 1815 ax2 = fig.add_subplot(gs[1]) 1816 ax2.plot(df_internal.index, df_internal[col_c2h6], c=line_color) 1817 if ylim_c2h6: 1818 ax2.set_ylim(ylim_c2h6) 1819 if yscale_log_c2h6: 1820 ax2.set_yscale("log") 1821 ax2.set_ylabel(ylabel_c2h6, labelpad=label_pad) 1822 ax2.grid(True, alpha=0.3) 1823 ax2.set_xlim(plot_time_min, plot_time_max) 1824 1825 # H2Oプロット 1826 ax3 = fig.add_subplot(gs[2]) 1827 ax3.plot(df_internal.index, df_internal[col_h2o], c=line_color) 1828 if ylim_h2o: 1829 ax3.set_ylim(ylim_h2o) 1830 if yscale_log_h2o: 1831 ax3.set_yscale("log") 1832 ax3.set_ylabel(ylabel_h2o, labelpad=label_pad) 1833 ax3.grid(True, alpha=0.3) 1834 ax3.set_xlim(plot_time_min, plot_time_max) 1835 1836 # ホットスポットの比率プロット 1837 ax4 = fig.add_subplot(gs[3]) 1838 1839 if hotspots: 1840 # ホットスポットをDataFrameに変換 1841 hotspot_df = pd.DataFrame( 1842 [ 1843 { 1844 "timestamp": pd.to_datetime(spot.timestamp), 1845 "delta_ratio": spot.delta_ratio, 1846 "type": spot.type, 1847 } 1848 for spot in hotspots 1849 ] 1850 ) 1851 1852 # タイプごとにプロット 1853 for spot_type in set(hotspot_df["type"]): 1854 type_data = hotspot_df[hotspot_df["type"] == spot_type] 1855 1856 # 点をプロット 1857 ax4.scatter( 1858 type_data["timestamp"], 1859 type_data["delta_ratio"], 1860 c=hotspot_colors.get(spot_type, "black"), 1861 label=spot_type, 1862 alpha=0.6, 1863 s=hotspot_size, 1864 ) 1865 1866 ax4.set_ylabel(ylabel_ratio, labelpad=label_pad) 1867 if ylim_ratio: 1868 ax4.set_ylim(ylim_ratio) 1869 if yscale_log_ratio: 1870 ax4.set_yscale("log") 1871 ax4.grid(True, alpha=0.3) 1872 ax4.set_xlim(plot_time_min, plot_time_max) # 他のプロットと同じ時間範囲を設定 1873 1874 # 凡例を図の下部に配置 1875 if hotspots and add_legend: 1876 ncol = ( 1877 legend_ncol if legend_ncol is not None else len(set(hotspot_df["type"])) 1878 ) 1879 # markerscaleは元のサイズに対する倍率を指定するため、 1880 # 目的のサイズ(100)をプロットのマーカーサイズで割ることで、適切な倍率を計算しています 1881 fig.legend( 1882 bbox_to_anchor=legend_bbox_to_anchor, 1883 loc="upper center", 1884 ncol=ncol, 1885 columnspacing=1.0, 1886 handletextpad=0.5, 1887 markerscale=hotspot_markerscale, 1888 ) 1889 1890 # x軸のフォーマット調整(全てのサブプロットで共通) 1891 for ax in [ax1, ax2, ax3, ax4]: 1892 ax.xaxis.set_major_formatter(mdates.DateFormatter("%H:%M")) 1893 ax.xaxis.set_major_locator(mdates.AutoDateLocator()) 1894 ax.tick_params(axis="both", which="major", labelsize=font_size) 1895 ax.grid(True, alpha=0.3) 1896 1897 # サブプロット間の間隔調整と凡例のためのスペース確保 1898 plt.subplots_adjust(hspace=0.38, bottom=0.12) # bottomを0.15から0.12に変更 1899 1900 # 図の保存 1901 if save_fig: 1902 if output_dirpath is None: 1903 raise ValueError( 1904 "save_fig = True の場合、 output_dirpath を指定する必要があります。有効なディレクトリパスを指定してください。" 1905 ) 1906 os.makedirs(output_dirpath, exist_ok=True) 1907 output_filepath = os.path.join(output_dirpath, output_filename) 1908 plt.savefig(output_filepath, bbox_inches="tight", dpi=dpi) 1909 1910 if show_fig: 1911 plt.show() 1912 plt.close(fig=fig)
時系列データとホットスポットをプロットします。
Parameters
hotspots: list[HotspotData] | None, optional
表示するホットスポットのリスト。デフォルトはNoneで、この場合ホットスポットは表示されません。
output_dirpath: str | Path | None, optional
出力先ディレクトリのパス。save_fig=Trueの場合は必須です。デフォルトはNoneです。
output_filename: str, optional
保存するファイル名。デフォルトは"timeseries_with_hotspots.png"です。
figsize: tuple[float, float], optional
図のサイズ。デフォルトは(8, 6)です。
dpi: float | None, optional
図の解像度。デフォルトは350です。
save_fig: bool, optional
図を保存するかどうか。デフォルトはTrueです。
show_fig: bool, optional
図を表示するかどうか。デフォルトはTrueです。
col_ch4: str, optional
CH4データのカラム名。デフォルトは"ch4_ppm"です。
col_c2h6: str, optional
C2H6データのカラム名。デフォルトは"c2h6_ppb"です。
col_h2o: str, optional
H2Oデータのカラム名。デフォルトは"h2o_ppm"です。
add_legend: bool, optional
ホットスポットの凡例を表示するかどうか。デフォルトはTrueです。
legend_bbox_to_anchor: tuple[float, float], optional
ホットスポットの凡例の位置。デフォルトは(0.5, 0.05)です。
legend_ncol: int | None, optional
凡例のカラム数。デフォルトはNoneで、この場合ホットスポットの種類数に基づいて一行で表示します。
font_size: float, optional
基本フォントサイズ。デフォルトは12です。
label_pad: float, optional
y軸ラベルのパディング。デフォルトは10です。
line_color: str, optional
線の色。デフォルトは"black"です。
hotspot_colors: dict[HotspotType, str] | None, optional
ホットスポットタイプごとの色指定。デフォルトはNoneで、{"bio": "blue", "gas": "red", "comb": "green"}が使用されます。
hotspot_markerscale: float, optional
ホットスポットの凡例でのサイズ。hotspot_sizeに対する相対値。デフォルトは1です。
hotspot_size: int, optional
ホットスポットの図でのサイズ。デフォルトは10です。
time_margin_minutes: float, optional
プロットの時間軸の余白(分)。デフォルトは2.0分です。
ylim_ch4: tuple[float, float] | None, optional
CH4プロットのy軸範囲。デフォルトはNoneで、自動設定されます。
ylim_c2h6: tuple[float, float] | None, optional
C2H6プロットのy軸範囲。デフォルトはNoneで、自動設定されます。
ylim_h2o: tuple[float, float] | None, optional
H2Oプロットのy軸範囲。デフォルトはNoneで、自動設定されます。
ylim_ratio: tuple[float, float] | None, optional
比率プロットのy軸範囲。デフォルトはNoneで、自動設定されます。
yscale_log_ch4: bool, optional
CH4データのy軸を対数スケールで表示するかどうか。デフォルトはFalseです。
yscale_log_c2h6: bool, optional
C2H6データのy軸を対数スケールで表示するかどうか。デフォルトはFalseです。
yscale_log_h2o: bool, optional
H2Oデータのy軸を対数スケールで表示するかどうか。デフォルトはFalseです。
yscale_log_ratio: bool, optional
比率データのy軸を対数スケールで表示するかどうか。デフォルトはFalseです。
ylabel_ch4: str, optional
CH4プロットのy軸ラベル。デフォルトは"$\mathregular{CH_{4}}$ (ppm)"です。
ylabel_c2h6: str, optional
C2H6プロットのy軸ラベル。デフォルトは"$\mathregular{C_{2}H_{6}}$ (ppb)"です。
ylabel_h2o: str, optional
H2Oプロットのy軸ラベル。デフォルトは"$\mathregular{H_{2}O}$ (ppm)"です。
ylabel_ratio: str, optional
比率プロットのy軸ラベル。デフォルトは"ΔC$_2$H$_6$/ΔCH$_4$\n(ppb ppm$^{-1}$)"です。
Examples
基本的な使用方法:
>>> analyzer = MobileMeasurementAnalyzer(df)
>>> analyzer.plot_conc_timeseries_with_hotspots()
ホットスポットを指定して保存する:
>>> hotspots = [HotspotData(...), HotspotData(...)]
>>> analyzer.plot_conc_timeseries_with_hotspots(
... hotspots=hotspots,
... output_dirpath="output",
... save_fig=True
... )
カスタマイズした表示:
>>> analyzer.plot_conc_timeseries_with_hotspots(
... figsize=(12, 8),
... ylim_ch4=(1.8, 2.5),
... yscale_log_c2h6=True,
... hotspot_colors={"bio": "purple", "gas": "orange"}
... )
2468 def remove_c2c1_ratio_duplicates( 2469 self, 2470 df: pd.DataFrame, 2471 min_time_threshold_seconds: float = 300, 2472 max_time_threshold_hours: float = 12.0, 2473 check_time_all: bool = True, 2474 hotspot_area_meter: float = 50.0, 2475 col_ch4_ppm: str = "ch4_ppm", 2476 col_ch4_ppm_mv: str = "ch4_ppm_mv", 2477 col_ch4_ppm_delta: str = "ch4_ppm_delta", 2478 ): 2479 """ 2480 メタン濃度の増加が閾値を超えた地点から、重複を除外してユニークなホットスポットを抽出する関数。 2481 2482 Parameters 2483 ---------- 2484 df: pd.DataFrame 2485 入力データフレーム。必須カラム: 2486 - latitude: 緯度 2487 - longitude: 経度 2488 - ch4_ppm: メタン濃度(ppm) 2489 - ch4_ppm_mv: メタン濃度の移動平均(ppm) 2490 - ch4_ppm_delta: メタン濃度の増加量(ppm) 2491 min_time_threshold_seconds: float, optional 2492 重複とみなす最小時間差(秒)。デフォルト値は300秒(5分)。 2493 max_time_threshold_hours: float, optional 2494 別ポイントとして扱う最大時間差(時間)。デフォルト値は12時間。 2495 check_time_all: bool, optional 2496 時間閾値を超えた場合の重複チェックを継続するかどうか。デフォルト値はTrue。 2497 hotspot_area_meter: float, optional 2498 重複とみなす距離の閾値(メートル)。デフォルト値は50メートル。 2499 col_ch4_ppm: str, optional 2500 メタン濃度のカラム名。デフォルト値は"ch4_ppm"。 2501 col_ch4_ppm_mv: str, optional 2502 メタン濃度移動平均のカラム名。デフォルト値は"ch4_ppm_mv"。 2503 col_ch4_ppm_delta: str, optional 2504 メタン濃度増加量のカラム名。デフォルト値は"ch4_ppm_delta"。 2505 2506 Returns 2507 ---------- 2508 pd.DataFrame 2509 ユニークなホットスポットのデータフレーム。 2510 2511 Examples 2512 ---------- 2513 >>> analyzer = MobileMeasurementAnalyzer() 2514 >>> df = pd.read_csv("measurement_data.csv") 2515 >>> unique_spots = analyzer.remove_c2c1_ratio_duplicates( 2516 ... df, 2517 ... min_time_threshold_seconds=300, 2518 ... hotspot_area_meter=50.0 2519 ... ) 2520 """ 2521 df_data: pd.DataFrame = df.copy() 2522 # メタン濃度の増加が閾値を超えた点を抽出 2523 mask = ( 2524 df_data[col_ch4_ppm] - df_data[col_ch4_ppm_mv] > self._ch4_enhance_threshold 2525 ) 2526 hotspot_candidates = df_data[mask].copy() 2527 2528 # ΔCH4の降順でソート 2529 sorted_hotspots = hotspot_candidates.sort_values( 2530 by=col_ch4_ppm_delta, ascending=False 2531 ) 2532 used_positions = [] 2533 unique_hotspots = pd.DataFrame() 2534 2535 for _, spot in sorted_hotspots.iterrows(): 2536 should_add = True 2537 for used_lat, used_lon, used_time in used_positions: 2538 # 距離チェック 2539 distance = geodesic( 2540 (spot.latitude, spot.longitude), (used_lat, used_lon) 2541 ).meters 2542 2543 if distance < hotspot_area_meter: 2544 # 時間差の計算(秒単位) 2545 time_diff = pd.Timedelta( 2546 pd.to_datetime(spot.name) - pd.to_datetime(used_time) 2547 ).total_seconds() 2548 time_diff_abs = abs(time_diff) 2549 2550 # 時間差に基づく判定 2551 if check_time_all: 2552 # 時間に関係なく、距離が近ければ重複とみなす 2553 # ΔCH4が大きい方を残す(現在のスポットは必ず小さい) 2554 should_add = False 2555 break 2556 else: 2557 # 時間窓による判定を行う 2558 if time_diff_abs <= min_time_threshold_seconds: 2559 # Case 1: 最小時間閾値以内は重複とみなす 2560 should_add = False 2561 break 2562 elif time_diff_abs > max_time_threshold_hours * 3600: 2563 # Case 2: 最大時間閾値を超えた場合は重複チェックをスキップ 2564 continue 2565 # Case 3: その間の時間差の場合は、距離が近ければ重複とみなす 2566 should_add = False 2567 break 2568 2569 if should_add: 2570 unique_hotspots = pd.concat([unique_hotspots, pd.DataFrame([spot])]) 2571 used_positions.append((spot.latitude, spot.longitude, spot.name)) 2572 2573 return unique_hotspots
メタン濃度の増加が閾値を超えた地点から、重複を除外してユニークなホットスポットを抽出する関数。
Parameters
df: pd.DataFrame
入力データフレーム。必須カラム:
- latitude: 緯度
- longitude: 経度
- ch4_ppm: メタン濃度(ppm)
- ch4_ppm_mv: メタン濃度の移動平均(ppm)
- ch4_ppm_delta: メタン濃度の増加量(ppm)
min_time_threshold_seconds: float, optional
重複とみなす最小時間差(秒)。デフォルト値は300秒(5分)。
max_time_threshold_hours: float, optional
別ポイントとして扱う最大時間差(時間)。デフォルト値は12時間。
check_time_all: bool, optional
時間閾値を超えた場合の重複チェックを継続するかどうか。デフォルト値はTrue。
hotspot_area_meter: float, optional
重複とみなす距離の閾値(メートル)。デフォルト値は50メートル。
col_ch4_ppm: str, optional
メタン濃度のカラム名。デフォルト値は"ch4_ppm"。
col_ch4_ppm_mv: str, optional
メタン濃度移動平均のカラム名。デフォルト値は"ch4_ppm_mv"。
col_ch4_ppm_delta: str, optional
メタン濃度増加量のカラム名。デフォルト値は"ch4_ppm_delta"。
Returns
pd.DataFrame
ユニークなホットスポットのデータフレーム。
Examples
>>> analyzer = MobileMeasurementAnalyzer()
>>> df = pd.read_csv("measurement_data.csv")
>>> unique_spots = analyzer.remove_c2c1_ratio_duplicates(
... df,
... min_time_threshold_seconds=300,
... hotspot_area_meter=50.0
... )
2575 @staticmethod 2576 def remove_hotspots_duplicates( 2577 hotspots: list[HotspotData], 2578 check_time_all: bool, 2579 min_time_threshold_seconds: float = 300, 2580 max_time_threshold_hours: float = 12, 2581 hotspot_area_meter: float = 50, 2582 ) -> list[HotspotData]: 2583 """ 2584 重複するホットスポットを除外します。 2585 2586 このメソッドは、与えられたホットスポットのリストから重複を検出し、 2587 一意のホットスポットのみを返します。重複の判定は、指定された 2588 時間および距離の閾値に基づいて行われます。 2589 2590 Parameters 2591 ---------- 2592 hotspots: list[HotspotData] 2593 重複を除外する対象のホットスポットのリスト 2594 check_time_all: bool 2595 時間に関係なく重複チェックを行うかどうか 2596 min_time_threshold_seconds: float, optional 2597 重複とみなす最小時間の閾値(秒)。デフォルト値は300秒 2598 max_time_threshold_hours: float, optional 2599 重複チェックを一時的に無視する最大時間の閾値(時間)。デフォルト値は12時間 2600 hotspot_area_meter: float, optional 2601 重複とみなす距離の閾値(メートル)。デフォルト値は50メートル 2602 2603 Returns 2604 ---------- 2605 list[HotspotData] 2606 重複を除去したホットスポットのリスト 2607 2608 Examples 2609 ---------- 2610 >>> hotspots = [HotspotData(...), HotspotData(...)] # ホットスポットのリスト 2611 >>> analyzer = MobileMeasurementAnalyzer() 2612 >>> unique_spots = analyzer.remove_hotspots_duplicates( 2613 ... hotspots=hotspots, 2614 ... check_time_all=True, 2615 ... min_time_threshold_seconds=300, 2616 ... max_time_threshold_hours=12, 2617 ... hotspot_area_meter=50 2618 ... ) 2619 """ 2620 # ΔCH4の降順でソート 2621 sorted_hotspots: list[HotspotData] = sorted( 2622 hotspots, key=lambda x: x.delta_ch4, reverse=True 2623 ) 2624 used_positions_by_type: dict[ 2625 HotspotType, list[tuple[float, float, str, float]] 2626 ] = { 2627 "bio": [], 2628 "gas": [], 2629 "comb": [], 2630 } 2631 unique_hotspots: list[HotspotData] = [] 2632 2633 for spot in sorted_hotspots: 2634 is_duplicate = MobileMeasurementAnalyzer._is_duplicate_spot( 2635 current_lat=spot.avg_lat, 2636 current_lon=spot.avg_lon, 2637 current_time=spot.timestamp, 2638 used_positions=used_positions_by_type[spot.type], 2639 check_time_all=check_time_all, 2640 min_time_threshold_seconds=min_time_threshold_seconds, 2641 max_time_threshold_hours=max_time_threshold_hours, 2642 hotspot_area_meter=hotspot_area_meter, 2643 ) 2644 2645 if not is_duplicate: 2646 unique_hotspots.append(spot) 2647 used_positions_by_type[spot.type].append( 2648 (spot.avg_lat, spot.avg_lon, spot.timestamp, spot.delta_ch4) 2649 ) 2650 2651 return unique_hotspots
重複するホットスポットを除外します。
このメソッドは、与えられたホットスポットのリストから重複を検出し、 一意のホットスポットのみを返します。重複の判定は、指定された 時間および距離の閾値に基づいて行われます。
Parameters
hotspots: list[HotspotData]
重複を除外する対象のホットスポットのリスト
check_time_all: bool
時間に関係なく重複チェックを行うかどうか
min_time_threshold_seconds: float, optional
重複とみなす最小時間の閾値(秒)。デフォルト値は300秒
max_time_threshold_hours: float, optional
重複チェックを一時的に無視する最大時間の閾値(時間)。デフォルト値は12時間
hotspot_area_meter: float, optional
重複とみなす距離の閾値(メートル)。デフォルト値は50メートル
Returns
list[HotspotData]
重複を除去したホットスポットのリスト
Examples
>>> hotspots = [HotspotData(...), HotspotData(...)] # ホットスポットのリスト
>>> analyzer = MobileMeasurementAnalyzer()
>>> unique_spots = analyzer.remove_hotspots_duplicates(
... hotspots=hotspots,
... check_time_all=True,
... min_time_threshold_seconds=300,
... max_time_threshold_hours=12,
... hotspot_area_meter=50
... )
206@dataclass 207class MobileMeasurementConfig: 208 """ 209 MobileMeasurementAnalyzerのconfigsに与える設定の値を保持するデータクラス 210 211 Parameters 212 ---------- 213 fs: float 214 サンプリング周波数(Hz) 215 lag: float 216 測器の遅れ時間(秒) 217 path: Path | str 218 ファイルパス 219 bias_removal: BiasRemovalConfig | None, optional 220 バイアス除去の設定。未指定の場合は補正を実施しません。 221 h2o_correction: H2OCorrectionConfig | None, optional 222 水蒸気補正の設定。未指定の場合は補正を実施しません。 223 224 Examples 225 -------- 226 >>> # 水蒸気補正の設定 227 >>> h2o_config = H2OCorrectionConfig( 228 ... coef_b=0.001, 229 ... coef_c=0.0001, 230 ... h2o_ppm_threshold=2000, 231 ... target_h2o_ppm=10000 232 ... ) 233 >>> # バイアス除去の設定 234 >>> bias_config = BiasRemovalConfig( 235 ... quantile_value=0.05, 236 ... base_ch4_ppm=2.0, 237 ... base_c2h6_ppb=0 238 ... ) 239 >>> # 車載観測の設定 240 >>> analyzer_config = MobileMeasurementConfig( 241 ... fs=1.0, # サンプリング周波数 1Hz 242 ... lag=2.0, # 遅れ時間 2秒 243 ... path="data.csv", 244 ... bias_removal=bias_config, 245 ... h2o_correction=h2o_config 246 ... ) 247 """ 248 249 fs: float 250 lag: float 251 path: Path | str 252 bias_removal: BiasRemovalConfig | None = None 253 h2o_correction: H2OCorrectionConfig | None = None 254 255 def __post_init__(self) -> None: 256 """ 257 インスタンス生成後に入力値の検証を行います。 258 """ 259 # fsが有効かを確認 260 if not isinstance(self.fs, int | float) or self.fs <= 0: 261 raise ValueError( 262 f"Invalid sampling frequency: {self.fs}. Must be a positive float." 263 ) 264 # lagが0以上のfloatかを確認 265 if not isinstance(self.lag, int | float) or self.lag < 0: 266 raise ValueError( 267 f"Invalid lag value: {self.lag}. Must be a non-negative float." 268 ) 269 # 拡張子の確認 270 supported_extensions: list[str] = [".txt", ".csv"] 271 extension = Path(self.path).suffix 272 if extension not in supported_extensions: 273 raise ValueError( 274 f"Unsupported file extension: '{extension}'. Supported: {supported_extensions}" 275 ) 276 # ファイルの存在確認 277 if not os.path.exists(self.path): 278 raise FileNotFoundError(f"'{self.path}'") 279 280 @classmethod 281 def validate_and_create( 282 cls, 283 fs: float, 284 lag: float, 285 path: Path | str, 286 bias_removal: BiasRemovalConfig | None = None, 287 h2o_correction: H2OCorrectionConfig | None = None, 288 ) -> "MobileMeasurementConfig": 289 """ 290 入力値を検証し、MobileMeasurementConfigインスタンスを生成するファクトリメソッドです。 291 292 Parameters 293 ---------- 294 fs: float 295 サンプリング周波数(Hz)。正の値である必要があります。 296 lag: float 297 遅延時間(秒)。0以上の値である必要があります。 298 path: Path | str 299 入力ファイルのパス。サポートされている拡張子は.txtと.csvです。 300 bias_removal: BiasRemovalConfig | None, optional 301 バイアス除去の設定。未指定の場合は補正を実施しません。 302 h2o_correction: H2OCorrectionConfig | None, optional 303 水蒸気補正の設定。未指定の場合は補正を実施しません。 304 305 Returns 306 ------- 307 MobileMeasurementConfig 308 検証された入力設定を持つMobileMeasurementConfigオブジェクト 309 310 Examples 311 -------- 312 >>> config = MobileMeasurementConfig.validate_and_create( 313 ... fs=1.0, 314 ... lag=2.0, 315 ... path="data.csv" 316 ... ) 317 """ 318 return cls( 319 fs=fs, 320 lag=lag, 321 path=path, 322 bias_removal=bias_removal, 323 h2o_correction=h2o_correction, 324 )
MobileMeasurementAnalyzerのconfigsに与える設定の値を保持するデータクラス
Parameters
fs: float
サンプリング周波数(Hz)
lag: float
測器の遅れ時間(秒)
path: Path | str
ファイルパス
bias_removal: BiasRemovalConfig | None, optional
バイアス除去の設定。未指定の場合は補正を実施しません。
h2o_correction: H2OCorrectionConfig | None, optional
水蒸気補正の設定。未指定の場合は補正を実施しません。
Examples
>>> # 水蒸気補正の設定
>>> h2o_config = H2OCorrectionConfig(
... coef_b=0.001,
... coef_c=0.0001,
... h2o_ppm_threshold=2000,
... target_h2o_ppm=10000
... )
>>> # バイアス除去の設定
>>> bias_config = BiasRemovalConfig(
... quantile_value=0.05,
... base_ch4_ppm=2.0,
... base_c2h6_ppb=0
... )
>>> # 車載観測の設定
>>> analyzer_config = MobileMeasurementConfig(
... fs=1.0, # サンプリング周波数 1Hz
... lag=2.0, # 遅れ時間 2秒
... path="data.csv",
... bias_removal=bias_config,
... h2o_correction=h2o_config
... )
280 @classmethod 281 def validate_and_create( 282 cls, 283 fs: float, 284 lag: float, 285 path: Path | str, 286 bias_removal: BiasRemovalConfig | None = None, 287 h2o_correction: H2OCorrectionConfig | None = None, 288 ) -> "MobileMeasurementConfig": 289 """ 290 入力値を検証し、MobileMeasurementConfigインスタンスを生成するファクトリメソッドです。 291 292 Parameters 293 ---------- 294 fs: float 295 サンプリング周波数(Hz)。正の値である必要があります。 296 lag: float 297 遅延時間(秒)。0以上の値である必要があります。 298 path: Path | str 299 入力ファイルのパス。サポートされている拡張子は.txtと.csvです。 300 bias_removal: BiasRemovalConfig | None, optional 301 バイアス除去の設定。未指定の場合は補正を実施しません。 302 h2o_correction: H2OCorrectionConfig | None, optional 303 水蒸気補正の設定。未指定の場合は補正を実施しません。 304 305 Returns 306 ------- 307 MobileMeasurementConfig 308 検証された入力設定を持つMobileMeasurementConfigオブジェクト 309 310 Examples 311 -------- 312 >>> config = MobileMeasurementConfig.validate_and_create( 313 ... fs=1.0, 314 ... lag=2.0, 315 ... path="data.csv" 316 ... ) 317 """ 318 return cls( 319 fs=fs, 320 lag=lag, 321 path=path, 322 bias_removal=bias_removal, 323 h2o_correction=h2o_correction, 324 )
入力値を検証し、MobileMeasurementConfigインスタンスを生成するファクトリメソッドです。
Parameters
fs: float
サンプリング周波数(Hz)。正の値である必要があります。
lag: float
遅延時間(秒)。0以上の値である必要があります。
path: Path | str
入力ファイルのパス。サポートされている拡張子は.txtと.csvです。
bias_removal: BiasRemovalConfig | None, optional
バイアス除去の設定。未指定の場合は補正を実施しません。
h2o_correction: H2OCorrectionConfig | None, optional
水蒸気補正の設定。未指定の場合は補正を実施しません。
Returns
MobileMeasurementConfig
検証された入力設定を持つMobileMeasurementConfigオブジェクト
Examples
>>> config = MobileMeasurementConfig.validate_and_create(
... fs=1.0,
... lag=2.0,
... path="data.csv"
... )
13class MonthlyConverter: 14 """ 15 Monthlyシート(Excel)を一括で読み込み、DataFrameに変換するクラス。 16 デフォルトは'SA.Ultra.*.xlsx'に対応していますが、コンストラクタのfile_patternを 17 変更すると別のシートにも対応可能です(例: 'SA.Picaro.*.xlsx')。 18 """ 19 20 FILE_DATE_FORMAT = "%Y.%m" # ファイル名用 21 PERIOD_DATE_FORMAT = "%Y-%m-%d" # 期間指定用 22 23 def __init__( 24 self, 25 directory: str | Path, 26 file_pattern: str = "SA.Ultra.*.xlsx", 27 na_values: list[str] | None = None, 28 logger: Logger | None = None, 29 logging_debug: bool = False, 30 ): 31 """ 32 MonthlyConverterクラスのコンストラクタ 33 34 Parameters 35 ---------- 36 directory: str | Path 37 Excelファイルが格納されているディレクトリのパス 38 file_pattern: str, optional 39 ファイル名のパターン。デフォルト値は'SA.Ultra.*.xlsx'です。 40 na_values: list[str] | None, optional 41 NaNと判定する値のパターン。デフォルト値はNoneで、その場合は以下の値が使用されます: 42 ```python 43 ["#DIV/0!", "#VALUE!", "#REF!", "#N/A", "#NAME?", "NAN", "nan"] 44 ``` 45 logger: Logger | None, optional 46 使用するロガー。デフォルト値はNoneで、その場合は新しいロガーが作成されます。 47 logging_debug: bool, optional 48 ログレベルを"DEBUG"に設定するかどうか。デフォルト値はFalseで、その場合はINFO以上のレベルのメッセージが出力されます。 49 50 Examples 51 -------- 52 >>> converter = MonthlyConverter("path/to/excel/files") 53 >>> converter = MonthlyConverter( 54 ... "path/to/excel/files", 55 ... file_pattern="SA.Picaro.*.xlsx", 56 ... logging_debug=True 57 ... ) 58 """ 59 # ロガー 60 log_level: int = INFO 61 if logging_debug: 62 log_level = DEBUG 63 self.logger: Logger = setup_logger(logger=logger, log_level=log_level) 64 65 if na_values is None: 66 na_values = ["#DIV/0!", "#VALUE!", "#REF!", "#N/A", "#NAME?", "NAN", "nan"] 67 self._na_values: list[str] = na_values 68 self._directory = Path(directory) 69 if not self._directory.exists(): 70 raise NotADirectoryError(f"Directory not found: {self._directory}") 71 72 # Excelファイルのパスを保持 73 self._excel_files: dict[str, pd.ExcelFile] = {} 74 self._file_pattern: str = file_pattern 75 76 def close(self) -> None: 77 """ 78 すべてのExcelファイルをクローズする 79 """ 80 for excel_file in self._excel_files.values(): 81 excel_file.close() 82 self._excel_files.clear() 83 84 def get_available_dates(self) -> list[str]: 85 """ 86 利用可能なファイルの日付一覧を返却します。 87 88 Returns 89 ---------- 90 list[str] 91 'yyyy.MM'形式の日付リスト 92 93 Examples 94 -------- 95 >>> converter = MonthlyConverter("path/to/excel/files") 96 >>> dates = converter.get_available_dates() 97 >>> print(dates) 98 ['2023.01', '2023.02', '2023.03'] 99 """ 100 dates = [] 101 for filename in self._directory.glob(self._file_pattern): 102 try: 103 date = self._extract_date(filename.name) 104 dates.append(date.strftime(self.FILE_DATE_FORMAT)) 105 except ValueError: 106 continue 107 return sorted(dates) 108 109 def get_sheet_names(self, filename: str) -> list[str]: 110 """ 111 指定されたファイルで利用可能なシート名の一覧を返却します。 112 113 Parameters 114 ---------- 115 filename: str 116 対象のExcelファイル名を指定します。ファイル名のみを指定し、パスは含めません。 117 118 Returns 119 ---------- 120 list[str] 121 シート名のリスト 122 123 Examples 124 -------- 125 >>> converter = MonthlyConverter("path/to/excel/files") 126 >>> sheets = converter.get_sheet_names("2023.01.xlsx") 127 >>> print(sheets) 128 ['Sheet1', 'Sheet2', 'Sheet3'] 129 """ 130 if filename not in self._excel_files: 131 filepath = self._directory / filename 132 if not filepath.exists(): 133 raise FileNotFoundError(f"File not found: {filepath}") 134 self._excel_files[filename] = pd.ExcelFile(filepath) 135 return [str(name) for name in self._excel_files[filename].sheet_names] 136 137 def read_sheets( 138 self, 139 sheet_names: str | list[str], 140 columns: list[str] | None = None, 141 col_datetime: str = "Date", 142 header: int = 0, 143 skiprows: int | list[int] | None = None, 144 start_date: str | None = None, 145 end_date: str | None = None, 146 include_end_date: bool = True, 147 sort_by_date: bool = True, 148 ) -> pd.DataFrame: 149 """ 150 指定されたシートを読み込み、DataFrameとして返却します。 151 デフォルトでは2行目(単位の行)はスキップされます。 152 重複するカラム名がある場合は、より先に指定されたシートに存在するカラムの値を保持します。 153 154 Parameters 155 ---------- 156 sheet_names: str | list[str] 157 読み込むシート名を指定します。文字列または文字列のリストを指定できます。 158 columns: list[str] | None, optional 159 残すカラム名のリストを指定します。Noneの場合は全てのカラムを保持します。 160 col_datetime: str, optional 161 日付と時刻の情報が含まれるカラム名を指定します。デフォルト値は'Date'です。 162 header: int, optional 163 データのヘッダー行を指定します。デフォルト値は0です。 164 skiprows: int | list[int] | None, optional 165 スキップする行数を指定します。Noneの場合のデフォルトでは2行目をスキップします。 166 start_date: str | None, optional 167 開始日を'yyyy-MM-dd'形式で指定します。この日付の'00:00:00'のデータが開始行となります。 168 end_date: str | None, optional 169 終了日を'yyyy-MM-dd'形式で指定します。この日付をデータに含めるかはinclude_end_dateフラグによって変わります。 170 include_end_date: bool, optional 171 終了日を含めるかどうかを指定します。デフォルト値はTrueです。 172 sort_by_date: bool, optional 173 ファイルの日付でソートするかどうかを指定します。デフォルト値はTrueです。 174 175 Returns 176 ---------- 177 pd.DataFrame 178 読み込まれたデータを結合したDataFrameを返します。 179 180 Examples 181 -------- 182 >>> converter = MonthlyConverter("path/to/excel/files") 183 >>> # 単一シートの読み込み 184 >>> df = converter.read_sheets("Sheet1") 185 >>> # 複数シートの読み込み 186 >>> df = converter.read_sheets(["Sheet1", "Sheet2"]) 187 >>> # 特定の期間のデータ読み込み 188 >>> df = converter.read_sheets( 189 ... "Sheet1", 190 ... start_date="2023-01-01", 191 ... end_date="2023-12-31" 192 ... ) 193 """ 194 if skiprows is None: 195 skiprows = [1] 196 if isinstance(sheet_names, str): 197 sheet_names = [sheet_names] 198 199 self._load_excel_files(start_date, end_date) 200 201 if not self._excel_files: 202 raise ValueError("No Excel files found matching the criteria") 203 204 # ファイルを日付順にソート 205 sorted_files = ( 206 sorted(self._excel_files.items(), key=lambda x: self._extract_date(x[0])) 207 if sort_by_date 208 else self._excel_files.items() 209 ) 210 211 # 各シートのデータを格納するリスト 212 sheet_dfs = {sheet_name: [] for sheet_name in sheet_names} 213 214 # 各ファイルからデータを読み込む 215 for filename, excel_file in sorted_files: 216 file_date = self._extract_date(filename) 217 218 for sheet_name in sheet_names: 219 if sheet_name in excel_file.sheet_names: 220 df = pd.read_excel( 221 excel_file, 222 sheet_name=sheet_name, 223 header=header, 224 skiprows=skiprows, 225 na_values=self._na_values, 226 ) 227 # 年と月を追加 228 df["year"] = file_date.year 229 df["month"] = file_date.month 230 sheet_dfs[sheet_name].append(df) 231 232 if not any(sheet_dfs.values()): 233 raise ValueError(f"No sheets found matching: {sheet_names}") 234 235 # 各シートのデータを結合 236 combined_sheets = {} 237 for sheet_name, dfs in sheet_dfs.items(): 238 if dfs: # シートにデータがある場合のみ結合 239 combined_sheets[sheet_name] = pd.concat(dfs, ignore_index=True) 240 241 # 最初のシートをベースにする 242 base_df = combined_sheets[sheet_names[0]] 243 244 # 2つ目以降のシートを結合 245 for sheet_name in sheet_names[1:]: 246 if sheet_name in combined_sheets: 247 base_df = self.merge_dataframes( 248 base_df, combined_sheets[sheet_name], date_column=col_datetime 249 ) 250 251 # 日付でフィルタリング 252 if start_date: 253 start_dt = pd.to_datetime(start_date) 254 base_df = base_df[base_df[col_datetime] >= start_dt] 255 256 if end_date: 257 end_dt = pd.to_datetime(end_date) 258 if include_end_date: 259 end_dt += pd.Timedelta(days=1) 260 base_df = base_df[base_df[col_datetime] < end_dt] 261 262 # カラムの選択 263 if columns is not None: 264 required_columns = [col_datetime, "year", "month"] 265 available_columns = base_df.columns.tolist() # 利用可能なカラムを取得 266 if not all(col in available_columns for col in columns): 267 raise ValueError( 268 f"指定されたカラムが見つかりません: {columns}. 利用可能なカラム: {available_columns}" 269 ) 270 selected_columns = list(set(columns + required_columns)) 271 base_df = base_df[selected_columns] 272 273 return base_df 274 275 def __enter__(self) -> "MonthlyConverter": 276 return self 277 278 def __exit__(self, exc_type, exc_val, exc_tb) -> None: 279 self.close() 280 281 @staticmethod 282 def get_last_day_of_month(year: int, month: int) -> int: 283 """ 284 指定された年月の最終日を返します。 285 286 Parameters 287 ---------- 288 year: int 289 年を指定します。 290 month: int 291 月を指定します。1から12の整数を指定してください。 292 293 Returns 294 ---------- 295 int 296 指定された年月の最終日の日付。1から31の整数で返されます。 297 298 Examples 299 ---------- 300 >>> MonthlyConverter.get_last_day_of_month(2023, 2) 301 28 302 >>> MonthlyConverter.get_last_day_of_month(2024, 2) # 閏年の場合 303 29 304 """ 305 next_month = ( 306 datetime(year, month % 12 + 1, 1) 307 if month < 12 308 else datetime(year + 1, 1, 1) 309 ) 310 last_day = (next_month - timedelta(days=1)).day 311 return last_day 312 313 @staticmethod 314 def extract_period_data( 315 df: pd.DataFrame, 316 start_date: str | pd.Timestamp, 317 end_date: str | pd.Timestamp, 318 include_end_date: bool = True, 319 datetime_column: str = "Date", 320 ) -> pd.DataFrame: 321 """ 322 指定された期間のデータを抽出します。 323 324 Parameters 325 ---------- 326 df: pd.DataFrame 327 入力データフレーム。 328 start_date: str | pd.Timestamp 329 開始日。YYYY-MM-DD形式の文字列またはTimestampで指定します。 330 end_date: str | pd.Timestamp 331 終了日。YYYY-MM-DD形式の文字列またはTimestampで指定します。 332 include_end_date: bool, optional 333 終了日を含めるかどうかを指定します。デフォルトはTrueです。 334 datetime_column: str, optional 335 日付を含む列の名前を指定します。デフォルトは"Date"です。 336 337 Returns 338 ---------- 339 pd.DataFrame 340 指定された期間のデータのみを含むデータフレーム。 341 342 Examples 343 ---------- 344 >>> df = pd.DataFrame({ 345 ... 'Date': ['2023-01-01', '2023-01-02', '2023-01-03'], 346 ... 'Value': [1, 2, 3] 347 ... }) 348 >>> MonthlyConverter.extract_period_data( 349 ... df, 350 ... '2023-01-01', 351 ... '2023-01-02' 352 ... ) 353 Date Value 354 0 2023-01-01 1 355 1 2023-01-02 2 356 """ 357 # データフレームのコピーを作成 358 df_internal = df.copy() 359 df_internal[datetime_column] = pd.to_datetime(df_internal[datetime_column]) 360 start_dt = pd.to_datetime(start_date) 361 end_dt = pd.to_datetime(end_date) 362 363 # 開始日と終了日の順序チェック 364 if start_dt > end_dt: 365 raise ValueError("start_date は end_date より前である必要があります") 366 367 # 期間でフィルタリング 368 period_data = df_internal[ 369 (df_internal[datetime_column] >= start_dt) 370 & ( 371 df_internal[datetime_column] 372 < (end_dt + pd.Timedelta(days=1) if include_end_date else end_dt) 373 ) 374 ] 375 376 return period_data 377 378 @staticmethod 379 def merge_dataframes( 380 df1: pd.DataFrame, df2: pd.DataFrame, date_column: str = "Date" 381 ) -> pd.DataFrame: 382 """ 383 2つのDataFrameを結合します。重複するカラムは元の名前とサフィックス付きの両方を保持します。 384 385 Parameters 386 ---------- 387 df1: pd.DataFrame 388 ベースとなるデータフレームを指定します。 389 df2: pd.DataFrame 390 結合するデータフレームを指定します。 391 date_column: str, optional 392 日付カラムの名前を指定します。デフォルトは"Date"です。 393 394 Returns 395 ---------- 396 pd.DataFrame 397 結合されたデータフレームを返します。重複するカラムには_xと_yのサフィックスが付与されます。 398 399 Examples 400 ---------- 401 >>> df1 = pd.DataFrame({ 402 ... 'Date': ['2023-01-01', '2023-01-02'], 403 ... 'Value': [1, 2] 404 ... }) 405 >>> df2 = pd.DataFrame({ 406 ... 'Date': ['2023-01-01', '2023-01-02'], 407 ... 'Value': [10, 20] 408 ... }) 409 >>> MonthlyConverter.merge_dataframes(df1, df2) 410 Date Value Value_x Value_y 411 0 2023-01-01 1 1 10 412 1 2023-01-02 2 2 20 413 """ 414 # インデックスをリセット 415 df1 = df1.reset_index(drop=True) 416 df2 = df2.reset_index(drop=True) 417 418 # 日付カラムを統一 419 df2[date_column] = df1[date_column] 420 421 # 重複しないカラムと重複するカラムを分離 422 duplicate_cols = [date_column, "year", "month"] # 常に除外するカラム 423 overlapping_cols = [ 424 col 425 for col in df2.columns 426 if col in df1.columns and col not in duplicate_cols 427 ] 428 unique_cols = [ 429 col 430 for col in df2.columns 431 if col not in df1.columns and col not in duplicate_cols 432 ] 433 434 # 結果のDataFrameを作成 435 result = df1.copy() 436 437 # 重複しないカラムを追加 438 for col in unique_cols: 439 result[col] = df2[col] 440 441 # 重複するカラムを処理 442 for col in overlapping_cols: 443 # 元のカラムはdf1の値を保持(既に result に含まれている) 444 # _x サフィックスでdf1の値を追加 445 result[f"{col}_x"] = df1[col] 446 # _y サフィックスでdf2の値を追加 447 result[f"{col}_y"] = df2[col] 448 449 return result 450 451 def _extract_date(self, filename: str) -> datetime: 452 """ 453 ファイル名から日付を抽出する 454 455 Parameters 456 ---------- 457 filename: str 458 "SA.Ultra.yyyy.MM.xlsx"または"SA.Picaro.yyyy.MM.xlsx"形式のファイル名 459 460 Returns 461 ---------- 462 datetime 463 抽出された日付 464 """ 465 # ファイル名から日付部分を抽出 466 date_str = ".".join(filename.split(".")[-3:-1]) # "yyyy.MM"の部分を取得 467 return datetime.strptime(date_str, self.FILE_DATE_FORMAT) 468 469 def _load_excel_files( 470 self, start_date: str | None = None, end_date: str | None = None 471 ) -> None: 472 """ 473 指定された日付範囲のExcelファイルを読み込む 474 475 Parameters 476 ---------- 477 start_date: str | None 478 開始日 ('yyyy-MM-dd'形式) 479 end_date: str | None 480 終了日 ('yyyy-MM-dd'形式) 481 """ 482 # 期間指定がある場合は、yyyy-MM-dd形式から年月のみを抽出 483 start_dt = None 484 end_dt = None 485 if start_date: 486 temp_dt = datetime.strptime(start_date, self.PERIOD_DATE_FORMAT) 487 start_dt = datetime(temp_dt.year, temp_dt.month, 1) 488 if end_date: 489 temp_dt = datetime.strptime(end_date, self.PERIOD_DATE_FORMAT) 490 end_dt = datetime(temp_dt.year, temp_dt.month, 1) 491 492 # 既存のファイルをクリア 493 self.close() 494 495 for excel_path in self._directory.glob(self._file_pattern): 496 try: 497 file_date = self._extract_date(excel_path.name) 498 499 # 日付範囲チェック 500 if start_dt and file_date < start_dt: 501 continue 502 if end_dt and file_date > end_dt: 503 continue 504 505 if excel_path.name not in self._excel_files: 506 self._excel_files[excel_path.name] = pd.ExcelFile(excel_path) 507 508 except ValueError as e: 509 self.logger.warning( 510 f"Could not parse date from file {excel_path.name}: {e}" 511 ) 512 513 @staticmethod 514 def extract_monthly_data( 515 df: pd.DataFrame, 516 target_months: list[int], 517 start_day: int | None = None, 518 end_day: int | None = None, 519 datetime_column: str = "Date", 520 ) -> pd.DataFrame: 521 """ 522 指定された月と期間のデータを抽出します。 523 524 Parameters 525 ---------- 526 df: pd.DataFrame 527 入力データフレーム。 528 target_months: list[int] 529 抽出したい月のリスト(1から12の整数)。 530 start_day: int | None 531 開始日(1から31の整数)。Noneの場合は月初め。 532 end_day: int | None 533 終了日(1から31の整数)。Noneの場合は月末。 534 datetime_column: str, optional 535 日付を含む列の名前。デフォルトは"Date"。 536 537 Returns 538 ---------- 539 pd.DataFrame 540 指定された期間のデータのみを含むデータフレーム。 541 542 .. warning:: 543 このメソッドは非推奨です。代わりに `extract_period_data` を使用してください。 544 v1.0.0 で削除される予定です。 545 """ 546 try: 547 ver = version("py_flux_tracer") 548 # print(ver) 549 if ver.startswith("0."): 550 warnings.warn( 551 "`extract_monthly_data` is deprecated. Please use `extract_period_data` instead. This method will be removed in v1.0.0.", 552 FutureWarning, 553 stacklevel=2, # 警告メッセージでライブラリの内部実装ではなく、非推奨のメソッドを使用しているユーザーのコードの行を指し示すことができる 554 ) 555 except Exception: 556 pass 557 558 # 入力チェック 559 if not all(1 <= month <= 12 for month in target_months): 560 raise ValueError("target_monthsは1から12の間である必要があります") 561 562 if start_day is not None and not 1 <= start_day <= 31: 563 raise ValueError("start_dayは1から31の間である必要があります") 564 565 if end_day is not None and not 1 <= end_day <= 31: 566 raise ValueError("end_dayは1から31の間である必要があります") 567 568 if start_day is not None and end_day is not None and start_day > end_day: 569 raise ValueError("start_dayはend_day以下である必要があります") 570 571 # datetime_column をDatetime型に変換 572 df_internal = df.copy() 573 df_internal[datetime_column] = pd.to_datetime(df_internal[datetime_column]) 574 575 # 月でフィルタリング 576 monthly_data = df_internal[ 577 df_internal[datetime_column].dt.month.isin(target_months) 578 ] 579 580 # 日付範囲でフィルタリング 581 if start_day is not None: 582 monthly_data = monthly_data[ 583 monthly_data[datetime_column].dt.day >= start_day 584 ] 585 if end_day is not None: 586 monthly_data = monthly_data[monthly_data[datetime_column].dt.day <= end_day] 587 588 return monthly_data
Monthlyシート(Excel)を一括で読み込み、DataFrameに変換するクラス。 デフォルトは'SA.Ultra..xlsx'に対応していますが、コンストラクタのfile_patternを 変更すると別のシートにも対応可能です(例: 'SA.Picaro..xlsx')。
23 def __init__( 24 self, 25 directory: str | Path, 26 file_pattern: str = "SA.Ultra.*.xlsx", 27 na_values: list[str] | None = None, 28 logger: Logger | None = None, 29 logging_debug: bool = False, 30 ): 31 """ 32 MonthlyConverterクラスのコンストラクタ 33 34 Parameters 35 ---------- 36 directory: str | Path 37 Excelファイルが格納されているディレクトリのパス 38 file_pattern: str, optional 39 ファイル名のパターン。デフォルト値は'SA.Ultra.*.xlsx'です。 40 na_values: list[str] | None, optional 41 NaNと判定する値のパターン。デフォルト値はNoneで、その場合は以下の値が使用されます: 42 ```python 43 ["#DIV/0!", "#VALUE!", "#REF!", "#N/A", "#NAME?", "NAN", "nan"] 44 ``` 45 logger: Logger | None, optional 46 使用するロガー。デフォルト値はNoneで、その場合は新しいロガーが作成されます。 47 logging_debug: bool, optional 48 ログレベルを"DEBUG"に設定するかどうか。デフォルト値はFalseで、その場合はINFO以上のレベルのメッセージが出力されます。 49 50 Examples 51 -------- 52 >>> converter = MonthlyConverter("path/to/excel/files") 53 >>> converter = MonthlyConverter( 54 ... "path/to/excel/files", 55 ... file_pattern="SA.Picaro.*.xlsx", 56 ... logging_debug=True 57 ... ) 58 """ 59 # ロガー 60 log_level: int = INFO 61 if logging_debug: 62 log_level = DEBUG 63 self.logger: Logger = setup_logger(logger=logger, log_level=log_level) 64 65 if na_values is None: 66 na_values = ["#DIV/0!", "#VALUE!", "#REF!", "#N/A", "#NAME?", "NAN", "nan"] 67 self._na_values: list[str] = na_values 68 self._directory = Path(directory) 69 if not self._directory.exists(): 70 raise NotADirectoryError(f"Directory not found: {self._directory}") 71 72 # Excelファイルのパスを保持 73 self._excel_files: dict[str, pd.ExcelFile] = {} 74 self._file_pattern: str = file_pattern
MonthlyConverterクラスのコンストラクタ
Parameters
directory: str | Path
Excelファイルが格納されているディレクトリのパス
file_pattern: str, optional
ファイル名のパターン。デフォルト値は'SA.Ultra.*.xlsx'です。
na_values: list[str] | None, optional
NaNと判定する値のパターン。デフォルト値はNoneで、その場合は以下の値が使用されます:
["#DIV/0!", "#VALUE!", "#REF!", "#N/A", "#NAME?", "NAN", "nan"]
logger: Logger | None, optional
使用するロガー。デフォルト値はNoneで、その場合は新しいロガーが作成されます。
logging_debug: bool, optional
ログレベルを"DEBUG"に設定するかどうか。デフォルト値はFalseで、その場合はINFO以上のレベルのメッセージが出力されます。
Examples
>>> converter = MonthlyConverter("path/to/excel/files")
>>> converter = MonthlyConverter(
... "path/to/excel/files",
... file_pattern="SA.Picaro.*.xlsx",
... logging_debug=True
... )
76 def close(self) -> None: 77 """ 78 すべてのExcelファイルをクローズする 79 """ 80 for excel_file in self._excel_files.values(): 81 excel_file.close() 82 self._excel_files.clear()
すべてのExcelファイルをクローズする
84 def get_available_dates(self) -> list[str]: 85 """ 86 利用可能なファイルの日付一覧を返却します。 87 88 Returns 89 ---------- 90 list[str] 91 'yyyy.MM'形式の日付リスト 92 93 Examples 94 -------- 95 >>> converter = MonthlyConverter("path/to/excel/files") 96 >>> dates = converter.get_available_dates() 97 >>> print(dates) 98 ['2023.01', '2023.02', '2023.03'] 99 """ 100 dates = [] 101 for filename in self._directory.glob(self._file_pattern): 102 try: 103 date = self._extract_date(filename.name) 104 dates.append(date.strftime(self.FILE_DATE_FORMAT)) 105 except ValueError: 106 continue 107 return sorted(dates)
利用可能なファイルの日付一覧を返却します。
Returns
list[str]
'yyyy.MM'形式の日付リスト
Examples
>>> converter = MonthlyConverter("path/to/excel/files")
>>> dates = converter.get_available_dates()
>>> print(dates)
['2023.01', '2023.02', '2023.03']
109 def get_sheet_names(self, filename: str) -> list[str]: 110 """ 111 指定されたファイルで利用可能なシート名の一覧を返却します。 112 113 Parameters 114 ---------- 115 filename: str 116 対象のExcelファイル名を指定します。ファイル名のみを指定し、パスは含めません。 117 118 Returns 119 ---------- 120 list[str] 121 シート名のリスト 122 123 Examples 124 -------- 125 >>> converter = MonthlyConverter("path/to/excel/files") 126 >>> sheets = converter.get_sheet_names("2023.01.xlsx") 127 >>> print(sheets) 128 ['Sheet1', 'Sheet2', 'Sheet3'] 129 """ 130 if filename not in self._excel_files: 131 filepath = self._directory / filename 132 if not filepath.exists(): 133 raise FileNotFoundError(f"File not found: {filepath}") 134 self._excel_files[filename] = pd.ExcelFile(filepath) 135 return [str(name) for name in self._excel_files[filename].sheet_names]
指定されたファイルで利用可能なシート名の一覧を返却します。
Parameters
filename: str
対象のExcelファイル名を指定します。ファイル名のみを指定し、パスは含めません。
Returns
list[str]
シート名のリスト
Examples
>>> converter = MonthlyConverter("path/to/excel/files")
>>> sheets = converter.get_sheet_names("2023.01.xlsx")
>>> print(sheets)
['Sheet1', 'Sheet2', 'Sheet3']
137 def read_sheets( 138 self, 139 sheet_names: str | list[str], 140 columns: list[str] | None = None, 141 col_datetime: str = "Date", 142 header: int = 0, 143 skiprows: int | list[int] | None = None, 144 start_date: str | None = None, 145 end_date: str | None = None, 146 include_end_date: bool = True, 147 sort_by_date: bool = True, 148 ) -> pd.DataFrame: 149 """ 150 指定されたシートを読み込み、DataFrameとして返却します。 151 デフォルトでは2行目(単位の行)はスキップされます。 152 重複するカラム名がある場合は、より先に指定されたシートに存在するカラムの値を保持します。 153 154 Parameters 155 ---------- 156 sheet_names: str | list[str] 157 読み込むシート名を指定します。文字列または文字列のリストを指定できます。 158 columns: list[str] | None, optional 159 残すカラム名のリストを指定します。Noneの場合は全てのカラムを保持します。 160 col_datetime: str, optional 161 日付と時刻の情報が含まれるカラム名を指定します。デフォルト値は'Date'です。 162 header: int, optional 163 データのヘッダー行を指定します。デフォルト値は0です。 164 skiprows: int | list[int] | None, optional 165 スキップする行数を指定します。Noneの場合のデフォルトでは2行目をスキップします。 166 start_date: str | None, optional 167 開始日を'yyyy-MM-dd'形式で指定します。この日付の'00:00:00'のデータが開始行となります。 168 end_date: str | None, optional 169 終了日を'yyyy-MM-dd'形式で指定します。この日付をデータに含めるかはinclude_end_dateフラグによって変わります。 170 include_end_date: bool, optional 171 終了日を含めるかどうかを指定します。デフォルト値はTrueです。 172 sort_by_date: bool, optional 173 ファイルの日付でソートするかどうかを指定します。デフォルト値はTrueです。 174 175 Returns 176 ---------- 177 pd.DataFrame 178 読み込まれたデータを結合したDataFrameを返します。 179 180 Examples 181 -------- 182 >>> converter = MonthlyConverter("path/to/excel/files") 183 >>> # 単一シートの読み込み 184 >>> df = converter.read_sheets("Sheet1") 185 >>> # 複数シートの読み込み 186 >>> df = converter.read_sheets(["Sheet1", "Sheet2"]) 187 >>> # 特定の期間のデータ読み込み 188 >>> df = converter.read_sheets( 189 ... "Sheet1", 190 ... start_date="2023-01-01", 191 ... end_date="2023-12-31" 192 ... ) 193 """ 194 if skiprows is None: 195 skiprows = [1] 196 if isinstance(sheet_names, str): 197 sheet_names = [sheet_names] 198 199 self._load_excel_files(start_date, end_date) 200 201 if not self._excel_files: 202 raise ValueError("No Excel files found matching the criteria") 203 204 # ファイルを日付順にソート 205 sorted_files = ( 206 sorted(self._excel_files.items(), key=lambda x: self._extract_date(x[0])) 207 if sort_by_date 208 else self._excel_files.items() 209 ) 210 211 # 各シートのデータを格納するリスト 212 sheet_dfs = {sheet_name: [] for sheet_name in sheet_names} 213 214 # 各ファイルからデータを読み込む 215 for filename, excel_file in sorted_files: 216 file_date = self._extract_date(filename) 217 218 for sheet_name in sheet_names: 219 if sheet_name in excel_file.sheet_names: 220 df = pd.read_excel( 221 excel_file, 222 sheet_name=sheet_name, 223 header=header, 224 skiprows=skiprows, 225 na_values=self._na_values, 226 ) 227 # 年と月を追加 228 df["year"] = file_date.year 229 df["month"] = file_date.month 230 sheet_dfs[sheet_name].append(df) 231 232 if not any(sheet_dfs.values()): 233 raise ValueError(f"No sheets found matching: {sheet_names}") 234 235 # 各シートのデータを結合 236 combined_sheets = {} 237 for sheet_name, dfs in sheet_dfs.items(): 238 if dfs: # シートにデータがある場合のみ結合 239 combined_sheets[sheet_name] = pd.concat(dfs, ignore_index=True) 240 241 # 最初のシートをベースにする 242 base_df = combined_sheets[sheet_names[0]] 243 244 # 2つ目以降のシートを結合 245 for sheet_name in sheet_names[1:]: 246 if sheet_name in combined_sheets: 247 base_df = self.merge_dataframes( 248 base_df, combined_sheets[sheet_name], date_column=col_datetime 249 ) 250 251 # 日付でフィルタリング 252 if start_date: 253 start_dt = pd.to_datetime(start_date) 254 base_df = base_df[base_df[col_datetime] >= start_dt] 255 256 if end_date: 257 end_dt = pd.to_datetime(end_date) 258 if include_end_date: 259 end_dt += pd.Timedelta(days=1) 260 base_df = base_df[base_df[col_datetime] < end_dt] 261 262 # カラムの選択 263 if columns is not None: 264 required_columns = [col_datetime, "year", "month"] 265 available_columns = base_df.columns.tolist() # 利用可能なカラムを取得 266 if not all(col in available_columns for col in columns): 267 raise ValueError( 268 f"指定されたカラムが見つかりません: {columns}. 利用可能なカラム: {available_columns}" 269 ) 270 selected_columns = list(set(columns + required_columns)) 271 base_df = base_df[selected_columns] 272 273 return base_df
指定されたシートを読み込み、DataFrameとして返却します。 デフォルトでは2行目(単位の行)はスキップされます。 重複するカラム名がある場合は、より先に指定されたシートに存在するカラムの値を保持します。
Parameters
sheet_names: str | list[str]
読み込むシート名を指定します。文字列または文字列のリストを指定できます。
columns: list[str] | None, optional
残すカラム名のリストを指定します。Noneの場合は全てのカラムを保持します。
col_datetime: str, optional
日付と時刻の情報が含まれるカラム名を指定します。デフォルト値は'Date'です。
header: int, optional
データのヘッダー行を指定します。デフォルト値は0です。
skiprows: int | list[int] | None, optional
スキップする行数を指定します。Noneの場合のデフォルトでは2行目をスキップします。
start_date: str | None, optional
開始日を'yyyy-MM-dd'形式で指定します。この日付の'00:00:00'のデータが開始行となります。
end_date: str | None, optional
終了日を'yyyy-MM-dd'形式で指定します。この日付をデータに含めるかはinclude_end_dateフラグによって変わります。
include_end_date: bool, optional
終了日を含めるかどうかを指定します。デフォルト値はTrueです。
sort_by_date: bool, optional
ファイルの日付でソートするかどうかを指定します。デフォルト値はTrueです。
Returns
pd.DataFrame
読み込まれたデータを結合したDataFrameを返します。
Examples
>>> converter = MonthlyConverter("path/to/excel/files")
>>> # 単一シートの読み込み
>>> df = converter.read_sheets("Sheet1")
>>> # 複数シートの読み込み
>>> df = converter.read_sheets(["Sheet1", "Sheet2"])
>>> # 特定の期間のデータ読み込み
>>> df = converter.read_sheets(
... "Sheet1",
... start_date="2023-01-01",
... end_date="2023-12-31"
... )
281 @staticmethod 282 def get_last_day_of_month(year: int, month: int) -> int: 283 """ 284 指定された年月の最終日を返します。 285 286 Parameters 287 ---------- 288 year: int 289 年を指定します。 290 month: int 291 月を指定します。1から12の整数を指定してください。 292 293 Returns 294 ---------- 295 int 296 指定された年月の最終日の日付。1から31の整数で返されます。 297 298 Examples 299 ---------- 300 >>> MonthlyConverter.get_last_day_of_month(2023, 2) 301 28 302 >>> MonthlyConverter.get_last_day_of_month(2024, 2) # 閏年の場合 303 29 304 """ 305 next_month = ( 306 datetime(year, month % 12 + 1, 1) 307 if month < 12 308 else datetime(year + 1, 1, 1) 309 ) 310 last_day = (next_month - timedelta(days=1)).day 311 return last_day
指定された年月の最終日を返します。
Parameters
year: int
年を指定します。
month: int
月を指定します。1から12の整数を指定してください。
Returns
int
指定された年月の最終日の日付。1から31の整数で返されます。
Examples
>>> MonthlyConverter.get_last_day_of_month(2023, 2)
28
>>> MonthlyConverter.get_last_day_of_month(2024, 2) # 閏年の場合
29
313 @staticmethod 314 def extract_period_data( 315 df: pd.DataFrame, 316 start_date: str | pd.Timestamp, 317 end_date: str | pd.Timestamp, 318 include_end_date: bool = True, 319 datetime_column: str = "Date", 320 ) -> pd.DataFrame: 321 """ 322 指定された期間のデータを抽出します。 323 324 Parameters 325 ---------- 326 df: pd.DataFrame 327 入力データフレーム。 328 start_date: str | pd.Timestamp 329 開始日。YYYY-MM-DD形式の文字列またはTimestampで指定します。 330 end_date: str | pd.Timestamp 331 終了日。YYYY-MM-DD形式の文字列またはTimestampで指定します。 332 include_end_date: bool, optional 333 終了日を含めるかどうかを指定します。デフォルトはTrueです。 334 datetime_column: str, optional 335 日付を含む列の名前を指定します。デフォルトは"Date"です。 336 337 Returns 338 ---------- 339 pd.DataFrame 340 指定された期間のデータのみを含むデータフレーム。 341 342 Examples 343 ---------- 344 >>> df = pd.DataFrame({ 345 ... 'Date': ['2023-01-01', '2023-01-02', '2023-01-03'], 346 ... 'Value': [1, 2, 3] 347 ... }) 348 >>> MonthlyConverter.extract_period_data( 349 ... df, 350 ... '2023-01-01', 351 ... '2023-01-02' 352 ... ) 353 Date Value 354 0 2023-01-01 1 355 1 2023-01-02 2 356 """ 357 # データフレームのコピーを作成 358 df_internal = df.copy() 359 df_internal[datetime_column] = pd.to_datetime(df_internal[datetime_column]) 360 start_dt = pd.to_datetime(start_date) 361 end_dt = pd.to_datetime(end_date) 362 363 # 開始日と終了日の順序チェック 364 if start_dt > end_dt: 365 raise ValueError("start_date は end_date より前である必要があります") 366 367 # 期間でフィルタリング 368 period_data = df_internal[ 369 (df_internal[datetime_column] >= start_dt) 370 & ( 371 df_internal[datetime_column] 372 < (end_dt + pd.Timedelta(days=1) if include_end_date else end_dt) 373 ) 374 ] 375 376 return period_data
指定された期間のデータを抽出します。
Parameters
df: pd.DataFrame
入力データフレーム。
start_date: str | pd.Timestamp
開始日。YYYY-MM-DD形式の文字列またはTimestampで指定します。
end_date: str | pd.Timestamp
終了日。YYYY-MM-DD形式の文字列またはTimestampで指定します。
include_end_date: bool, optional
終了日を含めるかどうかを指定します。デフォルトはTrueです。
datetime_column: str, optional
日付を含む列の名前を指定します。デフォルトは"Date"です。
Returns
pd.DataFrame
指定された期間のデータのみを含むデータフレーム。
Examples
>>> df = pd.DataFrame({
... 'Date': ['2023-01-01', '2023-01-02', '2023-01-03'],
... 'Value': [1, 2, 3]
... })
>>> MonthlyConverter.extract_period_data(
... df,
... '2023-01-01',
... '2023-01-02'
... )
Date Value
0 2023-01-01 1
1 2023-01-02 2
378 @staticmethod 379 def merge_dataframes( 380 df1: pd.DataFrame, df2: pd.DataFrame, date_column: str = "Date" 381 ) -> pd.DataFrame: 382 """ 383 2つのDataFrameを結合します。重複するカラムは元の名前とサフィックス付きの両方を保持します。 384 385 Parameters 386 ---------- 387 df1: pd.DataFrame 388 ベースとなるデータフレームを指定します。 389 df2: pd.DataFrame 390 結合するデータフレームを指定します。 391 date_column: str, optional 392 日付カラムの名前を指定します。デフォルトは"Date"です。 393 394 Returns 395 ---------- 396 pd.DataFrame 397 結合されたデータフレームを返します。重複するカラムには_xと_yのサフィックスが付与されます。 398 399 Examples 400 ---------- 401 >>> df1 = pd.DataFrame({ 402 ... 'Date': ['2023-01-01', '2023-01-02'], 403 ... 'Value': [1, 2] 404 ... }) 405 >>> df2 = pd.DataFrame({ 406 ... 'Date': ['2023-01-01', '2023-01-02'], 407 ... 'Value': [10, 20] 408 ... }) 409 >>> MonthlyConverter.merge_dataframes(df1, df2) 410 Date Value Value_x Value_y 411 0 2023-01-01 1 1 10 412 1 2023-01-02 2 2 20 413 """ 414 # インデックスをリセット 415 df1 = df1.reset_index(drop=True) 416 df2 = df2.reset_index(drop=True) 417 418 # 日付カラムを統一 419 df2[date_column] = df1[date_column] 420 421 # 重複しないカラムと重複するカラムを分離 422 duplicate_cols = [date_column, "year", "month"] # 常に除外するカラム 423 overlapping_cols = [ 424 col 425 for col in df2.columns 426 if col in df1.columns and col not in duplicate_cols 427 ] 428 unique_cols = [ 429 col 430 for col in df2.columns 431 if col not in df1.columns and col not in duplicate_cols 432 ] 433 434 # 結果のDataFrameを作成 435 result = df1.copy() 436 437 # 重複しないカラムを追加 438 for col in unique_cols: 439 result[col] = df2[col] 440 441 # 重複するカラムを処理 442 for col in overlapping_cols: 443 # 元のカラムはdf1の値を保持(既に result に含まれている) 444 # _x サフィックスでdf1の値を追加 445 result[f"{col}_x"] = df1[col] 446 # _y サフィックスでdf2の値を追加 447 result[f"{col}_y"] = df2[col] 448 449 return result
2つのDataFrameを結合します。重複するカラムは元の名前とサフィックス付きの両方を保持します。
Parameters
df1: pd.DataFrame
ベースとなるデータフレームを指定します。
df2: pd.DataFrame
結合するデータフレームを指定します。
date_column: str, optional
日付カラムの名前を指定します。デフォルトは"Date"です。
Returns
pd.DataFrame
結合されたデータフレームを返します。重複するカラムには_xと_yのサフィックスが付与されます。
Examples
>>> df1 = pd.DataFrame({
... 'Date': ['2023-01-01', '2023-01-02'],
... 'Value': [1, 2]
... })
>>> df2 = pd.DataFrame({
... 'Date': ['2023-01-01', '2023-01-02'],
... 'Value': [10, 20]
... })
>>> MonthlyConverter.merge_dataframes(df1, df2)
Date Value Value_x Value_y
0 2023-01-01 1 1 10
1 2023-01-02 2 2 20
513 @staticmethod 514 def extract_monthly_data( 515 df: pd.DataFrame, 516 target_months: list[int], 517 start_day: int | None = None, 518 end_day: int | None = None, 519 datetime_column: str = "Date", 520 ) -> pd.DataFrame: 521 """ 522 指定された月と期間のデータを抽出します。 523 524 Parameters 525 ---------- 526 df: pd.DataFrame 527 入力データフレーム。 528 target_months: list[int] 529 抽出したい月のリスト(1から12の整数)。 530 start_day: int | None 531 開始日(1から31の整数)。Noneの場合は月初め。 532 end_day: int | None 533 終了日(1から31の整数)。Noneの場合は月末。 534 datetime_column: str, optional 535 日付を含む列の名前。デフォルトは"Date"。 536 537 Returns 538 ---------- 539 pd.DataFrame 540 指定された期間のデータのみを含むデータフレーム。 541 542 .. warning:: 543 このメソッドは非推奨です。代わりに `extract_period_data` を使用してください。 544 v1.0.0 で削除される予定です。 545 """ 546 try: 547 ver = version("py_flux_tracer") 548 # print(ver) 549 if ver.startswith("0."): 550 warnings.warn( 551 "`extract_monthly_data` is deprecated. Please use `extract_period_data` instead. This method will be removed in v1.0.0.", 552 FutureWarning, 553 stacklevel=2, # 警告メッセージでライブラリの内部実装ではなく、非推奨のメソッドを使用しているユーザーのコードの行を指し示すことができる 554 ) 555 except Exception: 556 pass 557 558 # 入力チェック 559 if not all(1 <= month <= 12 for month in target_months): 560 raise ValueError("target_monthsは1から12の間である必要があります") 561 562 if start_day is not None and not 1 <= start_day <= 31: 563 raise ValueError("start_dayは1から31の間である必要があります") 564 565 if end_day is not None and not 1 <= end_day <= 31: 566 raise ValueError("end_dayは1から31の間である必要があります") 567 568 if start_day is not None and end_day is not None and start_day > end_day: 569 raise ValueError("start_dayはend_day以下である必要があります") 570 571 # datetime_column をDatetime型に変換 572 df_internal = df.copy() 573 df_internal[datetime_column] = pd.to_datetime(df_internal[datetime_column]) 574 575 # 月でフィルタリング 576 monthly_data = df_internal[ 577 df_internal[datetime_column].dt.month.isin(target_months) 578 ] 579 580 # 日付範囲でフィルタリング 581 if start_day is not None: 582 monthly_data = monthly_data[ 583 monthly_data[datetime_column].dt.day >= start_day 584 ] 585 if end_day is not None: 586 monthly_data = monthly_data[monthly_data[datetime_column].dt.day <= end_day] 587 588 return monthly_data
指定された月と期間のデータを抽出します。
Parameters
df: pd.DataFrame
入力データフレーム。
target_months: list[int]
抽出したい月のリスト(1から12の整数)。
start_day: int | None
開始日(1から31の整数)。Noneの場合は月初め。
end_day: int | None
終了日(1から31の整数)。Noneの場合は月末。
datetime_column: str, optional
日付を含む列の名前。デフォルトは"Date"。
Returns
pd.DataFrame
指定された期間のデータのみを含むデータフレーム。
このメソッドは非推奨です。代わりに extract_period_data
を使用してください。
v1.0.0 で削除される予定です。
63class MonthlyFiguresGenerator: 64 def __init__( 65 self, 66 logger: Logger | None = None, 67 logging_debug: bool = False, 68 ) -> None: 69 """ 70 Monthlyシートから作成したDataFrameを解析して図を作成するクラス 71 72 Parameters 73 ------ 74 logger: Logger | None 75 使用するロガー。Noneの場合は新しいロガーを作成します。 76 logging_debug: bool 77 ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。 78 """ 79 # ロガー 80 log_level: int = INFO 81 if logging_debug: 82 log_level = DEBUG 83 self.logger: Logger = setup_logger(logger=logger, log_level=log_level) 84 85 def plot_c1c2_concs_and_fluxes_timeseries( 86 self, 87 df: pd.DataFrame, 88 output_dirpath: str | Path | None = None, 89 output_filename: str = "conc_flux_timeseries.png", 90 col_datetime: str = "Date", 91 col_ch4_conc: str = "CH4_ultra", 92 col_ch4_flux: str = "Fch4_ultra", 93 col_c2h6_conc: str = "C2H6_ultra", 94 col_c2h6_flux: str = "Fc2h6_ultra", 95 ylim_ch4_conc: tuple = (1.8, 2.6), 96 ylim_ch4_flux: tuple = (-100, 600), 97 ylim_c2h6_conc: tuple = (-12, 20), 98 ylim_c2h6_flux: tuple = (-20, 40), 99 figsize: tuple[float, float] = (12, 16), 100 dpi: float | None = 350, 101 save_fig: bool = True, 102 show_fig: bool = True, 103 print_summary: bool = False, 104 ) -> None: 105 """CH4とC2H6の濃度とフラックスの時系列プロットを作成します。 106 107 Parameters 108 ---------- 109 df: pd.DataFrame 110 月別データを含むDataFrameを指定します。 111 output_dirpath: str | Path | None, optional 112 出力ディレクトリのパスを指定します。save_fig=Trueの場合は必須です。 113 output_filename: str, optional 114 出力ファイル名を指定します。デフォルト値は"conc_flux_timeseries.png"です。 115 col_datetime: str, optional 116 日付列の名前を指定します。デフォルト値は"Date"です。 117 col_ch4_conc: str, optional 118 CH4濃度列の名前を指定します。デフォルト値は"CH4_ultra"です。 119 col_ch4_flux: str, optional 120 CH4フラックス列の名前を指定します。デフォルト値は"Fch4_ultra"です。 121 col_c2h6_conc: str, optional 122 C2H6濃度列の名前を指定します。デフォルト値は"C2H6_ultra"です。 123 col_c2h6_flux: str, optional 124 C2H6フラックス列の名前を指定します。デフォルト値は"Fc2h6_ultra"です。 125 ylim_ch4_conc: tuple, optional 126 CH4濃度のy軸範囲を指定します。デフォルト値は(1.8, 2.6)です。 127 ylim_ch4_flux: tuple, optional 128 CH4フラックスのy軸範囲を指定します。デフォルト値は(-100, 600)です。 129 ylim_c2h6_conc: tuple, optional 130 C2H6濃度のy軸範囲を指定します。デフォルト値は(-12, 20)です。 131 ylim_c2h6_flux: tuple, optional 132 C2H6フラックスのy軸範囲を指定します。デフォルト値は(-20, 40)です。 133 figsize: tuple[float, float], optional 134 プロットのサイズを指定します。デフォルト値は(12, 16)です。 135 dpi: float | None, optional 136 プロットのdpiを指定します。デフォルト値は350です。 137 save_fig: bool, optional 138 図を保存するかどうかを指定します。デフォルト値はTrueです。 139 show_fig: bool, optional 140 図を表示するかどうかを指定します。デフォルト値はTrueです。 141 print_summary: bool, optional 142 解析情報をprintするかどうかを指定します。デフォルト値はFalseです。 143 144 Examples 145 -------- 146 >>> generator = MonthlyFiguresGenerator() 147 >>> generator.plot_c1c2_concs_and_fluxes_timeseries( 148 ... df=monthly_data, 149 ... output_dirpath="output", 150 ... ylim_ch4_conc=(1.5, 3.0), 151 ... print_summary=True 152 ... ) 153 """ 154 # dfのコピー 155 df_internal: pd.DataFrame = df.copy() 156 if print_summary: 157 # 統計情報の計算と表示 158 for name, col in [ 159 ("CH4 concentration", col_ch4_conc), 160 ("CH4 flux", col_ch4_flux), 161 ("C2H6 concentration", col_c2h6_conc), 162 ("C2H6 flux", col_c2h6_flux), 163 ]: 164 # NaNを除外してから統計量を計算 165 valid_data = df_internal[col].dropna() 166 167 if len(valid_data) > 0: 168 # quantileで計算(0-1の範囲) 169 quantile_05 = valid_data.quantile(0.05) 170 quantile_95 = valid_data.quantile(0.95) 171 mean_value = np.nanmean(valid_data) 172 positive_ratio = (valid_data > 0).mean() * 100 173 174 print(f"\n{name}:") 175 # パーセンタイルで表示(0-100の範囲) 176 print( 177 f"90パーセンタイルレンジ: {quantile_05:.2f} - {quantile_95:.2f}" 178 ) 179 print(f"平均値: {mean_value:.2f}") 180 print(f"正の値の割合: {positive_ratio:.1f}%") 181 else: 182 print(f"\n{name}: データが存在しません") 183 184 # プロットの作成 185 _, (ax1, ax2, ax3, ax4) = plt.subplots(4, 1, figsize=figsize, sharex=True) 186 187 # CH4濃度のプロット 188 ax1.scatter( 189 df_internal[col_datetime], 190 df_internal[col_ch4_conc], 191 color="red", 192 alpha=0.5, 193 s=20, 194 ) 195 ax1.set_ylabel("CH$_4$ Concentration\n(ppm)") 196 ax1.set_ylim(*ylim_ch4_conc) # 引数からy軸範囲を設定 197 ax1.text(0.02, 0.98, "(a)", transform=ax1.transAxes, va="top", fontsize=20) 198 ax1.grid(True, alpha=0.3) 199 200 # CH4フラックスのプロット 201 ax2.scatter( 202 df_internal[col_datetime], 203 df_internal[col_ch4_flux], 204 color="red", 205 alpha=0.5, 206 s=20, 207 ) 208 ax2.set_ylabel("CH$_4$ flux\n(nmol m$^{-2}$ s$^{-1}$)") 209 ax2.set_ylim(*ylim_ch4_flux) # 引数からy軸範囲を設定 210 ax2.text(0.02, 0.98, "(b)", transform=ax2.transAxes, va="top", fontsize=20) 211 ax2.grid(True, alpha=0.3) 212 213 # C2H6濃度のプロット 214 ax3.scatter( 215 df_internal[col_datetime], 216 df_internal[col_c2h6_conc], 217 color="orange", 218 alpha=0.5, 219 s=20, 220 ) 221 ax3.set_ylabel("C$_2$H$_6$ Concentration\n(ppb)") 222 ax3.set_ylim(*ylim_c2h6_conc) # 引数からy軸範囲を設定 223 ax3.text(0.02, 0.98, "(c)", transform=ax3.transAxes, va="top", fontsize=20) 224 ax3.grid(True, alpha=0.3) 225 226 # C2H6フラックスのプロット 227 ax4.scatter( 228 df_internal[col_datetime], 229 df_internal[col_c2h6_flux], 230 color="orange", 231 alpha=0.5, 232 s=20, 233 ) 234 ax4.set_ylabel("C$_2$H$_6$ flux\n(nmol m$^{-2}$ s$^{-1}$)") 235 ax4.set_ylim(*ylim_c2h6_flux) # 引数からy軸範囲を設定 236 ax4.text(0.02, 0.98, "(d)", transform=ax4.transAxes, va="top", fontsize=20) 237 ax4.grid(True, alpha=0.3) 238 239 # x軸の設定 240 ax4.xaxis.set_major_locator(mdates.MonthLocator(interval=1)) 241 ax4.xaxis.set_major_formatter(mdates.DateFormatter("%m")) 242 plt.setp(ax4.get_xticklabels(), rotation=0, ha="right") 243 ax4.set_xlabel("Month") 244 245 # レイアウトの調整と保存 246 plt.tight_layout() 247 248 # 図の保存 249 if save_fig: 250 if output_dirpath is None: 251 raise ValueError( 252 "save_fig = True のとき、 output_dirpath に有効なディレクトリパスを指定する必要があります。" 253 ) 254 # 出力ディレクトリの作成 255 os.makedirs(output_dirpath, exist_ok=True) 256 output_filepath: str = os.path.join(output_dirpath, output_filename) 257 plt.savefig(output_filepath, dpi=dpi, bbox_inches="tight") 258 if show_fig: 259 plt.show() 260 plt.close() 261 262 if print_summary: 263 264 def analyze_top_values(df, column_name, top_percent=20): 265 print(f"\n{column_name}の上位{top_percent}%の分析:") 266 # DataFrameのコピーを作成し、日時関連の列を追加 267 df_analysis = df.copy() 268 df_analysis["hour"] = pd.to_datetime(df_analysis[col_datetime]).dt.hour 269 df_analysis["month"] = pd.to_datetime( 270 df_analysis[col_datetime] 271 ).dt.month 272 df_analysis["weekday"] = pd.to_datetime( 273 df_analysis[col_datetime] 274 ).dt.dayofweek 275 276 # 上位20%のしきい値を計算 277 threshold = df_analysis[column_name].quantile(1 - top_percent / 100) 278 high_values = df_analysis[df_analysis[column_name] > threshold] 279 280 # 月ごとの分析 281 print("\n月別分布:") 282 monthly_counts = high_values.groupby("month").size() 283 total_counts = df_analysis.groupby("month").size() 284 monthly_percentages = (monthly_counts / total_counts * 100).round(1) 285 286 # 月ごとのデータを安全に表示 287 available_months = set(monthly_counts.index) & set(total_counts.index) 288 for month in sorted(available_months): 289 print( 290 f"月{month}: {monthly_percentages[month]}% ({monthly_counts[month]}件/{total_counts[month]}件)" 291 ) 292 293 # 時間帯ごとの分析(3時間区切り) 294 print("\n時間帯別分布:") 295 # copyを作成して新しい列を追加 296 high_values = high_values.copy() 297 high_values["time_block"] = high_values["hour"] // 3 * 3 298 time_blocks = high_values.groupby("time_block").size() 299 total_time_blocks = df_analysis.groupby( 300 df_analysis["hour"] // 3 * 3 301 ).size() 302 time_percentages = (time_blocks / total_time_blocks * 100).round(1) 303 304 # 時間帯ごとのデータを安全に表示 305 available_blocks = set(time_blocks.index) & set(total_time_blocks.index) 306 for block in sorted(available_blocks): 307 print( 308 f"{block:02d}:00-{block + 3:02d}:00: {time_percentages[block]}% ({time_blocks[block]}件/{total_time_blocks[block]}件)" 309 ) 310 311 # 曜日ごとの分析 312 print("\n曜日別分布:") 313 weekday_names = ["月曜", "火曜", "水曜", "木曜", "金曜", "土曜", "日曜"] 314 weekday_counts = high_values.groupby("weekday").size() 315 total_weekdays = df_analysis.groupby("weekday").size() 316 weekday_percentages = (weekday_counts / total_weekdays * 100).round(1) 317 318 # 曜日ごとのデータを安全に表示 319 available_days = set(weekday_counts.index) & set(total_weekdays.index) 320 for day in sorted(available_days): 321 if 0 <= day <= 6: # 有効な曜日インデックスのチェック 322 print( 323 f"{weekday_names[day]}: {weekday_percentages[day]}% ({weekday_counts[day]}件/{total_weekdays[day]}件)" 324 ) 325 326 # 濃度とフラックスそれぞれの分析を実行 327 print("\n=== 上位値の時間帯・曜日分析 ===") 328 analyze_top_values(df_internal, col_ch4_conc) 329 analyze_top_values(df_internal, col_ch4_flux) 330 analyze_top_values(df_internal, col_c2h6_conc) 331 analyze_top_values(df_internal, col_c2h6_flux) 332 333 def plot_c1c2_timeseries( 334 self, 335 df: pd.DataFrame, 336 col_ch4_flux: str, 337 col_c2h6_flux: str, 338 output_dirpath: str | Path | None = None, 339 output_filename: str = "timeseries_year.png", 340 col_datetime: str = "Date", 341 window_size: int = 24 * 7, 342 confidence_interval: float = 0.95, 343 subplot_label_ch4: str | None = "(a)", 344 subplot_label_c2h6: str | None = "(b)", 345 subplot_fontsize: int = 20, 346 show_ci: bool = True, 347 ch4_ylim: tuple[float, float] | None = None, 348 c2h6_ylim: tuple[float, float] | None = None, 349 start_date: str | None = None, 350 end_date: str | None = None, 351 figsize: tuple[float, float] = (16, 6), 352 dpi: float | None = 350, 353 save_fig: bool = True, 354 show_fig: bool = True, 355 ) -> None: 356 """CH4とC2H6フラックスの時系列変動をプロットします。 357 358 Parameters 359 ---------- 360 df: pd.DataFrame 361 プロットするデータを含むDataFrameを指定します。 362 col_ch4_flux: str 363 CH4フラックスのカラム名を指定します。 364 col_c2h6_flux: str 365 C2H6フラックスのカラム名を指定します。 366 output_dirpath: str | Path | None, optional 367 出力ディレクトリのパスを指定します。save_fig=Trueの場合は必須です。 368 output_filename: str, optional 369 出力ファイル名を指定します。デフォルト値は"timeseries_year.png"です。 370 col_datetime: str, optional 371 日時カラムの名前を指定します。デフォルト値は"Date"です。 372 window_size: int, optional 373 移動平均の窓サイズを指定します。デフォルト値は24*7(1週間)です。 374 confidence_interval: float, optional 375 信頼区間を指定します。0から1の間の値で、デフォルト値は0.95(95%)です。 376 subplot_label_ch4: str | None, optional 377 CH4プロットのラベルを指定します。デフォルト値は"(a)"です。 378 subplot_label_c2h6: str | None, optional 379 C2H6プロットのラベルを指定します。デフォルト値は"(b)"です。 380 subplot_fontsize: int, optional 381 サブプロットのフォントサイズを指定します。デフォルト値は20です。 382 show_ci: bool, optional 383 信頼区間を表示するかどうかを指定します。デフォルト値はTrueです。 384 ch4_ylim: tuple[float, float] | None, optional 385 CH4のy軸範囲を指定します。未指定の場合は自動で設定されます。 386 c2h6_ylim: tuple[float, float] | None, optional 387 C2H6のy軸範囲を指定します。未指定の場合は自動で設定されます。 388 start_date: str | None, optional 389 開始日を"YYYY-MM-DD"形式で指定します。未指定の場合はデータの最初の日付が使用されます。 390 end_date: str | None, optional 391 終了日を"YYYY-MM-DD"形式で指定します。未指定の場合はデータの最後の日付が使用されます。 392 figsize: tuple[float, float], optional 393 プロットのサイズを指定します。デフォルト値は(16, 6)です。 394 dpi: float | None, optional 395 プロットのdpiを指定します。デフォルト値は350です。 396 save_fig: bool, optional 397 プロットを保存するかどうかを指定します。デフォルト値はTrueです。 398 show_fig: bool, optional 399 プロットを表示するかどうかを指定します。デフォルト値はTrueです。 400 401 Examples 402 -------- 403 >>> generator = MonthlyFiguresGenerator() 404 >>> generator.plot_c1c2_timeseries( 405 ... df=monthly_data, 406 ... col_ch4_flux="Fch4_ultra", 407 ... col_c2h6_flux="Fc2h6_ultra", 408 ... output_dirpath="output", 409 ... start_date="2023-01-01", 410 ... end_date="2023-12-31" 411 ... ) 412 """ 413 # データの準備 414 df_internal = df.copy() 415 if not isinstance(df_internal.index, pd.DatetimeIndex): 416 df_internal[col_datetime] = pd.to_datetime(df_internal[col_datetime]) 417 df_internal.set_index(col_datetime, inplace=True) 418 419 # 日付範囲の処理 420 if start_date is not None: 421 start_dt = pd.to_datetime(start_date).normalize() # 時刻を00:00:00に設定 422 # df_min_date = ( 423 # df_internal.index.normalize().min().normalize() 424 # ) # 日付のみの比較のため正規化 425 df_min_date = pd.to_datetime(df_internal.index.min()).normalize() 426 427 # データの最小日付が指定開始日より後の場合にのみ警告 428 if df_min_date.date() > start_dt.date(): 429 self.logger.warning( 430 f"指定された開始日{start_date}がデータの開始日{df_min_date.strftime('%Y-%m-%d')}より前です。" 431 f"データの開始日を使用します。" 432 ) 433 start_dt = df_min_date 434 else: 435 # start_dt = df_internal.index.normalize().min() 436 start_dt = pd.to_datetime(df_internal.index.min()).normalize() 437 438 if end_date is not None: 439 end_dt = ( 440 pd.to_datetime(end_date).normalize() 441 + pd.Timedelta(days=1) 442 - pd.Timedelta(seconds=1) 443 ) 444 # df_max_date = ( 445 # df_internal.index.normalize().max().normalize() 446 # ) # 日付のみの比較のため正規化 447 df_max_date = pd.to_datetime(df_internal.index.max()).normalize() 448 449 # データの最大日付が指定終了日より前の場合にのみ警告 450 if df_max_date.date() < pd.to_datetime(end_date).date(): 451 self.logger.warning( 452 f"指定された終了日{end_date}がデータの終了日{df_max_date.strftime('%Y-%m-%d')}より後です。" 453 f"データの終了日を使用します。" 454 ) 455 end_dt = df_internal.index.max() 456 else: 457 end_dt = df_internal.index.max() 458 459 # 指定された期間のデータを抽出 460 mask = (df_internal.index >= start_dt) & (df_internal.index <= end_dt) 461 df_internal = df_internal[mask] 462 463 # CH4とC2H6の移動平均と信頼区間を計算 464 ch4_mean, ch4_lower, ch4_upper = calculate_rolling_stats( 465 df_internal[col_ch4_flux], window_size, confidence_interval 466 ) 467 c2h6_mean, c2h6_lower, c2h6_upper = calculate_rolling_stats( 468 df_internal[col_c2h6_flux], window_size, confidence_interval 469 ) 470 471 # プロットの作成 472 fig, (ax1, ax2) = plt.subplots(1, 2, figsize=figsize) 473 474 # CH4プロット 475 ax1.plot(df_internal.index, ch4_mean, "red", label="CH$_4$") 476 if show_ci: 477 ax1.fill_between( 478 df_internal.index, ch4_lower, ch4_upper, color="red", alpha=0.2 479 ) 480 if subplot_label_ch4: 481 ax1.text( 482 0.02, 483 0.98, 484 subplot_label_ch4, 485 transform=ax1.transAxes, 486 va="top", 487 fontsize=subplot_fontsize, 488 ) 489 ax1.set_ylabel("CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)") 490 if ch4_ylim is not None: 491 ax1.set_ylim(ch4_ylim) 492 ax1.grid(True, alpha=0.3) 493 494 # C2H6プロット 495 ax2.plot(df_internal.index, c2h6_mean, "orange", label="C$_2$H$_6$") 496 if show_ci: 497 ax2.fill_between( 498 df_internal.index, c2h6_lower, c2h6_upper, color="orange", alpha=0.2 499 ) 500 if subplot_label_c2h6: 501 ax2.text( 502 0.02, 503 0.98, 504 subplot_label_c2h6, 505 transform=ax2.transAxes, 506 va="top", 507 fontsize=subplot_fontsize, 508 ) 509 ax2.set_ylabel("C$_2$H$_6$ flux (nmol m$^{-2}$ s$^{-1}$)") 510 if c2h6_ylim is not None: 511 ax2.set_ylim(c2h6_ylim) 512 ax2.grid(True, alpha=0.3) 513 514 # x軸の設定 515 for ax in [ax1, ax2]: 516 ax.set_xlabel("Month") 517 # x軸の範囲を設定 518 ax.set_xlim(start_dt, end_dt) 519 520 # 1ヶ月ごとの主目盛り 521 ax.xaxis.set_major_locator(mdates.MonthLocator(interval=1)) 522 523 # カスタムフォーマッタの作成(数字を通常フォントで表示) 524 def date_formatter(x, p): 525 date = mdates.num2date(x) 526 return f"{date.strftime('%m')}" 527 528 ax.xaxis.set_major_formatter(FuncFormatter(date_formatter)) 529 530 # 補助目盛りの設定 531 ax.xaxis.set_minor_locator(mdates.MonthLocator()) 532 # ティックラベルの回転と位置調整 533 plt.setp(ax.xaxis.get_majorticklabels(), ha="right") 534 535 plt.tight_layout() 536 537 if save_fig: 538 if output_dirpath is None: 539 raise ValueError( 540 "save_fig = True のとき、 output_dirpath に有効なディレクトリパスを指定する必要があります。" 541 ) 542 # 出力ディレクトリの作成 543 os.makedirs(output_dirpath, exist_ok=True) 544 output_filepath: str = os.path.join(output_dirpath, output_filename) 545 plt.savefig(output_filepath, dpi=dpi, bbox_inches="tight") 546 if show_fig: 547 plt.show() 548 plt.close(fig=fig) 549 550 def plot_fluxes_comparison( 551 self, 552 df: pd.DataFrame, 553 cols_flux: list[str], 554 labels: list[str], 555 colors: list[str], 556 output_dirpath: str | Path | None, 557 output_filename: str = "ch4_flux_comparison.png", 558 col_datetime: str = "Date", 559 window_size: int = 24 * 7, 560 confidence_interval: float = 0.95, 561 subplot_label: str | None = None, 562 subplot_fontsize: int = 20, 563 show_ci: bool = True, 564 ylim: tuple[float, float] | None = None, 565 start_date: str | None = None, 566 end_date: str | None = None, 567 include_end_date: bool = True, 568 legend_loc: str = "upper right", 569 apply_ma: bool = True, 570 hourly_mean: bool = False, 571 x_interval: Literal["month", "10days"] = "month", 572 xlabel: str = "Month", 573 ylabel: str = "CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)", 574 figsize: tuple[float, float] = (12, 6), 575 dpi: float | None = 350, 576 save_fig: bool = True, 577 show_fig: bool = True, 578 ) -> None: 579 """複数のCH4フラックスの時系列変動を比較するプロットを作成します。 580 581 Parameters 582 ---------- 583 df: pd.DataFrame 584 プロットするデータを含むDataFrameを指定します。 585 cols_flux: list[str] 586 比較するフラックスのカラム名のリストを指定します。 587 labels: list[str] 588 凡例に表示する各フラックスのラベルのリストを指定します。 589 colors: list[str] 590 各フラックスの色のリストを指定します。 591 output_dirpath: str | Path | None 592 出力ディレクトリのパスを指定します。save_fig=Trueの場合は必須です。 593 output_filename: str, optional 594 出力ファイル名を指定します。デフォルト値は"ch4_flux_comparison.png"です。 595 col_datetime: str, optional 596 日時カラムの名前を指定します。デフォルト値は"Date"です。 597 window_size: int, optional 598 移動平均の窓サイズを指定します。デフォルト値は24*7(1週間)です。 599 confidence_interval: float, optional 600 信頼区間を指定します。0から1の間の値で、デフォルト値は0.95(95%)です。 601 subplot_label: str | None, optional 602 プロットのラベルを指定します。デフォルト値はNoneです。 603 subplot_fontsize: int, optional 604 サブプロットのフォントサイズを指定します。デフォルト値は20です。 605 show_ci: bool, optional 606 信頼区間を表示するかどうかを指定します。デフォルト値はTrueです。 607 ylim: tuple[float, float] | None, optional 608 y軸の範囲を指定します。デフォルト値はNoneです。 609 start_date: str | None, optional 610 開始日をYYYY-MM-DD形式で指定します。デフォルト値はNoneです。 611 end_date: str | None, optional 612 終了日をYYYY-MM-DD形式で指定します。デフォルト値はNoneです。 613 include_end_date: bool, optional 614 終了日を含めるかどうかを指定します。デフォルト値はTrueです。 615 legend_loc: str, optional 616 凡例の位置を指定します。デフォルト値は"upper right"です。 617 apply_ma: bool, optional 618 移動平均を適用するかどうかを指定します。デフォルト値はTrueです。 619 hourly_mean: bool, optional 620 1時間平均を適用するかどうかを指定します。デフォルト値はFalseです。 621 x_interval: Literal["month", "10days"], optional 622 x軸の目盛り間隔を指定します。"month"(月初めのみ)または"10days"(10日刻み)を指定できます。デフォルト値は"month"です。 623 xlabel: str, optional 624 x軸のラベルを指定します。デフォルト値は"Month"です。 625 ylabel: str, optional 626 y軸のラベルを指定します。デフォルト値は"CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)"です。 627 figsize: tuple[float, float], optional 628 プロットのサイズを指定します。デフォルト値は(12, 6)です。 629 dpi: float | None, optional 630 プロットのdpiを指定します。デフォルト値は350です。 631 save_fig: bool, optional 632 図を保存するかどうかを指定します。デフォルト値はTrueです。 633 show_fig: bool, optional 634 図を表示するかどうかを指定します。デフォルト値はTrueです。 635 636 Examples 637 ------- 638 >>> generator = MonthlyFiguresGenerator() 639 >>> generator.plot_fluxes_comparison( 640 ... df=monthly_data, 641 ... cols_flux=["Fch4_ultra", "Fch4_picarro"], 642 ... labels=["Ultra", "Picarro"], 643 ... colors=["red", "blue"], 644 ... output_dirpath="output", 645 ... start_date="2023-01-01", 646 ... end_date="2023-12-31" 647 ... ) 648 """ 649 # データの準備 650 df_internal = df.copy() 651 652 # インデックスを日時型に変換 653 df_internal.index = pd.to_datetime(df_internal.index) 654 655 # 1時間平均の適用 656 if hourly_mean: 657 # 時間情報のみを使用してグループ化 658 df_internal = df_internal.groupby( 659 [df_internal.index.strftime("%Y-%m-%d"), df_internal.index.hour] 660 ).mean() 661 # マルチインデックスを日時インデックスに変換 662 df_internal.index = pd.to_datetime( 663 [f"{date} {hour:02d}:00:00" for date, hour in df_internal.index] 664 ) 665 666 # 日付範囲の処理 667 if start_date is not None: 668 start_dt = pd.to_datetime(start_date).normalize() # 時刻を00:00:00に設定 669 df_min_date = ( 670 df_internal.index.normalize().min().normalize() 671 ) # 日付のみの比較のため正規化 672 673 # データの最小日付が指定開始日より後の場合にのみ警告 674 if df_min_date.date() > start_dt.date(): 675 self.logger.warning( 676 f"指定された開始日{start_date}がデータの開始日{df_min_date.strftime('%Y-%m-%d')}より前です。" 677 f"データの開始日を使用します。" 678 ) 679 start_dt = df_min_date 680 else: 681 start_dt = df_internal.index.normalize().min() 682 683 if end_date is not None: 684 if include_end_date: 685 end_dt = ( 686 pd.to_datetime(end_date).normalize() 687 + pd.Timedelta(days=1) 688 - pd.Timedelta(seconds=1) 689 ) 690 else: 691 # 終了日を含まない場合、終了日の前日の23:59:59まで 692 end_dt = pd.to_datetime(end_date).normalize() - pd.Timedelta(seconds=1) 693 694 df_max_date = ( 695 df_internal.index.normalize().max().normalize() 696 ) # 日付のみの比較のため正規化 697 698 # データの最大日付が指定終了日より前の場合にのみ警告 699 compare_date = pd.to_datetime(end_date).date() 700 if not include_end_date: 701 compare_date = compare_date - pd.Timedelta(days=1) 702 703 if df_max_date.date() < compare_date: 704 self.logger.warning( 705 f"指定された終了日{end_date}がデータの終了日{df_max_date.strftime('%Y-%m-%d')}より後です。" 706 f"データの終了日を使用します。" 707 ) 708 end_dt = df_internal.index.max() 709 else: 710 end_dt = df_internal.index.max() 711 712 # 指定された期間のデータを抽出 713 mask = (df_internal.index >= start_dt) & (df_internal.index <= end_dt) 714 df_internal = df_internal[mask] 715 716 # プロットの作成 717 fig, ax = plt.subplots(figsize=figsize) 718 719 # 各フラックスのプロット 720 for flux_col, label, color in zip(cols_flux, labels, colors, strict=True): 721 if apply_ma: 722 # 移動平均の計算 723 mean, lower, upper = calculate_rolling_stats( 724 df_internal[flux_col], window_size, confidence_interval 725 ) 726 ax.plot(df_internal.index, mean, color, label=label, alpha=0.7) 727 if show_ci: 728 ax.fill_between( 729 df_internal.index, lower, upper, color=color, alpha=0.2 730 ) 731 else: 732 # 生データのプロット 733 ax.plot( 734 df_internal.index, 735 df_internal[flux_col], 736 color, 737 label=label, 738 alpha=0.7, 739 ) 740 741 # プロットの設定 742 if subplot_label: 743 ax.text( 744 0.02, 745 0.98, 746 subplot_label, 747 transform=ax.transAxes, 748 va="top", 749 fontsize=subplot_fontsize, 750 ) 751 752 ax.set_xlabel(xlabel) 753 ax.set_ylabel(ylabel) 754 755 if ylim is not None: 756 ax.set_ylim(ylim) 757 758 ax.grid(True, alpha=0.3) 759 ax.legend(loc=legend_loc) 760 761 # x軸の設定 762 ax.set_xlim(float(mdates.date2num(start_dt)), float(mdates.date2num(end_dt))) 763 764 if x_interval == "month": 765 # 月初めにメジャー線のみ表示 766 ax.xaxis.set_major_locator(mdates.MonthLocator()) 767 ax.xaxis.set_minor_locator(NullLocator()) # マイナー線を非表示 768 elif x_interval == "10days": 769 # 月初め(1日)、10日、20日、30日に目盛りを表示 770 class Custom10DayLocator(mdates.DateLocator): 771 def __call__(self): 772 dmin, dmax = self.viewlim_to_dt() 773 dates = [] 774 current = pd.to_datetime(dmin).normalize() 775 end = pd.to_datetime(dmax).normalize() 776 777 while current <= end: 778 # その月の1日、10日、20日、30日を追加 779 for day in [1, 10, 20, 30]: 780 try: 781 date = current.replace(day=day) 782 if dmin <= date <= dmax: 783 dates.append(date) 784 except ValueError: 785 # 30日が存在しない月(2月など)の場合は 786 # その月の最終日を使用 787 if day == 30: 788 last_day = ( 789 current + pd.DateOffset(months=1) 790 ).replace(day=1) - pd.Timedelta(days=1) 791 if dmin <= last_day <= dmax: 792 dates.append(last_day) 793 794 # 次の月へ 795 current = (current + pd.DateOffset(months=1)).replace(day=1) 796 797 return self.raise_if_exceeds( 798 [float(mdates.date2num(date)) for date in dates] 799 ) 800 801 ax.xaxis.set_major_locator(Custom10DayLocator()) 802 ax.xaxis.set_minor_locator(mdates.DayLocator()) 803 ax.grid(True, which="minor", alpha=0.1) 804 805 # カスタムフォーマッタの作成 806 def date_formatter(x, p): 807 date = mdates.num2date(x) 808 if x_interval == "month": 809 # 月初めの1日の場合のみ月を表示 810 if date.day == 1: 811 return f"{date.strftime('%m')}" 812 return "" 813 else: # "10days"の場合 814 # MM/DD形式で表示し、/を中心に配置 815 month = f"{date.strftime('%m'):>2}" # 右寄せで2文字 816 day = f"{date.strftime('%d'):<2}" # 左寄せで2文字 817 return f"{month}/{day}" 818 819 ax.xaxis.set_major_formatter(FuncFormatter(date_formatter)) 820 plt.setp( 821 ax.xaxis.get_majorticklabels(), ha="center", rotation=0 822 ) # 中央揃えに変更 823 824 plt.tight_layout() 825 826 if save_fig: 827 if output_dirpath is None: 828 raise ValueError( 829 "save_fig = True のとき、 output_dirpath に有効なディレクトリパスを指定する必要があります。" 830 ) 831 # 出力ディレクトリの作成 832 os.makedirs(output_dirpath, exist_ok=True) 833 output_filepath: str = os.path.join(output_dirpath, output_filename) 834 plt.savefig(output_filepath, dpi=dpi, bbox_inches="tight") 835 if show_fig: 836 plt.show() 837 plt.close(fig=fig) 838 839 def plot_c1c2_fluxes_diurnal_patterns( 840 self, 841 df: pd.DataFrame, 842 y_cols_ch4: list[str], 843 y_cols_c2h6: list[str], 844 labels_ch4: list[str], 845 labels_c2h6: list[str], 846 colors_ch4: list[str], 847 colors_c2h6: list[str], 848 output_dirpath: str | Path | None = None, 849 output_filename: str = "diurnal.png", 850 legend_only_ch4: bool = False, 851 add_label: bool = True, 852 add_legend: bool = True, 853 show_std: bool = False, 854 std_alpha: float = 0.2, 855 figsize: tuple[float, float] = (12, 5), 856 dpi: float | None = 350, 857 subplot_fontsize: int = 20, 858 subplot_label_ch4: str | None = "(a)", 859 subplot_label_c2h6: str | None = "(b)", 860 ax1_ylim: tuple[float, float] | None = None, 861 ax2_ylim: tuple[float, float] | None = None, 862 save_fig: bool = True, 863 show_fig: bool = True, 864 ) -> None: 865 """CH4とC2H6の日変化パターンを1つの図に並べてプロットします。 866 867 Parameters 868 ---------- 869 df: pd.DataFrame 870 入力データフレームを指定します。 871 y_cols_ch4: list[str] 872 CH4のプロットに使用するカラム名のリストを指定します。 873 y_cols_c2h6: list[str] 874 C2H6のプロットに使用するカラム名のリストを指定します。 875 labels_ch4: list[str] 876 CH4の各ラインに対応するラベルのリストを指定します。 877 labels_c2h6: list[str] 878 C2H6の各ラインに対応するラベルのリストを指定します。 879 colors_ch4: list[str] 880 CH4の各ラインに使用する色のリストを指定します。 881 colors_c2h6: list[str] 882 C2H6の各ラインに使用する色のリストを指定します。 883 output_dirpath: str | Path | None, optional 884 出力先ディレクトリのパスを指定します。save_fig=Trueの場合は必須です。 885 output_filename: str, optional 886 出力ファイル名を指定します。デフォルト値は"diurnal.png"です。 887 legend_only_ch4: bool, optional 888 CH4の凡例のみを表示するかどうかを指定します。デフォルト値はFalseです。 889 add_label: bool, optional 890 サブプロットラベルを表示するかどうかを指定します。デフォルト値はTrueです。 891 add_legend: bool, optional 892 凡例を表示するかどうかを指定します。デフォルト値はTrueです。 893 show_std: bool, optional 894 標準偏差を表示するかどうかを指定します。デフォルト値はFalseです。 895 std_alpha: float, optional 896 標準偏差の透明度を指定します。デフォルト値は0.2です。 897 figsize: tuple[float, float], optional 898 プロットのサイズを指定します。デフォルト値は(12, 5)です。 899 dpi: float | None, optional 900 プロットのdpiを指定します。デフォルト値は350です。 901 subplot_fontsize: int, optional 902 サブプロットのフォントサイズを指定します。デフォルト値は20です。 903 subplot_label_ch4: str | None, optional 904 CH4プロットのラベルを指定します。デフォルト値は"(a)"です。 905 subplot_label_c2h6: str | None, optional 906 C2H6プロットのラベルを指定します。デフォルト値は"(b)"です。 907 ax1_ylim: tuple[float, float] | None, optional 908 CH4プロットのy軸の範囲を指定します。デフォルト値はNoneです。 909 ax2_ylim: tuple[float, float] | None, optional 910 C2H6プロットのy軸の範囲を指定します。デフォルト値はNoneです。 911 save_fig: bool, optional 912 プロットを保存するかどうかを指定します。デフォルト値はTrueです。 913 show_fig: bool, optional 914 プロットを表示するかどうかを指定します。デフォルト値はTrueです。 915 916 Examples 917 -------- 918 >>> generator = MonthlyFiguresGenerator() 919 >>> generator.plot_c1c2_fluxes_diurnal_patterns( 920 ... df=monthly_data, 921 ... y_cols_ch4=["CH4_flux1", "CH4_flux2"], 922 ... y_cols_c2h6=["C2H6_flux1", "C2H6_flux2"], 923 ... labels_ch4=["CH4 1", "CH4 2"], 924 ... labels_c2h6=["C2H6 1", "C2H6 2"], 925 ... colors_ch4=["red", "blue"], 926 ... colors_c2h6=["green", "orange"], 927 ... output_dirpath="output", 928 ... show_std=True 929 ... ) 930 """ 931 # データの準備 932 df_internal: pd.DataFrame = df.copy() 933 df_internal.index = pd.to_datetime(df_internal.index) 934 target_columns = y_cols_ch4 + y_cols_c2h6 935 hourly_means, time_points = self._prepare_diurnal_data( 936 df_internal, target_columns 937 ) 938 939 # 標準偏差の計算を追加 940 hourly_stds = {} 941 if show_std: 942 hourly_stds = df_internal.groupby(df_internal.index.hour)[ 943 target_columns 944 ].std() 945 # 24時間目のデータ点を追加 946 last_hour = hourly_stds.iloc[0:1].copy() 947 last_hour.index = pd.Index([24]) 948 hourly_stds = pd.concat([hourly_stds, last_hour]) 949 950 # プロットの作成 951 fig, (ax1, ax2) = plt.subplots(1, 2, figsize=figsize) 952 953 # CH4のプロット (左側) 954 ch4_lines = [] 955 for col_y, label, color in zip(y_cols_ch4, labels_ch4, colors_ch4, strict=True): 956 mean_values = hourly_means["all"][col_y] 957 line = ax1.plot( 958 time_points, 959 mean_values, 960 "-o", 961 label=label, 962 color=color, 963 ) 964 ch4_lines.extend(line) 965 966 # 標準偏差の表示 967 if show_std: 968 std_values = hourly_stds[col_y] 969 ax1.fill_between( 970 time_points, 971 mean_values - std_values, 972 mean_values + std_values, 973 color=color, 974 alpha=std_alpha, 975 ) 976 977 # C2H6のプロット (右側) 978 c2h6_lines = [] 979 for col_y, label, color in zip( 980 y_cols_c2h6, labels_c2h6, colors_c2h6, strict=True 981 ): 982 mean_values = hourly_means["all"][col_y] 983 line = ax2.plot( 984 time_points, 985 mean_values, 986 "o-", 987 label=label, 988 color=color, 989 ) 990 c2h6_lines.extend(line) 991 992 # 標準偏差の表示 993 if show_std: 994 std_values = hourly_stds[col_y] 995 ax2.fill_between( 996 time_points, 997 mean_values - std_values, 998 mean_values + std_values, 999 color=color, 1000 alpha=std_alpha, 1001 ) 1002 1003 # 軸の設定 1004 for ax, ylabel, subplot_label in [ 1005 (ax1, r"CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)", subplot_label_ch4), 1006 (ax2, r"C$_2$H$_6$ flux (nmol m$^{-2}$ s$^{-1}$)", subplot_label_c2h6), 1007 ]: 1008 self._setup_diurnal_axes( 1009 ax=ax, 1010 time_points=time_points, 1011 ylabel=ylabel, 1012 subplot_label=subplot_label, 1013 add_label=add_label, 1014 add_legend=False, # 個別の凡例は表示しない 1015 subplot_fontsize=subplot_fontsize, 1016 ) 1017 1018 if ax1_ylim is not None: 1019 ax1.set_ylim(ax1_ylim) 1020 ax1.yaxis.set_major_locator(MultipleLocator(20)) 1021 ax1.yaxis.set_major_formatter(FuncFormatter(lambda x, p: f"{x:.0f}")) 1022 1023 if ax2_ylim is not None: 1024 ax2.set_ylim(ax2_ylim) 1025 ax2.yaxis.set_major_locator(MultipleLocator(1)) 1026 ax2.yaxis.set_major_formatter(FuncFormatter(lambda x, p: f"{x:.1f}")) 1027 1028 plt.tight_layout() 1029 1030 # 共通の凡例 1031 if add_legend: 1032 all_lines = ch4_lines 1033 all_labels = [line.get_label() for line in ch4_lines] 1034 if not legend_only_ch4: 1035 all_lines += c2h6_lines 1036 all_labels += [line.get_label() for line in c2h6_lines] 1037 fig.legend( 1038 all_lines, 1039 all_labels, 1040 loc="center", 1041 bbox_to_anchor=(0.5, 0.02), 1042 ncol=len(all_lines), 1043 ) 1044 plt.subplots_adjust(bottom=0.25) # 下部に凡例用のスペースを確保 1045 1046 if save_fig: 1047 if output_dirpath is None: 1048 raise ValueError( 1049 "save_fig = True の場合、 output_dirpath を指定する必要があります。有効なディレクトリパスを指定してください。" 1050 ) 1051 os.makedirs(output_dirpath, exist_ok=True) 1052 output_filepath: str = os.path.join(output_dirpath, output_filename) 1053 fig.savefig(output_filepath, dpi=dpi, bbox_inches="tight") 1054 if show_fig: 1055 plt.show() 1056 plt.close(fig=fig) 1057 1058 def plot_c1c2_fluxes_diurnal_patterns_by_date( 1059 self, 1060 df: pd.DataFrame, 1061 y_col_ch4: str, 1062 y_col_c2h6: str, 1063 output_dirpath: str | Path | None = None, 1064 output_filename: str = "diurnal_by_date.png", 1065 plot_all: bool = True, 1066 plot_weekday: bool = True, 1067 plot_weekend: bool = True, 1068 plot_holiday: bool = True, 1069 add_label: bool = True, 1070 add_legend: bool = True, 1071 show_std: bool = False, 1072 std_alpha: float = 0.2, 1073 legend_only_ch4: bool = False, 1074 subplot_fontsize: int = 20, 1075 subplot_label_ch4: str | None = "(a)", 1076 subplot_label_c2h6: str | None = "(b)", 1077 ax1_ylim: tuple[float, float] | None = None, 1078 ax2_ylim: tuple[float, float] | None = None, 1079 figsize: tuple[float, float] = (12, 5), 1080 dpi: float | None = 350, 1081 save_fig: bool = True, 1082 show_fig: bool = True, 1083 print_summary: bool = False, 1084 ) -> None: 1085 """CH4とC2H6の日変化パターンを日付分類して1つの図に並べてプロットします。 1086 1087 Parameters 1088 ---------- 1089 df: pd.DataFrame 1090 入力データフレームを指定します。 1091 y_col_ch4: str 1092 CH4フラックスを含むカラム名を指定します。 1093 y_col_c2h6: str 1094 C2H6フラックスを含むカラム名を指定します。 1095 output_dirpath: str | Path | None, optional 1096 出力先ディレクトリのパスを指定します。save_fig=Trueの場合は必須です。 1097 output_filename: str, optional 1098 出力ファイル名を指定します。デフォルト値は"diurnal_by_date.png"です。 1099 plot_all: bool, optional 1100 すべての日をプロットするかどうかを指定します。デフォルト値はTrueです。 1101 plot_weekday: bool, optional 1102 平日をプロットするかどうかを指定します。デフォルト値はTrueです。 1103 plot_weekend: bool, optional 1104 週末をプロットするかどうかを指定します。デフォルト値はTrueです。 1105 plot_holiday: bool, optional 1106 祝日をプロットするかどうかを指定します。デフォルト値はTrueです。 1107 add_label: bool, optional 1108 サブプロットラベルを表示するかどうかを指定します。デフォルト値はTrueです。 1109 add_legend: bool, optional 1110 凡例を表示するかどうかを指定します。デフォルト値はTrueです。 1111 show_std: bool, optional 1112 標準偏差を表示するかどうかを指定します。デフォルト値はFalseです。 1113 std_alpha: float, optional 1114 標準偏差の透明度を指定します。デフォルト値は0.2です。 1115 legend_only_ch4: bool, optional 1116 CH4の凡例のみを表示するかどうかを指定します。デフォルト値はFalseです。 1117 subplot_fontsize: int, optional 1118 サブプロットのフォントサイズを指定します。デフォルト値は20です。 1119 subplot_label_ch4: str | None, optional 1120 CH4プロットのラベルを指定します。デフォルト値は"(a)"です。 1121 subplot_label_c2h6: str | None, optional 1122 C2H6プロットのラベルを指定します。デフォルト値は"(b)"です。 1123 ax1_ylim: tuple[float, float] | None, optional 1124 CH4プロットのy軸の範囲を指定します。デフォルト値はNoneです。 1125 ax2_ylim: tuple[float, float] | None, optional 1126 C2H6プロットのy軸の範囲を指定します。デフォルト値はNoneです。 1127 figsize: tuple[float, float], optional 1128 プロットのサイズを指定します。デフォルト値は(12, 5)です。 1129 dpi: float | None, optional 1130 プロットのdpiを指定します。デフォルト値は350です。 1131 save_fig: bool, optional 1132 プロットを保存するかどうかを指定します。デフォルト値はTrueです。 1133 show_fig: bool, optional 1134 プロットを表示するかどうかを指定します。デフォルト値はTrueです。 1135 print_summary: bool, optional 1136 統計情報を表示するかどうかを指定します。デフォルト値はFalseです。 1137 1138 Examples 1139 ------- 1140 >>> generator = MonthlyFiguresGenerator() 1141 >>> generator.plot_c1c2_fluxes_diurnal_patterns_by_date( 1142 ... df=monthly_data, 1143 ... y_col_ch4="CH4_flux", 1144 ... y_col_c2h6="C2H6_flux", 1145 ... output_dirpath="output", 1146 ... show_std=True, 1147 ... print_summary=True 1148 ... ) 1149 """ 1150 # データの準備 1151 df_internal: pd.DataFrame = df.copy() 1152 df_internal.index = pd.to_datetime(df_internal.index) 1153 target_columns = [y_col_ch4, y_col_c2h6] 1154 hourly_means, time_points = self._prepare_diurnal_data( 1155 df_internal, target_columns, include_date_types=True 1156 ) 1157 1158 # 標準偏差の計算を追加 1159 hourly_stds = {} 1160 if show_std: 1161 for condition in ["all", "weekday", "weekend", "holiday"]: 1162 if condition == "all": 1163 condition_data = df_internal 1164 elif condition == "weekday": 1165 condition_data = df_internal[ 1166 ~( 1167 df_internal.index.dayofweek.isin([5, 6]) 1168 | df_internal.index.map( 1169 lambda x: jpholiday.is_holiday(x.date()) 1170 ) 1171 ) 1172 ] 1173 elif condition == "weekend": 1174 condition_data = df_internal[ 1175 df_internal.index.dayofweek.isin([5, 6]) 1176 ] 1177 else: # holiday 1178 condition_data = df_internal[ 1179 df_internal.index.map(lambda x: jpholiday.is_holiday(x.date())) 1180 ] 1181 1182 hourly_stds[condition] = condition_data.groupby( 1183 pd.to_datetime(condition_data.index).hour 1184 )[target_columns].std() 1185 # 24時間目のデータ点を追加 1186 last_hour = hourly_stds[condition].iloc[0:1].copy() 1187 last_hour.index = [24] 1188 hourly_stds[condition] = pd.concat([hourly_stds[condition], last_hour]) 1189 1190 # プロットスタイルの設定 1191 styles = { 1192 "all": { 1193 "color": "black", 1194 "linestyle": "-", 1195 "alpha": 1.0, 1196 "label": "All days", 1197 }, 1198 "weekday": { 1199 "color": "blue", 1200 "linestyle": "-", 1201 "alpha": 0.8, 1202 "label": "Weekdays", 1203 }, 1204 "weekend": { 1205 "color": "red", 1206 "linestyle": "-", 1207 "alpha": 0.8, 1208 "label": "Weekends", 1209 }, 1210 "holiday": { 1211 "color": "green", 1212 "linestyle": "-", 1213 "alpha": 0.8, 1214 "label": "Weekends & Holidays", 1215 }, 1216 } 1217 1218 # プロット対象の条件を選択 1219 plot_conditions = { 1220 "all": plot_all, 1221 "weekday": plot_weekday, 1222 "weekend": plot_weekend, 1223 "holiday": plot_holiday, 1224 } 1225 selected_conditions = { 1226 col: means 1227 for col, means in hourly_means.items() 1228 if plot_conditions.get(col) 1229 } 1230 1231 # プロットの作成 1232 fig, (ax1, ax2) = plt.subplots(1, 2, figsize=figsize) 1233 1234 # CH4とC2H6のプロット用のラインオブジェクトを保存 1235 ch4_lines = [] 1236 c2h6_lines = [] 1237 1238 # CH4とC2H6のプロット 1239 for condition, means in selected_conditions.items(): 1240 style = styles[condition].copy() 1241 1242 # CH4プロット 1243 mean_values_ch4 = means[y_col_ch4] 1244 line_ch4 = ax1.plot(time_points, mean_values_ch4, marker="o", **style) 1245 ch4_lines.extend(line_ch4) 1246 1247 if show_std and condition in hourly_stds: 1248 std_values = hourly_stds[condition][y_col_ch4] 1249 ax1.fill_between( 1250 time_points, 1251 mean_values_ch4 - std_values, 1252 mean_values_ch4 + std_values, 1253 color=style["color"], 1254 alpha=std_alpha, 1255 ) 1256 1257 # C2H6プロット 1258 style["linestyle"] = "--" 1259 mean_values_c2h6 = means[y_col_c2h6] 1260 line_c2h6 = ax2.plot(time_points, mean_values_c2h6, marker="o", **style) 1261 c2h6_lines.extend(line_c2h6) 1262 1263 if show_std and condition in hourly_stds: 1264 std_values = hourly_stds[condition][y_col_c2h6] 1265 ax2.fill_between( 1266 time_points, 1267 mean_values_c2h6 - std_values, 1268 mean_values_c2h6 + std_values, 1269 color=style["color"], 1270 alpha=std_alpha, 1271 ) 1272 1273 # 軸の設定 1274 for ax, ylabel, subplot_label in [ 1275 (ax1, r"CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)", subplot_label_ch4), 1276 (ax2, r"C$_2$H$_6$ flux (nmol m$^{-2}$ s$^{-1}$)", subplot_label_c2h6), 1277 ]: 1278 self._setup_diurnal_axes( 1279 ax=ax, 1280 time_points=time_points, 1281 ylabel=ylabel, 1282 subplot_label=subplot_label, 1283 add_label=add_label, 1284 add_legend=False, 1285 subplot_fontsize=subplot_fontsize, 1286 ) 1287 1288 if ax1_ylim is not None: 1289 ax1.set_ylim(ax1_ylim) 1290 ax1.yaxis.set_major_locator(MultipleLocator(20)) 1291 ax1.yaxis.set_major_formatter(FuncFormatter(lambda x, p: f"{x:.0f}")) 1292 1293 if ax2_ylim is not None: 1294 ax2.set_ylim(ax2_ylim) 1295 ax2.yaxis.set_major_locator(MultipleLocator(1)) 1296 ax2.yaxis.set_major_formatter(FuncFormatter(lambda x, p: f"{x:.1f}")) 1297 1298 plt.tight_layout() 1299 1300 # 共通の凡例を図の下部に配置 1301 if add_legend: 1302 lines_to_show = ( 1303 ch4_lines if legend_only_ch4 else ch4_lines[: len(selected_conditions)] 1304 ) 1305 fig.legend( 1306 lines_to_show, 1307 [ 1308 style["label"] 1309 for style in list(styles.values())[: len(lines_to_show)] 1310 ], 1311 loc="center", 1312 bbox_to_anchor=(0.5, 0.02), 1313 ncol=len(lines_to_show), 1314 ) 1315 plt.subplots_adjust(bottom=0.25) # 下部に凡例用のスペースを確保 1316 1317 if save_fig: 1318 if output_dirpath is None: 1319 raise ValueError( 1320 "save_fig = True の場合、 output_dirpath を指定する必要があります。有効なディレクトリパスを指定してください。" 1321 ) 1322 os.makedirs(output_dirpath, exist_ok=True) 1323 output_filepath: str = os.path.join(output_dirpath, output_filename) 1324 fig.savefig(output_filepath, dpi=dpi, bbox_inches="tight") 1325 if show_fig: 1326 plt.show() 1327 plt.close(fig=fig) 1328 1329 # 日変化パターンの統計分析を追加 1330 if print_summary: 1331 # 平日と休日のデータを準備 1332 dates = pd.to_datetime(df_internal.index) 1333 is_weekend = dates.dayofweek.isin([5, 6]) 1334 is_holiday = dates.map(lambda x: jpholiday.is_holiday(x.date())) 1335 is_weekday = ~(is_weekend | is_holiday) 1336 1337 weekday_data = df_internal[is_weekday] 1338 holiday_data = df_internal[is_weekend | is_holiday] 1339 1340 def get_diurnal_stats(data, column): 1341 # 時間ごとの平均値を計算 1342 hourly_means = data.groupby(data.index.hour)[column].mean() 1343 1344 # 8-16時の時間帯の統計 1345 daytime_means = hourly_means[ 1346 (hourly_means.index >= 8) & (hourly_means.index <= 16) 1347 ] 1348 1349 if len(daytime_means) == 0: 1350 return None 1351 1352 return { 1353 "mean": daytime_means.mean(), 1354 "max": daytime_means.max(), 1355 "max_hour": daytime_means.idxmax(), 1356 "min": daytime_means.min(), 1357 "min_hour": daytime_means.idxmin(), 1358 "hours_count": len(daytime_means), 1359 } 1360 1361 # CH4とC2H6それぞれの統計を計算 1362 for col, gas_name in [(y_col_ch4, "CH4"), (y_col_c2h6, "C2H6")]: 1363 print(f"\n=== {gas_name} フラックス 8-16時の統計分析 ===") 1364 1365 weekday_stats = get_diurnal_stats(weekday_data, col) 1366 holiday_stats = get_diurnal_stats(holiday_data, col) 1367 1368 if weekday_stats and holiday_stats: 1369 print("\n平日:") 1370 print(f" 平均値: {weekday_stats['mean']:.2f}") 1371 print( 1372 f" 最大値: {weekday_stats['max']:.2f} ({weekday_stats['max_hour']}時)" 1373 ) 1374 print( 1375 f" 最小値: {weekday_stats['min']:.2f} ({weekday_stats['min_hour']}時)" 1376 ) 1377 print(f" 集計時間数: {weekday_stats['hours_count']}") 1378 1379 print("\n休日:") 1380 print(f" 平均値: {holiday_stats['mean']:.2f}") 1381 print( 1382 f" 最大値: {holiday_stats['max']:.2f} ({holiday_stats['max_hour']}時)" 1383 ) 1384 print( 1385 f" 最小値: {holiday_stats['min']:.2f} ({holiday_stats['min_hour']}時)" 1386 ) 1387 print(f" 集計時間数: {holiday_stats['hours_count']}") 1388 1389 # 平日/休日の比率を計算 1390 print("\n平日/休日の比率:") 1391 print( 1392 f" 平均値比: {weekday_stats['mean'] / holiday_stats['mean']:.2f}" 1393 ) 1394 print( 1395 f" 最大値比: {weekday_stats['max'] / holiday_stats['max']:.2f}" 1396 ) 1397 print( 1398 f" 最小値比: {weekday_stats['min'] / holiday_stats['min']:.2f}" 1399 ) 1400 else: 1401 print("十分なデータがありません") 1402 1403 def plot_diurnal_concentrations( 1404 self, 1405 df: pd.DataFrame, 1406 col_ch4_conc: str = "CH4_ultra_cal", 1407 col_c2h6_conc: str = "C2H6_ultra_cal", 1408 col_datetime: str = "Date", 1409 output_dirpath: str | Path | None = None, 1410 output_filename: str = "diurnal_concentrations.png", 1411 show_std: bool = True, 1412 alpha_std: float = 0.2, 1413 add_legend: bool = True, 1414 print_summary: bool = False, 1415 subplot_label_ch4: str | None = None, 1416 subplot_label_c2h6: str | None = None, 1417 subplot_fontsize: int = 24, 1418 ch4_ylim: tuple[float, float] | None = None, 1419 c2h6_ylim: tuple[float, float] | None = None, 1420 interval: Literal["30min", "1H"] = "1H", 1421 figsize: tuple[float, float] = (12, 5), 1422 dpi: float | None = 350, 1423 save_fig: bool = True, 1424 show_fig: bool = True, 1425 ) -> None: 1426 """CH4とC2H6の濃度の日内変動を描画します。 1427 1428 Parameters 1429 ---------- 1430 df: pd.DataFrame 1431 濃度データを含むDataFrameを指定します。 1432 col_ch4_conc: str, optional 1433 CH4濃度のカラム名を指定します。デフォルト値は"CH4_ultra_cal"です。 1434 col_c2h6_conc: str, optional 1435 C2H6濃度のカラム名を指定します。デフォルト値は"C2H6_ultra_cal"です。 1436 col_datetime: str, optional 1437 日時カラム名を指定します。デフォルト値は"Date"です。 1438 output_dirpath: str | Path | None, optional 1439 出力ディレクトリのパスを指定します。save_fig=Trueの場合は必須です。 1440 output_filename: str, optional 1441 出力ファイル名を指定します。デフォルト値は"diurnal_concentrations.png"です。 1442 show_std: bool, optional 1443 標準偏差を表示するかどうかを指定します。デフォルト値はTrueです。 1444 alpha_std: float, optional 1445 標準偏差の透明度を指定します。デフォルト値は0.2です。 1446 add_legend: bool, optional 1447 凡例を追加するかどうかを指定します。デフォルト値はTrueです。 1448 print_summary: bool, optional 1449 統計情報を表示するかどうかを指定します。デフォルト値はFalseです。 1450 subplot_label_ch4: str | None, optional 1451 CH4プロットのラベルを指定します。デフォルト値はNoneです。 1452 subplot_label_c2h6: str | None, optional 1453 C2H6プロットのラベルを指定します。デフォルト値はNoneです。 1454 subplot_fontsize: int, optional 1455 サブプロットのフォントサイズを指定します。デフォルト値は24です。 1456 ch4_ylim: tuple[float, float] | None, optional 1457 CH4のy軸範囲を指定します。デフォルト値はNoneです。 1458 c2h6_ylim: tuple[float, float] | None, optional 1459 C2H6のy軸範囲を指定します。デフォルト値はNoneです。 1460 interval: Literal["30min", "1H"], optional 1461 時間間隔を指定します。"30min"または"1H"を指定できます。デフォルト値は"1H"です。 1462 figsize: tuple[float, float], optional 1463 プロットのサイズを指定します。デフォルト値は(12, 5)です。 1464 dpi: float | None, optional 1465 プロットのdpiを指定します。デフォルト値は350です。 1466 save_fig: bool, optional 1467 プロットを保存するかどうかを指定します。デフォルト値はTrueです。 1468 show_fig: bool, optional 1469 プロットを表示するかどうかを指定します。デフォルト値はTrueです。 1470 1471 Examples 1472 -------- 1473 >>> generator = MonthlyFiguresGenerator() 1474 >>> generator.plot_diurnal_concentrations( 1475 ... df=monthly_data, 1476 ... output_dirpath="output", 1477 ... show_std=True, 1478 ... interval="30min" 1479 ... ) 1480 """ 1481 # データの準備 1482 df_internal = df.copy() 1483 df_internal.index = pd.to_datetime(df_internal.index) 1484 if interval == "30min": 1485 # 30分間隔の場合、時間と30分を別々に取得 1486 df_internal["hour"] = pd.to_datetime(df_internal[col_datetime]).dt.hour 1487 df_internal["minute"] = pd.to_datetime(df_internal[col_datetime]).dt.minute 1488 df_internal["time_bin"] = df_internal["hour"] + df_internal["minute"].map( 1489 {0: 0, 30: 0.5} 1490 ) 1491 else: 1492 # 1時間間隔の場合 1493 df_internal["time_bin"] = pd.to_datetime(df_internal[col_datetime]).dt.hour 1494 1495 # 時間ごとの平均値と標準偏差を計算 1496 hourly_stats = df_internal.groupby("time_bin")[ 1497 [col_ch4_conc, col_c2h6_conc] 1498 ].agg(["mean", "std"]) 1499 1500 # 最後のデータポイントを追加(最初のデータを使用) 1501 last_point = hourly_stats.iloc[0:1].copy() 1502 last_point.index = pd.Index( 1503 [hourly_stats.index[-1] + (0.5 if interval == "30min" else 1)] 1504 ) 1505 hourly_stats = pd.concat([hourly_stats, last_point]) 1506 1507 # 時間軸の作成 1508 if interval == "30min": 1509 time_points = pd.date_range("2024-01-01", periods=49, freq="30min") 1510 x_ticks = [0, 6, 12, 18, 24] # 主要な時間のティック 1511 else: 1512 time_points = pd.date_range("2024-01-01", periods=25, freq="1H") 1513 x_ticks = [0, 6, 12, 18, 24] 1514 1515 # プロットの作成 1516 fig, (ax1, ax2) = plt.subplots(1, 2, figsize=figsize) 1517 1518 # CH4濃度プロット 1519 mean_ch4 = hourly_stats[col_ch4_conc]["mean"] 1520 if show_std: 1521 std_ch4 = hourly_stats[col_ch4_conc]["std"] 1522 ax1.fill_between( 1523 time_points, 1524 mean_ch4 - std_ch4, 1525 mean_ch4 + std_ch4, 1526 color="red", 1527 alpha=alpha_std, 1528 ) 1529 ch4_line = ax1.plot(time_points, mean_ch4, "red", label="CH$_4$")[0] 1530 1531 ax1.set_ylabel("CH$_4$ (ppm)") 1532 if ch4_ylim is not None: 1533 ax1.set_ylim(ch4_ylim) 1534 if subplot_label_ch4: 1535 ax1.text( 1536 0.02, 1537 0.98, 1538 subplot_label_ch4, 1539 transform=ax1.transAxes, 1540 va="top", 1541 fontsize=subplot_fontsize, 1542 ) 1543 1544 # C2H6濃度プロット 1545 mean_c2h6 = hourly_stats[col_c2h6_conc]["mean"] 1546 if show_std: 1547 std_c2h6 = hourly_stats[col_c2h6_conc]["std"] 1548 ax2.fill_between( 1549 time_points, 1550 mean_c2h6 - std_c2h6, 1551 mean_c2h6 + std_c2h6, 1552 color="orange", 1553 alpha=alpha_std, 1554 ) 1555 c2h6_line = ax2.plot(time_points, mean_c2h6, "orange", label="C$_2$H$_6$")[0] 1556 1557 ax2.set_ylabel("C$_2$H$_6$ (ppb)") 1558 if c2h6_ylim is not None: 1559 ax2.set_ylim(c2h6_ylim) 1560 if subplot_label_c2h6: 1561 ax2.text( 1562 0.02, 1563 0.98, 1564 subplot_label_c2h6, 1565 transform=ax2.transAxes, 1566 va="top", 1567 fontsize=subplot_fontsize, 1568 ) 1569 1570 # 両プロットの共通設定 1571 for ax in [ax1, ax2]: 1572 ax.set_xlabel("Time (hour)") 1573 ax.xaxis.set_major_formatter(mdates.DateFormatter("%-H")) 1574 ax.xaxis.set_major_locator(mdates.HourLocator(byhour=x_ticks)) 1575 ax.set_xlim(time_points[0], time_points[-1]) 1576 # 1時間ごとの縦線を表示 1577 ax.grid(True, which="major", alpha=0.3) 1578 1579 # 共通の凡例を図の下部に配置 1580 if add_legend: 1581 fig.legend( 1582 [ch4_line, c2h6_line], 1583 ["CH$_4$", "C$_2$H$_6$"], 1584 loc="center", 1585 bbox_to_anchor=(0.5, 0.02), 1586 ncol=2, 1587 ) 1588 plt.subplots_adjust(bottom=0.2) 1589 1590 plt.tight_layout() 1591 if save_fig: 1592 if output_dirpath is None: 1593 raise ValueError() 1594 # 出力ディレクトリの作成 1595 os.makedirs(output_dirpath, exist_ok=True) 1596 output_filepath: str = os.path.join(output_dirpath, output_filename) 1597 plt.savefig(output_filepath, dpi=dpi, bbox_inches="tight") 1598 if show_fig: 1599 plt.show() 1600 plt.close(fig=fig) 1601 1602 if print_summary: 1603 # 統計情報の表示 1604 for name, col in [("CH4", col_ch4_conc), ("C2H6", col_c2h6_conc)]: 1605 stats = hourly_stats[col] 1606 mean_vals = stats["mean"] 1607 1608 print(f"\n{name}濃度の日内変動統計:") 1609 print(f"最小値: {mean_vals.min():.3f} (Hour: {mean_vals.idxmin()})") 1610 print(f"最大値: {mean_vals.max():.3f} (Hour: {mean_vals.idxmax()})") 1611 print(f"平均値: {mean_vals.mean():.3f}") 1612 print(f"日内変動幅: {mean_vals.max() - mean_vals.min():.3f}") 1613 print(f"最大/最小比: {mean_vals.max() / mean_vals.min():.3f}") 1614 1615 def plot_flux_diurnal_patterns_with_std( 1616 self, 1617 df: pd.DataFrame, 1618 col_ch4_flux: str = "Fch4", 1619 col_c2h6_flux: str = "Fc2h6", 1620 ch4_label: str = r"$\mathregular{CH_{4}}$フラックス", 1621 c2h6_label: str = r"$\mathregular{C_{2}H_{6}}$フラックス", 1622 col_datetime: str = "Date", 1623 output_dirpath: str | Path | None = None, 1624 output_filename: str = "diurnal_patterns.png", 1625 window_size: int = 6, 1626 show_std: bool = True, 1627 alpha_std: float = 0.1, 1628 figsize: tuple[float, float] = (12, 5), 1629 dpi: float | None = 350, 1630 save_fig: bool = True, 1631 show_fig: bool = True, 1632 print_summary: bool = False, 1633 ) -> None: 1634 """CH4とC2H6フラックスの日変化パターンをプロットします。 1635 1636 Parameters 1637 ---------- 1638 df: pd.DataFrame 1639 プロットするデータを含むDataFrameを指定します。 1640 col_ch4_flux: str, optional 1641 CH4フラックスのカラム名を指定します。デフォルト値は"Fch4"です。 1642 col_c2h6_flux: str, optional 1643 C2H6フラックスのカラム名を指定します。デフォルト値は"Fc2h6"です。 1644 ch4_label: str, optional 1645 CH4フラックスの凡例ラベルを指定します。デフォルト値は"CH4フラックス"です。 1646 c2h6_label: str, optional 1647 C2H6フラックスの凡例ラベルを指定します。デフォルト値は"C2H6フラックス"です。 1648 col_datetime: str, optional 1649 日時カラムの名前を指定します。デフォルト値は"Date"です。 1650 output_dirpath: str | Path | None, optional 1651 出力ディレクトリのパスを指定します。save_fig=Trueの場合は必須です。 1652 output_filename: str, optional 1653 出力ファイル名を指定します。デフォルト値は"diurnal_patterns.png"です。 1654 window_size: int, optional 1655 移動平均の窓サイズを指定します。デフォルト値は6です。 1656 show_std: bool, optional 1657 標準偏差を表示するかどうかを指定します。デフォルト値はTrueです。 1658 alpha_std: float, optional 1659 標準偏差の透明度を0-1の範囲で指定します。デフォルト値は0.1です。 1660 figsize: tuple[float, float], optional 1661 プロットのサイズを指定します。デフォルト値は(12, 5)です。 1662 dpi: float | None, optional 1663 プロットの解像度を指定します。デフォルト値は350です。 1664 save_fig: bool, optional 1665 プロットを保存するかどうかを指定します。デフォルト値はTrueです。 1666 show_fig: bool, optional 1667 プロットを表示するかどうかを指定します。デフォルト値はTrueです。 1668 print_summary: bool, optional 1669 統計情報を表示するかどうかを指定します。デフォルト値はFalseです。 1670 1671 Examples 1672 -------- 1673 >>> generator = MonthlyFiguresGenerator() 1674 >>> df = pd.read_csv("flux_data.csv") 1675 >>> generator.plot_flux_diurnal_patterns_with_std( 1676 ... df, 1677 ... col_ch4_flux="CH4_flux", 1678 ... col_c2h6_flux="C2H6_flux", 1679 ... output_dirpath="output", 1680 ... show_std=True 1681 ... ) 1682 """ 1683 # 日時インデックスの処理 1684 df_internal = df.copy() 1685 if not isinstance(df_internal.index, pd.DatetimeIndex): 1686 df_internal[col_datetime] = pd.to_datetime(df_internal[col_datetime]) 1687 df_internal.set_index(col_datetime, inplace=True) 1688 df_internal.index = pd.to_datetime(df_internal.index) 1689 # 時刻データの抽出とグループ化 1690 df_internal["hour"] = df_internal.index.hour 1691 hourly_means = df_internal.groupby("hour")[[col_ch4_flux, col_c2h6_flux]].agg( 1692 ["mean", "std"] 1693 ) 1694 1695 # 24時間目のデータ点を追加(0時のデータを使用) 1696 last_hour = hourly_means.iloc[0:1].copy() 1697 last_hour.index = pd.Index([24]) 1698 hourly_means = pd.concat([hourly_means, last_hour]) 1699 1700 # 24時間分のデータポイントを作成 1701 time_points = pd.date_range("2024-01-01", periods=25, freq="h") 1702 1703 # プロットの作成 1704 fig, (ax1, ax2) = plt.subplots(1, 2, figsize=figsize) 1705 1706 # 移動平均の計算と描画 1707 ch4_mean = ( 1708 hourly_means[(col_ch4_flux, "mean")] 1709 .rolling(window=window_size, center=True, min_periods=1) 1710 .mean() 1711 ) 1712 c2h6_mean = ( 1713 hourly_means[(col_c2h6_flux, "mean")] 1714 .rolling(window=window_size, center=True, min_periods=1) 1715 .mean() 1716 ) 1717 1718 if show_std: 1719 ch4_std = ( 1720 hourly_means[(col_ch4_flux, "std")] 1721 .rolling(window=window_size, center=True, min_periods=1) 1722 .mean() 1723 ) 1724 c2h6_std = ( 1725 hourly_means[(col_c2h6_flux, "std")] 1726 .rolling(window=window_size, center=True, min_periods=1) 1727 .mean() 1728 ) 1729 1730 ax1.fill_between( 1731 time_points, 1732 ch4_mean - ch4_std, 1733 ch4_mean + ch4_std, 1734 color="blue", 1735 alpha=alpha_std, 1736 ) 1737 ax2.fill_between( 1738 time_points, 1739 c2h6_mean - c2h6_std, 1740 c2h6_mean + c2h6_std, 1741 color="red", 1742 alpha=alpha_std, 1743 ) 1744 1745 # メインのラインプロット 1746 ax1.plot(time_points, ch4_mean, "blue", label=ch4_label) 1747 ax2.plot(time_points, c2h6_mean, "red", label=c2h6_label) 1748 1749 # 軸の設定 1750 for ax, ylabel in [ 1751 (ax1, r"CH$_4$ (nmol m$^{-2}$ s$^{-1}$)"), 1752 (ax2, r"C$_2$H$_6$ (nmol m$^{-2}$ s$^{-1}$)"), 1753 ]: 1754 ax.set_xlabel("Time") 1755 ax.set_ylabel(ylabel) 1756 ax.xaxis.set_major_formatter(mdates.DateFormatter("%-H")) 1757 ax.xaxis.set_major_locator(mdates.HourLocator(byhour=[0, 6, 12, 18, 24])) 1758 ax.set_xlim(time_points[0], time_points[-1]) 1759 ax.grid(True, alpha=0.3) 1760 ax.legend() 1761 1762 # グラフの保存 1763 plt.tight_layout() 1764 1765 if save_fig: 1766 if output_dirpath is None: 1767 raise ValueError( 1768 "save_fig = True の場合、 output_dirpath を指定する必要があります。有効なディレクトリパスを指定してください。" 1769 ) 1770 # 出力ディレクトリの作成 1771 os.makedirs(output_dirpath, exist_ok=True) 1772 output_filepath: str = os.path.join(output_dirpath, output_filename) 1773 plt.savefig(output_filepath, dpi=350, bbox_inches="tight") 1774 if show_fig: 1775 plt.show() 1776 plt.close(fig=fig) 1777 1778 # 統計情報の表示(オプション) 1779 if print_summary: 1780 for col, name in [(col_ch4_flux, "CH4"), (col_c2h6_flux, "C2H6")]: 1781 mean_val = hourly_means[(col, "mean")].mean() 1782 min_val = hourly_means[(col, "mean")].min() 1783 max_val = hourly_means[(col, "mean")].max() 1784 min_time = hourly_means[(col, "mean")].idxmin() 1785 max_time = hourly_means[(col, "mean")].idxmax() 1786 1787 self.logger.info(f"{name} Statistics:") 1788 self.logger.info(f"Mean: {mean_val:.2f}") 1789 self.logger.info(f"Min: {min_val:.2f} (Hour: {min_time})") 1790 self.logger.info(f"Max: {max_val:.2f} (Hour: {max_time})") 1791 self.logger.info(f"Max/Min ratio: {max_val / min_val:.2f}\n") 1792 1793 def plot_gas_ratio_diurnal( 1794 self, 1795 df: pd.DataFrame, 1796 col_ratio_1: str, 1797 col_ratio_2: str, 1798 label_1: str, 1799 label_2: str, 1800 color_1: str, 1801 color_2: str, 1802 output_dirpath: str | Path | None = None, 1803 output_filename: str = "gas_ratio_diurnal.png", 1804 add_xlabel: bool = True, 1805 add_ylabel: bool = True, 1806 add_legend: bool = True, 1807 xlabel: str = "Hour", 1808 ylabel: str = "都市ガスが占める排出比率 (%)", 1809 subplot_fontsize: int = 20, 1810 subplot_label: str | None = None, 1811 y_max: float | None = 100, 1812 figsize: tuple[float, float] = (12, 5), 1813 dpi: float | None = 350, 1814 save_fig: bool = True, 1815 show_fig: bool = False, 1816 ) -> None: 1817 """2つの比率の日変化を比較するプロットを作成します。 1818 1819 Parameters 1820 ---------- 1821 df: pd.DataFrame 1822 プロットするデータを含むDataFrameを指定します。 1823 col_ratio_1: str 1824 1つ目の比率データを含むカラム名を指定します。 1825 col_ratio_2: str 1826 2つ目の比率データを含むカラム名を指定します。 1827 label_1: str 1828 1つ目の比率データの凡例ラベルを指定します。 1829 label_2: str 1830 2つ目の比率データの凡例ラベルを指定します。 1831 color_1: str 1832 1つ目の比率データのプロット色を指定します。 1833 color_2: str 1834 2つ目の比率データのプロット色を指定します。 1835 output_dirpath: str | Path | None, optional 1836 出力先ディレクトリのパスを指定します。save_fig=Trueの場合は必須です。 1837 output_filename: str, optional 1838 出力ファイル名を指定します。デフォルト値は"gas_ratio_diurnal.png"です。 1839 add_xlabel: bool, optional 1840 x軸ラベルを表示するかどうかを指定します。デフォルト値はTrueです。 1841 add_ylabel: bool, optional 1842 y軸ラベルを表示するかどうかを指定します。デフォルト値はTrueです。 1843 add_legend: bool, optional 1844 凡例を表示するかどうかを指定します。デフォルト値はTrueです。 1845 xlabel: str, optional 1846 x軸のラベルを指定します。デフォルト値は"Hour"です。 1847 ylabel: str, optional 1848 y軸のラベルを指定します。デフォルト値は"都市ガスが占める排出比率 (%)"です。 1849 subplot_fontsize: int, optional 1850 サブプロットのフォントサイズを指定します。デフォルト値は20です。 1851 subplot_label: str | None, optional 1852 サブプロットのラベルを指定します。デフォルト値はNoneです。 1853 y_max: float | None, optional 1854 y軸の最大値を指定します。デフォルト値は100です。 1855 figsize: tuple[float, float], optional 1856 図のサイズを指定します。デフォルト値は(12, 5)です。 1857 dpi: float | None, optional 1858 図の解像度を指定します。デフォルト値は350です。 1859 save_fig: bool, optional 1860 図を保存するかどうかを指定します。デフォルト値はTrueです。 1861 show_fig: bool, optional 1862 図を表示するかどうかを指定します。デフォルト値はFalseです。 1863 1864 Examples 1865 ------- 1866 >>> df = pd.DataFrame({ 1867 ... 'ratio1': [80, 85, 90], 1868 ... 'ratio2': [70, 75, 80] 1869 ... }) 1870 >>> generator = MonthlyFiguresGenerator() 1871 >>> generator.plot_gas_ratio_diurnal( 1872 ... df=df, 1873 ... col_ratio_1='ratio1', 1874 ... col_ratio_2='ratio2', 1875 ... label_1='比率1', 1876 ... label_2='比率2', 1877 ... color_1='blue', 1878 ... color_2='red', 1879 ... output_dirpath='output' 1880 ... ) 1881 """ 1882 df_internal: pd.DataFrame = df.copy() 1883 df_internal.index = pd.to_datetime(df_internal.index) 1884 1885 # 時刻でグループ化して平均を計算 1886 hourly_means = df_internal.groupby(df_internal.index.hour)[ 1887 [col_ratio_1, col_ratio_2] 1888 ].mean() 1889 hourly_stds = df_internal.groupby(df_internal.index.hour)[ 1890 [col_ratio_1, col_ratio_2] 1891 ].std() 1892 1893 # 24時間目のデータ点を追加(0時のデータを使用) 1894 last_hour = hourly_means.iloc[0:1].copy() 1895 last_hour.index = pd.Index([24]) 1896 hourly_means = pd.concat([hourly_means, last_hour]) 1897 1898 last_hour_std = hourly_stds.iloc[0:1].copy() 1899 last_hour_std.index = pd.Index([24]) 1900 hourly_stds = pd.concat([hourly_stds, last_hour_std]) 1901 1902 # 24時間分の時刻を生成 1903 time_points: pd.DatetimeIndex = pd.date_range( 1904 "2024-01-01", periods=25, freq="h" 1905 ) 1906 1907 # プロットの作成 1908 fig, ax = plt.subplots(figsize=figsize) 1909 1910 # 1つ目の比率 1911 ax.plot( 1912 time_points, # [:-1]を削除 1913 hourly_means[col_ratio_1], 1914 color=color_1, 1915 label=label_1, 1916 alpha=0.7, 1917 ) 1918 ax.fill_between( 1919 time_points, # [:-1]を削除 1920 hourly_means[col_ratio_1] - hourly_stds[col_ratio_1], 1921 hourly_means[col_ratio_1] + hourly_stds[col_ratio_1], 1922 color=color_1, 1923 alpha=0.2, 1924 ) 1925 1926 # 2つ目の比率 1927 ax.plot( 1928 time_points, # [:-1]を削除 1929 hourly_means[col_ratio_2], 1930 color=color_2, 1931 label=label_2, 1932 alpha=0.7, 1933 ) 1934 ax.fill_between( 1935 time_points, # [:-1]を削除 1936 hourly_means[col_ratio_2] - hourly_stds[col_ratio_2], 1937 hourly_means[col_ratio_2] + hourly_stds[col_ratio_2], 1938 color=color_2, 1939 alpha=0.2, 1940 ) 1941 1942 # 軸の設定 1943 if add_xlabel: 1944 ax.set_xlabel(xlabel) 1945 if add_ylabel: 1946 ax.set_ylabel(ylabel) 1947 1948 # y軸の範囲設定 1949 if y_max is not None: 1950 ax.set_ylim(0, y_max) 1951 1952 # グリッド線の追加 1953 ax.grid(True, alpha=0.3) 1954 ax.grid(True, which="minor", alpha=0.1) 1955 1956 # x軸の設定 1957 ax.xaxis.set_major_formatter(mdates.DateFormatter("%-H")) 1958 ax.xaxis.set_major_locator(mdates.HourLocator(byhour=[0, 6, 12, 18, 24])) 1959 ax.set_xlim( 1960 float(mdates.date2num(time_points[0])), 1961 float(mdates.date2num(time_points[-1])), 1962 ) 1963 ax.set_xticks(time_points[::6]) 1964 ax.set_xticklabels(["0", "6", "12", "18", "24"]) 1965 1966 # サブプロットラベルの追加 1967 if subplot_label: 1968 ax.text( 1969 0.02, 1970 0.98, 1971 subplot_label, 1972 transform=ax.transAxes, 1973 va="top", 1974 fontsize=subplot_fontsize, 1975 ) 1976 1977 # 凡例の追加 1978 if add_legend: 1979 # 凡例を図の下部中央に配置 1980 ax.legend( 1981 loc="center", 1982 bbox_to_anchor=(0.5, -0.25), # 図の下部に配置 1983 ncol=2, # 2列で表示 1984 frameon=False, # 枠を非表示 1985 ) 1986 # 凡例のために下部のマージンを調整 1987 plt.subplots_adjust(bottom=0.2) 1988 1989 # プロットの保存と表示 1990 plt.tight_layout() 1991 if save_fig: 1992 if output_dirpath is None: 1993 raise ValueError( 1994 "save_fig = True の場合、 output_dirpath を指定する必要があります。有効なディレクトリパスを指定してください。" 1995 ) 1996 # 出力ディレクトリの作成 1997 os.makedirs(output_dirpath, exist_ok=True) 1998 output_filepath = os.path.join(output_dirpath, output_filename) 1999 plt.savefig(output_filepath, dpi=dpi, bbox_inches="tight") 2000 if show_fig: 2001 plt.show() 2002 plt.close(fig=fig) 2003 2004 def plot_scatter( 2005 self, 2006 df: pd.DataFrame, 2007 col_x: str, 2008 col_y: str, 2009 output_dirpath: str | Path | None = None, 2010 output_filename: str = "scatter.png", 2011 add_label: bool = True, 2012 xlabel: str | None = None, 2013 ylabel: str | None = None, 2014 x_axis_range: tuple | None = None, 2015 y_axis_range: tuple | None = None, 2016 x_scientific: bool = False, 2017 y_scientific: bool = False, 2018 fixed_slope: float = 0.076, 2019 show_fixed_slope: bool = False, 2020 figsize: tuple[float, float] = (6, 6), 2021 dpi: float | None = 350, 2022 save_fig: bool = True, 2023 show_fig: bool = True, 2024 ) -> None: 2025 """散布図を作成し、TLS回帰直線を描画します。 2026 2027 Parameters 2028 ---------- 2029 df: pd.DataFrame 2030 プロットに使用するデータフレームを指定します。 2031 col_x: str 2032 x軸に使用する列名を指定します。 2033 col_y: str 2034 y軸に使用する列名を指定します。 2035 output_dirpath: str | Path | None, optional 2036 出力先ディレクトリを指定します。save_fig=Trueの場合は必須です。 2037 output_filename: str, optional 2038 出力ファイル名を指定します。デフォルト値は"scatter.png"です。 2039 add_label: bool, optional 2040 軸ラベルを表示するかどうかを指定します。デフォルト値はTrueです。 2041 xlabel: str | None, optional 2042 x軸のラベルを指定します。デフォルト値はNoneです。 2043 ylabel: str | None, optional 2044 y軸のラベルを指定します。デフォルト値はNoneです。 2045 x_axis_range: tuple | None, optional 2046 x軸の表示範囲を(最小値, 最大値)で指定します。デフォルト値はNoneです。 2047 y_axis_range: tuple | None, optional 2048 y軸の表示範囲を(最小値, 最大値)で指定します。デフォルト値はNoneです。 2049 x_scientific: bool, optional 2050 x軸を科学的記法で表示するかどうかを指定します。デフォルト値はFalseです。 2051 y_scientific: bool, optional 2052 y軸を科学的記法で表示するかどうかを指定します。デフォルト値はFalseです。 2053 fixed_slope: float, optional 2054 固定傾きの値を指定します。デフォルト値は0.076です。 2055 show_fixed_slope: bool, optional 2056 固定傾きの線を表示するかどうかを指定します。デフォルト値はFalseです。 2057 figsize: tuple[float, float], optional 2058 プロットのサイズを(幅, 高さ)で指定します。デフォルト値は(6, 6)です。 2059 dpi: float | None, optional 2060 プロットの解像度を指定します。デフォルト値は350です。 2061 save_fig: bool, optional 2062 プロットを保存するかどうかを指定します。デフォルト値はTrueです。 2063 show_fig: bool, optional 2064 プロットを表示するかどうかを指定します。デフォルト値はTrueです。 2065 2066 Examples 2067 -------- 2068 >>> df = pd.DataFrame({ 2069 ... 'x': [1, 2, 3, 4, 5], 2070 ... 'y': [2, 4, 6, 8, 10] 2071 ... }) 2072 >>> generator = MonthlyFiguresGenerator() 2073 >>> generator.plot_scatter( 2074 ... df=df, 2075 ... col_x='x', 2076 ... col_y='y', 2077 ... xlabel='X軸', 2078 ... ylabel='Y軸', 2079 ... output_dirpath='output' 2080 ... ) 2081 """ 2082 # 有効なデータの抽出 2083 df_internal = MonthlyFiguresGenerator.get_valid_data( 2084 df=df, col_x=col_x, col_y=col_y 2085 ) 2086 2087 # データの準備 2088 x = df_internal[col_x].values 2089 y = df_internal[col_y].values 2090 2091 # データの中心化 2092 x_array = np.array(x) 2093 y_array = np.array(y) 2094 x_mean = np.mean(x_array, axis=0) 2095 y_mean = np.mean(y_array, axis=0) 2096 x_c = x - x_mean 2097 y_c = y - y_mean 2098 2099 # TLS回帰の計算 2100 data_matrix = np.vstack((x_c, y_c)) 2101 cov_matrix = np.cov(data_matrix) 2102 _, eigenvecs = linalg.eigh(cov_matrix) 2103 largest_eigenvec = eigenvecs[:, -1] 2104 2105 slope = largest_eigenvec[1] / largest_eigenvec[0] 2106 intercept = y_mean - slope * x_mean 2107 2108 # R²とRMSEの計算 2109 y_pred = slope * x + intercept 2110 r_squared = 1 - np.sum((y - y_pred) ** 2) / np.sum((y - y_mean) ** 2) 2111 rmse = np.sqrt(np.mean((y - y_pred) ** 2)) 2112 2113 # プロットの作成 2114 fig, ax = plt.subplots(figsize=figsize) 2115 2116 # データ点のプロット 2117 ax.scatter(x_array, y_array, color="black") 2118 2119 # データの範囲を取得 2120 if x_axis_range is None: 2121 x_axis_range = (df_internal[col_x].min(), df_internal[col_x].max()) 2122 if y_axis_range is None: 2123 y_axis_range = (df_internal[col_y].min(), df_internal[col_y].max()) 2124 2125 # 回帰直線のプロット 2126 x_range = np.linspace(x_axis_range[0], x_axis_range[1], 150) 2127 y_range = slope * x_range + intercept 2128 ax.plot(x_range, y_range, "r", label="TLS regression") 2129 2130 # 傾き固定の線を追加(フラグがTrueの場合) 2131 if show_fixed_slope: 2132 fixed_intercept = ( 2133 y_mean - fixed_slope * x_mean 2134 ) # 中心点を通るように切片を計算 2135 y_fixed = fixed_slope * x_range + fixed_intercept 2136 ax.plot(x_range, y_fixed, "b--", label=f"Slope = {fixed_slope}", alpha=0.7) 2137 2138 # 軸の設定 2139 ax.set_xlim(x_axis_range) 2140 ax.set_ylim(y_axis_range) 2141 2142 # 指数表記の設定 2143 if x_scientific: 2144 ax.ticklabel_format(style="sci", axis="x", scilimits=(0, 0)) 2145 ax.xaxis.get_offset_text().set_position((1.1, 0)) # 指数の位置調整 2146 if y_scientific: 2147 ax.ticklabel_format(style="sci", axis="y", scilimits=(0, 0)) 2148 ax.yaxis.get_offset_text().set_position((0, 1.1)) # 指数の位置調整 2149 2150 if add_label: 2151 if xlabel is not None: 2152 ax.set_xlabel(xlabel) 2153 if ylabel is not None: 2154 ax.set_ylabel(ylabel) 2155 2156 # 1:1の関係を示す点線(軸の範囲が同じ場合のみ表示) 2157 if ( 2158 x_axis_range is not None 2159 and y_axis_range is not None 2160 and x_axis_range == y_axis_range 2161 ): 2162 ax.plot( 2163 [x_axis_range[0], x_axis_range[1]], 2164 [x_axis_range[0], x_axis_range[1]], 2165 "k--", 2166 alpha=0.5, 2167 ) 2168 2169 # 回帰情報の表示 2170 equation = ( 2171 f"y = {slope:.2f}x {'+' if intercept >= 0 else '-'} {abs(intercept):.2f}" 2172 ) 2173 position_x = 0.05 2174 fig_ha: str = "left" 2175 ax.text( 2176 position_x, 2177 0.95, 2178 equation, 2179 transform=ax.transAxes, 2180 va="top", 2181 ha=fig_ha, 2182 color="red", 2183 ) 2184 ax.text( 2185 position_x, 2186 0.88, 2187 f"R² = {r_squared:.2f}", 2188 transform=ax.transAxes, 2189 va="top", 2190 ha=fig_ha, 2191 color="red", 2192 ) 2193 ax.text( 2194 position_x, 2195 0.81, # RMSEのための新しい位置 2196 f"RMSE = {rmse:.2f}", 2197 transform=ax.transAxes, 2198 va="top", 2199 ha=fig_ha, 2200 color="red", 2201 ) 2202 # 目盛り線の設定 2203 ax.grid(True, alpha=0.3) 2204 2205 if save_fig: 2206 if output_dirpath is None: 2207 raise ValueError( 2208 "save_fig = True の場合、 output_dirpath を指定する必要があります。有効なディレクトリパスを指定してください。" 2209 ) 2210 os.makedirs(output_dirpath, exist_ok=True) 2211 output_filepath: str = os.path.join(output_dirpath, output_filename) 2212 fig.savefig(output_filepath, dpi=dpi, bbox_inches="tight") 2213 if show_fig: 2214 plt.show() 2215 plt.close(fig=fig) 2216 2217 def plot_source_contributions_diurnal( 2218 self, 2219 df: pd.DataFrame, 2220 col_ch4_flux: str, 2221 col_c2h6_flux: str, 2222 col_datetime: str = "Date", 2223 color_bio: str = "blue", 2224 color_gas: str = "red", 2225 label_bio: str = "bio", 2226 label_gas: str = "gas", 2227 flux_alpha: float = 0.6, 2228 output_dirpath: str | Path | None = None, 2229 output_filename: str = "source_contributions.png", 2230 window_size: int = 6, 2231 print_summary: bool = False, 2232 add_xlabel: bool = True, 2233 add_ylabel: bool = True, 2234 add_legend: bool = True, 2235 smooth: bool = False, 2236 y_max: float = 100, 2237 subplot_label: str | None = None, 2238 subplot_fontsize: int = 20, 2239 figsize: tuple[float, float] = (10, 6), 2240 dpi: float | None = 350, 2241 save_fig: bool = True, 2242 show_fig: bool = True, 2243 ) -> None: 2244 """CH4フラックスの都市ガス起源と生物起源の日変化を積み上げグラフとして表示します。 2245 2246 Parameters 2247 ---------- 2248 df: pd.DataFrame 2249 CH4フラックスデータを含むデータフレームを指定します。 2250 col_ch4_flux: str 2251 CH4フラックスの列名を指定します。 2252 col_c2h6_flux: str 2253 C2H6フラックスの列名を指定します。 2254 col_datetime: str, optional 2255 日時の列名を指定します。デフォルト値は"Date"です。 2256 color_bio: str, optional 2257 生物起源の色を指定します。デフォルト値は"blue"です。 2258 color_gas: str, optional 2259 都市ガス起源の色を指定します。デフォルト値は"red"です。 2260 label_bio: str, optional 2261 生物起源のラベルを指定します。デフォルト値は"bio"です。 2262 label_gas: str, optional 2263 都市ガスのラベルを指定します。デフォルト値は"gas"です。 2264 flux_alpha: float, optional 2265 フラックスの透明度を指定します。デフォルト値は0.6です。 2266 output_dirpath: str | Path | None, optional 2267 出力先のディレクトリを指定します。save_fig=Trueの場合は必須です。 2268 output_filename: str, optional 2269 出力ファイル名を指定します。デフォルト値は"source_contributions.png"です。 2270 window_size: int, optional 2271 移動平均の窓サイズを指定します。デフォルト値は6です。 2272 print_summary: bool, optional 2273 統計情報を表示するかどうかを指定します。デフォルト値はFalseです。 2274 add_xlabel: bool, optional 2275 x軸のラベルを追加するかどうかを指定します。デフォルト値はTrueです。 2276 add_ylabel: bool, optional 2277 y軸のラベルを追加するかどうかを指定します。デフォルト値はTrueです。 2278 add_legend: bool, optional 2279 凡例を追加するかどうかを指定します。デフォルト値はTrueです。 2280 smooth: bool, optional 2281 移動平均を適用するかどうかを指定します。デフォルト値はFalseです。 2282 y_max: float, optional 2283 y軸の上限値を指定します。デフォルト値は100です。 2284 subplot_label: str | None, optional 2285 サブプロットのラベルを指定します。デフォルト値はNoneです。 2286 subplot_fontsize: int, optional 2287 サブプロットラベルのフォントサイズを指定します。デフォルト値は20です。 2288 figsize: tuple[float, float], optional 2289 プロットのサイズを指定します。デフォルト値は(10, 6)です。 2290 dpi: float | None, optional 2291 プロットのdpiを指定します。デフォルト値は350です。 2292 save_fig: bool, optional 2293 プロットを保存するかどうかを指定します。デフォルト値はTrueです。 2294 show_fig: bool, optional 2295 プロットを表示するかどうかを指定します。デフォルト値はTrueです。 2296 2297 Examples 2298 -------- 2299 >>> df = pd.read_csv("flux_data.csv") 2300 >>> generator = MonthlyFiguresGenerator() 2301 >>> generator.plot_source_contributions_diurnal( 2302 ... df=df, 2303 ... col_ch4_flux="Fch4", 2304 ... col_c2h6_flux="Fc2h6", 2305 ... output_dirpath="output", 2306 ... output_filename="diurnal_sources.png" 2307 ... ) 2308 """ 2309 # 起源の計算 2310 df_with_sources = self._calculate_source_contributions( 2311 df=df, 2312 col_ch4_flux=col_ch4_flux, 2313 col_c2h6_flux=col_c2h6_flux, 2314 col_datetime=col_datetime, 2315 ) 2316 df_with_sources.index = pd.to_datetime(df_with_sources.index) 2317 2318 # 時刻データの抽出とグループ化 2319 df_with_sources["hour"] = df_with_sources.index.hour 2320 hourly_means = df_with_sources.groupby("hour")[["ch4_gas", "ch4_bio"]].mean() 2321 2322 # 24時間目のデータ点を追加(0時のデータを使用) 2323 last_hour = hourly_means.iloc[0:1].copy() 2324 last_hour.index = pd.Index([24]) 2325 hourly_means = pd.concat([hourly_means, last_hour]) 2326 2327 # 移動平均の適用 2328 hourly_means_smoothed = hourly_means 2329 if smooth: 2330 hourly_means_smoothed = hourly_means.rolling( 2331 window=window_size, center=True, min_periods=1 2332 ).mean() 2333 2334 # 24時間分のデータポイントを作成 2335 time_points = pd.date_range("2024-01-01", periods=25, freq="h") 2336 2337 # プロットの作成 2338 fig = plt.figure(figsize=figsize) 2339 ax = plt.gca() 2340 2341 # サブプロットラベルの追加(subplot_labelが指定されている場合) 2342 if subplot_label: 2343 ax.text( 2344 0.02, # x位置 2345 0.98, # y位置 2346 subplot_label, 2347 transform=ax.transAxes, 2348 va="top", 2349 fontsize=subplot_fontsize, 2350 ) 2351 2352 # 積み上げプロット 2353 ax.fill_between( 2354 time_points, 2355 0, 2356 hourly_means_smoothed["ch4_bio"], 2357 color=color_bio, 2358 alpha=flux_alpha, 2359 label=label_bio, 2360 ) 2361 ax.fill_between( 2362 time_points, 2363 hourly_means_smoothed["ch4_bio"], 2364 hourly_means_smoothed["ch4_bio"] + hourly_means_smoothed["ch4_gas"], 2365 color=color_gas, 2366 alpha=flux_alpha, 2367 label=label_gas, 2368 ) 2369 2370 # 合計値のライン 2371 total_flux = hourly_means_smoothed["ch4_bio"] + hourly_means_smoothed["ch4_gas"] 2372 ax.plot(time_points, total_flux, "-", color="black", alpha=0.5) 2373 2374 # 軸の設定 2375 if add_xlabel: 2376 ax.set_xlabel("Time (hour)") 2377 if add_ylabel: 2378 ax.set_ylabel(r"CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)") 2379 ax.xaxis.set_major_formatter(mdates.DateFormatter("%-H")) 2380 ax.xaxis.set_major_locator(mdates.HourLocator(byhour=[0, 6, 12, 18, 24])) 2381 ax.set_xlim( 2382 float(mdates.date2num(time_points[0])), 2383 float(mdates.date2num(time_points[-1])), 2384 ) 2385 ax.set_ylim(0, y_max) # y軸の範囲を設定 2386 ax.grid(True, alpha=0.3) 2387 2388 # 凡例を図の下部に配置 2389 if add_legend: 2390 handles, labels = ax.get_legend_handles_labels() 2391 fig = plt.gcf() # 現在の図を取得 2392 fig.legend( 2393 handles, 2394 labels, 2395 loc="center", 2396 bbox_to_anchor=(0.5, 0.01), 2397 ncol=len(handles), 2398 ) 2399 plt.subplots_adjust(bottom=0.2) # 下部に凡例用のスペースを確保 2400 plt.tight_layout() 2401 2402 # グラフの保存、表示 2403 if save_fig: 2404 if output_dirpath is None: 2405 raise ValueError( 2406 "save_fig = True の場合、 output_dirpath を指定する必要があります。有効なディレクトリパスを指定してください。" 2407 ) 2408 os.makedirs(output_dirpath, exist_ok=True) 2409 output_filepath: str = os.path.join(output_dirpath, output_filename) 2410 plt.savefig(output_filepath, dpi=dpi, bbox_inches="tight") 2411 if show_fig: 2412 plt.show() 2413 plt.close(fig=fig) 2414 2415 # 統計情報の表示 2416 if print_summary: 2417 # 昼夜の時間帯を定義 2418 daytime_range: list[int] = [6, 19] # 6~18時 2419 daytime_hours = range(daytime_range[0], daytime_range[1]) 2420 nighttime_hours = list(range(0, daytime_range[0])) + list( 2421 range(daytime_range[1], 24) 2422 ) 2423 2424 # 都市ガスと生物起源のデータを取得 2425 gas_flux = hourly_means["ch4_gas"] 2426 bio_flux = hourly_means["ch4_bio"] 2427 total_flux = gas_flux + bio_flux 2428 2429 # 都市ガス比率を計算 2430 gas_ratio = (gas_flux / total_flux) * 100 2431 daytime_gas_ratio = ( 2432 pd.Series(gas_flux).iloc[np.array(list(daytime_hours))].sum() 2433 / pd.Series(total_flux).iloc[np.array(list(daytime_hours))].sum() 2434 ) * 100 2435 nighttime_gas_ratio = ( 2436 pd.Series(gas_flux).iloc[np.array(list(nighttime_hours))].sum() 2437 / pd.Series(total_flux).iloc[np.array(list(nighttime_hours))].sum() 2438 ) * 100 2439 2440 stats = { 2441 "都市ガス起源": gas_flux, 2442 "生物起源": bio_flux, 2443 "合計": total_flux, 2444 } 2445 2446 # 都市ガス比率の統計を出力 2447 self.logger.info("\n都市ガス比率の統計:") 2448 print(f" 全体の都市ガス比率: {gas_ratio.mean():.1f}%") 2449 print( 2450 f" 昼間({daytime_range[0]}~{daytime_range[1] - 1}時)の都市ガス比率: {daytime_gas_ratio:.1f}%" 2451 ) 2452 print( 2453 f" 夜間({daytime_range[1] - 1}~{daytime_range[0]}時)の都市ガス比率: {nighttime_gas_ratio:.1f}%" 2454 ) 2455 print(f" 最小比率: {gas_ratio.min():.1f}% (Hour: {gas_ratio.idxmin()})") 2456 print(f" 最大比率: {gas_ratio.max():.1f}% (Hour: {gas_ratio.idxmax()})") 2457 2458 # 各フラックスの統計を出力 2459 for source, data in stats.items(): 2460 mean_val = data.mean() 2461 min_val = data.min() 2462 max_val = data.max() 2463 min_time = data.idxmin() 2464 max_time = data.idxmax() 2465 2466 # 昼間と夜間のデータを抽出 2467 daytime_data = pd.Series(data).iloc[np.array(list(daytime_hours))] 2468 nighttime_data = pd.Series(data).iloc[np.array(list(nighttime_hours))] 2469 2470 daytime_mean = daytime_data.mean() 2471 nighttime_mean = nighttime_data.mean() 2472 2473 self.logger.info(f"\n{source}の統計:") 2474 print(f" 平均値: {mean_val:.2f}") 2475 print(f" 最小値: {min_val:.2f} (Hour: {min_time})") 2476 print(f" 最大値: {max_val:.2f} (Hour: {max_time})") 2477 if min_val != 0: 2478 print(f" 最大/最小比: {max_val / min_val:.2f}") 2479 print( 2480 f" 昼間({daytime_range[0]}~{daytime_range[1] - 1}時)の平均: {daytime_mean:.2f}" 2481 ) 2482 print( 2483 f" 夜間({daytime_range[1] - 1}~{daytime_range[0]}時)の平均: {nighttime_mean:.2f}" 2484 ) 2485 if nighttime_mean != 0: 2486 print(f" 昼/夜比: {daytime_mean / nighttime_mean:.2f}") 2487 2488 def plot_source_contributions_diurnal_by_date( 2489 self, 2490 df: pd.DataFrame, 2491 col_ch4_flux: str, 2492 col_c2h6_flux: str, 2493 col_datetime: str = "Date", 2494 color_bio: str = "blue", 2495 color_gas: str = "red", 2496 label_bio: str = "bio", 2497 label_gas: str = "gas", 2498 flux_alpha: float = 0.6, 2499 output_dirpath: str | Path | None = None, 2500 output_filename: str = "source_contributions_by_date.png", 2501 add_xlabel: bool = True, 2502 add_ylabel: bool = True, 2503 add_legend: bool = True, 2504 print_summary: bool = False, 2505 subplot_fontsize: int = 20, 2506 subplot_label_weekday: str | None = None, 2507 subplot_label_weekend: str | None = None, 2508 y_max: float | None = None, 2509 figsize: tuple[float, float] = (12, 5), 2510 dpi: float | None = 350, 2511 save_fig: bool = True, 2512 show_fig: bool = True, 2513 ) -> None: 2514 """CH4フラックスの都市ガス起源と生物起源の日変化を平日・休日別に表示します。 2515 2516 Parameters 2517 ---------- 2518 df: pd.DataFrame 2519 CH4フラックスとC2H6フラックスのデータを含むデータフレームを指定します。 2520 col_ch4_flux: str 2521 CH4フラックスのカラム名を指定します。 2522 col_c2h6_flux: str 2523 C2H6フラックスのカラム名を指定します。 2524 col_datetime: str, optional 2525 日時カラムの名前を指定します。デフォルト値は"Date"です。 2526 color_bio: str, optional 2527 生物起源のプロット色を指定します。デフォルト値は"blue"です。 2528 color_gas: str, optional 2529 都市ガス起源のプロット色を指定します。デフォルト値は"red"です。 2530 label_bio: str, optional 2531 生物起源の凡例ラベルを指定します。デフォルト値は"bio"です。 2532 label_gas: str, optional 2533 都市ガスの凡例ラベルを指定します。デフォルト値は"gas"です。 2534 flux_alpha: float, optional 2535 フラックスプロットの透明度を指定します。デフォルト値は0.6です。 2536 output_dirpath: str | Path | None, optional 2537 出力ディレクトリのパスを指定します。save_fig=Trueの場合は必須です。 2538 output_filename: str, optional 2539 出力ファイル名を指定します。デフォルト値は"source_contributions_by_date.png"です。 2540 add_xlabel: bool, optional 2541 x軸のラベルを表示するかどうかを指定します。デフォルト値はTrueです。 2542 add_ylabel: bool, optional 2543 y軸のラベルを表示するかどうかを指定します。デフォルト値はTrueです。 2544 add_legend: bool, optional 2545 凡例を表示するかどうかを指定します。デフォルト値はTrueです。 2546 print_summary: bool, optional 2547 統計情報を表示するかどうかを指定します。デフォルト値はFalseです。 2548 subplot_fontsize: int, optional 2549 サブプロットのフォントサイズを指定します。デフォルト値は20です。 2550 subplot_label_weekday: str | None, optional 2551 平日グラフのラベルを指定します。デフォルト値はNoneです。 2552 subplot_label_weekend: str | None, optional 2553 休日グラフのラベルを指定します。デフォルト値はNoneです。 2554 y_max: float | None, optional 2555 y軸の上限値を指定します。デフォルト値はNoneです。 2556 figsize: tuple[float, float], optional 2557 プロットのサイズを(幅, 高さ)で指定します。デフォルト値は(12, 5)です。 2558 dpi: float | None, optional 2559 プロットの解像度を指定します。デフォルト値は350です。 2560 save_fig: bool, optional 2561 プロットを保存するかどうかを指定します。デフォルト値はTrueです。 2562 show_fig: bool, optional 2563 プロットを表示するかどうかを指定します。デフォルト値はTrueです。 2564 2565 Examples 2566 -------- 2567 >>> df = pd.DataFrame({ 2568 ... 'Date': pd.date_range('2024-01-01', periods=48, freq='H'), 2569 ... 'Fch4': [1.2] * 48, 2570 ... 'Fc2h6': [0.1] * 48 2571 ... }) 2572 >>> generator = MonthlyFiguresGenerator() 2573 >>> generator.plot_source_contributions_diurnal_by_date( 2574 ... df=df, 2575 ... col_ch4_flux='Fch4', 2576 ... col_c2h6_flux='Fc2h6', 2577 ... output_dirpath='output' 2578 ... ) 2579 """ 2580 # 起源の計算 2581 df_with_sources = self._calculate_source_contributions( 2582 df=df, 2583 col_ch4_flux=col_ch4_flux, 2584 col_c2h6_flux=col_c2h6_flux, 2585 col_datetime=col_datetime, 2586 ) 2587 df_with_sources.index = pd.to_datetime(df_with_sources.index) 2588 2589 # 日付タイプの分類 2590 dates = pd.to_datetime(df_with_sources.index) 2591 is_weekend = dates.dayofweek.isin([5, 6]) 2592 is_holiday = dates.map(lambda x: jpholiday.is_holiday(x.date())) 2593 is_weekday = ~(is_weekend | is_holiday) 2594 2595 # データの分類 2596 data_weekday = df_with_sources[is_weekday] 2597 data_holiday = df_with_sources[is_weekend | is_holiday] 2598 2599 # プロットの作成 2600 fig, (ax1, ax2) = plt.subplots(1, 2, figsize=figsize) 2601 2602 # 平日と休日それぞれのプロット 2603 for ax, data, _ in [ 2604 (ax1, data_weekday, "Weekdays"), 2605 (ax2, data_holiday, "Weekends & Holidays"), 2606 ]: 2607 # 時間ごとの平均値を計算 2608 hourly_means = data.groupby(pd.to_datetime(data.index).hour)[ 2609 ["ch4_gas", "ch4_bio"] 2610 ].mean() 2611 2612 # 24時間目のデータ点を追加 2613 last_hour = hourly_means.iloc[0:1].copy() 2614 last_hour.index = pd.Index([24]) 2615 hourly_means = pd.concat([hourly_means, last_hour]) 2616 2617 # 24時間分のデータポイントを作成 2618 time_points = pd.date_range("2024-01-01", periods=25, freq="h") 2619 2620 # 積み上げプロット 2621 ax.fill_between( 2622 time_points, 2623 0, 2624 hourly_means["ch4_bio"], 2625 color=color_bio, 2626 alpha=flux_alpha, 2627 label=label_bio, 2628 ) 2629 ax.fill_between( 2630 time_points, 2631 hourly_means["ch4_bio"], 2632 hourly_means["ch4_bio"] + hourly_means["ch4_gas"], 2633 color=color_gas, 2634 alpha=flux_alpha, 2635 label=label_gas, 2636 ) 2637 2638 # 合計値のライン 2639 total_flux = hourly_means["ch4_bio"] + hourly_means["ch4_gas"] 2640 ax.plot(time_points, total_flux, "-", color="black", alpha=0.5) 2641 2642 # 軸の設定 2643 if add_xlabel: 2644 ax.set_xlabel("Time (hour)") 2645 if add_ylabel: 2646 if ax == ax1: # 左側のプロットのラベル 2647 ax.set_ylabel("CH$_4$ flux\n" r"(nmol m$^{-2}$ s$^{-1}$)") 2648 else: # 右側のプロットのラベル 2649 ax.set_ylabel("CH$_4$ flux\n" r"(nmol m$^{-2}$ s$^{-1}$)") 2650 2651 ax.xaxis.set_major_formatter(mdates.DateFormatter("%-H")) 2652 ax.xaxis.set_major_locator(mdates.HourLocator(byhour=[0, 6, 12, 18, 24])) 2653 ax.set_xlim( 2654 float(mdates.date2num(time_points[0])), 2655 float(mdates.date2num(time_points[-1])), 2656 ) 2657 if y_max is not None: 2658 ax.set_ylim(0, y_max) 2659 ax.grid(True, alpha=0.3) 2660 2661 # サブプロットラベルの追加 2662 if subplot_label_weekday: 2663 ax1.text( 2664 0.02, 2665 0.98, 2666 subplot_label_weekday, 2667 transform=ax1.transAxes, 2668 va="top", 2669 fontsize=subplot_fontsize, 2670 ) 2671 if subplot_label_weekend: 2672 ax2.text( 2673 0.02, 2674 0.98, 2675 subplot_label_weekend, 2676 transform=ax2.transAxes, 2677 va="top", 2678 fontsize=subplot_fontsize, 2679 ) 2680 2681 # 凡例を図の下部に配置 2682 if add_legend: 2683 # 最初のプロットから凡例のハンドルとラベルを取得 2684 handles, labels = ax1.get_legend_handles_labels() 2685 # 図の下部に凡例を配置 2686 fig.legend( 2687 handles, 2688 labels, 2689 loc="center", 2690 bbox_to_anchor=(0.5, 0.01), # x=0.5で中央、y=0.01で下部に配置 2691 ncol=len(handles), # ハンドルの数だけ列を作成(一行に表示) 2692 ) 2693 # 凡例用のスペースを確保 2694 plt.subplots_adjust(bottom=0.2) # 下部に30%のスペースを確保 2695 2696 plt.tight_layout() 2697 # グラフの保存または表示 2698 if save_fig: 2699 if output_dirpath is None: 2700 raise ValueError( 2701 "save_fig = True の場合、 output_dirpath を指定する必要があります。有効なディレクトリパスを指定してください。" 2702 ) 2703 os.makedirs(output_dirpath, exist_ok=True) 2704 output_filepath: str = os.path.join(output_dirpath, output_filename) 2705 plt.savefig(output_filepath, dpi=dpi, bbox_inches="tight") 2706 if show_fig: 2707 plt.show() 2708 plt.close(fig=fig) 2709 2710 # 統計情報の表示 2711 if print_summary: 2712 for data, label in [ 2713 (data_weekday, "Weekdays"), 2714 (data_holiday, "Weekends & Holidays"), 2715 ]: 2716 hourly_means = data.groupby(pd.to_datetime(data.index).hour)[ 2717 ["ch4_gas", "ch4_bio"] 2718 ].mean() 2719 2720 print(f"\n{label}の統計:") 2721 2722 # 都市ガス起源の統計 2723 gas_flux = hourly_means["ch4_gas"] 2724 bio_flux = hourly_means["ch4_bio"] 2725 2726 # 昼夜の時間帯を定義 2727 daytime_range: list[int] = [6, 19] # m~n時の場合、[m ,(n+1)]と定義 2728 daytime_hours = range(daytime_range[0], daytime_range[1]) 2729 nighttime_hours = list(range(0, daytime_range[0])) + list( 2730 range(daytime_range[1], 24) 2731 ) 2732 2733 # 昼間の統計 2734 daytime_gas = pd.Series(gas_flux).iloc[np.array(list(daytime_hours))] 2735 daytime_bio = pd.Series(bio_flux).iloc[np.array(list(daytime_hours))] 2736 daytime_total = daytime_gas + daytime_bio 2737 daytime_total = daytime_gas + daytime_bio 2738 daytime_ratio = (daytime_gas.sum() / daytime_total.sum()) * 100 2739 2740 # 夜間の統計 2741 nighttime_gas = pd.Series(gas_flux).iloc[ 2742 np.array(list(nighttime_hours)) 2743 ] 2744 nighttime_bio = pd.Series(bio_flux).iloc[ 2745 np.array(list(nighttime_hours)) 2746 ] 2747 nighttime_total = nighttime_gas + nighttime_bio 2748 nighttime_ratio = (nighttime_gas.sum() / nighttime_total.sum()) * 100 2749 2750 print("\n都市ガス起源:") 2751 print(f" 平均値: {gas_flux.mean():.2f}") 2752 print(f" 最小値: {gas_flux.min():.2f} (Hour: {gas_flux.idxmin()})") 2753 print(f" 最大値: {gas_flux.max():.2f} (Hour: {gas_flux.idxmax()})") 2754 if gas_flux.min() != 0: 2755 print(f" 最大/最小比: {gas_flux.max() / gas_flux.min():.2f}") 2756 print( 2757 f" 全体に占める割合: {(gas_flux.sum() / (gas_flux.sum() + hourly_means['ch4_bio'].sum()) * 100):.1f}%" 2758 ) 2759 print( 2760 f" 昼間({daytime_range[0]}~{daytime_range[1] - 1}時)の割合: {daytime_ratio:.1f}%" 2761 ) 2762 print( 2763 f" 夜間({daytime_range[1] - 1}~{daytime_range[0]}時)の割合: {nighttime_ratio:.1f}%" 2764 ) 2765 2766 # 生物起源の統計 2767 bio_flux = hourly_means["ch4_bio"] 2768 print("\n生物起源:") 2769 print(f" 平均値: {bio_flux.mean():.2f}") 2770 print(f" 最小値: {bio_flux.min():.2f} (Hour: {bio_flux.idxmin()})") 2771 print(f" 最大値: {bio_flux.max():.2f} (Hour: {bio_flux.idxmax()})") 2772 if bio_flux.min() != 0: 2773 print(f" 最大/最小比: {bio_flux.max() / bio_flux.min():.2f}") 2774 print( 2775 f" 全体に占める割合: {(bio_flux.sum() / (gas_flux.sum() + bio_flux.sum()) * 100):.1f}%" 2776 ) 2777 2778 # 合計フラックスの統計 2779 total_flux = gas_flux + bio_flux 2780 print("\n合計:") 2781 print(f" 平均値: {total_flux.mean():.2f}") 2782 print(f" 最小値: {total_flux.min():.2f} (Hour: {total_flux.idxmin()})") 2783 print(f" 最大値: {total_flux.max():.2f} (Hour: {total_flux.idxmax()})") 2784 if total_flux.min() != 0: 2785 print(f" 最大/最小比: {total_flux.max() / total_flux.min():.2f}") 2786 2787 def plot_wind_rose_sources( 2788 self, 2789 df: pd.DataFrame, 2790 output_dirpath: str | Path | None = None, 2791 output_filename: str = "edp_wind_rose.png", 2792 col_datetime: str = "Date", 2793 col_ch4_flux: str = "Fch4", 2794 col_c2h6_flux: str = "Fc2h6", 2795 col_wind_dir: str = "Wind direction", 2796 flux_unit: str = r"(nmol m$^{-2}$ s$^{-1}$)", 2797 ymax: float | None = None, 2798 color_bio: str = "blue", 2799 color_gas: str = "red", 2800 label_bio: str = "生物起源", 2801 label_gas: str = "都市ガス起源", 2802 flux_alpha: float = 0.4, 2803 num_directions: int = 8, 2804 gap_degrees: float = 0.0, 2805 center_on_angles: bool = True, 2806 subplot_label: str | None = None, 2807 add_legend: bool = True, 2808 stack_bars: bool = True, 2809 print_summary: bool = False, 2810 figsize: tuple[float, float] = (8, 8), 2811 dpi: float | None = 350, 2812 save_fig: bool = True, 2813 show_fig: bool = True, 2814 ) -> None: 2815 """CH4フラックスの都市ガス起源と生物起源の風配図を作成します。 2816 2817 Parameters 2818 ---------- 2819 df: pd.DataFrame 2820 風配図を作成するためのデータフレーム 2821 output_dirpath: str | Path | None, optional 2822 生成された図を保存するディレクトリのパス。デフォルトはNone 2823 output_filename: str, optional 2824 保存するファイル名。デフォルトは"edp_wind_rose.png" 2825 col_datetime: str, optional 2826 日時を示すカラム名。デフォルトは"Date" 2827 col_ch4_flux: str, optional 2828 CH4フラックスを示すカラム名。デフォルトは"Fch4" 2829 col_c2h6_flux: str, optional 2830 C2H6フラックスを示すカラム名。デフォルトは"Fc2h6" 2831 col_wind_dir: str, optional 2832 風向を示すカラム名。デフォルトは"Wind direction" 2833 flux_unit: str, optional 2834 フラックスの単位。デフォルトは"(nmol m$^{-2}$ s$^{-1}$)" 2835 ymax: float | None, optional 2836 y軸の上限値。指定しない場合はデータの最大値に基づいて自動設定。デフォルトはNone 2837 color_bio: str, optional 2838 生物起源のフラックスに対する色。デフォルトは"blue" 2839 color_gas: str, optional 2840 都市ガス起源のフラックスに対する色。デフォルトは"red" 2841 label_bio: str, optional 2842 生物起源のフラックスに対するラベル。デフォルトは"生物起源" 2843 label_gas: str, optional 2844 都市ガス起源のフラックスに対するラベル。デフォルトは"都市ガス起源" 2845 flux_alpha: float, optional 2846 フラックスの透明度。デフォルトは0.4 2847 num_directions: int, optional 2848 風向の数。デフォルトは8 2849 gap_degrees: float, optional 2850 セクター間の隙間の大きさ(度数)。0の場合は隙間なし。デフォルトは0.0 2851 center_on_angles: bool, optional 2852 45度刻みの線を境界として扇形を描画するかどうか。Trueの場合は境界として、Falseの場合は中間(22.5度)を中心として描画。デフォルトはTrue 2853 subplot_label: str | None, optional 2854 サブプロットに表示するラベル。デフォルトはNone 2855 add_legend: bool, optional 2856 凡例を表示するかどうか。デフォルトはTrue 2857 stack_bars: bool, optional 2858 生物起源の上に都市ガス起源を積み上げるかどうか。Trueの場合は積み上げ、Falseの場合は両方を0から積み上げ。デフォルトはTrue 2859 print_summary: bool, optional 2860 統計情報を表示するかどうか。デフォルトはFalse 2861 figsize: tuple[float, float], optional 2862 プロットのサイズ。デフォルトは(8, 8) 2863 dpi: float | None, optional 2864 プロットのdpi。デフォルトは350 2865 save_fig: bool, optional 2866 図を保存するかどうか。デフォルトはTrue 2867 show_fig: bool, optional 2868 図を表示するかどうか。デフォルトはTrue 2869 2870 Returns 2871 ---------- 2872 None 2873 2874 Examples 2875 ---------- 2876 >>> # 基本的な使用方法 2877 >>> generator = MonthlyFiguresGenerator() 2878 >>> generator.plot_wind_rose_sources( 2879 ... df=data, 2880 ... output_dirpath="output/figures", 2881 ... output_filename="wind_rose_2023.png" 2882 ... ) 2883 2884 >>> # カスタマイズした例 2885 >>> generator.plot_wind_rose_sources( 2886 ... df=data, 2887 ... num_directions=16, # 16方位で表示 2888 ... stack_bars=False, # 積み上げない 2889 ... color_bio="green", # 色を変更 2890 ... color_gas="orange" 2891 ... ) 2892 """ 2893 # 起源の計算 2894 df_with_sources = self._calculate_source_contributions( 2895 df=df, 2896 col_ch4_flux=col_ch4_flux, 2897 col_c2h6_flux=col_c2h6_flux, 2898 col_datetime=col_datetime, 2899 ) 2900 2901 # 方位の定義 2902 direction_ranges = self._define_direction_ranges( 2903 num_directions, center_on_angles 2904 ) 2905 2906 # 方位ごとのデータを集計 2907 direction_data = self._aggregate_direction_data( 2908 df_with_sources, col_wind_dir, direction_ranges 2909 ) 2910 2911 # プロットの作成 2912 fig = plt.figure(figsize=figsize, dpi=dpi) 2913 ax = fig.add_subplot(111, projection="polar") 2914 2915 # 方位の角度(ラジアン)を計算 2916 theta = np.array( 2917 [np.radians(angle) for angle in direction_data["center_angle"]] 2918 ) 2919 2920 # セクターの幅を計算(隙間を考慮) 2921 sector_width = np.radians((360.0 / num_directions) - gap_degrees) 2922 2923 # 積み上げ方式に応じてプロット 2924 if stack_bars: 2925 # 生物起源を基準として描画 2926 ax.bar( 2927 theta, 2928 direction_data["bio_flux"], 2929 width=sector_width, # 隙間を考慮した幅 2930 bottom=0.0, 2931 color=color_bio, 2932 alpha=flux_alpha, 2933 label=label_bio, 2934 ) 2935 # 都市ガス起源を生物起源の上に積み上げ 2936 ax.bar( 2937 theta, 2938 direction_data["gas_flux"], 2939 width=sector_width, # 隙間を考慮した幅 2940 bottom=direction_data["bio_flux"], 2941 color=color_gas, 2942 alpha=flux_alpha, 2943 label=label_gas, 2944 ) 2945 else: 2946 # 両方を0から積み上げ 2947 ax.bar( 2948 theta, 2949 direction_data["bio_flux"], 2950 width=sector_width, # 隙間を考慮した幅 2951 bottom=0.0, 2952 color=color_bio, 2953 alpha=flux_alpha, 2954 label=label_bio, 2955 ) 2956 ax.bar( 2957 theta, 2958 direction_data["gas_flux"], 2959 width=sector_width, # 隙間を考慮した幅 2960 bottom=0.0, 2961 color=color_gas, 2962 alpha=flux_alpha, 2963 label=label_gas, 2964 ) 2965 2966 # y軸の範囲を設定 2967 if ymax is not None: 2968 ax.set_ylim(0, ymax) 2969 else: 2970 # データの最大値に基づいて自動設定 2971 max_value = max( 2972 direction_data["bio_flux"].max(), direction_data["gas_flux"].max() 2973 ) 2974 ax.set_ylim(0, max_value * 1.1) # 最大値の1.1倍を上限に設定 2975 2976 # 方位ラベルの設定 2977 # 北を上に設定 2978 ax.set_theta_zero_location("N") # type:ignore 2979 # 時計回りに設定 2980 ax.set_theta_direction(-1) # type:ignore 2981 2982 # 方位ラベルの表示 2983 labels = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"] 2984 angles = np.radians(np.linspace(0, 360, len(labels), endpoint=False)) 2985 ax.set_xticks(angles) 2986 ax.set_xticklabels(labels) 2987 2988 # プロット領域の調整(上部と下部にスペースを確保) 2989 plt.subplots_adjust( 2990 top=0.8, # 上部に20%のスペースを確保 2991 bottom=0.2, # 下部に20%のスペースを確保(凡例用) 2992 ) 2993 2994 # サブプロットラベルの追加(デフォルトは左上) 2995 if subplot_label: 2996 ax.text( 2997 0.01, 2998 0.99, 2999 subplot_label, 3000 transform=ax.transAxes, 3001 ) 3002 3003 # 単位の追加(図の下部中央に配置) 3004 plt.figtext( 3005 0.5, # x位置(中央) 3006 0.1, # y位置(下部) 3007 flux_unit, 3008 ha="center", # 水平方向の位置揃え 3009 va="bottom", # 垂直方向の位置揃え 3010 ) 3011 3012 # 凡例の追加(単位の下に配置) 3013 if add_legend: 3014 # 最初のプロットから凡例のハンドルとラベルを取得 3015 handles, labels = ax.get_legend_handles_labels() 3016 # 図の下部に凡例を配置 3017 fig.legend( 3018 handles, 3019 labels, 3020 loc="center", 3021 bbox_to_anchor=(0.5, 0.05), # x=0.5で中央、y=0.05で下部に配置 3022 ncol=len(handles), # ハンドルの数だけ列を作成(一行に表示) 3023 ) 3024 3025 # グラフの保存または表示 3026 if save_fig: 3027 if output_dirpath is None: 3028 raise ValueError( 3029 "save_fig = True の場合、 output_dirpath を指定する必要があります。有効なディレクトリパスを指定してください。" 3030 ) 3031 os.makedirs(output_dirpath, exist_ok=True) 3032 output_filepath: str = os.path.join(output_dirpath, output_filename) 3033 plt.savefig(output_filepath, dpi=dpi, bbox_inches="tight") 3034 if show_fig: 3035 plt.show() 3036 plt.close(fig=fig) 3037 3038 # 統計情報の表示 3039 if print_summary: 3040 for source in ["gas", "bio"]: 3041 flux_data = direction_data[f"{source}_flux"] 3042 mean_val = flux_data.mean() 3043 max_val = flux_data.max() 3044 max_dir = direction_data.loc[flux_data.idxmax(), "name"] 3045 3046 self.logger.info( 3047 f"{label_gas if source == 'gas' else label_bio}の統計:" 3048 ) 3049 print(f" 平均フラックス: {mean_val:.2f}") 3050 print(f" 最大フラックス: {max_val:.2f}") 3051 print(f" 最大フラックスの方位: {max_dir}") 3052 3053 def _define_direction_ranges( 3054 self, 3055 num_directions: int = 8, 3056 center_on_angles: bool = False, 3057 ) -> pd.DataFrame: 3058 """方位の範囲を定義 3059 3060 Parameters 3061 ---------- 3062 num_directions: int 3063 方位の数(デフォルトは8) 3064 center_on_angles: bool 3065 Trueの場合、45度刻みの線を境界として扇形を描画します。 3066 Falseの場合、45度の中間(22.5度)を中心として扇形を描画します。 3067 3068 Returns 3069 ---------- 3070 pd.DataFrame 3071 方位の定義を含むDataFrame 3072 """ 3073 if num_directions == 8: 3074 if center_on_angles: 3075 # 45度刻みの線を境界とする場合 3076 directions = pd.DataFrame( 3077 { 3078 "name": ["N", "NE", "E", "SE", "S", "SW", "W", "NW"], 3079 "center_angle": [ 3080 22.5, 3081 67.5, 3082 112.5, 3083 157.5, 3084 202.5, 3085 247.5, 3086 292.5, 3087 337.5, 3088 ], 3089 } 3090 ) 3091 else: 3092 # 従来通り45度を中心とする場合 3093 directions = pd.DataFrame( 3094 { 3095 "name": ["N", "NE", "E", "SE", "S", "SW", "W", "NW"], 3096 "center_angle": [0, 45, 90, 135, 180, 225, 270, 315], 3097 } 3098 ) 3099 else: 3100 raise ValueError(f"現在{num_directions}方位はサポートされていません") 3101 3102 # 各方位の範囲を計算 3103 angle_range = 360 / num_directions 3104 directions["start_angle"] = directions["center_angle"] - angle_range / 2 3105 directions["end_angle"] = directions["center_angle"] + angle_range / 2 3106 3107 # -180度から180度の範囲に正規化 3108 directions["start_angle"] = np.where( 3109 directions["start_angle"] > 180, 3110 directions["start_angle"] - 360, 3111 directions["start_angle"], 3112 ) 3113 directions["end_angle"] = np.where( 3114 directions["end_angle"] > 180, 3115 directions["end_angle"] - 360, 3116 directions["end_angle"], 3117 ) 3118 3119 return directions 3120 3121 def _aggregate_direction_data( 3122 self, 3123 df: pd.DataFrame, 3124 col_wind_dir: str, 3125 direction_ranges: pd.DataFrame, 3126 ) -> pd.DataFrame: 3127 """方位ごとのフラックスデータを集計 3128 3129 Parameters 3130 ---------- 3131 df: pd.DataFrame 3132 ソース分離済みのデータフレーム 3133 col_wind_dir: str 3134 風向のカラム名 3135 direction_ranges: pd.DataFrame 3136 方位の定義 3137 3138 Returns 3139 ---------- 3140 pd.DataFrame 3141 方位ごとの集計データ 3142 """ 3143 df_internal: pd.DataFrame = df.copy() 3144 result_data = direction_ranges.copy() 3145 result_data["gas_flux"] = 0.0 3146 result_data["bio_flux"] = 0.0 3147 for idx, row in direction_ranges.iterrows(): 3148 if row["start_angle"] < row["end_angle"]: 3149 mask = (df_internal[col_wind_dir] > row["start_angle"]) & ( 3150 df_internal[col_wind_dir] <= row["end_angle"] 3151 ) 3152 else: # 北方向など、-180度と180度をまたぐ場合 3153 mask = (df_internal[col_wind_dir] > row["start_angle"]) | ( 3154 df_internal[col_wind_dir] <= row["end_angle"] 3155 ) 3156 result_data.at[idx, "gas_flux"] = np.nanmean( 3157 pd.to_numeric(df_internal.loc[mask, "ch4_gas"]) 3158 ) 3159 result_data.at[idx, "bio_flux"] = np.nanmean( 3160 pd.to_numeric(df_internal.loc[mask, "ch4_bio"]) 3161 ) 3162 # NaNを0に置換 3163 result_data = result_data.fillna(0) 3164 return result_data 3165 3166 def _calculate_source_contributions( 3167 self, 3168 df: pd.DataFrame, 3169 col_ch4_flux: str, 3170 col_c2h6_flux: str, 3171 gas_ratio_c1c2: float = 0.076, 3172 col_datetime: str = "Date", 3173 ) -> pd.DataFrame: 3174 """ 3175 CH4フラックスの都市ガス起源と生物起源の寄与を計算する。 3176 このロジックでは、燃焼起源のCH4フラックスは考慮せず計算している。 3177 3178 Parameters 3179 ---------- 3180 df: pd.DataFrame 3181 入力データフレーム 3182 col_ch4_flux: str 3183 CH4フラックスのカラム名 3184 col_c2h6_flux: str 3185 C2H6フラックスのカラム名 3186 gas_ratio_c1c2: float 3187 ガスのC2H6/CH4比(無次元) 3188 col_datetime: str 3189 日時カラムの名前 3190 3191 Returns 3192 ---------- 3193 pd.DataFrame 3194 起源別のフラックス値を含むデータフレーム 3195 """ 3196 df_internal = df.copy() 3197 3198 # 日時インデックスの処理 3199 if not isinstance(df_internal.index, pd.DatetimeIndex): 3200 df_internal[col_datetime] = pd.to_datetime(df_internal[col_datetime]) 3201 df_internal.set_index(col_datetime, inplace=True) 3202 3203 # C2H6/CH4比の計算 3204 df_internal["c2c1_ratio"] = ( 3205 df_internal[col_c2h6_flux] / df_internal[col_ch4_flux] 3206 ) 3207 3208 # 都市ガスの標準組成に基づく都市ガス比率の計算 3209 df_internal["gas_ratio"] = df_internal["c2c1_ratio"] / gas_ratio_c1c2 * 100 3210 3211 # gas_ratioに基づいて都市ガス起源と生物起源の寄与を比例配分 3212 df_internal["ch4_gas"] = df_internal[col_ch4_flux] * np.clip( 3213 df_internal["gas_ratio"] / 100, 0, 1 3214 ) 3215 df_internal["ch4_bio"] = df_internal[col_ch4_flux] * ( 3216 1 - np.clip(df_internal["gas_ratio"] / 100, 0, 1) 3217 ) 3218 3219 return df_internal 3220 3221 def _prepare_diurnal_data( 3222 self, 3223 df: pd.DataFrame, 3224 target_columns: list[str], 3225 include_date_types: bool = False, 3226 ) -> tuple[dict[str, pd.DataFrame], pd.DatetimeIndex]: 3227 """ 3228 日変化パターンの計算に必要なデータを準備する。 3229 3230 Parameters 3231 ---------- 3232 df: pd.DataFrame 3233 入力データフレーム 3234 target_columns: list[str] 3235 計算対象の列名のリスト 3236 include_date_types: bool 3237 日付タイプ(平日/休日など)の分類を含めるかどうか 3238 3239 Returns 3240 ---------- 3241 tuple[dict[str, pd.DataFrame], pd.DatetimeIndex] 3242 - 時間帯ごとの平均値を含むDataFrameの辞書 3243 - 24時間分の時間点 3244 """ 3245 df_internal = df.copy() 3246 df_internal["hour"] = pd.to_datetime(df_internal["Date"]).dt.hour 3247 3248 # 時間ごとの平均値を計算する関数 3249 def calculate_hourly_means(data_df, condition=None): 3250 if condition is not None: 3251 data_df = data_df[condition] 3252 return data_df.groupby("hour")[target_columns].mean().reset_index() 3253 3254 # 基本の全日データを計算 3255 hourly_means = {"all": calculate_hourly_means(df_internal)} 3256 3257 # 日付タイプによる分類が必要な場合 3258 if include_date_types: 3259 dates = pd.to_datetime(df_internal["Date"]) 3260 is_weekend = dates.dt.dayofweek.isin([5, 6]) 3261 is_holiday = dates.map(lambda x: jpholiday.is_holiday(x.date())) 3262 is_weekday = ~(is_weekend | is_holiday) 3263 3264 hourly_means.update( 3265 { 3266 "weekday": calculate_hourly_means(df_internal, is_weekday), 3267 "weekend": calculate_hourly_means(df_internal, is_weekend), 3268 "holiday": calculate_hourly_means( 3269 df_internal, is_weekend | is_holiday 3270 ), 3271 } 3272 ) 3273 3274 # 24時目のデータを追加 3275 for col in hourly_means: 3276 last_row = hourly_means[col].iloc[0:1].copy() 3277 last_row["hour"] = 24 3278 hourly_means[col] = pd.concat( 3279 [hourly_means[col], last_row], ignore_index=True 3280 ) 3281 3282 # 24時間分のデータポイントを作成 3283 time_points = pd.date_range("2024-01-01", periods=25, freq="h") 3284 3285 return hourly_means, time_points 3286 3287 def _setup_diurnal_axes( 3288 self, 3289 ax: Axes, 3290 time_points: pd.DatetimeIndex, 3291 ylabel: str, 3292 subplot_label: str | None = None, 3293 add_label: bool = True, 3294 add_legend: bool = True, 3295 subplot_fontsize: int = 20, 3296 ) -> None: 3297 """日変化プロットの軸の設定を行う 3298 3299 Parameters 3300 ---------- 3301 ax: plt.Axes 3302 設定対象の軸 3303 time_points: pd.DatetimeIndex 3304 時間軸のポイント 3305 ylabel: str 3306 y軸のラベル 3307 subplot_label: str | None 3308 サブプロットのラベル 3309 add_label: bool 3310 軸ラベルを表示するかどうか 3311 add_legend: bool 3312 凡例を表示するかどうか 3313 subplot_fontsize: int 3314 サブプロットのフォントサイズ 3315 """ 3316 if add_label: 3317 ax.set_xlabel("Time (hour)") 3318 ax.set_ylabel(ylabel) 3319 3320 ax.xaxis.set_major_formatter(mdates.DateFormatter("%-H")) 3321 ax.xaxis.set_major_locator(mdates.HourLocator(byhour=[0, 6, 12, 18, 24])) 3322 ax.set_xlim( 3323 float(mdates.date2num(time_points[0])), 3324 float(mdates.date2num(time_points[-1])), 3325 ) 3326 ax.set_xticks(time_points[::6]) 3327 ax.set_xticklabels(["0", "6", "12", "18", "24"]) 3328 3329 if subplot_label: 3330 ax.text( 3331 0.02, 3332 0.98, 3333 subplot_label, 3334 transform=ax.transAxes, 3335 va="top", 3336 fontsize=subplot_fontsize, 3337 ) 3338 3339 if add_legend: 3340 ax.legend() 3341 3342 @staticmethod 3343 def get_valid_data(df: pd.DataFrame, col_x: str, col_y: str) -> pd.DataFrame: 3344 """指定された列の有効なデータ(NaNを除いた)を取得します。 3345 3346 Parameters 3347 ---------- 3348 df: pd.DataFrame 3349 データフレームを指定します。 3350 col_x: str 3351 X軸の列名を指定します。 3352 col_y: str 3353 Y軸の列名を指定します。 3354 3355 Returns 3356 ---------- 3357 pd.DataFrame 3358 有効なデータのみを含むDataFrameを返します。 3359 3360 Examples 3361 -------- 3362 >>> df = pd.DataFrame({ 3363 ... 'x': [1, 2, np.nan, 4], 3364 ... 'y': [1, np.nan, 3, 4] 3365 ... }) 3366 >>> valid_df = MonthlyFiguresGenerator.get_valid_data(df, 'x', 'y') 3367 >>> print(valid_df) 3368 x y 3369 0 1 1 3370 3 4 4 3371 """ 3372 return df.copy().dropna(subset=[col_x, col_y])
64 def __init__( 65 self, 66 logger: Logger | None = None, 67 logging_debug: bool = False, 68 ) -> None: 69 """ 70 Monthlyシートから作成したDataFrameを解析して図を作成するクラス 71 72 Parameters 73 ------ 74 logger: Logger | None 75 使用するロガー。Noneの場合は新しいロガーを作成します。 76 logging_debug: bool 77 ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。 78 """ 79 # ロガー 80 log_level: int = INFO 81 if logging_debug: 82 log_level = DEBUG 83 self.logger: Logger = setup_logger(logger=logger, log_level=log_level)
Monthlyシートから作成したDataFrameを解析して図を作成するクラス
Parameters
logger: Logger | None
使用するロガー。Noneの場合は新しいロガーを作成します。
logging_debug: bool
ログレベルを"DEBUG"に設定するかどうか。デフォルトはFalseで、Falseの場合はINFO以上のレベルのメッセージが出力されます。
85 def plot_c1c2_concs_and_fluxes_timeseries( 86 self, 87 df: pd.DataFrame, 88 output_dirpath: str | Path | None = None, 89 output_filename: str = "conc_flux_timeseries.png", 90 col_datetime: str = "Date", 91 col_ch4_conc: str = "CH4_ultra", 92 col_ch4_flux: str = "Fch4_ultra", 93 col_c2h6_conc: str = "C2H6_ultra", 94 col_c2h6_flux: str = "Fc2h6_ultra", 95 ylim_ch4_conc: tuple = (1.8, 2.6), 96 ylim_ch4_flux: tuple = (-100, 600), 97 ylim_c2h6_conc: tuple = (-12, 20), 98 ylim_c2h6_flux: tuple = (-20, 40), 99 figsize: tuple[float, float] = (12, 16), 100 dpi: float | None = 350, 101 save_fig: bool = True, 102 show_fig: bool = True, 103 print_summary: bool = False, 104 ) -> None: 105 """CH4とC2H6の濃度とフラックスの時系列プロットを作成します。 106 107 Parameters 108 ---------- 109 df: pd.DataFrame 110 月別データを含むDataFrameを指定します。 111 output_dirpath: str | Path | None, optional 112 出力ディレクトリのパスを指定します。save_fig=Trueの場合は必須です。 113 output_filename: str, optional 114 出力ファイル名を指定します。デフォルト値は"conc_flux_timeseries.png"です。 115 col_datetime: str, optional 116 日付列の名前を指定します。デフォルト値は"Date"です。 117 col_ch4_conc: str, optional 118 CH4濃度列の名前を指定します。デフォルト値は"CH4_ultra"です。 119 col_ch4_flux: str, optional 120 CH4フラックス列の名前を指定します。デフォルト値は"Fch4_ultra"です。 121 col_c2h6_conc: str, optional 122 C2H6濃度列の名前を指定します。デフォルト値は"C2H6_ultra"です。 123 col_c2h6_flux: str, optional 124 C2H6フラックス列の名前を指定します。デフォルト値は"Fc2h6_ultra"です。 125 ylim_ch4_conc: tuple, optional 126 CH4濃度のy軸範囲を指定します。デフォルト値は(1.8, 2.6)です。 127 ylim_ch4_flux: tuple, optional 128 CH4フラックスのy軸範囲を指定します。デフォルト値は(-100, 600)です。 129 ylim_c2h6_conc: tuple, optional 130 C2H6濃度のy軸範囲を指定します。デフォルト値は(-12, 20)です。 131 ylim_c2h6_flux: tuple, optional 132 C2H6フラックスのy軸範囲を指定します。デフォルト値は(-20, 40)です。 133 figsize: tuple[float, float], optional 134 プロットのサイズを指定します。デフォルト値は(12, 16)です。 135 dpi: float | None, optional 136 プロットのdpiを指定します。デフォルト値は350です。 137 save_fig: bool, optional 138 図を保存するかどうかを指定します。デフォルト値はTrueです。 139 show_fig: bool, optional 140 図を表示するかどうかを指定します。デフォルト値はTrueです。 141 print_summary: bool, optional 142 解析情報をprintするかどうかを指定します。デフォルト値はFalseです。 143 144 Examples 145 -------- 146 >>> generator = MonthlyFiguresGenerator() 147 >>> generator.plot_c1c2_concs_and_fluxes_timeseries( 148 ... df=monthly_data, 149 ... output_dirpath="output", 150 ... ylim_ch4_conc=(1.5, 3.0), 151 ... print_summary=True 152 ... ) 153 """ 154 # dfのコピー 155 df_internal: pd.DataFrame = df.copy() 156 if print_summary: 157 # 統計情報の計算と表示 158 for name, col in [ 159 ("CH4 concentration", col_ch4_conc), 160 ("CH4 flux", col_ch4_flux), 161 ("C2H6 concentration", col_c2h6_conc), 162 ("C2H6 flux", col_c2h6_flux), 163 ]: 164 # NaNを除外してから統計量を計算 165 valid_data = df_internal[col].dropna() 166 167 if len(valid_data) > 0: 168 # quantileで計算(0-1の範囲) 169 quantile_05 = valid_data.quantile(0.05) 170 quantile_95 = valid_data.quantile(0.95) 171 mean_value = np.nanmean(valid_data) 172 positive_ratio = (valid_data > 0).mean() * 100 173 174 print(f"\n{name}:") 175 # パーセンタイルで表示(0-100の範囲) 176 print( 177 f"90パーセンタイルレンジ: {quantile_05:.2f} - {quantile_95:.2f}" 178 ) 179 print(f"平均値: {mean_value:.2f}") 180 print(f"正の値の割合: {positive_ratio:.1f}%") 181 else: 182 print(f"\n{name}: データが存在しません") 183 184 # プロットの作成 185 _, (ax1, ax2, ax3, ax4) = plt.subplots(4, 1, figsize=figsize, sharex=True) 186 187 # CH4濃度のプロット 188 ax1.scatter( 189 df_internal[col_datetime], 190 df_internal[col_ch4_conc], 191 color="red", 192 alpha=0.5, 193 s=20, 194 ) 195 ax1.set_ylabel("CH$_4$ Concentration\n(ppm)") 196 ax1.set_ylim(*ylim_ch4_conc) # 引数からy軸範囲を設定 197 ax1.text(0.02, 0.98, "(a)", transform=ax1.transAxes, va="top", fontsize=20) 198 ax1.grid(True, alpha=0.3) 199 200 # CH4フラックスのプロット 201 ax2.scatter( 202 df_internal[col_datetime], 203 df_internal[col_ch4_flux], 204 color="red", 205 alpha=0.5, 206 s=20, 207 ) 208 ax2.set_ylabel("CH$_4$ flux\n(nmol m$^{-2}$ s$^{-1}$)") 209 ax2.set_ylim(*ylim_ch4_flux) # 引数からy軸範囲を設定 210 ax2.text(0.02, 0.98, "(b)", transform=ax2.transAxes, va="top", fontsize=20) 211 ax2.grid(True, alpha=0.3) 212 213 # C2H6濃度のプロット 214 ax3.scatter( 215 df_internal[col_datetime], 216 df_internal[col_c2h6_conc], 217 color="orange", 218 alpha=0.5, 219 s=20, 220 ) 221 ax3.set_ylabel("C$_2$H$_6$ Concentration\n(ppb)") 222 ax3.set_ylim(*ylim_c2h6_conc) # 引数からy軸範囲を設定 223 ax3.text(0.02, 0.98, "(c)", transform=ax3.transAxes, va="top", fontsize=20) 224 ax3.grid(True, alpha=0.3) 225 226 # C2H6フラックスのプロット 227 ax4.scatter( 228 df_internal[col_datetime], 229 df_internal[col_c2h6_flux], 230 color="orange", 231 alpha=0.5, 232 s=20, 233 ) 234 ax4.set_ylabel("C$_2$H$_6$ flux\n(nmol m$^{-2}$ s$^{-1}$)") 235 ax4.set_ylim(*ylim_c2h6_flux) # 引数からy軸範囲を設定 236 ax4.text(0.02, 0.98, "(d)", transform=ax4.transAxes, va="top", fontsize=20) 237 ax4.grid(True, alpha=0.3) 238 239 # x軸の設定 240 ax4.xaxis.set_major_locator(mdates.MonthLocator(interval=1)) 241 ax4.xaxis.set_major_formatter(mdates.DateFormatter("%m")) 242 plt.setp(ax4.get_xticklabels(), rotation=0, ha="right") 243 ax4.set_xlabel("Month") 244 245 # レイアウトの調整と保存 246 plt.tight_layout() 247 248 # 図の保存 249 if save_fig: 250 if output_dirpath is None: 251 raise ValueError( 252 "save_fig = True のとき、 output_dirpath に有効なディレクトリパスを指定する必要があります。" 253 ) 254 # 出力ディレクトリの作成 255 os.makedirs(output_dirpath, exist_ok=True) 256 output_filepath: str = os.path.join(output_dirpath, output_filename) 257 plt.savefig(output_filepath, dpi=dpi, bbox_inches="tight") 258 if show_fig: 259 plt.show() 260 plt.close() 261 262 if print_summary: 263 264 def analyze_top_values(df, column_name, top_percent=20): 265 print(f"\n{column_name}の上位{top_percent}%の分析:") 266 # DataFrameのコピーを作成し、日時関連の列を追加 267 df_analysis = df.copy() 268 df_analysis["hour"] = pd.to_datetime(df_analysis[col_datetime]).dt.hour 269 df_analysis["month"] = pd.to_datetime( 270 df_analysis[col_datetime] 271 ).dt.month 272 df_analysis["weekday"] = pd.to_datetime( 273 df_analysis[col_datetime] 274 ).dt.dayofweek 275 276 # 上位20%のしきい値を計算 277 threshold = df_analysis[column_name].quantile(1 - top_percent / 100) 278 high_values = df_analysis[df_analysis[column_name] > threshold] 279 280 # 月ごとの分析 281 print("\n月別分布:") 282 monthly_counts = high_values.groupby("month").size() 283 total_counts = df_analysis.groupby("month").size() 284 monthly_percentages = (monthly_counts / total_counts * 100).round(1) 285 286 # 月ごとのデータを安全に表示 287 available_months = set(monthly_counts.index) & set(total_counts.index) 288 for month in sorted(available_months): 289 print( 290 f"月{month}: {monthly_percentages[month]}% ({monthly_counts[month]}件/{total_counts[month]}件)" 291 ) 292 293 # 時間帯ごとの分析(3時間区切り) 294 print("\n時間帯別分布:") 295 # copyを作成して新しい列を追加 296 high_values = high_values.copy() 297 high_values["time_block"] = high_values["hour"] // 3 * 3 298 time_blocks = high_values.groupby("time_block").size() 299 total_time_blocks = df_analysis.groupby( 300 df_analysis["hour"] // 3 * 3 301 ).size() 302 time_percentages = (time_blocks / total_time_blocks * 100).round(1) 303 304 # 時間帯ごとのデータを安全に表示 305 available_blocks = set(time_blocks.index) & set(total_time_blocks.index) 306 for block in sorted(available_blocks): 307 print( 308 f"{block:02d}:00-{block + 3:02d}:00: {time_percentages[block]}% ({time_blocks[block]}件/{total_time_blocks[block]}件)" 309 ) 310 311 # 曜日ごとの分析 312 print("\n曜日別分布:") 313 weekday_names = ["月曜", "火曜", "水曜", "木曜", "金曜", "土曜", "日曜"] 314 weekday_counts = high_values.groupby("weekday").size() 315 total_weekdays = df_analysis.groupby("weekday").size() 316 weekday_percentages = (weekday_counts / total_weekdays * 100).round(1) 317 318 # 曜日ごとのデータを安全に表示 319 available_days = set(weekday_counts.index) & set(total_weekdays.index) 320 for day in sorted(available_days): 321 if 0 <= day <= 6: # 有効な曜日インデックスのチェック 322 print( 323 f"{weekday_names[day]}: {weekday_percentages[day]}% ({weekday_counts[day]}件/{total_weekdays[day]}件)" 324 ) 325 326 # 濃度とフラックスそれぞれの分析を実行 327 print("\n=== 上位値の時間帯・曜日分析 ===") 328 analyze_top_values(df_internal, col_ch4_conc) 329 analyze_top_values(df_internal, col_ch4_flux) 330 analyze_top_values(df_internal, col_c2h6_conc) 331 analyze_top_values(df_internal, col_c2h6_flux)
CH4とC2H6の濃度とフラックスの時系列プロットを作成します。
Parameters
df: pd.DataFrame
月別データを含むDataFrameを指定します。
output_dirpath: str | Path | None, optional
出力ディレクトリのパスを指定します。save_fig=Trueの場合は必須です。
output_filename: str, optional
出力ファイル名を指定します。デフォルト値は"conc_flux_timeseries.png"です。
col_datetime: str, optional
日付列の名前を指定します。デフォルト値は"Date"です。
col_ch4_conc: str, optional
CH4濃度列の名前を指定します。デフォルト値は"CH4_ultra"です。
col_ch4_flux: str, optional
CH4フラックス列の名前を指定します。デフォルト値は"Fch4_ultra"です。
col_c2h6_conc: str, optional
C2H6濃度列の名前を指定します。デフォルト値は"C2H6_ultra"です。
col_c2h6_flux: str, optional
C2H6フラックス列の名前を指定します。デフォルト値は"Fc2h6_ultra"です。
ylim_ch4_conc: tuple, optional
CH4濃度のy軸範囲を指定します。デフォルト値は(1.8, 2.6)です。
ylim_ch4_flux: tuple, optional
CH4フラックスのy軸範囲を指定します。デフォルト値は(-100, 600)です。
ylim_c2h6_conc: tuple, optional
C2H6濃度のy軸範囲を指定します。デフォルト値は(-12, 20)です。
ylim_c2h6_flux: tuple, optional
C2H6フラックスのy軸範囲を指定します。デフォルト値は(-20, 40)です。
figsize: tuple[float, float], optional
プロットのサイズを指定します。デフォルト値は(12, 16)です。
dpi: float | None, optional
プロットのdpiを指定します。デフォルト値は350です。
save_fig: bool, optional
図を保存するかどうかを指定します。デフォルト値はTrueです。
show_fig: bool, optional
図を表示するかどうかを指定します。デフォルト値はTrueです。
print_summary: bool, optional
解析情報をprintするかどうかを指定します。デフォルト値はFalseです。
Examples
>>> generator = MonthlyFiguresGenerator()
>>> generator.plot_c1c2_concs_and_fluxes_timeseries(
... df=monthly_data,
... output_dirpath="output",
... ylim_ch4_conc=(1.5, 3.0),
... print_summary=True
... )
333 def plot_c1c2_timeseries( 334 self, 335 df: pd.DataFrame, 336 col_ch4_flux: str, 337 col_c2h6_flux: str, 338 output_dirpath: str | Path | None = None, 339 output_filename: str = "timeseries_year.png", 340 col_datetime: str = "Date", 341 window_size: int = 24 * 7, 342 confidence_interval: float = 0.95, 343 subplot_label_ch4: str | None = "(a)", 344 subplot_label_c2h6: str | None = "(b)", 345 subplot_fontsize: int = 20, 346 show_ci: bool = True, 347 ch4_ylim: tuple[float, float] | None = None, 348 c2h6_ylim: tuple[float, float] | None = None, 349 start_date: str | None = None, 350 end_date: str | None = None, 351 figsize: tuple[float, float] = (16, 6), 352 dpi: float | None = 350, 353 save_fig: bool = True, 354 show_fig: bool = True, 355 ) -> None: 356 """CH4とC2H6フラックスの時系列変動をプロットします。 357 358 Parameters 359 ---------- 360 df: pd.DataFrame 361 プロットするデータを含むDataFrameを指定します。 362 col_ch4_flux: str 363 CH4フラックスのカラム名を指定します。 364 col_c2h6_flux: str 365 C2H6フラックスのカラム名を指定します。 366 output_dirpath: str | Path | None, optional 367 出力ディレクトリのパスを指定します。save_fig=Trueの場合は必須です。 368 output_filename: str, optional 369 出力ファイル名を指定します。デフォルト値は"timeseries_year.png"です。 370 col_datetime: str, optional 371 日時カラムの名前を指定します。デフォルト値は"Date"です。 372 window_size: int, optional 373 移動平均の窓サイズを指定します。デフォルト値は24*7(1週間)です。 374 confidence_interval: float, optional 375 信頼区間を指定します。0から1の間の値で、デフォルト値は0.95(95%)です。 376 subplot_label_ch4: str | None, optional 377 CH4プロットのラベルを指定します。デフォルト値は"(a)"です。 378 subplot_label_c2h6: str | None, optional 379 C2H6プロットのラベルを指定します。デフォルト値は"(b)"です。 380 subplot_fontsize: int, optional 381 サブプロットのフォントサイズを指定します。デフォルト値は20です。 382 show_ci: bool, optional 383 信頼区間を表示するかどうかを指定します。デフォルト値はTrueです。 384 ch4_ylim: tuple[float, float] | None, optional 385 CH4のy軸範囲を指定します。未指定の場合は自動で設定されます。 386 c2h6_ylim: tuple[float, float] | None, optional 387 C2H6のy軸範囲を指定します。未指定の場合は自動で設定されます。 388 start_date: str | None, optional 389 開始日を"YYYY-MM-DD"形式で指定します。未指定の場合はデータの最初の日付が使用されます。 390 end_date: str | None, optional 391 終了日を"YYYY-MM-DD"形式で指定します。未指定の場合はデータの最後の日付が使用されます。 392 figsize: tuple[float, float], optional 393 プロットのサイズを指定します。デフォルト値は(16, 6)です。 394 dpi: float | None, optional 395 プロットのdpiを指定します。デフォルト値は350です。 396 save_fig: bool, optional 397 プロットを保存するかどうかを指定します。デフォルト値はTrueです。 398 show_fig: bool, optional 399 プロットを表示するかどうかを指定します。デフォルト値はTrueです。 400 401 Examples 402 -------- 403 >>> generator = MonthlyFiguresGenerator() 404 >>> generator.plot_c1c2_timeseries( 405 ... df=monthly_data, 406 ... col_ch4_flux="Fch4_ultra", 407 ... col_c2h6_flux="Fc2h6_ultra", 408 ... output_dirpath="output", 409 ... start_date="2023-01-01", 410 ... end_date="2023-12-31" 411 ... ) 412 """ 413 # データの準備 414 df_internal = df.copy() 415 if not isinstance(df_internal.index, pd.DatetimeIndex): 416 df_internal[col_datetime] = pd.to_datetime(df_internal[col_datetime]) 417 df_internal.set_index(col_datetime, inplace=True) 418 419 # 日付範囲の処理 420 if start_date is not None: 421 start_dt = pd.to_datetime(start_date).normalize() # 時刻を00:00:00に設定 422 # df_min_date = ( 423 # df_internal.index.normalize().min().normalize() 424 # ) # 日付のみの比較のため正規化 425 df_min_date = pd.to_datetime(df_internal.index.min()).normalize() 426 427 # データの最小日付が指定開始日より後の場合にのみ警告 428 if df_min_date.date() > start_dt.date(): 429 self.logger.warning( 430 f"指定された開始日{start_date}がデータの開始日{df_min_date.strftime('%Y-%m-%d')}より前です。" 431 f"データの開始日を使用します。" 432 ) 433 start_dt = df_min_date 434 else: 435 # start_dt = df_internal.index.normalize().min() 436 start_dt = pd.to_datetime(df_internal.index.min()).normalize() 437 438 if end_date is not None: 439 end_dt = ( 440 pd.to_datetime(end_date).normalize() 441 + pd.Timedelta(days=1) 442 - pd.Timedelta(seconds=1) 443 ) 444 # df_max_date = ( 445 # df_internal.index.normalize().max().normalize() 446 # ) # 日付のみの比較のため正規化 447 df_max_date = pd.to_datetime(df_internal.index.max()).normalize() 448 449 # データの最大日付が指定終了日より前の場合にのみ警告 450 if df_max_date.date() < pd.to_datetime(end_date).date(): 451 self.logger.warning( 452 f"指定された終了日{end_date}がデータの終了日{df_max_date.strftime('%Y-%m-%d')}より後です。" 453 f"データの終了日を使用します。" 454 ) 455 end_dt = df_internal.index.max() 456 else: 457 end_dt = df_internal.index.max() 458 459 # 指定された期間のデータを抽出 460 mask = (df_internal.index >= start_dt) & (df_internal.index <= end_dt) 461 df_internal = df_internal[mask] 462 463 # CH4とC2H6の移動平均と信頼区間を計算 464 ch4_mean, ch4_lower, ch4_upper = calculate_rolling_stats( 465 df_internal[col_ch4_flux], window_size, confidence_interval 466 ) 467 c2h6_mean, c2h6_lower, c2h6_upper = calculate_rolling_stats( 468 df_internal[col_c2h6_flux], window_size, confidence_interval 469 ) 470 471 # プロットの作成 472 fig, (ax1, ax2) = plt.subplots(1, 2, figsize=figsize) 473 474 # CH4プロット 475 ax1.plot(df_internal.index, ch4_mean, "red", label="CH$_4$") 476 if show_ci: 477 ax1.fill_between( 478 df_internal.index, ch4_lower, ch4_upper, color="red", alpha=0.2 479 ) 480 if subplot_label_ch4: 481 ax1.text( 482 0.02, 483 0.98, 484 subplot_label_ch4, 485 transform=ax1.transAxes, 486 va="top", 487 fontsize=subplot_fontsize, 488 ) 489 ax1.set_ylabel("CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)") 490 if ch4_ylim is not None: 491 ax1.set_ylim(ch4_ylim) 492 ax1.grid(True, alpha=0.3) 493 494 # C2H6プロット 495 ax2.plot(df_internal.index, c2h6_mean, "orange", label="C$_2$H$_6$") 496 if show_ci: 497 ax2.fill_between( 498 df_internal.index, c2h6_lower, c2h6_upper, color="orange", alpha=0.2 499 ) 500 if subplot_label_c2h6: 501 ax2.text( 502 0.02, 503 0.98, 504 subplot_label_c2h6, 505 transform=ax2.transAxes, 506 va="top", 507 fontsize=subplot_fontsize, 508 ) 509 ax2.set_ylabel("C$_2$H$_6$ flux (nmol m$^{-2}$ s$^{-1}$)") 510 if c2h6_ylim is not None: 511 ax2.set_ylim(c2h6_ylim) 512 ax2.grid(True, alpha=0.3) 513 514 # x軸の設定 515 for ax in [ax1, ax2]: 516 ax.set_xlabel("Month") 517 # x軸の範囲を設定 518 ax.set_xlim(start_dt, end_dt) 519 520 # 1ヶ月ごとの主目盛り 521 ax.xaxis.set_major_locator(mdates.MonthLocator(interval=1)) 522 523 # カスタムフォーマッタの作成(数字を通常フォントで表示) 524 def date_formatter(x, p): 525 date = mdates.num2date(x) 526 return f"{date.strftime('%m')}" 527 528 ax.xaxis.set_major_formatter(FuncFormatter(date_formatter)) 529 530 # 補助目盛りの設定 531 ax.xaxis.set_minor_locator(mdates.MonthLocator()) 532 # ティックラベルの回転と位置調整 533 plt.setp(ax.xaxis.get_majorticklabels(), ha="right") 534 535 plt.tight_layout() 536 537 if save_fig: 538 if output_dirpath is None: 539 raise ValueError( 540 "save_fig = True のとき、 output_dirpath に有効なディレクトリパスを指定する必要があります。" 541 ) 542 # 出力ディレクトリの作成 543 os.makedirs(output_dirpath, exist_ok=True) 544 output_filepath: str = os.path.join(output_dirpath, output_filename) 545 plt.savefig(output_filepath, dpi=dpi, bbox_inches="tight") 546 if show_fig: 547 plt.show() 548 plt.close(fig=fig)
CH4とC2H6フラックスの時系列変動をプロットします。
Parameters
df: pd.DataFrame
プロットするデータを含むDataFrameを指定します。
col_ch4_flux: str
CH4フラックスのカラム名を指定します。
col_c2h6_flux: str
C2H6フラックスのカラム名を指定します。
output_dirpath: str | Path | None, optional
出力ディレクトリのパスを指定します。save_fig=Trueの場合は必須です。
output_filename: str, optional
出力ファイル名を指定します。デフォルト値は"timeseries_year.png"です。
col_datetime: str, optional
日時カラムの名前を指定します。デフォルト値は"Date"です。
window_size: int, optional
移動平均の窓サイズを指定します。デフォルト値は24*7(1週間)です。
confidence_interval: float, optional
信頼区間を指定します。0から1の間の値で、デフォルト値は0.95(95%)です。
subplot_label_ch4: str | None, optional
CH4プロットのラベルを指定します。デフォルト値は"(a)"です。
subplot_label_c2h6: str | None, optional
C2H6プロットのラベルを指定します。デフォルト値は"(b)"です。
subplot_fontsize: int, optional
サブプロットのフォントサイズを指定します。デフォルト値は20です。
show_ci: bool, optional
信頼区間を表示するかどうかを指定します。デフォルト値はTrueです。
ch4_ylim: tuple[float, float] | None, optional
CH4のy軸範囲を指定します。未指定の場合は自動で設定されます。
c2h6_ylim: tuple[float, float] | None, optional
C2H6のy軸範囲を指定します。未指定の場合は自動で設定されます。
start_date: str | None, optional
開始日を"YYYY-MM-DD"形式で指定します。未指定の場合はデータの最初の日付が使用されます。
end_date: str | None, optional
終了日を"YYYY-MM-DD"形式で指定します。未指定の場合はデータの最後の日付が使用されます。
figsize: tuple[float, float], optional
プロットのサイズを指定します。デフォルト値は(16, 6)です。
dpi: float | None, optional
プロットのdpiを指定します。デフォルト値は350です。
save_fig: bool, optional
プロットを保存するかどうかを指定します。デフォルト値はTrueです。
show_fig: bool, optional
プロットを表示するかどうかを指定します。デフォルト値はTrueです。
Examples
>>> generator = MonthlyFiguresGenerator()
>>> generator.plot_c1c2_timeseries(
... df=monthly_data,
... col_ch4_flux="Fch4_ultra",
... col_c2h6_flux="Fc2h6_ultra",
... output_dirpath="output",
... start_date="2023-01-01",
... end_date="2023-12-31"
... )
550 def plot_fluxes_comparison( 551 self, 552 df: pd.DataFrame, 553 cols_flux: list[str], 554 labels: list[str], 555 colors: list[str], 556 output_dirpath: str | Path | None, 557 output_filename: str = "ch4_flux_comparison.png", 558 col_datetime: str = "Date", 559 window_size: int = 24 * 7, 560 confidence_interval: float = 0.95, 561 subplot_label: str | None = None, 562 subplot_fontsize: int = 20, 563 show_ci: bool = True, 564 ylim: tuple[float, float] | None = None, 565 start_date: str | None = None, 566 end_date: str | None = None, 567 include_end_date: bool = True, 568 legend_loc: str = "upper right", 569 apply_ma: bool = True, 570 hourly_mean: bool = False, 571 x_interval: Literal["month", "10days"] = "month", 572 xlabel: str = "Month", 573 ylabel: str = "CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)", 574 figsize: tuple[float, float] = (12, 6), 575 dpi: float | None = 350, 576 save_fig: bool = True, 577 show_fig: bool = True, 578 ) -> None: 579 """複数のCH4フラックスの時系列変動を比較するプロットを作成します。 580 581 Parameters 582 ---------- 583 df: pd.DataFrame 584 プロットするデータを含むDataFrameを指定します。 585 cols_flux: list[str] 586 比較するフラックスのカラム名のリストを指定します。 587 labels: list[str] 588 凡例に表示する各フラックスのラベルのリストを指定します。 589 colors: list[str] 590 各フラックスの色のリストを指定します。 591 output_dirpath: str | Path | None 592 出力ディレクトリのパスを指定します。save_fig=Trueの場合は必須です。 593 output_filename: str, optional 594 出力ファイル名を指定します。デフォルト値は"ch4_flux_comparison.png"です。 595 col_datetime: str, optional 596 日時カラムの名前を指定します。デフォルト値は"Date"です。 597 window_size: int, optional 598 移動平均の窓サイズを指定します。デフォルト値は24*7(1週間)です。 599 confidence_interval: float, optional 600 信頼区間を指定します。0から1の間の値で、デフォルト値は0.95(95%)です。 601 subplot_label: str | None, optional 602 プロットのラベルを指定します。デフォルト値はNoneです。 603 subplot_fontsize: int, optional 604 サブプロットのフォントサイズを指定します。デフォルト値は20です。 605 show_ci: bool, optional 606 信頼区間を表示するかどうかを指定します。デフォルト値はTrueです。 607 ylim: tuple[float, float] | None, optional 608 y軸の範囲を指定します。デフォルト値はNoneです。 609 start_date: str | None, optional 610 開始日をYYYY-MM-DD形式で指定します。デフォルト値はNoneです。 611 end_date: str | None, optional 612 終了日をYYYY-MM-DD形式で指定します。デフォルト値はNoneです。 613 include_end_date: bool, optional 614 終了日を含めるかどうかを指定します。デフォルト値はTrueです。 615 legend_loc: str, optional 616 凡例の位置を指定します。デフォルト値は"upper right"です。 617 apply_ma: bool, optional 618 移動平均を適用するかどうかを指定します。デフォルト値はTrueです。 619 hourly_mean: bool, optional 620 1時間平均を適用するかどうかを指定します。デフォルト値はFalseです。 621 x_interval: Literal["month", "10days"], optional 622 x軸の目盛り間隔を指定します。"month"(月初めのみ)または"10days"(10日刻み)を指定できます。デフォルト値は"month"です。 623 xlabel: str, optional 624 x軸のラベルを指定します。デフォルト値は"Month"です。 625 ylabel: str, optional 626 y軸のラベルを指定します。デフォルト値は"CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)"です。 627 figsize: tuple[float, float], optional 628 プロットのサイズを指定します。デフォルト値は(12, 6)です。 629 dpi: float | None, optional 630 プロットのdpiを指定します。デフォルト値は350です。 631 save_fig: bool, optional 632 図を保存するかどうかを指定します。デフォルト値はTrueです。 633 show_fig: bool, optional 634 図を表示するかどうかを指定します。デフォルト値はTrueです。 635 636 Examples 637 ------- 638 >>> generator = MonthlyFiguresGenerator() 639 >>> generator.plot_fluxes_comparison( 640 ... df=monthly_data, 641 ... cols_flux=["Fch4_ultra", "Fch4_picarro"], 642 ... labels=["Ultra", "Picarro"], 643 ... colors=["red", "blue"], 644 ... output_dirpath="output", 645 ... start_date="2023-01-01", 646 ... end_date="2023-12-31" 647 ... ) 648 """ 649 # データの準備 650 df_internal = df.copy() 651 652 # インデックスを日時型に変換 653 df_internal.index = pd.to_datetime(df_internal.index) 654 655 # 1時間平均の適用 656 if hourly_mean: 657 # 時間情報のみを使用してグループ化 658 df_internal = df_internal.groupby( 659 [df_internal.index.strftime("%Y-%m-%d"), df_internal.index.hour] 660 ).mean() 661 # マルチインデックスを日時インデックスに変換 662 df_internal.index = pd.to_datetime( 663 [f"{date} {hour:02d}:00:00" for date, hour in df_internal.index] 664 ) 665 666 # 日付範囲の処理 667 if start_date is not None: 668 start_dt = pd.to_datetime(start_date).normalize() # 時刻を00:00:00に設定 669 df_min_date = ( 670 df_internal.index.normalize().min().normalize() 671 ) # 日付のみの比較のため正規化 672 673 # データの最小日付が指定開始日より後の場合にのみ警告 674 if df_min_date.date() > start_dt.date(): 675 self.logger.warning( 676 f"指定された開始日{start_date}がデータの開始日{df_min_date.strftime('%Y-%m-%d')}より前です。" 677 f"データの開始日を使用します。" 678 ) 679 start_dt = df_min_date 680 else: 681 start_dt = df_internal.index.normalize().min() 682 683 if end_date is not None: 684 if include_end_date: 685 end_dt = ( 686 pd.to_datetime(end_date).normalize() 687 + pd.Timedelta(days=1) 688 - pd.Timedelta(seconds=1) 689 ) 690 else: 691 # 終了日を含まない場合、終了日の前日の23:59:59まで 692 end_dt = pd.to_datetime(end_date).normalize() - pd.Timedelta(seconds=1) 693 694 df_max_date = ( 695 df_internal.index.normalize().max().normalize() 696 ) # 日付のみの比較のため正規化 697 698 # データの最大日付が指定終了日より前の場合にのみ警告 699 compare_date = pd.to_datetime(end_date).date() 700 if not include_end_date: 701 compare_date = compare_date - pd.Timedelta(days=1) 702 703 if df_max_date.date() < compare_date: 704 self.logger.warning( 705 f"指定された終了日{end_date}がデータの終了日{df_max_date.strftime('%Y-%m-%d')}より後です。" 706 f"データの終了日を使用します。" 707 ) 708 end_dt = df_internal.index.max() 709 else: 710 end_dt = df_internal.index.max() 711 712 # 指定された期間のデータを抽出 713 mask = (df_internal.index >= start_dt) & (df_internal.index <= end_dt) 714 df_internal = df_internal[mask] 715 716 # プロットの作成 717 fig, ax = plt.subplots(figsize=figsize) 718 719 # 各フラックスのプロット 720 for flux_col, label, color in zip(cols_flux, labels, colors, strict=True): 721 if apply_ma: 722 # 移動平均の計算 723 mean, lower, upper = calculate_rolling_stats( 724 df_internal[flux_col], window_size, confidence_interval 725 ) 726 ax.plot(df_internal.index, mean, color, label=label, alpha=0.7) 727 if show_ci: 728 ax.fill_between( 729 df_internal.index, lower, upper, color=color, alpha=0.2 730 ) 731 else: 732 # 生データのプロット 733 ax.plot( 734 df_internal.index, 735 df_internal[flux_col], 736 color, 737 label=label, 738 alpha=0.7, 739 ) 740 741 # プロットの設定 742 if subplot_label: 743 ax.text( 744 0.02, 745 0.98, 746 subplot_label, 747 transform=ax.transAxes, 748 va="top", 749 fontsize=subplot_fontsize, 750 ) 751 752 ax.set_xlabel(xlabel) 753 ax.set_ylabel(ylabel) 754 755 if ylim is not None: 756 ax.set_ylim(ylim) 757 758 ax.grid(True, alpha=0.3) 759 ax.legend(loc=legend_loc) 760 761 # x軸の設定 762 ax.set_xlim(float(mdates.date2num(start_dt)), float(mdates.date2num(end_dt))) 763 764 if x_interval == "month": 765 # 月初めにメジャー線のみ表示 766 ax.xaxis.set_major_locator(mdates.MonthLocator()) 767 ax.xaxis.set_minor_locator(NullLocator()) # マイナー線を非表示 768 elif x_interval == "10days": 769 # 月初め(1日)、10日、20日、30日に目盛りを表示 770 class Custom10DayLocator(mdates.DateLocator): 771 def __call__(self): 772 dmin, dmax = self.viewlim_to_dt() 773 dates = [] 774 current = pd.to_datetime(dmin).normalize() 775 end = pd.to_datetime(dmax).normalize() 776 777 while current <= end: 778 # その月の1日、10日、20日、30日を追加 779 for day in [1, 10, 20, 30]: 780 try: 781 date = current.replace(day=day) 782 if dmin <= date <= dmax: 783 dates.append(date) 784 except ValueError: 785 # 30日が存在しない月(2月など)の場合は 786 # その月の最終日を使用 787 if day == 30: 788 last_day = ( 789 current + pd.DateOffset(months=1) 790 ).replace(day=1) - pd.Timedelta(days=1) 791 if dmin <= last_day <= dmax: 792 dates.append(last_day) 793 794 # 次の月へ 795 current = (current + pd.DateOffset(months=1)).replace(day=1) 796 797 return self.raise_if_exceeds( 798 [float(mdates.date2num(date)) for date in dates] 799 ) 800 801 ax.xaxis.set_major_locator(Custom10DayLocator()) 802 ax.xaxis.set_minor_locator(mdates.DayLocator()) 803 ax.grid(True, which="minor", alpha=0.1) 804 805 # カスタムフォーマッタの作成 806 def date_formatter(x, p): 807 date = mdates.num2date(x) 808 if x_interval == "month": 809 # 月初めの1日の場合のみ月を表示 810 if date.day == 1: 811 return f"{date.strftime('%m')}" 812 return "" 813 else: # "10days"の場合 814 # MM/DD形式で表示し、/を中心に配置 815 month = f"{date.strftime('%m'):>2}" # 右寄せで2文字 816 day = f"{date.strftime('%d'):<2}" # 左寄せで2文字 817 return f"{month}/{day}" 818 819 ax.xaxis.set_major_formatter(FuncFormatter(date_formatter)) 820 plt.setp( 821 ax.xaxis.get_majorticklabels(), ha="center", rotation=0 822 ) # 中央揃えに変更 823 824 plt.tight_layout() 825 826 if save_fig: 827 if output_dirpath is None: 828 raise ValueError( 829 "save_fig = True のとき、 output_dirpath に有効なディレクトリパスを指定する必要があります。" 830 ) 831 # 出力ディレクトリの作成 832 os.makedirs(output_dirpath, exist_ok=True) 833 output_filepath: str = os.path.join(output_dirpath, output_filename) 834 plt.savefig(output_filepath, dpi=dpi, bbox_inches="tight") 835 if show_fig: 836 plt.show() 837 plt.close(fig=fig)
複数のCH4フラックスの時系列変動を比較するプロットを作成します。
Parameters
df: pd.DataFrame
プロットするデータを含むDataFrameを指定します。
cols_flux: list[str]
比較するフラックスのカラム名のリストを指定します。
labels: list[str]
凡例に表示する各フラックスのラベルのリストを指定します。
colors: list[str]
各フラックスの色のリストを指定します。
output_dirpath: str | Path | None
出力ディレクトリのパスを指定します。save_fig=Trueの場合は必須です。
output_filename: str, optional
出力ファイル名を指定します。デフォルト値は"ch4_flux_comparison.png"です。
col_datetime: str, optional
日時カラムの名前を指定します。デフォルト値は"Date"です。
window_size: int, optional
移動平均の窓サイズを指定します。デフォルト値は24*7(1週間)です。
confidence_interval: float, optional
信頼区間を指定します。0から1の間の値で、デフォルト値は0.95(95%)です。
subplot_label: str | None, optional
プロットのラベルを指定します。デフォルト値はNoneです。
subplot_fontsize: int, optional
サブプロットのフォントサイズを指定します。デフォルト値は20です。
show_ci: bool, optional
信頼区間を表示するかどうかを指定します。デフォルト値はTrueです。
ylim: tuple[float, float] | None, optional
y軸の範囲を指定します。デフォルト値はNoneです。
start_date: str | None, optional
開始日をYYYY-MM-DD形式で指定します。デフォルト値はNoneです。
end_date: str | None, optional
終了日をYYYY-MM-DD形式で指定します。デフォルト値はNoneです。
include_end_date: bool, optional
終了日を含めるかどうかを指定します。デフォルト値はTrueです。
legend_loc: str, optional
凡例の位置を指定します。デフォルト値は"upper right"です。
apply_ma: bool, optional
移動平均を適用するかどうかを指定します。デフォルト値はTrueです。
hourly_mean: bool, optional
1時間平均を適用するかどうかを指定します。デフォルト値はFalseです。
x_interval: Literal["month", "10days"], optional
x軸の目盛り間隔を指定します。"month"(月初めのみ)または"10days"(10日刻み)を指定できます。デフォルト値は"month"です。
xlabel: str, optional
x軸のラベルを指定します。デフォルト値は"Month"です。
ylabel: str, optional
y軸のラベルを指定します。デフォルト値は"CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)"です。
figsize: tuple[float, float], optional
プロットのサイズを指定します。デフォルト値は(12, 6)です。
dpi: float | None, optional
プロットのdpiを指定します。デフォルト値は350です。
save_fig: bool, optional
図を保存するかどうかを指定します。デフォルト値はTrueです。
show_fig: bool, optional
図を表示するかどうかを指定します。デフォルト値はTrueです。
Examples
>>> generator = MonthlyFiguresGenerator()
>>> generator.plot_fluxes_comparison(
... df=monthly_data,
... cols_flux=["Fch4_ultra", "Fch4_picarro"],
... labels=["Ultra", "Picarro"],
... colors=["red", "blue"],
... output_dirpath="output",
... start_date="2023-01-01",
... end_date="2023-12-31"
... )
839 def plot_c1c2_fluxes_diurnal_patterns( 840 self, 841 df: pd.DataFrame, 842 y_cols_ch4: list[str], 843 y_cols_c2h6: list[str], 844 labels_ch4: list[str], 845 labels_c2h6: list[str], 846 colors_ch4: list[str], 847 colors_c2h6: list[str], 848 output_dirpath: str | Path | None = None, 849 output_filename: str = "diurnal.png", 850 legend_only_ch4: bool = False, 851 add_label: bool = True, 852 add_legend: bool = True, 853 show_std: bool = False, 854 std_alpha: float = 0.2, 855 figsize: tuple[float, float] = (12, 5), 856 dpi: float | None = 350, 857 subplot_fontsize: int = 20, 858 subplot_label_ch4: str | None = "(a)", 859 subplot_label_c2h6: str | None = "(b)", 860 ax1_ylim: tuple[float, float] | None = None, 861 ax2_ylim: tuple[float, float] | None = None, 862 save_fig: bool = True, 863 show_fig: bool = True, 864 ) -> None: 865 """CH4とC2H6の日変化パターンを1つの図に並べてプロットします。 866 867 Parameters 868 ---------- 869 df: pd.DataFrame 870 入力データフレームを指定します。 871 y_cols_ch4: list[str] 872 CH4のプロットに使用するカラム名のリストを指定します。 873 y_cols_c2h6: list[str] 874 C2H6のプロットに使用するカラム名のリストを指定します。 875 labels_ch4: list[str] 876 CH4の各ラインに対応するラベルのリストを指定します。 877 labels_c2h6: list[str] 878 C2H6の各ラインに対応するラベルのリストを指定します。 879 colors_ch4: list[str] 880 CH4の各ラインに使用する色のリストを指定します。 881 colors_c2h6: list[str] 882 C2H6の各ラインに使用する色のリストを指定します。 883 output_dirpath: str | Path | None, optional 884 出力先ディレクトリのパスを指定します。save_fig=Trueの場合は必須です。 885 output_filename: str, optional 886 出力ファイル名を指定します。デフォルト値は"diurnal.png"です。 887 legend_only_ch4: bool, optional 888 CH4の凡例のみを表示するかどうかを指定します。デフォルト値はFalseです。 889 add_label: bool, optional 890 サブプロットラベルを表示するかどうかを指定します。デフォルト値はTrueです。 891 add_legend: bool, optional 892 凡例を表示するかどうかを指定します。デフォルト値はTrueです。 893 show_std: bool, optional 894 標準偏差を表示するかどうかを指定します。デフォルト値はFalseです。 895 std_alpha: float, optional 896 標準偏差の透明度を指定します。デフォルト値は0.2です。 897 figsize: tuple[float, float], optional 898 プロットのサイズを指定します。デフォルト値は(12, 5)です。 899 dpi: float | None, optional 900 プロットのdpiを指定します。デフォルト値は350です。 901 subplot_fontsize: int, optional 902 サブプロットのフォントサイズを指定します。デフォルト値は20です。 903 subplot_label_ch4: str | None, optional 904 CH4プロットのラベルを指定します。デフォルト値は"(a)"です。 905 subplot_label_c2h6: str | None, optional 906 C2H6プロットのラベルを指定します。デフォルト値は"(b)"です。 907 ax1_ylim: tuple[float, float] | None, optional 908 CH4プロットのy軸の範囲を指定します。デフォルト値はNoneです。 909 ax2_ylim: tuple[float, float] | None, optional 910 C2H6プロットのy軸の範囲を指定します。デフォルト値はNoneです。 911 save_fig: bool, optional 912 プロットを保存するかどうかを指定します。デフォルト値はTrueです。 913 show_fig: bool, optional 914 プロットを表示するかどうかを指定します。デフォルト値はTrueです。 915 916 Examples 917 -------- 918 >>> generator = MonthlyFiguresGenerator() 919 >>> generator.plot_c1c2_fluxes_diurnal_patterns( 920 ... df=monthly_data, 921 ... y_cols_ch4=["CH4_flux1", "CH4_flux2"], 922 ... y_cols_c2h6=["C2H6_flux1", "C2H6_flux2"], 923 ... labels_ch4=["CH4 1", "CH4 2"], 924 ... labels_c2h6=["C2H6 1", "C2H6 2"], 925 ... colors_ch4=["red", "blue"], 926 ... colors_c2h6=["green", "orange"], 927 ... output_dirpath="output", 928 ... show_std=True 929 ... ) 930 """ 931 # データの準備 932 df_internal: pd.DataFrame = df.copy() 933 df_internal.index = pd.to_datetime(df_internal.index) 934 target_columns = y_cols_ch4 + y_cols_c2h6 935 hourly_means, time_points = self._prepare_diurnal_data( 936 df_internal, target_columns 937 ) 938 939 # 標準偏差の計算を追加 940 hourly_stds = {} 941 if show_std: 942 hourly_stds = df_internal.groupby(df_internal.index.hour)[ 943 target_columns 944 ].std() 945 # 24時間目のデータ点を追加 946 last_hour = hourly_stds.iloc[0:1].copy() 947 last_hour.index = pd.Index([24]) 948 hourly_stds = pd.concat([hourly_stds, last_hour]) 949 950 # プロットの作成 951 fig, (ax1, ax2) = plt.subplots(1, 2, figsize=figsize) 952 953 # CH4のプロット (左側) 954 ch4_lines = [] 955 for col_y, label, color in zip(y_cols_ch4, labels_ch4, colors_ch4, strict=True): 956 mean_values = hourly_means["all"][col_y] 957 line = ax1.plot( 958 time_points, 959 mean_values, 960 "-o", 961 label=label, 962 color=color, 963 ) 964 ch4_lines.extend(line) 965 966 # 標準偏差の表示 967 if show_std: 968 std_values = hourly_stds[col_y] 969 ax1.fill_between( 970 time_points, 971 mean_values - std_values, 972 mean_values + std_values, 973 color=color, 974 alpha=std_alpha, 975 ) 976 977 # C2H6のプロット (右側) 978 c2h6_lines = [] 979 for col_y, label, color in zip( 980 y_cols_c2h6, labels_c2h6, colors_c2h6, strict=True 981 ): 982 mean_values = hourly_means["all"][col_y] 983 line = ax2.plot( 984 time_points, 985 mean_values, 986 "o-", 987 label=label, 988 color=color, 989 ) 990 c2h6_lines.extend(line) 991 992 # 標準偏差の表示 993 if show_std: 994 std_values = hourly_stds[col_y] 995 ax2.fill_between( 996 time_points, 997 mean_values - std_values, 998 mean_values + std_values, 999 color=color, 1000 alpha=std_alpha, 1001 ) 1002 1003 # 軸の設定 1004 for ax, ylabel, subplot_label in [ 1005 (ax1, r"CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)", subplot_label_ch4), 1006 (ax2, r"C$_2$H$_6$ flux (nmol m$^{-2}$ s$^{-1}$)", subplot_label_c2h6), 1007 ]: 1008 self._setup_diurnal_axes( 1009 ax=ax, 1010 time_points=time_points, 1011 ylabel=ylabel, 1012 subplot_label=subplot_label, 1013 add_label=add_label, 1014 add_legend=False, # 個別の凡例は表示しない 1015 subplot_fontsize=subplot_fontsize, 1016 ) 1017 1018 if ax1_ylim is not None: 1019 ax1.set_ylim(ax1_ylim) 1020 ax1.yaxis.set_major_locator(MultipleLocator(20)) 1021 ax1.yaxis.set_major_formatter(FuncFormatter(lambda x, p: f"{x:.0f}")) 1022 1023 if ax2_ylim is not None: 1024 ax2.set_ylim(ax2_ylim) 1025 ax2.yaxis.set_major_locator(MultipleLocator(1)) 1026 ax2.yaxis.set_major_formatter(FuncFormatter(lambda x, p: f"{x:.1f}")) 1027 1028 plt.tight_layout() 1029 1030 # 共通の凡例 1031 if add_legend: 1032 all_lines = ch4_lines 1033 all_labels = [line.get_label() for line in ch4_lines] 1034 if not legend_only_ch4: 1035 all_lines += c2h6_lines 1036 all_labels += [line.get_label() for line in c2h6_lines] 1037 fig.legend( 1038 all_lines, 1039 all_labels, 1040 loc="center", 1041 bbox_to_anchor=(0.5, 0.02), 1042 ncol=len(all_lines), 1043 ) 1044 plt.subplots_adjust(bottom=0.25) # 下部に凡例用のスペースを確保 1045 1046 if save_fig: 1047 if output_dirpath is None: 1048 raise ValueError( 1049 "save_fig = True の場合、 output_dirpath を指定する必要があります。有効なディレクトリパスを指定してください。" 1050 ) 1051 os.makedirs(output_dirpath, exist_ok=True) 1052 output_filepath: str = os.path.join(output_dirpath, output_filename) 1053 fig.savefig(output_filepath, dpi=dpi, bbox_inches="tight") 1054 if show_fig: 1055 plt.show() 1056 plt.close(fig=fig)
CH4とC2H6の日変化パターンを1つの図に並べてプロットします。
Parameters
df: pd.DataFrame
入力データフレームを指定します。
y_cols_ch4: list[str]
CH4のプロットに使用するカラム名のリストを指定します。
y_cols_c2h6: list[str]
C2H6のプロットに使用するカラム名のリストを指定します。
labels_ch4: list[str]
CH4の各ラインに対応するラベルのリストを指定します。
labels_c2h6: list[str]
C2H6の各ラインに対応するラベルのリストを指定します。
colors_ch4: list[str]
CH4の各ラインに使用する色のリストを指定します。
colors_c2h6: list[str]
C2H6の各ラインに使用する色のリストを指定します。
output_dirpath: str | Path | None, optional
出力先ディレクトリのパスを指定します。save_fig=Trueの場合は必須です。
output_filename: str, optional
出力ファイル名を指定します。デフォルト値は"diurnal.png"です。
legend_only_ch4: bool, optional
CH4の凡例のみを表示するかどうかを指定します。デフォルト値はFalseです。
add_label: bool, optional
サブプロットラベルを表示するかどうかを指定します。デフォルト値はTrueです。
add_legend: bool, optional
凡例を表示するかどうかを指定します。デフォルト値はTrueです。
show_std: bool, optional
標準偏差を表示するかどうかを指定します。デフォルト値はFalseです。
std_alpha: float, optional
標準偏差の透明度を指定します。デフォルト値は0.2です。
figsize: tuple[float, float], optional
プロットのサイズを指定します。デフォルト値は(12, 5)です。
dpi: float | None, optional
プロットのdpiを指定します。デフォルト値は350です。
subplot_fontsize: int, optional
サブプロットのフォントサイズを指定します。デフォルト値は20です。
subplot_label_ch4: str | None, optional
CH4プロットのラベルを指定します。デフォルト値は"(a)"です。
subplot_label_c2h6: str | None, optional
C2H6プロットのラベルを指定します。デフォルト値は"(b)"です。
ax1_ylim: tuple[float, float] | None, optional
CH4プロットのy軸の範囲を指定します。デフォルト値はNoneです。
ax2_ylim: tuple[float, float] | None, optional
C2H6プロットのy軸の範囲を指定します。デフォルト値はNoneです。
save_fig: bool, optional
プロットを保存するかどうかを指定します。デフォルト値はTrueです。
show_fig: bool, optional
プロットを表示するかどうかを指定します。デフォルト値はTrueです。
Examples
>>> generator = MonthlyFiguresGenerator()
>>> generator.plot_c1c2_fluxes_diurnal_patterns(
... df=monthly_data,
... y_cols_ch4=["CH4_flux1", "CH4_flux2"],
... y_cols_c2h6=["C2H6_flux1", "C2H6_flux2"],
... labels_ch4=["CH4 1", "CH4 2"],
... labels_c2h6=["C2H6 1", "C2H6 2"],
... colors_ch4=["red", "blue"],
... colors_c2h6=["green", "orange"],
... output_dirpath="output",
... show_std=True
... )
1058 def plot_c1c2_fluxes_diurnal_patterns_by_date( 1059 self, 1060 df: pd.DataFrame, 1061 y_col_ch4: str, 1062 y_col_c2h6: str, 1063 output_dirpath: str | Path | None = None, 1064 output_filename: str = "diurnal_by_date.png", 1065 plot_all: bool = True, 1066 plot_weekday: bool = True, 1067 plot_weekend: bool = True, 1068 plot_holiday: bool = True, 1069 add_label: bool = True, 1070 add_legend: bool = True, 1071 show_std: bool = False, 1072 std_alpha: float = 0.2, 1073 legend_only_ch4: bool = False, 1074 subplot_fontsize: int = 20, 1075 subplot_label_ch4: str | None = "(a)", 1076 subplot_label_c2h6: str | None = "(b)", 1077 ax1_ylim: tuple[float, float] | None = None, 1078 ax2_ylim: tuple[float, float] | None = None, 1079 figsize: tuple[float, float] = (12, 5), 1080 dpi: float | None = 350, 1081 save_fig: bool = True, 1082 show_fig: bool = True, 1083 print_summary: bool = False, 1084 ) -> None: 1085 """CH4とC2H6の日変化パターンを日付分類して1つの図に並べてプロットします。 1086 1087 Parameters 1088 ---------- 1089 df: pd.DataFrame 1090 入力データフレームを指定します。 1091 y_col_ch4: str 1092 CH4フラックスを含むカラム名を指定します。 1093 y_col_c2h6: str 1094 C2H6フラックスを含むカラム名を指定します。 1095 output_dirpath: str | Path | None, optional 1096 出力先ディレクトリのパスを指定します。save_fig=Trueの場合は必須です。 1097 output_filename: str, optional 1098 出力ファイル名を指定します。デフォルト値は"diurnal_by_date.png"です。 1099 plot_all: bool, optional 1100 すべての日をプロットするかどうかを指定します。デフォルト値はTrueです。 1101 plot_weekday: bool, optional 1102 平日をプロットするかどうかを指定します。デフォルト値はTrueです。 1103 plot_weekend: bool, optional 1104 週末をプロットするかどうかを指定します。デフォルト値はTrueです。 1105 plot_holiday: bool, optional 1106 祝日をプロットするかどうかを指定します。デフォルト値はTrueです。 1107 add_label: bool, optional 1108 サブプロットラベルを表示するかどうかを指定します。デフォルト値はTrueです。 1109 add_legend: bool, optional 1110 凡例を表示するかどうかを指定します。デフォルト値はTrueです。 1111 show_std: bool, optional 1112 標準偏差を表示するかどうかを指定します。デフォルト値はFalseです。 1113 std_alpha: float, optional 1114 標準偏差の透明度を指定します。デフォルト値は0.2です。 1115 legend_only_ch4: bool, optional 1116 CH4の凡例のみを表示するかどうかを指定します。デフォルト値はFalseです。 1117 subplot_fontsize: int, optional 1118 サブプロットのフォントサイズを指定します。デフォルト値は20です。 1119 subplot_label_ch4: str | None, optional 1120 CH4プロットのラベルを指定します。デフォルト値は"(a)"です。 1121 subplot_label_c2h6: str | None, optional 1122 C2H6プロットのラベルを指定します。デフォルト値は"(b)"です。 1123 ax1_ylim: tuple[float, float] | None, optional 1124 CH4プロットのy軸の範囲を指定します。デフォルト値はNoneです。 1125 ax2_ylim: tuple[float, float] | None, optional 1126 C2H6プロットのy軸の範囲を指定します。デフォルト値はNoneです。 1127 figsize: tuple[float, float], optional 1128 プロットのサイズを指定します。デフォルト値は(12, 5)です。 1129 dpi: float | None, optional 1130 プロットのdpiを指定します。デフォルト値は350です。 1131 save_fig: bool, optional 1132 プロットを保存するかどうかを指定します。デフォルト値はTrueです。 1133 show_fig: bool, optional 1134 プロットを表示するかどうかを指定します。デフォルト値はTrueです。 1135 print_summary: bool, optional 1136 統計情報を表示するかどうかを指定します。デフォルト値はFalseです。 1137 1138 Examples 1139 ------- 1140 >>> generator = MonthlyFiguresGenerator() 1141 >>> generator.plot_c1c2_fluxes_diurnal_patterns_by_date( 1142 ... df=monthly_data, 1143 ... y_col_ch4="CH4_flux", 1144 ... y_col_c2h6="C2H6_flux", 1145 ... output_dirpath="output", 1146 ... show_std=True, 1147 ... print_summary=True 1148 ... ) 1149 """ 1150 # データの準備 1151 df_internal: pd.DataFrame = df.copy() 1152 df_internal.index = pd.to_datetime(df_internal.index) 1153 target_columns = [y_col_ch4, y_col_c2h6] 1154 hourly_means, time_points = self._prepare_diurnal_data( 1155 df_internal, target_columns, include_date_types=True 1156 ) 1157 1158 # 標準偏差の計算を追加 1159 hourly_stds = {} 1160 if show_std: 1161 for condition in ["all", "weekday", "weekend", "holiday"]: 1162 if condition == "all": 1163 condition_data = df_internal 1164 elif condition == "weekday": 1165 condition_data = df_internal[ 1166 ~( 1167 df_internal.index.dayofweek.isin([5, 6]) 1168 | df_internal.index.map( 1169 lambda x: jpholiday.is_holiday(x.date()) 1170 ) 1171 ) 1172 ] 1173 elif condition == "weekend": 1174 condition_data = df_internal[ 1175 df_internal.index.dayofweek.isin([5, 6]) 1176 ] 1177 else: # holiday 1178 condition_data = df_internal[ 1179 df_internal.index.map(lambda x: jpholiday.is_holiday(x.date())) 1180 ] 1181 1182 hourly_stds[condition] = condition_data.groupby( 1183 pd.to_datetime(condition_data.index).hour 1184 )[target_columns].std() 1185 # 24時間目のデータ点を追加 1186 last_hour = hourly_stds[condition].iloc[0:1].copy() 1187 last_hour.index = [24] 1188 hourly_stds[condition] = pd.concat([hourly_stds[condition], last_hour]) 1189 1190 # プロットスタイルの設定 1191 styles = { 1192 "all": { 1193 "color": "black", 1194 "linestyle": "-", 1195 "alpha": 1.0, 1196 "label": "All days", 1197 }, 1198 "weekday": { 1199 "color": "blue", 1200 "linestyle": "-", 1201 "alpha": 0.8, 1202 "label": "Weekdays", 1203 }, 1204 "weekend": { 1205 "color": "red", 1206 "linestyle": "-", 1207 "alpha": 0.8, 1208 "label": "Weekends", 1209 }, 1210 "holiday": { 1211 "color": "green", 1212 "linestyle": "-", 1213 "alpha": 0.8, 1214 "label": "Weekends & Holidays", 1215 }, 1216 } 1217 1218 # プロット対象の条件を選択 1219 plot_conditions = { 1220 "all": plot_all, 1221 "weekday": plot_weekday, 1222 "weekend": plot_weekend, 1223 "holiday": plot_holiday, 1224 } 1225 selected_conditions = { 1226 col: means 1227 for col, means in hourly_means.items() 1228 if plot_conditions.get(col) 1229 } 1230 1231 # プロットの作成 1232 fig, (ax1, ax2) = plt.subplots(1, 2, figsize=figsize) 1233 1234 # CH4とC2H6のプロット用のラインオブジェクトを保存 1235 ch4_lines = [] 1236 c2h6_lines = [] 1237 1238 # CH4とC2H6のプロット 1239 for condition, means in selected_conditions.items(): 1240 style = styles[condition].copy() 1241 1242 # CH4プロット 1243 mean_values_ch4 = means[y_col_ch4] 1244 line_ch4 = ax1.plot(time_points, mean_values_ch4, marker="o", **style) 1245 ch4_lines.extend(line_ch4) 1246 1247 if show_std and condition in hourly_stds: 1248 std_values = hourly_stds[condition][y_col_ch4] 1249 ax1.fill_between( 1250 time_points, 1251 mean_values_ch4 - std_values, 1252 mean_values_ch4 + std_values, 1253 color=style["color"], 1254 alpha=std_alpha, 1255 ) 1256 1257 # C2H6プロット 1258 style["linestyle"] = "--" 1259 mean_values_c2h6 = means[y_col_c2h6] 1260 line_c2h6 = ax2.plot(time_points, mean_values_c2h6, marker="o", **style) 1261 c2h6_lines.extend(line_c2h6) 1262 1263 if show_std and condition in hourly_stds: 1264 std_values = hourly_stds[condition][y_col_c2h6] 1265 ax2.fill_between( 1266 time_points, 1267 mean_values_c2h6 - std_values, 1268 mean_values_c2h6 + std_values, 1269 color=style["color"], 1270 alpha=std_alpha, 1271 ) 1272 1273 # 軸の設定 1274 for ax, ylabel, subplot_label in [ 1275 (ax1, r"CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)", subplot_label_ch4), 1276 (ax2, r"C$_2$H$_6$ flux (nmol m$^{-2}$ s$^{-1}$)", subplot_label_c2h6), 1277 ]: 1278 self._setup_diurnal_axes( 1279 ax=ax, 1280 time_points=time_points, 1281 ylabel=ylabel, 1282 subplot_label=subplot_label, 1283 add_label=add_label, 1284 add_legend=False, 1285 subplot_fontsize=subplot_fontsize, 1286 ) 1287 1288 if ax1_ylim is not None: 1289 ax1.set_ylim(ax1_ylim) 1290 ax1.yaxis.set_major_locator(MultipleLocator(20)) 1291 ax1.yaxis.set_major_formatter(FuncFormatter(lambda x, p: f"{x:.0f}")) 1292 1293 if ax2_ylim is not None: 1294 ax2.set_ylim(ax2_ylim) 1295 ax2.yaxis.set_major_locator(MultipleLocator(1)) 1296 ax2.yaxis.set_major_formatter(FuncFormatter(lambda x, p: f"{x:.1f}")) 1297 1298 plt.tight_layout() 1299 1300 # 共通の凡例を図の下部に配置 1301 if add_legend: 1302 lines_to_show = ( 1303 ch4_lines if legend_only_ch4 else ch4_lines[: len(selected_conditions)] 1304 ) 1305 fig.legend( 1306 lines_to_show, 1307 [ 1308 style["label"] 1309 for style in list(styles.values())[: len(lines_to_show)] 1310 ], 1311 loc="center", 1312 bbox_to_anchor=(0.5, 0.02), 1313 ncol=len(lines_to_show), 1314 ) 1315 plt.subplots_adjust(bottom=0.25) # 下部に凡例用のスペースを確保 1316 1317 if save_fig: 1318 if output_dirpath is None: 1319 raise ValueError( 1320 "save_fig = True の場合、 output_dirpath を指定する必要があります。有効なディレクトリパスを指定してください。" 1321 ) 1322 os.makedirs(output_dirpath, exist_ok=True) 1323 output_filepath: str = os.path.join(output_dirpath, output_filename) 1324 fig.savefig(output_filepath, dpi=dpi, bbox_inches="tight") 1325 if show_fig: 1326 plt.show() 1327 plt.close(fig=fig) 1328 1329 # 日変化パターンの統計分析を追加 1330 if print_summary: 1331 # 平日と休日のデータを準備 1332 dates = pd.to_datetime(df_internal.index) 1333 is_weekend = dates.dayofweek.isin([5, 6]) 1334 is_holiday = dates.map(lambda x: jpholiday.is_holiday(x.date())) 1335 is_weekday = ~(is_weekend | is_holiday) 1336 1337 weekday_data = df_internal[is_weekday] 1338 holiday_data = df_internal[is_weekend | is_holiday] 1339 1340 def get_diurnal_stats(data, column): 1341 # 時間ごとの平均値を計算 1342 hourly_means = data.groupby(data.index.hour)[column].mean() 1343 1344 # 8-16時の時間帯の統計 1345 daytime_means = hourly_means[ 1346 (hourly_means.index >= 8) & (hourly_means.index <= 16) 1347 ] 1348 1349 if len(daytime_means) == 0: 1350 return None 1351 1352 return { 1353 "mean": daytime_means.mean(), 1354 "max": daytime_means.max(), 1355 "max_hour": daytime_means.idxmax(), 1356 "min": daytime_means.min(), 1357 "min_hour": daytime_means.idxmin(), 1358 "hours_count": len(daytime_means), 1359 } 1360 1361 # CH4とC2H6それぞれの統計を計算 1362 for col, gas_name in [(y_col_ch4, "CH4"), (y_col_c2h6, "C2H6")]: 1363 print(f"\n=== {gas_name} フラックス 8-16時の統計分析 ===") 1364 1365 weekday_stats = get_diurnal_stats(weekday_data, col) 1366 holiday_stats = get_diurnal_stats(holiday_data, col) 1367 1368 if weekday_stats and holiday_stats: 1369 print("\n平日:") 1370 print(f" 平均値: {weekday_stats['mean']:.2f}") 1371 print( 1372 f" 最大値: {weekday_stats['max']:.2f} ({weekday_stats['max_hour']}時)" 1373 ) 1374 print( 1375 f" 最小値: {weekday_stats['min']:.2f} ({weekday_stats['min_hour']}時)" 1376 ) 1377 print(f" 集計時間数: {weekday_stats['hours_count']}") 1378 1379 print("\n休日:") 1380 print(f" 平均値: {holiday_stats['mean']:.2f}") 1381 print( 1382 f" 最大値: {holiday_stats['max']:.2f} ({holiday_stats['max_hour']}時)" 1383 ) 1384 print( 1385 f" 最小値: {holiday_stats['min']:.2f} ({holiday_stats['min_hour']}時)" 1386 ) 1387 print(f" 集計時間数: {holiday_stats['hours_count']}") 1388 1389 # 平日/休日の比率を計算 1390 print("\n平日/休日の比率:") 1391 print( 1392 f" 平均値比: {weekday_stats['mean'] / holiday_stats['mean']:.2f}" 1393 ) 1394 print( 1395 f" 最大値比: {weekday_stats['max'] / holiday_stats['max']:.2f}" 1396 ) 1397 print( 1398 f" 最小値比: {weekday_stats['min'] / holiday_stats['min']:.2f}" 1399 ) 1400 else: 1401 print("十分なデータがありません")
CH4とC2H6の日変化パターンを日付分類して1つの図に並べてプロットします。
Parameters
df: pd.DataFrame
入力データフレームを指定します。
y_col_ch4: str
CH4フラックスを含むカラム名を指定します。
y_col_c2h6: str
C2H6フラックスを含むカラム名を指定します。
output_dirpath: str | Path | None, optional
出力先ディレクトリのパスを指定します。save_fig=Trueの場合は必須です。
output_filename: str, optional
出力ファイル名を指定します。デフォルト値は"diurnal_by_date.png"です。
plot_all: bool, optional
すべての日をプロットするかどうかを指定します。デフォルト値はTrueです。
plot_weekday: bool, optional
平日をプロットするかどうかを指定します。デフォルト値はTrueです。
plot_weekend: bool, optional
週末をプロットするかどうかを指定します。デフォルト値はTrueです。
plot_holiday: bool, optional
祝日をプロットするかどうかを指定します。デフォルト値はTrueです。
add_label: bool, optional
サブプロットラベルを表示するかどうかを指定します。デフォルト値はTrueです。
add_legend: bool, optional
凡例を表示するかどうかを指定します。デフォルト値はTrueです。
show_std: bool, optional
標準偏差を表示するかどうかを指定します。デフォルト値はFalseです。
std_alpha: float, optional
標準偏差の透明度を指定します。デフォルト値は0.2です。
legend_only_ch4: bool, optional
CH4の凡例のみを表示するかどうかを指定します。デフォルト値はFalseです。
subplot_fontsize: int, optional
サブプロットのフォントサイズを指定します。デフォルト値は20です。
subplot_label_ch4: str | None, optional
CH4プロットのラベルを指定します。デフォルト値は"(a)"です。
subplot_label_c2h6: str | None, optional
C2H6プロットのラベルを指定します。デフォルト値は"(b)"です。
ax1_ylim: tuple[float, float] | None, optional
CH4プロットのy軸の範囲を指定します。デフォルト値はNoneです。
ax2_ylim: tuple[float, float] | None, optional
C2H6プロットのy軸の範囲を指定します。デフォルト値はNoneです。
figsize: tuple[float, float], optional
プロットのサイズを指定します。デフォルト値は(12, 5)です。
dpi: float | None, optional
プロットのdpiを指定します。デフォルト値は350です。
save_fig: bool, optional
プロットを保存するかどうかを指定します。デフォルト値はTrueです。
show_fig: bool, optional
プロットを表示するかどうかを指定します。デフォルト値はTrueです。
print_summary: bool, optional
統計情報を表示するかどうかを指定します。デフォルト値はFalseです。
Examples
>>> generator = MonthlyFiguresGenerator()
>>> generator.plot_c1c2_fluxes_diurnal_patterns_by_date(
... df=monthly_data,
... y_col_ch4="CH4_flux",
... y_col_c2h6="C2H6_flux",
... output_dirpath="output",
... show_std=True,
... print_summary=True
... )
1403 def plot_diurnal_concentrations( 1404 self, 1405 df: pd.DataFrame, 1406 col_ch4_conc: str = "CH4_ultra_cal", 1407 col_c2h6_conc: str = "C2H6_ultra_cal", 1408 col_datetime: str = "Date", 1409 output_dirpath: str | Path | None = None, 1410 output_filename: str = "diurnal_concentrations.png", 1411 show_std: bool = True, 1412 alpha_std: float = 0.2, 1413 add_legend: bool = True, 1414 print_summary: bool = False, 1415 subplot_label_ch4: str | None = None, 1416 subplot_label_c2h6: str | None = None, 1417 subplot_fontsize: int = 24, 1418 ch4_ylim: tuple[float, float] | None = None, 1419 c2h6_ylim: tuple[float, float] | None = None, 1420 interval: Literal["30min", "1H"] = "1H", 1421 figsize: tuple[float, float] = (12, 5), 1422 dpi: float | None = 350, 1423 save_fig: bool = True, 1424 show_fig: bool = True, 1425 ) -> None: 1426 """CH4とC2H6の濃度の日内変動を描画します。 1427 1428 Parameters 1429 ---------- 1430 df: pd.DataFrame 1431 濃度データを含むDataFrameを指定します。 1432 col_ch4_conc: str, optional 1433 CH4濃度のカラム名を指定します。デフォルト値は"CH4_ultra_cal"です。 1434 col_c2h6_conc: str, optional 1435 C2H6濃度のカラム名を指定します。デフォルト値は"C2H6_ultra_cal"です。 1436 col_datetime: str, optional 1437 日時カラム名を指定します。デフォルト値は"Date"です。 1438 output_dirpath: str | Path | None, optional 1439 出力ディレクトリのパスを指定します。save_fig=Trueの場合は必須です。 1440 output_filename: str, optional 1441 出力ファイル名を指定します。デフォルト値は"diurnal_concentrations.png"です。 1442 show_std: bool, optional 1443 標準偏差を表示するかどうかを指定します。デフォルト値はTrueです。 1444 alpha_std: float, optional 1445 標準偏差の透明度を指定します。デフォルト値は0.2です。 1446 add_legend: bool, optional 1447 凡例を追加するかどうかを指定します。デフォルト値はTrueです。 1448 print_summary: bool, optional 1449 統計情報を表示するかどうかを指定します。デフォルト値はFalseです。 1450 subplot_label_ch4: str | None, optional 1451 CH4プロットのラベルを指定します。デフォルト値はNoneです。 1452 subplot_label_c2h6: str | None, optional 1453 C2H6プロットのラベルを指定します。デフォルト値はNoneです。 1454 subplot_fontsize: int, optional 1455 サブプロットのフォントサイズを指定します。デフォルト値は24です。 1456 ch4_ylim: tuple[float, float] | None, optional 1457 CH4のy軸範囲を指定します。デフォルト値はNoneです。 1458 c2h6_ylim: tuple[float, float] | None, optional 1459 C2H6のy軸範囲を指定します。デフォルト値はNoneです。 1460 interval: Literal["30min", "1H"], optional 1461 時間間隔を指定します。"30min"または"1H"を指定できます。デフォルト値は"1H"です。 1462 figsize: tuple[float, float], optional 1463 プロットのサイズを指定します。デフォルト値は(12, 5)です。 1464 dpi: float | None, optional 1465 プロットのdpiを指定します。デフォルト値は350です。 1466 save_fig: bool, optional 1467 プロットを保存するかどうかを指定します。デフォルト値はTrueです。 1468 show_fig: bool, optional 1469 プロットを表示するかどうかを指定します。デフォルト値はTrueです。 1470 1471 Examples 1472 -------- 1473 >>> generator = MonthlyFiguresGenerator() 1474 >>> generator.plot_diurnal_concentrations( 1475 ... df=monthly_data, 1476 ... output_dirpath="output", 1477 ... show_std=True, 1478 ... interval="30min" 1479 ... ) 1480 """ 1481 # データの準備 1482 df_internal = df.copy() 1483 df_internal.index = pd.to_datetime(df_internal.index) 1484 if interval == "30min": 1485 # 30分間隔の場合、時間と30分を別々に取得 1486 df_internal["hour"] = pd.to_datetime(df_internal[col_datetime]).dt.hour 1487 df_internal["minute"] = pd.to_datetime(df_internal[col_datetime]).dt.minute 1488 df_internal["time_bin"] = df_internal["hour"] + df_internal["minute"].map( 1489 {0: 0, 30: 0.5} 1490 ) 1491 else: 1492 # 1時間間隔の場合 1493 df_internal["time_bin"] = pd.to_datetime(df_internal[col_datetime]).dt.hour 1494 1495 # 時間ごとの平均値と標準偏差を計算 1496 hourly_stats = df_internal.groupby("time_bin")[ 1497 [col_ch4_conc, col_c2h6_conc] 1498 ].agg(["mean", "std"]) 1499 1500 # 最後のデータポイントを追加(最初のデータを使用) 1501 last_point = hourly_stats.iloc[0:1].copy() 1502 last_point.index = pd.Index( 1503 [hourly_stats.index[-1] + (0.5 if interval == "30min" else 1)] 1504 ) 1505 hourly_stats = pd.concat([hourly_stats, last_point]) 1506 1507 # 時間軸の作成 1508 if interval == "30min": 1509 time_points = pd.date_range("2024-01-01", periods=49, freq="30min") 1510 x_ticks = [0, 6, 12, 18, 24] # 主要な時間のティック 1511 else: 1512 time_points = pd.date_range("2024-01-01", periods=25, freq="1H") 1513 x_ticks = [0, 6, 12, 18, 24] 1514 1515 # プロットの作成 1516 fig, (ax1, ax2) = plt.subplots(1, 2, figsize=figsize) 1517 1518 # CH4濃度プロット 1519 mean_ch4 = hourly_stats[col_ch4_conc]["mean"] 1520 if show_std: 1521 std_ch4 = hourly_stats[col_ch4_conc]["std"] 1522 ax1.fill_between( 1523 time_points, 1524 mean_ch4 - std_ch4, 1525 mean_ch4 + std_ch4, 1526 color="red", 1527 alpha=alpha_std, 1528 ) 1529 ch4_line = ax1.plot(time_points, mean_ch4, "red", label="CH$_4$")[0] 1530 1531 ax1.set_ylabel("CH$_4$ (ppm)") 1532 if ch4_ylim is not None: 1533 ax1.set_ylim(ch4_ylim) 1534 if subplot_label_ch4: 1535 ax1.text( 1536 0.02, 1537 0.98, 1538 subplot_label_ch4, 1539 transform=ax1.transAxes, 1540 va="top", 1541 fontsize=subplot_fontsize, 1542 ) 1543 1544 # C2H6濃度プロット 1545 mean_c2h6 = hourly_stats[col_c2h6_conc]["mean"] 1546 if show_std: 1547 std_c2h6 = hourly_stats[col_c2h6_conc]["std"] 1548 ax2.fill_between( 1549 time_points, 1550 mean_c2h6 - std_c2h6, 1551 mean_c2h6 + std_c2h6, 1552 color="orange", 1553 alpha=alpha_std, 1554 ) 1555 c2h6_line = ax2.plot(time_points, mean_c2h6, "orange", label="C$_2$H$_6$")[0] 1556 1557 ax2.set_ylabel("C$_2$H$_6$ (ppb)") 1558 if c2h6_ylim is not None: 1559 ax2.set_ylim(c2h6_ylim) 1560 if subplot_label_c2h6: 1561 ax2.text( 1562 0.02, 1563 0.98, 1564 subplot_label_c2h6, 1565 transform=ax2.transAxes, 1566 va="top", 1567 fontsize=subplot_fontsize, 1568 ) 1569 1570 # 両プロットの共通設定 1571 for ax in [ax1, ax2]: 1572 ax.set_xlabel("Time (hour)") 1573 ax.xaxis.set_major_formatter(mdates.DateFormatter("%-H")) 1574 ax.xaxis.set_major_locator(mdates.HourLocator(byhour=x_ticks)) 1575 ax.set_xlim(time_points[0], time_points[-1]) 1576 # 1時間ごとの縦線を表示 1577 ax.grid(True, which="major", alpha=0.3) 1578 1579 # 共通の凡例を図の下部に配置 1580 if add_legend: 1581 fig.legend( 1582 [ch4_line, c2h6_line], 1583 ["CH$_4$", "C$_2$H$_6$"], 1584 loc="center", 1585 bbox_to_anchor=(0.5, 0.02), 1586 ncol=2, 1587 ) 1588 plt.subplots_adjust(bottom=0.2) 1589 1590 plt.tight_layout() 1591 if save_fig: 1592 if output_dirpath is None: 1593 raise ValueError() 1594 # 出力ディレクトリの作成 1595 os.makedirs(output_dirpath, exist_ok=True) 1596 output_filepath: str = os.path.join(output_dirpath, output_filename) 1597 plt.savefig(output_filepath, dpi=dpi, bbox_inches="tight") 1598 if show_fig: 1599 plt.show() 1600 plt.close(fig=fig) 1601 1602 if print_summary: 1603 # 統計情報の表示 1604 for name, col in [("CH4", col_ch4_conc), ("C2H6", col_c2h6_conc)]: 1605 stats = hourly_stats[col] 1606 mean_vals = stats["mean"] 1607 1608 print(f"\n{name}濃度の日内変動統計:") 1609 print(f"最小値: {mean_vals.min():.3f} (Hour: {mean_vals.idxmin()})") 1610 print(f"最大値: {mean_vals.max():.3f} (Hour: {mean_vals.idxmax()})") 1611 print(f"平均値: {mean_vals.mean():.3f}") 1612 print(f"日内変動幅: {mean_vals.max() - mean_vals.min():.3f}") 1613 print(f"最大/最小比: {mean_vals.max() / mean_vals.min():.3f}")
CH4とC2H6の濃度の日内変動を描画します。
Parameters
df: pd.DataFrame
濃度データを含むDataFrameを指定します。
col_ch4_conc: str, optional
CH4濃度のカラム名を指定します。デフォルト値は"CH4_ultra_cal"です。
col_c2h6_conc: str, optional
C2H6濃度のカラム名を指定します。デフォルト値は"C2H6_ultra_cal"です。
col_datetime: str, optional
日時カラム名を指定します。デフォルト値は"Date"です。
output_dirpath: str | Path | None, optional
出力ディレクトリのパスを指定します。save_fig=Trueの場合は必須です。
output_filename: str, optional
出力ファイル名を指定します。デフォルト値は"diurnal_concentrations.png"です。
show_std: bool, optional
標準偏差を表示するかどうかを指定します。デフォルト値はTrueです。
alpha_std: float, optional
標準偏差の透明度を指定します。デフォルト値は0.2です。
add_legend: bool, optional
凡例を追加するかどうかを指定します。デフォルト値はTrueです。
print_summary: bool, optional
統計情報を表示するかどうかを指定します。デフォルト値はFalseです。
subplot_label_ch4: str | None, optional
CH4プロットのラベルを指定します。デフォルト値はNoneです。
subplot_label_c2h6: str | None, optional
C2H6プロットのラベルを指定します。デフォルト値はNoneです。
subplot_fontsize: int, optional
サブプロットのフォントサイズを指定します。デフォルト値は24です。
ch4_ylim: tuple[float, float] | None, optional
CH4のy軸範囲を指定します。デフォルト値はNoneです。
c2h6_ylim: tuple[float, float] | None, optional
C2H6のy軸範囲を指定します。デフォルト値はNoneです。
interval: Literal["30min", "1H"], optional
時間間隔を指定します。"30min"または"1H"を指定できます。デフォルト値は"1H"です。
figsize: tuple[float, float], optional
プロットのサイズを指定します。デフォルト値は(12, 5)です。
dpi: float | None, optional
プロットのdpiを指定します。デフォルト値は350です。
save_fig: bool, optional
プロットを保存するかどうかを指定します。デフォルト値はTrueです。
show_fig: bool, optional
プロットを表示するかどうかを指定します。デフォルト値はTrueです。
Examples
>>> generator = MonthlyFiguresGenerator()
>>> generator.plot_diurnal_concentrations(
... df=monthly_data,
... output_dirpath="output",
... show_std=True,
... interval="30min"
... )
1615 def plot_flux_diurnal_patterns_with_std( 1616 self, 1617 df: pd.DataFrame, 1618 col_ch4_flux: str = "Fch4", 1619 col_c2h6_flux: str = "Fc2h6", 1620 ch4_label: str = r"$\mathregular{CH_{4}}$フラックス", 1621 c2h6_label: str = r"$\mathregular{C_{2}H_{6}}$フラックス", 1622 col_datetime: str = "Date", 1623 output_dirpath: str | Path | None = None, 1624 output_filename: str = "diurnal_patterns.png", 1625 window_size: int = 6, 1626 show_std: bool = True, 1627 alpha_std: float = 0.1, 1628 figsize: tuple[float, float] = (12, 5), 1629 dpi: float | None = 350, 1630 save_fig: bool = True, 1631 show_fig: bool = True, 1632 print_summary: bool = False, 1633 ) -> None: 1634 """CH4とC2H6フラックスの日変化パターンをプロットします。 1635 1636 Parameters 1637 ---------- 1638 df: pd.DataFrame 1639 プロットするデータを含むDataFrameを指定します。 1640 col_ch4_flux: str, optional 1641 CH4フラックスのカラム名を指定します。デフォルト値は"Fch4"です。 1642 col_c2h6_flux: str, optional 1643 C2H6フラックスのカラム名を指定します。デフォルト値は"Fc2h6"です。 1644 ch4_label: str, optional 1645 CH4フラックスの凡例ラベルを指定します。デフォルト値は"CH4フラックス"です。 1646 c2h6_label: str, optional 1647 C2H6フラックスの凡例ラベルを指定します。デフォルト値は"C2H6フラックス"です。 1648 col_datetime: str, optional 1649 日時カラムの名前を指定します。デフォルト値は"Date"です。 1650 output_dirpath: str | Path | None, optional 1651 出力ディレクトリのパスを指定します。save_fig=Trueの場合は必須です。 1652 output_filename: str, optional 1653 出力ファイル名を指定します。デフォルト値は"diurnal_patterns.png"です。 1654 window_size: int, optional 1655 移動平均の窓サイズを指定します。デフォルト値は6です。 1656 show_std: bool, optional 1657 標準偏差を表示するかどうかを指定します。デフォルト値はTrueです。 1658 alpha_std: float, optional 1659 標準偏差の透明度を0-1の範囲で指定します。デフォルト値は0.1です。 1660 figsize: tuple[float, float], optional 1661 プロットのサイズを指定します。デフォルト値は(12, 5)です。 1662 dpi: float | None, optional 1663 プロットの解像度を指定します。デフォルト値は350です。 1664 save_fig: bool, optional 1665 プロットを保存するかどうかを指定します。デフォルト値はTrueです。 1666 show_fig: bool, optional 1667 プロットを表示するかどうかを指定します。デフォルト値はTrueです。 1668 print_summary: bool, optional 1669 統計情報を表示するかどうかを指定します。デフォルト値はFalseです。 1670 1671 Examples 1672 -------- 1673 >>> generator = MonthlyFiguresGenerator() 1674 >>> df = pd.read_csv("flux_data.csv") 1675 >>> generator.plot_flux_diurnal_patterns_with_std( 1676 ... df, 1677 ... col_ch4_flux="CH4_flux", 1678 ... col_c2h6_flux="C2H6_flux", 1679 ... output_dirpath="output", 1680 ... show_std=True 1681 ... ) 1682 """ 1683 # 日時インデックスの処理 1684 df_internal = df.copy() 1685 if not isinstance(df_internal.index, pd.DatetimeIndex): 1686 df_internal[col_datetime] = pd.to_datetime(df_internal[col_datetime]) 1687 df_internal.set_index(col_datetime, inplace=True) 1688 df_internal.index = pd.to_datetime(df_internal.index) 1689 # 時刻データの抽出とグループ化 1690 df_internal["hour"] = df_internal.index.hour 1691 hourly_means = df_internal.groupby("hour")[[col_ch4_flux, col_c2h6_flux]].agg( 1692 ["mean", "std"] 1693 ) 1694 1695 # 24時間目のデータ点を追加(0時のデータを使用) 1696 last_hour = hourly_means.iloc[0:1].copy() 1697 last_hour.index = pd.Index([24]) 1698 hourly_means = pd.concat([hourly_means, last_hour]) 1699 1700 # 24時間分のデータポイントを作成 1701 time_points = pd.date_range("2024-01-01", periods=25, freq="h") 1702 1703 # プロットの作成 1704 fig, (ax1, ax2) = plt.subplots(1, 2, figsize=figsize) 1705 1706 # 移動平均の計算と描画 1707 ch4_mean = ( 1708 hourly_means[(col_ch4_flux, "mean")] 1709 .rolling(window=window_size, center=True, min_periods=1) 1710 .mean() 1711 ) 1712 c2h6_mean = ( 1713 hourly_means[(col_c2h6_flux, "mean")] 1714 .rolling(window=window_size, center=True, min_periods=1) 1715 .mean() 1716 ) 1717 1718 if show_std: 1719 ch4_std = ( 1720 hourly_means[(col_ch4_flux, "std")] 1721 .rolling(window=window_size, center=True, min_periods=1) 1722 .mean() 1723 ) 1724 c2h6_std = ( 1725 hourly_means[(col_c2h6_flux, "std")] 1726 .rolling(window=window_size, center=True, min_periods=1) 1727 .mean() 1728 ) 1729 1730 ax1.fill_between( 1731 time_points, 1732 ch4_mean - ch4_std, 1733 ch4_mean + ch4_std, 1734 color="blue", 1735 alpha=alpha_std, 1736 ) 1737 ax2.fill_between( 1738 time_points, 1739 c2h6_mean - c2h6_std, 1740 c2h6_mean + c2h6_std, 1741 color="red", 1742 alpha=alpha_std, 1743 ) 1744 1745 # メインのラインプロット 1746 ax1.plot(time_points, ch4_mean, "blue", label=ch4_label) 1747 ax2.plot(time_points, c2h6_mean, "red", label=c2h6_label) 1748 1749 # 軸の設定 1750 for ax, ylabel in [ 1751 (ax1, r"CH$_4$ (nmol m$^{-2}$ s$^{-1}$)"), 1752 (ax2, r"C$_2$H$_6$ (nmol m$^{-2}$ s$^{-1}$)"), 1753 ]: 1754 ax.set_xlabel("Time") 1755 ax.set_ylabel(ylabel) 1756 ax.xaxis.set_major_formatter(mdates.DateFormatter("%-H")) 1757 ax.xaxis.set_major_locator(mdates.HourLocator(byhour=[0, 6, 12, 18, 24])) 1758 ax.set_xlim(time_points[0], time_points[-1]) 1759 ax.grid(True, alpha=0.3) 1760 ax.legend() 1761 1762 # グラフの保存 1763 plt.tight_layout() 1764 1765 if save_fig: 1766 if output_dirpath is None: 1767 raise ValueError( 1768 "save_fig = True の場合、 output_dirpath を指定する必要があります。有効なディレクトリパスを指定してください。" 1769 ) 1770 # 出力ディレクトリの作成 1771 os.makedirs(output_dirpath, exist_ok=True) 1772 output_filepath: str = os.path.join(output_dirpath, output_filename) 1773 plt.savefig(output_filepath, dpi=350, bbox_inches="tight") 1774 if show_fig: 1775 plt.show() 1776 plt.close(fig=fig) 1777 1778 # 統計情報の表示(オプション) 1779 if print_summary: 1780 for col, name in [(col_ch4_flux, "CH4"), (col_c2h6_flux, "C2H6")]: 1781 mean_val = hourly_means[(col, "mean")].mean() 1782 min_val = hourly_means[(col, "mean")].min() 1783 max_val = hourly_means[(col, "mean")].max() 1784 min_time = hourly_means[(col, "mean")].idxmin() 1785 max_time = hourly_means[(col, "mean")].idxmax() 1786 1787 self.logger.info(f"{name} Statistics:") 1788 self.logger.info(f"Mean: {mean_val:.2f}") 1789 self.logger.info(f"Min: {min_val:.2f} (Hour: {min_time})") 1790 self.logger.info(f"Max: {max_val:.2f} (Hour: {max_time})") 1791 self.logger.info(f"Max/Min ratio: {max_val / min_val:.2f}\n")
CH4とC2H6フラックスの日変化パターンをプロットします。
Parameters
df: pd.DataFrame
プロットするデータを含むDataFrameを指定します。
col_ch4_flux: str, optional
CH4フラックスのカラム名を指定します。デフォルト値は"Fch4"です。
col_c2h6_flux: str, optional
C2H6フラックスのカラム名を指定します。デフォルト値は"Fc2h6"です。
ch4_label: str, optional
CH4フラックスの凡例ラベルを指定します。デフォルト値は"CH4フラックス"です。
c2h6_label: str, optional
C2H6フラックスの凡例ラベルを指定します。デフォルト値は"C2H6フラックス"です。
col_datetime: str, optional
日時カラムの名前を指定します。デフォルト値は"Date"です。
output_dirpath: str | Path | None, optional
出力ディレクトリのパスを指定します。save_fig=Trueの場合は必須です。
output_filename: str, optional
出力ファイル名を指定します。デフォルト値は"diurnal_patterns.png"です。
window_size: int, optional
移動平均の窓サイズを指定します。デフォルト値は6です。
show_std: bool, optional
標準偏差を表示するかどうかを指定します。デフォルト値はTrueです。
alpha_std: float, optional
標準偏差の透明度を0-1の範囲で指定します。デフォルト値は0.1です。
figsize: tuple[float, float], optional
プロットのサイズを指定します。デフォルト値は(12, 5)です。
dpi: float | None, optional
プロットの解像度を指定します。デフォルト値は350です。
save_fig: bool, optional
プロットを保存するかどうかを指定します。デフォルト値はTrueです。
show_fig: bool, optional
プロットを表示するかどうかを指定します。デフォルト値はTrueです。
print_summary: bool, optional
統計情報を表示するかどうかを指定します。デフォルト値はFalseです。
Examples
>>> generator = MonthlyFiguresGenerator()
>>> df = pd.read_csv("flux_data.csv")
>>> generator.plot_flux_diurnal_patterns_with_std(
... df,
... col_ch4_flux="CH4_flux",
... col_c2h6_flux="C2H6_flux",
... output_dirpath="output",
... show_std=True
... )
1793 def plot_gas_ratio_diurnal( 1794 self, 1795 df: pd.DataFrame, 1796 col_ratio_1: str, 1797 col_ratio_2: str, 1798 label_1: str, 1799 label_2: str, 1800 color_1: str, 1801 color_2: str, 1802 output_dirpath: str | Path | None = None, 1803 output_filename: str = "gas_ratio_diurnal.png", 1804 add_xlabel: bool = True, 1805 add_ylabel: bool = True, 1806 add_legend: bool = True, 1807 xlabel: str = "Hour", 1808 ylabel: str = "都市ガスが占める排出比率 (%)", 1809 subplot_fontsize: int = 20, 1810 subplot_label: str | None = None, 1811 y_max: float | None = 100, 1812 figsize: tuple[float, float] = (12, 5), 1813 dpi: float | None = 350, 1814 save_fig: bool = True, 1815 show_fig: bool = False, 1816 ) -> None: 1817 """2つの比率の日変化を比較するプロットを作成します。 1818 1819 Parameters 1820 ---------- 1821 df: pd.DataFrame 1822 プロットするデータを含むDataFrameを指定します。 1823 col_ratio_1: str 1824 1つ目の比率データを含むカラム名を指定します。 1825 col_ratio_2: str 1826 2つ目の比率データを含むカラム名を指定します。 1827 label_1: str 1828 1つ目の比率データの凡例ラベルを指定します。 1829 label_2: str 1830 2つ目の比率データの凡例ラベルを指定します。 1831 color_1: str 1832 1つ目の比率データのプロット色を指定します。 1833 color_2: str 1834 2つ目の比率データのプロット色を指定します。 1835 output_dirpath: str | Path | None, optional 1836 出力先ディレクトリのパスを指定します。save_fig=Trueの場合は必須です。 1837 output_filename: str, optional 1838 出力ファイル名を指定します。デフォルト値は"gas_ratio_diurnal.png"です。 1839 add_xlabel: bool, optional 1840 x軸ラベルを表示するかどうかを指定します。デフォルト値はTrueです。 1841 add_ylabel: bool, optional 1842 y軸ラベルを表示するかどうかを指定します。デフォルト値はTrueです。 1843 add_legend: bool, optional 1844 凡例を表示するかどうかを指定します。デフォルト値はTrueです。 1845 xlabel: str, optional 1846 x軸のラベルを指定します。デフォルト値は"Hour"です。 1847 ylabel: str, optional 1848 y軸のラベルを指定します。デフォルト値は"都市ガスが占める排出比率 (%)"です。 1849 subplot_fontsize: int, optional 1850 サブプロットのフォントサイズを指定します。デフォルト値は20です。 1851 subplot_label: str | None, optional 1852 サブプロットのラベルを指定します。デフォルト値はNoneです。 1853 y_max: float | None, optional 1854 y軸の最大値を指定します。デフォルト値は100です。 1855 figsize: tuple[float, float], optional 1856 図のサイズを指定します。デフォルト値は(12, 5)です。 1857 dpi: float | None, optional 1858 図の解像度を指定します。デフォルト値は350です。 1859 save_fig: bool, optional 1860 図を保存するかどうかを指定します。デフォルト値はTrueです。 1861 show_fig: bool, optional 1862 図を表示するかどうかを指定します。デフォルト値はFalseです。 1863 1864 Examples 1865 ------- 1866 >>> df = pd.DataFrame({ 1867 ... 'ratio1': [80, 85, 90], 1868 ... 'ratio2': [70, 75, 80] 1869 ... }) 1870 >>> generator = MonthlyFiguresGenerator() 1871 >>> generator.plot_gas_ratio_diurnal( 1872 ... df=df, 1873 ... col_ratio_1='ratio1', 1874 ... col_ratio_2='ratio2', 1875 ... label_1='比率1', 1876 ... label_2='比率2', 1877 ... color_1='blue', 1878 ... color_2='red', 1879 ... output_dirpath='output' 1880 ... ) 1881 """ 1882 df_internal: pd.DataFrame = df.copy() 1883 df_internal.index = pd.to_datetime(df_internal.index) 1884 1885 # 時刻でグループ化して平均を計算 1886 hourly_means = df_internal.groupby(df_internal.index.hour)[ 1887 [col_ratio_1, col_ratio_2] 1888 ].mean() 1889 hourly_stds = df_internal.groupby(df_internal.index.hour)[ 1890 [col_ratio_1, col_ratio_2] 1891 ].std() 1892 1893 # 24時間目のデータ点を追加(0時のデータを使用) 1894 last_hour = hourly_means.iloc[0:1].copy() 1895 last_hour.index = pd.Index([24]) 1896 hourly_means = pd.concat([hourly_means, last_hour]) 1897 1898 last_hour_std = hourly_stds.iloc[0:1].copy() 1899 last_hour_std.index = pd.Index([24]) 1900 hourly_stds = pd.concat([hourly_stds, last_hour_std]) 1901 1902 # 24時間分の時刻を生成 1903 time_points: pd.DatetimeIndex = pd.date_range( 1904 "2024-01-01", periods=25, freq="h" 1905 ) 1906 1907 # プロットの作成 1908 fig, ax = plt.subplots(figsize=figsize) 1909 1910 # 1つ目の比率 1911 ax.plot( 1912 time_points, # [:-1]を削除 1913 hourly_means[col_ratio_1], 1914 color=color_1, 1915 label=label_1, 1916 alpha=0.7, 1917 ) 1918 ax.fill_between( 1919 time_points, # [:-1]を削除 1920 hourly_means[col_ratio_1] - hourly_stds[col_ratio_1], 1921 hourly_means[col_ratio_1] + hourly_stds[col_ratio_1], 1922 color=color_1, 1923 alpha=0.2, 1924 ) 1925 1926 # 2つ目の比率 1927 ax.plot( 1928 time_points, # [:-1]を削除 1929 hourly_means[col_ratio_2], 1930 color=color_2, 1931 label=label_2, 1932 alpha=0.7, 1933 ) 1934 ax.fill_between( 1935 time_points, # [:-1]を削除 1936 hourly_means[col_ratio_2] - hourly_stds[col_ratio_2], 1937 hourly_means[col_ratio_2] + hourly_stds[col_ratio_2], 1938 color=color_2, 1939 alpha=0.2, 1940 ) 1941 1942 # 軸の設定 1943 if add_xlabel: 1944 ax.set_xlabel(xlabel) 1945 if add_ylabel: 1946 ax.set_ylabel(ylabel) 1947 1948 # y軸の範囲設定 1949 if y_max is not None: 1950 ax.set_ylim(0, y_max) 1951 1952 # グリッド線の追加 1953 ax.grid(True, alpha=0.3) 1954 ax.grid(True, which="minor", alpha=0.1) 1955 1956 # x軸の設定 1957 ax.xaxis.set_major_formatter(mdates.DateFormatter("%-H")) 1958 ax.xaxis.set_major_locator(mdates.HourLocator(byhour=[0, 6, 12, 18, 24])) 1959 ax.set_xlim( 1960 float(mdates.date2num(time_points[0])), 1961 float(mdates.date2num(time_points[-1])), 1962 ) 1963 ax.set_xticks(time_points[::6]) 1964 ax.set_xticklabels(["0", "6", "12", "18", "24"]) 1965 1966 # サブプロットラベルの追加 1967 if subplot_label: 1968 ax.text( 1969 0.02, 1970 0.98, 1971 subplot_label, 1972 transform=ax.transAxes, 1973 va="top", 1974 fontsize=subplot_fontsize, 1975 ) 1976 1977 # 凡例の追加 1978 if add_legend: 1979 # 凡例を図の下部中央に配置 1980 ax.legend( 1981 loc="center", 1982 bbox_to_anchor=(0.5, -0.25), # 図の下部に配置 1983 ncol=2, # 2列で表示 1984 frameon=False, # 枠を非表示 1985 ) 1986 # 凡例のために下部のマージンを調整 1987 plt.subplots_adjust(bottom=0.2) 1988 1989 # プロットの保存と表示 1990 plt.tight_layout() 1991 if save_fig: 1992 if output_dirpath is None: 1993 raise ValueError( 1994 "save_fig = True の場合、 output_dirpath を指定する必要があります。有効なディレクトリパスを指定してください。" 1995 ) 1996 # 出力ディレクトリの作成 1997 os.makedirs(output_dirpath, exist_ok=True) 1998 output_filepath = os.path.join(output_dirpath, output_filename) 1999 plt.savefig(output_filepath, dpi=dpi, bbox_inches="tight") 2000 if show_fig: 2001 plt.show() 2002 plt.close(fig=fig)
2つの比率の日変化を比較するプロットを作成します。
Parameters
df: pd.DataFrame
プロットするデータを含むDataFrameを指定します。
col_ratio_1: str
1つ目の比率データを含むカラム名を指定します。
col_ratio_2: str
2つ目の比率データを含むカラム名を指定します。
label_1: str
1つ目の比率データの凡例ラベルを指定します。
label_2: str
2つ目の比率データの凡例ラベルを指定します。
color_1: str
1つ目の比率データのプロット色を指定します。
color_2: str
2つ目の比率データのプロット色を指定します。
output_dirpath: str | Path | None, optional
出力先ディレクトリのパスを指定します。save_fig=Trueの場合は必須です。
output_filename: str, optional
出力ファイル名を指定します。デフォルト値は"gas_ratio_diurnal.png"です。
add_xlabel: bool, optional
x軸ラベルを表示するかどうかを指定します。デフォルト値はTrueです。
add_ylabel: bool, optional
y軸ラベルを表示するかどうかを指定します。デフォルト値はTrueです。
add_legend: bool, optional
凡例を表示するかどうかを指定します。デフォルト値はTrueです。
xlabel: str, optional
x軸のラベルを指定します。デフォルト値は"Hour"です。
ylabel: str, optional
y軸のラベルを指定します。デフォルト値は"都市ガスが占める排出比率 (%)"です。
subplot_fontsize: int, optional
サブプロットのフォントサイズを指定します。デフォルト値は20です。
subplot_label: str | None, optional
サブプロットのラベルを指定します。デフォルト値はNoneです。
y_max: float | None, optional
y軸の最大値を指定します。デフォルト値は100です。
figsize: tuple[float, float], optional
図のサイズを指定します。デフォルト値は(12, 5)です。
dpi: float | None, optional
図の解像度を指定します。デフォルト値は350です。
save_fig: bool, optional
図を保存するかどうかを指定します。デフォルト値はTrueです。
show_fig: bool, optional
図を表示するかどうかを指定します。デフォルト値はFalseです。
Examples
>>> df = pd.DataFrame({
... 'ratio1': [80, 85, 90],
... 'ratio2': [70, 75, 80]
... })
>>> generator = MonthlyFiguresGenerator()
>>> generator.plot_gas_ratio_diurnal(
... df=df,
... col_ratio_1='ratio1',
... col_ratio_2='ratio2',
... label_1='比率1',
... label_2='比率2',
... color_1='blue',
... color_2='red',
... output_dirpath='output'
... )
2004 def plot_scatter( 2005 self, 2006 df: pd.DataFrame, 2007 col_x: str, 2008 col_y: str, 2009 output_dirpath: str | Path | None = None, 2010 output_filename: str = "scatter.png", 2011 add_label: bool = True, 2012 xlabel: str | None = None, 2013 ylabel: str | None = None, 2014 x_axis_range: tuple | None = None, 2015 y_axis_range: tuple | None = None, 2016 x_scientific: bool = False, 2017 y_scientific: bool = False, 2018 fixed_slope: float = 0.076, 2019 show_fixed_slope: bool = False, 2020 figsize: tuple[float, float] = (6, 6), 2021 dpi: float | None = 350, 2022 save_fig: bool = True, 2023 show_fig: bool = True, 2024 ) -> None: 2025 """散布図を作成し、TLS回帰直線を描画します。 2026 2027 Parameters 2028 ---------- 2029 df: pd.DataFrame 2030 プロットに使用するデータフレームを指定します。 2031 col_x: str 2032 x軸に使用する列名を指定します。 2033 col_y: str 2034 y軸に使用する列名を指定します。 2035 output_dirpath: str | Path | None, optional 2036 出力先ディレクトリを指定します。save_fig=Trueの場合は必須です。 2037 output_filename: str, optional 2038 出力ファイル名を指定します。デフォルト値は"scatter.png"です。 2039 add_label: bool, optional 2040 軸ラベルを表示するかどうかを指定します。デフォルト値はTrueです。 2041 xlabel: str | None, optional 2042 x軸のラベルを指定します。デフォルト値はNoneです。 2043 ylabel: str | None, optional 2044 y軸のラベルを指定します。デフォルト値はNoneです。 2045 x_axis_range: tuple | None, optional 2046 x軸の表示範囲を(最小値, 最大値)で指定します。デフォルト値はNoneです。 2047 y_axis_range: tuple | None, optional 2048 y軸の表示範囲を(最小値, 最大値)で指定します。デフォルト値はNoneです。 2049 x_scientific: bool, optional 2050 x軸を科学的記法で表示するかどうかを指定します。デフォルト値はFalseです。 2051 y_scientific: bool, optional 2052 y軸を科学的記法で表示するかどうかを指定します。デフォルト値はFalseです。 2053 fixed_slope: float, optional 2054 固定傾きの値を指定します。デフォルト値は0.076です。 2055 show_fixed_slope: bool, optional 2056 固定傾きの線を表示するかどうかを指定します。デフォルト値はFalseです。 2057 figsize: tuple[float, float], optional 2058 プロットのサイズを(幅, 高さ)で指定します。デフォルト値は(6, 6)です。 2059 dpi: float | None, optional 2060 プロットの解像度を指定します。デフォルト値は350です。 2061 save_fig: bool, optional 2062 プロットを保存するかどうかを指定します。デフォルト値はTrueです。 2063 show_fig: bool, optional 2064 プロットを表示するかどうかを指定します。デフォルト値はTrueです。 2065 2066 Examples 2067 -------- 2068 >>> df = pd.DataFrame({ 2069 ... 'x': [1, 2, 3, 4, 5], 2070 ... 'y': [2, 4, 6, 8, 10] 2071 ... }) 2072 >>> generator = MonthlyFiguresGenerator() 2073 >>> generator.plot_scatter( 2074 ... df=df, 2075 ... col_x='x', 2076 ... col_y='y', 2077 ... xlabel='X軸', 2078 ... ylabel='Y軸', 2079 ... output_dirpath='output' 2080 ... ) 2081 """ 2082 # 有効なデータの抽出 2083 df_internal = MonthlyFiguresGenerator.get_valid_data( 2084 df=df, col_x=col_x, col_y=col_y 2085 ) 2086 2087 # データの準備 2088 x = df_internal[col_x].values 2089 y = df_internal[col_y].values 2090 2091 # データの中心化 2092 x_array = np.array(x) 2093 y_array = np.array(y) 2094 x_mean = np.mean(x_array, axis=0) 2095 y_mean = np.mean(y_array, axis=0) 2096 x_c = x - x_mean 2097 y_c = y - y_mean 2098 2099 # TLS回帰の計算 2100 data_matrix = np.vstack((x_c, y_c)) 2101 cov_matrix = np.cov(data_matrix) 2102 _, eigenvecs = linalg.eigh(cov_matrix) 2103 largest_eigenvec = eigenvecs[:, -1] 2104 2105 slope = largest_eigenvec[1] / largest_eigenvec[0] 2106 intercept = y_mean - slope * x_mean 2107 2108 # R²とRMSEの計算 2109 y_pred = slope * x + intercept 2110 r_squared = 1 - np.sum((y - y_pred) ** 2) / np.sum((y - y_mean) ** 2) 2111 rmse = np.sqrt(np.mean((y - y_pred) ** 2)) 2112 2113 # プロットの作成 2114 fig, ax = plt.subplots(figsize=figsize) 2115 2116 # データ点のプロット 2117 ax.scatter(x_array, y_array, color="black") 2118 2119 # データの範囲を取得 2120 if x_axis_range is None: 2121 x_axis_range = (df_internal[col_x].min(), df_internal[col_x].max()) 2122 if y_axis_range is None: 2123 y_axis_range = (df_internal[col_y].min(), df_internal[col_y].max()) 2124 2125 # 回帰直線のプロット 2126 x_range = np.linspace(x_axis_range[0], x_axis_range[1], 150) 2127 y_range = slope * x_range + intercept 2128 ax.plot(x_range, y_range, "r", label="TLS regression") 2129 2130 # 傾き固定の線を追加(フラグがTrueの場合) 2131 if show_fixed_slope: 2132 fixed_intercept = ( 2133 y_mean - fixed_slope * x_mean 2134 ) # 中心点を通るように切片を計算 2135 y_fixed = fixed_slope * x_range + fixed_intercept 2136 ax.plot(x_range, y_fixed, "b--", label=f"Slope = {fixed_slope}", alpha=0.7) 2137 2138 # 軸の設定 2139 ax.set_xlim(x_axis_range) 2140 ax.set_ylim(y_axis_range) 2141 2142 # 指数表記の設定 2143 if x_scientific: 2144 ax.ticklabel_format(style="sci", axis="x", scilimits=(0, 0)) 2145 ax.xaxis.get_offset_text().set_position((1.1, 0)) # 指数の位置調整 2146 if y_scientific: 2147 ax.ticklabel_format(style="sci", axis="y", scilimits=(0, 0)) 2148 ax.yaxis.get_offset_text().set_position((0, 1.1)) # 指数の位置調整 2149 2150 if add_label: 2151 if xlabel is not None: 2152 ax.set_xlabel(xlabel) 2153 if ylabel is not None: 2154 ax.set_ylabel(ylabel) 2155 2156 # 1:1の関係を示す点線(軸の範囲が同じ場合のみ表示) 2157 if ( 2158 x_axis_range is not None 2159 and y_axis_range is not None 2160 and x_axis_range == y_axis_range 2161 ): 2162 ax.plot( 2163 [x_axis_range[0], x_axis_range[1]], 2164 [x_axis_range[0], x_axis_range[1]], 2165 "k--", 2166 alpha=0.5, 2167 ) 2168 2169 # 回帰情報の表示 2170 equation = ( 2171 f"y = {slope:.2f}x {'+' if intercept >= 0 else '-'} {abs(intercept):.2f}" 2172 ) 2173 position_x = 0.05 2174 fig_ha: str = "left" 2175 ax.text( 2176 position_x, 2177 0.95, 2178 equation, 2179 transform=ax.transAxes, 2180 va="top", 2181 ha=fig_ha, 2182 color="red", 2183 ) 2184 ax.text( 2185 position_x, 2186 0.88, 2187 f"R² = {r_squared:.2f}", 2188 transform=ax.transAxes, 2189 va="top", 2190 ha=fig_ha, 2191 color="red", 2192 ) 2193 ax.text( 2194 position_x, 2195 0.81, # RMSEのための新しい位置 2196 f"RMSE = {rmse:.2f}", 2197 transform=ax.transAxes, 2198 va="top", 2199 ha=fig_ha, 2200 color="red", 2201 ) 2202 # 目盛り線の設定 2203 ax.grid(True, alpha=0.3) 2204 2205 if save_fig: 2206 if output_dirpath is None: 2207 raise ValueError( 2208 "save_fig = True の場合、 output_dirpath を指定する必要があります。有効なディレクトリパスを指定してください。" 2209 ) 2210 os.makedirs(output_dirpath, exist_ok=True) 2211 output_filepath: str = os.path.join(output_dirpath, output_filename) 2212 fig.savefig(output_filepath, dpi=dpi, bbox_inches="tight") 2213 if show_fig: 2214 plt.show() 2215 plt.close(fig=fig)
散布図を作成し、TLS回帰直線を描画します。
Parameters
df: pd.DataFrame
プロットに使用するデータフレームを指定します。
col_x: str
x軸に使用する列名を指定します。
col_y: str
y軸に使用する列名を指定します。
output_dirpath: str | Path | None, optional
出力先ディレクトリを指定します。save_fig=Trueの場合は必須です。
output_filename: str, optional
出力ファイル名を指定します。デフォルト値は"scatter.png"です。
add_label: bool, optional
軸ラベルを表示するかどうかを指定します。デフォルト値はTrueです。
xlabel: str | None, optional
x軸のラベルを指定します。デフォルト値はNoneです。
ylabel: str | None, optional
y軸のラベルを指定します。デフォルト値はNoneです。
x_axis_range: tuple | None, optional
x軸の表示範囲を(最小値, 最大値)で指定します。デフォルト値はNoneです。
y_axis_range: tuple | None, optional
y軸の表示範囲を(最小値, 最大値)で指定します。デフォルト値はNoneです。
x_scientific: bool, optional
x軸を科学的記法で表示するかどうかを指定します。デフォルト値はFalseです。
y_scientific: bool, optional
y軸を科学的記法で表示するかどうかを指定します。デフォルト値はFalseです。
fixed_slope: float, optional
固定傾きの値を指定します。デフォルト値は0.076です。
show_fixed_slope: bool, optional
固定傾きの線を表示するかどうかを指定します。デフォルト値はFalseです。
figsize: tuple[float, float], optional
プロットのサイズを(幅, 高さ)で指定します。デフォルト値は(6, 6)です。
dpi: float | None, optional
プロットの解像度を指定します。デフォルト値は350です。
save_fig: bool, optional
プロットを保存するかどうかを指定します。デフォルト値はTrueです。
show_fig: bool, optional
プロットを表示するかどうかを指定します。デフォルト値はTrueです。
Examples
>>> df = pd.DataFrame({
... 'x': [1, 2, 3, 4, 5],
... 'y': [2, 4, 6, 8, 10]
... })
>>> generator = MonthlyFiguresGenerator()
>>> generator.plot_scatter(
... df=df,
... col_x='x',
... col_y='y',
... xlabel='X軸',
... ylabel='Y軸',
... output_dirpath='output'
... )
2217 def plot_source_contributions_diurnal( 2218 self, 2219 df: pd.DataFrame, 2220 col_ch4_flux: str, 2221 col_c2h6_flux: str, 2222 col_datetime: str = "Date", 2223 color_bio: str = "blue", 2224 color_gas: str = "red", 2225 label_bio: str = "bio", 2226 label_gas: str = "gas", 2227 flux_alpha: float = 0.6, 2228 output_dirpath: str | Path | None = None, 2229 output_filename: str = "source_contributions.png", 2230 window_size: int = 6, 2231 print_summary: bool = False, 2232 add_xlabel: bool = True, 2233 add_ylabel: bool = True, 2234 add_legend: bool = True, 2235 smooth: bool = False, 2236 y_max: float = 100, 2237 subplot_label: str | None = None, 2238 subplot_fontsize: int = 20, 2239 figsize: tuple[float, float] = (10, 6), 2240 dpi: float | None = 350, 2241 save_fig: bool = True, 2242 show_fig: bool = True, 2243 ) -> None: 2244 """CH4フラックスの都市ガス起源と生物起源の日変化を積み上げグラフとして表示します。 2245 2246 Parameters 2247 ---------- 2248 df: pd.DataFrame 2249 CH4フラックスデータを含むデータフレームを指定します。 2250 col_ch4_flux: str 2251 CH4フラックスの列名を指定します。 2252 col_c2h6_flux: str 2253 C2H6フラックスの列名を指定します。 2254 col_datetime: str, optional 2255 日時の列名を指定します。デフォルト値は"Date"です。 2256 color_bio: str, optional 2257 生物起源の色を指定します。デフォルト値は"blue"です。 2258 color_gas: str, optional 2259 都市ガス起源の色を指定します。デフォルト値は"red"です。 2260 label_bio: str, optional 2261 生物起源のラベルを指定します。デフォルト値は"bio"です。 2262 label_gas: str, optional 2263 都市ガスのラベルを指定します。デフォルト値は"gas"です。 2264 flux_alpha: float, optional 2265 フラックスの透明度を指定します。デフォルト値は0.6です。 2266 output_dirpath: str | Path | None, optional 2267 出力先のディレクトリを指定します。save_fig=Trueの場合は必須です。 2268 output_filename: str, optional 2269 出力ファイル名を指定します。デフォルト値は"source_contributions.png"です。 2270 window_size: int, optional 2271 移動平均の窓サイズを指定します。デフォルト値は6です。 2272 print_summary: bool, optional 2273 統計情報を表示するかどうかを指定します。デフォルト値はFalseです。 2274 add_xlabel: bool, optional 2275 x軸のラベルを追加するかどうかを指定します。デフォルト値はTrueです。 2276 add_ylabel: bool, optional 2277 y軸のラベルを追加するかどうかを指定します。デフォルト値はTrueです。 2278 add_legend: bool, optional 2279 凡例を追加するかどうかを指定します。デフォルト値はTrueです。 2280 smooth: bool, optional 2281 移動平均を適用するかどうかを指定します。デフォルト値はFalseです。 2282 y_max: float, optional 2283 y軸の上限値を指定します。デフォルト値は100です。 2284 subplot_label: str | None, optional 2285 サブプロットのラベルを指定します。デフォルト値はNoneです。 2286 subplot_fontsize: int, optional 2287 サブプロットラベルのフォントサイズを指定します。デフォルト値は20です。 2288 figsize: tuple[float, float], optional 2289 プロットのサイズを指定します。デフォルト値は(10, 6)です。 2290 dpi: float | None, optional 2291 プロットのdpiを指定します。デフォルト値は350です。 2292 save_fig: bool, optional 2293 プロットを保存するかどうかを指定します。デフォルト値はTrueです。 2294 show_fig: bool, optional 2295 プロットを表示するかどうかを指定します。デフォルト値はTrueです。 2296 2297 Examples 2298 -------- 2299 >>> df = pd.read_csv("flux_data.csv") 2300 >>> generator = MonthlyFiguresGenerator() 2301 >>> generator.plot_source_contributions_diurnal( 2302 ... df=df, 2303 ... col_ch4_flux="Fch4", 2304 ... col_c2h6_flux="Fc2h6", 2305 ... output_dirpath="output", 2306 ... output_filename="diurnal_sources.png" 2307 ... ) 2308 """ 2309 # 起源の計算 2310 df_with_sources = self._calculate_source_contributions( 2311 df=df, 2312 col_ch4_flux=col_ch4_flux, 2313 col_c2h6_flux=col_c2h6_flux, 2314 col_datetime=col_datetime, 2315 ) 2316 df_with_sources.index = pd.to_datetime(df_with_sources.index) 2317 2318 # 時刻データの抽出とグループ化 2319 df_with_sources["hour"] = df_with_sources.index.hour 2320 hourly_means = df_with_sources.groupby("hour")[["ch4_gas", "ch4_bio"]].mean() 2321 2322 # 24時間目のデータ点を追加(0時のデータを使用) 2323 last_hour = hourly_means.iloc[0:1].copy() 2324 last_hour.index = pd.Index([24]) 2325 hourly_means = pd.concat([hourly_means, last_hour]) 2326 2327 # 移動平均の適用 2328 hourly_means_smoothed = hourly_means 2329 if smooth: 2330 hourly_means_smoothed = hourly_means.rolling( 2331 window=window_size, center=True, min_periods=1 2332 ).mean() 2333 2334 # 24時間分のデータポイントを作成 2335 time_points = pd.date_range("2024-01-01", periods=25, freq="h") 2336 2337 # プロットの作成 2338 fig = plt.figure(figsize=figsize) 2339 ax = plt.gca() 2340 2341 # サブプロットラベルの追加(subplot_labelが指定されている場合) 2342 if subplot_label: 2343 ax.text( 2344 0.02, # x位置 2345 0.98, # y位置 2346 subplot_label, 2347 transform=ax.transAxes, 2348 va="top", 2349 fontsize=subplot_fontsize, 2350 ) 2351 2352 # 積み上げプロット 2353 ax.fill_between( 2354 time_points, 2355 0, 2356 hourly_means_smoothed["ch4_bio"], 2357 color=color_bio, 2358 alpha=flux_alpha, 2359 label=label_bio, 2360 ) 2361 ax.fill_between( 2362 time_points, 2363 hourly_means_smoothed["ch4_bio"], 2364 hourly_means_smoothed["ch4_bio"] + hourly_means_smoothed["ch4_gas"], 2365 color=color_gas, 2366 alpha=flux_alpha, 2367 label=label_gas, 2368 ) 2369 2370 # 合計値のライン 2371 total_flux = hourly_means_smoothed["ch4_bio"] + hourly_means_smoothed["ch4_gas"] 2372 ax.plot(time_points, total_flux, "-", color="black", alpha=0.5) 2373 2374 # 軸の設定 2375 if add_xlabel: 2376 ax.set_xlabel("Time (hour)") 2377 if add_ylabel: 2378 ax.set_ylabel(r"CH$_4$ flux (nmol m$^{-2}$ s$^{-1}$)") 2379 ax.xaxis.set_major_formatter(mdates.DateFormatter("%-H")) 2380 ax.xaxis.set_major_locator(mdates.HourLocator(byhour=[0, 6, 12, 18, 24])) 2381 ax.set_xlim( 2382 float(mdates.date2num(time_points[0])), 2383 float(mdates.date2num(time_points[-1])), 2384 ) 2385 ax.set_ylim(0, y_max) # y軸の範囲を設定 2386 ax.grid(True, alpha=0.3) 2387 2388 # 凡例を図の下部に配置 2389 if add_legend: 2390 handles, labels = ax.get_legend_handles_labels() 2391 fig = plt.gcf() # 現在の図を取得 2392 fig.legend( 2393 handles, 2394 labels, 2395 loc="center", 2396 bbox_to_anchor=(0.5, 0.01), 2397 ncol=len(handles), 2398 ) 2399 plt.subplots_adjust(bottom=0.2) # 下部に凡例用のスペースを確保 2400 plt.tight_layout() 2401 2402 # グラフの保存、表示 2403 if save_fig: 2404 if output_dirpath is None: 2405 raise ValueError( 2406 "save_fig = True の場合、 output_dirpath を指定する必要があります。有効なディレクトリパスを指定してください。" 2407 ) 2408 os.makedirs(output_dirpath, exist_ok=True) 2409 output_filepath: str = os.path.join(output_dirpath, output_filename) 2410 plt.savefig(output_filepath, dpi=dpi, bbox_inches="tight") 2411 if show_fig: 2412 plt.show() 2413 plt.close(fig=fig) 2414 2415 # 統計情報の表示 2416 if print_summary: 2417 # 昼夜の時間帯を定義 2418 daytime_range: list[int] = [6, 19] # 6~18時 2419 daytime_hours = range(daytime_range[0], daytime_range[1]) 2420 nighttime_hours = list(range(0, daytime_range[0])) + list( 2421 range(daytime_range[1], 24) 2422 ) 2423 2424 # 都市ガスと生物起源のデータを取得 2425 gas_flux = hourly_means["ch4_gas"] 2426 bio_flux = hourly_means["ch4_bio"] 2427 total_flux = gas_flux + bio_flux 2428 2429 # 都市ガス比率を計算 2430 gas_ratio = (gas_flux / total_flux) * 100 2431 daytime_gas_ratio = ( 2432 pd.Series(gas_flux).iloc[np.array(list(daytime_hours))].sum() 2433 / pd.Series(total_flux).iloc[np.array(list(daytime_hours))].sum() 2434 ) * 100 2435 nighttime_gas_ratio = ( 2436 pd.Series(gas_flux).iloc[np.array(list(nighttime_hours))].sum() 2437 / pd.Series(total_flux).iloc[np.array(list(nighttime_hours))].sum() 2438 ) * 100 2439 2440 stats = { 2441 "都市ガス起源": gas_flux, 2442 "生物起源": bio_flux, 2443 "合計": total_flux, 2444 } 2445 2446 # 都市ガス比率の統計を出力 2447 self.logger.info("\n都市ガス比率の統計:") 2448 print(f" 全体の都市ガス比率: {gas_ratio.mean():.1f}%") 2449 print( 2450 f" 昼間({daytime_range[0]}~{daytime_range[1] - 1}時)の都市ガス比率: {daytime_gas_ratio:.1f}%" 2451 ) 2452 print( 2453 f" 夜間({daytime_range[1] - 1}~{daytime_range[0]}時)の都市ガス比率: {nighttime_gas_ratio:.1f}%" 2454 ) 2455 print(f" 最小比率: {gas_ratio.min():.1f}% (Hour: {gas_ratio.idxmin()})") 2456 print(f" 最大比率: {gas_ratio.max():.1f}% (Hour: {gas_ratio.idxmax()})") 2457 2458 # 各フラックスの統計を出力 2459 for source, data in stats.items(): 2460 mean_val = data.mean() 2461 min_val = data.min() 2462 max_val = data.max() 2463 min_time = data.idxmin() 2464 max_time = data.idxmax() 2465 2466 # 昼間と夜間のデータを抽出 2467 daytime_data = pd.Series(data).iloc[np.array(list(daytime_hours))] 2468 nighttime_data = pd.Series(data).iloc[np.array(list(nighttime_hours))] 2469 2470 daytime_mean = daytime_data.mean() 2471 nighttime_mean = nighttime_data.mean() 2472 2473 self.logger.info(f"\n{source}の統計:") 2474 print(f" 平均値: {mean_val:.2f}") 2475 print(f" 最小値: {min_val:.2f} (Hour: {min_time})") 2476 print(f" 最大値: {max_val:.2f} (Hour: {max_time})") 2477 if min_val != 0: 2478 print(f" 最大/最小比: {max_val / min_val:.2f}") 2479 print( 2480 f" 昼間({daytime_range[0]}~{daytime_range[1] - 1}時)の平均: {daytime_mean:.2f}" 2481 ) 2482 print( 2483 f" 夜間({daytime_range[1] - 1}~{daytime_range[0]}時)の平均: {nighttime_mean:.2f}" 2484 ) 2485 if nighttime_mean != 0: 2486 print(f" 昼/夜比: {daytime_mean / nighttime_mean:.2f}")
CH4フラックスの都市ガス起源と生物起源の日変化を積み上げグラフとして表示します。
Parameters
df: pd.DataFrame
CH4フラックスデータを含むデータフレームを指定します。
col_ch4_flux: str
CH4フラックスの列名を指定します。
col_c2h6_flux: str
C2H6フラックスの列名を指定します。
col_datetime: str, optional
日時の列名を指定します。デフォルト値は"Date"です。
color_bio: str, optional
生物起源の色を指定します。デフォルト値は"blue"です。
color_gas: str, optional
都市ガス起源の色を指定します。デフォルト値は"red"です。
label_bio: str, optional
生物起源のラベルを指定します。デフォルト値は"bio"です。
label_gas: str, optional
都市ガスのラベルを指定します。デフォルト値は"gas"です。
flux_alpha: float, optional
フラックスの透明度を指定します。デフォルト値は0.6です。
output_dirpath: str | Path | None, optional
出力先のディレクトリを指定します。save_fig=Trueの場合は必須です。
output_filename: str, optional
出力ファイル名を指定します。デフォルト値は"source_contributions.png"です。
window_size: int, optional
移動平均の窓サイズを指定します。デフォルト値は6です。
print_summary: bool, optional
統計情報を表示するかどうかを指定します。デフォルト値はFalseです。
add_xlabel: bool, optional
x軸のラベルを追加するかどうかを指定します。デフォルト値はTrueです。
add_ylabel: bool, optional
y軸のラベルを追加するかどうかを指定します。デフォルト値はTrueです。
add_legend: bool, optional
凡例を追加するかどうかを指定します。デフォルト値はTrueです。
smooth: bool, optional
移動平均を適用するかどうかを指定します。デフォルト値はFalseです。
y_max: float, optional
y軸の上限値を指定します。デフォルト値は100です。
subplot_label: str | None, optional
サブプロットのラベルを指定します。デフォルト値はNoneです。
subplot_fontsize: int, optional
サブプロットラベルのフォントサイズを指定します。デフォルト値は20です。
figsize: tuple[float, float], optional
プロットのサイズを指定します。デフォルト値は(10, 6)です。
dpi: float | None, optional
プロットのdpiを指定します。デフォルト値は350です。
save_fig: bool, optional
プロットを保存するかどうかを指定します。デフォルト値はTrueです。
show_fig: bool, optional
プロットを表示するかどうかを指定します。デフォルト値はTrueです。
Examples
>>> df = pd.read_csv("flux_data.csv")
>>> generator = MonthlyFiguresGenerator()
>>> generator.plot_source_contributions_diurnal(
... df=df,
... col_ch4_flux="Fch4",
... col_c2h6_flux="Fc2h6",
... output_dirpath="output",
... output_filename="diurnal_sources.png"
... )
2488 def plot_source_contributions_diurnal_by_date( 2489 self, 2490 df: pd.DataFrame, 2491 col_ch4_flux: str, 2492 col_c2h6_flux: str, 2493 col_datetime: str = "Date", 2494 color_bio: str = "blue", 2495 color_gas: str = "red", 2496 label_bio: str = "bio", 2497 label_gas: str = "gas", 2498 flux_alpha: float = 0.6, 2499 output_dirpath: str | Path | None = None, 2500 output_filename: str = "source_contributions_by_date.png", 2501 add_xlabel: bool = True, 2502 add_ylabel: bool = True, 2503 add_legend: bool = True, 2504 print_summary: bool = False, 2505 subplot_fontsize: int = 20, 2506 subplot_label_weekday: str | None = None, 2507 subplot_label_weekend: str | None = None, 2508 y_max: float | None = None, 2509 figsize: tuple[float, float] = (12, 5), 2510 dpi: float | None = 350, 2511 save_fig: bool = True, 2512 show_fig: bool = True, 2513 ) -> None: 2514 """CH4フラックスの都市ガス起源と生物起源の日変化を平日・休日別に表示します。 2515 2516 Parameters 2517 ---------- 2518 df: pd.DataFrame 2519 CH4フラックスとC2H6フラックスのデータを含むデータフレームを指定します。 2520 col_ch4_flux: str 2521 CH4フラックスのカラム名を指定します。 2522 col_c2h6_flux: str 2523 C2H6フラックスのカラム名を指定します。 2524 col_datetime: str, optional 2525 日時カラムの名前を指定します。デフォルト値は"Date"です。 2526 color_bio: str, optional 2527 生物起源のプロット色を指定します。デフォルト値は"blue"です。 2528 color_gas: str, optional 2529 都市ガス起源のプロット色を指定します。デフォルト値は"red"です。 2530 label_bio: str, optional 2531 生物起源の凡例ラベルを指定します。デフォルト値は"bio"です。 2532 label_gas: str, optional 2533 都市ガスの凡例ラベルを指定します。デフォルト値は"gas"です。 2534 flux_alpha: float, optional 2535 フラックスプロットの透明度を指定します。デフォルト値は0.6です。 2536 output_dirpath: str | Path | None, optional 2537 出力ディレクトリのパスを指定します。save_fig=Trueの場合は必須です。 2538 output_filename: str, optional 2539 出力ファイル名を指定します。デフォルト値は"source_contributions_by_date.png"です。 2540 add_xlabel: bool, optional 2541 x軸のラベルを表示するかどうかを指定します。デフォルト値はTrueです。 2542 add_ylabel: bool, optional 2543 y軸のラベルを表示するかどうかを指定します。デフォルト値はTrueです。 2544 add_legend: bool, optional 2545 凡例を表示するかどうかを指定します。デフォルト値はTrueです。 2546 print_summary: bool, optional 2547 統計情報を表示するかどうかを指定します。デフォルト値はFalseです。 2548 subplot_fontsize: int, optional 2549 サブプロットのフォントサイズを指定します。デフォルト値は20です。 2550 subplot_label_weekday: str | None, optional 2551 平日グラフのラベルを指定します。デフォルト値はNoneです。 2552 subplot_label_weekend: str | None, optional 2553 休日グラフのラベルを指定します。デフォルト値はNoneです。 2554 y_max: float | None, optional 2555 y軸の上限値を指定します。デフォルト値はNoneです。 2556 figsize: tuple[float, float], optional 2557 プロットのサイズを(幅, 高さ)で指定します。デフォルト値は(12, 5)です。 2558 dpi: float | None, optional 2559 プロットの解像度を指定します。デフォルト値は350です。 2560 save_fig: bool, optional 2561 プロットを保存するかどうかを指定します。デフォルト値はTrueです。 2562 show_fig: bool, optional 2563 プロットを表示するかどうかを指定します。デフォルト値はTrueです。 2564 2565 Examples 2566 -------- 2567 >>> df = pd.DataFrame({ 2568 ... 'Date': pd.date_range('2024-01-01', periods=48, freq='H'), 2569 ... 'Fch4': [1.2] * 48, 2570 ... 'Fc2h6': [0.1] * 48 2571 ... }) 2572 >>> generator = MonthlyFiguresGenerator() 2573 >>> generator.plot_source_contributions_diurnal_by_date( 2574 ... df=df, 2575 ... col_ch4_flux='Fch4', 2576 ... col_c2h6_flux='Fc2h6', 2577 ... output_dirpath='output' 2578 ... ) 2579 """ 2580 # 起源の計算 2581 df_with_sources = self._calculate_source_contributions( 2582 df=df, 2583 col_ch4_flux=col_ch4_flux, 2584 col_c2h6_flux=col_c2h6_flux, 2585 col_datetime=col_datetime, 2586 ) 2587 df_with_sources.index = pd.to_datetime(df_with_sources.index) 2588 2589 # 日付タイプの分類 2590 dates = pd.to_datetime(df_with_sources.index) 2591 is_weekend = dates.dayofweek.isin([5, 6]) 2592 is_holiday = dates.map(lambda x: jpholiday.is_holiday(x.date())) 2593 is_weekday = ~(is_weekend | is_holiday) 2594 2595 # データの分類 2596 data_weekday = df_with_sources[is_weekday] 2597 data_holiday = df_with_sources[is_weekend | is_holiday] 2598 2599 # プロットの作成 2600 fig, (ax1, ax2) = plt.subplots(1, 2, figsize=figsize) 2601 2602 # 平日と休日それぞれのプロット 2603 for ax, data, _ in [ 2604 (ax1, data_weekday, "Weekdays"), 2605 (ax2, data_holiday, "Weekends & Holidays"), 2606 ]: 2607 # 時間ごとの平均値を計算 2608 hourly_means = data.groupby(pd.to_datetime(data.index).hour)[ 2609 ["ch4_gas", "ch4_bio"] 2610 ].mean() 2611 2612 # 24時間目のデータ点を追加 2613 last_hour = hourly_means.iloc[0:1].copy() 2614 last_hour.index = pd.Index([24]) 2615 hourly_means = pd.concat([hourly_means, last_hour]) 2616 2617 # 24時間分のデータポイントを作成 2618 time_points = pd.date_range("2024-01-01", periods=25, freq="h") 2619 2620 # 積み上げプロット 2621 ax.fill_between( 2622 time_points, 2623 0, 2624 hourly_means["ch4_bio"], 2625 color=color_bio, 2626 alpha=flux_alpha, 2627 label=label_bio, 2628 ) 2629 ax.fill_between( 2630 time_points, 2631 hourly_means["ch4_bio"], 2632 hourly_means["ch4_bio"] + hourly_means["ch4_gas"], 2633 color=color_gas, 2634 alpha=flux_alpha, 2635 label=label_gas, 2636 ) 2637 2638 # 合計値のライン 2639 total_flux = hourly_means["ch4_bio"] + hourly_means["ch4_gas"] 2640 ax.plot(time_points, total_flux, "-", color="black", alpha=0.5) 2641 2642 # 軸の設定 2643 if add_xlabel: 2644 ax.set_xlabel("Time (hour)") 2645 if add_ylabel: 2646 if ax == ax1: # 左側のプロットのラベル 2647 ax.set_ylabel("CH$_4$ flux\n" r"(nmol m$^{-2}$ s$^{-1}$)") 2648 else: # 右側のプロットのラベル 2649 ax.set_ylabel("CH$_4$ flux\n" r"(nmol m$^{-2}$ s$^{-1}$)") 2650 2651 ax.xaxis.set_major_formatter(mdates.DateFormatter("%-H")) 2652 ax.xaxis.set_major_locator(mdates.HourLocator(byhour=[0, 6, 12, 18, 24])) 2653 ax.set_xlim( 2654 float(mdates.date2num(time_points[0])), 2655 float(mdates.date2num(time_points[-1])), 2656 ) 2657 if y_max is not None: 2658 ax.set_ylim(0, y_max) 2659 ax.grid(True, alpha=0.3) 2660 2661 # サブプロットラベルの追加 2662 if subplot_label_weekday: 2663 ax1.text( 2664 0.02, 2665 0.98, 2666 subplot_label_weekday, 2667 transform=ax1.transAxes, 2668 va="top", 2669 fontsize=subplot_fontsize, 2670 ) 2671 if subplot_label_weekend: 2672 ax2.text( 2673 0.02, 2674 0.98, 2675 subplot_label_weekend, 2676 transform=ax2.transAxes, 2677 va="top", 2678 fontsize=subplot_fontsize, 2679 ) 2680 2681 # 凡例を図の下部に配置 2682 if add_legend: 2683 # 最初のプロットから凡例のハンドルとラベルを取得 2684 handles, labels = ax1.get_legend_handles_labels() 2685 # 図の下部に凡例を配置 2686 fig.legend( 2687 handles, 2688 labels, 2689 loc="center", 2690 bbox_to_anchor=(0.5, 0.01), # x=0.5で中央、y=0.01で下部に配置 2691 ncol=len(handles), # ハンドルの数だけ列を作成(一行に表示) 2692 ) 2693 # 凡例用のスペースを確保 2694 plt.subplots_adjust(bottom=0.2) # 下部に30%のスペースを確保 2695 2696 plt.tight_layout() 2697 # グラフの保存または表示 2698 if save_fig: 2699 if output_dirpath is None: 2700 raise ValueError( 2701 "save_fig = True の場合、 output_dirpath を指定する必要があります。有効なディレクトリパスを指定してください。" 2702 ) 2703 os.makedirs(output_dirpath, exist_ok=True) 2704 output_filepath: str = os.path.join(output_dirpath, output_filename) 2705 plt.savefig(output_filepath, dpi=dpi, bbox_inches="tight") 2706 if show_fig: 2707 plt.show() 2708 plt.close(fig=fig) 2709 2710 # 統計情報の表示 2711 if print_summary: 2712 for data, label in [ 2713 (data_weekday, "Weekdays"), 2714 (data_holiday, "Weekends & Holidays"), 2715 ]: 2716 hourly_means = data.groupby(pd.to_datetime(data.index).hour)[ 2717 ["ch4_gas", "ch4_bio"] 2718 ].mean() 2719 2720 print(f"\n{label}の統計:") 2721 2722 # 都市ガス起源の統計 2723 gas_flux = hourly_means["ch4_gas"] 2724 bio_flux = hourly_means["ch4_bio"] 2725 2726 # 昼夜の時間帯を定義 2727 daytime_range: list[int] = [6, 19] # m~n時の場合、[m ,(n+1)]と定義 2728 daytime_hours = range(daytime_range[0], daytime_range[1]) 2729 nighttime_hours = list(range(0, daytime_range[0])) + list( 2730 range(daytime_range[1], 24) 2731 ) 2732 2733 # 昼間の統計 2734 daytime_gas = pd.Series(gas_flux).iloc[np.array(list(daytime_hours))] 2735 daytime_bio = pd.Series(bio_flux).iloc[np.array(list(daytime_hours))] 2736 daytime_total = daytime_gas + daytime_bio 2737 daytime_total = daytime_gas + daytime_bio 2738 daytime_ratio = (daytime_gas.sum() / daytime_total.sum()) * 100 2739 2740 # 夜間の統計 2741 nighttime_gas = pd.Series(gas_flux).iloc[ 2742 np.array(list(nighttime_hours)) 2743 ] 2744 nighttime_bio = pd.Series(bio_flux).iloc[ 2745 np.array(list(nighttime_hours)) 2746 ] 2747 nighttime_total = nighttime_gas + nighttime_bio 2748 nighttime_ratio = (nighttime_gas.sum() / nighttime_total.sum()) * 100 2749 2750 print("\n都市ガス起源:") 2751 print(f" 平均値: {gas_flux.mean():.2f}") 2752 print(f" 最小値: {gas_flux.min():.2f} (Hour: {gas_flux.idxmin()})") 2753 print(f" 最大値: {gas_flux.max():.2f} (Hour: {gas_flux.idxmax()})") 2754 if gas_flux.min() != 0: 2755 print(f" 最大/最小比: {gas_flux.max() / gas_flux.min():.2f}") 2756 print( 2757 f" 全体に占める割合: {(gas_flux.sum() / (gas_flux.sum() + hourly_means['ch4_bio'].sum()) * 100):.1f}%" 2758 ) 2759 print( 2760 f" 昼間({daytime_range[0]}~{daytime_range[1] - 1}時)の割合: {daytime_ratio:.1f}%" 2761 ) 2762 print( 2763 f" 夜間({daytime_range[1] - 1}~{daytime_range[0]}時)の割合: {nighttime_ratio:.1f}%" 2764 ) 2765 2766 # 生物起源の統計 2767 bio_flux = hourly_means["ch4_bio"] 2768 print("\n生物起源:") 2769 print(f" 平均値: {bio_flux.mean():.2f}") 2770 print(f" 最小値: {bio_flux.min():.2f} (Hour: {bio_flux.idxmin()})") 2771 print(f" 最大値: {bio_flux.max():.2f} (Hour: {bio_flux.idxmax()})") 2772 if bio_flux.min() != 0: 2773 print(f" 最大/最小比: {bio_flux.max() / bio_flux.min():.2f}") 2774 print( 2775 f" 全体に占める割合: {(bio_flux.sum() / (gas_flux.sum() + bio_flux.sum()) * 100):.1f}%" 2776 ) 2777 2778 # 合計フラックスの統計 2779 total_flux = gas_flux + bio_flux 2780 print("\n合計:") 2781 print(f" 平均値: {total_flux.mean():.2f}") 2782 print(f" 最小値: {total_flux.min():.2f} (Hour: {total_flux.idxmin()})") 2783 print(f" 最大値: {total_flux.max():.2f} (Hour: {total_flux.idxmax()})") 2784 if total_flux.min() != 0: 2785 print(f" 最大/最小比: {total_flux.max() / total_flux.min():.2f}")
CH4フラックスの都市ガス起源と生物起源の日変化を平日・休日別に表示します。
Parameters
df: pd.DataFrame
CH4フラックスとC2H6フラックスのデータを含むデータフレームを指定します。
col_ch4_flux: str
CH4フラックスのカラム名を指定します。
col_c2h6_flux: str
C2H6フラックスのカラム名を指定します。
col_datetime: str, optional
日時カラムの名前を指定します。デフォルト値は"Date"です。
color_bio: str, optional
生物起源のプロット色を指定します。デフォルト値は"blue"です。
color_gas: str, optional
都市ガス起源のプロット色を指定します。デフォルト値は"red"です。
label_bio: str, optional
生物起源の凡例ラベルを指定します。デフォルト値は"bio"です。
label_gas: str, optional
都市ガスの凡例ラベルを指定します。デフォルト値は"gas"です。
flux_alpha: float, optional
フラックスプロットの透明度を指定します。デフォルト値は0.6です。
output_dirpath: str | Path | None, optional
出力ディレクトリのパスを指定します。save_fig=Trueの場合は必須です。
output_filename: str, optional
出力ファイル名を指定します。デフォルト値は"source_contributions_by_date.png"です。
add_xlabel: bool, optional
x軸のラベルを表示するかどうかを指定します。デフォルト値はTrueです。
add_ylabel: bool, optional
y軸のラベルを表示するかどうかを指定します。デフォルト値はTrueです。
add_legend: bool, optional
凡例を表示するかどうかを指定します。デフォルト値はTrueです。
print_summary: bool, optional
統計情報を表示するかどうかを指定します。デフォルト値はFalseです。
subplot_fontsize: int, optional
サブプロットのフォントサイズを指定します。デフォルト値は20です。
subplot_label_weekday: str | None, optional
平日グラフのラベルを指定します。デフォルト値はNoneです。
subplot_label_weekend: str | None, optional
休日グラフのラベルを指定します。デフォルト値はNoneです。
y_max: float | None, optional
y軸の上限値を指定します。デフォルト値はNoneです。
figsize: tuple[float, float], optional
プロットのサイズを(幅, 高さ)で指定します。デフォルト値は(12, 5)です。
dpi: float | None, optional
プロットの解像度を指定します。デフォルト値は350です。
save_fig: bool, optional
プロットを保存するかどうかを指定します。デフォルト値はTrueです。
show_fig: bool, optional
プロットを表示するかどうかを指定します。デフォルト値はTrueです。
Examples
>>> df = pd.DataFrame({
... 'Date': pd.date_range('2024-01-01', periods=48, freq='H'),
... 'Fch4': [1.2] * 48,
... 'Fc2h6': [0.1] * 48
... })
>>> generator = MonthlyFiguresGenerator()
>>> generator.plot_source_contributions_diurnal_by_date(
... df=df,
... col_ch4_flux='Fch4',
... col_c2h6_flux='Fc2h6',
... output_dirpath='output'
... )
2787 def plot_wind_rose_sources( 2788 self, 2789 df: pd.DataFrame, 2790 output_dirpath: str | Path | None = None, 2791 output_filename: str = "edp_wind_rose.png", 2792 col_datetime: str = "Date", 2793 col_ch4_flux: str = "Fch4", 2794 col_c2h6_flux: str = "Fc2h6", 2795 col_wind_dir: str = "Wind direction", 2796 flux_unit: str = r"(nmol m$^{-2}$ s$^{-1}$)", 2797 ymax: float | None = None, 2798 color_bio: str = "blue", 2799 color_gas: str = "red", 2800 label_bio: str = "生物起源", 2801 label_gas: str = "都市ガス起源", 2802 flux_alpha: float = 0.4, 2803 num_directions: int = 8, 2804 gap_degrees: float = 0.0, 2805 center_on_angles: bool = True, 2806 subplot_label: str | None = None, 2807 add_legend: bool = True, 2808 stack_bars: bool = True, 2809 print_summary: bool = False, 2810 figsize: tuple[float, float] = (8, 8), 2811 dpi: float | None = 350, 2812 save_fig: bool = True, 2813 show_fig: bool = True, 2814 ) -> None: 2815 """CH4フラックスの都市ガス起源と生物起源の風配図を作成します。 2816 2817 Parameters 2818 ---------- 2819 df: pd.DataFrame 2820 風配図を作成するためのデータフレーム 2821 output_dirpath: str | Path | None, optional 2822 生成された図を保存するディレクトリのパス。デフォルトはNone 2823 output_filename: str, optional 2824 保存するファイル名。デフォルトは"edp_wind_rose.png" 2825 col_datetime: str, optional 2826 日時を示すカラム名。デフォルトは"Date" 2827 col_ch4_flux: str, optional 2828 CH4フラックスを示すカラム名。デフォルトは"Fch4" 2829 col_c2h6_flux: str, optional 2830 C2H6フラックスを示すカラム名。デフォルトは"Fc2h6" 2831 col_wind_dir: str, optional 2832 風向を示すカラム名。デフォルトは"Wind direction" 2833 flux_unit: str, optional 2834 フラックスの単位。デフォルトは"(nmol m$^{-2}$ s$^{-1}$)" 2835 ymax: float | None, optional 2836 y軸の上限値。指定しない場合はデータの最大値に基づいて自動設定。デフォルトはNone 2837 color_bio: str, optional 2838 生物起源のフラックスに対する色。デフォルトは"blue" 2839 color_gas: str, optional 2840 都市ガス起源のフラックスに対する色。デフォルトは"red" 2841 label_bio: str, optional 2842 生物起源のフラックスに対するラベル。デフォルトは"生物起源" 2843 label_gas: str, optional 2844 都市ガス起源のフラックスに対するラベル。デフォルトは"都市ガス起源" 2845 flux_alpha: float, optional 2846 フラックスの透明度。デフォルトは0.4 2847 num_directions: int, optional 2848 風向の数。デフォルトは8 2849 gap_degrees: float, optional 2850 セクター間の隙間の大きさ(度数)。0の場合は隙間なし。デフォルトは0.0 2851 center_on_angles: bool, optional 2852 45度刻みの線を境界として扇形を描画するかどうか。Trueの場合は境界として、Falseの場合は中間(22.5度)を中心として描画。デフォルトはTrue 2853 subplot_label: str | None, optional 2854 サブプロットに表示するラベル。デフォルトはNone 2855 add_legend: bool, optional 2856 凡例を表示するかどうか。デフォルトはTrue 2857 stack_bars: bool, optional 2858 生物起源の上に都市ガス起源を積み上げるかどうか。Trueの場合は積み上げ、Falseの場合は両方を0から積み上げ。デフォルトはTrue 2859 print_summary: bool, optional 2860 統計情報を表示するかどうか。デフォルトはFalse 2861 figsize: tuple[float, float], optional 2862 プロットのサイズ。デフォルトは(8, 8) 2863 dpi: float | None, optional 2864 プロットのdpi。デフォルトは350 2865 save_fig: bool, optional 2866 図を保存するかどうか。デフォルトはTrue 2867 show_fig: bool, optional 2868 図を表示するかどうか。デフォルトはTrue 2869 2870 Returns 2871 ---------- 2872 None 2873 2874 Examples 2875 ---------- 2876 >>> # 基本的な使用方法 2877 >>> generator = MonthlyFiguresGenerator() 2878 >>> generator.plot_wind_rose_sources( 2879 ... df=data, 2880 ... output_dirpath="output/figures", 2881 ... output_filename="wind_rose_2023.png" 2882 ... ) 2883 2884 >>> # カスタマイズした例 2885 >>> generator.plot_wind_rose_sources( 2886 ... df=data, 2887 ... num_directions=16, # 16方位で表示 2888 ... stack_bars=False, # 積み上げない 2889 ... color_bio="green", # 色を変更 2890 ... color_gas="orange" 2891 ... ) 2892 """ 2893 # 起源の計算 2894 df_with_sources = self._calculate_source_contributions( 2895 df=df, 2896 col_ch4_flux=col_ch4_flux, 2897 col_c2h6_flux=col_c2h6_flux, 2898 col_datetime=col_datetime, 2899 ) 2900 2901 # 方位の定義 2902 direction_ranges = self._define_direction_ranges( 2903 num_directions, center_on_angles 2904 ) 2905 2906 # 方位ごとのデータを集計 2907 direction_data = self._aggregate_direction_data( 2908 df_with_sources, col_wind_dir, direction_ranges 2909 ) 2910 2911 # プロットの作成 2912 fig = plt.figure(figsize=figsize, dpi=dpi) 2913 ax = fig.add_subplot(111, projection="polar") 2914 2915 # 方位の角度(ラジアン)を計算 2916 theta = np.array( 2917 [np.radians(angle) for angle in direction_data["center_angle"]] 2918 ) 2919 2920 # セクターの幅を計算(隙間を考慮) 2921 sector_width = np.radians((360.0 / num_directions) - gap_degrees) 2922 2923 # 積み上げ方式に応じてプロット 2924 if stack_bars: 2925 # 生物起源を基準として描画 2926 ax.bar( 2927 theta, 2928 direction_data["bio_flux"], 2929 width=sector_width, # 隙間を考慮した幅 2930 bottom=0.0, 2931 color=color_bio, 2932 alpha=flux_alpha, 2933 label=label_bio, 2934 ) 2935 # 都市ガス起源を生物起源の上に積み上げ 2936 ax.bar( 2937 theta, 2938 direction_data["gas_flux"], 2939 width=sector_width, # 隙間を考慮した幅 2940 bottom=direction_data["bio_flux"], 2941 color=color_gas, 2942 alpha=flux_alpha, 2943 label=label_gas, 2944 ) 2945 else: 2946 # 両方を0から積み上げ 2947 ax.bar( 2948 theta, 2949 direction_data["bio_flux"], 2950 width=sector_width, # 隙間を考慮した幅 2951 bottom=0.0, 2952 color=color_bio, 2953 alpha=flux_alpha, 2954 label=label_bio, 2955 ) 2956 ax.bar( 2957 theta, 2958 direction_data["gas_flux"], 2959 width=sector_width, # 隙間を考慮した幅 2960 bottom=0.0, 2961 color=color_gas, 2962 alpha=flux_alpha, 2963 label=label_gas, 2964 ) 2965 2966 # y軸の範囲を設定 2967 if ymax is not None: 2968 ax.set_ylim(0, ymax) 2969 else: 2970 # データの最大値に基づいて自動設定 2971 max_value = max( 2972 direction_data["bio_flux"].max(), direction_data["gas_flux"].max() 2973 ) 2974 ax.set_ylim(0, max_value * 1.1) # 最大値の1.1倍を上限に設定 2975 2976 # 方位ラベルの設定 2977 # 北を上に設定 2978 ax.set_theta_zero_location("N") # type:ignore 2979 # 時計回りに設定 2980 ax.set_theta_direction(-1) # type:ignore 2981 2982 # 方位ラベルの表示 2983 labels = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"] 2984 angles = np.radians(np.linspace(0, 360, len(labels), endpoint=False)) 2985 ax.set_xticks(angles) 2986 ax.set_xticklabels(labels) 2987 2988 # プロット領域の調整(上部と下部にスペースを確保) 2989 plt.subplots_adjust( 2990 top=0.8, # 上部に20%のスペースを確保 2991 bottom=0.2, # 下部に20%のスペースを確保(凡例用) 2992 ) 2993 2994 # サブプロットラベルの追加(デフォルトは左上) 2995 if subplot_label: 2996 ax.text( 2997 0.01, 2998 0.99, 2999 subplot_label, 3000 transform=ax.transAxes, 3001 ) 3002 3003 # 単位の追加(図の下部中央に配置) 3004 plt.figtext( 3005 0.5, # x位置(中央) 3006 0.1, # y位置(下部) 3007 flux_unit, 3008 ha="center", # 水平方向の位置揃え 3009 va="bottom", # 垂直方向の位置揃え 3010 ) 3011 3012 # 凡例の追加(単位の下に配置) 3013 if add_legend: 3014 # 最初のプロットから凡例のハンドルとラベルを取得 3015 handles, labels = ax.get_legend_handles_labels() 3016 # 図の下部に凡例を配置 3017 fig.legend( 3018 handles, 3019 labels, 3020 loc="center", 3021 bbox_to_anchor=(0.5, 0.05), # x=0.5で中央、y=0.05で下部に配置 3022 ncol=len(handles), # ハンドルの数だけ列を作成(一行に表示) 3023 ) 3024 3025 # グラフの保存または表示 3026 if save_fig: 3027 if output_dirpath is None: 3028 raise ValueError( 3029 "save_fig = True の場合、 output_dirpath を指定する必要があります。有効なディレクトリパスを指定してください。" 3030 ) 3031 os.makedirs(output_dirpath, exist_ok=True) 3032 output_filepath: str = os.path.join(output_dirpath, output_filename) 3033 plt.savefig(output_filepath, dpi=dpi, bbox_inches="tight") 3034 if show_fig: 3035 plt.show() 3036 plt.close(fig=fig) 3037 3038 # 統計情報の表示 3039 if print_summary: 3040 for source in ["gas", "bio"]: 3041 flux_data = direction_data[f"{source}_flux"] 3042 mean_val = flux_data.mean() 3043 max_val = flux_data.max() 3044 max_dir = direction_data.loc[flux_data.idxmax(), "name"] 3045 3046 self.logger.info( 3047 f"{label_gas if source == 'gas' else label_bio}の統計:" 3048 ) 3049 print(f" 平均フラックス: {mean_val:.2f}") 3050 print(f" 最大フラックス: {max_val:.2f}") 3051 print(f" 最大フラックスの方位: {max_dir}")
CH4フラックスの都市ガス起源と生物起源の風配図を作成します。
Parameters
df: pd.DataFrame
風配図を作成するためのデータフレーム
output_dirpath: str | Path | None, optional
生成された図を保存するディレクトリのパス。デフォルトはNone
output_filename: str, optional
保存するファイル名。デフォルトは"edp_wind_rose.png"
col_datetime: str, optional
日時を示すカラム名。デフォルトは"Date"
col_ch4_flux: str, optional
CH4フラックスを示すカラム名。デフォルトは"Fch4"
col_c2h6_flux: str, optional
C2H6フラックスを示すカラム名。デフォルトは"Fc2h6"
col_wind_dir: str, optional
風向を示すカラム名。デフォルトは"Wind direction"
flux_unit: str, optional
フラックスの単位。デフォルトは"(nmol m$^{-2}$ s$^{-1}$)"
ymax: float | None, optional
y軸の上限値。指定しない場合はデータの最大値に基づいて自動設定。デフォルトはNone
color_bio: str, optional
生物起源のフラックスに対する色。デフォルトは"blue"
color_gas: str, optional
都市ガス起源のフラックスに対する色。デフォルトは"red"
label_bio: str, optional
生物起源のフラックスに対するラベル。デフォルトは"生物起源"
label_gas: str, optional
都市ガス起源のフラックスに対するラベル。デフォルトは"都市ガス起源"
flux_alpha: float, optional
フラックスの透明度。デフォルトは0.4
num_directions: int, optional
風向の数。デフォルトは8
gap_degrees: float, optional
セクター間の隙間の大きさ(度数)。0の場合は隙間なし。デフォルトは0.0
center_on_angles: bool, optional
45度刻みの線を境界として扇形を描画するかどうか。Trueの場合は境界として、Falseの場合は中間(22.5度)を中心として描画。デフォルトはTrue
subplot_label: str | None, optional
サブプロットに表示するラベル。デフォルトはNone
add_legend: bool, optional
凡例を表示するかどうか。デフォルトはTrue
stack_bars: bool, optional
生物起源の上に都市ガス起源を積み上げるかどうか。Trueの場合は積み上げ、Falseの場合は両方を0から積み上げ。デフォルトはTrue
print_summary: bool, optional
統計情報を表示するかどうか。デフォルトはFalse
figsize: tuple[float, float], optional
プロットのサイズ。デフォルトは(8, 8)
dpi: float | None, optional
プロットのdpi。デフォルトは350
save_fig: bool, optional
図を保存するかどうか。デフォルトはTrue
show_fig: bool, optional
図を表示するかどうか。デフォルトはTrue
Returns
None
Examples
>>> # 基本的な使用方法
>>> generator = MonthlyFiguresGenerator()
>>> generator.plot_wind_rose_sources(
... df=data,
... output_dirpath="output/figures",
... output_filename="wind_rose_2023.png"
... )
>>> # カスタマイズした例
>>> generator.plot_wind_rose_sources(
... df=data,
... num_directions=16, # 16方位で表示
... stack_bars=False, # 積み上げない
... color_bio="green", # 色を変更
... color_gas="orange"
... )
3342 @staticmethod 3343 def get_valid_data(df: pd.DataFrame, col_x: str, col_y: str) -> pd.DataFrame: 3344 """指定された列の有効なデータ(NaNを除いた)を取得します。 3345 3346 Parameters 3347 ---------- 3348 df: pd.DataFrame 3349 データフレームを指定します。 3350 col_x: str 3351 X軸の列名を指定します。 3352 col_y: str 3353 Y軸の列名を指定します。 3354 3355 Returns 3356 ---------- 3357 pd.DataFrame 3358 有効なデータのみを含むDataFrameを返します。 3359 3360 Examples 3361 -------- 3362 >>> df = pd.DataFrame({ 3363 ... 'x': [1, 2, np.nan, 4], 3364 ... 'y': [1, np.nan, 3, 4] 3365 ... }) 3366 >>> valid_df = MonthlyFiguresGenerator.get_valid_data(df, 'x', 'y') 3367 >>> print(valid_df) 3368 x y 3369 0 1 1 3370 3 4 4 3371 """ 3372 return df.copy().dropna(subset=[col_x, col_y])
指定された列の有効なデータ(NaNを除いた)を取得します。
Parameters
df: pd.DataFrame
データフレームを指定します。
col_x: str
X軸の列名を指定します。
col_y: str
Y軸の列名を指定します。
Returns
pd.DataFrame
有効なデータのみを含むDataFrameを返します。
Examples
>>> df = pd.DataFrame({
... 'x': [1, 2, np.nan, 4],
... 'y': [1, np.nan, 3, 4]
... })
>>> valid_df = MonthlyFiguresGenerator.get_valid_data(df, 'x', 'y')
>>> print(valid_df)
x y
0 1 1
3 4 4
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 power_ylabel: str 75 パワースペクトルのy軸ラベル。 76 LaTeXの数式表記が使用可能(例r"$fS_{\\mathrm{CH_4}} / s_{\\mathrm{CH_4}}^2$")。 77 co_ylabel: str 78 コスペクトルのy軸ラベル。 79 LaTeXの数式表記が使用可能(例:r"$fC_{w\\mathrm{CH_4}} / \\overline{w'\\mathrm{CH_4}'}$")。 80 color: str 81 プロットの色。matplotlib.colorsで定義されている色名または16進数カラーコードを指定。 82 label: str | None, optional 83 凡例に表示するラベル。Noneの場合、凡例は表示されない。デフォルトはNone。 84 85 Examples 86 -------- 87 >>> ch4_config = SpectralPlotConfig( 88 ... power_ylabel=r"$fS_{\\mathrm{CH_4}} / s_{\\mathrm{CH_4}}^2$", 89 ... co_ylabel=r"$fC_{w\\mathrm{CH_4}} / \\overline{w'\\mathrm{CH_4}'}$", 90 ... color="red", 91 ... label="CH4" 92 ... ) 93 """ 94 95 power_ylabel: str 96 co_ylabel: str 97 color: str 98 label: str | None = None
スペクトルプロット設定用のデータクラス
Parameters
power_ylabel: str
パワースペクトルのy軸ラベル。
LaTeXの数式表記が使用可能(例r"$fS_{\mathrm{CH_4}} / s_{\mathrm{CH_4}}^2$")。
co_ylabel: str
コスペクトルのy軸ラベル。
LaTeXの数式表記が使用可能(例:r"$fC_{w\mathrm{CH_4}} / \overline{w'\mathrm{CH_4}'}$")。
color: str
プロットの色。matplotlib.colorsで定義されている色名または16進数カラーコードを指定。
label: str | None, optional
凡例に表示するラベル。Noneの場合、凡例は表示されない。デフォルトはNone。
Examples
>>> ch4_config = SpectralPlotConfig(
... power_ylabel=r"$fS_{\mathrm{CH_4}} / s_{\mathrm{CH_4}}^2$",
... co_ylabel=r"$fC_{w\mathrm{CH_4}} / \overline{w'\mathrm{CH_4}'}$",
... color="red",
... label="CH4"
... )
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_filepaths: list[str | Path] | None = None, 60 font_size: float = 20, 61 legend_size: float = 20, 62 tick_size: float = 20, 63 title_size: float = 20, 64 plot_params: dict[str, Any] | None = None, 65) -> None: 66 """ 67 matplotlibのプロットパラメータを設定します。 68 69 Parameters 70 ---------- 71 font_family: list[str] | None, optional 72 使用するフォントファミリーのリスト。デフォルト値は["Arial", "MS Gothic", "sans-serif"]です。 73 font_filepaths: list[str | Path] | None, optional 74 フォントファイルのパスのリスト。デフォルト値はNoneです。指定された場合、fontManagerでフォントを登録します。 75 font_size: float, optional 76 軸ラベルのフォントサイズ。デフォルト値は20です。 77 legend_size: float, optional 78 凡例のフォントサイズ。デフォルト値は20です。 79 tick_size: float, optional 80 軸目盛りのフォントサイズ。デフォルト値は20です。 81 title_size: float, optional 82 タイトルのフォントサイズ。デフォルト値は20です。 83 plot_params: dict[str, Any] | None, optional 84 matplotlibのプロットパラメータの辞書。デフォルト値はNoneです。指定された場合、デフォルトのパラメータに上書きされます。 85 86 Returns 87 ------- 88 None 89 戻り値はありません。 90 91 Examples 92 -------- 93 >>> # デフォルト設定でプロットパラメータを設定 94 >>> setup_plot_params() 95 96 >>> # カスタムフォントとサイズを指定 97 >>> setup_plot_params( 98 ... font_family=["Helvetica", "sans-serif"], 99 ... font_size=16, 100 ... legend_size=14 101 ... ) 102 103 >>> # カスタムプロットパラメータを追加 104 >>> custom_params = {"figure.figsize": (10, 6), "lines.linewidth": 2} 105 >>> setup_plot_params(plot_params=custom_params) 106 """ 107 # フォントファイルの登録 108 if font_filepaths: 109 for path in font_filepaths: 110 if not os.path.exists(path): 111 raise FileNotFoundError(f"The font file at {path} does not exist.") 112 fm.fontManager.addfont(path) 113 114 # デフォルト値の設定 115 if font_family is None: 116 font_family = ["Arial", "MS Gothic", "sans-serif"] 117 118 # デフォルトのプロットパラメータ 119 default_params = { 120 "axes.linewidth": 1.0, 121 "axes.titlesize": title_size, # タイトル 122 "grid.color": "gray", 123 "grid.linewidth": 1.0, 124 "font.family": font_family, 125 "font.size": font_size, # 軸ラベル 126 "legend.fontsize": legend_size, # 凡例 127 "text.color": "black", 128 "xtick.color": "black", 129 "ytick.color": "black", 130 "xtick.labelsize": tick_size, # 軸目盛 131 "ytick.labelsize": tick_size, # 軸目盛 132 "xtick.major.size": 0, 133 "ytick.major.size": 0, 134 "ytick.direction": "out", 135 "ytick.major.width": 1.0, 136 } 137 138 # plot_paramsが定義されている場合、デフォルトに追記 139 if plot_params: 140 default_params.update(plot_params) 141 142 plt.rcParams.update(default_params) # プロットパラメータを更新
matplotlibのプロットパラメータを設定します。
Parameters
font_family: list[str] | None, optional
使用するフォントファミリーのリスト。デフォルト値は["Arial", "MS Gothic", "sans-serif"]です。
font_filepaths: list[str | Path] | None, optional
フォントファイルのパスのリスト。デフォルト値はNoneです。指定された場合、fontManagerでフォントを登録します。
font_size: float, optional
軸ラベルのフォントサイズ。デフォルト値は20です。
legend_size: float, optional
凡例のフォントサイズ。デフォルト値は20です。
tick_size: float, optional
軸目盛りのフォントサイズ。デフォルト値は20です。
title_size: float, optional
タイトルのフォントサイズ。デフォルト値は20です。
plot_params: dict[str, Any] | None, optional
matplotlibのプロットパラメータの辞書。デフォルト値はNoneです。指定された場合、デフォルトのパラメータに上書きされます。
Returns
None 戻り値はありません。
Examples
>>> # デフォルト設定でプロットパラメータを設定
>>> setup_plot_params()
>>> # カスタムフォントとサイズを指定
>>> setup_plot_params(
... font_family=["Helvetica", "sans-serif"],
... font_size=16,
... legend_size=14
... )
>>> # カスタムプロットパラメータを追加
>>> custom_params = {"figure.figsize": (10, 6), "lines.linewidth": 2}
>>> setup_plot_params(plot_params=custom_params)