!

Neighbourhood livability scores that move property values

Photo of Alex Wilkinson
Alex Wilkinson
CEO of Houski
2026-06-24

Two houses on opposite sides of the same city can have identical square footage, identical bedroom counts, and identical lot sizes, and sell for tens of thousands of dollars apart. Appraisers and automated valuation model (AVM) builders spend their careers explaining that gap. A large part of it is not the building at all. It is whether you can walk to a coffee shop, catch a train downtown, park without circling the block, and get there without sitting in traffic.

Those are measurable. Houski carries five livability scores that capture exactly this layer of value, score_walkability, score_transit, score_bicycle, score_parking, and score_traffic. They are the features that a comparable-sales adjustment grid struggles to quantify and that a valuation model leaves on the table when it only sees beds, baths, and floor area.

This post walks through what the five scores measure, how to pull them for a single address, how to aggregate them across a city, and how to feed them into a valuation model as location features that actually carry signal.

What the livability scores measure

Each score is an integer from 0 to 10 on every property in the database. Higher is always better, and each describes the area around the property rather than the structure.

  • score_walkability. How much of daily life is reachable on foot. High scores mean shops, schools, and services are a short walk away.
  • score_transit. Quality and reach of public transit nearby. High scores mean frequent service and good coverage.
  • score_bicycle. How rideable the area is, from bike paths to connected lanes. High scores mean safe, connected cycling.
  • score_parking. Parking supply in the area. A high score means parking is plentiful and easy to find.
  • score_traffic. Local traffic conditions. A high score means light, free-flowing traffic. A low score means congestion.

One nuance worth stating to anyone building a model. score_parking measures supply, so a dense downtown core with great transit and great walkability can legitimately carry a low parking score, and that is not a contradiction. The five scores describe different and sometimes opposing things about the same place, which is exactly why a valuation model wants all five rather than a single blended livability number.

Because higher is better across all five, the filters read naturally. score_walkability_gte=8 returns the most walkable end of the distribution. score_traffic_lte=3 returns the most congested.

Pulling the livability layer for one address

Resolve a street address to a property_id first. This is a live call against the real API, regenerated on every page render:

API request
TypeScript code
const houski_data = async (): Promise<SearchResponse> => {

    // You must copy the SearchResponse type declarations from the 
    // Houski API documentation to strongly type the response

    const url = new URL('https://api.houski.ca/search');
    url.searchParams.set('api_key', 'YOUR_API_KEY');
    url.searchParams.set('max_results', '1');
    url.searchParams.set('query', '62+Forest+Manor+Rd+Toronto+ON');

    const response = await fetch(url);
    const data = await response.json();

    return data;
}

(async () => {
let data: SearchResponse = await houski_data();

// Log the response
console.log(data);
})();
API response
JSON
{
  "cache_hit": true,
  "cost_cents": 0.019999999552965164,
  "data": [
    {
      "address": "609 62 Forest Manor Road",
      "property_id": "16da199304dbddb1"
    }
  ],
  "error": "",
  "match_meta": [
    {
      "match_value": 1.0,
      "property_id": "16da199304dbddb1"
    }
  ],
  "price_quote": false,
  "result_total": 1,
  "time_ms": 597
}

Then pull all five livability scores for that property_id at once:

API request
TypeScript code
const houski_data = async (): Promise<PropertiesResponse> => {

    // You must copy the PropertiesResponse type declarations from the 
    // Houski API documentation to strongly type the response

    const url = new URL('https://api.houski.ca/properties');
    url.searchParams.set('api_key', 'YOUR_API_KEY');
    url.searchParams.set('property_id_eq', '10000a939ca95e87');
    url.searchParams.set('select', 'address,city,score_walkability,score_transit,score_bicycle,score_parking,score_traffic');

    const response = await fetch(url);
    const data = await response.json();

    return data;
}

(async () => {
let data: PropertiesResponse = await houski_data();

// Log the response
console.log(data);
})();
API response
JSON
{
  "cache_hit": false,
  "cost_cents": 0.5299999713897705,
  "data": [
    {
      "address": "2202 62 Forest Manor Road",
      "city": "Toronto",
      "property_id": "10000a939ca95e87",
      "score_bicycle": 6,
      "score_parking": 8,
      "score_traffic": 0,
      "score_transit": 8,
      "score_walkability": 9
    }
  ],
  "error": "",
  "pagination": {
    "current_page": 1,
    "has_next_page": false,
    "has_previous_page": false,
    "page_total": 1
  },
  "price_quote": false,
  "result_total": 1,
  "time_ms": 25,
  "ui_info": {
    "address": "2202 62 Forest Manor Road",
    "address_link": "ca/on/toronto/henry-farm/2202-62-forest-manor-road",
    "address_slug": "2202-62-forest-manor-road",
    "city": "Toronto",
    "city_id": "6cdbdee2492718ed",
    "city_link": "ca/on/toronto",
    "city_slug": "toronto",
    "community": "Henry Farm",
    "community_id": "b9b28936a3929915",
    "community_link": "ca/on/toronto/henry-farm",
    "community_slug": "henry_farm",
    "country": "Canada",
    "country_abbreviation": "CA",
    "country_abbreviation_id": "9ace2b6431b7f1be",
    "country_abbreviation_link": "ca",
    "country_slug": "canada",
    "property_id": "10000a939ca95e87",
    "province": "Ontario",
    "province_abbreviation": "ON",
    "province_abbreviation_id": "146699ee774499d3",
    "province_abbreviation_link": "ca/on",
    "province_slug": "ontario"
  }
}

This North York address scores high on walkability and transit with comfortable parking, which is the classic transit-node profile that an appraiser would otherwise capture only as a vague qualitative note. Now it is five numbers you can put straight into an adjustment grid or a feature vector.

Set select to only the scores you use so you never pay for fields you ignore.

Aggregating livability across a city

The aggregate endpoint returns one number for a whole market using the same filters as the property endpoint.

Median walkability for the city of Toronto:

API request
TypeScript code
const houski_data = async (): Promise<AggregateResponse> => {

    // You must copy the AggregateResponse type declarations from the 
    // Houski API documentation to strongly type the response

    const url = new URL('https://api.houski.ca/aggregate');
    url.searchParams.set('aggregation', 'median');
    url.searchParams.set('api_key', 'YOUR_API_KEY');
    url.searchParams.set('city', 'Toronto');
    url.searchParams.set('country_abbreviation', 'ca');
    url.searchParams.set('field', 'score_walkability');
    url.searchParams.set('province_abbreviation', 'on');

    const response = await fetch(url);
    const data = await response.json();

    return data;
}

(async () => {
let data: AggregateResponse = await houski_data();

// Log the response
console.log(data);
})();
API response
JSON
{
  "cache_hit": false,
  "cost_cents": 1.0,
  "data": [
    {
      "aggregation": "median",
      "field": "score_walkability",
      "value": "9"
    }
  ],
  "error": "",
  "price_quote": false,
  "time_ms": 57
}

Median transit reach for Montreal:

API request
TypeScript code
const houski_data = async (): Promise<AggregateResponse> => {

    // You must copy the AggregateResponse type declarations from the 
    // Houski API documentation to strongly type the response

    const url = new URL('https://api.houski.ca/aggregate');
    url.searchParams.set('aggregation', 'median');
    url.searchParams.set('api_key', 'YOUR_API_KEY');
    url.searchParams.set('city', 'Montreal');
    url.searchParams.set('country_abbreviation', 'ca');
    url.searchParams.set('field', 'score_transit');
    url.searchParams.set('province_abbreviation', 'qc');

    const response = await fetch(url);
    const data = await response.json();

    return data;
}

(async () => {
let data: AggregateResponse = await houski_data();

// Log the response
console.log(data);
})();
API response
JSON
{
  "cache_hit": false,
  "cost_cents": 1.0,
  "data": [
    {
      "aggregation": "median",
      "field": "score_transit",
      "value": "9"
    }
  ],
  "error": "",
  "price_quote": false,
  "time_ms": 56
}

These city-level medians are the baseline you regress individual properties against. A property scoring well above its city median on walkability and transit is carrying a location premium that a beds-and-baths model will systematically miss.

A livability comparison across markets

Run each score as a median across the markets you value in and you get a livability fingerprint per city. Each cell is the indicative median score from 0 to 10 with a plain read in brackets so the meaning is obvious: (good), (ok), (bad). Higher is always better, and numbers shift as the model updates:

CityProvinceWalkabilityTransitBicycleParkingTraffic
TorontoON8 (good)8 (good)5 (ok)5 (ok)3 (bad)
MontrealQC8 (good)8 (good)5 (ok)5 (ok)3 (bad)
VancouverBC8 (good)8 (good)8 (good)5 (ok)3 (bad)
CalgaryAB5 (ok)5 (ok)5 (ok)8 (good)5 (ok)
HalifaxNS5 (ok)5 (ok)5 (ok)8 (good)5 (ok)

The pattern that matters for valuation is the trade-off. The dense, walkable, transit-rich cores buy that access with congestion and tighter parking. The more suburban markets give back walkability and transit in exchange for parking supply and lighter traffic. A model that treats all five as one index erases the trade-off that buyers are actually pricing.

Feeding livability into a valuation model

The cleanest pattern for an automated valuation model is to pull the five scores as standalone location features alongside the structural fields, then let the model learn their weights per market rather than hard-coding them. The property call to pull a training sample of active inventory in one city looks like this:

API request
Python code
from dataclasses import dataclass
from dataclasses_json import dataclass_json
import requests

params = {}
params['api_key'] = 'YOUR_API_KEY'
params['city'] = 'Toronto'
params['country_abbreviation'] = 'ca'
params['has_expand_listings_eq'] = 'true'
params['province_abbreviation'] = 'on'
params['results_per_page'] = '5'
params['select'] = 'address,score_walkability,score_transit,score_bicycle,score_parking,score_traffic'

response = requests.get('https://api.houski.ca/properties', params=params)

if response.status_code == 200:
    json_data = response.json()

    print(json_data)

    # You must copy the PropertiesResponse type declarations from the 
    # Houski API documentation to strongly type the response
    typed_response = PropertiesResponse.from_dict(json_data)

    # Log the response
    print(typed_response)
else:
    print(f'Failed to get data: {response}')
API response
JSON
{
  "cache_hit": false,
  "cost_cents": 2.5999999046325684,
  "data": [
    {
      "address": "2202 62 Forest Manor Road",
      "property_id": "10000a939ca95e87",
      "score_bicycle": 6,
      "score_parking": 8,
      "score_traffic": 0,
      "score_transit": 8,
      "score_walkability": 9
    },
    {
      "address": "78 Northey Drive",
      "property_id": "100109271a96670",
      "score_bicycle": 6,
      "score_parking": 8,
      "score_traffic": 3,
      "score_transit": 5,
      "score_walkability": 7
    },
    {
      "address": "710 2835 Islington Avenue",
      "property_id": "10014a83618bbb08",
      "score_bicycle": 6,
      "score_parking": 6,
      "score_traffic": 0,
      "score_transit": 3,
      "score_walkability": 6
    },
    {
      "address": "108 Gradwell Drive",
      "property_id": "100163fd49c7cc3b",
      "score_bicycle": 6,
      "score_parking": 6,
      "score_traffic": 2,
      "score_transit": 4,
      "score_walkability": 5
    },
    {
      "address": "707 26 Norton Avenue",
      "property_id": "10017b8fb9a9b39c",
      "score_bicycle": 6,
      "score_parking": 10,
      "score_traffic": 2,
      "score_transit": 10,
      "score_walkability": 10
    }
  ],
  "error": "",
  "pagination": {
    "current_page": 1,
    "has_next_page": true,
    "has_previous_page": false,
    "page_total": 25392
  },
  "price_quote": false,
  "result_total": 126960,
  "time_ms": 70,
  "ui_info": {
    "city": "Toronto",
    "city_id": "6cdbdee2492718ed",
    "city_link": "ca/on/toronto",
    "city_slug": "toronto",
    "country": "Canada",
    "country_abbreviation": "CA",
    "country_abbreviation_id": "9ace2b6431b7f1be",
    "country_abbreviation_link": "ca",
    "country_slug": "canada",
    "province": "Ontario",
    "province_abbreviation": "ON",
    "province_abbreviation_id": "146699ee774499d3",
    "province_abbreviation_link": "ca/on",
    "province_slug": "ontario"
  }
}

The has_expand_listings_eq=true filter restricts the pull to properties with active listings, which is the marketable inventory you want for training a valuation model rather than the long tail of listing-less parcels. Page through the results to build the sample.

Three practical notes for model builders. First, walkability and transit tend to correlate, so consider them together or let a tree-based model sort the interaction out. Second, parking and walkability often move in opposite directions, so keep them separate rather than averaging. Third, traffic is the one where lower raw congestion is the premium, and because the score is already oriented so that higher is better, you can feed it in raw without inverting anything.

For appraisers without a model

You do not need a model to use these. In a comparable-sales grid, the five scores give you a defensible, numeric basis for a location adjustment between the subject and each comparable. If the subject scores 9 on walkability and a comparable scores 5, that gap is no longer a hand-waved qualitative note. It is a documented difference you can footnote with the field and the value, which is exactly what stands up in a review.

Limitations to be honest about

  • The scores describe the area, not the individual property. A quiet cul-de-sac inside a high-traffic district still inherits the district score.
  • They update on a schedule, not in real time. A transit line that opened last month may not be reflected yet.
  • Each score is a relative ranking, not an absolute unit. A walkability 8 is more walkable than a 5. It is not eight of anything.
  • The scores reflect the present, so a model trained on them captures current location value, not a forecast of how a neighbourhood will change.

A full example: location-adjusting a set of comparables

The workflow appraisers ask about most is adjusting comparables for location. You have a subject property and a handful of comparables, and you want a numeric location adjustment rather than a qualitative shrug.

Resolve each address to a property_id with the search endpoint, then pull the five livability scores for each property_id. The resolve step is:

API request
Python code
from dataclasses import dataclass
from dataclasses_json import dataclass_json
import requests

params = {}
params['api_key'] = 'YOUR_API_KEY'
params['max_results'] = '1'
params['query'] = '62+Forest+Manor+Rd+Toronto+ON'

response = requests.get('https://api.houski.ca/search', params=params)

if response.status_code == 200:
    json_data = response.json()

    print(json_data)

    # You must copy the SearchResponse type declarations from the 
    # Houski API documentation to strongly type the response
    typed_response = SearchResponse.from_dict(json_data)

    # Log the response
    print(typed_response)
else:
    print(f'Failed to get data: {response}')
API response
JSON
{
  "cache_hit": true,
  "cost_cents": 0.019999999552965164,
  "data": [
    {
      "address": "609 62 Forest Manor Road",
      "property_id": "16da199304dbddb1"
    }
  ],
  "error": "",
  "match_meta": [
    {
      "match_value": 1.0,
      "property_id": "16da199304dbddb1"
    }
  ],
  "price_quote": false,
  "result_total": 1,
  "time_ms": 597
}

And the score pull against the returned property_id is:

API request
Python code
from dataclasses import dataclass
from dataclasses_json import dataclass_json
import requests

params = {}
params['api_key'] = 'YOUR_API_KEY'
params['property_id_eq'] = '10000a939ca95e87'
params['select'] = 'score_walkability,score_transit,score_bicycle,score_parking,score_traffic'

response = requests.get('https://api.houski.ca/properties', params=params)

if response.status_code == 200:
    json_data = response.json()

    print(json_data)

    # You must copy the PropertiesResponse type declarations from the 
    # Houski API documentation to strongly type the response
    typed_response = PropertiesResponse.from_dict(json_data)

    # Log the response
    print(typed_response)
else:
    print(f'Failed to get data: {response}')
API response
JSON
{
  "cache_hit": false,
  "cost_cents": 0.5199999809265137,
  "data": [
    {
      "address": "2202 62 Forest Manor Road",
      "property_id": "10000a939ca95e87",
      "score_bicycle": 6,
      "score_parking": 8,
      "score_traffic": 0,
      "score_transit": 8,
      "score_walkability": 9
    }
  ],
  "error": "",
  "pagination": {
    "current_page": 1,
    "has_next_page": false,
    "has_previous_page": false,
    "page_total": 1
  },
  "price_quote": false,
  "result_total": 1,
  "time_ms": 47,
  "ui_info": {
    "address": "2202 62 Forest Manor Road",
    "address_link": "ca/on/toronto/henry-farm/2202-62-forest-manor-road",
    "address_slug": "2202-62-forest-manor-road",
    "city": "Toronto",
    "city_id": "6cdbdee2492718ed",
    "city_link": "ca/on/toronto",
    "city_slug": "toronto",
    "community": "Henry Farm",
    "community_id": "b9b28936a3929915",
    "community_link": "ca/on/toronto/henry-farm",
    "community_slug": "henry_farm",
    "country": "Canada",
    "country_abbreviation": "CA",
    "country_abbreviation_id": "9ace2b6431b7f1be",
    "country_abbreviation_link": "ca",
    "country_slug": "canada",
    "property_id": "10000a939ca95e87",
    "province": "Ontario",
    "province_abbreviation": "ON",
    "province_abbreviation_id": "146699ee774499d3",
    "province_abbreviation_link": "ca/on",
    "province_slug": "ontario"
  }
}

In your own code, difference each comparable against the subject on each of the five scores, apply your per-point dollar adjustment per market, and you have a documented location adjustment for every comparable in the grid. The same five numbers that justify the adjustment also defend it in review.

What to build next

Whether you are training a valuation model, building a comparable-sales grid, or just want a defensible location adjustment, all of it starts with one property call or one aggregate call. The fields are documented, the filters compose cleanly, and scanning a whole city for a one-time analysis costs a coffee.

Get started at /api-documentation/quick-start. Pricing starts at $99 a month, and you can have a livability comparison running in your terminal in under five minutes.