Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions doc/ref/api/manual/hvplot.hvPlot.heatmap.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,28 @@
"df.hvplot.heatmap(x=\"cat1\", y=\"cat2\", C=\"values\", reduce_function=np.mean)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Counting occurrences\n",
"\n",
"Leaving `C` unset counts the number of rows falling in each `(x, y)` cell, so no value field or `reduce_function` is needed."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import hvplot.pandas # noqa\n",
"\n",
"df = hvplot.sampledata.earthquakes('pandas')\n",
"\n",
"df.hvplot.heatmap(x='mag_class', y='depth_class')"
]
},
{
"cell_type": "markdown",
"metadata": {},
Expand Down
35 changes: 32 additions & 3 deletions hvplot/converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -2988,20 +2988,49 @@ def heatmap(self, x=None, y=None, data=None):
data = self.data if data is None else data
cur_opts, compat_opts = self._get_compat_opts('HeatMap')

counted = False
if not (x or y) or (x == 'columns' and y in ('index', data.index.name)):
cur_opts['labelled'] = []
x, y = 'columns', 'index'
data = (data.columns, data.index, data.values)
z = ['value']
else:
z = self.kwds.get('C', next(c for c in data.columns if c not in (x, y)))
z = [z, *self.hover_cols]
C = self.kwds.get('C')
self.use_index = False
# Derived dimensions such as 'time.hour' only become real columns here,
# so any aggregation has to happen afterwards.
data, x, y = self._process_chart_args(data, x, y, single_y=True)
if C is None:
ignored = [
name
for name, given in (
('hover_cols', self.hover_cols),
('reduce_function', 'reduce_function' in self.kwds),
)
if given
]
if ignored:
warnings.warn(
f'{" and ".join(ignored)} ignored because C is not set, as each '
'cell holds a row count rather than the values behind it. '
'Set C to use them.',
stacklevel=_find_stack_level(),
)
# observed=True keeps categorical x/y from expanding to the full
# product of categories, which is mostly empty cells and can dwarf
# the data. Passing it explicitly also avoids relying on the pandas
# default, which is due to change.
data = data.groupby([x, y], observed=True).size().to_frame('Count').reset_index()
z = ['Count']
counted = True
else:
z = [C, *self.hover_cols]

redim = self._merge_redim({z[0]: self._dim_ranges['c']})
hmap = HeatMap(data, [x, y], z, **self._relabel)
if 'reduce_function' in self.kwds:
# Counting already reduced each cell to one row, so reducing again would
# just measure that single row (np.size would flatten every count to 1).
if 'reduce_function' in self.kwds and not counted:
hmap = hmap.aggregate(function=self.kwds['reduce_function'])
return redim_(hmap, **redim).apply(
self._set_backends_opts, cur_opts=cur_opts, compat_opts=compat_opts
Expand Down
7 changes: 5 additions & 2 deletions hvplot/plotting/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -637,14 +637,17 @@ def heatmap(self, x=None, y=None, C=None, colorbar=True, logz=False, **kwds):
which can be explicitly declared by setting y to ``'index'`` or
to the index name. Can refer to continuous and categorical data.
C : str, optional
Field to draw heatmap color from. If not specified a simple count will be used.
Field to draw heatmap color from. If not specified, the number of rows
falling in each ``(x, y)`` cell is counted and shown as ``'Count'``.
colorbar : bool, default True
Whether to display a colorbar.
logz : bool, default False
Whether to apply log scaling to the z-axis.
reduce_function : function, optional
Function to compute statistics for heatmap, for example ``np.mean``.
If omitted, no aggregation is applied and duplicate values are dropped.
If omitted, no aggregation is applied to ``C`` and duplicate values are
dropped. Ignored when ``C`` is not set, since each cell then holds a row
count rather than the values behind it.
**kwds : optional
Additional keyword arguments are documented in :ref:`plot-options`.
Run ``hvplot.help('heatmap')`` for the full method documentation.
Expand Down
45 changes: 45 additions & 0 deletions hvplot/tests/testcharts.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ def setUp(self):
'temp': np.sin(np.linspace(0, 5 * 2 * np.pi, 5 * 24)).cumsum(),
}
)
# Repeated (x, y) pairs with deliberately uneven counts per cell.
self.dup_df = pd.DataFrame(
{
'x': ['a', 'a', 'a', 'b', 'b', 'c'],
'y': ['p', 'p', 'q', 'q', 'q', 'p'],
'value': [1, 2, 3, 4, 5, 6],
}
)

@parameterized.expand([('points', Points), ('paths', Path)])
def test_2d_defaults(self, kind, element):
Expand Down Expand Up @@ -74,6 +82,42 @@ def test_heatmap_2d_derived_x_and_y(self):
assert plot.kdims == ['time.hour', 'time.day']
assert plot.vdims == ['temp']

def test_heatmap_counts_when_C_not_set(self):
plot = self.dup_df.hvplot.heatmap(x='x', y='y')
assert plot.vdims == ['Count']
counts = {(r['x'], r['y']): r['Count'] for r in plot.dframe().to_dict('records')}
assert counts == {('a', 'p'): 2, ('a', 'q'): 1, ('b', 'q'): 2, ('c', 'p'): 1}

def test_heatmap_counts_match_between_call_styles(self):
self.assertEqual(
self.dup_df.hvplot.heatmap(x='x', y='y'),
self.dup_df.hvplot(x='x', y='y', kind='heatmap'),
)

def test_heatmap_counts_categorical_x_and_y(self):
categorical_df = self.dup_df.astype({'x': 'category', 'y': 'category'})
plot = categorical_df.hvplot.heatmap(x='x', y='y')
counts = {(r['x'], r['y']): r['Count'] for r in plot.dframe().to_dict('records')}
# Only the pairs present in the data, not the full product of categories,
# which would be mostly empty cells for high cardinality columns.
assert counts == {('a', 'p'): 2, ('a', 'q'): 1, ('b', 'q'): 2, ('c', 'p'): 1}

def test_heatmap_warns_options_ignored_when_C_not_set(self):
with pytest.warns(UserWarning, match='hover_cols'):
self.dup_df.hvplot.heatmap(x='x', y='y', hover_cols=['value'])
with pytest.warns(UserWarning, match='reduce_function'):
self.dup_df.hvplot.heatmap(x='x', y='y', reduce_function=np.size)

def test_heatmap_counts_ignore_reduce_function(self):
# np.size would otherwise flatten every count to 1.
plot = self.dup_df.hvplot.heatmap(x='x', y='y', reduce_function=np.size)
counts = {(r['x'], r['y']): r['Count'] for r in plot.dframe().to_dict('records')}
assert counts == {('a', 'p'): 2, ('a', 'q'): 1, ('b', 'q'): 2, ('c', 'p'): 1}

def test_heatmap_C_field_still_takes_precedence(self):
plot = self.dup_df.hvplot.heatmap(x='x', y='y', C='value')
assert plot.vdims == ['value']

def test_xarray_dataset_with_attrs(self):
try:
import xarray as xr
Expand Down Expand Up @@ -102,6 +146,7 @@ def setUp(self):

self.df = dd.from_pandas(self.df, npartitions=2)
self.cat_df = dd.from_pandas(self.cat_df, npartitions=3)
self.dup_df = dd.from_pandas(self.dup_df, npartitions=2)

@expectedFailure
def test_heatmap_2d_index_columns(self):
Expand Down