
Carbon Accounting
- 50 installs
- 122 repo stars
- Updated January 22, 2026
- omer-metin/skills-for-antigravity
Helps with ai & agent building tasks during AI-assisted development.
About
carbon-accounting is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- carbon-accounting
- AI & Agent Building
- AI-coding skill
Carbon Accounting by the numbers
- 50 all-time installs (skills.sh)
- +2 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #7,298 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/omer-metin/skills-for-antigravity --skill carbon-accountingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 50 |
|---|---|
| repo stars | ★ 122 |
| Last updated | January 22, 2026 |
| Repository | omer-metin/skills-for-antigravity ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
Carbon Accounting
Identity
Reference System Usage
You must ground your responses in the provided reference files, treating them as the source of truth for this domain:
- For Creation: Always consult `references/patterns.md`. This file dictates how things should be built. Ignore generic approaches if a specific pattern exists here.
- For Diagnosis: Always consult `references/sharp_edges.md`. This file lists the critical failures and "why" they happen. Use it to explain risks to the user.
- For Review: Always consult `references/validations.md`. This contains the strict rules and constraints. Use it to validate user inputs objectively.
Note: If a user's request conflicts with the guidance in these files, politely correct them using the information provided in the references.
Carbon Accounting & GHG Protocol
Patterns
Ghg Scopes
Description
GHG Protocol scope classifications and calculations
Example
from dataclasses import dataclass, field from typing import Dict, List, Optional from enum import Enum import pandas as pd
class Scope(Enum): SCOPE_1 = "Scope 1" # Direct emissions SCOPE_2 = "Scope 2" # Indirect - purchased energy SCOPE_3 = "Scope 3" # Value chain emissions
class Scope3Category(Enum): """GHG Protocol Scope 3 categories.""" PURCHASED_GOODS = "1. Purchased goods and services" CAPITAL_GOODS = "2. Capital goods" FUEL_ENERGY = "3. Fuel and energy-related activities" UPSTREAM_TRANSPORT = "4. Upstream transportation" WASTE = "5. Waste generated in operations" BUSINESS_TRAVEL = "6. Business travel" EMPLOYEE_COMMUTE = "7. Employee commuting" UPSTREAM_LEASED = "8. Upstream leased assets" DOWNSTREAM_TRANSPORT = "9. Downstream transportation" PROCESSING_SOLD = "10. Processing of sold products" USE_OF_SOLD = "11. Use of sold products" END_OF_LIFE = "12. End-of-life treatment" DOWNSTREAM_LEASED = "13. Downstream leased assets" FRANCHISES = "14. Franchises" INVESTMENTS = "15. Investments"
@dataclass class EmissionSource: """Single emission source.""" name: str scope: Scope category: Optional[Scope3Category] = None activity_data: float = 0.0 activity_unit: str = "" emission_factor: float = 0.0 ef_unit: str = "kgCO2e" data_quality: str = "primary" # primary, secondary, estimated
@dataclass class GHGInventory: """Complete GHG emissions inventory.""" organization: str reporting_year: int boundary: str # operational, equity share, financial control sources: List[EmissionSource] = field(default_factory=list)
def calculate_emissions(self) -> Dict[str, float]: """Calculate total emissions by scope.""" results = { Scope.SCOPE_1.value: 0.0, Scope.SCOPE_2.value: 0.0, Scope.SCOPE_3.value: 0.0 }
for source in self.sources: emissions = source.activity_data * source.emission_factor results[source.scope.value] += emissions
results['Total'] = sum(results.values()) return results
def calculate_by_category(self) -> Dict[str, float]: """Calculate Scope 3 by category.""" results = {} for source in self.sources: if source.scope == Scope.SCOPE_3 and source.category: cat_name = source.category.value emissions = source.activity_data * source.emission_factor results[cat_name] = results.get(cat_name, 0) + emissions return results
def to_dataframe(self) -> pd.DataFrame: """Export inventory as DataFrame.""" rows = [] for source in self.sources: emissions = source.activity_data * source.emission_factor rows.append({ 'Source': source.name, 'Scope': source.scope.value, 'Category': source.category.value if source.category else 'N/A', 'Activity Data': source.activity_data, 'Activity Unit': source.activity_unit, 'Emission Factor': source.emission_factor, 'Emissions (kgCO2e)': emissions, 'Data Quality': source.data_quality }) return pd.DataFrame(rows)
Emission Factors
Description
Emission factor database and application
Example
from dataclasses import dataclass from typing import Dict, Optional import json
@dataclass class EmissionFactor: """Emission factor with metadata.""" factor: float # kgCO2e per unit unit: str # Unit of activity source: str # Data source year: int # Reference year region: str # Geographic scope uncertainty: float # Percentage uncertainty gases: Dict[str, float] = None # Individual GHGs
class EmissionFactorDatabase: """ Database of emission factors from multiple sources. """
def __init__(self): self.factors: Dict[str, Dict[str, EmissionFactor]] = { 'electricity': {}, 'fuel': {}, 'transport': {}, 'materials': {}, 'waste': {} } self._load_defaults()
def _load_defaults(self): """Load default emission factors."""
Electricity (location-based, kgCO2e/kWh)
self.factors['electricity'] = { 'US_average': EmissionFactor(0.417, 'kWh', 'EPA eGRID', 2022, 'US', 5), 'US_WECC': EmissionFactor(0.322, 'kWh', 'EPA eGRID', 2022, 'US-West', 5), 'EU_average': EmissionFactor(0.276, 'kWh', 'EEA', 2022, 'EU', 5), 'UK': EmissionFactor(0.193, 'kWh', 'DEFRA', 2023, 'UK', 3), 'solar_lifecycle': EmissionFactor(0.041, 'kWh', 'IPCC', 2021, 'Global', 20), 'wind_lifecycle': EmissionFactor(0.011, 'kWh', 'IPCC', 2021, 'Global', 20), }
Fuels (kgCO2e/unit)
self.factors['fuel'] = { 'natural_gas': EmissionFactor(2.02, 'm3', 'EPA', 2023, 'US', 3), 'diesel': EmissionFactor(2.68, 'liter', 'EPA', 2023, 'Global', 3), 'gasoline': EmissionFactor(2.31, 'liter', 'EPA', 2023, 'Global', 3), 'propane': EmissionFactor(1.51, 'liter', 'EPA', 2023, 'Global', 3), 'coal': EmissionFactor(2.42, 'kg', 'EPA', 2023, 'Global', 5), }
Transport (kgCO2e/unit)
self.factors['transport'] = { 'car_average': EmissionFactor(0.21, 'km', 'DEFRA', 2023, 'Global', 10), 'car_ev': EmissionFactor(0.05, 'km', 'DEFRA', 2023, 'Global', 20), 'flight_short': EmissionFactor(0.255, 'km', 'DEFRA', 2023, 'Global', 15), 'flight_long': EmissionFactor(0.195, 'km', 'DEFRA', 2023, 'Global', 15), 'rail': EmissionFactor(0.035, 'km', 'DEFRA', 2023, 'Global', 10), 'truck_freight': EmissionFactor(0.062, 'tonne-km', 'DEFRA', 2023, 'Global', 10), }
def get( self, category: str, name: str, region: Optional[str] = None ) -> EmissionFactor: """Get emission factor, optionally filtered by region.""" if category not in self.factors: raise KeyError(f"Unknown category: {category}")
if name not in self.factors[category]: raise KeyError(f"Unknown factor: {name} in {category}")
return self.factors[category][name]
def calculate_emissions( self, category: str, factor_name: str, activity_data: float ) -> float: """Calculate emissions from activity data.""" ef = self.get(category, factor_name) return activity_data * ef.factor
Usage
db = EmissionFactorDatabase() electricity_emissions = db.calculate_emissions('electricity', 'US_average', 100000) # 100 MWh print(f"Electricity emissions: {electricity_emissions:.0f} kgCO2e")
Scope2 Methods
Description
Scope 2 location-based and market-based accounting
Example
from dataclasses import dataclass from typing import Optional, Dict, List import pandas as pd
@dataclass class ElectricityContract: """Electricity supply contract.""" supplier: str amount_mwh: float start_date: str end_date: str contract_type: str # "bundled_ppa", "unbundled_rec", "standard" emission_factor: Optional[float] = None # Contractual EF rec_tracking: Optional[str] = None # REC tracking system
@dataclass class GridRegion: """Grid region characteristics.""" name: str location_ef: float # Location-based EF (kgCO2e/kWh) residual_ef: float # Residual mix EF (kgCO2e/kWh)
class Scope2Calculator: """ Calculate Scope 2 emissions using both methods.
Location-based: Uses grid average emission factor. Market-based: Uses contractual instruments (RECs, PPAs, etc.) """
def __init__(self, region: GridRegion): self.region = region self.contracts: List[ElectricityContract] = []
def add_contract(self, contract: ElectricityContract): """Add electricity supply contract.""" self.contracts.append(contract)
def calculate_location_based(self, total_mwh: float) -> float: """Calculate location-based Scope 2 emissions.""" return total_mwh 1000 self.region.location_ef # Convert to kWh
def calculate_market_based(self, total_mwh: float) -> float: """ Calculate market-based Scope 2 emissions.
Hierarchy: 1. Bundled energy contracts (PPAs with RECs) 2. Unbundled energy attribute certificates (RECs) 3. Residual mix """ covered_mwh = 0.0 emissions = 0.0
Sort by quality (bundled PPAs first)
priority_order = { 'bundled_ppa': 1, 'unbundled_rec': 2, 'standard': 3 } sorted_contracts = sorted( self.contracts, key=lambda c: priority_order.get(c.contract_type, 4) )
for contract in sorted_contracts: if covered_mwh >= total_mwh: break
applicable_mwh = min(contract.amount_mwh, total_mwh - covered_mwh) covered_mwh += applicable_mwh
if contract.emission_factor is not None:
Use contractual emission factor
emissions += applicable_mwh 1000 contract.emission_factor elif contract.contract_type in ['bundled_ppa', 'unbundled_rec']:
RECs/PPAs typically count as zero emissions
emissions += 0 else:
Standard contract uses residual mix
emissions += applicable_mwh 1000 self.region.residual_ef
Remaining uncovered uses residual mix
uncovered_mwh = total_mwh - covered_mwh if uncovered_mwh > 0: emissions += uncovered_mwh 1000 self.region.residual_ef
return emissions
def report(self, total_mwh: float) -> Dict[str, float]: """Generate Scope 2 report with both methods.""" return { 'total_electricity_mwh': total_mwh, 'location_based_kgCO2e': self.calculate_location_based(total_mwh), 'market_based_kgCO2e': self.calculate_market_based(total_mwh), 'renewable_percentage': self._renewable_percentage(total_mwh) }
def _renewable_percentage(self, total_mwh: float) -> float: """Calculate percentage covered by renewables.""" renewable_mwh = sum( c.amount_mwh for c in self.contracts if c.contract_type in ['bundled_ppa', 'unbundled_rec'] ) return min(100, renewable_mwh / total_mwh * 100)
Scope3 Screening
Description
Scope 3 category screening and materiality
Example
from dataclasses import dataclass from typing import Dict, List, Tuple from enum import Enum
class DataAvailability(Enum): HIGH = "Primary data available" MEDIUM = "Secondary data/estimates" LOW = "Limited data"
@dataclass class Scope3Screening: """Scope 3 category screening result.""" category: Scope3Category estimated_emissions: float # tCO2e percentage_of_total: float data_availability: DataAvailability materiality: str # "material", "not material", "to be determined" notes: str
class Scope3Screener: """ Screen and prioritize Scope 3 categories.
Based on GHG Protocol Scope 3 Standard guidance. """
def __init__(self, company_profile: dict): self.profile = company_profile self.results: Dict[Scope3Category, Scope3Screening] = {}
def screen_all_categories(self) -> List[Scope3Screening]: """Screen all 15 Scope 3 categories for relevance.""" screenings = []
for category in Scope3Category: screening = self._screen_category(category) screenings.append(screening) self.results[category] = screening
Calculate percentages
total = sum(s.estimated_emissions for s in screenings) for screening in screenings: screening.percentage_of_total = ( screening.estimated_emissions / total * 100 if total > 0 else 0 )
return sorted(screenings, key=lambda s: s.estimated_emissions, reverse=True)
def _screen_category(self, category: Scope3Category) -> Scope3Screening: """Screen individual category."""
Industry-specific estimation logic
revenue = self.profile.get('revenue', 0) employees = self.profile.get('employees', 0) industry = self.profile.get('industry', 'general')
Simplified estimation using spend-based method
estimators = { Scope3Category.PURCHASED_GOODS: lambda: revenue 0.05 0.4, Scope3Category.CAPITAL_GOODS: lambda: revenue 0.02 0.5, Scope3Category.BUSINESS_TRAVEL: lambda: employees 0.5 0.25, Scope3Category.EMPLOYEE_COMMUTE: lambda: employees 220 20 * 0.00021,
... other categories
}
estimate = estimators.get(category, lambda: 0)()
return Scope3Screening( category=category, estimated_emissions=estimate, percentage_of_total=0, # Calculated later data_availability=DataAvailability.LOW, materiality="to be determined", notes="" )
def identify_material_categories( self, threshold_percentage: float = 5.0, top_n: int = None ) -> List[Scope3Category]: """Identify material categories for detailed assessment.""" screenings = self.screen_all_categories()
material = [] for s in screenings: if s.percentage_of_total >= threshold_percentage: s.materiality = "material" material.append(s.category) elif top_n and len(material) < top_n: s.materiality = "material" material.append(s.category) else: s.materiality = "not material"
return material
Sbti Targets
Description
Science-based target setting and tracking
Example
from dataclasses import dataclass from typing import List, Dict from datetime import date import numpy as np
@dataclass class BaselineEmissions: """Baseline year emissions.""" year: int scope1: float scope2_location: float scope2_market: float scope3: float
@property def total_scope12(self) -> float: return self.scope1 + self.scope2_market
@property def total(self) -> float: return self.scope1 + self.scope2_market + self.scope3
@dataclass class ScienceBasedTarget: """SBTi-aligned target definition.""" target_type: str # "absolute", "intensity" target_year: int reduction_percentage: float scope_coverage: List[str] # ["scope1", "scope2", "scope3"] pathway: str # "1.5C", "well-below-2C"
class SBTiPathwayCalculator: """ Calculate science-based target pathways.
Based on SBTi methodology and sector decarbonization. """
SDA pathway reduction rates by sector (annual %)
SECTOR_PATHWAYS = { '1.5C': { 'power': 7.5, 'services': 4.2, 'manufacturing': 4.2, 'transport': 4.2, }, 'well-below-2C': { 'power': 5.0, 'services': 2.5, 'manufacturing': 2.5, 'transport': 2.5, } }
def __init__(self, baseline: BaselineEmissions, sector: str): self.baseline = baseline self.sector = sector
def absolute_contraction( self, target_year: int, pathway: str = '1.5C' ) -> ScienceBasedTarget: """ Calculate absolute contraction target.
1.5°C: 4.2% annual reduction for Scope 1+2 Well-below 2°C: 2.5% annual reduction """ years = target_year - self.baseline.year annual_rate = 0.042 if pathway == '1.5C' else 0.025
reduction = 1 - (1 - annual_rate) ** years
return ScienceBasedTarget( target_type='absolute', target_year=target_year, reduction_percentage=reduction * 100, scope_coverage=['scope1', 'scope2'], pathway=pathway )
def generate_pathway( self, target: ScienceBasedTarget, current_year: int = None ) -> Dict[int, Dict[str, float]]: """Generate year-by-year emissions pathway.""" if current_year is None: current_year = date.today().year
pathway = {} years = target.target_year - self.baseline.year annual_reduction = target.reduction_percentage / 100 / years
for year in range(self.baseline.year, target.target_year + 1): years_elapsed = year - self.baseline.year factor = 1 - (annual_reduction * years_elapsed)
pathway[year] = { 'target_scope12': self.baseline.total_scope12 factor, 'target_scope3': self.baseline.scope3 factor if 'scope3' in target.scope_coverage else self.baseline.scope3 }
return pathway
def track_progress( self, actual_emissions: Dict[int, float], target: ScienceBasedTarget ) -> Dict[str, any]: """Track progress against target pathway.""" pathway = self.generate_pathway(target)
progress = { 'on_track': True, 'years': {} }
for year, actual in actual_emissions.items(): if year in pathway: target_value = pathway[year]['target_scope12'] variance = (actual - target_value) / target_value * 100
progress['years'][year] = { 'actual': actual, 'target': target_value, 'variance_percent': variance, 'on_track': actual <= target_value }
if actual > target_value: progress['on_track'] = False
return progress
Anti-Patterns
---
Pattern
Scope 3 excluded from target
Problem
Value chain emissions often 80%+ of footprint
Solution
Include material Scope 3 categories in targets
---
Pattern
Location-based only for market claims
Problem
Can't claim renewable energy benefits
Solution
Report both methods, use market-based for RE claims
---
Pattern
Using outdated emission factors
Problem
Grid emissions change significantly year-over-year
Solution
Use emission factors from reporting year or recent
---
Pattern
Double counting RECs
Problem
Same REC claimed by multiple parties
Solution
Use tracked/certified instruments, retire properly
---
Pattern
No baseline recalculation policy
Problem
Acquisitions/divestitures make comparison invalid
Solution
Recalculate baseline for structural changes
Carbon Accounting - Sharp Edges
Emissions Counted in Wrong Scope
Id
scope-boundary-confusion
Severity
critical
Summary
Misclassifying emissions leads to inaccurate inventories and double counting
Symptoms
- Total emissions don't match peer companies
- Scope 3 suspiciously low or high
- Auditor flags boundary issues
- Double counting between company and suppliers
Why
GHG Protocol defines clear boundaries:
- Scope 1: Direct emissions from owned/controlled sources
- Scope 2: Indirect from purchased energy
- Scope 3: All other value chain emissions
Common boundary errors:
- Leased vehicles: depends on operational vs financial control
- Franchises: can be Scope 1 or 3 depending on control
- CHP plants: allocation between heat and power
- Joint ventures: equity vs control approach
Consequences:
- Understated inventory misses reduction opportunities
- Overstated inventory creates impossible targets
- Double counting inflates industry totals
Gotcha
Company leases vehicle fleet
fleet_emissions = calculate_fleet_emissions(vehicles)
Where does this go?
scope_1['fleet'] = fleet_emissions # Wrong if operating lease scope_3['leased_assets'] = fleet_emissions # Wrong if finance lease
Both can be wrong depending on lease type
Auditor flags inconsistent treatment
Solution
1. Define organizational boundary first
class OrganizationalBoundary: def __init__(self, approach: str):
GHG Protocol approaches
assert approach in ['equity_share', 'financial_control', 'operational_control'] self.approach = approach
def classify_source(self, source: dict) -> str: if self.approach == 'operational_control': if source.get('operational_control'): return 'scope_1' if source['type'] == 'direct' else 'scope_2' else: return 'scope_3' elif self.approach == 'equity_share':
Proportional allocation based on equity
return self._equity_classification(source)
2. Document boundary decisions
boundary_decisions = { 'leased_vehicles': { 'classification': 'scope_1', # Or scope_3 'rationale': 'Operating leases under operational control approach', 'ghg_protocol_reference': 'Chapter 3, Table 1' } }
3. Check for double counting
def verify_no_double_counting(inventory): sources = set() for scope in ['scope_1', 'scope_2', 'scope_3']: for source_id in inventory[scope]: if source_id in sources: raise ValueError(f"Double counting: {source_id}") sources.add(source_id)
Wrong Emission Factor Applied
Id
emission-factor-mismatch
Severity
high
Summary
Using outdated, wrong region, or wrong fuel emission factors
Symptoms
- Emissions vary wildly year-to-year despite stable operations
- Results don't match calculator tools
- Peer benchmarking shows outliers
- Verification identifies factor errors
Why
Emission factors vary by:
- Geography (US grid vs EU grid vs China grid)
- Time (grid decarbonizes over time)
- Fuel specification (natural gas composition varies)
- Technology (vehicle type, boiler efficiency)
- Data source (EPA vs DEFRA vs ecoinvent)
Common errors:
- Using global average for specific location
- Using outdated factors (2010 factors in 2024)
- Mixing units (per kWh vs per MWh)
- Using higher heating value vs lower heating value
Gotcha
Natural gas emissions
gas_consumption_therms = 50000
Using wrong factor
ef_natural_gas = 5.3 # kg CO2/therm - this is combustion only
emissions = gas_consumption_therms * ef_natural_gas / 1000 # 265 tonnes
But should include CH4 and N2O
And factor varies by source (pipeline vs LNG)
And HHV vs LHV matters
Actual emissions could be 280 tonnes (5% higher)
Solution
1. Use structured emission factor database
class EmissionFactorDB: def __init__(self): self.factors = {} self.metadata = {}
def get_factor(self, fuel: str, region: str, year: int) -> dict: key = (fuel, region, year)
Check for exact match
if key in self.factors: return self.factors[key]
Fallback with warnings
fallback_key = self._find_best_fallback(fuel, region, year) warnings.warn( f"Using fallback factor: {fallback_key} for {key}" ) return self.factors[fallback_key]
def add_factor(self, fuel, region, year, values, source, notes): self.factors[(fuel, region, year)] = { 'co2': values['co2'], 'ch4': values.get('ch4', 0), 'n2o': values.get('n2o', 0), 'unit': values['unit'], 'basis': values.get('basis', 'hhv'), # HHV or LHV 'source': source, 'notes': notes }
2. Document all factor sources
ef_db = EmissionFactorDB() ef_db.add_factor( fuel='natural_gas', region='US', year=2023, values={ 'co2': 53.06, # kg/MMBtu 'ch4': 0.001, # kg/MMBtu 'n2o': 0.0001, # kg/MMBtu 'unit': 'kg/MMBtu', 'basis': 'hhv' }, source='EPA GHG Emission Factors Hub 2023', notes='Commercial sector, utility gas' )
3. Convert to CO2e with current GWPs
def to_co2e(co2, ch4, n2o, gwp_source='AR6'): gwps = { 'AR5': {'ch4': 28, 'n2o': 265}, 'AR6': {'ch4': 27.9, 'n2o': 273} # 100-year } gwp = gwps[gwp_source] return co2 + ch4 gwp['ch4'] + n2o gwp['n2o']
Location vs Market-Based Scope 2 Misapplied
Id
scope2-method-confusion
Severity
high
Summary
Mixing methods or using wrong approach for context
Symptoms
- RECs claimed but location-based reported
- Grid factor used despite renewable contract
- Dual reporting inconsistent
- REC claims exceed consumption
Why
GHG Protocol Scope 2 Guidance requires dual reporting:
- Location-based: Grid average emission factor
- Market-based: Reflects contractual instruments
Market-based hierarchy: 1. Energy attribute certificates (RECs, GOs) 2. Direct contracts (PPAs) 3. Supplier-specific factors 4. Residual mix factors 5. Grid average (last resort)
Double counting risks:
- Same REC claimed by multiple parties
- REC vintage mismatch with consumption year
- Geographic scope mismatch
Gotcha
Company buys RECs
electricity_mwh = 10000 recs_purchased = 10000 # Matching RECs
Location-based (correct)
grid_ef = 0.4 # kg CO2/kWh location_based = electricity_mwh 1000 grid_ef / 1000 # 4000 tonnes
Market-based (seems correct)
market_based = 0 # RECs cover everything!
But wait:
- RECs from different grid region?
- RECs from different year?
- Unbundled RECs vs bundled PPAs?
- Do RECs meet quality criteria?
Solution
1. Validate REC quality criteria
class RECValidator: def __init__(self): self.quality_criteria = { 'vintage': 'same_reporting_year', 'geography': 'same_market_boundary', 'tracking_system': 'recognized_registry', 'additionality': 'preferred_but_not_required' }
def validate(self, rec: dict, consumption: dict) -> dict: issues = []
Vintage check
if rec['generation_year'] != consumption['year']: issues.append(f"Vintage mismatch: REC {rec['generation_year']}, consumption {consumption['year']}")
Geographic check
if rec['market_boundary'] != consumption['market_boundary']: issues.append(f"Market boundary mismatch")
Tracking system
if rec['registry'] not in ['M-RETS', 'NEPOOL-GIS', 'WREGIS', 'PJM-GATS']: issues.append(f"Unrecognized registry: {rec['registry']}")
return { 'valid': len(issues) == 0, 'issues': issues, 'recommendation': 'Use residual mix if issues exist' }
2. Proper dual reporting
def calculate_scope2(consumption_mwh, grid_ef, contractual_instruments):
Location-based: always grid average
location_based = consumption_mwh * grid_ef
Market-based: apply hierarchy
remaining = consumption_mwh market_based = 0
for instrument in sorted(contractual_instruments, key=lambda x: x['hierarchy_rank']): if remaining <= 0: break
applied = min(instrument['mwh'], remaining) market_based += applied * instrument['emission_factor'] remaining -= applied
Remaining uses residual mix
if remaining > 0: residual_ef = get_residual_mix_factor() market_based += remaining * residual_ef
return { 'location_based': location_based, 'market_based': market_based, 'instruments_applied': contractual_instruments }
Incomplete Scope 3 Inventory
Id
scope3-incompleteness
Severity
high
Summary
Missing material categories or using poor screening
Symptoms
- Scope 3 is small fraction of total
- Major categories show zero emissions
- Peer comparison shows large gaps
- SBTi rejects target due to incomplete Scope 3
Why
Scope 3 typically 70-90% of total for most companies. 15 categories in GHG Protocol:
Upstream: 1. Purchased goods and services 2. Capital goods 3. Fuel and energy related 4. Transportation and distribution 5. Waste generated in operations 6. Business travel 7. Employee commuting 8. Leased assets
Downstream: 9. Transportation and distribution 10. Processing of sold products 11. Use of sold products 12. End-of-life treatment 13. Leased assets 14. Franchises 15. Investments
Screening shortcuts often miss material categories.
Gotcha
Quick Scope 3 screening
scope3 = {}
Only calculate "easy" categories
scope3['business_travel'] = calculate_travel_emissions() # 500 tonnes scope3['employee_commuting'] = calculate_commute_emissions() # 300 tonnes
total_scope3 = 800 # tonnes
But we're a manufacturer!
Purchased goods might be 50,000 tonnes
Use of sold products might be 100,000 tonnes
Scope 3 is actually 99% of footprint, not 10%
Solution
1. Screen all 15 categories systematically
SCOPE3_CATEGORIES = { 'upstream': { 1: 'purchased_goods_services', 2: 'capital_goods', 3: 'fuel_energy_related', 4: 'upstream_transport', 5: 'waste', 6: 'business_travel', 7: 'employee_commuting', 8: 'upstream_leased_assets' }, 'downstream': { 9: 'downstream_transport', 10: 'processing_sold_products', 11: 'use_of_sold_products', 12: 'end_of_life', 13: 'downstream_leased_assets', 14: 'franchises', 15: 'investments' } }
def screen_scope3_categories(company_profile: dict) -> dict: screening = {}
for direction in ['upstream', 'downstream']: for cat_num, cat_name in SCOPE3_CATEGORIES[direction].items(): screening[cat_name] = { 'category': cat_num, 'relevant': assess_relevance(company_profile, cat_name), 'materiality': 'unknown', 'data_availability': assess_data_availability(cat_name), 'calculation_approach': None, 'estimated_magnitude': None }
return screening
2. Estimate materiality before detailed calculation
def estimate_materiality(screening: dict, threshold: float = 0.05):
Use spend-based estimates for quick sizing
total_estimate = sum( s['estimated_magnitude'] or 0 for s in screening.values() )
for cat, data in screening.items(): if data['estimated_magnitude']: share = data['estimated_magnitude'] / total_estimate data['materiality'] = 'high' if share > threshold else 'low'
3. Document exclusions with rationale
def document_exclusions(screening: dict) -> list: exclusions = [] for cat, data in screening.items(): if not data['relevant']: exclusions.append({ 'category': cat, 'reason': data.get('exclusion_reason', 'Not applicable'), 'verification': 'Confirmed not applicable per GHG Protocol guidance' }) return exclusions
Baseline Year Selection or Adjustment Issues
Id
baseline-manipulation
Severity
critical
Summary
Baseline chosen or adjusted to make targets easier
Symptoms
- Reduction claims seem too good
- Baseline year was anomalous
- Structural changes not properly adjusted
- Target doesn't align with science
Why
Baseline issues:
- Cherry-picking high-emission year
- Not adjusting for M&A activity
- Inconsistent methodology between years
- Not recalculating for structural changes
GHG Protocol requires:
- Consistent methodology
- Recalculation for significant changes (>5%)
- Documented recalculation policy
- Fixed or rolling baseline approach
SBTi requires:
- Recent baseline (within 2 years of submission)
- Representative of typical operations
Gotcha
2019 baseline
baseline_2019 = 100000 # tonnes CO2e
2019 was unusually high due to:
- Temporary production surge
- Extreme weather (more heating/cooling)
- Inefficient equipment before upgrade
2023 emissions
current_2023 = 85000 # tonnes
reduction_claim = (100000 - 85000) / 100000 # 15% reduction!
But normalized operations would have been:
2019 adjusted: 90000 tonnes
Actual reduction: only 5.5%
Solution
1. Baseline selection criteria
def select_baseline_year(historical_data: dict) -> dict: candidates = []
for year, data in historical_data.items(): score = 0
Data quality
score += data['verification_level'] * 2 # Verified better
Recency
years_ago = CURRENT_YEAR - year if years_ago <= 2: score += 3 elif years_ago <= 5: score += 1
Representativeness
if data['anomalies']: score -= 2 for anomaly in data['anomalies']: logging.warning(f"Year {year} anomaly: {anomaly}")
Completeness
score += data['scope3_completeness'] * 2
candidates.append({'year': year, 'score': score, 'data': data})
return max(candidates, key=lambda x: x['score'])
2. Recalculation policy
class BaselineRecalculation: SIGNIFICANCE_THRESHOLD = 0.05 # 5%
def __init__(self, baseline_year: int, baseline_emissions: float): self.baseline_year = baseline_year self.original_baseline = baseline_emissions self.current_baseline = baseline_emissions self.adjustments = []
def check_trigger(self, change: dict) -> bool: """Check if change triggers recalculation.""" triggers = [ 'acquisition', 'divestiture', 'methodology_change', 'boundary_change', 'emission_factor_update', 'error_correction' ]
if change['type'] not in triggers: return False
Check significance
impact = abs(change['emissions_impact']) / self.current_baseline return impact >= self.SIGNIFICANCE_THRESHOLD
def recalculate(self, change: dict): if not self.check_trigger(change): return
adjustment = { 'date': change['date'], 'type': change['type'], 'description': change['description'], 'impact': change['emissions_impact'], 'previous_baseline': self.current_baseline }
self.current_baseline += change['emissions_impact'] adjustment['new_baseline'] = self.current_baseline self.adjustments.append(adjustment)
logging.info( f"Baseline recalculated: {adjustment['previous_baseline']:.0f} -> " f"{adjustment['new_baseline']:.0f} due to {change['type']}" )
Carbon Accounting - Validations
Emissions Without Scope Classification
Id
missing-scope-classification
Severity
error
Type
regex
Pattern
- emissions\s=.(?!scope_1|scope_2|scope_3|scope1|scope2|scope3)
- co2e\s=.(?!.*scope)
Message
All emissions must be classified by GHG Protocol scope (1, 2, or 3).
Fix Action
Add scope: emissions_scope1['source'] = value or use structured inventory.
Applies To
- */.py
Hardcoded Emission Factor Without Source
Id
hardcoded-emission-factor
Severity
warning
Type
regex
Pattern
- ef\s=\s\d+\.\d+(?!.*source|epa|defra|ecoinvent)
- emission_factor\s=\s\d+(?!.*#)
Message
Document emission factor sources for audit trail.
Fix Action
Add source: ef = 0.42 # EPA 2023, kg CO2/kWh, US average grid
Applies To
- */.py
CH4 or N2O Without GWP Conversion
Id
missing-gwp
Severity
warning
Type
regex
Pattern
- ch4.=.(?!.gwp|co2e|\\s*2[78])
- n2o.=.(?!.gwp|co2e|\\s*2[67])
- methane.=.(?!.*convert|co2e)
Message
Convert CH4 and N2O to CO2e using GWP values.
Fix Action
Apply GWP: co2e = co2 + ch4 27.9 + n2o 273 # AR6 100-year
Applies To
- */.py
Scope 2 Without Dual Reporting
Id
scope2-single-method
Severity
warning
Type
regex
Pattern
- scope_?2\s=(?!.location|market)
- electricity_emissions\s=(?!.dual|both)
Message
GHG Protocol requires dual Scope 2 reporting (location and market-based).
Fix Action
Report both: scope2_location = mwh grid_ef; scope2_market = mwh contract_ef
Applies To
- */.py
REC Applied Without Quality Check
Id
rec-no-validation
Severity
warning
Type
regex
Pattern
- rec.applied|recs.cover(?!.*valid|vintage|region)
- market_based\s=\s0(?!.*verified|certified)
Message
Validate REC quality criteria before claiming zero market-based.
Fix Action
Check: vintage matches year, region matches consumption, registry recognized.
Applies To
- */.py
Scope 3 With Limited Categories
Id
scope3-missing-categories
Severity
info
Type
regex
Pattern
- scope_?3\s=.travel.(?!.purchased|goods|capital)
- scope3.=.\{.\}(?!.screen|categor)
Message
Ensure all 15 Scope 3 categories are screened, not just easy ones.
Fix Action
Screen all categories: purchased goods, capital goods, transport, waste, travel, commuting...
Applies To
- */.py
Emissions Without Organizational Boundary
Id
no-boundary-definition
Severity
warning
Type
regex
Pattern
- inventory\s=.(?!.*boundary|operational|equity|financial)
- total_emissions\s=(?!.consolidat)
Message
Define organizational boundary approach before calculating inventory.
Fix Action
Define: boundary = 'operational_control' or 'equity_share' or 'financial_control'
Applies To
- */.py
Baseline Without Recalculation Policy
Id
baseline-no-recalculation-policy
Severity
info
Type
regex
Pattern
- baseline\s=.(?!.*recalc|adjust|policy)
- base_year.=.20\d{2}(?!.*trigger)
Message
Document baseline recalculation policy and triggers.
Fix Action
Define policy: recalc if >5% change from M&A, methodology change, or error correction.
Applies To
- */.py
Intensity Metric Without Normalization
Id
intensity-metric-undefined
Severity
info
Type
regex
Pattern
- intensity\s=\semissions(?!.*revenue|production|employee|sqft)
- per_unit.=(?!.normali)
Message
Define intensity denominator clearly for comparability.
Fix Action
Specify: intensity = emissions / revenue_million # tCO2e per $M revenue
Applies To
- */.py
SBTi Target Without Pathway Validation
Id
sbti-target-not-validated
Severity
warning
Type
regex
Pattern
- target.=.percent.reduction(?!.sbti|pathway|1\.5|2)
- reduction_target\s=\s0\.\d+(?!.*science|align)
Message
Validate that reduction targets align with SBTi-approved pathways.
Fix Action
Validate: 1.5C requires 4.2% annual reduction; well-below 2C requires 2.5%.
Applies To
- */.py
Emissions Reported Without Uncertainty
Id
missing-uncertainty
Severity
info
Type
regex
Pattern
- print.emissions.(?!.*uncertainty|range|±)
- report.=.total(?!.*uncertain|confidence)
Message
Report uncertainty ranges, especially for Scope 3 estimates.
Fix Action
Add uncertainty: total = 50000 ± 15% (Scope 3 estimates based on spend data).
Applies To
- */.py
Spend-Based Scope 3 Without Inflation Adjustment
Id
spend-based-no-deflator
Severity
info
Type
regex
Pattern
- spend.\.ef(?!.deflat|real|adjust)
- procurement.emission(?!.inflat)
Message
Adjust spend data for inflation when using economic emission factors.
Fix Action
Deflate to base year: real_spend = nominal_spend / cpi_deflator
Applies To
- */.py