OPIS PROJEKTU
Projekt przedstawia drogę od surowych danych pobranych z katalogu szkół do działającej warstwy danych przygotowanej pod interaktywną mapę w WordPressie.
Skupiam się zarówno na rezultacie, jak i na sposobie myślenia: analizie jakości danych, diagnostyce problemów, świadomym doborze narzędzi oraz iteracyjnym dochodzeniu do rozwiązania.
01 / PROCES
Cel projektu
Celem projektu było stworzenie w WordPressie interaktywnej mapy szkół na podstawie danych pobranych z katalogu internetowego. Mapa miała umożliwiać przeglądanie placówek na terenie Polski, przybliżanie mapy, klikanie w punkty i odczytywanie informacji o szkole. Od początku projektowałem rozwiązanie tak, aby można było później dodać filtrowanie po województwie, klastry markerów, wyszukiwarkę miasta i listę placówek.
Projekt potraktowałem jako pełny przepływ danych: źródło → analiza jakości → czyszczenie → geokodowanie → walidacja → GeoJSON → WordPress → Leaflet → interakcja użytkownika. Był to praktyczny projekt łączący data processing, web development i myślenie produktowe.
02 / PROCES
Dane wejściowe i pierwsza diagnoza
Punktem wyjścia był zbiór 146 placówek z polami: name, wojewodztwo, miasto, adres, strona, lat i lng. Pierwsza analiza wykazała, że tylko 34 rekordy miały współrzędne geograficzne, a 112 ich nie posiadało.
Kluczowy wniosek: problemem nie była sama mapa. Bez poprawnych współrzędnych nie da się wiarygodnie narysować punktów. Najpierw należało przygotować dane. W JupyterLab sprawdziłem ich strukturę i liczbę rekordów z geolokalizacją.
python
df.info()
df["lat"].notna().sum()- Rekordów ogółem
- 146
- Z lat/lng
- 34
- Bez współrzędnych
- 112
03 / PROCES
Dlaczego automatycznie udało się zlokalizować tak mało szkół
Niska liczba współrzędnych nie wynikała wyłącznie z ograniczeń geokodera. Głównym problemem była jakość danych pobranych ze strony źródłowej. Adresy miały różne formaty: nadmiarowe spacje, skróty „ul.”, „al.” i „pl.”, numery lokali, oznaczenia budynków, powtórzone nazwy miejscowości, literówki oraz niepełne nazwy ulic.
Dane wejściowe
ul. Powstańców 7A lok. U2, 05-800 Pruszków
al. Aleja Komisji Edukacji Narodowej 95 lok. U29
ul. Barskich 33, lok 2
ul.Szkolna 14, 38-481 Rymanów
ul. Szkona 4Takie dane są czytelne dla człowieka, ale geokoder może mieć problem z ich jednoznacznym dopasowaniem. Zamiast uznać niski wynik za ograniczenie narzędzia, przeanalizowałem wzorce w błędnych rekordach i zaprojektowałem etap normalizacji.
04 / PROCES
Wybór narzędzi
Do obróbki danych wykorzystałem Python, pandas i JupyterLab. JupyterLab pozwolił mi pracować iteracyjnie: wykonywać kolejne etapy w osobnych komórkach, sprawdzać wyniki, poprawiać dane i ponawiać tylko potrzebne fragmenty procesu.
Do geokodowania użyłem biblioteki geopy oraz usługi Nominatim opartej o OpenStreetMap. Do wizualizacji na stronie wybrałem Leaflet i podkład OpenStreetMap. GeoJSON posłużył jako format wymiany danych pomiędzy ich przygotowaniem a frontendem WordPressa.
Rozwiązanie miało być lekkie, łatwe do rozwijania, niezależne od płatnego kreatora map i kompatybilne z WordPressem.
05 / PROCES
Czyszczenie i normalizacja adresów
Zachowałem pełny adres do wyświetlenia użytkownikowi, a równolegle utworzyłem uproszczoną wersję przeznaczoną tylko do geokodowania.
Przed / po
adres: ul. Powstańców 7A lok. U2, 05-800 Pruszków
adres_geocode: Powstańców 7A, 05-800 Pruszkówpython
import re
import pandas as pd
def clean_for_geocode(addr):
if pd.isna(addr):
return ""
addr = re.sub(r"lok\.?\s*[\w/-]+", "", addr, flags=re.IGNORECASE)
addr = re.sub(r"bud\.?\s*[\w/-]+", "", addr, flags=re.IGNORECASE)
addr = re.sub(r"\b(ul|al|pl)\.?\s*", "", addr, flags=re.IGNORECASE)
addr = re.sub(r"\s+", " ", addr)
return addr.strip(" ,")
df["adres_geocode"] = df["adres"].apply(clean_for_geocode)To rozdzielenie było ważne: dane zoptymalizowane dla algorytmu nie muszą być identyczne z danymi prezentowanymi użytkownikowi.
06 / PROCES
Geokodowanie brakujących adresów
Dla rekordów bez lat/lng utworzyłem zapytania do geokodera na podstawie oczyszczonego adresu, miasta i kraju.
python
from geopy.geocoders import Nominatim
import time
geolocator = Nominatim(user_agent="szkoly_map")
for i, row in df.iterrows():
if pd.isna(row["lat"]) or pd.isna(row["lng"]):
query = f"{row['adres_geocode']}, {row['miasto']}, Polska"
try:
location = geolocator.geocode(query)
if location:
df.at[i, "lat"] = location.latitude
df.at[i, "lng"] = location.longitude
except Exception:
pass
time.sleep(1)Opóźnienie time.sleep(1) zostało dodane celowo, aby ograniczyć częstotliwość zapytań do publicznej usługi. Po normalizacji i ponownym geokodowaniu liczba poprawnie zlokalizowanych placówek wzrosła z 34 do 124.
Ten fragment dokumentuje przebieg projektu. W rozwijanym procesie obsługę wyjątków warto uzupełnić o zapis przyczyny błędu zamiast cichego pass; sam odstęp między zapytaniami nie potwierdza spełnienia wszystkich aktualnych warunków usługi.
07 / PROCES
Ręczna obsługa wyjątków
Po automatycznym etapie pozostały 22 rekordy bez współrzędnych. Zamiast komplikować system dla niewielkiej grupy wyjątków, wyświetliłem je osobno i zacząłem weryfikować ręcznie.
python
df[df["lat"].isna()][["name", "miasto", "adres"]]Przykładowe ręczne korekty:
python
df.loc[34, "adres"] = "ul. Szkolna 4"
df.loc[25, "adres"] = "ul. Energetyków 1, Lublin"
df.loc[9, "adres"] = "ul. Barskich 33, Wrocław"Automatyzuję większość procesu, ale przy nietypowych wyjątkach wybieram prostszy i bardziej wiarygodny sposób zamiast budować nadmiernie złożoną logikę.
08 / PROCES
Walidacja jakości danych
Po kolejnych etapach sprawdzałem liczbę rekordów ze współrzędnymi i analizowałem te, które nadal pozostawały bez geolokalizacji.
python
df[["lat", "lng"]].notna().all(axis=1).sum()
df[df[["lat", "lng"]].isna().any(axis=1)][
["name", "miasto", "adres"]
]
df[["lat", "lng"]].describe()Dodatkowo kontrolowałem, czy uzyskane współrzędne mieszczą się w realistycznym zakresie dla Polski. Sam fakt, że API zwróciło współrzędne, nie był dowodem poprawności. Wyniki wymagały weryfikacji.
09 / PROCES
Pierwszy błąd GeoJSON i jego diagnoza
Pierwszy wygenerowany GeoJSON nie ładował się w przeglądarce. W konsoli pojawił się błąd parsowania JSON. Analiza pliku wykazała wartości:
Niepoprawny fragment JSON
"coordinates": [
NaN,
NaN
]NaN jest wartością spotykaną w pandas/NumPy, ale nie jest poprawną wartością standardu JSON. Jeden taki rekord uniemożliwiał przeglądarce sparsowanie całego pliku. Rozwiązaniem było odrzucenie rekordów bez współrzędnych przed eksportem:
python
df_clean = df.dropna(subset=["lat", "lng"])Nie zmieniałem losowo kodu mapy. Najpierw odseparowałem warstwę danych od warstwy wizualizacji i znalazłem źródło problemu w niepoprawnym eksporcie.
10 / PROCES
Generowanie poprawnego GeoJSON
Po oczyszczeniu danych wygenerowałem kolekcję GeoJSON. Kolejność współrzędnych to [lng, lat], a nie [lat, lng].
python
import json
df_clean = df.dropna(subset=["lat", "lng"])
features = []
for _, row in df_clean.iterrows():
features.append({
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
float(row["lng"]),
float(row["lat"])
]
},
"properties": {
"name": row["name"],
"miasto": row["miasto"],
"wojewodztwo": row["wojewodztwo"],
"adres": row["adres"],
"strona": row["strona"]
}
})
geojson = {
"type": "FeatureCollection",
"features": features
}
with open("szkoly_map.geojson", "w", encoding="utf-8") as f:
json.dump(geojson, f, ensure_ascii=False, indent=2)Jawna konwersja do float zapobiegała problemom z typami NumPy podczas serializacji. ensure_ascii=False zachowywało polskie znaki.
Dalsze zabezpieczenie eksportu to allow_nan=False oraz kontrola wszystkich właściwości, nie tylko współrzędnych. Wówczas niedozwolona wartość zatrzyma eksport zamiast trafić do przeglądarki.
11 / PROCES
Integracja z WordPressem
Plik GeoJSON umieściłem na serwerze jako niezależną warstwę danych:
Ścieżka danych
/wp-content/maps/szkoly_map.geojsonDzięki temu można go podmieniać i w przyszłości automatycznie generować bez uzależnienia od biblioteki mediów WordPressa. Mapę zbudowałem na Leaflet.
HTML / JavaScript
<div id="map" style="height:600px;"></div>
<link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css">
<script src="https://unpkg.com/leaflet/dist/leaflet.js"></script>
<script>
const map = L.map("map").setView([52.1, 19.3], 6);
L.tileLayer(
"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
{ attribution: "© OpenStreetMap" }
).addTo(map);
</script>Fragment pokazuje mechanikę osadzenia. Przy utrzymaniu wdrożenia zasoby należy ładować mechanizmami enqueue WordPressa, z ustaloną wersją biblioteki. Kod w tym case study jest wyświetlany jako tekst — nie uruchamia mapy ani zewnętrznych usług na tej stronie.
12 / PROCES
Dynamiczne wczytywanie danych i popupy
GeoJSON jest ładowany dynamicznie przez fetch() i przekazywany do Leaflet. Poniższy fragment pokazuje pierwotną logikę popupu:
JavaScript
fetch("/wp-content/maps/szkoly_map.geojson")
.then(response => response.json())
.then(data => {
L.geoJSON(data, {
onEachFeature(feature, layer) {
const school = feature.properties;
layer.bindPopup(`
<strong>${school.name}</strong><br>
${school.miasto}<br>
${school.adres}<br>
<a href="${school.strona}" target="_blank">
Przejdź na stronę szkoły
</a>
`);
}
}).addTo(map);
});W popupie zachowuję pełny adres, nawet jeśli do geokodowania używana była jego uproszczona wersja.
Przy rozwijaniu tej implementacji dane źródłowe wymagają walidacji. Popup należy budować bez wstrzykiwania niezaufanego HTML, np. przez elementy DOM i textContent, a adres strony dopuścić tylko dla protokołów HTTP/HTTPS. Wczytywanie danych wymaga też obsługi błędnego statusu HTTP, JSON i niedostępności pliku.
13 / PROCES
Projekt filtrowania po województwie
Każdy rekord zawiera pole wojewodztwo, dlatego dane są przygotowane do filtrowania bez zmiany źródłowego zbioru. Przykładowa logika:
JavaScript
const filteredFeatures = schoolsData.features.filter(
feature =>
feature.properties.wojewodztwo === selectedRegion
);
const filteredCollection = {
type: "FeatureCollection",
features: filteredFeatures
};To kierunek rozwoju, nie deklaracja wdrożonego filtra. Docelowo użytkownik może wybrać województwo, a mapa pokaże odpowiadające mu placówki. Rozwiązanie można rozszerzyć o liczniki szkół w regionie, klikane granice województw, klastry markerów, wyszukiwarkę miasta i synchronizowaną listę wyników.
14 / PROCES
Debugging i sposób rozwiązywania problemów
Do diagnostyki wykorzystałem konsolę przeglądarki i zakładkę Network. Sprawdzałem, czy plik GeoJSON jest pobierany, czy zwraca poprawny status HTTP oraz czy parser JSON może go odczytać.
Do oddzielenia problemu mapy od problemu danych można użyć prostego markera testowego:
JavaScript
L.marker([52.2297, 21.0122]).addTo(map);Jeśli marker pojawia się poprawnie, podstawowa warstwa Leaflet działa. Kolejny krok to sprawdzenie danych, ich ładowania i konfiguracji warstwy GeoJSON. Takie podejście ogranicza przypadkowe zmiany i pozwala debugować system warstwa po warstwie.
15 / PROCES
Rezultat
- Analiza zbioru 146 placówek i identyfikacja 112 brakujących geolokalizacji.
- Rozpoznanie problemu jakości i niespójności adresów.
- Przygotowanie procesu normalizacji danych w Pythonie.
- Zwiększenie liczby poprawnie zlokalizowanych szkół z 34 do 124.
- Zdiagnozowanie wartości NaN blokujących parser JSON i wygenerowanie poprawnego GeoJSON.
- Przygotowanie integracji WordPress + Leaflet + OpenStreetMap oraz popupów.
- Zaprojektowanie filtrowania po województwie i architektury do dalszej automatyzacji.
124 zlokalizowane placówki to wynik po normalizacji i ponownym geokodowaniu. Opis ręcznej weryfikacji pozostałych 22 rekordów nie oznacza, że wszystkie zostały ostatecznie zlokalizowane.
16 / PROCES
Co ten projekt pokazuje w moim portfolio
Potrafię przeprowadzić problem przez kolejne warstwy: od jakości danych źródłowych, przez ich przetwarzanie i walidację, po interfejs użytkownika. Wybieram narzędzia do zadania, sprawdzam hipotezy i rozdzielam problemy danych od problemów wizualizacji. Automatyzację uzupełniam ręczną weryfikacją tam, gdzie jest to prostsze i bardziej wiarygodne.
W projekcie wykorzystałem Python, pandas, JupyterLab, geopy, Nominatim, OpenStreetMap, GeoJSON, JavaScript, Leaflet, WordPress i DevTools oraz podstawy pracy z danymi przestrzennymi przy wsparciu modeli AI.