Skip to content

openavmkit.land

LandSLICEModel

LandSLICEModel(alpha, beta, gam_L, med_size, size_field)

SLICE stands for "Smooth Location w/ Increasing-Concavity Equation."

Source code in openavmkit/land.py
793
794
795
796
797
798
799
800
801
802
803
804
805
def __init__(
    self,
    alpha: float,
    beta: float,
    gam_L: LinearGAM,
    med_size: float,
    size_field: str
):
    self.alpha = alpha
    self.beta = beta
    self.gam_L = gam_L
    self.med_size = med_size
    self.size_field = size_field

fit_land_SLICE_model

fit_land_SLICE_model(df_in, size_field='land_area_sqft', value_field='land_value', verbose=False)

Fits land values using SLICE: "Smooth Location with Increasing-Concavity Equation"

This model takes already-existing raw per-parcel land values and separates the contribution of land size and locational premium. It also enforces three constraints: 1. Locational premium must change smoothly over space 2. Land value in any fixed location must increase monotonically with land size 3. The marginal value of each additional unit of land size must decrease monotonically

The output is an object that encodes the final fitted land values, the locational premiums, and the local land factors. Fitted land values are derived by simply multiplying locational premium times local land factor.

Parameters:

Name Type Description Default
df_in DataFrame

Input data

required
size_field str

The name of your land size field

'land_area_sqft'
value_field str

The name of your land value field

'land_value'
verbose bool

Whether to print verbose output

False
Source code in openavmkit/land.py
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
def fit_land_SLICE_model(
    df_in : pd.DataFrame,
    size_field: str = "land_area_sqft",
    value_field: str = "land_value",
    verbose: bool = False
)->LandSLICEModel:
    """
    Fits land values using SLICE: "Smooth Location with Increasing-Concavity Equation"

    This model takes already-existing raw per-parcel land values and separates the contribution of land size and locational premium.
    It also enforces three constraints: 
    1. Locational premium must change smoothly over space
    2. Land value in any fixed location must increase monotonically with land size
    3. The marginal value of each additional unit of land size must decrease monotonically

    The output is an object that encodes the final fitted land values, the locational premiums, and the local land factors. Fitted land
    values are derived by simply multiplying locational premium times local land factor.

    Parameters
    ----------
    df_in : pd.DataFrame
        Input data
    size_field : str
        The name of your land size field
    value_field : str
        The name of your land value field
    verbose : bool
        Whether to print verbose output
    """


    class Progress(CallBack):
        def on_loop_end(self, diff):
            # self.iter is automatically tracked inside Callback
            print(f"iter {self.iter:>3d}   dev.change={diff:9.3e}")

    if verbose:
        print("Fitting land SLICE model...")


    df = df_in[[value_field, size_field, "latitude", "longitude"]].copy()
    med_land_size = float(np.median(df[size_field]))

    # Y = Size-detrended location factor
    df["Y"] = div_series_z_safe(
        df[value_field],
        np.sqrt(
            df[size_field] / med_land_size
        )
    )

    if verbose:
        print("-->fitting thin-plate spline for location factor...")

    # Fit a thin-plate spline for location factor L(lat, lon)
    basis = te(0, 1, n_splines=40, spline_order=3)
    gam_L : LinearGAM = LinearGAM(
        basis,
        max_iter=40,
        callbacks=[Progress()],
        verbose=verbose
    )
    gam_L.fit(
        df[['latitude', 'longitude']].values,
        np.log(df['Y']).values
    )

    if verbose:
        print("-->estimating initial location factor...")
    # L_hat = Initial estimated location factor (mostly depends on latitude/longitude)
    df['L_hat'] = np.exp(gam_L.predict(df[['latitude', 'longitude']].values))

    # Z = Location-detrended land values (mostly depends on size)
    df["Z"] = df[value_field] / df["L_hat"]

    # Define a power law curve function
    def power_curve(s, alpha, beta):
        return alpha * (s / med_land_size)**beta

    # Solve for location-detrended-land-value and observed size to fit the power law curve
    # - with bounds: alpha>0 (always positive), 0<beta<1 (monotonic-up & concave)
    # - this enforces that land increases in value with size, but with diminishing returns to marginal size
    if verbose:
        print("-->fitting power law curve for size factor...")
    popt, _ = curve_fit(
        f=power_curve,
        p0=[np.median(df["Z"]),0.5],
        xdata=df[size_field].values,
        ydata=df["Z"].values,
        bounds=([0, 1e-6], [np.inf, 0.999])
    )

    # Coefficients for the power law curve:
    alpha_hat, beta_hat = popt

    # Function to call the power law curve with memorized coefficients and a given size
    def F_hat(s):
        return power_curve(np.asarray(s), alpha_hat, beta_hat)

    if verbose:
        print("-->tightening up values with one more iteration...")

    # Tighten up our values with an extra iteration
    df["Y2"] = df[value_field] / F_hat(df[size_field])
    gam_L2 : LinearGAM = gam_L.fit(df[["latitude", "longitude"]], np.log(df["Y2"]))   # refit L

    if verbose:
        print("-->estimating final location factor...")

    # L_hat = Final estimated location factor
    df["L_hat"] = np.exp( gam_L2.predict(df[["latitude", "longitude"]]))

    # could refit L_hat once more here if desired
    return LandSLICEModel(
        alpha_hat,
        beta_hat,
        gam_L2,
        med_land_size,
        size_field
    )