-
Notifications
You must be signed in to change notification settings - Fork 68
Expand file tree
/
Copy pathhist.py
More file actions
172 lines (148 loc) · 5.83 KB
/
Copy pathhist.py
File metadata and controls
172 lines (148 loc) · 5.83 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
# Copyright 2023 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import itertools
from typing import Literal
import numpy as np
import pandas as pd
import bigframes.constants as constants
import bigframes.operations._matplotlib.core as bfplt
class HistPlot(bfplt.MPLPlot):
@property
def _kind(self) -> Literal["hist"]:
return "hist"
def __init__(
self,
data,
bins: int = 10,
**kwargs,
) -> None:
self.bins = bins
self.label = kwargs.get("label", None)
self.by = kwargs.pop("by", None)
self.kwargs = kwargs
if self.by is not None:
raise NotImplementedError(
f"Non-none `by` argument is not yet supported. {constants.FEEDBACK_LINK}"
)
if not isinstance(self.bins, int):
raise NotImplementedError(
f"Only integer values are supported for the `bins` argument. {constants.FEEDBACK_LINK}"
)
if kwargs.get("weight", None) is not None:
raise NotImplementedError(
f"Non-none `weight` argument is not yet supported. {constants.FEEDBACK_LINK}"
)
self.data = self._compute_plot_data(data)
def generate(self) -> None:
"""
Calculates weighted histograms through BigQuery and plots them through pandas
native histogram plot.
"""
hist_bars = self._calculate_hist_bars(self.data, self.bins)
bin_edges = self._calculate_bin_edges(
hist_bars, self.bins, self.kwargs.get("range", None)
)
weights = {
col_name: hist_bar.values for col_name, hist_bar in hist_bars.items()
}
hist_x = {
col_name: pd.Series(
(
hist_bar.index.get_level_values("left_exclusive")
+ hist_bar.index.get_level_values("right_inclusive")
)
/ 2.0
)
for col_name, hist_bar in hist_bars.items()
}
# Align DataFrames for plotting despite potential differences in column
# lengths, filling shorter columns with zeros.
hist_x_pd = pd.DataFrame(
list(itertools.zip_longest(*hist_x.values())), columns=list(hist_x.keys())
).sort_index(axis=1)[self.data.columns.values]
weights_pd = pd.DataFrame(
list(itertools.zip_longest(*weights.values())), columns=list(weights.keys())
).sort_index(axis=1)[self.data.columns.values]
# Prevents pandas from dropping NA values and causing length mismatches by
# filling them with zeros.
hist_x_pd.fillna(0, inplace=True)
weights_pd.fillna(0, inplace=True)
self.axes = hist_x_pd.plot.hist(
bins=bin_edges,
weights=np.array(weights_pd.values),
**self.kwargs,
) # type: ignore
def _compute_plot_data(self, data):
"""
Prepares data for plotting, focusing on numeric data types.
Raises:
TypeError: If the input data contains no numeric columns.
"""
# Importing at the top of the file causes a circular import.
import bigframes.series as series
if isinstance(data, series.Series):
label = self.label
if label is None and data.name is None:
label = ""
if label is None:
data = data.to_frame()
else:
data = data.to_frame(name=label)
# TODO(chelsealin): Support timestamp/date types here.
include_type = ["number"]
numeric_data = data.select_dtypes(include=include_type)
try:
is_empty = numeric_data.columns.empty
except AttributeError:
is_empty = not len(numeric_data)
if is_empty:
raise TypeError("no numeric data to plot")
return numeric_data
@staticmethod
def _calculate_hist_bars(data, bins):
"""
Calculates histogram bars for each column in a BigFrames DataFrame, and
returns a dictionary where keys are column names and values are pandas
Series. The series values are the histogram bins' heights with a
multi-index defining 'left_exclusive' and 'right_inclusive' bin edges.
"""
import bigframes.pandas as bpd
# TODO: Optimize this by batching multiple jobs into one.
hist_bar = {}
for _, col in enumerate(data.columns):
cutted_data = bpd.cut(data[col], bins=bins, labels=None)
hist_bar[col] = (
cutted_data.struct.explode()
.value_counts()
.to_pandas()
.sort_index(level="left_exclusive")
)
return hist_bar
@staticmethod
def _calculate_bin_edges(hist_bars, bins, range):
"""
Calculate bin edges from the histogram bars.
"""
bin_edges = None
for _, hist_bar in hist_bars.items():
left = hist_bar.index.get_level_values("left_exclusive")
right = hist_bar.index.get_level_values("right_inclusive")
if bin_edges is None:
bin_edges = left.union(right)
else:
bin_edges = left.union(right).union(bin_edges)
if bin_edges is None:
return None
_, bins = np.histogram(bin_edges, bins=bins, range=range)
return bins