# Core financial data (all figures in Million EUR)
ulysse <- tribble(
~year, ~revenue, ~net_income, ~total_assets, ~equity,
2020, 342, 8, 135, 90,
2021, 376, 9, 133, 96,
2022, 376, 9, 131, 99,
2023, 363, 9, 131, 101,
2024, 375, 5, 135, 101
)
# EUR/TND exchange rates (annual averages)
# Source: European Central Bank reference rates
# Note: Higher rate = TND weakening vs EUR (favorable for EUR-reported revenues)
exchange_rates <- tribble(
~year, ~eur_tnd_rate,
2020, 3.45,
2021, 3.29,
2022, 3.35,
2023, 3.33,
2024, 3.38
)
# 2020 used as base year for constant currency calculations
BASE_FX_RATE <- 3.45
# Define key periods for boycott analysis
PRE_BOYCOTT_YEARS <- 2021:2022 # Baseline period
BOYCOTT_START_YEAR <- 2023 # Q4 2023
POST_BOYCOTT_YEAR <- 2024 # Full year impact
# Calculate baseline metrics
baseline_metrics <- ulysse %>%
filter(year %in% PRE_BOYCOTT_YEARS) %>%
summarise(
baseline_revenue = mean(revenue),
baseline_income = mean(net_income),
baseline_margin = mean(net_income / revenue * 100),
.groups = 'drop'
)
# Join with exchange rates and calculate basic metrics
ulysse_analysis <- ulysse %>%
left_join(exchange_rates, by = "year") %>%
arrange(year) %>%
mutate(
# Profitability ratios
net_margin = (net_income / revenue) * 100,
roe = (net_income / equity) * 100,
roa = (net_income / total_assets) * 100,
# Balance sheet metrics
debt = total_assets - equity,
debt_to_equity = debt / equity,
equity_ratio = (equity / total_assets) * 100,
# Asset efficiency
asset_turnover = revenue / total_assets,
# Constant currency calculations
revenue_constant_fx = revenue * (eur_tnd_rate / BASE_FX_RATE),
net_income_constant_fx = net_income * (eur_tnd_rate / BASE_FX_RATE),
# FX impact
fx_impact_revenue = revenue - revenue_constant_fx,
fx_impact_pct = ((revenue / revenue_constant_fx) - 1) * 100,
# Period classification
period = case_when(
year < BOYCOTT_START_YEAR ~ "Pre-Boycott",
year == BOYCOTT_START_YEAR ~ "Boycott Start",
year >= POST_BOYCOTT_YEAR ~ "Post-Boycott"
),
period = factor(period, levels = c("Pre-Boycott", "Boycott Start", "Post-Boycott"))
)
# Calculate growth rates manually (avoiding lag() issues)
# Create vectors for prior year values
n_rows <- nrow(ulysse_analysis)
ulysse_analysis <- ulysse_analysis %>%
mutate(
# Manual growth calculations
revenue_growth = c(NA,
diff(revenue) / head(revenue, -1) * 100),
income_growth = c(NA,
diff(net_income) / head(net_income, -1) * 100),
revenue_growth_constant_fx = c(NA,
diff(revenue_constant_fx) / head(revenue_constant_fx, -1) * 100),
# Organic growth is same as constant currency growth
organic_growth = revenue_growth_constant_fx,
# FX contribution
fx_contribution = revenue_growth - revenue_growth_constant_fx
)
# Display sample of calculated metrics
ulysse_analysis %>%
dplyr::select(year, revenue, revenue_growth, organic_growth, fx_contribution, net_margin) %>%
knitr::kable(digits = 2, caption = "Sample of Calculated Metrics")
| year | revenue | revenue_growth | organic_growth | fx_contribution | net_margin |
|---|---|---|---|---|---|
| 2020 | 342 | NA | NA | NA | 2.34 |
| 2021 | 376 | 9.94 | 4.84 | 5.10 | 2.39 |
| 2022 | 376 | 0.00 | 1.82 | -1.82 | 2.39 |
| 2023 | 363 | -3.46 | -4.03 | 0.58 | 2.48 |
| 2024 | 375 | 3.31 | 4.86 | -1.55 | 1.33 |
ulysse_analysis %>%
dplyr::select(year, revenue, net_income, net_margin, roe, roa, debt_to_equity) %>%
gt() %>%
tab_header(
title = "Ulysse (Tunisia) - Key Financial Metrics",
subtitle = "2020-2024 Performance Summary"
) %>%
cols_label(
year = "Year",
revenue = "Revenue",
net_income = "Net Income",
net_margin = "Net Margin",
roe = "ROE",
roa = "ROA",
debt_to_equity = "D/E Ratio"
) %>%
fmt_number(
columns = c(revenue, net_income),
decimals = 0,
suffix = "M€"
) %>%
fmt_number(
columns = c(net_margin, roe, roa),
decimals = 1,
suffix = "%"
) %>%
fmt_number(
columns = debt_to_equity,
decimals = 2
) %>%
tab_style(
style = cell_fill(color = "#f0f0f0"),
locations = cells_body(rows = year == 2024)
) %>%
tab_style(
style = cell_fill(color = "#ffe6e6"),
locations = cells_body(
columns = c(net_margin, roe),
rows = year == 2024
)
)
| Ulysse (Tunisia) - Key Financial Metrics | ||||||
| 2020-2024 Performance Summary | ||||||
| Year | Revenue | Net Income | Net Margin | ROE | ROA | D/E Ratio |
|---|---|---|---|---|---|---|
| 2020 | 342 | 8 | 2.3 | 8.9 | 5.9 | 0.50 |
| 2021 | 376 | 9 | 2.4 | 9.4 | 6.8 | 0.39 |
| 2022 | 376 | 9 | 2.4 | 9.1 | 6.9 | 0.32 |
| 2023 | 363 | 9 | 2.5 | 8.9 | 6.9 | 0.30 |
| 2024 | 375 | 5 | 1.3 | 5.0 | 3.7 | 0.34 |
p1 <- ggplot(ulysse_analysis, aes(x = year, y = revenue)) +
geom_line(color = colors$primary, linewidth = 1.2) +
geom_point(size = 3, color = colors$primary) +
geom_text(aes(label = paste0(revenue, "M€")),
vjust = -1, size = 3.5) +
labs(
title = "Revenue Trend 2020-2024",
x = "Year",
y = "Revenue (Million €)"
) +
scale_x_continuous(breaks = ulysse_analysis$year) +
scale_y_continuous(limits = c(320, 390))
p2 <- ulysse_analysis %>%
filter(!is.na(revenue_growth)) %>%
ggplot(aes(x = year, y = revenue_growth, fill = revenue_growth > 0)) +
geom_col(width = 0.6) +
geom_text(aes(label = sprintf("%.1f%%", revenue_growth),
vjust = ifelse(revenue_growth > 0, -0.5, 1.5)),
size = 3.5) +
scale_fill_manual(values = c(colors$negative, colors$positive), guide = "none") +
labs(
title = "Year-over-Year Revenue Growth",
x = "Year",
y = "Growth Rate (%)"
) +
scale_x_continuous(breaks = unique(ulysse_analysis$year[!is.na(ulysse_analysis$revenue_growth)])) +
geom_hline(yintercept = 0, linetype = "dashed", color = "gray50")
p1 + p2
p3 <- ggplot(ulysse_analysis, aes(x = year, y = net_income)) +
geom_col(fill = colors$info, width = 0.6) +
geom_text(aes(label = paste0(net_income, "M€")),
vjust = -0.5, size = 3.5) +
labs(
title = "Net Income",
x = "Year",
y = "Net Income (Million €)"
) +
scale_x_continuous(breaks = ulysse_analysis$year) +
scale_y_continuous(limits = c(0, 11))
p4 <- ggplot(ulysse_analysis, aes(x = year, y = net_margin)) +
geom_line(color = "#e67e22", linewidth = 1.2) +
geom_point(size = 3, color = "#e67e22") +
geom_text(aes(label = paste0(round(net_margin, 1), "%")),
vjust = -1, size = 3.5) +
labs(
title = "Net Profit Margin",
x = "Year",
y = "Margin (%)"
) +
scale_x_continuous(breaks = ulysse_analysis$year) +
scale_y_continuous(limits = c(0, 3.5))
p3 + p4
ulysse_analysis %>%
dplyr::select(year, roe, roa) %>%
pivot_longer(-year, names_to = "metric", values_to = "value") %>%
ggplot(aes(x = year, y = value, color = metric, group = metric)) +
geom_line(linewidth = 1.2) +
geom_point(size = 3) +
geom_text(aes(label = paste0(round(value, 1), "%")),
vjust = -1, size = 3) +
scale_color_manual(
values = c("roe" = "#9b59b6", "roa" = "#16a085"),
labels = c("Return on Equity (ROE)", "Return on Assets (ROA)")
) +
labs(
title = "Return on Equity vs Return on Assets",
x = "Year",
y = "Return (%)",
color = NULL
) +
scale_x_continuous(breaks = ulysse_analysis$year)
p5 <- ulysse_analysis %>%
dplyr::select(year, total_assets, equity, debt) %>%
pivot_longer(-year, names_to = "item", values_to = "value") %>%
mutate(item = factor(item,
levels = c("total_assets", "equity", "debt"),
labels = c("Total Assets", "Equity", "Debt"))) %>%
ggplot(aes(x = year, y = value, color = item, group = item)) +
geom_line(linewidth = 1.2) +
geom_point(size = 3) +
scale_color_manual(values = c("#34495e", colors$positive, colors$negative)) +
labs(
title = "Balance Sheet Components",
x = "Year",
y = "Amount (Million €)",
color = NULL
) +
scale_x_continuous(breaks = ulysse_analysis$year)
p6 <- ggplot(ulysse_analysis, aes(x = year, y = equity_ratio)) +
geom_line(color = "#16a085", linewidth = 1.2) +
geom_point(size = 3, color = "#16a085") +
geom_text(aes(label = paste0(round(equity_ratio, 1), "%")),
vjust = -1, size = 3.5) +
labs(
title = "Equity Ratio (Financial Strength)",
x = "Year",
y = "Equity / Total Assets (%)"
) +
scale_x_continuous(breaks = ulysse_analysis$year) +
scale_y_continuous(limits = c(60, 80))
p5 + p6
ggplot(ulysse_analysis, aes(x = year, y = debt_to_equity)) +
geom_col(fill = "#c0392b", width = 0.6) +
geom_text(aes(label = round(debt_to_equity, 2)),
vjust = -0.5, size = 3.5) +
labs(
title = "Debt-to-Equity Ratio",
subtitle = "Lower values indicate less financial leverage and lower risk",
x = "Year",
y = "Debt / Equity Ratio"
) +
scale_x_continuous(breaks = ulysse_analysis$year) +
geom_hline(yintercept = 0.5, linetype = "dashed", color = "gray50", linewidth = 0.5) +
annotate("text", x = 2020.3, y = 0.52, label = "0.50 Reference Line",
size = 3, hjust = 0)
ggplot(ulysse_analysis, aes(x = year, y = asset_turnover)) +
geom_line(color = "#8e44ad", linewidth = 1.2) +
geom_point(size = 3, color = "#8e44ad") +
geom_text(aes(label = round(asset_turnover, 2)),
vjust = -1, size = 3.5) +
labs(
title = "Asset Turnover Ratio",
subtitle = "Revenue generated per euro of assets (higher is better)",
x = "Year",
y = "Asset Turnover (x)"
) +
scale_x_continuous(breaks = ulysse_analysis$year) +
scale_y_continuous(limits = c(2.3, 3.0))
Following the events of October 7, 2023, Carrefour faced boycott campaigns in several markets, including Tunisia. The boycott began in Q4 2023, with full-year impact visible in 2024 results.
p_boycott1 <- ggplot(ulysse_analysis, aes(x = year, y = revenue, fill = period)) +
geom_col(width = 0.7) +
geom_text(aes(label = paste0(revenue, "M€")), vjust = -0.5, size = 3.5) +
scale_fill_manual(values = c("Pre-Boycott" = colors$info,
"Boycott Start" = colors$warning,
"Post-Boycott" = colors$negative)) +
labs(
title = "Revenue Before and After October 7 Boycott Campaign",
subtitle = "Boycott began Q4 2023, full-year impact visible in 2024",
x = "Year",
y = "Revenue (Million €)",
fill = "Period"
) +
scale_x_continuous(breaks = ulysse_analysis$year) +
annotate("segment", x = 2023.5, xend = 2023.5, y = 0, yend = 380,
linetype = "dashed", color = "red", linewidth = 0.8) +
annotate("text", x = 2023.5, y = 385, label = "Boycott Starts",
color = "red", size = 3, hjust = 0.5)
# Calculate deviation from baseline
p_boycott2 <- ulysse_analysis %>%
filter(year >= 2021) %>%
mutate(
revenue_gap = revenue - baseline_metrics$baseline_revenue
) %>%
ggplot(aes(x = year, y = revenue_gap, fill = revenue_gap < 0)) +
geom_col(width = 0.6) +
geom_text(aes(label = paste0(ifelse(revenue_gap > 0, "+", ""),
round(revenue_gap, 0), "M€"),
vjust = ifelse(revenue_gap > 0, -0.5, 1.5)),
size = 3.5) +
scale_fill_manual(values = c(colors$negative, colors$positive), guide = "none") +
labs(
title = "Revenue Gap vs Pre-Boycott Baseline",
subtitle = paste0("Baseline: ", round(baseline_metrics$baseline_revenue, 0), "M€ (2021-2022 average)"),
x = "Year",
y = "Deviation from Baseline (Million €)"
) +
scale_x_continuous(breaks = seq(2021, 2024, 1)) +
geom_hline(yintercept = 0, linetype = "solid", color = "black")
p_boycott1 / p_boycott2
p_profit1 <- ggplot(ulysse_analysis, aes(x = year, y = net_income, fill = period)) +
geom_col(width = 0.7) +
geom_text(aes(label = paste0(net_income, "M€")), vjust = -0.5, size = 3.5) +
scale_fill_manual(values = c("Pre-Boycott" = colors$info,
"Boycott Start" = colors$warning,
"Post-Boycott" = colors$negative)) +
labs(
title = "Net Income: Dramatic Decline Post-Boycott",
x = "Year",
y = "Net Income (Million €)",
fill = "Period"
) +
scale_x_continuous(breaks = ulysse_analysis$year) +
annotate("segment", x = 2023.5, xend = 2023.5, y = 0, yend = 9.5,
linetype = "dashed", color = "red", linewidth = 0.8)
p_margin1 <- ggplot(ulysse_analysis, aes(x = year, y = net_margin, color = period, group = 1)) +
geom_line(linewidth = 1.2) +
geom_point(size = 4) +
geom_text(aes(label = paste0(round(net_margin, 1), "%")),
vjust = -1.5, size = 3.5, color = "black") +
scale_color_manual(values = c("Pre-Boycott" = colors$info,
"Boycott Start" = colors$warning,
"Post-Boycott" = colors$negative)) +
labs(
title = "Net Margin Erosion",
x = "Year",
y = "Net Margin (%)",
color = "Period"
) +
scale_x_continuous(breaks = ulysse_analysis$year) +
annotate("segment", x = 2023.5, xend = 2023.5, y = 0, yend = 3,
linetype = "dashed", color = "red", linewidth = 0.8)
p_profit1 + p_margin1
# Calculate expected vs actual performance
revenue_2024 <- ulysse_analysis$revenue[ulysse_analysis$year == 2024]
expected_income_2024 <- revenue_2024 * (baseline_metrics$baseline_margin / 100)
actual_income_2024 <- ulysse_analysis$net_income[ulysse_analysis$year == 2024]
profit_loss <- expected_income_2024 - actual_income_2024
loss_summary <- tribble(
~Description, ~Value,
"2024 Revenue", paste0(revenue_2024, "M€"),
"Expected Net Margin (2021-2022 avg)", paste0(round(baseline_metrics$baseline_margin, 2), "%"),
"Expected Net Income at Historical Margin", paste0(round(expected_income_2024, 1), "M€"),
"Actual Net Income 2024", paste0(actual_income_2024, "M€"),
"Estimated Profit Loss", paste0(round(profit_loss, 1), "M€"),
"Percentage Loss vs Expected", paste0(round((profit_loss/expected_income_2024)*100, 1), "%")
)
loss_summary %>%
gt() %>%
tab_header(
title = "Estimated Financial Impact of Boycott on 2024 Results",
subtitle = "Assuming margins would have remained at pre-boycott levels"
) %>%
cols_label(
Description = "",
Value = "Amount"
) %>%
tab_style(
style = cell_fill(color = "#fff3cd"),
locations = cells_body(rows = Description == "Estimated Profit Loss")
) %>%
tab_style(
style = cell_text(weight = "bold", size = px(14)),
locations = cells_body(rows = Description %in% c("Estimated Profit Loss", "Percentage Loss vs Expected"))
)
| Estimated Financial Impact of Boycott on 2024 Results | |
| Assuming margins would have remained at pre-boycott levels | |
| Amount | |
|---|---|
| 2024 Revenue | 375M€ |
| Expected Net Margin (2021-2022 avg) | 2.39% |
| Expected Net Income at Historical Margin | 9M€ |
| Actual Net Income 2024 | 5M€ |
| Estimated Profit Loss | 4M€ |
| Percentage Loss vs Expected | 44.3% |
This section separates foreign exchange translation effects from true operational performance to assess the real impact of the boycott.
ggplot(ulysse_analysis, aes(x = year, y = eur_tnd_rate)) +
geom_line(linewidth = 1.2, color = colors$primary) +
geom_point(size = 4, color = colors$primary) +
geom_text(aes(label = round(eur_tnd_rate, 2)), vjust = -1, size = 3.5) +
labs(
title = "EUR/TND Exchange Rate Evolution",
subtitle = "Higher rate = TND weakening vs EUR (favorable for EUR-reported revenues)",
x = "Year",
y = "EUR/TND Rate"
) +
scale_x_continuous(breaks = ulysse_analysis$year) +
geom_hline(yintercept = BASE_FX_RATE,
linetype = "dashed", color = "red") +
annotate("text", x = 2020.3, y = BASE_FX_RATE,
label = paste0("Base Rate (2020): ", BASE_FX_RATE),
vjust = -0.5, hjust = 0, size = 3, color = "red")
revenue_comparison <- ulysse_analysis %>%
dplyr::select(year, revenue, revenue_constant_fx) %>%
pivot_longer(cols = c(revenue, revenue_constant_fx),
names_to = "type",
values_to = "value") %>%
mutate(type = factor(type,
levels = c("revenue", "revenue_constant_fx"),
labels = c("Reported (Actual EUR)", "Constant Currency (2020 FX)")))
p_rev_comp <- ggplot(revenue_comparison, aes(x = year, y = value, color = type, group = type)) +
geom_line(linewidth = 1.2) +
geom_point(size = 3) +
scale_color_manual(values = c("Reported (Actual EUR)" = colors$info,
"Constant Currency (2020 FX)" = "#e67e22")) +
labs(
title = "Reported vs Constant Currency Revenue",
subtitle = "Separating FX effects from operational performance",
x = "Year",
y = "Revenue (Million €)",
color = NULL
) +
scale_x_continuous(breaks = ulysse_analysis$year)
p_fx_impact <- ggplot(ulysse_analysis, aes(x = year, y = fx_impact_revenue)) +
geom_col(aes(fill = fx_impact_revenue > 0), width = 0.6) +
geom_text(aes(label = paste0(ifelse(fx_impact_revenue > 0, "+", ""),
round(fx_impact_revenue, 1), "M€"),
vjust = ifelse(fx_impact_revenue > 0, -0.5, 1.5)),
size = 3.5) +
scale_fill_manual(values = c(colors$negative, colors$positive), guide = "none") +
labs(
title = "FX Translation Impact on Revenue",
subtitle = "Positive = FX benefit, Negative = FX headwind",
x = "Year",
y = "FX Impact (Million €)"
) +
scale_x_continuous(breaks = ulysse_analysis$year) +
geom_hline(yintercept = 0, linetype = "solid", color = "black")
p_rev_comp / p_fx_impact
growth_data <- ulysse_analysis %>%
filter(!is.na(revenue_growth)) %>%
dplyr::select(year, revenue_growth, organic_growth, fx_contribution) %>%
pivot_longer(cols = c(organic_growth, fx_contribution),
names_to = "component",
values_to = "value") %>%
mutate(component = factor(component,
levels = c("organic_growth", "fx_contribution"),
labels = c("Organic Growth (Constant FX)", "FX Impact")))
ggplot(growth_data, aes(x = year, y = value, fill = component)) +
geom_col(position = "stack", width = 0.6) +
geom_hline(yintercept = 0, linetype = "solid", color = "black") +
scale_fill_manual(values = c("Organic Growth (Constant FX)" = colors$info,
"FX Impact" = colors$neutral)) +
labs(
title = "Revenue Growth Decomposition: Organic vs FX Effects",
subtitle = "Stacked bars show contribution from operations vs currency",
x = "Year",
y = "Growth Contribution (%)",
fill = "Component"
) +
scale_x_continuous(breaks = unique(growth_data$year)) +
geom_text(data = ulysse_analysis %>% filter(!is.na(revenue_growth)),
aes(x = year, y = revenue_growth, label = paste0(round(revenue_growth, 1), "%")),
inherit.aes = FALSE, vjust = -0.5, size = 3.5, fontface = "bold")
ulysse_analysis %>%
filter(!is.na(revenue_growth)) %>%
dplyr::select(year, revenue_growth, organic_growth, fx_contribution, eur_tnd_rate) %>%
gt() %>%
tab_header(
title = "Revenue Growth Decomposition by Year",
subtitle = "Separating operational performance from currency effects"
) %>%
cols_label(
year = "Year",
revenue_growth = "Reported Growth",
organic_growth = "Organic Growth",
fx_contribution = "FX Contribution",
eur_tnd_rate = "EUR/TND Rate"
) %>%
fmt_number(
columns = c(revenue_growth, organic_growth, fx_contribution),
decimals = 1,
suffix = "%"
) %>%
fmt_number(
columns = eur_tnd_rate,
decimals = 2
) %>%
tab_style(
style = cell_fill(color = "#e8f4f8"),
locations = cells_body(columns = organic_growth)
) %>%
tab_style(
style = cell_fill(color = "#fff3cd"),
locations = cells_body(columns = fx_contribution)
) %>%
tab_footnote(
footnote = "Organic growth shows operational performance at constant 2020 exchange rates",
locations = cells_column_labels(columns = organic_growth)
)
| Revenue Growth Decomposition by Year | ||||
| Separating operational performance from currency effects | ||||
| Year | Reported Growth | Organic Growth1 | FX Contribution | EUR/TND Rate |
|---|---|---|---|---|
| 2021 | 9.9 | 4.8 | 5.1 | 3.29 |
| 2022 | 0.0 | 1.8 | −1.8 | 3.35 |
| 2023 | −3.5 | −4.0 | 0.6 | 3.33 |
| 2024 | 3.3 | 4.9 | −1.6 | 3.38 |
| 1 Organic growth shows operational performance at constant 2020 exchange rates | ||||
# Calculate constant currency baseline
baseline_revenue_constant <- ulysse_analysis %>%
filter(year %in% PRE_BOYCOTT_YEARS) %>%
pull(revenue_constant_fx) %>%
mean()
organic_comparison <- ulysse_analysis %>%
dplyr::select(year, revenue_constant_fx, period) %>%
mutate(
deviation = revenue_constant_fx - baseline_revenue_constant
)
p_organic <- ggplot(organic_comparison, aes(x = year, y = revenue_constant_fx, fill = period)) +
geom_col(width = 0.7) +
geom_hline(yintercept = baseline_revenue_constant,
linetype = "dashed", color = "black", linewidth = 0.8) +
geom_text(aes(label = paste0(round(revenue_constant_fx, 0), "M€")),
vjust = -0.5, size = 3.5) +
scale_fill_manual(values = c("Pre-Boycott" = colors$info,
"Boycott Start" = colors$warning,
"Post-Boycott" = colors$negative)) +
labs(
title = "Organic Revenue Performance (Constant Currency)",
subtitle = "Removes FX effects to show true operational performance",
x = "Year",
y = "Revenue at Constant 2020 FX (Million €)",
fill = "Period"
) +
scale_x_continuous(breaks = organic_comparison$year) +
annotate("segment", x = 2023.5, xend = 2023.5, y = 330, yend = 380,
linetype = "dashed", color = "red", linewidth = 0.8) +
annotate("text", x = 2020.3, y = baseline_revenue_constant,
label = paste0("Baseline: ", round(baseline_revenue_constant, 0), "M€"),
hjust = 0, vjust = -0.5, size = 3)
p_organic_gap <- organic_comparison %>%
filter(year >= 2021) %>%
ggplot(aes(x = year, y = deviation, fill = deviation < 0)) +
geom_col(width = 0.6) +
geom_text(aes(label = paste0(ifelse(deviation > 0, "+", ""),
round(deviation, 0), "M€"),
vjust = ifelse(deviation > 0, -0.5, 1.5)),
size = 3.5) +
scale_fill_manual(values = c(colors$negative, colors$positive), guide = "none") +
labs(
title = "Organic Revenue Gap vs Pre-Boycott Baseline",
subtitle = "FX-adjusted deviation from 2021-2022 average",
x = "Year",
y = "Deviation (Million €)"
) +
scale_x_continuous(breaks = seq(2021, 2024, 1)) +
geom_hline(yintercept = 0, linetype = "solid", color = "black")
p_organic / p_organic_gap
# Calculate 2024 impact components
baseline_revenue_reported <- baseline_metrics$baseline_revenue
actual_2024_constant <- ulysse_analysis$revenue_constant_fx[ulysse_analysis$year == POST_BOYCOTT_YEAR]
actual_2024_reported <- ulysse_analysis$revenue[ulysse_analysis$year == POST_BOYCOTT_YEAR]
organic_impact <- actual_2024_constant - baseline_revenue_constant
fx_impact_2024 <- actual_2024_reported - actual_2024_constant
total_impact <- actual_2024_reported - baseline_revenue_reported
impact_breakdown <- tribble(
~Component, ~Impact_MEur, ~Percentage,
"2021-2022 Average Revenue (Baseline)", baseline_revenue_reported, 100.0,
"Organic/Operational Change (Constant FX)", organic_impact, (organic_impact/baseline_revenue_reported)*100,
"Currency Translation Effect", fx_impact_2024, (fx_impact_2024/baseline_revenue_reported)*100,
"Total Change (Reported)", total_impact, (total_impact/baseline_revenue_reported)*100,
"2024 Actual Revenue", actual_2024_reported, (actual_2024_reported/baseline_revenue_reported)*100
)
impact_breakdown %>%
gt() %>%
tab_header(
title = "2024 Revenue Change Breakdown: Operational vs Currency Effects",
subtitle = "Comparing 2024 to Pre-Boycott Average (2021-2022)"
) %>%
cols_label(
Component = "",
Impact_MEur = "Amount (M€)",
Percentage = "% of Baseline"
) %>%
fmt_number(
columns = Impact_MEur,
decimals = 1
) %>%
fmt_number(
columns = Percentage,
decimals = 1,
suffix = "%"
) %>%
tab_style(
style = cell_fill(color = "#f0f0f0"),
locations = cells_body(rows = Component %in% c("2021-2022 Average Revenue (Baseline)", "2024 Actual Revenue"))
) %>%
tab_style(
style = cell_fill(color = "#ffe6e6"),
locations = cells_body(rows = Component == "Organic/Operational Change (Constant FX)")
) %>%
tab_style(
style = cell_fill(color = "#fff3cd"),
locations = cells_body(rows = Component == "Currency Translation Effect")
) %>%
tab_style(
style = cell_text(weight = "bold"),
locations = cells_body(rows = Component == "Total Change (Reported)")
)
| 2024 Revenue Change Breakdown: Operational vs Currency Effects | ||
| Comparing 2024 to Pre-Boycott Average (2021-2022) | ||
| Amount (M€) | % of Baseline | |
|---|---|---|
| 2021-2022 Average Revenue (Baseline) | 376.0 | 100.0 |
| Organic/Operational Change (Constant FX) | 5.6 | 1.5 |
| Currency Translation Effect | 7.6 | 2.0 |
| Total Change (Reported) | −1.0 | −0.3 |
| 2024 Actual Revenue | 375.0 | 99.7 |
# Calculate key impact metrics
organic_decline_pct <- (organic_impact / baseline_revenue_constant) * 100
fx_benefit_pct <- (fx_impact_2024 / baseline_revenue_reported) * 100
margin_decline_bps <- (baseline_metrics$baseline_margin -
ulysse_analysis$net_margin[ulysse_analysis$year == POST_BOYCOTT_YEAR])
summary_metrics <- tribble(
~Metric, ~Value, ~Interpretation,
"Organic Revenue Decline", paste0(round(abs(organic_decline_pct), 1), "%"), "True operational impact",
"FX Translation Benefit", paste0("+", round(fx_benefit_pct, 1), "%"), "Currency tailwind masking decline",
"Net Reported Revenue Change", paste0(round((total_impact/baseline_revenue_reported)*100, 1), "%"), "Appears stable but misleading",
"Net Income Decline", "44%", "Severe profitability crisis",
"Margin Compression", paste0(round(margin_decline_bps, 1), " pp"), "From 2.4% to 1.3%",
"ROE Decline", "47%", "From 9.4% to 5.0%",
"Estimated Profit Loss", paste0(round(profit_loss, 1), "M€"), "vs historical margin baseline"
)
summary_metrics %>%
gt() %>%
tab_header(
title = "Boycott Campaign Impact: Summary Metrics",
subtitle = "Comprehensive assessment of 2024 vs 2021-2022 baseline"
) %>%
cols_label(
Metric = "Metric",
Value = "2024 Impact",
Interpretation = "Interpretation"
) %>%
tab_style(
style = cell_fill(color = "#ffe6e6"),
locations = cells_body(rows = Metric %in% c("Net Income Decline", "Estimated Profit Loss"))
) %>%
tab_style(
style = cell_fill(color = "#fff3cd"),
locations = cells_body(rows = Metric == "FX Translation Benefit")
)
| Boycott Campaign Impact: Summary Metrics | ||
| Comprehensive assessment of 2024 vs 2021-2022 baseline | ||
| Metric | 2024 Impact | Interpretation |
|---|---|---|
| Organic Revenue Decline | 1.5% | True operational impact |
| FX Translation Benefit | +2% | Currency tailwind masking decline |
| Net Reported Revenue Change | -0.3% | Appears stable but misleading |
| Net Income Decline | 44% | Severe profitability crisis |
| Margin Compression | 1.1 pp | From 2.4% to 1.3% |
| ROE Decline | 47% | From 9.4% to 5.0% |
| Estimated Profit Loss | 4M€ | vs historical margin baseline |
Constant Currency Calculations: - Base year: 2020 (EUR/TND rate of 3.45) - Formula: Revenue (constant FX) = Revenue (reported) × (Current rate / Base rate) - This approach isolates operational performance from currency translation effects
Baseline Period Selection: - Pre-boycott baseline: 2021-2022 average - Rationale: 2020 excluded due to potential COVID-19 distortions - 2023 excluded as boycott began in Q4
Growth Calculations: - Reported growth: Year-over-year change in EUR-reported figures - Organic growth: Year-over-year change in constant currency figures - FX contribution: Difference between reported and organic growth
Limitations: - Annual data only; quarterly granularity would improve boycott impact precision - Limited to 5 years of data; longer time series would strengthen trend analysis - No external market data for competitive context - Exchange rates are annual averages; intra-year volatility not captured