Clustering Lighthouses in Wikidata

Physical type k-means, k=4

Signal family k-modes, k=7

This map is formed from plotting the geographic locations of 5,625 lighthouses that were identified in Wikidata and processed through both K-mean and K-modes clustering. The lighthouses flash in real-time according to the light characteristic that is present in Wikidata.

The K-means clustering was performed on the physical properties of the lighthouses, while the K-modes clustering was performed on the signal characteristics of the lighthouses. Buttons at the top can be utilized to filter the visible lighthouses by combinations of k-means and k-modes clusters. Click on a lighthouse to bring up detailed information about it including an explanation of its properties as reported by Wikidata and a visual of the flash pattern of the lighthouse's signal.

1. Research Questions

Research questions

The questions that I came up with based on the properties in WikiData are:

Short answers

K-means: Lighthouses in the dataset fall into a few broad types that that have a significant amount of overlap. The main factor that separates clusters is the height of the light above sea level, with towers built atop cliffs being a clearly distinct grouping. In the remaining lighthouses the main component that affects cluster membership seems to be the height of the height of the constructed tower itself. Taller, long-range towers separate from short, limited-range ones though a significant amount of overlap muddies these two groups. A final group emerged that appears more defined by age rather than physical form.

K-modes: Going into this project, I did not know that the International Association of Marine Aids to Navigation and Lighthouse Authorities (IALA) provides guidance on lighthouse signals based on navigational needs in the area. Signals are already a designed code, so the k-modes mostly recovered its most common "words" (steady lights, single, double and triple flashes, quick flicker) rather than any real hidden grammar. There are indications of geographically defined clusters where it seems that IALA guidance is ignored, but these signaling differences are mixed in with those associated with more unique navigational needs. In the end, I believe that though signal patters do constitute a language of sorts for lighthouses, there is enough variation and nuance provided by distinct navigational challenges that K-modes clustering is not the right tool for addressing this question.

What this produces

The final presentation of this project is index.html and the associated imagry. This artifact comprises the data collection, written reflection, and visualization of the research conducted in this exercise.

A deployed version of this project is available on my personal site

Included in this repo are the raw datasets obtained from Wikidata and all source code for the pipeline that ultimately produces the refined data as CSV files, the images for elbow plots, and ultimately index.html.

2. Wikidata Queries

Querying

Full notes and (almost) every query are in data/Dataset Ideas.md.

This section is semi-interactive, if you want to engage with it. Each query used in this project is documented below along with a summary and the ability to run the query live against Wikidata and see the results.

QID Q39715 is the primary class identifier for all lighthouses. Different types of lighthouses are recorded as subclasses of Q39715. This query returns a list of every subclass and the count of items which are instances of that subclass.

SPARQL
SELECT ?class ?classLabel (COUNT(?item) AS ?n) WHERE {
  ?class wdt:P279* wd:Q39715 .     # same family tree
  ?item  wdt:P31   ?class .        # items that are an instance of that class
  SERVICE wikibase:label { bd:serviceParam wikibase:language "en". }
}
GROUP BY ?class ?classLabel
ORDER BY DESC(?n)

Many subclasses were excluded from the project. For example, sector lights (Q787078) are excluded as they are a different type of navigation aid and will not have the properties needed for this analysis. This query asks Wikidata how many items are an instance of any subclass of Q39715 that are not members of unrelated or unusable classes.

SPARQL
SELECT (COUNT(DISTINCT ?item) AS ?n) WHERE {
  ?item wdt:P31/wdt:P279* wd:Q39715 .     # instance of lighthouse, or of any kind of lighthouse

  MINUS {                                  # remove anything that matches this block
    VALUES ?excluded {                     # your exclusion list
      wd:Q787078 wd:Q1930418 wd:Q108840273 wd:Q33129345
      wd:Q66350506 wd:Q33129347 wd:Q33185258 wd:Q25324528
    }
    ?item wdt:P31 ?excluded .              # ...if the item is filed under any excluded class
  }
}

This query asks which properties the usable lighthouses have, and how many lighthouses have each property. This query was primarily used for identifying what properties would be useful for the two types of analysis used.

This query can run long and get timed out by the Wikidata query endpoint. This page runs a smaller query to retrieve properties for only a few subclasses.

SPARQL: as run for this project
SELECT ?prop ?propLabel (COUNT(DISTINCT ?item) AS ?count) WHERE {
  ?item wdt:P31/wdt:P279* wd:Q39715 .
  MINUS {
    VALUES ?excluded {
      wd:Q787078 wd:Q1930418 wd:Q108840273 wd:Q33129345
      wd:Q66350506 wd:Q33129347 wd:Q33185258 wd:Q25324528
    }
    ?item wdt:P31 ?excluded .
  }
  ?item ?p ?value .                        # ANY statement: the verb itself is a blank
  ?prop wikibase:directClaim ?p .          # translate the verb back to its property page, to get a name
  SERVICE wikibase:label { bd:serviceParam wikibase:language "en". }
}
GROUP BY ?prop ?propLabel
ORDER BY DESC(?count)
SPARQL: smaller version the Test query button runs
# Smaller version for the live test: only three kinds of lighthouse (caisson, wooden and
# Integral, about 240 items) instead of all 8,603, so it finishes inside Wikidata's 60 s limit.
SELECT ?prop ?propLabel (COUNT(DISTINCT ?item) AS ?count) WHERE {
  VALUES ?class { wd:Q5017835 wd:Q66088335 wd:Q3278565 }   # caisson, wooden, Integral lighthouse
  ?item wdt:P31 ?class .
  MINUS {
    VALUES ?excluded {
      wd:Q787078 wd:Q1930418 wd:Q108840273 wd:Q33129345
      wd:Q66350506 wd:Q33129347 wd:Q33185258 wd:Q25324528
    }
    ?item wdt:P31 ?excluded .
  }
  ?item ?p ?value .                        # ANY statement: the verb itself is a blank
  ?prop wikibase:directClaim ?p .          # translate the verb back to its property page, to get a name
  SERVICE wikibase:label { bd:serviceParam wikibase:language "en". }
}
GROUP BY ?prop ?propLabel
ORDER BY DESC(?count)

The properties I chose for K-means clustering were focal height above sea level for the light, construction date (inception), visible range of the light, height of the tower. Geographic coordinates and name (itemLabel) were included for mapping.

This query was heavily optimized by both Claude and Gemini as it repeatedly timed out. I do not understand the optimizations, but the results are what I expected.

SPARQL
SELECT ?item ?itemLabel ?coords ?focalHeight ?inception ?range ?height
WHERE {
  {
    SELECT ?item
      (GROUP_CONCAT(DISTINCT STR(?coord); separator=" | ") AS ?coords)
      (GROUP_CONCAT(DISTINCT CONCAT(STR(?focalAmt), " ", STRAFTER(STR(?focalUnit), "entity/")); separator=" | ") AS ?focalHeight)
      (GROUP_CONCAT(DISTINCT STR(?inception); separator=" | ") AS ?inception)
      (GROUP_CONCAT(DISTINCT CONCAT(STR(?rangeAmt), " ", STRAFTER(STR(?rangeUnit), "entity/")); separator=" | ") AS ?range)
      (GROUP_CONCAT(DISTINCT CONCAT(STR(?heightAmt), " ", STRAFTER(STR(?heightUnit), "entity/")); separator=" | ") AS ?height)
    WHERE {
      # 1. Restrict the base set as quickly as possible
      ?item wdt:P31/wdt:P279* wd:Q39715 .
      ?item wdt:P625 ?coord . # Bind coordinates immediately to drop non-geographic items
      
      # 2. Exclude your explicit classes
      MINUS {
        VALUES ?excluded {
          wd:Q787078 wd:Q1930418 wd:Q108840273 wd:Q33129345
          wd:Q66350506 wd:Q33129347 wd:Q33185258 wd:Q25324528
        }
        ?item wdt:P31 ?excluded .
      }

      # 3. Pull optional properties for the matched subset
      OPTIONAL { 
        ?item p:P2923/psv:P2923 ?f .
        ?f wikibase:quantityAmount ?focalAmt ; wikibase:quantityUnit ?focalUnit . 
      }
      OPTIONAL { ?item wdt:P571 ?inception . }
      OPTIONAL { 
        ?item p:P2929/psv:P2929 ?r .
        ?r wikibase:quantityAmount ?rangeAmt ; wikibase:quantityUnit ?rangeUnit . 
      }
      OPTIONAL { 
        ?item p:P2048/psv:P2048 ?h .
        ?h wikibase:quantityAmount ?heightAmt ; wikibase:quantityUnit ?heightUnit . 
      }
    }
    GROUP BY ?item
  }
  
  # 4. Apply labels at the very end to the reduced dataset
  SERVICE wikibase:label { bd:serviceParam wikibase:language "en". }
}

The categorical properties for lighthouses are very sparsely populated, and I wound up discarding most of the properties queried. Please see the "What didn't" section below for details.

This query was also heavily optimized by both Claude and Gemini as it repeatedly timed out. I do not understand the optimizations, but the results are what I expected.

SPARQL
SELECT ?item ?itemLabel
  (GROUP_CONCAT(DISTINCT STRAFTER(STR(?her), "entity/"); separator=" | ") AS ?heritage)
  (GROUP_CONCAT(DISTINCT STRAFTER(STR(?nrg), "entity/"); separator=" | ") AS ?energy)
  (GROUP_CONCAT(DISTINCT STRAFTER(STR(?mat), "entity/"); separator=" | ") AS ?material)
  (GROUP_CONCAT(DISTINCT STRAFTER(STR(?col), "entity/"); separator=" | ") AS ?color)
  (GROUP_CONCAT(DISTINCT STRAFTER(STR(?use), "entity/"); separator=" | ") AS ?hasUse)
  (GROUP_CONCAT(DISTINCT STRAFTER(STR(?lens), "entity/"); separator=" | ") AS ?lensType)
WITH {
  SELECT DISTINCT ?item WHERE {
    ?item wdt:P31/wdt:P279* wd:Q39715 .
    ?item wdt:P625 [] .
    MINUS {
      VALUES ?excluded {
        wd:Q787078 wd:Q1930418 wd:Q108840273 wd:Q33129345
        wd:Q66350506 wd:Q33129347 wd:Q33185258 wd:Q25324528
      }
      ?item wdt:P31 ?excluded .
    }
  }
} AS %lighthouses
WHERE {
  INCLUDE %lighthouses
  OPTIONAL { ?item wdt:P1435 ?her . }
  OPTIONAL { ?item wdt:P618  ?nrg . }
  OPTIONAL { ?item wdt:P186  ?mat . }
  OPTIONAL { ?item wdt:P462  ?col . }
  OPTIONAL { ?item wdt:P366  ?use . }
  OPTIONAL { ?item wdt:P9597 ?lens . }
  SERVICE wikibase:label { bd:serviceParam wikibase:language "en". }
}
GROUP BY ?item ?itemLabel

Assessing Wikidata

What worked

Wikidata queries provide a robust tool to retrieve a set of properties for thousands of instances of Q39715 and its subclasses. Standardization of units was easy due to cross referncing QID's that defined which unit values were stored in.

What didn't

The clustering algorithms for K-means and K-modes require all included records to have valid values for all fields used to fit the models. No N/A, no null values. This proved a major challenge for the K-modes analysis that I originally intended. I had chosen cultural heritage status, energy source, construction material, and color properties for K-modes. Of 8000+ lighthouses, only 58 have values for every field.

I repeatedly ran into timeout issues with many of the queries, which ultimately was solved by AI tooling which optimized the queries. Without access to those tools, I would almost certainly have changed project ideas.

Content in Wikidata is largely community-entered and community-maintained, which opens the platform up to a lot of biases that are inherent in volunteer created and maintained content.

What data went into each analysis

3. How to run the code

NOTE: venv + pip install -r requirements.txt (repo root; Python 3.12 tested). The Wikidata extracts ARE in the repo, so no network access is needed.

One command will run the full pipeline: python lighthouses.py

The individual steps are documented below:

step script output
1 scripts/prep_kmeans.py data/k-means-lighthouses.csv
2 scripts/kmeans_lighthouses.py images/kmeans_elbow.png, data/output/kmeans_clusters.csv
3 scripts/prep_kmodes.py data/k-modes-features.csv
4 scripts/kmodes_lighthouses.py images/kmodes_elbow.png, data/output/kmodes_clusters.csv
5 scripts/map_clusters.py 4 maps in images/ (needs steps 2 and 4)
6 scripts/scatter_clusters.py 2 scatters in images/ (needs steps 2 and 4)

4. K-means: physical form/age

Choosing k

The k-means data was fit at a range of k values and an elbow plot was generated.

k-means elbow plot for a range of k values

I struggled to find a compelling choice of k. Values 3, 4, and 5 were still quite steep with a long tail at higher k. I settled on k=4 for this analysis after inspecting the groupings at k=5. I felt like at k=4 the groupings were quite logical:

K=5 groups seemed to be less logical, and k=3 struggled to separate towers into useful clusters.

The four types

Values are the median for each cluster.

Cluster Lighthouses Focal height Range Tower height Year Interpretation
0 1,162 20 m 11 nmi 12 m 1905 small, short-range lights
1 252 111 m 21 nmi 14 m 1902 short tower, very high lamp (likely cliffs)
2 290 52 m 23 nmi 41 m 1902 tall towers close to water level
3 720 37 m 18 nmi 20 m 1866 mid-size, older

scatter plot of k-means analysis at k=4

There is clear separation in this scatter plot between cluster 0 (small, short-range lighthouses) and cluster 1 (towers on cliffs), with very little overlap. This indicates that the fit model has good resolving power for which of these two groups a record belongs to.

In contrast, on the 2D scatter plot as generated, there appears to be a lot of overlap between cluster 2 (tall towers at water level) and cluster 3 (old towers of middle height). These records have similar heights and visible range of the light, but a significant 36 year difference in year of construction. If a 3D plot were made with the year as the additional axis, I suspect there would be obvious separation between the two clusters that is not visible with only two dimensions.

5. K-modes: light signals

Preparing the data

Claude was used to decode the maritime chart navigational light flash codes into the distinct data types used for analysis.

Some examples of light characteristic codes and how they ended up in the processed data:

Code What it means type group color period
F R fixed (steady) red light F none R none
Fl(2) W 10s white flashes in groups of 2, repeating every 10 seconds Fl 2 W 6-10s
Iso WRG 4s equal on and off, white, red or green depending on direction, every 4 seconds Iso none WRG <=4s
Fl(2+1) W 15s white group of 2 flashes then 1 flash, every 15 seconds Fl compound W 10-15s

Choosing k

Like with k-means, an elbow plot was generated for a range of k values.

elbow chart for k-modes analysis at multiple k values

This chart has a more evident elbow at k=7.

Seven signal families

scatter plot of k-modes analysis clusters

Each dot in this chart is a lighthouse, placed by its signal type and period and colored by which of the seven clusters k-modes put it in. Because these are categories, the dots are spread out randomly into blocks so you can see how many lighthouses share a signal, and the red X marks each family's most typical signal. At k=7 most blocks are a single color, so each family lines up with a recognizable signal.

In transparency, Claude suggested this chart. No example chart was included in the course demo, and I was struggling to tell if the groups made sense. This chart demonstrates that light signal patterns largely group together in k=7, with most of the overlap occurring in longer-duration flash cycles.

6. Bringing the two together

2,069 lighthouses made it into both analyses, so for those I can compare the physical type from k-means with the signal family from k-modes.

Physical type Lighthouses Most common signal family Fast flashes (4s or less) White light
Small, short-range 1,023 medium single flashes (30%) 23% 62%
Cliffs 186 slow single flashes (46%) 2% 95%
Tall towers 254 slow single flashes (33%) 6% 90%
Mid-size, older 606 slow single flashes (37%) 6% 81%

The two analyses line up more than I expected. The small, short-range lighthouses are where most of the variety is: they have almost all of the fast flashes (240 of 296), the steady lights and the quick flicker, and only 62% of them show a white light. The big lights are much more uniform. Lighthouses on cliffs are 95% white and almost half of them use a slow single flash, and the tall towers look similar. My guess is that this comes down to what each light is for: small lights mark harbors and channels where color and quick signals help, while the big lights need to be seen far out at sea.

Light color by physical type

This map shows the light colors for each physical type. Its percentages are a bit different from the table because it also counts lighthouses that have no usable light characteristic.

7. Reflection

Decisions that shaped the results

A few choices I made along the way had a big effect on what came out:

Limits of the tools

K-means is sensitive to extreme values. At k=5 it spends a whole cluster on just 15 very old lighthouses (median year 1543), which is part of why I didn't use k=5. The elbow plot also never had a sharp bend, so choosing the k value for the k-means analysis was a judgment call.

K-modes treats every mismatch the same. To the algorithm, a light that flashes every 5 seconds is just as "different" from one that flashes every 7 seconds as a flashing light is from a steady one. K-modes can also be thrown off by how the algorithm is initialized.

In this exercise I have learned that clustering is a powerful tool, but ultimately a very subjective one. Determining the "best" number of clusters to use for both analyses essentially boiled down to inspecting the values for each cluster and determining if the result felt good and seemed logical. What feels good and seems logical to me may be viewed very differently by another person.

Bias in the data

One look at the mapped locations of lighthouses in this dataset reveals several forms of bias that are built into the data itself. Extremely few inland lighthouses on rivers or lakes are present. I quite like the one Amazonian lighthouse in Brazil.

There is clearly a geographic bias present at the largest scale of this data. The global south is under-represented compared to the dominant European and North American data, despite the paths around southern Africa and South America being both heavily utilized and very dangerous.

I have no reason to suspect that there are genuinely fewer lighthouses in these areas. Instead, I suspect that Wikidata itself is biased toward the northern hemisphere and first-world countries.

Where the results are weak or ambiguous

8. AI disclosure

Claude and Gemini (via Google Search) were used in this project, particularly in the following areas:

The bulk of the concept of this project along with the analysis, choice of k values, evaluation, and interpretation of the results are human-powered.