diff --git a/mcmthesis-demo.pdf b/mcmthesis-demo.pdf index ca8b3e8..31a5a82 100644 Binary files a/mcmthesis-demo.pdf and b/mcmthesis-demo.pdf differ diff --git a/p3/interaction_heatmap.png b/p3/interaction_heatmap.png index 3934590..b878899 100644 Binary files a/p3/interaction_heatmap.png and b/p3/interaction_heatmap.png differ diff --git a/p3/tornado_analysis.png b/p3/tornado_analysis.png index b17a43e..b7fdfc0 100644 Binary files a/p3/tornado_analysis.png and b/p3/tornado_analysis.png differ diff --git a/p3/water_sensitivity_analysis.py b/p3/water_sensitivity_analysis.py index 0af7c1b..b6070f6 100644 --- a/p3/water_sensitivity_analysis.py +++ b/p3/water_sensitivity_analysis.py @@ -806,7 +806,7 @@ def sensitivity_buffer_days(baseline: BaselineParameters, save_dir: str): def tornado_analysis(baseline: BaselineParameters, save_dir: str): """ - Tornado图 - 多参数灵敏度排序分析 + Tornado图 - 多参数灵敏度排序分析(马卡龙色系简约版) """ print("生成Tornado图...") @@ -825,7 +825,7 @@ def tornado_analysis(baseline: BaselineParameters, save_dir: str): baseline_annual = baseline_demand['annual_supply_tons'] baseline_initial = baseline_demand['initial_transport_tons'] - # 定义参数范围 + # 定义参数范围 - 根据论述调整(删除Confidence Level) params = [ ('Recycle Rate (η)', 'recycle_rate', 0.80, 0.95, baseline.recycle_rate), ('Comfort Factor (α)', 'comfort_factor', 25, 100, baseline.comfort_factor), @@ -833,7 +833,6 @@ def tornado_analysis(baseline: BaselineParameters, save_dir: str): ('Sickness Rate', 'sickness_rate', 0.01, 0.04, baseline.sickness_rate), ('Medical Water/Event', 'medical_water', 3, 10, baseline.medical_water_per_event), ('Buffer Days', 'buffer_days', 15, 45, baseline.safety_buffer_days), - ('Confidence Level', 'confidence', 0.95, 0.999, baseline.confidence_level), ] results_annual = [] @@ -883,13 +882,6 @@ def tornado_analysis(baseline: BaselineParameters, save_dir: str): demand_high = calculate_water_demand(baseline.population, baseline.survival_water, baseline.hygiene_water_base, baseline.comfort_factor, baseline.medical_water_per_event, baseline.sickness_rate, baseline.confidence_level, baseline.recycle_rate, int(high)) - elif param_type == 'confidence': - demand_low = calculate_water_demand(baseline.population, baseline.survival_water, baseline.hygiene_water_base, - baseline.comfort_factor, baseline.medical_water_per_event, baseline.sickness_rate, - low, baseline.recycle_rate, baseline.safety_buffer_days) - demand_high = calculate_water_demand(baseline.population, baseline.survival_water, baseline.hygiene_water_base, - baseline.comfort_factor, baseline.medical_water_per_event, baseline.sickness_rate, - high, baseline.recycle_rate, baseline.safety_buffer_days) # 计算变化百分比 change_low_annual = (demand_low['annual_supply_tons'] - baseline_annual) / baseline_annual * 100 @@ -914,76 +906,109 @@ def tornado_analysis(baseline: BaselineParameters, save_dir: str): 'swing': swing_initial }) - # 按swing排序 + # 按年度需求的swing排序(统一排序顺序) results_annual = sorted(results_annual, key=lambda x: x['swing'], reverse=True) - results_initial = sorted(results_initial, key=lambda x: x['swing'], reverse=True) - # 绘图 - fig, axes = plt.subplots(1, 2, figsize=(16, 8)) + # 按相同顺序重排initial结果 + param_order = [r['param'] for r in results_annual] + results_initial_dict = {r['param']: r for r in results_initial} + results_initial = [results_initial_dict[p] for p in param_order] - # 图1: 年补充量Tornado - ax = axes[0] - y_pos = np.arange(len(results_annual)) + # 马卡龙色系 + color_decrease = '#A8D8B9' # 薄荷绿 - 减少 + color_increase = '#F7C5A8' # 杏橙 - 增加 + color_decrease_dark = '#7BC08F' + color_increase_dark = '#E8A07A' + + # 绘图 - 简约风格(缩小尺寸) + fig, axes = plt.subplots(1, 2, figsize=(9, 3.5)) + fig.patch.set_facecolor('#FAFAFA') + + # 统一的参数标签 params_sorted = [r['param'] for r in results_annual] + y_pos = np.arange(len(params_sorted)) + + # ========== 图1: 年补充量Tornado ========== + ax = axes[0] + ax.set_facecolor('#FAFAFA') lows = [r['low'] for r in results_annual] highs = [r['high'] for r in results_annual] - # 绘制条形图 + # 绘制条形图 - 分开绘制正负部分 + for i, (l, h) in enumerate(zip(lows, highs)): + # 负值部分(减少) + if l < 0: + ax.barh(i, l, color=color_decrease, edgecolor=color_decrease_dark, + linewidth=1, height=0.65) + # 正值部分(增加) + if h > 0: + ax.barh(i, h, color=color_increase, edgecolor=color_increase_dark, + linewidth=1, height=0.65) + + # 添加数值标注 for i, (l, h) in enumerate(zip(lows, highs)): if l < 0: - ax.barh(i, l, color='green', alpha=0.7, height=0.6) + ax.text(l - 2, i, f'{l:.0f}%', ha='right', va='center', fontsize=8, color='#555555') if h > 0: - ax.barh(i, h, color='red', alpha=0.7, height=0.6) - if l >= 0: - ax.barh(i, l, left=0, color='green', alpha=0.7, height=0.6) - if h <= 0: - ax.barh(i, h, left=0, color='red', alpha=0.7, height=0.6) - # 如果跨越0,绘制完整范围 - if l < 0 < h: - pass # 已经绘制 - elif l >= 0: - ax.barh(i, h - l, left=l, color='orange', alpha=0.7, height=0.6) - elif h <= 0: - ax.barh(i, h - l, left=l, color='orange', alpha=0.7, height=0.6) + ax.text(h + 2, i, f'+{h:.0f}%', ha='left', va='center', fontsize=8, color='#555555') - ax.axvline(x=0, color='black', linewidth=1.5) + ax.axvline(x=0, color='#333333', linewidth=1.2) ax.set_yticks(y_pos) - ax.set_yticklabels(params_sorted) - ax.set_xlabel('Change in Annual Supply (%)', fontsize=12) - ax.set_title('Tornado Diagram: Annual Water Supply', fontsize=14) - ax.grid(True, alpha=0.3, axis='x') + ax.set_yticklabels(params_sorted, fontsize=10) + ax.set_xlabel('Change in Annual Supply (%)', fontsize=11, color='#444444') + ax.spines['top'].set_visible(False) + ax.spines['right'].set_visible(False) + ax.spines['left'].set_color('#CCCCCC') + ax.spines['bottom'].set_color('#CCCCCC') + ax.grid(True, alpha=0.15, axis='x', linestyle='--') + ax.set_xlim(-65, 115) - # 图2: 首批运输量Tornado + # ========== 图2: 首批运输量Tornado ========== ax = axes[1] - params_sorted = [r['param'] for r in results_initial] + ax.set_facecolor('#FAFAFA') lows = [r['low'] for r in results_initial] highs = [r['high'] for r in results_initial] for i, (l, h) in enumerate(zip(lows, highs)): - width = h - l - left = l - color = 'steelblue' if width > 0 else 'gray' - ax.barh(i, width, left=left, color=color, alpha=0.7, height=0.6) + # 负值部分(减少) + if l < 0: + ax.barh(i, l, color=color_decrease, edgecolor=color_decrease_dark, + linewidth=1, height=0.65) + # 正值部分(增加) + if h > 0: + ax.barh(i, h, color=color_increase, edgecolor=color_increase_dark, + linewidth=1, height=0.65) - ax.axvline(x=0, color='black', linewidth=1.5) + # 添加数值标注 + for i, (l, h) in enumerate(zip(lows, highs)): + if l < 0: + ax.text(l - 2, i, f'{l:.0f}%', ha='right', va='center', fontsize=8, color='#555555') + if h > 0: + ax.text(h + 2, i, f'+{h:.0f}%', ha='left', va='center', fontsize=8, color='#555555') + + ax.axvline(x=0, color='#333333', linewidth=1.2) ax.set_yticks(y_pos) - ax.set_yticklabels(params_sorted) - ax.set_xlabel('Change in Initial Transport (%)', fontsize=12) - ax.set_title('Tornado Diagram: Initial Transport Volume', fontsize=14) - ax.grid(True, alpha=0.3, axis='x') + ax.set_yticklabels([]) # 删除右图纵坐标标签 + ax.set_xlabel('Change in Initial Transport (%)', fontsize=11, color='#444444') + ax.spines['top'].set_visible(False) + ax.spines['right'].set_visible(False) + ax.spines['left'].set_visible(False) # 隐藏左边框 + ax.spines['bottom'].set_color('#CCCCCC') + ax.grid(True, alpha=0.15, axis='x', linestyle='--') + ax.set_xlim(-65, 105) - # 添加图例说明 + # 简化图例 - 放在图的下方中间 from matplotlib.patches import Patch legend_elements = [ - Patch(facecolor='green', alpha=0.7, label='Decrease'), - Patch(facecolor='red', alpha=0.7, label='Increase'), + Patch(facecolor=color_decrease, edgecolor=color_decrease_dark, label='Decrease'), + Patch(facecolor=color_increase, edgecolor=color_increase_dark, label='Increase'), ] - axes[0].legend(handles=legend_elements, loc='lower right') + fig.legend(handles=legend_elements, loc='lower center', ncol=2, fontsize=9, + framealpha=0.9, edgecolor='#CCCCCC', bbox_to_anchor=(0.55, -0.02)) - plt.suptitle('Multi-Parameter Sensitivity Analysis (Tornado Diagram)\nBaseline: α=50, η=90%, N=100k', - fontsize=14, y=1.02) plt.tight_layout() - plt.savefig(f'{save_dir}/tornado_analysis.png', dpi=150, bbox_inches='tight') + plt.subplots_adjust(bottom=0.15) + plt.savefig(f'{save_dir}/tornado_analysis.png', dpi=150, bbox_inches='tight', facecolor='#FAFAFA') print(f" 保存: {save_dir}/tornado_analysis.png") return results_annual, results_initial @@ -991,10 +1016,12 @@ def tornado_analysis(baseline: BaselineParameters, save_dir: str): def interaction_heatmap(baseline: BaselineParameters, save_dir: str): """ - 双参数交互热力图 - α vs η + 双参数交互热力图 - α vs η(马卡龙色系简约版) """ print("生成双参数交互热力图...") + from matplotlib.colors import LinearSegmentedColormap + alphas = np.array([1, 25, 50, 100, 150, 200, 250]) etas = np.array([0.70, 0.75, 0.80, 0.85, 0.90, 0.95]) @@ -1019,52 +1046,64 @@ def interaction_heatmap(baseline: BaselineParameters, save_dir: str): annual_matrix[i, j] = demand['annual_supply_tons'] / 1000 # 千吨 capacity_matrix[i, j] = demand['annual_supply_tons'] / baseline.total_elevator_capacity * 100 - fig, axes = plt.subplots(1, 2, figsize=(16, 7)) + # 马卡龙色系(中等饱和度) + # 图1: 淡蓝→蓝紫→玫粉(年补充量) + colors_annual = ['#C8E6F0', '#A8D0E8', '#98B8D8', '#B898C8', '#D098B0', '#D88898', '#C86878'] + cmap_annual = LinearSegmentedColormap.from_list('macaron_annual', colors_annual) + + # 图2: 薄荷绿→淡黄→杏橙→珊瑚红(运力占比) + colors_capacity = ['#A8D8B8', '#C8E0A0', '#E0D890', '#E8C078', '#E8A070', '#D88070', '#C86868'] + cmap_capacity = LinearSegmentedColormap.from_list('macaron_capacity', colors_capacity) + + # 缩小图片尺寸 + fig, axes = plt.subplots(1, 2, figsize=(10, 4)) + fig.patch.set_facecolor('#FAFAFA') # 图1: 年补充量热力图 ax = axes[0] - im = ax.imshow(annual_matrix, cmap='YlOrRd', aspect='auto') + ax.set_facecolor('#FAFAFA') + im = ax.imshow(annual_matrix, cmap=cmap_annual, aspect='auto') ax.set_xticks(range(len(alphas))) - ax.set_xticklabels([f'{a}' for a in alphas]) + ax.set_xticklabels([f'{a}' for a in alphas], fontsize=9) ax.set_yticks(range(len(etas))) - ax.set_yticklabels([f'{e*100:.0f}%' for e in etas]) - ax.set_xlabel('Comfort Factor (α)', fontsize=12) - ax.set_ylabel('Recycle Rate (η)', fontsize=12) - ax.set_title('Annual Water Supply (kt)', fontsize=13) + ax.set_yticklabels([f'{e*100:.0f}%' for e in etas], fontsize=9) + ax.set_xlabel('Comfort Factor (α)', fontsize=10, color='#444444') + ax.set_ylabel('Recycle Rate (η)', fontsize=10, color='#444444') # 添加数值标注 for i in range(len(etas)): for j in range(len(alphas)): - text_color = 'white' if annual_matrix[i, j] > annual_matrix.max() * 0.6 else 'black' - ax.text(j, i, f'{annual_matrix[i, j]:.0f}', ha='center', va='center', fontsize=9, color=text_color) + ax.text(j, i, f'{annual_matrix[i, j]:.0f}', ha='center', va='center', + fontsize=8, color='#333333', fontweight='medium') - plt.colorbar(im, ax=ax, label='Annual Supply (kt)') + cbar1 = plt.colorbar(im, ax=ax, shrink=0.8) + cbar1.ax.tick_params(labelsize=8) + cbar1.set_label('Annual Supply (kt)', fontsize=9, color='#444444') # 图2: 运力占比热力图 ax = axes[1] - im = ax.imshow(capacity_matrix, cmap='RdYlGn_r', aspect='auto', vmin=0, vmax=150) + ax.set_facecolor('#FAFAFA') + im = ax.imshow(capacity_matrix, cmap=cmap_capacity, aspect='auto', vmin=0, vmax=150) ax.set_xticks(range(len(alphas))) - ax.set_xticklabels([f'{a}' for a in alphas]) + ax.set_xticklabels([f'{a}' for a in alphas], fontsize=9) ax.set_yticks(range(len(etas))) - ax.set_yticklabels([f'{e*100:.0f}%' for e in etas]) - ax.set_xlabel('Comfort Factor (α)', fontsize=12) - ax.set_ylabel('Recycle Rate (η)', fontsize=12) - ax.set_title('Elevator Capacity Usage (%)', fontsize=13) + ax.set_yticklabels([f'{e*100:.0f}%' for e in etas], fontsize=9) + ax.set_xlabel('Comfort Factor (α)', fontsize=10, color='#444444') + ax.set_ylabel('Recycle Rate (η)', fontsize=10, color='#444444') # 添加数值标注 for i in range(len(etas)): for j in range(len(alphas)): val = capacity_matrix[i, j] - text_color = 'white' if val > 60 else 'black' - ax.text(j, i, f'{val:.0f}%', ha='center', va='center', fontsize=9, color=text_color) + ax.text(j, i, f'{val:.0f}%', ha='center', va='center', + fontsize=8, color='#333333', fontweight='medium') - # 添加100%等高线 - cbar = plt.colorbar(im, ax=ax, label='Capacity Usage (%)') + cbar2 = plt.colorbar(im, ax=ax, shrink=0.8) + cbar2.ax.tick_params(labelsize=8) + cbar2.set_label('Capacity Usage (%)', fontsize=9, color='#444444') - plt.suptitle('Two-Parameter Interaction Analysis: α vs η\n(N=100,000, Buffer=30 days)', - fontsize=14, y=1.02) plt.tight_layout() - plt.savefig(f'{save_dir}/interaction_heatmap.png', dpi=150, bbox_inches='tight') + plt.savefig(f'{save_dir}/interaction_heatmap.png', dpi=150, bbox_inches='tight', facecolor='#FAFAFA') print(f" 保存: {save_dir}/interaction_heatmap.png") return annual_matrix, capacity_matrix