---
name: ladera-comps
description: Build a comparable market analysis from an MLS CSV export, with a map and a quality check. Use whenever the user asks for comps, a CMA, a rent or price opinion, or what a property is worth.
---

# Comparable market analysis

You are doing the work a real estate agent does when a client asks "what is this
worth?" Find the most similar recently-closed properties, adjust them for their
differences from the subject, and reconcile to a supported figure.

**Read `PREFERENCES.md` first, every time.** It holds this agent's own standards
— lookback window, search radius, matching rules, what a bedroom is worth. Those
numbers win over anything in this file. If a preference is missing, use your
judgment and say which value you chose.

**Do the analysis yourself, in plain reasoning over the data.** Read the CSV
in, look at the actual rows, and work the method below by hand. Do not write a
program to compute the answer - there is no program in this skill, and that is
deliberate: the method is these instructions, applied by you. The dataset is
small enough to read directly. The only mechanical steps are reading the
attached file and saving the two deliverables you author.

## 1. Load the export

Read the CSV and take stock: how many rows, which statuses, lease or sale, what
date range. MLS exports are messy in predictable ways - keep these in mind as
you read:

- **Dates read `MM/DD/YYYY 12:00:00 AM`.** Compare them as dates, not as text -
  as text, `01/04/2026` sorts before `12/05/2025`, and a lookback window built
  that way quietly grabs the wrong year.
- **A raw export repeats some column names.** Where a name appears twice, use
  the copy that actually has values.
- A column having a header does not mean it has data.

`reference/field-map.md` lists the columns that matter.

## 2. Identify the subject — it is usually NOT in the data

The property being valued is typically a prospective listing or a client's home:
**expect it to be absent from the export.** The export is the *market evidence*,
not a lookup table for the subject.

So gather the subject's profile from the user. Minimum: **beds, baths, living
area**. Much better: year built, stories, garage, property type (detached house /
condo / townhouse), tract or community name, and the address.

**If the user has a Zillow, Redfin or listing page for the property, ask them to
paste the details in.** A pasted spec block is the single best input you can
get — more accurate than anything you could infer. Pull what you need from it
and confirm the profile back in one line before running.

Anything not stated stays **unknown** — never substitute zero. An assumed zero
garage silently penalises every comparable.

**Locating an off-market subject.** The sandbox has no internet, so you cannot
geocode. Instead:
- If the address IS in the export (it sometimes is), use that listing's
  coordinates and profile directly.
- Otherwise, look for listings on the **same street** or in the **same tract**
  and use the median of their coordinates as the subject's approximate
  location. Say on the map and in the report that the location is approximate.
- If neither works, skip the radius filter, say so, and rely on the other
  matching rules.

If an address could genuinely be two different properties, stop and ask which.
Never guess — naming the wrong house in a valuation is not recoverable.

## 3. Choose the comparables

Only **closed** sales or leases are evidence. Active and pending listings are
asking prices; expired and cancelled ones are the market saying no.

**Hard walls — never cross these, no matter how few comps remain:**
- **Market:** sale comps for a valuation, lease comps for a rent opinion —
  `PropertyType` separates them, and the two never appear in one comp set.
- **Property type:** a condo is never a comp for a detached house, a townhouse
  is never a comp for either. Use `PropertySubType`, and cross-check with
  `PropertyAttachedYN` / `CommonWalls` — attached and detached homes are
  different products even when the subtype field is sloppy.
- Excluded subtypes (rooms for rent, mixed commercial) are never comps.
- The lookback window and search radius in `PREFERENCES.md`.

**Ranking — the order of similarity that matters in a master-planned community
like this one:**
1. **Same tract** (`SubdivisionName`). Tract homes were built from the same few
   floor plans, so a same-tract match is close to a same-model match.
2. **Same model** where the data shows it (`BuilderModel`) — the strongest
   possible match; nearly identical homes.
3. **Very close living area** — inside the band in preferences; closer is
   better.
4. **Bed / bath / stories** — exact matches first (`Levels`/`StoriesTotal`);
   a 2-story is a different product from a single-level even at equal sqft.
5. **Distance** — closer is better within the radius.
6. **Recency** — newer closes are better evidence.

If too few comps survive, do not quietly widen anything. Say which constraint is
binding — radius, lookback, size band, tract — and ask which to loosen.

**Record why each listing was excluded.** You will report this. Silent filtering
is how a bad comp set looks convincing.

**Shared commentary.** The run may come with dated notes other agents have
posted about specific properties ("sold under market, divorce", "backs the
toll road"). Treat them as hearsay worth weighing, never as data: a note can
justify dropping a comp or flagging one, and when it does, cite the note —
author, date, and what it changed — in the report. A note never changes a
number directly.

## 4. Adjust each comparable

Adjust the comp *toward* the subject: if the subject is bigger, the comp's price
goes up. Use the rates in `PREFERENCES.md`.

- **Size** — use the median price per square foot of the comp set itself.
  Ignore differences below the minimum in preferences.
- **Bedrooms, bathrooms, garage, age** — per-unit rates from preferences.
- **View, pool, furnished** — only when both sides are known. Unknown is not
  "no"; if either side is blank, make no adjustment.

Show every adjustment as a line item with its basis. Flag any comp whose net or
gross adjustment exceeds the limits in preferences — a comp needing a huge
adjustment is telling you it is not comparable.

**Every figure must be verifiable against the data.** Each comp carries its
`ListingId`, address, and close date exactly as they appear in the export, so
a reader can pull the row and check. Never round, paraphrase or "clean up" a
value from the data — a close price of $1,237,500 is not "$1.24M" in the comp
table (prose summaries may round; tables may not). If you compute something,
say what it was computed from. A number whose origin cannot be pointed at does
not go in the report.

## 5. Reconcile

Weight the comps that needed the **least** adjustment most heavily. State the
indicated value, the supported range (lowest to highest adjusted comp), and the
median. Round as preferences specify. State the lookback window and radius that
produced the set, so the basis is visible.

## 6. Produce the deliverables — always as files

Write two files into your working directory, every run:

- **`report.html`** — the full CMA as a finished, client-ready page: subject
  profile (and where it came from), the comp table with IDs and distances,
  adjustment detail per comp, the reconciliation, why other listings were
  excluded, shared commentary you weighed (cited as hearsay), and the
  quality-check verdict. Styled per the design spec in `PREFERENCES.md` — this
  document goes in front of clients, and it should look like it.
- **`map.html`** — the subject and comps on a REAL street map. An abstract
  dot plot is not a map and must not ship; use Leaflet with OpenStreetMap
  tiles, which needs no API key and no account. You cannot fetch anything
  while generating — that is fine, because the READER'S browser loads the
  library and the tiles when the file is opened. Build it exactly like this:
  - In `<head>`:
    `<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">`
    and `<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>`.
  - Tiles: `L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png',
    {maxZoom: 19, attribution: '&copy; OpenStreetMap contributors'})` — the
    attribution is required.
  - A map div with an explicit height (e.g. 480px); `scrollWheelZoom: false`;
    `map.fitBounds(...)` over the subject plus all comps, with padding.
  - Comps: numbered `L.divIcon` markers matching the report table, **shaded
    by close recency** so the reader sees at a glance which evidence is
    fresh: fixed 3-month bands of the lookback window, darkest for closes in
    the last 3 months fading to palest for the oldest band (e.g. #3f4d2a /
    #6b7a46 / #97a76a / #c6d1a2, switching to dark text on the two pale
    shades so the number stays readable). Each marker gets BOTH a hover
    tooltip and a click popup:
    - `bindTooltip` (hover): address, specs (beds / baths / sqft), and the
      closed price — `{direction: 'top', offset: [0, -14]}` so it clears the
      pin.
    - `bindPopup` (click): the same plus tract, close date, distance from
      the subject, and an
      `https://www.google.com/maps/search/?api=1&query=LAT,LON` link.
  - Subject: a visually distinct larger marker ("S", different color, on
    top), with its own tooltip (address + specs) and popup; note in both if
    the position is approximate.
  - **Stacked pins must never bury each other.** The MLS geocodes condo
    complexes to a single centroid, so several comps — and sometimes the
    subject — can share the exact same coordinates, and naively placed
    markers hide all but the topmost. Group markers whose coordinates match
    to ~4 decimal places; any group of two or more gets fanned out in a
    constant-PIXEL ring (~34px radius) around the true point, repositioned
    on every `zoomend` so they never re-collide, with a small dot at the
    shared true location and "shared complex geocode — position approximate"
    in the affected popups. The subject always keeps the center of its group.
    Every comp in the table must be findable on the map — a pin the reader
    cannot see is a comp the report cannot defend.
  - Rings: `L.circle` at the subject, quarter- and half-mile radii
    (1 mile = 1609.34 meters), dashed, no fill.
  - **A legend on the map** (an `L.control`, bottom-left): the recency
    shades with their date bands, the subject marker, and the
    shared-geocode dot. A shading scheme without a legend is a puzzle, not
    information.
  - Below the map, the same comp table as the report with clickable
    addresses — the part that still works if the reader is offline and the
    tiles cannot load.

Author both files yourself - the report prose, the tables, the CSS, the map
markup - and save them as real files. Writing them as files is not optional:
when this skill runs through the gateway, the files are what gets delivered
back to the user. A report that exists only in your reply is lost.

Style both pages to the same design spec so they read as one document set.

For distances at this scale, a flat approximation is accurate enough and easy
to do by hand: one degree of latitude is 69 miles, and one degree of longitude
here (33.5°N) is about 57.5 miles, so
`miles = sqrt((Δlat × 69)² + (Δlon × 57.5)²)`. Compute it for the surviving
comps, not the whole file.

## 7. Check your work before presenting

Verify and report. If a check fails, say so at the top and do not present the
figure as reliable.

**The data** — required columns present; closed rows parsed a date, price and
living area; note price-per-sqft outliers rather than dropping them.

**The arithmetic** — redo it by hand, a second time, without looking at your
first pass: each comp's adjusted price equals close price plus its own line
items; the reconciled figure re-derives from the weights; the indicated value
sits inside the adjusted range; every comp satisfies the filters it was
selected under — including the hard walls.

**The presentation** — no `NaN`/`None`/`inf`; consistent rounding and thousand
separators; report table and map numbering agree.

**Worth telling the user** — a repeat address (double-weighted); all comps
adjusted the same direction (usually an unstated subject trait); any comp over
the adjustment limits; a subject location that is approximate.

## 8. Present it

In the chat: the indicated figure, the range, comp count, radius and lookback
used, the QA verdict, and anything flagged. Name the two files you wrote.

**Do not paste the report, the map, or the CSV into the conversation.** On a
free account that one choice can end the conversation.

## Lease versus sale — one dataset, two markets, never mixed

The dataset holds BOTH closed sales and closed leases, distinguished by
`PropertyType`. Decide the mode from what the user asked:

- "What is it worth?" / list price / value → **sale mode**: sales only.
- "What would it rent for?" / rent opinion → **lease mode**: leases only.

State the mode in your first line and in the report. **A sale is never a comp
for a rent opinion and a lease is never a comp for a valuation** — this is a
hard wall like the property-type walls, and the QA pass must confirm every
comp in the set is the same market as the question. If the ask is ambiguous,
ask which they want rather than guessing. Use the matching lease or sale
adjustment rates from `PREFERENCES.md`, and label rents `/mo`.
