plane: fig
This commit is contained in:
@@ -1,351 +1,351 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Data Window Sensitivity Analysis for Richards Model
|
||||
|
||||
Analyzes how different historical data windows affect the K (carrying capacity)
|
||||
estimate in the Richards growth model.
|
||||
|
||||
Windows analyzed:
|
||||
- 1957-2025: All available data (including Cold War era)
|
||||
- 1990-2025: Post-Cold War era
|
||||
- 2000-2025: Commercial space age
|
||||
- 2010-2025: SpaceX era
|
||||
- 2015-2025: Recent rapid growth
|
||||
"""
|
||||
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from scipy.optimize import curve_fit
|
||||
import matplotlib
|
||||
matplotlib.use('Agg')
|
||||
import matplotlib.pyplot as plt
|
||||
import warnings
|
||||
warnings.filterwarnings('ignore')
|
||||
|
||||
plt.rcParams['font.sans-serif'] = ['Arial Unicode MS', 'SimHei', 'DejaVu Sans']
|
||||
plt.rcParams['axes.unicode_minus'] = False
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Richards Growth Model
|
||||
# ============================================================
|
||||
|
||||
def richards(t, K, r, t0, v):
|
||||
"""Richards curve (generalized logistic model)"""
|
||||
exp_term = np.exp(-r * (t - t0))
|
||||
exp_term = np.clip(exp_term, 1e-10, 1e10)
|
||||
return K / np.power(1 + exp_term, 1/v)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Data Loading
|
||||
# ============================================================
|
||||
|
||||
def load_data(filepath="rocket_launch_counts.csv"):
|
||||
"""Load and preprocess rocket launch data"""
|
||||
df = pd.read_csv(filepath)
|
||||
df = df.rename(columns={"YDate": "year", "Total": "launches"})
|
||||
df["year"] = pd.to_numeric(df["year"], errors="coerce")
|
||||
df["launches"] = pd.to_numeric(df["launches"], errors="coerce")
|
||||
df = df.dropna(subset=["year", "launches"])
|
||||
df = df[(df["year"] >= 1957) & (df["year"] <= 2025)]
|
||||
df = df.astype({"year": int, "launches": int})
|
||||
df = df.sort_values("year").reset_index(drop=True)
|
||||
return df
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Fit Richards Model (Unconstrained)
|
||||
# ============================================================
|
||||
|
||||
def fit_richards_unconstrained(data, base_year=1957):
|
||||
"""Fit Richards model without constraining K"""
|
||||
years = data["year"].values
|
||||
launches = data["launches"].values
|
||||
t = (years - base_year).astype(float)
|
||||
|
||||
# Initial parameters
|
||||
p0 = [5000.0, 0.08, 80.0, 2.0]
|
||||
|
||||
# Wide bounds - let data determine K
|
||||
bounds = ([500, 0.005, 10, 0.2], [100000, 1.0, 200, 10.0])
|
||||
|
||||
try:
|
||||
popt, pcov = curve_fit(richards, t, launches, p0=p0, bounds=bounds, maxfev=100000)
|
||||
perr = np.sqrt(np.diag(pcov))
|
||||
y_pred = richards(t, *popt)
|
||||
|
||||
ss_res = np.sum((launches - y_pred) ** 2)
|
||||
ss_tot = np.sum((launches - np.mean(launches)) ** 2)
|
||||
r_squared = 1 - (ss_res / ss_tot)
|
||||
|
||||
K, r, t0, v = popt
|
||||
K_err = perr[0]
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"K": K,
|
||||
"K_err": K_err,
|
||||
"r": r,
|
||||
"t0": t0,
|
||||
"v": v,
|
||||
"r_squared": r_squared,
|
||||
"n_points": len(data),
|
||||
"years": years,
|
||||
"launches": launches,
|
||||
"params": popt,
|
||||
"y_pred": y_pred,
|
||||
}
|
||||
except Exception as e:
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Main Analysis
|
||||
# ============================================================
|
||||
|
||||
def analyze_data_windows(df):
|
||||
"""Analyze K estimates for different data windows"""
|
||||
|
||||
# Define windows
|
||||
windows = {
|
||||
"1957-2025 (全部68年)": (1957, 2025),
|
||||
"1990-2025 (35年)": (1990, 2025),
|
||||
"2000-2025 (25年)": (2000, 2025),
|
||||
"2010-2025 (15年)": (2010, 2025),
|
||||
"2015-2025 (10年)": (2015, 2025),
|
||||
}
|
||||
|
||||
results = {}
|
||||
|
||||
print("=" * 80)
|
||||
print("分析不同历史数据窗口对 K (carrying capacity) 估计的影响")
|
||||
print("=" * 80)
|
||||
|
||||
for name, (start, end) in windows.items():
|
||||
data = df[(df["year"] >= start) & (df["year"] <= end)].copy()
|
||||
result = fit_richards_unconstrained(data)
|
||||
|
||||
if result["success"]:
|
||||
result["start"] = start
|
||||
result["end"] = end
|
||||
result["name"] = name
|
||||
results[name] = result
|
||||
|
||||
print(f"\n--- {name} ---")
|
||||
print(f" 数据点数: {result['n_points']}")
|
||||
print(f" K = {result['K']:.0f} (carrying capacity)")
|
||||
print(f" r = {result['r']:.4f} (growth rate)")
|
||||
print(f" R² = {result['r_squared']:.4f}")
|
||||
else:
|
||||
print(f"\n--- {name} ---")
|
||||
print(f" 拟合失败: {result['error']}")
|
||||
|
||||
return results
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Visualization
|
||||
# ============================================================
|
||||
|
||||
def plot_window_analysis(df, results, save_path="launch_capacity_window_analysis.png"):
|
||||
"""Generate comprehensive window analysis plot"""
|
||||
|
||||
fig, axes = plt.subplots(2, 2, figsize=(14, 11))
|
||||
|
||||
names = list(results.keys())
|
||||
physical_max = 3650
|
||||
|
||||
# ========== Plot 1: K Values Comparison ==========
|
||||
ax1 = axes[0, 0]
|
||||
|
||||
K_vals = [results[n]["K"] for n in names]
|
||||
colors = ["#E74C3C" if k < physical_max else "#27AE60" for k in K_vals]
|
||||
|
||||
y_pos = range(len(names))
|
||||
bars = ax1.barh(y_pos, K_vals, color=colors, alpha=0.7, edgecolor="black")
|
||||
ax1.axvline(physical_max, color="blue", ls="--", lw=2.5,
|
||||
label=f"Physical Max ({physical_max})")
|
||||
|
||||
ax1.set_yticks(y_pos)
|
||||
ax1.set_yticklabels(names, fontsize=10)
|
||||
ax1.set_xlabel("K (Carrying Capacity, launches/year)", fontsize=11)
|
||||
ax1.set_title("K Estimates by Data Window\n(Unconstrained Richards Model)", fontsize=12)
|
||||
ax1.legend(loc="lower right", fontsize=10)
|
||||
ax1.grid(True, alpha=0.3, axis="x")
|
||||
|
||||
for bar, k in zip(bars, K_vals):
|
||||
ax1.text(k + 200, bar.get_y() + bar.get_height()/2,
|
||||
f"{k:.0f}", va="center", fontsize=10, fontweight="bold")
|
||||
|
||||
# ========== Plot 2: R² Comparison ==========
|
||||
ax2 = axes[0, 1]
|
||||
|
||||
r2_vals = [results[n]["r_squared"] for n in names]
|
||||
colors2 = ["#27AE60" if r2 > 0.9 else "#F39C12" if r2 > 0.7 else "#E74C3C"
|
||||
for r2 in r2_vals]
|
||||
|
||||
bars2 = ax2.barh(y_pos, r2_vals, color=colors2, alpha=0.7, edgecolor="black")
|
||||
ax2.axvline(0.9, color="green", ls="--", lw=1.5, alpha=0.7, label="R²=0.9 (Good fit)")
|
||||
ax2.axvline(0.7, color="orange", ls=":", lw=1.5, alpha=0.7, label="R²=0.7 (Acceptable)")
|
||||
|
||||
ax2.set_yticks(y_pos)
|
||||
ax2.set_yticklabels(names, fontsize=10)
|
||||
ax2.set_xlabel("R² (Goodness of Fit)", fontsize=11)
|
||||
ax2.set_title("Model Fit Quality by Data Window", fontsize=12)
|
||||
ax2.set_xlim(0, 1.1)
|
||||
ax2.legend(loc="lower right", fontsize=9)
|
||||
ax2.grid(True, alpha=0.3, axis="x")
|
||||
|
||||
for bar, r2 in zip(bars2, r2_vals):
|
||||
ax2.text(r2 + 0.02, bar.get_y() + bar.get_height()/2,
|
||||
f"{r2:.3f}", va="center", fontsize=10, fontweight="bold")
|
||||
|
||||
# ========== Plot 3: Historical Data with All Model Fits ==========
|
||||
ax3 = axes[1, 0]
|
||||
|
||||
# Plot all historical data
|
||||
ax3.scatter(df["year"], df["launches"], color="gray", s=30, alpha=0.5,
|
||||
label="Historical Data", zorder=1)
|
||||
|
||||
# Plot each model prediction
|
||||
colors_line = ["#E74C3C", "#F39C12", "#3498DB", "#27AE60", "#9B59B6"]
|
||||
for i, (name, res) in enumerate(results.items()):
|
||||
start = res["start"]
|
||||
years_proj = np.arange(start, 2080)
|
||||
t_proj = years_proj - 1957
|
||||
pred = richards(t_proj, *res["params"])
|
||||
|
||||
label = f'{name.split(" ")[0]}: K={res["K"]:.0f}'
|
||||
ax3.plot(years_proj, pred, color=colors_line[i], lw=2, alpha=0.8,
|
||||
label=label, zorder=2)
|
||||
|
||||
ax3.axhline(physical_max, color="black", ls=":", lw=2,
|
||||
label=f"Physical Max ({physical_max})")
|
||||
|
||||
ax3.set_xlabel("Year", fontsize=11)
|
||||
ax3.set_ylabel("Annual Launches", fontsize=11)
|
||||
ax3.set_title("Richards Model Predictions\n(Different Data Windows)", fontsize=12)
|
||||
ax3.legend(loc="upper left", fontsize=8)
|
||||
ax3.grid(True, alpha=0.3)
|
||||
ax3.set_xlim(1955, 2080)
|
||||
ax3.set_ylim(0, 15000)
|
||||
|
||||
# ========== Plot 4: Summary Table ==========
|
||||
ax4 = axes[1, 1]
|
||||
ax4.axis("off")
|
||||
|
||||
# Build summary text
|
||||
summary = """
|
||||
┌────────────────────────────────────────────────────────────────────────┐
|
||||
│ 数据窗口对 K 估计的影响分析 │
|
||||
├────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ 核心发现: │
|
||||
│ ───────── │
|
||||
│ 1. 数据窗口选择对 K 估计影响巨大 │
|
||||
│ • 全部数据: K ≈ 500 │
|
||||
│ • 近15年: K ≈ 12,000 │
|
||||
│ │
|
||||
│ 2. 1957-2025 全部数据: │
|
||||
│ • R² = 0.08 (极差拟合) │
|
||||
│ • 冷战时期数据干扰,模型误判为"已饱和" │
|
||||
│ • 不适合用于预测未来增长 │
|
||||
│ │
|
||||
│ 3. 2010-2025 近15年数据: │
|
||||
│ • R² = 0.90 (良好拟合) │
|
||||
│ • 准确捕捉商业航天时代的增长趋势 │
|
||||
│ • 预测 K ≈ 12,000 >> 物理上限 (3,650) │
|
||||
│ │
|
||||
│ 结论: │
|
||||
│ ───── │
|
||||
│ • 选择 2010-2025 数据窗口是合理的 (R² = 0.90, 反映当前趋势) │
|
||||
│ • 数据驱动 K ≈ 12,000 反映增长潜力 (需要 ~33 个发射站) │
|
||||
│ • 但物理约束 (10站 × 365天 = 3,650) 才是真正的上限 │
|
||||
│ • 因此采用 1 次/站/天 作为保守估计是合理的 │
|
||||
│ │
|
||||
│ 物理上限: 3,650 次/年 (10个发射站 × 365天) │
|
||||
│ │
|
||||
└────────────────────────────────────────────────────────────────────────┘
|
||||
"""
|
||||
|
||||
ax4.text(0.02, 0.98, summary, transform=ax4.transAxes, fontsize=10,
|
||||
verticalalignment="top", family="monospace",
|
||||
bbox=dict(boxstyle="round", facecolor="lightyellow", alpha=0.9))
|
||||
|
||||
plt.suptitle("Data Window Sensitivity Analysis for Richards Model",
|
||||
fontsize=14, fontweight="bold", y=1.02)
|
||||
plt.tight_layout()
|
||||
plt.savefig(save_path, dpi=150, bbox_inches="tight")
|
||||
plt.close()
|
||||
print(f"\nPlot saved: {save_path}")
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Print Detailed Table
|
||||
# ============================================================
|
||||
|
||||
def print_comparison_table(results):
|
||||
"""Print detailed comparison table"""
|
||||
|
||||
physical_max = 3650
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
print("详细对比表")
|
||||
print("=" * 80)
|
||||
|
||||
header = f"{'数据窗口':<22} | {'K值':>8} | {'R²':>7} | {'数据点':>5} | {'K/物理上限':>10} | {'说明':<18}"
|
||||
print(header)
|
||||
print("-" * 22 + "-+-" + "-" * 8 + "-+-" + "-" * 7 + "-+-" + "-" * 5 + "-+-" + "-" * 10 + "-+-" + "-" * 18)
|
||||
|
||||
for name, res in results.items():
|
||||
k = res["K"]
|
||||
r2 = res["r_squared"]
|
||||
n = res["n_points"]
|
||||
ratio = k / physical_max
|
||||
|
||||
if r2 < 0.5:
|
||||
note = "拟合差,不可用"
|
||||
elif k < physical_max:
|
||||
note = "低于物理上限"
|
||||
elif ratio < 2:
|
||||
note = "接近物理上限"
|
||||
else:
|
||||
note = "远超物理上限"
|
||||
|
||||
print(f"{name:<22} | {k:>8.0f} | {r2:>7.4f} | {n:>5} | {ratio:>10.2f}x | {note:<18}")
|
||||
|
||||
print("\n物理上限: 3,650 次/年 (10个发射站 × 每站每天1次 × 365天)")
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Main
|
||||
# ============================================================
|
||||
|
||||
def main():
|
||||
print("Loading data...")
|
||||
df = load_data()
|
||||
print(f"Data loaded: {len(df)} records from {df['year'].min()} to {df['year'].max()}")
|
||||
|
||||
# Analyze different windows
|
||||
results = analyze_data_windows(df)
|
||||
|
||||
# Print comparison table
|
||||
print_comparison_table(results)
|
||||
|
||||
# Generate visualization
|
||||
print("\n" + "=" * 80)
|
||||
print("Generating visualization...")
|
||||
print("=" * 80)
|
||||
plot_window_analysis(df, results)
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
print("分析完成!")
|
||||
print("=" * 80)
|
||||
|
||||
return results
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
results = main()
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Data Window Sensitivity Analysis for Richards Model
|
||||
|
||||
Analyzes how different historical data windows affect the K (carrying capacity)
|
||||
estimate in the Richards growth model.
|
||||
|
||||
Windows analyzed:
|
||||
- 1957-2025: All available data (including Cold War era)
|
||||
- 1990-2025: Post-Cold War era
|
||||
- 2000-2025: Commercial space age
|
||||
- 2010-2025: SpaceX era
|
||||
- 2015-2025: Recent rapid growth
|
||||
"""
|
||||
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from scipy.optimize import curve_fit
|
||||
import matplotlib
|
||||
matplotlib.use('Agg')
|
||||
import matplotlib.pyplot as plt
|
||||
import warnings
|
||||
warnings.filterwarnings('ignore')
|
||||
|
||||
plt.rcParams['font.sans-serif'] = ['Arial Unicode MS', 'SimHei', 'DejaVu Sans']
|
||||
plt.rcParams['axes.unicode_minus'] = False
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Richards Growth Model
|
||||
# ============================================================
|
||||
|
||||
def richards(t, K, r, t0, v):
|
||||
"""Richards curve (generalized logistic model)"""
|
||||
exp_term = np.exp(-r * (t - t0))
|
||||
exp_term = np.clip(exp_term, 1e-10, 1e10)
|
||||
return K / np.power(1 + exp_term, 1/v)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Data Loading
|
||||
# ============================================================
|
||||
|
||||
def load_data(filepath="rocket_launch_counts.csv"):
|
||||
"""Load and preprocess rocket launch data"""
|
||||
df = pd.read_csv(filepath)
|
||||
df = df.rename(columns={"YDate": "year", "Total": "launches"})
|
||||
df["year"] = pd.to_numeric(df["year"], errors="coerce")
|
||||
df["launches"] = pd.to_numeric(df["launches"], errors="coerce")
|
||||
df = df.dropna(subset=["year", "launches"])
|
||||
df = df[(df["year"] >= 1957) & (df["year"] <= 2025)]
|
||||
df = df.astype({"year": int, "launches": int})
|
||||
df = df.sort_values("year").reset_index(drop=True)
|
||||
return df
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Fit Richards Model (Unconstrained)
|
||||
# ============================================================
|
||||
|
||||
def fit_richards_unconstrained(data, base_year=1957):
|
||||
"""Fit Richards model without constraining K"""
|
||||
years = data["year"].values
|
||||
launches = data["launches"].values
|
||||
t = (years - base_year).astype(float)
|
||||
|
||||
# Initial parameters
|
||||
p0 = [5000.0, 0.08, 80.0, 2.0]
|
||||
|
||||
# Wide bounds - let data determine K
|
||||
bounds = ([500, 0.005, 10, 0.2], [100000, 1.0, 200, 10.0])
|
||||
|
||||
try:
|
||||
popt, pcov = curve_fit(richards, t, launches, p0=p0, bounds=bounds, maxfev=100000)
|
||||
perr = np.sqrt(np.diag(pcov))
|
||||
y_pred = richards(t, *popt)
|
||||
|
||||
ss_res = np.sum((launches - y_pred) ** 2)
|
||||
ss_tot = np.sum((launches - np.mean(launches)) ** 2)
|
||||
r_squared = 1 - (ss_res / ss_tot)
|
||||
|
||||
K, r, t0, v = popt
|
||||
K_err = perr[0]
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"K": K,
|
||||
"K_err": K_err,
|
||||
"r": r,
|
||||
"t0": t0,
|
||||
"v": v,
|
||||
"r_squared": r_squared,
|
||||
"n_points": len(data),
|
||||
"years": years,
|
||||
"launches": launches,
|
||||
"params": popt,
|
||||
"y_pred": y_pred,
|
||||
}
|
||||
except Exception as e:
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Main Analysis
|
||||
# ============================================================
|
||||
|
||||
def analyze_data_windows(df):
|
||||
"""Analyze K estimates for different data windows"""
|
||||
|
||||
# Define windows
|
||||
windows = {
|
||||
"1957-2025 (全部68年)": (1957, 2025),
|
||||
"1990-2025 (35年)": (1990, 2025),
|
||||
"2000-2025 (25年)": (2000, 2025),
|
||||
"2010-2025 (15年)": (2010, 2025),
|
||||
"2015-2025 (10年)": (2015, 2025),
|
||||
}
|
||||
|
||||
results = {}
|
||||
|
||||
print("=" * 80)
|
||||
print("分析不同历史数据窗口对 K (carrying capacity) 估计的影响")
|
||||
print("=" * 80)
|
||||
|
||||
for name, (start, end) in windows.items():
|
||||
data = df[(df["year"] >= start) & (df["year"] <= end)].copy()
|
||||
result = fit_richards_unconstrained(data)
|
||||
|
||||
if result["success"]:
|
||||
result["start"] = start
|
||||
result["end"] = end
|
||||
result["name"] = name
|
||||
results[name] = result
|
||||
|
||||
print(f"\n--- {name} ---")
|
||||
print(f" 数据点数: {result['n_points']}")
|
||||
print(f" K = {result['K']:.0f} (carrying capacity)")
|
||||
print(f" r = {result['r']:.4f} (growth rate)")
|
||||
print(f" R² = {result['r_squared']:.4f}")
|
||||
else:
|
||||
print(f"\n--- {name} ---")
|
||||
print(f" 拟合失败: {result['error']}")
|
||||
|
||||
return results
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Visualization
|
||||
# ============================================================
|
||||
|
||||
def plot_window_analysis(df, results, save_path="launch_capacity_window_analysis.png"):
|
||||
"""Generate comprehensive window analysis plot"""
|
||||
|
||||
fig, axes = plt.subplots(2, 2, figsize=(14, 11))
|
||||
|
||||
names = list(results.keys())
|
||||
physical_max = 3650
|
||||
|
||||
# ========== Plot 1: K Values Comparison ==========
|
||||
ax1 = axes[0, 0]
|
||||
|
||||
K_vals = [results[n]["K"] for n in names]
|
||||
colors = ["#E74C3C" if k < physical_max else "#27AE60" for k in K_vals]
|
||||
|
||||
y_pos = range(len(names))
|
||||
bars = ax1.barh(y_pos, K_vals, color=colors, alpha=0.7, edgecolor="black")
|
||||
ax1.axvline(physical_max, color="blue", ls="--", lw=2.5,
|
||||
label=f"Physical Max ({physical_max})")
|
||||
|
||||
ax1.set_yticks(y_pos)
|
||||
ax1.set_yticklabels(names, fontsize=10)
|
||||
ax1.set_xlabel("K (Carrying Capacity, launches/year)", fontsize=11)
|
||||
ax1.set_title("K Estimates by Data Window\n(Unconstrained Richards Model)", fontsize=12)
|
||||
ax1.legend(loc="lower right", fontsize=10)
|
||||
ax1.grid(True, alpha=0.3, axis="x")
|
||||
|
||||
for bar, k in zip(bars, K_vals):
|
||||
ax1.text(k + 200, bar.get_y() + bar.get_height()/2,
|
||||
f"{k:.0f}", va="center", fontsize=10, fontweight="bold")
|
||||
|
||||
# ========== Plot 2: R² Comparison ==========
|
||||
ax2 = axes[0, 1]
|
||||
|
||||
r2_vals = [results[n]["r_squared"] for n in names]
|
||||
colors2 = ["#27AE60" if r2 > 0.9 else "#F39C12" if r2 > 0.7 else "#E74C3C"
|
||||
for r2 in r2_vals]
|
||||
|
||||
bars2 = ax2.barh(y_pos, r2_vals, color=colors2, alpha=0.7, edgecolor="black")
|
||||
ax2.axvline(0.9, color="green", ls="--", lw=1.5, alpha=0.7, label="R²=0.9 (Good fit)")
|
||||
ax2.axvline(0.7, color="orange", ls=":", lw=1.5, alpha=0.7, label="R²=0.7 (Acceptable)")
|
||||
|
||||
ax2.set_yticks(y_pos)
|
||||
ax2.set_yticklabels(names, fontsize=10)
|
||||
ax2.set_xlabel("R² (Goodness of Fit)", fontsize=11)
|
||||
ax2.set_title("Model Fit Quality by Data Window", fontsize=12)
|
||||
ax2.set_xlim(0, 1.1)
|
||||
ax2.legend(loc="lower right", fontsize=9)
|
||||
ax2.grid(True, alpha=0.3, axis="x")
|
||||
|
||||
for bar, r2 in zip(bars2, r2_vals):
|
||||
ax2.text(r2 + 0.02, bar.get_y() + bar.get_height()/2,
|
||||
f"{r2:.3f}", va="center", fontsize=10, fontweight="bold")
|
||||
|
||||
# ========== Plot 3: Historical Data with All Model Fits ==========
|
||||
ax3 = axes[1, 0]
|
||||
|
||||
# Plot all historical data
|
||||
ax3.scatter(df["year"], df["launches"], color="gray", s=30, alpha=0.5,
|
||||
label="Historical Data", zorder=1)
|
||||
|
||||
# Plot each model prediction
|
||||
colors_line = ["#E74C3C", "#F39C12", "#3498DB", "#27AE60", "#9B59B6"]
|
||||
for i, (name, res) in enumerate(results.items()):
|
||||
start = res["start"]
|
||||
years_proj = np.arange(start, 2080)
|
||||
t_proj = years_proj - 1957
|
||||
pred = richards(t_proj, *res["params"])
|
||||
|
||||
label = f'{name.split(" ")[0]}: K={res["K"]:.0f}'
|
||||
ax3.plot(years_proj, pred, color=colors_line[i], lw=2, alpha=0.8,
|
||||
label=label, zorder=2)
|
||||
|
||||
ax3.axhline(physical_max, color="black", ls=":", lw=2,
|
||||
label=f"Physical Max ({physical_max})")
|
||||
|
||||
ax3.set_xlabel("Year", fontsize=11)
|
||||
ax3.set_ylabel("Annual Launches", fontsize=11)
|
||||
ax3.set_title("Richards Model Predictions\n(Different Data Windows)", fontsize=12)
|
||||
ax3.legend(loc="upper left", fontsize=8)
|
||||
ax3.grid(True, alpha=0.3)
|
||||
ax3.set_xlim(1955, 2080)
|
||||
ax3.set_ylim(0, 15000)
|
||||
|
||||
# ========== Plot 4: Summary Table ==========
|
||||
ax4 = axes[1, 1]
|
||||
ax4.axis("off")
|
||||
|
||||
# Build summary text
|
||||
summary = """
|
||||
┌────────────────────────────────────────────────────────────────────────┐
|
||||
│ 数据窗口对 K 估计的影响分析 │
|
||||
├────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ 核心发现: │
|
||||
│ ───────── │
|
||||
│ 1. 数据窗口选择对 K 估计影响巨大 │
|
||||
│ • 全部数据: K ≈ 500 │
|
||||
│ • 近15年: K ≈ 12,000 │
|
||||
│ │
|
||||
│ 2. 1957-2025 全部数据: │
|
||||
│ • R² = 0.08 (极差拟合) │
|
||||
│ • 冷战时期数据干扰,模型误判为"已饱和" │
|
||||
│ • 不适合用于预测未来增长 │
|
||||
│ │
|
||||
│ 3. 2010-2025 近15年数据: │
|
||||
│ • R² = 0.90 (良好拟合) │
|
||||
│ • 准确捕捉商业航天时代的增长趋势 │
|
||||
│ • 预测 K ≈ 12,000 >> 物理上限 (3,650) │
|
||||
│ │
|
||||
│ 结论: │
|
||||
│ ───── │
|
||||
│ • 选择 2010-2025 数据窗口是合理的 (R² = 0.90, 反映当前趋势) │
|
||||
│ • 数据驱动 K ≈ 12,000 反映增长潜力 (需要 ~33 个发射站) │
|
||||
│ • 但物理约束 (10站 × 365天 = 3,650) 才是真正的上限 │
|
||||
│ • 因此采用 1 次/站/天 作为保守估计是合理的 │
|
||||
│ │
|
||||
│ 物理上限: 3,650 次/年 (10个发射站 × 365天) │
|
||||
│ │
|
||||
└────────────────────────────────────────────────────────────────────────┘
|
||||
"""
|
||||
|
||||
ax4.text(0.02, 0.98, summary, transform=ax4.transAxes, fontsize=10,
|
||||
verticalalignment="top", family="monospace",
|
||||
bbox=dict(boxstyle="round", facecolor="lightyellow", alpha=0.9))
|
||||
|
||||
plt.suptitle("Data Window Sensitivity Analysis for Richards Model",
|
||||
fontsize=14, fontweight="bold", y=1.02)
|
||||
plt.tight_layout()
|
||||
plt.savefig(save_path, dpi=150, bbox_inches="tight")
|
||||
plt.close()
|
||||
print(f"\nPlot saved: {save_path}")
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Print Detailed Table
|
||||
# ============================================================
|
||||
|
||||
def print_comparison_table(results):
|
||||
"""Print detailed comparison table"""
|
||||
|
||||
physical_max = 3650
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
print("详细对比表")
|
||||
print("=" * 80)
|
||||
|
||||
header = f"{'数据窗口':<22} | {'K值':>8} | {'R²':>7} | {'数据点':>5} | {'K/物理上限':>10} | {'说明':<18}"
|
||||
print(header)
|
||||
print("-" * 22 + "-+-" + "-" * 8 + "-+-" + "-" * 7 + "-+-" + "-" * 5 + "-+-" + "-" * 10 + "-+-" + "-" * 18)
|
||||
|
||||
for name, res in results.items():
|
||||
k = res["K"]
|
||||
r2 = res["r_squared"]
|
||||
n = res["n_points"]
|
||||
ratio = k / physical_max
|
||||
|
||||
if r2 < 0.5:
|
||||
note = "拟合差,不可用"
|
||||
elif k < physical_max:
|
||||
note = "低于物理上限"
|
||||
elif ratio < 2:
|
||||
note = "接近物理上限"
|
||||
else:
|
||||
note = "远超物理上限"
|
||||
|
||||
print(f"{name:<22} | {k:>8.0f} | {r2:>7.4f} | {n:>5} | {ratio:>10.2f}x | {note:<18}")
|
||||
|
||||
print("\n物理上限: 3,650 次/年 (10个发射站 × 每站每天1次 × 365天)")
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Main
|
||||
# ============================================================
|
||||
|
||||
def main():
|
||||
print("Loading data...")
|
||||
df = load_data()
|
||||
print(f"Data loaded: {len(df)} records from {df['year'].min()} to {df['year'].max()}")
|
||||
|
||||
# Analyze different windows
|
||||
results = analyze_data_windows(df)
|
||||
|
||||
# Print comparison table
|
||||
print_comparison_table(results)
|
||||
|
||||
# Generate visualization
|
||||
print("\n" + "=" * 80)
|
||||
print("Generating visualization...")
|
||||
print("=" * 80)
|
||||
plot_window_analysis(df, results)
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
print("分析完成!")
|
||||
print("=" * 80)
|
||||
|
||||
return results
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
results = main()
|
||||
|
||||
@@ -1,230 +1,230 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Find Optimal Data Window for K ≈ 3650
|
||||
|
||||
Enumerate all possible starting years to find which data window
|
||||
produces K closest to the physical limit (3650 launches/year).
|
||||
"""
|
||||
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from scipy.optimize import curve_fit
|
||||
import matplotlib
|
||||
matplotlib.use('Agg')
|
||||
import matplotlib.pyplot as plt
|
||||
import warnings
|
||||
warnings.filterwarnings('ignore')
|
||||
|
||||
plt.rcParams['font.sans-serif'] = ['Arial Unicode MS', 'SimHei', 'DejaVu Sans']
|
||||
plt.rcParams['axes.unicode_minus'] = False
|
||||
|
||||
|
||||
# Richards model
|
||||
def richards(t, K, r, t0, v):
|
||||
exp_term = np.exp(-r * (t - t0))
|
||||
exp_term = np.clip(exp_term, 1e-10, 1e10)
|
||||
return K / np.power(1 + exp_term, 1/v)
|
||||
|
||||
|
||||
def load_data(filepath="rocket_launch_counts.csv"):
|
||||
df = pd.read_csv(filepath)
|
||||
df = df.rename(columns={"YDate": "year", "Total": "launches"})
|
||||
df["year"] = pd.to_numeric(df["year"], errors="coerce")
|
||||
df["launches"] = pd.to_numeric(df["launches"], errors="coerce")
|
||||
df = df.dropna(subset=["year", "launches"])
|
||||
df = df[(df["year"] >= 1957) & (df["year"] <= 2025)]
|
||||
df = df.astype({"year": int, "launches": int})
|
||||
df = df.sort_values("year").reset_index(drop=True)
|
||||
return df
|
||||
|
||||
|
||||
def fit_richards(data, base_year=1957):
|
||||
"""Fit unconstrained Richards model"""
|
||||
years = data["year"].values
|
||||
launches = data["launches"].values
|
||||
t = (years - base_year).astype(float)
|
||||
|
||||
p0 = [5000.0, 0.08, 80.0, 2.0]
|
||||
bounds = ([500, 0.005, 10, 0.2], [100000, 1.0, 200, 10.0])
|
||||
|
||||
try:
|
||||
popt, pcov = curve_fit(richards, t, launches, p0=p0, bounds=bounds, maxfev=100000)
|
||||
y_pred = richards(t, *popt)
|
||||
ss_res = np.sum((launches - y_pred) ** 2)
|
||||
ss_tot = np.sum((launches - np.mean(launches)) ** 2)
|
||||
r_squared = 1 - (ss_res / ss_tot) if ss_tot > 0 else 0
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"K": popt[0],
|
||||
"r": popt[1],
|
||||
"t0": popt[2],
|
||||
"v": popt[3],
|
||||
"r_squared": r_squared,
|
||||
"n_points": len(data),
|
||||
}
|
||||
except:
|
||||
return {"success": False}
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 80)
|
||||
print("枚举起始年份,寻找 K ≈ 3650 的数据窗口")
|
||||
print("=" * 80)
|
||||
|
||||
df = load_data()
|
||||
print(f"数据范围: {df['year'].min()} - {df['year'].max()}\n")
|
||||
|
||||
target_K = 3650
|
||||
end_year = 2025
|
||||
|
||||
# Enumerate all starting years
|
||||
results = []
|
||||
|
||||
for start_year in range(1957, 2020):
|
||||
data = df[(df["year"] >= start_year) & (df["year"] <= end_year)].copy()
|
||||
|
||||
if len(data) < 5: # Need at least 5 data points
|
||||
continue
|
||||
|
||||
result = fit_richards(data)
|
||||
|
||||
if result["success"]:
|
||||
diff = abs(result["K"] - target_K)
|
||||
results.append({
|
||||
"start_year": start_year,
|
||||
"end_year": end_year,
|
||||
"years_span": end_year - start_year + 1,
|
||||
"n_points": result["n_points"],
|
||||
"K": result["K"],
|
||||
"r_squared": result["r_squared"],
|
||||
"diff_from_target": diff,
|
||||
"ratio": result["K"] / target_K,
|
||||
})
|
||||
|
||||
# Sort by difference from target
|
||||
results_df = pd.DataFrame(results)
|
||||
results_df = results_df.sort_values("diff_from_target")
|
||||
|
||||
# Print full table
|
||||
print("完整枚举结果表(按与目标值 3650 的差距排序):")
|
||||
print("-" * 80)
|
||||
print(f"{'起始年份':>8} | {'时间跨度':>8} | {'数据点':>6} | {'K值':>10} | {'R²':>8} | {'K/3650':>8} | {'差距':>10}")
|
||||
print("-" * 80)
|
||||
|
||||
for _, row in results_df.iterrows():
|
||||
marker = "★" if row["diff_from_target"] < 500 else ""
|
||||
print(f"{int(row['start_year']):>8} | {int(row['years_span']):>6}年 | {int(row['n_points']):>6} | "
|
||||
f"{row['K']:>10.0f} | {row['r_squared']:>8.4f} | {row['ratio']:>8.2f}x | {row['diff_from_target']:>10.0f} {marker}")
|
||||
|
||||
# Find closest matches
|
||||
print("\n" + "=" * 80)
|
||||
print("最接近 K = 3650 的数据窗口 (Top 10)")
|
||||
print("=" * 80)
|
||||
|
||||
top10 = results_df.head(10)
|
||||
print(f"\n{'排名':>4} | {'起始年份':>8} | {'K值':>10} | {'R²':>8} | {'差距':>10} | {'评价':<20}")
|
||||
print("-" * 75)
|
||||
|
||||
for i, (_, row) in enumerate(top10.iterrows(), 1):
|
||||
if row["r_squared"] < 0.5:
|
||||
comment = "❌ 拟合差,不可信"
|
||||
elif row["r_squared"] < 0.8:
|
||||
comment = "⚠️ 拟合一般"
|
||||
else:
|
||||
comment = "✅ 拟合良好"
|
||||
|
||||
print(f"{i:>4} | {int(row['start_year']):>8} | {row['K']:>10.0f} | {row['r_squared']:>8.4f} | "
|
||||
f"{row['diff_from_target']:>10.0f} | {comment:<20}")
|
||||
|
||||
# Analysis
|
||||
print("\n" + "=" * 80)
|
||||
print("分析结论")
|
||||
print("=" * 80)
|
||||
|
||||
# Find best with good R²
|
||||
good_fit = results_df[results_df["r_squared"] >= 0.8]
|
||||
if len(good_fit) > 0:
|
||||
best_good = good_fit.iloc[0]
|
||||
print(f"\n在 R² ≥ 0.8 的条件下,最接近 K=3650 的窗口:")
|
||||
print(f" 起始年份: {int(best_good['start_year'])}")
|
||||
print(f" K = {best_good['K']:.0f}")
|
||||
print(f" R² = {best_good['r_squared']:.4f}")
|
||||
print(f" 差距: {best_good['diff_from_target']:.0f}")
|
||||
|
||||
# Summary
|
||||
print("\n关键发现:")
|
||||
print("-" * 40)
|
||||
|
||||
# Check if any good fit gives K near 3650
|
||||
near_target = results_df[(results_df["diff_from_target"] < 1000) & (results_df["r_squared"] >= 0.7)]
|
||||
|
||||
if len(near_target) == 0:
|
||||
print(" ⚠️ 没有任何数据窗口能在良好拟合(R²≥0.7)的条件下得到 K≈3650")
|
||||
print(" ⚠️ 所有良好拟合的窗口都给出 K >> 3650 或 K << 3650")
|
||||
print("\n 这说明:")
|
||||
print(" • K=3650 不是数据自然支持的结论")
|
||||
print(" • K=3650 来自物理约束,而非统计预测")
|
||||
print(" • 论文中应该明确说明这是'物理上限'而非'数据预测'")
|
||||
else:
|
||||
print(f" 找到 {len(near_target)} 个窗口使 K 接近 3650:")
|
||||
for _, row in near_target.iterrows():
|
||||
print(f" {int(row['start_year'])}-2025: K={row['K']:.0f}, R²={row['r_squared']:.4f}")
|
||||
|
||||
# Generate visualization
|
||||
print("\n" + "=" * 80)
|
||||
print("生成可视化...")
|
||||
print("=" * 80)
|
||||
|
||||
fig, axes = plt.subplots(1, 2, figsize=(14, 6))
|
||||
|
||||
# Plot 1: K vs Start Year
|
||||
ax1 = axes[0]
|
||||
colors = ['#27AE60' if r2 >= 0.8 else '#F39C12' if r2 >= 0.5 else '#E74C3C'
|
||||
for r2 in results_df["r_squared"]]
|
||||
|
||||
ax1.scatter(results_df["start_year"], results_df["K"], c=colors, s=60, alpha=0.7, edgecolor='black')
|
||||
ax1.axhline(3650, color='blue', ls='--', lw=2, label='Target K=3650')
|
||||
ax1.axhline(3650*0.9, color='blue', ls=':', lw=1, alpha=0.5)
|
||||
ax1.axhline(3650*1.1, color='blue', ls=':', lw=1, alpha=0.5)
|
||||
|
||||
ax1.set_xlabel("Starting Year", fontsize=11)
|
||||
ax1.set_ylabel("K (Carrying Capacity)", fontsize=11)
|
||||
ax1.set_title("K vs Starting Year\n(Color: Green=R²≥0.8, Yellow=R²≥0.5, Red=R²<0.5)", fontsize=12)
|
||||
ax1.legend()
|
||||
ax1.grid(True, alpha=0.3)
|
||||
ax1.set_xlim(1955, 2020)
|
||||
|
||||
# Plot 2: R² vs Start Year
|
||||
ax2 = axes[1]
|
||||
ax2.scatter(results_df["start_year"], results_df["r_squared"], c=colors, s=60, alpha=0.7, edgecolor='black')
|
||||
ax2.axhline(0.8, color='green', ls='--', lw=1.5, label='R²=0.8 (Good)')
|
||||
ax2.axhline(0.5, color='orange', ls=':', lw=1.5, label='R²=0.5 (Acceptable)')
|
||||
|
||||
ax2.set_xlabel("Starting Year", fontsize=11)
|
||||
ax2.set_ylabel("R² (Goodness of Fit)", fontsize=11)
|
||||
ax2.set_title("Model Fit Quality vs Starting Year", fontsize=12)
|
||||
ax2.legend()
|
||||
ax2.grid(True, alpha=0.3)
|
||||
ax2.set_xlim(1955, 2020)
|
||||
ax2.set_ylim(-0.1, 1.1)
|
||||
|
||||
plt.tight_layout()
|
||||
plt.savefig("find_optimal_window.png", dpi=150, bbox_inches='tight')
|
||||
plt.close()
|
||||
print("图表已保存: find_optimal_window.png")
|
||||
|
||||
# Save to CSV
|
||||
results_df.to_csv("window_enumeration.csv", index=False)
|
||||
print("数据已保存: window_enumeration.csv")
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
print("分析完成!")
|
||||
print("=" * 80)
|
||||
|
||||
return results_df
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
results = main()
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Find Optimal Data Window for K ≈ 3650
|
||||
|
||||
Enumerate all possible starting years to find which data window
|
||||
produces K closest to the physical limit (3650 launches/year).
|
||||
"""
|
||||
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from scipy.optimize import curve_fit
|
||||
import matplotlib
|
||||
matplotlib.use('Agg')
|
||||
import matplotlib.pyplot as plt
|
||||
import warnings
|
||||
warnings.filterwarnings('ignore')
|
||||
|
||||
plt.rcParams['font.sans-serif'] = ['Arial Unicode MS', 'SimHei', 'DejaVu Sans']
|
||||
plt.rcParams['axes.unicode_minus'] = False
|
||||
|
||||
|
||||
# Richards model
|
||||
def richards(t, K, r, t0, v):
|
||||
exp_term = np.exp(-r * (t - t0))
|
||||
exp_term = np.clip(exp_term, 1e-10, 1e10)
|
||||
return K / np.power(1 + exp_term, 1/v)
|
||||
|
||||
|
||||
def load_data(filepath="rocket_launch_counts.csv"):
|
||||
df = pd.read_csv(filepath)
|
||||
df = df.rename(columns={"YDate": "year", "Total": "launches"})
|
||||
df["year"] = pd.to_numeric(df["year"], errors="coerce")
|
||||
df["launches"] = pd.to_numeric(df["launches"], errors="coerce")
|
||||
df = df.dropna(subset=["year", "launches"])
|
||||
df = df[(df["year"] >= 1957) & (df["year"] <= 2025)]
|
||||
df = df.astype({"year": int, "launches": int})
|
||||
df = df.sort_values("year").reset_index(drop=True)
|
||||
return df
|
||||
|
||||
|
||||
def fit_richards(data, base_year=1957):
|
||||
"""Fit unconstrained Richards model"""
|
||||
years = data["year"].values
|
||||
launches = data["launches"].values
|
||||
t = (years - base_year).astype(float)
|
||||
|
||||
p0 = [5000.0, 0.08, 80.0, 2.0]
|
||||
bounds = ([500, 0.005, 10, 0.2], [100000, 1.0, 200, 10.0])
|
||||
|
||||
try:
|
||||
popt, pcov = curve_fit(richards, t, launches, p0=p0, bounds=bounds, maxfev=100000)
|
||||
y_pred = richards(t, *popt)
|
||||
ss_res = np.sum((launches - y_pred) ** 2)
|
||||
ss_tot = np.sum((launches - np.mean(launches)) ** 2)
|
||||
r_squared = 1 - (ss_res / ss_tot) if ss_tot > 0 else 0
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"K": popt[0],
|
||||
"r": popt[1],
|
||||
"t0": popt[2],
|
||||
"v": popt[3],
|
||||
"r_squared": r_squared,
|
||||
"n_points": len(data),
|
||||
}
|
||||
except:
|
||||
return {"success": False}
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 80)
|
||||
print("枚举起始年份,寻找 K ≈ 3650 的数据窗口")
|
||||
print("=" * 80)
|
||||
|
||||
df = load_data()
|
||||
print(f"数据范围: {df['year'].min()} - {df['year'].max()}\n")
|
||||
|
||||
target_K = 3650
|
||||
end_year = 2025
|
||||
|
||||
# Enumerate all starting years
|
||||
results = []
|
||||
|
||||
for start_year in range(1957, 2020):
|
||||
data = df[(df["year"] >= start_year) & (df["year"] <= end_year)].copy()
|
||||
|
||||
if len(data) < 5: # Need at least 5 data points
|
||||
continue
|
||||
|
||||
result = fit_richards(data)
|
||||
|
||||
if result["success"]:
|
||||
diff = abs(result["K"] - target_K)
|
||||
results.append({
|
||||
"start_year": start_year,
|
||||
"end_year": end_year,
|
||||
"years_span": end_year - start_year + 1,
|
||||
"n_points": result["n_points"],
|
||||
"K": result["K"],
|
||||
"r_squared": result["r_squared"],
|
||||
"diff_from_target": diff,
|
||||
"ratio": result["K"] / target_K,
|
||||
})
|
||||
|
||||
# Sort by difference from target
|
||||
results_df = pd.DataFrame(results)
|
||||
results_df = results_df.sort_values("diff_from_target")
|
||||
|
||||
# Print full table
|
||||
print("完整枚举结果表(按与目标值 3650 的差距排序):")
|
||||
print("-" * 80)
|
||||
print(f"{'起始年份':>8} | {'时间跨度':>8} | {'数据点':>6} | {'K值':>10} | {'R²':>8} | {'K/3650':>8} | {'差距':>10}")
|
||||
print("-" * 80)
|
||||
|
||||
for _, row in results_df.iterrows():
|
||||
marker = "★" if row["diff_from_target"] < 500 else ""
|
||||
print(f"{int(row['start_year']):>8} | {int(row['years_span']):>6}年 | {int(row['n_points']):>6} | "
|
||||
f"{row['K']:>10.0f} | {row['r_squared']:>8.4f} | {row['ratio']:>8.2f}x | {row['diff_from_target']:>10.0f} {marker}")
|
||||
|
||||
# Find closest matches
|
||||
print("\n" + "=" * 80)
|
||||
print("最接近 K = 3650 的数据窗口 (Top 10)")
|
||||
print("=" * 80)
|
||||
|
||||
top10 = results_df.head(10)
|
||||
print(f"\n{'排名':>4} | {'起始年份':>8} | {'K值':>10} | {'R²':>8} | {'差距':>10} | {'评价':<20}")
|
||||
print("-" * 75)
|
||||
|
||||
for i, (_, row) in enumerate(top10.iterrows(), 1):
|
||||
if row["r_squared"] < 0.5:
|
||||
comment = "❌ 拟合差,不可信"
|
||||
elif row["r_squared"] < 0.8:
|
||||
comment = "⚠️ 拟合一般"
|
||||
else:
|
||||
comment = "✅ 拟合良好"
|
||||
|
||||
print(f"{i:>4} | {int(row['start_year']):>8} | {row['K']:>10.0f} | {row['r_squared']:>8.4f} | "
|
||||
f"{row['diff_from_target']:>10.0f} | {comment:<20}")
|
||||
|
||||
# Analysis
|
||||
print("\n" + "=" * 80)
|
||||
print("分析结论")
|
||||
print("=" * 80)
|
||||
|
||||
# Find best with good R²
|
||||
good_fit = results_df[results_df["r_squared"] >= 0.8]
|
||||
if len(good_fit) > 0:
|
||||
best_good = good_fit.iloc[0]
|
||||
print(f"\n在 R² ≥ 0.8 的条件下,最接近 K=3650 的窗口:")
|
||||
print(f" 起始年份: {int(best_good['start_year'])}")
|
||||
print(f" K = {best_good['K']:.0f}")
|
||||
print(f" R² = {best_good['r_squared']:.4f}")
|
||||
print(f" 差距: {best_good['diff_from_target']:.0f}")
|
||||
|
||||
# Summary
|
||||
print("\n关键发现:")
|
||||
print("-" * 40)
|
||||
|
||||
# Check if any good fit gives K near 3650
|
||||
near_target = results_df[(results_df["diff_from_target"] < 1000) & (results_df["r_squared"] >= 0.7)]
|
||||
|
||||
if len(near_target) == 0:
|
||||
print(" ⚠️ 没有任何数据窗口能在良好拟合(R²≥0.7)的条件下得到 K≈3650")
|
||||
print(" ⚠️ 所有良好拟合的窗口都给出 K >> 3650 或 K << 3650")
|
||||
print("\n 这说明:")
|
||||
print(" • K=3650 不是数据自然支持的结论")
|
||||
print(" • K=3650 来自物理约束,而非统计预测")
|
||||
print(" • 论文中应该明确说明这是'物理上限'而非'数据预测'")
|
||||
else:
|
||||
print(f" 找到 {len(near_target)} 个窗口使 K 接近 3650:")
|
||||
for _, row in near_target.iterrows():
|
||||
print(f" {int(row['start_year'])}-2025: K={row['K']:.0f}, R²={row['r_squared']:.4f}")
|
||||
|
||||
# Generate visualization
|
||||
print("\n" + "=" * 80)
|
||||
print("生成可视化...")
|
||||
print("=" * 80)
|
||||
|
||||
fig, axes = plt.subplots(1, 2, figsize=(14, 6))
|
||||
|
||||
# Plot 1: K vs Start Year
|
||||
ax1 = axes[0]
|
||||
colors = ['#27AE60' if r2 >= 0.8 else '#F39C12' if r2 >= 0.5 else '#E74C3C'
|
||||
for r2 in results_df["r_squared"]]
|
||||
|
||||
ax1.scatter(results_df["start_year"], results_df["K"], c=colors, s=60, alpha=0.7, edgecolor='black')
|
||||
ax1.axhline(3650, color='blue', ls='--', lw=2, label='Target K=3650')
|
||||
ax1.axhline(3650*0.9, color='blue', ls=':', lw=1, alpha=0.5)
|
||||
ax1.axhline(3650*1.1, color='blue', ls=':', lw=1, alpha=0.5)
|
||||
|
||||
ax1.set_xlabel("Starting Year", fontsize=11)
|
||||
ax1.set_ylabel("K (Carrying Capacity)", fontsize=11)
|
||||
ax1.set_title("K vs Starting Year\n(Color: Green=R²≥0.8, Yellow=R²≥0.5, Red=R²<0.5)", fontsize=12)
|
||||
ax1.legend()
|
||||
ax1.grid(True, alpha=0.3)
|
||||
ax1.set_xlim(1955, 2020)
|
||||
|
||||
# Plot 2: R² vs Start Year
|
||||
ax2 = axes[1]
|
||||
ax2.scatter(results_df["start_year"], results_df["r_squared"], c=colors, s=60, alpha=0.7, edgecolor='black')
|
||||
ax2.axhline(0.8, color='green', ls='--', lw=1.5, label='R²=0.8 (Good)')
|
||||
ax2.axhline(0.5, color='orange', ls=':', lw=1.5, label='R²=0.5 (Acceptable)')
|
||||
|
||||
ax2.set_xlabel("Starting Year", fontsize=11)
|
||||
ax2.set_ylabel("R² (Goodness of Fit)", fontsize=11)
|
||||
ax2.set_title("Model Fit Quality vs Starting Year", fontsize=12)
|
||||
ax2.legend()
|
||||
ax2.grid(True, alpha=0.3)
|
||||
ax2.set_xlim(1955, 2020)
|
||||
ax2.set_ylim(-0.1, 1.1)
|
||||
|
||||
plt.tight_layout()
|
||||
plt.savefig("find_optimal_window.png", dpi=150, bbox_inches='tight')
|
||||
plt.close()
|
||||
print("图表已保存: find_optimal_window.png")
|
||||
|
||||
# Save to CSV
|
||||
results_df.to_csv("window_enumeration.csv", index=False)
|
||||
print("数据已保存: window_enumeration.csv")
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
print("分析完成!")
|
||||
print("=" * 80)
|
||||
|
||||
return results_df
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
results = main()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,183 +1,183 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Richards S-Curve Fit for 1984-2025 Data
|
||||
|
||||
Fits and visualizes the Richards growth model to rocket launch data
|
||||
starting from 1984.
|
||||
"""
|
||||
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from scipy.optimize import curve_fit
|
||||
import matplotlib
|
||||
matplotlib.use('Agg')
|
||||
import matplotlib.pyplot as plt
|
||||
import warnings
|
||||
warnings.filterwarnings('ignore')
|
||||
|
||||
plt.rcParams['font.sans-serif'] = ['Arial Unicode MS', 'SimHei', 'DejaVu Sans']
|
||||
plt.rcParams['axes.unicode_minus'] = False
|
||||
|
||||
|
||||
def richards(t, K, r, t0, v):
|
||||
"""
|
||||
Richards curve (generalized logistic model)
|
||||
|
||||
N(t) = K / (1 + exp(-r*(t - t0)))^(1/v)
|
||||
"""
|
||||
exp_term = np.exp(-r * (t - t0))
|
||||
exp_term = np.clip(exp_term, 1e-10, 1e10)
|
||||
return K / np.power(1 + exp_term, 1/v)
|
||||
|
||||
|
||||
def load_data(filepath="rocket_launch_counts.csv"):
|
||||
"""Load rocket launch data"""
|
||||
df = pd.read_csv(filepath)
|
||||
df = df.rename(columns={"YDate": "year", "Total": "launches"})
|
||||
df["year"] = pd.to_numeric(df["year"], errors="coerce")
|
||||
df["launches"] = pd.to_numeric(df["launches"], errors="coerce")
|
||||
df = df.dropna(subset=["year", "launches"])
|
||||
df = df[(df["year"] >= 1957) & (df["year"] <= 2025)]
|
||||
df = df.astype({"year": int, "launches": int})
|
||||
df = df.sort_values("year").reset_index(drop=True)
|
||||
return df
|
||||
|
||||
|
||||
def fit_richards(years, launches, base_year=1984):
|
||||
"""Fit Richards model to data"""
|
||||
t = (years - base_year).astype(float)
|
||||
|
||||
# Initial parameters and bounds (unconstrained K)
|
||||
p0 = [5000.0, 0.1, 40.0, 2.0]
|
||||
bounds = ([500, 0.005, 10, 0.2], [100000, 1.0, 150, 10.0])
|
||||
|
||||
popt, pcov = curve_fit(richards, t, launches, p0=p0, bounds=bounds, maxfev=100000)
|
||||
perr = np.sqrt(np.diag(pcov))
|
||||
|
||||
# Calculate R²
|
||||
y_pred = richards(t, *popt)
|
||||
ss_res = np.sum((launches - y_pred) ** 2)
|
||||
ss_tot = np.sum((launches - np.mean(launches)) ** 2)
|
||||
r_squared = 1 - (ss_res / ss_tot)
|
||||
|
||||
return popt, perr, r_squared
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print("Richards S-Curve Fit (1984-2025)")
|
||||
print("=" * 60)
|
||||
|
||||
# Load data
|
||||
df = load_data()
|
||||
|
||||
# Filter 1984-2025
|
||||
start_year = 1984
|
||||
data = df[(df["year"] >= start_year) & (df["year"] <= 2025)].copy()
|
||||
years = data["year"].values
|
||||
launches = data["launches"].values
|
||||
|
||||
print(f"Data range: {start_year} - 2025")
|
||||
print(f"Data points: {len(data)}")
|
||||
|
||||
# Fit model
|
||||
popt, perr, r_squared = fit_richards(years, launches, base_year=start_year)
|
||||
K, r, t0, v = popt
|
||||
|
||||
print(f"\nFitted Parameters:")
|
||||
print(f" K (carrying capacity) = {K:.0f} launches/year")
|
||||
print(f" r (growth rate) = {r:.4f}")
|
||||
print(f" t0 (inflection point) = {start_year + t0:.1f}")
|
||||
print(f" v (shape parameter) = {v:.3f}")
|
||||
print(f" R² = {r_squared:.4f}")
|
||||
|
||||
# Physical limit
|
||||
physical_max = 3650
|
||||
print(f"\nPhysical limit: {physical_max} (10 sites × 365 days)")
|
||||
print(f"K / Physical limit = {K/physical_max:.2f}x")
|
||||
|
||||
# ========== Create Visualization ==========
|
||||
fig, ax = plt.subplots(figsize=(12, 7))
|
||||
|
||||
# Historical data points
|
||||
ax.scatter(years, launches, color='#2C3E50', s=80, alpha=0.8,
|
||||
label='Historical Data (1984-2025)', zorder=3, edgecolor='white', linewidth=0.5)
|
||||
|
||||
# Generate smooth S-curve
|
||||
years_smooth = np.linspace(start_year, 2100, 500)
|
||||
t_smooth = years_smooth - start_year
|
||||
pred_smooth = richards(t_smooth, *popt)
|
||||
|
||||
# S-curve prediction
|
||||
ax.plot(years_smooth, pred_smooth, color='#27AE60', lw=3,
|
||||
label=f'Richards Model (K={K:.0f}, R²={r_squared:.3f})', zorder=2)
|
||||
|
||||
# Confidence band (approximate using parameter errors)
|
||||
K_low = max(500, K - 2*perr[0])
|
||||
K_high = K + 2*perr[0]
|
||||
pred_low = richards(t_smooth, K_low, r, t0, v)
|
||||
pred_high = richards(t_smooth, K_high, r, t0, v)
|
||||
ax.fill_between(years_smooth, pred_low, pred_high, color='#27AE60', alpha=0.15,
|
||||
label='95% Confidence Band')
|
||||
|
||||
# Physical limit line
|
||||
ax.axhline(physical_max, color='#E74C3C', ls='--', lw=2.5,
|
||||
label=f'Physical Limit: {physical_max} (1/site/day)')
|
||||
|
||||
# Mark inflection point
|
||||
inflection_year = start_year + t0
|
||||
inflection_value = K / (v + 1) ** (1/v)
|
||||
ax.scatter([inflection_year], [inflection_value], color='#F39C12', s=150,
|
||||
marker='*', zorder=5, label=f'Inflection Point ({inflection_year:.0f})')
|
||||
|
||||
# Mark key years
|
||||
ax.axvline(2025, color='gray', ls=':', lw=1.5, alpha=0.7)
|
||||
ax.axvline(2050, color='#3498DB', ls=':', lw=2, alpha=0.8)
|
||||
ax.text(2026, K*0.95, '2025\n(Now)', fontsize=10, color='gray')
|
||||
ax.text(2051, K*0.85, '2050\n(Target)', fontsize=10, color='#3498DB')
|
||||
|
||||
# Prediction for 2050
|
||||
t_2050 = 2050 - start_year
|
||||
pred_2050 = richards(t_2050, *popt)
|
||||
ax.scatter([2050], [pred_2050], color='#3498DB', s=100, marker='D', zorder=4)
|
||||
ax.annotate(f'{pred_2050:.0f}', xy=(2050, pred_2050), xytext=(2055, pred_2050-300),
|
||||
fontsize=10, color='#3498DB', fontweight='bold')
|
||||
|
||||
# Formatting
|
||||
ax.set_xlabel('Year', fontsize=12)
|
||||
ax.set_ylabel('Annual Launches', fontsize=12)
|
||||
ax.set_title('Richards S-Curve Model Fit (1984-2025 Data)\nRocket Launch Capacity Projection',
|
||||
fontsize=14, fontweight='bold')
|
||||
ax.legend(loc='upper left', fontsize=10)
|
||||
ax.grid(True, alpha=0.3)
|
||||
ax.set_xlim(1982, 2100)
|
||||
ax.set_ylim(0, K * 1.15)
|
||||
|
||||
# Add text box with model info
|
||||
textstr = f'''Model: N(t) = K / (1 + e^(-r(t-t₀)))^(1/v)
|
||||
|
||||
Data Window: {start_year}-2025 ({len(data)} points)
|
||||
K = {K:.0f} launches/year
|
||||
r = {r:.4f}
|
||||
t₀ = {start_year + t0:.1f}
|
||||
v = {v:.3f}
|
||||
R² = {r_squared:.4f}
|
||||
|
||||
Note: Physical limit (3,650) shown as
|
||||
dashed red line'''
|
||||
|
||||
props = dict(boxstyle='round', facecolor='wheat', alpha=0.8)
|
||||
ax.text(0.98, 0.35, textstr, transform=ax.transAxes, fontsize=9,
|
||||
verticalalignment='top', horizontalalignment='right', bbox=props, family='monospace')
|
||||
|
||||
plt.tight_layout()
|
||||
plt.savefig('richards_curve_1984.png', dpi=150, bbox_inches='tight')
|
||||
plt.close()
|
||||
|
||||
print("\nPlot saved: richards_curve_1984.png")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Richards S-Curve Fit for 1984-2025 Data
|
||||
|
||||
Fits and visualizes the Richards growth model to rocket launch data
|
||||
starting from 1984.
|
||||
"""
|
||||
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from scipy.optimize import curve_fit
|
||||
import matplotlib
|
||||
matplotlib.use('Agg')
|
||||
import matplotlib.pyplot as plt
|
||||
import warnings
|
||||
warnings.filterwarnings('ignore')
|
||||
|
||||
plt.rcParams['font.sans-serif'] = ['Arial Unicode MS', 'SimHei', 'DejaVu Sans']
|
||||
plt.rcParams['axes.unicode_minus'] = False
|
||||
|
||||
|
||||
def richards(t, K, r, t0, v):
|
||||
"""
|
||||
Richards curve (generalized logistic model)
|
||||
|
||||
N(t) = K / (1 + exp(-r*(t - t0)))^(1/v)
|
||||
"""
|
||||
exp_term = np.exp(-r * (t - t0))
|
||||
exp_term = np.clip(exp_term, 1e-10, 1e10)
|
||||
return K / np.power(1 + exp_term, 1/v)
|
||||
|
||||
|
||||
def load_data(filepath="rocket_launch_counts.csv"):
|
||||
"""Load rocket launch data"""
|
||||
df = pd.read_csv(filepath)
|
||||
df = df.rename(columns={"YDate": "year", "Total": "launches"})
|
||||
df["year"] = pd.to_numeric(df["year"], errors="coerce")
|
||||
df["launches"] = pd.to_numeric(df["launches"], errors="coerce")
|
||||
df = df.dropna(subset=["year", "launches"])
|
||||
df = df[(df["year"] >= 1957) & (df["year"] <= 2025)]
|
||||
df = df.astype({"year": int, "launches": int})
|
||||
df = df.sort_values("year").reset_index(drop=True)
|
||||
return df
|
||||
|
||||
|
||||
def fit_richards(years, launches, base_year=1984):
|
||||
"""Fit Richards model to data"""
|
||||
t = (years - base_year).astype(float)
|
||||
|
||||
# Initial parameters and bounds (unconstrained K)
|
||||
p0 = [5000.0, 0.1, 40.0, 2.0]
|
||||
bounds = ([500, 0.005, 10, 0.2], [100000, 1.0, 150, 10.0])
|
||||
|
||||
popt, pcov = curve_fit(richards, t, launches, p0=p0, bounds=bounds, maxfev=100000)
|
||||
perr = np.sqrt(np.diag(pcov))
|
||||
|
||||
# Calculate R²
|
||||
y_pred = richards(t, *popt)
|
||||
ss_res = np.sum((launches - y_pred) ** 2)
|
||||
ss_tot = np.sum((launches - np.mean(launches)) ** 2)
|
||||
r_squared = 1 - (ss_res / ss_tot)
|
||||
|
||||
return popt, perr, r_squared
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print("Richards S-Curve Fit (1984-2025)")
|
||||
print("=" * 60)
|
||||
|
||||
# Load data
|
||||
df = load_data()
|
||||
|
||||
# Filter 1984-2025
|
||||
start_year = 1984
|
||||
data = df[(df["year"] >= start_year) & (df["year"] <= 2025)].copy()
|
||||
years = data["year"].values
|
||||
launches = data["launches"].values
|
||||
|
||||
print(f"Data range: {start_year} - 2025")
|
||||
print(f"Data points: {len(data)}")
|
||||
|
||||
# Fit model
|
||||
popt, perr, r_squared = fit_richards(years, launches, base_year=start_year)
|
||||
K, r, t0, v = popt
|
||||
|
||||
print(f"\nFitted Parameters:")
|
||||
print(f" K (carrying capacity) = {K:.0f} launches/year")
|
||||
print(f" r (growth rate) = {r:.4f}")
|
||||
print(f" t0 (inflection point) = {start_year + t0:.1f}")
|
||||
print(f" v (shape parameter) = {v:.3f}")
|
||||
print(f" R² = {r_squared:.4f}")
|
||||
|
||||
# Physical limit
|
||||
physical_max = 3650
|
||||
print(f"\nPhysical limit: {physical_max} (10 sites × 365 days)")
|
||||
print(f"K / Physical limit = {K/physical_max:.2f}x")
|
||||
|
||||
# ========== Create Visualization ==========
|
||||
fig, ax = plt.subplots(figsize=(12, 7))
|
||||
|
||||
# Historical data points
|
||||
ax.scatter(years, launches, color='#2C3E50', s=80, alpha=0.8,
|
||||
label='Historical Data (1984-2025)', zorder=3, edgecolor='white', linewidth=0.5)
|
||||
|
||||
# Generate smooth S-curve
|
||||
years_smooth = np.linspace(start_year, 2100, 500)
|
||||
t_smooth = years_smooth - start_year
|
||||
pred_smooth = richards(t_smooth, *popt)
|
||||
|
||||
# S-curve prediction
|
||||
ax.plot(years_smooth, pred_smooth, color='#27AE60', lw=3,
|
||||
label=f'Richards Model (K={K:.0f}, R²={r_squared:.3f})', zorder=2)
|
||||
|
||||
# Confidence band (approximate using parameter errors)
|
||||
K_low = max(500, K - 2*perr[0])
|
||||
K_high = K + 2*perr[0]
|
||||
pred_low = richards(t_smooth, K_low, r, t0, v)
|
||||
pred_high = richards(t_smooth, K_high, r, t0, v)
|
||||
ax.fill_between(years_smooth, pred_low, pred_high, color='#27AE60', alpha=0.15,
|
||||
label='95% Confidence Band')
|
||||
|
||||
# Physical limit line
|
||||
ax.axhline(physical_max, color='#E74C3C', ls='--', lw=2.5,
|
||||
label=f'Physical Limit: {physical_max} (1/site/day)')
|
||||
|
||||
# Mark inflection point
|
||||
inflection_year = start_year + t0
|
||||
inflection_value = K / (v + 1) ** (1/v)
|
||||
ax.scatter([inflection_year], [inflection_value], color='#F39C12', s=150,
|
||||
marker='*', zorder=5, label=f'Inflection Point ({inflection_year:.0f})')
|
||||
|
||||
# Mark key years
|
||||
ax.axvline(2025, color='gray', ls=':', lw=1.5, alpha=0.7)
|
||||
ax.axvline(2050, color='#3498DB', ls=':', lw=2, alpha=0.8)
|
||||
ax.text(2026, K*0.95, '2025\n(Now)', fontsize=10, color='gray')
|
||||
ax.text(2051, K*0.85, '2050\n(Target)', fontsize=10, color='#3498DB')
|
||||
|
||||
# Prediction for 2050
|
||||
t_2050 = 2050 - start_year
|
||||
pred_2050 = richards(t_2050, *popt)
|
||||
ax.scatter([2050], [pred_2050], color='#3498DB', s=100, marker='D', zorder=4)
|
||||
ax.annotate(f'{pred_2050:.0f}', xy=(2050, pred_2050), xytext=(2055, pred_2050-300),
|
||||
fontsize=10, color='#3498DB', fontweight='bold')
|
||||
|
||||
# Formatting
|
||||
ax.set_xlabel('Year', fontsize=12)
|
||||
ax.set_ylabel('Annual Launches', fontsize=12)
|
||||
ax.set_title('Richards S-Curve Model Fit (1984-2025 Data)\nRocket Launch Capacity Projection',
|
||||
fontsize=14, fontweight='bold')
|
||||
ax.legend(loc='upper left', fontsize=10)
|
||||
ax.grid(True, alpha=0.3)
|
||||
ax.set_xlim(1982, 2100)
|
||||
ax.set_ylim(0, K * 1.15)
|
||||
|
||||
# Add text box with model info
|
||||
textstr = f'''Model: N(t) = K / (1 + e^(-r(t-t₀)))^(1/v)
|
||||
|
||||
Data Window: {start_year}-2025 ({len(data)} points)
|
||||
K = {K:.0f} launches/year
|
||||
r = {r:.4f}
|
||||
t₀ = {start_year + t0:.1f}
|
||||
v = {v:.3f}
|
||||
R² = {r_squared:.4f}
|
||||
|
||||
Note: Physical limit (3,650) shown as
|
||||
dashed red line'''
|
||||
|
||||
props = dict(boxstyle='round', facecolor='wheat', alpha=0.8)
|
||||
ax.text(0.98, 0.35, textstr, transform=ax.transAxes, fontsize=9,
|
||||
verticalalignment='top', horizontalalignment='right', bbox=props, family='monospace')
|
||||
|
||||
plt.tight_layout()
|
||||
plt.savefig('richards_curve_1984.png', dpi=150, bbox_inches='tight')
|
||||
plt.close()
|
||||
|
||||
print("\nPlot saved: richards_curve_1984.png")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
BIN
plane/__pycache__/cost_analysis.cpython-314.pyc
Normal file
BIN
plane/__pycache__/cost_analysis.cpython-314.pyc
Normal file
Binary file not shown.
BIN
plane/__pycache__/cost_analysis.cpython-38.pyc
Normal file
BIN
plane/__pycache__/cost_analysis.cpython-38.pyc
Normal file
Binary file not shown.
@@ -556,6 +556,143 @@ def plot_comprehensive_summary(data, save_path='aviation_cost_summary.png'):
|
||||
return fig
|
||||
|
||||
|
||||
# ============== 图表6: 燃油占比趋势(简洁版,中低饱和度)==============
|
||||
|
||||
def plot_fuel_share_trend_simple(data, save_path='fuel_share_trend.png'):
|
||||
"""绘制燃油成本占总成本的比例趋势 - 简洁版,无标题,中低饱和度色调"""
|
||||
|
||||
years = data['years']
|
||||
fuel_cents = data['fuel_cents']
|
||||
total_cents = data['total_cents']
|
||||
|
||||
# 计算燃油占比
|
||||
fuel_share = (fuel_cents / total_cents) * 100
|
||||
|
||||
# 创建图表 - 更小但保持2:1比例
|
||||
fig, ax1 = plt.subplots(figsize=(10, 5))
|
||||
|
||||
# 中低饱和度色调
|
||||
color1 = '#B87373' # 柔和的红色/玫瑰色
|
||||
color2 = '#6B8FAF' # 柔和的蓝色
|
||||
color3 = '#7AAE7A' # 柔和的绿色
|
||||
|
||||
# 绘制燃油占比(左轴)
|
||||
ax1.fill_between(years, 0, fuel_share, alpha=0.25, color=color1)
|
||||
line1, = ax1.plot(years, fuel_share, 'o-', color=color1, linewidth=2,
|
||||
markersize=6, label='Fuel Share (%)')
|
||||
ax1.set_xlabel('Year', fontsize=11)
|
||||
ax1.set_ylabel('Fuel as % of Total Operating Cost', fontsize=11, color=color1)
|
||||
ax1.tick_params(axis='y', labelcolor=color1)
|
||||
ax1.set_ylim(0, 45)
|
||||
|
||||
# 添加平均线
|
||||
avg_share = np.mean(fuel_share)
|
||||
ax1.axhline(y=avg_share, color=color1, linestyle='--', linewidth=1.2, alpha=0.6)
|
||||
ax1.text(years[-1]+0.5, avg_share+1, f'Avg: {avg_share:.1f}%',
|
||||
fontsize=10, color=color1, fontweight='bold')
|
||||
|
||||
# 右轴:绝对成本
|
||||
ax2 = ax1.twinx()
|
||||
line2, = ax2.plot(years, total_cents, 's--', color=color2, linewidth=1.8,
|
||||
markersize=5, alpha=0.75, label='Total CASM (cents)')
|
||||
line3, = ax2.plot(years, fuel_cents, '^--', color=color3, linewidth=1.8,
|
||||
markersize=5, alpha=0.75, label='Fuel CASM (cents)')
|
||||
ax2.set_ylabel('Cost per ASM (cents)', fontsize=11, color=color2)
|
||||
ax2.tick_params(axis='y', labelcolor=color2)
|
||||
|
||||
# 标注关键年份
|
||||
# 2008年油价高峰
|
||||
idx_2008 = np.where(years == 2008)[0][0]
|
||||
ax1.annotate(f'2008 Oil Crisis\n{fuel_share[idx_2008]:.1f}%',
|
||||
xy=(2008, fuel_share[idx_2008]),
|
||||
xytext=(2005, fuel_share[idx_2008]+8),
|
||||
fontsize=9, ha='center',
|
||||
arrowprops=dict(arrowstyle='->', color='#888888'),
|
||||
bbox=dict(boxstyle='round,pad=0.3', facecolor='#FFF5E6', alpha=0.8))
|
||||
|
||||
# 2015年油价低谷
|
||||
idx_2015 = np.where(years == 2015)[0][0]
|
||||
ax1.annotate(f'2015 Oil Crash\n{fuel_share[idx_2015]:.1f}%',
|
||||
xy=(2015, fuel_share[idx_2015]),
|
||||
xytext=(2017, fuel_share[idx_2015]-8),
|
||||
fontsize=9, ha='center',
|
||||
arrowprops=dict(arrowstyle='->', color='#888888'),
|
||||
bbox=dict(boxstyle='round,pad=0.3', facecolor='#E6F3FA', alpha=0.8))
|
||||
|
||||
# 图例
|
||||
lines = [line1, line2, line3]
|
||||
labels = [l.get_label() for l in lines]
|
||||
ax1.legend(lines, labels, loc='upper left', fontsize=10)
|
||||
|
||||
# 添加数据来源
|
||||
fig.text(0.99, 0.01, 'Data Source: MIT Airline Data Project / US DOT Form 41',
|
||||
fontsize=8, ha='right', style='italic', alpha=0.6)
|
||||
|
||||
plt.tight_layout()
|
||||
plt.savefig(save_path, dpi=150, bbox_inches='tight')
|
||||
print(f"图表已保存: {save_path}")
|
||||
|
||||
return fig
|
||||
|
||||
|
||||
# ============== 图表7: 燃油-总成本相关性散点图(单独版)==============
|
||||
|
||||
def plot_fuel_total_correlation_scatter(data, save_path='fuel_total_correlation.png'):
|
||||
"""绘制燃油成本与总成本的相关性散点图 - 单独版,无标题,中低饱和度色调"""
|
||||
|
||||
years = data['years']
|
||||
fuel = data['fuel_cents']
|
||||
total = data['total_cents']
|
||||
|
||||
# 计算相关系数
|
||||
correlation = np.corrcoef(fuel, total)[0, 1]
|
||||
|
||||
# 线性回归
|
||||
coeffs = np.polyfit(fuel, total, 1)
|
||||
poly = np.poly1d(coeffs)
|
||||
fuel_range = np.linspace(fuel.min(), fuel.max(), 100)
|
||||
|
||||
# 创建图表 - 与fuel_share_trend类似的尺寸比例
|
||||
fig, ax = plt.subplots(figsize=(10, 5))
|
||||
|
||||
# 中低饱和度色调 - 与fuel_share_trend类似但不完全相同
|
||||
scatter_cmap = 'YlGnBu' # 柔和的黄-绿-蓝渐变色
|
||||
line_color = '#9B7B6B' # 柔和的棕红色
|
||||
text_bg_color = '#D4E8D4' # 柔和的淡绿色
|
||||
|
||||
# 散点图
|
||||
scatter = ax.scatter(fuel, total, c=years, cmap=scatter_cmap, s=90, alpha=0.85,
|
||||
edgecolors='white', linewidth=1.2)
|
||||
|
||||
# 回归线
|
||||
ax.plot(fuel_range, poly(fuel_range), '--', color=line_color, linewidth=2,
|
||||
label=f'Linear Fit (R²={correlation**2:.3f})')
|
||||
|
||||
# 颜色条
|
||||
cbar = plt.colorbar(scatter, ax=ax)
|
||||
cbar.set_label('Year', fontsize=10)
|
||||
|
||||
ax.set_xlabel('Fuel Cost per ASM (cents)', fontsize=11)
|
||||
ax.set_ylabel('Total Cost per ASM (cents)', fontsize=11)
|
||||
ax.legend(fontsize=10, loc='lower right')
|
||||
ax.grid(True, alpha=0.3)
|
||||
|
||||
# 添加相关性解释
|
||||
ax.text(0.05, 0.95, f'Pearson r = {correlation:.3f}\nEnergy drives ~{correlation**2*100:.0f}% of cost variance',
|
||||
transform=ax.transAxes, fontsize=10, verticalalignment='top',
|
||||
bbox=dict(boxstyle='round', facecolor=text_bg_color, alpha=0.8))
|
||||
|
||||
# 添加数据来源
|
||||
fig.text(0.99, 0.01, 'Data Source: MIT Airline Data Project / US DOT Form 41',
|
||||
fontsize=8, ha='right', style='italic', alpha=0.6)
|
||||
|
||||
plt.tight_layout()
|
||||
plt.savefig(save_path, dpi=150, bbox_inches='tight')
|
||||
print(f"图表已保存: {save_path}")
|
||||
|
||||
return fig
|
||||
|
||||
|
||||
# ============== 主程序 ==============
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 196 KiB After Width: | Height: | Size: 138 KiB |
BIN
plane/fuel_total_correlation.png
Normal file
BIN
plane/fuel_total_correlation.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 91 KiB |
BIN
prob/p_B.pdf
BIN
prob/p_B.pdf
Binary file not shown.
Reference in New Issue
Block a user