From 9ac7858f1d2569c08596a8375139e654b82219cb Mon Sep 17 00:00:00 2001 From: e-belfer Date: Tue, 7 Jan 2025 14:33:13 -0500 Subject: [PATCH] Address ruff failures and unit test failure, move analyzing code to notebook --- .../phmsagas_distribution.ipynb | 827 +++++------------- src/pudl/helpers.py | 179 +--- test/unit/metadata_test.py | 8 +- 3 files changed, 220 insertions(+), 794 deletions(-) diff --git a/notebooks/work-in-progress/phmsagas_distribution.ipynb b/notebooks/work-in-progress/phmsagas_distribution.ipynb index 69538f3dad..f746db056a 100644 --- a/notebooks/work-in-progress/phmsagas_distribution.ipynb +++ b/notebooks/work-in-progress/phmsagas_distribution.ipynb @@ -9,7 +9,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -24,25 +24,16 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "No dagster instance configuration file (dagster.yaml) found at /Users/sam/Documents/pudl-data/dagster_home. Defaulting to loading and storing all metadata with /Users/sam/Documents/pudl-data/dagster_home. If this is the desired behavior, create an empty dagster.yaml file in /Users/sam/Documents/pudl-data/dagster_home.\n", - "2024-11-03 16:45:36 -0500 - dagster - DEBUG - system - Loading file from: /Users/sam/Documents/pudl-data/dagster_home/storage/raw_phmsagas__yearly_distribution using PickledObjectFilesystemIOManager...\n" - ] - } - ], + "outputs": [], "source": [ "raw_df = defs.load_asset_value(AssetKey(\"raw_phmsagas__yearly_distribution\"))" ] }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -105,18 +96,9 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/Users/sam/Documents/pudl/src/pudl/helpers.py:1033: FutureWarning: Downcasting behavior in `replace` is deprecated and will be removed in a future version. To retain the old behavior, explicitly call `result.infer_objects(copy=False)`. To opt-in to the future behavior, set `pd.set_option('future.no_silent_downcasting', True)`\n", - " df = df.replace(na_patterns, np.nan, regex=True)\n" - ] - } - ], + "outputs": [], "source": [ "df = raw_df.loc[\n", " :, YEARLY_DISTRIBUTION_OPERATORS_COLUMNS[\"columns_to_keep\"]\n", @@ -190,332 +172,36 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
report_datereport_numberreport_submission_typereport_yearoperator_id_phmsaoperator_name_phmsaoffice_address_streetoffice_address_cityoffice_address_stateoffice_address_zipoffice_address_countyheadquarters_address_streetheadquarters_address_cityheadquarters_address_stateheadquarters_address_zipheadquarters_address_countyexcavation_damage_excavation_practicesexcavation_damage_locating_practicesexcavation_damage_one_call_notificationexcavation_damage_otherexcavation_damage_totalexcavation_ticketsservices_efv_in_systemservices_efv_installedservices_shutoff_valve_in_systemservices_shutoff_valve_installedfederal_land_leaks_repaired_or_scheduledpercent_unaccounted_for_gasadditional_informationpreparer_emailpreparer_faxpreparer_namepreparer_phonepreparer_title
0NaT19901506NaN199018Abbyville, City OfP O Box 100AbbyvilleKS<NA>RenoNaNNaN<NA><NA>NaN<NA><NA><NA><NA><NA><NA><NA><NA><NA><NA>0.0NaNNaNNaN<NA>Debra Ehling<NA>NaN
1NaT19900095NaN199027Abita Springs Nat Gas & WaterLevel StreetAbita SpringsLA<NA>St. TammanyNaNNaN<NA><NA>NaN<NA><NA><NA><NA><NA><NA><NA><NA><NA><NA>0.04.0NaNNaN<NA>Barbara Giancontieri<NA>NaN
2NaT19900947NaN199045Adairsville, City OfP.O. Box 830AdairsvilleGA<NA>BartowNaNNaN<NA><NA>NaN<NA><NA><NA><NA><NA><NA><NA><NA><NA><NA>0.00.0NaNNaN<NA>Chris Strippelhoff - Consultant<NA>NaN
3NaT19901193NaN199049Adamsville Gas Dept, Town Of231 East Main StreetAdamsvilleTN<NA>McnairyNaNNaN<NA><NA>NaN<NA><NA><NA><NA><NA><NA><NA><NA><NA><NA>0.03.8NaNNaN<NA>E. George Leckner, Jr. - Gas System Analyst<NA>NaN
4NaT19900948NaN199054Adel Gas Dept, City OfCity Hall - P.O. Box 658AdelGA<NA>CookNaNNaN<NA><NA>NaN<NA><NA><NA><NA><NA><NA><NA><NA><NA><NA>0.03.5NaNNaN<NA>Chris Strippelhoff - Consultant<NA>NaN
\n", - "
" - ], - "text/plain": [ - " report_date report_number report_submission_type report_year operator_id_phmsa operator_name_phmsa office_address_street office_address_city office_address_state office_address_zip office_address_county headquarters_address_street headquarters_address_city headquarters_address_state headquarters_address_zip headquarters_address_county excavation_damage_excavation_practices excavation_damage_locating_practices excavation_damage_one_call_notification excavation_damage_other excavation_damage_total excavation_tickets services_efv_in_system services_efv_installed services_shutoff_valve_in_system services_shutoff_valve_installed federal_land_leaks_repaired_or_scheduled percent_unaccounted_for_gas additional_information preparer_email preparer_fax preparer_name preparer_phone preparer_title\n", - "0 NaT 19901506 NaN 1990 18 Abbyville, City Of P O Box 100 Abbyville KS Reno NaN NaN NaN 0.0 NaN NaN NaN Debra Ehling NaN\n", - "1 NaT 19900095 NaN 1990 27 Abita Springs Nat Gas & Water Level Street Abita Springs LA St. Tammany NaN NaN NaN 0.0 4.0 NaN NaN Barbara Giancontieri NaN\n", - "2 NaT 19900947 NaN 1990 45 Adairsville, City Of P.O. Box 830 Adairsville GA Bartow NaN NaN NaN 0.0 0.0 NaN NaN Chris Strippelhoff - Consultant NaN\n", - "3 NaT 19901193 NaN 1990 49 Adamsville Gas Dept, Town Of 231 East Main Street Adamsville TN Mcnairy NaN NaN NaN 0.0 3.8 NaN NaN E. George Leckner, Jr. - Gas System Analyst NaN\n", - "4 NaT 19900948 NaN 1990 54 Adel Gas Dept, City Of City Hall - P.O. Box 658 Adel GA Cook NaN NaN NaN 0.0 3.5 NaN NaN Chris Strippelhoff - Consultant NaN" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "df.head()" ] }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "0 NaN\n", - "1 4.0\n", - "2 0.0\n", - "3 3.8\n", - "4 3.5\n", - "Name: percent_unaccounted_for_gas, dtype: float64" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "df.percent_unaccounted_for_gas.head()" ] }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "0.033715048084940795\n" - ] - } - ], + "outputs": [], "source": [ "print(negative_count / (positive_count + negative_count)) " ] }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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REREtV6tCU3vvvsufvvtNyQmJsrmlxxyAwA3Nze0b98eTk5O2LlzJwYNGlTh+oQQUKn+75DWw/9+kpqHRUdHY/r06RXvFBERET03asXhubCwMGzfvh0///wzGjZsWGmtvb09nJyccO7cOQCAVqtFYWEhcnJyZHXZ2dnSyJFWq8W1a9fKrOv69euymtIjSjk5OSgqKiozAlVi8uTJ0Ol00pSRkaFsh4mIiOiZU6OhSQiBd999F5s3b8a+ffvQpEmTRy5z48YNZGRkwN7eHgDg4eEBQ0NDJCQkSDWZmZlIS0tDx44dAQDe3t7Q6XQ4cuSIVHP48GHodDpZTVpaGjIzM6WaPXv2QK1Ww8PDo9y+qNVqWFhYyCYiIiJ6PtXo1XPjx4/HunXrsG3bNjg7O0vzNRoNTExMcPv2bURFRWHw4MGwt7fHxYsXMWXKFFy6dAnp6ekwNzcHALz99tvYsWMHYmNjYWVlhcjISNy4cQMpKSkwMDAA8ODcqKtXr2LZsmUAgLFjx8LJyQk//PADgAe3HGjTpg3s7OwwZ84c3Lx5E8HBwRgwYAAWLVqkaH949RxRzeHVc0RUVc/E1XNLly6FTqdD165dYW9vL00bNmwAABgYGODkyZPo378/mjdvjtGjR6N58+ZISkqSAhMALFiwAAMGDMCwYcPQqVMnmJqa4ocffpACEwCsXbsW7u7u8PX1ha+vL1q1aoXVq1dL7QYGBti5cyeMjY3RqVMnDBs2DAMGDMDcuXP/uSeEiIiIaq1adZ+mZx1HmohqDkeaiKiqnomRJiIiIqJnBUMTERERkQIMTUREREQKMDQRERERKcDQRERERKQAQxMRERGRAgxNRERERAowNBEREREpwNBEREREpABDExEREZECDE1ERERECjA0ERERESnA0ERERESkAEMTERERkQIMTUREREQKMDQRERERKcDQRERERKQAQxMRERGRAgxNRERERAowNBEREREpwNBEREREpABDExEREZECDE1ERERECjA0ERERESnA0ERERESkAEMTERERkQIMTUREREQKMDQRERERKcDQRERERKQAQxMRERGRAgxNRERERAowNBEREREpwNBEREREpABDExEREZECDE1ERERECjA0ERERESnA0ERERESkAEMTERERkQIMTUREREQKMDQRERERKVCjoSk6OhodOnSAubk5bG1t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kpKCoqIiWY2DgwPc3NykmqSkJGg0GikwAYCXlxc0Go1UUxG9Xo+8vDzZRERERM+nWh2aPD09sWrVKvz4449Yvnw5srKy0LFjR9y4cQNZWVkAADs7O9kydnZ2UltWVhaMjIxgaWlZaY2trW2Zbdva2ko1FYmOjpbOg9JoNHB0dKzyvhIREVHtVqtDU+/evTF48GC4u7vDx8cHO3fuBPDgMFwJlUolW0YIUWZeaaVryqtXsp7JkydDp9NJU0ZGxiP3iYiIiJ5NtTo0lWZmZgZ3d3ecO3dOOs+p9GhQdna2NPqk1WpRWFiInJycSmuuXbtWZlvXr18vM4pVmlqthoWFhWwiIiKi59MzFZr0ej3S09Nhb2+PJk2aQKvVIiEhQWovLCzEgQMH0LFjRwCAh4cHDA0NZTWZmZlIS0uTary9vaHT6XDkyBGp5vDhw9DpdFINERERUa2+ei4yMhIBAQFo1KgRsrOz8emnnyIvLw+jR4+GSqVCREQEZs6ciWbNmqFZs2aYOXMmTE1NERgYCADQaDQICQnBxIkTYW1tDSsrK0RGRkqH+wDAxcUFvXr1QmhoKJYtWwYAGDt2LPz9/XnlHBEREUlqdWi6fPkyRowYgb///hv169eHl5cXkpOT4eTkBACYNGkSCgoKMH78eOTk5MDT0xN79uyBubm5tI4FCxagbt26GDZsGAoKCtCjRw/ExsbCwMBAqlm7di3Cw8Olq+z69euHxYsX/7M7S0RERLWaSggharoTz4u8vDxoNBrodLpqP79JNb3yk9KJ/u3ENH6UEVHVKP3+fqbOaSIiIiKqKQxNRERERAowNBEREREpwNBEREREpABDExEREZECDE1ERERECjA0ERERESnA0ERERESkAEMTERERkQIMTUREREQKMDQRERERKcDQRERERKQAQxMRERGRAgxNRERERAowNBEREREpwNBEREREpABDExEREZECDE1ERERECjA0ERERESnA0ERERESkAEMTERERkQIMTUREREQKMDQRERERKcDQRERERKQAQxMRERGRAgxNRERERAowNBEREREpwNBEREREpABDExEREZECDE1ERERECjA0ERERESlQt6Y7QERED1GparoHRLWXEDW6eY40ERERESnA0ERERESkAEMTERERkQIMTUREREQKMDQRERERKcDQRERERKQAQxMRERGRAgxN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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "import matplotlib.pyplot as plt\n", "\n", @@ -537,290 +223,18 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
report_datereport_numberreport_submission_typereport_yearoperator_id_phmsaoperator_name_phmsaoffice_address_streetoffice_address_cityoffice_address_stateoffice_address_zipoffice_address_countyheadquarters_address_streetheadquarters_address_cityheadquarters_address_stateheadquarters_address_zipheadquarters_address_countyexcavation_damage_excavation_practicesexcavation_damage_locating_practicesexcavation_damage_one_call_notificationexcavation_damage_otherexcavation_damage_totalexcavation_ticketsservices_efv_in_systemservices_efv_installedservices_shutoff_valve_in_systemservices_shutoff_valve_installedfederal_land_leaks_repaired_or_scheduledpercent_unaccounted_for_gasadditional_informationpreparer_emailpreparer_faxpreparer_namepreparer_phonepreparer_title
1047NaT19902721NaN199015233Pfg Gas, Inc55 S. Third StreetOxfordPA<NA>ChesterNaNNaN<NA><NA>NaN<NA><NA><NA><NA><NA><NA><NA><NA><NA><NA>0.0-0.90NaNNaN<NA>Robert Beard<NA>NaN
2146NaT19910719NaN19917650Humboldt Utilities - Gas Dept207 S 13Th Ave. P.O. Box 850HumboldtTN<NA>GibsonNaNNaN<NA><NA>NaN<NA><NA><NA><NA><NA><NA><NA><NA><NA><NA>0.0-1.67NaNNaN<NA>Gregory D Hall<NA>NaN
3545NaT19920497NaN199211064Lafayette Gas & Utilities Dept, City Of200 East Locust St.LafayetteTN<NA>MaconNaNNaN<NA><NA>NaN<NA><NA><NA><NA><NA><NA><NA><NA><NA><NA>0.0-4.00NaNNaN<NA>Phillip Brawner - Gas Supt.<NA>NaN
3904NaT19920894NaN199214130Ohio Gas Co200 West High StBryanOH<NA>WilliamsNaNNaN<NA><NA>NaN<NA><NA><NA><NA><NA><NA><NA><NA><NA><NA>0.0-0.62NaNNaN<NA>Anton H Jessberger<NA>NaN
4681NaT19930082NaN1993828Atmore Utilities Board, City Of201 E. Louisville AvenueAtmoreAL<NA>EscambiaNaNNaN<NA><NA>NaN<NA><NA><NA><NA><NA><NA><NA><NA><NA><NA>0.0-4.00NaNNaN<NA>Vickie M. James - Clerk Of The Board<NA>NaN
\n", - "
" - ], - "text/plain": [ - " report_date report_number report_submission_type report_year operator_id_phmsa operator_name_phmsa office_address_street office_address_city office_address_state office_address_zip office_address_county headquarters_address_street headquarters_address_city headquarters_address_state headquarters_address_zip headquarters_address_county excavation_damage_excavation_practices excavation_damage_locating_practices excavation_damage_one_call_notification excavation_damage_other excavation_damage_total excavation_tickets services_efv_in_system services_efv_installed services_shutoff_valve_in_system services_shutoff_valve_installed federal_land_leaks_repaired_or_scheduled percent_unaccounted_for_gas additional_information preparer_email preparer_fax preparer_name preparer_phone preparer_title\n", - "1047 NaT 19902721 NaN 1990 15233 Pfg Gas, Inc 55 S. Third Street Oxford PA Chester NaN NaN NaN 0.0 -0.90 NaN NaN Robert Beard NaN\n", - "2146 NaT 19910719 NaN 1991 7650 Humboldt Utilities - Gas Dept 207 S 13Th Ave. P.O. Box 850 Humboldt TN Gibson NaN NaN NaN 0.0 -1.67 NaN NaN Gregory D Hall NaN\n", - "3545 NaT 19920497 NaN 1992 11064 Lafayette Gas & Utilities Dept, City Of 200 East Locust St. Lafayette TN Macon NaN NaN NaN 0.0 -4.00 NaN NaN Phillip Brawner - Gas Supt. NaN\n", - "3904 NaT 19920894 NaN 1992 14130 Ohio Gas Co 200 West High St Bryan OH Williams NaN NaN NaN 0.0 -0.62 NaN NaN Anton H Jessberger NaN\n", - "4681 NaT 19930082 NaN 1993 828 Atmore Utilities Board, City Of 201 E. Louisville Avenue Atmore AL Escambia NaN NaN NaN 0.0 -4.00 NaN NaN Vickie M. James - Clerk Of The Board NaN" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "df[df.percent_unaccounted_for_gas<0]" ] }, { "cell_type": "code", - "execution_count": 14, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "Index(['report_date', 'report_number', 'report_submission_type', 'report_year', 'operator_id_phmsa', 'operator_name_phmsa', 'office_address_street', 'office_address_city', 'office_address_state', 'office_address_zip', 'office_address_county', 'headquarters_address_street', 'headquarters_address_city', 'headquarters_address_state', 'headquarters_address_zip', 'headquarters_address_county', 'excavation_damage_excavation_practices', 'excavation_damage_locating_practices', 'excavation_damage_one_call_notification', 'excavation_damage_other', 'excavation_damage_total', 'excavation_tickets', 'services_efv_in_system', 'services_efv_installed', 'services_shutoff_valve_in_system', 'services_shutoff_valve_installed', 'federal_land_leaks_repaired_or_scheduled', 'percent_unaccounted_for_gas', 'additional_information', 'preparer_email', 'preparer_fax', 'preparer_name', 'preparer_phone', 'preparer_title'], dtype='object')" - ] - }, - "execution_count": 14, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "df.columns" ] @@ -834,7 +248,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -904,7 +318,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -969,7 +383,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -981,7 +395,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -1011,7 +425,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -1038,7 +452,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -1054,7 +468,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -1065,7 +479,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -1081,6 +495,195 @@ "metadata": {}, "outputs": [], "source": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Below is code that can be used to analyze missing values in a dataset:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "def analyze_missing_values(\n", + " df: pd.DataFrame, custom_missing_values: list[str] = None\n", + ") -> list[str]:\n", + " \"\"\"Analyze columns of a DataFrame for missing or invalid values.\n", + "\n", + " PLEASE NOTE: No calls to this method should be included in any final\n", + " transformation scripts. This is purely for analysis and does not perform\n", + " any data transformation or cleaning.\n", + "\n", + " This function checks each column for missing or custom missing values\n", + " and logs a summary of the findings for string (object), numeric, and\n", + " datetime columns.\n", + "\n", + " Args:\n", + " df: The DataFrame to analyze.\n", + " custom_missing_values: Optional list of custom values to consider\n", + " as \"missing\" (e.g., empty strings, specific strings like \"NA\",\n", + " \"NULL\", etc.). If not provided, defaults to a standard set.\n", + "\n", + " Returns:\n", + " exception_cols: List of names of columns that couldn't be analyzed\n", + " due to a caught exception.\n", + " \"\"\"\n", + " nan_cols = []\n", + " exception_cols = []\n", + "\n", + " # Use a default set of custom missing values if none are provided\n", + " if custom_missing_values is None:\n", + " custom_missing_values = [\n", + " \"\",\n", + " \" \",\n", + " \"NA\",\n", + " \"N/A\",\n", + " \"NULL\",\n", + " \"-\",\n", + " \"None\",\n", + " \"NaN\",\n", + " \"?\",\n", + " \"*\",\n", + " \"#\",\n", + " ]\n", + "\n", + " # Analyze columns for missing values\n", + " for col in df.columns:\n", + " try:\n", + " logger.info(f\"Analyzing column: {col}\")\n", + "\n", + " # Get the column values\n", + " col_data = df[col]\n", + "\n", + " # Check if the column is of string (object) type\n", + " if col_data.dtype == \"object\":\n", + " # Count rows where the value is NaN, None, empty string, or custom missing values\n", + " none_count = col_data.isna().sum() # Count None (NaN)\n", + " empty_string_count = (\n", + " col_data.str.strip() == \"\"\n", + " ).sum() # Count empty strings\n", + " custom_missing_count = col_data.isin(\n", + " custom_missing_values\n", + " ).sum() # Count custom missing values\n", + "\n", + " total_nan_count = none_count + empty_string_count + custom_missing_count\n", + "\n", + " if total_nan_count > 0:\n", + " nan_cols.append(col)\n", + "\n", + " # Output counts\n", + " logger.info(f\"Column '{col}' is a string type.\")\n", + " if none_count > 0:\n", + " logger.warning(f\"Rows with None values: {none_count}\")\n", + " logger.warning(df[df[col].isna()].head())\n", + " if empty_string_count > 0:\n", + " logger.warning(f\"Rows with empty strings: {empty_string_count}\")\n", + " logger.warning(df[df[col].str.strip() == \"\"].head())\n", + " if custom_missing_count > 0:\n", + " logger.warning(\n", + " f\"Rows with custom missing values: {custom_missing_count}\"\n", + " )\n", + " logger.warning(df[df[col].isin(custom_missing_values)].head())\n", + " if (\n", + " none_count == 0\n", + " and empty_string_count == 0\n", + " and custom_missing_count == 0\n", + " ):\n", + " logger.info(\"Found nothing worth reporting here\")\n", + "\n", + " # Check if the column is numeric (int or float)\n", + " elif pd.api.types.is_numeric_dtype(col_data):\n", + " # Count NA values in the column\n", + " na_count = col_data.isna().sum()\n", + " # Count custom missing values in numeric columns (if applicable)\n", + " custom_missing_numeric_count = col_data.isin(\n", + " [0]\n", + " ).sum() # Assuming 0 is considered a missing value\n", + "\n", + " if na_count > 0 or custom_missing_numeric_count > 0:\n", + " nan_cols.append(col)\n", + "\n", + " # Handle the non-NA data for further analysis\n", + " col_data_cleaned = col_data.dropna()\n", + "\n", + " if not col_data_cleaned.empty:\n", + " # Calculate min and max\n", + " min_val = col_data_cleaned.min()\n", + " max_val = col_data_cleaned.max()\n", + "\n", + " if min_val < 0 or na_count > 0 or custom_missing_numeric_count > 0:\n", + " logger.warning(f\"Min value: {min_val}\")\n", + " logger.warning(f\"Max value: {max_val}\")\n", + " if na_count > 0:\n", + " logger.warning(f\"Rows with NA values: {na_count}\")\n", + " logger.warning(df[df[col].isna()].head())\n", + " if custom_missing_numeric_count > 0:\n", + " logger.warning(\n", + " f\"Custom missing values (e.g., 0): {custom_missing_numeric_count}\"\n", + " )\n", + " logger.warning(df[df[col].isin([0])].head())\n", + " if (\n", + " min_val > 0\n", + " and na_count == 0\n", + " and custom_missing_numeric_count == 0\n", + " ):\n", + " logger.info(\"Found nothing worth reporting here\")\n", + " else:\n", + " logger.warning(\n", + " f\"Column '{col}' is numeric but contains only NA values.\"\n", + " )\n", + "\n", + " # Check if the column is a datetime type\n", + " elif pd.api.types.is_datetime64_any_dtype(col_data):\n", + " # Count NA values in the datetime column\n", + " na_count = col_data.isna().sum()\n", + " # Assuming custom missing values might be present in string form before conversion\n", + " custom_missing_count = col_data.isin(custom_missing_values).sum()\n", + "\n", + " if na_count > 0 or custom_missing_count > 0:\n", + " nan_cols.append(col)\n", + "\n", + " # Handle the non-NA data for further analysis\n", + " col_data_cleaned = col_data.dropna()\n", + "\n", + " if not col_data_cleaned.empty:\n", + " # Output min and max datetime values\n", + " min_date = col_data_cleaned.min()\n", + " max_date = col_data_cleaned.max()\n", + "\n", + " if na_count > 0 or custom_missing_count > 0:\n", + " logger.warning(f\"Min date: {min_date}\")\n", + " logger.warning(f\"Max date: {max_date}\")\n", + " logger.warning(f\"Rows with NA values: {na_count}\")\n", + " logger.warning(df[df[col].isna()].head())\n", + " logger.warning(f\"Custom missing values: {custom_missing_count}\")\n", + " logger.warning(df[df[col].isin(custom_missing_values)].head())\n", + " if na_count == 0 and custom_missing_count == 0:\n", + " logger.info(\"Found nothing worth reporting here\")\n", + " else:\n", + " logger.warning(\n", + " f\"Column '{col}' is datetime but contains only NA values.\"\n", + " )\n", + "\n", + " # If the column is of some other type, simply note the type\n", + " else:\n", + " logger.info(f\"Column '{col}' is of type {col_data.dtype}.\")\n", + "\n", + " except Exception as e:\n", + " exception_cols.append(col)\n", + " logger.warning(f\"Caught exception for column {col}: {e}\\n\")\n", + " continue\n", + "\n", + " logger.info(f\"Columns with NaNs or custom missing values: {nan_cols}\")\n", + " logger.info(f\"Columns with exceptions during processing: {exception_cols}\")\n", + "\n", + " return exception_cols\n" + ] } ], "metadata": { diff --git a/src/pudl/helpers.py b/src/pudl/helpers.py index 7efc43ce9e..6fa5a1db6d 100644 --- a/src/pudl/helpers.py +++ b/src/pudl/helpers.py @@ -2245,185 +2245,8 @@ def standardize_phone_column(df: pd.DataFrame, columns: list[str]) -> pd.DataFra # Replace invalid or empty phone numbers with NaN invalid_mask = ( - (phone_main.isna()) - | (phone_main.str.fullmatch(r"0+") == True) - | (phone_main == "") + (phone_main.isna()) | (phone_main.str.fullmatch(r"0+")) | (phone_main == "") ) df[column] = df[column].mask(invalid_mask, np.nan) return df - - -def analyze_missing_values( - df: pd.DataFrame, custom_missing_values: list[str] = None -) -> list[str]: - """Analyze columns of a DataFrame for missing or invalid values. - - PLEASE NOTE: No calls to this method should be included in any final - transformation scripts. This is purely for analysis and does not perform - any data transformation or cleaning. - - This function checks each column for missing or custom missing values - and logs a summary of the findings for string (object), numeric, and - datetime columns. - - Args: - df: The DataFrame to analyze. - custom_missing_values: Optional list of custom values to consider - as "missing" (e.g., empty strings, specific strings like "NA", - "NULL", etc.). If not provided, defaults to a standard set. - - Returns: - exception_cols: List of names of columns that couldn't be analyzed - due to a caught exception. - """ - nan_cols = [] - exception_cols = [] - - # Use a default set of custom missing values if none are provided - if custom_missing_values is None: - custom_missing_values = [ - "", - " ", - "NA", - "N/A", - "NULL", - "-", - "None", - "NaN", - "?", - "*", - "#", - ] - - # Analyze columns for missing values - for col in df.columns: - try: - logger.info(f"Analyzing column: {col}") - - # Get the column values - col_data = df[col] - - # Check if the column is of string (object) type - if col_data.dtype == "object": - # Count rows where the value is NaN, None, empty string, or custom missing values - none_count = col_data.isna().sum() # Count None (NaN) - empty_string_count = ( - col_data.str.strip() == "" - ).sum() # Count empty strings - custom_missing_count = col_data.isin( - custom_missing_values - ).sum() # Count custom missing values - - total_nan_count = none_count + empty_string_count + custom_missing_count - - if total_nan_count > 0: - nan_cols.append(col) - - # Output counts - logger.info(f"Column '{col}' is a string type.") - if none_count > 0: - logger.warning(f"Rows with None values: {none_count}") - logger.warning(df[df[col].isna()].head()) - if empty_string_count > 0: - logger.warning(f"Rows with empty strings: {empty_string_count}") - logger.warning(df[df[col].str.strip() == ""].head()) - if custom_missing_count > 0: - logger.warning( - f"Rows with custom missing values: {custom_missing_count}" - ) - logger.warning(df[df[col].isin(custom_missing_values)].head()) - if ( - none_count == 0 - and empty_string_count == 0 - and custom_missing_count == 0 - ): - logger.info("Found nothing worth reporting here") - - # Check if the column is numeric (int or float) - elif pd.api.types.is_numeric_dtype(col_data): - # Count NA values in the column - na_count = col_data.isna().sum() - # Count custom missing values in numeric columns (if applicable) - custom_missing_numeric_count = col_data.isin( - [0] - ).sum() # Assuming 0 is considered a missing value - - if na_count > 0 or custom_missing_numeric_count > 0: - nan_cols.append(col) - - # Handle the non-NA data for further analysis - col_data_cleaned = col_data.dropna() - - if not col_data_cleaned.empty: - # Calculate min and max - min_val = col_data_cleaned.min() - max_val = col_data_cleaned.max() - - if min_val < 0 or na_count > 0 or custom_missing_numeric_count > 0: - logger.warning(f"Min value: {min_val}") - logger.warning(f"Max value: {max_val}") - if na_count > 0: - logger.warning(f"Rows with NA values: {na_count}") - logger.warning(df[df[col].isna()].head()) - if custom_missing_numeric_count > 0: - logger.warning( - f"Custom missing values (e.g., 0): {custom_missing_numeric_count}" - ) - logger.warning(df[df[col].isin([0])].head()) - if ( - min_val > 0 - and na_count == 0 - and custom_missing_numeric_count == 0 - ): - logger.info("Found nothing worth reporting here") - else: - logger.warning( - f"Column '{col}' is numeric but contains only NA values." - ) - - # Check if the column is a datetime type - elif pd.api.types.is_datetime64_any_dtype(col_data): - # Count NA values in the datetime column - na_count = col_data.isna().sum() - # Assuming custom missing values might be present in string form before conversion - custom_missing_count = col_data.isin(custom_missing_values).sum() - - if na_count > 0 or custom_missing_count > 0: - nan_cols.append(col) - - # Handle the non-NA data for further analysis - col_data_cleaned = col_data.dropna() - - if not col_data_cleaned.empty: - # Output min and max datetime values - min_date = col_data_cleaned.min() - max_date = col_data_cleaned.max() - - if na_count > 0 or custom_missing_count > 0: - logger.warning(f"Min date: {min_date}") - logger.warning(f"Max date: {max_date}") - logger.warning(f"Rows with NA values: {na_count}") - logger.warning(df[df[col].isna()].head()) - logger.warning(f"Custom missing values: {custom_missing_count}") - logger.warning(df[df[col].isin(custom_missing_values)].head()) - if na_count == 0 and custom_missing_count == 0: - logger.info("Found nothing worth reporting here") - else: - logger.warning( - f"Column '{col}' is datetime but contains only NA values." - ) - - # If the column is of some other type, simply note the type - else: - logger.info(f"Column '{col}' is of type {col_data.dtype}.") - - except Exception as e: - exception_cols.append(col) - logger.warning(f"Caught exception for column {col}: {e}\n") - continue - - logger.info(f"Columns with NaNs or custom missing values: {nan_cols}") - logger.info(f"Columns with exceptions during processing: {exception_cols}") - - return exception_cols diff --git a/test/unit/metadata_test.py b/test/unit/metadata_test.py index f1e98dcb9d..a04e25644c 100644 --- a/test/unit/metadata_test.py +++ b/test/unit/metadata_test.py @@ -125,7 +125,7 @@ def dummy_pandera_schema(): { "description": "test resource based on core_eia__entity_plants", "schema": { - "fields": ["plant_id_eia", "city", "state"], + "fields": ["plant_id_eia", "city", "capacity_mw"], "primary_key": ["plant_id_eia"], }, "sources": ["eia860", "eia923"], @@ -146,7 +146,7 @@ def test_resource_descriptors_can_encode_schemas(dummy_pandera_schema): { "plant_id_eia": [12345, 12346], "city": ["Bloomington", "Springfield"], - "state": ["IL", "IL"], + "capacity_mw": [1.3, 1.0], } ).pipe(apply_pudl_dtypes) assert not dummy_pandera_schema.validate(good_dataframe).empty @@ -166,7 +166,7 @@ def test_resource_descriptors_can_encode_schemas(dummy_pandera_schema): { "plant_id_eia": ["non_number"], "city": ["Bloomington"], - "state": ["IL"], + "capacity_mw": [1.3], } ).astype(str), id="bad dtype", @@ -177,7 +177,7 @@ def test_resource_descriptors_can_encode_schemas(dummy_pandera_schema): { "plant_id_eia": [12345, 12345], "city": ["Bloomington", "Springfield"], - "state": ["IL", "IL"], + "capacity_mw": [1.3, 1.0], } ).pipe(apply_pudl_dtypes), id="duplicate PK",