strategicc 2.2.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Shlhnj
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,123 @@
1
+ Metadata-Version: 2.4
2
+ Name: strategicc
3
+ Version: 2.2.0
4
+ Summary: State and Transition Integrated Economic-Environmental Accounting
5
+ Author: Shlhnj
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 Shlhnj
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://github.com/Shlhnj/strategicc
29
+ Project-URL: Repository, https://github.com/Shlhnj/strategicc
30
+ Project-URL: Issues, https://github.com/Shlhnj/strategicc/issues
31
+ Keywords: STSM,land cover,ecosystem accounting,SEEA-EA,landscape ecology,Monte Carlo
32
+ Classifier: Programming Language :: Python :: 3
33
+ Classifier: License :: OSI Approved :: MIT License
34
+ Classifier: Operating System :: OS Independent
35
+ Classifier: Topic :: Scientific/Engineering :: GIS
36
+ Classifier: Intended Audience :: Science/Research
37
+ Requires-Python: >=3.10
38
+ Description-Content-Type: text/markdown
39
+ License-File: LICENSE
40
+ Requires-Dist: numpy>=1.24
41
+ Requires-Dist: pandas>=2.0
42
+ Requires-Dist: Pillow>=10.0
43
+ Requires-Dist: matplotlib>=3.7
44
+ Provides-Extra: dev
45
+ Requires-Dist: pytest>=7.0; extra == "dev"
46
+ Requires-Dist: pytest-cov; extra == "dev"
47
+ Dynamic: license-file
48
+
49
+ # strategicc
50
+ STRATEGICC: State and Transition Integrated Economic-Environmental Accounting
51
+
52
+ A python package implementation of State-and-Transition Simulation Model (STSM) framework laid by Daniel et al (2016) (https://doi.org/10.1111/2041-210X.12597), integrated with the System of Economic-Environmental Accounting - Ecosystem Accounting (SEEA-EA) by United Nations (https://seea.un.org/ecosystem-accounting).
53
+
54
+ ## Usage
55
+ ### install the package
56
+ ```
57
+ !pip install git+https://github.com/Shlhnj/strategicc.git
58
+ ```
59
+
60
+
61
+ ### Set Configuration
62
+
63
+ ```
64
+ import strategicc.config as cfg
65
+ from pathlib import Path
66
+
67
+ cfg.LULC_PATH = Path("2022.tif")
68
+ cfg.STATE_CLASSES_CSV = Path("25062026 State Class.csv")
69
+ cfg.TRANSITIONS_CSV = Path("23062026_stsm_transition_probabilities.csv")
70
+ cfg.SPATIAL_MULT_CSV = Path("27062026 Transition Spatial Multipliers.csv")
71
+ cfg.TRANSITION_MULT_CSV= Path("27062026 Transition Multipliers.csv")
72
+ cfg.MULT_DIR = Path("mult_spat/") #folder for transition spatial multiplier
73
+ cfg.OUT_DIR = Path("strategicc_test") #output folder
74
+
75
+
76
+ cfg.ADJACENCY_STRENGTH = 2
77
+ cfg.START_YEAR = 2022
78
+ cfg.N_TIMESTEPS = 30
79
+ cfg.N_ITERATIONS = 100
80
+ cfg.RNG_SEED = 42
81
+
82
+ cfg.USE_ADJACENCY = True
83
+ cfg.USE_SPATIAL_MULT = True
84
+ cfg.USE_TRANS_MULTIPLIER = True
85
+ ```
86
+
87
+ ### Diagnose Configuration
88
+
89
+ ```
90
+ from strategicc import STSMEngine
91
+
92
+ engine = STSMEngine.from_config()
93
+ engine.load()
94
+ engine.diagnostic()
95
+ ```
96
+
97
+ ### Run Engine
98
+
99
+ ```
100
+ engine.run()
101
+ ```
102
+
103
+ ### Show Summary Plot
104
+
105
+ ```
106
+ from strategicc import outputs
107
+
108
+ summary_dir = engine.out_dir / "summary"
109
+
110
+ print("Building summary tables...")
111
+ area_df, trans_df = outputs.build_summary_tables(engine.iter_dirs, summary_dir)
112
+
113
+ print("Plotting area envelope...")
114
+ outputs.plot_area_envelope(area_df, engine.classes, summary_dir)
115
+
116
+ print("Plotting transition envelope...")
117
+ outputs.plot_transition_envelope(trans_df, summary_dir)
118
+
119
+ Show plots inline
120
+ from IPython.display import Image, display
121
+ display(Image(str(summary_dir / "area_envelope.png")))
122
+ display(Image(str(summary_dir / "transition_envelope.png")))
123
+ ```
@@ -0,0 +1,75 @@
1
+ # strategicc
2
+ STRATEGICC: State and Transition Integrated Economic-Environmental Accounting
3
+
4
+ A python package implementation of State-and-Transition Simulation Model (STSM) framework laid by Daniel et al (2016) (https://doi.org/10.1111/2041-210X.12597), integrated with the System of Economic-Environmental Accounting - Ecosystem Accounting (SEEA-EA) by United Nations (https://seea.un.org/ecosystem-accounting).
5
+
6
+ ## Usage
7
+ ### install the package
8
+ ```
9
+ !pip install git+https://github.com/Shlhnj/strategicc.git
10
+ ```
11
+
12
+
13
+ ### Set Configuration
14
+
15
+ ```
16
+ import strategicc.config as cfg
17
+ from pathlib import Path
18
+
19
+ cfg.LULC_PATH = Path("2022.tif")
20
+ cfg.STATE_CLASSES_CSV = Path("25062026 State Class.csv")
21
+ cfg.TRANSITIONS_CSV = Path("23062026_stsm_transition_probabilities.csv")
22
+ cfg.SPATIAL_MULT_CSV = Path("27062026 Transition Spatial Multipliers.csv")
23
+ cfg.TRANSITION_MULT_CSV= Path("27062026 Transition Multipliers.csv")
24
+ cfg.MULT_DIR = Path("mult_spat/") #folder for transition spatial multiplier
25
+ cfg.OUT_DIR = Path("strategicc_test") #output folder
26
+
27
+
28
+ cfg.ADJACENCY_STRENGTH = 2
29
+ cfg.START_YEAR = 2022
30
+ cfg.N_TIMESTEPS = 30
31
+ cfg.N_ITERATIONS = 100
32
+ cfg.RNG_SEED = 42
33
+
34
+ cfg.USE_ADJACENCY = True
35
+ cfg.USE_SPATIAL_MULT = True
36
+ cfg.USE_TRANS_MULTIPLIER = True
37
+ ```
38
+
39
+ ### Diagnose Configuration
40
+
41
+ ```
42
+ from strategicc import STSMEngine
43
+
44
+ engine = STSMEngine.from_config()
45
+ engine.load()
46
+ engine.diagnostic()
47
+ ```
48
+
49
+ ### Run Engine
50
+
51
+ ```
52
+ engine.run()
53
+ ```
54
+
55
+ ### Show Summary Plot
56
+
57
+ ```
58
+ from strategicc import outputs
59
+
60
+ summary_dir = engine.out_dir / "summary"
61
+
62
+ print("Building summary tables...")
63
+ area_df, trans_df = outputs.build_summary_tables(engine.iter_dirs, summary_dir)
64
+
65
+ print("Plotting area envelope...")
66
+ outputs.plot_area_envelope(area_df, engine.classes, summary_dir)
67
+
68
+ print("Plotting transition envelope...")
69
+ outputs.plot_transition_envelope(trans_df, summary_dir)
70
+
71
+ Show plots inline
72
+ from IPython.display import Image, display
73
+ display(Image(str(summary_dir / "area_envelope.png")))
74
+ display(Image(str(summary_dir / "transition_envelope.png")))
75
+ ```
@@ -0,0 +1,44 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "strategicc"
7
+ version = "2.2.0"
8
+ description = "State and Transition Integrated Economic-Environmental Accounting"
9
+ readme = "README.md"
10
+ license = { file = "LICENSE" }
11
+ authors = [{ name = "Shlhnj" }]
12
+ keywords = [
13
+ "STSM", "land cover", "ecosystem accounting",
14
+ "SEEA-EA", "landscape ecology", "Monte Carlo",
15
+ ]
16
+ classifiers = [
17
+ "Programming Language :: Python :: 3",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Operating System :: OS Independent",
20
+ "Topic :: Scientific/Engineering :: GIS",
21
+ "Intended Audience :: Science/Research",
22
+ ]
23
+ requires-python = ">=3.10"
24
+ dependencies = [
25
+ "numpy>=1.24",
26
+ "pandas>=2.0",
27
+ "Pillow>=10.0",
28
+ "matplotlib>=3.7",
29
+ ]
30
+
31
+ [project.optional-dependencies]
32
+ dev = ["pytest>=7.0", "pytest-cov"]
33
+
34
+ [project.urls]
35
+ Homepage = "https://github.com/Shlhnj/strategicc"
36
+ Repository = "https://github.com/Shlhnj/strategicc"
37
+ Issues = "https://github.com/Shlhnj/strategicc/issues"
38
+
39
+ [project.scripts]
40
+ strategicc = "strategicc.run:main"
41
+
42
+ [tool.setuptools.packages.find]
43
+ where = ["."]
44
+ include = ["strategicc*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,9 @@
1
+ """
2
+ strategicc — State and Transition Integrated Economic-Environmental Accounting
3
+ v2.0
4
+ """
5
+
6
+ from .engine import StrategiccEngine
7
+
8
+ __version__ = "2.2.0"
9
+ __all__ = ["StrategiccEngine"]
@@ -0,0 +1,12 @@
1
+ """
2
+ strategicc/accounting — SEEA-EA accounting module v2.0
3
+ ----------------------------------------------------------
4
+ Produces ecosystem extent, physical flow, monetary flow,
5
+ transition matrix, and change-in-value accounts from
6
+ simulation outputs.
7
+ """
8
+
9
+ from .seea import SEEAAccount
10
+ from .csv_loader import load_ecosystem_services, EcosystemService
11
+
12
+ __all__ = ["SEEAAccount", "load_ecosystem_services", "EcosystemService"]
@@ -0,0 +1,113 @@
1
+ """
2
+ strategicc/accounting/csv_loader.py
3
+ -------------------------------------
4
+ Parse EcosystemServices.csv into EcosystemService dataclasses.
5
+
6
+ CSV format (two modes supported):
7
+
8
+ Mode A — monetary value per ha only (no physical unit):
9
+ StateClassId, ServiceName, ServiceType, ValuePerHa, Currency
10
+ Mangrove, Ecotourism, Cultural, 12500000, IDR
11
+
12
+ Mode B — physical unit + monetary value per ha:
13
+ StateClassId, ServiceName, ServiceType, ValuePerHa, Currency, PhysicalUnit, PhysicalValuePerHa
14
+ Mangrove, Carbon Sequestration, Regulating, 97500000, IDR, MgC/ha, 1300
15
+
16
+ ServiceType must be one of: Provisioning, Regulating, Cultural
17
+ """
18
+
19
+ from __future__ import annotations
20
+ import csv
21
+ from dataclasses import dataclass
22
+ from pathlib import Path
23
+
24
+
25
+ VALID_SERVICE_TYPES = {"Provisioning", "Regulating", "Cultural"}
26
+
27
+
28
+ @dataclass
29
+ class EcosystemService:
30
+ """One ecosystem service entry from EcosystemServices.csv."""
31
+ state_class: str # matches StateClass name e.g. "Mangrove"
32
+ service_name: str # e.g. "Carbon Sequestration"
33
+ service_type: str # "Provisioning" | "Regulating" | "Cultural"
34
+ value_per_ha: float # monetary value per hectare per year
35
+ currency: str # e.g. "IDR"
36
+ physical_unit: str | None # e.g. "MgC/ha" — None if Mode A
37
+ physical_per_ha: float | None # physical quantity per ha — None if Mode A
38
+
39
+ @property
40
+ def has_physical(self) -> bool:
41
+ return self.physical_unit is not None and self.physical_per_ha is not None
42
+
43
+
44
+ def load_ecosystem_services(path: str | Path) -> list[EcosystemService]:
45
+ """
46
+ Parse EcosystemServices.csv.
47
+
48
+ Required columns:
49
+ StateClassId, ServiceName, ServiceType, ValuePerHa, Currency
50
+
51
+ Optional columns (Mode B):
52
+ PhysicalUnit, PhysicalValuePerHa
53
+
54
+ Returns
55
+ -------
56
+ list of EcosystemService
57
+ """
58
+ path = Path(path)
59
+ services: list[EcosystemService] = []
60
+
61
+ with path.open(newline="", encoding="utf-8-sig") as fh:
62
+ reader = csv.DictReader(fh)
63
+ for i, row in enumerate(reader, start=2):
64
+ state_class = row.get("StateClassId", "").strip()
65
+ service_name = row.get("ServiceName", "").strip()
66
+ service_type = row.get("ServiceType", "").strip()
67
+ currency = row.get("Currency", "").strip()
68
+
69
+ # Parse monetary value
70
+ try:
71
+ value_per_ha = float(row.get("ValuePerHa", "").strip())
72
+ except (ValueError, AttributeError):
73
+ print(f" [Warning] Row {i}: invalid ValuePerHa — skipped")
74
+ continue
75
+
76
+ # Validate service type
77
+ if service_type not in VALID_SERVICE_TYPES:
78
+ print(f" [Warning] Row {i}: unknown ServiceType '{service_type}' "
79
+ f"— must be one of {VALID_SERVICE_TYPES}")
80
+ continue
81
+
82
+ if not state_class or not service_name:
83
+ print(f" [Warning] Row {i}: missing StateClassId or ServiceName — skipped")
84
+ continue
85
+
86
+ # Optional physical columns (Mode B)
87
+ phys_unit = row.get("PhysicalUnit", "").strip() or None
88
+ phys_raw = row.get("PhysicalValuePerHa", "").strip()
89
+ try:
90
+ phys_per_ha = float(phys_raw) if phys_raw else None
91
+ except ValueError:
92
+ phys_per_ha = None
93
+
94
+ # Both must be present for Mode B, or both absent for Mode A
95
+ if (phys_unit is None) != (phys_per_ha is None):
96
+ print(f" [Warning] Row {i} ({state_class} / {service_name}): "
97
+ "PhysicalUnit and PhysicalValuePerHa must both be present "
98
+ "or both absent — treating as Mode A")
99
+ phys_unit = phys_per_ha = None
100
+
101
+ services.append(EcosystemService(
102
+ state_class = state_class,
103
+ service_name = service_name,
104
+ service_type = service_type,
105
+ value_per_ha = value_per_ha,
106
+ currency = currency,
107
+ physical_unit = phys_unit,
108
+ physical_per_ha = phys_per_ha,
109
+ ))
110
+
111
+ print(f" {len(services)} ecosystem service entries loaded "
112
+ f"({sum(s.has_physical for s in services)} with physical units)")
113
+ return services
@@ -0,0 +1,260 @@
1
+ """
2
+ strategicc/accounting/outputs.py — SEEA-EA output functions v2.0
3
+ --------------------------------------------------------------------
4
+ Saves all ecosystem accounts as CSVs and generates plots.
5
+
6
+ Functions
7
+ ---------
8
+ save_all_accounts — save all account tables to CSV
9
+ plot_monetary_flows — stacked area chart of total ecosystem value over time
10
+ plot_value_by_service — line chart per service type over time
11
+ plot_transition_heatmap — heatmap of transition matrix (area and value)
12
+ """
13
+
14
+ from __future__ import annotations
15
+ from pathlib import Path
16
+
17
+ import numpy as np
18
+ import pandas as pd
19
+ import matplotlib
20
+ matplotlib.use("Agg")
21
+ import matplotlib.pyplot as plt
22
+ import matplotlib.ticker as mticker
23
+
24
+ from strategicc.io.csv_loader import StateClass
25
+ from strategicc.accounting.seea import SEEAAccount
26
+
27
+
28
+ # ── Color helpers ─────────────────────────────────────────────────────────────
29
+
30
+ def _class_colors(classes: dict[int, StateClass]) -> dict[str, tuple]:
31
+ return {
32
+ sc.name: (sc.color[1]/255, sc.color[2]/255, sc.color[3]/255)
33
+ for sc in classes.values()
34
+ }
35
+
36
+
37
+ # ── Save all account tables ───────────────────────────────────────────────────
38
+
39
+ def save_all_accounts(
40
+ acct: SEEAAccount,
41
+ out_dir: Path,
42
+ ) -> None:
43
+ """Save all SEEA-EA account tables as CSVs."""
44
+ out_dir.mkdir(parents=True, exist_ok=True)
45
+
46
+ acct.extent_account().to_csv(out_dir / "seea_extent_account.csv")
47
+ print(f" Saved: seea_extent_account.csv")
48
+
49
+ acct.transition_matrix().to_csv(out_dir / "seea_transition_matrix_area.csv")
50
+ print(f" Saved: seea_transition_matrix_area.csv")
51
+
52
+ acct.value_change_matrix().to_csv(out_dir / "seea_transition_matrix_value.csv")
53
+ print(f" Saved: seea_transition_matrix_value.csv")
54
+
55
+ acct.monetary_flow_account().to_csv(out_dir / "seea_monetary_flow_account.csv")
56
+ print(f" Saved: seea_monetary_flow_account.csv")
57
+
58
+ phys = acct.physical_flow_account()
59
+ if phys is not None:
60
+ phys.to_csv(out_dir / "seea_physical_flow_account.csv")
61
+ print(f" Saved: seea_physical_flow_account.csv")
62
+
63
+ acct.total_value_by_class().to_csv(out_dir / "seea_total_value_by_class.csv")
64
+ print(f" Saved: seea_total_value_by_class.csv")
65
+
66
+ acct.change_in_value().to_csv(out_dir / "seea_change_in_value.csv")
67
+ print(f" Saved: seea_change_in_value.csv")
68
+
69
+ acct.uncertainty_summary().to_csv(out_dir / "seea_uncertainty_summary.csv", index=False)
70
+ print(f" Saved: seea_uncertainty_summary.csv")
71
+
72
+
73
+ # ── Plot: stacked area — total ecosystem value over time ──────────────────────
74
+
75
+ def plot_monetary_flows(
76
+ acct: SEEAAccount,
77
+ classes: dict[int, StateClass],
78
+ out_dir: Path,
79
+ filename: str = "seea_monetary_flows.png",
80
+ ) -> None:
81
+ """
82
+ Stacked area chart: total ecosystem service value per class over time.
83
+ Shows which classes contribute most to total landscape value.
84
+ """
85
+ out_dir.mkdir(parents=True, exist_ok=True)
86
+ tv = acct.total_value_by_class()
87
+ colors = _class_colors(classes)
88
+ years = tv.index.tolist()
89
+
90
+ fig, axes = plt.subplots(2, 1, figsize=(12, 10))
91
+
92
+ # ── Panel 1: stacked area by class ────────────────────────────────────────
93
+ ax1 = axes[0]
94
+ bottom = np.zeros(len(years))
95
+ for col in tv.columns:
96
+ vals = tv[col].values
97
+ color = colors.get(col, (0.5, 0.5, 0.5))
98
+ ax1.fill_between(years, bottom, bottom + vals,
99
+ alpha=0.85, color=color, label=col)
100
+ bottom += vals
101
+
102
+ ax1.set_ylabel("Total ecosystem value (currency/yr)", fontsize=10)
103
+ ax1.set_title("Total Ecosystem Service Value by Class", fontsize=11)
104
+ ax1.legend(loc="upper right", fontsize=8, framealpha=0.8)
105
+ ax1.yaxis.set_major_formatter(mticker.FuncFormatter(
106
+ lambda x, _: f"{x/1e6:.1f}M" if x >= 1e6 else f"{x:,.0f}"
107
+ ))
108
+ ax1.grid(True, alpha=0.2)
109
+
110
+ # ── Panel 2: year-on-year change in total value ───────────────────────────
111
+ ax2 = axes[1]
112
+ delta = acct.change_in_value()["Total"].dropna()
113
+ colors_bar = ["#2ecc71" if v >= 0 else "#e74c3c" for v in delta.values]
114
+ ax2.bar(delta.index, delta.values, color=colors_bar, alpha=0.85, width=0.7)
115
+ ax2.axhline(0, color="black", linewidth=0.8)
116
+ ax2.set_xlabel("Year", fontsize=10)
117
+ ax2.set_ylabel("Change in value (currency/yr)", fontsize=10)
118
+ ax2.set_title("Year-on-Year Change in Total Ecosystem Value", fontsize=11)
119
+ ax2.yaxis.set_major_formatter(mticker.FuncFormatter(
120
+ lambda x, _: f"{x/1e6:.1f}M" if abs(x) >= 1e6 else f"{x:,.0f}"
121
+ ))
122
+ ax2.grid(True, alpha=0.2, axis="y")
123
+
124
+ # ── Uncertainty band on panel 1 ───────────────────────────────────────────
125
+ unc = acct.uncertainty_summary().set_index("Year")
126
+ total = acct.total_value_by_class().sum(axis=1)
127
+ if not unc.empty:
128
+ ax1.fill_between(
129
+ unc.index, unc["Min value"], unc["Max value"],
130
+ alpha=0.12, color="grey", label="Min–Max range"
131
+ )
132
+ ax1.legend(loc="upper right", fontsize=8, framealpha=0.8)
133
+
134
+ plt.tight_layout()
135
+ out_path = out_dir / filename
136
+ fig.savefig(out_path, dpi=150, bbox_inches="tight")
137
+ plt.close(fig)
138
+ print(f" Saved: {out_path}")
139
+
140
+
141
+ # ── Plot: line chart per service type ─────────────────────────────────────────
142
+
143
+ def plot_value_by_service(
144
+ acct: SEEAAccount,
145
+ out_dir: Path,
146
+ filename: str = "seea_value_by_service.png",
147
+ ) -> None:
148
+ """
149
+ Line chart: total monetary value per service type over time.
150
+ One line per service (Provisioning / Regulating / Cultural).
151
+ """
152
+ out_dir.mkdir(parents=True, exist_ok=True)
153
+ mf = acct.monetary_flow_account()
154
+ years = mf.index.tolist()
155
+
156
+ # Aggregate by service type (top level of MultiIndex columns)
157
+ type_totals: dict[str, list[float]] = {}
158
+ for col in mf.columns:
159
+ stype = col[0]
160
+ type_totals.setdefault(stype, np.zeros(len(years)))
161
+ type_totals[stype] += mf[col].values
162
+
163
+ type_colors = {
164
+ "Provisioning": "#e67e22",
165
+ "Regulating": "#27ae60",
166
+ "Cultural": "#8e44ad",
167
+ }
168
+
169
+ fig, ax = plt.subplots(figsize=(12, 5))
170
+ for stype, vals in type_totals.items():
171
+ color = type_colors.get(stype, "#2c3e50")
172
+ ax.plot(years, vals, color=color, linewidth=2.5, label=stype, zorder=3)
173
+ ax.fill_between(years, 0, vals, color=color, alpha=0.08, zorder=2)
174
+
175
+ # Also plot individual services as thin dashed lines
176
+ for col in mf.columns:
177
+ stype = col[0]
178
+ sname = col[1]
179
+ color = type_colors.get(stype, "#2c3e50")
180
+ ax.plot(years, mf[col].values, color=color,
181
+ linewidth=0.8, linestyle="--", alpha=0.5,
182
+ label=f" {sname}")
183
+
184
+ ax.set_xlabel("Year", fontsize=10)
185
+ ax.set_ylabel("Value (currency/yr)", fontsize=10)
186
+ ax.set_title("Ecosystem Service Value by Service Type", fontsize=11)
187
+ ax.yaxis.set_major_formatter(mticker.FuncFormatter(
188
+ lambda x, _: f"{x/1e6:.1f}M" if x >= 1e6 else f"{x:,.0f}"
189
+ ))
190
+ ax.legend(loc="upper right", fontsize=8, framealpha=0.8, ncol=2)
191
+ ax.grid(True, alpha=0.2)
192
+
193
+ plt.tight_layout()
194
+ out_path = out_dir / filename
195
+ fig.savefig(out_path, dpi=150, bbox_inches="tight")
196
+ plt.close(fig)
197
+ print(f" Saved: {out_path}")
198
+
199
+
200
+ # ── Plot: transition heatmap ──────────────────────────────────────────────────
201
+
202
+ def plot_transition_heatmap(
203
+ acct: SEEAAccount,
204
+ out_dir: Path,
205
+ filename: str = "seea_transition_heatmap.png",
206
+ ) -> None:
207
+ """
208
+ Two-panel heatmap:
209
+ Left — area (ha) converted between classes (transition matrix)
210
+ Right — monetary value change from those conversions
211
+ """
212
+ out_dir.mkdir(parents=True, exist_ok=True)
213
+ tm = acct.transition_matrix()
214
+ vm = acct.value_change_matrix()
215
+
216
+ if tm.empty:
217
+ print(" [Skip] transition matrix empty — no heatmap generated")
218
+ return
219
+
220
+ fig, axes = plt.subplots(1, 2, figsize=(16, 6))
221
+
222
+ for ax, data, title, cmap, fmt in [
223
+ (axes[0], tm, "Area converted (ha)", "YlOrBr", ".1f"),
224
+ (axes[1], vm, "Value change (currency)", "RdYlGn", ".0f"),
225
+ ]:
226
+ arr = data.values.astype(float)
227
+
228
+ # Mask diagonal (no-change cells)
229
+ mask_diag = np.eye(arr.shape[0], dtype=bool)
230
+ arr_plot = np.where(mask_diag, np.nan, arr)
231
+
232
+ vmax = np.nanmax(np.abs(arr_plot)) if not np.all(np.isnan(arr_plot)) else 1
233
+ vmin = -vmax if cmap == "RdYlGn" else 0
234
+
235
+ im = ax.imshow(arr_plot, cmap=cmap, vmin=vmin, vmax=vmax, aspect="auto")
236
+ plt.colorbar(im, ax=ax, shrink=0.8)
237
+
238
+ labels = list(data.index)
239
+ ax.set_xticks(range(len(labels)))
240
+ ax.set_yticks(range(len(labels)))
241
+ ax.set_xticklabels(labels, rotation=45, ha="right", fontsize=8)
242
+ ax.set_yticklabels(labels, fontsize=8)
243
+ ax.set_xlabel("To class", fontsize=9)
244
+ ax.set_ylabel("From class", fontsize=9)
245
+ ax.set_title(title, fontsize=10)
246
+
247
+ # Annotate non-zero, non-diagonal cells
248
+ for i in range(arr.shape[0]):
249
+ for j in range(arr.shape[1]):
250
+ if not mask_diag[i, j] and arr[i, j] != 0:
251
+ ax.text(j, i, f"{arr[i,j]:{fmt}}",
252
+ ha="center", va="center", fontsize=7,
253
+ color="black")
254
+
255
+ plt.suptitle("Ecosystem Transition Matrix", fontsize=12, y=1.01)
256
+ plt.tight_layout()
257
+ out_path = out_dir / filename
258
+ fig.savefig(out_path, dpi=150, bbox_inches="tight")
259
+ plt.close(fig)
260
+ print(f" Saved: {out_path}")