text: # AUTOGENERATED! DO NOT EDIT! File to edit: 01_utils.ipynb (unless otherwise specified).
  
  __all__ = ['extend_sync_timepoints', 'align_sync_timepoints', 'resample_to_timepoints', 'flip_stimulus',
             'flip_gratings', 'stim_to_dataChunk', 'phy_results_dict', 'spike_to_dataChunk', 'get_calcium_stack_lenghts',
             'twoP_dataChunks', 'img_2d_fit', 'fill_nan', 'stim_inten_norm', 'group_direction_response',
             'group_chirp_bumps', 'get_repeat_corrected', 'buszaki_shank_channels', 'format_pval', 'stim_recap_df']
  
  # Cell
  import numpy as np
  import pandas as pd
  import os
  import glob
  import re
  from typing import Dict, Tuple, Sequence, Union, Callable
  import scipy.interpolate as interpolate
  from scipy.ndimage import convolve1d
  
  from .core import *
  
  # Cell
  def extend_sync_timepoints(timepoints:np.ndarray, signals:np.ndarray,
                             up_bound, low_bound=0) -> Tuple[DataChunk, DataChunk]:
      """
      Extend arrays of timepoints and signals (with identical shape) from the low_bound up to the up_bound.
      For example, the first timepoint could be 2000, and with a low_bound of 0, it would add the
      timepoints 0, 500, 1000, 1500 if the timepoint distance is of 500 (obtained by averaging the timepoints
      distances).
  
      params:
          - timepoints: Timepoints to extend
          - signals: Signals to extend
          - up_bound: Up bound to which to extend both timepoints and signals
          - low_bound: Low bound to which to extend both timepoints and signals
  
      returns:
          - timepoint: Extended timepoints
          - signals: The datachunk array is not modified, but the idx attribute is increased by the number
          of frames added with the low_bound.
      """
      assert len(timepoints) == len(signals)
      timepoints = np.array(timepoints)
      signals = np.array(signals)
      spb = np.mean(timepoints[1:]-timepoints[:-1]) #spf: sample_per_bin
  
      #Left and right side are just prolongation of the sample_times up
      # from (0-sample_per_fr) to (len+sample_per_fr) so it covers all timepoints
      left_side  = np.arange(timepoints[0]-spb , low_bound - spb, -spb)[::-1].astype(int)
      right_side = np.arange(timepoints[-1]+spb,  up_bound + spb,  spb).astype(int)
  
      new_timepoints = np.concatenate((left_side,
                                       timepoints,
                                       right_side))
  
      timepoint_chunk = DataChunk(data=new_timepoints, idx=0, group="sync")
      signal_chunk    = DataChunk(data=signals, idx=len(left_side), group="sync")
      return (timepoint_chunk, signal_chunk)
  
  # Cell
  def align_sync_timepoints(timepoints:DataChunk, signals:DataChunk,
                            ref_timepoints:DataChunk, ref_signals:DataChunk) -> Tuple[DataChunk, DataChunk, DataChunk]:
      """
      Align the signals of a timepoints timeserie to a reference ref_timepoints with the corresponding
      ref_signals. ref_timepoints is extended to match ref_timepoints lenght.
  
      params:
          - timepoints: timepoints to align
          - signals: signals to align
          - ref_timepoints: reference timepoints
          - ref_signals: reference signals
  
      return:
          - Aligned timepoints (DataChunk)
          - Aligned signals (DataChunk)
      """
      shift_left = ((np.where(ref_signals)[0][0] + ref_signals.idx)
                    - (np.where(signals)[0][0] + signals.idx))
      shift_right   = len(ref_timepoints) - (len(timepoints) + shift_left)
  
      spb     = np.mean(timepoints[1:]-timepoints[:-1]) #spf: sample_per_bin
      spb_ref = np.mean(ref_timepoints[1:]-ref_timepoints[:-1]) #spf: sample_per_bin
  
      left_timepoints      = np.zeros(0)
      left_timepoints_ref  = np.zeros(0)
      right_timepoints     = np.zeros(0)
      right_timepoints_ref = np.zeros(0)
  
      if shift_left > 0: #the ref started before, need to extend the other
          init  = timepoints[0]-spb
          left_timepoints = np.arange(init ,
                                      init-(spb*shift_left+1),
                                      -spb)[:shift_left][::-1].astype(int)
      else:
          shift_left = abs(shift_left)
          init  = ref_timepoints[0]-spb_ref
          left_timepoints_ref = np.arange(init ,
                                          init-(spb_ref*shift_left+1),
                                          -spb_ref)[:shift_left][::-1].astype(int)
          #We also need to shift the index of the ref signals since we increased the size of the ref_timepoints
          ref_signals.idx = ref_signals.idx + len(left_timepoints_ref)
  
      if shift_right > 0: #the ref ended after, need to extend the other
          init  = timepoints[-1]+spb
          right_timepoints = np.arange(init ,
                                      init+(spb*shift_right+1),
                                      spb)[:shift_right].astype(int)
      else:
          shift_right = abs(shift_right)
          init  = ref_timepoints[-1]+spb_ref
          right_timepoints_ref = np.arange(init ,
                                      init+(spb_ref*shift_right+1),
                                      spb_ref)[:shift_right].astype(int)
  
      timepoint    = DataChunk(data=np.concatenate((left_timepoints,
                                       timepoints,
                                       right_timepoints)), idx=0, group="sync")
  
      timepoint_ref = DataChunk(data=np.concatenate((left_timepoints_ref,
                                       ref_timepoints,
                                       right_timepoints_ref)), idx=0, group="sync")
  
      return (timepoint, timepoint_ref, ref_signals)
  
  # Cell
  def resample_to_timepoints(timepoints:np.ndarray, data:np.ndarray,
                               ref_timepoints:DataChunk, group="data") -> DataChunk:
      """
      Resample the data at timepoints to new timepoints given by ref_timepoints.
      Return a DataChunk of the resampled data belonging to a specified group.
  
      params:
          - timepoints: Original timepoints of the data
          - data: Data to resample of shape (t, ...)
          - ref_timepoints: Target timepoints for the resampling
          - group: Group assigned to the returned DataChunk
  
      return:
          - Resampled datachunk with appropriate idx.
      """
  
      assert len(timepoints) == len(data)
      timepoints = np.array(timepoints)
      data = np.array(data)
  
      start_idx = np.argmax(ref_timepoints >= timepoints[0])
      stop_idx  = np.argmax(ref_timepoints >= timepoints[-1])
      if stop_idx == 0:
          stop_idx = len(ref_timepoints)
  
      if len(ref_timepoints[start_idx:stop_idx]) < len(timepoints): #Downsampling
          distance = (np.argmax(timepoints>ref_timepoints[start_idx+1])
                  - np.argmax(timepoints>ref_timepoints[start_idx]))
  
          kernel = np.ones(distance)/distance
          data = convolve1d(data, kernel, axis=0) #Smooting to avoid weird sampling
  
      new_data = interpolate.interp1d(timepoints, data, axis=0)(ref_timepoints[start_idx:stop_idx])
  
      idx = ref_timepoints.idx + start_idx
      return DataChunk(data=new_data, idx = idx, group=group)
  
  # Cell
  def flip_stimulus(stim_inten, ud_inv, lr_inv):
      """
      Flip QDSpy stimuli arrays to match the up/down left/right orientation of the stimulus displayed to
      the mouse.
  
      params:
          - stim_inten: Stimulus matrix to flip of shape (t, color, y, x)
          - ud_inv: Up down inversion boolean (1 to make the flip, 0 for no operation)
          - lr_inv: Up down inversion boolean (1 to make the flip, 0 for no operation)
  
      return:
          - Flipped stimulus array
      """
      if lr_inv:
          stim_inten = np.flip(stim_inten, axis=3) # Axis 0:t 1:color 2:y 3:x
      if not ud_inv:
          #Numpy and QDSpy orientation are different.
          #This way reorientate the stimulus approriatly for display with matplotlib and potential
          #eye tracking corrections
          stim_inten = np.flip(stim_inten, axis=2)
      return stim_inten
  
  def flip_gratings(stim_shader, ud_inv, lr_inv):
      """
      Flip gratings to match the up/down left/right orientation of the stimulus displayed to
      the mouse. A grating is encoded by an array of shape (t, 3(size, angle, speed)).
      Therefore the angles of the grating are modified to encode the "flipped" grating.
  
      params:
          - stim_shader: Grating matrix to flip of shape (t, 3(size, angle(degree), speed))
          - ud_inv: Up down inversion boolean (1 to make the flip, 0 for no operation)
          - lr_inv: Up down inversion boolean (1 to make the flip, 0 for no operation)
  
      return:
          - Flipped grating array
      """
      mask_epochs = ~np.all(stim_shader==0,axis=1)
      if lr_inv:
          stim_shader[mask_epochs,1] = (360 + (180 - stim_shader[mask_epochs,1])) % 360
      if ud_inv:
          stim_shader[mask_epochs,1] = (360 - stim_shader[mask_epochs,1]) % 360
      return stim_shader
  
  def stim_to_dataChunk(stim_inten, stim_start_idx, reference:DataChunk) -> DataChunk:
      """
      Factory function for DataChunk of a stimulus, that squeeze the stim_inten matrix.
  
      params:
          - stim_inten: Stimulus matrix of shape (t, ...)
          - stim_start_idx: Starting frame index of the stimulus
          - reference: DataChunk signal reference used to determine the starting index of the stimulus
  
      return:
          - Datachunk of the stimulus
      """
      return DataChunk(data=np.squeeze(stim_inten), idx = (stim_start_idx + reference.idx), group="stim")
  
  # Cell
  def phy_results_dict(phy_dir):
      """
      Open the result arrays of spike sorting after manual merging with phy.
  
      params:
          - phy_dir: path to the phy results
  
      return:
          - Dictionnary of the phy arrays (amplitudes, channel_map, channel_positions, spike_clusters,
          spike_templates, spike_times, templates)
      """
      res_dict = {}
      res_dict["amplitudes"] = np.load(phy_dir+"/amplitudes.npy")
      res_dict["channel_map"] = np.load(phy_dir+"/channel_map.npy")
      res_dict["channel_positions"] = np.load(phy_dir+"/channel_positions.npy")
      res_dict["spike_clusters"] = np.load(phy_dir+"/spike_clusters.npy")
      res_dict["spike_templates"] = np.load(phy_dir+"/spike_templates.npy")
      res_dict["spike_times"] = np.load(phy_dir+"/spike_times.npy")
      res_dict["templates"] = np.load(phy_dir+"/templates.npy")
      return res_dict
  
  def spike_to_dataChunk(spike_timepoints, ref_timepoints:DataChunk) -> DataChunk:
      """
      Factory function of a DataChunk for spiking count of cells from spike timepoints.
  
      params:
          - spike_timepoints: Dictionnary of the cells spike timepoints (list)
          - ref_timepoints: Reference DataChunk to align the newly created spike count Datachunk
  
      return:
          - Spike count datachunk of shape (t, n_cell)
      """
      type_cast = type(list(spike_timepoints.keys())[0])
      cell_keys = sorted(map(int,
                                      spike_timepoints.keys()))
      cell_map = dict([ (cell_key, i) for i, cell_key in enumerate(cell_keys) ])
      spike_bins = np.zeros((ref_timepoints.shape[0], len(cell_keys)))
      bins = np.concatenate((ref_timepoints[:], [(ref_timepoints[-1]*2)-ref_timepoints[-2]]))
  
      for i, cell in enumerate(cell_keys):
          spike_bins[:, i] = np.histogram(spike_timepoints[type_cast(cell)], bins)[0]
  
      datachunk = DataChunk(data=spike_bins, idx = ref_timepoints.idx, group="cell")
      datachunk.attrs["cell_map"] = cell_map
      return datachunk
  
  # Cell
  def get_calcium_stack_lenghts(folder):
      """
      Function to extract calcium stack lenghts from imageJ macro files associated to the stacks.
  
      params:
          - folder: path of the folder containing the IJ macros files
  
      return:
          - list of stack lenghts
      """
      record_lenghts = []
      pattern_nFrame = r".*number=(\d*) .*"
      for fn in glob.glob(folder+"/*.txt"):
          with open(fn) as f:
              line = f.readline()
              record_lenghts.append(int(re.findall(pattern_nFrame, line)[0]))
      return record_lenghts
  
  def twoP_dataChunks(ref_timepoints:DataChunk, frame_timepoints, len_epochs, C_matrix, S_matrix):
      """
      Factory function for two photon data.
  
      params:
          - ref_timepoints: Reference timepoints to create the DataChunk
          - frame_timepoints: List of frame timepoints for each sequence of two photon frame recorded.
          - len_epochs: Lenght of the recorded epochs (<= than the corresponding frame_timepoints)
          - C_matrix: C_matrix of all frames detected by CaImAn
          - S_matrix: S_matrix of all frames detected by CaImAn
  
      return:
          - C_datachunk_l: A list of C_matrix datachunks
          - S_datachunk_l: A list of S_matrix datachunks
      """
      C_datachunk_l = []
      S_datachunk_l = []
      cursor = 0
      for i, len_epoch in enumerate(len_epochs):
          start_idx = np.argmax(ref_timepoints>frame_timepoints[i][0])
          stop_idx  = np.argmax(ref_timepoints>frame_timepoints[i][len_epoch-1])
          sub_C, sub_S = C_matrix.T[cursor:cursor+len_epoch], S_matrix.T[cursor:cursor+len_epoch]
          cursor += len_epoch
          f = interpolate.interp1d(range(len_epoch), sub_C, axis=0)
          C_datachunk_l.append(DataChunk(data=f(np.linspace(0,len_epoch-1,stop_idx-start_idx)),
                                         idx=start_idx,
                                         group="cell"))
          f = interpolate.interp1d(range(len_epoch), sub_S, axis=0)
          S_datachunk_l.append(DataChunk(data=f(np.linspace(0,len_epoch-1,stop_idx-start_idx)),
                                         idx=start_idx,
                                         group="cell"))
      return (C_datachunk_l, S_datachunk_l)
  
  # Cell
  def img_2d_fit(shape, param_d, f):
      """
      Helper function to generate the 2D image of a fit.
  
      params:
          - shape: Shape of the image in (y, x).
          - param_d: Fit dictionnary.
          - f: Function used of the fit.
      """
      y_, x_ = shape
      xy = np.meshgrid(range(x_), range(y_))
      return f(xy, **param_d).reshape(y_, x_)
  
  # Cell
  def fill_nan(A):
      """
      Fill nan values with interpolation. Credits to BRYAN WOODS@StackOverflow
      """
      inds = np.arange(A.shape[0])
      good = np.where(np.isfinite(A))
      f = interpolate.interp1d(inds[good], A[good],bounds_error=False)
      B = np.where(np.isfinite(A),A,f(inds))
      return B
  
  # Cell
  def stim_inten_norm(stim_inten):
      """
      Normalize a stimulus with intensity in the 8bit range (0-255) to -1 to 1 range.
      """
      stim_inten = stim_inten.astype(float)
      stim_inten -= np.min(stim_inten)
      stim_inten -= np.max(stim_inten)/2
      stim_inten /= np.max(np.abs(stim_inten))
      return np.round(stim_inten, 5)
  
  # Cell
  def group_direction_response(stim_prop, spike_counts, n_repeat, n_cond=32):
      """
      Group the cells responses from shuffled grating stimulus repetitions. Retrieves a dictionnary
      with a key for each condition.
  
      params:
          - stim_prop: Grating array of shape (t, 3(size, angle, speed))
          - spike_counts: Spike counts response of the cells of shape (t, n_cell)
          - n_repeat: Number of repeat of each condition
          - n_cond: Total number of condition (speed/size condition * n_angle)
  
      return:
          - dictionnary of the spike counts for each condition (speed/size), with shape (n_angle, n_repeat, len, n_cell)
      """
  
      n_cell = spike_counts.shape[-1]
      condition_repeat = stim_prop.reshape(n_repeat*n_cond,-1,3)[:,10,:] #Take the condition for each repeat
      # We take it at the 10th frame in case of frame replacement during synchronisation
      #(the 10th should be unchanged)
  
      #Reshape the spike response to (n_cond, len, n_cell)
      spike_resh       = spike_counts.reshape(n_repeat*n_cond,-1,n_cell)
  
      angles = np.unique(condition_repeat[:,1])
  
      data_dict = {}
      for cond in np.unique(condition_repeat, axis=0):
          spat_freq, angle, speed = tuple(cond)
          idx_cond = np.argwhere(np.all(condition_repeat==cond, axis=1))[:,0]
  
          cond_key = str(spat_freq)+"@"+str(round(speed,2))
          if cond_key not in data_dict.keys():
              data_dict[cond_key] = np.empty((len(angles), len(idx_cond), *spike_resh[0].shape))
  
          idx_angle = np.where(angle==angles)[0][0]
          data_dict[cond_key][idx_angle] = np.array([spike_resh[idx] for idx in idx_cond])
      return data_dict
  
  # Cell
  def group_chirp_bumps(stim_inten, spike_counts, n_repeat):
      """
      Find the cells response to the OFF-ON-OFF initial parts of the chirps.
  
      params:
          - stim_inten: Stimulus intensity array
          - spike_counts: Spike counts array of shape (t, n_cell)
          - n_repeat: Number of repetitions of the chirp stimulus
  
      return:
          - Dictionnary of cells response to the different ON or OFF stimuli
      """
  
      repeat = stim_inten.reshape(n_repeat,-1)[0]
      spike_counts = spike_counts.reshape(n_repeat,-1,spike_counts.shape[-1])
      epoch_l = [0]
      end_l = [len(repeat)]
      i = 1
      curr = repeat[0]
  
      while True:
          while repeat[i]==curr:
              i+=1
          epoch_l.append(i)
          curr = repeat[i]
          if curr==repeat[i+1]:
              continue
          else:
              break
  
      i = len(repeat)-2
      curr = repeat[-1]
  
      while True:
          while repeat[i]==curr:
              i-=1
          end_l.insert(0,i)
          curr = repeat[i]
          if curr==repeat[i-1]:
              continue
          else:
              break
      slices = [slice(epoch_l[i-1],epoch_l[i]) for i in range(1,len(epoch_l))]
      slices.extend([slice(end_l[i-1],end_l[i]) for i in range(1,len(end_l))])
  
      res_d = {}
      for slc in slices:
          key = str(stim_inten[slc.start])+"@"+str(slc.start)
          res_d[key] = spike_counts[:,slc]
  
      return res_d
  
  # Cell
  def get_repeat_corrected(stim_inten, spike_counts, n_repeats=10):
      """
      Apply shifts (detected during synchro) to the chirp repetition.
  
      params:
          - stim_inten: Stimulus DataChunk (containing the shifts and frame replacements info)
          - spike_counts: Spike count matrix of shape (t, n_cell)
          - n_repeats: Number of repeats of the chirp
  
      return:
          - aligned cells response to stimulus, of shape (n_repeat, t, n_cell)
          - Number of duplicated frame per repetition.
      """
      def count_repl_in_range(fr_replaced, _range):
          return sum([repl[0] in _range for repl in fr_replaced])
  
      signal_shifts     = stim_inten.attrs["signal_shifts"]
      frame_replacement = stim_inten.attrs["frame_replacement"]
  
      spike_count_corr = spike_counts.copy()
      shift_cursor = 0
      prev_del = np.zeros((1, spike_counts.shape[1]))
      for shift, direction in signal_shifts:
          if direction=="ins":
              spike_count_corr[shift+1:] = spike_count_corr[shift:-1]
              prev_del = spike_count_corr[-1:]
          else:
              spike_count_corr[shift-1:-1] = spike_count_corr[shift:]
              spike_count_corr[-1:] = prev_del
  
      len_epoch = len(stim_inten)//n_repeats
      spike_counts_corrected = []
      errors_per_repeat      = []
      for i in range(n_repeats):
          errors_per_repeat.append(count_repl_in_range(frame_replacement, range(len_epoch*i, len_epoch*(i+1))))
          spike_counts_corrected.append(spike_count_corr[len_epoch*i:len_epoch*(i+1)])
      return np.array(spike_counts_corrected), np.array(errors_per_repeat)
  
  # Cell
  def buszaki_shank_channels(channel_positions):
      """
      Group the channels of a Buzsaki32 silicone probe into their shanks
      from the channel position.
  
      params:
          - channel_positions: List of channel positions
  
      return:
          - array of grouped channel index of shape (n_shank(4), n_channel(8))
      """
      shank_1_mask = channel_positions[:,0]<180
      shank_1_idx  = np.argwhere(shank_1_mask)[:,0]
      shank_2_mask = (channel_positions[:,0]<380) & np.invert(shank_1_mask)
      shank_2_idx  = np.argwhere(shank_2_mask)[:,0]
      shank_4_mask = channel_positions[:,0]>580
      shank_4_idx  = np.argwhere(shank_4_mask)[:,0]
      shank_3_mask = (channel_positions[:,0]>380) & np.invert(shank_4_mask)
      shank_3_idx  = np.argwhere(shank_3_mask)[:,0]
  
      shanks_idx = np.zeros((4,8), dtype=int) - 1 #Initialize with -1 in case of channel missing
      shanks_idx[0,:len(shank_1_idx)] = shank_1_idx
      shanks_idx[1,:len(shank_2_idx)] = shank_2_idx
      shanks_idx[2,:len(shank_3_idx)] = shank_3_idx
      shanks_idx[3,:len(shank_4_idx)] = shank_4_idx
      return shanks_idx
  
  # Cell
  def format_pval(pval, significant_figures=2):
      """
      Helper function to format pvalue into string.
      """
      return '{:g}'.format(float('{:.{p}g}'.format(pval, p=significant_figures)))
  
  # Cell
  def stim_recap_df(reM):
      """
      Extract stimuli parameters (originally from the Database) to put them into a
      dataframe that will be displayed in the recapitulation plot.
  
      params:
          - reM: RecordMaster to extract stimuli parameters from
  
      return:
          - dataframe with the stimuli important informations
      """
      def parse_stim(stim_dc):
          param_d = {}
          param_d["hash"]        = stim_dc.attrs["md5"][:10] #the first 10 letters are more than enough
          param_d["n frames"]    = len(stim_dc)
          param_d["stimulus"]    = stim_dc.attrs["name"]
  
          if stim_dc.attrs["name"] in ["checkerboard", "fullfield_flicker", "flickering_bars", "flickering_bars_pr"]:
              param_d["frequency"] = stim_dc.attrs["refresh_rate"]
          elif stim_dc.attrs["name"] in ["chirp_am","chirp_fm","chirp_freq_epoch", "chirp_co"]:
              param_d["n ON"]      = int(stim_dc.attrs["tSteadyON_s"]*60)
              param_d["n OFF"]     = int(stim_dc.attrs["tSteadyOFF_s"]*60)
              param_d["n repeats"] = int(stim_dc.attrs["n_repeat"])
              if stim_dc.attrs["name"] in ["chirp_am","chirp_co"]:
                  param_d["frequency"] = stim_dc.attrs["contrast_frequency"]
              elif stim_dc.attrs["name"]=="chirp_fm":
                  param_d["frequency"] = stim_dc.attrs["max_frequency"]
              elif stim_dc.attrs["name"]=="chirp_freq_epoch":
                  param_d["frequency"] = str([round(60/nfr,2) for nfr in dc.attrs["n_frame_cycle"]])
          elif stim_dc.attrs["name"] in ["fullfield_color_mix"]:
              param_d["n ON"]      = int(stim_dc.attrs["n_frame_on"])
              param_d["n OFF"]     = int(stim_dc.attrs["n_frame_off"])
              param_d["n repeats"] = int(stim_dc.attrs["n_repeat"])
          elif stim_dc.attrs["name"]=="moving_gratings":
              param_d["n repeats"]           = stim_dc.attrs["n_repeat"]
              param_d["n ON"]                = stim_dc.attrs["n_frame_on"]
              param_d["n OFF"]               = stim_dc.attrs["n_frame_off"]
              param_d["speeds"]              = stim_dc.attrs["speeds"]
              param_d["spatial frequencies"] = stim_dc.attrs["spatial_frequencies"]
  
          if "frame_replacement" in stim_dc.attrs:
              param_d["total drop"] = len(stim_dc.attrs["frame_replacement"])
          if "signal_shifts" in stim_dc.attrs:
              shift = 0
              for _, which_shift in stim_dc.attrs["signal_shifts"]:
                  if which_shift=="ins":
                      shift += 1
                  elif which_shift=="del":
                      shift -= 1
              param_d["total shift"] = shift
  
          return param_d
  
      df = pd.DataFrame(columns=["stimulus", "hash", "n frames", "n repeats",
                                 "frequency", "n ON", "n OFF", "speeds", "spatial frequencies",
                                "total shift", "total drop"])
      cursor = 0
      for seq in reM._sequences:
          for k, dc_l in seq:
              dc = dc_l[0]
              if dc.group == "stim":
                  serie = pd.Series(data=parse_stim(dc), name=cursor)
                  df = df.append(serie, ignore_index=False)
                  cursor+=1
  
      df = df.fillna("")
      return df