View or edit on GitHub
This page is synchronized from trase/models/deduce/v1/notebooks/comparison.ipynb. Last modified on 2026-11-02 10:41 CET by dependabot[bot].
Please view or edit the original file there; changes should be reflected here after a midnight build (CET time),
or manually triggering it with a GitHub action (link).
import pandas as pd
from trase.tools.aws.aws_helpers_cached import get_pandas_df_once
from trase.tools.aws.aws_helpers import read_s3_parquet
Config Session
PATH_REFERENCE_DEDUCE = (
"data/Forest_loss_to_CLASSIFICATION_United States_PythonAPI_2024-06-03T170956.csv"
)
PATH_DEDUCE_NEW = "deduce/tables/DeDuCE_area_statistics_commodity_year_GLOBAL_names.csv"
PATH_FAO_COUNTRY_CODE = "deduce/tables/country_codes.csv"
KEY_COLUMNS = [
#'country_code',
"country_name",
"year",
"class_code_str",
"area_hectares",
]
KEY_COLS_REF = [
"country_name",
"class_code",
] + ["loss_" + str(year) for year in list(range(2001, 2023))]
Load Inputs
df_deduce_new = get_pandas_df_once(key=PATH_DEDUCE_NEW, sep=",")
df_deduce_ref = pd.read_csv(PATH_REFERENCE_DEDUCE)
df_fao_country_code = get_pandas_df_once(key=PATH_FAO_COUNTRY_CODE, sep=",")
Pre-processing
df_deduce_new["year"] = (
df_deduce_new["year"]
.astype(str)
.str.replace(".", "", regex=False)
.str.replace(",", "", regex=False)
.astype(int)
)
df_deduce_new_pivot = (
df_deduce_new[KEY_COLUMNS]
.groupby(by=["country_name", "year", "class_code_str"], as_index=False)[
"area_hectares"
]
.sum()
)
df_pivot = (
df_deduce_new_pivot.pivot_table(
index=["country_name", "class_code_str"],
columns="year",
values="area_hectares",
aggfunc="sum",
)
.reset_index()
.fillna(0)
)
# Rename year columns
df_pivot.columns = [
f"loss_{col}" if str(col).isdigit() else col for col in df_pivot.columns
]
df_deduce_ref = df_deduce_ref.rename(
columns={"COUNTRY": "country_name", "Class": "class_code"}
)
df_pivot = df_pivot.rename(columns={"class_code_str": "class_code"})
# replace country name
df_deduce_ref = df_deduce_ref.replace(
{"country_name": {"United States": "United States of America"}}
)
Display Charts
df_deduce_country_new = df_pivot.query(
'country_name.str.startswith("United States of Am")'
)
df_deduce_country_ref = (
df_deduce_ref[KEY_COLS_REF]
.groupby(by=["country_name", "class_code"], as_index=False)
.sum()
)
# Set version
df_deduce_country_new["version"] = "New"
df_deduce_country_ref["version"] = "Ref"
df_comp = pd.concat([df_deduce_country_new, df_deduce_country_ref]).fillna(0)
/var/folders/42/_z7t3f3n2b74bggwwctg77z80000gn/T/ipykernel_36379/3274307982.py:10: SettingWithCopyWarning:
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead
See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
df_deduce_country_new['version'] = 'New'
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Patch
# 1. Reshape (Melt) the dataframe from wide to long format
loss_cols = [col for col in df_comp.columns if str(col).startswith("loss_")]
df_melt = df_comp.melt(
id_vars=["version", "class_code"],
value_vars=loss_cols,
var_name="year_col",
value_name="loss",
)
# Extract the integer year from the column names
df_melt["year"] = df_melt["year_col"].str.replace("loss_", "").astype(int)
# 1. Force class_code to be a string so matrix alignment never misses a column
df_melt["class_code"] = df_melt["class_code"].astype(str)
# 2. Strip commas and spaces from the loss values, then convert to strict numbers
df_melt["loss"] = (
df_melt["loss"]
.astype(str)
.str.replace(",", "", regex=False)
.str.replace(" ", "", regex=False)
)
df_melt["loss"] = pd.to_numeric(df_melt["loss"], errors="coerce").fillna(0)
# 2. Group the data by year, version, and class_code
grouped = df_melt.groupby(["year", "version", "class_code"])["loss"].sum().reset_index()
unique_years = sorted(grouped["year"].unique())
class_codes = sorted(grouped["class_code"].unique())
versions = ["Ref", "New"]
# --- NOVO: Limite dinâmico para anotações ---
# Calcula o valor máximo de um bloco individual para definir o que é "alto volume".
# Ajuste o multiplicador (ex: 0.10 para 10%) para mostrar mais ou menos anotações.
volume_threshold = grouped["loss"].abs().max() * 0.10
# Estilo da caixa de texto para garantir leitura fácil sobre qualquer cor
bbox_props = dict(
boxstyle="round,pad=0.2", facecolor="white", alpha=0.7, edgecolor="none"
)
# 3. Setup the Matplotlib Plot
fig, ax = plt.subplots(figsize=(18, 9))
width = 0.35 # Set the width of the bars
# Create a colormap dictionary for consistent coloring of each class_code
colors = plt.cm.get_cmap("tab20")
color_dict = {cc: colors(i % 20) for i, cc in enumerate(class_codes)}
legend_dict = {}
# 4. Plot the stacked bars
for version in versions:
offset = -width / 2 if version == "Ref" else width / 2
edge_color = "black" if version == "New" else "white"
line_width = 1.2 if version == "New" else 0.5
v_data = grouped[grouped["version"] == version]
# Force strict matrix alignment
pivot = v_data.pivot(index="year", columns="class_code", values="loss")
pivot = pivot.reindex(index=unique_years, columns=class_codes, fill_value=0)
# Keep track of the bottom edge to appropriately stack the segments
bottom = np.zeros(len(unique_years), dtype="float64")
# Looping through the strictly sorted class_codes
for cc in class_codes:
# Explicitly extract values as floats
values = pivot[cc].astype(float).values
bars = ax.bar(
np.array(unique_years) + offset,
values,
width,
bottom=bottom,
color=color_dict[cc],
edgecolor=edge_color,
linewidth=line_width,
)
# --- NOVO: Adiciona o rótulo dentro dos maiores blocos ---
for bar in bars:
height = bar.get_height()
if abs(height) >= volume_threshold:
ax.text(
bar.get_x() + bar.get_width() / 2,
bar.get_y() + height / 2,
str(cc),
ha="center",
va="center",
fontsize=8,
fontweight="bold",
color="black",
bbox=bbox_props,
)
bottom += values
if cc not in legend_dict:
legend_dict[cc] = bars[0]
# Add totals on top of the completed stacked bars
for i, year in enumerate(unique_years):
total_val = bottom[i]
if total_val > 0:
ax.text(
year + offset,
total_val + (total_val * 0.01),
f"{total_val:,.0f}",
ha="center",
va="bottom",
rotation=90,
fontsize=9,
)
# 5. Format the axes and labels
ax.set_xticks(unique_years)
ax.set_xticklabels(unique_years, rotation=45, fontsize=11)
# Increase the upper y-limit slightly to make room for the text totals
current_ylim = ax.get_ylim()
ax.set_ylim(current_ylim[0], current_ylim[1] * 1.15)
ax.set_ylabel("Total Loss", fontweight="bold", fontsize=12)
ax.set_title("Yearly Loss Comparison: Ref vs New - USA", fontweight="bold", fontsize=14)
# 6. Customize the legend
handles = [legend_dict[cc] for cc in sorted(legend_dict.keys())]
labels = [f"Class {cc}" for cc in sorted(legend_dict.keys())]
ref_marker = Patch(
facecolor="white", edgecolor="gray", label="Left Bar: Ref (No Border)"
)
new_marker = Patch(
facecolor="white",
edgecolor="black",
linewidth=1.5,
label="Right Bar: New (Black Border)",
)
handles.extend([ref_marker, new_marker])
labels.extend(["Left Bar: Ref", "Right Bar: New"])
ax.legend(
handles=handles,
labels=labels,
title="Legend",
bbox_to_anchor=(1.02, 1),
loc="upper left",
ncol=2,
fontsize=10,
)
plt.subplots_adjust(right=0.8, top=0.9)
plt.tight_layout()
plt.show()
/var/folders/42/_z7t3f3n2b74bggwwctg77z80000gn/T/ipykernel_36379/1245224827.py:46: MatplotlibDeprecationWarning: The get_cmap function was deprecated in Matplotlib 3.7 and will be removed in 3.11. Use ``matplotlib.colormaps[name]`` or ``matplotlib.colormaps.get_cmap()`` or ``pyplot.get_cmap()`` instead.
colors = plt.cm.get_cmap('tab20')

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Patch
df_postive = df_comp.query("class_code > 0")
# 1. Reshape (Melt) the dataframe from wide to long format
loss_cols = [col for col in df_postive.columns if str(col).startswith("loss_")]
df_melt = df_postive.melt(
id_vars=["version", "class_code"],
value_vars=loss_cols,
var_name="year_col",
value_name="loss",
)
# Extract the integer year from the column names
df_melt["year"] = df_melt["year_col"].str.replace("loss_", "").astype(int)
# 1. Force class_code to be a string so matrix alignment never misses a column
df_melt["class_code"] = df_melt["class_code"].astype(str)
# 2. Strip commas and spaces from the loss values, then convert to strict numbers
df_melt["loss"] = (
df_melt["loss"]
.astype(str)
.str.replace(",", "", regex=False)
.str.replace(" ", "", regex=False)
)
df_melt["loss"] = pd.to_numeric(df_melt["loss"], errors="coerce").fillna(0)
# 2. Group the data by year, version, and class_code
grouped = df_melt.groupby(["year", "version", "class_code"])["loss"].sum().reset_index()
unique_years = sorted(grouped["year"].unique())
class_codes = sorted(grouped["class_code"].unique())
versions = ["Ref", "New"]
# 3. Setup the Matplotlib Plot
fig, ax = plt.subplots(figsize=(18, 9))
width = 0.35 # Set the width of the bars
# Create a colormap dictionary for consistent coloring of each class_code
colors = plt.cm.get_cmap("tab20")
color_dict = {cc: colors(i % 20) for i, cc in enumerate(class_codes)}
legend_dict = {}
# 4. Plot the stacked bars
for version in versions:
offset = -width / 2 if version == "Ref" else width / 2
edge_color = "black" if version == "New" else "white"
line_width = 1.2 if version == "New" else 0.5
v_data = grouped[grouped["version"] == version]
# Force strict matrix alignment
pivot = v_data.pivot(index="year", columns="class_code", values="loss")
pivot = pivot.reindex(index=unique_years, columns=class_codes, fill_value=0)
# Keep track of the bottom edge to appropriately stack the segments
bottom = np.zeros(len(unique_years), dtype="float64")
# Looping through the strictly sorted class_codes
for cc in class_codes:
# Explicitly extract values as floats
values = pivot[cc].astype(float).values
bars = ax.bar(
np.array(unique_years) + offset,
values,
width,
bottom=bottom,
color=color_dict[cc],
edgecolor=edge_color,
linewidth=line_width,
)
bottom += values
if cc not in legend_dict:
legend_dict[cc] = bars[0]
# Add totals on top of the completed stacked bars
for i, year in enumerate(unique_years):
total_val = bottom[i]
if total_val > 0:
ax.text(
year + offset,
total_val + (total_val * 0.01),
f"{total_val:,.0f}",
ha="center",
va="bottom",
rotation=90,
fontsize=9,
)
# 5. Format the axes and labels
ax.set_xticks(unique_years)
ax.set_xticklabels(unique_years, rotation=45, fontsize=11)
# Increase the upper y-limit slightly to make room for the text totals
current_ylim = ax.get_ylim()
ax.set_ylim(current_ylim[0], current_ylim[1] * 1.15)
ax.set_ylabel("Total Loss", fontweight="bold", fontsize=12)
ax.set_title(
"Yearly Loss Comparison: Ref vs New - USA (positive codes)",
fontweight="bold",
fontsize=14,
)
# 6. Customize the legend
handles = [legend_dict[cc] for cc in sorted(legend_dict.keys())]
labels = [f"Class {cc}" for cc in sorted(legend_dict.keys())]
ref_marker = Patch(
facecolor="white", edgecolor="gray", label="Left Bar: Ref (No Border)"
)
new_marker = Patch(
facecolor="white",
edgecolor="black",
linewidth=1.5,
label="Right Bar: New (Black Border)",
)
handles.extend([ref_marker, new_marker])
labels.extend(["Left Bar: Ref", "Right Bar: New"])
ax.legend(
handles=handles,
labels=labels,
title="Legend",
bbox_to_anchor=(1.02, 1),
loc="upper left",
ncol=2,
fontsize=10,
)
plt.subplots_adjust(right=0.8, top=0.9)
plt.tight_layout()
plt.show()
/var/folders/42/_z7t3f3n2b74bggwwctg77z80000gn/T/ipykernel_36379/1459930092.py:40: MatplotlibDeprecationWarning: The get_cmap function was deprecated in Matplotlib 3.7 and will be removed in 3.11. Use ``matplotlib.colormaps[name]`` or ``matplotlib.colormaps.get_cmap()`` or ``pyplot.get_cmap()`` instead.
colors = plt.cm.get_cmap('tab20')

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Patch
# 1. Reshape (Melt) the dataframe from wide to long format
loss_cols = [col for col in df_comp.columns if str(col).startswith("loss_")]
df_melt = df_comp.melt(
id_vars=["version", "class_code"],
value_vars=loss_cols,
var_name="year_col",
value_name="loss",
)
# Extract the integer year
df_melt["year"] = df_melt["year_col"].str.replace("loss_", "").astype(int)
# Clean the data to ensure no strings/commas break the math
df_melt["class_code"] = df_melt["class_code"].astype(str)
df_melt["loss"] = (
df_melt["loss"]
.astype(str)
.str.replace(",", "", regex=False)
.str.replace(" ", "", regex=False)
)
df_melt["loss"] = pd.to_numeric(df_melt["loss"], errors="coerce").fillna(0)
# 2. Group the data by year, version, and class_code
grouped = df_melt.groupby(["year", "version", "class_code"])["loss"].sum().reset_index()
# 3. Calculate the Difference (New - Ref) per year and class code
diff_df = grouped.pivot_table(
index=["year", "class_code"], columns="version", values="loss", fill_value=0
).reset_index()
if "New" not in diff_df.columns:
diff_df["New"] = 0
if "Ref" not in diff_df.columns:
diff_df["Ref"] = 0
diff_df["difference"] = diff_df["New"] - diff_df["Ref"]
chart_data = diff_df.pivot(
index="year", columns="class_code", values="difference"
).fillna(0)
unique_years = sorted(chart_data.index)
class_codes = sorted(chart_data.columns)
# 4. Setup the Matplotlib Plot
fig, ax = plt.subplots(figsize=(18, 9))
width = 0.6
colors = plt.cm.get_cmap("tab20")
color_dict = {cc: colors(i % 20) for i, cc in enumerate(class_codes)}
legend_dict = {}
pos_bottom = np.zeros(len(unique_years))
neg_bottom = np.zeros(len(unique_years))
# --- DYNAMIC THRESHOLD ---
# Calculate a threshold to define "high volume" (e.g., 10% of the largest single block).
# Adjust this decimal to show MORE labels (e.g., 0.05) or FEWER labels (e.g., 0.20).
volume_threshold = diff_df["difference"].abs().max() * 0.10
# Helper dictionary for the text box styling to guarantee readability
bbox_props = dict(
boxstyle="round,pad=0.2", facecolor="white", alpha=0.7, edgecolor="none"
)
for cc in class_codes:
values = chart_data[cc].values
pos_values = np.where(values > 0, values, 0)
neg_values = np.where(values < 0, values, 0)
pos_bars = ax.bar(
unique_years,
pos_values,
width,
bottom=pos_bottom,
color=color_dict[cc],
edgecolor="white",
linewidth=0.5,
)
neg_bars = ax.bar(
unique_years,
neg_values,
width,
bottom=neg_bottom,
color=color_dict[cc],
edgecolor="white",
linewidth=0.5,
)
# --- ADD LABELS TO POSITIVE BARS ---
for bar in pos_bars:
height = bar.get_height()
if height >= volume_threshold:
ax.text(
bar.get_x() + bar.get_width() / 2,
bar.get_y() + height / 2,
str(cc),
ha="center",
va="center",
fontsize=8,
fontweight="bold",
color="black",
bbox=bbox_props,
)
# --- ADD LABELS TO NEGATIVE BARS ---
for bar in neg_bars:
height = bar.get_height()
if abs(height) >= volume_threshold:
ax.text(
bar.get_x() + bar.get_width() / 2,
bar.get_y() + height / 2,
str(cc),
ha="center",
va="center",
fontsize=8,
fontweight="bold",
color="black",
bbox=bbox_props,
)
pos_bottom += pos_values
neg_bottom += neg_values
if cc not in legend_dict:
legend_dict[cc] = pos_bars[0] if sum(pos_values) > 0 else neg_bars[0]
ax.axhline(0, color="black", linewidth=1.5, zorder=0)
# 5. Format the axes and labels
ax.set_xticks(unique_years)
ax.set_xticklabels(unique_years, rotation=45, fontsize=11)
ax.set_ylabel("Loss Difference (New - Ref)", fontweight="bold", fontsize=12)
ax.set_title(
"Yearly Loss Difference per Class Code (New minus Ref)",
fontweight="bold",
fontsize=14,
)
# 6. Customize the legend
handles = [legend_dict[cc] for cc in class_codes]
labels = [f"Class {cc}" for cc in class_codes]
ax.legend(
handles=handles,
labels=labels,
title="Class Codes",
bbox_to_anchor=(1.02, 1),
loc="upper left",
ncol=2,
fontsize=10,
)
plt.subplots_adjust(right=0.8)
plt.tight_layout()
plt.show()
/var/folders/42/_z7t3f3n2b74bggwwctg77z80000gn/T/ipykernel_36379/311525573.py:48: MatplotlibDeprecationWarning: The get_cmap function was deprecated in Matplotlib 3.7 and will be removed in 3.11. Use ``matplotlib.colormaps[name]`` or ``matplotlib.colormaps.get_cmap()`` or ``pyplot.get_cmap()`` instead.
colors = plt.cm.get_cmap('tab20')
