Metadata-Version: 2.4
Name: landmix
Version: 0.1.1
Summary: Land-use mix and diversity indices for urban analysis: normalised entropy, Herfindahl-Hirschman, Simpson, Hill numbers, balance, jobs-housing and dissimilarity indices for zones, grids and moving windows, with OpenStreetMap category mapping.
Author-email: Yusuf Eminoğlu <geospacephilo@gmail.com>
License-Expression: MIT
Project-URL: Homepage, https://github.com/YusufEminoglu/landmix_sdk
Project-URL: Repository, https://github.com/YusufEminoglu/landmix_sdk
Project-URL: Issues, https://github.com/YusufEminoglu/landmix_sdk/issues
Project-URL: Changelog, https://github.com/YusufEminoglu/landmix_sdk/blob/main/CHANGELOG.md
Keywords: land-use-mix,mixed-use,entropy,diversity-index,herfindahl,simpson,hill-numbers,jobs-housing-balance,urban-planning,walkability,openstreetmap,geojson,gis
Classifier: Development Status :: 4 - Beta
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Operating System :: OS Independent
Classifier: Environment :: Console
Classifier: Topic :: Scientific/Engineering :: GIS
Classifier: Intended Audience :: Science/Research
Classifier: Intended Audience :: Developers
Classifier: Typing :: Typed
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: pandas
Requires-Dist: pandas>=1.3.0; extra == "pandas"
Provides-Extra: shapely
Requires-Dist: shapely>=2.0.0; extra == "shapely"
Provides-Extra: dev
Requires-Dist: pytest>=7.0.0; extra == "dev"
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
Requires-Dist: ruff>=0.1.0; extra == "dev"
Requires-Dist: mypy>=1.0.0; extra == "dev"
Dynamic: license-file

# landmix

[![PyPI version](https://img.shields.io/pypi/v/landmix.svg)](https://pypi.org/project/landmix/)
[![Python versions](https://img.shields.io/pypi/pyversions/landmix.svg)](https://pypi.org/project/landmix/)
[![CI](https://github.com/YusufEminoglu/landmix_sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/YusufEminoglu/landmix_sdk/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)

**Land-use mix and diversity indices for urban analysis.**

`landmix` computes the land-use mix measures used in planning, transport and
walkability research from the amount of each land use in a zone. It runs on the
Python standard library alone. The indices are:

- **Entropy**: normalised Shannon entropy, with K set to all study categories
  (Cervero & Kockelman 1997; Frank et al. 2004, 2005) or to the categories present.
  The Cervero & Kockelman mean-of-hectares form is also included.
- **Concentration**: Herfindahl–Hirschman, normalised HHI, Simpson's λ, Gini–Simpson,
  and Simpson's finite-sample form for counts.
- **Hill numbers**: the effective number of land uses for any order *q*.
- **Two-use balance**: the balance index, jobs–housing ratio and jobs–housing balance
  (Ewing et al. 2002), and Hoek's MXI.
- **Spatial**: the Cervero & Kockelman (1997) dissimilarity index on a grid of
  dominant uses.

You can read inputs from a dict, a CSV file (wide or long layout) or a pandas
DataFrame. GeoJSON points and polygons can be aggregated to zone polygons, to a
regular grid or to moving-window buffers. This uses pure-Python point-in-polygon,
or exact area splitting if shapely is installed. OpenStreetMap tags can be
classified into planning land uses. Results are written to CSV, JSON, Markdown or
GeoJSON from Python or the `landmix` command.

```bash
pip install landmix              # stdlib only
pip install "landmix[shapely]"   # + exact polygon/zone intersection
pip install "landmix[pandas]"    # + DataFrame in/out
```

Requires Python 3.9 or newer.

---

## Quick start (Python)

```python
from landmix import entropy_index, hhi, hill_number, mix_table, Balance

USES = ["residential", "commercial", "office", "institutional", "entertainment"]
zone = {"residential": 6000, "commercial": 2500, "office": 1500}   # m2 floor area

entropy_index(zone, USES)                        # 0.583  (K = 5 study categories)
entropy_index(zone, normalization="present")     # 0.853  (K = 3 uses present)
hhi(zone)                                        # 0.445
hill_number(zone, q=1)                           # 2.554 effective land uses

zones = {
    "Centre": {"residential": 12000, "commercial": 9000, "office": 15000, "jobs": 1400, "housing": 300},
    "Suburb": {"residential": 45000, "commercial": 500, "jobs": 40, "housing": 700},
}
rows = mix_table(zones, USES, balances=[Balance("jhb", ["jobs"], ["housing"])])
```

`mix_table` returns one dict per zone. The default columns are `total`, `richness`,
`entropy`, `entropy_present`, `hhi`, `gini_simpson`, `hill1`, `hill2`,
`dominant_share` and `dominant`, plus `<name>` and `<name>_ratio` for each
`Balance`. Categories that are not listed in `USES` (here `jobs`, `housing`) are
left out of the mix indices, but a balance can still use them.

### GeoJSON: zones, grid, moving windows

```python
from landmix import (load_geojson, aggregate_to_zones, aggregate_to_grid,
                     moving_window, grid_centres, features_bounds,
                     osm_category, dissimilarity_index, mix_table)

pois = load_geojson("osm_pois.geojson")          # projected CRS, metres
zones = load_geojson("neighbourhoods.geojson")

table = aggregate_to_zones(pois, zones, category=osm_category, zone_id="name")
rows = mix_table(table)

grid = aggregate_to_grid(pois, 100, category=osm_category)        # 1 ha cells
di = dissimilarity_index(grid.dominant())                         # Cervero & Kockelman

centres = grid_centres(features_bounds(pois), 200)
windows = moving_window(pois, centres, radius=400, category=osm_category)

# polygons by floor area property, split exactly across zones (needs shapely)
blocks = load_geojson("blocks.geojson")
table = aggregate_to_zones(blocks, zones, category="use", measure="floor_area",
                           method="intersection", zone_id="name")
```

`measure` is `"count"`, `"area"` (polygon area) or the name of a numeric property.
`category` is a property name or a function of the feature properties.

## Quick start (command line)

```bash
# wide CSV: zone,residential,commercial,office,...,jobs,housing
landmix table zones.csv --categories residential,commercial,office,institutional,entertainment \
    --balance jhb=jobs/housing -o mix.csv

# long CSV: zone,category,value
landmix table parcels.csv --layout long --zone-col mahalle --category-col use --value-col m2

# OSM points aggregated to zone polygons, written back as GeoJSON with the indices
landmix geojson pois.geojson --osm --zones zones.geojson --zone-id name -o zones_mix.geojson

# 100 m grid (prints the dissimilarity index), 400 m moving windows every 200 m
landmix geojson landuse.geojson --category-field use --measure area --grid 100 -o grid.geojson
landmix geojson pois.geojson --osm --window 400 --spacing 200 -o windows.csv

landmix indices        # list indices and their sources
landmix osm-mapping    # print the default OSM tag table (JSON) to copy and edit
```

The output format follows the extension (`.csv`, `.json`, `.md`, `.geojson`).
Without `-o`, a Markdown table is printed. `--balance NAME=X1+X2/Y1@ratio` adds a
balance column. When the ratio is left out, the study-wide ratio is the target.

## API overview

| Function | Formula / meaning |
|---|---|
| `shannon(a, base=e)` | H = −Σ pⱼ ln pⱼ |
| `entropy_index(a, categories, k=None, normalization="all"\|"present")` | H / ln K |
| `cervero_kockelman_entropy(cells, categories)` | mean of cell entropies over developed cells |
| `hhi(a, scale=1)`, `normalized_hhi(a, k)` | Σ (s·pⱼ)²; (HHI − 1/K)/(1 − 1/K) |
| `simpson(a)`, `gini_simpson(a)` | λ = Σ pⱼ²; 1 − λ |
| `simpson_unbiased(counts)`, `gini_simpson_unbiased(counts)` | Σ nⱼ(nⱼ−1) / N(N−1) |
| `hill_number(a, q)`, `hill_profile(a, qs)` | ᵠD = (Σ pⱼ^q)^(1/(1−q)); q→1: exp H; q=∞: 1/max p |
| `balance_index(x, y, a=1)` | 1 − \|x − a·y\| / (x + a·y) |
| `jobs_housing_ratio(J, H)`, `jobs_housing_balance(J, H, J*, H*)` | J/H; 1 − \|J − kH\|/(J + kH), k = J*/H* |
| `mixed_use_index_hoek(a, housing)` | MXI = 100 · housing GFA / total GFA |
| `area_index(a, relevant)` | share of zone area in purpose-relevant uses (see Limitations) |
| `dissimilarity_index(grid, neighbours=8, edge="valid"\|"fixed")` | (1/K) Σₖ Σₗ Xₗ / 8 |
| `richness(a)`, `dominant_share(a)`, `proportions(a)` | helpers |

Here `a` is a `{category: amount}` mapping or a sequence of amounts. Pass
`categories` so that categories missing from a zone count as zero and every zone
shares the same K. Negative, NaN or infinite amounts raise `ValueError`. A zone
with a total of zero returns NaN. A zone with one land use has entropy 0, HHI 1
and Gini–Simpson 0.

Other modules:

- `landmix.io`: `read_csv`, `from_records`, `from_dataframe`, `to_dataframe`.
- `landmix.geo`: `load_geojson`, `point_in_polygon`, `geometry_area` (planar or
  spherical), `geometry_centroid`, `aggregate_to_zones`, `aggregate_to_grid` →
  `Grid` (`zone_table()`, `dominant()`, `to_geojson()`), `moving_window`,
  `grid_centres`, `haversine`.
- `landmix.categories`: `classify_osm`, `OSM_DEFAULT_MAPPING`, `default_mapping()`,
  `recode`, and `FRANK_CLASSES` (the five Frank et al. land uses).
- `landmix.report`: `mix_table`, `Balance`, `summary`, `to_markdown`, `to_json`,
  `write_csv`.

## Method notes and sources

**Normalised entropy.** `LUM = −Σⱼ pⱼ ln pⱼ / ln K`. Cervero & Kockelman (1997) and
Frank et al. (2004, 2005) take *K* as the number of land-use categories in the
study, so a zone reaches 1 only when every study use has an equal share. This is
`normalization="all"`. Here a missing category matters, so give the full
`categories` list or `k`. Dividing by the number of uses *present*
(`normalization="present"`) measures evenness among those uses only. Song, Merlin &
Rodriguez (2013) discuss how sensitive the entropy index is to this choice. The
original Cervero & Kockelman entropy averages the per-hectare entropy over the
actively developed hectares of a tract (`cervero_kockelman_entropy`).

**Dissimilarity index** (Cervero & Kockelman 1997). For each developed hectare,
this is the share of its 8 neighbours whose land use differs from the centre's,
averaged over hectares. Build the input with `aggregate_to_grid(...).dominant()`.
The published form divides by 8. The default here (`edge="valid"`) divides by the
number of developed neighbours that exist, so that cells on the edge of the study
area are not penalised. Use `edge="fixed"` for the published denominator.

**HHI and Simpson.** HHI = Σ pⱼ² (Hirschman 1964; Rhoades 1993), which is
Simpson's λ (Simpson 1949). Gini–Simpson 1 − λ is the probability that two random
units of land belong to different uses. For counts of discrete units, Simpson's
finite-sample form Σ n(n−1)/N(N−1) avoids the small-sample bias.

**Hill numbers** (Hill 1973; Jost 2006) express diversity as an effective number
of equally common uses: order 0 is richness, order 1 is exp(H) and order 2 is the
inverse Simpson index. Simons (2023) argues for them in pedestrian-scale
mixed-use analysis.

**Balance and jobs–housing.** The balance index
`1 − |X − aY| / (X + aY)` equals 1 when `X/Y` equals the target ratio `a` and 0
when one of the uses is absent. Ewing, Pendall & Chen (2002) use the regional
jobs-per-resident ratio as the target. `mix_table` uses the study-wide ratio
unless `a` is given. Cervero (1989) discusses jobs–housing ratios.

**MXI** (van den Hoek 2008) is the housing share of gross floor area, in percent.

**Areas and distances.** Coordinates are planar by default, so reproject to a
local metric CRS. With `lonlat=True`, polygon areas use the spherical formula of
Chamberlain & Duquette (2007) with the WGS84 equatorial radius, and window
distances use the haversine formula with the mean Earth radius (6371008.8 m).

**OSM categories.** The default table follows the OSM wiki definitions of
`landuse`, `amenity`, `shop`, `office`, `leisure`, `tourism`, `craft` and
`building`. Keys are tried in a fixed priority order, so a `shop=*` on an apartment
building counts as commercial. The wiki defines `landuse=commercial` as
predominantly offices, so it maps to `office`, and `landuse=retail` maps to
`commercial`. The table is a judgement call: copy `default_mapping()`, edit it and
pass it to `classify_osm`.

### References

- Bordoloi, R., Mote, A., Sarkar, P. P., & Mallikarjuna, C. (2013). Quantification of land use diversity in the context of mixed land use. *Procedia – Social and Behavioral Sciences*, 104, 563–572. https://doi.org/10.1016/j.sbspro.2013.11.150
- Cervero, R. (1989). Jobs-housing balancing and regional mobility. *Journal of the American Planning Association*, 55(2), 136–150.
- Cervero, R., & Kockelman, K. (1997). Travel demand and the 3Ds: Density, diversity, and design. *Transportation Research Part D*, 2(3), 199–219.
- Chamberlain, R. G., & Duquette, W. H. (2007). *Some algorithms for polygons on a sphere*. JPL Publication 07-03.
- Ewing, R., Pendall, R., & Chen, D. (2002). *Measuring Sprawl and Its Impact*. Smart Growth America.
- Frank, L. D., Andresen, M. A., & Schmid, T. L. (2004). Obesity relationships with community design, physical activity, and time spent in cars. *American Journal of Preventive Medicine*, 27(2), 87–96.
- Frank, L. D., Schmid, T. L., Sallis, J. F., Chapman, J., & Saelens, B. E. (2005). Linking objectively measured physical activity with objectively measured urban form. *American Journal of Preventive Medicine*, 28(2S2), 117–125.
- Hill, M. O. (1973). Diversity and evenness: a unifying notation and its consequences. *Ecology*, 54(2), 427–432.
- Hirschman, A. O. (1964). The paternity of an index. *American Economic Review*, 54(5), 761.
- Iannillo, A., & Fasolino, I. (2021). Land-use mix and urban sustainability: Benefits and indicators analysis. *Sustainability*, 13(23), 13460. https://doi.org/10.3390/su132313460 (used as a published test case)
- Jost, L. (2006). Entropy and diversity. *Oikos*, 113(2), 363–375.
- Rhoades, S. A. (1993). The Herfindahl-Hirschman index. *Federal Reserve Bulletin*, 79, 188–189.
- Shannon, C. E. (1948). A mathematical theory of communication. *Bell System Technical Journal*, 27(3), 379–423.
- Simons, G. D. (2023). The cityseer Python package for pedestrian-scale network-based urban analysis. *Environment and Planning B*, 50(5), 1328–1344.
- Simpson, E. H. (1949). Measurement of diversity. *Nature*, 163, 688.
- Song, Y., Merlin, L., & Rodriguez, D. (2013). Comparing measures of urban land use mix. *Computers, Environment and Urban Systems*, 42, 1–13.
- van den Hoek, J. W. (2008). The MXI (Mixed-use Index) as tool for urban planning and analysis. In *Corporations and Cities*, TU Delft.

## Testing

The test suite checks every index against hand-computed values. Examples
include shares (0.5, 0.25, 0.25), checkerboard grids for the dissimilarity
index, and an exact graticule cell for the spherical area. Edge cases covered
are single categories, all-zero zones, missing categories and invalid amounts.
The suite also reproduces the published two-decimal values of Iannillo &
Fasolino (2021, Table 6) for Shannon–Wiener, entropy and HHI.

```bash
pip install -e ".[dev,pandas,shapely]"
python -m pytest
```

## Limitations

- **Bordoloi et al. (2013).** The paper proposes modified dissimilarity and entropy
  indices, a per-trip-purpose *Area Index* and a *Mix type Index*. The full text
  could not be consulted for this release, so only `area_index` is provided. It
  implements the paper's description: the share of zone land in uses relevant to
  one trip purpose. It is **not verified against the paper's equation**. The Mix
  type Index is not implemented.
- The dissimilarity index uses the dominant use of each cell. How edge cells and
  undeveloped (`None`) neighbours are treated is a documented choice (`edge=`).
- With `method="centroid"`, a polygon goes whole to the zone or cell containing its
  centroid, and the centroid of a strongly concave polygon can fall outside it.
  Use `method="intersection"` (shapely) for exact area splitting.
- A point exactly on a shared zone edge goes to the zone on its right or upper
  side, the same rule the grid uses.
- Moving windows are straight-line (Euclidean or haversine) buffers on feature
  representative points. Network buffers are out of scope.
- The grid is built in coordinate units. Grids in longitude/latitude have cells of
  unequal area.
- The OSM category table is not a standard and should be reviewed for each study.

## License

MIT © 2026 Yusuf Eminoğlu. See [LICENSE](LICENSE). To cite landmix, see
[CITATION.cff](CITATION.cff).

## Academic citation

```bibtex
@software{eminoglu_landmix,
  author  = {Eminoğlu, Yusuf},
  title   = {landmix},
  year    = {2026},
  version = {0.1.1},
  url     = {https://github.com/YusufEminoglu/landmix_sdk}
}
```

See also `CITATION.cff`.
