sbti-finance-tool 1.0.9__tar.gz → 1.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of sbti-finance-tool might be problematic. Click here for more details.

Files changed (31) hide show
  1. {sbti-finance-tool-1.0.9 → sbti_finance_tool-1.1.0}/LICENSE +1 -1
  2. {sbti-finance-tool-1.0.9 → sbti_finance_tool-1.1.0}/PKG-INFO +18 -6
  3. {sbti-finance-tool-1.0.9 → sbti_finance_tool-1.1.0}/README.md +10 -1
  4. {sbti-finance-tool-1.0.9 → sbti_finance_tool-1.1.0}/SBTi/configs.py +5 -2
  5. sbti_finance_tool-1.1.0/SBTi/data/sbti.py +332 -0
  6. sbti_finance_tool-1.1.0/SBTi/inputs/current-Companies-Taking-Action.xlsx +0 -0
  7. {sbti-finance-tool-1.0.9 → sbti_finance_tool-1.1.0}/SBTi/utils.py +53 -22
  8. {sbti-finance-tool-1.0.9 → sbti_finance_tool-1.1.0}/pyproject.toml +2 -2
  9. sbti-finance-tool-1.0.9/SBTi/data/sbti.py +0 -115
  10. sbti-finance-tool-1.0.9/SBTi/inputs/current-Companies-Taking-Action-191.xlsx +0 -0
  11. sbti-finance-tool-1.0.9/SBTi/inputs/current-Companies-Taking-Action.xlsx +0 -0
  12. sbti-finance-tool-1.0.9/setup.py +0 -36
  13. {sbti-finance-tool-1.0.9 → sbti_finance_tool-1.1.0}/SBTi/.DS_Store +0 -0
  14. {sbti-finance-tool-1.0.9 → sbti_finance_tool-1.1.0}/SBTi/__init__.py +0 -0
  15. {sbti-finance-tool-1.0.9 → sbti_finance_tool-1.1.0}/SBTi/data/__init__.py +0 -0
  16. {sbti-finance-tool-1.0.9 → sbti_finance_tool-1.1.0}/SBTi/data/bloomberg.py +0 -0
  17. {sbti-finance-tool-1.0.9 → sbti_finance_tool-1.1.0}/SBTi/data/cdp.py +0 -0
  18. {sbti-finance-tool-1.0.9 → sbti_finance_tool-1.1.0}/SBTi/data/csv.py +0 -0
  19. {sbti-finance-tool-1.0.9 → sbti_finance_tool-1.1.0}/SBTi/data/data_provider.py +0 -0
  20. {sbti-finance-tool-1.0.9 → sbti_finance_tool-1.1.0}/SBTi/data/excel.py +0 -0
  21. {sbti-finance-tool-1.0.9 → sbti_finance_tool-1.1.0}/SBTi/data/iss.py +0 -0
  22. {sbti-finance-tool-1.0.9 → sbti_finance_tool-1.1.0}/SBTi/data/msci.py +0 -0
  23. {sbti-finance-tool-1.0.9 → sbti_finance_tool-1.1.0}/SBTi/data/trucost.py +0 -0
  24. {sbti-finance-tool-1.0.9 → sbti_finance_tool-1.1.0}/SBTi/data/urgentem.py +0 -0
  25. {sbti-finance-tool-1.0.9 → sbti_finance_tool-1.1.0}/SBTi/inputs/regression_model_summary.xlsx +0 -0
  26. {sbti-finance-tool-1.0.9 → sbti_finance_tool-1.1.0}/SBTi/inputs/sr15_mapping.xlsx +0 -0
  27. {sbti-finance-tool-1.0.9 → sbti_finance_tool-1.1.0}/SBTi/interfaces.py +0 -0
  28. {sbti-finance-tool-1.0.9 → sbti_finance_tool-1.1.0}/SBTi/portfolio_aggregation.py +0 -0
  29. {sbti-finance-tool-1.0.9 → sbti_finance_tool-1.1.0}/SBTi/portfolio_coverage_tvp.py +0 -0
  30. {sbti-finance-tool-1.0.9 → sbti_finance_tool-1.1.0}/SBTi/target_validation.py +0 -0
  31. {sbti-finance-tool-1.0.9 → sbti_finance_tool-1.1.0}/SBTi/temperature_score.py +0 -0
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2020 World Resources Institute
3
+ Copyright (c) 2025 World Resources Institute; Science Based Targets initiative
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -1,11 +1,11 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: sbti-finance-tool
3
- Version: 1.0.9
3
+ Version: 1.1.0
4
4
  Summary: This package helps companies and financial institutions to assess the temperature alignment of current targets, commitments, and investment and lending portfolios, and to use this information to develop targets for official validation by the SBTi.'
5
5
  License: MIT
6
6
  Keywords: Climate,SBTi,Finance
7
7
  Author: sbti
8
- Author-email: finance@sciencebasedtargets.org
8
+ Author-email: financialinstitutions@sciencebasedtargets.org
9
9
  Requires-Python: >=3.9,<4.0
10
10
  Classifier: Intended Audience :: Developers
11
11
  Classifier: Intended Audience :: Science/Research
@@ -15,10 +15,13 @@ Classifier: Operating System :: Microsoft :: Windows
15
15
  Classifier: Operating System :: Unix
16
16
  Classifier: Programming Language :: Python
17
17
  Classifier: Programming Language :: Python :: 3
18
- Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Programming Language :: Python :: 3.9
19
19
  Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Programming Language :: Python :: 3 :: Only
20
24
  Classifier: Programming Language :: Python :: 3.8
21
- Classifier: Programming Language :: Python :: 3.9
22
25
  Classifier: Topic :: Scientific/Engineering
23
26
  Classifier: Topic :: Software Development
24
27
  Requires-Dist: cleo (>=2.0.1,<3.0.0)
@@ -35,7 +38,7 @@ Description-Content-Type: text/markdown
35
38
 
36
39
  > Visit https://sciencebasedtargets.github.io/SBTi-finance-tool/ for the full documentation
37
40
 
38
- > If you have any additional questions or comments send a mail to: finance@sciencebasedtargets.org
41
+ > If you have any additional questions or comments send a mail to: financialinstitutions@sciencebasedtargets.org
39
42
 
40
43
  # SBTi Temperature Alignment tool
41
44
 
@@ -114,6 +117,15 @@ poetry install
114
117
 
115
118
  This will create a virtual environment inside the project folder under `.venv`.
116
119
 
120
+ ### SBTi Companies Taking Action (CTA) Data
121
+
122
+ The tool supports multiple formats of the SBTi CTA file:
123
+ - **Per-company format** (default, recommended): One row per company with aggregated target status
124
+ - **Per-target format**: Multiple rows per company with detailed target information
125
+ - **Legacy format**: Original Title Case column format
126
+
127
+ The tool automatically detects and handles all formats, defaulting to the per-company format for consistency.
128
+
117
129
  ### Testing
118
130
 
119
131
  Each class should be unit tested. The unit tests are written using the Nose2 framework.
@@ -1,6 +1,6 @@
1
1
  > Visit https://sciencebasedtargets.github.io/SBTi-finance-tool/ for the full documentation
2
2
 
3
- > If you have any additional questions or comments send a mail to: finance@sciencebasedtargets.org
3
+ > If you have any additional questions or comments send a mail to: financialinstitutions@sciencebasedtargets.org
4
4
 
5
5
  # SBTi Temperature Alignment tool
6
6
 
@@ -79,6 +79,15 @@ poetry install
79
79
 
80
80
  This will create a virtual environment inside the project folder under `.venv`.
81
81
 
82
+ ### SBTi Companies Taking Action (CTA) Data
83
+
84
+ The tool supports multiple formats of the SBTi CTA file:
85
+ - **Per-company format** (default, recommended): One row per company with aggregated target status
86
+ - **Per-target format**: Multiple rows per company with detailed target information
87
+ - **Legacy format**: Original Title Case column format
88
+
89
+ The tool automatically detects and handles all formats, defaulting to the per-company format for consistency.
90
+
82
91
  ### Testing
83
92
 
84
93
  Each class should be unit tested. The unit tests are written using the Nose2 framework.
@@ -162,7 +162,9 @@ class PortfolioCoverageTVPConfig(PortfolioAggregationConfig):
162
162
  "current-Companies-Taking-Action.xlsx",
163
163
  )
164
164
  # Temporary URL until the SBTi website is updated
165
- CTA_FILE_URL = "https://sciencebasedtargets.org/download/target-dashboard"
165
+ CTA_FILE_URL = "https://sciencebasedtargets.org/resources/files/companies-excel.xlsx" # Default to per-company
166
+ CTA_FILE_URL_PER_COMPANY = "https://files.sciencebasedtargets.org/production/files/companies-excel.xlsx"
167
+ CTA_FILE_URL_PER_TARGET = "https://sciencebasedtargets.org/resources/files/targets-excel.xlsx"
166
168
  OUTPUT_TARGET_STATUS = "sbti_target_status"
167
169
  OUTPUT_WEIGHTED_TARGET_STATUS = "weighted_sbti_target_status"
168
170
  VALUE_TARGET_NO = "No target"
@@ -181,4 +183,5 @@ class PortfolioCoverageTVPConfig(PortfolioAggregationConfig):
181
183
  COL_COMPANY_ISIN = "ISIN"
182
184
  COL_COMPANY_LEI = "LEI"
183
185
  COL_ACTION = "Action"
184
- COL_TARGET = "Target"
186
+ COL_TARGET = "Target"
187
+ COL_DATE_PUBLISHED = "Date Published"
@@ -0,0 +1,332 @@
1
+ from typing import List, Type, Dict, Tuple, Optional
2
+ import requests
3
+ import pandas as pd
4
+ import warnings
5
+ import datetime
6
+ import re
7
+
8
+ from SBTi.configs import PortfolioCoverageTVPConfig
9
+ from SBTi.interfaces import IDataProviderCompany, IDataProviderTarget, EScope, ETimeFrames
10
+
11
+
12
+ class SBTi:
13
+ """
14
+ Data provider skeleton for SBTi. This class only provides the sbti_validated field for existing companies.
15
+ Updated to DEFAULT to per-company format for TR Testing consistency.
16
+ Enhanced to extract detailed target information when available.
17
+ """
18
+
19
+ def __init__(
20
+ self, config: Type[PortfolioCoverageTVPConfig] = PortfolioCoverageTVPConfig
21
+ ):
22
+ self.c = config
23
+
24
+ # DEFAULT TO PER-COMPANY FORMAT for consistency with TR Testing baseline
25
+ # Override the config to ensure per-company format is used
26
+ original_url = self.c.CTA_FILE_URL
27
+ self.c.CTA_FILE_URL = "https://files.sciencebasedtargets.org/production/files/companies-excel.xlsx"
28
+
29
+
30
+ fallback_err_log_statement = 'Will read older file from this package version'
31
+ try:
32
+ # Fetch CTA file from SBTi website
33
+ resp = requests.get(self.c.CTA_FILE_URL)
34
+
35
+ # If status code == 200 then write CTA file to disk
36
+ if resp.ok:
37
+ with open(self.c.FILE_TARGETS, 'wb') as output:
38
+ output.write(resp.content)
39
+ print(f'Status code from fetching the CTA file: {resp.status_code}, 200 = OK')
40
+ else:
41
+ print(f'Non-200 status code when fetching the CTA file from the SBTi website: {resp.status_code}')
42
+ print(fallback_err_log_statement)
43
+
44
+ except requests.exceptions.RequestException as e:
45
+ print(f'Exception when fetching the CTA file from the SBTi website: {e}')
46
+ print(fallback_err_log_statement)
47
+
48
+ # Read CTA file into pandas dataframe
49
+ # Suppress warning about openpyxl
50
+ warnings.filterwarnings('ignore', category=UserWarning, module='openpyxl')
51
+ self.targets = pd.read_excel(self.c.FILE_TARGETS)
52
+
53
+ # Detect and convert column format if needed
54
+ self.targets = self._ensure_compatible_format(self.targets)
55
+
56
+ print(f"CTA format: {getattr(self, 'format_type', 'unknown')} | Companies: {len(self.targets)}")
57
+
58
+ def _detect_format(self, df):
59
+ """
60
+ Detect the format of the CTA file:
61
+ - 'old': Original format with Title Case columns
62
+ - 'new_company': New per-company format (one row per company) - PREFERRED
63
+ - 'new_target': New per-target format (multiple rows per company)
64
+ """
65
+ # Check for key columns to determine format
66
+ if 'Company Name' in df.columns:
67
+ return 'old'
68
+ elif 'company_name' in df.columns:
69
+ # Distinguish between per-company and per-target formats
70
+ if 'near_term_status' in df.columns:
71
+ return 'new_company' # PREFERRED FORMAT
72
+ elif 'target_wording' in df.columns or 'row_entry_id' in df.columns:
73
+ return 'new_target'
74
+ else:
75
+ # Default to per-company if we can't determine
76
+ return 'new_company'
77
+ else:
78
+ raise ValueError("Unrecognized CTA file format")
79
+
80
+ def _ensure_compatible_format(self, df):
81
+ """
82
+ Convert CTA file to the format expected by the configs
83
+ OPTIMIZED FOR PER-COMPANY FORMAT
84
+ """
85
+ format_type = self._detect_format(df)
86
+
87
+ if format_type in ['new_target', 'new_company']:
88
+ # Map new format columns to old format for backward compatibility
89
+ column_mapping = {
90
+ 'company_name': self.c.COL_COMPANY_NAME,
91
+ 'isin': self.c.COL_COMPANY_ISIN,
92
+ 'lei': self.c.COL_COMPANY_LEI,
93
+ 'action': self.c.COL_ACTION,
94
+ 'target': self.c.COL_TARGET,
95
+ 'date_published': self.c.COL_DATE_PUBLISHED
96
+ }
97
+
98
+ # Handle different format types
99
+ if format_type == 'new_company':
100
+ # Map near_term_status to create synthetic Action and Target columns
101
+ if 'near_term_status' in df.columns:
102
+ df['action'] = df['near_term_status'].apply(
103
+ lambda x: 'Target' if x == 'Targets set' else 'Commitment'
104
+ )
105
+ df['target'] = df['near_term_status'].apply(
106
+ lambda x: 'Near-term' if x == 'Targets set' else None
107
+ )
108
+
109
+ # Only rename columns that exist
110
+ rename_dict = {}
111
+ for new_col, old_col in column_mapping.items():
112
+ if new_col in df.columns:
113
+ rename_dict[new_col] = old_col
114
+
115
+ df = df.rename(columns=rename_dict)
116
+
117
+ # Store format type for later use
118
+ self.format_type = format_type
119
+
120
+ # Keep additional columns for potential future use
121
+ if 'sbti_id' in df.columns:
122
+ df['SBTI_ID'] = df['sbti_id']
123
+ if 'target_classification_short' in df.columns:
124
+ df['Target Classification'] = df['target_classification_short']
125
+ if 'scope' in df.columns:
126
+ df['Scope'] = df['scope']
127
+ if 'base_year' in df.columns:
128
+ df['Base Year'] = df['base_year']
129
+ if 'target_year' in df.columns:
130
+ df['Target Year'] = df['target_year']
131
+ else:
132
+ self.format_type = 'old'
133
+
134
+ return df
135
+
136
+ def filter_cta_file(self, targets):
137
+ """
138
+ Filter the CTA file to create a dataframe that has one row per company
139
+ with the columns "Action" and "Target".
140
+ If Action = Target then only keep the rows where Target = Near-term.
141
+
142
+ Handles all three formats: old, new per-company, and new per-target.
143
+ """
144
+
145
+ # Create a new dataframe with only the columns "Action" and "Target"
146
+ # and the columns that are needed for identifying the company
147
+ required_cols = [
148
+ self.c.COL_COMPANY_NAME,
149
+ self.c.COL_COMPANY_ISIN,
150
+ self.c.COL_COMPANY_LEI,
151
+ self.c.COL_ACTION,
152
+ self.c.COL_TARGET
153
+ ]
154
+
155
+ # Only select columns that exist
156
+ existing_cols = [col for col in required_cols if col in targets.columns]
157
+ targets_filtered = targets[existing_cols].copy()
158
+
159
+ # Keep rows where Action = Target and Target = Near-term
160
+ df_nt_targets = targets_filtered[
161
+ (targets_filtered[self.c.COL_ACTION] == self.c.VALUE_ACTION_TARGET) &
162
+ (targets_filtered[self.c.COL_TARGET] == self.c.VALUE_TARGET_SET)]
163
+
164
+ # For per-target format, we need to deduplicate at company level
165
+ # since there can be multiple target rows per company
166
+ if hasattr(self, 'format_type') and self.format_type == 'new_target':
167
+ print("Processing per-target format - deduplicating companies...")
168
+
169
+ # Drop duplicates in the dataframe by waterfall approach
170
+ # LEI first, then ISIN, then company name
171
+ identifier_cols = [self.c.COL_COMPANY_LEI, self.c.COL_COMPANY_ISIN, self.c.COL_COMPANY_NAME]
172
+
173
+ for identifier_col in identifier_cols:
174
+ if identifier_col in df_nt_targets.columns:
175
+ # Drop duplicates based on this identifier, keeping first occurrence
176
+ before_count = len(df_nt_targets)
177
+ df_nt_targets = df_nt_targets.drop_duplicates(subset=[identifier_col], keep='first')
178
+ after_count = len(df_nt_targets)
179
+ if before_count != after_count and hasattr(self, 'format_type') and self.format_type == 'new_target':
180
+ print(f" Deduplicated {before_count - after_count} entries using {identifier_col}")
181
+
182
+ return df_nt_targets
183
+
184
+ def get_company_targets(self, company_name: str = None, isin: str = None, lei: str = None):
185
+ """
186
+ Get all targets for a specific company.
187
+ Only works with per-target format.
188
+
189
+ :param company_name: Company name to search for
190
+ :param isin: ISIN to search for
191
+ :param lei: LEI to search for
192
+ :return: DataFrame with all targets for the company
193
+ """
194
+ if hasattr(self, 'format_type') and self.format_type == 'new_target':
195
+ if lei:
196
+ return self.targets[self.targets['lei'] == lei]
197
+ elif isin:
198
+ return self.targets[self.targets['isin'] == isin]
199
+ elif company_name:
200
+ return self.targets[self.targets[self.c.COL_COMPANY_NAME] == company_name]
201
+ return pd.DataFrame() # Empty dataframe if not per-target format
202
+
203
+ def get_companies(
204
+ self, companies: List[IDataProviderCompany], id_map: Dict[str, Tuple[str, str]]
205
+ ) -> List[IDataProviderCompany]:
206
+ """
207
+ Get company data from SBTi database and add sbti_validated field.
208
+
209
+ :param companies: A list of IDataProviderCompany instances
210
+ :param id_map: A map from company id to a tuple of (ISIN, LEI)
211
+ :return: A list of IDataProviderCompany instances, supplemented with the SBTi information
212
+ """
213
+ # Filter targets for validation check
214
+ filtered_targets = self.filter_cta_file(self.targets)
215
+
216
+ # Track matching statistics for debugging
217
+ matched_lei = matched_isin = matched_name = 0
218
+
219
+ for company in companies:
220
+ isin, lei = id_map.get(company.company_id, (None, None))
221
+
222
+ # Skip if no identifiers
223
+ if not isin and not lei and not company.company_name:
224
+ continue
225
+
226
+ # Check lei and length of lei to avoid zeros
227
+ if lei and not lei.lower() == 'nan' and len(str(lei)) > 3:
228
+ targets = filtered_targets[
229
+ filtered_targets[self.c.COL_COMPANY_LEI] == lei
230
+ ]
231
+ if len(targets) > 0:
232
+ company.sbti_validated = True
233
+ matched_lei += 1
234
+ continue
235
+
236
+ if isin and not isin.lower() == 'nan':
237
+ targets = filtered_targets[
238
+ filtered_targets[self.c.COL_COMPANY_ISIN] == isin
239
+ ]
240
+ if len(targets) > 0:
241
+ company.sbti_validated = True
242
+ matched_isin += 1
243
+ continue
244
+
245
+ # Try company name matching as fallback
246
+ if company.company_name:
247
+ targets = filtered_targets[
248
+ filtered_targets[self.c.COL_COMPANY_NAME].str.lower() == company.company_name.lower()
249
+ ]
250
+ if len(targets) > 0:
251
+ company.sbti_validated = True
252
+ matched_name += 1
253
+ continue
254
+
255
+ # No match found
256
+ company.sbti_validated = False
257
+
258
+ total_matched = matched_lei + matched_isin + matched_name
259
+ print(f"SBTi matches: {total_matched}/{len(companies)} (LEI: {matched_lei}, ISIN: {matched_isin}, Name: {matched_name})")
260
+
261
+ return companies
262
+
263
+ def get_sbti_targets(
264
+ self, companies: List[IDataProviderCompany], id_map: Dict[str, Tuple[str, str]]
265
+ ) -> Tuple[List[IDataProviderCompany], Dict[str, List[IDataProviderTarget]]]:
266
+ """
267
+ Enhanced version that returns both validation status AND target details when available.
268
+
269
+ :param companies: A list of IDataProviderCompany instances
270
+ :param id_map: A map from company id to a tuple of (ISIN, LEI)
271
+ :return: A tuple of:
272
+ - List of IDataProviderCompany instances, supplemented with the SBTi information
273
+ - Dictionary mapping company_id to list of IDataProviderTarget instances
274
+ """
275
+ # Store original unfiltered targets for detailed extraction
276
+ original_targets = self.targets.copy()
277
+
278
+ # Filter out information about targets for validation check
279
+ self.targets = self.filter_cta_file(self.targets)
280
+
281
+ # Dictionary to store detailed target data
282
+ sbti_target_data = {}
283
+
284
+ for company in companies:
285
+ isin, lei = id_map.get(company.company_id, (None, None))
286
+
287
+ # Skip if no identifiers
288
+ if not isin and not lei:
289
+ continue
290
+
291
+ # Check lei and length of lei to avoid zeros
292
+ if lei and not lei.lower() == 'nan' and len(str(lei)) > 3:
293
+ targets = self.targets[
294
+ self.targets[self.c.COL_COMPANY_LEI] == lei
295
+ ]
296
+ # Get all targets for detailed extraction
297
+ if hasattr(self, 'format_type') and self.format_type == 'new_target':
298
+ all_company_targets = original_targets[
299
+ original_targets[self.c.COL_COMPANY_LEI if self.c.COL_COMPANY_LEI in original_targets.columns else 'lei'] == lei
300
+ ]
301
+ elif isin and not isin.lower() == 'nan':
302
+ targets = self.targets[
303
+ self.targets[self.c.COL_COMPANY_ISIN] == isin
304
+ ]
305
+ # Get all targets for detailed extraction
306
+ if hasattr(self, 'format_type') and self.format_type == 'new_target':
307
+ all_company_targets = original_targets[
308
+ original_targets[self.c.COL_COMPANY_ISIN if self.c.COL_COMPANY_ISIN in original_targets.columns else 'isin'] == isin
309
+ ]
310
+ else:
311
+ continue
312
+
313
+ if len(targets) > 0:
314
+ company.sbti_validated = True
315
+
316
+ # Extract detailed target information if available
317
+ if hasattr(self, 'format_type') and self.format_type == 'new_target' and len(all_company_targets) > 0:
318
+ targets_list = []
319
+ for _, target_row in all_company_targets.iterrows():
320
+ target_data = {
321
+ 'company_id': company.company_id,
322
+ 'target_type': target_row.get('target_classification_short', 'Unknown'),
323
+ 'scope': target_row.get('scope', 'Unknown'),
324
+ 'base_year': target_row.get('base_year'),
325
+ 'target_year': target_row.get('target_year'),
326
+ }
327
+ targets_list.append(target_data)
328
+ sbti_target_data[company.company_id] = targets_list
329
+ else:
330
+ company.sbti_validated = False
331
+
332
+ return companies, sbti_target_data
@@ -164,24 +164,6 @@ def _make_id_map(df_portfolio: pd.DataFrame) -> dict:
164
164
  }
165
165
 
166
166
 
167
- # def _make_isin_map(df_portfolio: pd.DataFrame) -> dict:
168
- # """
169
- # Create a mapping from company_id to ISIN (required for the SBTi matching).
170
-
171
- # :param df_portfolio: The complete portfolio
172
- # :return: A mapping from company_id to ISIN
173
- # """
174
- # return {
175
- # company_id: company[ColumnsConfig.COMPANY_ISIN]
176
- # for company_id, company in df_portfolio[
177
- # [ColumnsConfig.COMPANY_ID, ColumnsConfig.COMPANY_ISIN]
178
- # ]
179
- # .set_index(ColumnsConfig.COMPANY_ID)
180
- # .to_dict(orient="index")
181
- # .items()
182
- # }
183
-
184
-
185
167
  def dataframe_to_portfolio(df_portfolio: pd.DataFrame) -> List[PortfolioCompany]:
186
168
  """
187
169
  Convert a data frame to a list of portfolio company objects.
@@ -199,28 +181,77 @@ def dataframe_to_portfolio(df_portfolio: pd.DataFrame) -> List[PortfolioCompany]
199
181
  ]
200
182
 
201
183
 
184
+ def merge_target_data(
185
+ provider_targets: List[IDataProviderTarget],
186
+ sbti_targets: Dict[str, List[IDataProviderTarget]]
187
+ ) -> List[IDataProviderTarget]:
188
+ """
189
+ Merge targets from data providers with SBTi targets, preferring SBTi data for validated companies.
190
+
191
+ :param provider_targets: List of targets from data providers
192
+ :param sbti_targets: Dictionary mapping company_id to list of SBTi targets
193
+ :return: Merged list of targets
194
+ """
195
+ # Create lookup of provider targets by company
196
+ provider_by_company = {}
197
+ for target in provider_targets:
198
+ if target.company_id not in provider_by_company:
199
+ provider_by_company[target.company_id] = []
200
+ provider_by_company[target.company_id].append(target)
201
+
202
+ # Replace with SBTi targets where available
203
+ for company_id, sbti_company_targets in sbti_targets.items():
204
+ if sbti_company_targets: # Only replace if we have valid SBTi targets
205
+ provider_by_company[company_id] = sbti_company_targets
206
+ logging.getLogger(__name__).info(
207
+ f"Using {len(sbti_company_targets)} SBTi targets for company {company_id}"
208
+ )
209
+
210
+ # Flatten back to list
211
+ merged_targets = []
212
+ for company_targets in provider_by_company.values():
213
+ merged_targets.extend(company_targets)
214
+
215
+ return merged_targets
216
+
217
+
202
218
  def get_data(
203
219
  data_providers: List[data.DataProvider], portfolio: List[PortfolioCompany]
204
220
  ) -> pd.DataFrame:
205
221
  """
206
222
  Get the required data from the data provider(s), validate the targets and return a 9-box grid for each company.
223
+ Enhanced to use SBTi authoritative target data when available.
207
224
 
208
225
  :param data_providers: A list of DataProvider instances
209
226
  :param portfolio: A list of PortfolioCompany models
210
227
  :return: A data frame containing the relevant company-target data
211
228
  """
229
+ logger = logging.getLogger(__name__)
230
+
212
231
  df_portfolio = pd.DataFrame.from_records(
213
232
  [_flatten_user_fields(c) for c in portfolio]
214
233
  )
215
234
  company_data = get_company_data(data_providers, df_portfolio["company_id"].tolist())
216
235
  target_data = get_targets(data_providers, df_portfolio["company_id"].tolist())
217
236
 
237
+ # Supplement the company data with the SBTi target status and get detailed targets
238
+ sbti = SBTi()
239
+ company_data, sbti_targets = sbti.get_sbti_targets(company_data, _make_id_map(df_portfolio))
240
+
241
+ # Log information about SBTi targets found
242
+ if sbti_targets:
243
+ logger.info(f"Found SBTi targets for {len(sbti_targets)} companies")
244
+ for company_id, targets in sbti_targets.items():
245
+ logger.info(f"Company {company_id}: {len(targets)} SBTi targets")
246
+
247
+ # Merge SBTi targets with provider targets
248
+ if sbti_targets:
249
+ target_data = merge_target_data(target_data, sbti_targets)
250
+ logger.info(f"Total targets after merging: {len(target_data)}")
251
+
218
252
  if len(target_data) == 0:
219
253
  raise ValueError("No targets found")
220
254
 
221
- # Supplement the company data with the SBTi target status
222
- company_data = SBTi().get_sbti_targets(company_data, _make_id_map(df_portfolio))
223
-
224
255
  # Prepare the data
225
256
  portfolio_data = TargetProtocol().process(target_data, company_data)
226
257
  portfolio_data = pd.merge(
@@ -280,4 +311,4 @@ def calculate(
280
311
  if anonymize:
281
312
  scores = ts.anonymize_data_dump(scores)
282
313
 
283
- return scores, aggregations
314
+ return scores, aggregations
@@ -1,8 +1,8 @@
1
1
  [tool.poetry]
2
2
  name = "sbti-finance-tool"
3
- version = "1.0.9"
3
+ version = "1.1.0"
4
4
  description = "This package helps companies and financial institutions to assess the temperature alignment of current targets, commitments, and investment and lending portfolios, and to use this information to develop targets for official validation by the SBTi.'"
5
- authors = ["sbti <finance@sciencebasedtargets.org>"]
5
+ authors = ["sbti <financialinstitutions@sciencebasedtargets.org>"]
6
6
  license = "MIT"
7
7
  readme = "README.md"
8
8
  classifiers = [
@@ -1,115 +0,0 @@
1
- from typing import List, Type
2
- import requests
3
- import pandas as pd
4
- import warnings
5
-
6
-
7
- from SBTi.configs import PortfolioCoverageTVPConfig
8
- from SBTi.interfaces import IDataProviderCompany
9
-
10
-
11
- class SBTi:
12
- """
13
- Data provider skeleton for SBTi. This class only provides the sbti_validated field for existing companies.
14
- """
15
-
16
- def __init__(
17
- self, config: Type[PortfolioCoverageTVPConfig] = PortfolioCoverageTVPConfig
18
- ):
19
- self.c = config
20
- # Fetch CTA file from SBTi website
21
- resp = requests.get(self.c.CTA_FILE_URL)
22
- # If status code == 200 then Write CTA file to disk
23
- if resp.status_code == 200:
24
- with open(self.c.FILE_TARGETS, 'wb') as output:
25
- output.write(resp.content)
26
- print(f'Status code from fetching the CTA file: {resp.status_code}, 200 = OK')
27
- # Read CTA file into pandas dataframe
28
- # Suppress warning about openpyxl - check if this is still needed in the released version.
29
- else:
30
- print('Could not fetch the CTA file from the SBTi website')
31
- print('Will read older file from this package version')
32
-
33
- warnings.filterwarnings('ignore', category=UserWarning, module='openpyxl')
34
- self.targets = pd.read_excel(self.c.FILE_TARGETS)
35
-
36
-
37
- def filter_cta_file(self, targets):
38
- """
39
- Filter the CTA file to create a datafram that has on row per company
40
- with the columns "Action" and "Target".
41
- If Action = Target then only keep the rows where Target = Near-term.
42
- """
43
-
44
- # Create a new dataframe with only the columns "Action" and "Target"
45
- # and the columns that are needed for identifying the company
46
- targets = targets[
47
- [
48
- self.c.COL_COMPANY_NAME,
49
- self.c.COL_COMPANY_ISIN,
50
- self.c.COL_COMPANY_LEI,
51
- self.c.COL_ACTION,
52
- self.c.COL_TARGET
53
- ]
54
- ]
55
-
56
- # Keep rows where Action = Target and Target = Near-term
57
- df_nt_targets = targets[
58
- (targets[self.c.COL_ACTION] == self.c.VALUE_ACTION_TARGET) &
59
- (targets[self.c.COL_TARGET] == self.c.VALUE_TARGET_SET)]
60
-
61
- # Drop duplicates in the dataframe by waterfall.
62
- # Do company name last due to risk of misspelled names
63
- # First drop duplicates on LEI, then on ISIN, then on company name
64
- df_nt_targets = pd.concat([
65
- df_nt_targets[~df_nt_targets[self.c.COL_COMPANY_LEI].isnull()].drop_duplicates(
66
- subset=self.c.COL_COMPANY_LEI, keep='first'
67
- ),
68
- df_nt_targets[df_nt_targets[self.c.COL_COMPANY_LEI].isnull()]
69
- ])
70
-
71
- df_nt_targets = pd.concat([
72
- df_nt_targets[~df_nt_targets[self.c.COL_COMPANY_ISIN].isnull()].drop_duplicates(
73
- subset=self.c.COL_COMPANY_ISIN, keep='first'
74
- ),
75
- df_nt_targets[df_nt_targets[self.c.COL_COMPANY_ISIN].isnull()]
76
- ])
77
-
78
- df_nt_targets.drop_duplicates(subset=self.c.COL_COMPANY_NAME, inplace=True)
79
-
80
- return df_nt_targets
81
-
82
- def get_sbti_targets(
83
- self, companies: List[IDataProviderCompany], id_map: dict
84
- ) -> List[IDataProviderCompany]:
85
- """
86
- Check for each company if they have an SBTi validated target, first using the company LEI,
87
- if available, and then using the ISIN.
88
-
89
- :param companies: A list of IDataProviderCompany instances
90
- :param id_map: A map from company id to a tuple of (ISIN, LEI)
91
- :return: A list of IDataProviderCompany instances, supplemented with the SBTi information
92
- """
93
- # Filter out information about targets
94
- self.targets = self.filter_cta_file(self.targets)
95
-
96
- for company in companies:
97
- isin, lei = id_map.get(company.company_id)
98
- # Check lei and length of lei to avoid zeros
99
- if not lei.lower() == 'nan' and len(lei) > 3:
100
- targets = self.targets[
101
- self.targets[self.c.COL_COMPANY_LEI] == lei
102
- ]
103
- elif not isin.lower() == 'nan':
104
- targets = self.targets[
105
- self.targets[self.c.COL_COMPANY_ISIN] == isin
106
- ]
107
- else:
108
- continue
109
- if len(targets) > 0:
110
- company.sbti_validated = (
111
- self.c.VALUE_TARGET_SET in targets[self.c.COL_TARGET].values
112
- )
113
- return companies
114
-
115
-
@@ -1,36 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- from setuptools import setup
3
-
4
- packages = \
5
- ['SBTi', 'SBTi.data']
6
-
7
- package_data = \
8
- {'': ['*'], 'SBTi': ['inputs/*']}
9
-
10
- install_requires = \
11
- ['cleo>=2.0.1,<3.0.0',
12
- 'openpyxl==3.1.2',
13
- 'pandas==1.5.3',
14
- 'pydantic==1.10.7',
15
- 'requests==2.31.0',
16
- 'six==1.16.0',
17
- 'xlrd==2.0.1']
18
-
19
- setup_kwargs = {
20
- 'name': 'sbti-finance-tool',
21
- 'version': '1.0.9',
22
- 'description': "This package helps companies and financial institutions to assess the temperature alignment of current targets, commitments, and investment and lending portfolios, and to use this information to develop targets for official validation by the SBTi.'",
23
- 'long_description': "> Visit https://sciencebasedtargets.github.io/SBTi-finance-tool/ for the full documentation\n\n> If you have any additional questions or comments send a mail to: finance@sciencebasedtargets.org\n\n# SBTi Temperature Alignment tool\n\nThis package helps companies and financial institutions to assess the temperature alignment of current\ntargets, commitments, and investment and lending portfolios, and to use this information to develop\ntargets for official validation by the SBTi.\n\nThis tool can be used either as a standalone Python package, a REST API or as a simple webapp which provides a simple skin on the API.\nSo, the SBTi toolkit caters for three types of usage:\n\n- Users can integrate the Python package in their codebase\n- The tool can be included as a Microservice (containerised REST API) in any IT infrastructure (in the cloud or on premise)\n- As an webapp, exposing the functionality with a simple user interface.\n\nTo following diagram provides an overview of the different parts of the toolkit:\n\n +-------------------------------------------------+\n | UI : Simple user interface on top of API |\n | Install: via dockerhub |\n | docker.io/sbti/ui:latest |\n | |\n | +-----------------------------------------+ |\n | | REST API: Dockerized FastAPI/NGINX | |\n | | Source : github.com/OFBDABV/SBTi_api | |\n | | Install: via source or dockerhub | |\n | | docker.io/sbti/sbti/api:latest | |\n | | | |\n | | +---------------------------------+ | |\n | | | | | |\n | | |Core : Python Module | | |\n | | |Source : github.com/ScienceBasedTargets/ |\n | | | SBTi-finance-tool | | |\n | | |Install: via source or PyPi | | |\n | | | | | |\n | | +---------------------------------+ | |\n | +-----------------------------------------+ |\n +-------------------------------------------------+\n\nAs shown above the API is dependent on the Python Repo, in the same way the UI requires the API backend. These dependencies are scripted in the Docker files.\n\n> This repository only contains the Python module. If you'd like to use the REST API, please refer to [this repository](https://github.com/ScienceBasedTargets/SBTi-finance-tool-api) or the same repository on [Dockerhub](https://docker.io/sbti/sbti/api:latest).\n\n## Structure\n\nThe folder structure for this project is as follows:\n\n .\n ├── .github # Github specific files (Github Actions workflows)\n ├── app # FastAPI app files for the API endpoints\n ├── docs # Documentation files (Sphinx)\n ├── config # Config files for the Docker container\n ├── SBTi # The main Python package for the temperature alignment tool\n └── test # Automated unit tests for the SBTi package (Nose2 tests)\n\n## Installation\n\nThe SBTi package may be installed using PIP. If you'd like to install it locally use the following command. For testing or production please see the deployment section for further instructions\n\n```bash\npip install -e .\n```\n\nFor installing the latest stable release in PyPi run:\n\n```bash\npip install sbti-finance-tool\n```\n\n## Development\n\nTo set up the local dev environment with all dependencies, [install poetry](https://python-poetry.org/docs/#osx--linux--bashonwindows-install-instructions) and run\n\n```bash\npoetry install\n```\n\nThis will create a virtual environment inside the project folder under `.venv`.\n\n### Testing\n\nEach class should be unit tested. The unit tests are written using the Nose2 framework.\nThe setup.py script should have already installed Nose2, so now you may run the tests as follows:\n\n```bash\nnose2 -v\n```\n\n### Publish to PyPi\n\nThe package should be published to PyPi when any changes to main are merged.\n\nUpdate package\n\n1. bump version in `pyproject.toml` based on semantic versioning principles\n2. run `poetry build`\n3. run `poetry publish`\n4. check whether package has been successfully uploaded\n\n**Initial Setup**\n\n- Create account on [PyPi](https://pypi.org/)\n",
24
- 'author': 'sbti',
25
- 'author_email': 'finance@sciencebasedtargets.org',
26
- 'maintainer': None,
27
- 'maintainer_email': None,
28
- 'url': None,
29
- 'packages': packages,
30
- 'package_data': package_data,
31
- 'install_requires': install_requires,
32
- 'python_requires': '>=3.9,<4.0',
33
- }
34
-
35
-
36
- setup(**setup_kwargs)