├── .gitignore ├── README.md ├── example.png ├── examples └── plot_plz_areas.py ├── plz_plot ├── __init__.py ├── core.py ├── io.py └── resources │ ├── plz-5stellig.dbf │ ├── plz-5stellig.prj │ ├── plz-5stellig.shp │ └── plz-5stellig.shx ├── pyproject.toml ├── setup.cfg └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *.cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # Jupyter Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # SageMath parsed files 79 | *.sage.py 80 | 81 | # Environments 82 | .env 83 | .venv 84 | env/ 85 | venv/ 86 | ENV/ 87 | 88 | # Spyder project settings 89 | .spyderproject 90 | .spyproject 91 | 92 | # Rope project settings 93 | .ropeproject 94 | 95 | # mkdocs documentation 96 | /site 97 | 98 | # mypy 99 | .mypy_cache/ 100 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # plz_plot 2 | 3 | Plot data on Postal Code Areas of Germany 4 | 5 | This is slow but cool. 6 | 7 | ![img](example.png) 8 | 9 | 10 | Thanks to https://www.suche-postleitzahl.org/downloads for 11 | providing the plz area data! 12 | -------------------------------------------------------------------------------- /example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxnoe/plz_plot/e6877bf3d8f78695f746e91ba9e7a60d8ff7ce90/example.png -------------------------------------------------------------------------------- /examples/plot_plz_areas.py: -------------------------------------------------------------------------------- 1 | from plz_plot.core import plot_plz_data 2 | from plz_plot.io import get_plz_dataframe 3 | import matplotlib.pyplot as plt 4 | from matplotlib.colors import LogNorm 5 | from cartopy import feature 6 | 7 | from cartopy import crs 8 | 9 | df = get_plz_dataframe() 10 | 11 | fig = plt.figure(figsize=(8, 8)) 12 | ax = fig.add_subplot(1, 1, 1, projection=crs.Mercator()) 13 | 14 | ax.set_extent([4, 16, 47, 56], crs.PlateCarree()) 15 | 16 | land = feature.NaturalEarthFeature( 17 | 'physical', 'land', '10m', 18 | facecolor=feature.COLORS['land'], 19 | ) 20 | lakes = feature.NaturalEarthFeature( 21 | 'physical', 'lakes', '10m', 22 | facecolor=feature.COLORS['water'], 23 | ) 24 | lakes_europe = feature.NaturalEarthFeature( 25 | 'physical', 'lakes_europe', '10m', 26 | facecolor=feature.COLORS['water'], 27 | ) 28 | countries = feature.NaturalEarthFeature( 29 | 'cultural', 'admin_0_countries', '10m', 30 | facecolor='none', 31 | edgecolor='k', 32 | ) 33 | 34 | ax.add_feature(land) 35 | ax.add_feature(countries) 36 | 37 | 38 | df['inhabitants_per_area'] = df['inhabitants'] / df['area'] 39 | df.loc[df['inhabitants'] == 0, 'inhabitants_per_area'] = 1e-3 40 | 41 | plot = plot_plz_data( 42 | df['inhabitants_per_area'], 43 | ax=ax, 44 | norm=LogNorm(), 45 | vmin=1e1, 46 | vmax=2.5e4, 47 | ) 48 | plot.set_edgecolor('lightgray') 49 | plot.set_linewidth(0.2) 50 | 51 | ax.add_feature(lakes) 52 | ax.add_feature(lakes_europe) 53 | 54 | ax.set_title('Inhabitants per Square Kilometer') 55 | 56 | fig.colorbar(plot, ax=ax) 57 | fig.tight_layout() 58 | fig.savefig('example.png', dpi=300) 59 | -------------------------------------------------------------------------------- /plz_plot/__init__.py: -------------------------------------------------------------------------------- 1 | from .core import plot_plz_data 2 | from .io import load_plz_records, get_plz_dataframe 3 | 4 | 5 | __all__ = ['plot_plz_data', 'load_plz_records', 'get_plz_dataframe'] 6 | -------------------------------------------------------------------------------- /plz_plot/core.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | from matplotlib.patches import Polygon 3 | from matplotlib.collections import PatchCollection 4 | from cartopy import crs 5 | import numpy as np 6 | from shapely.geometry import MultiLineString 7 | 8 | from .io import load_plz_records 9 | 10 | 11 | def build_patch_collection(recs): 12 | patches = [] 13 | repeat = np.zeros(len(recs), dtype=int) 14 | for i, rec in enumerate(recs): 15 | if rec.geometry.geom_type == 'Polygon': 16 | iterable_geo = [rec.geometry] 17 | else: 18 | iterable_geo = rec.geometry 19 | 20 | for geom in iterable_geo: 21 | repeat[i] += 1 22 | 23 | if isinstance(geom.boundary, MultiLineString): 24 | xy = np.array(geom.boundary[0].xy).T 25 | else: 26 | xy = np.array(geom.boundary.xy).T 27 | 28 | patches.append(Polygon(xy, closed=True)) 29 | 30 | return PatchCollection(patches, transform=crs.Mercator.GOOGLE, zorder=2), repeat 31 | 32 | 33 | def plot_plz_data( 34 | data, 35 | cmap=None, 36 | vmin=None, 37 | vmax=None, 38 | ax=None, 39 | projection=None, 40 | norm=None 41 | ): 42 | data = np.asanyarray(data) 43 | 44 | recs = load_plz_records() 45 | col, repeat = build_patch_collection(recs) 46 | data = np.repeat(data, repeat) 47 | 48 | if ax is None: 49 | ax = plt.axes(projection=projection or crs.Mercator()) 50 | ax.set_extent([4, 16, 47, 56], crs.PlateCarree()) 51 | 52 | col.set_array(data) 53 | col.set_cmap(cmap) 54 | col.set_norm(norm) 55 | col.set_clim(vmin, vmax) 56 | 57 | ax.add_collection(col) 58 | 59 | return col 60 | -------------------------------------------------------------------------------- /plz_plot/io.py: -------------------------------------------------------------------------------- 1 | from cartopy.io.shapereader import Reader 2 | from pkg_resources import resource_filename 3 | import pandas as pd 4 | 5 | _HAS_FIONA = False 6 | try: 7 | import fiona 8 | _HAS_FIONA = True 9 | except ImportError: 10 | pass 11 | 12 | # Workaround cartopy bug 13 | if _HAS_FIONA: 14 | PATH = resource_filename('plz_plot', 'resources/plz-5stellig.shp') 15 | else: 16 | PATH = resource_filename('plz_plot', 'resources/plz-5stellig') 17 | 18 | 19 | def load_plz_records(path=PATH): 20 | 21 | reader = Reader(path) 22 | recs = list(reader.records()) 23 | 24 | return recs 25 | 26 | 27 | def get_plz_dataframe(path=PATH): 28 | recs = load_plz_records(path) 29 | df = pd.DataFrame([r.attributes for r in recs]).set_index('plz') 30 | 31 | df.rename( 32 | columns={'qkm': 'area', 'einwohner': 'inhabitants'}, 33 | inplace=True, 34 | ) 35 | df['names'] = ( 36 | df['note'] 37 | .str.rstrip('\x00') 38 | .str.split(' ') 39 | .apply(lambda l: ' '.join(l[1:])) 40 | ) 41 | df.drop('note', axis=1, inplace=True) 42 | 43 | return df 44 | 45 | 46 | def get_plz_geometries(path=PATH): 47 | recs = load_plz_records(path) 48 | return [r.geometry for r in recs] 49 | -------------------------------------------------------------------------------- /plz_plot/resources/plz-5stellig.dbf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxnoe/plz_plot/e6877bf3d8f78695f746e91ba9e7a60d8ff7ce90/plz_plot/resources/plz-5stellig.dbf -------------------------------------------------------------------------------- /plz_plot/resources/plz-5stellig.prj: -------------------------------------------------------------------------------- 1 | PROJCS["WGS 84 / Pseudo-Mercator", 2 | GEOGCS["WGS 84", 3 | DATUM["WGS_1984", 4 | SPHEROID["WGS 84",6378137,298.257223563, 5 | AUTHORITY["EPSG","7030"]], 6 | AUTHORITY["EPSG","6326"]], 7 | PRIMEM["Greenwich",0, 8 | AUTHORITY["EPSG","8901"]], 9 | UNIT["degree",0.0174532925199433, 10 | AUTHORITY["EPSG","9122"]], 11 | AUTHORITY["EPSG","4326"]], 12 | PROJECTION["Mercator_1SP"], 13 | PARAMETER["central_meridian",0], 14 | PARAMETER["scale_factor",1], 15 | PARAMETER["false_easting",0], 16 | PARAMETER["false_northing",0], 17 | UNIT["metre",1, 18 | AUTHORITY["EPSG","9001"]], 19 | AXIS["X",EAST], 20 | AXIS["Y",NORTH], 21 | EXTENSION["PROJ4","+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs"], 22 | AUTHORITY["EPSG","3857"]] -------------------------------------------------------------------------------- /plz_plot/resources/plz-5stellig.shp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxnoe/plz_plot/e6877bf3d8f78695f746e91ba9e7a60d8ff7ce90/plz_plot/resources/plz-5stellig.shp -------------------------------------------------------------------------------- /plz_plot/resources/plz-5stellig.shx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxnoe/plz_plot/e6877bf3d8f78695f746e91ba9e7a60d8ff7ce90/plz_plot/resources/plz-5stellig.shx -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [aliases] 2 | test=pytest 3 | 4 | [tool:pytest] 5 | addopts = -v 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name='plz_plot', 5 | version='0.1.0', 6 | description='Plot data on a map using cartopy and german postal codes', 7 | url='http://github.com/MaxNoe/plz_plot', 8 | author='Maximilian Noethe', 9 | author_email='maximilian.noethe@tu-dortmund.de', 10 | license='MIT', 11 | packages=[ 12 | 'plz_plot', 13 | ], 14 | package_data={ 15 | '': [ 16 | 'resources/*', 17 | ] 18 | }, 19 | python_requires='>=3.6', 20 | tests_require=['pytest>=3.0.0'], 21 | setup_requires=['pytest-runner'], 22 | install_requires=[ 23 | 'numpy', 24 | 'cartopy', 25 | 'matplotlib>=1.5', 26 | 'pandas', 27 | ], 28 | zip_safe=False, 29 | ) 30 | --------------------------------------------------------------------------------