Evaluating infrastructure adaptation options¶
This notebook forms the basis of “Hands-On 8” in the CCG course.
Take the risk results for the Ghana road damage and disruption analysis from previous hands-on sessions
Assume some adaptation options - explain what this means - and show their costs
Explain cost-benefit analysis (CBA) and show how to calculate Net Present Values for benefits (avoided risks) and costs
By the end of this tutorial you should be able to: * Quantify the potential risk reduction of adaptation options * Prioritise assets based on cost-benefit analysis for different adaptation options
[ ]:
# Imports from Python standard library
import math
import os
import warnings
from glob import glob
from pathlib import Path
# Imports from other Python packages
import geopandas as gpd
import networkx as nx
import numpy as np
import pandas as pd
import seaborn as sns
from tqdm.notebook import tqdm
from matplotlib import pyplot as plt
Change this to point to your data folder as in the previous tutorial:
[ ]:
dir = (
Path(os.getcwd()).resolve().parents[3]
) # get parent directory of snail package
data_folder = os.path.join(dir, "ghana_tutorial")
# data_folder = Path("YOUR_PATH/ghana_tutorial")
1. Load risk results¶
Read in regions:
[ ]:
regions = gpd.read_file(
data_folder + "/geoBoundaries-GHA-ADM1-all" + "/geoBoundaries-GHA-ADM1.shp"
)[["shapeName", "shapeISO", "geometry"]]
[ ]:
f, ax = plt.subplots(1, 1)
regions.plot(
ax=ax, alpha=1, linewidth=0.5, column="shapeName", edgecolor="black"
)
ax.set_title("Admin boundaries of Ghana")
ax.set_xlabel("Longitude [deg]")
ax.set_ylabel("Latitude [deg]")
Read in roads, join regions:
[ ]:
roads = gpd.read_file(
data_folder + "/GHA_OSM_roads.gpkg", layer="edges"
).rename(columns={"id": "road_id"})
roads = gpd.sjoin(roads, regions).drop(columns="index_right")
roads.head()
Read in risk:
[ ]:
risk = pd.read_csv(data_folder + "/results" + "/inunriver_damages_ead.csv")[
["id", "rcp", "gcm", "epoch", "ead_usd"]
].rename(columns={"id": "road_id"})
risk.head()
[ ]:
exposed_roads = roads[roads.road_id.isin(risk.road_id.unique())]
exposed_roads.head()
[ ]:
exposure = pd.read_csv(data_folder + "/results" + "/inunriver_damages_rp.csv")[
["id", "length_m", "rcp", "gcm", "epoch", "rp"]
].rename(columns={"id": "road_id", "length_m": "flood_length_m"})
# sum over any segments exposed within the same return period
exposure = exposure.groupby(["road_id", "rcp", "gcm", "epoch", "rp"]).sum()
# pick max length exposed over all return periods
exposure = (
exposure.reset_index()
.groupby(["road_id", "rcp", "gcm", "epoch"])
.max()
.reset_index()
)
exposure.head()
[ ]:
roads_with_risk = exposed_roads.merge(risk, on="road_id", how="inner").merge(
exposure, on=["road_id", "rcp", "gcm", "epoch"]
)
roads_with_risk.head()
2. Introduce adaptation options¶
Introduce costs of road upgrade options.
These costs are taken purely as an example, and further research is required to make reasonable estimates. They are intended to represent upgrade to a bituminous or concrete road design, with a single-lane design for currently-unpaved roads. The routine maintenance costs are estimated for rehabilitation and routine maintenance that should take place every year. The periodic maintenance costs are estimated for resurfacing and surface treatment that may take place approximately every five years.
As before with cost estimates, the analysis is likely to be highly sensitive to these assumptions, which should be replaced by better estimates if available.
[ ]:
options = pd.DataFrame(
{
"kind": ["four_lane", "two_lane", "single_lane"],
"initial_cost_usd_per_km": [1_000_000, 500_000, 125_000],
"routine_usd_per_km": [20_000, 10_000, 5_000],
"periodic_usd_per_km": [100_000, 50_000, 25_000],
}
)
options
Set a discount rate. This will be used to discount the cost of annual and periodic maintenance, as well as the present value of future expected annual damages.
This is another sensitive parameter which will affect the net present value calculations for both costs and benefits. As an exercise, try re-running the remainder of the analysis with different values here. What economic or financial justification could there be for assuming different discount rates?
[ ]:
discount_rate_percentage = 3
Given initial and routine costs and a discount rate, we can calculate the net present value for each adaptation option.
start by calculating the normalised discount rate for each year over the time horizon
add the initial costs for each option
calculate the discounted routine costs for each option (assumed to be incurred each year)
calculate the discounted periodic costs for each option (assumed to be incurred every five years)
[ ]:
# set up a costs dataframe
costs = pd.DataFrame()
# create a row per year over the time-horizon of interest
costs["year"] = np.arange(2020, 2081)
costs["year_from_start"] = costs.year - 2020
# calculate the normalised discount rate
discount_rate = 1 + discount_rate_percentage / 100
costs["discount_rate_norm"] = costs.year_from_start.apply(
lambda y: 1.0 / math.pow(discount_rate, y)
)
# calculate the sum over normalised discount rates for the time horizon
# this will be useful later, to calculate NPV of expected damages
discount_rate_norm = costs.discount_rate_norm.sum()
# link each of the options, so we have a row per-option, per-year
costs["link"] = 1
options["link"] = 1
costs = costs.merge(options, on="link").drop(columns="link")
# set initial costs to zero in all years except start year
costs.loc[costs.year_from_start > 0, "initial_cost_usd_per_km"] = 0
# discount routine and periodic maintenance costs
costs.routine_usd_per_km = costs.discount_rate_norm * costs.routine_usd_per_km
costs.periodic_usd_per_km = (
costs.discount_rate_norm * costs.periodic_usd_per_km
)
# set periodic costs to zero except for every five years
costs.loc[costs.year_from_start == 0, "periodic_usd_per_km"] = 0
costs.loc[costs.year_from_start % 5 != 0, "periodic_usd_per_km"] = 0
costs.head()
This table can then be summarised by summing over all years in the time horizon, to calculate the net present value of all that future investment in maintenance.
[ ]:
npv_costs = (
costs[
[
"kind",
"initial_cost_usd_per_km",
"routine_usd_per_km",
"periodic_usd_per_km",
]
]
.groupby("kind")
.sum()
.reset_index()
)
npv_costs["total_cost_usd_per_km"] = (
npv_costs.initial_cost_usd_per_km
+ npv_costs.routine_usd_per_km
+ npv_costs.periodic_usd_per_km
)
npv_costs
3. Estimate costs and benefits¶
Apply road kind assumptions for adaptation upgrades:
[ ]:
def kind(road_type):
if road_type in ("trunk", "trunk_link", "motorway"):
return "four_lane"
elif road_type in ("primary", "primary_link", "secondary"):
return "two_lane"
else:
return "single_lane"
roads_with_risk["kind"] = roads_with_risk.road_type.apply(kind)
Join adaptation cost estimates (per km)
[ ]:
roads_with_costs = roads_with_risk.merge(
npv_costs[["kind", "total_cost_usd_per_km"]], on="kind"
)
Calculate total cost estimate for length of roads exposed
[ ]:
roads_with_costs["total_adaptation_cost_usd"] = (
roads_with_costs.total_cost_usd_per_km
/ 1e3
* roads_with_costs.flood_length_m
)
Calculate net present value of avoided damages over the time horizon:
[ ]:
roads_with_costs["total_adaptation_benefit_usd"] = (
roads_with_costs.ead_usd * discount_rate_norm
)
[ ]:
discount_rate_norm
Calculate benefit-cost ratio
[ ]:
roads_with_costs["bcr"] = (
roads_with_costs.total_adaptation_benefit_usd
/ roads_with_costs.total_adaptation_cost_usd
)
Filter to pull out just the historical climate scenario:
[ ]:
historical = roads_with_costs[roads_with_costs.rcp == "historical"]
historical.describe()
Filter to find cost-beneficial adaptation options under historic flood scenarios
[ ]:
candidates = historical[historical.bcr > 1]
candidates.head(5)
Summarise by region to explore where cost-beneficial adaptation options might be located.
We need to sum over exposed lengths of road, costs and benefits, while finding the mean benefit-cost ratio.
[ ]:
candidates.groupby("shapeName").agg(
{
"flood_length_m": "sum",
"total_adaptation_benefit_usd": "sum",
"total_adaptation_cost_usd": "sum",
"bcr": "mean",
}
)
Given the aggregation, filtering and plotting you’ve seen throughout these tutorials, what other statistics would be interesting to explore from these results?
[ ]:
# optional: delete .zip files downloaded in the process
# for item in os.listdir(data_folder):
# if item.endswith(".zip"):
# os.remove(os.path.join(data_folder, item))