Real Estate Analysis: Market Filtering
Filter residential properties by price, size, and proximity to schools to identify optimal investment opportunities.
Scenario Overviewβ
Goal: Find single-family homes priced $200k-$400k, >150mΒ², within 1km of highly-rated schools.
Real-World Application:
- Real estate investors finding properties matching criteria
- Home buyers searching for family-friendly neighborhoods
- Real estate agents providing data-driven recommendations
- Market analysts evaluating property values vs. amenities
Estimated Time: 8 minutes
Difficulty: β Beginner
Prerequisitesβ
Required Dataβ
-
Residential Properties Layer (points or polygons)
- Property listings or parcel data
- Required attributes:
price(numeric)area_sqmorliving_area(numeric)property_type(text: 'single_family', 'condo', etc.)
- Optional:
bedrooms,bathrooms,year_built
-
Schools Layer (points)
- School locations
- Optional but useful:
rating,school_level,name - Covers your study area
Sample Data Sourcesβ
Real Estate Data:
- MLS (Multiple Listing Service) exports
- Zillow/Trulia data feeds (if available)
- Municipal property assessment databases
- OpenStreetMap buildings with tags
Schools Data:
# QGIS QuickOSM plugin
Key: "amenity", Value: "school"
Key: "school", Value: "*"
# Or government data:
- National Center for Education Statistics (USA)
- Department for Education (UK)
- Local education authority databases
Backend Recommendationβ
Multi-Backend Comparison - This workflow demonstrates all three:
- PostgreSQL: Fastest if you have >10k properties
- Spatialite: Good middle ground for city-scale data
- OGR: Works everywhere, acceptable performance for <5k properties
Step-by-Step Instructionsβ
Step 1: Load and Inspect Property Dataβ
-
Load properties layer:
residential_properties.gpkg -
Open Attribute Table (F6)
-
Verify required fields exist:
β price (numeric)
β area_sqm (numeric)
β property_type (text) -
Check data quality:
Sort by price: Look for unrealistic values (0, NULL, >$10M)
Sort by area: Check for 0 or NULL values
Filter property_type: Identify valid categories
If you have missing values:
-- Filter out incomplete records FIRST
"price" IS NOT NULL
AND "area_sqm" > 0
AND "property_type" IS NOT NULL
Step 2: Apply Basic Attribute Filtersβ
Using FilterMate:
- Open FilterMate panel
- Select residential_properties layer
- Choose any backend (attribute filtering works equally on all)
- Enter expression:
- Basic Filter
- Advanced (Multiple Types)
- Investment Focused
-- Price between $200k and $400k
-- Area greater than 150mΒ²
-- Single-family homes only
"price" >= 200000
AND "price" <= 400000
AND "area_sqm" >= 150
AND "property_type" = 'single_family'
-- Accept multiple property types
"price" BETWEEN 200000 AND 400000
AND "area_sqm" >= 150
AND "property_type" IN ('single_family', 'townhouse')
AND "bedrooms" >= 3
-- Find undervalued properties (price per sqm)
"price" BETWEEN 200000 AND 400000
AND "area_sqm" >= 150
AND "property_type" = 'single_family'
AND ("price" / "area_sqm") < 2000 -- Less than $2000/mΒ²
- Click Apply Filter
- Review count: "Showing X of Y features"
Expected Result: Properties narrowed down by price, size, and type
Step 3: Add Spatial Filter for School Proximityβ
Now add the location-based criterion:
- Ensure schools layer is loaded:
schools.gpkg - Modify FilterMate expression to add spatial component:
- OGR / Spatialite
- PostgreSQL
-- Combine attribute filters + spatial proximity
"price" >= 200000
AND "price" <= 400000
AND "area_sqm" >= 150
AND "property_type" = 'single_family'
AND distance(
$geometry,
aggregate(
layer:='schools',
aggregate:='collect',
expression:=$geometry
)
) <= 1000
Alternative using overlay functions:
-- Same criteria + check any school within 1km exists
"price" BETWEEN 200000 AND 400000
AND "area_sqm" >= 150
AND "property_type" = 'single_family'
AND array_length(
overlay_within(
'schools',
buffer($geometry, 1000)
)
) > 0
-- Using PostGIS spatial functions
price >= 200000
AND price <= 400000
AND area_sqm >= 150
AND property_type = 'single_family'
AND EXISTS (
SELECT 1
FROM schools s
WHERE ST_DWithin(
properties.geom,
s.geom,
1000 -- 1km in meters
)
)
Or with distance calculation:
-- Include distance to nearest school as output
SELECT
p.*,
MIN(ST_Distance(p.geom, s.geom)) AS distance_to_school
FROM properties p
JOIN schools s ON ST_DWithin(p.geom, s.geom, 1000)
WHERE price BETWEEN 200000 AND 400000
AND area_sqm >= 150
AND property_type = 'single_family'
GROUP BY p.property_id
- Click Apply Filter
- Review results on map (should be concentrated near schools)
Step 4: Refine by School Quality (Optional)β
If your schools layer has rating data:
-- Only properties near highly-rated schools (rating β₯ 8/10)
"price" BETWEEN 200000 AND 400000
AND "area_sqm" >= 150
AND "property_type" = 'single_family'
AND array_max(
array_foreach(
overlay_within('schools', buffer($geometry, 1000)),
attribute(@element, 'rating')
)
) >= 8
What this does:
- Finds all schools within 1km buffer
- Gets their rating values
- Keeps properties where at least one nearby school has rating β₯8
Step 5: Calculate Distance to Nearest Schoolβ
Add field showing exact distance:
-
Open Field Calculator (Ctrl+I) on filtered layer
-
Create new field:
Field name: nearest_school_m
Type: Decimal (double)
Precision: 1
Expression:
round(
array_min(
array_foreach(
overlay_nearest('schools', $geometry, limit:=1),
distance(geometry(@element), $geometry)
)
),
0
) -
Add school name (optional):
Field name: nearest_school_name
Type: Text (string)
Expression:
attribute(
overlay_nearest('schools', $geometry, limit:=1)[0],
'name'
)
Step 6: Rank Properties by Valueβ
Create a value score combining multiple factors:
-
Open Field Calculator
-
Create calculated field:
Field name: value_score
Type: Decimal (double)
Expression:
-- Higher score = better value
-- Weighted factors:
(400000 - "price") / 1000 * 0.4 + -- Lower price = better (40% weight)
("area_sqm" - 150) * 0.3 + -- Larger area = better (30% weight)
(1000 - "nearest_school_m") * 0.3 -- Closer school = better (30% weight) -
Sort by value_score descending to see best deals first
Step 7: Visualize Resultsβ
Color by Distance to School:
- Right-click layer β Symbology
- Choose Graduated
- Value:
nearest_school_m - Method: Natural Breaks
- Colors: Green (close) β Yellow β Red (far)
Add Labels:
Label with: concat('$', "price"/1000, 'k - ', round("nearest_school_m",0), 'm to school')
Size: 10pt
Buffer: White, 1mm
Step 8: Export Matches for Analysisβ
-
In FilterMate: Click Export Filtered Features
Format: GeoPackage
Filename: properties_investment_targets.gpkg
CRS: WGS84 (for portability)
Include all attributes: β -
Export attribute table as spreadsheet:
Right-click layer β Export β Save Features As
Format: CSV or XLSX
Fields: Select relevant columns only -
Create simple report (optional):
# Python Console
layer = iface.activeLayer()
features = list(layer.getFeatures())
print("=== Property Investment Report ===")
print(f"Matching properties: {len(features)}")
print(f"Average price: ${sum(f['price'] for f in features)/len(features):,.0f}")
print(f"Average area: {sum(f['area_sqm'] for f in features)/len(features):.0f} mΒ²")
print(f"Average distance to school: {sum(f['nearest_school_m'] for f in features)/len(features):.0f} m")
print(f"Price range: ${min(f['price'] for f in features):,} - ${max(f['price'] for f in features):,}")
Understanding the Resultsβ
What the Filter Showsβ
β Selected properties: Match ALL criteria:
- Price: $200,000 - $400,000
- Size: β₯150mΒ²
- Type: Single-family home
- Location: β€1km from school
β Excluded properties: Fail ANY criterion above
Interpreting Property Matchesβ
High Value Score (>500):
- Below-market pricing for area
- Good size for price point
- Very close to school (family appeal)
- Action: Priority viewing/offer
Medium Score (250-500):
- Fair market value
- Acceptable location
- Consider other factors (condition, neighborhood)
- Action: Compare with similar properties
Low Score (<250):
- May be overpriced
- Far edge of school proximity
- Smaller size for price
- Action: Negotiate or wait for better options
Quality Checksβ
-
Sanity check: View 5-10 random results
- Verify prices are realistic
- Measure school distance manually
- Check property_type matches expectations
-
Outlier detection:
-- Find unusually cheap properties (may be errors or great deals)
"price" / "area_sqm" < 1500 -- Less than $1500/mΒ² -
Map patterns: Results should cluster near schools (if not, check CRS)
Best Practicesβ
Search Strategy Refinementβ
Start Broad, Narrow Gradually:
- First pass: Apply only price + size filters
- Review count: If >100 results, add property_type filter
- Add spatial: Apply school proximity
- Fine-tune: Add school rating, bedrooms, etc.
Save Filter History:
- FilterMate automatically saves your expressions
- Use Filter History panel to compare different criteria sets
- Save best performing filters as Favorites
Performance Considerationsβ
Backend Selection Guide:
Properties | Schools | Recommended Backend
-----------|---------|--------------------
< 1,000 | Any | OGR (simplest)
1k - 10k | < 100 | Spatialite
> 10k | Any | PostgreSQL
Any | > 500 | PostgreSQL + spatial index
Optimization Tips:
-
Apply attribute filters first (cheapest):
-- Good: Attributes first, spatial last
"price" BETWEEN 200000 AND 400000 AND distance(...) <= 1000
-- Bad: Spatial first (slower)
distance(...) <= 1000 AND "price" BETWEEN 200000 AND 400000 -
Use spatial index (automatic in PostgreSQL, create manually for Spatialite):
Layer Properties β Create Spatial Index -
Simplify school geometry if complex:
Vector β Geometry β Centroids (schools β points)
Real Estate Best Practicesβ
Market Analysis:
- Run this filter weekly to track new listings
- Compare value_score trends over time
- Export results with timestamps for historical analysis
Price Adjustment:
-- Adjust for inflation or market changes
"price" * 1.05 BETWEEN 200000 AND 400000 -- +5% market growth
Seasonal Patterns:
-- School proximity more valuable in spring (family moving season)
-- Adjust weight in value_score calculation
Common Issuesβ
Issue 1: No results or very few resultsβ
Cause: Criteria too strict or data quality issues
Solutions:
1. Relax price range: 150k-500k instead of 200k-400k
2. Reduce minimum area: 120mΒ² instead of 150mΒ²
3. Increase school distance: 2000m instead of 1000m
4. Check for NULL values in attributes
5. Verify schools layer covers same area as properties
Issue 2: Distance calculation returns errorsβ
Cause: CRS mismatch or layer not found
Solution:
1. Verify schools layer name matches exactly (case-sensitive)
2. Check both layers use same CRS (reproject if needed)
3. Ensure schools layer is in current project
4. Try simpler aggregate approach:
distance(
$geometry,
aggregate('schools', 'collect', $geometry)
) <= 1000
Issue 3: Performance slow (>30 seconds)β
Cause: Large dataset or complex spatial query
Solutions:
1. Switch to PostgreSQL backend (major speedup)
2. Create spatial index on both layers
3. Pre-filter properties to smaller region:
"city" = 'Boston' AND [rest of expression]
4. Reduce school query complexity:
- Use buffer once: overlay_within('schools', buffer($geometry, 1000))
- Cache in temporary field
Issue 4: Results not near schools visuallyβ
Cause: CRS using degrees instead of meters
Solution:
1. Check layer CRS: Properties β Information
2. If EPSG:4326 (lat/lon), reproject to local UTM:
Vector β Data Management β Reproject Layer
3. Update distance from 1000 to 0.01 if using degrees (not recommended)
Next Stepsβ
Related Workflowsβ
- Urban Planning Transit: Similar proximity analysis
- Emergency Services: Inverse distance queries
- Transportation Planning: Export and CRS handling
Advanced Techniquesβ
1. Multi-Amenity Scoring (schools + parks + shopping):
-- Properties near multiple amenities
array_length(overlay_within('schools', buffer($geometry, 1000))) > 0
AND array_length(overlay_within('parks', buffer($geometry, 500))) > 0
AND array_length(overlay_within('shops', buffer($geometry, 800))) > 0
2. Appreciation Potential (combine demographics):
-- Areas with improving demographics
"median_income_2023" > "median_income_2020" * 1.1 -- 10% income growth
AND distance(centroid, aggregate('new_developments', 'collect', $geometry)) < 2000
3. Commute Time Analysis (requires road network):
Processing β Network Analysis β Service Area
Origin: Properties
Destination: Employment centers
Time limit: 30 minutes
4. Market Comparison (price per sqm by neighborhood):
-- Find properties below neighborhood average
"price" / "area_sqm" <
aggregate(
layer:='all_properties',
aggregate:='avg',
expression:="price"/"area_sqm",
filter:="neighborhood" = attribute(@parent, 'neighborhood')
) * 0.9 -- 10% below average
5. Time-Series Tracking (monitor listing duration):
-- Properties on market >30 days (motivated sellers)
"days_on_market" > 30
AND "price_reduced" = 1
Further Learningβ
- π Spatial Predicates Reference
- π Filtering Basics
- π Filter History & Favorites
- π Field Calculator Deep Dive
Summaryβ
β You've learned:
- Combining attribute and spatial filters
- Distance calculations to nearest features
- Creating value scores from multiple criteria
- Exporting filtered results for analysis
- Managing filter history for different searches
β Key techniques:
BETWEENoperator for range filteringdistance()function for proximityoverlay_within()for spatial relationships- Field calculator for derived attributes
- Multi-backend comparison
π― Real-world impact: This workflow helps real estate professionals make data-driven decisions, investors identify opportunities quickly, and home buyers find properties matching complex criteria that would take days to research manually.
π‘ Pro tip: Save multiple filter variations as Favorites with descriptive names like "Investment: Family Homes Near Schools" or "Budget: Starter Homes Transit Access" to instantly recreate searches.