Spaces:
Sleeping
Sleeping
Upload 5 files
#1
by
GiammaSoriano
- opened
- .gitattributes +1 -0
- Dockerfile +7 -15
- Requirements.txt +5 -0
- WR_PMP_MultiCalib_Policy (8).xlsx +3 -0
- app.py +564 -319
- crop_surface_subterraneotheft_parsed (1).xlsx +0 -0
.gitattributes
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
WR_PMP_MultiCalib_Policy[[:space:]](8).xlsx filter=lfs diff=lfs merge=lfs -text
|
Dockerfile
CHANGED
|
@@ -1,16 +1,8 @@
|
|
| 1 |
-
FROM python:3.
|
| 2 |
-
COPY --from=ghcr.io/astral-sh/uv:0.4.20 /uv /bin/uv
|
| 3 |
-
|
| 4 |
-
RUN useradd -m -u 1000 user
|
| 5 |
-
ENV PATH="/home/user/.local/bin:$PATH"
|
| 6 |
-
ENV UV_SYSTEM_PYTHON=1
|
| 7 |
-
|
| 8 |
WORKDIR /app
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
CMD ["gunicorn", "app:server", "--workers", "4", "--bind", "0.0.0.0:7860"]
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
WORKDIR /app
|
| 3 |
+
COPY requirements.txt /app/
|
| 4 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 5 |
+
COPY . /app
|
| 6 |
+
ENV PORT=7860
|
| 7 |
+
EXPOSE 7860
|
| 8 |
+
CMD ["gunicorn", "-b", "0.0.0.0:7860", "app:server"]
|
|
|
|
|
|
Requirements.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
dash==2.17.1
|
| 2 |
+
pandas
|
| 3 |
+
plotly
|
| 4 |
+
openpyxl
|
| 5 |
+
gunicorn
|
WR_PMP_MultiCalib_Policy (8).xlsx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:3b441e1cd5c950052db6628d95805a2a25401bdade0271261994d77f02de2b76
|
| 3 |
+
size 150119254
|
app.py
CHANGED
|
@@ -1,332 +1,577 @@
|
|
| 1 |
-
|
| 2 |
-
import
|
|
|
|
|
|
|
|
|
|
| 3 |
import plotly.express as px
|
| 4 |
-
|
| 5 |
-
from
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
server = app.server
|
| 10 |
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
fig.update_layout(
|
| 33 |
-
template="plotly_dark",
|
| 34 |
-
paper_bgcolor="rgba(0,0,0,0)",
|
| 35 |
-
plot_bgcolor="rgba(0,0,0,0)",
|
| 36 |
-
)
|
| 37 |
-
|
| 38 |
-
return fig
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
def create_line_chart(selected_country):
|
| 42 |
-
country_data = df[df["country"] == selected_country]
|
| 43 |
-
fig = px.line(
|
| 44 |
-
country_data,
|
| 45 |
-
x="year",
|
| 46 |
-
y="lifeExp",
|
| 47 |
-
title=f"{selected_country} - Life Expectancy",
|
| 48 |
-
)
|
| 49 |
-
fig.update_layout(
|
| 50 |
-
template="plotly_dark",
|
| 51 |
-
paper_bgcolor="rgba(0,0,0,0)",
|
| 52 |
-
plot_bgcolor="rgba(0,0,0,0)",
|
| 53 |
-
)
|
| 54 |
-
return fig
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
def create_bar_chart(selected_year):
|
| 58 |
-
year_data = df[df["year"] == selected_year]
|
| 59 |
-
continent_stats = year_data.groupby("continent")["lifeExp"].mean().reset_index()
|
| 60 |
-
fig = px.bar(
|
| 61 |
-
continent_stats,
|
| 62 |
-
x="continent",
|
| 63 |
-
y="lifeExp",
|
| 64 |
-
color="continent",
|
| 65 |
-
title=f"Average Life Expectancy by Continent ({selected_year})",
|
| 66 |
-
)
|
| 67 |
-
fig.update_layout(
|
| 68 |
-
template="plotly_dark",
|
| 69 |
-
paper_bgcolor="rgba(0,0,0,0)",
|
| 70 |
-
plot_bgcolor="rgba(0,0,0,0)",
|
| 71 |
-
showlegend=False,
|
| 72 |
-
)
|
| 73 |
-
return fig
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
def create_datacard(title, value, icon, color):
|
| 77 |
-
return dmc.Card(
|
| 78 |
-
[
|
| 79 |
-
dmc.Group(
|
| 80 |
-
[
|
| 81 |
-
DashIconify(icon=icon, width=30, color=color),
|
| 82 |
-
html.Div(
|
| 83 |
-
[
|
| 84 |
-
dmc.Text(value, size="xl", fw=700, c="white"),
|
| 85 |
-
dmc.Text(title, size="sm", c="dimmed"),
|
| 86 |
-
]
|
| 87 |
-
),
|
| 88 |
-
],
|
| 89 |
-
align="center",
|
| 90 |
-
gap="md",
|
| 91 |
-
)
|
| 92 |
-
],
|
| 93 |
-
p="md",
|
| 94 |
-
className="datacard",
|
| 95 |
-
)
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
app.layout = dmc.MantineProvider(
|
| 99 |
-
[
|
| 100 |
-
html.Link(
|
| 101 |
-
href="https://fonts.googleapis.com/css2?family=Outfit:wght@100..900&display=swap",
|
| 102 |
-
rel="stylesheet",
|
| 103 |
-
),
|
| 104 |
-
dmc.Group(
|
| 105 |
-
[
|
| 106 |
-
DashIconify(icon="twemoji:globe-with-meridians", width=45),
|
| 107 |
-
dmc.Text(
|
| 108 |
-
"Gapminder World Data Explorer", ml=10, size="xl", fw=900, c="white"
|
| 109 |
-
),
|
| 110 |
-
],
|
| 111 |
-
align="center",
|
| 112 |
-
className="header",
|
| 113 |
-
mb="md",
|
| 114 |
-
),
|
| 115 |
-
dmc.Grid(
|
| 116 |
-
[
|
| 117 |
-
dmc.GridCol(
|
| 118 |
-
[
|
| 119 |
-
dmc.Stack(
|
| 120 |
-
[
|
| 121 |
-
dmc.Card(
|
| 122 |
-
[
|
| 123 |
-
dmc.Text("Controls", size="lg", mb="md"),
|
| 124 |
-
dmc.Stack(
|
| 125 |
-
[
|
| 126 |
-
html.Div(
|
| 127 |
-
[
|
| 128 |
-
dmc.Text(
|
| 129 |
-
"Year:", size="sm", mb=5
|
| 130 |
-
),
|
| 131 |
-
dmc.Slider(
|
| 132 |
-
id="year-slider",
|
| 133 |
-
min=1952,
|
| 134 |
-
max=2007,
|
| 135 |
-
step=5,
|
| 136 |
-
value=2007,
|
| 137 |
-
marks=[
|
| 138 |
-
{
|
| 139 |
-
"value": year,
|
| 140 |
-
"label": str(year),
|
| 141 |
-
}
|
| 142 |
-
for year in [
|
| 143 |
-
1952,
|
| 144 |
-
1967,
|
| 145 |
-
1982,
|
| 146 |
-
1997,
|
| 147 |
-
2007,
|
| 148 |
-
]
|
| 149 |
-
],
|
| 150 |
-
),
|
| 151 |
-
]
|
| 152 |
-
),
|
| 153 |
-
html.Div(
|
| 154 |
-
[
|
| 155 |
-
dmc.Text(
|
| 156 |
-
"Continent Filter:",
|
| 157 |
-
size="sm",
|
| 158 |
-
mb=5,
|
| 159 |
-
),
|
| 160 |
-
dmc.Select(
|
| 161 |
-
id="continent-dropdown",
|
| 162 |
-
data=[
|
| 163 |
-
{
|
| 164 |
-
"value": "All",
|
| 165 |
-
"label": "All Continents",
|
| 166 |
-
}
|
| 167 |
-
]
|
| 168 |
-
+ [
|
| 169 |
-
{
|
| 170 |
-
"value": cont,
|
| 171 |
-
"label": cont,
|
| 172 |
-
}
|
| 173 |
-
for cont in sorted(
|
| 174 |
-
df[
|
| 175 |
-
"continent"
|
| 176 |
-
].unique()
|
| 177 |
-
)
|
| 178 |
-
],
|
| 179 |
-
value="All",
|
| 180 |
-
),
|
| 181 |
-
]
|
| 182 |
-
),
|
| 183 |
-
html.Div(
|
| 184 |
-
[
|
| 185 |
-
dmc.Text(
|
| 186 |
-
"Select Country:",
|
| 187 |
-
size="sm",
|
| 188 |
-
mb=5,
|
| 189 |
-
),
|
| 190 |
-
dmc.Select(
|
| 191 |
-
id="country-dropdown",
|
| 192 |
-
data=[
|
| 193 |
-
{
|
| 194 |
-
"value": country,
|
| 195 |
-
"label": country,
|
| 196 |
-
}
|
| 197 |
-
for country in sorted(
|
| 198 |
-
df[
|
| 199 |
-
"country"
|
| 200 |
-
].unique()
|
| 201 |
-
)
|
| 202 |
-
],
|
| 203 |
-
value="United States",
|
| 204 |
-
searchable=True,
|
| 205 |
-
),
|
| 206 |
-
]
|
| 207 |
-
),
|
| 208 |
-
],
|
| 209 |
-
gap="lg",
|
| 210 |
-
),
|
| 211 |
-
],
|
| 212 |
-
p="md",
|
| 213 |
-
className="control-card",
|
| 214 |
-
)
|
| 215 |
-
]
|
| 216 |
-
)
|
| 217 |
-
],
|
| 218 |
-
span=3,
|
| 219 |
-
),
|
| 220 |
-
dmc.GridCol(
|
| 221 |
-
[
|
| 222 |
-
dmc.Stack(
|
| 223 |
-
[
|
| 224 |
-
html.Div(id="stats-cards"),
|
| 225 |
-
dmc.Card(
|
| 226 |
-
[dcc.Graph(id="scatter-plot")],
|
| 227 |
-
p="sm",
|
| 228 |
-
className="chart-card",
|
| 229 |
-
),
|
| 230 |
-
],
|
| 231 |
-
gap="md",
|
| 232 |
-
)
|
| 233 |
-
],
|
| 234 |
-
span=9,
|
| 235 |
-
),
|
| 236 |
-
],
|
| 237 |
-
gutter="md",
|
| 238 |
-
),
|
| 239 |
-
dmc.Grid(
|
| 240 |
-
[
|
| 241 |
-
dmc.GridCol(
|
| 242 |
-
[
|
| 243 |
-
dmc.Card(
|
| 244 |
-
[dcc.Graph(id="line-chart")], p="sm", className="chart-card"
|
| 245 |
-
)
|
| 246 |
-
],
|
| 247 |
-
span=6,
|
| 248 |
-
),
|
| 249 |
-
dmc.GridCol(
|
| 250 |
-
[
|
| 251 |
-
dmc.Card(
|
| 252 |
-
[dcc.Graph(id="bar-chart")], p="sm", className="chart-card"
|
| 253 |
-
)
|
| 254 |
-
],
|
| 255 |
-
span=6,
|
| 256 |
-
),
|
| 257 |
-
],
|
| 258 |
-
gutter="md",
|
| 259 |
-
mt="md",
|
| 260 |
),
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 264 |
)
|
| 265 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 266 |
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 270 |
)
|
| 271 |
-
def
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
"
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
)
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 329 |
|
|
|
|
| 330 |
|
|
|
|
| 331 |
if __name__ == "__main__":
|
| 332 |
-
|
|
|
|
|
|
| 1 |
+
# app.py
|
| 2 |
+
import os
|
| 3 |
+
import re
|
| 4 |
+
import pandas as pd
|
| 5 |
+
import plotly.graph_objs as go
|
| 6 |
import plotly.express as px
|
| 7 |
+
import dash
|
| 8 |
+
from dash import dcc, html, Input, Output
|
| 9 |
+
|
| 10 |
+
# ========= CONFIG =========
|
| 11 |
+
EXCEL_MAIN = "WR_PMP_MultiCalib_Policy.xlsx"
|
| 12 |
+
EXCEL_MUN = "crop_surface_subterraneotheft_parsed.xlsx"
|
| 13 |
+
BASE_YEAR = 2015 # se assente, fallback = primo anno del gruppo
|
| 14 |
+
|
| 15 |
+
SCENARIO_DESCR = {
|
| 16 |
+
"bau": "Business-as-usual: evoluzione osservata, senza nuove misure restrittive.",
|
| 17 |
+
"scenario2": "Scenario 2: restrizioni/progressive o pricing moderato.",
|
| 18 |
+
"scenario5": "Scenario 5: regolazione stringente con segnali di prezzo più forti.",
|
| 19 |
+
"nwt": "No Water Theft: pieno enforcement e azzeramento dei prelievi illegali.",
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
# ========= HELPERS =========
|
| 23 |
+
def _first_existing_column(df, candidates, required=True):
|
| 24 |
+
for c in candidates:
|
| 25 |
+
if c in df.columns:
|
| 26 |
+
return c
|
| 27 |
+
if required:
|
| 28 |
+
raise KeyError(f"Nessuna colonna trovata tra: {candidates}")
|
| 29 |
+
return None
|
| 30 |
+
|
| 31 |
+
def _find_column_regex(df, pattern, required=False):
|
| 32 |
+
"""Find first column matching regex (case-insensitive)."""
|
| 33 |
+
pat = re.compile(pattern, re.I)
|
| 34 |
+
for col in df.columns:
|
| 35 |
+
if pat.search(str(col)):
|
| 36 |
+
return col
|
| 37 |
+
if required:
|
| 38 |
+
raise KeyError(f"Nessuna colonna che combacia con regex: {pattern}")
|
| 39 |
+
return None
|
| 40 |
+
|
| 41 |
+
def municipio_label(code, mapper):
|
| 42 |
+
if pd.isna(code):
|
| 43 |
+
return "NA"
|
| 44 |
+
code = int(code)
|
| 45 |
+
name = mapper.get(code, str(code))
|
| 46 |
+
return f"{name} ({code})"
|
| 47 |
+
|
| 48 |
+
def is_donana(v):
|
| 49 |
+
return v == "DONANA"
|
| 50 |
+
|
| 51 |
+
def weighted_mean(group_df, value_col, weights_df):
|
| 52 |
+
g = group_df.merge(weights_df, on="Municipio", how="left")
|
| 53 |
+
g = g.dropna(subset=[value_col, "Hectares"])
|
| 54 |
+
denom = g["Hectares"].sum()
|
| 55 |
+
if denom == 0:
|
| 56 |
+
return None
|
| 57 |
+
return (g[value_col] * g["Hectares"]).sum() / denom
|
| 58 |
+
|
| 59 |
+
# ========= LOAD WR MAIN =========
|
| 60 |
+
def load_wr():
|
| 61 |
+
xf = pd.ExcelFile(EXCEL_MAIN, engine="openpyxl")
|
| 62 |
+
water_sheets = [s for s in xf.sheet_names if s.startswith("WaterSimulations_")]
|
| 63 |
+
crop_sheets = [s for s in xf.sheet_names if s.startswith("CropWaterSim_")]
|
| 64 |
+
if not water_sheets:
|
| 65 |
+
raise ValueError("Nessun foglio 'WaterSimulations_*' trovato.")
|
| 66 |
+
if not crop_sheets:
|
| 67 |
+
raise ValueError("Nessun foglio 'CropWaterSim_*' trovato.")
|
| 68 |
+
|
| 69 |
+
df_gm = pd.concat([pd.read_excel(xf, s) for s in water_sheets], ignore_index=True)
|
| 70 |
+
df_crp = pd.concat([pd.read_excel(xf, s) for s in crop_sheets], ignore_index=True)
|
| 71 |
+
|
| 72 |
+
# Normalizza colonne base
|
| 73 |
+
df_gm = df_gm.rename(columns={"Policy Scenario": "Scenario"})
|
| 74 |
+
df_crp = df_crp.rename(columns={"Policy Scenario": "Scenario"})
|
| 75 |
+
for d in (df_gm, df_crp):
|
| 76 |
+
d["Scenario"] = d["Scenario"].astype(str).str.lower()
|
| 77 |
+
d["Municipio"] = pd.to_numeric(d["Municipio"], errors="coerce").astype("Int64")
|
| 78 |
+
d["Year"] = pd.to_numeric(d["Year"], errors="coerce").astype("Int64")
|
| 79 |
+
|
| 80 |
+
# MainInfo -> ettari per municipio (pesi)
|
| 81 |
+
if "MainInfo" not in xf.sheet_names:
|
| 82 |
+
raise ValueError("Foglio 'MainInfo' non trovato in WR_PMP_MultiCalib_Policy.xlsx")
|
| 83 |
+
df_main = pd.read_excel(xf, sheet_name="MainInfo").rename(columns={"Policy Scenario": "Scenario"})
|
| 84 |
+
df_main["Scenario"] = df_main["Scenario"].astype(str).str.lower()
|
| 85 |
+
df_main["Municipio"] = pd.to_numeric(df_main["Municipio"], errors="coerce").astype("Int64")
|
| 86 |
+
hect_col = _first_existing_column(df_main, ["Total hectares", "Total Hectares", "Hectares"])
|
| 87 |
+
weights = (df_main.groupby("Municipio", as_index=False)[hect_col]
|
| 88 |
+
.first()
|
| 89 |
+
.rename(columns={hect_col: "Hectares"}))
|
| 90 |
+
return df_gm, df_crp, weights
|
| 91 |
|
| 92 |
+
df_gm, df_crop, w_hect = load_wr()
|
| 93 |
+
df_crop["Crop"] = df_crop["Crop"].astype(str).str.strip()
|
| 94 |
+
KNOWN_CROPS = set(df_crop["Crop"].dropna().astype(str).str.strip().unique())
|
| 95 |
|
| 96 |
+
# ========= MUNICIPIO NAMES + WATER REQUIREMENT (robusto) =========
|
| 97 |
+
def load_municipio_names_and_wr(known_crops):
|
| 98 |
+
"""
|
| 99 |
+
Dal file 'crop_surface_subterraneotheft_parsed.xlsx':
|
| 100 |
+
- Mapping codice -> nome municipio
|
| 101 |
+
- Water requirement per crop×municipio = total water / total superficie [m3/ha]
|
| 102 |
+
Robusto a header diversi: cerca con regex e, se manca la colonna 'crop',
|
| 103 |
+
la deduce confrontando valori con i crop di df_crop.
|
| 104 |
+
"""
|
| 105 |
+
try:
|
| 106 |
+
mxf = pd.ExcelFile(EXCEL_MUN, engine="openpyxl")
|
| 107 |
+
df = pd.read_excel(mxf, sheet_name=mxf.sheet_names[0])
|
| 108 |
+
except Exception as e:
|
| 109 |
+
print(f"[WR-LOAD] Impossibile leggere {EXCEL_MUN}: {e}")
|
| 110 |
+
return {}, pd.DataFrame(columns=["Municipio","Crop","wr_m3_ha"])
|
| 111 |
+
|
| 112 |
+
# municipio code col (regex per 'municip')
|
| 113 |
+
code_col = _find_column_regex(df, r"(id|cod|codigo).*(munic|muni)", required=False)
|
| 114 |
+
if code_col is None:
|
| 115 |
+
# fallback: prova una lista ampia
|
| 116 |
+
try:
|
| 117 |
+
code_col = _first_existing_column(df, [
|
| 118 |
+
"ID PROVINCIA/MUNICIPIO","ID_PROVINCIA/MUNICIPIO","ID MUNICIPIO","ID_MUNICIPIO",
|
| 119 |
+
"MUNICIPIO_ID","COD_MUNICIPIO","CODIGO_MUNICIPIO","COD MUNICIPIO","CODIGO MUNICIPIO"
|
| 120 |
+
])
|
| 121 |
+
except Exception:
|
| 122 |
+
# ultima spiaggia: qualunque colonna int-like con pochi NaN
|
| 123 |
+
int_candidates = [c for c in df.columns if pd.api.types.is_integer_dtype(pd.to_numeric(df[c], errors="ignore"))]
|
| 124 |
+
code_col = int_candidates[0] if int_candidates else df.columns[0]
|
| 125 |
+
|
| 126 |
+
# municipio name col
|
| 127 |
+
name_col = _find_column_regex(df, r"(munic|municipios|nombre.*munic)", required=False)
|
| 128 |
+
if name_col is None:
|
| 129 |
+
name_col = _first_existing_column(df, [
|
| 130 |
+
"MUNICIPIOS","Municipio","NOMBRE_MUNICIPIO","NOMBRE MUNICIPIO","MUNICIPIO"
|
| 131 |
+
], required=False)
|
| 132 |
+
|
| 133 |
+
# crop col (regex ampia: crop|cult|variedad|especie)
|
| 134 |
+
crop_col = _find_column_regex(df, r"(crop|cultiv|coltur|variedad|especie|cult)", required=False)
|
| 135 |
+
if crop_col is None:
|
| 136 |
+
# prova elenco esteso
|
| 137 |
+
for c in ["Crop","CROP","Cultivo","CULTIVO","CULTURE","Cultivar","Variedad","Especie","Nombre cultivo","Nombre Cultivo","CULTIVO NOMBRE"]:
|
| 138 |
+
if c in df.columns:
|
| 139 |
+
crop_col = c
|
| 140 |
+
break
|
| 141 |
+
if crop_col is None:
|
| 142 |
+
# detezione per matching con KNOWN_CROPS
|
| 143 |
+
best_col, best_overlap = None, -1
|
| 144 |
+
cropy_cols = [c for c in df.columns if df[c].dtype == object]
|
| 145 |
+
for c in cropy_cols:
|
| 146 |
+
vals = set(df[c].dropna().astype(str).str.strip().unique())
|
| 147 |
+
overlap = len(vals & known_crops)
|
| 148 |
+
if overlap > best_overlap:
|
| 149 |
+
best_overlap, best_col = overlap, c
|
| 150 |
+
if best_col is not None and best_overlap > 0:
|
| 151 |
+
crop_col = best_col
|
| 152 |
+
else:
|
| 153 |
+
print("[WR-LOAD] Colonna 'Crop' non trovata. Disabilito il grafico 'Total water consumption'.")
|
| 154 |
+
# costruiamo comunque il mapper dei municipi se possibile:
|
| 155 |
+
mapper = {}
|
| 156 |
+
try:
|
| 157 |
+
dd = df[[code_col, name_col]].copy() if name_col else df[[code_col]].copy()
|
| 158 |
+
dd[code_col] = pd.to_numeric(dd[code_col], errors="coerce").astype("Int64")
|
| 159 |
+
if name_col:
|
| 160 |
+
mapper = dict(zip(dd[code_col], dd[name_col]))
|
| 161 |
+
except Exception as _:
|
| 162 |
+
pass
|
| 163 |
+
return mapper, pd.DataFrame(columns=["Municipio","Crop","wr_m3_ha"])
|
| 164 |
+
|
| 165 |
+
# superficie & water col
|
| 166 |
+
sup_col = _find_column_regex(df, r"(total\s*)?(superf|ha)", required=False)
|
| 167 |
+
wat_col = _find_column_regex(df, r"(total\s*)?(agua|water|m3)", required=False)
|
| 168 |
+
if sup_col is None:
|
| 169 |
+
sup_col = _first_existing_column(df, [
|
| 170 |
+
"total superficie","Total Superficie","TOTAL_SUPERFICIE","Superficie total","SUPERFICIE TOTAL",
|
| 171 |
+
"Superficie","SUPERFICIE","Hectareas","Hectáreas","ha","HA"
|
| 172 |
+
], required=False)
|
| 173 |
+
if wat_col is None:
|
| 174 |
+
wat_col = _first_existing_column(df, [
|
| 175 |
+
"total water","Total Water","TOTAL_WATER","Agua total","Total Agua","TOTAL AGUA",
|
| 176 |
+
"m3","M3","m³","M³","Volumen","VOLUMEN"
|
| 177 |
+
], required=False)
|
| 178 |
+
|
| 179 |
+
if sup_col is None or wat_col is None:
|
| 180 |
+
print("[WR-LOAD] Colonne 'total superficie' o 'total water' non trovate. Disabilito il grafico consumo.")
|
| 181 |
+
sup_col = sup_col or df.columns[0]
|
| 182 |
+
wat_col = wat_col or df.columns[1]
|
| 183 |
+
|
| 184 |
+
# mapper municipio (se possibile)
|
| 185 |
+
mapper = {}
|
| 186 |
+
try:
|
| 187 |
+
dd = df[[code_col, name_col]].copy() if name_col else df[[code_col]].copy()
|
| 188 |
+
dd[code_col] = pd.to_numeric(dd[code_col], errors="coerce").astype("Int64")
|
| 189 |
+
if name_col:
|
| 190 |
+
mapper = dict(zip(dd[code_col], dd[name_col]))
|
| 191 |
+
except Exception as e:
|
| 192 |
+
print(f"[WR-LOAD] Impossibile costruire il mapper municipio: {e}")
|
| 193 |
+
|
| 194 |
+
# build wr table
|
| 195 |
+
tmp = df[[code_col, crop_col, sup_col, wat_col]].copy()
|
| 196 |
+
tmp[code_col] = pd.to_numeric(tmp[code_col], errors="coerce").astype("Int64")
|
| 197 |
+
tmp = tmp.dropna(subset=[code_col, crop_col, sup_col, wat_col])
|
| 198 |
+
tmp = tmp[pd.to_numeric(tmp[sup_col], errors="coerce") > 0]
|
| 199 |
+
tmp["wr_m3_ha"] = pd.to_numeric(tmp[wat_col], errors="coerce") / pd.to_numeric(tmp[sup_col], errors="coerce")
|
| 200 |
+
wr = tmp[[code_col, crop_col, "wr_m3_ha"]].rename(columns={code_col:"Municipio", crop_col:"Crop"})
|
| 201 |
+
wr["Crop"] = wr["Crop"].astype(str).str.strip()
|
| 202 |
+
return mapper, wr
|
| 203 |
+
|
| 204 |
+
mun_code_to_name, df_wr = load_municipio_names_and_wr(KNOWN_CROPS)
|
| 205 |
+
|
| 206 |
+
# ========= GM % vs base =========
|
| 207 |
+
group_keys = ["Scenario", "Municipio", "Method", "AM", "CM", "RCP"]
|
| 208 |
+
gm_base_2015 = (df_gm[df_gm["Year"].eq(BASE_YEAR)]
|
| 209 |
+
.groupby(group_keys)["GM_PMP"].first().rename("GM_base_2015"))
|
| 210 |
+
first_year_base = (df_gm.sort_values("Year")
|
| 211 |
+
.groupby(group_keys)["GM_PMP"].first().rename("GM_base_fallback"))
|
| 212 |
+
gm_merged = (df_gm.merge(gm_base_2015, on=group_keys, how="left")
|
| 213 |
+
.merge(first_year_base, on=group_keys, how="left"))
|
| 214 |
+
gm_merged["GM_base"] = gm_merged["GM_base_2015"].fillna(gm_merged["GM_base_fallback"])
|
| 215 |
+
gm_merged["GM_perc"] = 100.0 * gm_merged["GM_PMP"] / gm_merged["GM_base"]
|
| 216 |
+
|
| 217 |
+
# ========= Water availability vs BAU baseline (100%) =========
|
| 218 |
+
is_bau = df_gm["Scenario"].eq("bau")
|
| 219 |
+
bau_first = (df_gm[is_bau].sort_values("Year")
|
| 220 |
+
.groupby(["Municipio","Method","AM","CM","RCP"], as_index=False)
|
| 221 |
+
.first()[["Municipio","Method","AM","CM","RCP","Water_PMP"]]
|
| 222 |
+
.rename(columns={"Water_PMP":"Water_BAU_base"}))
|
| 223 |
+
water_all = df_gm.merge(bau_first, on=["Municipio","Method","AM","CM","RCP"], how="left")
|
| 224 |
+
water_all["Water_rel"] = 100.0 * water_all["Water_PMP"] / water_all["Water_BAU_base"]
|
| 225 |
+
|
| 226 |
+
# ========= DASH APP =========
|
| 227 |
+
external_stylesheets = ["https://fonts.googleapis.com/css2?family=Montserrat:wght@500;700&display=swap"]
|
| 228 |
+
app = dash.Dash(__name__, external_stylesheets=external_stylesheets)
|
| 229 |
server = app.server
|
| 230 |
|
| 231 |
+
# Dropdown values
|
| 232 |
+
scenarios = sorted(gm_merged["Scenario"].dropna().unique())
|
| 233 |
+
rcps = sorted(gm_merged["RCP"].dropna().unique())
|
| 234 |
+
methods = sorted(gm_merged["Method"].dropna().unique())
|
| 235 |
+
mun_codes = sorted(gm_merged["Municipio"].dropna().unique())
|
| 236 |
+
mun_options = [{"label": "Doñana (media pesata)", "value": "DONANA"}] + [
|
| 237 |
+
{"label": municipio_label(c, mun_code_to_name), "value": int(c)} for c in mun_codes
|
| 238 |
+
]
|
| 239 |
+
methods_options = [{"label": "Tutti", "value": "Tutti"}] + [{"label": m, "value": m} for m in methods]
|
| 240 |
+
|
| 241 |
+
app.layout = html.Div(
|
| 242 |
+
style={"fontFamily": "'Montserrat', sans-serif", "backgroundColor": "#f5f7fa", "minHeight": "100vh"},
|
| 243 |
+
children=[
|
| 244 |
+
html.Div(
|
| 245 |
+
style={"background": "linear-gradient(90deg,#0a3967 0,#009ee0 100%)","padding": "22px 0 14px 0",
|
| 246 |
+
"marginBottom": "16px","textAlign": "center","color": "white","boxShadow": "0 6px 20px rgba(10,57,103,.19)"},
|
| 247 |
+
children=[
|
| 248 |
+
html.H1("🌱 Water Reallocation PMP Dashboard 👨🌾",
|
| 249 |
+
style={"fontWeight": "700", "fontSize": "2.3rem", "margin": "0"}),
|
| 250 |
+
html.H4("WWF Doñana & IMDEA Water", style={"fontWeight": "500", "marginTop": "6px"})
|
| 251 |
+
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 252 |
),
|
| 253 |
+
|
| 254 |
+
# Controls + Scenario description
|
| 255 |
+
html.Div([
|
| 256 |
+
html.Div([
|
| 257 |
+
html.Label("Policy Scenario:", style={"marginRight": "8px", "fontWeight": "600"}),
|
| 258 |
+
dcc.Dropdown(id='scenario', options=[{"label": s.upper(), "value": s} for s in scenarios],
|
| 259 |
+
value=scenarios[0] if scenarios else None, clearable=False,
|
| 260 |
+
style={'width': '180px','display': 'inline-block'}),
|
| 261 |
+
|
| 262 |
+
html.Label("Municipio:", style={"marginLeft": "18px","marginRight":"8px","fontWeight":"600"}),
|
| 263 |
+
dcc.Dropdown(id='municipio', options=mun_options, value="DONANA", clearable=False,
|
| 264 |
+
style={'width': '300px','display': 'inline-block'}),
|
| 265 |
+
|
| 266 |
+
html.Label("RCP:", style={"marginLeft": "18px","marginRight":"8px","fontWeight":"600"}),
|
| 267 |
+
dcc.Dropdown(id='rcp', options=[{"label": r, "value": r} for r in rcps],
|
| 268 |
+
value=rcps[0] if rcps else None, clearable=False,
|
| 269 |
+
style={'width': '150px','display': 'inline-block'}),
|
| 270 |
+
|
| 271 |
+
html.Label("Method:", style={"marginLeft": "18px","marginRight":"8px","fontWeight":"600"}),
|
| 272 |
+
dcc.Dropdown(id='method', options=methods_options, value="Tutti", clearable=False,
|
| 273 |
+
style={'width': '160px','display': 'inline-block'}),
|
| 274 |
+
], style={'padding': '16px 18px 10px 18px', "background":"#fff","borderRadius":"10px",
|
| 275 |
+
"boxShadow":"0 2px 12px rgba(10,57,103,.09)", "margin":"0 14px"}),
|
| 276 |
+
|
| 277 |
+
html.Div(id="scenario-desc",
|
| 278 |
+
style={"margin":"8px 14px 0 14px","background":"#fff","borderRadius":"10px",
|
| 279 |
+
"boxShadow":"0 2px 12px rgba(10,57,103,.08)","padding":"10px 14px",
|
| 280 |
+
"fontSize":"0.95rem","color":"#113"})
|
| 281 |
+
]),
|
| 282 |
+
|
| 283 |
+
# Row 1: GM trend / Crop stacked
|
| 284 |
+
html.Div([
|
| 285 |
+
html.Div([
|
| 286 |
+
html.Div([
|
| 287 |
+
html.H4("Gross Margin Trend (% vs base)"),
|
| 288 |
+
dcc.Graph(id='gm-graph', config={"displayModeBar": False})
|
| 289 |
+
], style={"background":"#fff","borderRadius":"12px","boxShadow":"0 2px 10px rgba(10,57,103,.10)",
|
| 290 |
+
"padding":"16px","margin":"0 10px"})
|
| 291 |
+
], style={'width':'49%','display':'inline-block','verticalAlign':'top'}),
|
| 292 |
+
|
| 293 |
+
html.Div([
|
| 294 |
+
html.Div([
|
| 295 |
+
html.H4("Allocated Area by Crop (stacked)"),
|
| 296 |
+
dcc.Graph(id='crop-graph', config={"displayModeBar": False})
|
| 297 |
+
], style={"background":"#fff","borderRadius":"12px","boxShadow":"0 2px 10px rgba(10,57,103,.10)",
|
| 298 |
+
"padding":"16px","margin":"0 10px"})
|
| 299 |
+
], style={'width':'49%','display':'inline-block','verticalAlign':'top'}),
|
| 300 |
+
], style={'width':'100%','paddingTop':'6px'}),
|
| 301 |
+
|
| 302 |
+
# Row 2: Water availability vs BAU baseline (100%) + Total water consumption
|
| 303 |
+
html.Div([
|
| 304 |
+
html.Div([
|
| 305 |
+
html.H4("Water availability vs BAU baseline (100%)"),
|
| 306 |
+
dcc.Graph(id='water-graph', config={"displayModeBar": False})
|
| 307 |
+
], style={"background":"#fff","borderRadius":"12px","boxShadow":"0 2px 10px rgba(10,57,103,.10)",
|
| 308 |
+
"padding":"16px","margin":"10px 24px"}),
|
| 309 |
+
|
| 310 |
+
html.Div([
|
| 311 |
+
html.H4("Total water consumption (m³)"),
|
| 312 |
+
dcc.Graph(id='cons-graph', config={"displayModeBar": False})
|
| 313 |
+
], style={"background":"#fff","borderRadius":"12px","boxShadow":"0 2px 10px rgba(10,57,103,.10)",
|
| 314 |
+
"padding":"16px","margin":"10px 24px"})
|
| 315 |
+
])
|
| 316 |
+
]
|
| 317 |
)
|
| 318 |
|
| 319 |
+
# ========= CORE: compute total water consumption =========
|
| 320 |
+
def compute_total_water_timeseries(selected_scenario, selected_mun, selected_rcp, selected_method):
|
| 321 |
+
"""
|
| 322 |
+
Calcola (per anno) il consumo idrico assoluto:
|
| 323 |
+
sum_{municipio,crop} [ share(PMP Area) * Hectares(Municipio) * wr_m3_ha(Crop,Municipio) ].
|
| 324 |
+
Media su AM (mean) + banda min/max.
|
| 325 |
+
Se df_wr è vuoto (non rilevato), ritorna None.
|
| 326 |
+
"""
|
| 327 |
+
if df_wr is None or df_wr.empty:
|
| 328 |
+
return None
|
| 329 |
+
|
| 330 |
+
# 1) filtro crop portfolio
|
| 331 |
+
crp = df_crop[(df_crop["Scenario"]==selected_scenario) &
|
| 332 |
+
(df_crop["RCP"]==selected_rcp)].copy()
|
| 333 |
+
if selected_method != "Tutti":
|
| 334 |
+
crp = crp[crp["Method"]==selected_method]
|
| 335 |
|
| 336 |
+
# per Doñana includo tutti i municipi; altrimenti 1 municipio
|
| 337 |
+
if not is_donana(selected_mun):
|
| 338 |
+
code = int(selected_mun)
|
| 339 |
+
crp = crp[crp["Municipio"]==code].copy()
|
| 340 |
+
|
| 341 |
+
if crp.empty:
|
| 342 |
+
return None
|
| 343 |
+
|
| 344 |
+
# 2) media su CM (e su Method se "Tutti") → manteniamo AM per banda min-max
|
| 345 |
+
crp_mean = (crp.groupby(["Year","AM","Municipio","Crop"], as_index=False)["PMP Area"]
|
| 346 |
+
.mean())
|
| 347 |
+
# 3) merge con ettari e water requirement
|
| 348 |
+
crp_mean = crp_mean.merge(w_hect, on="Municipio", how="left")
|
| 349 |
+
crp_mean = crp_mean.merge(df_wr, on=["Municipio","Crop"], how="left")
|
| 350 |
+
|
| 351 |
+
crp_mean = crp_mean.dropna(subset=["Hectares","wr_m3_ha","PMP Area"])
|
| 352 |
+
if crp_mean.empty:
|
| 353 |
+
return None
|
| 354 |
+
|
| 355 |
+
# 4) acqua assoluta = share * Ha * m3/ha
|
| 356 |
+
crp_mean["water_m3"] = crp_mean["PMP Area"] * crp_mean["Hectares"] * crp_mean["wr_m3_ha"]
|
| 357 |
+
|
| 358 |
+
# 5) aggregazione per anno & AM (somma su municipi e crop)
|
| 359 |
+
agg = (crp_mean.groupby(["Year","AM"], as_index=False)["water_m3"].sum())
|
| 360 |
+
|
| 361 |
+
# 6) riassunto su AM → mean / min / max per banda
|
| 362 |
+
summary = agg.groupby("Year")["water_m3"].agg(["mean","min","max"]).reset_index()
|
| 363 |
+
return summary
|
| 364 |
+
|
| 365 |
+
# ========= CALLBACK =========
|
| 366 |
+
@app.callback(
|
| 367 |
+
Output("scenario-desc","children"),
|
| 368 |
+
Output("gm-graph","figure"),
|
| 369 |
+
Output("crop-graph","figure"),
|
| 370 |
+
Output("water-graph","figure"),
|
| 371 |
+
Output("cons-graph","figure"),
|
| 372 |
+
Input("scenario","value"),
|
| 373 |
+
Input("municipio","value"),
|
| 374 |
+
Input("rcp","value"),
|
| 375 |
+
Input("method","value")
|
| 376 |
)
|
| 377 |
+
def update_all(selected_scenario, selected_mun, selected_rcp, selected_method):
|
| 378 |
+
# ---------- 1) Descrizione scenario ----------
|
| 379 |
+
desc = SCENARIO_DESCR.get(selected_scenario, "Scenario non documentato.")
|
| 380 |
+
desc_div = html.Div([html.B(selected_scenario.upper()+": "), html.Span(desc)])
|
| 381 |
+
|
| 382 |
+
# ---------- 2) Filtri base ----------
|
| 383 |
+
gmp = gm_merged[(gm_merged["Scenario"]==selected_scenario) &
|
| 384 |
+
(gm_merged["RCP"]==selected_rcp)].copy()
|
| 385 |
+
crp = df_crop[(df_crop["Scenario"]==selected_scenario) &
|
| 386 |
+
(df_crop["RCP"]==selected_rcp)].copy()
|
| 387 |
+
wdf = water_all[(water_all["Scenario"]==selected_scenario) &
|
| 388 |
+
(water_all["RCP"]==selected_rcp)].copy()
|
| 389 |
+
if selected_method != "Tutti":
|
| 390 |
+
gmp = gmp[gmp["Method"]==selected_method]
|
| 391 |
+
crp = crp[crp["Method"]==selected_method]
|
| 392 |
+
wdf = wdf[wdf["Method"]==selected_method]
|
| 393 |
+
|
| 394 |
+
# ---------- 3) GM Trend (% vs base) — y-range [0,100] ----------
|
| 395 |
+
if is_donana(selected_mun):
|
| 396 |
+
tmp = gmp.dropna(subset=["GM_perc"]).copy()
|
| 397 |
+
if tmp.empty:
|
| 398 |
+
fig_gm = go.Figure().update_layout(title="No data", template="plotly_white",
|
| 399 |
+
yaxis=dict(range=[0,100]))
|
| 400 |
+
else:
|
| 401 |
+
wm = (tmp.groupby(["Year","AM"])
|
| 402 |
+
.apply(lambda x: weighted_mean(x, "GM_perc", w_hect))
|
| 403 |
+
.reset_index(name="GM_perc_w")
|
| 404 |
+
.dropna(subset=["GM_perc_w"]))
|
| 405 |
+
summary = wm.groupby("Year")["GM_perc_w"].agg(["mean","min","max"]).reset_index()
|
| 406 |
+
fig_gm = go.Figure()
|
| 407 |
+
fig_gm.add_trace(go.Scatter(x=summary["Year"], y=summary["mean"], mode="lines+markers",
|
| 408 |
+
name="GM Trend", line=dict(width=3)))
|
| 409 |
+
fig_gm.add_trace(go.Scatter(
|
| 410 |
+
x=summary["Year"].tolist()+summary["Year"][::-1].tolist(),
|
| 411 |
+
y=summary["max"].tolist()+summary["min"][::-1].tolist(),
|
| 412 |
+
fill="toself", fillcolor="rgba(9,103,174,0.13)",
|
| 413 |
+
line=dict(color="rgba(255,255,255,0)"), hoverinfo="skip",
|
| 414 |
+
showlegend=True, name="Range (Min–Max)"
|
| 415 |
+
))
|
| 416 |
+
fig_gm.add_hline(y=100, line_dash="dash", line_color="#8e8e8e",
|
| 417 |
+
annotation_text="Baseline", annotation_position="top left")
|
| 418 |
+
fig_gm.update_layout(yaxis_title="Gross Margin [% vs base]", xaxis_title="Year",
|
| 419 |
+
template="plotly_white", hovermode="x unified",
|
| 420 |
+
legend=dict(bgcolor="rgba(255,255,255,0.88)"),
|
| 421 |
+
yaxis=dict(range=[0,100]))
|
| 422 |
+
else:
|
| 423 |
+
code = int(selected_mun)
|
| 424 |
+
tmp = gmp[gmp["Municipio"]==code].copy().sort_values(["AM","Year"])
|
| 425 |
+
if tmp.empty:
|
| 426 |
+
fig_gm = go.Figure().update_layout(title="No data", template="plotly_white",
|
| 427 |
+
yaxis=dict(range=[0,100]))
|
| 428 |
+
else:
|
| 429 |
+
tmp["GM_perc_MA"] = tmp.groupby("AM")["GM_perc"].transform(lambda x: x.rolling(3, min_periods=1).mean())
|
| 430 |
+
summary = tmp.groupby("Year")["GM_perc_MA"].agg(["mean","min","max"]).reset_index()
|
| 431 |
+
fig_gm = go.Figure()
|
| 432 |
+
fig_gm.add_trace(go.Scatter(x=summary["Year"], y=summary["mean"], mode="lines+markers",
|
| 433 |
+
name="GM Trend", line=dict(width=3)))
|
| 434 |
+
fig_gm.add_trace(go.Scatter(
|
| 435 |
+
x=summary["Year"].tolist()+summary["Year"][::-1].tolist(),
|
| 436 |
+
y=summary["max"].tolist()+summary["min"][::-1].tolist(),
|
| 437 |
+
fill="toself", fillcolor="rgba(9,103,174,0.13)",
|
| 438 |
+
line=dict(color="rgba(255,255,255,0)"), hoverinfo="skip",
|
| 439 |
+
showlegend=True, name="Range (Min–Max)"
|
| 440 |
+
))
|
| 441 |
+
fig_gm.add_hline(y=100, line_dash="dash", line_color="#8e8e8e",
|
| 442 |
+
annotation_text="Baseline", annotation_position="top left")
|
| 443 |
+
fig_gm.update_layout(yaxis_title="Gross Margin [% vs base]", xaxis_title="Year",
|
| 444 |
+
template="plotly_white", hovermode="x unified",
|
| 445 |
+
legend=dict(bgcolor="rgba(255,255,255,0.88)"),
|
| 446 |
+
title=dict(text=municipio_label(code, mun_code_to_name),
|
| 447 |
+
y=0.98, x=0.02, xanchor='left', yanchor='top'),
|
| 448 |
+
yaxis=dict(range=[0,100]))
|
| 449 |
+
|
| 450 |
+
# ---------- 4) Crop stacked (share 0–1 → 0–100%) ----------
|
| 451 |
+
if is_donana(selected_mun):
|
| 452 |
+
tc = crp.dropna(subset=["PMP Area"]).copy()
|
| 453 |
+
if tc.empty:
|
| 454 |
+
fig_crop = go.Figure().update_layout(title="No crop data", template="plotly_white",
|
| 455 |
+
yaxis=dict(range=[0,1], tickformat=".0%"))
|
| 456 |
+
else:
|
| 457 |
+
tc = tc.merge(w_hect, on="Municipio", how="left").dropna(subset=["Hectares"])
|
| 458 |
+
tc["w_area"] = tc["PMP Area"] * tc["Hectares"]
|
| 459 |
+
wm = (tc.groupby(["Year","Crop"], as_index=False)
|
| 460 |
+
.agg(w_share=("w_area","sum"), H=("Hectares","sum")))
|
| 461 |
+
wm["share"] = wm["w_share"] / wm["H"]
|
| 462 |
+
pivot = wm.pivot(index="Year", columns="Crop", values="share").fillna(0)
|
| 463 |
+
cols = [c for c in pivot.columns if c != "Secano"] + (["Secano"] if "Secano" in pivot.columns else [])
|
| 464 |
+
pivot = pivot[cols]
|
| 465 |
+
fig_crop = go.Figure()
|
| 466 |
+
palette = px.colors.qualitative.Plotly + px.colors.qualitative.Pastel
|
| 467 |
+
for i, crop in enumerate(pivot.columns):
|
| 468 |
+
fig_crop.add_trace(go.Scatter(
|
| 469 |
+
x=pivot.index, y=pivot[crop], mode="lines", stackgroup="one", name=crop,
|
| 470 |
+
line=dict(width=0.8), opacity=0.98, fillcolor=palette[i % len(palette)]
|
| 471 |
+
))
|
| 472 |
+
fig_crop.update_layout(yaxis_title="Allocated area (share)",
|
| 473 |
+
xaxis_title="Year", template="plotly_white",
|
| 474 |
+
hovermode="x unified", legend_title_text="Crop",
|
| 475 |
+
yaxis=dict(range=[0,1], tickformat=".0%"))
|
| 476 |
+
else:
|
| 477 |
+
code = int(selected_mun)
|
| 478 |
+
tc = crp[crp["Municipio"]==code].copy()
|
| 479 |
+
if tc.empty:
|
| 480 |
+
fig_crop = go.Figure().update_layout(title="No crop data", template="plotly_white",
|
| 481 |
+
yaxis=dict(range=[0,1], tickformat=".0%"))
|
| 482 |
+
else:
|
| 483 |
+
pivot = tc.pivot_table(index="Year", columns="Crop", values="PMP Area", aggfunc="mean").fillna(0)
|
| 484 |
+
cols = [c for c in pivot.columns if c != "Secano"] + (["Secano"] if "Secano" in pivot.columns else [])
|
| 485 |
+
pivot = pivot[cols]
|
| 486 |
+
fig_crop = go.Figure()
|
| 487 |
+
palette = px.colors.qualitative.Plotly + px.colors.qualitative.Pastel
|
| 488 |
+
for i, crop in enumerate(pivot.columns):
|
| 489 |
+
fig_crop.add_trace(go.Scatter(
|
| 490 |
+
x=pivot.index, y=pivot[crop], mode="lines", stackgroup="one", name=crop,
|
| 491 |
+
line=dict(width=0.8), opacity=0.98, fillcolor=palette[i % len(palette)]
|
| 492 |
+
))
|
| 493 |
+
fig_crop.update_layout(yaxis_title="Allocated area (share)",
|
| 494 |
+
xaxis_title="Year", template="plotly_white",
|
| 495 |
+
hovermode="x unified", legend_title_text="Crop",
|
| 496 |
+
title=dict(text=municipio_label(code, mun_code_to_name),
|
| 497 |
+
y=0.98, x=0.02, xanchor='left', yanchor='top'),
|
| 498 |
+
yaxis=dict(range=[0,1], tickformat=".0%"))
|
| 499 |
+
|
| 500 |
+
# ---------- 5) Water availability vs BAU baseline — y-range [0,100] ----------
|
| 501 |
+
if is_donana(selected_mun):
|
| 502 |
+
tw = wdf.copy().merge(w_hect, on="Municipio", how="left").dropna(subset=["Hectares","Water_rel"])
|
| 503 |
+
if tw.empty:
|
| 504 |
+
fig_w = go.Figure().update_layout(title="No water data", template="plotly_white",
|
| 505 |
+
yaxis=dict(range=[0,100]))
|
| 506 |
+
else:
|
| 507 |
+
tw["w_val"] = tw["Water_rel"] * tw["Hectares"]
|
| 508 |
+
wm = (tw.groupby(["Year","AM"], as_index=False)
|
| 509 |
+
.agg(val=("w_val","sum"), H=("Hectares","sum")))
|
| 510 |
+
wm["Water_rel_w"] = wm["val"] / wm["H"] # in %
|
| 511 |
+
summary = wm.groupby("Year")["Water_rel_w"].agg(["mean","min","max"]).reset_index()
|
| 512 |
+
fig_w = go.Figure()
|
| 513 |
+
fig_w.add_trace(go.Scatter(x=summary["Year"], y=summary["mean"], mode="lines+markers",
|
| 514 |
+
name="Water availability (rel. BAU=100)", line=dict(width=3)))
|
| 515 |
+
fig_w.add_trace(go.Scatter(
|
| 516 |
+
x=summary["Year"].tolist()+summary["Year"][::-1].tolist(),
|
| 517 |
+
y=summary["max"].tolist()+summary["min"][::-1].tolist(),
|
| 518 |
+
fill="toself", fillcolor="rgba(9,103,174,0.13)",
|
| 519 |
+
line=dict(color="rgba(255,255,255,0)"), hoverinfo="skip",
|
| 520 |
+
showlegend=True, name="Range (Min–Max)"
|
| 521 |
+
))
|
| 522 |
+
fig_w.add_hline(y=100, line_dash="dash", line_color="#8e8e8e",
|
| 523 |
+
annotation_text="BAU baseline", annotation_position="top left")
|
| 524 |
+
fig_w.update_layout(yaxis_title="Water availability [% of BAU baseline]",
|
| 525 |
+
xaxis_title="Year", template="plotly_white", hovermode="x unified",
|
| 526 |
+
yaxis=dict(range=[0,100]))
|
| 527 |
+
else:
|
| 528 |
+
code = int(selected_mun)
|
| 529 |
+
tw = wdf[wdf["Municipio"]==code].copy().sort_values(["AM","Year"])
|
| 530 |
+
if tw.empty:
|
| 531 |
+
fig_w = go.Figure().update_layout(title="No water data", template="plotly_white",
|
| 532 |
+
yaxis=dict(range=[0,100]))
|
| 533 |
+
else:
|
| 534 |
+
summary = tw.groupby("Year")["Water_rel"].agg(["mean","min","max"]).reset_index()
|
| 535 |
+
fig_w = go.Figure()
|
| 536 |
+
fig_w.add_trace(go.Scatter(x=summary["Year"], y=summary["mean"], mode="lines+markers",
|
| 537 |
+
name="Water availability (rel. BAU=100)", line=dict(width=3)))
|
| 538 |
+
fig_w.add_trace(go.Scatter(
|
| 539 |
+
x=summary["Year"].tolist()+summary["Year"][::-1].tolist(),
|
| 540 |
+
y=summary["max"].tolist()+summary["min"][::-1].tolist(),
|
| 541 |
+
fill="toself", fillcolor="rgba(9,103,174,0.13)",
|
| 542 |
+
line=dict(color="rgba(255,255,255,0)"), hoverinfo="skip",
|
| 543 |
+
showlegend=True, name="Range (Min–Max)"
|
| 544 |
+
))
|
| 545 |
+
fig_w.add_hline(y=100, line_dash="dash", line_color="#8e8e8e",
|
| 546 |
+
annotation_text="BAU baseline", annotation_position="top left")
|
| 547 |
+
fig_w.update_layout(yaxis_title="Water availability [% of BAU baseline]",
|
| 548 |
+
xaxis_title="Year", template="plotly_white", hovermode="x unified",
|
| 549 |
+
title=dict(text=municipio_label(code, mun_code_to_name),
|
| 550 |
+
y=0.98, x=0.02, xanchor='left', yanchor='top'),
|
| 551 |
+
yaxis=dict(range=[0,100]))
|
| 552 |
+
|
| 553 |
+
# ---------- 6) Total water consumption (m³) ----------
|
| 554 |
+
cons = compute_total_water_timeseries(selected_scenario, selected_mun, selected_rcp, selected_method)
|
| 555 |
+
if cons is None or cons.empty:
|
| 556 |
+
fig_c = go.Figure().update_layout(title="Total water consumption not available (check crop/WR columns).",
|
| 557 |
+
template="plotly_white")
|
| 558 |
+
else:
|
| 559 |
+
fig_c = go.Figure()
|
| 560 |
+
fig_c.add_trace(go.Scatter(x=cons["Year"], y=cons["mean"], mode="lines+markers",
|
| 561 |
+
name="Total water (mean)", line=dict(width=3)))
|
| 562 |
+
fig_c.add_trace(go.Scatter(
|
| 563 |
+
x=cons["Year"].tolist()+cons["Year"][::-1].tolist(),
|
| 564 |
+
y=cons["max"].tolist()+cons["min"][::-1].tolist(),
|
| 565 |
+
fill="toself", fillcolor="rgba(9,103,174,0.13)",
|
| 566 |
+
line=dict(color="rgba(255,255,255,0)"), hoverinfo="skip",
|
| 567 |
+
showlegend=True, name="Range (Min–Max)"
|
| 568 |
+
))
|
| 569 |
+
fig_c.update_layout(yaxis_title="m³", xaxis_title="Year",
|
| 570 |
+
template="plotly_white", hovermode="x unified")
|
| 571 |
|
| 572 |
+
return desc_div, fig_gm, fig_crop, fig_w, fig_c
|
| 573 |
|
| 574 |
+
# ========= MAIN =========
|
| 575 |
if __name__ == "__main__":
|
| 576 |
+
port = int(os.environ.get("PORT", 7860))
|
| 577 |
+
app.run(host="0.0.0.0", port=port, debug=False)
|
crop_surface_subterraneotheft_parsed (1).xlsx
ADDED
|
Binary file (16.8 kB). View file
|
|
|