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.
No description has been provided for this image
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.
No description has been provided for this image
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
No description has been provided for this image
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
No description has been provided for this image
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
No description has been provided for this image
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
No description has been provided for this image
In [ ]: