From e4dd06e330deef8f4cbafb09e610d16194768ae0 Mon Sep 17 00:00:00 2001 From: Paul Sohn Date: Wed, 29 Mar 2017 17:12:58 -0700 Subject: [PATCH 01/24] PEP8 --- developer/tests/test_sqftproforma.py | 6 ++++-- setup.py | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/developer/tests/test_sqftproforma.py b/developer/tests/test_sqftproforma.py index 6d00758..3a2c9b7 100644 --- a/developer/tests/test_sqftproforma.py +++ b/developer/tests/test_sqftproforma.py @@ -164,8 +164,10 @@ def test_reasonable_feasibility_results(): assert first.total_cost == first.building_cost + df.iloc[0].land_cost # revenue per sqft should be between 200 and 800 per sqft assert 200 < first.building_revenue/first.building_sqft < 800 - assert first.residential_sqft == first.building_sqft * pf.building_efficiency - # because of parcel inefficiency, stories should be greater than far, but not too much more + assert first.residential_sqft == (first.building_sqft + * pf.building_efficiency) + # because of parcel inefficiency, + # stories should be greater than far, but not too much more assert first.max_profit_far < first.stories < first.max_profit_far * 3.0 assert first.non_residential_sqft == 0 assert first.max_profit > 0 diff --git a/setup.py b/setup.py index 472f57e..6ff1456 100644 --- a/setup.py +++ b/setup.py @@ -28,4 +28,4 @@ extras_require={ 'pandana': ['pandana>=0.1'] } -) \ No newline at end of file +) From f4a1d6b528de21be43dcacf5a5701c419ec32e16 Mon Sep 17 00:00:00 2001 From: Paul Sohn Date: Wed, 29 Mar 2017 17:15:29 -0700 Subject: [PATCH 02/24] Add pycodestyle to Travis build --- .travis.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 7d95f9b..4b63b10 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,6 +18,7 @@ install: conda create -q -n test-environment python=$TRAVIS_PYTHON_VERSION pip numpy pandas pytest matplotlib scipy statsmodels - source activate test-environment - conda list +- pip install pycodestyle - pip install https://github.com/UDST/orca/archive/master.zip - pip install https://github.com/pksohn/urbansim/archive/python3.zip - if [[ "$TRAVIS_PYTHON_VERSION" == "2.7" ]]; then pip install pandana; else @@ -26,7 +27,9 @@ install: - pip install . - cd .. && git clone git@github.com:urbansim/urbansim_parcels.git - pip install ./urbansim_parcels +- cd "$TRAVIS_BUILD_DIR" script: -- cd "$TRAVIS_BUILD_DIR" && py.test +- pycodestyle developer +- py.test - cd ../urbansim_parcels/sf_example && python simulate.py \ No newline at end of file From bce0a5fe79120c6a2098249cb0711051ef6049df Mon Sep 17 00:00:00 2001 From: Paul Sohn Date: Mon, 3 Apr 2017 10:02:29 -0700 Subject: [PATCH 03/24] Update osmnet to pip install in Travis build --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 4b63b10..dbc6181 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,8 +21,8 @@ install: - pip install pycodestyle - pip install https://github.com/UDST/orca/archive/master.zip - pip install https://github.com/pksohn/urbansim/archive/python3.zip +- pip install osmnet - if [[ "$TRAVIS_PYTHON_VERSION" == "2.7" ]]; then pip install pandana; else - pip install https://github.com/UDST/osmnet/archive/python3-support.zip && pip install https://github.com/UDST/pandana/archive/python3-support.zip; fi - pip install . - cd .. && git clone git@github.com:urbansim/urbansim_parcels.git From c6c4e15d2323becd24253d0cb89790239a4345f2 Mon Sep 17 00:00:00 2001 From: Paul Sohn Date: Mon, 10 Apr 2017 15:28:44 -0700 Subject: [PATCH 04/24] Progress on vacancy model --- developer/develop.py | 27 ++++++++++++++++++++++++--- developer/sqftproforma.py | 17 ++++++++++++++++- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/developer/develop.py b/developer/develop.py index d68c88c..909c467 100644 --- a/developer/develop.py +++ b/developer/develop.py @@ -175,8 +175,8 @@ def pick(self, profit_to_prob_func=None): DataFrame that is returned from feasibility. """ - if len(self.feasibility) == 0: - # no feasible buildings, might as well bail + if self._exit_check() is True: + # Check for empty DataFrames return # Get DataFrame of potential buildings from SqFtProForma steps @@ -203,6 +203,23 @@ def pick(self, profit_to_prob_func=None): return new_df + def _exit_check(self): + if self.forms is None: + if len(self.feasibility) == 0 or self.feasibility.size == 0: + # no feasible buildings, might as well bail + return True + elif isinstance(self.forms, list): + for form in self.forms: + df = self.feasibility[form] + if 'max_profit' not in df.columns.tolist(): + return True + else: + df = self.feasibility[self.forms] + if len(df) == 0 or df.size == 0: + # no feasible buildings, might as well bail + return True + return False + def _get_dataframe_of_buildings(self): """ Helper method to pick(). Returns a DataFrame of buildings from @@ -381,7 +398,11 @@ def _buildings_to_build(self, df, p): """ - if df.net_units.sum() < self.target_units: + if self.target_units is None: + print("BUILDING ALL WITH PROFIT > $100K") + profitable = df.loc[df.max_profit > 100000.0] + build_idx = profitable.index.values + elif df.net_units.sum() < self.target_units: print("WARNING THERE WERE NOT ENOUGH PROFITABLE UNITS TO", "MATCH DEMAND") build_idx = df.index.values diff --git a/developer/sqftproforma.py b/developer/sqftproforma.py index 469b1b4..accb74e 100644 --- a/developer/sqftproforma.py +++ b/developer/sqftproforma.py @@ -840,6 +840,11 @@ def lookup(self, form, df): This is required if max_dua is passed above, otherwise it is optional. This is the same as the parameter to Developer.pick() (it should be the same series). + occ : dataframe, optional + A set of columns, one for each of the uses passed in the + configuration. Values are proportion of new development that would + be expected to be occupied. Same names as rent columns, with "occ_" + prefix. Typical names would be "occ_residential", "occ_retail", etc Returns ------- @@ -958,6 +963,15 @@ def _lookup_parking_cfg(self, form, parking_config, df): # weighted rent for this form df['weighted_rent'] = np.dot(df[self.uses], self.forms[form]) + # weighted occupancy for this form + occupancies = ['occ_{}'.format(use) for use in self.uses] + if set(occupancies).issubset(set(df.columns.tolist())): + df['weighted_occupancy'] = np.dot( + df[occupancies], + self.forms[form]) + else: + df['weighted_occupancy'] = 1.0 + # min between max_fars and max_heights df['max_far_from_heights'] = (df.max_height / self.height_per_story * @@ -1003,7 +1017,8 @@ def _lookup_parking_cfg(self, form, parking_config, df): building_revenue = (building_bulks * (1 - parking_sqft_ratio) * self.building_efficiency * - df.weighted_rent.values / + df.weighted_rent.values * + df.weighted_occupancy.values / self.cap_rate) # profit for each form From 70ac93ba5e7fae44e36fdeda99c85807ffbbeabf Mon Sep 17 00:00:00 2001 From: Paul Sohn Date: Mon, 10 Apr 2017 18:02:03 -0700 Subject: [PATCH 05/24] Fix pandana installation in CI, add SD simulation --- .travis.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index dbc6181..96a3a0f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,9 +21,7 @@ install: - pip install pycodestyle - pip install https://github.com/UDST/orca/archive/master.zip - pip install https://github.com/pksohn/urbansim/archive/python3.zip -- pip install osmnet -- if [[ "$TRAVIS_PYTHON_VERSION" == "2.7" ]]; then pip install pandana; else - pip install https://github.com/UDST/pandana/archive/python3-support.zip; fi +- pip install osmnet pandana - pip install . - cd .. && git clone git@github.com:urbansim/urbansim_parcels.git - pip install ./urbansim_parcels @@ -32,4 +30,5 @@ install: script: - pycodestyle developer - py.test -- cd ../urbansim_parcels/sf_example && python simulate.py \ No newline at end of file +- cd ../urbansim_parcels/sf_example && python simulate.py +- cd ../sd_example && python Simulation.py \ No newline at end of file From e9f81c5bd926112f1eb09fb62273d7ec9108c38a Mon Sep 17 00:00:00 2001 From: Paul Sohn Date: Tue, 11 Apr 2017 10:02:29 -0700 Subject: [PATCH 06/24] Minor formatting updates --- developer/sqftproforma.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/developer/sqftproforma.py b/developer/sqftproforma.py index accb74e..57c7dbf 100644 --- a/developer/sqftproforma.py +++ b/developer/sqftproforma.py @@ -1014,12 +1014,12 @@ def _lookup_parking_cfg(self, form, parking_config, df): total_costs = building_costs + df.land_cost.values # rent to make for the new building - building_revenue = (building_bulks * - (1 - parking_sqft_ratio) * - self.building_efficiency * - df.weighted_rent.values * - df.weighted_occupancy.values / - self.cap_rate) + building_revenue = (building_bulks + * (1 - parking_sqft_ratio) + * self.building_efficiency + * df.weighted_rent.values + * df.weighted_occupancy.values + / self.cap_rate) # profit for each form profit = building_revenue - total_costs From 33ed59901c0f5c40eb4aa2fff8349c19a3e05310 Mon Sep 17 00:00:00 2001 From: Paul Sohn Date: Tue, 11 Apr 2017 17:42:16 -0700 Subject: [PATCH 07/24] Change profit cutoff to profit per square foot --- developer/develop.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/developer/develop.py b/developer/develop.py index 909c467..c95d03d 100644 --- a/developer/develop.py +++ b/developer/develop.py @@ -399,8 +399,8 @@ def _buildings_to_build(self, df, p): """ if self.target_units is None: - print("BUILDING ALL WITH PROFIT > $100K") - profitable = df.loc[df.max_profit > 100000.0] + print("BUILDING ALL WITH PROFIT > $20 / sqft") + profitable = df.loc[df.max_profit_per_size > 20.0] build_idx = profitable.index.values elif df.net_units.sum() < self.target_units: print("WARNING THERE WERE NOT ENOUGH PROFITABLE UNITS TO", From 9846bee20c84b03b353ed4db658e676edf31054a Mon Sep 17 00:00:00 2001 From: Paul Sohn Date: Wed, 12 Apr 2017 12:27:15 -0700 Subject: [PATCH 08/24] Expose profit floor parameter in pick method --- developer/develop.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/developer/develop.py b/developer/develop.py index c95d03d..0e42152 100644 --- a/developer/develop.py +++ b/developer/develop.py @@ -153,7 +153,7 @@ def to_yaml(self, str_or_buffer=None): logger.debug('serializing Developer model to YAML') return utils.convert_to_yaml(self.to_dict, str_or_buffer) - def pick(self, profit_to_prob_func=None): + def pick(self, profit_to_prob_func=None, min_profit_per_sqft=None): """ Choose the buildings from the list that are feasible to build in order to match the specified demand. @@ -193,7 +193,7 @@ def pick(self, profit_to_prob_func=None): # Generate development probabilities and pick buildings to build p, df = self._calculate_probabilities(df, profit_to_prob_func) - build_idx = self._buildings_to_build(df, p) + build_idx = self._buildings_to_build(df, p, min_profit_per_sqft) # Drop built buildings from self.feasibility attribute if desired self._drop_built_buildings(build_idx) @@ -379,7 +379,7 @@ def _calculate_probabilities(df, profit_to_prob_func): p = df.max_profit_per_size.values / df.max_profit_per_size.sum() return p, df - def _buildings_to_build(self, df, p): + def _buildings_to_build(self, df, p, min_profit_per_sqft=None): """ Helper method to pick(). Selects buildings to build based on development probabilities. @@ -390,6 +390,8 @@ def _buildings_to_build(self, df, p): DataFrame of buildings from _calculate_probabilities method p : Series Probabilities from _calculate_probabilities method + min_profit_per_sqft : numeric + Minimum profit per sqft required to build, if target units is None Returns ------- @@ -399,8 +401,9 @@ def _buildings_to_build(self, df, p): """ if self.target_units is None: - print("BUILDING ALL WITH PROFIT > $20 / sqft") - profitable = df.loc[df.max_profit_per_size > 20.0] + print("BUILDING ALL BUILDINGS WITH PROFIT > ${:.2f} / sqft" + .format(min_profit_per_sqft)) + profitable = df.loc[df.max_profit_per_size > min_profit_per_sqft] build_idx = profitable.index.values elif df.net_units.sum() < self.target_units: print("WARNING THERE WERE NOT ENOUGH PROFITABLE UNITS TO", From 26ad430645976743ea143a6101fb8c72680db6ff Mon Sep 17 00:00:00 2001 From: Paul Sohn Date: Wed, 12 Apr 2017 14:50:47 -0700 Subject: [PATCH 09/24] Update docstrings and refactor reference dataframe method --- developer/sqftproforma.py | 138 +++++++++++++++++++++++++++++--------- 1 file changed, 105 insertions(+), 33 deletions(-) diff --git a/developer/sqftproforma.py b/developer/sqftproforma.py index 57c7dbf..0b9d599 100644 --- a/developer/sqftproforma.py +++ b/developer/sqftproforma.py @@ -644,43 +644,64 @@ def _generate_reference(self): self.reference_dict = df_d def _reference_dataframe(self, name, uses_distrib, parking_config): + """ + This generates a reference DataFrame for each form and parking + configuration, which provides development information for various + floor-to-area ratios. - # going to make a dataframe to store values to make - # pro forma results transparent - df = pd.DataFrame(index=self.fars) + Parameters + ---------- + name : str + Name of form + uses_distrib : ndarray + The distribution of uses in this form + parking_config : str + Name of parking configuration - df['far'] = self.fars - df['pclsz'] = self.tiled_parcel_sizes + Returns + ------- + df : DataFrame + """ + + df = pd.DataFrame(index=self.fars) + # Array of square footage values for each FAR building_bulk = self._building_bulk(uses_distrib, parking_config) - df['building_sqft'] = building_bulk - parkingstalls = (building_bulk * - np.sum(uses_distrib * self.parking_rates) / - self.sqft_per_rate) - df['spaces'] = parkingstalls + # Array of parking stalls required for each FAR + parking_stalls = (building_bulk + * np.sum(uses_distrib * self.parking_rates) + / self.sqft_per_rate) + + # Array of stories built at each FAR + stories = self._stories(parking_config, building_bulk, parking_stalls) + + # Square feet of parking required for this configuration (constant) + park_sqft = self._park_sqft(parking_config, parking_stalls) + + # Array of total parking cost required for each FAR + park_cost = (self.parking_cost_d[parking_config] + * parking_stalls + * self.parking_sqft_d[parking_config]) - df['park_sqft'] = self._park_sqft(parking_config, parkingstalls) - stories = self._stories(parking_config, building_bulk, parkingstalls) + # Array of building cost per square foot for each FAR + building_cost_per_sqft = self._building_cost(uses_distrib, stories) + df['far'] = self.fars + df['pclsz'] = self.tiled_parcel_sizes + df['building_sqft'] = building_bulk + df['spaces'] = parking_stalls + df['park_sqft'] = park_sqft df['total_built_sqft'] = df.building_sqft + df.park_sqft df['parking_sqft_ratio'] = df.park_sqft / df.total_built_sqft - - stories /= self.parcel_coverage df['stories'] = np.ceil(stories) - df['height'] = df.stories * self.height_per_story - df['build_cost_sqft'] = self._building_cost(uses_distrib, stories) + df['build_cost_sqft'] = building_cost_per_sqft df['build_cost'] = df.build_cost_sqft * df.building_sqft - - df['park_cost'] = (self.parking_cost_d[parking_config] * - parkingstalls * - self.parking_sqft_d[parking_config]) - + df['park_cost'] = park_cost df['cost'] = df.build_cost + df.park_cost - df['ave_cost_sqft'] = ( - (df.cost / df.total_built_sqft) * - self.profit_factor) + df['ave_cost_sqft'] = ((df.cost / df.total_built_sqft) + * self.profit_factor) if name == 'retail': df['ave_cost_sqft'][ @@ -752,31 +773,67 @@ def _building_bulk(self, uses_distrib, parking_config): return building_bulk - def _park_sqft(self, parking_config, parkingstalls): + def _park_sqft(self, parking_config, parking_stalls): + """ + Generate building square footage required for a parking configuration + + Parameters + ---------- + parking_config : str + Name of parking configuration + parking_stalls : numeric + Number of parking stalls required + + Returns + ------- + park_sqft : numeric + """ if parking_config in ['underground', 'deck']: - return parkingstalls * self.parking_sqft_d[parking_config] + return parking_stalls * self.parking_sqft_d[parking_config] if parking_config == 'surface': return 0 - def _stories(self, parking_config, building_bulk, parkingstalls): + def _stories(self, parking_config, building_bulk, parking_stalls): + """ + Calculates number of stories built at various FARs, given + building bulk, number of parking stalls, and parking configuration + + Parameters + ---------- + parking_config : str + Name of parking configuration + building_bulk : ndarray + Array of total square footage values for each FAR + parking_stalls : ndarray + Number of parking stalls required for each FAR + + Returns + ------- + stories : ndarray + + """ if parking_config == 'underground': stories = building_bulk / self.tiled_parcel_sizes if parking_config == 'deck': - stories = ((building_bulk + parkingstalls * - self.parking_sqft_d[parking_config]) / - self.tiled_parcel_sizes) + stories = ((building_bulk + + parking_stalls + * self.parking_sqft_d[parking_config]) + / self.tiled_parcel_sizes) if parking_config == 'surface': - stories = (building_bulk / - (self.tiled_parcel_sizes - parkingstalls * - self.parking_sqft_d[parking_config])) + stories = (building_bulk + / (self.tiled_parcel_sizes + - parking_stalls + * self.parking_sqft_d[parking_config])) # not all fars support surface parking stories[stories < 0.0] = np.nan # I think we can assume that stories over 3 # do not work with surface parking stories[stories > 5.0] = np.nan + stories /= self.parcel_coverage + return stories @@ -949,6 +1006,21 @@ def _max_profit_parking(df): return result def _lookup_parking_cfg(self, form, parking_config, df): + """ + This is the core square foot pro forma calculation. For each form and + parking configuration, generate DataFrame with profitability + information + + Parameters + ---------- + form : str + parking_config : str + df : DataFrame + + Returns + ------- + outdf : DataFrame + """ dev_info = self.reference_dict[(form, parking_config)] From 4358462ce57f74c5f3a7df6e30a260f02a0e2a32 Mon Sep 17 00:00:00 2001 From: Paul Sohn Date: Wed, 12 Apr 2017 16:43:25 -0700 Subject: [PATCH 10/24] Partial refactoring of core lookup --- developer/sqftproforma.py | 61 +++++++++++++++++++++++---------------- 1 file changed, 36 insertions(+), 25 deletions(-) diff --git a/developer/sqftproforma.py b/developer/sqftproforma.py index 0b9d599..9567d8f 100644 --- a/developer/sqftproforma.py +++ b/developer/sqftproforma.py @@ -1014,28 +1014,35 @@ def _lookup_parking_cfg(self, form, parking_config, df): Parameters ---------- form : str + Name of form parking_config : str + Name of parking configuration df : DataFrame + DataFrame of developable sites/parcels passed to pick() method Returns ------- outdf : DataFrame """ + # don't really mean to edit the df that's passed in + df = df.copy() + # Reference table for this form and parking configuration dev_info = self.reference_dict[(form, parking_config)] + # Helper values cost_sqft_col = columnize(dev_info.ave_cost_sqft.values) cost_sqft_index_col = columnize(dev_info.index.values) parking_sqft_ratio = columnize(dev_info.parking_sqft_ratio.values) heights = columnize(dev_info.height.values) + resratio = self.res_ratios[form] + nonresratio = 1.0 - resratio - # don't really mean to edit the df that's passed in - df = df.copy() - - # weighted rent for this form + # ADD COLUMNS df['weighted_rent'] = np.dot(df[self.uses], self.forms[form]) # weighted occupancy for this form + # TODO expose df to modifier functions here occupancies = ['occ_{}'.format(use) for use in self.uses] if set(occupancies).issubset(set(df.columns.tolist())): df['weighted_occupancy'] = np.dot( @@ -1044,38 +1051,25 @@ def _lookup_parking_cfg(self, form, parking_config, df): else: df['weighted_occupancy'] = 1.0 - # min between max_fars and max_heights - df['max_far_from_heights'] = (df.max_height / - self.height_per_story * - self.parcel_coverage) - - resratio = self.res_ratios[form] - nonresratio = 1.0 - resratio - - # now also minimize with max_dua from zoning - since this pro forma is - # really geared toward per sqft metrics, this is a bit tricky. dua - # is converted to floorspace and everything just works (floor space - # will get covered back to units in developer.pick() but we need to - # test the profitability of the floorspace allowed by max_dua here. + # ZONING FILTERS + # Minimize between max_fars and max_heights + df['max_far_from_heights'] = (df.max_height + / self.height_per_story + * self.parcel_coverage) df['min_max_fars'] = self._min_max_fars(df, resratio) if self.only_built: df = df.query('min_max_fars > 0 and parcel_size > 0') - fars = np.repeat(cost_sqft_index_col, len(df.index), axis=1) - - # turn fars into nans which are not allowed by zoning + # turn fars and heights into nans which are not allowed by zoning # (so we can fillna with one of the other zoning constraints) + fars = np.repeat(cost_sqft_index_col, len(df.index), axis=1) fars[fars > df.min_max_fars.values + .01] = np.nan - - # same thing for heights heights = np.repeat(heights, len(df.index), axis=1) - - # turn heights into nans which are not allowed by zoning - # (so we can fillna with one of the other zoning constraints) fars[heights > df.max_height.values + .01] = np.nan + # PROFIT CALCULATION # parcel sizes * possible fars building_bulks = fars * df.parcel_size.values @@ -1094,6 +1088,7 @@ def _lookup_parking_cfg(self, form, parking_config, df): / self.cap_rate) # profit for each form + # TODO expose functions here profit = building_revenue - total_costs profit = profit.astype('float') @@ -1134,6 +1129,22 @@ def twod_get(indexes, arr): return outdf def _min_max_fars(self, df, resratio): + """ + In case max_dua is passed in the DataFrame, + now also minimize with max_dua from zoning - since this pro forma is + really geared toward per sqft metrics, this is a bit tricky. dua + is converted to floorspace and everything just works (floor space + will get covered back to units in developer.pick() but we need to + test the profitability of the floorspace allowed by max_dua here. + Parameters + ---------- + df + resratio + + Returns + ------- + + """ if 'max_dua' in df.columns and resratio > 0: # if max_dua is in the data frame, ave_unit_size must also be there From d142cf7d3a421eb449b9b9d50bd76afba6da4650 Mon Sep 17 00:00:00 2001 From: Paul Sohn Date: Thu, 13 Apr 2017 14:16:28 -0700 Subject: [PATCH 11/24] Modify exit behavior in pick() method --- developer/develop.py | 32 +++++++++++--------------------- developer/sqftproforma.py | 1 + 2 files changed, 12 insertions(+), 21 deletions(-) diff --git a/developer/develop.py b/developer/develop.py index 0e42152..f944bab 100644 --- a/developer/develop.py +++ b/developer/develop.py @@ -174,9 +174,12 @@ def pick(self, profit_to_prob_func=None, min_profit_per_sqft=None): DataFrame of buildings to add. These buildings are rows from the DataFrame that is returned from feasibility. """ + df = self.feasibility + empty_df = len(df) == 0 or df.empty + empty_warn = "WARNING THERE ARE NO FEASIBLE BUILDINGS TO CHOOSE FROM" - if self._exit_check() is True: - # Check for empty DataFrames + if empty_df: + print(empty_warn) return # Get DataFrame of potential buildings from SqFtProForma steps @@ -184,8 +187,8 @@ def pick(self, profit_to_prob_func=None, min_profit_per_sqft=None): df = self._remove_infeasible_buildings(df) df = self._calculate_net_units(df) - if len(df) == 0: - print("WARNING THERE ARE NO FEASIBLE BUILDING TO CHOOSE FROM") + if empty_df: + print(empty_warn) return print("Sum of net units that are profitable: {:,}".format( @@ -203,23 +206,6 @@ def pick(self, profit_to_prob_func=None, min_profit_per_sqft=None): return new_df - def _exit_check(self): - if self.forms is None: - if len(self.feasibility) == 0 or self.feasibility.size == 0: - # no feasible buildings, might as well bail - return True - elif isinstance(self.forms, list): - for form in self.forms: - df = self.feasibility[form] - if 'max_profit' not in df.columns.tolist(): - return True - else: - df = self.feasibility[self.forms] - if len(df) == 0 or df.size == 0: - # no feasible buildings, might as well bail - return True - return False - def _get_dataframe_of_buildings(self): """ Helper method to pick(). Returns a DataFrame of buildings from @@ -312,6 +298,8 @@ def _remove_infeasible_buildings(self, df): ------- df : DataFrame """ + if len(df) == 0 or df.empty: + return df df = df[df.max_profit_far > 0] self.ave_unit_size[ @@ -343,6 +331,8 @@ def _calculate_net_units(self, df): ------- df : DataFrame """ + if len(df) == 0 or df.empty: + return df if self.residential: df['net_units'] = df.residential_units - df.current_units diff --git a/developer/sqftproforma.py b/developer/sqftproforma.py index 9567d8f..141db69 100644 --- a/developer/sqftproforma.py +++ b/developer/sqftproforma.py @@ -1136,6 +1136,7 @@ def _min_max_fars(self, df, resratio): is converted to floorspace and everything just works (floor space will get covered back to units in developer.pick() but we need to test the profitability of the floorspace allowed by max_dua here. + Parameters ---------- df From ef4221c92bfc6b5590c91379329ce9d71625a1c4 Mon Sep 17 00:00:00 2001 From: Paul Sohn Date: Thu, 13 Apr 2017 16:59:24 -0700 Subject: [PATCH 12/24] Allow users to inject functions to modify profit calculations in pro forma --- developer/develop.py | 5 +-- developer/sqftproforma.py | 91 +++++++++++++++++++++++++++------------ 2 files changed, 66 insertions(+), 30 deletions(-) diff --git a/developer/develop.py b/developer/develop.py index f944bab..a29ecad 100644 --- a/developer/develop.py +++ b/developer/develop.py @@ -175,10 +175,9 @@ def pick(self, profit_to_prob_func=None, min_profit_per_sqft=None): DataFrame that is returned from feasibility. """ df = self.feasibility - empty_df = len(df) == 0 or df.empty empty_warn = "WARNING THERE ARE NO FEASIBLE BUILDINGS TO CHOOSE FROM" - if empty_df: + if len(df) == 0 or df.empty: print(empty_warn) return @@ -187,7 +186,7 @@ def pick(self, profit_to_prob_func=None, min_profit_per_sqft=None): df = self._remove_infeasible_buildings(df) df = self._calculate_net_units(df) - if empty_df: + if len(df) == 0 or df.empty: print(empty_warn) return diff --git a/developer/sqftproforma.py b/developer/sqftproforma.py index 141db69..d8d259e 100644 --- a/developer/sqftproforma.py +++ b/developer/sqftproforma.py @@ -419,7 +419,8 @@ def to_yaml(self, str_or_buffer=None): logger.debug('serializing SqftProForma model to YAML') return utils.convert_to_yaml(self.to_dict, str_or_buffer) - def lookup(self, form, df): + def lookup(self, form, df, modify_df=None, modify_revenues=None, + modify_costs=None, modify_profits=None): """ This function does the developer model lookups for all the actual input data. @@ -431,6 +432,18 @@ def lookup(self, form, df): df: dataframe Pass in a single data frame which is indexed by parcel_id and has the following columns + modify_df : function + Function to modify lookup DataFrame before profit calculations. + Must have (self, form, df) as parameters. + modify_revenues : function + Function to modify revenue array during profit calculations. + Must have (self, form, df, revenues) as parameters. + modify_costs : function + Function to modify cost array during profit calculations. + Must have (self, form, df, costs) as parameters. + modify_profits : function + Function to modify profit array during profit calculations. + Must have (self, form, df, profits) as parameters. Input Dataframe Columns rent : dataframe @@ -488,7 +501,8 @@ def lookup(self, form, df): """ lookup_object = SqFtProFormaLookup(**self.__dict__) - return lookup_object.lookup(form, df) + return lookup_object.lookup(form, df, modify_df, modify_revenues, + modify_costs, modify_profits) def get_debug_info(self, form, parking_config): """ @@ -858,7 +872,8 @@ def __init__(self, reference_dict, res_ratios, uses, forms, self.pass_through = pass_through self.simple_zoning = simple_zoning - def lookup(self, form, df): + def lookup(self, form, df, modify_df, modify_revenues, + modify_costs, modify_profits): """ This function does the developer model lookups for all the actual input data. @@ -867,10 +882,22 @@ def lookup(self, form, df): ---------- form : string One of the forms specified in the configuration file - df: dataframe + df : DataFrame Pass in a single data frame which is indexed by parcel_id and has the following columns - + modify_df : function + Function to modify lookup DataFrame before profit calculations. + Must have (self, form, df) as parameters. + modify_revenues : function + Function to modify revenue array during profit calculations. + Must have (self, form, df, revenues) as parameters. + modify_costs : function + Function to modify cost array during profit calculations. + Must have (self, form, df, costs) as parameters. + modify_profits : function + Function to modify profit array during profit calculations. + Must have (self, form, df, profits) as parameters. + Input Dataframe Columns rent : dataframe A set of columns, one for each of the uses passed in the @@ -885,7 +912,7 @@ def lookup(self, form, df): A series representing the maximum far allowed by zoning. Buildings will not be built above these fars. max_height : series - A series representing the maxmium height allowed by zoning. + A series representing the maximum height allowed by zoning. Buildings will not be built above these heights. Will pick between the min of the far and height, will ignore on of them if one is nan, but will not build if both are nan. @@ -897,11 +924,6 @@ def lookup(self, form, df): This is required if max_dua is passed above, otherwise it is optional. This is the same as the parameter to Developer.pick() (it should be the same series). - occ : dataframe, optional - A set of columns, one for each of the uses passed in the - configuration. Values are proportion of new development that would - be expected to be occupied. Same names as rent columns, with "occ_" - prefix. Typical names would be "occ_residential", "occ_retail", etc Returns ------- @@ -935,7 +957,9 @@ def lookup(self, form, df): df = self._simple_zoning(form, df) lookup = pd.concat( - self._lookup_parking_cfg(form, parking_config, df) + self._lookup_parking_cfg(form, parking_config, df, modify_df, + modify_revenues, modify_costs, + modify_profits) for parking_config in self.parking_configs) if len(lookup) == 0: @@ -1005,7 +1029,9 @@ def _max_profit_parking(df): return result - def _lookup_parking_cfg(self, form, parking_config, df): + def _lookup_parking_cfg(self, form, parking_config, df, + modify_df, modify_revenues, modify_costs, + modify_profits): """ This is the core square foot pro forma calculation. For each form and parking configuration, generate DataFrame with profitability @@ -1019,6 +1045,18 @@ def _lookup_parking_cfg(self, form, parking_config, df): Name of parking configuration df : DataFrame DataFrame of developable sites/parcels passed to pick() method + modify_df : func + Function to modify lookup DataFrame before profit calculations. + Must have (self, form, df) as parameters. + modify_revenues : func + Function to modify revenue array during profit calculations. + Must have (self, form, df, revenues) as parameters. + modify_costs : func + Function to modify cost array during profit calculations. + Must have (self, form, df, costs) as parameters. + modify_profits : func + Function to modify profit array during profit calculations. + Must have (self, form, df, profits) as parameters. Returns ------- @@ -1037,19 +1075,10 @@ def _lookup_parking_cfg(self, form, parking_config, df): heights = columnize(dev_info.height.values) resratio = self.res_ratios[form] nonresratio = 1.0 - resratio - - # ADD COLUMNS df['weighted_rent'] = np.dot(df[self.uses], self.forms[form]) - # weighted occupancy for this form - # TODO expose df to modifier functions here - occupancies = ['occ_{}'.format(use) for use in self.uses] - if set(occupancies).issubset(set(df.columns.tolist())): - df['weighted_occupancy'] = np.dot( - df[occupancies], - self.forms[form]) - else: - df['weighted_occupancy'] = 1.0 + # Allow for user modification of DataFrame here + df = modify_df(self, form, df) if modify_df else df # ZONING FILTERS # Minimize between max_fars and max_heights @@ -1084,13 +1113,21 @@ def _lookup_parking_cfg(self, form, parking_config, df): * (1 - parking_sqft_ratio) * self.building_efficiency * df.weighted_rent.values - * df.weighted_occupancy.values / self.cap_rate) - # profit for each form - # TODO expose functions here + # profit for each form, including user modification of + # revenues, costs, and/or profits + building_revenue = (modify_revenues(self, form, df, building_revenue) + if modify_revenues else building_revenue) + + total_costs = (modify_costs(self, form, df, total_costs) + if modify_costs else total_costs) + profit = building_revenue - total_costs + profit = (modify_profits(self, form, df, profit) + if modify_profits else profit) + profit = profit.astype('float') profit[np.isnan(profit)] = -np.inf maxprofitind = np.argmax(profit, axis=0) From ba0dcf0c5e7037a81b6299c59990baad9ae7d3bf Mon Sep 17 00:00:00 2001 From: Paul Sohn Date: Thu, 13 Apr 2017 17:40:09 -0700 Subject: [PATCH 13/24] PEP 8 --- developer/sqftproforma.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/developer/sqftproforma.py b/developer/sqftproforma.py index d8d259e..0d6582e 100644 --- a/developer/sqftproforma.py +++ b/developer/sqftproforma.py @@ -897,7 +897,7 @@ def lookup(self, form, df, modify_df, modify_revenues, modify_profits : function Function to modify profit array during profit calculations. Must have (self, form, df, profits) as parameters. - + Input Dataframe Columns rent : dataframe A set of columns, one for each of the uses passed in the @@ -1173,7 +1173,7 @@ def _min_max_fars(self, df, resratio): is converted to floorspace and everything just works (floor space will get covered back to units in developer.pick() but we need to test the profitability of the floorspace allowed by max_dua here. - + Parameters ---------- df From a9f19502e9a99229fd527429301546eca9a21674 Mon Sep 17 00:00:00 2001 From: Paul Sohn Date: Mon, 17 Apr 2017 13:18:33 -0700 Subject: [PATCH 14/24] Move lookup back into main SqftProForma object --- developer/sqftproforma.py | 1037 +++++++++++++++++-------------------- 1 file changed, 466 insertions(+), 571 deletions(-) diff --git a/developer/sqftproforma.py b/developer/sqftproforma.py index 0d6582e..3b5d0ba 100644 --- a/developer/sqftproforma.py +++ b/developer/sqftproforma.py @@ -1,4 +1,5 @@ from __future__ import print_function, division, absolute_import +import inspect import numpy as np import pandas as pd import logging @@ -429,20 +430,20 @@ def lookup(self, form, df, modify_df=None, modify_revenues=None, ---------- form : string One of the forms specified in the configuration file - df: dataframe + df : DataFrame Pass in a single data frame which is indexed by parcel_id and has the following columns modify_df : function Function to modify lookup DataFrame before profit calculations. Must have (self, form, df) as parameters. modify_revenues : function - Function to modify revenue array during profit calculations. + Function to modify revenue ndarray during profit calculations. Must have (self, form, df, revenues) as parameters. modify_costs : function - Function to modify cost array during profit calculations. + Function to modify cost ndarray during profit calculations. Must have (self, form, df, costs) as parameters. modify_profits : function - Function to modify profit array during profit calculations. + Function to modify profit ndarray during profit calculations. Must have (self, form, df, profits) as parameters. Input Dataframe Columns @@ -459,7 +460,7 @@ def lookup(self, form, df, modify_df=None, modify_revenues=None, A series representing the maximum far allowed by zoning. Buildings will not be built above these fars. max_height : series - A series representing the maxmium height allowed by zoning. + A series representing the maximum height allowed by zoning. Buildings will not be built above these heights. Will pick between the min of the far and height, will ignore on of them if one is nan, but will not build if both are nan. @@ -500,720 +501,614 @@ def lookup(self, form, df, modify_df=None, modify_revenues=None, max_far and max_height from the input dataframe). """ - lookup_object = SqFtProFormaLookup(**self.__dict__) - return lookup_object.lookup(form, df, modify_df, modify_revenues, - modify_costs, modify_profits) + if self.simple_zoning: + df = self._simple_zoning(form, df) - def get_debug_info(self, form, parking_config): - """ - Get the debug info after running the pro forma for a given form and - parking configuration + lookup = pd.concat( + self._lookup_parking_cfg(form, parking_config, df, modify_df, + modify_revenues, modify_costs, + modify_profits) + for parking_config in self.parking_configs) - Parameters - ---------- - form : string - The form to get debug info for - parking_config : string - The parking configuration to get debug info for + if len(lookup) == 0: + return pd.DataFrame() - Returns - ------- - debug_info : dataframe - A dataframe where the index is the far with many columns - representing intermediate steps in the pro forma computation. - Additional documentation will be added at a later date, although - many of the columns should be fairly self-expanatory. + result = self._max_profit_parking(lookup) - """ - return self.reference_dict[(form, parking_config)] + if self.residential_to_yearly and "residential" in self.pass_through: + result["residential"] /= self.cap_rate - def get_ave_cost_sqft(self, form, parking_config): + return result + + @staticmethod + def _simple_zoning(form, df): """ - Get the average cost per sqft for the pro forma for a given form + Replaces max_height and either max_far or max_dua with NaNs + Parameters ---------- - form : string - Get a series representing the average cost per sqft for each form - in the config - parking_config : string - The parking configuration to get debug info for + form : str + Name of form passed to lookup method + df : DataFrame + DataFrame passed to lookup method + Returns ------- - cost : series - A series where the index is the far and the values are the average - cost per sqft at which the building is "break even" given the - configuration parameters that were passed at run time. - """ - return self.reference_dict[(form, parking_config)].ave_cost_sqft - - def _debug_output(self): - """ - this code creates the debugging plots to understand - the behavior of the hypothetical building model - + df : DataFrame """ - import matplotlib - matplotlib.use('Agg') - import matplotlib.pyplot as plt - - df_d = self.reference_dict - keys = list(df_d.keys()) - keys.sort() - for key in keys: - logger.debug("\n" + str(key) + "\n") - logger.debug(df_d[key]) - for form in self.forms: - logger.debug("\n" + str(key) + "\n") - logger.debug(self.get_ave_cost_sqft(form, "surface")) - - keys = list(self.forms.keys()) - keys.sort() - cnt = 1 - share = None - fig = plt.figure(figsize=(12, 3 * len(keys))) - fig.suptitle('Profitable rents by use', fontsize=40) - for name in keys: - sumdf = None - for parking_config in self.parking_configs: - df = df_d[(name, parking_config)] - if sumdf is None: - sumdf = pd.DataFrame(df['far']) - sumdf[parking_config] = df['ave_cost_sqft'] - far = sumdf['far'] - del sumdf['far'] - - if share is None: - share = plt.subplot(len(keys) / 2, 2, cnt) - else: - plt.subplot(len(keys) / 2, 2, cnt, sharex=share, - sharey=share) - - handles = plt.plot(far, sumdf) - plt.ylabel('even_rent') - plt.xlabel('FAR') - plt.title('Rents for use type %s' % name) - plt.legend( - handles, self.parking_configs, loc='lower right', - title='Parking type') - cnt += 1 - plt.savefig('even_rents.png', bbox_inches=0) - - -class SqFtProFormaReference(object): - """ - Generate reference table for square foot pro forma analysis - """ - - def __init__(self, parcel_sizes, fars, forms, - profit_factor, parcel_coverage, parking_rates, sqft_per_rate, - parking_configs, costs, heights_for_costs, parking_sqft_d, - parking_cost_d, height_per_story, max_retail_height, - max_industrial_height, **kwargs): - - self.fars = fars - self.parcel_sizes = parcel_sizes - self.forms = forms - self.profit_factor = profit_factor - self.parcel_coverage = parcel_coverage - self.parking_rates = parking_rates - self.sqft_per_rate = sqft_per_rate - self.parking_configs = parking_configs - self.costs = costs - self.heights_for_costs = heights_for_costs - self.parking_sqft_d = parking_sqft_d - self.parking_cost_d = parking_cost_d - self.height_per_story = height_per_story - self.max_retail_height = max_retail_height - self.max_industrial_height = max_industrial_height - self.tiled_parcel_sizes = columnize( - np.repeat(self.parcel_sizes, self.fars.size)) + if form == "residential": + # these are new computed in the effective max_dua method + df["max_far"] = pd.Series() + df["max_height"] = pd.Series() + else: + # these are new computed in the effective max_far method + df["max_dua"] = pd.Series() + df["max_height"] = pd.Series() - self._generate_reference() + return df - def _generate_reference(self): + @staticmethod + def _max_profit_parking(df): """ - Run the developer model on all possible inputs specified in the - configuration object - not generally called by the user. This part - computes the final cost per sqft of the building to construct and - then turns it into the yearly rent necessary to make break even on - that cost. + Return parcels DataFrame with parking configuration that maximizes + profit + + Parameters + ---------- + df: DataFrame + DataFrame passed to lookup method + Returns + ------- + result : DataFrame """ - # get all the building forms we can use - keys = list(self.forms.keys()) - keys.sort() - df_d = {} - for name in keys: - # get the use distribution for each - uses_distrib = self.forms[name] + max_profit_ind = df.pivot( + columns="parking_config", + values="max_profit").idxmax(axis=1).to_frame("parking_config") - for parking_config in self.parking_configs: + df.set_index(["parking_config"], append=True, inplace=True) + max_profit_ind.set_index(["parking_config"], append=True, + inplace=True) - df = self._reference_dataframe(name, uses_distrib, - parking_config) - df_d[(name, parking_config)] = df + # get the max_profit idx + result = df.loc[max_profit_ind.index].reset_index(1) - self.reference_dict = df_d + return result - def _reference_dataframe(self, name, uses_distrib, parking_config): + def _lookup_parking_cfg(self, form, parking_config, df, + modify_df, modify_revenues, modify_costs, + modify_profits): """ - This generates a reference DataFrame for each form and parking - configuration, which provides development information for various - floor-to-area ratios. + This is the core square foot pro forma calculation. For each form and + parking configuration, generate DataFrame with profitability + information Parameters ---------- - name : str + form : str Name of form - uses_distrib : ndarray - The distribution of uses in this form parking_config : str Name of parking configuration + df : DataFrame + DataFrame of developable sites/parcels passed to pick() method + modify_df : func + Function to modify lookup DataFrame before profit calculations. + Must have (self, form, df) as parameters. + modify_revenues : func + Function to modify revenue ndarray during profit calculations. + Must have (self, form, df, revenues) as parameters. + modify_costs : func + Function to modify cost ndarray during profit calculations. + Must have (self, form, df, costs) as parameters. + modify_profits : func + Function to modify profit ndarray during profit calculations. + Must have (self, form, df, profits) as parameters. Returns ------- - df : DataFrame + outdf : DataFrame """ + # don't really mean to edit the df that's passed in + df = df.copy() - df = pd.DataFrame(index=self.fars) - - # Array of square footage values for each FAR - building_bulk = self._building_bulk(uses_distrib, parking_config) + # Reference table for this form and parking configuration + dev_info = self.reference_dict[(form, parking_config)] - # Array of parking stalls required for each FAR - parking_stalls = (building_bulk - * np.sum(uses_distrib * self.parking_rates) - / self.sqft_per_rate) + # Helper values + cost_sqft_col = columnize(dev_info.ave_cost_sqft.values) + cost_sqft_index_col = columnize(dev_info.index.values) + parking_sqft_ratio = columnize(dev_info.parking_sqft_ratio.values) + heights = columnize(dev_info.height.values) + resratio = self.res_ratios[form] + nonresratio = 1.0 - resratio + df['weighted_rent'] = np.dot(df[self.uses], self.forms[form]) - # Array of stories built at each FAR - stories = self._stories(parking_config, building_bulk, parking_stalls) + # Allow for user modification of DataFrame here + df = modify_df(self, form, df) if modify_df else df - # Square feet of parking required for this configuration (constant) - park_sqft = self._park_sqft(parking_config, parking_stalls) + # ZONING FILTERS + # Minimize between max_fars and max_heights + df['max_far_from_heights'] = (df.max_height + / self.height_per_story + * self.parcel_coverage) - # Array of total parking cost required for each FAR - park_cost = (self.parking_cost_d[parking_config] - * parking_stalls - * self.parking_sqft_d[parking_config]) + df['min_max_fars'] = self._min_max_fars(df, resratio) - # Array of building cost per square foot for each FAR - building_cost_per_sqft = self._building_cost(uses_distrib, stories) + if self.only_built: + df = df.query('min_max_fars > 0 and parcel_size > 0') - df['far'] = self.fars - df['pclsz'] = self.tiled_parcel_sizes - df['building_sqft'] = building_bulk - df['spaces'] = parking_stalls - df['park_sqft'] = park_sqft - df['total_built_sqft'] = df.building_sqft + df.park_sqft - df['parking_sqft_ratio'] = df.park_sqft / df.total_built_sqft - df['stories'] = np.ceil(stories) - df['height'] = df.stories * self.height_per_story - df['build_cost_sqft'] = building_cost_per_sqft - df['build_cost'] = df.build_cost_sqft * df.building_sqft - df['park_cost'] = park_cost - df['cost'] = df.build_cost + df.park_cost - df['ave_cost_sqft'] = ((df.cost / df.total_built_sqft) - * self.profit_factor) - - if name == 'retail': - df['ave_cost_sqft'][ - self.fars > self.max_retail_height] = np.nan - if name == 'industrial': - df['ave_cost_sqft'][ - self.fars > self.max_industrial_height] = np.nan + # turn fars and heights into nans which are not allowed by zoning + # (so we can fillna with one of the other zoning constraints) + fars = np.repeat(cost_sqft_index_col, len(df.index), axis=1) + fars[fars > df.min_max_fars.values + .01] = np.nan + heights = np.repeat(heights, len(df.index), axis=1) + fars[heights > df.max_height.values + .01] = np.nan - return df + # PROFIT CALCULATION + # parcel sizes * possible fars + building_bulks = fars * df.parcel_size.values - def _building_cost(self, use_mix, stories): - """ - Generate building cost for a set of buildings + # cost to build the new building + building_costs = building_bulks * cost_sqft_col - Parameters - ---------- - use_mix : array - The mix of uses for this form - stories : series - A Pandas Series of stories + # add cost to buy the current building + total_costs = building_costs + df.land_cost.values - Returns - ------- - array - The cost per sqft for this unit mix and height. + # rent to make for the new building + building_revenue = (building_bulks + * (1 - parking_sqft_ratio) + * self.building_efficiency + * df.weighted_rent.values + / self.cap_rate) - """ + # profit for each form, including user modification of + # revenues, costs, and/or profits - # stories to heights - heights = stories * self.height_per_story - # cost index for this height - costs = np.searchsorted(self.heights_for_costs, heights) - # this will get set to nan later - costs[np.isnan(heights)] = 0 - # compute cost with matrix multiply - costs = np.dot(np.squeeze(self.costs[costs.astype('int32')]), - use_mix) - # some heights aren't allowed - cost should be nan - costs[np.isnan(stories).flatten()] = np.nan - return costs.flatten() + building_revenue = (modify_revenues(self, form, df, building_revenue) + if modify_revenues else building_revenue) - def _building_bulk(self, uses_distrib, parking_config): - """ - Multiplies parcel sizes by FARs, with adjustment for deck parking. + total_costs = (modify_costs(self, form, df, total_costs) + if modify_costs else total_costs) - Parameters - ---------- - uses_distrib : ndarray - The distribution of uses in this form - parking_config : str - Name of current parking configuration + profit = building_revenue - total_costs - Returns - ------- - building_bulk : ndarray - """ + profit = (modify_profits(self, form, df, profit) + if modify_profits else profit) - building_bulk = (columnize(self.parcel_sizes) * - columnize(self.fars)) - building_bulk = columnize(building_bulk) + profit = profit.astype('float') + profit[np.isnan(profit)] = -np.inf + maxprofitind = np.argmax(profit, axis=0) - # need to converge in on exactly how much far is available for - # deck pkg - if parking_config == 'deck': - building_bulk /= ( - 1.0 + np.sum(uses_distrib * self.parking_rates) * - self.parking_sqft_d[parking_config] / - self.sqft_per_rate) + def twod_get(indexes, arr): + return arr[indexes, np.arange(indexes.size)].astype('float') - return building_bulk + outdf = pd.DataFrame({ + 'building_sqft': twod_get(maxprofitind, building_bulks), + 'building_cost': twod_get(maxprofitind, building_costs), + 'parking_ratio': parking_sqft_ratio[maxprofitind].flatten(), + 'stories': twod_get(maxprofitind, + heights) / self.height_per_story, + 'total_cost': twod_get(maxprofitind, total_costs), + 'building_revenue': twod_get(maxprofitind, building_revenue), + 'max_profit_far': twod_get(maxprofitind, fars), + 'max_profit': twod_get(maxprofitind, profit), + 'parking_config': parking_config + }, index=df.index) - def _park_sqft(self, parking_config, parking_stalls): - """ - Generate building square footage required for a parking configuration + if self.pass_through: + outdf[self.pass_through] = df[self.pass_through] - Parameters - ---------- - parking_config : str - Name of parking configuration - parking_stalls : numeric - Number of parking stalls required + outdf["residential_sqft"] = (outdf.building_sqft * + self.building_efficiency * + resratio) + outdf["non_residential_sqft"] = (outdf.building_sqft * + self.building_efficiency * + nonresratio) - Returns - ------- - park_sqft : numeric - """ + if self.only_built: + outdf = outdf.query('max_profit > 0').copy() + else: + outdf = outdf.loc[outdf.max_profit != -np.inf].copy() - if parking_config in ['underground', 'deck']: - return parking_stalls * self.parking_sqft_d[parking_config] - if parking_config == 'surface': - return 0 + return outdf - def _stories(self, parking_config, building_bulk, parking_stalls): + def _min_max_fars(self, df, resratio): """ - Calculates number of stories built at various FARs, given - building bulk, number of parking stalls, and parking configuration + In case max_dua is passed in the DataFrame, + now also minimize with max_dua from zoning - since this pro forma is + really geared toward per sqft metrics, this is a bit tricky. dua + is converted to floorspace and everything just works (floor space + will get covered back to units in developer.pick() but we need to + test the profitability of the floorspace allowed by max_dua here. Parameters ---------- - parking_config : str - Name of parking configuration - building_bulk : ndarray - Array of total square footage values for each FAR - parking_stalls : ndarray - Number of parking stalls required for each FAR + df + resratio Returns ------- - stories : ndarray """ - if parking_config == 'underground': - stories = building_bulk / self.tiled_parcel_sizes - if parking_config == 'deck': - stories = ((building_bulk - + parking_stalls - * self.parking_sqft_d[parking_config]) - / self.tiled_parcel_sizes) - if parking_config == 'surface': - stories = (building_bulk - / (self.tiled_parcel_sizes - - parking_stalls - * self.parking_sqft_d[parking_config])) - # not all fars support surface parking - stories[stories < 0.0] = np.nan - # I think we can assume that stories over 3 - # do not work with surface parking - stories[stories > 5.0] = np.nan - - stories /= self.parcel_coverage + if 'max_dua' in df.columns and resratio > 0: + # if max_dua is in the data frame, ave_unit_size must also be there + assert 'ave_unit_size' in df.columns - return stories + df['max_far_from_dua'] = ( + # this is the max_dua times the parcel size in acres, which + # gives the number of units that are allowable on the parcel + df.max_dua * (df.parcel_size / 43560) * + # times by the average unit size which gives the square footage + # of those units + df.ave_unit_size / -class SqFtProFormaLookup(object): + # divided by the building efficiency which is a + # factor that indicates that the actual units are not the whole + # FAR of the building + self.building_efficiency / - def __init__(self, reference_dict, res_ratios, uses, forms, - building_efficiency, parcel_coverage, cap_rate, - parking_configs, height_per_story, residential_to_yearly, - only_built, pass_through, simple_zoning, **kwargs): + # divided by the resratio which is a factor that indicates + # that the actual units are not the only use of the building + resratio / - self.reference_dict = reference_dict - self.res_ratios = res_ratios - self.uses = uses - self.forms = forms - self.building_efficiency = building_efficiency - self.parcel_coverage = parcel_coverage - self.cap_rate = cap_rate - self.parking_configs = parking_configs - self.height_per_story = height_per_story - self.residential_to_yearly = residential_to_yearly - self.only_built = only_built - self.pass_through = pass_through - self.simple_zoning = simple_zoning + # divided by the parcel size again in order to get FAR. + # I recognize that parcel_size actually + # cancels here as it should, but the calc was hard to get right + # and it's just so much more transparent to have it in there + # twice + df.parcel_size) + return df[['max_far_from_heights', + 'max_far', 'max_far_from_dua']].min(axis=1) + else: + return df[ + ['max_far_from_heights', 'max_far']].min(axis=1) - def lookup(self, form, df, modify_df, modify_revenues, - modify_costs, modify_profits): + def get_debug_info(self, form, parking_config): """ - This function does the developer model lookups for all the actual input - data. + Get the debug info after running the pro forma for a given form and + parking configuration Parameters ---------- form : string - One of the forms specified in the configuration file - df : DataFrame - Pass in a single data frame which is indexed by parcel_id and has - the following columns - modify_df : function - Function to modify lookup DataFrame before profit calculations. - Must have (self, form, df) as parameters. - modify_revenues : function - Function to modify revenue array during profit calculations. - Must have (self, form, df, revenues) as parameters. - modify_costs : function - Function to modify cost array during profit calculations. - Must have (self, form, df, costs) as parameters. - modify_profits : function - Function to modify profit array during profit calculations. - Must have (self, form, df, profits) as parameters. + The form to get debug info for + parking_config : string + The parking configuration to get debug info for - Input Dataframe Columns - rent : dataframe - A set of columns, one for each of the uses passed in the - configuration. Values are yearly rents for that use. Typical column - names would be "residential", "retail", "industrial" and "office" - land_cost : series - A series representing the CURRENT yearly rent for each parcel. - Used to compute acquisition costs for the parcel. - parcel_size : series - A series representing the parcel size for each parcel. - max_far : series - A series representing the maximum far allowed by zoning. Buildings - will not be built above these fars. - max_height : series - A series representing the maximum height allowed by zoning. - Buildings will not be built above these heights. Will pick between - the min of the far and height, will ignore on of them if one is - nan, but will not build if both are nan. - max_dua : series, optional - A series representing the maximum dwelling units per acre allowed - by zoning. If max_dua is passed, the average unit size should be - passed below to translate from dua to floor space. - ave_unit_size : series, optional - This is required if max_dua is passed above, otherwise it is - optional. This is the same as the parameter to Developer.pick() - (it should be the same series). + Returns + ------- + debug_info : dataframe + A dataframe where the index is the far with many columns + representing intermediate steps in the pro forma computation. + Additional documentation will be added at a later date, although + many of the columns should be fairly self-expanatory. + """ + return self.reference_dict[(form, parking_config)] + + def get_ave_cost_sqft(self, form, parking_config): + """ + Get the average cost per sqft for the pro forma for a given form + Parameters + ---------- + form : string + Get a series representing the average cost per sqft for each form + in the config + parking_config : string + The parking configuration to get debug info for Returns ------- - index : Series, int - parcel identifiers - building_sqft : Series, float - The number of square feet for the building to build. Keep in mind - this includes parking and common space. Will need a helpful - function to convert from gross square feet to actual usable square - feet in residential units. - building_cost : Series, float - The cost of constructing the building as given by the - ave_cost_per_sqft from the cost model (for this FAR) and the number - of square feet. - total_cost : Series, float - The cost of constructing the building plus the cost of acquisition - of the current parcel/building. - building_revenue : Series, float - The NPV of the revenue for the building to be built, which is the - number of square feet times the yearly rent divided by the cap - rate (with a few adjustment factors including building efficiency). - max_profit_far : Series, float - The FAR of the maximum profit building (constrained by the max_far - and max_height from the input dataframe). - max_profit : - The profit for the maximum profit building (constrained by the - max_far and max_height from the input dataframe). + cost : series + A series where the index is the far and the values are the average + cost per sqft at which the building is "break even" given the + configuration parameters that were passed at run time. """ + return self.reference_dict[(form, parking_config)].ave_cost_sqft - if self.simple_zoning: - df = self._simple_zoning(form, df) + def _debug_output(self): + """ + this code creates the debugging plots to understand + the behavior of the hypothetical building model - lookup = pd.concat( - self._lookup_parking_cfg(form, parking_config, df, modify_df, - modify_revenues, modify_costs, - modify_profits) - for parking_config in self.parking_configs) + """ + import matplotlib + matplotlib.use('Agg') + import matplotlib.pyplot as plt - if len(lookup) == 0: - return pd.DataFrame() + df_d = self.reference_dict + keys = list(df_d.keys()) + keys.sort() + for key in keys: + logger.debug("\n" + str(key) + "\n") + logger.debug(df_d[key]) + for form in self.forms: + logger.debug("\n" + str(key) + "\n") + logger.debug(self.get_ave_cost_sqft(form, "surface")) - result = self._max_profit_parking(lookup) + keys = list(self.forms.keys()) + keys.sort() + cnt = 1 + share = None + fig = plt.figure(figsize=(12, 3 * len(keys))) + fig.suptitle('Profitable rents by use', fontsize=40) + for name in keys: + sumdf = None + for parking_config in self.parking_configs: + df = df_d[(name, parking_config)] + if sumdf is None: + sumdf = pd.DataFrame(df['far']) + sumdf[parking_config] = df['ave_cost_sqft'] + far = sumdf['far'] + del sumdf['far'] - if self.residential_to_yearly and "residential" in self.pass_through: - result["residential"] /= self.cap_rate + if share is None: + share = plt.subplot(len(keys) / 2, 2, cnt) + else: + plt.subplot(len(keys) / 2, 2, cnt, sharex=share, + sharey=share) - return result + handles = plt.plot(far, sumdf) + plt.ylabel('even_rent') + plt.xlabel('FAR') + plt.title('Rents for use type %s' % name) + plt.legend( + handles, self.parking_configs, loc='lower right', + title='Parking type') + cnt += 1 + plt.savefig('even_rents.png', bbox_inches=0) - @staticmethod - def _simple_zoning(form, df): - """ - Replaces max_height and either max_far or max_dua with NaNs - Parameters - ---------- - form : str - Name of form passed to lookup method - df : DataFrame - DataFrame passed to lookup method +class SqFtProFormaReference(object): + """ + Generate reference table for square foot pro forma analysis + """ - Returns - ------- - df : DataFrame - """ + def __init__(self, parcel_sizes, fars, forms, + profit_factor, parcel_coverage, parking_rates, sqft_per_rate, + parking_configs, costs, heights_for_costs, parking_sqft_d, + parking_cost_d, height_per_story, max_retail_height, + max_industrial_height, **kwargs): - if form == "residential": - # these are new computed in the effective max_dua method - df["max_far"] = pd.Series() - df["max_height"] = pd.Series() - else: - # these are new computed in the effective max_far method - df["max_dua"] = pd.Series() - df["max_height"] = pd.Series() + self.fars = fars + self.parcel_sizes = parcel_sizes + self.forms = forms + self.profit_factor = profit_factor + self.parcel_coverage = parcel_coverage + self.parking_rates = parking_rates + self.sqft_per_rate = sqft_per_rate + self.parking_configs = parking_configs + self.costs = costs + self.heights_for_costs = heights_for_costs + self.parking_sqft_d = parking_sqft_d + self.parking_cost_d = parking_cost_d + self.height_per_story = height_per_story + self.max_retail_height = max_retail_height + self.max_industrial_height = max_industrial_height - return df + self.tiled_parcel_sizes = columnize( + np.repeat(self.parcel_sizes, self.fars.size)) - @staticmethod - def _max_profit_parking(df): - """ - Return parcels DataFrame with parking configuration that maximizes - profit + self._generate_reference() - Parameters - ---------- - df: DataFrame - DataFrame passed to lookup method + def _generate_reference(self): + """ + Run the developer model on all possible inputs specified in the + configuration object - not generally called by the user. This part + computes the final cost per sqft of the building to construct and + then turns it into the yearly rent necessary to make break even on + that cost. - Returns - ------- - result : DataFrame """ - max_profit_ind = df.pivot( - columns="parking_config", - values="max_profit").idxmax(axis=1).to_frame("parking_config") + # get all the building forms we can use + keys = list(self.forms.keys()) + keys.sort() + df_d = {} + for name in keys: + # get the use distribution for each + uses_distrib = self.forms[name] - df.set_index(["parking_config"], append=True, inplace=True) - max_profit_ind.set_index(["parking_config"], append=True, - inplace=True) + for parking_config in self.parking_configs: - # get the max_profit idx - result = df.loc[max_profit_ind.index].reset_index(1) + df = self._reference_dataframe(name, uses_distrib, + parking_config) + df_d[(name, parking_config)] = df - return result + self.reference_dict = df_d - def _lookup_parking_cfg(self, form, parking_config, df, - modify_df, modify_revenues, modify_costs, - modify_profits): + def _reference_dataframe(self, name, uses_distrib, parking_config): """ - This is the core square foot pro forma calculation. For each form and - parking configuration, generate DataFrame with profitability - information + This generates a reference DataFrame for each form and parking + configuration, which provides development information for various + floor-to-area ratios. Parameters ---------- - form : str + name : str Name of form + uses_distrib : ndarray + The distribution of uses in this form parking_config : str Name of parking configuration - df : DataFrame - DataFrame of developable sites/parcels passed to pick() method - modify_df : func - Function to modify lookup DataFrame before profit calculations. - Must have (self, form, df) as parameters. - modify_revenues : func - Function to modify revenue array during profit calculations. - Must have (self, form, df, revenues) as parameters. - modify_costs : func - Function to modify cost array during profit calculations. - Must have (self, form, df, costs) as parameters. - modify_profits : func - Function to modify profit array during profit calculations. - Must have (self, form, df, profits) as parameters. Returns ------- - outdf : DataFrame + df : DataFrame """ - # don't really mean to edit the df that's passed in - df = df.copy() - # Reference table for this form and parking configuration - dev_info = self.reference_dict[(form, parking_config)] - - # Helper values - cost_sqft_col = columnize(dev_info.ave_cost_sqft.values) - cost_sqft_index_col = columnize(dev_info.index.values) - parking_sqft_ratio = columnize(dev_info.parking_sqft_ratio.values) - heights = columnize(dev_info.height.values) - resratio = self.res_ratios[form] - nonresratio = 1.0 - resratio - df['weighted_rent'] = np.dot(df[self.uses], self.forms[form]) + df = pd.DataFrame(index=self.fars) - # Allow for user modification of DataFrame here - df = modify_df(self, form, df) if modify_df else df + # Array of square footage values for each FAR + building_bulk = self._building_bulk(uses_distrib, parking_config) - # ZONING FILTERS - # Minimize between max_fars and max_heights - df['max_far_from_heights'] = (df.max_height - / self.height_per_story - * self.parcel_coverage) + # Array of parking stalls required for each FAR + parking_stalls = (building_bulk + * np.sum(uses_distrib * self.parking_rates) + / self.sqft_per_rate) - df['min_max_fars'] = self._min_max_fars(df, resratio) + # Array of stories built at each FAR + stories = self._stories(parking_config, building_bulk, parking_stalls) - if self.only_built: - df = df.query('min_max_fars > 0 and parcel_size > 0') + # Square feet of parking required for this configuration (constant) + park_sqft = self._park_sqft(parking_config, parking_stalls) - # turn fars and heights into nans which are not allowed by zoning - # (so we can fillna with one of the other zoning constraints) - fars = np.repeat(cost_sqft_index_col, len(df.index), axis=1) - fars[fars > df.min_max_fars.values + .01] = np.nan - heights = np.repeat(heights, len(df.index), axis=1) - fars[heights > df.max_height.values + .01] = np.nan + # Array of total parking cost required for each FAR + park_cost = (self.parking_cost_d[parking_config] + * parking_stalls + * self.parking_sqft_d[parking_config]) - # PROFIT CALCULATION - # parcel sizes * possible fars - building_bulks = fars * df.parcel_size.values + # Array of building cost per square foot for each FAR + building_cost_per_sqft = self._building_cost(uses_distrib, stories) - # cost to build the new building - building_costs = building_bulks * cost_sqft_col + df['far'] = self.fars + df['pclsz'] = self.tiled_parcel_sizes + df['building_sqft'] = building_bulk + df['spaces'] = parking_stalls + df['park_sqft'] = park_sqft + df['total_built_sqft'] = df.building_sqft + df.park_sqft + df['parking_sqft_ratio'] = df.park_sqft / df.total_built_sqft + df['stories'] = np.ceil(stories) + df['height'] = df.stories * self.height_per_story + df['build_cost_sqft'] = building_cost_per_sqft + df['build_cost'] = df.build_cost_sqft * df.building_sqft + df['park_cost'] = park_cost + df['cost'] = df.build_cost + df.park_cost + df['ave_cost_sqft'] = ((df.cost / df.total_built_sqft) + * self.profit_factor) - # add cost to buy the current building - total_costs = building_costs + df.land_cost.values + if name == 'retail': + df['ave_cost_sqft'][ + self.fars > self.max_retail_height] = np.nan + if name == 'industrial': + df['ave_cost_sqft'][ + self.fars > self.max_industrial_height] = np.nan - # rent to make for the new building - building_revenue = (building_bulks - * (1 - parking_sqft_ratio) - * self.building_efficiency - * df.weighted_rent.values - / self.cap_rate) + return df - # profit for each form, including user modification of - # revenues, costs, and/or profits - building_revenue = (modify_revenues(self, form, df, building_revenue) - if modify_revenues else building_revenue) + def _building_cost(self, use_mix, stories): + """ + Generate building cost for a set of buildings - total_costs = (modify_costs(self, form, df, total_costs) - if modify_costs else total_costs) + Parameters + ---------- + use_mix : array + The mix of uses for this form + stories : series + A Pandas Series of stories - profit = building_revenue - total_costs + Returns + ------- + array + The cost per sqft for this unit mix and height. - profit = (modify_profits(self, form, df, profit) - if modify_profits else profit) + """ - profit = profit.astype('float') - profit[np.isnan(profit)] = -np.inf - maxprofitind = np.argmax(profit, axis=0) + # stories to heights + heights = stories * self.height_per_story + # cost index for this height + costs = np.searchsorted(self.heights_for_costs, heights) + # this will get set to nan later + costs[np.isnan(heights)] = 0 + # compute cost with matrix multiply + costs = np.dot(np.squeeze(self.costs[costs.astype('int32')]), + use_mix) + # some heights aren't allowed - cost should be nan + costs[np.isnan(stories).flatten()] = np.nan + return costs.flatten() - def twod_get(indexes, arr): - return arr[indexes, np.arange(indexes.size)].astype('float') + def _building_bulk(self, uses_distrib, parking_config): + """ + Multiplies parcel sizes by FARs, with adjustment for deck parking. - outdf = pd.DataFrame({ - 'building_sqft': twod_get(maxprofitind, building_bulks), - 'building_cost': twod_get(maxprofitind, building_costs), - 'parking_ratio': parking_sqft_ratio[maxprofitind].flatten(), - 'stories': twod_get(maxprofitind, - heights) / self.height_per_story, - 'total_cost': twod_get(maxprofitind, total_costs), - 'building_revenue': twod_get(maxprofitind, building_revenue), - 'max_profit_far': twod_get(maxprofitind, fars), - 'max_profit': twod_get(maxprofitind, profit), - 'parking_config': parking_config - }, index=df.index) + Parameters + ---------- + uses_distrib : ndarray + The distribution of uses in this form + parking_config : str + Name of current parking configuration - if self.pass_through: - outdf[self.pass_through] = df[self.pass_through] + Returns + ------- + building_bulk : ndarray + """ - outdf["residential_sqft"] = (outdf.building_sqft * - self.building_efficiency * - resratio) - outdf["non_residential_sqft"] = (outdf.building_sqft * - self.building_efficiency * - nonresratio) + building_bulk = (columnize(self.parcel_sizes) * + columnize(self.fars)) + building_bulk = columnize(building_bulk) - if self.only_built: - outdf = outdf.query('max_profit > 0').copy() - else: - outdf = outdf.loc[outdf.max_profit != -np.inf].copy() + # need to converge in on exactly how much far is available for + # deck pkg + if parking_config == 'deck': + building_bulk /= ( + 1.0 + np.sum(uses_distrib * self.parking_rates) * + self.parking_sqft_d[parking_config] / + self.sqft_per_rate) - return outdf + return building_bulk - def _min_max_fars(self, df, resratio): + def _park_sqft(self, parking_config, parking_stalls): """ - In case max_dua is passed in the DataFrame, - now also minimize with max_dua from zoning - since this pro forma is - really geared toward per sqft metrics, this is a bit tricky. dua - is converted to floorspace and everything just works (floor space - will get covered back to units in developer.pick() but we need to - test the profitability of the floorspace allowed by max_dua here. + Generate building square footage required for a parking configuration Parameters ---------- - df - resratio + parking_config : str + Name of parking configuration + parking_stalls : numeric + Number of parking stalls required Returns ------- + park_sqft : numeric + """ + + if parking_config in ['underground', 'deck']: + return parking_stalls * self.parking_sqft_d[parking_config] + if parking_config == 'surface': + return 0 + def _stories(self, parking_config, building_bulk, parking_stalls): """ + Calculates number of stories built at various FARs, given + building bulk, number of parking stalls, and parking configuration - if 'max_dua' in df.columns and resratio > 0: - # if max_dua is in the data frame, ave_unit_size must also be there - assert 'ave_unit_size' in df.columns + Parameters + ---------- + parking_config : str + Name of parking configuration + building_bulk : ndarray + Array of total square footage values for each FAR + parking_stalls : ndarray + Number of parking stalls required for each FAR - df['max_far_from_dua'] = ( - # this is the max_dua times the parcel size in acres, which - # gives the number of units that are allowable on the parcel - df.max_dua * (df.parcel_size / 43560) * + Returns + ------- + stories : ndarray - # times by the average unit size which gives the square footage - # of those units - df.ave_unit_size / + """ - # divided by the building efficiency which is a - # factor that indicates that the actual units are not the whole - # FAR of the building - self.building_efficiency / + if parking_config == 'underground': + stories = building_bulk / self.tiled_parcel_sizes + if parking_config == 'deck': + stories = ((building_bulk + + parking_stalls + * self.parking_sqft_d[parking_config]) + / self.tiled_parcel_sizes) + if parking_config == 'surface': + stories = (building_bulk + / (self.tiled_parcel_sizes + - parking_stalls + * self.parking_sqft_d[parking_config])) + # not all fars support surface parking + stories[stories < 0.0] = np.nan + # I think we can assume that stories over 3 + # do not work with surface parking + stories[stories > 5.0] = np.nan - # divided by the resratio which is a factor that indicates - # that the actual units are not the only use of the building - resratio / + stories /= self.parcel_coverage - # divided by the parcel size again in order to get FAR. - # I recognize that parcel_size actually - # cancels here as it should, but the calc was hard to get right - # and it's just so much more transparent to have it in there - # twice - df.parcel_size) - return df[['max_far_from_heights', - 'max_far', 'max_far_from_dua']].min(axis=1) - else: - return df[ - ['max_far_from_heights', 'max_far']].min(axis=1) + return stories From faadc20292e7774f5ec16c7ec664696ec2d9463d Mon Sep 17 00:00:00 2001 From: Paul Sohn Date: Mon, 17 Apr 2017 14:58:06 -0700 Subject: [PATCH 15/24] Allow users to inject function that selects which buildings to build --- developer/develop.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/developer/develop.py b/developer/develop.py index a29ecad..a00bfe3 100644 --- a/developer/develop.py +++ b/developer/develop.py @@ -153,7 +153,7 @@ def to_yaml(self, str_or_buffer=None): logger.debug('serializing Developer model to YAML') return utils.convert_to_yaml(self.to_dict, str_or_buffer) - def pick(self, profit_to_prob_func=None, min_profit_per_sqft=None): + def pick(self, profit_to_prob_func=None, custom_selection_func=None): """ Choose the buildings from the list that are feasible to build in order to match the specified demand. @@ -166,6 +166,11 @@ def pick(self, profit_to_prob_func=None, min_profit_per_sqft=None): a function which takes the feasibility dataframe and returns a series of probabilities. If no function is passed, the behavior of this method will not change + custom_selection_func: func + User passed function that decides how to select buildings for + development after probabilities are calculated. Must have + parameters (self, df, p) and return a numpy array of buildings to + build (i.e. df.index.values) Returns ------- @@ -195,7 +200,7 @@ def pick(self, profit_to_prob_func=None, min_profit_per_sqft=None): # Generate development probabilities and pick buildings to build p, df = self._calculate_probabilities(df, profit_to_prob_func) - build_idx = self._buildings_to_build(df, p, min_profit_per_sqft) + build_idx = self._select_buildings(df, p, custom_selection_func) # Drop built buildings from self.feasibility attribute if desired self._drop_built_buildings(build_idx) @@ -368,7 +373,7 @@ def _calculate_probabilities(df, profit_to_prob_func): p = df.max_profit_per_size.values / df.max_profit_per_size.sum() return p, df - def _buildings_to_build(self, df, p, min_profit_per_sqft=None): + def _select_buildings(self, df, p, custom_selection_func): """ Helper method to pick(). Selects buildings to build based on development probabilities. @@ -379,8 +384,11 @@ def _buildings_to_build(self, df, p, min_profit_per_sqft=None): DataFrame of buildings from _calculate_probabilities method p : Series Probabilities from _calculate_probabilities method - min_profit_per_sqft : numeric - Minimum profit per sqft required to build, if target units is None + custom_selection_func: func + User passed function that decides how to select buildings for + development after probabilities are calculated. Must have + parameters (self, df, p) and return a numpy array of buildings to + build (i.e. df.index.values) Returns ------- @@ -389,11 +397,8 @@ def _buildings_to_build(self, df, p, min_profit_per_sqft=None): """ - if self.target_units is None: - print("BUILDING ALL BUILDINGS WITH PROFIT > ${:.2f} / sqft" - .format(min_profit_per_sqft)) - profitable = df.loc[df.max_profit_per_size > min_profit_per_sqft] - build_idx = profitable.index.values + if custom_selection_func is not None: + build_idx = custom_selection_func(self, df, p) elif df.net_units.sum() < self.target_units: print("WARNING THERE WERE NOT ENOUGH PROFITABLE UNITS TO", "MATCH DEMAND") From c6b3b1da3fbd8111392889cc50ee8fae9ebd2cd0 Mon Sep 17 00:00:00 2001 From: Paul Sohn Date: Tue, 18 Apr 2017 09:56:13 -0700 Subject: [PATCH 16/24] Add docstrings to _min_max_fars helper method --- developer/sqftproforma.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/developer/sqftproforma.py b/developer/sqftproforma.py index 3b5d0ba..2ce20a5 100644 --- a/developer/sqftproforma.py +++ b/developer/sqftproforma.py @@ -592,7 +592,7 @@ def _lookup_parking_cfg(self, form, parking_config, df, parking_config : str Name of parking configuration df : DataFrame - DataFrame of developable sites/parcels passed to pick() method + DataFrame of developable sites/parcels passed to lookup() method modify_df : func Function to modify lookup DataFrame before profit calculations. Must have (self, form, df) as parameters. @@ -725,12 +725,14 @@ def _min_max_fars(self, df, resratio): Parameters ---------- - df - resratio + df : DataFrame + DataFrame of developable sites/parcels passed to lookup() method + resratio : numeric + Residential ratio for this form Returns ------- - + Series """ if 'max_dua' in df.columns and resratio > 0: From 0be0893f780a4522fde17e55785dd4505430b24b Mon Sep 17 00:00:00 2001 From: Paul Sohn Date: Wed, 19 Apr 2017 09:21:55 -0700 Subject: [PATCH 17/24] Update Travis configuration with Orca 1.4 --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 96a3a0f..ef4836c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,8 +19,8 @@ install: - source activate test-environment - conda list - pip install pycodestyle -- pip install https://github.com/UDST/orca/archive/master.zip -- pip install https://github.com/pksohn/urbansim/archive/python3.zip +- pip install orca +- pip install https://github.com/udst/urbansim/archive/python3.zip - pip install osmnet pandana - pip install . - cd .. && git clone git@github.com:urbansim/urbansim_parcels.git From 78394c8a88a41ff1a6e2ee44d8621456c8f4afd1 Mon Sep 17 00:00:00 2001 From: Paul Sohn Date: Wed, 19 Apr 2017 10:35:03 -0700 Subject: [PATCH 18/24] Add conda installation of pytables to Travis build --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index ef4836c..4ef062c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,7 +15,7 @@ install: - conda update -q conda - conda info -a - | - conda create -q -n test-environment python=$TRAVIS_PYTHON_VERSION pip numpy pandas pytest matplotlib scipy statsmodels + conda create -q -n test-environment python=$TRAVIS_PYTHON_VERSION pip numpy pandas pytest matplotlib scipy statsmodels pytables - source activate test-environment - conda list - pip install pycodestyle From c0a7085027fab7e1cd636d60cbfff817a8b5d044 Mon Sep 17 00:00:00 2001 From: Paul Sohn Date: Fri, 21 Apr 2017 15:03:25 -0700 Subject: [PATCH 19/24] Basic financing mechanism Changes to SqFtProForma and related Reference objects, revised tests --- developer/sqftproforma.py | 109 ++++++++++++++++++++++++--- developer/tests/test_sqftproforma.py | 6 +- 2 files changed, 103 insertions(+), 12 deletions(-) diff --git a/developer/sqftproforma.py b/developer/sqftproforma.py index 2ce20a5..37dbb72 100644 --- a/developer/sqftproforma.py +++ b/developer/sqftproforma.py @@ -152,6 +152,8 @@ def __init__(self, parcel_sizes, fars, uses, residential_uses, forms, cap_rate, parking_rates, sqft_per_rate, parking_configs, costs, heights_for_costs, parking_sqft_d, parking_cost_d, height_per_story, max_retail_height, max_industrial_height, + construction_months, construction_sqft_for_months, + loan_to_cost_ratio, drawdown_factor, interest_rate, loan_fees, residential_to_yearly=True, forms_to_test=None, only_built=True, pass_through=None, simple_zoning=False, parcel_filter=None @@ -176,6 +178,12 @@ def __init__(self, parcel_sizes, fars, uses, residential_uses, forms, self.height_per_story = height_per_story self.max_retail_height = max_retail_height self.max_industrial_height = max_industrial_height + self.construction_months = construction_months + self.construction_sqft_for_months = construction_sqft_for_months + self.loan_to_cost_ratio = loan_to_cost_ratio + self.drawdown_factor = drawdown_factor + self.interest_rate = interest_rate + self.loan_fees = loan_fees self.residential_to_yearly = residential_to_yearly self.forms_to_test = forms_to_test or sorted(self.forms.keys()) @@ -245,6 +253,9 @@ def _convert_types(self): self.residential_uses].sum() self.costs = np.transpose( np.array([self.costs[use] for use in self.uses])) + self.construction_months = np.transpose( + np.array([self.construction_months[use] for use in self.uses]) + ) @classmethod def from_yaml(cls, yaml_str=None, str_or_buffer=None): @@ -275,6 +286,12 @@ def from_yaml(cls, yaml_str=None, str_or_buffer=None): cfg['parking_sqft_d'], cfg['parking_cost_d'], cfg['height_per_story'], cfg['max_retail_height'], cfg['max_industrial_height'], + cfg['construction_months'], + cfg['construction_sqft_for_months'], + cfg['loan_to_cost_ratio'], + cfg['drawdown_factor'], + cfg['interest_rate'], + cfg['loan_fees'], cfg.get('residential_to_yearly', True), cfg.get('forms_to_test', None), cfg.get('only_built', True), @@ -331,7 +348,17 @@ def get_defaults(): 'mixedresidential', 'office', 'residential', 'retail'], 'pass_through': [], - 'simple_zoning': False + 'simple_zoning': False, + 'construction_months': { + 'industrial': [12.0, 14.0, 18.0, 24.0], + 'office': [12.0, 14.0, 18.0, 24.0], + 'residential': [12.0, 14.0, 18.0, 24.0], + 'retail': [12.0, 14.0, 18.0, 24.0]}, + 'construction_sqft_for_months': [10000, 20000, 50000, np.inf], + 'loan_to_cost_ratio': .7, + 'drawdown_factor': .6, + 'interest_rate': .05, + 'loan_fees': .02 } @classmethod @@ -364,7 +391,9 @@ def to_dict(self): 'parking_sqft_d', 'parking_cost_d', 'height_per_story', 'max_retail_height', 'max_industrial_height', 'residential_to_yearly', 'parcel_filter', 'only_built', - 'forms_to_test', 'pass_through', 'simple_zoning'] + 'forms_to_test', 'pass_through', 'simple_zoning', + 'construction_sqft_for_months', 'loan_to_cost_ratio', + 'drawdown_factor', 'interest_rate', 'loan_fees'] results = {} for attribute in unconverted: @@ -397,6 +426,13 @@ def to_dict(self): costs[use] = values.tolist() results['costs'] = costs + time = {} + time_transposed = self.construction_months.transpose() + for index, use in enumerate(self.uses): + values = time_transposed[index] + time[use] = values.tolist() + results['construction_months'] = time + return results def to_yaml(self, str_or_buffer=None): @@ -621,6 +657,7 @@ def _lookup_parking_cfg(self, form, parking_config, df, cost_sqft_index_col = columnize(dev_info.index.values) parking_sqft_ratio = columnize(dev_info.parking_sqft_ratio.values) heights = columnize(dev_info.height.values) + months = columnize(dev_info.construction_months.values) resratio = self.res_ratios[form] nonresratio = 1.0 - resratio df['weighted_rent'] = np.dot(df[self.uses], self.forms[form]) @@ -654,7 +691,20 @@ def _lookup_parking_cfg(self, form, parking_config, df, building_costs = building_bulks * cost_sqft_col # add cost to buy the current building - total_costs = building_costs + df.land_cost.values + total_construction_costs = building_costs + df.land_cost.values + + # Financing costs + loan_amount = total_construction_costs * self.loan_to_cost_ratio + months = np.repeat(months, len(df.index), axis=1) + # construction_period = self._construction_time(self.forms[form], + # building_bulks) + interest = (loan_amount + * self.drawdown_factor + * (self.interest_rate / 12 * months)) + points = loan_amount * self.loan_fees + total_financing_costs = interest + points + total_development_costs = (total_construction_costs + + total_financing_costs) # rent to make for the new building building_revenue = (building_bulks @@ -669,10 +719,11 @@ def _lookup_parking_cfg(self, form, parking_config, df, building_revenue = (modify_revenues(self, form, df, building_revenue) if modify_revenues else building_revenue) - total_costs = (modify_costs(self, form, df, total_costs) - if modify_costs else total_costs) + total_development_costs = ( + modify_costs(self, form, df, total_development_costs) + if modify_costs else total_development_costs) - profit = building_revenue - total_costs + profit = building_revenue - total_development_costs profit = (modify_profits(self, form, df, profit) if modify_profits else profit) @@ -690,11 +741,13 @@ def twod_get(indexes, arr): 'parking_ratio': parking_sqft_ratio[maxprofitind].flatten(), 'stories': twod_get(maxprofitind, heights) / self.height_per_story, - 'total_cost': twod_get(maxprofitind, total_costs), + 'total_cost': twod_get(maxprofitind, total_development_costs), 'building_revenue': twod_get(maxprofitind, building_revenue), 'max_profit_far': twod_get(maxprofitind, fars), 'max_profit': twod_get(maxprofitind, profit), - 'parking_config': parking_config + 'parking_config': parking_config, + 'construction_time': twod_get(maxprofitind, months), + 'financing_cost': twod_get(maxprofitind, total_financing_costs) }, index=df.index) if self.pass_through: @@ -873,7 +926,8 @@ def __init__(self, parcel_sizes, fars, forms, profit_factor, parcel_coverage, parking_rates, sqft_per_rate, parking_configs, costs, heights_for_costs, parking_sqft_d, parking_cost_d, height_per_story, max_retail_height, - max_industrial_height, **kwargs): + max_industrial_height, construction_sqft_for_months, + construction_months, **kwargs): self.fars = fars self.parcel_sizes = parcel_sizes @@ -890,6 +944,8 @@ def __init__(self, parcel_sizes, fars, forms, self.height_per_story = height_per_story self.max_retail_height = max_retail_height self.max_industrial_height = max_industrial_height + self.construction_sqft_for_months = construction_sqft_for_months + self.construction_months = construction_months self.tiled_parcel_sizes = columnize( np.repeat(self.parcel_sizes, self.fars.size)) @@ -966,12 +1022,18 @@ def _reference_dataframe(self, name, uses_distrib, parking_config): # Array of building cost per square foot for each FAR building_cost_per_sqft = self._building_cost(uses_distrib, stories) + total_built_sqft = building_bulk + park_sqft + + # Array of construction time for each FAR + construction_months = self._construction_time(uses_distrib, + total_built_sqft) + df['far'] = self.fars df['pclsz'] = self.tiled_parcel_sizes df['building_sqft'] = building_bulk df['spaces'] = parking_stalls df['park_sqft'] = park_sqft - df['total_built_sqft'] = df.building_sqft + df.park_sqft + df['total_built_sqft'] = total_built_sqft df['parking_sqft_ratio'] = df.park_sqft / df.total_built_sqft df['stories'] = np.ceil(stories) df['height'] = df.stories * self.height_per_story @@ -981,6 +1043,7 @@ def _reference_dataframe(self, name, uses_distrib, parking_config): df['cost'] = df.build_cost + df.park_cost df['ave_cost_sqft'] = ((df.cost / df.total_built_sqft) * self.profit_factor) + df['construction_months'] = construction_months if name == 'retail': df['ave_cost_sqft'][ @@ -1114,3 +1177,29 @@ def _stories(self, parking_config, building_bulk, parking_stalls): stories /= self.parcel_coverage return stories + + def _construction_time(self, use_mix, building_bulks): + """ + Calculate construction time in months for each development site. + + Parameters + ---------- + use_mix : array + The mix of uses for this form + building_bulks : array + Array of square footage for each potential building + + Returns + ------- + construction_times : array + """ + + # Look at square footage and return matching index in list of + # construction times + month_indices = np.searchsorted(self.construction_sqft_for_months, + building_bulks) + # Get the construction time for each dev site, for all uses + months_array_all_uses = self.construction_months[month_indices] + # Dot product to get appropriate time for uses being evaluated + construction_times = np.dot(months_array_all_uses, use_mix) + return construction_times diff --git a/developer/tests/test_sqftproforma.py b/developer/tests/test_sqftproforma.py index 3a2c9b7..78affdb 100644 --- a/developer/tests/test_sqftproforma.py +++ b/developer/tests/test_sqftproforma.py @@ -40,7 +40,7 @@ def simple_dev_inputs_high_cost(): @pytest.fixture def simple_dev_inputs_low_cost(): sdi = simple_dev_inputs() - sdi.land_cost /= 20 + sdi.land_cost /= 100 return sdi @@ -161,7 +161,9 @@ def test_reasonable_feasibility_results(): # confirm cost per sqft is between 100 and 400 per sqft assert 100 < first.building_cost/first.building_sqft < 400 # total cost equals building cost plus land cost - assert first.total_cost == first.building_cost + df.iloc[0].land_cost + assert first.total_cost == (first.building_cost + + df.iloc[0].land_cost + + first.financing_cost) # revenue per sqft should be between 200 and 800 per sqft assert 200 < first.building_revenue/first.building_sqft < 800 assert first.residential_sqft == (first.building_sqft From 39d48beacb4d7fb36af82ca0a1c7de2b832b314a Mon Sep 17 00:00:00 2001 From: Paul Sohn Date: Fri, 21 Apr 2017 15:39:36 -0700 Subject: [PATCH 20/24] Point Travis build to market research branch --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 4ef062c..993cdc2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,7 +23,7 @@ install: - pip install https://github.com/udst/urbansim/archive/python3.zip - pip install osmnet pandana - pip install . -- cd .. && git clone git@github.com:urbansim/urbansim_parcels.git +- cd .. && git clone -b market_research git@github.com:urbansim/urbansim_parcels.git - pip install ./urbansim_parcels - cd "$TRAVIS_BUILD_DIR" From c65ab5a32a4cd125ca23f92dbddd2ac27c850d59 Mon Sep 17 00:00:00 2001 From: Paul Sohn Date: Mon, 24 Apr 2017 13:31:55 -0700 Subject: [PATCH 21/24] Remove unnecessary lines --- developer/sqftproforma.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/developer/sqftproforma.py b/developer/sqftproforma.py index 37dbb72..d8e3593 100644 --- a/developer/sqftproforma.py +++ b/developer/sqftproforma.py @@ -696,8 +696,6 @@ def _lookup_parking_cfg(self, form, parking_config, df, # Financing costs loan_amount = total_construction_costs * self.loan_to_cost_ratio months = np.repeat(months, len(df.index), axis=1) - # construction_period = self._construction_time(self.forms[form], - # building_bulks) interest = (loan_amount * self.drawdown_factor * (self.interest_rate / 12 * months)) From 7e6a735e7e31ed060dd1a4057914fb782374d47b Mon Sep 17 00:00:00 2001 From: Paul Sohn Date: Mon, 24 Apr 2017 15:08:41 -0700 Subject: [PATCH 22/24] Point Travis build to master branch of urbansim_parcels --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 993cdc2..63fc083 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,10 +20,10 @@ install: - conda list - pip install pycodestyle - pip install orca -- pip install https://github.com/udst/urbansim/archive/python3.zip +- pip install https://github.com/udst/urbansim/archive/master.zip - pip install osmnet pandana - pip install . -- cd .. && git clone -b market_research git@github.com:urbansim/urbansim_parcels.git +- cd .. && git clone git@github.com:urbansim/urbansim_parcels.git - pip install ./urbansim_parcels - cd "$TRAVIS_BUILD_DIR" From 60327f4155ef322ad074a489090a82a0d5379bb7 Mon Sep 17 00:00:00 2001 From: Paul Sohn Date: Mon, 1 May 2017 22:16:09 -0700 Subject: [PATCH 23/24] Minor changes to work with pipeline --- developer/develop.py | 8 ++++++-- developer/sqftproforma.py | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/developer/develop.py b/developer/develop.py index a00bfe3..9db4d4d 100644 --- a/developer/develop.py +++ b/developer/develop.py @@ -460,7 +460,11 @@ def _prepare_new_buildings(self, df, build_idx): """ new_df = df.loc[build_idx] - new_df.index.name = "parcel_id" + + drop = True + if 'parcel_id' not in df.columns: + new_df.index.name = "parcel_id" + drop = False if self.year is not None: new_df["year_built"] = self.year @@ -471,4 +475,4 @@ def _prepare_new_buildings(self, df, build_idx): new_df["stories"] = new_df.stories.apply(np.ceil) - return new_df.reset_index() + return new_df.reset_index(drop=drop) diff --git a/developer/sqftproforma.py b/developer/sqftproforma.py index d8e3593..46d9ec7 100644 --- a/developer/sqftproforma.py +++ b/developer/sqftproforma.py @@ -457,7 +457,7 @@ def to_yaml(self, str_or_buffer=None): return utils.convert_to_yaml(self.to_dict, str_or_buffer) def lookup(self, form, df, modify_df=None, modify_revenues=None, - modify_costs=None, modify_profits=None): + modify_costs=None, modify_profits=None, **kwargs): """ This function does the developer model lookups for all the actual input data. From e95422bb7dcee71d6b6bd5c3edf20382c7e68ec9 Mon Sep 17 00:00:00 2001 From: Paul Sohn Date: Tue, 2 May 2017 14:43:35 -0700 Subject: [PATCH 24/24] Bump version --- developer/__init__.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/developer/__init__.py b/developer/__init__.py index 7fd229a..0404d81 100644 --- a/developer/__init__.py +++ b/developer/__init__.py @@ -1 +1 @@ -__version__ = '0.2.0' +__version__ = '0.3.0' diff --git a/setup.py b/setup.py index 6ff1456..d03ff58 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( name='developer', - version='0.2.0', + version='0.3.0', description='Urbansim developer model', author='UrbanSim Inc.', author_email='info@urbansim.com',