class_docstr: An ``ElementData`` stores "shared" information about a set of a graph elements (nodes or
  edges). Elements of every type must have this information, such as the type itself or the
  source, target and weight for edges.
  
  It indexes these in terms of ilocs (see :class:`.ExternalIdIndex`). The data is stored as columns
  of raw numpy arrays, because indexing such arrays is significantly (orders of magnitude) faster
  than indexing pandas dataframes, series or indices.
  
  Args:
      ids (sequence): the IDs of each element
      type_info (list of tuple of type name, numpy array): the associated feature vectors of each type, where the size of the first dimension defines the elements of that type
class_docstr_tok: ["An","``","ElementData","``","stores","``","shared","''","information","about","a","set","of","a","graph","elements","(","nodes","or","edges",")",".","Elements","of","every","type","must","have","this","information",",","such","as","the","type","itself","or","the","source",",","target","and","weight","for","edges",".","It","indexes","these","in","terms","of","ilocs","(","see",":","class",":","`",".ExternalIdIndex","`",")",".","The","data","is","stored","as","columns","of","raw","numpy","arrays",",","because","indexing","such","arrays","is","significantly","(","orders","of","magnitude",")","faster","than","indexing","pandas","dataframes",",","series","or","indices",".","Args",":","ids","(","sequence",")",":","the","IDs","of","each","element","type_info","(","list","of","tuple","of","type","name",",","numpy","array",")",":","the","associated","feature","vectors","of","each","type",",","where","the","size","of","the","first","dimension","defines","the","elements","of","that","type"]
code: def __init__(self, ids, type_info):
      if not isinstance(type_info, list):
          raise TypeError(
              f"type_info: expected list, found {type(type_info).__name__}"
          )
  
      type_ranges = {}
      features = {}
      all_types = []
      type_sizes = []
  
      rows_so_far = 0
  
      # validation
      for type_name, data in type_info:
          if not isinstance(data, np.ndarray):
              raise TypeError(
                  f"type_info (for {type_name!r}): expected numpy array, found {type(data).__name__}"
              )
  
          if len(data.shape) < 2:
              raise ValueError(
                  f"type_info (for {type_name!r}): expected at least 2 dimensions, found {len(data.shape)}"
              )
  
          rows = data.shape[0]
          start = rows_so_far
  
          rows_so_far += rows
          stop = rows_so_far
  
          all_types.append(type_name)
          type_sizes.append(stop - start)
          type_ranges[type_name] = range(start, stop)
          features[type_name] = data
  
      if rows_so_far != len(ids):
          raise ValueError(
              f"type_info: expected features for each of the {len(ids)} IDs, found a total of {rows_so_far} features"
          )
  
      self._id_index = ExternalIdIndex(ids)
  
      # there's typically a small number of types, so we can map them down to a small integer type
      # (usually uint8) for minimum storage requirements
      self._type_index = ExternalIdIndex(all_types)
      self._type_column = self._type_index.to_iloc(all_types).repeat(
          type_sizes
      )
      self._type_element_ilocs = type_ranges
  
      self._features = features
  
imports: import itertools
  from collections import defaultdict
  import numpy
  import pandas
  import scipy.sparse
  from ..globalvar import SOURCE, TARGET, WEIGHT, TYPE_ATTR_NAME
  from .validation import require_dataframe_has_columns, comma_sep
label_desc: Use image node_id 1 to create a new ElementData object  with example: obj = ElementData(ids, type_info)