# Load libraries
library(here)
library(tidyverse)
library(jsonlite)
# Load JSON Data
raw_data_dir <- here("raw_data_files")
# I am adding this "pattern =" operation here so that list.files() only looks at
# and pulls .json files. This prevents issues in case I end up having non-json
# raw data.
raw_data_file_names <- list.files(raw_data_dir,
pattern = ".json")
# Magic loop to read in the JSON data
for (i in raw_data_file_names) {
current_file <- fromJSON(file.path(
raw_data_dir,
i
), flatten = T)
simple_name <- i |>
str_remove(".json")
assign(simple_name, current_file)
}Bad Data :(
Last time, I did the work of pulling in my found data set and getting it back to a state where I can start using it for projects, again.
As I work through data, I will continuously apply a variety of methods to check and make sure everything is looking good. You never really know where an issue is going to pop up. If my data is supposed to be tidy, that is one of the first things I check. Since I built this data set, I know how it SHOULD be structured, but when I translated it all to relational tables in MySQL, I broke apart the data and planned to use universally unique identifiers (UUID) as primary keys (PK) to link it all back together as needed. Before I went too much further, I decided to check to make sure my PK’s were still in tact. For me, the easiest way to check this is use dplyr’s group_by() + count() on the PKs in the base set, and then filter to anything that is > 1.
Before I do anything, I need to load everything up as outlined in the previous post.
Now, I group on the mainID (my UUID/PK) and count every record under that mainID. There should only be 1 record per UUID. If there are counts > 1, then my values are duplicated, and that will suck.
main_base |>
group_by(mainID) |>
count() |>
filter(n > 1)# A tibble: 147 × 2
# Groups: mainID [147]
mainID n
<chr> <int>
1 0LG2MOY0TDA9 2
2 0M1LCZSXB2PF 2
3 0VPSBL9DOWCM 2
4 0VSNH1PYDG4N 2
5 19RVC0OQBAAL 2
6 1GBTCR5DUAUU 2
7 1MUF71JB23LV 2
8 21IH1RUEQBOT 2
9 25OWMINUBWJT 2
10 26YHIRUVYF8C 2
# ℹ 137 more rows
DAMMIT!
…
OK, so. At some point, when I translated this data to SQL, I must have accidentally assigned some records the same mainID value. I can’t recall exactly what happened, but I likely just copied and pasted a set of IDs that had already been used on a previous a set of records. I have some suspicions as to WHEN this occurred, but I want to do some digging.
I’ll create a vector (dupe_main_ids) with these duplicated UUIDs, and use that to filter to the problem records. I want to see what these records might have in common. I will start with the top and bottom 10 records, using dplyr’s slice_head() and slice_tail() functions.
dupe_main_ids <- main_base |>
group_by(mainID) |>
count() |>
filter(n > 1) |>
# Use pull() to create a vector out of a single column.
pull(mainID)
# Slice first 10 records using the IDs in dupe_main_ids as a filter.
main_base |>
filter(mainID %in% dupe_main_ids) |>
arrange(mainID) |>
slice_head(n = 10) mainID entity locationID flagForDeletion
1 0LG2MOY0TDA9 Panemotichus JWJUHI80CZOH
2 0LG2MOY0TDA9 Hypaepa OSPGY6Y0VUL6
3 0M1LCZSXB2PF Tavium MQUX5CFU1NNJ
4 0M1LCZSXB2PF Isaura Nova X590S9QZE7WQ
5 0VPSBL9DOWCM Mater RFBDLM8APMI3
6 0VPSBL9DOWCM Orcistus WJFQ9W9SEY4R
7 0VSNH1PYDG4N Paleopolis D0A6EI38MYZO
8 0VSNH1PYDG4N Pacandus X6M1XLB49MF8
9 19RVC0OQBAAL Temnus HLCAJBTXUFTI
10 19RVC0OQBAAL Soloi OM1JXCJH8QTB
# Slice last 10 records.
main_base |>
filter(mainID %in% dupe_main_ids) |>
arrange(mainID) |>
slice_tail(n = 10) mainID entity locationID flagForDeletion
1 Z6RY9EEIAAXH Himeria OZDEE3U7X2XS
2 Z6RY9EEIAAXH Numana T4J7ZLQY4M0Y
3 ZFIGWBDNF5RG Cusae VZ92V4AA1ED1
4 ZFIGWBDNF5RG Foligno O7LK917CGUPW
5 ZKHST7ZWEBBV Busiris FBZOC6XJA5YZ
6 ZKHST7ZWEBBV Lorium WYS2COJIIT4K
7 ZOGWL6PFL8N7 Augustopolis 2LGSJWDRI3GM
8 ZOGWL6PFL8N7 Sulci JPRF28EPOHAR
9 ZY1KR1DP7ZGF Gabala 434YSWWCWTQ9
10 ZY1KR1DP7ZGF Kildare Karpathus
I arranged the output by mainID so that the identical UUIDs appear together. From what I immediately see, the duplicate ID’s at the head and tail of the data set belong to cities, towns, and villages. I say this with confidence because of how extensively I have worked with this data, and I remember breaking many of these places out into their own records - which is likely where these errors were introduced. I also notice that will the exception of Kildare, at the very end, the location ID’s appear to be intact.
Going to perform one more check…sometimes, I like to perform random spot checks using dplyr’s slice_sample() (related to slice_head/tail() functions I used above). This allows you to slice your data set to any number of random records. Each time you run the code, you get a new set. I will select 10 random records and run this code 3-4 times.
main_base |>
filter(mainID %in% dupe_main_ids) |>
slice_sample(n = 10) mainID entity locationID flagForDeletion
1 5CL3R7YKJWKM Gortina HRUGI2UNCU9X
2 UC645EYZQJRP Nilopolis 2FENUGKI77M5
3 QI8C95MTX01P Octodurum LDQJV9WMIMKI
4 NO8X4Q41X4EL Hirena 9MEQILLO8UIZ
5 HORX3L7D3N5N Málaga RS681LGC15WD
6 NM930AOY956U Theudalis LOSL2U4E5LB6
7 H6C8FODF8E7K Atrib LAHHPAUO5BTT
8 ER8B453EZN4X Philippi ALHL3JVDUH87
9 II0WN9R9NU4L Carpentras MQMUCRDOLQDZ
10 LYZKXBQF6H3E Lilibeo Q1MTDLVI12W0
I am pretty satisfied that these are all places.
Let’s see what happens when I connect them to the locations set. Hopefully, they connect to the right entries. I’ll use dplyr’s left_join() on the locationID (which is the PK for locations) to do this. I am only really interested in confirming that the place names line up, so I will only pull over ancientcity, ancientregion, and modernCountry. I will also spot check my records.
main_base |>
filter(mainID %in% dupe_main_ids) |>
left_join(
locations |>
# Pull over only these columns.
select(locationID,
ancientcity,
ancientregion,
modernCountry),
# Join on locationID.
by = join_by(locationID)) |>
# slice_sample 5 random records at a time.
slice_sample(n = 5) mainID entity locationID flagForDeletion
1 GBE3N0802QIL Scythopolis F3EYMFHUQ68M
2 OMCNX80SK5BG Elusa 0AW5GKUT61PM
3 4PPCCWYEGEZB Thelepte UIRQ5TK5YOQ4
4 D1WJ33D1ZW1X Chiusi 9WWQOPZPHB57
5 8NIYJ369LS1P Berenice QQXEWFRKBSAB
ancientcity ancientregion modernCountry
1 Baithsan/Scythopolis/Nysa (blank) Israel
2 Elusa Palaestina Tertia Israel
3 Thelepte/Sufetula Byzacene Tunisia
4 Chiusi - Camars - Clusium Etruria Italy
5 Berenice/‘Aydhab Egypt Egypt
IRL, I ran that code 5 times, and it looks like the place names line up. I also did a visual check with view(), since this isn’t a lot of entries. It did appear that everything was still properly connected location-wise. This is a bit of a relief. Finding the locations for a lot of these places was one of the more time-consuming, painstaking tasks I performed. Many of these locations have changed names over the past 2 millennia, and I spent a lot of time tracking some of them down. I am pretty happy I don’t have to do that over.
Despite the fact the locations remain intact, I can’t trust these ID’s to connect to the other sets. This was confirmed when I tried to connect these them to sources. Focusing on one set of these records, you can see the issue:
main_base |>
as_tibble() |>
filter(mainID == "0LG2MOY0TDA9") |>
left_join(
sources,
by = join_by(mainID)
) |>
arrange(sourceType) |>
print(n = Inf)Warning in left_join(filter(as_tibble(main_base), mainID == "0LG2MOY0TDA9"), : Detected an unexpected many-to-many relationship between `x` and `y`.
ℹ Row 1 of `x` matches multiple rows in `y`.
ℹ Row 2393 of `y` matches multiple rows in `x`.
ℹ If a many-to-many relationship is expected, set `relationship =
"many-to-many"` to silence this warning.
# A tibble: 14 × 6
mainID entity locationID flagForDeletion sourceURL sourceType
<chr> <chr> <chr> <chr> <chr> <chr>
1 0LG2MOY0TDA9 Panemotichus JWJUHI80CZOH "" 12822 darmcAtHa…
2 0LG2MOY0TDA9 Hypaepa OSPGY6Y0VUL6 "" 12822 darmcAtHa…
3 0LG2MOY0TDA9 Panemotichus JWJUHI80CZOH "" http://www… newAdvent…
4 0LG2MOY0TDA9 Panemotichus JWJUHI80CZOH "" http://www… newAdvent…
5 0LG2MOY0TDA9 Hypaepa OSPGY6Y0VUL6 "" http://www… newAdvent…
6 0LG2MOY0TDA9 Hypaepa OSPGY6Y0VUL6 "" http://www… newAdvent…
7 0LG2MOY0TDA9 Panemotichus JWJUHI80CZOH "" https://ww… other
8 0LG2MOY0TDA9 Hypaepa OSPGY6Y0VUL6 "" https://ww… other
9 0LG2MOY0TDA9 Panemotichus JWJUHI80CZOH "" https://pl… pleiades
10 0LG2MOY0TDA9 Hypaepa OSPGY6Y0VUL6 "" https://pl… pleiades
11 0LG2MOY0TDA9 Panemotichus JWJUHI80CZOH "" https://ww… talbert
12 0LG2MOY0TDA9 Hypaepa OSPGY6Y0VUL6 "" https://ww… talbert
13 0LG2MOY0TDA9 Panemotichus JWJUHI80CZOH "" https://en… wikipedia
14 0LG2MOY0TDA9 Hypaepa OSPGY6Y0VUL6 "" https://en… wikipedia
First and foremost, I get a warning that my data sets are matching multiple values on both sides, and since I did specify a left_join(), I probably wasn’t expecting or wanting that. Then, pretty clearly, I can see how all of the records are mixed together in the resulting source information. This sucks. I spent a lot of time hunting down sources for these things, too.

This blog entry obviously does not show it, but I spent a couple days thinking about what to do here. Eventually, I decided that I would flag these problem records and move on. These are places, and the important parts have remained untouched via the locations. I would have been much more upset had the event, writing, or person-based records had been compromised. I have a couple ideas of how I can eventually correct this, but for right now I am spinning my wheels and not getting very far. I may have to to do some hand-correcting, but I’m not that desperate (yet!)
Let’s take a small, quick detour to flag the bad records and do some data saving.
First, I flag the duplicate mainID records using dplyr’s if_else(). If the UUID is in the dupe_main_ids vector, it is “1”, if it’s not, it is “0”. Normally, I don’t like overwriting original sets, but in this case I want this information to be part of my main_base set from here on out. I also want to remove flagForDeletion because it is an artifact that I don’t need anymore.
main_base <- main_base |>
mutate(dupe_main_id = if_else(mainID %in% dupe_main_ids, 1, 0)) |>
select(-flagForDeletion)
# Check that the flag is properly set by checking a few of the duplicate
# mainID values from dupe_main_ids.
rando_dupe_main_ids <- sample(dupe_main_ids, 3)
# The following should always return "1" in the dupe_main_id column.
main_base |>
filter(mainID %in% rando_dupe_main_ids) |>
arrange(mainID) mainID entity locationID dupe_main_id
1 9VG4LNZFNJ9M Helenopolis V8CDZF6FVO97 1
2 9VG4LNZFNJ9M Constantia antea Tomi XYTHFET6R3Q9 1
3 D1WJ33D1ZW1X Sidyma QG0KIJYBQK5A 1
4 D1WJ33D1ZW1X Chiusi 9WWQOPZPHB57 1
5 U3IASRYNNEWO Siunia MKACZX9NATJ7 1
6 U3IASRYNNEWO Messina UMVHUPW0OP4I 1
And since I have created a new main_base data set with a new column, I am going to save that as an .rds file. Going forward I can load it into my environment with my changes in tact. I am going to save it to a new project folder called “data_objects”.
saveRDS(main_base, file.path(
here("data_objects"),
"main_base.rds"
))I bet there’s all kinds of weird stuff happening that should be looked at. So, for now I need to forget my bad UUID woes and move on with my life.