Code
:root { --crimson: #AA272F; }
h1, h2, h3 { color: var(--crimson); font-weight: 600; }
.callout-note { border-left-color: var(--crimson) !important; }
blockquote { border-left: 4px solid var(--crimson); padding-left: 1rem; }Econometrics Assignment #4 | Linear Regression, Explanation and Prediction
:root { --crimson: #AA272F; }
h1, h2, h3 { color: var(--crimson); font-weight: 600; }
.callout-note { border-left-color: var(--crimson) !important; }
blockquote { border-left: 4px solid var(--crimson); padding-left: 1rem; }Question: Is foreign direct investment (FDI) associated with renewable energy consumption in Egypt, Ethiopia, Kenya, Sudan, and Uganda?
The analysis utilizes ordinary linear regression (lm()). It is an introductory association study, not a causal or fully specified panel-data analysis.
Foreign investment could help renewable energy through technology and financing (a pollution halo mechanism), but it could also support fossil-fuel industries (a pollution haven mechanism). The data will help describe the association, not prove which mechanism caused it.
A statistical relationship can be useful for explanation and prediction without proving that changing FDI will cause renewable energy use to change.
library(ggplot2)
library(broom)
library(knitr)
library(kableExtra)
crimson <- "#AA272F"
table_print <- function(x, caption, digits = 2) {
knitr::kable(x, format = "html", digits = digits,
caption = caption, row.names = FALSE) |>
kableExtra::kable_styling(
bootstrap_options = c("striped", "hover"), full_width = FALSE)
}csv_path <- "udaje/LDC-Panel-Data.csv"
if (!file.exists(csv_path)) csv_path <- "LDC-Panel-Data.csv"
raw <- read.csv(csv_path, na.strings = c("", "NA"), stringsAsFactors = FALSE)
required <- c("Country_Code", "Year", "FDI_GDP", "GDP_USD", "IND_VA_GDP",
"NODA_RCV", "REN_ERG_CONS", "Trade_GDP")
if (!all(required %in% names(raw))) stop("The CSV does not have the expected column names.")
raw$Country <- factor(raw$Country_Code,
levels = c("EGY", "ETH", "KEN", "SDN", "UGA"),
labels = c("Egypt", "Ethiopia", "Kenya", "Sudan", "Uganda"))
raw$GDP_billions <- raw$GDP_USD / 1e9
stopifnot(!anyDuplicated(raw[c("Country_Code", "Year")]))
table_print(head(raw[c("Country", "Year", "FDI_GDP", "GDP_USD", "IND_VA_GDP",
"NODA_RCV", "REN_ERG_CONS", "Trade_GDP")], 10),
"First ten country-year observations")| Country | Year | FDI_GDP | GDP_USD | IND_VA_GDP | NODA_RCV | REN_ERG_CONS | Trade_GDP |
|---|---|---|---|---|---|---|---|
| Egypt | 1995 | 1.0 | 136218680413 | 30.2 | 3.4 | 9.4 | 50.25 |
| Egypt | 1996 | 0.9 | 143014263359 | 29.5 | 3.2 | 9.0 | 46.95 |
| Egypt | 1997 | 1.1 | 150869114035 | 29.0 | 2.5 | 8.6 | 43.74 |
| Egypt | 1998 | 1.3 | 159280817658 | 28.6 | 2.3 | 8.5 | 41.93 |
| Egypt | 1999 | 1.2 | 168922784450 | 28.4 | 1.8 | 8.8 | 38.36 |
| Egypt | 2000 | 1.2 | 179683172290 | 30.8 | 1.4 | 8.3 | 39.02 |
| Egypt | 2001 | 0.5 | 186035425185 | 30.9 | 1.3 | 7.8 | 39.81 |
| Egypt | 2002 | 0.8 | 190482051409 | 32.6 | 1.5 | 7.3 | 40.99 |
| Egypt | 2003 | 0.3 | 196565009504 | 33.4 | 1.3 | 7.4 | 46.18 |
| Egypt | 2004 | 1.6 | 204608590453 | 34.7 | 1.9 | 7.6 | 57.82 |
variable_dictionary <- data.frame(
Variable = c("REN_ERG_CONS", "FDI_GDP", "GDP_billions", "IND_VA_GDP", "NODA_RCV", "Trade_GDP"),
Meaning = c("Renewable energy consumption (% of total final energy consumption)",
"FDI net inflows (% of GDP)",
"GDP (constant 2015 US$, billions)",
"Industry, including construction, value added (% of GDP)",
"Net official development assistance received (% of GNI)",
"Trade (% of GDP)"))
table_print(variable_dictionary, "Variables and their measurement units")| Variable | Meaning |
|---|---|
| REN_ERG_CONS | Renewable energy consumption (% of total final energy consumption) |
| FDI_GDP | FDI net inflows (% of GDP) |
| GDP_billions | GDP (constant 2015 US$, billions) |
| IND_VA_GDP | Industry, including construction, value added (% of GDP) |
| NODA_RCV | Net official development assistance received (% of GNI) |
| Trade_GDP | Trade (% of GDP) |
All percentage variables are expressed in percentage points when we discuss a one-unit difference. GDP is rescaled to billions only for readability; the original GDP values are unchanged in economic meaning.
analysis_vars <- c("REN_ERG_CONS", "FDI_GDP", "GDP_billions", "IND_VA_GDP", "NODA_RCV", "Trade_GDP")
missing_table <- data.frame(
Variable = analysis_vars,
Missing = sapply(raw[analysis_vars], function(z) sum(is.na(z))))
table_print(missing_table, "Missing observations in the original dataset", 0)| Variable | Missing |
|---|---|
| REN_ERG_CONS | 19 |
| FDI_GDP | 5 |
| GDP_billions | 0 |
| IND_VA_GDP | 0 |
| NODA_RCV | 10 |
| Trade_GDP | 16 |
We use complete-case analysis: a row enters a model only if every variable needed for that model is observed. No missing value is replaced by a mean or median.
simple_data <- raw[complete.cases(raw[c("REN_ERG_CONS", "FDI_GDP")]), ]
multiple_data <- raw[complete.cases(raw[analysis_vars]), ]
if (nrow(multiple_data) < 10) stop("Too few complete observations for the multiple regression.")
sample_counts <- data.frame(
Sample = c("Original CSV", "Simple regression", "Multiple regression"),
Country_years = c(nrow(raw), nrow(simple_data), nrow(multiple_data)))
table_print(sample_counts, "Number of country-year observations used", 0)| Sample | Country_years |
|---|---|
| Original CSV | 155 |
| Simple regression | 136 |
| Multiple regression | 120 |
The models have different sample sizes because the multiple regression needs more observed variables. Missing observations may not be random, so the results describe the available complete observations.
descriptive <- data.frame(
Variable = analysis_vars,
Minimum = sapply(multiple_data[analysis_vars], min, na.rm = TRUE),
Median = sapply(multiple_data[analysis_vars], median, na.rm = TRUE),
Mean = sapply(multiple_data[analysis_vars], mean, na.rm = TRUE),
Maximum = sapply(multiple_data[analysis_vars], max, na.rm = TRUE),
Range = sapply(multiple_data[analysis_vars], function(x) diff(range(x, na.rm = TRUE))),
Variance = sapply(multiple_data[analysis_vars], var, na.rm = TRUE),
Std_Deviation = sapply(multiple_data[analysis_vars], sd, na.rm = TRUE),
row.names = NULL
)
table_print(descriptive, "Descriptive statistics: complete multiple-regression sample")| Variable | Minimum | Median | Mean | Maximum | Range | Variance | Std_Deviation |
|---|---|---|---|---|---|---|---|
| REN_ERG_CONS | 4.90 | 76.90 | 64.17 | 96.50 | 91.60 | 1078.49 | 32.84 |
| FDI_GDP | -0.20 | 2.54 | 2.56 | 9.30 | 9.50 | 3.56 | 1.89 |
| GDP_billions | 9.12 | 50.89 | 93.78 | 425.78 | 416.66 | 10342.23 | 101.70 |
| IND_VA_GDP | 9.50 | 21.65 | 23.08 | 39.90 | 30.40 | 53.34 | 7.30 |
| NODA_RCV | 0.00 | 3.82 | 4.85 | 16.34 | 16.34 | 14.96 | 3.87 |
| Trade_GDP | 4.13 | 37.34 | 38.97 | 71.75 | 67.62 | 162.17 | 12.73 |
ggplot(simple_data, aes(x = FDI_GDP, y = REN_ERG_CONS)) +
geom_point(aes(color = Country), alpha = 0.72, size = 2.3) +
geom_smooth(method = "lm", se = TRUE, color = crimson,
fill = "#E8BCC0", linewidth = 1.15) +
scale_color_brewer(palette = "Dark2") +
labs(title = "Foreign investment and renewable energy use",
subtitle = "Five developing countries; observed country-years",
x = "FDI net inflows (% of GDP)",
y = "Renewable energy consumption (% of total final energy)",
color = "Country",
caption = "Source: supplied World Bank-derived CSV.") +
theme_minimal(base_size = 12) +
theme(plot.title = element_text(face = "bold"), legend.position = "bottom")Reading the graph: The dots are real observations. The crimson line summarizes their average linear relationship. Points far above or below the line have larger residuals. Country differences and time trends may also influence the apparent relationship.
\[ REN_{it} = \beta_0 + \beta_1 FDI_{it} + u_{it}. \]
Here \(i\) identifies a country and \(t\) a year. The intercept is the predicted renewable share when FDI equals zero. The slope tells us the predicted difference in renewable share for a one-percentage-point difference in FDI as a share of GDP.
model_simple <- lm(REN_ERG_CONS ~ FDI_GDP, data = simple_data)
table_print(broom::tidy(model_simple)[c("term", "estimate", "std.error", "p.value")],
"Simple linear regression: coefficient estimates", 3)| term | estimate | std.error | p.value |
|---|---|---|---|
| (Intercept) | 64.975 | 4.696 | 0.000 |
| FDI_GDP | 1.109 | 1.494 | 0.459 |
The fitted equation is:
\[ \widehat{REN}_{it} = 64.975 + 1.109 FDI_{it}. \]
The interpretation: In this simple model, a one-percentage-point higher FDI-to-GDP ratio is associated with a 1.11-percentage-point higher predicted renewable energy consumption share. This is an average association, not an established effect of investment.
The intercept of 64.98 is the model’s prediction at FDI = 0, which should not be treated as a universal baseline for every country.
To account for other measured economic characteristics, we add GDP, industry, official development assistance (ODA), and trade openness to the model:
\[ REN_{it} = \beta_0 + \beta_1 FDI_{it} + \beta_2 GDP_{it} + \beta_3 Industry_{it} + \beta_4 ODA_{it} + \beta_5 Trade_{it} + u_{it}. \]
model_multiple <- lm(REN_ERG_CONS ~ FDI_GDP + GDP_billions + IND_VA_GDP +
NODA_RCV + Trade_GDP, data = multiple_data)
multiple_coefs <- broom::tidy(model_multiple)
table_print(multiple_coefs[c("term", "estimate", "std.error", "p.value")],
"Multiple linear regression: coefficient estimates", 3)| term | estimate | std.error | p.value |
|---|---|---|---|
| (Intercept) | 96.310 | 6.191 | 0.000 |
| FDI_GDP | 1.497 | 0.737 | 0.045 |
| GDP_billions | -0.187 | 0.022 | 0.000 |
| IND_VA_GDP | -1.041 | 0.292 | 0.001 |
| NODA_RCV | 1.850 | 0.399 | 0.000 |
| Trade_GDP | -0.086 | 0.100 | 0.394 |
terms <- c("FDI_GDP", "GDP_billions", "IND_VA_GDP", "NODA_RCV", "Trade_GDP")
units <- c("one percentage point of FDI (% of GDP)",
"one billion constant-2015 US dollars of GDP",
"one percentage point of industry's GDP share",
"one percentage point of ODA (% of GNI)",
"one percentage point of trade (% of GDP)")
b <- coef(model_multiple)[terms]
interpretation <- data.frame(
Variable = terms,
One_unit_change_in_X = units,
Expected_change_in_REN_percentage_points = round(unname(b), 3))
table_print(interpretation, "Marginal associations, holding other included variables constant", 3)| Variable | One_unit_change_in_X | Expected_change_in_REN_percentage_points |
|---|---|---|
| FDI_GDP | one percentage point of FDI (% of GDP) | 1.497 |
| GDP_billions | one billion constant-2015 US dollars of GDP | -0.187 |
| IND_VA_GDP | one percentage point of industry's GDP share | -1.041 |
| NODA_RCV | one percentage point of ODA (% of GNI) | 1.850 |
| Trade_GDP | one percentage point of trade (% of GDP) | -0.086 |
The multiple-model FDI coefficient is 1.497. This means that for country-years with the same measured GDP, industry share, ODA, and trade, one percentage point higher FDI is associated with 1.497 percentage points more predicted renewable consumption.
The GDP coefficient refers to one billion dollars, not one dollar or a one-percent GDP increase. Industry, ODA, and trade coefficients refer to one-percentage-point differences in their respective indicators.
# The first two rows describe each model's natural available sample.
# The last two use the same complete observations for a fairer comparison.
model_simple_common <- lm(REN_ERG_CONS ~ FDI_GDP, data = multiple_data)
model_list <- list(
"Simple (available sample)" = model_simple,
"Multiple (available sample)" = model_multiple,
"Simple (common sample)" = model_simple_common,
"Multiple (common sample)" = model_multiple)
comparison <- do.call(rbind, lapply(names(model_list), function(nm) {
g <- broom::glance(model_list[[nm]])
data.frame(Model = nm, N = g$nobs, R_squared = g$r.squared,
Residual_SD = g$sigma)
}))
table_print(comparison, "Model fit and sample sizes", 3)| Model | N | R_squared | Residual_SD |
|---|---|---|---|
| Simple (available sample) | 136 | 0.004 | 32.447 |
| Multiple (available sample) | 120 | 0.836 | 13.570 |
| Simple (common sample) | 120 | 0.008 | 32.852 |
| Multiple (common sample) | 120 | 0.836 | 13.570 |
\(R^2\) describes the proportion of variation in renewable consumption explained within the estimation equation. It does not mean that the model is correct a certain percentage of the time. Residual SD measures the typical size of the remaining errors in renewable-share percentage points. Adding predictors often raises \(R^2\) even when they add little practical value.
Question to consider: Does the FDI coefficient change after adding the other variables? This can happen because FDI is correlated with other economic conditions, or because the usable sample changes. The common-sample comparison helps separate these two issues.
A fitted value is the model’s estimate of an observed outcome. A residual is the observed value minus that fitted value:
\[ e_{it} = REN_{it} - \widehat{REN}_{it}. \]
actual_vs_fitted <- data.frame(
Country = multiple_data$Country,
Year = multiple_data$Year,
Actual_REN = multiple_data$REN_ERG_CONS,
Fitted_REN = fitted(model_multiple),
Residual = residuals(model_multiple))
table_print(head(actual_vs_fitted, 10),
"Observed, fitted and residual renewable energy shares", 2)| Country | Year | Actual_REN | Fitted_REN | Residual |
|---|---|---|---|---|
| Egypt | 1995 | 9.4 | 42.81 | -33.41 |
| Egypt | 1996 | 9.0 | 42.03 | -33.03 |
| Egypt | 1997 | 8.6 | 40.36 | -31.76 |
| Egypt | 1998 | 8.5 | 39.28 | -30.78 |
| Egypt | 1999 | 8.8 | 36.91 | -28.11 |
| Egypt | 2000 | 8.3 | 31.60 | -23.30 |
| Egypt | 2001 | 7.8 | 29.01 | -21.21 |
| Egypt | 2002 | 7.3 | 27.12 | -19.82 |
| Egypt | 2003 | 7.4 | 23.58 | -16.18 |
| Egypt | 2004 | 7.6 | 22.78 | -15.18 |
A positive residual means renewable energy use was higher than the model predicted. A negative residual means it was lower. Neither sign proves a policy success or failure.
Here we add one accessible check because an unusual observation can change the fitted slope. A large residual means a point lies far from the fitted line; Cook’s distance indicates whether a point might substantially affect the fitted model.
n <- nobs(model_multiple)
cooks_cutoff <- 4 / n # A simple screening rule, not a formal test.
influence <- data.frame(
Country = multiple_data$Country,
Year = multiple_data$Year,
FDI_GDP = multiple_data$FDI_GDP,
REN_ERG_CONS = multiple_data$REN_ERG_CONS,
Residual = residuals(model_multiple),
Cooks_D = cooks.distance(model_multiple))
influence <- influence[order(-influence$Cooks_D), ]
table_print(head(influence, 10),
"Ten observations with the largest Cook's distances", 3)| Country | Year | FDI_GDP | REN_ERG_CONS | Residual | Cooks_D |
|---|---|---|---|---|---|
| Sudan | 2021 | 1.528 | 61.0 | -26.214 | 0.097 |
| Egypt | 2006 | 9.300 | 6.2 | -19.739 | 0.089 |
| Egypt | 2021 | 1.200 | 6.1 | 19.133 | 0.083 |
| Egypt | 2020 | 1.500 | 6.7 | 21.146 | 0.071 |
| Egypt | 2007 | 8.900 | 6.0 | -17.331 | 0.069 |
| Egypt | 2019 | 2.800 | 6.2 | 19.994 | 0.043 |
| Egypt | 1995 | 1.000 | 9.4 | -33.407 | 0.041 |
| Egypt | 2005 | 6.000 | 6.5 | -20.042 | 0.035 |
| Sudan | 2020 | 2.652 | 62.6 | -22.180 | 0.034 |
| Egypt | 1996 | 0.900 | 9.0 | -33.026 | 0.033 |
plot(cooks.distance(model_multiple), type = "h", lwd = 2,
col = crimson, xlab = "Observation number in complete sample",
ylab = "Cook's distance", main = "Influence screening")
abline(h = cooks_cutoff, col = "gray35", lty = 2)We do not automatically delete observations above the threshold. First we check whether they reflect genuine country-year values rather than a data-entry error. To see how sensitive the FDI slope is, we can re-estimate the same model after excluding the flagged rows as a sensitivity check only.
flagged <- cooks.distance(model_multiple) > cooks_cutoff
if (sum(!flagged) > length(coef(model_multiple)) + 1 && any(flagged)) {
model_sensitivity <- lm(formula(model_multiple), data = multiple_data[!flagged, ])
sensitivity <- data.frame(
Model = c("Full complete sample", "Sensitivity: exclude flagged observations"),
N = c(nobs(model_multiple), nobs(model_sensitivity)),
FDI_coefficient = c(coef(model_multiple)["FDI_GDP"],
coef(model_sensitivity)["FDI_GDP"]))
} else {
sensitivity <- data.frame(
Model = "Full complete sample (no feasible flagged-case comparison)",
N = nobs(model_multiple),
FDI_coefficient = unname(coef(model_multiple)["FDI_GDP"]))
}
table_print(sensitivity, "Sensitivity of the FDI coefficient to influential observations", 3)| Model | N | FDI_coefficient |
|---|---|---|
| Full complete sample | 120 | 1.497 |
| Sensitivity: exclude flagged observations | 111 | 2.223 |
Decision: Keep the original complete-case model as the main result. The sensitivity model is presented separately, not used to select observations just because they produce a preferred result.
Explanation: After observing an FDI level, the simple model describes the expected renewable share. The slope measures the average difference in predicted renewable consumption for a one-unit change in FDI.
Prediction: Before observing a hypothetical renewable share, we insert an FDI value into the same fitted equation using predict().
new_fdi <- data.frame(FDI_GDP = c(1, 3, 5))
new_fdi$Predicted_REN <- predict(model_simple, newdata = new_fdi)
table_print(new_fdi, "Simple-model predictions at three hypothetical FDI levels", 2)| FDI_GDP | Predicted_REN |
|---|---|
| 1 | 66.08 |
| 3 | 68.30 |
| 5 | 70.52 |
For example, the prediction at FDI = 3% of GDP is 68.3% of total final energy consumption. This is a fitted average, not a guaranteed outcome for any country.
For a clear comparison, keep the other explanatory variables at their sample medians, then change only FDI. This is a hypothetical calculation, not a policy simulation.
new_multiple <- data.frame(
FDI_GDP = c(1, 3, 5),
GDP_billions = median(multiple_data$GDP_billions),
IND_VA_GDP = median(multiple_data$IND_VA_GDP),
NODA_RCV = median(multiple_data$NODA_RCV),
Trade_GDP = median(multiple_data$Trade_GDP))
new_multiple$Predicted_REN <- predict(model_multiple, newdata = new_multiple)
table_print(new_multiple, "Multiple-model predictions (other variables held at medians)", 2)| FDI_GDP | GDP_billions | IND_VA_GDP | NODA_RCV | Trade_GDP | Predicted_REN |
|---|---|---|---|---|---|
| 1 | 50.89 | 21.65 | 3.82 | 37.34 | 69.59 |
| 3 | 50.89 | 21.65 | 3.82 | 37.34 | 72.58 |
| 5 | 50.89 | 21.65 | 3.82 | 37.34 | 75.58 |
Important: These are point predictions. The renewable share is bounded between 0 and 100, but ordinary linear regression does not automatically enforce those bounds. We should not interpret predictions outside that range as physically meaningful, and should avoid extrapolating far beyond observed FDI values.
The simple regression estimates the overall association between FDI inflows and renewable energy consumption. The multiple regression compares country-years with similar measured GDP, industry share, development assistance, and trade openness. Predictions translate the estimated relationships into hypothetical renewable-energy shares, while the influence check shows whether a small number of observations may matter disproportionately.
Main takeaway: The multiple-model FDI slope is conditional on the included controls. This result is descriptive, not causal.
World Bank. World Development Indicators. https://databank.worldbank.org/source/world-development-indicators