Skip to content

openavmkit.value_studies

Assessment-quality studies on any value field, written to disk.

The assessment_quality notebook runs ratio, vertical-equity and horizontal-equity studies on a value field (typically the assessor's current market value) one model group at a time and displays the results. This module runs the same studies for every model group plus the whole jurisdiction, for one or more value fields, and writes the results as JSON so that reports and other front ends can use them without re-running anything.

No modeling is required: the studies only need sales, model groups and the value field(s). Horizontal equity additionally needs the equity clusters marked in the clean stage (he_id); without them it is skipped.

Sales are scored on raw sale_price within the ratio-study window (analysis.ratio_study.look_back_years before the valuation date), matching the formal ratio study.

run_value_field_studies

run_value_field_studies(sup, settings, fields, *, location=None, start_date=None, end_date=None, confidence_interval=0.9, max_trim=0.05, outpath='out/studies', sale_field='sale_price', trim_outliers=False, iqr_factor=1.5, trim_share=0.1, median_ci='bootstrap', standards=None, classes=None, region=None)

Run ratio and equity studies on value fields for every model group.

Parameters:

Name Type Description Default
sup SalesUniversePair

Sales and universe, after the clean stage (or at least after model groups are tagged).

required
settings dict

Settings dictionary.

required
fields list[str]

Universe value fields to study, e.g. ["assr_market_value"].

required
location str

Location field for the grouped vertical-equity quantiles. Defaults to the first entry of field_classification.important.locations present in the data.

None
start_date date - like

Sales window. Defaults to the ratio-study look-back window ending at the valuation date.

None
end_date date - like

Sales window. Defaults to the ratio-study look-back window ending at the valuation date.

None
confidence_interval float

Confidence level for bootstrap intervals. Defaults to 0.90.

0.9
max_trim float

Maximum share of ratios trimmed for the trimmed statistics. Defaults to 0.05.

0.05
outpath str or None

Folder to write <field>.json into (and, with trim_outliers, <field>.sales.parquet: each sale studied, its ratio and whether it was trimmed). None writes nothing.

'out/studies'
sale_field str

The price the values are compared with: sale_price (as sold, the formal ratio study) or e.g. sale_price_time_adj.

'sale_price'
trim_outliers bool

Also study each model group without its outlying ratios (outside iqr_factor × the interquartile range, at most 10% of the group, the furthest out first; none in a group of fewer than 10): trimmed_study beside the study of every sale (as OpenAVMKit's ratio study shows trimmed and untrimmed side by side), with the trimmed sales listed under trimmed. Default False.

False
iqr_factor float

1.5 (outliers, the IAAO's usual) or 3.0 (extreme outliers only).

1.5
trim_share float

The most of a group trimmed (default 0.10; the exposure draft's Appendix C.5: no more than 10% under 30 sales, 20% in the most extreme cases; 5 to 10% for larger samples).

0.1
median_ci str

"bootstrap" (default) or "iaao": the order-statistic interval of the Standard's Appendix D.2, as the VEI uses.

'bootstrap'
standards dict

Targets to judge each study against (IAAO_STANDARDS's keys); adds verdict. by_class (optional): class -> its own level, prb, prb_unacceptable, vei and min_sales, in place of the ones for every class, so each standard (a state's, an office's) can set all its own targets.

None
classes dict

Model group -> class (residential, income, vacant, any name standards has, or None: not judged), for the COD target (and the class's own targets, under by_class).

None
region str

A location field (a neighborhood, say): adds regions, the median ratio by the price level of the region each sale is in (openratiostudy.com's "neighborhood price level" statistics; not an IAAO test). It tells real vertical inequity (low-priced areas over-assessed) from invalid sales at low prices in dear areas.

None

Returns:

Type Description
dict

{field: {"window": ..., "groups": {model_group: {...}}, "overall": {...}}}.

Source code in openavmkit/value_studies.py
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
def run_value_field_studies(
    sup: SalesUniversePair,
    settings: dict,
    fields: list[str],
    *,
    location: Optional[str] = None,
    start_date=None,
    end_date=None,
    confidence_interval: float = 0.90,
    max_trim: float = 0.05,
    outpath: Optional[str] = "out/studies",
    sale_field: str = "sale_price",
    trim_outliers: bool = False,
    iqr_factor: float = 1.5,
    trim_share: float = 0.10,
    median_ci: str = "bootstrap",
    standards: Optional[dict] = None,
    classes: Optional[dict] = None,
    region: Optional[str] = None,
) -> dict:
    """Run ratio and equity studies on value fields for every model group.

    Parameters
    ----------
    sup : SalesUniversePair
        Sales and universe, after the clean stage (or at least after model groups
        are tagged).
    settings : dict
        Settings dictionary.
    fields : list[str]
        Universe value fields to study, e.g. ``["assr_market_value"]``.
    location : str, optional
        Location field for the grouped vertical-equity quantiles. Defaults to the
        first entry of ``field_classification.important.locations`` present in the data.
    start_date, end_date : date-like, optional
        Sales window. Defaults to the ratio-study look-back window ending at the
        valuation date.
    confidence_interval : float, optional
        Confidence level for bootstrap intervals. Defaults to 0.90.
    max_trim : float, optional
        Maximum share of ratios trimmed for the trimmed statistics. Defaults to 0.05.
    outpath : str or None, optional
        Folder to write ``<field>.json`` into (and, with ``trim_outliers``, ``<field>.sales.parquet``: each sale
        studied, its ratio and whether it was trimmed). ``None`` writes nothing.
    sale_field : str, optional
        The price the values are compared with: ``sale_price`` (as sold, the formal ratio study) or e.g.
        ``sale_price_time_adj``.
    trim_outliers : bool, optional
        Also study each model group without its outlying ratios (outside ``iqr_factor`` × the interquartile range,
        at most 10% of the group, the furthest out first; none in a group of fewer than 10): ``trimmed_study`` beside
        the study of every sale (as OpenAVMKit's ratio study shows trimmed and untrimmed side by side), with the
        trimmed sales listed under ``trimmed``. Default False.
    iqr_factor : float, optional
        1.5 (outliers, the IAAO's usual) or 3.0 (extreme outliers only).
    trim_share : float, optional
        The most of a group trimmed (default 0.10; the exposure draft's Appendix C.5: no more than 10% under 30
        sales, 20% in the most extreme cases; 5 to 10% for larger samples).
    median_ci : str, optional
        ``"bootstrap"`` (default) or ``"iaao"``: the order-statistic interval of the Standard's Appendix D.2, as
        the VEI uses.
    standards : dict, optional
        Targets to judge each study against (``IAAO_STANDARDS``'s keys); adds ``verdict``. ``by_class`` (optional):
        class -> its own ``level``, ``prb``, ``prb_unacceptable``, ``vei`` and ``min_sales``, in place of the ones
        for every class, so each standard (a state's, an office's) can set all its own targets.
    classes : dict, optional
        Model group -> class (``residential``, ``income``, ``vacant``, any name ``standards`` has, or None: not
        judged), for the COD target (and the class's own targets, under ``by_class``).
    region : str, optional
        A location field (a neighborhood, say): adds ``regions``, the median ratio by the price level of the region
        each sale is in (openratiostudy.com's "neighborhood price level" statistics; not an IAAO test). It tells
        real vertical inequity (low-priced areas over-assessed) from invalid sales at low prices in dear areas.

    Returns
    -------
    dict
        ``{field: {"window": ..., "groups": {model_group: {...}}, "overall": {...}}}``.
    """
    if start_date is None or end_date is None:
        start_date, end_date = get_look_back_dates(settings)
    start_date, end_date = pd.Timestamp(start_date), pd.Timestamp(end_date)

    df_sales = get_hydrated_sales_from_sup(sup)
    if sale_field != "sale_price":
        df_sales = df_sales.drop(columns=["sale_price"], errors="ignore").rename(columns={sale_field: "sale_price"})
    sale_dates = pd.to_datetime(df_sales["sale_date"], errors="coerce")
    df_sales = df_sales[sale_dates.ge(start_date) & sale_dates.le(end_date)]
    valid_field = "valid_for_ratio_study" if "valid_for_ratio_study" in df_sales else "valid_sale"
    if valid_field in df_sales:
        df_sales = df_sales[df_sales[valid_field].eq(True)]
    df_univ = sup.universe

    if location is None:
        locations = settings.get("field_classification", {}).get("important", {}).get("locations", [])
        location = next((loc for loc in locations if loc in df_sales), None)

    model_groups = [mg for mg in get_model_group_ids(settings, df_univ) if mg in set(df_univ["model_group"])]

    results = {}
    for field in fields:
        if field not in df_univ:
            raise ValueError(f"Value field '{field}' not found in the universe.")
        groups = {}
        opts = dict(confidence_interval=confidence_interval, max_trim=max_trim, median_ci=median_ci, region=region)
        usable = _usable(df_sales, field)
        trimmed = pd.Series(False, index=usable.index)
        if trim_outliers:
            for mg in model_groups:
                mine = usable["model_group"].eq(mg)
                trimmed[mine] = _trimmed(usable[mine], field, iqr_factor, trim_share)
        for mg in model_groups:
            mine = usable["model_group"].eq(mg)
            univ = df_univ[df_univ["model_group"].eq(mg)]
            # Every sale (as OpenAVMKit's ratio study reports it), and with trimming, the same study without the
            # outlying ratios beside it.
            groups[mg] = _study_one(usable[mine], univ, field, location, **opts)
            if trim_outliers:
                groups[mg]["trimmed"] = _sale_rows(usable[mine & trimmed], field)
                groups[mg]["trimmed_study"] = _study_one(usable[mine & ~trimmed], univ, field, location, **opts)
        overall = _study_one(usable, df_univ, field, location, **opts)
        if trim_outliers:
            overall["count_trimmed"] = int(trimmed.sum())
            overall["trimmed_study"] = _study_one(usable[~trimmed], df_univ, field, location, **opts)
        if standards is not None:
            for mg, g in groups.items():
                g["verdict"] = _verdict(g, standards, (classes or {}).get(mg))
                if "trimmed_study" in g:
                    t = g["trimmed_study"]
                    t["verdict"] = _verdict(t, standards, (classes or {}).get(mg))
        results[field] = {
            "field": field,
            "window": {"start": start_date.date().isoformat(), "end": end_date.date().isoformat()},
            "sale_field": sale_field,
            "confidence_interval": confidence_interval,
            "median_ci": median_ci,
            "trim_outliers": trim_outliers,
            "location": location,
            "groups": groups,
            "overall": overall,
        }
        if outpath is not None:
            os.makedirs(outpath, exist_ok=True)
            with open(os.path.join(outpath, f"{field}.json"), "w") as f:
                json.dump(results[field], f, indent=2, default=_json_default)
            if trim_outliers:
                table = _sale_table(usable, field, region)
                table["trimmed"] = trimmed.to_numpy()
                table.to_parquet(os.path.join(outpath, f"{field}.sales.parquet"), index=False)
    return results