diff --git a/FFT/FFT_analysis_threshold_ver.py b/FFT/FFT_analysis_threshold_ver.py index 27c3c93..4412e54 100644 --- a/FFT/FFT_analysis_threshold_ver.py +++ b/FFT/FFT_analysis_threshold_ver.py @@ -7,197 +7,307 @@ from pathlib import Path -# =============================== -# Sampling rate 계산 -# =============================== -def calculate_sample_rate(df, time_column="Time_us"): - if time_column not in df.columns: - time_column = df.columns[0] - - time_data = df[time_column].dropna().values - time_diffs = np.diff(time_data) - avg_dt_sec = np.mean(time_diffs) / 1_000_000 - return 1.0 / avg_dt_sec +# 저역통과 필터 함수 +def butter_lowpass(cutoff, fs, order=5): + nyq = 0.5 * fs + normal_cutoff = cutoff / nyq + b, a = butter(order, normal_cutoff, btype='low', analog=False) + return b, a -# =============================== -# Butterworth LPF -# =============================== -def apply_butterworth_filter(data, sample_rate, cutoff=100.0, order=4): - nyq = 0.5 * sample_rate - normal_cutoff = cutoff / nyq - b, a = butter(order, normal_cutoff, btype="low") +def butter_lowpass_filter(data, cutoff, fs, order=5): + b, a = butter_lowpass(cutoff, fs, order=order) return filtfilt(b, a, data) -# =============================== -# Band-limited CV -# =============================== -def calculate_band_cv(freqs, amps, f_min=1.0, f_max=100.0): - band = (freqs >= f_min) & (freqs <= f_max) - band_amps = amps[band] +# 샘플링 레이트 계산 함수 +def calculate_sample_rate(df, time_column='Time_us'): + """ + CSV 파일의 시간 열(마이크로초)에서 샘플링 레이트를 계산합니다. + """ + if time_column not in df.columns: + # 시간 열이 없으면 첫 번째 열을 시간으로 간주 + time_column = df.columns[0] - mean_val = np.mean(band_amps) - if mean_val < 1e-12: - return np.nan + time_data = df[time_column].dropna().values - return np.std(band_amps) / mean_val * 100.0 + if len(time_data) < 2: + raise ValueError("시간 데이터가 충분하지 않습니다.") + # 시간 간격 계산 (마이크로초 단위) + time_diffs = np.diff(time_data) -# =============================== -# FFT 분석 -# =============================== -def fft_analysis(data, sample_rate, window_size=4096, fft_size=16384): - data = data[:window_size] + # 평균 시간 간격 (마이크로초) + avg_time_diff_us = np.mean(time_diffs) + + # 평균 시간 간격을 초 단위로 변환 + avg_time_diff_sec = avg_time_diff_us / 1_000_000 + + # 샘플링 레이트 = 1 / 평균 시간 간격 + sample_rate = 1 / avg_time_diff_sec + + return sample_rate + + +# Threshold 계산 함수 +def calculate_threshold(amps, method="std", n_std=2.75, recon_error_value=0.3): + if method == "std": + mean = np.mean(amps) + std = np.std(amps) + threshold = mean + n_std * std + elif method == "percentile": + threshold = np.percentile(amps, 97.5) + elif method == "recon_error": + threshold = recon_error_value + else: + raise ValueError("지원하지 않는 threshold 방법입니다.") + return threshold + + +# FFT 분석 함수 +def fft_analysis( + data, + sample_rate=296, + fft_size=None, + threshold_method="std", + n_std=2.75, + recon_error_value=0.3 +): data = data - np.mean(data) - - if fft_size > window_size: - data = np.pad(data, (0, fft_size - window_size)) + n = fft_size if fft_size else len(data) + data = data[:n] y = fft(data) - x = fftfreq(fft_size, 1 / sample_rate) - - freqs = x[:fft_size // 2] - amps = np.abs(y[:fft_size // 2]) * 2 / window_size - - resonance_freq = freqs[np.argmax(amps)] - return freqs, amps, resonance_freq - - -# =============================== -# Class 추출 함수 (★ 추가된 부분) -# =============================== -def extract_class_from_filename(filename): - parts = filename.split("_") - for p in parts: - if p.lower().startswith("class"): - return p - return "Unknown" - - -# =============================== -# 단일 파일 분석 -# =============================== + x = fftfreq(n, 1 / sample_rate) + positive_freqs = x[:n // 2] + positive_amps = np.abs(y[:n // 2]) * 2 / n + resonance_freq = positive_freqs[np.argmax(positive_amps)] + + threshold = calculate_threshold(positive_amps, threshold_method, n_std, recon_error_value) + + # Threshold 이상 구간 탐색 + threshold_ranges = [] + above_threshold = positive_amps >= threshold + in_range = False + for i in range(len(positive_freqs)): + if above_threshold[i] and not in_range: + range_start = positive_freqs[i] + in_range = True + elif not above_threshold[i] and in_range: + range_end = positive_freqs[i - 1] + threshold_ranges.append((range_start, range_end)) + in_range = False + if in_range: + threshold_ranges.append((range_start, positive_freqs[-1])) + + return positive_freqs, positive_amps, resonance_freq, threshold_ranges, threshold + + +# 단일 파일 분석 함수 def analyze_single_file( - file_path, - axes, - window_size=4096, - fft_size=16384, - cutoff_freq=100.0, - time_column="Time_us" + file_path, + axes, + fft_size=8192, + apply_filter=True, + filter_order=5, + threshold_method="std", + n_std=2.75, + recon_error_value=0.3, + time_column='Time_us' ): df = pd.read_csv(file_path) file_title = os.path.splitext(os.path.basename(file_path))[0] - class_name = extract_class_from_filename(file_title) + # 샘플링 레이트 자동 계산 sample_rate = calculate_sample_rate(df, time_column) - print(f" Sample rate: {sample_rate:.2f} Hz") + print(f" 계산된 샘플링 레이트: {sample_rate:.2f} Hz") results = [] + + num_rows = 2 + num_cols = 3 + plt.figure(figsize=(18, 8)) for idx, axis in enumerate(axes): if axis not in df.columns: + print(f"[경고] {axis} 열이 CSV 파일에 없습니다: {file_path}") continue data = df[axis].dropna().values - data = apply_butterworth_filter( - data, - sample_rate=sample_rate, - cutoff=cutoff_freq, - order=4 - ) + if apply_filter: + cutoff = sample_rate / 4 + data = butter_lowpass_filter(data, cutoff=cutoff, fs=sample_rate, order=filter_order) - freqs, amps, resonance_freq = fft_analysis( + freqs, amps, resonance_freq, threshold_ranges, threshold = fft_analysis( data, - sample_rate, - window_size, - fft_size + sample_rate=sample_rate, + fft_size=fft_size, + threshold_method=threshold_method, + n_std=n_std, + recon_error_value=recon_error_value ) - band_cv = calculate_band_cv(freqs, amps, f_min=0.3, f_max=60.0) + # Threshold 범위를 문자열로 변환 + threshold_ranges_str = "" + if threshold_ranges: + ranges_list = [f"{r[0]:.2f}-{r[1]:.2f}Hz" for r in threshold_ranges] + threshold_ranges_str = "; ".join(ranges_list) + else: + threshold_ranges_str = "없음" results.append({ - "Class": class_name, # ★ 추가 - "File": file_title, - "Axis": axis, - "Resonance_Frequency_Hz": round(resonance_freq, 2), - "Band_CV_1_100Hz": round(band_cv, 2), - "Sample_Rate": round(sample_rate, 2), - "Window_Size": window_size, - "FFT_Size": fft_size, - "Filter": "Butterworth", - "Cutoff_Hz": cutoff_freq + 'File': file_title, + 'Axis': axis, + 'Resonance_Frequency_Hz': round(resonance_freq, 2), + 'Threshold_Value': round(threshold, 4), + 'Threshold_Method': threshold_method, + 'Threshold_Ranges': threshold_ranges_str, + 'Sample_Rate': round(sample_rate, 2), + 'FFT_Size': fft_size, + 'Filter_Applied': apply_filter, + 'Filter_Order': filter_order if apply_filter else 'N/A' }) - plt.subplot(2, 3, idx + 1) - plt.plot(freqs, amps) - plt.axvline(resonance_freq, color="r", linestyle="--") - plt.title(f"{axis} | {resonance_freq:.2f} Hz") + # subplot 그리기 + plt.subplot(num_rows, num_cols, idx + 1) + plt.plot(freqs, amps, label="Amplitude") + plt.axvline(resonance_freq, color='r', linestyle='--', label=f'Resonance: {resonance_freq:.2f} Hz') + plt.axhline(y=threshold, color='g', linestyle=':', label=f'Threshold: {threshold:.3f}') + plt.title(f"{axis}\nResonance: {resonance_freq:.2f} Hz") plt.xlabel("Frequency (Hz)") plt.ylabel("Amplitude") plt.grid(True) + plt.legend() - plt.suptitle(f"FFT Spectrum - {file_title}", fontsize=16) + plt.suptitle(f"FFT Frequency Spectrum - {file_title}", fontsize=16) plt.tight_layout(rect=[0, 0, 1, 0.93]) - plt.savefig( - os.path.join(os.path.dirname(file_path), f"{file_title}_fft.png"), - dpi=150 - ) + plt.savefig(os.path.join(os.path.dirname(file_path), f"{file_title}_fft_plot.png"), dpi=150) plt.close() return results -# =============================== -# 전체 파일 분석 -# =============================== +# 배치 분석 함수 (모든 파일 처리) def analyze_all_files( - data_dir, - axes, - window_size=4096, - fft_size=16384, - cutoff_freq=100.0, - time_column="Time_us" + data_dir, + axes=["Accel_X", "Accel_Y", "Accel_Z", "Gyro_X", "Gyro_Y", "Gyro_Z"], + fft_size=8192, + apply_filter=True, + filter_order=5, + threshold_method="std", + n_std=2.75, + recon_error_value=0.3, + time_column='Time_us' ): + """ + 지정된 디렉토리의 모든 CSV 파일에 대해 FFT 분석을 수행합니다. + """ data_path = Path(data_dir) - csv_files = sorted(data_path.glob("mpu_raw_optimized_*.csv")) + # CSV 파일 목록 가져오기 - mpu_raw_base_ 패턴으로 변경 + csv_files = sorted(list(data_path.glob("mpu_raw_base_*.csv"))) + + if not csv_files: + print(f"[오류] {data_dir}에서 mpu_raw_base_*.csv 파일을 찾을 수 없습니다.") + return None + + print(f"총 {len(csv_files)}개의 파일을 찾았습니다.\n") + print(f"[파일 목록]") + for f in csv_files: + print(f" - {f.name}") + + # 전체 결과를 저장할 리스트 all_results = [] + print(f"\n{'=' * 60}") + print(f"BASE 파일 분석 시작 ({len(csv_files)}개 파일)") + print(f"{'=' * 60}") + + # 모든 파일 처리 for idx, file_path in enumerate(csv_files, 1): - print(f"\n[{idx}/{len(csv_files)}] {file_path.name}") - - file_results = analyze_single_file( - file_path, - axes, - window_size, - fft_size, - cutoff_freq, - time_column - ) - all_results.extend(file_results) + print(f"\n[{idx}/{len(csv_files)}] 분석 중: {file_path.name}") + + try: + file_results = analyze_single_file( + file_path=str(file_path), + axes=axes, + fft_size=fft_size, + apply_filter=apply_filter, + filter_order=filter_order, + threshold_method=threshold_method, + n_std=n_std, + recon_error_value=recon_error_value, + time_column=time_column + ) + + # 클래스 정보를 'base'로 추가 + for result in file_results: + result['Class'] = 'base' + + all_results.extend(file_results) + print(f" ✓ 완료 ({len(file_results)}개 축 분석)") + + except Exception as e: + import traceback + print(f" ✗ 오류 발생: {e}") + print(f" ✗ 상세 오류:") + traceback.print_exc() + continue + + # 전체 결과를 하나의 CSV로 저장 + if all_results: + results_df = pd.DataFrame(all_results) - results_df = pd.DataFrame(all_results) - output_path = data_path / "fft_results_butterworth_bandCV.csv" - results_df.to_csv(output_path, index=False, encoding="utf-8-sig") + # 컬럼 순서 재정렬 + column_order = ['Class', 'File', 'Axis', 'Resonance_Frequency_Hz', + 'Threshold_Value', 'Threshold_Method', 'Threshold_Ranges', + 'Sample_Rate', 'FFT_Size', 'Filter_Applied', 'Filter_Order'] + results_df = results_df[column_order] - print(f"\n결과 저장 완료: {output_path}") - return results_df + output_path = data_path / "fft_analysis_base_results.csv" + results_df.to_csv(output_path, index=False, encoding='utf-8-sig') + + print(f"\n{'=' * 60}") + print(f"[완료] 전체 분석 결과 저장됨: {output_path}") + print(f"총 {len(all_results)}개의 분석 결과 (파일 {len(csv_files)}개 × 축 {len(axes)}개)") + print(f"{'=' * 60}") + + # 요약 통계 + print("\n[분석 요약]") + print(f" 분석된 파일 수: {len(csv_files)}개") + print(f" 분석된 축: {', '.join(axes)}") + print(f" 총 결과 개수: {len(all_results)}개") + + return results_df + else: + print("\n[오류] 분석된 결과가 없습니다.") + print("[가능한 원인]") + print(" 1. 모든 파일에서 예외가 발생했습니다") + print(" 2. CSV 파일의 열 이름이 예상과 다릅니다") + print(" 3. 데이터가 충분하지 않습니다") + return None -# =============================== # 실행 -# =============================== if __name__ == "__main__": - DATA_DIR = "/Users/seohyeon/PycharmProjects/AT_data/data_v1" + # 데이터 디렉토리 경로 설정 + DATA_DIR = "/Users/seohyeon/PycharmProjects/AT_data/data_v2" - analyze_all_files( + # 배치 분석 실행 + results = analyze_all_files( data_dir=DATA_DIR, axes=["Accel_X", "Accel_Y", "Accel_Z", "Gyro_X", "Gyro_Y", "Gyro_Z"], - window_size=4096, - fft_size=16384, - cutoff_freq=100.0, - time_column="Time_us" + fft_size=8192, # FFT 크기 8192로 설정 + apply_filter=True, + filter_order=5, + threshold_method="std", # "std", "percentile", "recon_error" + n_std=2.75, + recon_error_value=0.3, + time_column='Time_us' # 시간 열 이름 (마이크로초 단위) ) + + if results is not None: + print("\n[결과 미리보기]") + print(results.head(10)) \ No newline at end of file diff --git a/FFT/fft_analysis_results_baseline/frequency_comparison_boxplot_base.png b/FFT/fft_analysis_results_baseline/frequency_comparison_boxplot_base.png new file mode 100644 index 0000000..f1d920b Binary files /dev/null and b/FFT/fft_analysis_results_baseline/frequency_comparison_boxplot_base.png differ diff --git a/FFT/fft_analysis_results_baseline/frequency_distribution_base.png b/FFT/fft_analysis_results_baseline/frequency_distribution_base.png new file mode 100644 index 0000000..9572d41 Binary files /dev/null and b/FFT/fft_analysis_results_baseline/frequency_distribution_base.png differ diff --git a/FFT/fft_analysis_results_baseline/mpu_raw_base_10_20251223_200921_fft_plot.png b/FFT/fft_analysis_results_baseline/mpu_raw_base_10_20251223_200921_fft_plot.png new file mode 100644 index 0000000..9efb8ef Binary files /dev/null and b/FFT/fft_analysis_results_baseline/mpu_raw_base_10_20251223_200921_fft_plot.png differ diff --git a/FFT/fft_analysis_results_baseline/mpu_raw_base_2_20251223_194400_fft_plot.png b/FFT/fft_analysis_results_baseline/mpu_raw_base_2_20251223_194400_fft_plot.png new file mode 100644 index 0000000..3a7fb62 Binary files /dev/null and b/FFT/fft_analysis_results_baseline/mpu_raw_base_2_20251223_194400_fft_plot.png differ diff --git a/FFT/fft_analysis_results_baseline/mpu_raw_base_3_20251223_194807_fft_plot.png b/FFT/fft_analysis_results_baseline/mpu_raw_base_3_20251223_194807_fft_plot.png new file mode 100644 index 0000000..c680052 Binary files /dev/null and b/FFT/fft_analysis_results_baseline/mpu_raw_base_3_20251223_194807_fft_plot.png differ diff --git a/FFT/fft_analysis_results_baseline/mpu_raw_base_4_20251223_195053_fft_plot.png b/FFT/fft_analysis_results_baseline/mpu_raw_base_4_20251223_195053_fft_plot.png new file mode 100644 index 0000000..17e2fa3 Binary files /dev/null and b/FFT/fft_analysis_results_baseline/mpu_raw_base_4_20251223_195053_fft_plot.png differ diff --git a/FFT/fft_analysis_results_baseline/mpu_raw_base_5_20251223_195345_fft_plot.png b/FFT/fft_analysis_results_baseline/mpu_raw_base_5_20251223_195345_fft_plot.png new file mode 100644 index 0000000..0e756dd Binary files /dev/null and b/FFT/fft_analysis_results_baseline/mpu_raw_base_5_20251223_195345_fft_plot.png differ diff --git a/FFT/fft_analysis_results_baseline/mpu_raw_base_6_20251223_195633_fft_plot.png b/FFT/fft_analysis_results_baseline/mpu_raw_base_6_20251223_195633_fft_plot.png new file mode 100644 index 0000000..2515414 Binary files /dev/null and b/FFT/fft_analysis_results_baseline/mpu_raw_base_6_20251223_195633_fft_plot.png differ diff --git a/FFT/fft_analysis_results_baseline/mpu_raw_base_7_20251223_195920_fft_plot.png b/FFT/fft_analysis_results_baseline/mpu_raw_base_7_20251223_195920_fft_plot.png new file mode 100644 index 0000000..82b5dc0 Binary files /dev/null and b/FFT/fft_analysis_results_baseline/mpu_raw_base_7_20251223_195920_fft_plot.png differ diff --git a/FFT/fft_analysis_results_baseline/mpu_raw_base_8_20251223_200314_fft_plot.png b/FFT/fft_analysis_results_baseline/mpu_raw_base_8_20251223_200314_fft_plot.png new file mode 100644 index 0000000..0057a34 Binary files /dev/null and b/FFT/fft_analysis_results_baseline/mpu_raw_base_8_20251223_200314_fft_plot.png differ diff --git a/FFT/fft_analysis_results_baseline/mpu_raw_base_9_20251223_200604_fft_plot.png b/FFT/fft_analysis_results_baseline/mpu_raw_base_9_20251223_200604_fft_plot.png new file mode 100644 index 0000000..6ba8fb1 Binary files /dev/null and b/FFT/fft_analysis_results_baseline/mpu_raw_base_9_20251223_200604_fft_plot.png differ diff --git a/FFT/fft_analysis_results_baseline/stats_bar_comparison_base.png b/FFT/fft_analysis_results_baseline/stats_bar_comparison_base.png new file mode 100644 index 0000000..857c9af Binary files /dev/null and b/FFT/fft_analysis_results_baseline/stats_bar_comparison_base.png differ diff --git a/FFT/fft_analysis_results_baseline/stats_bar_mean_comparison_base.png b/FFT/fft_analysis_results_baseline/stats_bar_mean_comparison_base.png new file mode 100644 index 0000000..1ab94ad Binary files /dev/null and b/FFT/fft_analysis_results_baseline/stats_bar_mean_comparison_base.png differ diff --git a/FFT/fft_analysis_results_baseline/stats_heatmaps_base.png b/FFT/fft_analysis_results_baseline/stats_heatmaps_base.png new file mode 100644 index 0000000..2ff4099 Binary files /dev/null and b/FFT/fft_analysis_results_baseline/stats_heatmaps_base.png differ diff --git a/FFT/frequency_error_analysis.py b/FFT/frequency_error_analysis.py index a1111ab..73ed1ef 100644 --- a/FFT/frequency_error_analysis.py +++ b/FFT/frequency_error_analysis.py @@ -12,478 +12,119 @@ def analyze_frequency_distribution(results_csv_path, output_dir=None): """ FFT 분석 결과에서 주파수 오차 분포를 분석합니다. - - Parameters: - ----------- - results_csv_path : str - FFT 분석 결과 CSV 파일 경로 - output_dir : str - 결과 저장 디렉토리 (None이면 CSV와 같은 위치) + (모든 파일을 'base' 클래스로 통일) """ - # 결과 파일 읽기 + # 1. 파일 읽기 및 경로 설정 + if not Path(results_csv_path).exists(): + print(f"Error: 파일을 찾을 수 없습니다 -> {results_csv_path}") + return None + df = pd.read_csv(results_csv_path) + # [변경 사항] 모든 데이터를 'base' 클래스로 통일 + df['Class'] = 'base' + if output_dir is None: output_dir = Path(results_csv_path).parent else: output_dir = Path(output_dir) + output_dir.mkdir(parents=True, exist_ok=True) - # 클래스와 축 목록 - classes = df['Class'].unique() axes = df['Axis'].unique() print("=" * 70) - print("주파수 오차 분포 분석") + print(f"주파수 오차 및 통계 분석 (Base 모드)") print("=" * 70) - # 1. 클래스별, 축별 통계 - print("\n[1] 클래스별, 축별 공진 주파수 통계") - print("-" * 70) - + # 2. 축별 통계 계산 stats_list = [] - for class_name in classes: - for axis in axes: - data = df[(df['Class'] == class_name) & (df['Axis'] == axis)] - - if len(data) > 0: - freqs = data['Resonance_Frequency_Hz'].values - stats = { - 'Class': class_name, - 'Axis': axis, - 'Count': len(freqs), - 'Mean': np.mean(freqs), - 'Std': np.std(freqs), - 'Min': np.min(freqs), - 'Max': np.max(freqs), - 'Range': np.max(freqs) - np.min(freqs), - 'CV(%)': (np.std(freqs) / np.mean(freqs)) * 100 # 변동계수 - } - stats_list.append(stats) - - print(f"\n{class_name.upper()} - {axis}") - print(f" 샘플 수: {stats['Count']}") - print(f" 평균: {stats['Mean']:.2f} Hz") - print(f" 표준편차: {stats['Std']:.2f} Hz") - print(f" 범위: {stats['Min']:.2f} ~ {stats['Max']:.2f} Hz") - print(f" 변동계수: {stats['CV(%)']:.2f}%") + for axis in axes: + data = df[df['Axis'] == axis]['Resonance_Frequency_Hz'].values + if len(data) > 0: + stats = { + 'Class': 'base', + 'Axis': axis, + 'Count': len(data), + 'Mean': np.mean(data), + 'Std': np.std(data), + 'Min': np.min(data), + 'Max': np.max(data), + 'Range': np.max(data) - np.min(data), + 'CV(%)': (np.std(data) / np.mean(data)) * 100 + } + stats_list.append(stats) stats_df = pd.DataFrame(stats_list) - stats_output = output_dir / "frequency_statistics.csv" + stats_output = output_dir / "frequency_statistics_base.csv" stats_df.to_csv(stats_output, index=False, encoding='utf-8-sig') - print(f"\n✓ 통계 저장: {stats_output}") - - # 2. 클래스별 히스토그램 (축별로 서브플롯) - print("\n[2] 클래스별 주파수 분포 히스토그램 생성 중...") - - for class_name in classes: - class_data = df[df['Class'] == class_name] - - if len(class_data) == 0: - continue - - fig, axes_plot = plt.subplots(2, 3, figsize=(18, 10)) - fig.suptitle(f'공진 주파수 분포 - {class_name.upper()}', fontsize=16, fontweight='bold') - - for idx, axis in enumerate(axes): - row = idx // 3 - col = idx % 3 - ax = axes_plot[row, col] - - axis_data = class_data[class_data['Axis'] == axis]['Resonance_Frequency_Hz'].values - - if len(axis_data) > 0: - # 히스토그램 - ax.hist(axis_data, bins=20, alpha=0.7, color='steelblue', edgecolor='black') - - # 통계 정보 표시 - mean_val = np.mean(axis_data) - std_val = np.std(axis_data) - - ax.axvline(mean_val, color='red', linestyle='--', linewidth=2, - label=f'평균: {mean_val:.2f} Hz') - ax.axvline(mean_val - std_val, color='orange', linestyle=':', linewidth=1.5, - label=f'±1σ: {std_val:.2f} Hz') - ax.axvline(mean_val + std_val, color='orange', linestyle=':', linewidth=1.5) - - ax.set_title(f'{axis}\n(n={len(axis_data)})', fontweight='bold') - ax.set_xlabel('주파수 (Hz)') - ax.set_ylabel('빈도') - ax.legend() - ax.grid(True, alpha=0.3) - else: - ax.text(0.5, 0.5, '데이터 없음', ha='center', va='center', - transform=ax.transAxes, fontsize=12) - ax.set_title(axis) - - plt.tight_layout() - hist_output = output_dir / f"frequency_distribution_{class_name}.png" - plt.savefig(hist_output, dpi=150, bbox_inches='tight') - plt.close() - print(f" ✓ {class_name}: {hist_output}") - - # 3. 축별 박스플롯 (클래스 비교) - tick_labels로 수정 - print("\n[3] 축별 클래스 비교 박스플롯 생성 중...") - - fig, axes_plot = plt.subplots(2, 3, figsize=(18, 10)) - fig.suptitle('축별 공진 주파수 비교 (클래스별)', fontsize=16, fontweight='bold') - - for idx, axis in enumerate(axes): - row = idx // 3 - col = idx % 3 - ax = axes_plot[row, col] - - # 클래스별 데이터 준비 - data_for_box = [] - labels_for_box = [] - - for class_name in classes: - axis_data = df[(df['Class'] == class_name) & (df['Axis'] == axis)]['Resonance_Frequency_Hz'].values - if len(axis_data) > 0: - data_for_box.append(axis_data) - labels_for_box.append(class_name) - - if len(data_for_box) > 0: - bp = ax.boxplot(data_for_box, tick_labels=labels_for_box, patch_artist=True) - - # 박스 색상 설정 - colors = ['lightblue', 'lightgreen', 'lightyellow', 'lightcoral', 'plum'] - for patch, color in zip(bp['boxes'], colors[:len(bp['boxes'])]): - patch.set_facecolor(color) - - ax.set_title(f'{axis}', fontweight='bold', fontsize=12) - ax.set_ylabel('주파수 (Hz)') - ax.set_xlabel('클래스') - ax.grid(True, alpha=0.3, axis='y') - ax.tick_params(axis='x', rotation=45) - else: - ax.text(0.5, 0.5, '데이터 없음', ha='center', va='center', - transform=ax.transAxes, fontsize=12) - ax.set_title(axis) - - plt.tight_layout() - boxplot_output = output_dir / "frequency_comparison_boxplot.png" - plt.savefig(boxplot_output, dpi=150, bbox_inches='tight') - plt.close() - print(f" ✓ {boxplot_output}") - - # 4. 전체 바이올린 플롯 - print("\n[4] 전체 바이올린 플롯 생성 중...") - - fig, ax = plt.subplots(figsize=(16, 8)) - - # 데이터 재구성 - plot_data = [] - for _, row in df.iterrows(): - plot_data.append({ - 'Class_Axis': f"{row['Class']}\n{row['Axis']}", - 'Frequency': row['Resonance_Frequency_Hz'], - 'Class': row['Class'], - 'Axis': row['Axis'] - }) - - plot_df = pd.DataFrame(plot_data) - - # 바이올린 플롯 - sns.violinplot(data=plot_df, x='Axis', y='Frequency', hue='Class', ax=ax, split=False) - - ax.set_title('축별 주파수 분포 비교 (모든 클래스)', fontsize=16, fontweight='bold') - ax.set_xlabel('축', fontsize=12) - ax.set_ylabel('주파수 (Hz)', fontsize=12) - ax.legend(title='클래스', bbox_to_anchor=(1.05, 1), loc='upper left') - ax.grid(True, alpha=0.3, axis='y') - - plt.tight_layout() - violin_output = output_dir / "frequency_violin_plot.png" - plt.savefig(violin_output, dpi=150, bbox_inches='tight') - plt.close() - print(f" ✓ {violin_output}") - - # 5. 오차 범위 요약 - print("\n[5] 오차 범위 요약") - print("-" * 70) - - for axis in axes: - print(f"\n{axis}:") - axis_data = df[df['Axis'] == axis] - - for class_name in classes: - class_axis_data = axis_data[axis_data['Class'] == class_name]['Resonance_Frequency_Hz'].values - - if len(class_axis_data) > 0: - mean_val = np.mean(class_axis_data) - std_val = np.std(class_axis_data) - - # 68% 신뢰구간 (±1σ) - ci_68_lower = mean_val - std_val - ci_68_upper = mean_val + std_val - - # 95% 신뢰구간 (±2σ) - ci_95_lower = mean_val - 2 * std_val - ci_95_upper = mean_val + 2 * std_val - - print(f" {class_name}:") - print(f" 평균: {mean_val:.2f} Hz") - print(f" 68% 신뢰구간: [{ci_68_lower:.2f}, {ci_68_upper:.2f}] Hz") - print(f" 95% 신뢰구간: [{ci_95_lower:.2f}, {ci_95_upper:.2f}] Hz") - - print("\n" + "=" * 70) - print("기본 분석 완료! 이제 통계 CSV 시각화를 시작합니다...") - print("=" * 70) - - # 6. 통계 CSV 시각화 추가 - visualize_statistics_csv(stats_df, output_dir) - - return stats_df - - -def visualize_statistics_csv(stats_df, output_dir): - """ - frequency_statistics DataFrame을 다양한 플롯으로 시각화합니다. - - Parameters: - ----------- - stats_df : DataFrame - 통계 데이터프레임 - output_dir : Path - 결과 저장 디렉토리 - """ - print("\n" + "=" * 70) - print("통계 CSV 시각화") - print("=" * 70) - - classes = stats_df['Class'].unique() - axes = stats_df['Axis'].unique() - - colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#FFA07A', '#98D8C8'] + print(f"✓ 통계 CSV 저장 완료: {stats_output}") - # 6-1. 평균 주파수 히트맵 - print("\n[6-1] 평균 주파수 히트맵 생성 중...") + # 3. 히트맵 시각화 (Mean, Std, CV%) + print("\n[2] 히트맵 시각화 생성 중...") + # 히트맵을 위한 피벗 (Index: Axis, Columns: Class('base')) pivot_mean = stats_df.pivot(index='Axis', columns='Class', values='Mean') - - fig, ax = plt.subplots(figsize=(12, 8)) - sns.heatmap(pivot_mean, annot=True, fmt='.2f', cmap='YlOrRd', - linewidths=0.5, ax=ax, cbar_kws={'label': '평균 주파수 (Hz)'}) - ax.set_title('클래스별-축별 평균 공진 주파수 히트맵', fontsize=16, fontweight='bold', pad=20) - ax.set_xlabel('클래스', fontsize=12, fontweight='bold') - ax.set_ylabel('축', fontsize=12, fontweight='bold') - - plt.tight_layout() - output_path = output_dir / "stats_heatmap_mean.png" - plt.savefig(output_path, dpi=150, bbox_inches='tight') - plt.close() - print(f" ✓ {output_path}") - - # 6-2. 표준편차 히트맵 - print("\n[6-2] 표준편차 히트맵 생성 중...") - pivot_std = stats_df.pivot(index='Axis', columns='Class', values='Std') - - fig, ax = plt.subplots(figsize=(12, 8)) - sns.heatmap(pivot_std, annot=True, fmt='.2f', cmap='Blues', - linewidths=0.5, ax=ax, cbar_kws={'label': '표준편차 (Hz)'}) - ax.set_title('클래스별-축별 표준편차 히트맵 (오차 크기)', fontsize=16, fontweight='bold', pad=20) - ax.set_xlabel('클래스', fontsize=12, fontweight='bold') - ax.set_ylabel('축', fontsize=12, fontweight='bold') - - plt.tight_layout() - output_path = output_dir / "stats_heatmap_std.png" - plt.savefig(output_path, dpi=150, bbox_inches='tight') - plt.close() - print(f" ✓ {output_path}") - - # 6-3. 변동계수(CV) 히트맵 - print("\n[6-3] 변동계수(CV) 히트맵 생성 중...") - pivot_cv = stats_df.pivot(index='Axis', columns='Class', values='CV(%)') - fig, ax = plt.subplots(figsize=(12, 8)) - sns.heatmap(pivot_cv, annot=True, fmt='.2f', cmap='RdYlGn_r', - linewidths=0.5, ax=ax, cbar_kws={'label': '변동계수 (%)'}) - ax.set_title('클래스별-축별 변동계수 히트맵 (상대적 오차)', fontsize=16, fontweight='bold', pad=20) - ax.set_xlabel('클래스', fontsize=12, fontweight='bold') - ax.set_ylabel('축', fontsize=12, fontweight='bold') - - plt.tight_layout() - output_path = output_dir / "stats_heatmap_cv.png" - plt.savefig(output_path, dpi=150, bbox_inches='tight') - plt.close() - print(f" ✓ {output_path}") - - # 6-4. 축별 평균 주파수 비교 막대그래프 - print("\n[6-4] 축별 평균 주파수 막대그래프 생성 중...") - - fig, ax = plt.subplots(figsize=(14, 8)) - - x = np.arange(len(axes)) - width = 0.15 - - for idx, class_name in enumerate(classes): - class_data = stats_df[stats_df['Class'] == class_name] - means = [class_data[class_data['Axis'] == axis]['Mean'].values[0] - if len(class_data[class_data['Axis'] == axis]) > 0 else 0 - for axis in axes] - stds = [class_data[class_data['Axis'] == axis]['Std'].values[0] - if len(class_data[class_data['Axis'] == axis]) > 0 else 0 - for axis in axes] - - offset = width * (idx - len(classes) / 2 + 0.5) - ax.bar(x + offset, means, width, label=class_name, - yerr=stds, capsize=5, alpha=0.8, color=colors[idx % len(colors)]) - - ax.set_xlabel('축', fontsize=12, fontweight='bold') - ax.set_ylabel('평균 주파수 (Hz)', fontsize=12, fontweight='bold') - ax.set_title('클래스별 축 평균 공진 주파수 비교 (±표준편차)', fontsize=16, fontweight='bold') - ax.set_xticks(x) - ax.set_xticklabels(axes) - ax.legend(title='클래스', loc='upper left') - ax.grid(True, alpha=0.3, axis='y') - - plt.tight_layout() - output_path = output_dir / "stats_bar_mean_comparison.png" - plt.savefig(output_path, dpi=150, bbox_inches='tight') - plt.close() - print(f" ✓ {output_path}") - - # 6-5. 클래스별 표준편차 비교 막대그래프 - print("\n[6-5] 클래스별 표준편차 비교 막대그래프 생성 중...") - - fig, ax = plt.subplots(figsize=(14, 8)) + fig, axes_heat = plt.subplots(1, 3, figsize=(18, 6)) - for idx, class_name in enumerate(classes): - class_data = stats_df[stats_df['Class'] == class_name] - stds = [class_data[class_data['Axis'] == axis]['Std'].values[0] - if len(class_data[class_data['Axis'] == axis]) > 0 else 0 - for axis in axes] + # (1) 평균 주파수 히트맵 + sns.heatmap(pivot_mean, annot=True, fmt='.2f', cmap='YlOrRd', ax=axes_heat[0]) + axes_heat[0].set_title('평균 주파수 (Hz)') - offset = width * (idx - len(classes) / 2 + 0.5) - ax.bar(x + offset, stds, width, label=class_name, - alpha=0.8, color=colors[idx % len(colors)]) + # (2) 표준편차(절대 오차) 히트맵 + sns.heatmap(pivot_std, annot=True, fmt='.2f', cmap='Blues', ax=axes_heat[1]) + axes_heat[1].set_title('표준편차 (오차 크기)') - ax.set_xlabel('축', fontsize=12, fontweight='bold') - ax.set_ylabel('표준편차 (Hz)', fontsize=12, fontweight='bold') - ax.set_title('클래스별 축 표준편차 비교 (오차 크기)', fontsize=16, fontweight='bold') - ax.set_xticks(x) - ax.set_xticklabels(axes) - ax.legend(title='클래스', loc='upper left') - ax.grid(True, alpha=0.3, axis='y') + # (3) 변동계수(상대 오차) 히트맵 + sns.heatmap(pivot_cv, annot=True, fmt='.2f', cmap='RdYlGn_r', ax=axes_heat[2]) + axes_heat[2].set_title('변동계수 (%)') + plt.suptitle('축별 공진 주파수 통계 히트맵 (Base)', fontsize=16, fontweight='bold') plt.tight_layout() - output_path = output_dir / "stats_bar_std_comparison.png" - plt.savefig(output_path, dpi=150, bbox_inches='tight') + heatmap_output = output_dir / "stats_heatmaps_base.png" + plt.savefig(heatmap_output, dpi=150) plt.close() - print(f" ✓ {output_path}") - - # 6-6. Range(범위) 비교 - print("\n[6-6] 주파수 범위(Range) 비교 생성 중...") - - fig, ax = plt.subplots(figsize=(14, 8)) - - for idx, class_name in enumerate(classes): - class_data = stats_df[stats_df['Class'] == class_name] - ranges = [class_data[class_data['Axis'] == axis]['Range'].values[0] - if len(class_data[class_data['Axis'] == axis]) > 0 else 0 - for axis in axes] - - offset = width * (idx - len(classes) / 2 + 0.5) - ax.bar(x + offset, ranges, width, label=class_name, - alpha=0.8, color=colors[idx % len(colors)]) - - ax.set_xlabel('축', fontsize=12, fontweight='bold') - ax.set_ylabel('주파수 범위 (Hz)', fontsize=12, fontweight='bold') - ax.set_title('클래스별 축 주파수 범위 (Max - Min)', fontsize=16, fontweight='bold') - ax.set_xticks(x) - ax.set_xticklabels(axes) - ax.legend(title='클래스', loc='upper left') - ax.grid(True, alpha=0.3, axis='y') - - plt.tight_layout() - output_path = output_dir / "stats_bar_range_comparison.png" - plt.savefig(output_path, dpi=150, bbox_inches='tight') - plt.close() - print(f" ✓ {output_path}") - - # 6-7. 평균 vs 표준편차 산점도 - print("\n[6-7] 평균 vs 표준편차 산점도 생성 중...") - - fig, ax = plt.subplots(figsize=(12, 8)) - - for idx, class_name in enumerate(classes): - class_data = stats_df[stats_df['Class'] == class_name] - ax.scatter(class_data['Mean'], class_data['Std'], - s=150, alpha=0.6, label=class_name, - color=colors[idx % len(colors)]) - - # 축 이름 표시 - for _, row in class_data.iterrows(): - ax.annotate(row['Axis'], (row['Mean'], row['Std']), - xytext=(5, 5), textcoords='offset points', - fontsize=8, alpha=0.7) - - ax.set_xlabel('평균 주파수 (Hz)', fontsize=12, fontweight='bold') - ax.set_ylabel('표준편차 (Hz)', fontsize=12, fontweight='bold') - ax.set_title('평균 주파수 vs 표준편차 관계', fontsize=16, fontweight='bold') - ax.legend(title='클래스') - ax.grid(True, alpha=0.3) - - plt.tight_layout() - output_path = output_dir / "stats_scatter_mean_vs_std.png" - plt.savefig(output_path, dpi=150, bbox_inches='tight') - plt.close() - print(f" ✓ {output_path}") - - # 6-8. 축별 요약 플롯 (평균, 표준편차, CV 한번에) - print("\n[6-8] 축별 종합 요약 플롯 생성 중...") + print(f" ✓ 히트맵 저장: {heatmap_output}") + # 4. 히스토그램 (축별 분포) + print("\n[3] 주파수 분포 히스토그램 생성 중...") fig, axes_plot = plt.subplots(2, 3, figsize=(18, 10)) - fig.suptitle('축별 통계 종합 요약', fontsize=16, fontweight='bold') - for idx, axis in enumerate(axes): - row = idx // 3 - col = idx % 3 + row, col = idx // 3, idx % 3 ax = axes_plot[row, col] - - axis_data = stats_df[stats_df['Axis'] == axis] - - x_pos = np.arange(len(classes)) - - # 평균 막대 - means = axis_data['Mean'].values - stds = axis_data['Std'].values - - bars = ax.bar(x_pos, means, yerr=stds, capsize=5, - alpha=0.7, color=colors[:len(classes)]) - - # CV 값을 막대 위에 표시 - for i, (bar, cv) in enumerate(zip(bars, axis_data['CV(%)'].values)): - height = bar.get_height() - ax.text(bar.get_x() + bar.get_width() / 2., height + stds[i], - f'CV: {cv:.1f}%', ha='center', va='bottom', fontsize=8) - - ax.set_title(f'{axis}', fontweight='bold', fontsize=12) - ax.set_ylabel('평균 주파수 (Hz)') - ax.set_xticks(x_pos) - ax.set_xticklabels(axis_data['Class'].values, rotation=45, ha='right') - ax.grid(True, alpha=0.3, axis='y') - + axis_data = df[df['Axis'] == axis]['Resonance_Frequency_Hz'].values + if len(axis_data) > 0: + ax.hist(axis_data, bins=20, alpha=0.7, color='steelblue', edgecolor='black') + mean_val = np.mean(axis_data) + ax.axvline(mean_val, color='red', linestyle='--', label=f'Mean: {mean_val:.2f}') + ax.set_title(f'{axis}') + ax.legend() plt.tight_layout() - output_path = output_dir / "stats_summary_by_axis.png" - plt.savefig(output_path, dpi=150, bbox_inches='tight') + plt.savefig(output_dir / "frequency_distribution_base.png", dpi=150) + plt.close() + + # 5. 막대 그래프 (평균 및 범위 비교) + print("\n[4] 통계 막대 그래프 생성 중...") + plt.figure(figsize=(12, 6)) + plt.bar(stats_df['Axis'], stats_df['Mean'], yerr=stats_df['Std'], capsize=5, color='lightgray', edgecolor='black') + plt.title('축별 평균 공진 주파수 및 표준편차 (Base)') + plt.savefig(output_dir / "stats_bar_comparison_base.png", dpi=150) plt.close() - print(f" ✓ {output_path}") print("\n" + "=" * 70) - print("모든 시각화 완료!") + print("모든 분석 및 시각화 완료!") print("=" * 70) + return stats_df + -# 실행 +# --- 실행부 --- if __name__ == "__main__": - # FFT 분석 결과 CSV 파일 경로 - RESULTS_CSV = "/Users/seohyeon/PycharmProjects/AT_data/data_v1/all_files_fft_analysis_results_kalman.csv" + # 요청하신 data_v2 경로 및 파일명 + RESULTS_CSV = "/Users/seohyeon/PycharmProjects/AT_data/data_v2/fft_analysis_base_results.csv" - # 분석 실행 (통계 CSV 시각화 포함) stats = analyze_frequency_distribution(RESULTS_CSV) - - print("\n[통계 요약]") - print(stats.to_string(index=False)) \ No newline at end of file + if stats is not None: + print(stats.to_string(index=False)) \ No newline at end of file