def pudl_engine(ferc1_engine, live_pudl_db, live_ferc_db): """ Grab a conneciton to the PUDL Database. If we are using the test database, we initialize the PUDL DB from scratch. If we're using the live database, then we just make a conneciton to it. """ if not live_pudl_db: if live_ferc_db: ferc1_testing = False else: ferc1_testing = True init.init_db(ferc1_tables=pc.ferc1_pudl_tables, ferc1_years=pc.working_years['ferc1'], eia923_tables=pc.eia923_pudl_tables, eia923_years=pc.working_years['eia923'], eia860_tables=pc.eia860_pudl_tables, eia860_years=pc.working_years['eia860'], verbose=True, debug=False, pudl_testing=True, ferc1_testing=ferc1_testing) # Grab a connection to the freshly populated PUDL DB, and hand it off. pudl_engine = init.connect_db(testing=True) yield (pudl_engine) # Clean up after ourselves by dropping the test DB tables. init.drop_tables(pudl_engine) else: print("Connecting to the live PUDL database.") yield (init.connect_db(testing=False))
def pudl_engine_travis_ci(ferc1_engine_travis_ci): """ Grab a conneciton to the PUDL Database, with a limited amount of data. This fixture always initializes the DB from scratch, and only does a small subset of the data ETL, for structural testing within Travis CI. """ init.init_db(ferc1_tables=pc.ferc1_pudl_tables, ferc1_years=pc.travis_ci_ferc1_years, eia923_tables=pc.eia923_pudl_tables, eia923_years=pc.travis_ci_eia923_years, eia860_tables=pc.eia860_pudl_tables, eia860_years=pc.travis_ci_eia860_years, epacems_years=pc.travis_ci_epacems_years, epacems_states=pc.travis_ci_epacems_states, verbose=True, debug=False, pudl_testing=True, ferc1_testing=True) # Grab a connection to the freshly populated PUDL DB, and hand it off. pudl_engine = init.connect_db(testing=True) yield (pudl_engine) # Clean up after ourselves by dropping the test DB tables. init.drop_tables(pudl_engine)
def plants_eia860(start_date=None, end_date=None, testing=False): """Pull all fields from the EIA860 Plants table.""" pudl_engine = init.connect_db(testing=testing) plants_eia860_tbl = pt['plants_eia860'] plants_eia860_select = sa.sql.select([plants_eia860_tbl]) if start_date is not None: start_date = pd.to_datetime(start_date) plants_eia860_select = plants_eia860_select.where( plants_eia860_tbl.c.report_date >= start_date) if end_date is not None: end_date = pd.to_datetime(end_date) plants_eia860_select = plants_eia860_select.where( plants_eia860_tbl.c.report_date <= end_date) plants_eia860_df = pd.read_sql(plants_eia860_select, pudl_engine) plants_eia_tbl = pt['plants_eia'] plants_eia_select = sa.sql.select([ plants_eia_tbl.c.plant_id_eia, plants_eia_tbl.c.plant_id_pudl, ]) plants_eia_df = pd.read_sql(plants_eia_select, pudl_engine) out_df = pd.merge(plants_eia860_df, plants_eia_df, how='left', on=[ 'plant_id_eia', ]) utils_eia_tbl = pt['utilities_eia'] utils_eia_select = sa.sql.select([ utils_eia_tbl.c.utility_id_eia, utils_eia_tbl.c.util_id_pudl, ]) utils_eia_df = pd.read_sql(utils_eia_select, pudl_engine) out_df = pd.merge(out_df, utils_eia_df, how='left', on=[ 'utility_id_eia', ]) out_df = out_df.drop(['id'], axis=1) first_cols = [ 'report_date', 'utility_id_eia', 'util_id_pudl', 'utility_name', 'plant_id_eia', 'plant_id_pudl', 'plant_name', ] out_df = helpers.organize_cols(out_df, first_cols) out_df = helpers.extend_annual(out_df, start_date=start_date, end_date=end_date) return out_df
def fuel_ferc1(testing=False): """ Pull a useful dataframe related to FERC Form 1 fuel information. This function pulls the FERC Form 1 fuel data, and joins in the name of the reporting utility, as well as the PUDL IDs for that utility and the plant, allowing integration with other PUDL tables. Also calculates the total heat content consumed for each fuel, and the total cost for each fuel. Total cost is calculated in two different ways, on the basis of fuel units consumed (e.g. tons of coal, mcf of gas) and on the basis of heat content consumed. In theory these should give the same value for total cost, but this is not always the case. TODO: Check whether this includes all of the fuel_ferc1 fields... Args: ----- testing (bool): True if we're using the pudl_test DB, False if we're using the live PUDL DB. False by default. Returns: -------- fuel_df: a pandas dataframe. """ pudl_engine = init.connect_db(testing=testing) fuel_ferc1_tbl = pt['fuel_ferc1'] fuel_ferc1_select = sa.sql.select([fuel_ferc1_tbl, ]) fuel_df = pd.read_sql(fuel_ferc1_select, pudl_engine) # We have two different ways of assessing the total cost of fuel given cost # per unit delivered and cost per mmbtu. They *should* be the same, but we # know they aren't always. Calculate both so we can compare both. fuel_df['fuel_consumed_total_mmbtu'] = \ fuel_df['fuel_qty_burned'] * fuel_df['fuel_avg_mmbtu_per_unit'] fuel_df['fuel_consumed_total_cost_mmbtu'] = \ fuel_df['fuel_cost_per_mmbtu'] * fuel_df['fuel_consumed_total_mmbtu'] fuel_df['fuel_consumed_total_cost_unit'] = \ fuel_df['fuel_cost_per_unit_burned'] * fuel_df['fuel_qty_burned'] pu_ferc = plants_utils_ferc1(testing=testing) out_df = pd.merge(fuel_df, pu_ferc, on=['respondent_id', 'plant_name']) out_df = out_df.drop('id', axis=1) first_cols = [ 'report_year', 'respondent_id', 'util_id_pudl', 'respondent_name', 'plant_id_pudl', 'plant_name' ] out_df = helpers.organize_cols(out_df, first_cols) return out_df
def plants_utils_ferc1(testing=False): """Build a dataframe of useful FERC Plant & Utility information.""" pudl_engine = init.connect_db(testing=testing) utils_ferc_tbl = pt['utilities_ferc'] utils_ferc_select = sa.sql.select([utils_ferc_tbl, ]) utils_ferc = pd.read_sql(utils_ferc_select, pudl_engine) plants_ferc_tbl = pt['plants_ferc'] plants_ferc_select = sa.sql.select([plants_ferc_tbl, ]) plants_ferc = pd.read_sql(plants_ferc_select, pudl_engine) out_df = pd.merge(plants_ferc, utils_ferc, on='respondent_id') return out_df
def boiler_generator_assn(start_date=None, end_date=None, testing=False): """Pull the more complete PUDL/EIA boiler generator associations.""" pudl_engine = init.connect_db(testing=testing) bga_eia_tbl = pt['boiler_generator_assn_eia'] bga_eia_select = sa.sql.select([bga_eia_tbl]) if start_date is not None: start_date = pd.to_datetime(start_date) bga_eia_select = bga_eia_select.where( bga_eia_tbl.c.report_date >= start_date) if end_date is not None: end_date = pd.to_datetime(end_date) bga_eia_select = bga_eia_select.where( bga_eia_tbl.c.report_date <= end_date) bga_eia_df = pd.read_sql(bga_eia_select, pudl_engine) out_df = helpers.extend_annual(bga_eia_df, start_date=start_date, end_date=end_date) return out_df
def boiler_generator_assn_eia860(start_date=None, end_date=None, testing=False): """Pull all fields from the EIA 860 boiler generator association table.""" pudl_engine = init.connect_db(testing=testing) bga_eia860_tbl = pt['boiler_generator_assn_eia860'] bga_eia860_select = sa.sql.select([bga_eia860_tbl]) if start_date is not None: start_date = pd.to_datetime(start_date) bga_eia860_select = bga_eia860_select.where( bga_eia860_tbl.c.report_date >= start_date) if end_date is not None: end_date = pd.to_datetime(end_date) bga_eia860_select = bga_eia860_select.where( bga_eia860_tbl.c.report_date <= end_date) bga_eia860_df = pd.read_sql(bga_eia860_select, pudl_engine) out_df = helpers.extend_annual(bga_eia860_df, start_date=start_date, end_date=end_date) return out_df
def plants_steam_ferc1(testing=False): """ Select and join some useful fields from the FERC Form 1 steam table. Select the FERC Form 1 steam plant table entries, add in the reporting utility's name, and the PUDL ID for the plant and utility for readability and integration with other tables that have PUDL IDs. Args: ----- testing (bool) : True if we're using the pudl_test DB, False if we're using the live PUDL DB. False by default. Returns: -------- steam_df : a pandas dataframe. """ pudl_engine = init.connect_db(testing=testing) steam_ferc1_tbl = pt['plants_steam_ferc1'] steam_ferc1_select = sa.sql.select([steam_ferc1_tbl, ]) steam_df = pd.read_sql(steam_ferc1_select, pudl_engine) pu_ferc = plants_utils_ferc1(testing=testing) out_df = pd.merge(steam_df, pu_ferc, on=['respondent_id', 'plant_name']) first_cols = [ 'report_year', 'respondent_id', 'util_id_pudl', 'respondent_name', 'plant_id_pudl', 'plant_name' ] out_df = helpers.organize_cols(out_df, first_cols) return out_df
def generation_eia923(freq=None, testing=False, start_date=None, end_date=None): """ Sum net generation by generator at the specified frequency. In addition, some human readable plant and utility names, as well as some ID values for joining with other dataframes is added back in to the dataframe before it is returned. Args: ----- pudl_engine: An SQLAlchemy DB connection engine. freq: A string used to specify a time grouping frequency. testing (bool): True if we're using the pudl_test DB, False if we're using the live PUDL DB. False by default. Returns: -------- out_df: a pandas dataframe. """ pudl_engine = init.connect_db(testing=testing) g_eia923_tbl = pt['generation_eia923'] g_eia923_select = sa.sql.select([ g_eia923_tbl, ]) if start_date is not None: g_eia923_select = g_eia923_select.where( g_eia923_tbl.c.report_date >= start_date) if end_date is not None: g_eia923_select = g_eia923_select.where( g_eia923_tbl.c.report_date <= end_date) g_df = pd.read_sql(g_eia923_select, pudl_engine) # Index by date and aggregate net generation. # Create a date index for grouping based on freq by = ['plant_id_eia', 'generator_id'] if freq is not None: g_df = g_df.set_index(pd.DatetimeIndex(g_df.report_date)) by = by + [pd.Grouper(freq=freq)] g_gb = g_df.groupby(by=by) g_df = g_gb.agg({'net_generation_mwh': helpers.sum_na}).reset_index() # Grab EIA 860 plant and utility specific information: pu_eia = pudl.output.eia860.plants_utils_eia860(start_date=start_date, end_date=end_date, testing=testing) # Merge annual plant/utility data in with the more granular dataframe out_df = helpers.merge_on_date_year(g_df, pu_eia, on=['plant_id_eia']) if freq is None: out_df = out_df.drop(['id'], axis=1) # These ID fields are vital -- without them we don't have a complete record out_df = out_df.dropna(subset=[ 'plant_id_eia', 'plant_id_pudl', 'operator_id', 'util_id_pudl', 'generator_id', ]) first_cols = [ 'report_date', 'plant_id_eia', 'plant_id_pudl', 'plant_name', 'operator_id', 'util_id_pudl', 'operator_name', 'generator_id', ] # Re-arrange the columns for easier readability: out_df = helpers.organize_cols(out_df, first_cols) out_df['operator_id'] = out_df.operator_id.astype(int) out_df['util_id_pudl'] = out_df.util_id_pudl.astype(int) out_df['plant_id_pudl'] = out_df.plant_id_pudl.astype(int) return out_df
def boiler_fuel_eia923(freq=None, testing=False, start_date=None, end_date=None): """ Pull records from the boiler_fuel_eia923 table, in a given data range. Optionally, aggregate the records over some timescale -- monthly, yearly, quarterly, etc. as well as by fuel type within a plant. If the records are not being aggregated, all of the database fields are available. If they're being aggregated, then we preserve the following fields. Per-unit values are re-calculated based on the aggregated totals. Totals are summed across whatever time range is being used, within a given plant and fuel type. - fuel_consumed_units (sum) - fuel_mmbtu_per_unit (weighted average) - total_heat_content_mmbtu (sum) - sulfur_content_pct (weighted average) - ash_content_pct (weighted average) In addition, plant and utility names and IDs are pulled in from the EIA 860 tables. Args: ----- freq (str): a pandas timeseries offset alias. The original data is reported monthly, so the best time frequencies to use here are probably month start (freq='MS') and year start (freq='YS'). start_date & end_date: date-like objects, including strings of the form 'YYYY-MM-DD' which will be used to specify the date range of records to be pulled. Dates are inclusive. testing (bool): True if we're using the pudl_test DB, False if we're using the live PUDL DB. False by default. Returns: -------- bf_df: a pandas dataframe. """ pudl_engine = init.connect_db(testing=testing) bf_eia923_tbl = pt['boiler_fuel_eia923'] bf_eia923_select = sa.sql.select([ bf_eia923_tbl, ]) if start_date is not None: bf_eia923_select = bf_eia923_select.where( bf_eia923_tbl.c.report_date >= start_date) if end_date is not None: bf_eia923_select = bf_eia923_select.where( bf_eia923_tbl.c.report_date <= end_date) bf_df = pd.read_sql(bf_eia923_select, pudl_engine) # The total heat content is also useful in its own right, and we'll keep it # around. Also needed to calculate average heat content per unit of fuel. bf_df['total_heat_content_mmbtu'] = bf_df['fuel_consumed_units'] * \ bf_df['fuel_mmbtu_per_unit'] # Create a date index for grouping based on freq by = ['plant_id_eia', 'boiler_id', 'fuel_type_code_pudl'] if freq is not None: # In order to calculate the weighted average sulfur # content and ash content we need to calculate these totals. bf_df['total_sulfur_content'] = bf_df['fuel_consumed_units'] * \ bf_df['sulfur_content_pct'] bf_df['total_ash_content'] = bf_df['fuel_consumed_units'] * \ bf_df['ash_content_pct'] bf_df = bf_df.set_index(pd.DatetimeIndex(bf_df.report_date)) by = by + [pd.Grouper(freq=freq)] bf_gb = bf_df.groupby(by=by) # Sum up these totals within each group, and recalculate the per-unit # values (weighted in this case by fuel_consumed_units) bf_df = bf_gb.agg({ 'total_heat_content_mmbtu': helpers.sum_na, 'fuel_consumed_units': helpers.sum_na, 'total_sulfur_content': helpers.sum_na, 'total_ash_content': helpers.sum_na, }) bf_df['fuel_mmbtu_per_unit'] = \ bf_df['total_heat_content_mmbtu'] / bf_df['fuel_consumed_units'] bf_df['sulfur_content_pct'] = \ bf_df['total_sulfur_content'] / bf_df['fuel_consumed_units'] bf_df['ash_content_pct'] = \ bf_df['total_ash_content'] / bf_df['fuel_consumed_units'] bf_df = bf_df.reset_index() bf_df = bf_df.drop(['total_ash_content', 'total_sulfur_content'], axis=1) # Grab some basic plant & utility information to add. pu_eia = pudl.output.eia860.plants_utils_eia860(start_date=start_date, end_date=end_date, testing=False) out_df = helpers.merge_on_date_year(bf_df, pu_eia, on=['plant_id_eia']) if freq is None: out_df = out_df.drop(['id'], axis=1) out_df = out_df.dropna(subset=[ 'plant_id_eia', 'plant_id_pudl', 'operator_id', 'util_id_pudl', 'boiler_id', ]) first_cols = [ 'report_date', 'plant_id_eia', 'plant_id_pudl', 'plant_name', 'operator_id', 'util_id_pudl', 'operator_name', 'boiler_id', ] # Re-arrange the columns for easier readability: out_df = helpers.organize_cols(out_df, first_cols) out_df['operator_id'] = out_df.operator_id.astype(int) out_df['util_id_pudl'] = out_df.util_id_pudl.astype(int) out_df['plant_id_pudl'] = out_df.plant_id_pudl.astype(int) return out_df
def fuel_receipts_costs_eia923(freq=None, testing=False, start_date=None, end_date=None): """ Pull records from fuel_receipts_costs_eia923 table, in a given date range. Optionally, aggregate the records at a monthly or longer timescale, as well as by fuel type within a plant, by setting freq to something other than the default None value. If the records are not being aggregated, then all of the fields found in the PUDL database are available. If they are being aggregated, then the following fields are preserved, and appropriately summed or re-calculated based on the specified aggregation. In both cases, new total values are calculated, for total fuel heat content and total fuel cost. - plant_id_eia - report_date - fuel_type_code_pudl (formerly energy_source_simple) - fuel_qty_units (sum) - fuel_cost_per_mmbtu (weighted average) - total_fuel_cost (sum) - total_heat_content_mmbtu (sum) - heat_content_mmbtu_per_unit (weighted average) - sulfur_content_pct (weighted average) - ash_content_pct (weighted average) - mercury_content_ppm (weighted average) In addition, plant and utility names and IDs are pulled in from the EIA 860 tables. Args: ----- freq (str): a pandas timeseries offset alias. The original data is reported monthly, so the best time frequencies to use here are probably month start (freq='MS') and year start (freq='YS'). start_date & end_date: date-like objects, including strings of the form 'YYYY-MM-DD' which will be used to specify the date range of records to be pulled. Dates are inclusive. testing (bool): True if we're using the pudl_test DB, False if we're using the live PUDL DB. False by default. Returns: -------- frc_df: a pandas dataframe. """ pudl_engine = init.connect_db(testing=testing) # Most of the fields we want come direclty from Fuel Receipts & Costs frc_tbl = pt['fuel_receipts_costs_eia923'] frc_select = sa.sql.select([ frc_tbl, ]) # Need to re-integrate the MSHA coalmine info: cmi_tbl = pt['coalmine_eia923'] cmi_select = sa.sql.select([ cmi_tbl, ]) cmi_df = pd.read_sql(cmi_select, pudl_engine) if start_date is not None: frc_select = frc_select.where(frc_tbl.c.report_date >= start_date) if end_date is not None: frc_select = frc_select.where(frc_tbl.c.report_date <= end_date) frc_df = pd.read_sql(frc_select, pudl_engine) frc_df = pd.merge(frc_df, cmi_df, how='left', left_on='mine_id_pudl', right_on='id') cols_to_drop = ['fuel_receipt_id', 'mine_id_pudl', 'id'] frc_df = frc_df.drop(cols_to_drop, axis=1) # Calculate a few totals that are commonly needed: frc_df['total_heat_content_mmbtu'] = \ frc_df['heat_content_mmbtu_per_unit'] * frc_df['fuel_qty_units'] frc_df['total_fuel_cost'] = \ frc_df['total_heat_content_mmbtu'] * frc_df['fuel_cost_per_mmbtu'] if freq is not None: by = ['plant_id_eia', 'fuel_type_code_pudl', pd.Grouper(freq=freq)] # Create a date index for temporal resampling: frc_df = frc_df.set_index(pd.DatetimeIndex(frc_df.report_date)) # Sum up these values so we can calculate quantity weighted averages frc_df['total_ash_content'] = \ frc_df['ash_content_pct'] * frc_df['fuel_qty_units'] frc_df['total_sulfur_content'] = \ frc_df['sulfur_content_pct'] * frc_df['fuel_qty_units'] frc_df['total_mercury_content'] = \ frc_df['mercury_content_ppm'] * frc_df['fuel_qty_units'] frc_gb = frc_df.groupby(by=by) frc_df = frc_gb.agg({ 'fuel_qty_units': helpers.sum_na, 'total_heat_content_mmbtu': helpers.sum_na, 'total_fuel_cost': helpers.sum_na, 'total_sulfur_content': helpers.sum_na, 'total_ash_content': helpers.sum_na, 'total_mercury_content': helpers.sum_na, }) frc_df['fuel_cost_per_mmbtu'] = \ frc_df['total_fuel_cost'] / frc_df['total_heat_content_mmbtu'] frc_df['heat_content_mmbtu_per_unit'] = \ frc_df['total_heat_content_mmbtu'] / frc_df['fuel_qty_units'] frc_df['sulfur_content_pct'] = \ frc_df['total_sulfur_content'] / frc_df['fuel_qty_units'] frc_df['ash_content_pct'] = \ frc_df['total_ash_content'] / frc_df['fuel_qty_units'] frc_df['mercury_content_ppm'] = \ frc_df['total_mercury_content'] / frc_df['fuel_qty_units'] frc_df = frc_df.reset_index() frc_df = frc_df.drop([ 'total_ash_content', 'total_sulfur_content', 'total_mercury_content' ], axis=1) # Bring in some generic plant & utility information: pu_eia = pudl.output.eia860.plants_utils_eia860(start_date=start_date, end_date=end_date, testing=testing) out_df = helpers.merge_on_date_year(frc_df, pu_eia, on=['plant_id_eia']) # Drop any records where we've failed to get the 860 data merged in... out_df = out_df.dropna(subset=['operator_id', 'operator_name']) if freq is None: # There are a couple of invalid records with no specified fuel. out_df = out_df.dropna(subset=['fuel_group_code']) first_cols = [ 'report_date', 'plant_id_eia', 'plant_id_pudl', 'plant_name', 'operator_id', 'util_id_pudl', 'operator_name', ] # Re-arrange the columns for easier readability: out_df = helpers.organize_cols(out_df, first_cols) # Clean up the types of a few columns... out_df['plant_id_eia'] = out_df.plant_id_eia.astype(int) out_df['plant_id_pudl'] = out_df.plant_id_pudl.astype(int) out_df['operator_id'] = out_df.operator_id.astype(int) out_df['util_id_pudl'] = out_df.util_id_pudl.astype(int) return out_df
def generation_fuel_eia923(freq=None, testing=False, start_date=None, end_date=None): """ Pull records from the generation_fuel_eia923 table, in a given date range. Optionally, aggregate the records over some timescale -- monthly, yearly, quarterly, etc. as well as by fuel type within a plant. If the records are not being aggregated, all of the database fields are available. If they're being aggregated, then we preserve the following fields. Per-unit values are re-calculated based on the aggregated totals. Totals are summed across whatever time range is being used, within a given plant and fuel type. - plant_id_eia - report_date - fuel_type_code_pudl - fuel_consumed_units - fuel_consumed_for_electricity_units - fuel_mmbtu_per_unit - fuel_consumed_mmbtu - fuel_consumed_for_electricity_mmbtu - net_generation_mwh In addition, plant and utility names and IDs are pulled in from the EIA 860 tables. Args: ----- testing (bool): True if we are connecting to the pudl_test DB, False if we're using the live DB. False by default. freq (str): a pandas timeseries offset alias. The original data is reported monthly, so the best time frequencies to use here are probably month start (freq='MS') and year start (freq='YS'). start_date & end_date: date-like objects, including strings of the form 'YYYY-MM-DD' which will be used to specify the date range of records to be pulled. Dates are inclusive. Returns: -------- gf_df: a pandas dataframe. """ pudl_engine = init.connect_db(testing=testing) gf_tbl = pt['generation_fuel_eia923'] gf_select = sa.sql.select([ gf_tbl, ]) if start_date is not None: gf_select = gf_select.where(gf_tbl.c.report_date >= start_date) if end_date is not None: gf_select = gf_select.where(gf_tbl.c.report_date <= end_date) gf_df = pd.read_sql(gf_select, pudl_engine) cols_to_drop = ['id'] gf_df = gf_df.drop(cols_to_drop, axis=1) # fuel_type_code_pudl was formerly aer_fuel_category by = ['plant_id_eia', 'fuel_type_code_pudl'] if freq is not None: # Create a date index for temporal resampling: gf_df = gf_df.set_index(pd.DatetimeIndex(gf_df.report_date)) by = by + [pd.Grouper(freq=freq)] # Sum up these values so we can calculate quantity weighted averages gf_gb = gf_df.groupby(by=by) gf_df = gf_gb.agg({ 'fuel_consumed_units': helpers.sum_na, 'fuel_consumed_for_electricity_units': helpers.sum_na, 'fuel_consumed_mmbtu': helpers.sum_na, 'fuel_consumed_for_electricity_mmbtu': helpers.sum_na, 'net_generation_mwh': helpers.sum_na, }) gf_df['fuel_mmbtu_per_unit'] = \ gf_df['fuel_consumed_mmbtu'] / gf_df['fuel_consumed_units'] gf_df = gf_df.reset_index() # Bring in some generic plant & utility information: pu_eia = pudl.output.eia860.plants_utils_eia860(start_date=start_date, end_date=end_date, testing=testing) out_df = helpers.merge_on_date_year(gf_df, pu_eia, on=['plant_id_eia']) # Drop any records where we've failed to get the 860 data merged in... out_df = out_df.dropna(subset=[ 'plant_id_eia', 'plant_id_pudl', 'plant_name', 'operator_id', 'util_id_pudl', 'operator_name', ]) first_cols = [ 'report_date', 'plant_id_eia', 'plant_id_pudl', 'plant_name', 'operator_id', 'util_id_pudl', 'operator_name', ] out_df = helpers.organize_cols(out_df, first_cols) # Clean up the types of a few columns... out_df['plant_id_eia'] = out_df.plant_id_eia.astype(int) out_df['plant_id_pudl'] = out_df.plant_id_pudl.astype(int) out_df['operator_id'] = out_df.operator_id.astype(int) out_df['util_id_pudl'] = out_df.util_id_pudl.astype(int) return out_df
def fercplants(plant_tables=[ 'f1_steam', 'f1_gnrt_plant', 'f1_hydro', 'f1_pumped_storage' ], years=constants.working_years['ferc1'], new=True, min_capacity=5.0): """ Generate a list of FERC plants for matching with EIA plants. There are several kinds of FERC plants, with different information stored in different FERC database tables. FERC doesn't provide any kind of plant_id like EIA, so the unique identifier that we're using is a combination of the respondent_id (the utility) and plant_name. For each table in the FERC DB that contains per-plant information, we'll grab the respondent_id and plant_name, and join that with respondent_name so that the utility is more readily identifiable. We'll also add a column indicating what table the plant came from, and return a DataFrame with those four columns in it, for use in the matching. That matching currently happens in an Excel spreadsheet, so you will likely want to output the resulting DataFrame as a CSV or XLSX file. The function can generate an exhaustive list of plants, or it can only grab plants from a particular range of years. It can also optionally grab only new plants i.e. those which do not appear in the existing PUDL database. This is useful for finding new plants when a new year of FERC data comes out. Args: f1_tables (list): A list of tables in the FERC Form 1 DB whose plants you want to get information about. Can include any of: f1_steam, f1_gnrt_plant, f1_hydro, and f1_pumped_storage. years (list): The set of years for which you wish to obtain plant by plant information. new (boolean): If True (the default) then return only those plants which appear in the years of FERC data being specified by years, and NOT also in the currently initialized PUDL DB. min_capacity (float): The smallest size plant, in MW, that should be included in the output. This avoids most of the plants being tiny. Returns: DataFrame: with four columns: respondent_id, respondent_name, plant_name, and plant_table. """ # Need to be able to use years outside the "valid" range if we're trying # to get new plant ID info... if not new: for yr in years: assert yr in constants.working_years['ferc1'] okay_tbls = ['f1_steam', 'f1_gnrt_plant', 'f1_hydro', 'f1_pumped_storage'] # Function only knows how to work with these tables. for tbl in plant_tables: assert tbl in okay_tbls f1_engine = pudl.extract.ferc1.connect_db() # Need to make sure we have a populated metadata object, which isn't # always the case, since folks often are not initializing the FERC DB. pudl.extract.ferc1.define_db(max(constants.working_years['ferc1']), constants.ferc1_working_tables, pudl.extract.ferc1.ferc1_meta) f1_tbls = pudl.extract.ferc1.ferc1_meta.tables # FERC doesn't use the sme column names for the same values across all of # Their tables... but all of these are cpacity in MW. capacity_cols = { 'f1_steam': 'tot_capacity', 'f1_gnrt_plant': 'capacity_rating', 'f1_hydro': 'tot_capacity', 'f1_pumped_storage': 'tot_capacity' } rspndnt_tbl = f1_tbls['f1_respondent_id'] ferc1_plants_all = pd.DataFrame() for tbl in plant_tables: plant_select = sa.sql.select([ f1_tbls[tbl].c.respondent_id, f1_tbls[tbl].c.plant_name, rspndnt_tbl.c.respondent_name ]).distinct().where( sa.and_( f1_tbls[tbl].c.respondent_id == rspndnt_tbl.c.respondent_id, f1_tbls[tbl].c.plant_name != '', f1_tbls[tbl].columns[capacity_cols[tbl]] >= min_capacity, f1_tbls[tbl].c.report_year.in_(years))) # Add all the plants from the current table to our bigger list: new_plants = pd.read_sql(plant_select, f1_engine) new_plants.respondent_name = new_plants.respondent_name.str.strip() new_plants.respondent_name = new_plants.respondent_name.str.title() new_plants.plant_name = new_plants.plant_name.str.strip().str.title() new_plants['plant_table'] = tbl ferc1_plants_all = ferc1_plants_all.append(new_plants[[ 'respondent_id', 'respondent_name', 'plant_name', 'plant_table' ]]) # If we're only trying to get the NEW plants, then we need to see which # ones we've already got in the PUDL DB, and look at what's different. if new: ferc1_plants_all = ferc1_plants_all.set_index( ['respondent_id', 'plant_name']) pudl_engine = init.connect_db() pudl_tbls = pudl.models.PUDLBase.metadata.tables ferc1_plants_tbl = pudl_tbls['plants_ferc'] ferc1_plants_select = sa.sql.select( [ferc1_plants_tbl.c.respondent_id, ferc1_plants_tbl.c.plant_name]).distinct() ferc1_plants_old = pd.read_sql(ferc1_plants_select, pudl_engine) ferc1_plants_old = ferc1_plants_old.set_index( ['respondent_id', 'plant_name']) # Take the difference between the two table indexes -- I.e. get a # list of just the index values that appear in the FERC index, but # not in the PUDL index. new_index = ferc1_plants_all.index.difference(ferc1_plants_old.index) ferc1_plants = ferc1_plants_all.loc[new_index].reset_index() else: ferc1_plants = ferc1_plants_all return ferc1_plants
def ownership_eia860(start_date=None, end_date=None, testing=False): """ Pull a useful set of fields related to ownership_eia860 table. Args: ----- start_date (date): date of the earliest data to retrieve end_date (date): date of the latest data to retrieve testing (bool): True if we're connecting to the pudl_test DB, False if we're connecting to the live PUDL DB. False by default. Returns: -------- out_df (pandas dataframe) """ pudl_engine = init.connect_db(testing=testing) o_eia860_tbl = pt['ownership_eia860'] o_eia860_select = sa.sql.select([ o_eia860_tbl, ]) o_df = pd.read_sql(o_eia860_select, pudl_engine) pu_eia = plants_utils_eia860(start_date=start_date, end_date=end_date, testing=testing) pu_eia = pu_eia[[ 'plant_id_eia', 'plant_id_pudl', 'util_id_pudl', 'report_date' ]] o_df['report_date'] = pd.to_datetime(o_df.report_date) out_df = pd.merge(o_df, pu_eia, how='left', on=['report_date', 'plant_id_eia']) out_df = out_df.drop(['id'], axis=1) out_df = out_df.dropna(subset=[ 'plant_id_eia', 'plant_id_pudl', 'operator_id', 'util_id_pudl', 'generator_id', 'ownership_id', ]) first_cols = [ 'report_date', 'plant_id_eia', 'plant_id_pudl', 'plant_name', 'operator_id', 'util_id_pudl', 'operator_name', 'generator_id', 'ownership_id', 'owner_name', ] # Re-arrange the columns for easier readability: out_df = helpers.organize_cols(out_df, first_cols) out_df['plant_id_pudl'] = out_df.plant_id_pudl.astype(int) out_df['util_id_pudl'] = out_df.util_id_pudl.astype(int) out_df = helpers.extend_annual(out_df, start_date=start_date, end_date=end_date) return out_df
def generators_eia860(start_date=None, end_date=None, testing=False): """ Pull all fields reported in the generators_eia860 table. Merge in other useful fields including the latitude & longitude of the plant that the generators are part of, canonical plant & operator names and the PUDL IDs of the plant and operator, for merging with other PUDL data sources. Fill in data for adjacent years if requested, but never fill in earlier than the earliest working year of data for EIA923, and never add more than one year on after the reported data (since there should at most be a one year lag between EIA923 and EIA860 reporting) Args: start_date (date): the earliest EIA 860 data to retrieve or synthesize end_date (date): the latest EIA 860 data to retrieve or synthesize testing (bool): Connect to the live PUDL DB or the testing DB? Returns: A pandas dataframe. """ pudl_engine = init.connect_db(testing=testing) # Almost all the info we need will come from here. gens_eia860_tbl = pt['generators_eia860'] gens_eia860_select = sa.sql.select([ gens_eia860_tbl, ]) # To get the Lat/Lon coordinates plants_eia860_tbl = pt['plants_eia860'] plants_eia860_select = sa.sql.select([ plants_eia860_tbl.c.report_date, plants_eia860_tbl.c.plant_id_eia, plants_eia860_tbl.c.latitude, plants_eia860_tbl.c.longitude, plants_eia860_tbl.c.balancing_authority_code, plants_eia860_tbl.c.balancing_authority_name, plants_eia860_tbl.c.iso_rto, plants_eia860_tbl.c.iso_rto_code, ]) if start_date is not None: start_date = pd.to_datetime(start_date) # We don't want to get too crazy with the date extensions... # start_date shouldn't go back before the earliest working year of # EIA 923 eia923_start_date = \ pd.to_datetime('{}-01-01'.format( min(constants.working_years['eia923']))) assert start_date >= eia923_start_date gens_eia860_select = gens_eia860_select.where( gens_eia860_tbl.c.report_date >= start_date) plants_eia860_select = plants_eia860_select.where( plants_eia860_tbl.c.report_date >= start_date) if end_date is not None: end_date = pd.to_datetime(end_date) # end_date shouldn't be more than one year ahead of the most recent # year for which we have EIA 860 data: eia860_end_date = \ pd.to_datetime('{}-12-31'.format( max(constants.working_years['eia860']))) assert end_date <= eia860_end_date + pd.DateOffset(years=1) gens_eia860_select = gens_eia860_select.where( gens_eia860_tbl.c.report_date <= end_date) plants_eia860_select = plants_eia860_select.where( plants_eia860_tbl.c.report_date <= end_date) gens_eia860 = pd.read_sql(gens_eia860_select, pudl_engine) # Canonical sources for these fields are elsewhere. We will merge them in. gens_eia860 = gens_eia860.drop( ['operator_id', 'operator_name', 'plant_name'], axis=1) plants_eia860_df = pd.read_sql(plants_eia860_select, pudl_engine) out_df = pd.merge(gens_eia860, plants_eia860_df, how='left', on=['report_date', 'plant_id_eia']) out_df.report_date = pd.to_datetime(out_df.report_date) # Bring in some generic plant & utility information: pu_eia = plants_utils_eia860(start_date=start_date, end_date=end_date, testing=testing) out_df = pd.merge(out_df, pu_eia, on=['report_date', 'plant_id_eia']) # Drop a few extraneous fields... cols_to_drop = [ 'id', ] out_df = out_df.drop(cols_to_drop, axis=1) # In order to be able to differentiate betweet single and multi-fuel # plants, we need to count how many different simple energy sources there # are associated with plant's generators. This allows us to do the simple # lumping of an entire plant's fuel & generation if its primary fuels # are homogeneous, and split out fuel & generation by fuel if it is # hetereogeneous. ft_count = out_df[['plant_id_eia', 'fuel_type_code_pudl', 'report_date']].\ drop_duplicates().groupby(['plant_id_eia', 'report_date']).count() ft_count = ft_count.reset_index() ft_count = ft_count.rename( columns={'fuel_type_code_pudl': 'fuel_type_count'}) out_df = pd.merge(out_df, ft_count, how='left', on=['plant_id_eia', 'report_date']) first_cols = [ 'report_date', 'plant_id_eia', 'plant_id_pudl', 'plant_name', 'operator_id', 'util_id_pudl', 'operator_name', 'generator_id', ] # Re-arrange the columns for easier readability: out_df = helpers.organize_cols(out_df, first_cols) out_df = helpers.extend_annual(out_df, start_date=start_date, end_date=end_date) out_df = out_df.sort_values( ['report_date', 'plant_id_eia', 'generator_id']) return out_df