264 KiB
264 KiB
In [1]:
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as snsIn [2]:
df = pd.read_csv('~/Downloads/DOB_Sustainability_Compliance_Map__Local_Law_33.csv')In [3]:
df.shapeOut [3]:
(21681, 11)
In [4]:
df.head(3)Out [4]:
| Block | Lot | Building_Class | Tax_Class | Building_Count | DOF_Gross_Square_Footage | Address | BoroughName | BBL | ENERGY STAR Score | LetterScore | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1 | 10 | Y4 | 0 | 124 | 2598091 | 920 GRESHAM ROAD | MANHATTAN | 1000010010 | 1 | D |
| 1 | 2 | 23 | T2 | 0 | 1 | 24346 | 20 SOUTH STREET | MANHATTAN | 1000020023 | 0 | F |
| 2 | 4 | 7501 | R0 | 2 | 1 | 2542563 | 1 WATER STREET | MANHATTAN | 1000047501 | 61 | C |
In [5]:
df.columnsOut [5]:
Index(['Block', 'Lot', 'Building_Class', 'Tax_Class', 'Building_Count',
'DOF_Gross_Square_Footage', 'Address', 'BoroughName', 'BBL',
'ENERGY STAR Score', 'LetterScore'],
dtype='object')In [6]:
# Columns seem to be self-explanatory, except BBL. According to NYC OpenData:
# "Borough Block and Lot identifier as assigned by NYC Department of Finance".In [7]:
# Is this citywide or just Manhattan?
df['BoroughName'].value_counts()Out [7]:
MANHATTAN 7858 BROOKLYN 5469 BRONX 4349 QUEENS 3659 STATEN ISLAND 346 Name: BoroughName, dtype: int64
In [8]:
# Missing data?
df.isna().sum()Out [8]:
Block 0 Lot 0 Building_Class 2 Tax_Class 0 Building_Count 0 DOF_Gross_Square_Footage 0 Address 7 BoroughName 0 BBL 0 ENERGY STAR Score 0 LetterScore 0 dtype: int64
In [9]:
df[df['Building_Class'].isna()]Out [9]:
| Block | Lot | Building_Class | Tax_Class | Building_Count | DOF_Gross_Square_Footage | Address | BoroughName | BBL | ENERGY STAR Score | LetterScore | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 4254 | 1595 | 7501 | NaN | 0 | 0 | 0 | 1330 5 AVENUE | MANHATTAN | 1015950031 | 64 | C |
| 8124 | 3016 | 7502 | NaN | 0 | 0 | 0 | 1926 LONGFELLOW AVENUE | BRONX | 2030160038 | 100 | A |
In [10]:
df[df['Address'].isna()]Out [10]:
| Block | Lot | Building_Class | Tax_Class | Building_Count | DOF_Gross_Square_Footage | Address | BoroughName | BBL | ENERGY STAR Score | LetterScore | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 1228 | 506 | 12 | W3 | 0 | 1 | 49475 | NaN | MANHATTAN | 1005060012 | 10 | D |
| 7145 | 1734 | 1 | I1 | 0 | 5 | 1017118 | NaN | MANHATTAN | 1017340001 | 7 | D |
| 9225 | 2758 | 6 | N9 | 0 | 1 | 17200 | NaN | BRONX | 2027580006 | 89 | A |
| 9226 | 2758 | 36 | N9 | 0 | 1 | 37060 | NaN | BRONX | 2027580036 | 66 | C |
| 13711 | 1769 | 72 | C1 | 0 | 1 | 30720 | NaN | BROOKLYN | -2147483648 | 0 | F |
| 15056 | 1602 | 13 | C1 | 0 | 1 | 14720 | NaN | BROOKLYN | -2147483648 | 0 | F |
| 16381 | 3755 | 22 | C1 | 0 | 1 | 25564 | NaN | BROOKLYN | -2147483648 | 0 | F |
In [11]:
# Missing Address is not a big deal because the rest of the values are complete.
# But missing Building Class could be a problem.
# The two offending rows also have Building Count = 0.
# How is that possible, since they have Energy Star scores?
# In the next secion we may decide to drop those two rows.In [12]:
# Which columns are informative?
df.dtypesOut [12]:
Block int64 Lot int64 Building_Class object Tax_Class int64 Building_Count int64 DOF_Gross_Square_Footage int64 Address object BoroughName object BBL int64 ENERGY STAR Score int64 LetterScore object dtype: object
In [13]:
# Interesting for analysis:
# DOF_Gross_Square_Footage
# ENERGY STAR Score
# LetterScore
# Other columns are less interesting:
# Building_Count is the number of buildings in one Block.
# Block can have more than one Lot, but Lot only has one Block.
# Block, Lot and BBL are identifiers assigned by the city.
# A good visual reference is the Digital Tax Map put out by the NYC Department of Finance:
# http://gis.nyc.gov/taxmap/map.htmIn [14]:
# Can any identifiers be used as an index?
df['Block'].is_uniqueOut [14]:
False
In [15]:
df['Lot'].is_uniqueOut [15]:
False
In [16]:
df['BBL'].is_uniqueOut [16]:
False
In [17]:
# Since their values are not unique, they cannot be used as an index.In [18]:
# Shall we rename or discard any columns from this dataset?
# BBL and Tax Class could be eliminated. However, there are only 11 columns total, and since df.head() is easily readable on my monitor without scrolling horizontally (as you're doing now), I see no harm in keeping them.In [19]:
# Rename columns containing whitespace or camelcase
df.rename(columns = {"BoroughName": "Borough_Name",
"ENERGY STAR Score": "Energy_Star_Score",
"LetterScore": "Letter_Score"
}, inplace = True)In [20]:
df.head(1)Out [20]:
| Block | Lot | Building_Class | Tax_Class | Building_Count | DOF_Gross_Square_Footage | Address | Borough_Name | BBL | Energy_Star_Score | Letter_Score | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1 | 10 | Y4 | 0 | 124 | 2598091 | 920 GRESHAM ROAD | MANHATTAN | 1000010010 | 1 | D |
In [21]:
# Unforseen consequence of renaming: now I have to scroll horizontally.In [22]:
# Rename columns once more
df.rename(columns = {"DOF_Gross_Square_Footage": "Sq_Footage",
"Energy_Star_Score": "Energy_Score",
"Borough_Name": "Borough",
"Building_Class": "Bldg_Class",
"Building_Count": "Bldg_Count"
}, inplace = True)In [23]:
df.head(1)Out [23]:
| Block | Lot | Bldg_Class | Tax_Class | Bldg_Count | Sq_Footage | Address | Borough | BBL | Energy_Score | Letter_Score | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1 | 10 | Y4 | 0 | 124 | 2598091 | 920 GRESHAM ROAD | MANHATTAN | 1000010010 | 1 | D |
In [24]:
# Repeat the search for missing data
df.isna().sum()Out [24]:
Block 0 Lot 0 Bldg_Class 2 Tax_Class 0 Bldg_Count 0 Sq_Footage 0 Address 7 Borough 0 BBL 0 Energy_Score 0 Letter_Score 0 dtype: int64
In [25]:
# Ignore the 7 missing addresses, but drop the 2 rows with missing Building Class.
# Building Class is a feature that will be used in the df.groupby() function.
df.dropna(subset = ['Bldg_Class'], inplace = True)In [26]:
df.isna().sum()Out [26]:
Block 0 Lot 0 Bldg_Class 0 Tax_Class 0 Bldg_Count 0 Sq_Footage 0 Address 7 Borough 0 BBL 0 Energy_Score 0 Letter_Score 0 dtype: int64
In [27]:
# Search for unexpected data
# df['Energy_Score'].min() # looks good
# df['Energy_Score'].max() # looks good
# df['Sq_Footage'].max() # looks good
df['Sq_Footage'].min()Out [27]:
0
In [28]:
# A building cannot have zero square feet of floorspace.
# What's going on?
df[df['Sq_Footage'] == 0].groupby(['Letter_Score']).count()Out [28]:
| Block | Lot | Bldg_Class | Tax_Class | Bldg_Count | Sq_Footage | Address | Borough | BBL | Energy_Score | |
|---|---|---|---|---|---|---|---|---|---|---|
| Letter_Score | ||||||||||
| A | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 |
| B | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
| C | 5 | 5 | 5 | 5 | 5 | 5 | 5 | 5 | 5 | 5 |
| D | 6 | 6 | 6 | 6 | 6 | 6 | 6 | 6 | 6 | 6 |
| F | 14 | 14 | 14 | 14 | 14 | 14 | 14 | 14 | 14 | 14 |
In [29]:
# The ones with F can be explained:
# An F grade means that the building owner "didn’t submit required benchmarking information",
# according to Local Law 95 of 2019. So it's not that the building has no square footage,
# but that the data was not submitted. Thus the failing grade.
# We'll leave 0 square feet with F grade untouched.
# For more information, see https://www1.nyc.gov/site/buildings/codes/benchmarking.pageIn [30]:
# What to do with the others?
df[(df['Sq_Footage'] == 0) & (df['Letter_Score'] != 'F')].groupby(['Letter_Score']).count()Out [30]:
| Block | Lot | Bldg_Class | Tax_Class | Bldg_Count | Sq_Footage | Address | Borough | BBL | Energy_Score | |
|---|---|---|---|---|---|---|---|---|---|---|
| Letter_Score | ||||||||||
| A | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 |
| B | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
| C | 5 | 5 | 5 | 5 | 5 | 5 | 5 | 5 | 5 | 5 |
| D | 6 | 6 | 6 | 6 | 6 | 6 | 6 | 6 | 6 | 6 |
In [31]:
# 15 rows remain with 0 square feet of floorspace.
# Can we impute values from the mean square footage for each grade?
# (There must be an elegant way to do this. What you see below is not.)In [32]:
# First, get averages
subset0 = df[['Letter_Score', 'Sq_Footage']]
subset1 = subset0[(subset0['Letter_Score'] != 'F') & (subset0['Sq_Footage'] != 0)]
subset1.groupby(['Letter_Score']).mean()Out [32]:
| Sq_Footage | |
|---|---|
| Letter_Score | |
| A | 111197.291071 |
| B | 133270.963702 |
| C | 128833.575964 |
| D | 108170.778312 |
In [33]:
# Assign variables, rounding to whole numbers
mean_A = 111197
mean_B = 133271
mean_C = 128834
mean_D = 108171In [34]:
# Replace 0 values with mean_A, mean_B, etc.
df.loc[(df['Letter_Score'] == 'A') & (df['Sq_Footage'] == 0), 'Sq_Footage'] = mean_A
df.loc[(df['Letter_Score'] == 'B') & (df['Sq_Footage'] == 0), 'Sq_Footage'] = mean_B
df.loc[(df['Letter_Score'] == 'C') & (df['Sq_Footage'] == 0), 'Sq_Footage'] = mean_C
df.loc[(df['Letter_Score'] == 'D') & (df['Sq_Footage'] == 0), 'Sq_Footage'] = mean_DIn [35]:
# Now the only 0 values should be for F grades
df[df['Sq_Footage'] == 0].groupby(['Letter_Score']).count()Out [35]:
| Block | Lot | Bldg_Class | Tax_Class | Bldg_Count | Sq_Footage | Address | Borough | BBL | Energy_Score | |
|---|---|---|---|---|---|---|---|---|---|---|
| Letter_Score | ||||||||||
| F | 14 | 14 | 14 | 14 | 14 | 14 | 14 | 14 | 14 | 14 |
In [36]:
# Unexpected values, continued
df[df['Bldg_Count'] == 0]Out [36]:
| Block | Lot | Bldg_Class | Tax_Class | Bldg_Count | Sq_Footage | Address | Borough | BBL | Energy_Score | Letter_Score | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 11319 | 149 | 7502 | U7 | 0 | 0 | 0 | 138 WILLOUGHBY STREET | BROOKLYN | -2147483648 | 0 | F |
| 11611 | 165 | 7504 | U7 | 0 | 0 | 133271 | 35 HOYT STREET | BROOKLYN | -2147483648 | 75 | B |
| 13351 | 5804 | 2 | U6 | 0 | 0 | 0 | COLONIAL ROAD | BROOKLYN | -2147483648 | 0 | F |
| 14570 | 5322 | 4 | V1 | 0 | 0 | 111197 | 23 OCEAN PARKWAY | BROOKLYN | -2147483648 | 100 | A |
| 14668 | 5799 | 59 | D9 | 0 | 0 | 38315 | 3641 JOHNSON AVENUE | BRONX | 2057990059 | 0 | F |
| 15726 | 4282 | 100 | V1 | 0 | 0 | 0 | 25-70 REAR WHITESTONE EXPRESSWAY SR WEST | QUEENS | -2147483648 | 0 | F |
In [37]:
# How can a block have zero buildings?
# Again, we'll leave the F grades as is.
df[(df['Bldg_Count'] == 0) & (df['Letter_Score'] != 'F')]Out [37]:
| Block | Lot | Bldg_Class | Tax_Class | Bldg_Count | Sq_Footage | Address | Borough | BBL | Energy_Score | Letter_Score | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 11611 | 165 | 7504 | U7 | 0 | 0 | 133271 | 35 HOYT STREET | BROOKLYN | -2147483648 | 75 | B |
| 14570 | 5322 | 4 | V1 | 0 | 0 | 111197 | 23 OCEAN PARKWAY | BROOKLYN | -2147483648 | 100 | A |
In [38]:
# Have a peek at the Department of Finance Tax Map: http://gis.nyc.gov/taxmap/map.htm
# Looks like Bldg_Count = 1 for both. However, rather than eyeballing it, let's just drop them.In [39]:
# Drop two rows
df.drop([11611, 14570], inplace = True)In [40]:
# Finish cleaning
# df['Tax_Class'].value_counts() # looks good
# df['Bldg_Class'].value_counts() # looks goodIn [41]:
# There are many building types
df['Bldg_Class'].unique()Out [41]:
array(['Y4', 'T2', 'R0', 'O4', 'W1', 'O6', 'D5', 'D9', 'D0', 'O3', 'H3',
'H2', 'D7', 'V1', 'D6', 'K6', 'D8', 'W8', 'O2', 'H1', 'D3', 'K4',
'H9', 'K3', 'HB', 'RM', 'H5', 'O5', 'D4', 'HS', 'E9', 'D2', 'O7',
'W5', 'U7', 'M1', 'I1', 'K2', 'Z1', 'W6', 'K9', 'Z3', 'H8', 'S4',
'E1', 'C7', 'W2', 'C9', 'D1', 'C1', 'HR', 'O9', 'I9', 'K1', 'I6',
'G1', 'N2', 'Y2', 'I7', 'M9', 'G2', 'I5', 'C4', 'E7', 'P9', 'W9',
'P5', 'N9', 'S3', 'W3', 'J4', 'C6', 'M2', 'P7', 'W7', 'J3', 'H6',
'P8', 'F9', 'G9', 'Y8', 'J8', 'F5', 'C5', 'N4', 'I3', 'P3', 'J6',
'P2', 'W4', 'RC', 'I2', 'K5', 'J5', 'I4', 'M4', 'G8', 'J7', 'HH',
'O8', 'M3', 'U0', 'O1', 'F1', 'F2', 'F4', 'H4', 'E2', 'Y1', 'Y6',
'Z9', 'R2', 'Q6', 'K7', 'U6', 'RD', 'Y9', 'Q1', 'T9', 'V9', 'U9',
'K8', 'U5', 'R4', 'G7', 'F8', 'J9', 'N3', 'P6', 'J2', 'GW', 'T1',
'R3', 'C8', 'RS', 'Q2', 'V7', 'Q4', 'Y7'], dtype=object)In [42]:
# It wouldn't make sense to compare, say, residential with commercial buildings.
# For an apple to apples comparison, let's look at office buildings.
# O1 OFFICE ONLY - 1 STORY
# O2 OFFICE ONLY 2 - 6 STORIES
# O3 OFFICE ONLY 7 - 19 STORIES
# O4 OFFICE ONLY WITH OR WITHOUT COMM - 20 STORIES OR MORE
# O5 OFFICE WITH COMM - 1 TO 6 STORIES
# O6 OFFICE WITH COMM 7 - 19 STORIES
# O7 PROFESSIONAL BUILDINGS/STAND ALONE FUNERAL HOMES
# O8 OFFICE WITH APARTMENTS ONLY (NO COMM)
# O9 MISCELLANEOUS AND OLD STYLE BANK BLDGS
# Building glossary: https://www1.nyc.gov/assets/finance/jump/hlpbldgcode.htmlIn [43]:
# Context: 10 Hudson Yards. A new building with a poor energy rating.
# It's one of the large glass and steel buildings that have recently cropped up in Manhattan (2016).
# Unfortunately, the dataset does not contain the newest behemoths to arise since then,
# like 30 Hudson Yards.
df[(df['Block'] == 702) & (df['Lot'] == 10)]Out [43]:
| Block | Lot | Bldg_Class | Tax_Class | Bldg_Count | Sq_Footage | Address | Borough | BBL | Energy_Score | Letter_Score | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 1473 | 702 | 10 | O4 | 4 | 1 | 1835464 | 501 WEST 30 STREET | MANHATTAN | 1007020010 | 58 | C |
In [44]:
# That building belongs to class O4.
# What's the distribution of scores?
office = df[df['Bldg_Class'] == 'O4']
office['Energy_Score'].plot(kind = 'hist')
plt.savefig('office.png')In [45]:
# Distribution of grades
office.groupby(['Letter_Score'])['Block'].count()Out [45]:
Letter_Score A 33 B 147 C 102 D 75 F 5 Name: Block, dtype: int64
In [46]:
# Square Footage versus Energy Score
office.plot(kind = 'scatter', x = 'Sq_Footage', y = 'Energy_Score', figsize = (12, 9))
plt.savefig('scatter.png')In [47]:
# I can't tell from the plot whether there's any relationship between those two variables.In [48]:
# How about smaller office buildings?
office_small = df[df['Bldg_Class'] == 'O2']
office_small['Energy_Score'].plot(kind = 'hist')Out [48]:
<AxesSubplot:ylabel='Frequency'>
In [49]:
# Distribution of grades
office_small.groupby(['Letter_Score'])['Block'].count()Out [49]:
Letter_Score A 21 B 31 C 17 D 38 F 23 Name: Block, dtype: int64
In [50]:
# Square Footage versus Energy Score
office_small.plot(kind = 'scatter', x = 'Sq_Footage', y = 'Energy_Score', figsize = (20, 10))Out [50]:
<AxesSubplot:xlabel='Sq_Footage', ylabel='Energy_Score'>
In [51]:
# Something different: hotels
hotels = df[df['Bldg_Class'] == 'H2']
hotels['Energy_Score'].plot(kind = 'hist')
plt.savefig('hotels.png')In [52]:
# Distribution of grades
hotels.groupby(['Letter_Score'])['Block'].count()Out [52]:
Letter_Score A 6 B 3 C 9 D 64 F 18 Name: Block, dtype: int64
In [53]:
# Square Footage versus Energy Score
hotels.plot(kind = 'scatter', x = 'Sq_Footage', y = 'Energy_Score', figsize = (20, 10))Out [53]:
<AxesSubplot:xlabel='Sq_Footage', ylabel='Energy_Score'>
In [54]:
# Again, no apparent connection between score and building size.
# But it's obvious that hotels score worse than office buildings.In [55]:
# Lastly, let's take a look at residential buildings.
# This time we'll zoom out and consider two building classes:
# Walk up apartments and elevator apartments.In [56]:
walk_up = df[df['Bldg_Class'].str.contains('C')]
elevator = df[df['Bldg_Class'].str.contains('D')]In [57]:
walk_up['Energy_Score'].plot(kind = 'hist')Out [57]:
<AxesSubplot:ylabel='Frequency'>
In [58]:
walk_up.groupby(['Letter_Score'])['Block'].count()Out [58]:
Letter_Score A 403 B 302 C 388 D 1909 F 285 Name: Block, dtype: int64
In [59]:
# Many Ds and Fs, and the rest are evenly distributed.In [60]:
walk_up.plot(kind = 'scatter', x = 'Sq_Footage', y = 'Energy_Score', figsize = (20, 10))Out [60]:
<AxesSubplot:xlabel='Sq_Footage', ylabel='Energy_Score'>
In [61]:
elevator['Energy_Score'].plot(kind = 'hist')Out [61]:
<AxesSubplot:ylabel='Frequency'>
In [62]:
elevator.groupby(['Letter_Score'])['Block'].count()Out [62]:
Letter_Score A 1683 B 1863 C 1803 D 4136 F 756 Name: Block, dtype: int64
In [63]:
# Elevator apartments fared better overall than walk ups.In [64]:
elevator.plot(kind = 'scatter', x = 'Sq_Footage', y = 'Energy_Score', figsize = (20, 10))Out [64]:
<AxesSubplot:xlabel='Sq_Footage', ylabel='Energy_Score'>
In [65]:
# Scatter plots suggest that the relationship between a building's size and its energy rating
# is not simple. I had hoped the plots would show either a positive or negative relationship between square footage
# and energy rating.
# The analysis elaborated here cannot answer the research question. However, some trends were uncovered in the
# process. Hotels scored worse overall than office buildings. And elevator buildings performed better than walk up
# apartments.