Fix housing market accounting combined main#93
Conversation
jose-moran
left a comment
There was a problem hiding this comment.
This looks good, there's only a couple of changes I had in mind (mostly for readability) , and I also know it may not be possible to avoid the dataframe copy (but I think it's worth trying)
There was a problem hiding this comment.
I think we should do something for clarity, eg introduce
RENTING_INDEX = 3
(or with an Enum)
at the top of the file and then just have
`ind_renting = np.array(household_residence_tenure_status == RENTING_INDEX)```
that way this would not happen again.
| inhabitant_ids = housing_data["Corresponding Inhabitant Household ID"] | ||
| owner_wants_to_move = owner_ids.isin(household_ids_hoping_to_move) | ||
| # Owners can only list homes they can vacate: vacant or owner-occupied. | ||
| property_is_vacant = inhabitant_ids.isna() | inhabitant_ids.eq(-1) |
There was a problem hiding this comment.
same here, I don't even remember the difference between nan index and -1 (I'm guessing this is what is being tested here). I would introduce aliases for this like in the prev comment
| now_up_for_rent = np.where(np.isnan(housing_data["Corresponding Inhabitant Household ID"].values))[0] | ||
| newly_up_for_rent = [ind for ind in now_up_for_rent if ind not in prev_up_for_rent] | ||
| # Rental availability is property-index based; NaN and -1 both mean vacancy. | ||
| prev_up_for_rent = np.flatnonzero(housing_data["Up for Rent"].eq(True).values) |
There was a problem hiding this comment.
mixing parts where you write array == value and array.eq(value) is there a reason? Otherwise I'd prefer consistency
| break | ||
| feasible_sales = feasible_sales.loc[keep_transaction].copy() | ||
|
|
||
| return feasible_sales.reset_index(drop=True) |
There was a problem hiding this comment.
I agree with this fn but I wonder if it's necessary to copy and return a whole dataframe. This can be quite costly computationally. Do you think you can do a workaround where you only return the indices you care about, rather than forcing a copy of the whole df? Otherwise this seems a bit greedy to me
| self.states["current_sales"] = self._filter_feasible_current_sales( | ||
| household_received_mortgages=household_received_mortgages, | ||
| household_financial_wealth=household_financial_wealth, | ||
| ) |
There was a problem hiding this comment.
yeah so here you could maybe just keep track of the index, I'm not sure if possible though
| # Mix of renters (0) and people in social housing (-1) | ||
| household_residence_tenure_status = np.array([-1, 0, 0, 0, -1, 0, 0, -1, 0, 0]) | ||
| # Mix of renters (3) and people in social housing (-1) | ||
| household_residence_tenure_status = np.array([-1, 3, 3, 3, -1, 3, 3, -1, 3, 3]) |
agurgone
left a comment
There was a problem hiding this comment.
1. Households.compute_target_credit() should filter to sales internally
The PR fixes the immediate caller by making Country.prepare_credit_market_clearing() pass only "Sell" rows. That is good, but Households.compute_target_credit() still treats every received row as a house purchase.
So if a future caller passes the full current_sales table, "Rental" rows could again create mortgage demand.
Suggested behavior:
if "sales_types" in current_sales.columns:
current_sales = current_sales.loc[current_sales["sales_types"] == "Sell"]A direct household-level regression would be useful: pass one "Rental" row and one "Sell" row, and check that only the sale buyer gets mortgage demand.
2. The mortgage-stock mismatch is still open
This PR fixes the housing-market loading bug, but it does not fix the separate mismatch between initialized household mortgage debt and the credit-market mortgage book.
The diagnostic showed:
household mortgage_debt[t=0] ~= 825.8B
credit-market mortgage principal[t=0] ~= 534.0B
The likely cause is that household mortgage debt is initialized from:
HMR mortgages + mortgages on other properties
while the credit-market mortgage loan book appears to be built only from:
HMR mortgages
So after the first credit-market step, household mortgage_debt is recomputed from a smaller credit-market book. That is separate from the housing loader fix and should remain a follow-up issue.
Summary
This PR fixes several housing-market accounting and clearing issues that could leave households or properties in inconsistent states after market clearing.
The main goal is to make housing transactions internally consistent: households that buy or rent should end up inhabiting the corresponding property, sold properties should not also be rented in the same clearing, occupied homes should not be sold out from under households that did not move, and housing-market IDs should be preserved during initialization.
What changed
Correct private renter participation in housing demand.
Private renters use tenure status
3, but the demand code was checking the wrong renter status. This could prevent renters from entering the housing-market demand calculation.Fix sale listings for households hoping to move.
The moving-household mask is now converted to household IDs before comparing with property owner IDs. This avoids treating boolean mask positions as owner IDs.
Include
-1vacancies in rental listings.The rental market now treats both
NaNand-1inhabitant IDs as vacant homes.Use completed housing sales for mortgage demand.
Household mortgage demand is based on housing sales, not unrelated transaction rows.
Record observed housing ratios after clearing.
Price/value and rent/value ratios are now updated after transactions have been applied, so they reflect the post-clearing market state.
Preserve housing-market IDs during initialization.
Existing household/property IDs are kept, while only missing IDs are normalized to
-1.Prevent sold homes from also being rented.
Sales clear before rentals, and sold properties are removed from the rental pool before rental matching.
Handle empty automatic housing matches.
The automatic clearer now returns an empty transaction table when there is no demand or no available property, instead of trying to optimize an empty cost matrix.
Prevent infeasible occupied sales.
A sale cannot complete if it would overwrite an inhabitant who has not completed a move in the same clearing.
Preserve occupants during move chains/swaps.
When clearing a buyer’s previous residence, the code only clears it if that buyer is still the recorded inhabitant. This avoids deleting a new occupant in swap-like transaction chains.
Tests
Added regression coverage for:
3generating housing demand;-1vacant homes being listed for rent;tests\test_macromodel\unit\test_agents\test_households\func\test_property_probability.py tests\test_macromodel\unit\test_agents\test_households\func\test_property_performance.py tests\test_macromodel\unit\test_country\test_country.py tests\test_macromodel\unit\test_markets\test_housing_market\test_housing_market.py