diff --git a/tutorials/04-evaluate-adaptation-options.ipynb b/tutorials/04-evaluate-adaptation-options.ipynb index b843e0f..c40847e 100644 --- a/tutorials/04-evaluate-adaptation-options.ipynb +++ b/tutorials/04-evaluate-adaptation-options.ipynb @@ -26,6 +26,7 @@ "outputs": [], "source": [ "# Imports from Python standard library\n", + "import math\n", "import os\n", "import warnings\n", "from glob import glob\n", @@ -120,9 +121,11 @@ "source": [ "roads = read_file_without_warnings(\n", " os.path.join(data_folder, 'GHA_OSM_roads.gpkg'),\n", - " layer='edges')\n", + " layer='edges') \\\n", + " .rename(columns={'id': 'road_id'})\n", "roads = gpd.sjoin(roads, regions) \\\n", - " .drop(columns='index_right')" + " .drop(columns='index_right')\n", + "roads.head()" ] }, { @@ -130,7 +133,7 @@ "id": "surprising-vision", "metadata": {}, "source": [ - "Read in exposure, join regions:" + "Read in risk:" ] }, { @@ -140,10 +143,11 @@ "metadata": {}, "outputs": [], "source": [ - "exposure = read_file_without_warnings(\n", - " os.path.join(data_folder, 'results/flood_exposure.gpkg'))\n", - "exposure = gpd.sjoin(exposure, regions) \\\n", - " .drop(columns='index_right')" + "risk = pd.read_csv(\n", + " os.path.join(data_folder, 'results/flood_risk.csv')) \\\n", + " [['id', 'rcp', 'gcm', 'ead_usd']] \\\n", + " .rename(columns={'id': 'road_id'})\n", + "risk.head()" ] }, { @@ -153,7 +157,47 @@ "metadata": {}, "outputs": [], "source": [ - "exposure.columns" + "exposed_roads = roads[roads.road_id.isin(risk.road_id.unique())]\n", + "exposed_roads" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "compatible-familiar", + "metadata": {}, + "outputs": [], + "source": [ + "exposure = pd.read_csv(\n", + " os.path.join(data_folder, 'results/flood_exposure.csv')) \\\n", + " [['id', 'flood_length_m', 'hazard', 'rcp', 'gcm', 'rp']] \\\n", + " .rename(columns={'id': 'road_id'})\n", + "\n", + "# sum over any segments exposed within the same return period\n", + "exposure = exposure \\\n", + " .groupby(['road_id', 'rcp', 'gcm', 'rp']) \\\n", + " .sum()\n", + "\n", + "# pick max length exposed over all return periods\n", + "exposure = exposure \\\n", + " .groupby(['road_id', 'rcp', 'gcm']) \\\n", + " .max() \\\n", + " .reset_index()\n", + "\n", + "exposure" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "domestic-meter", + "metadata": {}, + "outputs": [], + "source": [ + "roads_with_risk = exposed_roads \\\n", + " .merge(risk, on='road_id') \\\n", + " .merge(exposure, on=['road_id', 'rcp', 'gcm'])\n", + "roads_with_risk.head(2)" ] }, { @@ -166,12 +210,12 @@ }, { "cell_type": "markdown", - "id": "wrong-ferry", + "id": "aggressive-input", "metadata": {}, "source": [ "Introduce costs of road upgrade options.\n", "\n", - "These costs are derived from the World Bank ROCKS database, simplified and with climate resilience cost uplifts applied. They represent upgrade to a bituminous or concrete road design, with a single-lane design for currently-unpaved roads.\n", + "These costs are taken purely as an example, and further research is required to make reasonable estimates. They are intended 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.\n", "\n", "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." ] @@ -184,20 +228,26 @@ "outputs": [], "source": [ "options = pd.DataFrame({\n", - " 'kind': ['paved_four_lane', 'paved_two_lane', 'unpaved'],\n", - " 'initial_cost_usd_per_km': [ 3_580_000, 1_290_000, 645_000 ]\n", + " 'kind': ['four_lane', 'two_lane', 'single_lane'],\n", + " 'initial_cost_usd_per_km': [ 1_000_000, 500_000, 125_000 ],\n", + " 'routine_usd_per_km': [ 20_000, 10_000, 5_000 ],\n", + " 'periodic_usd_per_km': [ 100_000, 50_000, 25_000 ],\n", "})\n", - "routine_maintenance = 294_000\n", - "periodic_maintenance = 346_000\n", + "discount_rate_percentage = 12\n", "options" ] }, { "cell_type": "markdown", - "id": "quantitative-torture", + "id": "included-cloud", "metadata": {}, "source": [ - "## 3. Estimate costs and benefits" + "Given initial and routine costs and a discount rate, we can calculate the net present value for each adaptation option.\n", + "\n", + "- start by calculating the normalised discount rate for each year over the time horizon\n", + "- add the initial costs for each option\n", + "- calculate the discounted routine costs for each option (assumed to be incurred each year)\n", + "- calculate the discounted periodic costs for each option (assumed to be incurred every five years)" ] }, { @@ -207,32 +257,170 @@ "metadata": {}, "outputs": [], "source": [ - "duration_list = np.arange(10,110,10)\n", - "discount_rate = 12\n", - "growth_rates = np.arange(-2,4,0.2)\n", + "# set up a costs dataframe\n", + "costs = pd.DataFrame()\n", + "\n", + "# create a row per year over the time-horizon of interest\n", + "costs['year'] = np.arange(2020, 2081)\n", + "costs['year_from_start'] = costs.year - 2020\n", "\n", - "const discount_rate = 0.12;\n", - "const start_year = 2016;\n", - "const end_year = 2050;\n", + "# calculate the normalised discount rate\n", + "discount_rate = 1 + discount_rate_percentage / 100\n", + "costs['discount_rate_norm'] = costs.year_from_start.apply(lambda y: 1.0/math.pow(discount_rate, y))\n", + "# calculate the sum over normalised discount rates for the time horizon\n", + "# this will be useful later, to calculate NPV of expected damages\n", + "discount_rate_norm = costs.discount_rate_norm.sum()\n", "\n", - "# const years_from_start = Array.from(\n", - "# Array(end_year - start_year).keys()\n", - "# )\n", + "# link each of the options, so we have a row per-option, per-year\n", + "costs['link'] = 1\n", + "options['link'] = 1\n", + "costs = costs.merge(options, on='link').drop(columns='link')\n", "\n", - "# const discount_rate_norms = years_from_start.map(\n", - "# year => 1.0 / Math.pow(1.0 + discount_rate, year)\n", - "# )\n", - "# const discount_rate_norm = discount_rate_norms.reduce((a, b) => a + b, 0)" + "# set initial costs to zero in all years except start year\n", + "costs.loc[costs.year_from_start > 0, 'initial_cost_usd_per_km'] = 0\n", + "\n", + "# discount routine and periodic maintenance costs\n", + "costs.routine_usd_per_km = costs.discount_rate_norm * costs.routine_usd_per_km\n", + "costs.periodic_usd_per_km = costs.discount_rate_norm * costs.periodic_usd_per_km\n", + "# set periodic costs to zero except for every five years\n", + "costs.loc[costs.year_from_start == 0, 'periodic_usd_per_km'] = 0\n", + "costs.loc[costs.year_from_start % 5 != 0, 'periodic_usd_per_km'] = 0\n", + "costs" + ] + }, + { + "cell_type": "markdown", + "id": "accompanied-therapy", + "metadata": {}, + "source": [ + "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." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "peaceful-alabama", + "metadata": {}, + "outputs": [], + "source": [ + "npv_costs = costs[['kind', 'initial_cost_usd_per_km', 'routine_usd_per_km', 'periodic_usd_per_km']] \\\n", + " .groupby('kind') \\\n", + " .sum() \\\n", + " .reset_index()\n", + "npv_costs['total_cost_usd_per_km'] = \\\n", + " npv_costs.initial_cost_usd_per_km \\\n", + " + npv_costs.routine_usd_per_km \\\n", + " + npv_costs.periodic_usd_per_km\n", + "npv_costs" + ] + }, + { + "cell_type": "markdown", + "id": "nonprofit-london", + "metadata": {}, + "source": [ + "## 3. Estimate costs and benefits" + ] + }, + { + "cell_type": "markdown", + "id": "continuing-alignment", + "metadata": {}, + "source": [ + "Apply road kind assumptions for adaptation upgrades:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "trained-layer", + "metadata": {}, + "outputs": [], + "source": [ + "def kind(road_type):\n", + " if road_type in ('trunk', 'trunk_link', 'motorway'):\n", + " return 'four_lane'\n", + " elif road_type in ('primary', 'primary_link', 'secondary'):\n", + " return 'two_lane'\n", + " else:\n", + " return 'single_lane'\n", + "roads_with_risk['kind'] = roads_with_risk.road_type.apply(kind)" + ] + }, + { + "cell_type": "markdown", + "id": "numerical-zimbabwe", + "metadata": {}, + "source": [ + "Join adaptation cost estimates (per km)" ] }, { "cell_type": "code", "execution_count": null, - "id": "paperback-neighbor", + "id": "favorite-identification", "metadata": {}, "outputs": [], "source": [ - "ead * discount_norm" + "roads_with_costs = roads_with_risk.merge(npv_costs[['kind', 'total_cost_usd_per_km']], on='kind')" + ] + }, + { + "cell_type": "markdown", + "id": "solar-question", + "metadata": {}, + "source": [ + "Calculate total cost estimate for length of roads exposed" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dutch-invite", + "metadata": {}, + "outputs": [], + "source": [ + "roads_with_costs['total_adaptation_cost_usd'] = \\\n", + " roads_with_costs.total_cost_usd_per_km / 1e3 \\\n", + " * roads_with_costs.flood_length_m" + ] + }, + { + "cell_type": "markdown", + "id": "covered-aurora", + "metadata": {}, + "source": [ + "Calculate net present value of avoided damages over the time horizon" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "billion-import", + "metadata": {}, + "outputs": [], + "source": [ + "roads_with_costs['total_adaptation_benefit_usd'] = \\\n", + " roads_with_costs.ead_usd \\\n", + " * discount_rate_norm" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "constant-advocate", + "metadata": {}, + "outputs": [], + "source": [ + "discount_rate_norm" + ] + }, + { + "cell_type": "markdown", + "id": "nasty-correlation", + "metadata": {}, + "source": [ + "Calculate benefit-cost ratio" ] }, { @@ -241,7 +429,75 @@ "id": "accepted-charger", "metadata": {}, "outputs": [], - "source": [] + "source": [ + "roads_with_costs['bcr'] = \\\n", + " roads_with_costs.total_adaptation_benefit_usd \\\n", + " / roads_with_costs.total_adaptation_cost_usd" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "technological-effectiveness", + "metadata": {}, + "outputs": [], + "source": [ + "historical = roads_with_costs[roads_with_costs.rcp == 'historical']\n", + "historical.describe()" + ] + }, + { + "cell_type": "markdown", + "id": "adopted-tracker", + "metadata": {}, + "source": [ + "Filter to find cost-beneficial adaptation options under historic flood scenarios" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "complicated-translator", + "metadata": {}, + "outputs": [], + "source": [ + "candidates = historical[historical.bcr > 1]\n", + "candidates" + ] + }, + { + "cell_type": "markdown", + "id": "english-washington", + "metadata": {}, + "source": [ + "Summarise by region to explore where cost-beneficial adaptation options might be located.\n", + "\n", + "We need to sum over exposed lengths of road, costs and benefits, while finding the mean benefit-cost ratio." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "suited-workshop", + "metadata": {}, + "outputs": [], + "source": [ + "candidates.groupby('ADM1_EN') \\\n", + " .agg({\n", + " 'flood_length_m' : np.sum,\n", + " 'total_adaptation_benefit_usd': np.sum,\n", + " 'total_adaptation_cost_usd': np.sum,\n", + " 'bcr': np.mean\n", + " })" + ] + }, + { + "cell_type": "markdown", + "id": "cubic-cruise", + "metadata": {}, + "source": [ + "Given the aggregation, filtering and plotting you've seen throughout these tutorials, what other statistics would be interesting to explore from these results?" + ] } ], "metadata": {