In [1]:
#1 Disaggregation:
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
import seaborn as sns
from matplotlib.lines import Line2D
# File paths and sheet configuration:
file_path = r"C:\Users\asbj20\OneDrive - University of Bath\Desktop\Disaggregation\Disaggregation\EXIOBASE 3.9.5\Cement lime and plaster\EXIOBASE 3.9.5 - cement, lime and plaster.xlsx"
sheet_name = "Disaggregation "
# Read Excel data:
df = pd.read_excel(file_path, sheet_name=sheet_name, header=None)
# Define rows and columns (0-based indexing):
start_row = 18 # Excel row 19
end_row_Z = 9817 # Excel row 9818
row_X = 9818 # Excel row 9819
row_F = 9819 # Excel row 9820
row_M = 9821 # Excel row 9822
col_C = 2 # Column C
col_AC = 28 # Column AC (29th column)
# Extract data:
Z = df.iloc[start_row:end_row_Z, col_C]
X_total = df.iloc[row_X, col_C]
F_total = df.iloc[row_F, col_C]
M_total = df.iloc[row_M, col_C]
multiplier = df.iloc[start_row:end_row_Z, col_AC]
# Base proportions (for most rows):
prop_cement_X = 0.7807
prop_lime_X = 0.0421
prop_plaster_X = 0.1772
# Emissions proportions:
prop_cement_F = 0.7765
prop_lime_F = 0.1834
prop_plaster_F = 0.0400
# Special row proportions (Excel rows converted to 0-based indices:
special_proportions = {
(5545, 5558): {"Cement": 0.7780, "Lime": 0.0900, "Plaster": 0.1320}, # Electricity (rows 5546-5559)
5503: {"Cement": 0.5817, "Lime": 0.1161, "Plaster": 0.3022}, # Plastic
5457: {"Cement": 0.6136, "Lime": 0.2063, "Plaster": 0.1801}, # Stone
5521: {"Cement": 0.9327, "Lime": 0.0673, "Plaster": 0.0000}, # Iron/Steel
#Trade data:
318: {"Cement": 0.805899936056, "Lime": 0.027845254723, "Plaster": 0.166254809221},
518: {"Cement": 0.980318899152, "Lime": 0.006704466705, "Plaster": 0.012976634143},
718: {"Cement": 0.195834695653, "Lime": 0.0, "Plaster": 0.804165304347},
918: {"Cement": 0.515518613494, "Lime": 0.0, "Plaster": 0.484481386506},
1118: {"Cement": 0.871769136911, "Lime": 0.0, "Plaster": 0.128230863089},
1318: {"Cement": 0.825926301402, "Lime": 0.005009616447, "Plaster": 0.169064082151},
1518: {"Cement": 0.422932851073, "Lime": 0.000453922167, "Plaster": 0.576613226760},
1718: {"Cement": 0.795666408771, "Lime": 0.0, "Plaster": 0.204333591229},
1918: {"Cement": 0.783365628242, "Lime": 0.000390730099, "Plaster": 0.216243641659},
2118: {"Cement": 0.999947536760, "Lime": 0.000004522693, "Plaster": 0.000047940547},
2318: {"Cement": 0.697287284121, "Lime": 0.077234926293, "Plaster": 0.225477789586},
2518: {"Cement": 0.999958777919, "Lime": 0.0, "Plaster": 0.000041222081},
2718: {"Cement": 0.999995215833, "Lime": 0.0, "Plaster": 0.000004784167},
2918: {"Cement": 0.237533208685, "Lime": 0.0, "Plaster": 0.762466791315},
3118: {"Cement": 0.908152414414, "Lime": 0.005752478332, "Plaster": 0.086095107254},
3318: {"Cement": 0.942243495535, "Lime": 0.001892842902, "Plaster": 0.055863661563},
3518: {"Cement": 0.990513873463, "Lime": 0.001455594678, "Plaster": 0.008030531858},
3718: {"Cement": 0.053396300669, "Lime": 0.0, "Plaster": 0.946603699331},
3918: {"Cement": 0.995095042965, "Lime": 0.0, "Plaster": 0.004904957035},
4118: {"Cement": 0.991566265060, "Lime": 0.0, "Plaster": 0.008433734940},
4318: {"Cement": 0.804611826461, "Lime": 0.002966929236, "Plaster": 0.192421244303},
4518: {"Cement": 0.777232949736, "Lime": 0.000722910041, "Plaster": 0.222044140223},
4718: {"Cement": 0.839982758387, "Lime": 0.025688699836, "Plaster": 0.134328541776},
4918: {"Cement": 0.918351990909, "Lime": 0.0, "Plaster": 0.081648009091},
5118: {"Cement": 0.900424126809, "Lime": 0.014444917754, "Plaster": 0.085130955436},
5318: {"Cement": 0.990350269563, "Lime": 0.0, "Plaster": 0.009649730437},
5718: {"Cement": 0.836814299861, "Lime": 0.000180959993, "Plaster": 0.163004740146},
5918: {"Cement": 0.954863153716, "Lime": 0.000338981825, "Plaster": 0.044797864459},
6118: {"Cement": 0.992631832004, "Lime": 0.0, "Plaster": 0.007368167996},
6318: {"Cement": 0.991872886567, "Lime": 0.000476506191, "Plaster": 0.008079462814},
6518: {"Cement": 0.982807216885, "Lime": 0.0, "Plaster": 0.017192783115},
6718: {"Cement": 0.933409602209, "Lime": 0.0, "Plaster": 0.066590397791},
6918: {"Cement": 1.0, "Lime": 0.0, "Plaster": 0.0},
7118: {"Cement": 0.987163328839, "Lime": 0.004038634701, "Plaster": 0.008798036460},
7318: {"Cement": 0.877896571489, "Lime": 0.0, "Plaster": 0.122103428511},
7518: {"Cement": 0.370712672007, "Lime": 0.0, "Plaster": 0.629287327993},
7718: {"Cement": 0.806569074411, "Lime": 0.0, "Plaster": 0.193430925589},
7918: {"Cement": 0.900737033484, "Lime": 0.082717172396, "Plaster": 0.016545794119},
8118: {"Cement": 0.644426136156, "Lime": 0.001541112740, "Plaster": 0.354032751104},
8318: {"Cement": 0.988678533382, "Lime": 0.0, "Plaster": 0.011321466618},
8518: {"Cement": 0.994039187430, "Lime": 0.0, "Plaster": 0.005960812570},
8718: {"Cement": 1.0, "Lime": 0.0, "Plaster": 0.0},
8918: {"Cement": 0.999026362381, "Lime": 0.0, "Plaster": 0.000973637619},
9118: {"Cement": 0.990053402072, "Lime": 0.000617500000, "Plaster": 0.009329097928},
9318: {"Cement": 0.821261123337, "Lime": 0.142857142857, "Plaster": 0.035881733805},
9518: {"Cement": 0.948631521031, "Lime": 0.013620714286, "Plaster": 0.037747764683},
9718: {"Cement": 0.750409920066, "Lime": 0.093187274910, "Plaster": 0.156402805024},
9918: {"Cement": 0.961849415543, "Lime": 0.0, "Plaster": 0.038150584457}
}
def disaggregate_Z(level, include_all_special=False):
if include_all_special:
allowed_keys = special_proportions.copy()
else:
allowed_keys = {}
if level >= 2:
allowed_keys.update({k:v for k,v in special_proportions.items() if isinstance(k, tuple) and k == (5545,5558)})
if level >= 3:
allowed_keys.update({5503: special_proportions[5503]})
if level >= 4:
allowed_keys.update({5457: special_proportions[5457]})
if level >= 5:
allowed_keys.update({5521: special_proportions[5521]})
if level >= 6:
trade_keys = [k for k in special_proportions if isinstance(k,int) and k not in [5503,5457,5521] and not (5545 <= k <= 5558)]
allowed_keys.update({k:special_proportions[k] for k in trade_keys})
Z_cement = pd.Series(index=Z.index, dtype=float)
Z_lime = pd.Series(index=Z.index, dtype=float)
Z_plaster = pd.Series(index=Z.index, dtype=float)
for idx in Z.index:
custom_props = None
for key in allowed_keys:
if isinstance(key, tuple) and key[0] <= idx <= key[1]:
custom_props = allowed_keys[key]
break
if not custom_props and idx in allowed_keys:
custom_props = allowed_keys[idx]
if not custom_props:
custom_props = {
"Cement": prop_cement_X,
"Lime": prop_lime_X,
"Plaster": prop_plaster_X
}
Z_cement[idx] = Z[idx] * custom_props["Cement"]
Z_lime[idx] = Z[idx] * custom_props["Lime"]
Z_plaster[idx] = Z[idx] * custom_props["Plaster"]
return Z_cement, Z_lime, Z_plaster
def calculate_embodied_carbon(level, include_all_special=False):
Z_cement, Z_lime, Z_plaster = disaggregate_Z(level, include_all_special=include_all_special)
X_cement = X_total * prop_cement_X
X_lime = X_total * prop_lime_X
X_plaster = X_total * prop_plaster_X
F_cement = F_total * prop_cement_F
F_lime = F_total * prop_lime_F
F_plaster = F_total * prop_plaster_F
e_cement = F_cement / X_cement if X_cement != 0 else 0.0
e_lime = F_lime / X_lime if X_lime != 0 else 0.0
e_plaster = F_plaster / X_plaster if X_plaster != 0 else 0.0
A_cement = Z_cement / X_cement
A_lime = Z_lime / X_lime
A_plaster = Z_plaster / X_plaster
A_cement_multiplied = A_cement * multiplier
A_lime_multiplied = A_lime * multiplier
A_plaster_multiplied = A_plaster * multiplier
sum_A_cement_total = A_cement_multiplied.sum() + e_cement
sum_A_lime_total = A_lime_multiplied.sum() + e_lime
sum_A_plaster_total = A_plaster_multiplied.sum() + e_plaster
embodied_carbon_cement_total = (sum_A_cement_total / 1e6) / 0.85276
embodied_carbon_lime_total = (sum_A_lime_total / 1e6) / 0.85276
embodied_carbon_plaster_total = (sum_A_plaster_total / 1e6) / 0.85276
embodied_carbon_cement_scope1 = (e_cement / 1e6) / 0.85276
embodied_carbon_lime_scope1 = (e_lime / 1e6) / 0.85276
embodied_carbon_plaster_scope1 = (e_plaster / 1e6) / 0.85276
# Scope 2: Electricity and Steam/Hot Water
scope2_rows = []
for region in range(49):
offset = region * 200
# Electricity sectors (rows 146-157 in Excel, 145-156 in 0-based)
electricity_start = offset + 146 - 1
electricity_end = offset + 157 - 1
scope2_rows.extend(range(electricity_start, electricity_end + 1))
# Steam and hot water supply services (row 166 in Excel, 165 in 0-based)
steam_hot_water_row = offset + 166 - 1
scope2_rows.append(steam_hot_water_row)
scope2_rows = [r for r in scope2_rows if r in Z.index]
sum_A_cement_scope2 = A_cement_multiplied.loc[scope2_rows].sum()
sum_A_lime_scope2 = A_lime_multiplied.loc[scope2_rows].sum()
sum_A_plaster_scope2 = A_plaster_multiplied.loc[scope2_rows].sum()
embodied_carbon_cement_scope2 = (sum_A_cement_scope2 / 1e6) / 0.85276
embodied_carbon_lime_scope2 = (sum_A_lime_scope2 / 1e6) / 0.85276
embodied_carbon_plaster_scope2 = (sum_A_plaster_scope2 / 1e6) / 0.85276
embodied_carbon_cement_scope3 = embodied_carbon_cement_total - embodied_carbon_cement_scope1 - embodied_carbon_cement_scope2
embodied_carbon_lime_scope3 = embodied_carbon_lime_total - embodied_carbon_lime_scope1 - embodied_carbon_lime_scope2
embodied_carbon_plaster_scope3 = embodied_carbon_plaster_total - embodied_carbon_plaster_scope1 - embodied_carbon_plaster_scope2
original_aggregated_embodied_carbon = (M_total / 1e6) / 0.85276
return {
"total": (embodied_carbon_cement_total, embodied_carbon_lime_total, embodied_carbon_plaster_total),
"scope1": (embodied_carbon_cement_scope1, embodied_carbon_lime_scope1, embodied_carbon_plaster_scope1),
"scope2": (embodied_carbon_cement_scope2, embodied_carbon_lime_scope2, embodied_carbon_plaster_scope2),
"scope3": (embodied_carbon_cement_scope3, embodied_carbon_lime_scope3, embodied_carbon_plaster_scope3),
"original_aggregated": original_aggregated_embodied_carbon,
"sum_A_cement": sum_A_cement_total,
"sum_A_lime": sum_A_lime_total,
"sum_A_plaster": sum_A_plaster_total,
"X_cem": X_cement,
"X_lim": X_lime,
"X_plas": X_plaster
}
levels = {
1: "Total Economic Output & Direct emissions",
2: "+Electricity",
3: "+Plastic",
4: "+Stone",
5: "+Iron",
6: "+Trade"
}
results = []
print("Embodied Carbon Results at Different Disaggregation Levels:\n")
for lvl in range(1, 7):
res = calculate_embodied_carbon(lvl)
total = res["total"]
scope1 = res["scope1"]
scope2 = res["scope2"]
scope3 = res["scope3"]
results.append({
"Level": levels[lvl],
"Cement": total[0],
"Lime": total[1],
"Plaster": total[2],
"Scope1_Cement": scope1[0],
"Scope1_Lime": scope1[1],
"Scope1_Plaster": scope1[2],
"Scope2_Cement": scope2[0],
"Scope2_Lime": scope2[1],
"Scope2_Plaster": scope2[2],
"Scope3_Cement": scope3[0],
"Scope3_Lime": scope3[1],
"Scope3_Plaster": scope3[2],
"sum_A_cement": res["sum_A_cement"],
"sum_A_lime": res["sum_A_lime"],
"sum_A_plaster": res["sum_A_plaster"],
"X_cem": res["X_cem"],
"X_lim": res["X_lim"],
"X_plas": res["X_plas"]
})
print(f"Level {lvl}: {levels[lvl]}\n Cement: {total[0]:.4f}\n Lime: {total[1]:.4f}\n Plaster: {total[2]:.4f}")
print(f" Scope 1 Cement: {scope1[0]:.4f}\n Scope 1 Lime: {scope1[1]:.4f}\n Scope 1 Plaster: {scope1[2]:.4f}")
print(f" Scope 2 Cement: {scope2[0]:.4f}\n Scope 2 Lime: {scope2[1]:.4f}\n Scope 2 Plaster: {scope2[2]:.4f}")
print(f" Scope 3 Cement: {scope3[0]:.4f}\n Scope 3 Lime: {scope3[1]:.4f}\n Scope 3 Plaster: {scope3[2]:.4f}\n")
df_results = pd.DataFrame(results)
last = results[-1]
original_M_by_X = M_total * X_total
disaggregated_M_by_X = (last["sum_A_cement"] * last["X_cem"]) + (last["sum_A_lime"] * last["X_lim"]) + (last["sum_A_plaster"] * last["X_plas"])
print("\nValidation:")
print(f"Original M by X: {original_M_by_X:.4e}")
print(f"Disaggregated M by disaggregated X: {disaggregated_M_by_X:.4e}")
validation_df = pd.DataFrame({
'Description': ['Original M by X', 'Disaggregated M by disaggregated X'],
'Value': [original_M_by_X, disaggregated_M_by_X]
})
output_path = r"C:\Users\asbj20\OneDrive - University of Bath\Desktop\Disaggregation\Disaggregation\EXIOBASE 3.9.5\Cement lime and plaster\Results.xlsx"
with pd.ExcelWriter(output_path) as writer:
# Disaggregated Z for last level
Z_cement, Z_lime, Z_plaster = disaggregate_Z(6)
pd.DataFrame({
'Excel Row': df.index[start_row:end_row_Z] + 1,
'Original Z': Z.values,
'Z_Cement': Z_cement.values,
'Z_Lime': Z_lime.values,
'Z_Plaster': Z_plaster.values
}).to_excel(writer, sheet_name='Disaggregated Z', index=False)
# Matrix A without multiplied
A_cement = Z_cement / (X_total * prop_cement_X)
A_lime = Z_lime / (X_total * prop_lime_X)
A_plaster = Z_plaster / (X_total * prop_plaster_X)
pd.DataFrame({
'Excel Row': df.index[start_row:end_row_Z] + 1,
'A_Cement': A_cement.values,
'A_Lime': A_lime.values,
'A_Plaster': A_plaster.values,
'Multiplier': multiplier.values,
}).to_excel(writer, sheet_name='Matrix A', index=False)
# Matrix A multiplied (separate sheet)
A_cement_multiplied = A_cement * multiplier
A_lime_multiplied = A_lime * multiplier
A_plaster_multiplied = A_plaster * multiplier
pd.DataFrame({
'Excel Row': df.index[start_row:end_row_Z] + 1,
'Multiplier': multiplier.values,
'A_Cement_Multiplied': A_cement_multiplied.values,
'A_Lime_Multiplied': A_lime_multiplied.values,
'A_Plaster_Multiplied': A_plaster_multiplied.values
}).to_excel(writer, sheet_name='Matrix A Multiplied', index=False)
# Results including Scope 1, 2, and 3
pd.DataFrame({
'Material': ['Cement', 'Lime', 'Plaster'],
'Embodied Carbon Total (kg CO2e/£)': [last["Cement"], last["Lime"], last["Plaster"]],
'Scope 1 (kgCO2e/£)': [last["Scope1_Cement"], last["Scope1_Lime"], last["Scope1_Plaster"]],
'Scope 2 (kgCO2e/£)': [last["Scope2_Cement"], last["Scope2_Lime"], last["Scope2_Plaster"]],
'Scope 3 (kgCO2e/£)': [last["Scope3_Cement"], last["Scope3_Lime"], last["Scope3_Plaster"]]
}).to_excel(writer, sheet_name='Results', index=False)
# Validation sheet
validation_df.to_excel(writer, sheet_name='Validation', index=False)
print(f"All results exported to:\n{output_path}")
Embodied Carbon Results at Different Disaggregation Levels: Level 1: Total Economic Output & Direct emissions Cement: 1.1523 Lime: 3.7163 Plaster: 0.5659 Scope 1 Cement: 0.7586 Scope 1 Lime: 3.3226 Scope 1 Plaster: 0.1722 Scope 2 Cement: 0.0151 Scope 2 Lime: 0.0151 Scope 2 Plaster: 0.0151 Scope 3 Cement: 0.3786 Scope 3 Lime: 0.3786 Scope 3 Plaster: 0.3786 Level 2: +Electricity Cement: 1.1522 Lime: 3.7317 Plaster: 0.5624 Scope 1 Cement: 0.7586 Scope 1 Lime: 3.3226 Scope 1 Plaster: 0.1722 Scope 2 Cement: 0.0150 Scope 2 Lime: 0.0289 Scope 2 Plaster: 0.0120 Scope 3 Cement: 0.3786 Scope 3 Lime: 0.3802 Scope 3 Plaster: 0.3783 Level 3: +Plastic Cement: 1.1513 Lime: 3.7380 Plaster: 0.5650 Scope 1 Cement: 0.7586 Scope 1 Lime: 3.3226 Scope 1 Plaster: 0.1722 Scope 2 Cement: 0.0150 Scope 2 Lime: 0.0289 Scope 2 Plaster: 0.0120 Scope 3 Cement: 0.3777 Scope 3 Lime: 0.3865 Scope 3 Plaster: 0.3808 Level 4: +Stone Cement: 1.1509 Lime: 3.7450 Plaster: 0.5650 Scope 1 Cement: 0.7586 Scope 1 Lime: 3.3226 Scope 1 Plaster: 0.1722 Scope 2 Cement: 0.0150 Scope 2 Lime: 0.0289 Scope 2 Plaster: 0.0120 Scope 3 Cement: 0.3773 Scope 3 Lime: 0.3935 Scope 3 Plaster: 0.3808 Level 5: +Iron Cement: 1.1525 Lime: 3.7497 Plaster: 0.5570 Scope 1 Cement: 0.7586 Scope 1 Lime: 3.3226 Scope 1 Plaster: 0.1722 Scope 2 Cement: 0.0150 Scope 2 Lime: 0.0289 Scope 2 Plaster: 0.0120 Scope 3 Cement: 0.3789 Scope 3 Lime: 0.3982 Scope 3 Plaster: 0.3729 Level 6: +Trade Cement: 1.1474 Lime: 3.7025 Plaster: 0.5906 Scope 1 Cement: 0.7586 Scope 1 Lime: 3.3226 Scope 1 Plaster: 0.1722 Scope 2 Cement: 0.0150 Scope 2 Lime: 0.0289 Scope 2 Plaster: 0.0120 Scope 3 Cement: 0.3738 Scope 3 Lime: 0.3510 Scope 3 Plaster: 0.4065 Validation: Original M by X: 1.0790e+10 Disaggregated M by disaggregated X: 1.0790e+10 All results exported to: C:\Users\asbj20\OneDrive - University of Bath\Desktop\Disaggregation\Disaggregation\EXIOBASE 3.9.5\Cement lime and plaster\Results.xlsx
In [2]:
2# Plot 1: Stacked Bar Chart for Scope 1, 2 and 3:
from scipy.interpolate import make_interp_spline
# Original aggregated embodied carbon:
original_aggregated_embodied_carbon = (M_total / 1e6) / 0.85276
materials = ['Cement', 'Lime', 'Plaster']
x = range(len(materials))
scope1_values = [
last["Scope1_Cement"],
last["Scope1_Lime"],
last["Scope1_Plaster"]
]
scope2_values = [
last["Scope2_Cement"],
last["Scope2_Lime"],
last["Scope2_Plaster"]
]
scope3_values = [
last["Scope3_Cement"],
last["Scope3_Lime"],
last["Scope3_Plaster"]
]
fig, ax = plt.subplots(figsize=(20, 16), facecolor='white') # <-- Added facecolor='white' here
bars1 = ax.bar(x, scope1_values, color='#4c72b0', edgecolor='black', label='Scope 1')
bars2 = ax.bar(x, scope2_values, bottom=scope1_values, color='#dd8452', edgecolor='black', label='Scope 2')
bottom_scope3 = [i+j for i,j in zip(scope1_values, scope2_values)]
bars3 = ax.bar(x, scope3_values, bottom=bottom_scope3, color='#55a868', edgecolor='black', label='Scope 3')
ax.axhline(y=original_aggregated_embodied_carbon, color='darkred', linestyle='--', linewidth=2, label='Original Aggregated Value')
for i in range(len(materials)):
total_stack = scope1_values[i] + scope2_values[i] + scope3_values[i]
ax.text(x[i], total_stack + 0.05, f'{total_stack:.2f}', ha='center', fontsize=20, color='black')
total_stack = [i+j+k for i,j,k in zip(scope1_values, scope2_values, scope3_values)]
direct_percentages = [f"{(v/s)*100:.1f}%" if s > 0 else "0%" for v,s in zip(scope1_values, total_stack)]
scope2_percentages = [f"{(v/s)*100:.1f}%" if s > 0 else "0%" for v,s in zip(scope2_values, total_stack)]
scope3_percentages = [f"{(v/s)*100:.1f}%" if s > 0 else "0%" for v,s in zip(scope3_values, total_stack)]
for bar, perc in zip(bars1, direct_percentages):
ax.text(bar.get_x() + bar.get_width()/2, bar.get_height()/2, perc,
ha='center', va='center', fontsize=20, color='white', weight='bold')
for bar, perc, bottom in zip(bars2, scope2_percentages, scope1_values):
ax.text(bar.get_x() + bar.get_width()/2, bottom + bar.get_height()/2, perc,
ha='center', va='center', fontsize=20, color='white', weight='bold')
for bar, perc, bottom in zip(bars3, scope3_percentages, bottom_scope3):
ax.text(bar.get_x() + bar.get_width()/2, bottom + bar.get_height()/2, perc,
ha='center', va='center', fontsize=20, color='white', weight='bold')
# Set font style for labels and axes to match second plot's font style
ax.set_xlabel('Materials', fontsize=20, fontweight='bold', family='serif', color='#2C3E50')
ax.set_ylabel('Total Embodied Carbon Intensity (kgCO$_2$e/£)', fontsize=20, fontweight='bold', family='serif', color='#2C3E50')
ax.set_xticks(x)
ax.set_xticklabels(materials, fontsize=20, family='serif', color='#34495E')
ax.tick_params(axis='y', labelsize=20, labelcolor='#34495E')
ax.legend(fontsize=20, loc='upper right', frameon=True, fancybox=True, shadow=True, edgecolor='black')
ax.grid(axis='y', linestyle='--', alpha=0.6, linewidth=0.8)
ax.yaxis.set_minor_locator(plt.MultipleLocator(0.1))
ax.grid(which='minor', axis='y', linestyle=':', alpha=0.4, linewidth=0.6)
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
# Save plot as PNG and EPS in high quality:
plot_path = r"C:\Users\asbj20\OneDrive - University of Bath\Desktop\Disaggregation\Disaggregation\EXIOBASE 3.9.5\Cement lime and plaster\stacked_bar_chart"
fig.savefig(f"{plot_path}.png", dpi=300, bbox_inches='tight') # High-quality PNG
fig.savefig(f"{plot_path}.eps", dpi=300, bbox_inches='tight') # High-quality EPS
plt.tight_layout()
plt.show()
The PostScript backend does not support transparency; partially transparent artists will be rendered opaque.
In [3]:
3# Plot 2: line chart
#The original aggregated embodied carbon:
original_aggregated_embodied_carbon = (M_total / 1e6) / 0.85276
# Create dataframe with aggregated starting point:
df_enhanced = df_results.copy()
# Add aggregated starting point as first row:
aggregated_row = {
'Level': 'Aggregated Value',
'Cement': original_aggregated_embodied_carbon,
'Lime': original_aggregated_embodied_carbon,
'Plaster': original_aggregated_embodied_carbon
}
# Insert aggregated row at the beginning:
df_enhanced = pd.concat([pd.DataFrame([aggregated_row]), df_enhanced], ignore_index=True)
# Create Level_num with aggregated point:
df_enhanced['Level_num'] = [i for i in range(len(df_enhanced))]
# Custom colors for materials:
colors = {
'Cement': '#D64541', # red
'Lime': '#27AE60', # green
'Plaster': '#2980B9' # blue
}
# Markers and line styles:
markers = ['o', 's', '^']
linestyles = ['-', '--', '-.']
# Create the figure:
fig, ax = plt.subplots(figsize=(20, 14), facecolor='white')
# Material names for plotting:
material_columns = ['Cement', 'Lime','Plaster']
material_labels = ['Cement', 'Lime','Plaster']
# Data range for y-axis scaling:
data_min = df_enhanced[['Cement', 'Lime', 'Plaster']].min().min()
data_max = df_enhanced[['Cement', 'Lime', 'Plaster']].max().max()
data_range = data_max - data_min
# Set y-axis:
y_min = 0 # Minimum y value
y_max = 4.5 # Maximum y value
y_step = 0.5 # Step size for y-ticks
# Plot the branching lines from aggregated point:
for i, (material_col, material_label) in enumerate(zip(material_columns, material_labels)):
y_data = df_enhanced[material_col]
x_data = df_enhanced["Level_num"]
# Interpolation for the full line:
if len(x_data) > 3: # Need at least 4 points for cubic spline
x_new = np.linspace(x_data.min(), x_data.max(), 400)
spline = make_interp_spline(x_data, y_data, k=3)
y_smooth = spline(x_new)
# Glow effect (soft wide line behind):
ax.plot(x_new, y_smooth, color=colors[material_col], linewidth=16, alpha=0.08, zorder=1)
# Main smooth line:
ax.plot(x_new, y_smooth, linestyle=linestyles[i], linewidth=4,
color=colors[material_col], zorder=3, alpha=0.9, label=material_label)
# Special styling for the aggregated point:
if i == 0: # Plot the aggregated point once
ax.scatter(x_data.iloc[0], y_data.iloc[0], marker='o', s=300,
edgecolor='white', linewidth=4, facecolor='#34495E', zorder=6,
alpha=0.95, label='Aggregated Value')
# Add a subtle ring around the aggregated point:
ax.scatter(x_data.iloc[0], y_data.iloc[0], marker='o', s=400,
edgecolor=colors[material_col], linewidth=2, facecolor='none', zorder=5,
alpha=0.6)
# Regular markers for disaggregated points:
ax.scatter(x_data.iloc[1:], y_data.iloc[1:], marker=markers[i], s=180,
edgecolor='white', linewidth=3, facecolor=colors[material_col], zorder=4,
alpha=0.95)
# Annotations:
for i, row in df_enhanced.iterrows():
x_pos = row["Level_num"]
# Special annotation for aggregated point:
if i == 0:
val = row['Cement'] # Use any material value (they're all the same for aggregated):
bbox = dict(boxstyle="round,pad=0.5",
facecolor='#ECF0F1', alpha=0.95, edgecolor='#34495E', linewidth=2)
ax.text(x_pos, val + (y_max - y_min) * 0.02, f"{val:.3f}",
ha='center', va='bottom', fontsize=12, fontweight='bold',
color='#2C3E50', bbox=bbox, zorder=7)
else:
# Default annotation logic for all materials:
for j, material_col in enumerate(material_columns):
val = row[material_col]
# Use the same y_offset:
y_offset = (y_max - y_min) * 0.012 # Regular positioning for all materials
va = 'bottom' # Default vertical alignment
bbox = dict(boxstyle="round,pad=0.4",
facecolor='white', alpha=0.9, edgecolor=colors[material_col], linewidth=1)
ax.text(x_pos, val + y_offset, f"{val:.3f}", ha='center', va=va,
fontsize=12, fontweight='semibold', color=colors[material_col],
bbox=bbox, zorder=5)
# spine styling:
for spine in ['left', 'bottom']:
ax.spines[spine].set_linewidth(3)
ax.spines[spine].set_color('#2C3E50')
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.spines['bottom'].set_position(('data', y_min))
# labels:
ax.set_xlabel('Data', fontsize=18, fontweight='bold',
color='#2C3E50', family='serif')
ax.set_ylabel('Total Embodied Carbon Intensity (kgCO$_2$e/£)', fontsize=18, fontweight='bold',
color='#2C3E50', family='serif')
# tick styling:
ax.tick_params(axis='both', which='major', labelsize=14, width=2.5, length=10, color='#2C3E50')
ax.tick_params(axis='both', which='minor', width=1.5, length=6, color='#7F8C8D')
#Set x-ticks with proper horizontal alignment and two-line labels:
x_labels = []
for label in df_enhanced["Level"]:
label = label.replace('emissions', 'Emissions')
if len(label) > 15: # Break long labels into two lines
words = label.split()
mid = len(words) // 2
line1 = ' '.join(words[:mid])
line2 = ' '.join(words[mid:])
x_labels.append(f"{line1}\n{line2}")
else:
x_labels.append(label)
ax.set_xticks([i for i in range(len(df_enhanced))])
ax.set_xticklabels(x_labels, fontweight='semibold', fontsize=14,
family='serif', color='#2C3E50', rotation=0, ha='center')
# Y-ticks:
# Set the major y-ticks and the minor y-ticks (between the major ones):
ax.set_yticks(np.arange(y_min, y_max, y_step)) # Major ticks
ax.set_yticks(np.arange(y_min, y_max + 0.1, 0.1), minor=True) # Minor ticks
# Adjust the tick parameters to differentiate major and minor ticks:
ax.tick_params(axis='y', which='major', length=10, width=2, colors='#2C3E50') # Major ticks
ax.tick_params(axis='y', which='minor', length=5, width=1.5, colors='#BDC3C7') # Minor ticks
ax.minorticks_on()
ax.grid(which='major', linestyle=':', linewidth=1.6, color='#7F8C8D', alpha=0.5) # Major grid lines
ax.grid(which='minor', linestyle='--', linewidth=1, color='#BDC3C7', alpha=0.3) # Minor grid lines
# Set x limits:
ax.set_xlim(-0.3, df_enhanced["Level_num"].max() + 0.5)
# Add Aggregated Value to the legend as a simple circle:
aggregated_legend = Line2D([0], [0], color='none', lw=0, marker='o',
markerfacecolor='#34495E', markeredgecolor='white', markeredgewidth=3,
markersize=12, label='Aggregated Value')
# Create the rest of the legend elements for materials:
legend_elements = [
Line2D([0], [0], color=colors[material_col], lw=3.5, marker=markers[i],
markerfacecolor=colors[material_col], markeredgecolor='white', markeredgewidth=2,
linestyle=linestyles[i], label=material_label)
for i, (material_col, material_label) in enumerate(zip(material_columns, material_labels))
]
# Add the Aggregated Value at the start of the legend elements:
legend_elements.insert(0, aggregated_legend)
#Legend:
ax.legend(handles=legend_elements, loc='upper right', bbox_to_anchor=(1.02, 1.02),
frameon=True, fancybox=True, shadow=True, fontsize=13, framealpha=0.95, edgecolor='#34495E')
# layout:
plt.tight_layout(rect=[0, 0, 0.82, 0.95])
# Save:
plot_path = r"C:\Users\asbj20\OneDrive - University of Bath\Desktop\Disaggregation\Disaggregation\EXIOBASE 3.9.5\Cement lime and plaster\branching_plot"
fig.savefig(f"{plot_path}.png", dpi=400, bbox_inches='tight', facecolor='white')
fig.savefig(f"{plot_path}.eps", dpi=400, bbox_inches='tight', facecolor='white')
plt.show()
The PostScript backend does not support transparency; partially transparent artists will be rendered opaque.
In [4]:
#4: OAT Sensitivity Analysis:
def run_full_oat_sensitivity(level=6,
base_X=(prop_cement_X, prop_lime_X, prop_plaster_X),
base_F=(prop_cement_F, prop_lime_F, prop_plaster_F),
special_props=special_proportions,
changes=[-1.0, -0.5, -0.25, -0.1, 0, 0.1, 0.25, 0.5, 1.0],
output_path="OAT_sensitivity_results.xlsx"):
"""
Run OAT sensitivity on:
- Base proportions X
- Base proportions F
- All special proportions
Save results to Excel at output_path.
"""
results = []
materials = ['Cement', 'Lime', 'Plaster']
def adjust_props(base_triplet, focus_idx, change):
base_list = list(base_triplet)
new_val = max(0, min(1, base_list[focus_idx] * (1 + change)))
others_sum = sum(base_list) - base_list[focus_idx]
if others_sum == 0:
new_others = [0, 0]
else:
factor = (1 - new_val) / others_sum
new_others = [base_list[i] * factor for i in range(3) if i != focus_idx]
new_triplet = []
j = 0
for i in range(3):
if i == focus_idx:
new_triplet.append(new_val)
else:
new_triplet.append(new_others[j])
j += 1
return tuple(new_triplet)
def run_with_base_props(new_X=None, new_F=None, new_special=None):
global prop_cement_X, prop_lime_X, prop_plaster_X
global prop_cement_F, prop_lime_F, prop_plaster_F
global special_proportions
orig_prop_cement_X, orig_prop_lime_X, orig_prop_plaster_X = prop_cement_X, prop_lime_X, prop_plaster_X
orig_prop_cement_F, orig_prop_lime_F, orig_prop_plaster_F = prop_cement_F, prop_lime_F, prop_plaster_F
orig_special_props = special_proportions.copy()
try:
if new_X:
prop_cement_X, prop_lime_X, prop_plaster_X = new_X
if new_F:
prop_cement_F, prop_lime_F, prop_plaster_F = new_F
if new_special:
key, triplet = new_special
sp_copy = special_proportions.copy()
sp_copy[key] = {"Cement": triplet[0], "Lime": triplet[1], "Plaster": triplet[2]}
special_proportions = sp_copy
return calculate_embodied_carbon(level)["total"]
finally:
prop_cement_X, prop_lime_X, prop_plaster_X = orig_prop_cement_X, orig_prop_lime_X, orig_prop_plaster_X
prop_cement_F, prop_lime_F, prop_plaster_F = orig_prop_cement_F, orig_prop_lime_F, orig_prop_plaster_F
special_proportions = orig_special_props
print(f"\n=== Running Full OAT Sensitivity at Level {level} ===\n")
# Base X:
print("Varying Base X proportions:")
for i, mat in enumerate(materials):
for ch in changes:
new_X = adjust_props(base_X, i, ch)
total = run_with_base_props(new_X=new_X)
results.append({
"Input_Group": "Base_X",
"Varied_Material": mat,
"Change_%": ch * 100,
"Cement_prop": new_X[0],
"Lime_prop": new_X[1],
"Plaster_prop": new_X[2],
"Emb_Cement": total[0],
"Emb_Lime": total[1],
"Emb_Plaster": total[2]
})
# Base F:
print("Varying Base F proportions:")
for i, mat in enumerate(materials):
for ch in changes:
new_F = adjust_props(base_F, i, ch)
total = run_with_base_props(new_F=new_F)
results.append({
"Input_Group": "Base_F",
"Varied_Material": mat,
"Change_%": ch * 100,
"Cement_prop": new_F[0],
"Lime_prop": new_F[1],
"Plaster_prop": new_F[2],
"Emb_Cement": total[0],
"Emb_Lime": total[1],
"Emb_Plaster": total[2]
})
# Special proportions (all keys):
print("Varying Special proportions:")
for key in list(special_props.keys()):
base_triplet = (special_props[key]['Cement'], special_props[key]['Lime'], special_props[key]['Plaster'])
for i, mat in enumerate(materials):
for ch in changes:
new_special_triplet = adjust_props(base_triplet, i, ch)
total = run_with_base_props(new_special=(key, new_special_triplet))
results.append({
"Input_Group": f"Special_{key}",
"Varied_Material": mat,
"Change_%": ch * 100,
"Cement_prop": new_special_triplet[0],
"Lime_prop": new_special_triplet[1],
"Plaster_prop": new_special_triplet[2],
"Emb_Cement": total[0],
"Emb_Lime": total[1],
"Emb_Plaster": total[2]
})
# Creating DataFrame from results:
df = pd.DataFrame(results)
df.to_excel(output_path, index=False)
print(f"\nOAT Sensitivity results saved to: {output_path}")
return df
# Call the function and get the results:
sensitivity_all = run_full_oat_sensitivity(level=6) # Or any level you prefer
# Convert the results into DataFrame and save it:
df_sens = pd.DataFrame(sensitivity_all)
print(df_sens)
df_sens.to_excel(r"C:\Users\asbj20\OneDrive - University of Bath\Desktop\Disaggregation\Disaggregation\EXIOBASE 3.9.5\Cement lime and plaster\OAT_sensitivity_results.xlsx", index=False)
# Call the function (make sure prop_cement_X etc. are defined in your scope):
sensitivity_all = run_full_oat_sensitivity(level=6) # or any level you prefer
df_sens = pd.DataFrame(sensitivity_all)
print(df_sens)
df_sens.to_excel(r"C:\Users\asbj20\OneDrive - University of Bath\Desktop\Disaggregation\Disaggregation\EXIOBASE 3.9.5\Cement lime and plaster\OAT_sensitivity_results.xlsx", index=False)
=== Running Full OAT Sensitivity at Level 6 ===
Varying Base X proportions:
Varying Base F proportions:
Varying Special proportions:
OAT Sensitivity results saved to: OAT_sensitivity_results.xlsx
Input_Group Varied_Material Change_% Cement_prop Lime_prop \
0 Base_X Cement -100.0 0.000000 0.191974
1 Base_X Cement -50.0 0.390350 0.117037
2 Base_X Cement -25.0 0.585525 0.079569
3 Base_X Cement -10.0 0.702630 0.057087
4 Base_X Cement 0.0 0.780700 0.042100
... ... ... ... ... ...
1453 Special_9918 Plaster 0.0 0.961849 0.000000
1454 Special_9918 Plaster 10.0 0.958034 0.000000
1455 Special_9918 Plaster 25.0 0.952312 0.000000
1456 Special_9918 Plaster 50.0 0.942774 0.000000
1457 Special_9918 Plaster 100.0 0.923699 0.000000
Plaster_prop Emb_Cement Emb_Lime Emb_Plaster
0 0.808026 inf 1.031937 0.349510
1 0.492613 2.013056 1.512252 0.392877
2 0.334906 1.435964 2.091678 0.445192
3 0.240283 1.243600 2.804421 0.509544
4 0.177200 1.147418 3.702472 0.590628
... ... ... ... ...
1453 0.038151 1.147418 3.702472 0.590628
1454 0.041966 1.147418 3.702472 0.590628
1455 0.047688 1.147418 3.702472 0.590628
1456 0.057226 1.147418 3.702472 0.590628
1457 0.076301 1.147418 3.702472 0.590628
[1458 rows x 9 columns]
=== Running Full OAT Sensitivity at Level 6 ===
Varying Base X proportions:
Varying Base F proportions:
Varying Special proportions:
OAT Sensitivity results saved to: OAT_sensitivity_results.xlsx
Input_Group Varied_Material Change_% Cement_prop Lime_prop \
0 Base_X Cement -100.0 0.000000 0.191974
1 Base_X Cement -50.0 0.390350 0.117037
2 Base_X Cement -25.0 0.585525 0.079569
3 Base_X Cement -10.0 0.702630 0.057087
4 Base_X Cement 0.0 0.780700 0.042100
... ... ... ... ... ...
1453 Special_9918 Plaster 0.0 0.961849 0.000000
1454 Special_9918 Plaster 10.0 0.958034 0.000000
1455 Special_9918 Plaster 25.0 0.952312 0.000000
1456 Special_9918 Plaster 50.0 0.942774 0.000000
1457 Special_9918 Plaster 100.0 0.923699 0.000000
Plaster_prop Emb_Cement Emb_Lime Emb_Plaster
0 0.808026 inf 1.031937 0.349510
1 0.492613 2.013056 1.512252 0.392877
2 0.334906 1.435964 2.091678 0.445192
3 0.240283 1.243600 2.804421 0.509544
4 0.177200 1.147418 3.702472 0.590628
... ... ... ... ...
1453 0.038151 1.147418 3.702472 0.590628
1454 0.041966 1.147418 3.702472 0.590628
1455 0.047688 1.147418 3.702472 0.590628
1456 0.057226 1.147418 3.702472 0.590628
1457 0.076301 1.147418 3.702472 0.590628
[1458 rows x 9 columns]
In [5]:
#5: OAT Plot:
import seaborn as sns
from pathlib import Path
plt.style.use('seaborn-v0_8-darkgrid')
sns.set_palette("husl")
file_path = r"C:\Users\asbj20\OneDrive - University of Bath\Desktop\Disaggregation\Disaggregation\EXIOBASE 3.9.5\Cement lime and plaster\OAT_sensitivity_results.xlsx"
df = pd.read_excel(file_path)
df = df.replace([np.inf, -np.inf], np.nan)
output_colors = {
'Emb_Cement': '#D64541', # red
'Emb_Lime': '#27AE60', # green
'Emb_Plaster': '#2980B9' #blue
}
material_markers = {
'Cement': 'o',
'Lime': 's',
'Plaster': '^',
'Steel': 'v',
'Electricity': 'D',
'Plastic': 'h',
'Stone': 'p',
'Iron': '*',
'Average': 'X'
}
line_styles = ['-', '--', '-.']
def plot_panel(ax, data_subset, title):
materials = data_subset['Varied_Material'].unique()
for i, material in enumerate(materials):
material_data = data_subset[data_subset['Varied_Material'] == material].sort_values('Change_%')
marker = material_markers.get(material, 'o')
for j, output in enumerate(['Emb_Cement', 'Emb_Lime', 'Emb_Plaster']):
valid_data = material_data.dropna(subset=[output])
if len(valid_data) > 0:
ax.plot(valid_data['Change_%'], valid_data[output],
linestyle=line_styles[j],
color=output_colors[output],
marker=marker,
markersize=6,
linewidth=2,
alpha=0.8)
ax.set_title(title, fontsize=18, fontweight='bold', pad=10)
ax.grid(True, alpha=0.3)
y_vals = []
for material in materials:
for output in ['Emb_Cement', 'Emb_Lime', 'Emb_Plaster']:
y_vals.extend(data_subset[data_subset['Varied_Material'] == material][output].dropna())
if y_vals and max(y_vals) / min([v for v in y_vals if v > 0]) > 100:
ax.set_yscale('log')
def plot_average_panel(ax, df, title):
specific_groups = ['Base_X', 'Base_F', 'Special_(5545, 5558)', 'Special_5503',
'Special_5457', 'Special_5521']
other_data = df[~df['Input_Group'].isin(specific_groups)]
if other_data.empty:
ax.text(0.5, 0.5, 'No trade data\nfor averaging', ha='center', va='center', transform=ax.transAxes, fontsize=16)
ax.set_title(title, fontsize=18, fontweight='bold', pad=10)
return
materials = ['Cement', 'Lime', 'Plaster']
outputs = ['Emb_Cement', 'Emb_Lime', 'Emb_Plaster']
for material, output in zip(materials, outputs):
material_data = other_data[other_data['Varied_Material'] == material]
if material_data.empty:
continue
avg_by_change = material_data.groupby('Change_%')[output].mean().reset_index().sort_values('Change_%').dropna(subset=[output])
if not avg_by_change.empty:
marker = material_markers.get(material, 'o')
color = output_colors[output]
line_style = line_styles[materials.index(material)]
ax.plot(avg_by_change['Change_%'], avg_by_change[output],
linestyle=line_style,
color=color,
marker=marker,
markersize=6,
linewidth=2,
alpha=0.8)
ax.set_title(title, fontsize=18, fontweight='bold', pad=10)
ax.grid(True, alpha=0.3)
fig, axes = plt.subplots(3, 3, figsize=(18, 15))
axes = axes.flatten()
panels_config = [
('Base_X', 'Total Economic Output (X)', 0),
('Base_F', 'Direct Emissions (F)', 1),
('Special_(5545, 5558)', 'Electricity', 2),
('Special_5503', 'Plastic', 3),
('Special_5457', 'Stone', 4),
('Special_5521', 'Iron', 5),
(None, 'Trade', 7)
]
for input_group, title, panel_idx in panels_config[:-1]:
subset = df[df['Input_Group'] == input_group]
if not subset.empty:
plot_panel(axes[panel_idx], subset, title)
else:
axes[panel_idx].text(0.5, 0.5, f'No data found\nfor {input_group}', ha='center', va='center', transform=axes[panel_idx].transAxes, fontsize=16)
axes[panel_idx].set_title(title, fontsize=18, fontweight='bold', pad=10)
plot_average_panel(axes[7], df, 'Trade')
axes[6].set_visible(False)
axes[8].set_visible(False)
# Change the font size of the tick labels (numbers) on x and y axes
for ax in axes:
ax.tick_params(axis='both', labelsize=16) # Adjust labelsize here
for i in [7]:
axes[i].set_xlabel('Change (%)', fontsize=20, fontweight='bold')
# Only middle-left panel gets y-axis label
axes[3].set_ylabel('Total Embodied Carbon Intensity (kgCO$_2$e/£)', fontsize=20, fontweight='bold')
legend_elements = [
plt.Line2D([0], [0], color='white', linewidth=0, label='Outputs:')
] + [
plt.Line2D([0], [0], color=color, linewidth=3, label=f' {output.replace("Emb_", "")}')
for output, color in output_colors.items()
] + [
plt.Line2D([0], [0], color='white', linewidth=0, label=''),
plt.Line2D([0], [0], color='white', linewidth=0, label='Inputs:')
] + [
plt.Line2D([0], [0], color='gray', marker=material_markers[m], markersize=8, linewidth=0, linestyle='None', label=f' {m}')
for m in ['Cement', 'Lime', 'Plaster']
]
fig.legend(handles=legend_elements, loc='center right', bbox_to_anchor=(0.95, 0.7), fontsize=14, frameon=True, fancybox=True, shadow=True)
plt.tight_layout()
plt.subplots_adjust(right=0.85)
save_path = Path(file_path).parent / 'OAT_Sensitivity_Analysis_MultiPanel.png'
plt.savefig(save_path.with_suffix('.png'), dpi=300, bbox_inches='tight')
plt.savefig(save_path.with_suffix('.eps'), dpi=300, bbox_inches='tight')
print(f"Saved PNG and EPS at: {save_path.parent}")
plt.show()
The PostScript backend does not support transparency; partially transparent artists will be rendered opaque.
Saved PNG and EPS at: C:\Users\asbj20\OneDrive - University of Bath\Desktop\Disaggregation\Disaggregation\EXIOBASE 3.9.5\Cement lime and plaster
In [6]:
import seaborn as sns
from pathlib import Path
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np
plt.style.use('seaborn-v0_8-darkgrid')
sns.set_palette("husl")
file_path = r"C:\Users\asbj20\OneDrive - University of Bath\Desktop\Disaggregation\Disaggregation\EXIOBASE 3.9.5\Cement lime and plaster\OAT_sensitivity_results.xlsx"
df = pd.read_excel(file_path)
df = df.replace([np.inf, -np.inf], np.nan)
output_colors = {
'Emb_Cement': '#D64541', # red
'Emb_Lime': '#27AE60', # green
'Emb_Plaster': '#2980B9' #blue
}
material_markers = {
'Cement': 'o',
'Lime': 's',
'Plaster': '^',
'Steel': 'v',
'Electricity': 'D',
'Plastic': 'h',
'Stone': 'p',
'Iron': '*',
'Average': 'X'
}
line_styles = ['-', '--', '-.']
def plot_panel(ax, data_subset, title):
materials = data_subset['Varied_Material'].unique()
for i, material in enumerate(materials):
material_data = data_subset[data_subset['Varied_Material'] == material].sort_values('Change_%')
marker = material_markers.get(material, 'o')
for j, output in enumerate(['Emb_Cement', 'Emb_Lime', 'Emb_Plaster']):
valid_data = material_data.dropna(subset=[output])
if len(valid_data) > 0:
ax.plot(valid_data['Change_%'], valid_data[output],
linestyle=line_styles[j],
color=output_colors[output],
marker=marker,
markersize=6,
linewidth=2,
alpha=0.8)
ax.set_title(title, fontsize=18, fontweight='bold', pad=10)
ax.grid(True, alpha=0.3)
y_vals = []
for material in materials:
for output in ['Emb_Cement', 'Emb_Lime', 'Emb_Plaster']:
y_vals.extend(data_subset[data_subset['Varied_Material'] == material][output].dropna())
if y_vals and max(y_vals) / min([v for v in y_vals if v > 0]) > 100:
ax.set_yscale('log')
def plot_combined_panel(ax, df, input_groups, title):
"""Plot combined data from multiple input groups"""
combined_data = df[df['Input_Group'].isin(input_groups)]
if combined_data.empty:
ax.text(0.5, 0.5, 'No data available', ha='center', va='center',
transform=ax.transAxes, fontsize=16)
ax.set_title(title, fontsize=18, fontweight='bold', pad=10)
return
materials = combined_data['Varied_Material'].unique()
for i, material in enumerate(materials):
material_data = combined_data[combined_data['Varied_Material'] == material].sort_values('Change_%')
marker = material_markers.get(material, 'o')
for j, output in enumerate(['Emb_Cement', 'Emb_Lime', 'Emb_Plaster']):
valid_data = material_data.dropna(subset=[output])
if len(valid_data) > 0:
ax.plot(valid_data['Change_%'], valid_data[output],
linestyle=line_styles[j],
color=output_colors[output],
marker=marker,
markersize=6,
linewidth=2,
alpha=0.8)
ax.set_title(title, fontsize=18, fontweight='bold', pad=10)
ax.grid(True, alpha=0.3)
y_vals = []
for material in materials:
for output in ['Emb_Cement', 'Emb_Lime', 'Emb_Plaster']:
y_vals.extend(combined_data[combined_data['Varied_Material'] == material][output].dropna())
if y_vals and max(y_vals) / min([v for v in y_vals if v > 0]) > 100:
ax.set_yscale('log')
# Create figure with 4 columns for better centering
fig = plt.figure(figsize=(18, 15))
gs = fig.add_gridspec(3, 4, hspace=0.2, wspace=0.1)
# Top row: X and F
ax1 = fig.add_subplot(gs[0, 0:2]) # Spans columns 0-1
ax2 = fig.add_subplot(gs[0, 2:4]) # Spans columns 2-3
# Middle row: Electricity and Iron
ax3 = fig.add_subplot(gs[1, 0:2])
ax4 = fig.add_subplot(gs[1, 2:4])
# Bottom row: Combined panel (centered, columns 1-2)
ax5 = fig.add_subplot(gs[2, 1:3])
axes = [ax1, ax2, ax3, ax4, ax5]
# Plot individual panels
panels_config = [
('Base_X', 'Total Economic Output (X)', 0),
('Base_F', 'Direct Emissions (F)', 1),
('Special_(5545, 5558)', 'Electricity', 2),
('Special_5521', 'Iron', 3),
]
for input_group, title, panel_idx in panels_config:
subset = df[df['Input_Group'] == input_group]
if not subset.empty:
plot_panel(axes[panel_idx], subset, title)
else:
axes[panel_idx].text(0.5, 0.5, f'No data found\nfor {input_group}',
ha='center', va='center', transform=axes[panel_idx].transAxes, fontsize=16)
axes[panel_idx].set_title(title, fontsize=18, fontweight='bold', pad=10)
# Plot combined panel for Plastic/Stone/Trade
combined_groups = ['Special_5503', 'Special_5457']
# Add trade data (everything not in specific groups)
specific_groups = ['Base_X', 'Base_F', 'Special_(5545, 5558)', 'Special_5521', 'Special_5503', 'Special_5457']
trade_data = df[~df['Input_Group'].isin(specific_groups)]
# Combine Plastic, Stone, and Trade data
plastic_stone_data = df[df['Input_Group'].isin(combined_groups)]
combined_df = pd.concat([plastic_stone_data, trade_data])
plot_combined_panel(axes[4], combined_df, combined_groups + [g for g in df['Input_Group'].unique() if g not in specific_groups],
'Plastic/Stone/Trade')
# Change the font size of the tick labels (numbers) on x and y axes
for ax in axes:
ax.tick_params(axis='both', labelsize=16)
# Format y-axis to show integers without decimal points
from matplotlib.ticker import FormatStrFormatter
ax.yaxis.set_major_formatter(FormatStrFormatter('%.0f'))
# Set x-axis label for bottom panel
axes[4].set_xlabel('Change (%)', fontsize=20, fontweight='bold')
# Set y-axis label for middle-left panel
axes[2].set_ylabel('Total Embodied Carbon Intensity (kgCO$_2$e/£)', fontsize=20, fontweight='bold')
# Create legend
legend_elements = [
plt.Line2D([0], [0], color='white', linewidth=0, label='Outputs:')
] + [
plt.Line2D([0], [0], color=color, linewidth=3, label=f' {output.replace("Emb_", "")}')
for output, color in output_colors.items()
] + [
plt.Line2D([0], [0], color='white', linewidth=0, label=''),
plt.Line2D([0], [0], color='white', linewidth=0, label='Inputs:')
] + [
plt.Line2D([0], [0], color='gray', marker=material_markers[m], markersize=8,
linewidth=0, linestyle='None', label=f' {m}')
for m in ['Cement', 'Lime', 'Plaster']
]
fig.legend(handles=legend_elements, loc='center right', bbox_to_anchor=(0.98, 0.6),
fontsize=14, frameon=True, fancybox=True, shadow=True)
plt.subplots_adjust(right=0.88)
save_path = Path(file_path).parent / 'OAT_Sensitivity_Analysis_5Panel.png'
plt.savefig(save_path.with_suffix('.png'), dpi=300, bbox_inches='tight')
plt.savefig(save_path.with_suffix('.eps'), dpi=300, bbox_inches='tight')
print(f"Saved PNG and EPS at: {save_path.parent}")
plt.show()
The PostScript backend does not support transparency; partially transparent artists will be rendered opaque.
Saved PNG and EPS at: C:\Users\asbj20\OneDrive - University of Bath\Desktop\Disaggregation\Disaggregation\EXIOBASE 3.9.5\Cement lime and plaster
In [7]:
import seaborn as sns
from pathlib import Path
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np
plt.style.use('seaborn-v0_8-darkgrid')
sns.set_palette("husl")
file_path = r"C:\Users\asbj20\OneDrive - University of Bath\Desktop\Disaggregation\Disaggregation\EXIOBASE 3.9.5\Cement lime and plaster\OAT_sensitivity_results.xlsx"
df = pd.read_excel(file_path)
df = df.replace([np.inf, -np.inf], np.nan)
output_colors = {
'Emb_Cement': '#D64541', # red
'Emb_Lime': '#27AE60', # green
'Emb_Plaster': '#2980B9' #blue
}
material_markers = {
'Cement': 'o',
'Lime': 's',
'Plaster': '^',
'Steel': 'v',
'Electricity': 'D',
'Plastic': 'h',
'Stone': 'p',
'Iron': '*',
'Average': 'X'
}
line_styles = ['-', '--', '-.']
def plot_panel(ax, data_subset, title):
materials = data_subset['Varied_Material'].unique()
for i, material in enumerate(materials):
material_data = data_subset[data_subset['Varied_Material'] == material].sort_values('Change_%')
marker = material_markers.get(material, 'o')
for j, output in enumerate(['Emb_Cement', 'Emb_Lime', 'Emb_Plaster']):
valid_data = material_data.dropna(subset=[output])
if len(valid_data) > 0:
ax.plot(valid_data['Change_%'], valid_data[output],
linestyle=line_styles[j],
color=output_colors[output],
marker=marker,
markersize=6,
linewidth=2,
alpha=0.8)
ax.set_title(title, fontsize=20, fontweight='bold', pad=10)
ax.grid(True, alpha=0.3)
# Collect y values for setting limits
y_vals = []
for material in materials:
for output in ['Emb_Cement', 'Emb_Lime', 'Emb_Plaster']:
y_vals.extend(data_subset[data_subset['Varied_Material'] == material][output].dropna())
if y_vals and max(y_vals) / min([v for v in y_vals if v > 0]) > 100:
ax.set_yscale('log')
elif y_vals:
# Add padding to y-axis limits to prevent values from appearing outside the box
y_min, y_max = min(y_vals), max(y_vals)
y_range = y_max - y_min
ax.set_ylim(y_min - 0.1 * y_range, y_max + 0.1 * y_range)
def plot_combined_panel(ax, df, input_groups, title):
"""Plot combined data from multiple input groups"""
combined_data = df[df['Input_Group'].isin(input_groups)]
if combined_data.empty:
ax.text(0.5, 0.5, 'No data available', ha='center', va='center',
transform=ax.transAxes, fontsize=18)
ax.set_title(title, fontsize=20, fontweight='bold', pad=10)
return
materials = combined_data['Varied_Material'].unique()
for i, material in enumerate(materials):
material_data = combined_data[combined_data['Varied_Material'] == material].sort_values('Change_%')
marker = material_markers.get(material, 'o')
for j, output in enumerate(['Emb_Cement', 'Emb_Lime', 'Emb_Plaster']):
valid_data = material_data.dropna(subset=[output])
if len(valid_data) > 0:
ax.plot(valid_data['Change_%'], valid_data[output],
linestyle=line_styles[j],
color=output_colors[output],
marker=marker,
markersize=6,
linewidth=2,
alpha=0.8)
ax.set_title(title, fontsize=20, fontweight='bold', pad=10)
ax.grid(True, alpha=0.3)
# Collect y values for setting limits
y_vals = []
for material in materials:
for output in ['Emb_Cement', 'Emb_Lime', 'Emb_Plaster']:
y_vals.extend(combined_data[combined_data['Varied_Material'] == material][output].dropna())
if y_vals and max(y_vals) / min([v for v in y_vals if v > 0]) > 100:
ax.set_yscale('log')
elif y_vals:
# Add padding to y-axis limits to prevent values from appearing outside the box
y_min, y_max = min(y_vals), max(y_vals)
y_range = y_max - y_min
ax.set_ylim(y_min - 0.1 * y_range, y_max + 0.1 * y_range)
# Create figure with 4 columns for better centering
fig = plt.figure(figsize=(18, 15))
gs = fig.add_gridspec(3, 4, hspace=0.2, wspace=0.1)
# Top row: X and F
ax1 = fig.add_subplot(gs[0, 0:2]) # Spans columns 0-1
ax2 = fig.add_subplot(gs[0, 2:4]) # Spans columns 2-3
# Middle row: Electricity and Iron
ax3 = fig.add_subplot(gs[1, 0:2])
ax4 = fig.add_subplot(gs[1, 2:4])
# Bottom row: Combined panel (centered, columns 1-2)
ax5 = fig.add_subplot(gs[2, 1:3])
axes = [ax1, ax2, ax3, ax4, ax5]
# Plot individual panels
panels_config = [
('Base_X', 'Total Economic Output (X)', 0),
('Base_F', 'Direct Emissions (F)', 1),
('Special_(5545, 5558)', 'Electricity', 2),
('Special_5521', 'Iron', 3),
]
for input_group, title, panel_idx in panels_config:
subset = df[df['Input_Group'] == input_group]
if not subset.empty:
plot_panel(axes[panel_idx], subset, title)
else:
axes[panel_idx].text(0.5, 0.5, f'No data found\nfor {input_group}',
ha='center', va='center', transform=axes[panel_idx].transAxes, fontsize=18)
axes[panel_idx].set_title(title, fontsize=20, fontweight='bold', pad=10)
# Plot combined panel for Plastic/Stone/Trade
combined_groups = ['Special_5503', 'Special_5457']
# Add trade data (everything not in specific groups)
specific_groups = ['Base_X', 'Base_F', 'Special_(5545, 5558)', 'Special_5521', 'Special_5503', 'Special_5457']
trade_data = df[~df['Input_Group'].isin(specific_groups)]
# Combine Plastic, Stone, and Trade data
plastic_stone_data = df[df['Input_Group'].isin(combined_groups)]
combined_df = pd.concat([plastic_stone_data, trade_data])
plot_combined_panel(axes[4], combined_df, combined_groups + [g for g in df['Input_Group'].unique() if g not in specific_groups],
'Plastic/Stone/Trade')
# Change the font size of the tick labels (numbers) on x and y axes - INCREASED
for ax in axes:
ax.tick_params(axis='both', labelsize=18) # Increased from 16 to 18
# Format y-axis to show integers without decimal points
from matplotlib.ticker import FormatStrFormatter
ax.yaxis.set_major_formatter(FormatStrFormatter('%.0f'))
# Set x-axis label for bottom panel
axes[4].set_xlabel('Change (%)', fontsize=22, fontweight='bold')
# Set y-axis label for middle-left panel
axes[2].set_ylabel('Total Embodied Carbon Intensity (kgCO$_2$e/£)', fontsize=22, fontweight='bold')
# Create legend with LARGER font size
legend_elements = [
plt.Line2D([0], [0], color='white', linewidth=0, label='Outputs:')
] + [
plt.Line2D([0], [0], color=color, linewidth=3, label=f' {output.replace("Emb_", "")}')
for output, color in output_colors.items()
] + [
plt.Line2D([0], [0], color='white', linewidth=0, label=''),
plt.Line2D([0], [0], color='white', linewidth=0, label='Inputs:')
] + [
plt.Line2D([0], [0], color='gray', marker=material_markers[m], markersize=10,
linewidth=0, linestyle='None', label=f' {m}')
for m in ['Cement', 'Lime', 'Plaster']
]
fig.legend(handles=legend_elements, loc='center right', bbox_to_anchor=(0.99, 0.62),
fontsize=16, frameon=True, fancybox=True, shadow=True) # Increased from 14 to 16
plt.subplots_adjust(right=0.88)
save_path = Path(file_path).parent / 'OAT_Sensitivity_Analysis_5Panel.png'
plt.savefig(save_path.with_suffix('.png'), dpi=300, bbox_inches='tight')
plt.savefig(save_path.with_suffix('.eps'), dpi=300, bbox_inches='tight')
print(f"Saved PNG and EPS at: {save_path.parent}")
plt.show()
The PostScript backend does not support transparency; partially transparent artists will be rendered opaque.
Saved PNG and EPS at: C:\Users\asbj20\OneDrive - University of Bath\Desktop\Disaggregation\Disaggregation\EXIOBASE 3.9.5\Cement lime and plaster
In [8]:
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np
from pathlib import Path
from matplotlib.ticker import MultipleLocator
plt.style.use('seaborn-v0_8-whitegrid')
# Load your data
file_path = r"C:\\Users\\asbj20\\OneDrive - University of Bath\\Desktop\\Disaggregation\\Disaggregation\\EXIOBASE 3.9.5\\Cement lime and plaster\\OAT_sensitivity_results.xlsx"
df = pd.read_excel(file_path)
df = df.replace([np.inf, -np.inf], np.nan)
# Color scheme
output_colors = {
'Emb_Cement': '#D64541', # red
'Emb_Lime': '#27AE60', # green
'Emb_Plaster': '#2980B9' # blue
}
# Define input groups
input_groups = {
'Economic\nOutput (X)': 'Base_X',
'Direct\nEmissions (F)': 'Base_F',
'Electricity': 'Special_(5545, 5558)',
'Iron': 'Special_5521',
'Plastic': 'Special_5503',
'Stone': 'Special_5457',
}
# Calculate Trade as average of non-specific groups
specific_groups = ['Base_X', 'Base_F', 'Special_(5545, 5558)', 'Special_5521',
'Special_5503', 'Special_5457']
trade_data = df[~df['Input_Group'].isin(specific_groups)]
# Function to get min/max for each input group
def get_ranges(df, input_group, output_col):
subset = df[df['Input_Group'] == input_group]
if subset.empty:
return None, None
valid = subset[output_col].dropna()
if len(valid) == 0:
return None, None
return valid.min(), valid.max()
def get_trade_ranges(trade_df, output_col):
"""Get average ranges for trade data"""
if trade_df.empty:
return None, None
# Group by Change_% and average across all trade items
avg_by_change = trade_df.groupby('Change_%')[output_col].mean()
if len(avg_by_change) == 0:
return None, None
return avg_by_change.min(), avg_by_change.max()
def get_baseline(df, input_group, output_col):
"""Get baseline value at 0% change"""
subset = df[df['Input_Group'] == input_group]
if subset.empty:
return None
baseline = subset[subset['Change_%'] == 0][output_col]
if len(baseline) == 0:
return None
return baseline.values[0]
# Collect ranges for all inputs
input_names = list(input_groups.keys()) + ['Trade']
ranges_data = {
'Cement': [],
'Lime': [],
'Plaster': []
}
# Get baseline values (at 0% change) - using Base_X as reference
baseline_cement = get_baseline(df, 'Base_X', 'Emb_Cement')
baseline_lime = get_baseline(df, 'Base_X', 'Emb_Lime')
baseline_plaster = get_baseline(df, 'Base_X', 'Emb_Plaster')
# Get ranges for defined input groups
for inp_name, inp_group in input_groups.items():
cement_min, cement_max = get_ranges(df, inp_group, 'Emb_Cement')
lime_min, lime_max = get_ranges(df, inp_group, 'Emb_Lime')
plaster_min, plaster_max = get_ranges(df, inp_group, 'Emb_Plaster')
ranges_data['Cement'].append([cement_min, cement_max] if cement_min is not None else [np.nan, np.nan])
ranges_data['Lime'].append([lime_min, lime_max] if lime_min is not None else [np.nan, np.nan])
ranges_data['Plaster'].append([plaster_min, plaster_max] if plaster_min is not None else [np.nan, np.nan])
# Get Trade ranges
cement_min, cement_max = get_trade_ranges(trade_data, 'Emb_Cement')
lime_min, lime_max = get_trade_ranges(trade_data, 'Emb_Lime')
plaster_min, plaster_max = get_trade_ranges(trade_data, 'Emb_Plaster')
ranges_data['Cement'].append([cement_min, cement_max] if cement_min is not None else [np.nan, np.nan])
ranges_data['Lime'].append([lime_min, lime_max] if lime_min is not None else [np.nan, np.nan])
ranges_data['Plaster'].append([plaster_min, plaster_max] if plaster_min is not None else [np.nan, np.nan])
# Sort by Lime range (most variable)
lime_ranges_for_sort = [(r[1] - r[0]) if not np.isnan(r[0]) else 0 for r in ranges_data['Lime']]
sorted_indices = np.argsort(lime_ranges_for_sort)[::-1]
inputs_sorted = [input_names[i] for i in sorted_indices]
cement_sorted = [ranges_data['Cement'][i] for i in sorted_indices]
lime_sorted = [ranges_data['Lime'][i] for i in sorted_indices]
plaster_sorted = [ranges_data['Plaster'][i] for i in sorted_indices]
# Create figure
fig, ax = plt.subplots(figsize=(14, 8))
y_positions = np.arange(len(inputs_sorted))
offset = 0.25
# Plot ranges as horizontal lines with markers
for i, (inp, cement, lime, plaster) in enumerate(zip(inputs_sorted, cement_sorted, lime_sorted, plaster_sorted)):
# Cement
if not np.isnan(cement[0]) and not np.isnan(cement[1]):
ax.plot([cement[0], cement[1]], [i - offset, i - offset],
color=output_colors['Emb_Cement'], linewidth=5, solid_capstyle='round', alpha=0.85)
ax.scatter([cement[0], cement[1]], [i - offset, i - offset],
color=output_colors['Emb_Cement'], s=120, zorder=5, edgecolors='white', linewidth=2)
# Lime
if not np.isnan(lime[0]) and not np.isnan(lime[1]):
ax.plot([lime[0], lime[1]], [i, i],
color=output_colors['Emb_Lime'], linewidth=5, solid_capstyle='round', alpha=0.85)
ax.scatter([lime[0], lime[1]], [i, i],
color=output_colors['Emb_Lime'], s=120, zorder=5, edgecolors='white', linewidth=2)
# Plaster
if not np.isnan(plaster[0]) and not np.isnan(plaster[1]):
ax.plot([plaster[0], plaster[1]], [i + offset, i + offset],
color=output_colors['Emb_Plaster'], linewidth=5, solid_capstyle='round', alpha=0.85)
ax.scatter([plaster[0], plaster[1]], [i + offset, i + offset],
color=output_colors['Emb_Plaster'], s=120, zorder=5, edgecolors='white', linewidth=2)
# Add vertical reference lines for baseline values (shortened to end near last input)
y_baseline_end = len(inputs_sorted) - 1 + 0.6 # end just above the last input parameter
if baseline_cement is not None and not np.isnan(baseline_cement):
ax.plot([baseline_cement, baseline_cement], [-0.5, y_baseline_end],
color=output_colors['Emb_Cement'], linestyle='--', linewidth=2.5, alpha=0.5, zorder=1)
if baseline_lime is not None and not np.isnan(baseline_lime):
ax.plot([baseline_lime, baseline_lime], [-0.5, y_baseline_end],
color=output_colors['Emb_Lime'], linestyle='--', linewidth=2.5, alpha=0.5, zorder=1)
if baseline_plaster is not None and not np.isnan(baseline_plaster):
ax.plot([baseline_plaster, baseline_plaster], [-0.5, y_baseline_end],
color=output_colors['Emb_Plaster'], linestyle='--', linewidth=2.5, alpha=0.5, zorder=1)
# Add horizontal separator lines between inputs for better readability
# Use thicker, darker lines to clearly separate input groups
for i in range(len(inputs_sorted)): # Changed to include line after last input
ax.axhline(y=i + 0.5, color='gray', linestyle='-', linewidth=1.2, alpha=0.5, zorder=0)
# Add baseline labels just slightly above the baseline line tips
y_top = y_baseline_end + 0.05 # position just slightly above baseline line end
# Lime baseline - centered (has enough space)
if baseline_lime is not None and not np.isnan(baseline_lime):
ax.text(baseline_lime, y_top, f'{baseline_lime:.2f}',
ha='center', va='bottom', fontsize=15, fontweight='bold',
color=output_colors['Emb_Lime'])
# Cement baseline - with arrow pointing from left
if baseline_cement is not None and not np.isnan(baseline_cement):
ax.annotate(f'{baseline_cement:.2f}',
xy=(baseline_cement, y_top), xytext=(baseline_cement + 1.2, y_top + 0.15),
ha='center', va='bottom', fontsize=15, fontweight='bold',
color=output_colors['Emb_Cement'],
arrowprops=dict(arrowstyle='->', color=output_colors['Emb_Cement'],
lw=2, shrinkA=0, shrinkB=0))
# Plaster baseline - with arrow pointing from right
if baseline_plaster is not None and not np.isnan(baseline_plaster):
ax.annotate(f'{baseline_plaster:.2f}',
xy=(baseline_plaster, y_top), xytext=(baseline_plaster - 1.2, y_top + 0.15),
ha='center', va='bottom', fontsize=15, fontweight='bold',
color=output_colors['Emb_Plaster'],
arrowprops=dict(arrowstyle='->', color=output_colors['Emb_Plaster'],
lw=2, shrinkA=0, shrinkB=0))
# Styling
ax.set_yticks(y_positions)
ax.set_yticklabels(inputs_sorted, fontsize=16, fontweight='bold')
ax.set_ylabel('Input Parameters', fontsize=18, fontweight='bold')
ax.set_xlabel('Total Embodied Carbon Intensity (kgCO$_2$e/£)', fontsize=18, fontweight='bold')
#X-axis with minor ticks every 1
ax.set_xlim(0, 35)
ax.set_xticks([0, 5, 10, 15, 20, 25, 30, 35])
ax.xaxis.set_minor_locator(MultipleLocator(1))
ax.tick_params(axis='both', which='major', labelsize=16, length=8)
ax.tick_params(axis='x', which='minor', labelsize=12, length=4, color='gray')
# Adjusted y-limits - slightly increased for arrows
ax.set_ylim(-0.7, len(inputs_sorted) + 0.35)
ax.grid(True, alpha=0.3, axis='x')
# Remove top and right spines
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
# Legend - NO "Output Type" title
legend_elements = [
plt.Line2D([0], [0], color=output_colors['Emb_Cement'], linewidth=5,
marker='o', markersize=11, label='Cement', markeredgecolor='white', markeredgewidth=2),
plt.Line2D([0], [0], color=output_colors['Emb_Lime'], linewidth=5,
marker='o', markersize=11, label='Lime', markeredgecolor='white', markeredgewidth=2),
plt.Line2D([0], [0], color=output_colors['Emb_Plaster'], linewidth=5,
marker='o', markersize=11, label='Plaster', markeredgecolor='white', markeredgewidth=2)
]
# Add single baseline indicator
legend_elements.append(plt.Line2D([0], [0], color='gray', linestyle='--', linewidth=2.5, alpha=0.5,
label='Baseline values'))
legend = ax.legend(handles=legend_elements, loc='upper right', fontsize=15,
frameon=True, shadow=True, fancybox=True, ncol=1)
legend.get_frame().set_facecolor('white')
legend.get_frame().set_alpha(0.95)
plt.tight_layout()
# Save
save_path = Path(file_path).parent / 'Range_Sensitivity_Analysis.png'
plt.savefig(save_path.with_suffix('.png'), dpi=300, bbox_inches='tight', facecolor='white')
plt.savefig(save_path.with_suffix('.eps'), dpi=300, bbox_inches='tight')
print(f"✓ Saved PNG and EPS at: {save_path.parent}")
print(f" Baseline values: Cement={baseline_cement:.2f}, Lime={baseline_lime:.2f}, Plaster={baseline_plaster:.2f}")
plt.show()
The PostScript backend does not support transparency; partially transparent artists will be rendered opaque.
✓ Saved PNG and EPS at: C:\Users\asbj20\OneDrive - University of Bath\Desktop\Disaggregation\Disaggregation\EXIOBASE 3.9.5\Cement lime and plaster Baseline values: Cement=1.15, Lime=3.70, Plaster=0.59
In [ ]: