"""
This file is part of CLIMADA.
Copyright (C) 2017 ETH Zurich, CLIMADA contributors listed in AUTHORS.
CLIMADA is free software: you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free
Software Foundation, version 3.
CLIMADA is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with CLIMADA. If not, see <https://www.gnu.org/licenses/>.
---
Define the SupplyChain class.
"""
__all__ = ['SupplyChain']
import logging
import datetime as dt
from pathlib import Path
import zipfile
from tqdm import tqdm
import numpy as np
import pandas as pd
from climada import CONFIG
from climada.util import files_handler as u_fh
import climada.util.coordinates as u_coord
from climada.engine import Impact
from climada.entity.exposures.base import Exposures
LOGGER = logging.getLogger(__name__)
WIOD_FILE_LINK = CONFIG.engine.supplychain.resources.wiod16.str()
"""Link to the 2016 release of the WIOD tables."""
WIOD_DIRECTORY = CONFIG.engine.supplychain.local_data.wiod.dir()
"""Directory where WIOD tables are downloaded into."""
[docs]class SupplyChain():
"""SupplyChain class.
The SupplyChain class provides methods for loading Multi-Regional Input-Output
Tables (MRIOT) and computing direct, indirect and total impacts.
Attributes
----------
mriot_data : np.array
The input-output table data.
mriot_reg_names : np.array
Names of regions considered in the input-output table.
sectors : np.array
Sectors considered in the input-output table.
total_prod : np.array
Countries' total production.
mriot_type : str
Type of the adopted input-output table.
reg_pos : dict
Regions' positions within the input-output table and impact arrays.
reg_dir_imp : list
Regions undergoing direct impacts.
years : np.array
Years of the considered hazard events for which impact is calculated.
direct_impact : np.array
Direct impact array.
direct_aai_agg : np.array
Average annual direct impact array.
indirect_impact : np.array
Indirect impact array.
indirect_aai_agg : np.array
Average annual indirect impact array.
total_impact : np.array
Total impact array.
total_aai_agg : np.array
Average annual total impact array.
io_data : dict
Dictionary with the coefficients, inverse and risk_structure matrixes and
the selected input-output modeling approach.
"""
[docs] def __init__(self):
"""Initialize SupplyChain."""
self.mriot_data = np.array([], dtype='f')
self.mriot_reg_names = np.array([], dtype='str')
self.sectors = np.array([], dtype='str')
self.total_prod = np.array([], dtype='f')
self.mriot_type = ''
self.reg_pos = {}
self.years = np.array([], dtype='f')
self.direct_impact = np.array([], dtype='f')
self.direct_aai_agg = np.array([], dtype='f')
self.indirect_impact = np.array([], dtype='f')
self.indirect_aai_agg = np.array([], dtype='f')
self.total_impact = np.array([], dtype='f')
self.total_aai_agg = np.array([], dtype='f')
self.io_data = {}
[docs] def read_wiod16(self, year=2014, range_rows=(5,2469),
range_cols=(4,2468), col_iso3=2,
col_sectors=1):
"""Read multi-regional input-output tables of the 2016 release of the
WIOD project: http://www.wiod.org/database/wiots16
Parameters
----------
year : int
Year of WIOD table to use. Valid years go from 2000 to 2014.
Default year is 2014.
range_rows : tuple
initial and end positions of data along rows. Default is (5,2469).
range_cols : tuple
initial and end positions of data along columns. Default is (4,2468).
col_iso3 : int
column with countries names in ISO3 codes. Default is 2.
col_sectors : int
column with sector names. Default is 1.
References
----------
[1] Timmer, M. P., Dietzenbacher, E., Los, B., Stehrer, R. and de Vries, G. J.
(2015), "An Illustrated User Guide to the World Input–Output Database: the Case
of Global Automotive Production", Review of International Economics., 23: 575–605
"""
file_name = 'WIOT{}_Nov16_ROW.xlsb'.format(year)
file_loc = WIOD_DIRECTORY / file_name
if not file_loc.exists():
if not WIOD_DIRECTORY.exists():
WIOD_DIRECTORY.mkdir()
LOGGER.info('Downloading folder with WIOD tables')
downloaded_file_name = u_fh.download_file(WIOD_FILE_LINK,
download_dir=WIOD_DIRECTORY)
downloaded_file_zip_path = Path(downloaded_file_name + '.zip')
Path(downloaded_file_name).rename(downloaded_file_zip_path)
with zipfile.ZipFile(downloaded_file_zip_path, 'r') as zip_ref:
zip_ref.extractall(WIOD_DIRECTORY)
mriot = pd.read_excel(file_loc, engine='pyxlsb')
start_row, end_row = range_rows
start_col, end_col = range_cols
self.sectors = mriot.iloc[start_row:end_row, col_sectors].unique()
self.mriot_reg_names = mriot.iloc[start_row:end_row, col_iso3].unique()
self.mriot_data = mriot.iloc[start_row:end_row,
start_col:end_col].values
self.total_prod = mriot.iloc[start_row:end_row, -1].values
self.reg_pos = {
name: range(len(self.sectors)*i, len(self.sectors)*(i+1))
for i, name in enumerate(self.mriot_reg_names)
}
self.mriot_type = 'WIOD'
[docs] def calc_sector_direct_impact(self, hazard, exposure, imp_fun_set,
selected_subsec="service"):
"""Calculate direct impacts.
Parameters
----------
hazard : Hazard
Hazard object for impact calculation.
exposure : Exposures
Exposures object for impact calculation. For WIOD tables, exposure.region_id
must be country names following ISO3 codes.
imp_fun_set : ImpactFuncSet
Set of impact functions.
selected_subsec : str or list
Positions of the selected sectors. These positions can be either
defined by the user by passing a list of values, or by using built-in
sectors' aggregations for the WIOD data passing a string with possible
values being "service", "manufacturing", "agriculture" or "mining".
Default is "service".
"""
if isinstance(selected_subsec, str):
built_in_subsec_pos = {'service': range(26, 56),
'manufacturing': range(4, 23),
'agriculture': range(0, 1),
'mining': range(3, 4)}
selected_subsec = built_in_subsec_pos[selected_subsec]
dates = [
dt.datetime.strptime(date, "%Y-%m-%d")
for date in hazard.get_event_date()
]
self.years = np.unique([date.year for date in dates])
unique_exp_regid = exposure.gdf.region_id.unique()
self.direct_impact = np.zeros(shape=(len(self.years),
len(self.mriot_reg_names)*len(self.sectors)))
self.reg_dir_imp = []
for exp_regid in unique_exp_regid:
reg_exp = Exposures(exposure.gdf[exposure.gdf.region_id == exp_regid])
reg_exp.check()
# Normalize exposure
total_reg_value = reg_exp.gdf['value'].sum()
reg_exp.gdf['value'] /= total_reg_value
# Calc impact for country
imp = Impact()
imp.calc(reg_exp, imp_fun_set, hazard)
imp_year_set = np.array(list(imp.calc_impact_year_set(imp).values()))
mriot_reg_name = self._map_exp_to_mriot(exp_regid, self.mriot_type)
self.reg_dir_imp.append(mriot_reg_name)
subsec_reg_pos = np.array(selected_subsec) + self.reg_pos[mriot_reg_name][0]
subsec_reg_prod = self.mriot_data[subsec_reg_pos].sum(axis=1)
imp_year_set = np.repeat(imp_year_set, len(selected_subsec)
).reshape(len(self.years),
len(selected_subsec))
direct_impact_reg = np.multiply(imp_year_set, subsec_reg_prod)
# Sum needed below in case of many ROWs, which are aggregated into
# one country as per WIOD table.
self.direct_impact[:, subsec_reg_pos] += direct_impact_reg.astype(np.float32)
# average impact across years
self.direct_aai_agg = self.direct_impact.mean(axis=0)
[docs] def calc_indirect_impact(self, io_approach='ghosh'):
"""Calculate indirect impacts according to the specified input-output
appraoch. This function needs to be run after calc_sector_direct_impact.
Parameters
----------
io_approach : str
The adopted input-output modeling approach. Possible approaches
are 'leontief', 'ghosh' and 'eeioa'. Default is 'gosh'.
References
----------
[1] W. W. Leontief, Output, employment, consumption, and investment,
The Quarterly Journal of Economics 58, 1944.
[2] Ghosh, A., Input-Output Approach in an Allocation System,
Economica, New Series, 25, no. 97: 58-64. doi:10.2307/2550694, 1958.
[3] Kitzes, J., An Introduction to Environmentally-Extended Input-Output
Analysis, Resources, 2, 489-503; doi:10.3390/resources2040489, 2013.
"""
io_switch = {'leontief': self._leontief_calc, 'ghosh': self._ghosh_calc,
'eeioa': self._eeioa_calc}
# Compute coefficients based on selected IO approach
coefficients = np.zeros_like(self.mriot_data, dtype=np.float32)
if io_approach in ['leontief', 'eeioa']:
for col_i, col in enumerate(self.mriot_data.T):
if self.total_prod[col_i] > 0:
coefficients[:, col_i] = np.divide(col, self.total_prod[col_i])
else:
coefficients[:, col_i] = 0
else:
for row_i, row in enumerate(self.mriot_data):
if self.total_prod[row_i] > 0:
coefficients[row_i, :] = np.divide(row, self.total_prod[row_i])
else:
coefficients[row_i, :] = 0
inverse = np.linalg.inv(np.identity(len(self.mriot_data)) - coefficients)
inverse = inverse.astype(np.float32)
# Calculate indirect impacts
self.indirect_impact = np.zeros_like(self.direct_impact, dtype=np.float32)
risk_structure = np.zeros(np.shape(self.mriot_data) + (len(self.years),),
dtype=np.float32)
# Loop over years indices:
for year_i, _ in enumerate(tqdm(self.years)):
direct_impact_yearly = self.direct_impact[year_i, :]
direct_intensity = np.zeros_like(direct_impact_yearly)
for idx, (impact, production) in enumerate(zip(direct_impact_yearly,
self.total_prod)):
if production > 0:
direct_intensity[idx] = impact/production
else:
direct_intensity[idx] = 0
# Calculate risk structure based on selected IO approach
risk_structure = io_switch[io_approach](direct_intensity, inverse,
risk_structure, year_i)
# Total indirect risk per sector/country-combination:
self.indirect_impact[year_i, :] = np.nansum(
risk_structure[:, :, year_i], axis=0)
self.indirect_aai_agg = self.indirect_impact.mean(axis=0)
self.io_data = {}
self.io_data.update({'coefficients': coefficients, 'inverse': inverse,
'risk_structure' : risk_structure,
'io_approach' : io_approach})
[docs] def calc_total_impact(self):
"""Calculate total impacts summing direct and indirect impacts."""
self.total_impact = self.indirect_impact + self.direct_impact
self.total_aai_agg = self.total_impact.mean(axis=0)
def _map_exp_to_mriot(self, exp_regid, mriot_type):
"""
Map regions names in exposure into Input-output regions names.
exp_regid must be according to ISO 3166 numeric country codes.
"""
if mriot_type == 'WIOD':
mriot_reg_name = u_coord.country_to_iso(exp_regid, "alpha3")
idx_country = np.where(self.mriot_reg_names == mriot_reg_name)[0]
if not idx_country.size > 0.:
mriot_reg_name = 'ROW'
elif mriot_type == '':
mriot_reg_name = exp_regid
return mriot_reg_name
def _leontief_calc(self, direct_intensity, inverse, risk_structure, year_i):
"""Calculate the risk_structure based on the Leontief approach."""
demand = self.total_prod - np.nansum(self.mriot_data, axis=1)
degr_demand = direct_intensity*demand
for idx, row in enumerate(inverse):
risk_structure[:, idx, year_i] = row * degr_demand
return risk_structure
def _ghosh_calc(self, direct_intensity, inverse, risk_structure, year_i):
"""Calculate the risk_structure based on the Ghosh approach."""
value_added = self.total_prod - np.nansum(self.mriot_data, axis=0)
degr_value_added = np.maximum(direct_intensity*value_added,\
np.zeros_like(value_added))
for idx, col in enumerate(inverse.T):
# Here, we iterate across columns of inverse (hence transpose used).
risk_structure[:, idx, year_i] = degr_value_added * col
return risk_structure
def _eeioa_calc(self, direct_intensity, inverse, risk_structure, year_i):
"""Calculate the risk_structure based on the EEIOA approach."""
for idx, col in enumerate(inverse.T):
risk_structure[:, idx, year_i] = (direct_intensity * col) * self.total_prod[idx]
return risk_structure