chalseee commited on
Commit
2152c06
·
verified ·
1 Parent(s): 615a60b

Sync from GitHub via hub-sync

Browse files
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ data/electricity.data filter=lfs diff=lfs merge=lfs -text
Dockerfile DELETED
@@ -1,20 +0,0 @@
1
- FROM python:3.13.5-slim
2
-
3
- WORKDIR /app
4
-
5
- RUN apt-get update && apt-get install -y \
6
- build-essential \
7
- curl \
8
- git \
9
- && rm -rf /var/lib/apt/lists/*
10
-
11
- COPY requirements.txt ./
12
- COPY src/ ./src/
13
-
14
- RUN pip3 install -r requirements.txt
15
-
16
- EXPOSE 8501
17
-
18
- HEALTHCHECK CMD curl --fail http://localhost:8501/_stcore/health
19
-
20
- ENTRYPOINT ["streamlit", "run", "src/streamlit_app.py", "--server.port=8501", "--server.address=0.0.0.0"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
README.md CHANGED
@@ -1,19 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
- title: Utilities Equity Efficiency Gap
3
- emoji: 🚀
4
- colorFrom: red
5
- colorTo: red
6
- sdk: docker
7
- app_port: 8501
8
- tags:
9
- - streamlit
10
- pinned: false
11
- short_description: Electricity Utility Residential Rate Analysis
12
  ---
13
 
14
- # Welcome to Streamlit!
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
- Edit `/src/streamlit_app.py` to customize this app to your heart's desire. :heart:
17
 
18
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
19
- forums](https://discuss.streamlit.io).
 
 
 
1
+ # Electricity Utility Inefficiency & Residential Rate Analysis
2
+
3
+ > **Research Question:** Are system-level inefficiencies — high energy losses and poor load factors — statistically correlated with higher retail rates for residential consumers?
4
+
5
+ ---
6
+
7
+ ## Overview
8
+
9
+ This project investigates a fundamental fairness question in the U.S. electricity sector: do residential customers end up paying more when their utility operates inefficiently? Using the [CORGIS Electricity Dataset](https://corgis-edu.github.io/corgis/), this analysis builds a set of derived efficiency and equity metrics, then examines their statistical relationships through a suite of interactive visualizations.
10
+
11
+ New York State serves as the primary case study. NY was selected through a data-driven ranking process (get_state_variance) that scores all 50 states across five analytical criteria: number of utilities, number of ownership types, residential price standard deviation, maximum system loss percentage, and industrial revenue dependency. New York ranks at or near the top on all five: it has over 100 utilities across 6 distinct ownership models, exhibits high residential price variance, and sits within one of the most actively scrutinized regulatory environments in the U.S.
12
+
13
+ [![Open in HF Space](https://huggingface.co/datasets/huggingface/badges/resolve/main/open-in-hf-spaces-sm.svg)](https://huggingface.co/spaces/chalseee/utilities-equity-efficiency-gap)
14
  ---
15
+
16
+ ## Key Metrics
17
+
18
+ | Metric | Description |
19
+ |---|---|
20
+ | `System Loss Percentage` | Energy lost in transmission/distribution as % of total supply |
21
+ | `Load Factor` | Ratio of actual energy delivered to theoretical maximum (demand efficiency) |
22
+ | `Residential Unit Price` | Residential rate in $/MWh |
23
+ | `Industrial Unit Price` | Industrial rate in $/MWh |
24
+ | `Price Spread` | Gap between residential and industrial rates (equity indicator) |
25
  ---
26
 
27
+ ## Visualizations
28
+
29
+ - **State Selection Table** — Ranking of top 10 states by analytical richness; justifies NY case study
30
+ - **Price Spread Strip Plot** — Residential premium over industrial rates by ownership model
31
+ - **Correlation Heatmap** — Statistical significance matrix across all key metrics
32
+ - **Fairness Audit Scatter** — System loss (%) and load factor (dual y-axis) vs. residential price by ownership model, with OLS trendline
33
+ - **Rate Disparity Dumbbell** — Top 10 utilities by residential–industrial price gap
34
+ - **Energy Flow Sankey Diagram (State)** — Per-state (or utility) breakdown of energy sources and uses as percentages
35
+ - **Energy Flow Sankey Diagram (US)** — National aggregate energy flow
36
+
37
+ ---
38
+
39
+ ## Project Structure
40
+
41
+ ```
42
+ .
43
+ ├── utility_efficiency_fairness.ipynb
44
+ ├── streamlit_app.py
45
+ ├── requirements.txt
46
+ ├── README.md
47
+ ├── data/
48
+ │ └── app.py
49
+ │ └── electricity.data
50
+ │ └── electricity.data
51
+ ├── src/
52
+ │ └── util/
53
+ │ ├── data_util.py
54
+ │ └── plot_util.py
55
+ └── images/
56
+ ```
57
+
58
+ The three modules work as a clean pipeline:
59
+ - **`electricity`** — CORGIS data loader (unmodified third-party)
60
+ - **`data_util`** — all data preparation, filtering, and metric engineering
61
+ - **`plot_util`** — all chart construction and SVG expor
62
+
63
+ ### `util.py` — Helper Functions
64
+
65
+ | Function | Purpose |
66
+ |---|---|
67
+ | `prepare_data(df)` | Calculates key metrics and other features from raw columns |
68
+ | `get_state_data(state, df)` | Filters and subsets data for a given state |
69
+ | `get_state_variance(df)` | Ranks states by residential price variance (for state selection) |
70
+ | `get_customer_utilities(df, customer)` | Filters utilities by customer type served |
71
+ | `get_residential_load_factor(df)` | Subset with valid load factor for residential utilities |
72
+ | `get_residential_sys_loss(df)` | Subset with valid loss % for residential utilities |
73
+ | `get_utility_usage(utility)` | Converts raw MWh values to % of total supply for Sankey |
74
+
75
+ ---
76
+
77
+ ## Setup & Usage
78
+
79
+ - The dataset is pre-bundled — no external downloads required for it.
80
+ - Run `jupytr notebook utility_efficiency_fairness.ipynb.`
81
+ - Run pip install pandas plotly scipy kaleido streamlit
82
+
83
+ ---
84
+
85
+ ## Data Source
86
+
87
+ **[CORGIS Electricity Dataset](https://corgis-edu.github.io/corgis/python/electricity/)** — a cleaned and structured snapshot of U.S. Energy Information Administration (EIA) [Form 861](https://www.eia.gov/electricity/data/eia861/) data, covering utility-level electricity generation, sales, revenues, and customer counts across all U.S. states.
88
+
89
+ ---
90
 
91
+ ## Potential Extensions
92
 
93
+ - Expand the analysis to all 50 states and compare regional patterns
94
+ - Incorporate time-series data to track changes in efficiency and pricing
95
+ - Apply regression modeling to control for utility size and customer density
96
+ - Explore the role of renewable energy mix on system loss rates
data/__init__.py ADDED
File without changes
data/app.py ADDED
@@ -0,0 +1,134 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ '''
2
+ Hello! Thank you for downloading a CORGIS library. However, you do not
3
+ need to open this file. Instead you should make your own Python file and
4
+ add the following line:
5
+
6
+ import electricity
7
+
8
+ Then just place the files you downloaded alongside it.
9
+ '''
10
+
11
+ import os as _os
12
+ import pickle as _pickle
13
+
14
+ __all__ = ['get_utility']
15
+
16
+ def _tifa_definitions():
17
+ return {"type": "ModuleType",
18
+ "fields": {
19
+ 'get': {
20
+ "type": "FunctionType",
21
+ "name": 'get',
22
+ "returns": {
23
+ "type": "ListType",
24
+ "empty": False,
25
+ "subtype": {"type": "NumType"}
26
+ }
27
+ },
28
+ 'get_utility': {
29
+ "type": "FunctionType",
30
+ "name": 'get_utility',
31
+ "returns":
32
+ {"type": "ListType", "subtype":
33
+ {"type": "DictType", "literals": [{"type": "LiteralStr", "value": 'Utility'}, {"type": "LiteralStr", "value": 'Demand'}, {"type": "LiteralStr", "value": 'Sources'}, {"type": "LiteralStr", "value": 'Uses'}, {"type": "LiteralStr", "value": 'Revenues'}, {"type": "LiteralStr", "value": 'Revenue'}, {"type": "LiteralStr", "value": 'Retail'}], "values": [
34
+ {"type": "DictType", "literals": [{"type": "LiteralStr", "value": 'Number'}, {"type": "LiteralStr", "value": 'Name'}, {"type": "LiteralStr", "value": 'State'}, {"type": "LiteralStr", "value": 'Type'}], "values": [
35
+ {"type": "NumType"},
36
+ {"type": "StrType"},
37
+ {"type": "StrType"},
38
+ {"type": "StrType"}]},
39
+ {"type": "DictType", "literals": [{"type": "LiteralStr", "value": 'Summer Peak'}, {"type": "LiteralStr", "value": 'Winter Peak'}], "values": [
40
+ {"type": "NumType"},
41
+ {"type": "NumType"}]},
42
+ {"type": "DictType", "literals": [{"type": "LiteralStr", "value": 'Generation'}, {"type": "LiteralStr", "value": 'Purchased'}, {"type": "LiteralStr", "value": 'Other'}, {"type": "LiteralStr", "value": 'Total'}], "values": [
43
+ {"type": "NumType"},
44
+ {"type": "NumType"},
45
+ {"type": "NumType"},
46
+ {"type": "NumType"}]},
47
+ {"type": "DictType", "literals": [{"type": "LiteralStr", "value": 'Retail'}, {"type": "LiteralStr", "value": 'Resale'}, {"type": "LiteralStr", "value": 'No Charge'}, {"type": "LiteralStr", "value": 'Consumed'}, {"type": "LiteralStr", "value": 'Losses'}, {"type": "LiteralStr", "value": 'Total'}], "values": [
48
+ {"type": "NumType"},
49
+ {"type": "NumType"},
50
+ {"type": "NumType"},
51
+ {"type": "NumType"},
52
+ {"type": "NumType"},
53
+ {"type": "NumType"}]},
54
+ {"type": "DictType", "literals": [{"type": "LiteralStr", "value": 'Retail'}], "values": [
55
+ {"type": "NumType"}]},
56
+ {"type": "DictType", "literals": [{"type": "LiteralStr", "value": 'Delivery'}, {"type": "LiteralStr", "value": 'Resale'}, {"type": "LiteralStr", "value": 'Adjustments'}, {"type": "LiteralStr", "value": 'Transmission'}, {"type": "LiteralStr", "value": 'Other'}, {"type": "LiteralStr", "value": 'Total'}], "values": [
57
+ {"type": "NumType"},
58
+ {"type": "NumType"},
59
+ {"type": "NumType"},
60
+ {"type": "NumType"},
61
+ {"type": "NumType"},
62
+ {"type": "NumType"}]},
63
+ {"type": "DictType", "literals": [{"type": "LiteralStr", "value": 'Residential'}, {"type": "LiteralStr", "value": 'Commercial'}, {"type": "LiteralStr", "value": 'Industrial'}, {"type": "LiteralStr", "value": 'Transportation'}, {"type": "LiteralStr", "value": 'Total'}], "values": [
64
+ {"type": "DictType", "literals": [{"type": "LiteralStr", "value": 'Revenue'}, {"type": "LiteralStr", "value": 'Sales'}, {"type": "LiteralStr", "value": 'Customers'}], "values": [
65
+ {"type": "NumType"},
66
+ {"type": "NumType"},
67
+ {"type": "NumType"}]},
68
+ {"type": "DictType", "literals": [{"type": "LiteralStr", "value": 'Revenue'}, {"type": "LiteralStr", "value": 'Sales'}, {"type": "LiteralStr", "value": 'Customers'}], "values": [
69
+ {"type": "NumType"},
70
+ {"type": "NumType"},
71
+ {"type": "NumType"}]},
72
+ {"type": "DictType", "literals": [{"type": "LiteralStr", "value": 'Revenue'}, {"type": "LiteralStr", "value": 'Sales'}, {"type": "LiteralStr", "value": 'Customers'}], "values": [
73
+ {"type": "NumType"},
74
+ {"type": "NumType"},
75
+ {"type": "NumType"}]},
76
+ {"type": "DictType", "literals": [{"type": "LiteralStr", "value": 'Revenue'}, {"type": "LiteralStr", "value": 'Sales'}, {"type": "LiteralStr", "value": 'Customers'}], "values": [
77
+ {"type": "NumType"},
78
+ {"type": "NumType"},
79
+ {"type": "NumType"}]},
80
+ {"type": "DictType", "literals": [{"type": "LiteralStr", "value": 'Revenue'}, {"type": "LiteralStr", "value": 'Sales'}, {"type": "LiteralStr", "value": 'Customers'}], "values": [
81
+ {"type": "NumType"},
82
+ {"type": "NumType"},
83
+ {"type": "NumType"}]}]}]}}
84
+ },
85
+ }
86
+ }
87
+
88
+ class _Constants(object):
89
+ '''
90
+ Global singleton object to hide some of the constants; some IDEs reveal
91
+ internal module details very aggressively, and there's no other way
92
+ to hide stuff.
93
+ '''
94
+
95
+ class DatasetException(Exception):
96
+ ''' Thrown when there is an error loading the dataset for some reason.'''
97
+
98
+ _Constants._DATABASE_NAME = _os.path.join(_os.path.dirname(__file__),
99
+ "electricity.data")
100
+ if not _os.access(_Constants._DATABASE_NAME, _os.F_OK):
101
+ raise DatasetException(("Error! Could not find a \"{0}\" file. "
102
+ "Make sure that there is a \"{0}\" in the "
103
+ "same directory as \"{1}.py\"! Spelling is "
104
+ "very important here."
105
+ ).format(_Constants._DATABASE_NAME, __name__))
106
+ elif not _os.access(_Constants._DATABASE_NAME, _os.R_OK):
107
+ raise DatasetException(("Error! Could not read the \"{0}\" file. "
108
+ "Make sure that it readable by changing its "
109
+ "permissions. You may need to get help from "
110
+ "your instructor."
111
+ ).format(_Constants._DATABASE_NAME, __name__))
112
+
113
+
114
+ _Constants._DATASET = None
115
+
116
+ def get_utility():
117
+ """
118
+ Retrieves all of the utility.
119
+ """
120
+ if _Constants._DATASET is None:
121
+ with open(_Constants._DATABASE_NAME, 'rb') as _:
122
+ _Constants._DATASET = _pickle.load(_)
123
+ return _Constants._DATASET
124
+
125
+ if __name__ == '__main__':
126
+ from pprint import pprint as _pprint
127
+ from timeit import default_timer as _default_timer
128
+
129
+ print(">>> get_utility()")
130
+
131
+ start_time = _default_timer()
132
+ result = get_utility()
133
+ print("Time taken: {}".format(_default_timer() - start_time))
134
+ _pprint(result[0])
data/electricity.data ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2073f807dca20eeac617d9ea9fb7be8b4f9eb8203f48afeb408b269bd472b948
3
+ size 4261662
data/electricity.py ADDED
@@ -0,0 +1,134 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ '''
2
+ Hello! Thank you for downloading a CORGIS library. However, you do not
3
+ need to open this file. Instead you should make your own Python file and
4
+ add the following line:
5
+
6
+ import electricity
7
+
8
+ Then just place the files you downloaded alongside it.
9
+ '''
10
+
11
+ import os as _os
12
+ import pickle as _pickle
13
+
14
+ __all__ = ['get_utility']
15
+
16
+ def _tifa_definitions():
17
+ return {"type": "ModuleType",
18
+ "fields": {
19
+ 'get': {
20
+ "type": "FunctionType",
21
+ "name": 'get',
22
+ "returns": {
23
+ "type": "ListType",
24
+ "empty": False,
25
+ "subtype": {"type": "NumType"}
26
+ }
27
+ },
28
+ 'get_utility': {
29
+ "type": "FunctionType",
30
+ "name": 'get_utility',
31
+ "returns":
32
+ {"type": "ListType", "subtype":
33
+ {"type": "DictType", "literals": [{"type": "LiteralStr", "value": 'Utility'}, {"type": "LiteralStr", "value": 'Demand'}, {"type": "LiteralStr", "value": 'Sources'}, {"type": "LiteralStr", "value": 'Uses'}, {"type": "LiteralStr", "value": 'Revenues'}, {"type": "LiteralStr", "value": 'Revenue'}, {"type": "LiteralStr", "value": 'Retail'}], "values": [
34
+ {"type": "DictType", "literals": [{"type": "LiteralStr", "value": 'Number'}, {"type": "LiteralStr", "value": 'Name'}, {"type": "LiteralStr", "value": 'State'}, {"type": "LiteralStr", "value": 'Type'}], "values": [
35
+ {"type": "NumType"},
36
+ {"type": "StrType"},
37
+ {"type": "StrType"},
38
+ {"type": "StrType"}]},
39
+ {"type": "DictType", "literals": [{"type": "LiteralStr", "value": 'Summer Peak'}, {"type": "LiteralStr", "value": 'Winter Peak'}], "values": [
40
+ {"type": "NumType"},
41
+ {"type": "NumType"}]},
42
+ {"type": "DictType", "literals": [{"type": "LiteralStr", "value": 'Generation'}, {"type": "LiteralStr", "value": 'Purchased'}, {"type": "LiteralStr", "value": 'Other'}, {"type": "LiteralStr", "value": 'Total'}], "values": [
43
+ {"type": "NumType"},
44
+ {"type": "NumType"},
45
+ {"type": "NumType"},
46
+ {"type": "NumType"}]},
47
+ {"type": "DictType", "literals": [{"type": "LiteralStr", "value": 'Retail'}, {"type": "LiteralStr", "value": 'Resale'}, {"type": "LiteralStr", "value": 'No Charge'}, {"type": "LiteralStr", "value": 'Consumed'}, {"type": "LiteralStr", "value": 'Losses'}, {"type": "LiteralStr", "value": 'Total'}], "values": [
48
+ {"type": "NumType"},
49
+ {"type": "NumType"},
50
+ {"type": "NumType"},
51
+ {"type": "NumType"},
52
+ {"type": "NumType"},
53
+ {"type": "NumType"}]},
54
+ {"type": "DictType", "literals": [{"type": "LiteralStr", "value": 'Retail'}], "values": [
55
+ {"type": "NumType"}]},
56
+ {"type": "DictType", "literals": [{"type": "LiteralStr", "value": 'Delivery'}, {"type": "LiteralStr", "value": 'Resale'}, {"type": "LiteralStr", "value": 'Adjustments'}, {"type": "LiteralStr", "value": 'Transmission'}, {"type": "LiteralStr", "value": 'Other'}, {"type": "LiteralStr", "value": 'Total'}], "values": [
57
+ {"type": "NumType"},
58
+ {"type": "NumType"},
59
+ {"type": "NumType"},
60
+ {"type": "NumType"},
61
+ {"type": "NumType"},
62
+ {"type": "NumType"}]},
63
+ {"type": "DictType", "literals": [{"type": "LiteralStr", "value": 'Residential'}, {"type": "LiteralStr", "value": 'Commercial'}, {"type": "LiteralStr", "value": 'Industrial'}, {"type": "LiteralStr", "value": 'Transportation'}, {"type": "LiteralStr", "value": 'Total'}], "values": [
64
+ {"type": "DictType", "literals": [{"type": "LiteralStr", "value": 'Revenue'}, {"type": "LiteralStr", "value": 'Sales'}, {"type": "LiteralStr", "value": 'Customers'}], "values": [
65
+ {"type": "NumType"},
66
+ {"type": "NumType"},
67
+ {"type": "NumType"}]},
68
+ {"type": "DictType", "literals": [{"type": "LiteralStr", "value": 'Revenue'}, {"type": "LiteralStr", "value": 'Sales'}, {"type": "LiteralStr", "value": 'Customers'}], "values": [
69
+ {"type": "NumType"},
70
+ {"type": "NumType"},
71
+ {"type": "NumType"}]},
72
+ {"type": "DictType", "literals": [{"type": "LiteralStr", "value": 'Revenue'}, {"type": "LiteralStr", "value": 'Sales'}, {"type": "LiteralStr", "value": 'Customers'}], "values": [
73
+ {"type": "NumType"},
74
+ {"type": "NumType"},
75
+ {"type": "NumType"}]},
76
+ {"type": "DictType", "literals": [{"type": "LiteralStr", "value": 'Revenue'}, {"type": "LiteralStr", "value": 'Sales'}, {"type": "LiteralStr", "value": 'Customers'}], "values": [
77
+ {"type": "NumType"},
78
+ {"type": "NumType"},
79
+ {"type": "NumType"}]},
80
+ {"type": "DictType", "literals": [{"type": "LiteralStr", "value": 'Revenue'}, {"type": "LiteralStr", "value": 'Sales'}, {"type": "LiteralStr", "value": 'Customers'}], "values": [
81
+ {"type": "NumType"},
82
+ {"type": "NumType"},
83
+ {"type": "NumType"}]}]}]}}
84
+ },
85
+ }
86
+ }
87
+
88
+ class _Constants(object):
89
+ '''
90
+ Global singleton object to hide some of the constants; some IDEs reveal
91
+ internal module details very aggressively, and there's no other way
92
+ to hide stuff.
93
+ '''
94
+
95
+ class DatasetException(Exception):
96
+ ''' Thrown when there is an error loading the dataset for some reason.'''
97
+
98
+ _Constants._DATABASE_NAME = _os.path.join(_os.path.dirname(__file__),
99
+ "electricity.data")
100
+ if not _os.access(_Constants._DATABASE_NAME, _os.F_OK):
101
+ raise DatasetException(("Error! Could not find a \"{0}\" file. "
102
+ "Make sure that there is a \"{0}\" in the "
103
+ "same directory as \"{1}.py\"! Spelling is "
104
+ "very important here."
105
+ ).format(_Constants._DATABASE_NAME, __name__))
106
+ elif not _os.access(_Constants._DATABASE_NAME, _os.R_OK):
107
+ raise DatasetException(("Error! Could not read the \"{0}\" file. "
108
+ "Make sure that it readable by changing its "
109
+ "permissions. You may need to get help from "
110
+ "your instructor."
111
+ ).format(_Constants._DATABASE_NAME, __name__))
112
+
113
+
114
+ _Constants._DATASET = None
115
+
116
+ def get_utility():
117
+ """
118
+ Retrieves all of the utility.
119
+ """
120
+ if _Constants._DATASET is None:
121
+ with open(_Constants._DATABASE_NAME, 'rb') as _:
122
+ _Constants._DATASET = _pickle.load(_)
123
+ return _Constants._DATASET
124
+
125
+ if __name__ == '__main__':
126
+ from pprint import pprint as _pprint
127
+ from timeit import default_timer as _default_timer
128
+
129
+ print(">>> get_utility()")
130
+
131
+ start_time = _default_timer()
132
+ result = get_utility()
133
+ print("Time taken: {}".format(_default_timer() - start_time))
134
+ _pprint(result[0])
images/energy_usage_ny_sankey_chart.svg ADDED
images/energy_usage_us_sankey_chart.svg ADDED
images/key_metrics_corr_heatmap.svg ADDED
images/rate_disparity_dumbbell_plot.svg ADDED
images/rate_fairness_dual_y_scatter_plot.svg ADDED
images/top_ten_state_res_variance_table.svg ADDED
images/utility_type_strip_plot.svg ADDED
requirements.txt CHANGED
@@ -1,3 +1,5 @@
1
- altair
2
  pandas
3
- streamlit
 
 
 
 
 
1
  pandas
2
+ plotly
3
+ scipy
4
+ streamlit
5
+ kaleido
src/__init__.py ADDED
File without changes
src/streamlit_app.py DELETED
@@ -1,40 +0,0 @@
1
- import altair as alt
2
- import numpy as np
3
- import pandas as pd
4
- import streamlit as st
5
-
6
- """
7
- # Welcome to Streamlit!
8
-
9
- Edit `/streamlit_app.py` to customize this app to your heart's desire :heart:.
10
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
11
- forums](https://discuss.streamlit.io).
12
-
13
- In the meantime, below is an example of what you can do with just a few lines of code:
14
- """
15
-
16
- num_points = st.slider("Number of points in spiral", 1, 10000, 1100)
17
- num_turns = st.slider("Number of turns in spiral", 1, 300, 31)
18
-
19
- indices = np.linspace(0, 1, num_points)
20
- theta = 2 * np.pi * num_turns * indices
21
- radius = indices
22
-
23
- x = radius * np.cos(theta)
24
- y = radius * np.sin(theta)
25
-
26
- df = pd.DataFrame({
27
- "x": x,
28
- "y": y,
29
- "idx": indices,
30
- "rand": np.random.randn(num_points),
31
- })
32
-
33
- st.altair_chart(alt.Chart(df, height=700, width=700)
34
- .mark_point(filled=True)
35
- .encode(
36
- x=alt.X("x", axis=None),
37
- y=alt.Y("y", axis=None),
38
- color=alt.Color("idx", legend=None, scale=alt.Scale()),
39
- size=alt.Size("rand", legend=None, scale=alt.Scale(range=[1, 150])),
40
- ))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/util/__init__.py ADDED
File without changes
src/util/data_util.py ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Functions to clean and filter data for use in utility data visualizations"""
2
+
3
+ import pandas as pd
4
+
5
+ HOURS_PER_YEAR = 8760
6
+
7
+
8
+ def get_state_variance(df: pd.DataFrame) -> pd.DataFrame:
9
+ """Summarizes key metrics for states to help choose state for analysis"""
10
+
11
+ agg_df = df.groupby('Utility.State').agg({
12
+ 'Utility.Name': 'nunique', # Want = More utilites to analyze
13
+ 'Utility.Type': 'nunique', # Want = More utility types to compare
14
+ 'ResidentialUnitPrice': 'std', # Want = High std for varied utilities
15
+ 'SystemLossPercentage': 'max', # Want = Great outlier stories
16
+ 'IndustrialRevenueRatio': 'mean' # Want = High for industrial bias
17
+ })
18
+
19
+ agg_df.rename(columns={
20
+ 'Utility.Name': '# Utilities',
21
+ 'Utility.Type': '# Utility Types',
22
+ 'Utility.State': 'State',
23
+ 'ResidentialUnitPrice': 'Residential Price Std. Dev.',
24
+ 'SystemLossPercentage': 'System Loss %',
25
+ 'IndustrialRevenueRatio': 'Industrial Revenue %'
26
+ }, inplace=True)
27
+
28
+ agg_df.columns = [f"{col[0]}_{col[1]}" if isinstance(col, tuple) else col
29
+ for col in agg_df.columns.values]
30
+ agg_df.sort_values('Residential Price Std. Dev.',
31
+ ascending=False, inplace=True)
32
+
33
+ return agg_df.reset_index().head(10)
34
+
35
+
36
+ def get_state_data(state: str, df: pd.DataFrame) -> pd.DataFrame:
37
+ """Select and filter out relevant columns for analysis"""
38
+ keep_columns = ["Utility.Name", "Utility.State", "Utility.Type",
39
+ "Sources.Total", "Sources.Generation", "Sources.Purchased",
40
+ "Sources.Other", "Retail.Residential.Revenue", "Retail.Residential.Sales",
41
+ "Retail.Residential.Customers", "Retail.Industrial.Revenue",
42
+ "Retail.Industrial.Sales", "Retail.Industrial.Customers",
43
+ "Uses.Retail", "Uses.Losses", "Uses.Resale",
44
+ "Uses.No Charge", "Uses.Consumed", "Uses.Total",
45
+ "Demand.Summer Peak", "Revenues.Retail"]
46
+
47
+ return df[df["Utility.State"] == state][keep_columns].copy()
48
+
49
+
50
+ def prepare_data(df: pd.DataFrame) -> pd.DataFrame:
51
+ """Perform calculations for key metrics and add them to the data"""
52
+ # Residential $ per MWh
53
+ df['ResidentialUnitPrice'] = (df['Retail.Residential.Revenue']
54
+ / df['Retail.Residential.Sales']) * 1000
55
+ df['ResidentialUnitPrice'] = df['ResidentialUnitPrice'].fillna(0)
56
+
57
+ # Industrial $ per MWh
58
+ df['IndustrialUnitPrice'] = df['Retail.Industrial.Revenue'] / \
59
+ df['Retail.Industrial.Sales']
60
+ df['IndustrialUnitPrice'] = df['IndustrialUnitPrice'].fillna(0)
61
+
62
+ # % Dependency on industrial revenue
63
+ df['IndustrialRevenueRatio'] = df['Retail.Industrial.Revenue'] / \
64
+ df['Revenues.Retail'] * 100
65
+ df['IndustrialRevenueRatio'] = df['IndustrialRevenueRatio'].fillna(0)
66
+
67
+ # Equity Metric
68
+ df['PriceSpread'] = df['ResidentialUnitPrice'] - df['IndustrialUnitPrice']
69
+
70
+ # Efficiency Metric
71
+ df['SystemLossPercentage'] = (
72
+ df['Uses.Losses'] / df['Sources.Total']) * 100
73
+
74
+ # Operational metric of 'stress' on system
75
+ df['LoadFactor'] = df['Sources.Total'] / \
76
+ (df['Demand.Summer Peak'] * HOURS_PER_YEAR)
77
+ df['LoadFactor'] = df['LoadFactor'].apply(
78
+ lambda load: 0 if load == float('inf') else load)
79
+
80
+ return df
81
+
82
+
83
+ def get_customer_utilities(df: pd.DataFrame, sector="Residential") -> pd.DataFrame:
84
+ """Filter data by customer type for use in plots"""
85
+ if sector == "Residential":
86
+ return df[df["Retail.Residential.Customers"] > 0]
87
+
88
+ elif sector == "Industrial":
89
+ return df[df["Retail.Industrial.Customers"] > 0]
90
+
91
+ return df[(df["Retail.Residential.Customers"] > 0)
92
+ & (df["Retail.Industrial.Customers"] > 0)]
93
+
94
+
95
+ def get_residential_load_factor(df: pd.DataFrame) -> pd.DataFrame:
96
+ """Filter data by customer type and utilities with a load factor"""
97
+ df = get_customer_utilities(df, "Residential")
98
+
99
+ return df[df["LoadFactor"] > 0]
100
+
101
+
102
+ def get_residential_sys_loss(df: pd.DataFrame) -> pd.DataFrame:
103
+ """Filter data by customer type and utilities with system loss"""
104
+ df = get_customer_utilities(df, "Residential")
105
+
106
+ return df[df["SystemLossPercentage"] > 0]
107
+
108
+
109
+ def get_utility_usage(utility: pd.Series, level: str = "State") -> pd.DataFrame:
110
+ """Create data with percentages of utilty usage within the sankey plot"""
111
+
112
+ # Convert raw values to percentages of the 'Total Sources'
113
+ keys = [
114
+ 'Sources.Generation', 'Sources.Purchased', 'Sources.Other',
115
+ 'Uses.Retail', 'Uses.Resale', 'Uses.Losses',
116
+ 'Uses.Consumed', 'Uses.No Charge'
117
+ ]
118
+
119
+ named_utility = (utility[keys] / utility['Sources.Total']) * 100
120
+
121
+ if level == "State":
122
+ named_utility['Utility.Name'] = "State of " + \
123
+ utility['Utility.State'][0:2]
124
+ elif level == "US":
125
+ named_utility['Utility.Name'] = "United States"
126
+ else:
127
+ named_utility['Utility.Name'] = utility['Utility.Name']
128
+
129
+ return named_utility
src/util/plot_util.py ADDED
@@ -0,0 +1,322 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ Functions to create and retrieves interactive plotly charts """
2
+
3
+ import os
4
+
5
+ import pandas as pd
6
+ import numpy as np
7
+
8
+ from scipy.stats import linregress
9
+
10
+ import plotly.express as px
11
+ import plotly.graph_objects as go
12
+ import plotly.io as pio
13
+
14
+ from plotly.subplots import make_subplots
15
+
16
+
17
+ def get_state_variance_table(df: pd.DataFrame) -> go.Figure:
18
+ """Retrieve table of states with highest mean residential unit price"""
19
+ target, row_h, header_h = 7, 35, 50
20
+
21
+ fig = go.Figure(go.Table(
22
+ header=dict(
23
+ values=list(df.columns),
24
+ fill_color='#f8f9fa',
25
+ font=dict(size=12, family="Arial Black",),
26
+ align='center',
27
+ height=header_h
28
+ ),
29
+ cells=dict(
30
+ values=df.values.T,
31
+ fill_color=[['dodgerblue' if i ==
32
+ target else 'white' for i in range(10)]],
33
+ font=dict(
34
+ color=[['white' if i == target else 'black' for i in range(10)]], size=12),
35
+ height=row_h, align='center',
36
+ format=[None, None, None, ".2f", ".2f", ".2f"]
37
+ )
38
+ ))
39
+
40
+ fig.update_layout(
41
+ margin=dict(l=5, r=5, t=5, b=5),
42
+ height=row_h * (len(df) + 2),
43
+ autosize=False
44
+ )
45
+
46
+ return fig
47
+
48
+
49
+ def get_price_spread_strip_plot(df: pd.DataFrame):
50
+ """
51
+ Strip plot of Price Spread by ownership model.
52
+ Shows which ownership types most consistently charge
53
+ residential customers more than industrial ones.
54
+ """
55
+ fig = px.strip(
56
+ df[df.PriceSpread > 0],
57
+ x="Utility.Type",
58
+ y="PriceSpread",
59
+ color="Utility.Type",
60
+ hover_name="Utility.Name",
61
+ color_discrete_sequence=px.colors.qualitative.Prism,
62
+ title="<b>Rate Equity by Ownership Model:</b> "
63
+ "Residential Premium Over Industrial Rates",
64
+ labels={
65
+ "Utility.Type": "Type",
66
+ "PriceSpread": "Residential Premium ($/MWh)",
67
+ },
68
+ template="plotly_white"
69
+ )
70
+
71
+ fig.update_layout(showlegend=False)
72
+
73
+ return fig
74
+
75
+
76
+ def get_key_metrics_corr_matrix(df: pd.DataFrame) -> go.Figure:
77
+ """Correlation matrix for key analysis metrics"""
78
+ key_metrics = {
79
+ 'SystemLossPercentage': 'System Loss %',
80
+ 'LoadFactor': 'Load Factor',
81
+ 'IndustrialRevenueRatio': 'Industrial Revenue %',
82
+ 'PriceSpread': 'Price Spread',
83
+ }
84
+
85
+ corr_matrix = df[list(key_metrics.keys())].corr()
86
+
87
+ return px.imshow(
88
+ corr_matrix.round(2),
89
+ x=list(key_metrics.values()),
90
+ y=list(key_metrics.values()),
91
+ color_continuous_scale='mint',
92
+ text_auto=True,
93
+ aspect="auto",
94
+ title='<b>Statistical Significance:</b> Correlation Heatmap of Key Metrics',
95
+ labels=dict(color="Score"),
96
+ template='plotly_white')
97
+
98
+
99
+ def add_fairness_trendline(fig: go.Figure, x_data: pd.Series,
100
+ y_data: pd.Series, row: int, col: int) -> None:
101
+ """Calculates OLS and adds centered stats inside the plot to avoid title overlap."""
102
+ mask = ~np.isnan(x_data) & ~np.isnan(y_data)
103
+ x_clean, y_clean = x_data[mask], y_data[mask]
104
+
105
+ if len(x_clean) > 1:
106
+ # Get linear regression
107
+ result = linregress(x_clean, y_clean)
108
+
109
+ # Trendline coordinates
110
+ x_range = np.array([x_clean.min(), x_clean.max()])
111
+ y_range = result.slope * x_range + result.intercept
112
+
113
+ # Add Trendline
114
+ fig.add_trace(
115
+ go.Scatter(
116
+ x=x_range, y=y_range,
117
+ mode='lines',
118
+ line=dict(color='black', width=2, dash='dash'),
119
+ name='Overall Trend',
120
+ legendgroup='trendline',
121
+ showlegend=(row == 1 and col == 1),
122
+ hoverinfo='skip'
123
+ ), row=row, col=col)
124
+
125
+ # 2. Annotation stats box
126
+ fig.add_annotation(
127
+ xref=f"x{col if col > 1 else ''} domain",
128
+ yref="y domain",
129
+ x=0.5, # Horizontal center
130
+ y=0.92, # Lowered to 92% of height (inside the plot)
131
+ xanchor="center",
132
+ yanchor="top", # Box hangs downward from the y=0.92 point
133
+ text=f"<b>R²:</b> {result.rvalue**2:.3f} | <b>p:</b> {result.pvalue:.4e}",
134
+ showarrow=False,
135
+ align="center",
136
+ # High opacity for readability
137
+ bgcolor="rgba(255, 255, 255, 0.85)",
138
+ bordercolor="rgba(0,0,0,0.3)",
139
+ borderwidth=1,
140
+ font=dict(size=10))
141
+
142
+
143
+ def get_fairness_dual_y_scatter_plot(df: pd.DataFrame) -> go.Figure:
144
+ """Get dual y-axis scatter plot of utility fairness metrics"""
145
+ fig = make_subplots(
146
+ rows=1, cols=2,
147
+ shared_yaxes=True,
148
+ horizontal_spacing=0.05,
149
+ subplot_titles=('<b>System Loss vs Price</b>',
150
+ '<b>Load Factor vs Price</b>'))
151
+
152
+ df['BubbleSize'] = np.log1p(df['Retail.Residential.Customers'])
153
+
154
+ colors = px.colors.qualitative.Prism
155
+ types = df['Utility.Type'].unique()
156
+ color_map = {t: colors[i % len(colors)] for i, t in enumerate(types)}
157
+
158
+ # Plot 1: System Loss
159
+ for t in types:
160
+ mask = df['Utility.Type'] == t
161
+ fig.add_trace(
162
+ go.Scatter(
163
+ x=df[mask]['SystemLossPercentage'], y=df[mask]['ResidentialUnitPrice'],
164
+ name=t, hovertext=df[mask]['Utility.Name'], mode='markers',
165
+ marker=dict(color=color_map[t],
166
+ size=df[mask]['BubbleSize']),
167
+ hovertemplate="<b>%{hovertext}</b><br>Loss: %{x}%<br>Price: $%{y}<extra></extra>",
168
+ showlegend=True), row=1, col=1)
169
+ add_fairness_trendline(
170
+ fig, df['SystemLossPercentage'], df['ResidentialUnitPrice'], 1, 1)
171
+
172
+ # Plot 2: Load Factor
173
+ for t in types:
174
+ mask = df['Utility.Type'] == t
175
+ fig.add_trace(
176
+ go.Scatter(
177
+ x=df[mask]['LoadFactor'], y=df[mask]['ResidentialUnitPrice'], name=t,
178
+ mode='markers', marker=dict(color=color_map[t], size=df[mask]['BubbleSize']),
179
+ hovertext=df[mask]['Utility.Name'],
180
+ hovertemplate="<b>%{hovertext}</b><br>Load: %{x}<br>Price: $%{y}<extra></extra>",
181
+ showlegend=False), row=1, col=2)
182
+ add_fairness_trendline(fig, df['LoadFactor'],
183
+ df['ResidentialUnitPrice'], 1, 2)
184
+
185
+ fig.update_layout(
186
+ template='plotly_white',
187
+ title_text='<b>Fairness Audit:</b> Correlation of Utility Metrics to Residential Price',
188
+ legend_title_text="Ownership Model", height=600)
189
+
190
+ fig.update_yaxes(title_text='Residential Price ($/MWh)', row=1, col=1)
191
+ fig.update_xaxes(title_text='System Energy Loss (%)', row=1, col=1)
192
+ fig.update_xaxes(title_text='Load Factor', row=1, col=2)
193
+
194
+ return fig
195
+
196
+
197
+ def get_rate_disparity_dumbbell_plot(df: pd.DataFrame, top_n: int = 10) -> go.Figure:
198
+ """Get dumbbell plot of highest disparities between industrial/residential rates"""
199
+ # Sort by spread to show the most "unfair" utilities at the top
200
+ df_sorted = df[df.PriceSpread > 0].sort_values(
201
+ 'PriceSpread', ascending=True).tail(top_n)
202
+
203
+ fig = go.Figure()
204
+
205
+ # Add lines connecting the dots
206
+ for i, row in df_sorted.iterrows():
207
+ fig.add_shape(
208
+ type='line', x0=row['IndustrialUnitPrice'], x1=row['ResidentialUnitPrice'],
209
+ y0=row['Utility.Name'], y1=row['Utility.Name'],
210
+ line=dict(color='lightgrey', width=2))
211
+
212
+ # Industrial dumbbells
213
+ fig.add_trace(go.Scatter(
214
+ x=df_sorted['IndustrialUnitPrice'], y=df_sorted['Utility.Name'],
215
+ mode='markers', name='Industrial Rate', marker=dict(color='#1f77b4', size=10)))
216
+
217
+ # Residential dumbbells
218
+ fig.add_trace(go.Scatter(
219
+ x=df_sorted['ResidentialUnitPrice'], y=df_sorted['Utility.Name'],
220
+ mode='markers', name='Residential Rate', marker=dict(color='#d62728', size=10)))
221
+
222
+ fig.update_layout(title="<b>Top Rate Disparites</b>",
223
+ xaxis_title="Rate ($/MWh)", yaxis_title="")
224
+ return fig
225
+
226
+
227
+ def add_utility_dropdown(fig: go.Figure, df: pd.DataFrame) -> go.Figure:
228
+ """Post-processing function to add a utility dropdown justified right."""
229
+ buttons = []
230
+
231
+ for _, r in df.iterrows():
232
+ buttons.append(dict(
233
+ method="update",
234
+ label=r["Utility.Name"],
235
+ args=[
236
+ {"link.value": [[
237
+ r["Sources.Generation"], r["Sources.Purchased"], r["Sources.Other"],
238
+ r["Uses.Retail"], r["Uses.Resale"], r["Uses.Losses"],
239
+ r["Uses.Consumed"], r["Uses.No Charge"]
240
+ ]]},
241
+ {"title.text": f"<b>Energy Flow: </b>{r['Utility.Name']}"}
242
+ ]
243
+ ))
244
+
245
+ first_row = df.iloc[0]
246
+ initial_values = [
247
+ first_row["Sources.Generation"], first_row["Sources.Purchased"], first_row["Sources.Other"],
248
+ first_row["Uses.Retail"], first_row["Uses.Resale"], first_row["Uses.Losses"],
249
+ first_row["Uses.Consumed"], first_row["Uses.No Charge"]
250
+ ]
251
+
252
+ # 2. Directly assign intial values to the intial Sankey
253
+ fig.data[0].link.value = initial_values
254
+
255
+ # 3. Apply the layout and the dropdown menu
256
+ fig.update_layout(
257
+ title_text=f"<b>Energy Flow: </b>{first_row['Utility.Name']}",
258
+ updatemenus=[dict(
259
+ buttons=buttons,
260
+ direction="down",
261
+ showactive=True,
262
+ x=1.0,
263
+ xanchor="right",
264
+ y=2,
265
+ yanchor="top",
266
+ active=0
267
+ )],
268
+ )
269
+
270
+ return fig
271
+
272
+
273
+ def get_energy_use_sankey_plot(row: pd.DataFrame) -> go.Figure:
274
+ """Get energy usage sankey plot"""
275
+ labels = ["Generated", "Purchased", "Other", "Uses", "Retail Sales",
276
+ "Resale", "Losses", "Consumed", "No Charge"]
277
+
278
+ fig = go.Figure(data=[go.Sankey(
279
+ valueformat=".1f",
280
+ valuesuffix="%",
281
+ node=dict(
282
+ label=labels,
283
+ color=px.colors.qualitative.Prism),
284
+ link=dict(
285
+ source=[0, 1, 2, 3, 3, 3, 3, 3],
286
+ target=[3, 3, 3, 4, 5, 6, 7, 8],
287
+ value=[
288
+ row["Sources.Generation"],
289
+ row["Sources.Purchased"],
290
+ row["Sources.Other"],
291
+ row["Uses.Retail"],
292
+ row["Uses.Resale"],
293
+ row["Uses.Losses"],
294
+ row["Uses.Consumed"],
295
+ row["Uses.No Charge"]
296
+ ],
297
+ ))])
298
+
299
+ fig.update_layout(
300
+ title_text=f"<b>Energy Flow: </b>{row['Utility.Name']}",
301
+ hovermode='x')
302
+
303
+ return fig
304
+
305
+
306
+ def export_plots_as_svg(plots: list[go.Figure]) -> None:
307
+ """Export plots as high-definition SVGs to the 'images' folder"""
308
+
309
+ script_dir = os.path.dirname(os.path.abspath(__file__))
310
+ target_dir = os.path.join(script_dir, "..", "..", "images")
311
+
312
+ if not os.path.exists(target_dir):
313
+ os.makedirs(target_dir)
314
+
315
+ pio.write_images(fig=plots,
316
+ file=["images/top_ten_state_res_variance_table.svg",
317
+ "images/utility_type_strip_plot.svg",
318
+ "images/key_metrics_corr_heatmap.svg",
319
+ "images/rate_fairness_dual_y_scatter_plot.svg",
320
+ "images/rate_disparity_dumbbell_plot.svg",
321
+ "images/energy_usage_ny_sankey_chart.svg",
322
+ "images/energy_usage_us_sankey_chart.svg"])
streamlit_app.py ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit.src.util.plot_util as plot_util
2
+ import streamlit.src.util.data_util as data_util
3
+ import data.electricity as electricity
4
+
5
+ import streamlit as st
6
+ import pandas as pd
7
+
8
+
9
+ st.set_page_config(
10
+ page_title="Utility Efficiency & Rates",
11
+ layout="wide"
12
+ )
13
+
14
+
15
+ @st.cache_data
16
+ def load_data(state: str) -> pd.DataFrame:
17
+ utility = electricity.get_utility()
18
+ df = pd.json_normalize(utility)
19
+ state_df = data_util.get_state_data(state, df)
20
+ return data_util.prepare_data(state_df), df
21
+
22
+
23
+ with st.sidebar:
24
+ state = st.selectbox(
25
+ "Select State",
26
+ options=["NY", "AK", "RI", "ME", "CA", "NJ", "CT", "NH", "MA", "AZ"],
27
+ index=0
28
+ )
29
+ st.markdown("---")
30
+ st.markdown(
31
+ "This app explores whether operational inefficiencies "
32
+ "— energy losses and poor load factors — correlate with "
33
+ "higher residential electricity rates."
34
+ )
35
+ st.markdown(
36
+ "[View on GitHub](https://github.com/chalseokorom/utilities-equity-efficiency-gap)")
37
+
38
+ state_df, full_df = load_data(state)
39
+
40
+ st.title("Electricity Utility Fairness Residential Rate Analysis")
41
+ st.caption(
42
+ f"Exploring {state} utilities — "
43
+ f"{len(state_df)} utilities across "
44
+ f"{state_df['Utility.Type'].nunique()} ownership models"
45
+ )
46
+ st.divider()
47
+
48
+ # ── Section 1: Fairness Audit ─────────────────────────────────
49
+ st.header("Fairness Audit — Efficiency vs. Residential Price")
50
+ st.caption(
51
+ "Do utilities with higher energy losses or lower load factors "
52
+ "charge residential customers more per MWh?"
53
+ )
54
+
55
+ scatter_df = data_util.get_residential_sys_loss(state_df)
56
+ scatter_df = data_util.get_residential_load_factor(scatter_df).round(2)
57
+
58
+ st.plotly_chart(
59
+ plot_util.get_fairness_dual_y_scatter_plot(scatter_df),
60
+ use_container_width=True
61
+ )
62
+ st.divider()
63
+
64
+ # ── Section 2: Ownership Models ───────────────────────────────
65
+ st.header("Ownership Model — Price Spread")
66
+
67
+ st.plotly_chart(
68
+ plot_util.get_price_spread_strip_plot(state_df.round(2)),
69
+ use_container_width=True
70
+ )
71
+ st.divider()
72
+
73
+ # ── Section 3: Rate Disparity ─────────────────────────────────
74
+ st.header("Rate Disparity — Residential vs. Industrial")
75
+
76
+ n_utilities = st.slider(
77
+ "Number of utilities to show",
78
+ min_value=5, max_value=20, value=10, step=1
79
+ )
80
+
81
+ both_df = data_util.get_customer_utilities(state_df, sector="Both").round(2)
82
+ st.plotly_chart(
83
+ plot_util.get_rate_disparity_dumbbell_plot(both_df, top_n=n_utilities),
84
+ use_container_width=True
85
+ )
86
+ st.divider()
87
+
88
+ # ── Section 4: Energy Flow ────────────────────────────────────
89
+ st.header("Energy Flow — Sources & Uses")
90
+ # Row 1: State aggregate
91
+ st.subheader(f"{state} — Aggregate Energy Flow")
92
+ st.caption(
93
+ "How all utilities in this state collectively source and distribute energy.")
94
+
95
+ numeric_sum = state_df.select_dtypes(include='number').sum()
96
+ numeric_sum['Utility.Name'] = state
97
+ state_flow = data_util.get_utility_usage(numeric_sum, level="US")
98
+
99
+ st.plotly_chart(
100
+ plot_util.get_energy_use_sankey_plot(state_flow),
101
+ use_container_width=True
102
+ )
103
+
104
+ st.divider()
105
+
106
+ # Row 2: Individual utility explorer
107
+ st.subheader("Individual Utility — Energy Flow")
108
+ st.caption(
109
+ "Select a utility to see its specific energy breakdown. "
110
+ "Compare the Losses band against the state aggregate above."
111
+ )
112
+
113
+ utility_names = sorted(state_df['Utility.Name'].dropna().unique())
114
+ selected_utility = st.selectbox("Select a utility", options=utility_names)
115
+ utility_row = state_df[state_df['Utility.Name'] == selected_utility].iloc[0]
116
+ utility_flow = data_util.get_utility_usage(utility_row, level="Utility")
117
+
118
+ st.plotly_chart(
119
+ plot_util.get_energy_use_sankey_plot(utility_flow),
120
+ use_container_width=True
121
+ )
utility_efficiency_fairness.ipynb ADDED
@@ -0,0 +1,1622 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "606940eb",
6
+ "metadata": {},
7
+ "source": [
8
+ "# Electricity Utility Fairness Residential Rate Analysis\n",
9
+ "\n",
10
+ "This notebook investigates a fundamental fairness question in the U.S. electricity sector: **do residential customers end up paying more when their utility operates inefficiently?**\n",
11
+ "\n",
12
+ "Using the [CORGIS Electricity Dataset](https://corgis-edu.github.io/corgis/python/electricity/) — a structured derivative of the U.S. EIA Form 861 survey — the analysis engineers a set of efficiency and equity metrics, then examines their statistical relationships through a suite of interactive visualizations. New York State serves as the primary case study."
13
+ ]
14
+ },
15
+ {
16
+ "cell_type": "markdown",
17
+ "id": "fe53b9ab",
18
+ "metadata": {},
19
+ "source": [
20
+ "---\n",
21
+ "## 1. Setup\n",
22
+ "- `electricity` is the CORGIS data loader.\n",
23
+ "<br>\n",
24
+ "- `data_util` handles all data preparation and filtering. \n",
25
+ "- `plot_util` contains every chart-building function."
26
+ ]
27
+ },
28
+ {
29
+ "cell_type": "code",
30
+ "execution_count": 1,
31
+ "id": "e862a1c5",
32
+ "metadata": {},
33
+ "outputs": [],
34
+ "source": [
35
+ "import pandas as pd\n",
36
+ "\n",
37
+ "import data.electricity as electricity\n",
38
+ "\n",
39
+ "import src.util.plot_util as plot_util\n",
40
+ "import src.util.data_util as data_util"
41
+ ]
42
+ },
43
+ {
44
+ "cell_type": "markdown",
45
+ "id": "549a099a",
46
+ "metadata": {},
47
+ "source": [
48
+ "### 1.1 Data Loading & Metric Engineering\n",
49
+ "\n",
50
+ "The raw dataset is a list of nested dictionaries — one record per utility. `prepare_data` engineers the following derived metrics from the raw columns:\n",
51
+ "\n",
52
+ "| Metric | Formula | Interpretation |\n",
53
+ "|---|---|---|\n",
54
+ "| `Residential Unit Price` | `(Residential Revenue / Residential Sales) × 1000` | Residential rate in $/MWh |\n",
55
+ "| `Industrial Unit Price` | `(Industrial Revenue / Industrial Sales) × 1000` | Industrial rate in $/MWh |\n",
56
+ "| `Industrial Revenue Ratio` | `(Industrial Revenue / Total Retail Revenue) × 100` | % of a utility's revenue from industrial customers |\n",
57
+ "| `Price Spread` | `Residential Unit Price − Industrial Unit Price` | Pricing gap between Residential and Industrial customers |\n",
58
+ "| `System Loss Percentage` | `(Energy Losses / Total Energy) × 100` | Energy lost in transmission/distribution as % of supply |\n",
59
+ "| `Load Factor` | `Total Energy / (Summer Peak Demand × Hours Per Year)` | Utilization efficiency — how fully the system capacity is used |\n",
60
+ "\n",
61
+ "**Note on LoadFactor:**\n",
62
+ " A value of 1.0 means the utility delivers energy at exactly its peak demand capacity around the clock — theoretically perfect utilization. Real utilities typically range from 0.3–0.7. A low load factor suggests underused infrastructure whose fixed costs are spread across fewer delivered MWh."
63
+ ]
64
+ },
65
+ {
66
+ "cell_type": "code",
67
+ "execution_count": 2,
68
+ "id": "6b7d5309",
69
+ "metadata": {},
70
+ "outputs": [],
71
+ "source": [
72
+ "# Retrieve entire US-based dataset\n",
73
+ "utility = electricity.get_utility()\n",
74
+ "df = pd.json_normalize(utility)\n",
75
+ "\n",
76
+ "# Limit to the state of New York and relevant columns\n",
77
+ "ny_df = data_util.get_state_data('NY', df)\n",
78
+ "\n",
79
+ "# Add key metrics for use in later plots\n",
80
+ "ny_df = data_util.prepare_data(ny_df)"
81
+ ]
82
+ },
83
+ {
84
+ "cell_type": "markdown",
85
+ "id": "7327b62c",
86
+ "metadata": {},
87
+ "source": [
88
+ "---\n",
89
+ "## 2. State Selection — Why New York?\n",
90
+ "\n",
91
+ "Rather than choosing a state arbitrarily, I created use `get_state_variance` to rank all 50 states across five dimensions that could make for a rich case study:\n",
92
+ "\n",
93
+ "| | Why it matters |\n",
94
+ "|---|---|\n",
95
+ "| **# Utilities** | More utilities = more data points and statistical power |\n",
96
+ "| **# Utility Types** | More ownership models = richer segmentation and comparison |\n",
97
+ "| **Residential Price Std. Dev.** | High spread = real price variation to explain, not a flat market |\n",
98
+ "| **Max System Loss %** | Outlier utilities = compelling examples for the core hypothesis |\n",
99
+ "| **Industrial Revenue %** | High industrial dependency = cross-subsidization dynamics worth examining |\n",
100
+ "\n",
101
+ "The table below highlights the top 10 states by residential price variance. **New York (highlighted)** ranks highly because it uniquely satisfies *all five* criteria simultaneously."
102
+ ]
103
+ },
104
+ {
105
+ "cell_type": "code",
106
+ "execution_count": 3,
107
+ "id": "ad5df068",
108
+ "metadata": {},
109
+ "outputs": [],
110
+ "source": [
111
+ "state_variance = data_util.get_state_variance(data_util.prepare_data(df))\n",
112
+ "\n",
113
+ "top_variance_table = plot_util.get_state_variance_table(state_variance)\n",
114
+ "\n",
115
+ "# top_variance_table.show()"
116
+ ]
117
+ },
118
+ {
119
+ "cell_type": "markdown",
120
+ "id": "3455bc7c",
121
+ "metadata": {},
122
+ "source": [
123
+ "### Why New York specifically?\n",
124
+ "\n",
125
+ "Beyond the rankings, New York offers several structural advantages for this analysis:\n",
126
+ "\n",
127
+ "- **6 distinct utility ownership types** — more than almost any other state, enabling meaningful cross-model comparisons within a single regulatory environment.\n",
128
+ "- **100+ individual utilities** — sufficient sample size to draw statistically meaningful conclusions.\n",
129
+ "- **Extreme geographic diversity** — Con Edison serves dense urban Manhattan while small rural cooperatives serve upstate communities. This natural variation creates a richer distribution of efficiency and pricing outcomes than a more homogeneous state would.\n",
130
+ "- **High system loss outliers** — NY's transmission infrastructure includes some of the oldest urban grid segments in the country, producing the high-loss edge cases that make the core hypothesis testable.\n",
131
+ "- **Active regulatory environment** — the NY Public Service Commission is one of the most scrutinized utility regulators in the U.S., meaning this analysis connects directly to real-world policy questions."
132
+ ]
133
+ },
134
+ {
135
+ "cell_type": "markdown",
136
+ "id": "1992b6ef",
137
+ "metadata": {},
138
+ "source": [
139
+ "---\n",
140
+ "## 3. Ownership Model Analysis\n",
141
+ "\n",
142
+ "Before testing the efficiency hypothesis, we establish baseline pricing distributions by ownership model. This matters because ownership type is a structural variable — investor-owned utilities (IOUs) answer to shareholders and profit motives, while cooperatives and municipals serve member-owners or public ratepayers. If ownership type already explains most of the price variance, efficiency metrics may be redundant."
143
+ ]
144
+ },
145
+ {
146
+ "cell_type": "code",
147
+ "execution_count": 11,
148
+ "id": "54f2b308",
149
+ "metadata": {},
150
+ "outputs": [
151
+ {
152
+ "data": {
153
+ "application/vnd.plotly.v1+json": {
154
+ "config": {
155
+ "plotlyServerURL": "https://plot.ly"
156
+ },
157
+ "data": [
158
+ {
159
+ "alignmentgroup": "True",
160
+ "boxpoints": "all",
161
+ "fillcolor": "rgba(255,255,255,0)",
162
+ "hoveron": "points",
163
+ "hovertemplate": "<b>%{hovertext}</b><br><br>Type=%{x}<br>Residential Premium ($/MWh)=%{y}<extra></extra>",
164
+ "hovertext": [
165
+ "Agway Energy Services, LLC",
166
+ "Energy Coop of New York, Inc",
167
+ "Robison Energy, LLC",
168
+ "U.S. Energy Partners LLC",
169
+ "NOCO Electric",
170
+ "Columbia Utilities Power, LLC",
171
+ "Hudson Energy Services",
172
+ "Palmco Power NJ, LLC",
173
+ "Major Energy Electric Services",
174
+ "Palmco Power PA, LLC",
175
+ "Palmco Power MD, LLC",
176
+ "Plymouth Rock Energy, LLC",
177
+ "Palmco Power OH, LLC",
178
+ "Stream Energy New York, LLC",
179
+ "EnergyMark, LLC",
180
+ "Palmco Power IL, LLC",
181
+ "Respond Power LLC",
182
+ "Kiwi Energy NY LLC",
183
+ "SmartEnergy Holdings, LLC",
184
+ "Alpha Gas and Electric, LLC",
185
+ "Energy.Me Midwest LLC",
186
+ "Marathon Power LLC",
187
+ "Harborside Energy, LLC",
188
+ "Abest Power & Gas, LLC",
189
+ "MPower Energy LLC",
190
+ "Palmco Power MA, LLC",
191
+ "Agera Energy LLC",
192
+ "Greenlight Energy Inc.",
193
+ "New Wave Energy Corporation",
194
+ "HIKO Energy, LLC",
195
+ "Aequitas Energy Inc",
196
+ "Great Eastern Energy",
197
+ "East Coast Power & Gas, LLC",
198
+ "Spring Energy RRH LLC dba Spring Power &",
199
+ "MPower Energy NJ LLC",
200
+ "Atlantic Energy LLC",
201
+ "Palmco Power DC, LLC",
202
+ "Palmco Power DE, LLC",
203
+ "South Bay Energy Corp.",
204
+ "Flanders Energy, LLC",
205
+ "Pure Energy USA, LLC",
206
+ "First Choice Energy LLC",
207
+ "Agressive Energy LLC"
208
+ ],
209
+ "legendgroup": "Retail Power Marketer",
210
+ "line": {
211
+ "color": "rgba(255,255,255,0)"
212
+ },
213
+ "marker": {
214
+ "color": "rgb(95, 70, 144)"
215
+ },
216
+ "name": "Retail Power Marketer",
217
+ "offsetgroup": "Retail Power Marketer",
218
+ "orientation": "v",
219
+ "pointpos": 0,
220
+ "showlegend": true,
221
+ "type": "box",
222
+ "x": [
223
+ "Retail Power Marketer",
224
+ "Retail Power Marketer",
225
+ "Retail Power Marketer",
226
+ "Retail Power Marketer",
227
+ "Retail Power Marketer",
228
+ "Retail Power Marketer",
229
+ "Retail Power Marketer",
230
+ "Retail Power Marketer",
231
+ "Retail Power Marketer",
232
+ "Retail Power Marketer",
233
+ "Retail Power Marketer",
234
+ "Retail Power Marketer",
235
+ "Retail Power Marketer",
236
+ "Retail Power Marketer",
237
+ "Retail Power Marketer",
238
+ "Retail Power Marketer",
239
+ "Retail Power Marketer",
240
+ "Retail Power Marketer",
241
+ "Retail Power Marketer",
242
+ "Retail Power Marketer",
243
+ "Retail Power Marketer",
244
+ "Retail Power Marketer",
245
+ "Retail Power Marketer",
246
+ "Retail Power Marketer",
247
+ "Retail Power Marketer",
248
+ "Retail Power Marketer",
249
+ "Retail Power Marketer",
250
+ "Retail Power Marketer",
251
+ "Retail Power Marketer",
252
+ "Retail Power Marketer",
253
+ "Retail Power Marketer",
254
+ "Retail Power Marketer",
255
+ "Retail Power Marketer",
256
+ "Retail Power Marketer",
257
+ "Retail Power Marketer",
258
+ "Retail Power Marketer",
259
+ "Retail Power Marketer",
260
+ "Retail Power Marketer",
261
+ "Retail Power Marketer",
262
+ "Retail Power Marketer",
263
+ "Retail Power Marketer",
264
+ "Retail Power Marketer",
265
+ "Retail Power Marketer"
266
+ ],
267
+ "x0": " ",
268
+ "xaxis": "x",
269
+ "y": {
270
+ "bdata": "1HdJqZaIVEDlcPM237NFQJ4t+TeSfVVA9Y6hQZjxREC7n0AeSF5FQAJayxan5mdA+NB2EXoPWEDkHoKBHplrQAf1wZiCkFxAdfEDDupzYUBcKbC7K7hjQNg9bETnNGBAwv1PMJqEXEBzGFLAfrxQQMb9Fc3b/0VA4ydnH2Q/VkBOiMxbaUtiQE1aVCBa8FtAdyZPlzdMWUD/BbayJZdaQNFZ0CR2H1NAVaW6iXyDUkAWCcnsvT5kQL6sGTMR6VRAOFE8FqijYEDwxWACoKxkQLReHZWsKFhANJ/x5rw1XUAUtkGpzFhMQEG4G/seUWFA7ZpZKnJNXEC+qlbjMDJZQMjcovLsXVRAJYv3tlpiWkAv2u+X74BeQM7YHwYLZ2BAFkOHeY7iW0DXGD4bRKpZQNjxRK4pNV9AnhCBas4qZEATgY2Ufz1jQNvxsf19yVhAbl5aGi50UUA=",
271
+ "dtype": "f8"
272
+ },
273
+ "y0": " ",
274
+ "yaxis": "y"
275
+ },
276
+ {
277
+ "alignmentgroup": "True",
278
+ "boxpoints": "all",
279
+ "fillcolor": "rgba(255,255,255,0)",
280
+ "hoveron": "points",
281
+ "hovertemplate": "<b>%{hovertext}</b><br><br>Type=%{x}<br>Residential Premium ($/MWh)=%{y}<extra></extra>",
282
+ "hovertext": [
283
+ "Village of Arcade - (NY)",
284
+ "Village of Akron - (NY)",
285
+ "Village of Andover - (NY)",
286
+ "Village of Angelica - (NY)",
287
+ "Bath Electric Gas & Water",
288
+ "Village of Bergen - (NY)",
289
+ "Village of Boonville - (NY)",
290
+ "Village of Brocton - (NY)",
291
+ "Village of Castile - (NY)",
292
+ "Village of Churchville - (NY)",
293
+ "Village of Endicott - (NY)",
294
+ "Village of Fairport - (NY)",
295
+ "Village of Frankfort - (NY)",
296
+ "Village of Freeport - (NY)",
297
+ "Village of Green Island - (NY)",
298
+ "Village of Greene - (NY)",
299
+ "Village of Groton - (NY)",
300
+ "Village of Hamilton - (NY)",
301
+ "Village of Holley - (NY)",
302
+ "Village of Ilion - (NY)",
303
+ "Jamestown Board of Public Util",
304
+ "Lake Placid Village, Inc - (NY)",
305
+ "Village of Little Valley - (NY)",
306
+ "Village of Marathon - (NY)",
307
+ "Town of Massena - (NY)",
308
+ "Village of Mayville - (NY)",
309
+ "Mohawk Municipal Comm",
310
+ "Village of Penn Yan - (NY)",
311
+ "Village of Philadelphia - (NY)",
312
+ "City of Plattsburgh - (NY)",
313
+ "Village of Richmondville - (NY)",
314
+ "Village of Rockville Centre - (NY)",
315
+ "Village of Rouses Point - (NY)",
316
+ "City of Salamanca - (NY)",
317
+ "Village of Sherburne - (NY)",
318
+ "City of Sherrill - (NY)",
319
+ "Village of Silver Springs - (NY)",
320
+ "Village of Skaneateles - (NY)",
321
+ "Village of Solvay - (NY)",
322
+ "Village of Spencerport - (NY)",
323
+ "Village of Springville - (NY)",
324
+ "Village of Theresa - (NY)",
325
+ "Village of Tupper Lake - (NY)",
326
+ "Village of Watkins Glen - (NY)",
327
+ "Village of Wellsville - (NY)",
328
+ "Village of Westfield - (NY)"
329
+ ],
330
+ "legendgroup": "Municipal",
331
+ "line": {
332
+ "color": "rgba(255,255,255,0)"
333
+ },
334
+ "marker": {
335
+ "color": "rgb(29, 105, 150)"
336
+ },
337
+ "name": "Municipal",
338
+ "offsetgroup": "Municipal",
339
+ "orientation": "v",
340
+ "pointpos": 0,
341
+ "showlegend": true,
342
+ "type": "box",
343
+ "x": [
344
+ "Municipal",
345
+ "Municipal",
346
+ "Municipal",
347
+ "Municipal",
348
+ "Municipal",
349
+ "Municipal",
350
+ "Municipal",
351
+ "Municipal",
352
+ "Municipal",
353
+ "Municipal",
354
+ "Municipal",
355
+ "Municipal",
356
+ "Municipal",
357
+ "Municipal",
358
+ "Municipal",
359
+ "Municipal",
360
+ "Municipal",
361
+ "Municipal",
362
+ "Municipal",
363
+ "Municipal",
364
+ "Municipal",
365
+ "Municipal",
366
+ "Municipal",
367
+ "Municipal",
368
+ "Municipal",
369
+ "Municipal",
370
+ "Municipal",
371
+ "Municipal",
372
+ "Municipal",
373
+ "Municipal",
374
+ "Municipal",
375
+ "Municipal",
376
+ "Municipal",
377
+ "Municipal",
378
+ "Municipal",
379
+ "Municipal",
380
+ "Municipal",
381
+ "Municipal",
382
+ "Municipal",
383
+ "Municipal",
384
+ "Municipal",
385
+ "Municipal",
386
+ "Municipal",
387
+ "Municipal",
388
+ "Municipal",
389
+ "Municipal"
390
+ ],
391
+ "x0": " ",
392
+ "xaxis": "x",
393
+ "y": {
394
+ "bdata": "4zx0l/glSkCrn6PKMxZKQDFIRJo2x0VAGOWbWs1/T0CCUB+yIaJNQEZpzEOSWVNAykfneWTjSUC5RSoUu+tGQD5k23fSFk1Azmj3sPXNUkDJCaumGFlQQBb2vzXcvktA7+AqSao3Q0CQqdv9nMlfQC8zhjbsPltAXfLkhENnS0DUHfrfiDZKQJdz0sw7pkhAl5Nt4wsgTkCCI8bcBMRKQNIB9qAznlRAw5X6vW53S0At0Q+nexBTQCjVNgwQnVFASdVlz85LTkAsdSyDtF5QQNhzo/V/JkVArLuSbKG6QkCXOXo4ck9OQMfHf05/EUdAK8FbUIDWUkDSX/M3ctteQF6gAPx8Ej1Aw45Qct0fSUC0jal315FLQNRIT1cjHU1AjVKkDWvMUUDTH1W7sehEQAeqFkKflkxAe68yr+vZRkBpZcd2fvFKQMx1qVre1FNApaDbJmbZSkCWccuMD2tCQNSJR5YBWU5ARDUJMx5yTEA=",
395
+ "dtype": "f8"
396
+ },
397
+ "y0": " ",
398
+ "yaxis": "y"
399
+ },
400
+ {
401
+ "alignmentgroup": "True",
402
+ "boxpoints": "all",
403
+ "fillcolor": "rgba(255,255,255,0)",
404
+ "hoveron": "points",
405
+ "hovertemplate": "<b>%{hovertext}</b><br><br>Type=%{x}<br>Residential Premium ($/MWh)=%{y}<extra></extra>",
406
+ "hovertext": [
407
+ "Central Hudson Gas & Elec Corp",
408
+ "Consolidated Edison Co-NY Inc",
409
+ "Fishers Island Utility Co Inc",
410
+ "Massachusetts Electric Co",
411
+ "New York State Elec & Gas Corp",
412
+ "Niagara Mohawk Power Corp.",
413
+ "Orange & Rockland Utils Inc",
414
+ "Pike County Light & Power Co",
415
+ "Rochester Gas & Electric Corp",
416
+ "Rockland Electric Co"
417
+ ],
418
+ "legendgroup": "Investor Owned",
419
+ "line": {
420
+ "color": "rgba(255,255,255,0)"
421
+ },
422
+ "marker": {
423
+ "color": "rgb(56, 166, 165)"
424
+ },
425
+ "name": "Investor Owned",
426
+ "offsetgroup": "Investor Owned",
427
+ "orientation": "v",
428
+ "pointpos": 0,
429
+ "showlegend": true,
430
+ "type": "box",
431
+ "x": [
432
+ "Investor Owned",
433
+ "Investor Owned",
434
+ "Investor Owned",
435
+ "Investor Owned",
436
+ "Investor Owned",
437
+ "Investor Owned",
438
+ "Investor Owned",
439
+ "Investor Owned",
440
+ "Investor Owned",
441
+ "Investor Owned"
442
+ ],
443
+ "x0": " ",
444
+ "xaxis": "x",
445
+ "y": {
446
+ "bdata": "DlfHeglgZEAky8QEPyJtQOHh4eHh4XNAiNVwftXYZkBAg+xCRm5aQLlstvUwUV1AP0jm2o/iZEC7M68wZX1eQCRaHECiYF5AzRC7X0JqY0A=",
447
+ "dtype": "f8"
448
+ },
449
+ "y0": " ",
450
+ "yaxis": "y"
451
+ },
452
+ {
453
+ "alignmentgroup": "True",
454
+ "boxpoints": "all",
455
+ "fillcolor": "rgba(255,255,255,0)",
456
+ "hoveron": "points",
457
+ "hovertemplate": "<b>%{hovertext}</b><br><br>Type=%{x}<br>Residential Premium ($/MWh)=%{y}<extra></extra>",
458
+ "hovertext": [
459
+ "Delaware County Elec Coop Inc",
460
+ "North Shore Towers Apts Inc",
461
+ "Otsego Electric Coop, Inc",
462
+ "Steuben Rural Elec Coop, Inc"
463
+ ],
464
+ "legendgroup": "Cooperative",
465
+ "line": {
466
+ "color": "rgba(255,255,255,0)"
467
+ },
468
+ "marker": {
469
+ "color": "rgb(15, 133, 84)"
470
+ },
471
+ "name": "Cooperative",
472
+ "offsetgroup": "Cooperative",
473
+ "orientation": "v",
474
+ "pointpos": 0,
475
+ "showlegend": true,
476
+ "type": "box",
477
+ "x": [
478
+ "Cooperative",
479
+ "Cooperative",
480
+ "Cooperative",
481
+ "Cooperative"
482
+ ],
483
+ "x0": " ",
484
+ "xaxis": "x",
485
+ "y": {
486
+ "bdata": "+aWVpF9ZYUDMuXx9SDRxQE3e4teNEl9Aw0BzNP3fXkA=",
487
+ "dtype": "f8"
488
+ },
489
+ "y0": " ",
490
+ "yaxis": "y"
491
+ },
492
+ {
493
+ "alignmentgroup": "True",
494
+ "boxpoints": "all",
495
+ "fillcolor": "rgba(255,255,255,0)",
496
+ "hoveron": "points",
497
+ "hovertemplate": "<b>%{hovertext}</b><br><br>Type=%{x}<br>Residential Premium ($/MWh)=%{y}<extra></extra>",
498
+ "hovertext": [
499
+ "Long Island Power Authority"
500
+ ],
501
+ "legendgroup": "State",
502
+ "line": {
503
+ "color": "rgba(255,255,255,0)"
504
+ },
505
+ "marker": {
506
+ "color": "rgb(115, 175, 72)"
507
+ },
508
+ "name": "State",
509
+ "offsetgroup": "State",
510
+ "orientation": "v",
511
+ "pointpos": 0,
512
+ "showlegend": true,
513
+ "type": "box",
514
+ "x": [
515
+ "State"
516
+ ],
517
+ "x0": " ",
518
+ "xaxis": "x",
519
+ "y": {
520
+ "bdata": "WOYOiWF7aUA=",
521
+ "dtype": "f8"
522
+ },
523
+ "y0": " ",
524
+ "yaxis": "y"
525
+ },
526
+ {
527
+ "alignmentgroup": "True",
528
+ "boxpoints": "all",
529
+ "fillcolor": "rgba(255,255,255,0)",
530
+ "hoveron": "points",
531
+ "hovertemplate": "<b>%{hovertext}</b><br><br>Type=%{x}<br>Residential Premium ($/MWh)=%{y}<extra></extra>",
532
+ "hovertext": [
533
+ "Greenbacker Renewable Energy Corporation"
534
+ ],
535
+ "legendgroup": "Behind the Meter",
536
+ "line": {
537
+ "color": "rgba(255,255,255,0)"
538
+ },
539
+ "marker": {
540
+ "color": "rgb(237, 173, 8)"
541
+ },
542
+ "name": "Behind the Meter",
543
+ "offsetgroup": "Behind the Meter",
544
+ "orientation": "v",
545
+ "pointpos": 0,
546
+ "showlegend": true,
547
+ "type": "box",
548
+ "x": [
549
+ "Behind the Meter"
550
+ ],
551
+ "x0": " ",
552
+ "xaxis": "x",
553
+ "y": {
554
+ "bdata": "tp977y/kYkA=",
555
+ "dtype": "f8"
556
+ },
557
+ "y0": " ",
558
+ "yaxis": "y"
559
+ }
560
+ ],
561
+ "layout": {
562
+ "boxmode": "overlay",
563
+ "legend": {
564
+ "title": {
565
+ "text": "Type"
566
+ },
567
+ "tracegroupgap": 0
568
+ },
569
+ "showlegend": false,
570
+ "template": {
571
+ "data": {
572
+ "bar": [
573
+ {
574
+ "error_x": {
575
+ "color": "#2a3f5f"
576
+ },
577
+ "error_y": {
578
+ "color": "#2a3f5f"
579
+ },
580
+ "marker": {
581
+ "line": {
582
+ "color": "white",
583
+ "width": 0.5
584
+ },
585
+ "pattern": {
586
+ "fillmode": "overlay",
587
+ "size": 10,
588
+ "solidity": 0.2
589
+ }
590
+ },
591
+ "type": "bar"
592
+ }
593
+ ],
594
+ "barpolar": [
595
+ {
596
+ "marker": {
597
+ "line": {
598
+ "color": "white",
599
+ "width": 0.5
600
+ },
601
+ "pattern": {
602
+ "fillmode": "overlay",
603
+ "size": 10,
604
+ "solidity": 0.2
605
+ }
606
+ },
607
+ "type": "barpolar"
608
+ }
609
+ ],
610
+ "carpet": [
611
+ {
612
+ "aaxis": {
613
+ "endlinecolor": "#2a3f5f",
614
+ "gridcolor": "#C8D4E3",
615
+ "linecolor": "#C8D4E3",
616
+ "minorgridcolor": "#C8D4E3",
617
+ "startlinecolor": "#2a3f5f"
618
+ },
619
+ "baxis": {
620
+ "endlinecolor": "#2a3f5f",
621
+ "gridcolor": "#C8D4E3",
622
+ "linecolor": "#C8D4E3",
623
+ "minorgridcolor": "#C8D4E3",
624
+ "startlinecolor": "#2a3f5f"
625
+ },
626
+ "type": "carpet"
627
+ }
628
+ ],
629
+ "choropleth": [
630
+ {
631
+ "colorbar": {
632
+ "outlinewidth": 0,
633
+ "ticks": ""
634
+ },
635
+ "type": "choropleth"
636
+ }
637
+ ],
638
+ "contour": [
639
+ {
640
+ "colorbar": {
641
+ "outlinewidth": 0,
642
+ "ticks": ""
643
+ },
644
+ "colorscale": [
645
+ [
646
+ 0,
647
+ "#0d0887"
648
+ ],
649
+ [
650
+ 0.1111111111111111,
651
+ "#46039f"
652
+ ],
653
+ [
654
+ 0.2222222222222222,
655
+ "#7201a8"
656
+ ],
657
+ [
658
+ 0.3333333333333333,
659
+ "#9c179e"
660
+ ],
661
+ [
662
+ 0.4444444444444444,
663
+ "#bd3786"
664
+ ],
665
+ [
666
+ 0.5555555555555556,
667
+ "#d8576b"
668
+ ],
669
+ [
670
+ 0.6666666666666666,
671
+ "#ed7953"
672
+ ],
673
+ [
674
+ 0.7777777777777778,
675
+ "#fb9f3a"
676
+ ],
677
+ [
678
+ 0.8888888888888888,
679
+ "#fdca26"
680
+ ],
681
+ [
682
+ 1,
683
+ "#f0f921"
684
+ ]
685
+ ],
686
+ "type": "contour"
687
+ }
688
+ ],
689
+ "contourcarpet": [
690
+ {
691
+ "colorbar": {
692
+ "outlinewidth": 0,
693
+ "ticks": ""
694
+ },
695
+ "type": "contourcarpet"
696
+ }
697
+ ],
698
+ "heatmap": [
699
+ {
700
+ "colorbar": {
701
+ "outlinewidth": 0,
702
+ "ticks": ""
703
+ },
704
+ "colorscale": [
705
+ [
706
+ 0,
707
+ "#0d0887"
708
+ ],
709
+ [
710
+ 0.1111111111111111,
711
+ "#46039f"
712
+ ],
713
+ [
714
+ 0.2222222222222222,
715
+ "#7201a8"
716
+ ],
717
+ [
718
+ 0.3333333333333333,
719
+ "#9c179e"
720
+ ],
721
+ [
722
+ 0.4444444444444444,
723
+ "#bd3786"
724
+ ],
725
+ [
726
+ 0.5555555555555556,
727
+ "#d8576b"
728
+ ],
729
+ [
730
+ 0.6666666666666666,
731
+ "#ed7953"
732
+ ],
733
+ [
734
+ 0.7777777777777778,
735
+ "#fb9f3a"
736
+ ],
737
+ [
738
+ 0.8888888888888888,
739
+ "#fdca26"
740
+ ],
741
+ [
742
+ 1,
743
+ "#f0f921"
744
+ ]
745
+ ],
746
+ "type": "heatmap"
747
+ }
748
+ ],
749
+ "histogram": [
750
+ {
751
+ "marker": {
752
+ "pattern": {
753
+ "fillmode": "overlay",
754
+ "size": 10,
755
+ "solidity": 0.2
756
+ }
757
+ },
758
+ "type": "histogram"
759
+ }
760
+ ],
761
+ "histogram2d": [
762
+ {
763
+ "colorbar": {
764
+ "outlinewidth": 0,
765
+ "ticks": ""
766
+ },
767
+ "colorscale": [
768
+ [
769
+ 0,
770
+ "#0d0887"
771
+ ],
772
+ [
773
+ 0.1111111111111111,
774
+ "#46039f"
775
+ ],
776
+ [
777
+ 0.2222222222222222,
778
+ "#7201a8"
779
+ ],
780
+ [
781
+ 0.3333333333333333,
782
+ "#9c179e"
783
+ ],
784
+ [
785
+ 0.4444444444444444,
786
+ "#bd3786"
787
+ ],
788
+ [
789
+ 0.5555555555555556,
790
+ "#d8576b"
791
+ ],
792
+ [
793
+ 0.6666666666666666,
794
+ "#ed7953"
795
+ ],
796
+ [
797
+ 0.7777777777777778,
798
+ "#fb9f3a"
799
+ ],
800
+ [
801
+ 0.8888888888888888,
802
+ "#fdca26"
803
+ ],
804
+ [
805
+ 1,
806
+ "#f0f921"
807
+ ]
808
+ ],
809
+ "type": "histogram2d"
810
+ }
811
+ ],
812
+ "histogram2dcontour": [
813
+ {
814
+ "colorbar": {
815
+ "outlinewidth": 0,
816
+ "ticks": ""
817
+ },
818
+ "colorscale": [
819
+ [
820
+ 0,
821
+ "#0d0887"
822
+ ],
823
+ [
824
+ 0.1111111111111111,
825
+ "#46039f"
826
+ ],
827
+ [
828
+ 0.2222222222222222,
829
+ "#7201a8"
830
+ ],
831
+ [
832
+ 0.3333333333333333,
833
+ "#9c179e"
834
+ ],
835
+ [
836
+ 0.4444444444444444,
837
+ "#bd3786"
838
+ ],
839
+ [
840
+ 0.5555555555555556,
841
+ "#d8576b"
842
+ ],
843
+ [
844
+ 0.6666666666666666,
845
+ "#ed7953"
846
+ ],
847
+ [
848
+ 0.7777777777777778,
849
+ "#fb9f3a"
850
+ ],
851
+ [
852
+ 0.8888888888888888,
853
+ "#fdca26"
854
+ ],
855
+ [
856
+ 1,
857
+ "#f0f921"
858
+ ]
859
+ ],
860
+ "type": "histogram2dcontour"
861
+ }
862
+ ],
863
+ "mesh3d": [
864
+ {
865
+ "colorbar": {
866
+ "outlinewidth": 0,
867
+ "ticks": ""
868
+ },
869
+ "type": "mesh3d"
870
+ }
871
+ ],
872
+ "parcoords": [
873
+ {
874
+ "line": {
875
+ "colorbar": {
876
+ "outlinewidth": 0,
877
+ "ticks": ""
878
+ }
879
+ },
880
+ "type": "parcoords"
881
+ }
882
+ ],
883
+ "pie": [
884
+ {
885
+ "automargin": true,
886
+ "type": "pie"
887
+ }
888
+ ],
889
+ "scatter": [
890
+ {
891
+ "fillpattern": {
892
+ "fillmode": "overlay",
893
+ "size": 10,
894
+ "solidity": 0.2
895
+ },
896
+ "type": "scatter"
897
+ }
898
+ ],
899
+ "scatter3d": [
900
+ {
901
+ "line": {
902
+ "colorbar": {
903
+ "outlinewidth": 0,
904
+ "ticks": ""
905
+ }
906
+ },
907
+ "marker": {
908
+ "colorbar": {
909
+ "outlinewidth": 0,
910
+ "ticks": ""
911
+ }
912
+ },
913
+ "type": "scatter3d"
914
+ }
915
+ ],
916
+ "scattercarpet": [
917
+ {
918
+ "marker": {
919
+ "colorbar": {
920
+ "outlinewidth": 0,
921
+ "ticks": ""
922
+ }
923
+ },
924
+ "type": "scattercarpet"
925
+ }
926
+ ],
927
+ "scattergeo": [
928
+ {
929
+ "marker": {
930
+ "colorbar": {
931
+ "outlinewidth": 0,
932
+ "ticks": ""
933
+ }
934
+ },
935
+ "type": "scattergeo"
936
+ }
937
+ ],
938
+ "scattergl": [
939
+ {
940
+ "marker": {
941
+ "colorbar": {
942
+ "outlinewidth": 0,
943
+ "ticks": ""
944
+ }
945
+ },
946
+ "type": "scattergl"
947
+ }
948
+ ],
949
+ "scattermap": [
950
+ {
951
+ "marker": {
952
+ "colorbar": {
953
+ "outlinewidth": 0,
954
+ "ticks": ""
955
+ }
956
+ },
957
+ "type": "scattermap"
958
+ }
959
+ ],
960
+ "scattermapbox": [
961
+ {
962
+ "marker": {
963
+ "colorbar": {
964
+ "outlinewidth": 0,
965
+ "ticks": ""
966
+ }
967
+ },
968
+ "type": "scattermapbox"
969
+ }
970
+ ],
971
+ "scatterpolar": [
972
+ {
973
+ "marker": {
974
+ "colorbar": {
975
+ "outlinewidth": 0,
976
+ "ticks": ""
977
+ }
978
+ },
979
+ "type": "scatterpolar"
980
+ }
981
+ ],
982
+ "scatterpolargl": [
983
+ {
984
+ "marker": {
985
+ "colorbar": {
986
+ "outlinewidth": 0,
987
+ "ticks": ""
988
+ }
989
+ },
990
+ "type": "scatterpolargl"
991
+ }
992
+ ],
993
+ "scatterternary": [
994
+ {
995
+ "marker": {
996
+ "colorbar": {
997
+ "outlinewidth": 0,
998
+ "ticks": ""
999
+ }
1000
+ },
1001
+ "type": "scatterternary"
1002
+ }
1003
+ ],
1004
+ "surface": [
1005
+ {
1006
+ "colorbar": {
1007
+ "outlinewidth": 0,
1008
+ "ticks": ""
1009
+ },
1010
+ "colorscale": [
1011
+ [
1012
+ 0,
1013
+ "#0d0887"
1014
+ ],
1015
+ [
1016
+ 0.1111111111111111,
1017
+ "#46039f"
1018
+ ],
1019
+ [
1020
+ 0.2222222222222222,
1021
+ "#7201a8"
1022
+ ],
1023
+ [
1024
+ 0.3333333333333333,
1025
+ "#9c179e"
1026
+ ],
1027
+ [
1028
+ 0.4444444444444444,
1029
+ "#bd3786"
1030
+ ],
1031
+ [
1032
+ 0.5555555555555556,
1033
+ "#d8576b"
1034
+ ],
1035
+ [
1036
+ 0.6666666666666666,
1037
+ "#ed7953"
1038
+ ],
1039
+ [
1040
+ 0.7777777777777778,
1041
+ "#fb9f3a"
1042
+ ],
1043
+ [
1044
+ 0.8888888888888888,
1045
+ "#fdca26"
1046
+ ],
1047
+ [
1048
+ 1,
1049
+ "#f0f921"
1050
+ ]
1051
+ ],
1052
+ "type": "surface"
1053
+ }
1054
+ ],
1055
+ "table": [
1056
+ {
1057
+ "cells": {
1058
+ "fill": {
1059
+ "color": "#EBF0F8"
1060
+ },
1061
+ "line": {
1062
+ "color": "white"
1063
+ }
1064
+ },
1065
+ "header": {
1066
+ "fill": {
1067
+ "color": "#C8D4E3"
1068
+ },
1069
+ "line": {
1070
+ "color": "white"
1071
+ }
1072
+ },
1073
+ "type": "table"
1074
+ }
1075
+ ]
1076
+ },
1077
+ "layout": {
1078
+ "annotationdefaults": {
1079
+ "arrowcolor": "#2a3f5f",
1080
+ "arrowhead": 0,
1081
+ "arrowwidth": 1
1082
+ },
1083
+ "autotypenumbers": "strict",
1084
+ "coloraxis": {
1085
+ "colorbar": {
1086
+ "outlinewidth": 0,
1087
+ "ticks": ""
1088
+ }
1089
+ },
1090
+ "colorscale": {
1091
+ "diverging": [
1092
+ [
1093
+ 0,
1094
+ "#8e0152"
1095
+ ],
1096
+ [
1097
+ 0.1,
1098
+ "#c51b7d"
1099
+ ],
1100
+ [
1101
+ 0.2,
1102
+ "#de77ae"
1103
+ ],
1104
+ [
1105
+ 0.3,
1106
+ "#f1b6da"
1107
+ ],
1108
+ [
1109
+ 0.4,
1110
+ "#fde0ef"
1111
+ ],
1112
+ [
1113
+ 0.5,
1114
+ "#f7f7f7"
1115
+ ],
1116
+ [
1117
+ 0.6,
1118
+ "#e6f5d0"
1119
+ ],
1120
+ [
1121
+ 0.7,
1122
+ "#b8e186"
1123
+ ],
1124
+ [
1125
+ 0.8,
1126
+ "#7fbc41"
1127
+ ],
1128
+ [
1129
+ 0.9,
1130
+ "#4d9221"
1131
+ ],
1132
+ [
1133
+ 1,
1134
+ "#276419"
1135
+ ]
1136
+ ],
1137
+ "sequential": [
1138
+ [
1139
+ 0,
1140
+ "#0d0887"
1141
+ ],
1142
+ [
1143
+ 0.1111111111111111,
1144
+ "#46039f"
1145
+ ],
1146
+ [
1147
+ 0.2222222222222222,
1148
+ "#7201a8"
1149
+ ],
1150
+ [
1151
+ 0.3333333333333333,
1152
+ "#9c179e"
1153
+ ],
1154
+ [
1155
+ 0.4444444444444444,
1156
+ "#bd3786"
1157
+ ],
1158
+ [
1159
+ 0.5555555555555556,
1160
+ "#d8576b"
1161
+ ],
1162
+ [
1163
+ 0.6666666666666666,
1164
+ "#ed7953"
1165
+ ],
1166
+ [
1167
+ 0.7777777777777778,
1168
+ "#fb9f3a"
1169
+ ],
1170
+ [
1171
+ 0.8888888888888888,
1172
+ "#fdca26"
1173
+ ],
1174
+ [
1175
+ 1,
1176
+ "#f0f921"
1177
+ ]
1178
+ ],
1179
+ "sequentialminus": [
1180
+ [
1181
+ 0,
1182
+ "#0d0887"
1183
+ ],
1184
+ [
1185
+ 0.1111111111111111,
1186
+ "#46039f"
1187
+ ],
1188
+ [
1189
+ 0.2222222222222222,
1190
+ "#7201a8"
1191
+ ],
1192
+ [
1193
+ 0.3333333333333333,
1194
+ "#9c179e"
1195
+ ],
1196
+ [
1197
+ 0.4444444444444444,
1198
+ "#bd3786"
1199
+ ],
1200
+ [
1201
+ 0.5555555555555556,
1202
+ "#d8576b"
1203
+ ],
1204
+ [
1205
+ 0.6666666666666666,
1206
+ "#ed7953"
1207
+ ],
1208
+ [
1209
+ 0.7777777777777778,
1210
+ "#fb9f3a"
1211
+ ],
1212
+ [
1213
+ 0.8888888888888888,
1214
+ "#fdca26"
1215
+ ],
1216
+ [
1217
+ 1,
1218
+ "#f0f921"
1219
+ ]
1220
+ ]
1221
+ },
1222
+ "colorway": [
1223
+ "#636efa",
1224
+ "#EF553B",
1225
+ "#00cc96",
1226
+ "#ab63fa",
1227
+ "#FFA15A",
1228
+ "#19d3f3",
1229
+ "#FF6692",
1230
+ "#B6E880",
1231
+ "#FF97FF",
1232
+ "#FECB52"
1233
+ ],
1234
+ "font": {
1235
+ "color": "#2a3f5f"
1236
+ },
1237
+ "geo": {
1238
+ "bgcolor": "white",
1239
+ "lakecolor": "white",
1240
+ "landcolor": "white",
1241
+ "showlakes": true,
1242
+ "showland": true,
1243
+ "subunitcolor": "#C8D4E3"
1244
+ },
1245
+ "hoverlabel": {
1246
+ "align": "left"
1247
+ },
1248
+ "hovermode": "closest",
1249
+ "mapbox": {
1250
+ "style": "light"
1251
+ },
1252
+ "paper_bgcolor": "white",
1253
+ "plot_bgcolor": "white",
1254
+ "polar": {
1255
+ "angularaxis": {
1256
+ "gridcolor": "#EBF0F8",
1257
+ "linecolor": "#EBF0F8",
1258
+ "ticks": ""
1259
+ },
1260
+ "bgcolor": "white",
1261
+ "radialaxis": {
1262
+ "gridcolor": "#EBF0F8",
1263
+ "linecolor": "#EBF0F8",
1264
+ "ticks": ""
1265
+ }
1266
+ },
1267
+ "scene": {
1268
+ "xaxis": {
1269
+ "backgroundcolor": "white",
1270
+ "gridcolor": "#DFE8F3",
1271
+ "gridwidth": 2,
1272
+ "linecolor": "#EBF0F8",
1273
+ "showbackground": true,
1274
+ "ticks": "",
1275
+ "zerolinecolor": "#EBF0F8"
1276
+ },
1277
+ "yaxis": {
1278
+ "backgroundcolor": "white",
1279
+ "gridcolor": "#DFE8F3",
1280
+ "gridwidth": 2,
1281
+ "linecolor": "#EBF0F8",
1282
+ "showbackground": true,
1283
+ "ticks": "",
1284
+ "zerolinecolor": "#EBF0F8"
1285
+ },
1286
+ "zaxis": {
1287
+ "backgroundcolor": "white",
1288
+ "gridcolor": "#DFE8F3",
1289
+ "gridwidth": 2,
1290
+ "linecolor": "#EBF0F8",
1291
+ "showbackground": true,
1292
+ "ticks": "",
1293
+ "zerolinecolor": "#EBF0F8"
1294
+ }
1295
+ },
1296
+ "shapedefaults": {
1297
+ "line": {
1298
+ "color": "#2a3f5f"
1299
+ }
1300
+ },
1301
+ "ternary": {
1302
+ "aaxis": {
1303
+ "gridcolor": "#DFE8F3",
1304
+ "linecolor": "#A2B1C6",
1305
+ "ticks": ""
1306
+ },
1307
+ "baxis": {
1308
+ "gridcolor": "#DFE8F3",
1309
+ "linecolor": "#A2B1C6",
1310
+ "ticks": ""
1311
+ },
1312
+ "bgcolor": "white",
1313
+ "caxis": {
1314
+ "gridcolor": "#DFE8F3",
1315
+ "linecolor": "#A2B1C6",
1316
+ "ticks": ""
1317
+ }
1318
+ },
1319
+ "title": {
1320
+ "x": 0.05
1321
+ },
1322
+ "xaxis": {
1323
+ "automargin": true,
1324
+ "gridcolor": "#EBF0F8",
1325
+ "linecolor": "#EBF0F8",
1326
+ "ticks": "",
1327
+ "title": {
1328
+ "standoff": 15
1329
+ },
1330
+ "zerolinecolor": "#EBF0F8",
1331
+ "zerolinewidth": 2
1332
+ },
1333
+ "yaxis": {
1334
+ "automargin": true,
1335
+ "gridcolor": "#EBF0F8",
1336
+ "linecolor": "#EBF0F8",
1337
+ "ticks": "",
1338
+ "title": {
1339
+ "standoff": 15
1340
+ },
1341
+ "zerolinecolor": "#EBF0F8",
1342
+ "zerolinewidth": 2
1343
+ }
1344
+ }
1345
+ },
1346
+ "title": {
1347
+ "text": "<b>Rate Equity by Ownership Model:</b> Residential Premium Over Industrial Rates"
1348
+ },
1349
+ "xaxis": {
1350
+ "anchor": "y",
1351
+ "categoryarray": [
1352
+ "Retail Power Marketer",
1353
+ "Municipal",
1354
+ "Investor Owned",
1355
+ "Cooperative",
1356
+ "State",
1357
+ "Behind the Meter"
1358
+ ],
1359
+ "categoryorder": "array",
1360
+ "domain": [
1361
+ 0,
1362
+ 1
1363
+ ],
1364
+ "title": {
1365
+ "text": "Type"
1366
+ }
1367
+ },
1368
+ "yaxis": {
1369
+ "anchor": "x",
1370
+ "domain": [
1371
+ 0,
1372
+ 1
1373
+ ],
1374
+ "title": {
1375
+ "text": "Residential Premium ($/MWh)"
1376
+ }
1377
+ }
1378
+ }
1379
+ }
1380
+ },
1381
+ "metadata": {},
1382
+ "output_type": "display_data"
1383
+ }
1384
+ ],
1385
+ "source": [
1386
+ "strip_plot = plot_util.get_price_spread_strip_plot(ny_df)\n",
1387
+ "\n",
1388
+ "strip_plot.show()"
1389
+ ]
1390
+ },
1391
+ {
1392
+ "cell_type": "markdown",
1393
+ "id": "99326360",
1394
+ "metadata": {},
1395
+ "source": [
1396
+ "**Interpretation:** Investor-owned utilities dominate rates on the high end, but the spread within each ownership category is wide enough that ownership type alone doesn't fully explain the disparity. There's meaningful variation within ownership types worth investigating."
1397
+ ]
1398
+ },
1399
+ {
1400
+ "cell_type": "markdown",
1401
+ "id": "42f0d824",
1402
+ "metadata": {},
1403
+ "source": [
1404
+ "---\n",
1405
+ "## 4. Correlation Analysis\n",
1406
+ "\n",
1407
+ "Before building directional charts, we compute a Pearson correlation matrix across all key metrics. This serves two purposes: it quantifies the *strength and direction* of every pairwise relationship, and it surfaces any unexpected correlations that warrant further investigation. A positive correlation between `SystemLossPercentage` and `ResidentialUnitPrice` would support the hypothesis. A negative correlation between `LoadFactor` and `ResidentialUnitPrice` (higher efficiency → lower price) would also support it."
1408
+ ]
1409
+ },
1410
+ {
1411
+ "cell_type": "code",
1412
+ "execution_count": 5,
1413
+ "id": "6e0480c9",
1414
+ "metadata": {},
1415
+ "outputs": [],
1416
+ "source": [
1417
+ "# Key metrics: System Loss %, Load Factor', Industrial Revenue %, Price Spread\n",
1418
+ "heatmap = plot_util.get_key_metrics_corr_matrix(ny_df)\n",
1419
+ "\n",
1420
+ "# heatmap.show()"
1421
+ ]
1422
+ },
1423
+ {
1424
+ "cell_type": "markdown",
1425
+ "id": "0003dd2d",
1426
+ "metadata": {},
1427
+ "source": [
1428
+ "**Interpretation:** The heatmap gives us a first look at which efficiency metrics are most predictive of residential pricing. The strong negative correlation between `LoadFactor` and both `ResidentialUnitPrice` and PriceSpread stands out immediately — it suggests infrastructure utilization is a meaningful driver of what residential customers pay. `SystemLossPercentage` shows a weaker positive relationship, which we'll test directly in the next section."
1429
+ ]
1430
+ },
1431
+ {
1432
+ "cell_type": "markdown",
1433
+ "id": "c92e34ee",
1434
+ "metadata": {},
1435
+ "source": [
1436
+ "---\n",
1437
+ "## 5. Fairness Audit — Efficiency vs. Residential Price\n",
1438
+ "\n",
1439
+ "This is the central test of the research question, presented as two side-by-side scatter plots sharing a y-axis (residential price). Each plot approaches inefficiency from a different angle:\n",
1440
+ "\n",
1441
+ "| Plot | X-axis | What it tests |\n",
1442
+ "|---|---|---|\n",
1443
+ "| Left | `SystemLossPercentage` | Does wasted energy in the grid cost residential customers more? |\n",
1444
+ "| Right | `LoadFactor` | Does underutilized infrastructure translate to higher per-MWh costs? |\n",
1445
+ "\n",
1446
+ "> **Filtering note:** This chart excludes utilities with zero system loss or zero load factor. These edge cases typically represent pass-through entities (pure resellers) or data reporting anomalies — including them would distort the OLS fit."
1447
+ ]
1448
+ },
1449
+ {
1450
+ "cell_type": "code",
1451
+ "execution_count": 6,
1452
+ "id": "de2b920d",
1453
+ "metadata": {},
1454
+ "outputs": [],
1455
+ "source": [
1456
+ "# Keep residential utilities that are using energy (instead of ONLY reseale, etc.)\n",
1457
+ "residential_lf_sys_loss_df = data_util.get_residential_sys_loss(ny_df)\n",
1458
+ "residential_lf_sys_loss_df = data_util.get_residential_load_factor(\n",
1459
+ " residential_lf_sys_loss_df).round(2)\n",
1460
+ "\n",
1461
+ "dual_y_scatter = plot_util.get_fairness_dual_y_scatter_plot(\n",
1462
+ " residential_lf_sys_loss_df)\n",
1463
+ "\n",
1464
+ "# dual_y_scatter.show()"
1465
+ ]
1466
+ },
1467
+ {
1468
+ "cell_type": "markdown",
1469
+ "id": "ccdd7bc3",
1470
+ "metadata": {},
1471
+ "source": [
1472
+ "**Interpretation:** The results are asymmetric: load factor explains 41.7% of residential price variance (R² = 0.417, p < 0.001), a statistically strong relationship. System loss explains 10.8% (R² = 0.108, p = 0.016) — modest but significant. The core hypothesis holds, but the mechanism differs: it is primarily underutilized infrastructure, not transmission waste, that correlates with higher residential rates in New York's utility landscape."
1473
+ ]
1474
+ },
1475
+ {
1476
+ "cell_type": "markdown",
1477
+ "id": "c69057f2",
1478
+ "metadata": {},
1479
+ "source": [
1480
+ "---\n",
1481
+ "## 6. Rate Disparity — Residential vs. Industrial\n",
1482
+ "\n",
1483
+ "Even if inefficiency drives prices up overall, the burden may not fall equally. This chart examines the **top 10 utilities by Price Spread** — the gap between what residential and industrial customers pay per MWh. Industrial customers typically negotiate volume discounts — some spread is expected. But when residential customers pay 2–3× the industrial rate at the same utility, it raises questions about whether the rate structure reflects true cost-of-service differences or something else. This chart identifies the specific utilities where that premium is most extreme."
1484
+ ]
1485
+ },
1486
+ {
1487
+ "cell_type": "code",
1488
+ "execution_count": 7,
1489
+ "id": "38d3d584",
1490
+ "metadata": {},
1491
+ "outputs": [],
1492
+ "source": [
1493
+ "# Keep utilities that offer both industrial and residential services\n",
1494
+ "res_ind_customers_df = data_util.get_customer_utilities(ny_df).round(2)\n",
1495
+ "\n",
1496
+ "dumbbell = plot_util.get_rate_disparity_dumbbell_plot(res_ind_customers_df)\n",
1497
+ "\n",
1498
+ "# dumbbell.show()"
1499
+ ]
1500
+ },
1501
+ {
1502
+ "cell_type": "markdown",
1503
+ "id": "86fe4d1f",
1504
+ "metadata": {},
1505
+ "source": [
1506
+ "**Interpretation:** Con Edison (Consolidated Edison) shows the largest residential–industrial spread in the dataset, charging residential customers roughly $233/MWh against a near-zero industrial rate. Eight of the top ten utilities by spread are investor-owned — consistent with the box plot finding that IOUs exhibit the widest pricing distributions. Notably, Con Edison also carries a high system loss percentage, placing it in the upper-right quadrant of the fairness audit scatter: both inefficient and inequitable in its rate structure."
1507
+ ]
1508
+ },
1509
+ {
1510
+ "cell_type": "markdown",
1511
+ "id": "8e8ae178",
1512
+ "metadata": {},
1513
+ "source": [
1514
+ "---\n",
1515
+ "## 7. Energy Flow Analysis\n",
1516
+ "\n",
1517
+ "The Sankey diagrams provide operational context for the efficiency metrics computed above. Rather than a single number, they show the *full picture* of where a utility's energy comes from and where it goes. A wide `Losses` band at a utility with high residential prices is the inefficiency–cost story in one image. We can compare the utility-level Sankey against the U.S. national average to understand whether NY's profile is typical or anomalous."
1518
+ ]
1519
+ },
1520
+ {
1521
+ "cell_type": "code",
1522
+ "execution_count": 8,
1523
+ "id": "0ae862c6",
1524
+ "metadata": {},
1525
+ "outputs": [],
1526
+ "source": [
1527
+ "# Look at a specific utility's energy usage/flow\n",
1528
+ "utility_usage = data_util.get_utility_usage(ny_df.sum(), level=\"State\")\n",
1529
+ "ny_sankey = plot_util.get_energy_use_sankey_plot(utility_usage)\n",
1530
+ "\n",
1531
+ "# ny_sankey.show()\n",
1532
+ "\n",
1533
+ "# Look at the entire country's energy usage/flow\n",
1534
+ "us_energy_flow = df.groupby([\"Utility.State\"]).sum().sum()[1:]\n",
1535
+ "us_energy_flow = data_util.get_utility_usage(us_energy_flow, level=\"US\")\n",
1536
+ "\n",
1537
+ "us_sankey = plot_util.get_energy_use_sankey_plot(us_energy_flow)\n",
1538
+ "\n",
1539
+ "# us_sankey.show()"
1540
+ ]
1541
+ },
1542
+ {
1543
+ "cell_type": "markdown",
1544
+ "id": "09b80967",
1545
+ "metadata": {},
1546
+ "source": [
1547
+ "**Interpretation:** The NY state aggregate Sankey shows a Losses band of approximately 3.2% — compare this against the U.S. national aggregate (~5-6% depending on aggregation method) to establish whether New York's grid is more or less efficient than the national benchmark. For individual utilities, the Losses band translates the abstract SystemLossPercentage into an immediate visual — and directly contextualizes the weakly significant but real correlation observed in the fairness audit."
1548
+ ]
1549
+ },
1550
+ {
1551
+ "cell_type": "markdown",
1552
+ "id": "570d133c",
1553
+ "metadata": {},
1554
+ "source": [
1555
+ "### 7b. Interactive Explorer — Individual Utility Flow"
1556
+ ]
1557
+ },
1558
+ {
1559
+ "cell_type": "code",
1560
+ "execution_count": 9,
1561
+ "id": "7b0d840c",
1562
+ "metadata": {},
1563
+ "outputs": [],
1564
+ "source": [
1565
+ "# Sankey chart with a drop down energy flow for each individual utility\n",
1566
+ "utility_sankey_with_dropdown = plot_util.add_utility_dropdown(ny_sankey, df=ny_df)\n",
1567
+ "\n",
1568
+ "# utility_sankey_with_dropdown.show()"
1569
+ ]
1570
+ },
1571
+ {
1572
+ "cell_type": "markdown",
1573
+ "id": "25a0456f",
1574
+ "metadata": {},
1575
+ "source": [
1576
+ "---\n",
1577
+ "## 8. Export\n",
1578
+ "\n",
1579
+ "Uncomment the following cell to export all charts as SVGs to the `/images` directory.\n",
1580
+ "\n",
1581
+ "> **Requires:** `kaleido` module"
1582
+ ]
1583
+ },
1584
+ {
1585
+ "cell_type": "code",
1586
+ "execution_count": 10,
1587
+ "id": "cb116404",
1588
+ "metadata": {},
1589
+ "outputs": [],
1590
+ "source": [
1591
+ "# %pip install kaleido\n",
1592
+ "\n",
1593
+ "# # Gather all plots and export them as SVGs\n",
1594
+ "# plots = [top_variance_table, strip_plot, heatmap,\n",
1595
+ "# dual_y_scatter, dumbbell, ny_sankey, us_sankey]\n",
1596
+ "\n",
1597
+ "# plot_util.export_plots_as_svg(plots)"
1598
+ ]
1599
+ }
1600
+ ],
1601
+ "metadata": {
1602
+ "kernelspec": {
1603
+ "display_name": "Python 3",
1604
+ "language": "python",
1605
+ "name": "python3"
1606
+ },
1607
+ "language_info": {
1608
+ "codemirror_mode": {
1609
+ "name": "ipython",
1610
+ "version": 3
1611
+ },
1612
+ "file_extension": ".py",
1613
+ "mimetype": "text/x-python",
1614
+ "name": "python",
1615
+ "nbconvert_exporter": "python",
1616
+ "pygments_lexer": "ipython3",
1617
+ "version": "3.13.5"
1618
+ }
1619
+ },
1620
+ "nbformat": 4,
1621
+ "nbformat_minor": 5
1622
+ }