25 juin 2023

[Dynamo+=Python] Les DataTables (Chapitre 1 - Construction)






Une Introduction sur les DataTables de l'API Microsoft et comment les manipuler avec Python

#DataTable #RevitAPI #DynamoBIM #Datas
if this article is not in your language, use the Google Translate widget ⬈ (bottom of page for Mobile version )



L'objet DataTable fait partie de l'API Microsoft et est utilisé pour représenter et manipuler des données tabulaires dans une structure en mémoire. Il s'agit d'une classe puissante et polyvalente qui offre des fonctionnalités pour la création, la modification et la manipulation de données tabulaires.

Une DataTable est composée de colonnes et de lignes. Chaque colonne est définie par son nom et son type de données, tel que texte, nombre, date, etc. Les lignes représentent les enregistrements individuels contenant les données dans chaque colonne.

Voici quelques caractéristiques importantes de l'objet DataTable :

  • Structure de colonnes : Vous pouvez définir les colonnes d'une DataTable avec des noms spécifiques et des types de données appropriés. Cela garantit que les données sont organisées de manière cohérente et permet des opérations plus efficaces sur les données.

  • Ajout et suppression de données : Vous pouvez ajouter ou supprimer des lignes dans une DataTable pour mettre à jour les données. Cela peut être fait individuellement pour chaque ligne ou en bloc pour plusieurs lignes à la fois.

  • Requêtes et filtrage : L'objet DataTable prend en charge des opérations de filtrage et de requêtes pour extraire des sous-ensembles de données spécifiques. Vous pouvez utiliser des expressions LINQ (Language Integrated Query) ou des méthodes intégrées pour effectuer des opérations de recherche, de tri et de filtrage sur les données.

  • Relation entre les tables : Vous pouvez également définir des relations entre différentes DataTables en utilisant des clés primaires et des clés étrangères. Cela permet d'établir des liens entre les données de différentes tables et facilite les opérations de jointure et de fusion.

  • Sérialisation et dé sérialisation : Les DataTables peuvent être facilement sérialisées dans un format tel que XML ou JSON, ce qui permet de les transmettre via des services Web ou de les enregistrer dans des fichiers. De plus, elles peuvent être désérialisées pour récupérer les données et les reconstituer en tant qu'objet DataTable.

  • Plusieurs DataTable peuvent etre contenus dans un objet DataSet

Pour les exemples, nous utiliserons ici IronPython (2 ou 3) avec de profiter de l'extension System.Data.DataTableExtensions


Exemple N°1 : construction d'une DataTable


  1. création d'une DataTable  avec le constructeur DataTable()

  2. ajout des colonnes en spécifiant le Nom et le Type (la spécification du Type est optionnel) avec la méthode DataTable.Columns.Add()

  3. Puis, on ajoute les lignes avec DataTable.Rows.Add() en passant en argument autant de valeurs que de colonnes 

  4. Exemple de rajout d'une ligne en haut de la Table (index 0)



import clr
import sys
import System
clr.AddReference("System.Numerics")
#
clr.AddReference('ProtoGeometry')
from Autodesk.DesignScript.Geometry import *
import Autodesk.DesignScript.Geometry as DS

#import Revit API
clr.AddReference('RevitAPI')
import Autodesk
from Autodesk.Revit.DB import *
import Autodesk.Revit.DB as DB

#import net library
from System import Array
from System.Collections.Generic import List, IList, Dictionary


clr.AddReference('System.Data')
from System.Data import *
clr.AddReference('System.Data.DataSetExtensions')
clr.ImportExtensions(System.Data.DataTableExtensions)


#import transactionManager and DocumentManager (RevitServices is specific to Dynamo)
clr.AddReference('RevitServices')
import RevitServices
from RevitServices.Persistence import DocumentManager
from RevitServices.Transactions import TransactionManager

doc = DocumentManager.Instance.CurrentDBDocument

clr.AddReference("System.Core")
clr.ImportExtensions(System.Linq)	
		
all_spaces = FilteredElementCollector(doc).OfCategory(BuiltInCategory.OST_MEPSpaces).WhereElementIsNotElementType().ToElements()

dt = DataTable("Spaces_DataTable")
dt.Columns.Add('Name', System.String)
dt.Columns.Add('Number', System.String)
dt.Columns.Add('DB_Element', DB.Element)
for e in all_spaces[:-1]:
	if e.Number.isdigit():
		dt.Rows.Add(e.Name, e.Number , e)
#
# Example to add a Row at Index
# we get the last space and insert at the top (index 0)
row = dt.NewRow();
row["Name"] = all_spaces[-1].Name
row["Number"] = all_spaces[-1].Number
row["DB_Element"] = all_spaces[-1]
dt.Rows.InsertAt(row, 0)


Si l'on passe l'objet DataTable dans un DataGridView (Winform) ou DataGrid (WPF) voici ce que l'on obtient




À titre d'information, voici la même opération avec un DataFrame Python (moteur CPython3)

import numpy as np
import pandas as pd
    
all_spaces = FilteredElementCollector(doc).OfCategory(BuiltInCategory.OST_MEPSpaces).WhereElementIsNotElementType().ToElements()

data ={}
data["Name"] = [x.get_Name() for x in all_spaces]
data["Number"] = [x.Number for x in all_spaces]
data["DB_Elements"] = all_spaces

df = pd.DataFrame.from_dict(data)


Exemple N°2 :  suppression de Lignes, ajout de colonnes et ajout d'un index

  • Suppression des lignes en double 
  • Ajout des colonnes Area, Volume, LevelName par extraction des propriétés des Espaces 
  • Ajout d'une colonne calculée en se basant sur 2 autres colonnes
  • Définir une colonne comme clé (index)

À titre de comparaison pour chaque type d'opération est rajouté un exemple avec un DataFrame pandas

# remove dulpicate Rows
dt = dt.DefaultView.ToTable(True)
# example with Pandas
# df = df.drop_duplicates()
# 
# Add columns with DB_Element property
dt.Columns.Add('Area', System.Double)
dt.Columns.Add('Volume', System.Double)
dt.Columns.Add('LevelName', System.String)
for row in dt.Rows:
	row['Area'] = round(row['DB_Element'].Area, 2) * 0.092903 # convert to m2
	row['Volume'] = round(row['DB_Element'].Volume, 2) * 0.0283168 # convert to m3
	row['LevelName'] = row['DB_Element'].Level.Name
#
# add a calculated Column
dt.Columns.Add('Height', System.String, "Volume / Area")
# example with Pandas
# df['Area'] = df.apply(lambda x: x['DB_Element'].Area  * 0.092903, axis=1)
# df['Volume'] = df.apply(lambda x: x['DB_Element'].Volume  * 0.0283168, axis=1)
# df['LevelName'] = df.apply(lambda x: x['DB_Element'].Level.Name, axis=1)
# df['Height'] = df['Volume'] / df['Area']

# set Number Column as Key
keys = System.Array[DataColumn]([DataColumn()])
keys[0] = dt.Columns["Number"]
dt.PrimaryKey = keys
# # similar example with pandas
# df.set_index("Number")


Exemple N°3 :  trier les lignes et redéfinir les colonnes

    
    # Sort rows
    dataView = dt.DefaultView
    dataView.Sort = "LevelName, Number"
    # with pandas DataFrame
    # df = df.sort_values(['LevelNameY', 'Number'])
    # Set New Order of Columns
    dt = dataView.ToTable(True, "Number", "Name", "Area", "LevelName")
    # with pandas DataFrame
    # df.loc[:,["Number", "Name", "Area", "LevelName"]]
    


    Exemple N°4 :  rechercher des valeurs dans cette Table de Données


    • trouver l'index de la colonne qui sert de clé
    
    # check the column index of primary key
    indexKeyColumn = dt.PrimaryKey[0].Ordinal
    
    • trouver une valeur suivant l'index de ligne et l'index de colonne
    
    # find value by indexes
    value_2_5 = dt.Rows[2][5]
    
    • trouver une valeur suivant le nom de la clé (ligne) et le nom de la colonne
    
    # Find Row by PrimaryKey 
    dtRowB = dt.Rows.Find("115")
    # find the number of row
    indexRowB = dt.Rows.IndexOf(dtRowB)
    # Find value PrimaryKey Row and column name
    volume_115 = dt.Rows.Find("115")["Area"]
    

    Exemple N°5 :  Itérer sur une DataTable

    
    for idxRow, row in enumerate(dt.Rows):
        for idxCol, col in enumerate(dt.Columns):
            xls_value = row[col] # or row[col.ColumnName] 

    Exemple N°6 : supprimer des colonnes

    
    # remove a columns
    dt.Columns.Remove("columnName")
    # or
    dt.Columns.Remove(datacolumn)
    # or 
    dt.Columns.RemoveAt(columnIndex)
    # remove several columns by set columns to keep
    # initial Columns "columnNameA", "columnNameB", "columnNameC", "columnNameD", "columnNameE", 
    dataView = dt.DefaultView
    dt = dataView.ToTable(True, "columnNameA", "columnNameB", "columnNameC")
    


    Exemple N°7 : Ajout de Méthodes sur une DataTable


    Comme la classe DataTable possède un constructeur, nous pouvons construire une nouvelle classe qui hérite de celle-ci pour y ajouter de nouvelles méthodes :

    • convertir un fichier csv en DataTable → FromCSVFile()
    • afficher le contenu d'une DataTable → Show()
    • grouper les lignes suivant les valeurs d'une colonne
      et sommer les valeurs des autres colonnes →  GroupSumByField()
    • Obtenir les valeurs d'une colonne →  ColumnValues()


    
    import clr
    import sys
    import re
    pyEngineName = sys.implementation.name 
    import System
    clr.AddReference("System.Numerics")
    
    # Import .NET libraries
    from System import Array
    from System.Collections.Generic import List, IList, Dictionary
    
    # Add references to required .NET assemblies
    clr.AddReference('System.Drawing')
    clr.AddReference('System.Windows.Forms')
    import System.Drawing
    import System.Windows.Forms
    
    from System.Drawing import *
    from System.Windows.Forms import *
    
    clr.AddReference('System.Data')
    from System.Data import *
    clr.AddReference('System.Data.DataSetExtensions')
    clr.ImportExtensions(System.Data.DataTableExtensions)
    
    clr.AddReference("System.Core")
    clr.ImportExtensions(System.Linq)
    import csv
    
    class FormDataTable(Form):
        """A Windows Form for displaying a DataTable"""
        def __init__(self, datatable, title=""):
            """
            Initialize the FormDataTable instance.
            
            Args:
                datatable (DataTable): The DataTable to display.
                title (str, optional): The title of the form. Defaults to an empty string.
            """
            self.datatable = datatable
            self.title = title
            self.InitializeComponent()
    
        def InitializeComponent(self):
            """Initialize the components of the FormDataTable"""
            self._dataGridView1 = System.Windows.Forms.DataGridView()
            self._dataGridView1.BeginInit()
            # Initialize DataGridView
            self.SuspendLayout()
            self._dataGridView1.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right
            self._dataGridView1.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize
            self._dataGridView1.Location = System.Drawing.Point(12, 33)
            self._dataGridView1.Name = "dataGridView1"
            self._dataGridView1.DataSource = self.datatable
            self._dataGridView1.Size = System.Drawing.Size(523, 467)
            self._dataGridView1.TabIndex = 0
            self._dataGridView1.DataBindingComplete += self.setRowNumber
            
            # Initialize Form
            self.ClientSize = System.Drawing.Size(547, 512)
            self.Controls.Add(self._dataGridView1)
            self.Name = "Form27"
            self.Text = self.title
            self._dataGridView1.EndInit()
            self.ResumeLayout(False)
    
        def setRowNumber(self, sender, e):
            """
            Set the row numbers for each row in the DataGridView.
    
            Args:
                sender: The sender object.
                e: The event arguments.
            """
            # Add index to each row
            for row in sender.Rows:
                row.HeaderCell.Value = System.String.Format("{0}", row.Index)
    
    class CustomDataTable(DataTable):
        """A custom DataTable class with additional functionalities"""
        # FOR PYTHON 2
        #def __new__(cls):
        #    return super(DataTable, cls).__new__(cls)
        #
        # OR 
        # FOR PYTHON 3 (Ironpython or PythonNet)
        def __init__(self):
            super().__init__()
            
        def __repr__(self):
            lst_size = []
            for col in self.Columns:
                max_size_txt = max([len(row[col]) for row in self.Rows])
                lst_size.append(max_size_txt)
            value = '\t'.join(['{0:{1}}'.format(col.ColumnName, lst_size[idx]) for idx, col in enumerate(list(self.Columns))]) + "\n"
            value += '\n'.join(['\t'.join(['{0:{1}}'.format(x, lst_size[idx]) for idx, x in enumerate(r.ItemArray)]) for r in self.Rows])
            return value
    
        @staticmethod
        def FromCsvFile(fullPathCsv):
            """
            Create a CustomDataTable instance from a CSV file.
    
            Args:
                fullPathCsv (str): The full path to the CSV file.
    
            Returns:
                CustomDataTable: The created CustomDataTable instance.
            """
            fileName = System.IO.Path.GetFileNameWithoutExtension(fullPathCsv)
            data = []
            with open(fullPathCsv, newline='') as csvfile:
                reader = csv.reader(csvfile, delimiter=',')
                data.extend([row for row in reader])
            header_array = data.pop(0)
            dt = CustomDataTable()
            dt.TableName = fileName
            
            # Create columns
            for item, value in zip(header_array, data[0]):
                if isinstance(value, (str, System.String)) and value.isdigit():
                    type_value = System.Int32
                elif isinstance(value, (str, System.String)) and re.match(r"\d+[\.,]\d+$", value) is not None:
                    type_value = System.Double
                else:
                    type_value = type(value)
                dt.Columns.Add(item, type_value)
            
            # Add rows
            for sublst_values in data:
                a = Array.CreateInstance(System.Object, len(sublst_values))
                for i, val in enumerate(sublst_values):
                    a[i] = val
                dt.Rows.Add(a)
            
            return dt
    
        def GroupSumByField(self, name_field):
            """
            Group the DataTable by a field and calculate the sum for numeric columns.
    
            Args:
                name_field (str): The name of the field to group by.
    
            Returns:
                CustomDataTable: The new CustomDataTable with grouped and summed values.
            """
            def SumFunc(g):
                row = self.NewRow()
                row[name_field] = g.Key
                for col in self.Columns:
                    if col.ColumnName != name_field:
                        if any(col.DataType == typ for typ in [System.Int32, System.Int64, System.Double, System.Numerics.BigInteger, int, float]):
                            row[col.ColumnName] = sum([r[col.ColumnName] for r in g])
                return row
            
            new_dt = CustomDataTable()
            for col in self.Columns:
                new_dt.Columns.Add(col.ColumnName, col.DataType)
            
            List[DataRow](self.AsEnumerable().GroupBy(lambda g : g[name_field]).Select(lambda g : SumFunc(g))).CopyToDataTable(new_dt, LoadOption.Upsert)
            return new_dt
        
        def Show(self):
            """Display the DataTable in a FormDataTable instance"""
            objForm = FormDataTable(self)
            objForm.ShowDialog()
            objForm.Dispose()
    
        def ColumnValues(self, columnName):
            """
            Get an array of column values for a given column name.
    
            Args:
                columnName (str): The name of the column.
    
            Returns:
                Array: An array of column values.
            """
            return self.AsEnumerable().Select(lambda s : s[columnName]).ToArray[System.Object]()
    
    
    csv_file_path = IN[0]
    
    dt = CustomDataTable.FromCsvFile(csv_file_path)
    dt.Show()
    ddt = dt.GroupSumByField("year")
    ddt.Show()
    OUT = ddt.ColumnValues("year"), ddt.ColumnValues("winner")
    


    DataTable d'origine (lecture du csv)



    DataTable après l'application de la méthode GroupSumByField


    Valeurs des Colonnes appliquées au nœud Dynamo Bar Chart




    0 commentaires:

    Enregistrer un commentaire