Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Revert "ELEX-2763-estimandizer" #74

Merged
merged 1 commit into from
Sep 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 0 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,26 +75,6 @@ Parameters for the CLI tool:
Note: When running the model with multiple fixed effects, make sure they are not linearly dependent. For example, `county_fips` and `county_classification` are linearly dependent when run together. That's because every county is in one county class, so all the fixed effect columns of the counties in the county class sum up to the fixed effect column of that county class.


#### Custom Estimands

It's possible to create a custom estimand based on other data elements. Here's how to create a new estimand called "my_estimand":

1. In `src/elexmodel/handlers/data/Estimandizer.py`, create a function with the signature `def my_estimand(data_df)`.
2. In `my_estimand()`, use the columns in `data_df` to create a new column, either `baseline_my_estimand` or `results_my_estimand` as necessary. See the `party_vote_share_dem` function for an example.
3. Specify `my_estimand` as one of your estimands. For example, via the command line:

```
elexmodel 2017-11-07_VA_G --estimands my_estimand --office_id=G --geographic_unit_type=county --percent_reporting 50
```

Your output should have columns including `baseline_my_estimand`, `results_my_estimand`, and related columns for the prediction intervals, if using them.

Here's an example showing multiple estimands, including `my_estimand`:

```
elexmodel 2017-11-07_VA_G --estimands=turnout --estimands my_estimand --estimands party_vote_share_dem --office_id=G --geographic_unit_type=county --percent_reporting 50
```

#### Python

##### Model Parameters
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ max-line-length = 120
[pylint]
max-line-length = 120
good-names= on, x, df, NonparametricElectionModel, GaussianElectionModel,
BaseElectionModel, qr, X, y, f, LiveData, n, Featurizer, Estimandizer, fe, PreprocessedData, CombinedData,
BaseElectionModel, qr, X, y, f, LiveData, n, Featurizer, fe, PreprocessedData, CombinedData,
ModelResults, GaussianModel, MODEL_THRESHOLD, LOG, w, df_X, df_y, v, n, g, a, b
disable=missing-function-docstring, missing-module-docstring, missing-class-docstring, #missing
too-many-arguments, too-many-locals, too-many-branches, too-many-instance-attributes, too-many-statements, #structure: too-many
Expand Down
4 changes: 2 additions & 2 deletions src/elexmodel/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ class PythonLiteralOption(click.Option):
def type_cast_value(self, ctx, value):
try:
return ast.literal_eval(value)
except ValueError as e:
raise click.BadParameter(value) from e
except ValueError:
raise click.BadParameter(value)


@click.command()
Expand Down
22 changes: 6 additions & 16 deletions src/elexmodel/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,27 +56,21 @@ def _check_input_parameters(
offices = config_handler.get_offices()
if office not in offices:
raise ValueError(f"Office '{office}' is not valid. Please check config.")

estimands_in_config = config_handler.get_estimands(office)
extra_estimands = set(estimands).difference(set(estimands_in_config))
if len(extra_estimands) > 0:
# this is ok; later they'll all be created by the Estimandizer
LOG.info("Found additional estimands that were not specified in the config file: %s", extra_estimands)

valid_estimands = config_handler.get_estimands(office)
for estimand in estimands:
if estimand not in valid_estimands:
raise ValueError(f"Estimand: '{estimand}' is not valid. Please check config")
geographic_unit_types = config_handler.get_geographic_unit_types(office)
if geographic_unit_type not in geographic_unit_types:
raise ValueError(f"Geographic unit type: '{geographic_unit_type}' is not valid. Please check config")

model_features = config_handler.get_features(office)
invalid_features = list(set(features).difference(set(model_features)))
invalid_features = [feature for feature in features if feature not in model_features]
if len(invalid_features) > 0:
raise ValueError(f"Feature(s): {invalid_features} not valid. Please check config")

model_aggregates = config_handler.get_aggregates(office)
invalid_aggregates = [aggregate for aggregate in aggregates if aggregate not in model_aggregates]
if len(invalid_aggregates) > 0:
raise ValueError(f"Aggregate(s): {invalid_aggregates} not valid. Please check config")

model_fixed_effects = config_handler.get_fixed_effects(office)
if isinstance(fixed_effects, dict):
invalid_fixed_effects = [
Expand All @@ -88,16 +82,14 @@ def _check_input_parameters(
]
if len(invalid_fixed_effects) > 0:
raise ValueError(f"Fixed effect(s): {invalid_fixed_effects} not valid. Please check config")

if pi_method not in {"gaussian", "nonparametric"}:
raise ValueError(
f"Prediction interval method: {pi_method} is not valid. \
pi_method has to be either `gaussian` or `nonparametric`."
)

if not isinstance(model_parameters, dict):
raise ValueError("model_paramters is not valid. Has to be a dict.")
if model_parameters != {}:
elif model_parameters != {}:
if "lambda_" in model_parameters and (
not isinstance(model_parameters["lambda_"], (float, int)) or model_parameters["lambda_"] < 0
):
Expand All @@ -110,10 +102,8 @@ def _check_input_parameters(
elif pi_method == "nonparametric":
if "robust" in model_parameters and not isinstance(model_parameters["robust"], bool):
raise ValueError("robust is not valid. Has to be a boolean.")

if handle_unreporting not in {"drop", "zero"}:
raise ValueError("handle_unreporting must be either `drop` or `zero`")

return True

def get_aggregate_list(self, office, aggregate):
Expand Down
9 changes: 1 addition & 8 deletions src/elexmodel/handlers/data/CombinedData.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from elexmodel.handlers import s3
from elexmodel.handlers.data.Estimandizer import Estimandizer
from elexmodel.utils.file_utils import S3_FILE_PATH, TARGET_BUCKET, convert_df_to_csv


Expand All @@ -17,12 +16,6 @@ def __init__(
handle_unreporting="drop",
):
self.estimands = estimands

estimandizer = Estimandizer()
(current_data, _) = estimandizer.check_and_create_estimands(
current_data.copy(), self.estimands, False, current_data=True
)

# if we're running this for a past election, drop results columns from preprocessed data
# so we use results_{estimand} numbers from current_data
preprocessed_results_columns = list(filter(lambda col: col.startswith("results_"), preprocessed_data.columns))
Expand All @@ -34,7 +27,7 @@ def __init__(
# if unreporting is 'drop' then drop units that are not reporting (ie. units where results are na)
# this is necessary if units will not be returning results in this election,
# but we didn't know that (ie. townships)
result_cols = [f"results_{estimand}" for estimand in self.estimands]
result_cols = [f"results_{estimand}" for estimand in estimands]
if handle_unreporting == "drop":
# Drop the whole row if an estimand is not reporting
data = data.dropna(axis=0, how="any", subset=result_cols)
Expand Down
88 changes: 0 additions & 88 deletions src/elexmodel/handlers/data/Estimandizer.py

This file was deleted.

27 changes: 18 additions & 9 deletions src/elexmodel/handlers/data/LiveData.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@
import numpy as np
import pandas as pd

from elexmodel.handlers.data.Estimandizer import Estimandizer
from elexmodel.utils.file_utils import get_directory_path


class MockLiveDataHandlerException(Exception):
pass


class MockLiveDataHandler:
"""
Handles current data, which we would pull from Dynamo on an election night
Expand All @@ -32,7 +35,6 @@ def __init__(
self.s3_client = s3_client
self.historical = historical
self.unexpected_rows = unexpected_units
self.estimandizer = Estimandizer()

self.shuffle_columns = [
"postal_code",
Expand All @@ -44,7 +46,7 @@ def __init__(
self.data = data
if data is not None:
# passed in as a df
data_for_estimands = self.load_data(data)
data_for_estimands = self.load_data(data, estimands, historical)
self.data = data_for_estimands
else:
self.data = self.get_data()
Expand Down Expand Up @@ -74,19 +76,26 @@ def get_data(self):
live_data,
dtype={"geographic_unit_fips": str, "geographic_unit_type": str, "county_fips": str, "district": str},
)
data = self.load_data(data)
data = self.load_data(data, self.estimands, self.historical)
return data

def get_live_data_file_path(self):
directory_path = get_directory_path()
return f"{directory_path}/data/{self.election_id}/{self.office_id}/data_{self.geographic_unit_type}.csv"

def load_data(self, data):
def load_data(self, data, estimands, historical):
columns_to_return = ["postal_code", "geographic_unit_fips"]

(data, more_columns) = self.estimandizer.check_and_create_estimands(data, self.estimands, self.historical)
columns_to_return += more_columns

for estimand in estimands:
if historical:
data[f"results_{estimand}"] = np.nan
results_column_names = [x for x in data.columns if x.startswith("results")]
# If this is not a historical run, then this is a live election
# so we are expecting that there will be actual results data
if not self.historical and len(results_column_names) == 0:
raise MockLiveDataHandlerException("This is not a test election, it is missing results data")
if f"results_{estimand}" not in results_column_names:
raise MockLiveDataHandlerException("This is missing results data for estimand: ", estimand)
columns_to_return.append(f"results_{estimand}")
self.shuffle_dataframe = data[self.shuffle_columns].copy()
return data[columns_to_return].copy()

Expand Down
21 changes: 15 additions & 6 deletions src/elexmodel/handlers/data/PreprocessedData.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

import pandas as pd

from elexmodel.handlers.data.Estimandizer import Estimandizer
from elexmodel.utils.file_utils import create_directory, get_directory_path

LOG = logging.getLogger(__name__)
Expand Down Expand Up @@ -36,12 +35,11 @@ def __init__(
self.s3_client = s3_client
self.estimand_baselines = estimand_baselines
self.historical = historical
self.estimandizer = Estimandizer()

self.local_file_path = self.get_preprocessed_data_path()

if data is not None:
self.data = self.load_data(data)
self.data = self.load_data(data, estimand_baselines)
else:
self.data = self.get_data()

Expand All @@ -63,7 +61,7 @@ def get_data(self):
preprocessed_data = self.local_file_path

data = pd.read_csv(preprocessed_data, dtype={"geographic_unit_fips": str, "county_fips": str, "district": str})
return self.load_data(data)
return self.load_data(data, self.estimand_baselines)

def get_preprocessed_data_path(self):
directory_path = get_directory_path()
Expand All @@ -78,13 +76,24 @@ def select_rows_in_states(self, data, states_with_election):
)
return data

def load_data(self, preprocessed_data):
def load_data(self, preprocessed_data, estimand_baselines):
"""
Load preprocessed csv data as df
"""
LOG.info("Loading preprocessed data: %s, %s, %s", self.election_id, self.office, self.geographic_unit_type)

return self.estimandizer.add_estimand_baselines(preprocessed_data, self.estimand_baselines, self.historical)
if self.historical:
# if we are in a historical election we are only reading preprocessed data to get
# the historical election results of the currently reporting units.
# so we don't care about the total voters or the baseline election.
return preprocessed_data

for estimand, pointer in estimand_baselines.items():
baseline_name = f"baseline_{pointer}"
# Adding one to prevent zero divison
preprocessed_data[f"last_election_results_{estimand}"] = preprocessed_data[baseline_name].copy() + 1

return preprocessed_data

def save_data(self, preprocessed_data):
if not Path(self.local_file_path).parent.exists():
Expand Down
2 changes: 1 addition & 1 deletion src/elexmodel/logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"elexmodel": {
"handlers": ["default"],
"level": "INFO",
"propagate": True,
"propagate": False,
}
},
}
Expand Down
Loading
Loading