Practical Data Merging Exercise in R

Objective

To ensure and teach that participant pause and inspect their data before hitting “run”.

Practical Example

_____________________________________________________________________________________________________________

Libraries

###############################################################################
# Load Libraries
###############################################################################

library(tidyverse)
library(readxl)
library(haven)
library(janitor)
library(skimr)
library(naniar)
library(lubridate)
library(stringr)
library(forcats)
library(here)
library(writexl)
library(psych)
library(glue)

1. Importing data

# Dataset1

df1 <- read_xlsx(here("Data", "Data1_2026.xlsx"))
cat("df1: ", dim(df1), "\n")
df1:  729 4 
# Dataset2
df2 <- read_xlsx(here("Data", "Data2_2026.xlsx"))
cat("df2: ", dim(df2))
df2:  749 4

Dataset1 (df1) has 729 rows and 34 columns

Dataset2 (df2) has 749 rows and 34 columns

2. Inspecting df1 and df2

Before you think about merging;

You need to inspect their structures. Because both datasets have exactly 4 columns, you are likely going to do one of two things:

  • A Vertical Join (Stacking rows): Combining them because they share the same columns but contain different rows of data (e.g., combining 2024 data with 2025 data).

  • A Horizontal Join (Merging by ID): Combining them because they have matching rows but different information across variables (columns), utilizing a unique identifier key.

3. Cleaning Variable names

We check the variable names to ensure that they are all well written

# Checking variable names
print(names(df1))
[1] "household_ID"   "region"         "water source"   "household size"
print(names(df2))
[1] "HH_ID"          "malaria_test"   "bednet_used"    "avg_hemoglobin"

Renaming the “hh_id” column name in df2 to match “household_id

df2 <- df2 %>%
  rename(household_id = HH_ID)

Changing column names from “water source” to “water_source”

# df1
df1 <- df1 %>%
  clean_names()

print(names(df1))
[1] "household_id"   "region"         "water_source"   "household_size"
# df2
df2 <- df2 %>%
  clean_names()

print(names(df2))
[1] "household_id"   "malaria_test"   "bednet_used"    "avg_hemoglobin"

4. Checking if the column names are a match

# 1. Checking if column names are identical in names and order
all_columns_match <- identical(names(df1), names(df2))

print(paste("Are all columns matching;", all_columns_match))
[1] "Are all columns matching; FALSE"
# Finding mismatching columns
if(!all_columns_match){
  print("Columns in df1 are not in df2:")
  print(setdiff(names(df1), names(df2)))
  
  
  print("Columns in df2 are not in df1:")
  print(setdiff(names(df2), names(df1)))
}
[1] "Columns in df1 are not in df2:"
[1] "region"         "water_source"   "household_size"
[1] "Columns in df2 are not in df1:"
[1] "malaria_test"   "bednet_used"    "avg_hemoglobin"

5. Checking for data type mismatch

Even if columns share the same name, a vertical merge will fail if one dataset treats a column as a character and the other treats it as a numeric.

mismatched_types <- compare_df_cols(df1, df2) %>%
  filter(df1 != df2)

print(mismatched_types)
[1] column_name df1         df2        
<0 rows> (or 0-length row.names)

There are no data type mismatches

6. Identifying Conflicting columns

Before running the merge, you need to check if there are other matching columns besides Household_ID. If two columns share the exact same name but contain different information, R will force-rename them to “column.x” and “column.y”.


# Find all intersecting columns in both datasets
overlapping_cols <- intersect(names(df1), names(df2))
print(overlapping_cols)
[1] "household_id"

7. Keeping only household_id’s that are present in both datasets

Since our datasets have a shared identifier (household_ID) but contain different unique columns, we shall perform a Horizontal Join.

You want to align your observations by “household” and expand your columns so that the final dataset contains all variables from both df1 and df2.

Because df1 has 729 rows and df2 has 749 rows, you must first choose a merging strategy based on how you want to handle households that might only exist in one of the datasets.

Strategy
Function What it does
Keep Everything full_join() Keeps all households from both datasets. Places NA where data is missing.
Keep Matches Only inner_join() Keeps only households present in both datasets. Drops the rest.
Prioritize df1 left_join() Keeps all households in df1. Drops unmatched rows from df2.
Prioritize df2 right_join() Keeps all households in df2. Drops unmatched rows from df1.

8. Merging:

household_id’s that dont match perfectly across both datasets (df1 and df2) will be dropped.

# Merging the df1 and df2
df_merged <- inner_join(df1, df2, by="household_id")

9. Final dataset

# Final clean dataset
df_final <- df_merged

# Checking final dimension
dim(df_final)
[1] 700   7

The final dataset (df_final) has 700 rows and 67 columns

10. Converting to csv

write_csv(df_final, here("Output", "Data_final.csv"))
LS0tDQp0aXRsZTogIlIgTm90ZWJvb2siDQpvdXRwdXQ6DQogIGh0bWxfbm90ZWJvb2s6IGRlZmF1bHQNCi0tLQ0KDQojIFsqKlByYWN0aWNhbCBEYXRhIE1lcmdpbmcgRXhlcmNpc2UgaW4gUioqXXsudW5kZXJsaW5lfQ0KDQojIyAqKk9iamVjdGl2ZSoqDQoNClRvIGVuc3VyZSBhbmQgdGVhY2ggdGhhdCBwYXJ0aWNpcGFudCBwYXVzZSBhbmQgaW5zcGVjdCB0aGVpciBkYXRhIGJlZm9yZSBoaXR0aW5nICoqInJ1biIuKioNCg0KIyMgKipQcmFjdGljYWwgRXhhbXBsZSoqDQoNClxfXF9cX1xfXF9cX1xfXF9cX1xfXF9cX1xfXF9cX1xfXF9cX1xfXF9cX1xfXF9cX1xfXF9cX1xfXF9cX1xfXF9cX1xfXF9cX1xfXF9cX1xfXF9cX1xfXF9cX1xfXF9cX1xfXF9cX1xfXF9cX1xfXF9cX1xfXF9cX1xfXF9cX1xfXF9cX1xfXF9cX1xfXF9cX1xfXF9cX1xfXF9cX1xfXF9cX1xfXF9cX1xfXF9cX1xfXF9cX1xfXF9cX1xfXF9cX1xfXF9cX1xfXF9cX1xfXF9cX1xfXF9cX1xfDQoNCiMjIFtMaWJyYXJpZXNdey51bmRlcmxpbmV9DQoNCmBgYHtyLCB3YXJuaW5nPUZBTFNFfQ0KIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIw0KIyBMb2FkIExpYnJhcmllcw0KIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIw0KDQpsaWJyYXJ5KHRpZHl2ZXJzZSkNCmxpYnJhcnkocmVhZHhsKQ0KbGlicmFyeShoYXZlbikNCmxpYnJhcnkoamFuaXRvcikNCmxpYnJhcnkoc2tpbXIpDQpsaWJyYXJ5KG5hbmlhcikNCmxpYnJhcnkobHVicmlkYXRlKQ0KbGlicmFyeShzdHJpbmdyKQ0KbGlicmFyeShmb3JjYXRzKQ0KbGlicmFyeShoZXJlKQ0KbGlicmFyeSh3cml0ZXhsKQ0KbGlicmFyeShwc3ljaCkNCmxpYnJhcnkoZ2x1ZSkNCg0KYGBgDQoNCiMjIFsqKjEuIEltcG9ydGluZyBkYXRhKipdey51bmRlcmxpbmV9DQoNCmBgYHtyfQ0KIyBEYXRhc2V0MQ0KDQpkZjEgPC0gcmVhZF94bHN4KGhlcmUoIkRhdGEiLCAiRGF0YTFfMjAyNi54bHN4IikpDQpjYXQoImRmMTogIiwgZGltKGRmMSksICJcbiIpDQoNCiMgRGF0YXNldDINCmRmMiA8LSByZWFkX3hsc3goaGVyZSgiRGF0YSIsICJEYXRhMl8yMDI2Lnhsc3giKSkNCmNhdCgiZGYyOiAiLCBkaW0oZGYyKSkNCg0KYGBgDQoNCkRhdGFzZXQxIChkZjEpIGhhcyA3Mjkgcm93cyBhbmQgMzQgY29sdW1ucw0KDQpEYXRhc2V0MiAoZGYyKSBoYXMgNzQ5IHJvd3MgYW5kIDM0IGNvbHVtbnMNCg0KIyMgWyoqMi4gSW5zcGVjdGluZyBkZjEgYW5kIGRmMioqXXsudW5kZXJsaW5lfQ0KDQpCZWZvcmUgeW91IHRoaW5rIGFib3V0IG1lcmdpbmc7DQoNCllvdSBuZWVkIHRvIGluc3BlY3QgdGhlaXIgc3RydWN0dXJlcy4gQmVjYXVzZSBib3RoIGRhdGFzZXRzIGhhdmUgZXhhY3RseSA0IGNvbHVtbnMsIHlvdSBhcmUgbGlrZWx5IGdvaW5nIHRvIGRvIG9uZSBvZiB0d28gdGhpbmdzOg0KDQotICoqQSBWZXJ0aWNhbCBKb2luIChTdGFja2luZyByb3dzKToqKiBDb21iaW5pbmcgdGhlbSBiZWNhdXNlIHRoZXkgc2hhcmUgdGhlIHNhbWUgY29sdW1ucyBidXQgY29udGFpbiBkaWZmZXJlbnQgcm93cyBvZiBkYXRhIChlLmcuLCBjb21iaW5pbmcgMjAyNCBkYXRhIHdpdGggMjAyNSBkYXRhKS4NCg0KLSAqKkEgSG9yaXpvbnRhbCBKb2luIChNZXJnaW5nIGJ5IElEKSoqOiBDb21iaW5pbmcgdGhlbSBiZWNhdXNlIHRoZXkgaGF2ZSBtYXRjaGluZyByb3dzIGJ1dCBkaWZmZXJlbnQgaW5mb3JtYXRpb24gYWNyb3NzIHZhcmlhYmxlcyAoKmNvbHVtbnMqKSwgdXRpbGl6aW5nIGEgdW5pcXVlIGlkZW50aWZpZXIga2V5Lg0KDQojIyBbKiozLiBDbGVhbmluZyBWYXJpYWJsZSBuYW1lcyoqXXsudW5kZXJsaW5lfQ0KDQpXZSBjaGVjayB0aGUgdmFyaWFibGUgbmFtZXMgdG8gZW5zdXJlIHRoYXQgdGhleSBhcmUgYWxsIHdlbGwgd3JpdHRlbg0KDQpgYGB7cn0NCiMgQ2hlY2tpbmcgdmFyaWFibGUgbmFtZXMNCnByaW50KG5hbWVzKGRmMSkpDQoNCnByaW50KG5hbWVzKGRmMikpDQoNCmBgYA0KDQpSZW5hbWluZyB0aGUgImhoX2lkIiBjb2x1bW4gbmFtZSBpbiBkZjIgdG8gbWF0Y2ggImhvdXNlaG9sZF9pZA0KDQpgYGB7cn0NCmRmMiA8LSBkZjIgJT4lDQogIHJlbmFtZShob3VzZWhvbGRfaWQgPSBISF9JRCkNCg0KYGBgDQoNCkNoYW5naW5nIGNvbHVtbiBuYW1lcyBmcm9tICJ3YXRlciBzb3VyY2UiIHRvICJ3YXRlcl9zb3VyY2UiDQoNCmBgYHtyfQ0KIyBkZjENCmRmMSA8LSBkZjEgJT4lDQogIGNsZWFuX25hbWVzKCkNCg0KcHJpbnQobmFtZXMoZGYxKSkNCg0KIyBkZjINCmRmMiA8LSBkZjIgJT4lDQogIGNsZWFuX25hbWVzKCkNCg0KcHJpbnQobmFtZXMoZGYyKSkNCg0KYGBgDQoNCiMjIFsqKjQuIENoZWNraW5nIGlmIHRoZSBjb2x1bW4gbmFtZXMgYXJlIGEgbWF0Y2gqKl17LnVuZGVybGluZX0NCg0KYGBge3J9DQojIDEuIENoZWNraW5nIGlmIGNvbHVtbiBuYW1lcyBhcmUgaWRlbnRpY2FsIGluIG5hbWVzIGFuZCBvcmRlcg0KYWxsX2NvbHVtbnNfbWF0Y2ggPC0gaWRlbnRpY2FsKG5hbWVzKGRmMSksIG5hbWVzKGRmMikpDQoNCnByaW50KHBhc3RlKCJBcmUgYWxsIGNvbHVtbnMgbWF0Y2hpbmc7IiwgYWxsX2NvbHVtbnNfbWF0Y2gpKQ0KDQojIEZpbmRpbmcgbWlzbWF0Y2hpbmcgY29sdW1ucw0KaWYoIWFsbF9jb2x1bW5zX21hdGNoKXsNCiAgcHJpbnQoIkNvbHVtbnMgaW4gZGYxIGFyZSBub3QgaW4gZGYyOiIpDQogIHByaW50KHNldGRpZmYobmFtZXMoZGYxKSwgbmFtZXMoZGYyKSkpDQogIA0KICANCiAgcHJpbnQoIkNvbHVtbnMgaW4gZGYyIGFyZSBub3QgaW4gZGYxOiIpDQogIHByaW50KHNldGRpZmYobmFtZXMoZGYyKSwgbmFtZXMoZGYxKSkpDQp9DQoNCmBgYA0KDQojIyBbKio1LiBDaGVja2luZyBmb3IgZGF0YSB0eXBlIG1pc21hdGNoKipdey51bmRlcmxpbmV9DQoNCkV2ZW4gaWYgY29sdW1ucyBzaGFyZSB0aGUgc2FtZSBuYW1lLCBhIHZlcnRpY2FsIG1lcmdlIHdpbGwgZmFpbCBpZiBvbmUgZGF0YXNldCB0cmVhdHMgYSBjb2x1bW4gYXMgYSBjaGFyYWN0ZXIgYW5kIHRoZSBvdGhlciB0cmVhdHMgaXQgYXMgYSBudW1lcmljLg0KDQpgYGB7cn0NCm1pc21hdGNoZWRfdHlwZXMgPC0gY29tcGFyZV9kZl9jb2xzKGRmMSwgZGYyKSAlPiUNCiAgZmlsdGVyKGRmMSAhPSBkZjIpDQoNCnByaW50KG1pc21hdGNoZWRfdHlwZXMpDQoNCmBgYA0KDQpUaGVyZSBhcmUgbm8gZGF0YSB0eXBlIG1pc21hdGNoZXMNCg0KIyMgKio2LiBJZGVudGlmeWluZyBDb25mbGljdGluZyBjb2x1bW5zKioNCg0KQmVmb3JlIHJ1bm5pbmcgdGhlIG1lcmdlLCB5b3UgbmVlZCB0byBjaGVjayBpZiB0aGVyZSBhcmUgb3RoZXIgbWF0Y2hpbmcgY29sdW1ucyBiZXNpZGVzIEhvdXNlaG9sZF9JRC4gSWYgdHdvIGNvbHVtbnMgc2hhcmUgdGhlIGV4YWN0IHNhbWUgbmFtZSBidXQgY29udGFpbiBkaWZmZXJlbnQgaW5mb3JtYXRpb24sIFIgd2lsbCBmb3JjZS1yZW5hbWUgdGhlbSB0byAiKmNvbHVtbi54KiIgYW5kICIqY29sdW1uLnkqIi4NCg0KYGBge3J9DQoNCiMgRmluZCBhbGwgaW50ZXJzZWN0aW5nIGNvbHVtbnMgaW4gYm90aCBkYXRhc2V0cw0Kb3ZlcmxhcHBpbmdfY29scyA8LSBpbnRlcnNlY3QobmFtZXMoZGYxKSwgbmFtZXMoZGYyKSkNCnByaW50KG92ZXJsYXBwaW5nX2NvbHMpDQoNCmBgYA0KDQojIyAqKjcuIEtlZXBpbmcgb25seSAqaG91c2Vob2xkX2lkKidzIHRoYXQgYXJlIHByZXNlbnQgaW4gYm90aCBkYXRhc2V0cyoqDQoNClNpbmNlIG91ciBkYXRhc2V0cyBoYXZlIGEgc2hhcmVkIGlkZW50aWZpZXIgKGBob3VzZWhvbGRfSURgKSBidXQgY29udGFpbiBkaWZmZXJlbnQgdW5pcXVlIGNvbHVtbnMsIHdlIHNoYWxsICoqcGVyZm9ybSBhIEhvcml6b250YWwgSm9pbioqLg0KDQpZb3Ugd2FudCB0byBhbGlnbiB5b3VyIG9ic2VydmF0aW9ucyBieSAiKmhvdXNlaG9sZCoiIGFuZCBleHBhbmQgeW91ciBjb2x1bW5zIHNvIHRoYXQgdGhlIGZpbmFsIGRhdGFzZXQgY29udGFpbnMgYWxsIHZhcmlhYmxlcyBmcm9tIGJvdGggYGRmMWAgYW5kIGBkZjJgLg0KDQpCZWNhdXNlIGBkZjFgIGhhcyA3Mjkgcm93cyBhbmQgYGRmMmAgaGFzIDc0OSByb3dzLCB5b3UgbXVzdCBmaXJzdCBjaG9vc2UgYSBtZXJnaW5nIHN0cmF0ZWd5IGJhc2VkIG9uIGhvdyB5b3Ugd2FudCB0byBoYW5kbGUgaG91c2Vob2xkcyB0aGF0IG1pZ2h0IG9ubHkgZXhpc3QgaW4gb25lIG9mIHRoZSBkYXRhc2V0cy4NCg0KKy0tLS0tLS0tLS0tLS0tLS0tLS0rLS0tLS0tLS0tLS0tLS0rLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tKw0KfCB8IFN0cmF0ZWd5ICAgICAgICB8IEZ1bmN0aW9uICAgICB8IFdoYXQgaXQgZG9lcyAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgfA0KKz09PT09PT09PT09PT09PT09PT0rPT09PT09PT09PT09PT0rPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09Kw0KfCBLZWVwIEV2ZXJ5dGhpbmcgICB8IGZ1bGxfam9pbigpICB8IEtlZXBzIGFsbCBob3VzZWhvbGRzIGZyb20gYm90aCBkYXRhc2V0cy4gUGxhY2VzIE5BIHdoZXJlIGRhdGEgaXMgbWlzc2luZy4gfA0KKy0tLS0tLS0tLS0tLS0tLS0tLS0rLS0tLS0tLS0tLS0tLS0rLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tKw0KfCBLZWVwIE1hdGNoZXMgT25seSB8IGlubmVyX2pvaW4oKSB8IEtlZXBzIG9ubHkgaG91c2Vob2xkcyBwcmVzZW50IGluIGJvdGggZGF0YXNldHMuIERyb3BzIHRoZSByZXN0LiAgICAgICAgICAgfA0KKy0tLS0tLS0tLS0tLS0tLS0tLS0rLS0tLS0tLS0tLS0tLS0rLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tKw0KfCBQcmlvcml0aXplIGRmMSAgICB8IGxlZnRfam9pbigpICB8IEtlZXBzIGFsbCBob3VzZWhvbGRzIGluIGRmMS4gRHJvcHMgdW5tYXRjaGVkIHJvd3MgZnJvbSBkZjIuICAgICAgICAgICAgICAgfA0KKy0tLS0tLS0tLS0tLS0tLS0tLS0rLS0tLS0tLS0tLS0tLS0rLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tKw0KfCBQcmlvcml0aXplIGRmMiAgICB8IHJpZ2h0X2pvaW4oKSB8IEtlZXBzIGFsbCBob3VzZWhvbGRzIGluIGRmMi4gRHJvcHMgdW5tYXRjaGVkIHJvd3MgZnJvbSBkZjEuICAgICAgICAgICAgICAgfA0KKy0tLS0tLS0tLS0tLS0tLS0tLS0rLS0tLS0tLS0tLS0tLS0rLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tKw0KDQojIyA4LiAqKk1lcmdpbmc6KioNCg0KKioqaG91c2Vob2xkX2lkKioqJ3MgdGhhdCBkb250IG1hdGNoIHBlcmZlY3RseSBhY3Jvc3MgYm90aCBkYXRhc2V0cyAoZGYxIGFuZCBkZjIpIHdpbGwgYmUgZHJvcHBlZC4NCg0KYGBge3J9DQojIE1lcmdpbmcgdGhlIGRmMSBhbmQgZGYyDQpkZl9tZXJnZWQgPC0gaW5uZXJfam9pbihkZjEsIGRmMiwgYnk9ImhvdXNlaG9sZF9pZCIpDQoNCmBgYA0KDQojIyAqKjkuIEZpbmFsIGRhdGFzZXQqKg0KDQpgYGB7cn0NCiMgRmluYWwgY2xlYW4gZGF0YXNldA0KZGZfZmluYWwgPC0gZGZfbWVyZ2VkDQoNCiMgQ2hlY2tpbmcgZmluYWwgZGltZW5zaW9uDQpkaW0oZGZfZmluYWwpDQpgYGANCg0KVGhlIGZpbmFsIGRhdGFzZXQgKGRmX2ZpbmFsKSBoYXMgNzAwIHJvd3MgYW5kIDY3IGNvbHVtbnMNCg0KIyMgKioxMC4gQ29udmVydGluZyB0byBjc3YqKg0KDQpgYGB7cn0NCndyaXRlX2NzdihkZl9maW5hbCwgaGVyZSgiT3V0cHV0IiwgIkRhdGFfZmluYWwuY3N2IikpDQpgYGANCg==