The Problem
Searching for a home is hard, and realtors are expensive. Everyone has preferences and requirements, but consistently applying those preferences is hard. Often consumers will compromise on their needs without full confidence that all they truly exhausted all of their options.
The Solution
Enter Artemis (the name is up for debate, I just need to name the the directory something). The core concept is to explicitly break down all your criteria for a home into a structured “preference tree”. We can then evaluate individual homes against this tree, returning a normalized score to allow like-for-like comparisons between candidates. This enables users to locate homes that meet their precise requirements. The end result should be simple enough for your mom to use, but complex enough for the nerd who built it.
As this project is still a work in progress, I want to start by clearly laying out a workflow with a couple of sample personas desribing my target audience.
Workflow
I open up Artemis and enter my criteria. For now, each criterion is equally weighted, but I will adjust that later.
Now I need to populate candidate locations. One day this will be integrated with listing providers, but not yet. For now, I navigate to Zillow and search for homes in the local area meeting my minimum requirements. I copy-paste each URL into Artemis, which pulls the list price & address, geocodes the address to a latitude/longitude pair, and saves it to my local Artemis database. I annotate each listing with with any additional personal notes, such as “Nice yard” or “Needs remodel”.
Once I’ve added all promising listings, I open the “Analysis” tab in Artemis. This presents a pivot table with each listing, all criteria, and the corresponding scores. There is an additional calculated column price/score, and this is the default sort order for the table, showing the highest value (i.e. lowest cost per unit of score value). To further verify my results, I navigate to the “Visualize” tab, which displays a map with all my saved listings and all associated POI data, with a heatmap corresponding to the total scores. If I click on an individual listing, all associated POI data linked to the criteria are also displayed, not just the closest POI that was included in the score.
Personas
John Doe: family man
John Doe is in the market for a new home. He currently has a job and a family and is looking for an upgrade in the local area. His criteria might look like this:
(
// Point of Interest (POI) proximity
Elementary School < 0.1 mile AND
Park < 0.2 mile AND
Library < 0.5 mile AND
Airport < 20 miles AND
(Whole Foods < 5 miles OR Trader Joes < 5 miles) AND
(Giant < 1 mile OR Safeway < 1 mile OR Harris Teeter < 1 mile) AND
(
Job < 0.5 mile OR
(Job < 5 miles AND Bike Trail < 1 mile) OR
(Job < 10 miles AND Train Station < 0.5 mile)
) AND
// Location characteristics
Walk Score > 50 AND
Weatherspark Comfortable Days > 30% AND
Pollen < 2 AND
Crime Rate < 5% AND
// Building characteristics
Bedrooms >= 4 AND
Bathrooms >= 2 AND
Home Area > 2000 ft^2 AND
Has Garage
)
Alice: social butterfly
Alice is a social butterfly, always looking to meet up with her friends in the neighborhood for dinner, movie, or a concert. Her criteria might look like this:
(
// Point of Interest (POI) proximity
Count of restaurants < 0.5 mile AND
Movie theater < 1 mile AND
(
Stadium < 2 miles OR
(Stadium < 10 miles AND Train Station < 0.25 mile)
)
(Whole Foods < 2.5 miles OR Trader Joes < 2.5 miles) AND
(Giant < 1 mile OR Safeway < 1 mile OR Harris Teeter < 1 mile) AND
(
Job < 0.5 mile OR
(Job < 5 miles AND Train Station < 0.25 mile)
) AND
// Location characteristics
Walk Score > 70 AND
Weatherspark Comfortable Days > 20% AND
Crime Rate < 10% AND
// Building characteristics
Bedrooms >= 2 AND
Bathrooms >= 1 AND
Home Area > 1200 ft^2
)
Data Model
Now that we have the overall vision for the tool laid out, let’s dig into how this would be represented in software.
The core data model for this application is the
criterion. Each criterion is part of a term
node, which is grouped into a tree with a series of group
node references. Groups may also be nested in other groups, allowing
more complex preference sets, as seen in the personas above.
Each term node takes a generic form like this:
[Type] [Category] [Operator] [Amount] [Unit]
Terms and groups are then combined like this:
group AND
term [POI] [Park] [<] [2.5] [Miles]
term [Weatherspark] [Comfortable Days] [>] [100] [Days/Year]
Type + Category is used to determine how to source comparable data points, and each pair must be configured within the analytics engine.
Listings must have a latitude and longitude, which can be derived from an address. But you will also want to annotate additional details, such as the source and asking price. Listings are evaluated by the criteria tree against a dataset of points of interest, along with a evaluation mode. These evaluation modes can take several forms:
Linear distance: Find the single closest matching POI and measure the distance between the POI and the listing. Scores are linear, so a grocery store 1 mile away is half as valuable as one 0.5 miles away. These scores are normalized such that the maximum distance specified by the user results in a score of 0. \[ \begin{gather*} c_k = \max\left(0,\; 1 - \frac{d_k}{d_{max,k}}\right) \\ score = \sum_{k=1}^{n} w_k \cdot \max\left(0,\; 1 - \frac{d_k}{d_{max,k}}\right) \end{gather*} \]
Distance with exponential decay: Find the single closest matching POI and measure the distance between the POI and the listing. Scores are exponential, so a grocery store 1 mile away is ~25% (depending on the
betaselected) as valuable as one 0.5 miles away. This mode needs to be tuned so that an appropriatebetavalue can be used. \[ \begin{gather*} c_k = e^{-\beta_k d_k} \\ score = \sum_{k=1}^{n} w_k \cdot e^{-\beta_k d_k} \end{gather*} \]Count of locations by distance: Count all matching POI within a specified distance. Score is linear, so 3 parks within a mile of a listing is 3 times as valuable as 1 park. \[ \begin{gather*} N_k = \left|\{\, p \in P_k : d(p,\, \text{listing}) \le d_{max,k} \,\}\right| \\ score = \sum_{k=1}^{n} w_k \cdot N_k \end{gather*} \]
Implementation Details
My current iteration on this project is built in F#, with a simple console app that will take in user input at runtime, load data from pre-configured data sources, store everything in a local DuckDB database, and then generate a static HTML file for visualization. Long-term I would like this to be a full web application, but there are additional headaches around hosting and user account management that I don’t want to distract from the core project. If you want to run this yourself feel free to clone the project from GitHub and run the console app yourself.
For the sake of this blog post, I have followed the baked data pattern here, pre-loading the data for a few samples. Below you can see an example of the final analysis & visualization layer, which hopefully conveys the general spirit of the project:
Conclusion
If you made it this far, thank you for your attention. Please feel free to drop a comment on GitHub if you have any thoughts on where this project should go in the future.