Ejemplo n.º 1
0
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
Ejemplo n.º 2
0
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
Ejemplo n.º 3
0
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
Ejemplo n.º 4
0
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
Ejemplo n.º 5
0
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
Ejemplo n.º 6
0
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
Ejemplo n.º 7
0
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
Ejemplo n.º 8
0
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
Ejemplo n.º 9
0
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
Ejemplo n.º 10
0
def mcoe(pudl_out,
         min_heat_rate=5.5, min_fuel_cost_per_mwh=0.0,
         min_cap_fact=0.0, max_cap_fact=1.5, verbose=False):
    """
    Compile marginal cost of electricity (MCOE) at the generator level.

    Use data from EIA 923, EIA 860, and (eventually) FERC Form 1 to estimate
    the MCOE of individual generating units. The calculation is performed at
    the time resolution, and for the period indicated by the pudl_out object.
    that is passed in.

    Args:
    -----
        pudl_out: a PudlTabl object, specifying the time resolution and
            date range for which the calculations should be performed.
        min_heat_rate: lowest plausible heat rate, in mmBTU/MWh. Any MCOE
            records with lower heat rates are presumed to be invalid, and are
            discarded before returning.
        min_cap_fact, max_cap_fact: minimum & maximum generator capacity
            factor. Generator records with a lower capacity factor will be
            filtered out before returning. This allows the user to exclude
            generators that aren't being used enough to have valid.
        min_fuel_cost_per_mwh: minimum fuel cost on a per MWh basis that is
            required for a generator record to be considered valid. For some
            reason there are now a large number of $0 fuel cost records, which
            previously would have been NaN.

    Returns:
    --------
        mcoe_out: a dataframe organized by date and generator, with lots of
            juicy information about the generators -- including fuel cost on a
            per MWh and MMBTU basis, heat rates, and neg generation.

    """
    # Bring together the fuel cost and capacity factor dataframes, which
    # also include heat rate information.
    mcoe_out = pd.merge(pudl_out.fuel_cost(verbose=verbose),
                        pudl_out.capacity_factor(verbose=verbose)[
                            ['report_date', 'plant_id_eia',
                             'generator_id', 'capacity_factor']],
                        on=['report_date', 'plant_id_eia', 'generator_id'],
                        how='left')

    # Bring the PUDL Unit IDs into the output dataframe so we can see how
    # the generators are really grouped.
    mcoe_out = helpers.merge_on_date_year(
        mcoe_out,
        pudl_out.bga()[['report_date',
                        'plant_id_eia',
                        'unit_id_pudl',
                        'generator_id']].drop_duplicates(),
        how='left',
        on=['plant_id_eia', 'generator_id'])

    # Instead of getting the total MMBTU through this multiplication... we
    # could also calculate the total fuel consumed on a per-unit basis, from
    # the boiler_fuel table, and then determine what proportion should be
    # distributed to each generator based on its heat-rate and net generation.
    mcoe_out['total_mmbtu'] = \
        mcoe_out.net_generation_mwh * mcoe_out.heat_rate_mmbtu_mwh
    mcoe_out['total_fuel_cost'] = \
        mcoe_out.total_mmbtu * mcoe_out.fuel_cost_per_mmbtu

    simplified_gens_eia860 = pudl_out.gens_eia860().drop([
        'plant_id_pudl',
        'plant_name',
        'operator_id',
        'util_id_pudl',
        'operator_name',
        'fuel_type_count',
        'fuel_type_code_pudl'
    ], axis=1)
    mcoe_out = helpers.merge_on_date_year(mcoe_out, simplified_gens_eia860,
                                          on=['plant_id_eia',
                                              'generator_id'])

    first_cols = ['report_date',
                  'plant_id_eia',
                  'plant_id_pudl',
                  'unit_id_pudl',
                  'generator_id',
                  'plant_name',
                  'operator_id',
                  'util_id_pudl',
                  'operator_name']
    mcoe_out = helpers.organize_cols(mcoe_out, first_cols)
    mcoe_out = mcoe_out.sort_values(
        ['plant_id_eia', 'unit_id_pudl', 'generator_id', 'report_date']
    )

    # Filter the output based on the range of validity supplied by the user:
    if min_heat_rate is not None:
        mcoe_out = mcoe_out[mcoe_out.heat_rate_mmbtu_mwh >= min_heat_rate]
    if min_fuel_cost_per_mwh is not None:
        mcoe_out = mcoe_out[mcoe_out.fuel_cost_per_mwh > min_fuel_cost_per_mwh]
    if min_cap_fact is not None:
        mcoe_out = mcoe_out[mcoe_out.capacity_factor >= min_cap_fact]
    if max_cap_fact is not None:
        mcoe_out = mcoe_out[mcoe_out.capacity_factor <= max_cap_fact]

    return mcoe_out