예제 #1
0
    def adjust_with_indexing(self, params_or_path, **kwargs):
        """
        Custom adjust method that handles special indexing logic. The logic
        is:

        1. If "parameter_indexing_CPI_offset" is adjusted, first set
        parameter_indexing_CPI_offset to zero before implementing the
        adjusted parameter_indexing_CPI_offset to avoid stacking adjustments.
        Then, revert all values of indexed parameters to the 'known' values:
            a. The current values of parameters that are being adjusted are
                deleted after the first year in which
                parameter_indexing_CPI_offset is adjusted.
            b. The current values of parameters that are not being adjusted
                (i.e. are not in params) are deleted after the last known year,
                with the exception of parameters that revert to their pre-TCJA
                values in 2026. Instead, these (2026) parameter values are
                recalculated using the new inflation rates.
            After the 'unknown' values have been deleted, the last known value
            is extrapolated through the budget window. If there are indexed
            parameters in the adjustment, they will be included in the final
            adjustment call (unless their indexed status is changed).
        2. If the "indexed" status is updated for any parameter:
            a. If a parameter has values that are being adjusted before
                the indexed status is adjusted, update those parameters first.
            b. Extend the values of that parameter to the year in which
                the status is changed.
            c. Change the indexed status for the parameter.
            d. Update parameter values in adjustment that are adjusted after
                the year in which the indexed status changes.
            e. Using the new "-indexed" status, extend the values of that
                parameter through the remaining years or until the -indexed
                status changes again.
        3. Update all parameters that are not indexing related, i.e. they are
            not "parameter_indexing_CPI_offset" or do not end with "-indexed".
        4. Return parsed adjustment with all adjustments, including "-indexed"
            parameters.

        Notable side-effects:
            - All values of a parameter whose indexed status is adjusted are
              wiped out after the year in which the value is adjusted for the
              same hard-coding reason.
        """
        # Temporarily turn off extra ops during the intermediary adjustments
        # so that expensive and unnecessary operations are not run.
        label_to_extend = self.label_to_extend
        array_first = self.array_first
        self.array_first = False
        self._gfactors = GrowFactors()

        params = self.read_params(params_or_path)

        # Check if parameter_indexing_CPI_offset is adjusted. If so, reset
        # values of all indexed parameters after year where
        # parameter_indexing_CPI_offset is changed. If
        # parameter_indexing_CPI_offset is changed multiple times, then
        # reset values after the first year in which the
        # parameter_indexing_CPI_offset is changed.
        needs_reset = []
        if params.get("parameter_indexing_CPI_offset") is not None:
            # Update parameter_indexing_CPI_offset with new value.
            cpi_adj = super().adjust(
                {
                    "parameter_indexing_CPI_offset":
                    params["parameter_indexing_CPI_offset"]
                }, **kwargs)
            # turn off extend now that parameter_indexing_CPI_offset
            # has been updated.
            self.label_to_extend = None
            # Get first year in which parameter_indexing_CPI_offset
            # is changed.
            cpi_min_year = min(cpi_adj["parameter_indexing_CPI_offset"],
                               key=lambda vo: vo["year"])

            rate_adjustment_vals = self.select_gte(
                "parameter_indexing_CPI_offset", year=cpi_min_year["year"])
            # "Undo" any existing parameter_indexing_CPI_offset for
            # years after parameter_indexing_CPI_offset has
            # been updated.
            self._inflation_rates = self._inflation_rates[:cpi_min_year[
                "year"] - self.start_year] + self._gfactors.price_inflation_rates(
                    cpi_min_year["year"], self.LAST_BUDGET_YEAR)

            # Then apply new parameter_indexing_CPI_offset values to
            # inflation rates
            for cpi_vo in rate_adjustment_vals:
                self._inflation_rates[cpi_vo["year"] -
                                      self.start_year] += cpi_vo["value"]
            # 1. Delete all unknown values.
            # 1.a For revision, these are years specified after cpi_min_year.
            init_vals = {}
            to_delete = {}
            for param in params:
                if (param == "parameter_indexing_CPI_offset"
                        or param in self._wage_indexed):
                    continue
                if param.endswith("-indexed"):
                    param = param.split("-indexed")[0]
                if self._data[param].get("indexed", False):
                    init_vals[param] = pt.select_lte(
                        self._init_values[param],
                        True,
                        {"year": cpi_min_year["year"]},
                    )
                    to_delete[param] = self.select_gt(
                        param, year=cpi_min_year["year"])
                    needs_reset.append(param)
            self.delete(to_delete, **kwargs)
            super().adjust(init_vals, **kwargs)

            # 1.b For all others, these are years after last_known_year.
            last_known_year = max(cpi_min_year["year"], self._last_known_year)
            # calculate 2026 value, using new inflation rates, for parameters
            # that revert to their pre-TCJA values.
            long_params = [
                'II_brk7', 'II_brk6', 'II_brk5', 'II_brk4', 'II_brk3',
                'II_brk2', 'II_brk1', 'PT_brk7', 'PT_brk6', 'PT_brk5',
                'PT_brk4', 'PT_brk3', 'PT_brk2', 'PT_brk1',
                'PT_qbid_taxinc_thd', 'ALD_BusinessLosses_c', 'STD', 'II_em',
                'II_em_ps', 'AMT_em', 'AMT_em_ps', 'AMT_em_pe', 'ID_ps',
                'ID_AllTaxes_c'
            ]
            final_ifactor = 1.0
            pyear = 2017  # prior year before TCJA first implemented
            fyear = 2026  # final year in which parameter values revert to
            # pre-TCJA values
            # construct final-year inflation factor from prior year
            # NOTE: pvalue[t+1] = pvalue[t] * ( 1 + irate[t] )
            for year in range(pyear, fyear):
                final_ifactor *= 1 + \
                    self._inflation_rates[year - self.start_year]

            long_param_vals = defaultdict(list)
            # compute final year parameter value
            for param in long_params:
                # only revert param in 2026 if it's not in revision
                if params.get(param) is None:
                    # grab param values from 2017
                    vos = self.select_eq(param, year=pyear)
                    # use final_ifactor to inflate from 2017 to 2026
                    for vo in vos:
                        long_param_vals[param].append(
                            # Create new dict to avoid modifying the original
                            dict(
                                vo,
                                value=min(
                                    9e99, round(vo["value"] * final_ifactor,
                                                0)),
                                year=fyear,
                            ))
                    needs_reset.append(param)
            super().adjust(long_param_vals, **kwargs)

            init_vals = {}
            to_delete = {}
            for param in self._data:
                if (param in params or param == "parameter_indexing_CPI_offset"
                        or param in self._wage_indexed):
                    continue
                if self._data[param].get("indexed", False):
                    init_vals[param] = pt.select_lte(self._init_values[param],
                                                     True,
                                                     {"year": last_known_year})
                    to_delete[param] = self.select_eq(param,
                                                      strict=True,
                                                      _auto=True)
                    needs_reset.append(param)

            self.delete(to_delete, **kwargs)
            super().adjust(init_vals, **kwargs)

            self.extend(label="year")

        # 2. Handle -indexed parameters.
        self.label_to_extend = None
        index_affected = set([])
        for param, values in params.items():
            if param.endswith("-indexed"):
                base_param = param.split("-indexed")[0]
                if not self._data[base_param].get("indexable", None):
                    msg = f"Parameter {base_param} is not indexable."
                    raise pt.ValidationError({"errors": {
                        base_param: msg
                    }},
                                             labels=None)
                index_affected |= {param, base_param}
                indexed_changes = {}
                if isinstance(values, bool):
                    indexed_changes[self.start_year] = values
                elif isinstance(values, list):
                    for vo in values:
                        indexed_changes[vo.get("year",
                                               self.start_year)] = vo["value"]
                else:
                    msg = ("Index adjustment parameter must be a boolean or "
                           "list.")
                    raise pt.ValidationError({"errors": {
                        base_param: msg
                    }},
                                             labels=None)
                # 2.a Adjust values less than first year in which index status
                # was changed.
                if base_param in params:
                    min_index_change_year = min(indexed_changes.keys())
                    vos = pt.select_lt(
                        params[base_param],
                        False,
                        {"year": min_index_change_year},
                    )
                    if vos:
                        min_adj_year = min(vos,
                                           key=lambda vo: vo["year"])["year"]
                        self.delete({
                            base_param:
                            self.select_gt(base_param, year=min_adj_year)
                        })
                        super().adjust({base_param: vos}, **kwargs)
                        self.extend(
                            params=[base_param],
                            label="year",
                            label_values=list(
                                range(self.start_year, min_index_change_year)),
                        )

                for year in sorted(indexed_changes):
                    indexed_val = indexed_changes[year]
                    # Get and delete all default values after year where
                    # indexed status changed.
                    self.delete(
                        {base_param: self.select_gt(base_param, year=year)})

                    # 2.b Extend values for this parameter to the year where
                    # the indexed status changes.
                    if year > self.start_year:
                        self.extend(
                            params=[base_param],
                            label="year",
                            label_values=list(range(self.start_year,
                                                    year + 1)),
                        )

                    # 2.c Set indexed status.
                    self._data[base_param]["indexed"] = indexed_val

                    # 2.d Adjust with values greater than or equal to current
                    # year in params
                    if base_param in params:
                        vos = pt.select_gte(params[base_param], False,
                                            {"year": year})
                        super().adjust({base_param: vos}, **kwargs)

                    # 2.e Extend values through remaining years.
                    self.extend(params=[base_param], label="year")

                needs_reset.append(base_param)
        # Re-instate ops.
        self.label_to_extend = label_to_extend
        self.array_first = array_first

        # Filter out "-indexed" params.
        nonindexed_params = {
            param: val
            for param, val in params.items() if param not in index_affected
        }

        needs_reset = set(needs_reset) - set(nonindexed_params.keys())
        if needs_reset:
            self._set_state(params=needs_reset)

        # 3. Do adjustment for all non-indexing related parameters.
        adj = super().adjust(nonindexed_params, **kwargs)

        # 4. Add indexing params back for return to user.
        adj.update({
            param: val
            for param, val in params.items() if param in index_affected
        })
        return adj
예제 #2
0
    def _update(self, revision, print_warnings, raise_errors):
        """
        A translation layer on top of Parameters.adjust. Projects
        that have historically used the `_update` method with
        Tax-Calculator styled adjustments can continue to do so
        without making any changes to how they handle adjustments.

        Converts reforms that are compatible with Tax-Calculator:

        adjustment = {
            "standard_deduction": {2024: [10000.0, 10000.0]},
            "ss_rate": {2024: 0.2}
        }

        into reforms that are compatible with ParamTools:

        {
            'standard_deduction': [
                {'year': 2024, 'marital_status': 'single', 'value': 10000.0},
                {'year': 2024, 'marital_status': 'joint', 'value': 10000.0}
            ],
            'ss_rate': [{'year': 2024, 'value': 0.2}]}
        }

        """
        if not isinstance(revision, dict):
            raise pt.ValidationError(
                {"errors": {
                    "schema": "Revision must be a dictionary."
                }}, None)
        new_params = defaultdict(list)
        for param, val in revision.items():
            if not isinstance(param, str):
                msg = f"Parameter {param} is not a string."
                raise pt.ValidationError({"errors": {"schema": msg}}, None)
            if (param not in self._data
                    and param.split("-indexed")[0] not in self._data):
                if self._removed_params and param in self._removed_params:
                    msg = f"{param} {self._removed_params[param]}"
                elif (self._redefined_params
                      and param in self._redefined_params):
                    msg = self._redefined_params[param]
                else:
                    msg = f"Parameter {param} does not exist."
                raise pt.ValidationError({"errors": {"schema": msg}}, None)
            if param.endswith("-indexed"):
                for year, yearval in val.items():
                    new_params[param] += [{"year": year, "value": yearval}]
            elif isinstance(val, dict):
                for year, yearval in val.items():
                    val = getattr(self, param)
                    if (self._data[param].get("type", None) == "str"
                            and isinstance(yearval, str)):
                        new_params[param] += [{"value": yearval}]
                        continue

                    yearval = np.array(yearval)
                    if (getattr(val, "shape", None)
                            and yearval.shape != val[0].shape):
                        exp_dims = val[0].shape
                        if exp_dims == tuple():
                            msg = (f"{param} is not an array " f"parameter.")
                        elif yearval.shape:
                            msg = (f"{param} has {yearval.shape[0]} elements "
                                   f"but should only have {exp_dims[0]} "
                                   f"elements.")
                        else:
                            msg = (f"{param} is an array parameter with "
                                   f"{exp_dims[0]} elements.")
                        raise pt.ValidationError({"errors": {
                            "schema": msg
                        }}, None)

                    value_objects = self.from_array(param,
                                                    yearval.reshape(
                                                        (1, *yearval.shape)),
                                                    year=year)
                    new_params[param] += value_objects
            else:
                msg = (f"{param} must be a year:value dictionary "
                       f"if you are not using the new adjust method.")
                raise pt.ValidationError({"errors": {"schema": msg}}, None)
        return self.adjust(new_params,
                           print_warnings=print_warnings,
                           raise_errors=raise_errors)
예제 #3
0
    def adjust(self, params_or_path, **kwargs):
        """
        Custom adjust method that handles special indexing logic. The logic
        is:

        1. If "CPI_offset" is adjusted, revert all values of indexed parameters
            to the 'known' values:
            a. The current values of parameters that are being adjusted are
                deleted after the first year in which CPI_offset is adjusted.
            b. The current values of parameters that are not being adjusted
                (i.e. are not in params) are deleted after the last known year.
            After the 'unknown' values have been deleted, the last known value
            is extrapolated through the budget window. If there are indexed
            parameters in the adjustment, they will be included in the final
            adjustment call (unless their indexed status is changed).
        2. If the "indexed" status is updated for any parameter:
            a. If a parameter has values that are being adjusted before
                the indexed status is adjusted, update those parameters first.
            b. Extend the values of that parameter to the year in which
                the status is changed.
            c. Change the the indexed status for the parameter.
            d. Update parameter values in adjustment that are adjusted after
                the year in which the indexed status changes.
            e. Using the new "-indexed" status, extend the values of that
                parameter through the remaining years or until the -indexed
                status changes again.
        3. Update all parameters that are not indexing related, i.e. they are
            not "CPI_offset" or do not end with "-indexed".
        4. Return parsed adjustment with all adjustments, including "-indexed"
            parameters.

        Notable side-effects:
            - All values of indexed parameters, including default values, are
                wiped out after the first year in which the "CPI_offset" is
                changed. This is only necessary because Tax-Calculator
                hard-codes inflated values. If Tax-Calculator only hard-coded
                values that were changed for non-inflation related reasons,
                then this would not be necessary for default values.
            - All values of a parameter whose indexed status is adjusted are
              wiped out after the year in which the value is adjusted for the
              same hard-coding reason.
        """
        min_year = min(self._stateless_label_grid["year"])

        # Temporarily turn off extra ops during the intermediary adjustments
        # so that expensive and unnecessary operations are not run.
        label_to_extend = self.label_to_extend
        array_first = self.array_first
        self.array_first = False

        params = self.read_params(params_or_path)

        # Check if CPI_offset is adjusted. If so, reset values of all indexed
        # parameters after year where CPI_offset is changed. If CPI_offset is
        # changed multiple times, then reset values after the first year in
        # which the CPI_offset is changed.
        needs_reset = []
        if params.get("CPI_offset") is not None:
            # Update CPI_offset with new value.
            cpi_adj = super().adjust({"CPI_offset": params["CPI_offset"]},
                                     **kwargs)
            # turn off extend now that CPI_offset has been updated.
            self.label_to_extend = None
            # Get first year in which CPI_offset is changed.
            cpi_min_year = min(cpi_adj["CPI_offset"],
                               key=lambda vo: vo["year"])
            # Apply new CPI_offset values to inflation rates
            rate_adjustment_vals = self.select_gt("CPI_offset",
                                                  True,
                                                  year=cpi_min_year["year"] -
                                                  1)
            for cpi_vo in rate_adjustment_vals:
                self._inflation_rates[cpi_vo["year"] - self.start_year] += \
                    cpi_vo["value"]
            # 1. delete all unknown values.
            # 1.a for revision these are years specified after cpi_min_year
            to_delete = {}
            for param in params:
                if param == "CPI_offset" or param in self._wage_indexed:
                    continue
                if param.endswith("-indexed"):
                    param = param.split("-indexed")[0]
                if self._data[param].get("indexed", False):
                    gt = self.select_gt(param, True, year=cpi_min_year["year"])
                    to_delete[param] = [
                        dict(vo, **{"value": None}) for vo in gt
                    ]
                    needs_reset.append(param)
            super().adjust(to_delete, **kwargs)

            # 1.b for all others these are years after last_known_year
            to_delete = {}
            last_known_year = max(cpi_min_year["year"], self._last_known_year)
            for param in self._data:
                if (param in params or param == "CPI_offset"
                        or param in self.WAGE_INDEXED_PARAMS):
                    continue
                if self._data[param].get("indexed", False):
                    gt = self.select_gt(param, True, year=last_known_year)
                    to_delete[param] = [
                        dict(vo, **{"value": None}) for vo in gt
                    ]
                    needs_reset.append(param)

            super().adjust(to_delete, **kwargs)

            self.extend(label_to_extend="year")

        # 2. Handle -indexed parameters.
        self.label_to_extend = None
        index_affected = set([])
        for param, values in params.items():
            if param.endswith("-indexed"):
                base_param = param.split("-indexed")[0]
                if not self._data[base_param].get("indexable", None):
                    msg = f"Parameter {base_param} is not indexable."
                    raise pt.ValidationError({"errors": {
                        base_param: msg
                    }},
                                             labels=None)
                index_affected |= {param, base_param}
                indexed_changes = {}
                if isinstance(values, bool):
                    indexed_changes[min_year] = values
                elif isinstance(values, list):
                    for vo in values:
                        indexed_changes[vo.get("year", min_year)] = vo["value"]
                else:
                    raise Exception(
                        "Index adjustment parameter must be a boolean or list."
                    )
                # 2.a Adjust values less than first year in which index status
                # was changed.
                if base_param in params:
                    min_index_change_year = min(indexed_changes.keys())
                    vos = select_lt(params[base_param], False,
                                    {"year": min_index_change_year})
                    if vos:
                        min_adj_year = min(vos,
                                           key=lambda vo: vo["year"])["year"]
                        gt = self.select_gt(base_param,
                                            True,
                                            year=min_adj_year)
                        super().adjust({
                            base_param:
                            [dict(vo, **{"value": None}) for vo in gt]
                        })
                        super().adjust({base_param: vos}, **kwargs)
                        self.extend(
                            params=[base_param],
                            label_to_extend="year",
                            label_to_extend_values=list(
                                range(min_year, min_index_change_year)),
                        )

                for year in sorted(indexed_changes):
                    indexed_val = indexed_changes[year]
                    # Get and delete all default values after year where
                    # indexed status changed.
                    gte = self.select_gt(base_param, True, year=year)
                    super().adjust({
                        base_param:
                        [dict(vo, **{"value": None}) for vo in gte]
                    })

                    # 2.b Extend values for this parameter to the year where
                    # the indexed status changes.
                    if year > min_year:
                        self.extend(
                            params=[base_param],
                            label_to_extend="year",
                            label_to_extend_values=list(
                                range(min_year, year + 1)),
                        )

                    # 2.c Set indexed status.
                    self._data[base_param]["indexed"] = indexed_val

                    # 2.d Adjust with values greater than or equal to current
                    # year in params
                    if base_param in params:
                        vos = pt.select_gt(params[base_param], False,
                                           {"year": year - 1})
                        super().adjust({base_param: vos}, **kwargs)

                    # 2.e Extend values through remaining years.
                    self.extend(params=[base_param], label_to_extend="year")

                needs_reset.append(base_param)
        # Re-instate ops.
        self.label_to_extend = label_to_extend
        self.array_first = array_first

        # Filter out "-indexed" params.
        nonindexed_params = {
            param: val
            for param, val in params.items() if param not in index_affected
        }

        needs_reset = set(needs_reset) - set(nonindexed_params.keys())
        if needs_reset:
            self._set_state(params=needs_reset)

        # 3. Do adjustment for all non-indexing related parameters.
        adj = super().adjust(nonindexed_params, **kwargs)

        # 4. Add indexing params back for return to user.
        adj.update({
            param: val
            for param, val in params.items() if param in index_affected
        })
        return adj