|
| 1 | +# Geospatial Workflow |
| 2 | + |
| 3 | +This workflow demonstrates: |
| 4 | + |
| 5 | +1. Data acquisition with IPUMS.jl |
| 6 | +2. Preprocessing and educational category labeling |
| 7 | +3. Integration of aggregated census metrics with geospatial geometries |
| 8 | +4. Choropleth visualization with colorbar and layout tuning |
| 9 | + |
| 10 | +The scripts in this page mirror the runnable workflow under `src/workflows/geospatial_census/src/`. The goal is to keep the code easy to read and easy to run: load data, summarize it by region, join to geometry, and plot maps. |
| 11 | + |
| 12 | +## Requirement Coverage |
| 13 | + |
| 14 | +This workflow explicitly covers all required tutorial goals: |
| 15 | + |
| 16 | +- Data acquisition: load census microdata and metadata through IPUMS.jl (`parse_ddi`, `load_ipums_extract`) and load geospatial boundaries (`load_ipums_nhgis`). |
| 17 | +- Preprocessing: filter and clean geometry rows, convert join keys, map coded education values to readable labels, and aggregate counts. |
| 18 | +- Normalization: compute min-max normalized counts across regions before choropleth coloring. |
| 19 | +- Integration: merge aggregated census summaries with geospatial geometries using key-based joins. |
| 20 | +- Visualization: produce choropleth maps with multiple color schemes, titles, layout tuning, and a final colorbar for interpretability. |
| 21 | +- Reproducibility: provide runnable Julia code, fixed script order, and config-driven local file paths. |
| 22 | + |
| 23 | +## 1. Load Required Packages |
| 24 | + |
| 25 | +```julia |
| 26 | +using CairoMakie, Chain, CSV, DataFrames, GeoDataFrames, GeoInterfaceMakie, GeoMakie, StatsBase |
| 27 | +import IPUMS: load_ipums_extract, load_ipums_nhgis, parse_ddi |
| 28 | +``` |
| 29 | + |
| 30 | +## 2. Load IPUMS Census Data |
| 31 | + |
| 32 | +This step reads the metadata dictionary (`.xml`) and the fixed-width microdata extract (`.dat`). |
| 33 | +Both paths are configured in `config.toml` when you run the script pipeline. |
| 34 | + |
| 35 | +```julia |
| 36 | +ddi = parse_ddi("poland_data/ipumsi_00001.xml") |
| 37 | +df = load_ipums_extract(ddi, "poland_data/ipumsi_00001.dat") |
| 38 | +``` |
| 39 | + |
| 40 | +## 3. Metadata Exploration |
| 41 | + |
| 42 | +This section documents data acquisition quality checks. You inspect dataset-level metadata first, then column-level descriptions to verify |
| 43 | +definitions before deriving indicators. |
| 44 | + |
| 45 | +```julia |
| 46 | +md_df = metadata(df) |
| 47 | +for md in keys(md_df) |
| 48 | + println("$(md):\n----------------\n\n $(md_df[md])\n\n") |
| 49 | +end |
| 50 | +``` |
| 51 | + |
| 52 | +Then inspect variable descriptions: |
| 53 | + |
| 54 | +```julia |
| 55 | +for colname in names(df) |
| 56 | + println("$(colname):\n----------------\n") |
| 57 | + try |
| 58 | + println(" $(colmetadata(df, colname, "description"))\n") |
| 59 | + catch |
| 60 | + println(" description metadata not available\n") |
| 61 | + end |
| 62 | +end |
| 63 | +``` |
| 64 | + |
| 65 | +## 4. Load and Filter Shapefile for Poland |
| 66 | + |
| 67 | +The boundary file is filtered to Poland, rows without geometry are removed, and the regional key |
| 68 | +is cast to `Int64` so it can be joined to aggregated census results. |
| 69 | + |
| 70 | +```julia |
| 71 | +geodf = load_ipums_nhgis("shapefiles/ENUTS2_2013.shp").geodataframe |
| 72 | +filter!(x -> x.CNTRY_NAME == "Poland", geodf) |
| 73 | +dropmissing!(geodf, :geometry) |
| 74 | +geodf[!, :ENUTS2] = parse.(Int64, geodf[!, :ENUTS2]) |
| 75 | +``` |
| 76 | + |
| 77 | +## 5. Aggregate Educational Attainment by Region |
| 78 | + |
| 79 | +This step maps raw education codes into readable categories (`PRIMARY`, `SECONDARY`, `UNIVERSITY`), |
| 80 | +then computes regional counts for each category. |
| 81 | + |
| 82 | +It also prepares data for comparative mapping by creating grouped regional counts that are later normalized. |
| 83 | + |
| 84 | +```julia |
| 85 | +using Chain |
| 86 | + |
| 87 | +edu_df = @chain df begin |
| 88 | + groupby(_[:, [:ENUTS2_2013, :EDUCPL]], [:ENUTS2_2013, :EDUCPL]) |
| 89 | + combine(nrow => :Count) |
| 90 | +end |
| 91 | + |
| 92 | +primary = [12, 20] |
| 93 | +secondary = [40, 41, 42, 43, 50] |
| 94 | +university = [70, 71, 72, 73] |
| 95 | + |
| 96 | +edu_df.EDUCPL = convert(Vector{Any}, edu_df.EDUCPL) |
| 97 | +replace!(x -> in(x, primary) ? "PRIMARY" : x, edu_df.EDUCPL) |
| 98 | +replace!(x -> in(x, secondary) ? "SECONDARY" : x, edu_df.EDUCPL) |
| 99 | +replace!(x -> in(x, university) ? "UNIVERSITY" : x, edu_df.EDUCPL) |
| 100 | + |
| 101 | +edu_counts = @chain edu_df begin |
| 102 | + filter!(row -> !isa(row.EDUCPL, Real), _) |
| 103 | + groupby([:ENUTS2_2013, :EDUCPL]) |
| 104 | + combine(:Count => sum => :Count) |
| 105 | + groupby([:EDUCPL]) |
| 106 | +end |
| 107 | +``` |
| 108 | + |
| 109 | +## 6. Visualize Educational Attainment Across Regions |
| 110 | + |
| 111 | +The workflow keeps both map styles: |
| 112 | + |
| 113 | +- rough exploration with multiple color schemes (`:Purples`, `:Greens`, `:Blues`, `:Reds`) |
| 114 | +- final cleaned map with `:Wistia` and a colorbar |
| 115 | + |
| 116 | +Inside the plotting loop, counts are normalized to the 0-1 range. This improves comparability between |
| 117 | +education categories and across regions, independent of absolute category sizes. |
| 118 | + |
| 119 | +```julia |
| 120 | +# Base region map |
| 121 | +fig_regions = Figure(size = (1200, 1400), fontsize = 20) |
| 122 | +ax_regions = CairoMakie.Axis(fig_regions[1, 1]) |
| 123 | +poly!(ax_regions, geodf.geometry, color = 1:16, colormap = :Reds, strokecolor = :black, strokewidth = 3) |
| 124 | +Label(fig_regions[:, :, Top()], "Voivodeships of Poland", fontsize = 50) |
| 125 | +hidedecorations!(ax_regions) |
| 126 | + |
| 127 | +# Multi-color comparison map |
| 128 | +fig_rough = Figure(size = (1200, 1400), fontsize = 20) |
| 129 | +axs_rough = [Axis(fig_rough[x, y]) for x in 1:2 for y in 1:2] |
| 130 | +colors = [:Purples, :Greens, :Blues, :Reds] |
| 131 | +poly!(axs_rough[1], geodf.geometry, color = :white, strokecolor = :black, strokewidth = 3) |
| 132 | +hidedecorations!(axs_rough[1]) |
| 133 | +axs_rough[1].title = "Voivodeships of Poland" |
| 134 | +Label(fig_rough[:, :, Top()], "Normalized Education Counts across Poland", fontsize = 50, padding = (0, 0, 30, 0)) |
| 135 | + |
| 136 | +for (idx, counts) in enumerate(edu_counts) |
| 137 | + geo_counts = outerjoin(counts, geodf; on = [:ENUTS2_2013 => :ENUTS2]) |
| 138 | + norm_counts = (geo_counts.Count .- minimum(geo_counts.Count)) / (maximum(geo_counts.Count) .- minimum(geo_counts.Count)) |
| 139 | + cmap = cgrad(colors[idx + 1], norm_counts) |
| 140 | + dropmissing!(geo_counts, :geometry) |
| 141 | + ax = axs_rough[idx + 1] |
| 142 | + poly!(ax, geo_counts.geometry, color = cmap[norm_counts], strokecolor = :black, strokewidth = 3) |
| 143 | + ax.title = "$(counts.EDUCPL |> first)" |
| 144 | + hidedecorations!(ax) |
| 145 | +end |
| 146 | + |
| 147 | +# Final cleaned visualization with a colorbar |
| 148 | +fig = Figure(size = (1200, 1400), fontsize = 20) |
| 149 | +axs = [Axis(fig[x, y]) for x in 1:2 for y in 1:2] |
| 150 | + |
| 151 | +poly!(axs[1], geodf.geometry, color = :white, strokecolor = :black, strokewidth = 2) |
| 152 | +axs[1].title = "Voivodeships of Poland" |
| 153 | +hidedecorations!(axs[1]) |
| 154 | +Label(fig[:, :, Top()], "Normalized Educational Attainment across Poland", fontsize = 50, padding = (0,0,30,0)) |
| 155 | + |
| 156 | +for (idx, counts) in enumerate(edu_counts) |
| 157 | + geo_counts = outerjoin(counts, geodf; on = [:ENUTS2_2013 => :ENUTS2]) |
| 158 | + norm_counts = (geo_counts.Count .- minimum(geo_counts.Count)) / (maximum(geo_counts.Count) .- minimum(geo_counts.Count)) |
| 159 | + |
| 160 | + cmap = cgrad(:Wistia, norm_counts) |
| 161 | + dropmissing!(geo_counts, :geometry) |
| 162 | + |
| 163 | + ax = axs[idx + 1] |
| 164 | + poly!(ax, geo_counts.geometry, color = cmap[norm_counts], strokecolor = :black, strokewidth = 2) |
| 165 | + ax.title = "$(counts.EDUCPL |> first)" |
| 166 | + hidedecorations!(ax) |
| 167 | +end |
| 168 | + |
| 169 | +Colorbar(fig[:, 3], limits = (0, 1), colormap = :Wistia) |
| 170 | + |
| 171 | +save(joinpath(OUTPUT_DIR, "voivodeships_poland.png"), fig_regions) |
| 172 | +save(joinpath(OUTPUT_DIR, "education_rough_multicolors.png"), fig_rough) |
| 173 | +save(joinpath(OUTPUT_DIR, OUTPUT_FIGURE), fig) |
| 174 | +``` |
| 175 | + |
| 176 | +Additional outputs produced by the scripted workflow: |
| 177 | + |
| 178 | +- `output/voivodeships_poland.png` |
| 179 | +- `output/education_rough_multicolors.png` |
| 180 | +- `output/education_choropleth_poland.png` |
| 181 | + |
| 182 | +## Reproducible Scripted Run |
| 183 | + |
| 184 | +From the workflow directory: |
| 185 | + |
| 186 | +```bash |
| 187 | +julia --project=. -e "using Pkg; Pkg.instantiate()" |
| 188 | +copy config.toml.example config.toml |
| 189 | +julia --project=. run.jl |
| 190 | +``` |
| 191 | + |
| 192 | +On macOS/Linux, replace `copy` with `cp`. |
| 193 | + |
| 194 | +No database is required for this workflow. Inputs are local files configured in `config.toml`: |
| 195 | + |
| 196 | +- IPUMS DDI metadata (`.xml`) |
| 197 | +- IPUMS extract data (`.dat`) |
| 198 | +- NHGIS shapefile (`.shp` and companion files) |
| 199 | + |
| 200 | +The scripted workflow is implemented in: |
| 201 | + |
| 202 | +- `src/workflows/geospatial_census/src/01_load_data.jl` |
| 203 | +- `src/workflows/geospatial_census/src/02_preprocess.jl` |
| 204 | +- `src/workflows/geospatial_census/src/03_visualize.jl` |
| 205 | + |
| 206 | +This keeps acquisition, preprocessing, integration, and visualization steps explicit and reproducible. |
0 commit comments