Skip to content

Instantly share code, notes, and snippets.

@matsubo
Created November 4, 2025 14:21
Show Gist options
  • Select an option

  • Save matsubo/0e5c9859633f3494a4ea090467c903c5 to your computer and use it in GitHub Desktop.

Select an option

Save matsubo/0e5c9859633f3494a4ea090467c903c5 to your computer and use it in GitHub Desktop.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
自転車エアロ性能分析スクリプト
Aeroad vs Tarmac の皇居4周データ比較
"""
import fitparse
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib
from datetime import datetime, timedelta
import json
# 日本語フォント設定
matplotlib.rcParams['font.family'] = ['Arial Unicode MS', 'Hiragino Sans', 'DejaVu Sans']
matplotlib.rcParams['axes.unicode_minus'] = False
# カラーテーマ設定
TARMAC_COLOR = '#50C878' # エメラルドグリーン
TARMAC_COLOR_LIGHT = '#7FD99A' # 明るいエメラルドグリーン
AEROAD_COLOR = '#6B7280' # グレー
AEROAD_COLOR_LIGHT = '#9CA3AF' # 明るいグレー
class FitFileAnalyzer:
def __init__(self, filepath, bike_name):
self.filepath = filepath
self.bike_name = bike_name
self.fitfile = fitparse.FitFile(filepath)
self.records = []
self.df = None
def parse_fit_file(self):
"""FITファイルをパースしてデータフレームに変換"""
records = []
for record in self.fitfile.get_messages('record'):
record_dict = {}
for record_data in record:
record_dict[record_data.name] = record_data.value
records.append(record_dict)
self.df = pd.DataFrame(records)
# タイムスタンプを変換
if 'timestamp' in self.df.columns:
self.df['timestamp'] = pd.to_datetime(self.df['timestamp'])
self.df['elapsed_time'] = (self.df['timestamp'] - self.df['timestamp'].iloc[0]).dt.total_seconds()
# 位置情報を度に変換
if 'position_lat' in self.df.columns:
self.df['latitude'] = self.df['position_lat'] * (180 / 2**31)
if 'position_long' in self.df.columns:
self.df['longitude'] = self.df['position_long'] * (180 / 2**31)
# 速度をm/sからkm/hに変換
if 'speed' in self.df.columns:
self.df['speed_kmh'] = self.df['speed'] * 3.6
# パワーとケイデンスのクリーニング
if 'power' in self.df.columns:
self.df['power'] = pd.to_numeric(self.df['power'], errors='coerce')
if 'cadence' in self.df.columns:
self.df['cadence'] = pd.to_numeric(self.df['cadence'], errors='coerce')
print(f"{self.bike_name}: {len(self.df)} records loaded")
print(f"Columns: {list(self.df.columns)}")
return self.df
def detect_imperial_palace_laps(self):
"""皇居周回を検出"""
if self.df is None or 'latitude' not in self.df.columns:
return None
# 皇居の中心座標(桜田門付近)
palace_lat = 35.6776
palace_lon = 139.7571
# 各ポイントから皇居中心までの距離を計算
self.df['dist_to_palace'] = np.sqrt(
((self.df['latitude'] - palace_lat) * 111.0)**2 +
((self.df['longitude'] - palace_lon) * 111.0 * np.cos(np.radians(palace_lat)))**2
)
# 皇居から1.5km以内のポイントを抽出
palace_data = self.df[self.df['dist_to_palace'] < 1.5].copy()
if len(palace_data) == 0:
print(f"{self.bike_name}: No data near Imperial Palace found")
return None
# 連続するセグメントに分割(10分以上の間隔で分割)
palace_data['time_diff'] = palace_data['timestamp'].diff().dt.total_seconds()
segment_breaks = palace_data[palace_data['time_diff'] > 600].index
if len(segment_breaks) > 0:
# 最初のブレイクまでのデータを使用
palace_data = palace_data.loc[:segment_breaks[0]-1]
# 周回数を推定(距離から)
if 'distance' in palace_data.columns:
total_distance = palace_data['distance'].max() - palace_data['distance'].min()
estimated_laps = total_distance / 5000 # 皇居1周約5km
print(f"{self.bike_name}: Estimated {estimated_laps:.1f} laps, distance: {total_distance:.0f}m")
return palace_data
def calculate_aero_metrics(self, df_segment):
"""エアロ性能指標を計算"""
if df_segment is None or len(df_segment) == 0:
return {}
metrics = {}
# 基本統計
if 'speed_kmh' in df_segment.columns:
metrics['avg_speed'] = df_segment['speed_kmh'].mean()
metrics['max_speed'] = df_segment['speed_kmh'].max()
metrics['std_speed'] = df_segment['speed_kmh'].std()
if 'power' in df_segment.columns:
df_segment = df_segment[df_segment['power'] > 0]
metrics['avg_power'] = df_segment['power'].mean()
metrics['normalized_power'] = self._calculate_normalized_power(df_segment['power'])
metrics['max_power'] = df_segment['power'].max()
# CdA推定(簡易版)
if 'power' in df_segment.columns and 'speed_kmh' in df_segment.columns:
# 平坦部分を抽出(勾配が小さい、または速度が比較的安定している部分)
df_flat = df_segment[
(df_segment['speed_kmh'] > 25) &
(df_segment['speed_kmh'] < 40) &
(df_segment['power'] > 150) &
(df_segment['power'] < 400)
].copy()
if len(df_flat) > 100:
# パワーと速度の関係からCdAを推定
# Power = CdA * 0.5 * rho * v^3 + Crr * m * g * v + other losses
# 簡易的に: CdA ≈ Power / (0.5 * rho * v^3) * factor
rho = 1.225 # 空気密度 kg/m^3
df_flat['speed_ms'] = df_flat['speed_kmh'] / 3.6
df_flat['aero_power_est'] = df_flat['power'] * 0.7 # 約70%がエアロ抵抗と仮定
df_flat['cda_est'] = df_flat['aero_power_est'] / (0.5 * rho * df_flat['speed_ms']**3)
# 外れ値を除去
cda_values = df_flat['cda_est']
cda_values = cda_values[(cda_values > 0.2) & (cda_values < 0.5)]
if len(cda_values) > 10:
metrics['estimated_cda'] = cda_values.median()
metrics['cda_std'] = cda_values.std()
metrics['cda_samples'] = len(cda_values)
# パワーと速度の効率指標
metrics['power_speed_ratio'] = df_flat['power'].mean() / df_flat['speed_kmh'].mean()
# 高速域でのパワー効率(35km/h以上)
if 'power' in df_segment.columns and 'speed_kmh' in df_segment.columns:
high_speed = df_segment[df_segment['speed_kmh'] > 35]
if len(high_speed) > 0:
metrics['high_speed_avg_power'] = high_speed['power'].mean()
metrics['high_speed_avg_speed'] = high_speed['speed_kmh'].mean()
metrics['high_speed_duration'] = len(high_speed)
# ケイデンス
if 'cadence' in df_segment.columns:
cadence_data = df_segment[df_segment['cadence'] > 0]
if len(cadence_data) > 0:
metrics['avg_cadence'] = cadence_data['cadence'].mean()
# 心拍数
if 'heart_rate' in df_segment.columns:
hr_data = df_segment[df_segment['heart_rate'] > 0]
if len(hr_data) > 0:
metrics['avg_heart_rate'] = hr_data['heart_rate'].mean()
# 標高情報
if 'altitude' in df_segment.columns:
metrics['elevation_gain'] = (df_segment['altitude'].diff()[df_segment['altitude'].diff() > 0]).sum()
metrics['elevation_loss'] = abs((df_segment['altitude'].diff()[df_segment['altitude'].diff() < 0]).sum())
# 距離と時間
if 'distance' in df_segment.columns:
metrics['total_distance'] = df_segment['distance'].max() - df_segment['distance'].min()
if 'elapsed_time' in df_segment.columns:
metrics['total_time'] = df_segment['elapsed_time'].max() - df_segment['elapsed_time'].min()
return metrics
def _calculate_normalized_power(self, power_series):
"""Normalized Power (NP) を計算"""
if len(power_series) < 30:
return power_series.mean()
# 30秒移動平均
rolling_avg = power_series.rolling(window=30, center=True).mean()
# 4乗の平均の4乗根
np_value = (rolling_avg**4).mean()**(1/4)
return np_value
def create_visualizations(aeroad_df, tarmac_df, aeroad_metrics, tarmac_metrics):
"""可視化図表を生成"""
figures = []
# 1. 速度分布の比較
fig, ax = plt.subplots(figsize=(10, 6))
if 'speed_kmh' in aeroad_df.columns and 'speed_kmh' in tarmac_df.columns:
ax.hist(aeroad_df['speed_kmh'].dropna(), bins=50, alpha=0.6, label='Aeroad CF SLX 8', color=AEROAD_COLOR)
ax.hist(tarmac_df['speed_kmh'].dropna(), bins=50, alpha=0.6, label='Specialized Tarmac SL5', color=TARMAC_COLOR)
ax.set_xlabel('Speed (km/h)', fontsize=11)
ax.set_ylabel('Frequency', fontsize=11)
ax.set_title('Speed Distribution Comparison', fontsize=13, fontweight='bold')
ax.legend(fontsize=10)
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('./speed_distribution.png', dpi=150)
figures.append('speed_distribution.png')
plt.close()
# 2. パワーと速度の関係
fig, ax = plt.subplots(figsize=(10, 6))
if 'power' in aeroad_df.columns and 'speed_kmh' in aeroad_df.columns:
aeroad_clean = aeroad_df[(aeroad_df['power'] > 0) & (aeroad_df['power'] < 500)]
tarmac_clean = tarmac_df[(tarmac_df['power'] > 0) & (tarmac_df['power'] < 500)]
ax.scatter(aeroad_clean['speed_kmh'], aeroad_clean['power'], alpha=0.3, s=1, label='Aeroad CF SLX 8', color=AEROAD_COLOR)
ax.scatter(tarmac_clean['speed_kmh'], tarmac_clean['power'], alpha=0.3, s=1, label='Specialized Tarmac SL5', color=TARMAC_COLOR)
# トレンドライン
if len(aeroad_clean) > 100:
z = np.polyfit(aeroad_clean['speed_kmh'].dropna(), aeroad_clean['power'].dropna(), 2)
p = np.poly1d(z)
x_line = np.linspace(aeroad_clean['speed_kmh'].min(), aeroad_clean['speed_kmh'].max(), 100)
ax.plot(x_line, p(x_line), color=AEROAD_COLOR_LIGHT, linewidth=2.5, label='Aeroad trend')
if len(tarmac_clean) > 100:
z = np.polyfit(tarmac_clean['speed_kmh'].dropna(), tarmac_clean['power'].dropna(), 2)
p = np.poly1d(z)
x_line = np.linspace(tarmac_clean['speed_kmh'].min(), tarmac_clean['speed_kmh'].max(), 100)
ax.plot(x_line, p(x_line), color=TARMAC_COLOR_LIGHT, linewidth=2.5, label='Tarmac trend')
ax.set_xlabel('Speed (km/h)', fontsize=11)
ax.set_ylabel('Power (W)', fontsize=11)
ax.set_title('Power vs Speed Relationship', fontsize=13, fontweight='bold')
ax.legend(fontsize=10)
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('./power_speed.png', dpi=150)
figures.append('power_speed.png')
plt.close()
# 3. 速度の時系列比較
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 8))
if 'elapsed_time' in aeroad_df.columns and 'speed_kmh' in aeroad_df.columns:
ax1.plot(aeroad_df['elapsed_time']/60, aeroad_df['speed_kmh'], color=AEROAD_COLOR, linewidth=0.8)
ax1.set_ylabel('Speed (km/h)', color=AEROAD_COLOR, fontsize=11)
ax1.set_title('Aeroad CF SLX 8 - Speed Over Time', fontsize=12, fontweight='bold')
ax1.grid(True, alpha=0.3)
ax1.set_xlabel('Time (minutes)', fontsize=11)
ax1.tick_params(axis='y', labelcolor=AEROAD_COLOR)
if 'power' in aeroad_df.columns:
ax1_2 = ax1.twinx()
ax1_2.plot(aeroad_df['elapsed_time']/60, aeroad_df['power'], color=AEROAD_COLOR_LIGHT, alpha=0.4, linewidth=0.6)
ax1_2.set_ylabel('Power (W)', color=AEROAD_COLOR_LIGHT, fontsize=11)
ax1_2.tick_params(axis='y', labelcolor=AEROAD_COLOR_LIGHT)
if 'elapsed_time' in tarmac_df.columns and 'speed_kmh' in tarmac_df.columns:
ax2.plot(tarmac_df['elapsed_time']/60, tarmac_df['speed_kmh'], color=TARMAC_COLOR, linewidth=0.8)
ax2.set_ylabel('Speed (km/h)', color=TARMAC_COLOR, fontsize=11)
ax2.set_title('Specialized Tarmac SL5 - Speed Over Time', fontsize=12, fontweight='bold')
ax2.grid(True, alpha=0.3)
ax2.set_xlabel('Time (minutes)', fontsize=11)
ax2.tick_params(axis='y', labelcolor=TARMAC_COLOR)
if 'power' in tarmac_df.columns:
ax2_2 = ax2.twinx()
ax2_2.plot(tarmac_df['elapsed_time']/60, tarmac_df['power'], color=TARMAC_COLOR_LIGHT, alpha=0.4, linewidth=0.6)
ax2_2.set_ylabel('Power (W)', color=TARMAC_COLOR_LIGHT, fontsize=11)
ax2_2.tick_params(axis='y', labelcolor=TARMAC_COLOR_LIGHT)
plt.tight_layout()
plt.savefig('./speed_timeline.png', dpi=150)
figures.append('speed_timeline.png')
plt.close()
# 4. パワー分布の比較
fig, ax = plt.subplots(figsize=(10, 6))
if 'power' in aeroad_df.columns and 'power' in tarmac_df.columns:
aeroad_power = aeroad_df[aeroad_df['power'] > 0]['power']
tarmac_power = tarmac_df[tarmac_df['power'] > 0]['power']
ax.hist(aeroad_power, bins=50, alpha=0.6, label='Aeroad CF SLX 8', color=AEROAD_COLOR, range=(0, 500))
ax.hist(tarmac_power, bins=50, alpha=0.6, label='Specialized Tarmac SL5', color=TARMAC_COLOR, range=(0, 500))
ax.set_xlabel('Power (W)', fontsize=11)
ax.set_ylabel('Frequency', fontsize=11)
ax.set_title('Power Distribution Comparison', fontsize=13, fontweight='bold')
ax.legend(fontsize=10)
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('./power_distribution.png', dpi=150)
figures.append('power_distribution.png')
plt.close()
# 5. CdA推定の比較(利用可能な場合)
if 'estimated_cda' in aeroad_metrics and 'estimated_cda' in tarmac_metrics:
fig, ax = plt.subplots(figsize=(8, 6))
bikes = ['Aeroad CF SLX 8', 'Specialized\nTarmac SL5']
cda_values = [aeroad_metrics['estimated_cda'], tarmac_metrics['estimated_cda']]
colors = [AEROAD_COLOR, TARMAC_COLOR]
bars = ax.bar(bikes, cda_values, color=colors, alpha=0.8, edgecolor='black', linewidth=1.5)
ax.set_ylabel('Estimated CdA (m²)', fontsize=11)
ax.set_title('Aerodynamic Drag Coefficient Comparison', fontsize=13, fontweight='bold')
ax.grid(True, alpha=0.3, axis='y')
# 値をバーの上に表示
for bar, value in zip(bars, cda_values):
height = bar.get_height()
ax.text(bar.get_x() + bar.get_width()/2., height,
f'{value:.3f}',
ha='center', va='bottom', fontsize=11, fontweight='bold')
plt.tight_layout()
plt.savefig('./cda_comparison.png', dpi=150)
figures.append('cda_comparison.png')
plt.close()
# 6. 主要メトリクスの比較バーチャート
fig, axes = plt.subplots(2, 2, figsize=(12, 10))
# 平均速度
if 'avg_speed' in aeroad_metrics and 'avg_speed' in tarmac_metrics:
ax = axes[0, 0]
bikes = ['Aeroad\nCF SLX 8', 'Specialized\nTarmac SL5']
speeds = [aeroad_metrics['avg_speed'], tarmac_metrics['avg_speed']]
bars = ax.bar(bikes, speeds, color=[AEROAD_COLOR, TARMAC_COLOR], alpha=0.8, edgecolor='black', linewidth=1.2)
ax.set_ylabel('Speed (km/h)', fontsize=11)
ax.set_title('Average Speed Comparison', fontsize=12, fontweight='bold')
ax.grid(True, alpha=0.3, axis='y')
for bar, value in zip(bars, speeds):
ax.text(bar.get_x() + bar.get_width()/2., bar.get_height(),
f'{value:.1f}', ha='center', va='bottom', fontsize=10, fontweight='bold')
# 平均パワー
if 'avg_power' in aeroad_metrics and 'avg_power' in tarmac_metrics:
ax = axes[0, 1]
bikes = ['Aeroad\nCF SLX 8', 'Specialized\nTarmac SL5']
powers = [aeroad_metrics['avg_power'], tarmac_metrics['avg_power']]
bars = ax.bar(bikes, powers, color=[AEROAD_COLOR, TARMAC_COLOR], alpha=0.8, edgecolor='black', linewidth=1.2)
ax.set_ylabel('Power (W)', fontsize=11)
ax.set_title('Average Power Comparison', fontsize=12, fontweight='bold')
ax.grid(True, alpha=0.3, axis='y')
for bar, value in zip(bars, powers):
ax.text(bar.get_x() + bar.get_width()/2., bar.get_height(),
f'{value:.1f}', ha='center', va='bottom', fontsize=10, fontweight='bold')
# パワー/速度比
if 'power_speed_ratio' in aeroad_metrics and 'power_speed_ratio' in tarmac_metrics:
ax = axes[1, 0]
bikes = ['Aeroad\nCF SLX 8', 'Specialized\nTarmac SL5']
ratios = [aeroad_metrics['power_speed_ratio'], tarmac_metrics['power_speed_ratio']]
bars = ax.bar(bikes, ratios, color=[AEROAD_COLOR, TARMAC_COLOR], alpha=0.8, edgecolor='black', linewidth=1.2)
ax.set_ylabel('Power/Speed (W/(km/h))', fontsize=11)
ax.set_title('Power-Speed Efficiency (Lower is Better)', fontsize=12, fontweight='bold')
ax.grid(True, alpha=0.3, axis='y')
for bar, value in zip(bars, ratios):
ax.text(bar.get_x() + bar.get_width()/2., bar.get_height(),
f'{value:.2f}', ha='center', va='bottom', fontsize=10, fontweight='bold')
# 高速域平均速度
if 'high_speed_avg_speed' in aeroad_metrics and 'high_speed_avg_speed' in tarmac_metrics:
ax = axes[1, 1]
bikes = ['Aeroad\nCF SLX 8', 'Specialized\nTarmac SL5']
hs_speeds = [aeroad_metrics['high_speed_avg_speed'], tarmac_metrics['high_speed_avg_speed']]
bars = ax.bar(bikes, hs_speeds, color=[AEROAD_COLOR, TARMAC_COLOR], alpha=0.8, edgecolor='black', linewidth=1.2)
ax.set_ylabel('Speed (km/h)', fontsize=11)
ax.set_title('High Speed Average (>35km/h)', fontsize=12, fontweight='bold')
ax.grid(True, alpha=0.3, axis='y')
for bar, value in zip(bars, hs_speeds):
ax.text(bar.get_x() + bar.get_width()/2., bar.get_height(),
f'{value:.1f}', ha='center', va='bottom', fontsize=10, fontweight='bold')
plt.tight_layout()
plt.savefig('./metrics_comparison.png', dpi=150)
figures.append('metrics_comparison.png')
plt.close()
return figures
def generate_html_report(aeroad_metrics, tarmac_metrics, figures, aeroad_df, tarmac_df):
"""HTML レポートを生成"""
# CdA差分の計算
cda_diff = None
cda_diff_percent = None
if 'estimated_cda' in aeroad_metrics and 'estimated_cda' in tarmac_metrics:
cda_diff = aeroad_metrics['estimated_cda'] - tarmac_metrics['estimated_cda']
cda_diff_percent = (cda_diff / tarmac_metrics['estimated_cda']) * 100
# 速度差分
speed_diff = None
speed_diff_percent = None
if 'avg_speed' in aeroad_metrics and 'avg_speed' in tarmac_metrics:
speed_diff = aeroad_metrics['avg_speed'] - tarmac_metrics['avg_speed']
speed_diff_percent = (speed_diff / tarmac_metrics['avg_speed']) * 100
# パワー差分
power_diff = None
power_diff_percent = None
if 'avg_power' in aeroad_metrics and 'avg_power' in tarmac_metrics:
power_diff = aeroad_metrics['avg_power'] - tarmac_metrics['avg_power']
power_diff_percent = (power_diff / tarmac_metrics['avg_power']) * 100
# 効率差分
efficiency_diff = None
efficiency_improvement = None
if 'power_speed_ratio' in aeroad_metrics and 'power_speed_ratio' in tarmac_metrics:
efficiency_diff = aeroad_metrics['power_speed_ratio'] - tarmac_metrics['power_speed_ratio']
efficiency_improvement = (efficiency_diff / tarmac_metrics['power_speed_ratio']) * 100
html_content = f"""
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>自転車エアロ性能分析レポート - Aeroad vs Tarmac</title>
<style>
body {{
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
}}
.header {{
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
border-radius: 10px;
margin-bottom: 30px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}}
h1 {{
margin: 0 0 10px 0;
font-size: 2.5em;
}}
.subtitle {{
font-size: 1.1em;
opacity: 0.9;
}}
.summary-grid {{
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-bottom: 30px;
}}
.summary-card {{
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}}
.summary-card h3 {{
margin-top: 0;
color: #333;
font-size: 1.1em;
}}
.metric-value {{
font-size: 2em;
font-weight: bold;
margin: 10px 0;
}}
.aeroad {{
color: #e74c3c;
}}
.tarmac {{
color: #3498db;
}}
.positive {{
color: #27ae60;
}}
.negative {{
color: #e74c3c;
}}
.diff {{
font-size: 0.9em;
color: #666;
}}
.section {{
background: white;
padding: 30px;
margin-bottom: 30px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}}
h2 {{
color: #2c3e50;
border-bottom: 3px solid #667eea;
padding-bottom: 10px;
}}
table {{
width: 100%;
border-collapse: collapse;
margin: 20px 0;
}}
th, td {{
padding: 12px;
text-align: left;
border-bottom: 1px solid #ddd;
}}
th {{
background-color: #667eea;
color: white;
font-weight: bold;
}}
tr:hover {{
background-color: #f5f5f5;
}}
.chart-container {{
margin: 30px 0;
text-align: center;
}}
.chart-container img {{
max-width: 100%;
height: auto;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}}
.insight {{
background: #e8f4f8;
border-left: 4px solid #3498db;
padding: 15px;
margin: 20px 0;
border-radius: 4px;
}}
.insight h4 {{
margin-top: 0;
color: #2980b9;
}}
.warning {{
background: #fff3cd;
border-left: 4px solid #ffc107;
padding: 15px;
margin: 20px 0;
border-radius: 4px;
}}
.warning h4 {{
margin-top: 0;
color: #856404;
}}
.methodology {{
background: #f8f9fa;
padding: 20px;
border-radius: 8px;
margin: 20px 0;
}}
.footer {{
text-align: center;
color: #666;
margin-top: 50px;
padding: 20px;
border-top: 1px solid #ddd;
}}
</style>
</head>
<body>
<div class="header">
<h1>🚴 自転車エアロ性能分析レポート</h1>
<div class="subtitle">Aeroad vs Tarmac - 皇居4周データ比較</div>
<div class="subtitle">分析日時: {datetime.now().strftime('%Y年%m月%d日 %H:%M')}</div>
</div>
<div class="section">
<h2>📊 エグゼクティブサマリー</h2>
<div class="summary-grid">
<div class="summary-card">
<h3>平均速度</h3>
<div class="metric-value aeroad">{aeroad_metrics.get('avg_speed', 0):.2f} km/h</div>
<div style="font-size: 0.9em; color: #666;">Aeroad</div>
<div class="metric-value tarmac">{tarmac_metrics.get('avg_speed', 0):.2f} km/h</div>
<div style="font-size: 0.9em; color: #666;">Tarmac</div>
{f'<div class="diff {"positive" if speed_diff and speed_diff > 0 else "negative"}">差: {speed_diff:+.2f} km/h ({speed_diff_percent:+.1f}%)</div>' if speed_diff is not None else ''}
</div>
<div class="summary-card">
<h3>平均パワー</h3>
<div class="metric-value aeroad">{aeroad_metrics.get('avg_power', 0):.1f} W</div>
<div style="font-size: 0.9em; color: #666;">Aeroad</div>
<div class="metric-value tarmac">{tarmac_metrics.get('avg_power', 0):.1f} W</div>
<div style="font-size: 0.9em; color: #666;">Tarmac</div>
{f'<div class="diff">差: {power_diff:+.1f} W ({power_diff_percent:+.1f}%)</div>' if power_diff is not None else ''}
</div>
{'<div class="summary-card"><h3>推定CdA</h3>' +
f'<div class="metric-value aeroad">{aeroad_metrics["estimated_cda"]:.3f} m²</div>' +
'<div style="font-size: 0.9em; color: #666;">Aeroad</div>' +
f'<div class="metric-value tarmac">{tarmac_metrics["estimated_cda"]:.3f} m²</div>' +
'<div style="font-size: 0.9em; color: #666;">Tarmac</div>' +
f'<div class="diff {"positive" if cda_diff < 0 else "negative"}">差: {cda_diff:+.3f} m² ({cda_diff_percent:+.1f}%)</div>' +
f'<div style="font-size: 0.8em; color: #999; margin-top: 10px;">サンプル: Aeroad {aeroad_metrics.get("cda_samples", 0)}, Tarmac {tarmac_metrics.get("cda_samples", 0)}</div>' +
'</div>' if 'estimated_cda' in aeroad_metrics and 'estimated_cda' in tarmac_metrics else ''}
{'<div class="summary-card"><h3>パワー効率</h3>' +
f'<div class="metric-value aeroad">{aeroad_metrics["power_speed_ratio"]:.2f}</div>' +
'<div style="font-size: 0.9em; color: #666;">Aeroad (W/km/h)</div>' +
f'<div class="metric-value tarmac">{tarmac_metrics["power_speed_ratio"]:.2f}</div>' +
'<div style="font-size: 0.9em; color: #666;">Tarmac (W/km/h)</div>' +
f'<div class="diff {"positive" if efficiency_diff < 0 else "negative"}">{f"Aeroadが{abs(efficiency_improvement):.1f}%効率的" if efficiency_diff < 0 else f"Tarmacが{abs(efficiency_improvement):.1f}%効率的"}</div>' +
'</div>' if 'power_speed_ratio' in aeroad_metrics and 'power_speed_ratio' in tarmac_metrics else ''}
</div>
</div>
<div class="warning">
<h4>⚠️ 分析上の注意事項</h4>
<p>この分析は異なる日時に収集されたデータを比較しているため、以下の要因が結果に影響している可能性があります:</p>
<ul>
<li><strong>気象条件:</strong> 風向き、風速、気温、湿度の違い</li>
<li><strong>ライダーコンディション:</strong> 疲労度、体調、モチベーションの違い</li>
<li><strong>交通状況:</strong> 信号待ち、他のサイクリストとの絡み方の違い</li>
<li><strong>機材設定:</strong> タイヤ空気圧、ポジション、ウェアの違い</li>
</ul>
<p>これらの要因を考慮した上で、統計的な傾向として結果を解釈することをお勧めします。</p>
</div>
<div class="section">
<h2>📈 詳細メトリクス比較</h2>
<table>
<thead>
<tr>
<th>メトリクス</th>
<th class="aeroad">Aeroad</th>
<th class="tarmac">Tarmac</th>
<th>差分</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>平均速度</strong></td>
<td>{aeroad_metrics.get('avg_speed', '-'):.2f} km/h</td>
<td>{tarmac_metrics.get('avg_speed', '-'):.2f} km/h</td>
<td>{f'{speed_diff:+.2f} km/h ({speed_diff_percent:+.1f}%)' if speed_diff is not None else '-'}</td>
</tr>
<tr>
<td><strong>最高速度</strong></td>
<td>{aeroad_metrics.get('max_speed', '-'):.2f} km/h</td>
<td>{tarmac_metrics.get('max_speed', '-'):.2f} km/h</td>
<td>{f'{aeroad_metrics.get("max_speed", 0) - tarmac_metrics.get("max_speed", 0):+.2f} km/h' if 'max_speed' in aeroad_metrics and 'max_speed' in tarmac_metrics else '-'}</td>
</tr>
<tr>
<td><strong>平均パワー</strong></td>
<td>{aeroad_metrics.get('avg_power', '-'):.1f} W</td>
<td>{tarmac_metrics.get('avg_power', '-'):.1f} W</td>
<td>{f'{power_diff:+.1f} W ({power_diff_percent:+.1f}%)' if power_diff is not None else '-'}</td>
</tr>
<tr>
<td><strong>Normalized Power</strong></td>
<td>{aeroad_metrics.get('normalized_power', '-'):.1f} W</td>
<td>{tarmac_metrics.get('normalized_power', '-'):.1f} W</td>
<td>{f'{aeroad_metrics.get("normalized_power", 0) - tarmac_metrics.get("normalized_power", 0):+.1f} W' if 'normalized_power' in aeroad_metrics and 'normalized_power' in tarmac_metrics else '-'}</td>
</tr>
<tr>
<td><strong>最大パワー</strong></td>
<td>{aeroad_metrics.get('max_power', '-'):.0f} W</td>
<td>{tarmac_metrics.get('max_power', '-'):.0f} W</td>
<td>{f'{aeroad_metrics.get("max_power", 0) - tarmac_metrics.get("max_power", 0):+.0f} W' if 'max_power' in aeroad_metrics and 'max_power' in tarmac_metrics else '-'}</td>
</tr>
<tr>
<td><strong>パワー/速度比</strong></td>
<td>{aeroad_metrics.get('power_speed_ratio', '-'):.2f} W/(km/h)</td>
<td>{tarmac_metrics.get('power_speed_ratio', '-'):.2f} W/(km/h)</td>
<td>{f'{efficiency_diff:+.2f} ({efficiency_improvement:+.1f}%)' if efficiency_diff is not None else '-'}</td>
</tr>
<tr>
<td><strong>高速域平均速度 (>35km/h)</strong></td>
<td>{aeroad_metrics.get('high_speed_avg_speed', '-'):.2f} km/h</td>
<td>{tarmac_metrics.get('high_speed_avg_speed', '-'):.2f} km/h</td>
<td>{f'{aeroad_metrics.get("high_speed_avg_speed", 0) - tarmac_metrics.get("high_speed_avg_speed", 0):+.2f} km/h' if 'high_speed_avg_speed' in aeroad_metrics and 'high_speed_avg_speed' in tarmac_metrics else '-'}</td>
</tr>
<tr>
<td><strong>高速域平均パワー</strong></td>
<td>{aeroad_metrics.get('high_speed_avg_power', '-'):.1f} W</td>
<td>{tarmac_metrics.get('high_speed_avg_power', '-'):.1f} W</td>
<td>{f'{aeroad_metrics.get("high_speed_avg_power", 0) - tarmac_metrics.get("high_speed_avg_power", 0):+.1f} W' if 'high_speed_avg_power' in aeroad_metrics and 'high_speed_avg_power' in tarmac_metrics else '-'}</td>
</tr>
<tr>
<td><strong>平均ケイデンス</strong></td>
<td>{aeroad_metrics.get('avg_cadence', '-'):.0f} rpm</td>
<td>{tarmac_metrics.get('avg_cadence', '-'):.0f} rpm</td>
<td>{f'{aeroad_metrics.get("avg_cadence", 0) - tarmac_metrics.get("avg_cadence", 0):+.0f} rpm' if 'avg_cadence' in aeroad_metrics and 'avg_cadence' in tarmac_metrics else '-'}</td>
</tr>
<tr>
<td><strong>平均心拍数</strong></td>
<td>{aeroad_metrics.get('avg_heart_rate', '-'):.0f} bpm</td>
<td>{tarmac_metrics.get('avg_heart_rate', '-'):.0f} bpm</td>
<td>{f'{aeroad_metrics.get("avg_heart_rate", 0) - tarmac_metrics.get("avg_heart_rate", 0):+.0f} bpm' if 'avg_heart_rate' in aeroad_metrics and 'avg_heart_rate' in tarmac_metrics else '-'}</td>
</tr>
<tr>
<td><strong>総距離</strong></td>
<td>{aeroad_metrics.get('total_distance', 0)/1000:.2f} km</td>
<td>{tarmac_metrics.get('total_distance', 0)/1000:.2f} km</td>
<td>{f'{(aeroad_metrics.get("total_distance", 0) - tarmac_metrics.get("total_distance", 0))/1000:+.2f} km' if 'total_distance' in aeroad_metrics and 'total_distance' in tarmac_metrics else '-'}</td>
</tr>
<tr>
<td><strong>総時間</strong></td>
<td>{int(aeroad_metrics.get('total_time', 0)//60)}{int(aeroad_metrics.get('total_time', 0)%60)}秒</td>
<td>{int(tarmac_metrics.get('total_time', 0)//60)}{int(tarmac_metrics.get('total_time', 0)%60)}秒</td>
<td>{f'{int((aeroad_metrics.get("total_time", 0) - tarmac_metrics.get("total_time", 0))//60):+d}{int(abs(aeroad_metrics.get("total_time", 0) - tarmac_metrics.get("total_time", 0))%60)}秒' if 'total_time' in aeroad_metrics and 'total_time' in tarmac_metrics else '-'}</td>
</tr>
<tr>
<td><strong>獲得標高</strong></td>
<td>{aeroad_metrics.get('elevation_gain', '-'):.0f} m</td>
<td>{tarmac_metrics.get('elevation_gain', '-'):.0f} m</td>
<td>{f'{aeroad_metrics.get("elevation_gain", 0) - tarmac_metrics.get("elevation_gain", 0):+.0f} m' if 'elevation_gain' in aeroad_metrics and 'elevation_gain' in tarmac_metrics else '-'}</td>
</tr>
</tbody>
</table>
</div>
<div class="section">
<h2>🎨 可視化分析</h2>
<div class="chart-container">
<h3>主要メトリクスの比較</h3>
<img src="metrics_comparison.png" alt="Metrics Comparison">
<p style="color: #666; margin-top: 10px;">
各種パフォーマンス指標を並べて比較。パワー/速度比が低いほど効率的(同じ速度を出すのに必要なパワーが少ない)。
</p>
</div>
{'<div class="chart-container"><h3>CdA (空気抵抗係数) 比較</h3><img src="cda_comparison.png" alt="CdA Comparison"><p style="color: #666; margin-top: 10px;">CdAは空気抵抗の指標。値が小さいほどエアロダイナミクスが優れている。</p></div>' if 'cda_comparison.png' in figures else ''}
<div class="chart-container">
<h3>速度分布の比較</h3>
<img src="speed_distribution.png" alt="Speed Distribution">
<p style="color: #666; margin-top: 10px;">
走行中の速度分布。分布の中心が右にあるほど平均的に速く、分布の幅が狭いほど安定した走行。
</p>
</div>
<div class="chart-container">
<h3>パワーと速度の関係</h3>
<img src="power_speed.png" alt="Power vs Speed">
<p style="color: #666; margin-top: 10px;">
パワーと速度の散布図とトレンドライン。同じパワーでより高い速度が出ているバイクの方がエアロダイナミクスが優れている可能性が高い。
</p>
</div>
<div class="chart-container">
<h3>速度と時系列の変化</h3>
<img src="speed_timeline.png" alt="Speed Timeline">
<p style="color: #666; margin-top: 10px;">
走行中の速度とパワーの時系列変化。走行パターンやペーシングの違いを視覚的に確認できる。
</p>
</div>
<div class="chart-container">
<h3>パワー分布の比較</h3>
<img src="power_distribution.png" alt="Power Distribution">
<p style="color: #666; margin-top: 10px;">
走行中のパワー分布。トレーニング負荷やライディングスタイルの違いを反映。
</p>
</div>
</div>
<div class="section">
<h2>💡 分析インサイト</h2>
<div class="insight">
<h4>🏆 エアロダイナミクス性能</h4>
{f'''<p><strong>Aeroadの推定CdA: {aeroad_metrics["estimated_cda"]:.3f} m² vs Tarmacの推定CdA: {tarmac_metrics["estimated_cda"]:.3f} m²</strong></p>
<p>{'Aeroadの方が' if cda_diff < 0 else 'Tarmacの方が'} <strong>{abs(cda_diff_percent):.1f}%</strong> エアロダイナミクスに優れていると推定されます。</p>
<p>CdA値の差 {abs(cda_diff):.3f} m² は、30km/hで走行時に約 {abs(cda_diff) * 0.5 * 1.225 * (30/3.6)**3:.1f}W のパワー差に相当します。</p>
<p><em>※ この推定は25-40km/hの速度域、150-400Wのパワー域のデータから計算されています(Aeroad: {aeroad_metrics.get("cda_samples", 0)}サンプル, Tarmac: {tarmac_metrics.get("cda_samples", 0)}サンプル)。実際のCdA値は風向き、ポジション、ウェアなど多くの要因に影響されます。</em></p>''' if 'estimated_cda' in aeroad_metrics and 'estimated_cda' in tarmac_metrics else '<p>CdA推定に十分なデータが得られませんでした。より多くの定常走行データが必要です。</p>'}
</div>
<div class="insight">
<h4>⚡ パワー効率</h4>
{f'''<p><strong>Aeroadのパワー/速度比: {aeroad_metrics["power_speed_ratio"]:.2f} W/(km/h) vs Tarmac: {tarmac_metrics["power_speed_ratio"]:.2f} W/(km/h)</strong></p>
<p>{'Aeroadの方が' if efficiency_diff < 0 else 'Tarmacの方が'} <strong>{abs(efficiency_improvement):.1f}%</strong> パワー効率が良いです。</p>
<p>同じ速度を維持するために必要なパワーが {'Aeroadの方が少なく' if efficiency_diff < 0 else 'Tarmacの方が少なく'}、総合的なエネルギー効率が優れています。</p>''' if 'power_speed_ratio' in aeroad_metrics and 'power_speed_ratio' in tarmac_metrics else '<p>パワー効率の比較に十分なデータが得られませんでした。</p>'}
</div>
<div class="insight">
<h4>🚀 高速域パフォーマンス (>35km/h)</h4>
{f'''<p><strong>Aeroad: {aeroad_metrics["high_speed_avg_speed"]:.2f} km/h @ {aeroad_metrics["high_speed_avg_power"]:.0f}W vs Tarmac: {tarmac_metrics["high_speed_avg_speed"]:.2f} km/h @ {tarmac_metrics["high_speed_avg_power"]:.0f}W</strong></p>
<p>高速域(35km/h以上)では、エアロダイナミクスの影響がより顕著に現れます。</p>
<p>{'Aeroadの方が' if aeroad_metrics["high_speed_avg_speed"] > tarmac_metrics["high_speed_avg_speed"] else 'Tarmacの方が'} {abs(aeroad_metrics["high_speed_avg_speed"] - tarmac_metrics["high_speed_avg_speed"]):.2f} km/h 速く、高速域でのアドバンテージがあります。</p>''' if 'high_speed_avg_speed' in aeroad_metrics and 'high_speed_avg_speed' in tarmac_metrics else '<p>高速域のデータが不十分です。</p>'}
</div>
<div class="insight">
<h4>📊 総合パフォーマンス</h4>
<p><strong>平均速度: Aeroad {aeroad_metrics.get('avg_speed', 0):.2f} km/h vs Tarmac {tarmac_metrics.get('avg_speed', 0):.2f} km/h</strong></p>
<p><strong>平均パワー: Aeroad {aeroad_metrics.get('avg_power', 0):.1f}W vs Tarmac {tarmac_metrics.get('avg_power', 0):.1f}W</strong></p>
{f'''<p>{'Aeroadの方が' if speed_diff > 0 else 'Tarmacの方が'} 平均 {abs(speed_diff):.2f} km/h ({abs(speed_diff_percent):.1f}%) 速く走行していますが、
パワーは {'Aeroadの方が' if power_diff > 0 else 'Tarmacの方が'} {abs(power_diff):.1f}W ({abs(power_diff_percent):.1f}%) {'高く' if power_diff > 0 else '低く'}なっています。</p>''' if speed_diff is not None and power_diff is not None else ''}
<p><em>※ この比較は異なる日時のデータであるため、気象条件やライダーコンディションの影響を受けている可能性があります。</em></p>
</div>
</div>
<div class="section">
<h2>🔬 分析手法</h2>
<div class="methodology">
<h4>データ処理</h4>
<ul>
<li>FITファイルから記録データを抽出</li>
<li>皇居周辺(中心から1.5km以内)のデータをフィルタリング</li>
<li>連続する走行セグメントを検出(10分以上の中断で分割)</li>
<li>異常値や停止時のデータをクリーニング</li>
</ul>
<h4>CdA推定方法</h4>
<ul>
<li>速度25-40 km/h、パワー150-400Wの定常走行データを抽出</li>
<li>エアロ抵抗式: Power_aero ≈ 0.5 × ρ × CdA × v³(ρ=1.225 kg/m³)</li>
<li>総パワーの約70%がエアロ抵抗と仮定(残りは転がり抵抗等)</li>
<li>外れ値を除去し、中央値を使用</li>
<li><strong>注意:</strong> この推定は簡易的なもので、勾配、風、その他の要因は完全には考慮されていません</li>
</ul>
<h4>Normalized Power (NP)</h4>
<ul>
<li>30秒移動平均を計算</li>
<li>各値を4乗し、平均を取り、4乗根を計算</li>
<li>変動の激しいパワーの生理学的負荷を反映</li>
</ul>
<h4>パワー効率指標</h4>
<ul>
<li>パワー/速度比: 1km/h速度を上げるのに必要なパワー(低いほど効率的)</li>
<li>高速域(>35km/h)での平均値を抽出し、エアロ性能を評価</li>
</ul>
</div>
</div>
<div class="section">
<h2>📝 推奨事項</h2>
<div class="insight">
<h4>より精度の高い比較のために</h4>
<ul>
<li><strong>同日・同時刻の比較:</strong> 可能であれば同じ気象条件下でテストを実施</li>
<li><strong>風の記録:</strong> 風速・風向データがあればより正確なCdA推定が可能</li>
<li><strong>パワーメーター校正:</strong> 両方のバイクで同じパワーメーターを使用するか、校正を確認</li>
<li><strong>ポジションの統一:</strong> できるだけ同じポジション(エアロポジションなど)で走行</li>
<li><strong>タイヤとウェアの統一:</strong> 転がり抵抗やウェアの空気抵抗の影響を排除</li>
<li><strong>複数回のテスト:</strong> データの再現性を確認するため、複数日のデータを収集</li>
<li><strong>心拍数の考慮:</strong> 同じ生理学的負荷で比較することも重要</li>
</ul>
</div>
</div>
<div class="footer">
<p>Generated by Fit Gear Stat Analysis Tool</p>
<p>データソース: aeroad.fit, tarmac.fit</p>
<p>{datetime.now().strftime('%Y年%m月%d日 %H:%M:%S')}</p>
</div>
</body>
</html>
"""
with open('./aero_analysis_report.html', 'w', encoding='utf-8') as f:
f.write(html_content)
print("HTML report generated: aero_analysis_report.html")
def main():
print("=" * 80)
print("自転車エアロ性能分析: Aeroad vs Tarmac")
print("=" * 80)
# Aeroad の分析
print("\n[1/2] Analyzing Aeroad...")
aeroad = FitFileAnalyzer('./aeroad.fit', 'Aeroad')
aeroad.parse_fit_file()
aeroad_palace = aeroad.detect_imperial_palace_laps()
aeroad_metrics = aeroad.calculate_aero_metrics(aeroad_palace)
print("\nAeroad Metrics:")
for key, value in aeroad_metrics.items():
print(f" {key}: {value}")
# Tarmac の分析
print("\n[2/2] Analyzing Tarmac...")
tarmac = FitFileAnalyzer('./tarmac.fit', 'Tarmac')
tarmac.parse_fit_file()
tarmac_palace = tarmac.detect_imperial_palace_laps()
tarmac_metrics = tarmac.calculate_aero_metrics(tarmac_palace)
print("\nTarmac Metrics:")
for key, value in tarmac_metrics.items():
print(f" {key}: {value}")
# 可視化
print("\n[3/4] Generating visualizations...")
figures = create_visualizations(aeroad_palace, tarmac_palace, aeroad_metrics, tarmac_metrics)
print(f"Generated {len(figures)} figures")
# レポート生成
print("\n[4/4] Generating HTML report...")
generate_html_report(aeroad_metrics, tarmac_metrics, figures, aeroad_palace, tarmac_palace)
print("\n" + "=" * 80)
print("分析完了!")
print("レポート: aero_analysis_report.html")
print("=" * 80)
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment