Source code for maad.util.visualization

#!/usr/bin/env python
""" 
Ensemble of functions that facilitate data visualization.
"""

# Import external modules
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import LinearSegmentedColormap
import matplotlib.patches as mpatches
from matplotlib.axes import Axes
import pandas as pd
from skimage.io import imsave
import colorsys
from ast import literal_eval
from datetime import datetime

# min value
import sys

_MIN_ = sys.float_info.min

# Importation from internal modules
from maad.util import linear_scale, power2dB

#%%
def _check_axes(axes):
    """Check if "axes" is an instance of an axis object. If not, use `gca`."""
    if axes is None:
        import matplotlib.pyplot as plt

        axes = plt.gca()
    elif not isinstance(axes, Axes):
        raise ValueError(
            "`axes` must be an instance of matplotlib.axes.Axes. "
            "Found type(axes)={}".format(type(axes))
        )
    return axes


#%%
def _is_single_value(value):
    """
    Check if a value is a single numeric value or a single character.

    Parameters:
    -----------
        value (int, float, str): The value to be checked.

    Returns:
    --------
        bool: True if the value is a single numeric value or a single character, False otherwise.
    """
    if isinstance(value, (int, float, str)):
        if isinstance(value, (int, float)):
            return True  # It's a numeric value
        elif isinstance(value, str):
            # Check if it's a single numeric character (0-9)
            if len(value) == 1 and value.isdigit():
                return True  # It's a single numeric character
            else:
                return False  # It's a string, but not a single character
    return False  # It's not a single value

#%%
MIN_V = 1 # min week number possible
MIN_m = 1 # min month number possible
MIN_d = 1 # min day number possible
MIN_H = 0 # min hour number possible
MIN_M = 0 # min minute number possible

MAX_V = 53 # max week number possible
MAX_m = 12 # max month number possible
MAX_d = 31 # max day number possible
MAX_H = 23 # max hour number possible
MAX_M = 59 # max minute number possible

def _filter_dataframe_by_datetime(df, date_format, date_range):
    """
    Filter a DataFrame based on a specified date format and date range.

    Parameters:
    -----------
        df : pandas.DataFrame
            The DataFrame to be filtered.
        date_format :str
            The format of the date to be extracted from the DataFrame's index.
        date_range : int, str, list
            The range of dates to filter the DataFrame by.

    Returns:
    --------
        pandas.DataFrame: The filtered DataFrame.

    Raises:
    -------
        ValueError: If the date_format is not allowed or if the date_range is not a single value or a list.

    Note:
    -----
        The date_format parameter can be one of the following:
        - "%V": Week number
        - "%Y": Year
        - "%m-%d": Month and day
        - "%a": Weekday
        - "%m": Month
        - "%d": Day
        - "%y-%m-%d": Year, Month, and Day
        - "%H:%M": Hour and minute (e.g., "13:42")

        The date_range parameter can be one of the following:
        - A single value (int or str) representing a specific date or time.
        - A list of values (int or str) representing a range of dates or times.
        For example, ["01-01", "01-31"] represents a range of dates from January 1st to January 31st.

    """
    # Extract the date portion from the DatetimeIndex based on the specified date format
    try:
        df['date_type'] = df.index.strftime(date_format)
    except :
        raise ValueError('Error in the date_format. Check the date_format parameter')

    if _is_single_value(date_range) == True :
        if (date_format == "%V") or (date_format == "%Y") or (date_format == "%m") or (date_format == "%d"):
            # Filter by week number or by year or by month
            df_filtered = df[df['date_type'].astype(int) == date_range]
        elif (date_format == "%m-%d") or (date_format == "%H:%M") or (date_format == "%a") or (date_format == "%y-%m-%d"):
            # Filter by time (hour:minutes)
            # Filter by weekday (e.g., "Mon" for Monday)
            # Filter by year-month-day (e.g., "23-01-01" for January 1, 2023)
            # Filter by month and day (e.g., "01-01" for January 1)
            df_filtered = df[df['date_type'] == date_range]
        else :
            print("Filtering was no possible. date_format {} is not allowed".format(date_format))
            df_filtered = df

    elif (len(date_range)==1) or(len(date_range)>2):
        if (date_format == "%V") or (date_format == "%Y") or (date_format == "%m") or (date_format == "%d"):
            # Filter by week number or by year or by month
            df_filtered = df[df['date_type'].astype(int).isin(date_range)]
        elif (date_format == "%m-%d") or (date_format == "%H:%M") or (date_format == "%a") or (date_format == "%y-%m-%d"):
            # Filter by time (hour:minutes)
            # Filter by weekday (e.g., "Mon" for Monday)
            # Filter by year-month-day (e.g., "23-01-01" for January 1, 2023)
            # Filter by month and day (e.g., "01-01" for January 1)
            df_filtered = df[df['date_type'].isin(date_range)]
        else :
            print("Filtering was no possible. date_format {} is not allowed".format(date_format))
            df_filtered = df
    else:
        if isinstance(date_range, list) : 
            if (date_format == "%V") or (date_format == "%Y") or (date_format == "%m") or (date_format == "%d"):
                # Filter by week number or by year or by month
                df_filtered = df[df['date_type'].astype(int).between(date_range[0], date_range[1])]
            elif (date_format == "%m-%d") or (date_format == "%H:%M") or (date_format == "%a") or (date_format == "%y-%m-%d"):
                # Filter by time (hour:minutes)
                # Filter by weekday (e.g., "Mon" for Monday)
                # Filter by year-month-day (e.g., "23-01-01" for January 1, 2023)
                # Filter by month and day (e.g., "01-01" for January 1)
                if date_range[0] <= date_range[1] :
                    df_filtered = df[df['date_type'].between(str(date_range[0]), str(date_range[1]))]
                else :
                    if date_format == "%H:%M" : 
                        MAX = f'{MAX_H:02}:{MAX_M:02}'
                        df_filtered_part1 = df[df['date_type'].between(str(date_range[0]), MAX)] 
                        MIN = f'{MIN_H:02}:{MIN_M:02}'
                        df_filtered_part2 = df[df['date_type'].between(MIN, str(date_range[1]))]  
                        df_filtered = pd.concat([df_filtered_part1, df_filtered_part2], axis = 0)
                    else :
                        print("Filtering was no possible. The range must increase")
                        df_filtered = df
            else :
                print("Filtering was no possible. date_format {} is not allowed".format(date_format))
                df_filtered = df
        else :
            print("Filtering was no possible. date_range must be a single value or a list")
            df_filtered = df

    # Check if 'data_type' is in the DataFrame's columns
    if 'date_type' in df_filtered.columns:
        # If it exists, drop the column
        df_filtered = df_filtered.drop('date_type', axis=1)

    return df_filtered

#%%
[docs] def plot_shape(shape, params, row=0, ax=None, display_values=False): """ Plot shape features in a bidimensional plot. Parameters ---------- shape: 1D array, pd.Series or pd.DataFrame Shape features computed with shape_features function. params: pd.DataFrame Pandas dataframe returned by maad.features_rois.shape_features row: int Observation to be visualized display_values: bool Set to True to display the coefficient values. Default is False. Returns ------- ax: matplotlib.axes Axes of the figure Examples -------- >>> from maad.sound import load, spectrogram >>> from maad.features import shape_features, plot_shape >>> import numpy as np >>> s, fs = load('../data/spinetail.wav') >>> Sxx, ts, f, ext = spectrogram(s, fs) >>> shape, params = shape_features(np.log10(Sxx), resolution='high') >>> plot_shape(shape, params) """ # compute shape of matrix dirs_size = params.theta.unique().shape[0] scale_size = np.unique(params.freq).size * np.unique(params.pyr_level).size # reshape feature vector idx = params.sort_values(["theta", "pyr_level", "scale"]).index if isinstance(shape, pd.DataFrame): shape_plt = shape.iloc[:, shape.columns.str.startswith("shp")] shape_plt = np.reshape(shape_plt.iloc[row, idx].values, (dirs_size, scale_size)) elif isinstance(shape, pd.Series): shape_plt = shape.iloc[shape.index.str.startswith("shp")] shape_plt = np.reshape(shape_plt.iloc[idx].values, (dirs_size, scale_size)) elif isinstance(shape, np.ndarray): shape_plt = np.reshape(shape_plt[idx], (dirs_size, scale_size)) unique_scale = params.scale[idx] * 2 ** params.pyr_level[idx] # get textlab textlab = shape_plt textlab = np.round(textlab, 2) # plot figure if ax is None: fig = plt.figure(figsize=(8, 6)) ax = fig.add_subplot(111) else: pass ax.imshow( shape_plt, aspect="auto", origin="lower", interpolation="None", cmap="viridis" ) if display_values: for (j, i), label in np.ndenumerate(textlab): ax.text(i, j, label, ha="center", va="center") yticklab = np.round(params.theta.unique(), 1) xticklab = np.reshape(unique_scale.values, (dirs_size, scale_size)) ax.set_xticks(np.arange(scale_size)) ax.set_xticklabels(np.round(xticklab, 1)[0, :]) ax.set_yticks(np.arange(dirs_size)) ax.set_yticklabels(yticklab) ax.set_xlabel("Scale") ax.set_ylabel("Theta") plt.show() return ax
#%%
[docs] def overlay_centroid(im_ref, centroid, savefig=None, **kwargs): """ Overlay centroids on the original spectrogram Parameters ---------- Sxx : 2D array Spectrogram centroid: pandas DataFrame DataFrame with centroid descriptors (centroid_f, centroid_t) Do format_features(rois,tn,fn) before using overlay_centroid to be sure that the format of the DataFrame is correct savefig : string, optional, default is None Root filename (with full path) is required to save the figures. Postfix is added to the root filename. kwargs, optional. This parameter is used by plt.plot and savefig functions - savefilename : str, optional, default :'_spectro_overlaycentroid.png' Postfix of the figure filename - extent : list of scalars [left, right, bottom, top], optional, default: None The location, in data-coordinates, of the lower-left and upper-right corners. If `None`, the image is positioned such that the pixel centers fall on zero-based (row, column) indices. - title : string, optional, default : 'Spectrogram' title of the figure - xlabel : string, optional, default : 'Time [s]' label of the horizontal axis - ylabel : string, optional, default : 'Amplitude [AU]' label of the vertical axis - cmap : string or Colormap object, optional, default is 'gray' See https://matplotlib.org/examples/color/colormaps_reference.html in order to get all the existing colormaps examples: 'hsv', 'hot', 'bone', 'tab20c', 'jet', 'seismic', 'viridis'... - vmin, vmax : scalar, optional, default: None `vmin` and `vmax` are used in conjunction with norm to normalize luminance data. Note if you pass a `norm` instance, your settings for `vmin` and `vmax` will be ignored. - dpi : integer, optional, default is 96 Dot per inch. For printed version, choose high dpi (i.e. dpi=300) => slow For screen version, choose low dpi (i.e. dpi=96) => fast - format : string, optional, default is 'png' Format to save the figure ... and more, see matplotlib Returns ------- ax axis object (see matplotlib) fig figure object (see matplotlib) Examples -------- Get centroid from the whole power spectrogram >>> from maad.sound import load, spectrogram >>> from maad.features import centroid_features >>> from maad.util import (power2dB, format_features, overlay_rois, plot2d, overlay_centroid) Load audio and compute spectrogram >>> s, fs = load('../data/spinetail.wav') >>> Sxx,tn,fn,ext = spectrogram(s, fs, db_range=80) >>> Sxx = power2dB(Sxx, db_range=80) Load annotations and plot >>> from maad.util import read_audacity_annot >>> rois = read_audacity_annot('../data/spinetail.txt') >>> rois = format_features(rois, tn, fn) >>> ax, fig = plot2d (Sxx, extent=ext) >>> ax, fig = overlay_rois(Sxx,rois, extent=ext, ax=ax, fig=fig) Compute the centroid of each rois, format to get results in the temporal and spectral domain and overlay the centroids. >>> centroid = centroid_features(Sxx, rois) >>> centroid = format_features(centroid, tn, fn) >>> ax, fig = overlay_centroid(Sxx,centroid, extent=ext, ax=ax, fig=fig) """ # Check format of the input data if type(centroid) is not pd.core.frame.DataFrame: raise TypeError("Rois must be of type pandas DataFrame") if not (("centroid_t" and "centroid_f") in centroid): raise TypeError( "Array must be a Pandas DataFrame with column names:(centroid_t, centroid_f). Check example in documentation." ) ylabel = kwargs.pop("ylabel", "Frequency [Hz]") xlabel = kwargs.pop("xlabel", "Time [s]") title = kwargs.pop("title", "ROIs Overlay") cmap = kwargs.pop("cmap", "gray") extent = kwargs.pop("ext", None) if extent is None: ylabel = "pseudofrequency [points]" xlabel = "pseudotime [points]" vmin = kwargs.pop("vmin", 0) vmax = kwargs.pop("vmax", 1) ax = kwargs.pop("ax", None) fig = kwargs.pop("fig", None) color = kwargs.pop("color", "firebrick") ms = kwargs.pop("ms", 2) marker = kwargs.pop("marker", "o") if (ax is None) and (fig is None): ax, fig = plot2d( im_ref, extent = extent, now = False, title = title, ylabel = ylabel, xlabel = xlabel, vmin = vmin, vmax = vmax, cmap = cmap, **kwargs ) ax.plot(centroid.centroid_t, centroid.centroid_f, marker, ms=ms, color=color) fig.canvas.draw() # SAVE FIGURE if savefig is not None: dpi = kwargs.pop("dpi", 96) format = kwargs.pop("format", "png") filename = kwargs.pop("filename", "_spectro_overlaycentroid") filename = savefig + filename + "." + format fig.savefig(filename, bbox_inches="tight", dpi=dpi, format=format, **kwargs) return ax, fig
#%%
[docs] def overlay_rois(im_ref, rois, edge_color=None, unique_labels= None, textbox_label=False, savefig=None, **kwargs): """ Display bounding boxes with time-frequency regions of interest over a spectrogram. Regions of interest (ROIs) must be provided. They can be loaded as manual annotations or computed using automated methods (see the example section). Parameters ---------- im_ref : 2d ndarray of scalars Spectrogram (or image) rois_bbox : pandas.DataFrame Contains the bounding box of each ROI. The pandas.DataFrame must have columns ['min_x', 'max_x', 'min_y', 'max_y']. If your data is in the time-frequency format ['min_t', 'max_t', 'min_f', 'max_f'], use the function maad.util.format_features to get the cooresponding x, y coordinates. edge_color : {None, string, tuple, list of tuples}, default is None Define the color of the bounding-box to display. if it's a string, select a color name if it's a tuple, give a tuple of floats [0,1] in the form (r,g,b,a), with r for red, g for green, b for blue and a for alpha (i.e transparency) for example - pure red : (1,0,0,0) - pure yellow : (1,1,0,0) - pure blue with transparency(0,0,1,0.5) if it's a list, give a list of tuple or string. The number of element in the list should be equal to the number of elements in unique_labels. if it's set to None, a random list of colors is used. unique_labels : list, default is None list of unique labels that could be either strings or numbers. When a list is given as argument, the number element in the list should be the same as the number of edge_color textbox_label: bool Display a text box above the bounding box indicating the name of the label. The rois_bbox pandas.DataFrame must have a column called 'label' with names assigned to each ROI. savefig : string, optional, default is None Root filename (with full path) is required to save the figures. Postfix is added to the root filename. kwargs, optional. This parameter is used by plt.plot and savefig functions - savefilename : str, optional, default :'_spectro_overlayrois.png' Postfix of the figure filename - extent : list of scalars [left, right, bottom, top], optional, default: None The location, in data-coordinates, of the lower-left and upper-right corners. If `None`, the image is positioned such that the pixel centers fall on zero-based (row, column) indices. - title : string, optional, default : 'Spectrogram' title of the figure - xlabel : string, optional, default : 'Time [s]' label of the horizontal axis - ylabel : string, optional, default : 'Amplitude [AU]' label of the vertical axis - cmap : string or Colormap object, optional, default is 'gray' See https://matplotlib.org/examples/color/colormaps_reference.html in order to get all the existing colormaps examples: 'hsv', 'hot', 'bone', 'tab20c', 'jet', 'seismic', 'viridis'... - vmin, vmax : scalar, optional, default: None `vmin` and `vmax` are used in conjunction with norm to normalize luminance data. Note if you pass a `norm` instance, your settings for `vmin` and `vmax` will be ignored. - dpi : integer, optional, default is 96 Dot per inch. For printed version, choose high dpi (i.e. dpi=300) => slow For screen version, choose low dpi (i.e. dpi=96) => fast - format : string, optional, default is 'png' Format to save the figure ... and more, see matplotlib Returns ------- ax : axis object (see matplotlib) fig : figure object (see matplotlib) Examples -------- Simple display of bounding boxes over the spectrogram. >>> from maad import sound, util >>> s, fs = sound.load('./data/spinetail.wav') >>> rois = util.read_audacity_annot('./data/spinetail.txt') >>> Sxx, tn, fn, ext = sound.spectrogram(s, fs, nperseg=512, noverlap=256) >>> rois = util.format_features(rois, tn, fn) >>> fig, ax = plt.subplots(figsize=(10,5)) >>> util.plot_spectrogram(Sxx,ext, db_range=70, ax=ax) >>> util.overlay_rois(Sxx, rois, fig=fig, ax=ax, textbox_label=True) Detect regions of interest and display them over the spectrogram. Load audio recording and compute the spectrogram. >>> s, fs = maad.sound.load('../data/cold_forest_daylight.wav') >>> Sxx,tn,fn,ext = maad.sound.spectrogram (s, fs, fcrop=(0,10000)) Subtract the background noise before finding ROIs. >>> Sxx_noNoise = maad.sound.median_equalizer(Sxx) Convert linear spectrogram into dB and smooth. >>> Sxx_noNoise_dB = maad.util.power2dB(Sxx_noNoise) >>> Sxx_noNoise_dB_blurred = maad.sound.smooth(Sxx_noNoise_dB) Detection of the acoustic signature => creation of a mask >>> im_bin = maad.rois.create_mask(Sxx_noNoise_dB_blurred, bin_std=6, bin_per=0.5, mode='relative') Select rois from the mask and display bounding box over the spectrogram without noise. >>> import numpy as np >>> im_rois, df_rois = maad.rois.select_rois(im_bin, min_roi=100) >>> maad.util.overlay_rois (Sxx_noNoise_dB, df_rois, extent=ext,vmin=np.median(Sxx_noNoise_dB), vmax=np.median(Sxx_noNoise_dB)+60) """ # Check format of the input data if type(rois) is not pd.core.frame.DataFrame: raise TypeError("Rois must be of type pandas DataFrame.") if not (("min_y" and "min_x" and "max_y" and "max_x") in rois): raise TypeError( "Array must be a Pandas DataFrame with column names: min_y, min_x, max_y, max_x. Check example in documentation." ) ylabel = kwargs.pop("ylabel", "Frequency [Hz]") xlabel = kwargs.pop("xlabel", "Time [s]") title = kwargs.pop("title", "ROIs Overlay") cmap = kwargs.pop("cmap", "gray") vmin = kwargs.pop("vmin", np.percentile(im_ref, 0.05)) vmax = kwargs.pop("vmax", np.percentile(im_ref, 0.95)) ax = kwargs.pop("ax", None) fig = kwargs.pop("fig", None) extent = kwargs.pop("extent", None) if extent is None: xlabel = "pseudoTime [points]" ylabel = "pseudoFrequency [points]" if (ax is None) and (fig is None): ax, fig = plot2d( im_ref, extent = extent, now = False, title = title, ylabel = ylabel, xlabel = xlabel, vmin = vmin, vmax = vmax, cmap = cmap, **kwargs ) # Convert pixels into time and frequency values y_len, x_len = im_ref.shape xmin, xmax = ax.get_xlim() ymin, ymax = ax.get_ylim() x_scaling = (xmax - xmin) / x_len y_scaling = (ymax - ymin) / y_len # if no list of unique labels is provided, try to find a "label" column if unique_labels is None : # test if rois has a label column if "label" in rois: # select the label column rois_label = rois.label.values uniqueLabels = list(np.unique(np.array(rois_label))) else: uniqueLabels = [0] else: if isinstance(unique_labels,list) : uniqueLabels = unique_labels elif isinstance(unique_labels,np.ndarray) : uniqueLabels = list(unique_labels) elif isinstance(unique_labels, str) : uniqueLabels = [unique_labels] else: # test if unique_labels is a number try: int(unique_labels) uniqueLabels = [unique_labels] except: raise TypeError( "unique_labels must be a list or a string or a number" ) # Colormap if edge_color is None : # in order to still have the color yellow by default in case of no label # or a unique label if len(uniqueLabels) == 1: color = tuple(["yellow"]) else: color_func = rand_cmap(len(uniqueLabels) + 1, first_color_black=False) color = color_func(np.arange(len(uniqueLabels))) # if a list of colors is defined, test if the length of the list is # enough compared to the number of unique labels # convert into tuple elif isinstance(edge_color, str) : color = tuple([edge_color]) * len(uniqueLabels) elif isinstance(edge_color, list) : nn = int(len(uniqueLabels)/len(edge_color)) +1 color = tuple(edge_color) * nn cc = 0 for index, row in rois.iterrows(): cc = cc + 1 y0 = row["min_y"] x0 = row["min_x"] y1 = row["max_y"] x1 = row["max_x"] # find the position of the label in uniqueLabels if "label" in row : ii = uniqueLabels.index(row["label"]) else: ii = 0 rect = mpatches.Rectangle( (x0 * x_scaling + xmin, y0 * y_scaling + ymin), (x1 - x0) * x_scaling, (y1 - y0) * y_scaling, fill=False, edgecolor=color[ii], linewidth=1, ) # draw the rectangle ax.add_patch(rect) if textbox_label: textbox = dict(boxstyle='square', fc=color[ii], ec=color[ii], alpha=1, pad=0, linewidth=2) ax.text(x0*x_scaling + xmin, (y1+2)*y_scaling + ymin, str(row.label), color='white', fontweight='semibold', fontsize='small', fontfamily='sans-serif', fontvariant='small-caps', bbox=textbox ) if fig is not None: fig.canvas.draw() # SAVE FIGURE if savefig is not None and fig is not None: dpi = kwargs.pop("dpi", 96) format = kwargs.pop("format", "png") filename = kwargs.pop("filename", "_spectro_overlayrois") filename = savefig + filename + "." + format fig.savefig(filename, bbox_inches="tight", dpi=dpi, format=format, **kwargs) return ax, fig
#%%
[docs] def plot1d(x, y, ax=None, **kwargs): """ Plot the waveform or spectrum of an audio signal. Parameters ---------- x : 1d ndarray of integer Vector containing the abscissa values (horizontal axis) y : 1d ndarray of scalar Vector containing the ordinate values (vertical axis) ax : axis, optional, default is None Draw the signal on this specific axis. Allow multiple plots on the same axis. kwargs, optional - figsize : tuple of integers, optional, default: (4,10) width, height in inches. - facecolor : matplotlib color, optional, default: 'w' (white) the background color. - edgecolor : matplotlib color, optional, default: 'k' (black) the border color. - color : matplotlib color, optional, default: 'k' (black) the line color The following color abbreviations are supported: ========== ======== character color ========== ======== 'b' blue 'g' green 'r' red 'c' cyan 'm' magenta 'y' yellow 'k' black 'w' white ========== ======== In addition, you can specify colors in many ways, including RGB tuples (0.2,1,0.5). See matplotlib color - linewidth : scalar, optional, default: 0.5 width in pixels - figtitle: string, optional, default: 'Audiogram' Title of the plot - xlabel : string, optional, default : 'Time [s]' label of the horizontal axis - ylabel : string, optional, default : 'Amplitude [AU]' label of the vertical axis - legend : string, optional, default : None Legend for the plot - now : boolean, optional, default : True if True, display now. Cannot display multiple plots. To display mutliple plots, set now=False until the last call for the last plot ...and more, see matplotlib Returns ------- fig : Figure The Figure instance ax : Axis The Axis instance Examples -------- >>> import numpy as np >>> s, fs = maad.sound.load('../data/cold_forest_daylight.wav') Plot the audiogram >>> tn = np.arange(0,len(s))/fs >>> fig, ax = maad.util.plot1d(tn,s) >>> Sxx_power,tn,fn,_ = maad.sound.spectrogram(s,fs) Convert spectrogram into dB SPL >>> Lxx = maad.spl.power2dBSPL(Sxx_power, gain=42) Plot the spectrum at t = 7s >>> index = maad.util.nearest_idx(tn,7) >>> fig_kwargs = {'figtitle':'Spectrum (PSD)', 'xlabel':'Frequency [Hz]', 'ylabel':'Power [dB]', 'linewidth': 0.5 } >>> fig, ax = maad.util.plot1d(fn, Lxx[:,index], **fig_kwargs) """ figsize = kwargs.pop("figsize", (4, 10)) facecolor = kwargs.pop("facecolor", "w") edgecolor = kwargs.pop("edgecolor", "k") linewidth = kwargs.pop("linewidth", 0.5) color = kwargs.pop("color", "k") title = kwargs.pop("figtitle", "") xlabel = kwargs.pop("xlabel", "Time [s]") ylabel = kwargs.pop("ylabel", "Amplitude [AU]") legend = kwargs.pop("legend", None) now = kwargs.pop("now", False) # if no ax, create a figure and a subplot associated a figure otherwise # find the figure that belongs to ax if ax is None: fig, ax = plt.subplots() # set the paramters of the figure fig.set_figheight(figsize[0]) fig.set_figwidth(figsize[1]) fig.set_facecolor(facecolor) fig.set_edgecolor(edgecolor) else: fig = ax.get_figure() # plot the data on the subplot line = ax.plot(x, y, color=color, linewidth=linewidth, **kwargs) # set legend to the line line[0].set_label(legend) # set the parameters of the subplot ax.set_title(title) ax.set_xlabel(xlabel) ax.set_ylabel(ylabel) #ax.axis("tight") ax.grid(True, linewidth=0.3) ax.margins(x=0) if legend is not None: ax.legend() # Display the figure now if now: plt.show() return ax, fig
#%%
[docs] def plot_wave(s, fs, tlims=None, ax=None, **kwargs): """ Plot audio waveform. Parameters ---------- s : 1d ndarray Audio signal. fs : int Sampling rate of audio signal. tlims : tuple, optional Minimum and maximum temporal limits for the display (min_t, max_t). The default is None. ax : matplotlib.axes, optional Pre-existing axes for the plot. The default is None. **kwargs : matplotlib figure properties Other keyword arguments that are passed down to matplotlib.axes. Returns ------- ax : matplotlib.axes The matplotlib axes associated to plot. See also -------- maad.sound.plot_spectrum Examples -------- Plot a waveform of an audio signal. >>> from maad import sound >>> from maad.util import plot_wave >>> s, fs = sound.load('../data/spinetail.wav') >>> ax = plot_wave(s, fs) Use `plot_wave` with predifined matplotlib axes. >>> import matplotlib.pyplot as plt >>> s, fs = sound.load('../data/spinetail.wav') >>> fig, ax = plt.subplots(2,1) >>> plot_wave(s, fs, ax=ax[0], xlabel='', figtitle='Spinetail') >>> plot_wave(s, fs, tlims=(5,8), ax=ax[1]) """ if tlims is not None: s = s[int(tlims[0] * fs) : int(tlims[1] * fs)] t = np.linspace(0, s.shape[0] / fs, s.shape[0]) t = t + tlims[0] # add minimum value else: t = np.linspace(0, s.shape[0] / fs, s.shape[0]) ax, fig = plot1d(t, s, ax, **kwargs) return ax
#%%
[docs] def plot_spectrum(pxx, f_idx, ax=None, flims=None, log_scale=False, fill=True, **kwargs): """ Plot power spectral density estimate (PSD). Parameters ---------- pxx : 1d ndarray Power spectral density estimate computed with `maad.sound.spectrum`. f_idx : 1d ndarray Index of frequencies associated with the PSD. ax : matplotlib.axes, optional Pre-existing axes for the plot. The default is None. flims : tuple, optional Minimum and maximum spectral limits for the display (min_f, max_f). Default is None. log_scale : bool, optional Use a logarithmic scale to display amplitude values. The default is False. fill : bool, optional Fill the area between the curve and the minimum value. **kwargs : matplotlib figure properties Other keyword arguments that are passed down to matplotlib.axes. Returns ------- ax : matplotlib.axes The matplotlib axes associated to plot. See also -------- maad.sound.spectrum, maad.util.plot_wave Examples -------- Plot a spectrum of an audio signal. >>> from maad import sound, util >>> s, fs = sound.load('../data/spinetail.wav') >>> pxx, f_idx = sound.spectrum(s, fs, nperseg=1024) >>> util.plot_spectrum(pxx, f_idx) Use `plot_spectrum` with predifined matplotlib axes. >>> import matplotlib.pyplot as plt >>> s, fs = sound.load('../data/spinetail.wav') >>> s_slice = sound.trim(s, fs, 5, 8) >>> pxx, f_idx = sound.spectrum(s_slice, fs, nperseg=1024) >>> fig, ax = plt.subplots(2,1, figsize=(10,6)) >>> util.plot_wave(s_slice, fs, ax=ax[0]) >>> util.plot_spectrum(pxx, f_idx, ax=ax[1], log_scale=True) """ ylabel = kwargs.pop("ylabel", "Amplitude [AU]") xlabel = kwargs.pop("xlabel", "Frequency [Hz]") if log_scale == True: pxx = power2dB(pxx) ylabel = 'Amplitude [dB]' if flims is not None: pxx = pxx[(f_idx >= flims[0]) & (f_idx <= flims[1])] f_idx = f_idx[(f_idx >= flims[0]) & (f_idx <= flims[1])] amp_min = pxx.min() ax, fig = plot1d(f_idx, pxx, ax=ax, ylabel=ylabel, xlabel=xlabel, **kwargs) if fill: ax.fill_between(f_idx, amp_min, pxx, alpha=0.3, fc="grey") return ax
#%%
[docs] def plot2d(im, ax=None, colorbar=True, **kwargs): """ Display the spectrogram of an audio signal. The spectrogram should be previously computed using the function ``maad.sound.spectrogram``. Parameters ---------- im : 2D numpy array Image or Spectrogram ax : axis, optional, default is None Draw the image on this specific axis. Allow multiple plots on the same figure. kwargs, optional - figsize : tuple of integers, optional, default: (4,13) width, height in inches. - title : string, optional, default : 'Spectrogram' title of the figure - xlabel : string, optional, default : 'Time [s]' label of the horizontal axis - ylabel : string, optional, default : 'Frequency [Hz]' label of the vertical axis - xticks : tuple of ndarrays, optional, default : none * ticks : array_like => A list of positions at which ticks should be placed. You can pass an empty list to disable yticks. * labels : array_like, optional => A list of explicit labels to place at the given locs. - yticks : tuple of ndarrays, optional, default : none * ticks : array_like => A list of positions at which ticks should be placed. You can pass an empty list to disable yticks. * labels : array_like, optional => A list of explicit labels to place at the given locs. - cmap : string or Colormap object, optional, default is 'gray' See https://matplotlib.org/examples/color/colormaps_reference.html in order to get all the existing colormaps examples: 'hsv', 'hot', 'bone', 'tab20c', 'jet', 'seismic', 'viridis'... - vmin, vmax : scalar, optional, default: None `vmin` and `vmax` are used in conjunction with norm to normalize luminance data. Note if you pass a `norm` instance, your settings for `vmin` and `vmax` will be ignored. - extent : list of scalars [left, right, bottom, top], optional, default: None The location, in data-coordinates, of the lower-left and upper-right corners. If `None`, the image is positioned such that the pixel centers fall on zero-based (row, column) indices. - now : boolean, optional, default : True if True, display now. Cannot display multiple images. To display mutliple images, set now=False until the last call for the last image - interpolation : list of string [None, 'none', 'nearest', 'bilinear', 'bicubic', 'spline16','spline36', 'hanning', 'hamming', 'hermite', 'kaiser', 'quadric','catrom', 'gaussian', 'bessel', 'mitchell', 'sinc', 'lanczos'], optional, default : None ... and more, see matplotlib Returns ------- fig : Figure The Figure instance ax : Axis The Axis instance Examples -------- >>> w, fs = maad.sound.load('../data/cold_forest_daylight.wav') >>> Sxx_power,tn,fn,_ = maad.sound.spectrogram(w,fs) >>> Lxx = maad.spl.power2dBSPL(Sxx_power, gain=42) # convert into dB SPL >>> fig_kwargs = {'vmax': Lxx.max(), 'vmin':0, 'extent':(tn[0], tn[-1], fn[0], fn[-1]), 'title':'Power spectrogram density (PSD) in dB SPL', 'xlabel':'Time [s]', 'ylabel':'Frequency [Hz]', } >>> fig, ax = maad.util.plot2d(Lxx,interpolation=None,**fig_kwargs) """ # matplotlib parameters title = kwargs.pop("title", "") ylabel = kwargs.pop("ylabel", "Frequency [Hz]") xlabel = kwargs.pop("xlabel", "Time [s]") xticks = kwargs.pop("xticks", None) yticks = kwargs.pop("yticks", None) cmap = kwargs.pop("cmap", "gray") vmin = kwargs.pop("vmin", None) vmax = kwargs.pop("vmax", None) extent = kwargs.pop("extent", None) now = kwargs.pop("now", False) if extent is not None: figsize = kwargs.pop( "figsize", (0.20 * (extent[3] - extent[2]) / 1000, 0.33 * (extent[1] - extent[0])), ) else: figsize = kwargs.pop("figsize", (4, 13)) # if no ax, create a figure and a subplot associated a figure otherwise # find the figure that belongs to ax if ax is None: fig, ax = plt.subplots() # set the paramters of the figure fig.set_facecolor("w") fig.set_edgecolor("k") fig.set_figheight(figsize[0] + 1) fig.set_figwidth(figsize[1]) else: fig = ax.get_figure() # display image _im = ax.imshow(im, extent=extent, origin="lower", cmap=cmap, vmin=vmin, vmax=vmax, **kwargs) if colorbar==True: plt.colorbar(_im, ax=ax) # set the parameters of the subplot ax.set_title(title) ax.set_xlabel(xlabel) ax.set_ylabel(ylabel) if xticks is not None: ax.set_xticks(ticks=xticks[0]) if len(xticks) == 2: ax.set_xticklabels(labels=xticks[1]) if yticks is not None: ax.set_yticks(ticks=yticks[0]) if len(yticks) == 2: ax.set_yticklabels(labels=yticks[1]) ax.axis("tight") fig.tight_layout() # Display the figure now if now: plt.show() return ax, fig
#%%
[docs] def plot_spectrogram(Sxx, extent, db_range=96, gain=0, log_scale=True, colorbar=True, interpolation='bilinear', **kwargs): """ Plot spectrogram represenation. Parameters ---------- Sxx : 2d ndarray Spectrogram computed using `maad.sound.spectrogram`. extent : list of scalars [left, right, bottom, top] Location, in data-coordinates, of the lower-left and upper-right corners. db_range : int or float, optional Range of values to display the spectrogram. The default is 96. gain : int or float, optional Gain in decibels added to the signal for display. The default is 0. log_scale : bool, optional Logarithmic transformation of spectrogram coefficients to enhace visual representation. The default is True. colorbar : bool, optional Plot the colorbar next to the spectrogram. The default is True. interpolation : bool, optional Interpolate spectrogram values to enhance visual representation. The default is 'bilinear'. **kwargs : matplotlib figure properties Other keyword arguments that are passed down to matplotlib.axes. Returns ------- ax : matplotlib.axes The matplotlib axes associated to plot. See also -------- maad.sound.plot_spectrum, maad.util.plot_wave Examples -------- Plot the spectrogram of an audio. >>> from maad import sound, util >>> s, fs = sound.load('../data/spinetail.wav') >>> Sxx, tn, fn, ext = sound.spectrogram(s,fs) >>> util.plot_spectrogram(Sxx, ext, db_range=50, gain=30, figsize=(4,10)) Use `plot_spectrogram` with predifined matplotlib axes. >>> import matplotlib.pyplot as plt >>> fig, ax = plt.subplots(2,1, figsize=(10,6)) >>> util.plot_wave(s, fs, ax=ax[0]) >>> util.plot_spectrogram(Sxx, ext, db_range=50, gain=30, colorbar=False, ax=ax[1]) Plot a single frase of the spinetail. >>> Sxx, tn, fn, ext = sound.spectrogram(s,fs, flims=(2000,15000), tlims=(5,8)) >>> util.plot_spectrogram(Sxx, ext, db_range=50, gain=30, figsize=(4,6)) """ if log_scale: Sxx_db = power2dB(Sxx, db_range, gain) else : Sxx_db = Sxx ax, fig = plot2d(Sxx_db, extent=extent, colorbar=colorbar, interpolation=interpolation, **kwargs) return ax
#%%
[docs] def rand_cmap( nlabels, type="bright", first_color_black=True, last_color_black=False, seed=321, verbose=False): """ Creates a random colormap to be used together with matplotlib. Useful for segmentation tasks Parameters ---------- nlabels : int Number of labels (size of colormap) type : string 'bright' for strong colors, 'soft' for pastel colors. Default is 'bright' first_color_black : bool, optional Option to use first color as black. Default is True last_color_black : bool, optional Option to use last color as black. Default is False seed : int, optional Fix the seed of the random engine. Default is 321 verbose : bool, optional Prints the number of labels and shows the colormap. Default is False Returns ------- random_colormap : Colormap Colormap type used by matplotlib References ---------- adapted from https://github.com/delestro/rand_cmap author : delestro """ # initialize the random seed in order to get always the same random order np.random.seed(seed=seed) if verbose: print("Number of labels: " + str(nlabels)) # Generate color map for bright colors, based on hsv if type == "bright": randHSVcolors = [ ( np.random.uniform(low=0.1, high=1), np.random.uniform(low=0.2, high=1), np.random.uniform(low=0.5, high=1), ) for i in range(nlabels) ] # Convert HSV list to RGB randRGBcolors = [] for HSVcolor in randHSVcolors: randRGBcolors.append( colorsys.hsv_to_rgb(HSVcolor[0], HSVcolor[1], HSVcolor[2]) ) # Generate soft pastel colors, by limiting the RGB spectrum if type == "soft": low = 0.6 high = 0.95 randRGBcolors = [ ( np.random.uniform(low=low, high=high), np.random.uniform(low=low, high=high), np.random.uniform(low=low, high=high), ) for i in range(nlabels) ] if first_color_black: randRGBcolors[0] = [0, 0, 0] if last_color_black: randRGBcolors[-1] = [0, 0, 0] random_colormap = LinearSegmentedColormap.from_list( "new_map", randRGBcolors, N=nlabels ) # Display colorbar if verbose: from matplotlib import colors, colorbar from matplotlib import pyplot as plt fig, ax = plt.subplots(1, 1, figsize=(15, 0.5)) bounds = np.linspace(0, nlabels, nlabels + 1) norm = colors.BoundaryNorm(bounds, nlabels) colorbar.ColorbarBase( ax, cmap=random_colormap, norm=norm, spacing="proportional", ticks=None, boundaries=bounds, format="%1i", orientation=u"horizontal", ) return random_colormap
#%%
[docs] def crop_image(im, tn, fn, fcrop=None, tcrop=None): """ Crop a spectrogram (or an image) in time (horizontal X axis) and frequency (vertical y axis) Parameters ---------- im : 2d ndarray image to be cropped tn, fn : 1d ndarray tn is the time vector. fn is the frequency vector. They are required in order to know the correspondance between pixels and (time,frequency) fcrop, tcrop : list of 2 scalars [min, max], optional, default is None fcrop corresponds to the min and max boundary frequency values tcrop corresponds to the min and max boundary time values Returns ------- im : 2d ndarray image cropped tn, fn, 1d ndarray new time and frequency vectors Examples -------- >>> w, fs = maad.sound.load('../data/cold_forest_daylight.wav') >>> Sxx_power,tn,fn,_ = maad.sound.spectrogram(w,fs) >>> Lxx = maad.spl.power2dBSPL(Sxx_power, gain=42) # convert into dB SPL >>> fig_kwargs = {'vmax': Lxx.max(), 'vmin':0, 'extent':(tn[0], tn[-1], fn[0], fn[-1]), 'title':'Power spectrogram density (PSD)', 'xlabel':'Time [s]', 'ylabel':'Frequency [Hz]', } >>> fig, ax = maad.util.plot2d(Lxx,**fig_kwargs) >>> Lxx_crop, tn_crop, fn_crop = maad.util.crop_image(Lxx, tn, fn, fcrop=(2000,10000), tcrop=(0,30)) >>> fig_kwargs = {'vmax': Lxx.max(), 'vmin':0, 'extent':(tn_crop[0], tn_crop[-1], fn_crop[0], fn_crop[-1]), 'title':'Crop of the power spectrogram density (PSD)', 'xlabel':'Time [s]', 'ylabel':'Frequency [Hz]', } >>> fig, ax = maad.util.plot2d(Lxx_crop,**fig_kwargs) """ if tcrop is not None: indt = (tn >= tcrop[0]) * (tn <= tcrop[1]) im = im[:, indt] # redefine tn tn = tn[np.where(indt > 0)] if fcrop is not None: indf = (fn >= fcrop[0]) * (fn <= fcrop[1]) im = im[ indf, ] # redefine fn fn = fn[np.where(indf > 0)] return im, tn, fn
#%%
[docs] def save_figlist(fname, figlist): """ Save a list of figures or spectrograms to disk. Parameters ---------- fname: string Suffix string add to the filename. The extension should be specified since it indicates the image format. Notes ----- This function does not return any variable. """ for i, fig in enumerate(figlist): fname_save = "%d_%s" % (i, fname) imsave(fname_save, fig)
#%%
[docs] def plot_features_map(df, norm=True, mode="24h", **kwargs): """ Plot features values on a heatmap. The plot has the features the vertical axis and the time on the horizontal axis. Parameters ---------- df : Panda DataFrame DataFrame with features (ie. indices). norm : boolean, default is True if True, the features are scaled between 0 to 1 mode : string in {'24h'}, default is '24h' Select if the timeline of the phenology: -'24h' : average of the results over a day - otherwise, the timeline is the timeline of the dataframe **kwargs Specific to this function: - ftime : Time format to display as x label by default '%Y-%m-%d'(see https://docs.python.org/fr/3.6/library/datetime.html?highlight=strftime#strftime-strptime-behavior) Specific to matplotlib: - figsize : tuple of integers, optional, default: (4,10) width, height in inches. - title : string, optional, default : 'Spectrogram' title of the figure - xlabel : string, optional, default : 'Time [s]' label of the horizontal axis - ylabel : string, optional, default : 'Amplitude [AU]' label of the vertical axis - xticks : tuple of ndarrays, optional, default : none * ticks : array_like => A list of positions at which ticks should be placed. You can pass an empty list to disable yticks. * labels : array_like, optional => A list of explicit labels to place at the given locs. - yticks : tuple of ndarrays, optional, default : none * ticks : array_like => A list of positions at which ticks should be placed. You can pass an empty list to disable yticks. * labels : array_like, optional => A list of explicit labels to place at the given locs. - cmap : string or Colormap object, optional, default is 'gray' See https://matplotlib.org/examples/color/colormaps_reference.html in order to get all the existing colormaps examples: 'hsv', 'hot', 'bone', 'tab20c', 'jet', 'seismic', 'viridis'... - vmin, vmax : scalar, optional, default: None `vmin` and `vmax` are used in conjunction with norm to normalize luminance data. Note if you pass a `norm` instance, your settings for `vmin` and `vmax` will be ignored. - extent : list of scalars [left, right, bottom, top], optional, default: None The location, in data-coordinates, of the lower-left and upper-right corners. If `None`, the image is positioned such that the pixel centers fall on zero-based (row, column) indices. - now : boolean, optional, default : True if True, display now. Cannot display multiple images. To display mutliple images, set now=False until the last call for the last image ... and more, see matplotlib Returns ------- fig : Figure The Figure instance ax : Axis The Axis instance Examples -------- see plot_extract_alpha_indices.py advanced example for a complete example >>> import numpy as np >>> import pandas as pd >>> np.random.seed(2021) >>> M = np.random.rand(24, 7) >>> df = pd.DataFrame(M) >>> indices = ['A','B','C','D','E','F','G'] >>> df.columns = indices >>> df.index =pd.date_range(start=pd.Timestamp('00:00:00'), end=pd.Timestamp('23:00:00'), freq='1H') >>> maad.util.plot_features_map(df) """ if isinstance(df, pd.DataFrame) == False: raise TypeError("df must be a Pandas Dataframe") elif isinstance(df.index, pd.DatetimeIndex) == False: raise TypeError("df must have an index of type DateTimeIndex") if mode == "24h": # Mean values by hour df = df.groupby(df.index.hour).mean() # Get the list of unique index of type 'hour' x_label = [i + j for i, j in zip(map(str, df.index.values), ["h"] * len(df))] else: # Get the list of unique index of type 'hour' ftime = kwargs.pop("ftime", "%Y-%m-%d") x_value = df.index.strftime(ftime) x_label = x_value.tolist() if norm: df = linear_scale(df) # kwargs cmap = kwargs.pop("cmap", "RdBu_r") figsize = kwargs.pop("figsize", None) # plot if figsize is None : fig = plt.figure(figsize=(len(df)*0.33, len(list(df))*0.27)) ax = fig.add_subplot(111) caxes = ax.matshow(df.transpose(), cmap=cmap, aspect="auto", **kwargs) if norm : fig.colorbar(caxes, shrink=0.75, label="Normalized value") else : fig.colorbar(caxes, shrink=0.75, label="Value") # Set ticks on both sides of axes on ax.tick_params(axis="x", bottom=True, top=False, labelbottom=True, labeltop=False) # We want to show all ticks... ax.set_yticks(np.arange(len(df.columns))) ax.set_xticks(np.arange(len(x_label))) # ... and label them with the respective list entries ax.set_yticklabels(df.columns) ax.set_xticklabels(x_label) # Rotate the tick labels and set their alignment. plt.setp(ax.get_yticklabels(), rotation=0, ha="right", fontsize=10) plt.setp(ax.get_xticklabels(), rotation=90, ha="center", fontsize=10) fig.tight_layout() plt.show() return fig, ax
#%%
[docs] def heatmap_by_date_and_time ( dataframe, disp_column, date_format = "%V", # see https://docs.python.org/3/library/datetime.html date_range = [1,53], time_resolution = "30T", time_range = ["00:00", "23:59"], start_hour = "00:00", full_display = False, date_min_to_disp = 1, date_max_to_disp = 53, cb_legend = "", display = True, verbose = False, **kwargs, ) : """ Plot a heatmap of a features by time (x-axis) and date (y-axis). Parameters ---------- dataframe : pandas.DataFrame The input DataFrame containing the data. Must contain a column (or index) date in the format %Y-%m-%d %H:%M%S disp_column : str The name of the column to be displayed. date_format : str, optional The format of the date. The default is "%V". (See https://docs.python.org/3/library/datetime.html for format codes) Possible format are : - "%V" for week number (from 1 to 53) - "%m" for Month (from 1 to 12) - "%d" for Day (from 1 to 31 depending on the month) - "%m-%d" for Date without year (from 01-01 to 12-31) - "%y-%m-%d" for Date with year date_range : list of int, optional The range of date types to include. The default is [1, 53]. The format of the range depends on date_format. For instance, to get all the samples in the March, date_range would be [03-01, 03-31] and date_format would be "%m-%d" time_resolution : str, optional The time resolution. The default is "30T". T is for minute. 30T means a time resolution of 30 min. Everything within this intervale is averaged Other time formats are H for hour and D for day. time_range : list of str, optional The time range to consider. The default is ["00:00", "23:59"]. start_hour : str, optional The start hour. The default is "12:00". full_display : bool, optional Whether to display the full date range. The default is False. date_min_to_disp : int, optional The minimum date on the y-axis. The default is 1. The value depends on the date_format. See date_format to know the possible formats and the range of possible values date_max_to_disp : int, optional The maximum date on the y-axis. The default is 53. The value depends on the date_format. See date_format to know the possible formats and the range of possible values cb_legend : str, optional The colorbar legend label. The default is "". verbose : bool, optional If True, display verbose information. The default is False. **kwargs Specific to this function: - ftime : Time format to display as x label by default '%Y-%m-%d'(see https://docs.python.org/fr/3.6/library/datetime.html?highlight=strftime#strftime-strptime-behavior) Specific to matplotlib: - figsize : tuple of integers, optional, default: (4,10) width, height in inches. - title : string, optional, default : 'Spectrogram' title of the figure - xlabel : string, optional, default : 'Time [s]' label of the horizontal axis - ylabel : string, optional, default : 'Amplitude [AU]' label of the vertical axis - xticks : tuple of ndarrays, optional, default : none * ticks : array_like => A list of positions at which ticks should be placed. You can pass an empty list to disable yticks. * labels : array_like, optional => A list of explicit labels to place at the given locs. - yticks : tuple of ndarrays, optional, default : none * ticks : array_like => A list of positions at which ticks should be placed. You can pass an empty list to disable yticks. * labels : array_like, optional => A list of explicit labels to place at the given locs. - cmap : string or Colormap object, optional, default is 'gray' See https://matplotlib.org/examples/color/colormaps_reference.html in order to get all the existing colormaps examples: 'hsv', 'hot', 'bone', 'tab20c', 'jet', 'seismic', 'viridis'... - vmin, vmax : scalar, optional, default: None `vmin` and `vmax` are used in conjunction with norm to normalize luminance data. Note if you pass a `norm` instance, your settings for `vmin` and `vmax` will be ignored. - extent : list of scalars [left, right, bottom, top], optional, default: None The location, in data-coordinates, of the lower-left and upper-right corners. If `None`, the image is positioned such that the pixel centers fall on zero-based (row, column) indices. - now : boolean, optional, default : True if True, display now. Cannot display multiple images. To display mutliple images, set now=False until the last call for the last image ... and more, see matplotlib Returns ------- df_mean : pd.DataFrame The heatmap with mean values. df_std :pd.DataFrame The heatmap with standard deviation values. fig : matplotlib.figure.Figure The generated matplotlib figure. ax : matplotlib.axes.Axes The generated matplotlib axis. """ # test if dataframe is not a pd.DataFrame if isinstance(dataframe, pd.DataFrame) == False: if verbose : print("***WARNING*** dataframe must be a valid pandas dataframe") return # copy the dataframe df = dataframe.copy() # keep some columns df = df.filter([disp_column,'date']) try : if not('date' in df) : df = df.reset_index(['date']) # convert date to datetime df['date'] = pd.to_datetime(df['date']) # set the index to date df.set_index(['date'], inplace=True) # convert index to datetime df.index = pd.DatetimeIndex(df.index) # sort dataframe by date df = df.sort_index(axis=0) except : raise Exception("***WARNING*** df must have a valid date index that could be converted in Datetime type") # filter by time df = _filter_dataframe_by_datetime(df, "%H:%M", time_range) # filter by date_type df = _filter_dataframe_by_datetime(df, date_format, date_range) # # Extract hour and minute df['time'] = df.index.strftime("%H:%M") # # Extract week, year, weekday or date df['date_type'] = df.index.strftime(date_format) # create a matrix with date_type (week, year, weekday, date) as columns and time (hour) as rows df_mean = df.groupby(['time', 'date_type'])[disp_column].aggregate('mean').unstack() # Resample the timeline. Need to reformat the time in datetime before resampling df_mean = df_mean.reset_index() df_mean.index = pd.to_datetime(df_mean['time'], format='%H:%M') df_mean.drop(columns=['time'], inplace = True) df_mean = df_mean.resample(time_resolution).mean() df_std = df_mean.resample(time_resolution).std() df_mean.index = df_mean.index.strftime("%H:%M") df_std.index = df_std.index.strftime("%H:%M") if display == True : # try to use seaborn if installed try: import seaborn as sns sns.set_theme("paper") except ImportError as e: if verbose : print("seaborn is not installed, use default settings") if full_display == True : # Create a list of unique index of type 'hour' for x axis time_min = datetime.strptime("00:00", "%H:%M") time_max = datetime.strptime("23:59", "%H:%M") x_label = pd.date_range(time_min, time_max, freq=time_resolution).strftime("%H:%M") x_label = x_label.values # Create a vector for y axis depending on date_format if date_format == "%V" : y_label = [f'{x:02d}' for x in np.arange(date_min_to_disp,date_max_to_disp+1)] elif date_format == "%m" : y_label = [f'{x:02d}' for x in np.arange(date_min_to_disp,date_max_to_disp+1)] elif (date_format == "%y-%m-%d") or (date_format == "%m-%d") or (date_format == "%d"): date_min_to_disp = datetime.strptime(str(date_min_to_disp), date_format) date_max_to_disp = datetime.strptime(str(date_max_to_disp), date_format) y_label = pd.date_range(date_min_to_disp, date_max_to_disp, freq='D').strftime(date_format) y_label = y_label.values else: # by default per week y_label = [f'{x:02d}' for x in np.arange(1,53)] # create an prefilled dataframe with all weeks and time df2 = pd.DataFrame(index=x_label, columns=y_label, dtype = 'float') df2.reset_index(inplace=True) df2 = df2.rename(columns={"index":"time"}) df2.set_index("time", inplace=True) for idx, row in df_mean.iterrows() : df2.loc[idx] = row df_mean=df2 # rename the unique column with the corresponding name if date_format == "%V" : df_mean.columns.name='Week number' if date_format == "%m" : df_mean.columns.name='Month' if date_format == "%d" : df_mean.columns.name='Day' if date_format == "%m-%d" : df_mean.columns.name='Date' if date_format == "%y-%m-%d" : df_mean.columns.name='Date' # shift the time to start at start_hour and finish at start_hour, in order to center # the graph at start_hour + 12h df_part1 = df_mean[df_mean.index >= start_hour] df_part2 = df_mean[df_mean.index < start_hour] df_part2.sort_index(inplace=True) df = pd.concat([df_part1,df_part2]) # Get the list of unique index of type 'hour' x_label = [i + j for i, j in zip(map(str, df.index.values), [" "] * len(df))] # plot # matplotlib parameters fig = kwargs.pop("fig", None) ax = kwargs.pop("ax", None) xlabel = kwargs.pop("xlabel", "Hours") ylabel = kwargs.pop("ylabel", df_mean.columns.name) now = kwargs.pop("now", True) vmin = kwargs.pop("vmin", np.nanpercentile(df,1)) vmax = kwargs.pop("vmax", np.nanpercentile(df,99)) cmap = kwargs.pop("cmap", "RdPu") title = kwargs.pop("title", None) figsize = kwargs.pop("figsize", (len(df)*0.33*0.75, len(list(df))*0.26*0.75 +0.75)) # Create a figure and a subplot associated if (fig is None) or (ax is None) : fig = plt.figure(figsize=figsize) ax = fig.add_subplot(111) # display image caxes = ax.matshow(df.transpose(), interpolation = "None", aspect="auto", cmap = cmap, vmin = vmin, vmax = vmax, **kwargs ) if cb_legend =="" : fig.colorbar(caxes, shrink=0.75, label=disp_column) else: fig.colorbar(caxes, shrink=0.75, label=cb_legend) # Set ticks on both sides of axes on ax.tick_params(which='major', axis="x", bottom=False, top=False, labelbottom=True, labeltop=False) ax.tick_params(which='minor', axis="x", length = 0) # We want to show all ticks... ax.set_yticks(np.arange(len(df.columns))) ax.set_xticks(np.arange(len(x_label))) # ... and label them with the respective list entries ax.set_yticklabels(df.columns) ax.set_xticklabels(x_label) # Minor ticks ax.set_xticks(np.arange(-0.5, len(df.index), 1), minor=True) ax.set_yticks(np.arange(-0.5, len(list(df)), 1), minor=True) # Gridlines based on minor ticks ax.grid(which='major', color='w', linestyle='-', linewidth=0) ax.grid(which='minor', color='w', linestyle='-', linewidth=1) # add title to the axis ax.set_xlabel(xlabel) ax.set_ylabel(ylabel) # Rotate the tick labels and set their alignment. plt.setp(ax.get_yticklabels(), rotation=0, ha="right", fontsize=10) plt.setp(ax.get_xticklabels(), rotation=90, ha="center", fontsize=10) plt.tight_layout() # Adding a title to the figure if title is not None: fig.suptitle(title) if now : plt.show() else : fig = None ax = None return df_mean, df_std, fig, ax
# =============================================================================
[docs] def plot_features(df, ax=None, norm=True, mode="24h", **kwargs): """ Plot the variation of features values (ie. indices) in the DataFrame obtained with ``maad.features``. Parameters ---------- df : Panda DataFrame DataFrame with features (ie. indices). norm : boolean, default is True if True, the features are normalized by the max mode : string in {'24h'}, default is '24h' Select if the timeline of the phenology : -'24h' : average of the results over a day - otherwise, the timeline is the timeline of the dataframe **kwargs - figsize : tuple of integers, optional, default: (4,10) width, height in inches. - figtitle : string, optional, default : '' title of the figure - xlabel : string, optional, default : 'Time [s]' label of the horizontal axis - ylabel : string, optional, default : 'Amplitude [AU]' label of the vertical axis - xticks : tuple of ndarrays, optional, default : none * ticks : array_like => A list of positions at which ticks should be placed. You can pass an empty list to disable yticks. * labels : array_like, optional => A list of explicit labels to place at the given locs. - yticks : tuple of ndarrays, optional, default : none * ticks : array_like => A list of positions at which ticks should be placed. You can pass an empty list to disable yticks. * labels : array_like, optional => A list of explicit labels to place at the given locs. - now : boolean, optional, default : True if True, display now. Cannot display multiple images. To display mutliple images, set now=False until the last call for the last image ... and more, see matplotlib Returns ------- fig : Figure The Figure instance ax : Axis The Axis instance Examples -------- see plot_extract_alpha_indices.py advanced example for a complete example >>> import numpy as np >>> import pandas as pd >>> np.random.seed(2021) >>> M = np.random.rand(24, 2) >>> df = pd.DataFrame(M) >>> indices = ['A','B'] >>> df.columns = indices >>> df.index =pd.date_range(start=pd.Timestamp('00:00:00'), end=pd.Timestamp('23:00:00'), freq='1H') >>> maad.util.plot_features(df) """ if isinstance(df, pd.DataFrame) == False: raise TypeError("df must be a Pandas Dataframe") elif isinstance(df.index, pd.DatetimeIndex) == False: raise TypeError("df must have an index of type DateTimeIndex") if mode == "24h": # Mean values by hour df = df.groupby(df.index.hour).mean() if norm: df = linear_scale(df) # plot import itertools from matplotlib.lines import Line2D list_markers = tuple(list(Line2D.markers.keys())[0:-4]) markers = itertools.cycle(list_markers) prop_cycle = plt.rcParams["axes.prop_cycle"] colors = itertools.cycle(prop_cycle.by_key()["color"]) figsize = kwargs.pop("figsize", (5, 5)) kwargs.pop("label", None) now = kwargs.pop("now", True) # if no ax, create a figure and a subplot associated a figure otherwise # find the figure that belongs to ax if ax is None: fig, ax = plt.subplots(**kwargs) fig.set_size_inches(figsize) else: fig = ax.get_figure() for indice in list(df): ax.plot( df.index, df[indice], marker=next(markers), color=next(colors), linestyle="-", label=indice, **kwargs ) if mode == "24h": ax.set_xlabel("Day time (Hour)") ax.grid() ax.legend() fig.tight_layout() # Display the figure now if now: plt.show() return fig, ax
# =============================================================================
[docs] def plot_correlation_map(df, R_threshold=0.75, method="spearman", **kwargs): """ Plot the correlation map between indices in the DataFrame obtained with ``maad``. Parameters ---------- df : Panda DataFrame DataFrame with features (ie. indices). R_threshold : scalar between 0 to 1, default is 0.75 Show correlations with R higher than R_threshold method : string in {'spearman', 'pearson'}, default is 'spearman' Choose the correlation type **kwargs - figsize : tuple of integers, optional, default: (4,10) width, height in inches. - title : string, optional, default : 'Spectrogram' title of the figure - xlabel : string, optional, default : 'Time [s]' label of the horizontal axis - ylabel : string, optional, default : 'Amplitude [AU]' label of the vertical axis - xticks : tuple of ndarrays, optional, default : none * ticks : array_like => A list of positions at which ticks should be placed. You can pass an empty list to disable yticks. * labels : array_like, optional => A list of explicit labels to place at the given locs. - yticks : tuple of ndarrays, optional, default : none * ticks : array_like => A list of positions at which ticks should be placed. You can pass an empty list to disable yticks. * labels : array_like, optional => A list of explicit labels to place at the given locs. - cmap : string or Colormap object, optional, default is 'gray' See https://matplotlib.org/examples/color/colormaps_reference.html in order to get all the existing colormaps examples: 'hsv', 'hot', 'bone', 'tab20c', 'jet', 'seismic', 'viridis'... - vmin, vmax : scalar, optional, default: None `vmin` and `vmax` are used in conjunction with norm to normalize luminance data. Note if you pass a `norm` instance, your settings for `vmin` and `vmax` will be ignored. - extent : list of scalars [left, right, bottom, top], optional, default: None The location, in data-coordinates, of the lower-left and upper-right corners. If `None`, the image is positioned such that the pixel centers fall on zero-based (row, column) indices. - now : boolean, optional, default : True if True, display now. Cannot display multiple images. To display mutliple images, set now=False until the last call for the last image ... and more, see matplotlib Returns ------- fig : Figure The Figure instance ax : Axis The Axis instance Examples -------- see plot_extract_alpha_indices.py advanced example for a complete example >>> import numpy as np >>> import pandas as pd >>> np.random.seed(2021) >>> M = np.random.rand(10, 10) >>> df = pd.DataFrame(M) >>> indices = ['A','B','C','D','E','F','G','H','I','J'] >>> df.columns = indices >>> maad.util.plot_correlation_map(df, R_threshold=0) """ # Correlation matrix with only the columns that contain numeric values numeric_columns = df.select_dtypes(include='number') corr_matrix = numeric_columns.corr() # pop kwargs figsize = kwargs.pop("figsize", (10, 8)) cmap = kwargs.pop("cmap", "RdBu_r") label_colorbar = kwargs.pop("label_colorbar", "R") fig = plt.figure() fig.set_size_inches(figsize) ax = fig.add_subplot(111) caxes = ax.matshow(corr_matrix[abs(corr_matrix) ** 2 > R_threshold ** 2], cmap=cmap) fig.colorbar(caxes, shrink=0.75, label=label_colorbar) # We want to show all ticks... ax.set_xticks(np.arange(len(corr_matrix.columns))) ax.set_yticks(np.arange(len(corr_matrix.columns))) # ... and label them with the respective list entries ax.set_xticklabels(corr_matrix.columns) ax.set_yticklabels(corr_matrix.columns) # Rotate the tick labels, set their alignment and fontsize plt.setp(ax.get_xticklabels(), rotation=90, ha="center", fontsize=7) # Rotate the tick labels and set their alignment. plt.setp(ax.get_yticklabels(), rotation=0, ha="right", fontsize=8) fig.tight_layout() plt.show() return fig, ax
# =============================================================================
[docs] def false_Color_Spectro( df, indices=None, plim=(1, 99), reverseLUT=False, permut=False, unit="minutes", verbose=False, display=False, savefig=None, **kwargs ): """ Create False Color Spectrogram from indices obtained by MAAD. Only indices than can be computed bin per bin (ie frequency per frequency) are used to create False Color Spectro. They are called xxx_per_bin. Parameters ---------- df : Panda DataFrame DataFrame with indices per frequency bin. indices : list, default is None List of indices. If permut is False (see permut), if indices is None : 1st indices is red (R), 2nd indice is green (G) and the 3rd indices is blue (B). if indices is a list of 3 indices or more, only the 3 first indices (triplet) are used to create the false color spectro. if permut is True, if indices is None : All indices in df are used if indices is a list of 3 indices or more, all the indices in the list are used to create the false color spectro with all the possibile permutations. plim : list of 2 scalars, default is (1,99) Set the minimum and maximum percentile to define the min and max value of each indice. These values are then used to clip the values of each indices between these limits. reverseLUT: boolean, default is False LUT : look-up table. if False, the highest indice value is the brigthest color (255) and the lowest indice value is the darkest (0) if True, it's the reverse order, the highest indice value is the darkest color (0) and the lowest is the brighest (255) permut : boolean, default is False if True, all the permutations possible for red, green, blue are computed among the list of indices (see indices) unit : string in {'minutes', 'hours', 'days', 'weeks'}, default is 'minutes' Select the unit base for x axis verbose : boolean, default is False print indices on the default terminal display : boolean, default is False Display False Color Spectro savefig : string, optional, default is None if not None, figures will be safe. Savefig is the prefix of the save filename. kwargs, optional - dpi : scalar, optional, default 96 - format : str, optional, default .png Returns ------- false_color_image : ndarray of scalars Matrix with ndim = 4 if multiple false color spectro or ndim = 3, if single false color spectro with 3 colors : R, G, B triplet : list List of triplet of indices corresponding to each false color spectro Examples -------- see plot_extract_alpha_indices.py advanced example for a complete example >>> import numpy as np >>> import pandas as pd >>> A_per_bin = [[0.5,0.4,0.3,0.2,0.1], [0.3,0.3,0.3,0.3,0.3], [0.5,0.5,0.3,0.0,0.0], [0.2,0.2,0.3,0.0,0.0], [0.0,0.0,0.3,0.0,0.0]] >>> B_per_bin = [[0.5,0.5,0.5,0.5,0.5], [0.5,0.5,0.5,0.5,0.5], [0.5,0.5,0.2,0.0,0.0], [0.5,0.2,0.1,0.0,0.0], [0.5,0.2,0.0,0.0,0.0]] >>> C_per_bin = [[0.0,0.7,0.0,0.5,0.5], [0.7,0.2,0.0,0.5,0.5], [0.5,0.5,0.7,0.2,0.2], [0.0,0.7,0.0,0.2,0.2], [0.7,0.2,0.0,0.2,0.2]] >>> df = pd.DataFrame([A_per_bin,B_per_bin,C_per_bin]) >>> df = df.T >>> df.columns = ['A_per_bin','B_per_bin','C_per_bin'] >>> df.index = pd.date_range('20200101', periods=len(df)) >>> maad.util.false_Color_Spectro (df, display=True ,unit='days', figsize=[3,3]) """ # sort dataframe by date df = df.sort_index(axis=0) # set index as DatetimeIndex df = df.set_index(pd.DatetimeIndex(df.index)) # remove column file if "file" in df.columns: df = df.drop(columns="file") # test if frequencies is in columns if "frequencies" in df.columns: # get frequencies and remove the column # test if type of df['frequencies'] is number or str if isinstance(df.frequencies.iloc[0], str): fn = df["frequencies"].apply(literal_eval).iloc[0] else: fn = df["frequencies"].iloc[0] # drop frequencies df = df.drop(columns="frequencies") else: fn = np.arange(0, len(df)) # Set the list of indices if indices is None if indices is None: indices = list(df) # for each indice, check if values type is string and convert into scalars # Values are vectors of strings when data are loading from csv for indice in indices: # test if type of df[indice] is string if isinstance(df[indice].iloc[0], str): # convert string into scalars df[indice] = df[indice].apply(literal_eval) # create a dataframe with normalized values for each indices df_z = pd.DataFrame() for indice in indices: z = [] if verbose: print(indice) for v in df[indice]: z.append(v) z = np.asarray(z).T # Select the min and max value for each indice z_min = np.percentile(z, plim[0]) z_max = np.percentile(z, plim[1]) # clip the value to the min and max found z = np.clip(z, z_min, z_max) # linear conversion if reverseLUT == True: # between 1 to 0 z = linear_scale(z, 1, 0) else: # between 0 to 1 z = linear_scale(z, 0, 1) df_z[indice] = [z * 255] # find all permutation of 3 indices among all indices if permut == True: import itertools per = itertools.permutations(list(df_z), 3) triplet = [] for val in per: triplet.append([*val]) else: triplet = [] triplet.append(indices[0]) triplet.append(indices[1]) triplet.append(indices[2]) triplet = [triplet] ##################### # get the number of pixels along frequency (Nf) and time (Nt) Nf, Nt = df_z[triplet[0][0]].values.tolist()[0].shape # test if figsize is in kwargs figsize = kwargs.pop("figsize", None) # if figsize is not in kwargs if figsize is None: figsize = (6 * Nt / 250, 10 * Nf / 512) fig_kwargs = {"figsize": figsize, "tight_layout": "tight_layout"} # number of days in the period deltaT = df.index.max() - df.index.min() # unit if unit == "minutes": normT = 60e9 xlabel = "Minutes" elif unit == "hours": normT = 60e9 * 60 xlabel = "Hours" elif unit == "days": normT = 60e9 * 60 * 24 xlabel = "Days" elif unit == "weeks": normT = 60e9 * 60 * 24 * 7 xlabel = "Weeks" else: normT = 60e9 xlabel = "Minutes" false_color_image = [] for tt in np.arange(len(triplet)): # create the false color image (R,G,B) z0 = df_z[triplet[tt][0]].values.tolist()[0] z1 = df_z[triplet[tt][1]].values.tolist()[0] z2 = df_z[triplet[tt][2]].values.tolist()[0] false_color_image.append((np.dstack((z0, z1, z2))).astype(np.uint8)) # Display the False Color Spectro if display: plt.rcParams.update({"font.size": 10}) plt.rcParams.update({"font.family": "serif"}) fig = plt.figure(facecolor="white", **fig_kwargs) plt.imshow( false_color_image[tt], aspect="auto", origin="lower", extent=(fn[0], deltaT.value / normT, 0, fn[-1]), **kwargs ) plt.xlabel(xlabel) plt.ylabel("Frequency (Hz)") plt.title( "False Color Spectro " + "\n" + " [R:" + triplet[tt][0][:-8] + "; " + "G:" + triplet[tt][1][:-8] + "; " + "B:" + triplet[tt][2][:-8] + "]", size=12, ) plt.xticks(fontsize=10) plt.yticks(fontsize=10) plt.tight_layout() # SAVE FIGURE if savefig is not None: dpi = kwargs.pop("dpi", 96) bbox_inches = "tight" format = kwargs.pop("format", "png") filename = ( "_fcs_" + triplet[tt][0][:-8] + "_" + triplet[tt][1][:-8] + "_" + triplet[tt][2][:-8] ) full_filename = savefig + filename + "." + format if verbose: print("\n" "save figure : %s" % full_filename) # save fig fig.savefig( fname=full_filename, dpi=dpi, bbox_inches=bbox_inches, format=format, **kwargs ) # close fig plt.close(fig) # convert into ndarray false_color_image = np.asarray(false_color_image) # test if there is only one False Color Spectro, then, remove the 1st dim if false_color_image.shape[0] == 1: false_color_image = false_color_image[0] triplet = triplet[0] return false_color_image, triplet