{ "cells": [ { "cell_type": "markdown", "id": "164926e0", "metadata": { "id": "6d0b0da0" }, "source": [ "## Spatial Data Science with CityJSON\n", "\n", "The purpose of this Notebook is to ***work with*** the product of [osm_LoD1_3DCityModel](https://github.com/AdrianKriger/osm_LoD1_3DCityModel); a previously created CityJSON city model." ] }, { "cell_type": "markdown", "id": "305f3f23", "metadata": { "id": "91998c9b" }, "source": [ "
This notebook will:\n", "\n", "> **1. allow the user to execute an application of Spatial Data Science** \n", ">\n", ">> **a) [population estimation](#Section1a)** _--with a previous census metric population growth rate and projected (future) population are also possible_ **and** \n", ">> **b) a measure of [Building Volume per Capita](#Section1b)**\n", ">\n", "> **2. further applications of Spatial Data Science** \n", ">\n", ">> i calculate percentage **homes and population with direct access to on-site renewable energy infrastructure** *--rooftop photovoltaic panels (PV) and solar water heaters (SWH).*\n", ">>\n", ">> ii. estimate the [**Annual Average Solar** *(photovoltaic)* **Potential**](https://www.worldbank.org/en/topic/energy/publication/solar-photovoltaic-power-potential-by-country), per home.\n", ">\n", "> **3. produce [an interactive visualization](#Section1b)** *- which a user can navigate, query and share* **that**;\n", "> > **a) [colour buildings by type](#Section2a)** *(to easily visualize building stock)* \n", ">\n", "> **4. propose several [Geography and Sustainable Development Education *conversation starters*](#Section3) for Secondary and Tertiary level students**\n", "
" ] }, { "cell_type": "markdown", "id": "af30e4b2-5af9-49f2-9362-1d604000bec9", "metadata": {}, "source": [ "
Please Note:\n", "\n", "***The [village](https://github.com/AdrianKriger/geo3D/tree/main/village)*** processing option is meant for areas with no more than for **2 500 buildings**.
" ] }, { "cell_type": "code", "execution_count": 1, "id": "5dd597d7", "metadata": { "id": "a4ea1fcc" }, "outputs": [], "source": [ "#- load the magic\n", "\n", "%matplotlib inline\n", "import os\n", "from pathlib import Path\n", "import requests\n", "\n", "import numpy as np\n", "import pandas as pd\n", "import shapely\n", "from shapely.geometry import Polygon, shape, mapping\n", "import json\n", "import geojson\n", "\n", "import city3D as city3D\n", "\n", "from cjio import cityjson\n", "\n", "import matplotlib.pyplot as plt\n", "#import pydeck as pdk" ] }, { "cell_type": "code", "execution_count": 2, "id": "f8d278a7-e16d-4afd-802c-d4d2b48b8e2a", "metadata": {}, "outputs": [], "source": [ "import warnings\n", "warnings.filterwarnings('ignore')" ] }, { "cell_type": "markdown", "id": "b1988f7d", "metadata": { "id": "d19acf91" }, "source": [ "**The area under investigation is [Mamre, Cape Town. South Africa](https://en.wikipedia.org/wiki/Mamre,_South_Africa).**" ] }, { "cell_type": "code", "execution_count": 3, "id": "5557ef08", "metadata": { "id": "529536e5" }, "outputs": [], "source": [ "#- change to harvest the appropriate CityJSON\n", "\n", "#jparams = json.load(open('uEstate_param.json'))\n", "#jparams = json.load(open('cput_param.json'))\n", "#jparams = json.load(open('saao_param.json'))\n", "jparams = json.load(open('mamre_param.json'))\n", "#jparams = json.load(open('sRiver_param.json'))" ] }, { "cell_type": "code", "execution_count": 4, "id": "d9717ddd", "metadata": { "id": "c45a674c" }, "outputs": [], "source": [ "cm = cityjson.load(path=jparams['cjsn_solid']) #-- citjsnClean_rural3D.json in the result folder" ] }, { "cell_type": "code", "execution_count": 5, "id": "0360987e", "metadata": { "scrolled": true }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "CityJSON version = 1.1\n", "EPSG = 32734\n", "bbox = [263859.53151571925, 6287898.265465543, 145.1199951171875, 266646.4893499555, 6290185.027937451, 254.5399932861328]\n", "=== CityObjects ===\n", "|-- TINRelief (1)\n", "|-- Building (2316)\n", "===================\n", "materials = False\n", "textures = False\n" ] } ], "source": [ "print(cm)" ] }, { "cell_type": "code", "execution_count": 6, "id": "f8e0fa63", "metadata": { "id": "c7ec404d" }, "outputs": [], "source": [ "df = cm.to_dataframe()\n", "#- remove the first feature: the terrain\n", "df = df[1:] \n", "\n", "#- harvest the crs\n", "theinfo = cm.get_info()\n", "crs = theinfo[1]\n", "\n", "# account for holes\n", "def coords_to_polygon(rings):\n", " outer = rings[0] # first ring is the shell\n", " holes = rings[1:] if len(rings) > 1 else None\n", " return Polygon(shell=outer, holes=holes)\n", "\n", "# Convert JSON string to Python list\n", "df['footprint_coords_list'] = df['footprint'].apply(json.loads)\n", "\n", "# create home-baked gdf\n", "gdf = city3D.GeoDataFrameLite(df)\n", "gdf['geometry'] = gdf['footprint_coords_list'].apply(coords_to_polygon)\n", "gdf.crs = crs[7:]\n", "# Drop columns inplace\n", "gdf.drop(columns=['footprint', 'footprint_coords_list'], inplace=True)\n", "#gdf.head(2)" ] }, { "cell_type": "markdown", "id": "17ae2c91", "metadata": { "id": "b87c1769" }, "source": [ "## 1. Spatial Data Science" ] }, { "cell_type": "markdown", "id": "61389a02", "metadata": { "id": "7d170bdc" }, "source": [ "
We start with basic spatial analysis \n", " \n", " \n", "- We'll [estimate the population](#Section1a), within our area of interest, and then \n", "- calculate the [Building Volume Per Capita (BVPC)](#Section1b).\n", "
" ] }, { "cell_type": "markdown", "id": "88f9c7f0", "metadata": { "id": "1bae2eb7" }, "source": [ "While estimating population is well documented; recent investigations to **understand overcrowding** have led to newer measurements. \n", "\n", "The most noteable of these is **Building Volume Per Capita (BVPC)** [(Ghosh, T; et al. 2020)](https://www.researchgate.net/publication/343185735_Building_Volume_Per_Capita_BVPC_A_Spatially_Explicit_Measure_of_Inequality_Relevant_to_the_SDGs). BVPC is the cubic meters of building per person. **BVPC tells us how much space one person has per residential living unit** (a house / apartment / etc.). It is ***a proxy measure of economic inequality and a direct measure of housing inequality***.\n", "\n", "BVPC builds on the work of [(Reddy, A and Leslie, T.F., 2013)](https://www.tandfonline.com/doi/abs/10.1080/02723638.2015.1060696?journalCode=rurb20) and attempts to integrate with several **[Sustainable Development Goals](https://sdgs.un.org/goals)** (most noteably: **[SDG 11: Developing sustainable cities and communities](https://sdgs.un.org/goals/goal11)**) and captures the average ***'living space'*** each person has in their home." ] }, { "cell_type": "markdown", "id": "693b09b6", "metadata": { "id": "28084941" }, "source": [ "
These analysis expect the user to have some basic knowledge about the environment under inquiry / investigation
" ] }, { "cell_type": "code", "execution_count": 7, "id": "a44108b5", "metadata": { "id": "9b5be780", "outputId": "fdc6bce5-8012-49fd-d999-4eb6ef7eb015" }, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
idosm_idbuildingbuilding:levelsbuilding_heightroof_heightground_heightplus_codeaddressamenityoperatorresidentialbottom_roof_heightbuilding:usegeometry
3281184460328118446.0yes14.1183.53179.4299934FRWFFVC+P79NaNNaNNaNNaNNaNNaNPOLYGON ((265041.6351569728 6289775.0592048075...
3281184471328118447.0church26.9185.94179.0399934FRWFFVC+H9QMoravian Church South Africa Kerk Street Mamre...place_of_worshipNaNNaNNaNNaNPOLYGON ((265070.9766813367 6289761.325392997,...
\n", "
" ], "text/plain": [ " id osm_id building building:levels building_height \\\n", "328118446 0 328118446.0 yes 1 4.1 \n", "328118447 1 328118447.0 church 2 6.9 \n", "\n", " roof_height ground_height plus_code \\\n", "328118446 183.53 179.429993 4FRWFFVC+P79 \n", "328118447 185.94 179.039993 4FRWFFVC+H9Q \n", "\n", " address \\\n", "328118446 NaN \n", "328118447 Moravian Church South Africa Kerk Street Mamre... \n", "\n", " amenity operator residential bottom_roof_height \\\n", "328118446 NaN NaN NaN NaN \n", "328118447 place_of_worship NaN NaN NaN \n", "\n", " building:use geometry \n", "328118446 NaN POLYGON ((265041.6351569728 6289775.0592048075... \n", "328118447 NaN POLYGON ((265070.9766813367 6289761.325392997,... " ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "gdf.head(2)" ] }, { "cell_type": "code", "execution_count": 8, "id": "1c87ffcf", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array(['yes', 'church', 'house', 'cabin', 'public', 'civic', 'office',\n", " 'retail', 'clinic', 'school', 'garage', 'greenhouse', 'roof',\n", " 'kindergarten', 'clubhouse', 'guest_house', 'service', 'detached',\n", " 'shed'], dtype=object)" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "#gdf.plot()\n", "# have a look at the building type and amenities available\n", "gdf['building'].unique()" ] }, { "cell_type": "markdown", "id": "931cdc34-fed5-4b13-880e-4f6c35753f39", "metadata": {}, "source": [ "" ] }, { "cell_type": "markdown", "id": "aeb2a0d7", "metadata": { "id": "49f5c85e" }, "source": [ "### 1. a) Estimate Population\n", "\n", "
\n", " \n", "_(with population growth rate and population projection possible too)_
" ] }, { "cell_type": "code", "execution_count": 9, "id": "842acc96", "metadata": { "id": "1bc556e3" }, "outputs": [], "source": [ "#--we only want building=house or =apartment or =residential\n", "#gdf2 = gdf[gdf[\"building\"].isin(['house', 'semidetached_house', 'terrace', 'terraced', 'apartments', 'residential', 'dormitory', 'cabin', 'garage'])].copy()" ] }, { "cell_type": "code", "execution_count": 10, "id": "901aaf60-25d2-4307-b2be-1b4cdeff061c", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
idosm_idbuildingbuilding:levelsbuilding_heightroof_heightground_heightplus_codeaddressamenityoperatorresidentialbottom_roof_heightbuilding:usegeometry
3281184460328118446.0yes14.1183.53179.4299934FRWFFVC+P79NaNNaNNaNNaNNaNNaNPOLYGON ((265041.6351569728 6289775.0592048075...
3281184471328118447.0church26.9185.94179.0399934FRWFFVC+H9QMoravian Church South Africa Kerk Street Mamre...place_of_worshipNaNNaNNaNNaNPOLYGON ((265070.9766813367 6289761.325392997,...
\n", "
" ], "text/plain": [ " id osm_id building building:levels building_height \\\n", "328118446 0 328118446.0 yes 1 4.1 \n", "328118447 1 328118447.0 church 2 6.9 \n", "\n", " roof_height ground_height plus_code \\\n", "328118446 183.53 179.429993 4FRWFFVC+P79 \n", "328118447 185.94 179.039993 4FRWFFVC+H9Q \n", "\n", " address \\\n", "328118446 NaN \n", "328118447 Moravian Church South Africa Kerk Street Mamre... \n", "\n", " amenity operator residential bottom_roof_height \\\n", "328118446 NaN NaN NaN NaN \n", "328118447 place_of_worship NaN NaN NaN \n", "\n", " building:use geometry \n", "328118446 NaN POLYGON ((265041.6351569728 6289775.0592048075... \n", "328118447 NaN POLYGON ((265070.9766813367 6289761.325392997,... " ] }, "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ "#- some data wrangling to replace 'bld:residential' to 'bld:student' if 'residential:student'\n", "gdf_pop = gdf.copy()\n", "\n", "if 'residential' in gdf_pop.columns:\n", " df_res = gdf_pop[gdf_pop['residential'] == 'student']\n", " #df_res = df2[df2['building:use'] != None]\n", " df_res = df_res[~df_res['residential'].isna()]\n", " gdf_pop.loc[df_res.index, 'building'] = df_res['residential'] \n", "\n", "#- some more data wrangling\n", "with pd.option_context(\"future.no_silent_downcasting\", True):\n", " if 'building:flats' in gdf_pop.columns: \n", " gdf_pop['building:flats'] = pd.to_numeric(gdf_pop['building:flats'].fillna(0).infer_objects(copy=False))\n", " if 'building:units' in gdf_pop.columns: \n", " gdf_pop['building:units'] = pd.to_numeric(gdf_pop['building:units'].fillna(0).infer_objects(copy=False))\n", " if 'beds' in gdf_pop.columns: \n", " gdf_pop['beds'] = pd.to_numeric(gdf_pop['beds'].fillna(0).infer_objects(copy=False))\n", " if 'rooms' in gdf_pop.columns: \n", " gdf_pop['rooms'] = pd.to_numeric(gdf_pop['rooms'].fillna(0).infer_objects(copy=False))\n", "\n", "gdf_pop[\"building:levels\"] = pd.to_numeric(gdf_pop[\"building:levels\"])\n", "\n", "gdf_pop.head(2)" ] }, { "cell_type": "code", "execution_count": 11, "id": "68efc119-6ee7-4f26-9717-916dabe57874", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "building\n", "house 1643\n", "cabin 376\n", "yes 159\n", "garage 94\n", "retail 11\n", "civic 7\n", "roof 6\n", "school 5\n", "church 3\n", "public 2\n", "detached 2\n", "clinic 1\n", "office 1\n", "greenhouse 1\n", "kindergarten 1\n", "clubhouse 1\n", "guest_house 1\n", "service 1\n", "shed 1\n", "Name: count, dtype: int64" ] }, "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ "gdf_pop['building'].value_counts()" ] }, { "cell_type": "markdown", "id": "0af2ad75-60bf-49f7-986f-30a5d6d59bd6", "metadata": {}, "source": [ "**This area** (Mamre) **is peri-urban with single level housing units. To estimate population is thus pretty straight forward.**\n", "\n", "
We start with local knowledge.
\n", "\n", "**On average there are roughly `6` people per `building:house` in this area.** \n", "\n", "An ***informal*** structure ([shack](https://en.wikipedia.org/wiki/Shack)) is tagged [building:cabin](https://wiki.openstreetmap.org/wiki/Tag:building%3Dcabin) and houses `4` people." ] }, { "cell_type": "markdown", "id": "83e4191e-2a53-4589-b123-c3096dfa85e6", "metadata": {}, "source": [ "
Your Participation! \n", " \n", "\n", "We will execute the calculation programmatically. **Fill in the relevant variables in the _`cell`_ below**
" ] }, { "cell_type": "code", "execution_count": 12, "id": "c148b87f-9d71-4cc8-8486-29f7b81c8def", "metadata": {}, "outputs": [], "source": [ "#- average number of residents per formal house\n", "f_house = 6\n", "#- average number of residents per informal structure\n", "inf_structure = 4" ] }, { "cell_type": "markdown", "id": "d2a7b8f4", "metadata": { "id": "c162ade5" }, "source": [ "
\n", " \n", "**Furthermore:** \n", " - **[social housing](https://en.wikipedia.org/wiki/Public_housing)** is tagged `building:residential` with the number of occupants iether *the number of informal structure occupants* or `building:flats * inf_structure` \n", " - A `social_facility` (carehome, shelter, etc.) harvests the `beds` *'key:value'* pair. \n", " - `building:apartment` harvests the `building:flats` *'key:value'* pair *(the number of units)* to calculate `*3` people per apartment. \n", " - ***Student accomodation***: \n", "> - University owed: is tagged `building:dormitory` with `residential:university` and harvests the `beds` *'key:value'* pair.\n", "> - Private for-profit: is tagged `building:residential` or `:dormitory` with `residential:student` and then harvests the `building:flats` or `:rooms` *'key:value'* pair *(the number of units)* to calculate `*1` people per apartment; if `level: > 1` else `*3` people in a house share.\n", " \n", "**The tagging scheme and numbers is based on *how your community is mapped* and local knowledge**\n", "
" ] }, { "cell_type": "code", "execution_count": 13, "id": "4f626da0-d852-4414-b9de-077542e5a1ce", "metadata": { "scrolled": true }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "The estimated population is: 11362.0\n" ] } ], "source": [ "c = gdf_pop.columns\n", "\n", "def pop(row):\n", " #- formal house\n", " if row['building'] == 'house' or row['building'] == 'semidetached_house':\n", " return f_house\n", " if row['building'] == 'terrace' and 'building:units' in c or row['building'] == 'terraced' and 'building:units' in c:\n", " return row['building:units'] * f_house\n", "\n", " #- informal structure (shack)\n", " if row['building'] == 'cabin':\n", " return inf_structure\n", "\n", " #- in this case social housing\n", " if row['building'] == 'residential' and 'social_facility' in c and row['social_facility'] is np.nan:\n", " if row['building:levels'] > 1:\n", " if 'rooms' in c and row['rooms'] != 0:\n", " return row['rooms']\n", " if 'building:flats' in c and row['building:flats'] != 0:\n", " return row['building:flats'] * inf_structure\n", " else:\n", " return inf_structure\n", "\n", " #-- social facility [shelter / carehome]\n", " if row['building'] == 'residential' and 'social_facility' in c and row['social_facility'] is not np.nan:\n", " if row['building:levels'] > 1:\n", " if 'building:units' in c and row['building:units'] != 0:\n", " return row['building:units'] \n", " if 'beds' in c and row['beds'] != 0:\n", " return row['beds']\n", " else:\n", " return inf_structure\n", "\n", " #- formal apartment\n", " if row['building'] == 'apartments':\n", " if 'rooms' in c and row['rooms'] != 0:\n", " return row['rooms']\n", " else:\n", " return row['building:flats'] * 3\n", " \n", " #- private student residence \n", " if row['building'] == 'student':\n", " if row['building:levels'] > 1:\n", " if 'rooms' in c and row['rooms'] != 0:\n", " return row['rooms']\n", " else:\n", " return row['building:flats']\n", " else:\n", " return 3\n", " \n", " # university owned student residence\n", " if row['building'] == 'dormitory' and row['residential'] == 'university':\n", " if row['building:levels'] > 1:\n", " if 'rooms' in c and row['rooms'] != 0:\n", " return row['rooms']\n", " if 'beds' in c and row['beds'] != 0:\n", " return row['beds']\n", " else:\n", " return 3\n", "\n", "gdf_pop['pop'] = gdf_pop.apply(lambda x: pop(x), axis=1)\n", "\n", "est_pop = gdf_pop['pop'].sum()\n", "print('The estimated population is:', est_pop)" ] }, { "cell_type": "markdown", "id": "48fe80cb", "metadata": { "id": "01f52225" }, "source": [ "**The official [STATSSA 2011 census figure](https://www.statssa.gov.za/?page_id=4286&id=291), for this community, is 9048.**\n", "\n", "We can calculate the annual population growth rate using the formula for **[Annual population growth](https://databank.worldbank.org/metadataglossary/health-nutrition-and-population-statistics/series/SP.POP.GROW):**\n", "\n", "$$r = \\frac{\\ln{[\\frac{End Population}{Start Population}}]}{n} * 100 = \\frac{\\ln{[\\frac{11 120^{*}}{9048}}]}{12} * 100 = 1.43\\%$$\n", "
\n", "* ***Notice!*** The estimated population (11176) is **NOT** the number in the formula (11 120). This community is frequently updated on OpenStreetMap and variations are common. " ] }, { "cell_type": "markdown", "id": "9ae85954-dd0a-43e2-9160-d0f6285a6ad1", "metadata": {}, "source": [ "
Your Participation! \n", " \n", "\n", "It is possible to execute the calculation programmatically. **Fill in the relevant variables in the _`cell`_ below**
" ] }, { "cell_type": "code", "execution_count": 14, "id": "471c0219-00c6-4b06-a67c-b2cdf30df942", "metadata": {}, "outputs": [], "source": [ "#- previous population\n", "start_population = 9048 #- uEstate estimate: 987 \n", "\n", "#- period in years from the previous census\n", "years = 12" ] }, { "cell_type": "code", "execution_count": 15, "id": "8efec5c3-9266-44f0-bc44-202b83ff6f2c", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "population growth rate of approximately: 1.9 %\n" ] } ], "source": [ "#-execute\n", "r = (np.log(est_pop/start_population)/years) * 100\n", "print('population growth rate of approximately:', round(r, 2), '%')" ] }, { "cell_type": "markdown", "id": "d77014f9-70da-4ebc-91e9-ea1888230ab8", "metadata": {}, "source": [ "To conclude; we can project into the future with a very basic formula to estimate the population _x_-years from now: \n", "\n", "$$p = P_o * (1 + r)^{t} = p = 10736 * (1 + 0.0143)^{10} = 12 368$$" ] }, { "cell_type": "markdown", "id": "fe147a37-0bf0-4a67-8e3f-0d7de83eae75", "metadata": {}, "source": [ "
Your Participation! \n", " \n", "\n", "It is possible to execute the calculation programmatically. **Fill in the variables in the _`cell`_ below**
" ] }, { "cell_type": "code", "execution_count": 16, "id": "bb306d3c-351c-45fb-b75c-f3831a676466", "metadata": {}, "outputs": [], "source": [ "#- period in years from now\n", "years = 10" ] }, { "cell_type": "code", "execution_count": 17, "id": "34876cd3-8660-4971-a600-818d6aabd7dd", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "estimated population 10 years from now: 13712\n" ] } ], "source": [ "#p = est_pop * (1 + (r/100))**years\n", "\n", "#print('estimated population', years ,'years from now:', int(p))\n", "\n", "#- account for non-residential areas without failure\n", "#- helper function\n", "def safe_population_estimate(est_pop, r, years):\n", " try:\n", " p = est_pop * (1 + (r / 100))**years\n", " return int(p)\n", " except Exception as e:\n", " print(f\"Population estimate failed: {e}\")\n", " return None # keeps notebook running\n", "\n", "#- execute function\n", "p = safe_population_estimate(est_pop, r, years)\n", "\n", "#- shows error and moves on\n", "if p is not None:\n", " print(f\"estimated population {years} years from now: {p}\")" ] }, { "cell_type": "markdown", "id": "a4946db7-90ad-46a7-8a0b-166f8d3694ca", "metadata": {}, "source": [ "" ] }, { "cell_type": "markdown", "id": "954226c4", "metadata": { "id": "395e1ac4" }, "source": [ "### 1. b) Building Volume Per Capita (BVPC)\n", "
\n", "BVPC = total population of a community divided by sum of building volume
" ] }, { "cell_type": "code", "execution_count": 18, "id": "538cc014-8002-4f1c-8b92-77890f4bed8c", "metadata": {}, "outputs": [], "source": [ "#gdf_pop.head(3)" ] }, { "cell_type": "code", "execution_count": 19, "id": "94d28d52", "metadata": { "id": "d9a75295", "outputId": "a4db8ecc-c9b3-4ce1-f638-312b2e82006e" }, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
idosm_idbuildingbuilding:levelsbuilding_heightroof_heightground_heightplus_codeaddressamenityoperatorresidentialbottom_roof_heightbuilding:usegeometrypopareavolumebvpc
12289266234112289266.0house14.1189.86185.7599954FRWFFMF+W7J22 Clarkeson Street Mamre 7347 Cape TownNaNNaNNaNNaNNaNPOLYGON ((265302.8715881496 6288753.930217357,...6.0344.6797591413.187012235.531169
12357148234212357148.0house14.1197.36193.2599954FRWFFPJ+P7W2 Tol Street Mamre 7347 Cape TownNaNNaNNaNNaNNaNPOLYGON ((265989.6485932828 6288995.734908563,...6.0329.1894351349.676685224.946114
\n", "
" ], "text/plain": [ " id osm_id building building:levels building_height \\\n", "12289266 2341 12289266.0 house 1 4.1 \n", "12357148 2342 12357148.0 house 1 4.1 \n", "\n", " roof_height ground_height plus_code \\\n", "12289266 189.86 185.759995 4FRWFFMF+W7J \n", "12357148 197.36 193.259995 4FRWFFPJ+P7W \n", "\n", " address amenity operator \\\n", "12289266 22 Clarkeson Street Mamre 7347 Cape Town NaN NaN \n", "12357148 2 Tol Street Mamre 7347 Cape Town NaN NaN \n", "\n", " residential bottom_roof_height building:use \\\n", "12289266 NaN NaN NaN \n", "12357148 NaN NaN NaN \n", "\n", " geometry pop area \\\n", "12289266 POLYGON ((265302.8715881496 6288753.930217357,... 6.0 344.679759 \n", "12357148 POLYGON ((265989.6485932828 6288995.734908563,... 6.0 329.189435 \n", "\n", " volume bvpc \n", "12289266 1413.187012 235.531169 \n", "12357148 1349.676685 224.946114 " ] }, "execution_count": 19, "metadata": {}, "output_type": "execute_result" } ], "source": [ "#gdf_pop['area'] = gdf_pop['geometry'].area#\\.map(lambda p: p.area)\n", "gdf_pop['area'] = gdf_pop['geometry'].apply(lambda geom: geom.area if geom else 0)\n", "gdf_pop['volume'] = gdf_pop['area'] * gdf_pop['building_height']\n", "\n", "#- remove the volume of the ground floor (unoccupied) when building:levels > 7 [this is an arbitrary number based on local knowledge]\n", "#- typically the space is reserved for some other function: retail, etc. \n", "gdf_pop['volume'] = [\n", " (row['volume'] - row['area'] * 2.8) if (\n", " 'social_facility' in row and row['social_facility'] is np.nan and row['building:levels'] > 7 and\n", " row['building'] in ['residential', 'apartments', 'student']\n", " ) else row['volume']\n", " for _, row in gdf_pop.iterrows()\n", "]\n", "\n", "gdf_pop['bvpc'] = gdf_pop['volume'] / gdf_pop['pop']\n", "\n", "gdf_pop.tail(2)" ] }, { "cell_type": "code", "execution_count": 20, "id": "ce47ce5e", "metadata": { "id": "6d44d125", "outputId": "2d3355f4-5870-492a-d2db-4ad3a7dfedc8" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "count 2019.000000\n", "mean 75.260050\n", "std 50.350782\n", "min 7.419412\n", "25% 33.119812\n", "50% 65.729917\n", "75% 101.878902\n", "max 400.313316\n", "Name: bvpc, dtype: float64\n" ] } ], "source": [ "print(gdf_pop['bvpc'].describe())" ] }, { "cell_type": "code", "execution_count": 21, "id": "e0abe768", "metadata": { "id": "c98dafa0", "outputId": "d2b038a0-fddb-4fae-9b34-ad020453140d" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Building Volume Per Capita (BVPC): 89.487\n" ] } ], "source": [ "bvpc = round(gdf_pop['volume'].sum() / est_pop, 3)\n", "\n", "print('Building Volume Per Capita (BVPC):', bvpc)" ] }, { "cell_type": "markdown", "id": "05e8659c", "metadata": { "id": "f6576dc6" }, "source": [ "
\n", "\n", "**This BVPC value is for all the buildings; we only want buildings people live in** *(homes)*. \n", "\n", "And we can seperate `building:house` from `building:cabin` and `building:residential` to undertand the differences between ***formal and informal*** housing in this area.\n", " \n", "**We want to understand the living space *(the cubic-meter BVPC value)* each person has in thier home**\n", "
" ] }, { "cell_type": "code", "execution_count": 22, "id": "29ac68f3-d8ec-4e78-82ba-746d8f050033", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "FORMAL: Population: 9858.0 with Building Volume Per Capita (BVPC): 84.108\n", "\n", "STUDENT RESIDENCE: Population: 0.0 with Building Volume Per Capita (BVPC): 0\n", "\n", "INFORMAL: Population: 1504.0 with Building Volume Per Capita (BVPC) 36.596\n" ] } ], "source": [ "formal = gdf_pop[gdf_pop[\"building\"].isin(['house', 'semidetached_house', 'terrace', 'terraced', 'apartments'])].copy()\n", "f_pop = formal['pop'].sum()\n", "#f_area = formal['area'].mean()\n", "\n", "informal = gdf_pop[gdf_pop[\"building\"].isin(['residential', 'cabin'])].copy()\n", "inf_pop = informal['pop'].sum()\n", "#inf_area = formal['area'].mean()\n", "\n", "#- student\n", "stu = gdf_pop[gdf_pop[\"building\"].isin(['student', 'dormitory'])].copy()\n", "stu_pop = stu['pop'].sum()\n", "\n", "bvpc_formal = round(formal['volume'].sum() / formal['pop'].sum()if formal['pop'].sum() != 0 else 0, 3)\n", "bvpc_informal = round(informal['volume'].sum() / informal['pop'].sum() if informal['pop'].sum() != 0 else 0, 3)\n", "bvpc_stu = round(stu['volume'].sum() / stu['pop'].sum() if stu['pop'].sum() != 0 else 0, 3)\n", "\n", "print('FORMAL: Population: ', f_pop, ' with Building Volume Per Capita (BVPC):', bvpc_formal)\n", "print('')\n", "print('STUDENT RESIDENCE: Population: ', stu_pop, ' with Building Volume Per Capita (BVPC):', bvpc_stu)\n", "print('')\n", "print('INFORMAL: Population: ', inf_pop, ' with Building Volume Per Capita (BVPC)', bvpc_informal)" ] }, { "cell_type": "markdown", "id": "e2d29b09-6b5e-481d-a208-52bb27dea51c", "metadata": {}, "source": [ "
Warning: \n", " \n", "\n", "These are LoD1 3D City Models and works well in these types of areas. \n", "LoD2 would offer a more representative BVpC [(Ghosh, T; et al. 2020)](https://www.researchgate.net/publication/343185735_Building_Volume_Per_Capita_BVPC_A_Spatially_Explicit_Measure_of_Inequality_Relevant_to_the_SDGs) value; when the complexity of the built environment increases. \n", "\n", "Think about a `house` with living space in the roof structure, so called *'attic living'*, or an `apartment` / `residential` building with different levels, loft apartments and/or units in the turrets of a `building`. \n", "\n", "***consider***: this area seperates [building:cabin](https://wiki.openstreetmap.org/wiki/Tag:building%3Dcabin) from `building:residential` to more precisely represent informal structures without typical roof trussess but account for [social housing](https://en.wikipedia.org/wiki/Public_housing) that does
" ] }, { "cell_type": "markdown", "id": "ce0f56aa-d5aa-4041-8864-a87138c8bd62", "metadata": {}, "source": [ "## 2. Further examples of Spatial Data Science *(renewable energy)*:\n", "\n", "
\n", "\n", "**Let's attempt to understand the % of homes and population served with renewable energy.**\n", "
\n", " \n", "[**SDG**](https://sdgs.un.org/goals) indicators are typically calculated at **region and national scales**. \n", "Here, because we are working with highly detailed, local data, we can explore what a [**Tier 3 local indicator**](https://unstats.un.org/sdgs/metadata/) might look like at a ***neighbourhood level***.\n", "
\n", "\n", "In this section 3. we evaluate [**SDG 7: Ensure access to affordable, reliable, sustainable and modern energy for all**](https://sdgs.un.org/goals/goal7) at a community level and estimate the **proportion of residential units and population** that have **direct access to on-site renewable energy infrastructure** *--rooftop photovoltaic panels (PV) and solar water heaters (SWH)*.\n", "\n", "- Percentage of **households** served by rooftop renewable energy \n", "- Percentage of the **population** served by rooftop renewable energy\n", "" ] }, { "cell_type": "code", "execution_count": 23, "id": "29d03a80-9f26-4c2d-b090-c59290f12013", "metadata": {}, "outputs": [], "source": [ "#- harvest rooftop solar\n", "\n", "query = \"\"\"\n", " [out:json][timeout:180];\n", " // --when areas have duplicate names given the world has a limited amount of uniquely named places\n", " area[name='{0}'] ->.b;\n", " // -- target area ~ can be way or relation\n", " wr(area.b)[name='{1}'];\n", " map_to_area -> .a;\n", " (\n", " way[\"power\"=\"generator\"][\"generator:source\"=\"solar\"](area.a); // Catches simple generators\n", " // way[\"power\"=\"solar_photovoltaic_panel\"](area.a); // Catches the alternate tag\n", " );\n", " out geom;\n", " \"\"\".format(jparams['LargeArea'], jparams['FocusArea'])\n", "\n", "#- execute function from city3D, harvest generator solar and return GeoDataFrameLite | home-baked gdf\n", "sol = city3D.overpass_to_gdf(query)\n", "#sol = sol.to_crs(epsg)\n", "sol = sol.to_crs(crs[7:])\n", "\n", "\n", "if len(sol) < 0:\n", " print(\"\\033[0m No rooftop solar are mapped in\", jparams['FocusArea'])" ] }, { "cell_type": "code", "execution_count": 24, "id": "06be3e3d-f262-4f48-a7ea-a836fcd54d9e", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
generator:methodgenerator:output:hot_watergenerator:sourcegenerator:typelocationpowerstart_dateareagenerator:output:electricitygeometryosm_idosm_type
0thermalyessolarsolar_thermal_collectorroofgeneratorNaNNaNNaNPOLYGON ((266074.7976815852 6288576.002370844,...1095737675way
1thermalyessolarsolar_thermal_collectorroofgeneratorNaNNaNNaNPOLYGON ((266194.47713799647 6288764.539267579...1095739760way
\n", "
" ], "text/plain": [ " generator:method generator:output:hot_water generator:source \\\n", "0 thermal yes solar \n", "1 thermal yes solar \n", "\n", " generator:type location power start_date area \\\n", "0 solar_thermal_collector roof generator NaN NaN \n", "1 solar_thermal_collector roof generator NaN NaN \n", "\n", " generator:output:electricity \\\n", "0 NaN \n", "1 NaN \n", "\n", " geometry osm_id osm_type \n", "0 POLYGON ((266074.7976815852 6288576.002370844,... 1095737675 way \n", "1 POLYGON ((266194.47713799647 6288764.539267579... 1095739760 way " ] }, "execution_count": 24, "metadata": {}, "output_type": "execute_result" } ], "source": [ "#- look\n", "sol.head(2)" ] }, { "cell_type": "code", "execution_count": 25, "id": "197a40d1-33d2-4a59-8021-ee1b5456d1d7", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "generator:method\n", "thermal 57\n", "photovoltaic 1\n", "Name: count, dtype: int64" ] }, "execution_count": 25, "metadata": {}, "output_type": "execute_result" } ], "source": [ "#- the number of renewable in AREA\n", "sol['generator:method'].value_counts()" ] }, { "cell_type": "code", "execution_count": 26, "id": "32edbff5-8f3b-4398-a026-1a7514bde0b0", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
idosm_idbuildingbuilding:levelsbuilding_heightroof_heightground_heightplus_codeaddressamenity...bottom_roof_heightbuilding:usegeometrypopareavolumebvpcsolar_idsgenerator:methodhas_solar
3281184460328118446.0yes14.1183.53179.4299934FRWFFVC+P79NaNNaN...NaNNaNPOLYGON ((265041.6351569728 6289775.0592048075...NaN268.4095501100.479153NaN[][]False
3281184471328118447.0church26.9185.94179.0399934FRWFFVC+H9QMoravian Church South Africa Kerk Street Mamre...place_of_worship...NaNNaNPOLYGON ((265070.9766813367 6289761.325392997,...NaN371.1688752561.065235NaN[][]False
\n", "

2 rows × 22 columns

\n", "
" ], "text/plain": [ " id osm_id building building:levels building_height \\\n", "328118446 0 328118446.0 yes 1 4.1 \n", "328118447 1 328118447.0 church 2 6.9 \n", "\n", " roof_height ground_height plus_code \\\n", "328118446 183.53 179.429993 4FRWFFVC+P79 \n", "328118447 185.94 179.039993 4FRWFFVC+H9Q \n", "\n", " address \\\n", "328118446 NaN \n", "328118447 Moravian Church South Africa Kerk Street Mamre... \n", "\n", " amenity ... bottom_roof_height building:use \\\n", "328118446 NaN ... NaN NaN \n", "328118447 place_of_worship ... NaN NaN \n", "\n", " geometry pop area \\\n", "328118446 POLYGON ((265041.6351569728 6289775.0592048075... NaN 268.409550 \n", "328118447 POLYGON ((265070.9766813367 6289761.325392997,... NaN 371.168875 \n", "\n", " volume bvpc solar_ids generator:method has_solar \n", "328118446 1100.479153 NaN [] [] False \n", "328118447 2561.065235 NaN [] [] False \n", "\n", "[2 rows x 22 columns]" ] }, "execution_count": 26, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# join (link) rooftop renewable energy to the appropriate bld\n", "def buildings_with_solar(gdf_buildings, gdf_solar):\n", " # Prepare output arrays\n", " solar_ids_per_building = [[] for _ in range(len(gdf_buildings[\"geometry\"]))]\n", " solar_types_per_building = [[] for _ in range(len(gdf_buildings))]\n", " \n", " for i, b_geom in enumerate(gdf_buildings[\"geometry\"]):\n", " for j, s_geom in enumerate(gdf_solar[\"geometry\"]):\n", " #if b_geom.intersects(s_geom):\n", " if b_geom.contains(s_geom):\n", " solar_ids_per_building[i].append(gdf_solar[\"osm_id\"].iloc[j])\n", " solar_types_per_building[i].append(gdf_solar[\"generator:method\"].iloc[j])\n", "\n", " #- keep only unique values\n", " #unique_methods_per_building = [list(set(lst)) for lst in solar_types_per_building]\n", " \n", " gdf_buildings[\"solar_ids\"] = solar_ids_per_building\n", " gdf_buildings[\"generator:method\"] = solar_types_per_building #unique_methods_per_building\n", " gdf_buildings[\"has_solar\"] = [len(lst) > 0 for lst in solar_ids_per_building]\n", " gdf_buildings[\"solar_ids\"] = solar_ids_per_building\n", " \n", " return gdf_buildings\n", "\n", "blds = buildings_with_solar(gdf_pop, sol)\n", "blds.head(2)" ] }, { "cell_type": "code", "execution_count": 27, "id": "c5ff3e82-03fe-42c3-8e0c-bf88ba4d4b23", "metadata": {}, "outputs": [], "source": [ "#--we only want buildings people live in (homes). building=house or =apartment or =residential, etc.\n", "blds = blds[blds[\"building\"].isin(['house', 'semidetached_house', 'terrace', 'terraced', 'apartments', 'residential', 'dormitory', 'cabin', 'garage'])].copy()" ] }, { "cell_type": "markdown", "id": "fc1779d2-7e08-482c-b21d-8a275b6c11a9", "metadata": {}, "source": [ "
Percentage of households served by rooftop renewable energy
\n", "\n", "$$ \\text{\\% homes with renewable energy} = \\frac{\\text{Number of dwellings with mapped solar PV or SWH}}{\\text{Total number of dwellings}} \\times 100 $$" ] }, { "cell_type": "code", "execution_count": 28, "id": "804952d2-985e-44f2-80c5-2aa8f87c422c", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\u001b[1m Percentage homes, \u001b[0m in Mamre , with rooftop photovoltaic panels (PV) and solar water heaters (SWH): 2.51\n" ] } ], "source": [ "#- harvest columns\n", "with_solar = sum(blds[\"has_solar\"])\n", "pop = est_pop #gdf[\"pop\"]\n", "total_homes = len(blds)\n", "\n", "solHms = round(with_solar / total_homes * 100, 2)\n", "\n", "print('\\033[1m Percentage homes, \\033[0m in', jparams['FocusArea'],', with rooftop photovoltaic panels (PV) and solar water heaters (SWH):', solHms)" ] }, { "cell_type": "markdown", "id": "4ab1d45d-7f16-4c85-be56-72c81d511c58", "metadata": {}, "source": [ "
\n", " NB: this number includes the OpenStreetMap building=garage building type. Go to Cell ±27 (above) to exclude this building type from the estimate.\n", "
\n", "
\n", "
Percentage of population served by rooftop renewable energy
\n", "\n", "$$ \\text{\\% population with renewable energy} = \\frac{\\text{Number of residents with mapped solar PV and SWH}}{\\text{Estimated population}} \\times 100 $$" ] }, { "cell_type": "code", "execution_count": 29, "id": "a8b206e7-c5fc-424f-9dbc-ff17a4ff7c86", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\u001b[1m Percentage population, \u001b[0m in Mamre , with rooftop photovoltaic panels (PV) and solar water heaters (SWH): 2.8\n" ] } ], "source": [ "#pop_total = gdf_pop[\"pop\"].sum()\n", "pop_total = blds[\"pop\"].sum()\n", "#pop_solar = gdf_pop[\"pop\"][blds[\"has_solar\"]].sum()\n", "pop_solar = blds[\"pop\"][blds[\"has_solar\"]].sum()\n", "\n", "\n", "solPop = round(pop_solar / pop_total * 100, 2)\n", "\n", "print('\\033[1m Percentage population, \\033[0m in', jparams['FocusArea'],', with rooftop photovoltaic panels (PV) and solar water heaters (SWH):', solPop)" ] }, { "cell_type": "code", "execution_count": 30, "id": "88d8b053-e5c2-458d-a948-e0592f830cd9", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "generator:method\n", "thermal 52\n", "photovoltaic 1\n", "Name: count, dtype: int64" ] }, "execution_count": 30, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# number of solar renewable on HOMES\n", "blds['generator:method'].explode().value_counts()" ] }, { "cell_type": "markdown", "id": "7cec7624-3686-49e3-aa9e-a35c675ace85", "metadata": {}, "source": [ "### 2. ii) Solar potential (MWh)\n", "\n", "
\n", "\n", "In this section, we attempt to understand how much ***'fuel'*** a rooftop can get from the sun.\n", "
\n", "\n", "We are calling the [NASA POWER API (Prediction Of Worldwide Energy Resources)](https://power.larc.nasa.gov/docs/services/api/temporal/monthly/). This is a global dataset that uses [NASA satellite observations and weather models](https://power.larc.nasa.gov) to tell us exactly how much solar radiation hits a specific location on Earth.\n", "\n", "What we are requesting:\n", "\n", "> **Parameter:** `ALLSKY_SFC_SW_DWN` (Global Horizontal Irradiance): a 30-year historical average of solar radiation. This ensures our communities solar potential is based on ***long-term climate trends*** rather than a single year of weather.\n", ">\n", "> **Source:** [NASA POWER Climatology API](https://power.larc.nasa.gov/docs/services/api/temporal/climatology/)\n", "> \n", "> **Goal:**\n", "> To calculate the Annual Total GHI (kWh/m2/year). This value tells us the cumulative **'solar pressure'** hitting our rooftops over an entire year, which we then use to calculate **how many Megawatt-hours (MWh) of clean electricity our neighborhood can generate**." ] }, { "cell_type": "code", "execution_count": 31, "id": "21ddbd18-fe4f-4a96-9cab-f122d1437a30", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Annual Average GHI: 2029.76 kWh/m²/year\n" ] } ], "source": [ "def get_ghi_data(lat, lon):#, year=\"2020\"):\n", " #url = \"https://power.larc.nasa.gov/api/temporal/daily/point\"\n", " #url = \"https://power.larc.nasa.gov/api/temporal/monthly/point\"\n", " url = \"https://power.larc.nasa.gov/api/temporal/climatology/point\"\n", " params = {\n", " \"parameters\": \"ALLSKY_SFC_SW_DWN\", # This is NASA's GHI code\n", " \"community\": \"RE\", # Renewable Energy community\n", " \"longitude\": lon,\n", " \"latitude\": lat,\n", " \"format\": \"JSON\"\n", " }\n", " \n", " response = requests.get(url, params=params)\n", " data = response.json()\n", " \n", " # Extract the GHI values into a list\n", " ghi_values = data['properties']['parameter']['ALLSKY_SFC_SW_DWN']\n", " long_term_monthly = ghi_values['ANN']\n", " \n", " # CONVERSION: Multiply by 365 to get the Yearly Total Sum\n", " annual_total_sum = long_term_monthly * 365\n", " \n", " return annual_total_sum\n", "\n", "\n", "# -- data wrangling\n", "gdf = gdf.to_crs(4326)\n", "# combine all geometries\n", "geom = shapely.unary_union(gdf['geometry'])\n", "# centroid\n", "xy = (geom.centroid.x, geom.centroid.y)\n", "lat, lon = xy[1], xy[0] \n", "\n", "#- execute function\n", "annual_avg = get_ghi_data(lat, lon)\n", "print(f\"Annual Average GHI: {round(annual_avg, 2)} kWh/m²/year\")" ] }, { "cell_type": "markdown", "id": "711899b7-ea02-4dc2-8d5e-c61cb0efa1dd", "metadata": {}, "source": [ "
Annual Solar Potential (MWh)
\n", "\n", "We use a simplified formula to provide a clear baseline.\n", "\n", "$$\n", "\\text{Potential (MWh)} = \\frac{(\\text{Surface Area} \\times \\text{utilization factor}) \\times \\text{GHI}_{\\text{annual}} \\times 0.2}{1000}\n", "$$\n", "\n", "\n", "
\n", "\n", "***Theoretical Framework:** The annual energy output of a photovoltaic system (E) is determined by the product of the total solar resource (GHI), the active area of the array (A), and the system's overall efficiency (η), adjusted by a Performance Ratio (PR) to account for real-world losses. — based on [NREL (2022)](https://joint-research-centre.ec.europa.eu/photovoltaic-geographical-information-system-pvgis_en) & [IEC 61724-1](https://webstore.iec.ch/en/publication/65561). We then adapt this formula and represents a combined value of 25% nominal panel efficiency and a 0.80 Performance Ratio with a **single 0.20 system efficiency value and account for usable area**, a heuristic for gabled roofs.* \n", "
\n", "\n", "
Your Participation! \n", " \n", "**Fill in the `utilization_factor` below** \n", "
\n", "\n", "As a **'rule-of-thumb'** a community with traditional gabled houses: `utilization_factor = 0.4` *(less than half)*, while a high-density suburb with flat-roofed apartments: `utilization_factor = 0.6`" ] }, { "cell_type": "code", "execution_count": 32, "id": "3ee22fee-89e9-47de-a47b-342bf8c75fd2", "metadata": {}, "outputs": [], "source": [ "# on average, how much of the roof faces the sun? \n", "utilization_factor = 0.4 # Adjust based on roof types" ] }, { "cell_type": "code", "execution_count": 33, "id": "b1d45330-f447-46cc-a396-0bf3473b7be2", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
idosm_idbuildingbuilding:levelsbuilding_heightroof_heightground_heightplus_codeaddressamenity...building:usegeometrypopareavolumebvpcsolar_idsgenerator:methodhas_solarsolar_mwh
6568409747656840974.0house14.1174.78170.6799934FRWFFM9+X9839 Dove Lane Mamre 7347 Cape TownNaN...NaNPOLYGON ((264864.98193212313 6288741.008398377...6.038.658416158.49950626.416584[][]False6.277400
6568409758656840975.0house14.1174.78170.6799934FRWFFM9+X9X37 Dove Lane Mamre 7347 Cape TownNaN...NaNPOLYGON ((264865.3504529182 6288749.140981195,...6.039.833421163.31702827.219505[1096772168][thermal]True6.468199
\n", "

2 rows × 23 columns

\n", "
" ], "text/plain": [ " id osm_id building building:levels building_height \\\n", "656840974 7 656840974.0 house 1 4.1 \n", "656840975 8 656840975.0 house 1 4.1 \n", "\n", " roof_height ground_height plus_code \\\n", "656840974 174.78 170.679993 4FRWFFM9+X98 \n", "656840975 174.78 170.679993 4FRWFFM9+X9X \n", "\n", " address amenity ... building:use \\\n", "656840974 39 Dove Lane Mamre 7347 Cape Town NaN ... NaN \n", "656840975 37 Dove Lane Mamre 7347 Cape Town NaN ... NaN \n", "\n", " geometry pop area \\\n", "656840974 POLYGON ((264864.98193212313 6288741.008398377... 6.0 38.658416 \n", "656840975 POLYGON ((264865.3504529182 6288749.140981195,... 6.0 39.833421 \n", "\n", " volume bvpc solar_ids generator:method has_solar \\\n", "656840974 158.499506 26.416584 [] [] False \n", "656840975 163.317028 27.219505 [1096772168] [thermal] True \n", "\n", " solar_mwh \n", "656840974 6.277400 \n", "656840975 6.468199 \n", "\n", "[2 rows x 23 columns]" ] }, "execution_count": 33, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Potential (MWh) = (Area * GHI * 0.20) / 1000\n", "blds['solar_mwh'] = ((blds['area'] * utilization_factor) * (annual_avg) * 0.20) / 1000\n", "blds.head(2)" ] }, { "cell_type": "code", "execution_count": 34, "id": "1fceb903-556f-4085-8575-dde893139e52", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ " \u001b[1mThe average solar potential, per home\u001b[0m , for Mamre is: 17.18 MWh/year\n" ] } ], "source": [ "# Calculate the average annual solar potential\n", "average_solar_potential = blds['solar_mwh'].mean()\n", "\n", "print(\" \\033[1mThe average solar potential, per home\\033[0m , for\", jparams['FocusArea'], \"is:\", round(average_solar_potential,2), \"MWh/year\")" ] }, { "cell_type": "markdown", "id": "bbb64ecc", "metadata": { "id": "c0daa088" }, "source": [ "
\n", " NB: this number includes the OpenStreetMap building=garage building type. Go to Cell ±27 to exclude this building type from the estimate.\n", "
\n", "
\n", "
\n", " \n", "What does this **MWh/year** value mean?\n", "
\n", " \n", "To put the value in context, **15 MWh/year**: \n", "- is enough to provide **100% of the electricity** for 4 to 5 average UK homes *(which use ~3.4 MWh each)* or 1.5 average US homes *(~10.7 MWh each)*\n", "- is enough power to drive an Electric Vehicle for **75,000 kilometers** *--that’s almost two full trips around the Earth*.\n", "- saves roughly **10 metric tons of Carbon Dioxide** from entering the atmosphere.\n", " \n", "
\n", "\n", "
Sanity Check!\n", "
\n", "\n", "- In Cell ±27 we excluded non-residential building types `=office, commercial, retail, warehouse, industrial, etc.` from the analysis.\n", " \n", "- Cape Town typically yields ~1.6–1.7 MWh **per installed kWp per year**; the higher per-household values reported here reflect **rooftop potential derived from available area** *--that considers a `utilization_factor`*, not a 1 kWp system.\n", "\n", " A 1 kWp solar PV system requires approximately 5–8 m² of panel area (e.g. panels of roughly 1 m × 1.7 m, depending on technology).\n", "\n", " We are NOT asking: How much energy (MWh/year) would a single 1 kWp PV system generate on a roof? \n", " We are asking: **How much energy (MWh/year) could these roofs harvest, given their available area and a realistic utilization factor?**\n", "\n", "- The **BVPC Warning** applies here too. These are **LoD1 3D City Models**, which represent buildings as simple extrusions.\n", "\n", " LoD2 models *—that capture roof form (e.g. gable, hipped, mansard, domes)—* would provide more representative estimates of both **BVPC and Average Annual Solar Potential**. In such cases, the `utilization_factor` becomes less critical, as usable roof geometry is explicitly modelled. \n", "\n", "
\n", "\n", "## 3. Interactive Visualization\n", "\n", "You might want to create and share an `html` visualization.\n", "\n", "
\n", " \n", "_In this example we identify building stock by **color** but you are limited only through your imagination and the data you have access too_\n", "
" ] }, { "cell_type": "code", "execution_count": 35, "id": "ad6fa443-4a22-41ce-b7a0-28d69886173c", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "\n", "Name: WGS 84\n", "Axis Info [ellipsoidal]:\n", "- Lat[north]: Geodetic latitude (degree)\n", "- Lon[east]: Geodetic longitude (degree)\n", "Area of Use:\n", "- name: World.\n", "- bounds: (-180.0, -90.0, 180.0, 90.0)\n", "Datum: World Geodetic System 1984 ensemble\n", "- Ellipsoid: WGS 84\n", "- Prime Meridian: Greenwich" ] }, "execution_count": 35, "metadata": {}, "output_type": "execute_result" } ], "source": [ "gdf.crs" ] }, { "cell_type": "markdown", "id": "c5a64b72-e1f0-4ee8-a346-8bbab4eae03f", "metadata": {}, "source": [ "\"proj\"\n", "
\n", "
\n", "\n", "We need a ***Geographic*** Coordinate Reference System.\n", "\n", " \n", " Name: WGS 84\n", " Axis Info [ellipsoidal]:\n", " - Lat[north]: Geodetic latitude (degree)\n", " - Lon[east]: Geodetic longitude (degree)\n", " Area of Use:\n", " - name: World.\n", " - bounds: (-180.0, -90.0, 180.0, 90.0)\n", " Datum: World Geodetic System 1984 ensemble\n", " - Ellipsoid: WGS 84\n", " - Prime Meridian: Greenwich" ] }, { "cell_type": "code", "execution_count": 36, "id": "48b114d1", "metadata": {}, "outputs": [], "source": [ "#- the pseudo-3Dviz needs geographic coords\n", "gdf = gdf.to_crs(4326)" ] }, { "cell_type": "code", "execution_count": 37, "id": "ebffd46f", "metadata": {}, "outputs": [], "source": [ "# -- get the location for 3Dviz\n", "\n", "# combine all geometries\n", "geom = shapely.unary_union(gdf['geometry'])\n", "# centroid\n", "xy = (geom.centroid.x, geom.centroid.y)\n", "\n", "# bounding box\n", "#minx, miny, maxx, maxy = geom.bounds\n", "#bbox = [minx, miny, maxx, maxy]" ] }, { "cell_type": "code", "execution_count": 38, "id": "6d620069", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array(['yes', 'church', 'house', 'cabin', 'public', 'civic', 'office',\n", " 'retail', 'clinic', 'school', 'garage', 'greenhouse', 'roof',\n", " 'kindergarten', 'clubhouse', 'guest_house', 'service', 'detached',\n", " 'shed'], dtype=object)" ] }, "execution_count": 38, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# have a look at the building type and amenities available\n", "gdf['building'].unique()" ] }, { "cell_type": "markdown", "id": "95db7a64-6670-4b1d-86ae-a1476ef72f78", "metadata": {}, "source": [ "\n", "
Building Stock: To differentiate a school, housing, retail, healthcare and community focused facilities (library, municipal office, community centre) we color the buildings - we harvest the osm tags [amenity and building type] directly.
" ] }, { "cell_type": "code", "execution_count": 39, "id": "f777c749", "metadata": {}, "outputs": [], "source": [ "# colour buildings based on use / amenity\n", "def color(bld):\n", " #- formal house\n", " if bld == 'house' or bld == 'semidetached_house' or bld == 'terrace': #- add maisonette, duplex, etc. \n", " return [255, 255, 204] #-grey\n", " if bld == 'apartments':\n", " return [252, 194, 3] #-orange \n", " #- informal structure / social housing / student\n", " if bld == 'residential' or bld == 'dormitory' or bld == 'student' or bld == 'cabin':\n", " return [119, 3, 252] #-purple\n", " \n", " if bld == 'garage' or bld == 'parking':\n", " return [3, 132, 252] #-blue \n", " if bld == 'retail' or bld == 'supermarket':\n", " return [253, 141, 60]\n", " if bld == 'office' or bld == 'commercial':\n", " return [185, 206, 37]\n", " if bld == 'school' or bld == 'kindergarten' or bld == 'university' or bld == 'college':\n", " return [128, 0, 38]\n", " if bld == 'clinic' or bld == 'doctors' or bld == 'hospital':\n", " return [89, 182, 178]\n", " if bld == 'community_centre' or bld == 'service' or bld == 'post_office' or bld == 'hall' \\\n", " or bld == 'townhall' or bld == 'police' or bld == 'library' or bld == 'fire_station' :\n", " return [181, 182, 89]\n", " if bld == 'warehouse' or bld == 'industrial':\n", " return [193, 255, 193]\n", " if bld == 'hotel':\n", " return [139, 117, 0]\n", " if bld == 'church' or bld == 'mosque' or bld == 'synagogue':\n", " return [225, 225, 51]\n", " else:\n", " return [255, 255, 204]\n", "\n", "gdf[\"fill_color\"] = gdf['building'].apply(lambda x: color(x))" ] }, { "cell_type": "code", "execution_count": 40, "id": "5e88b1da", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
idosm_idbuildingbuilding:levelsbuilding_heightroof_heightground_heightplus_codeaddressamenityoperatorresidentialbottom_roof_heightbuilding:usegeometryfill_color
3281184460328118446.0yes14.1183.53179.4299934FRWFFVC+P79NaNNaNNaNNaNNaNNaNPOLYGON ((18.4706033 -33.505787600000005, 18.4...[255, 255, 204]
3281184471328118447.0church26.9185.94179.0399934FRWFFVC+H9QMoravian Church South Africa Kerk Street Mamre...place_of_worshipNaNNaNNaNNaNPOLYGON ((18.4709153 -33.5059178, 18.4709287 -...[225, 225, 51]
\n", "
" ], "text/plain": [ " id osm_id building building:levels building_height \\\n", "328118446 0 328118446.0 yes 1 4.1 \n", "328118447 1 328118447.0 church 2 6.9 \n", "\n", " roof_height ground_height plus_code \\\n", "328118446 183.53 179.429993 4FRWFFVC+P79 \n", "328118447 185.94 179.039993 4FRWFFVC+H9Q \n", "\n", " address \\\n", "328118446 NaN \n", "328118447 Moravian Church South Africa Kerk Street Mamre... \n", "\n", " amenity operator residential bottom_roof_height \\\n", "328118446 NaN NaN NaN NaN \n", "328118447 place_of_worship NaN NaN NaN \n", "\n", " building:use geometry \\\n", "328118446 NaN POLYGON ((18.4706033 -33.505787600000005, 18.4... \n", "328118447 NaN POLYGON ((18.4709153 -33.5059178, 18.4709287 -... \n", "\n", " fill_color \n", "328118446 [255, 255, 204] \n", "328118447 [225, 225, 51] " ] }, "execution_count": 40, "metadata": {}, "output_type": "execute_result" } ], "source": [ "#- look\n", "gdf.head(2)" ] }, { "cell_type": "code", "execution_count": 41, "id": "e25a305f-cc16-44b7-bc9c-13a7cc67d0d6", "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", " \n", " " ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "#- create pseudo-3D viz\n", "\n", "file = './result/interactiveAlt.html' # will name and save html here\n", "\n", "html = city3D.create_maplibre_3Dviz(\n", " result_dir = file, \n", " buildings_gdf = gdf,\n", " center = xy\n", ")\n", "\n", "city3D.show_interactive_html(html)" ] }, { "cell_type": "markdown", "id": "7addad28", "metadata": {}, "source": [ "**on a laptop without a mouse:**\n", "\n", "- `trackpad left-click drag-left` and `-right`;\n", "- `Ctrl left-click drag-up`, `-down`, `-left` and `-right` to rotate and so-on and\n", "- `+` next to Backspace zoom-in and `-` next to `+` zoom-out.\n", "\n", "**Now you do your community.** ~ If your area needs [OpenStreetMap](https://en.wikipedia.org/wiki/OpenStreetMap) data and you want to contribute please follow the [Guide](https://wiki.openstreetmap.org/wiki/Beginners%27_guide)." ] }, { "cell_type": "markdown", "id": "46520c64-812d-43d8-8733-a2c2a8a5f724", "metadata": {}, "source": [ "" ] }, { "cell_type": "markdown", "id": "3d476cd9-7749-4d8c-836c-f990ad292f09", "metadata": {}, "source": [ "
\n", " \n", "**To understand the performance in an [Urban setting](https://en.wikipedia.org/wiki/Urban_area) change `cell [2]` above:**\n", "\n", "**jparams** = uEstate (['University Estate'](https://en.wikipedia.org/wiki/University_Estate)) or sRiver (['Salt River'](https://en.wikipedia.org/wiki/Salt_River,_Cape_Town)) or obs (['Observatory'](https://en.wikipedia.org/wiki/Observatory,_Cape_Town)) *(with residents per formal house = 4 | 5 in Salt River and residents per informal structure = 3)* and cput ('Cape Peninsula University of Technology (Bellville Campus)') \n", "
" ] }, { "cell_type": "markdown", "id": "b3d0d0d0-8712-4f5a-a992-5e6d7a9e741d", "metadata": {}, "source": [ "## 4. Possible Secondary and Tertiary level *conversations starters*:\n", "\n", "
communicate and exchange ideas and understanding
" ] }, { "cell_type": "markdown", "id": "e82860fd-6821-44b8-b3e3-1d5ddddc94c8", "metadata": {}, "source": [ "| **Topic** | **Secondary Level Questions** | **Tertiary Level Questions** |\n", "|------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n", "| **Basic Understanding and Observations** | - What types of buildings are most common in the area (houses, apartments, retail, etc.)?
- Can you identify any patterns in the distribution of different types of buildings (e.g., are retail stores concentrated in certain areas)? | - How does the building stock composition (e.g., ratio of houses) correlate with the population? *demographics (e.g., age distribution, household size) for the area will strengthen the analysis!*
- Analyze the relationship between building density and population. What urban planning theories can explain this relationship? |\n", "| **Spatial Relationships and Impacts** | - How does the location of residential areas compare to the location of retail and commercial areas?
- What impact might the density and distribution of buildings have on local traffic and transportation?
- How might the population distribution affect the demand for local services such as schools, hospitals, and parks? | - Evaluate the accessibility of essential services (e.g., healthcare, education) in relation to the population and building types.
- Assess the potential social and economic impacts of a proposed new residential or commercial development in the area. |\n", "| **Socioeconomic and Environmental Considerations** | - Are there any correlations between the types of housing available and the household size? *additional demographics (e.g., income level) for the area will strengthen the analysis!*
- How might the current building stock and population influence the local economy? *demographics (e.g., age distribution, household size) for the area will strengthen the analysis!*
- What are some potential environmental impacts of the current building distribution, such as green space availability or pollution levels? | - How does the current building stock support or hinder sustainable development goals (e.g., energy efficiency, reduced carbon footprint)?
- What strategies could be implemented to increase the resilience of the community to environmental or economic changes? |\n", "| **Future Planning and Development** | - Based on the current building stock and population metrics, what areas might benefit from additional housing or commercial development?
- How could urban planners use this information to improve the quality of life in the area?
- What changes would you recommend to better balance residential, commercial, and recreational spaces? | - How might different zoning regulations impact the distribution of residential, commercial, and industrial buildings in the future?
- Propose urban design solutions that could improve the sustainability and livability of the area, considering both current metrics and future projections. |\n", "| **Quantitative and Qualitative Research** | |- Design a research study to investigate the impact of building type diversity on community wellbeing. What methodologies would you use?
- Analyze historical data to understand trends in building development and population growth. How have these trends shaped the current urban landscape?
- Conduct a SWOT analysis (Strengths, Weaknesses, Opportunities, Threats) of the area based on the building stock and population metrics. |" ] }, { "cell_type": "markdown", "id": "7e1f3216-30fd-425c-add9-3291969387f6", "metadata": {}, "source": [ "***\n", "\n", "**Now you do your community.** ~ If your area needs [OpenStreetMap](https://en.wikipedia.org/wiki/OpenStreetMap) data and you want to contribute please follow the [Guide](https://wiki.openstreetmap.org/wiki/Beginners%27_guide). " ] } ], "metadata": { "colab": { "provenance": [] }, "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.9.23" } }, "nbformat": 4, "nbformat_minor": 5 }