18 août 2023

[Dynamo += Python] Pandas DataFrame extension

 




Étendre les capacités de Pandas avec les extensions
if this article is not in your language, use the Google Translate widget ⬈ (bottom of page for Mobile version )


Pandas, la bibliothèque Python populaire pour la manipulation et l'analyse de données, offre une grande flexibilité grâce à ses nombreuses fonctionnalités intégrées. Cependant, il y a des moments où vous pourriez souhaiter ajouter des fonctionnalités spécifiques à votre domaine.
C'est là que la fonction pd.api.extensions.register_dataframe_accessor entre en jeu.

pd.api.extensions.register_dataframe_accessor est une fonctionnalité avancée de Pandas qui vous permet d'ajouter des méthodes et des attributs personnalisés à un DataFrame.

Dans l'exemple ci-après, on va rajouter des méthodes pour afficher des Dataframes (ou graphes) au fur et à mesure de notre analyse. Ici, sera fait une simple analyse des tailles de chemin de câbles dans un projet.


Le moteur Python utilisé sera CPython3  (à ce jour ce moteur utilise PythonNet 2.5.x qui nécessite une implémentation assez particulière dès lors que l'on veut manipuler des objets .Net)

Note :
IronPython et PythonNet ont tous les deux des avantages, l'un est natif .Net l'autre rend possible l'utilisation de bibliothèques populaire Python. Autant profiter des 2.


Le script dynamo est constitué de 2 nœuds :


  •  un premier nœud Python qui affichera les DataTables et les Graphiques dans un Winform

    Note : l'affichage se fait au moyen d'un WebBrowser et la mise à jour du code html se fait par la mise à jour d'une propriété (getter et setter)
    La classe nécessite d'être une .Net Classe (voir documentation PythonNet)
    • Implémentation de l'attribut __namespace__
    • Modification de la propriété HtmlText via clrproperty (propriété accessible depuis le CLR)
  • un deuxième nœud Python où seront implémentées les méthodes d'extension Pandas

    • Une méthode  Show()  qui prend en arguments le nombre de lignes à afficher (si égal à 0  affiche tout le Dataframe).
      La méthode ne s'applique que sur un objet Dataframe, si vous avez un objet Series, il faudra au préalable le convertir avec la méthode to_frame()

    • Une méthode Plot() pour tracer quelques graphiques. Afin d'éviter de créer une méthode d'extension pour chaque méthode, celle-ci prend en argument le type de méthode Dataframe.plot sous forme de chaine de caractères.




Exemple d'arguments pour la méthode d'extension Plot




On pourrait ainsi ajouter autant de méthodes d'extension que l'on souhaite, pourquoi pas une méthode Save() pour sauvegarder le contenu HTML dans un fichier 💾.


Notes Importantes
A contrario d'une .Net DataTable (Microsoft.Data) ou d'un .Net DataFrame (Microsoft.Data.Analysis) un Dataframe pandas ne supporte pas complètement les objets .Net.
En conséquence, avant toutes opérations de type groupby ou autre, il sera nécessaire de supprimer les colonnes comportant des objets .Net pour éviter les erreurs (via un drop).




code python "dataframe extension"
MAJ 30/04/2024 code réunis dans un seul nœud python CPython3
https://gist.github.com/Cyril-Pop/0fa9c7465632fd78395ad203f6668ffc
.
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 specify namespace
from Autodesk.Revit.DB.Electrical import *
from Autodesk.Revit.DB.Structure import *
#import net library
from System import Array
from System.Collections.Generic import List, IList, Dictionary
#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
my_path = System.Environment.GetFolderPath(System.Environment.SpecialFolder.MyDocuments)
pf_path = System.Environment.GetFolderPath(System.Environment.SpecialFolder.ProgramFilesX86)
clr.AddReference('Python.Included')
import Python.Included as pyInc
path_py3_lib = pyInc.Installer.EmbeddedPythonHome
sys.path.append(path_py3_lib + r'\Lib\site-packages')
import numpy as np
import pandas as pd
import webbrowser
import matplotlib.pyplot as plt
import io
import base64
import re
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 *
class MyForm(Form):
#
# __namespace__ = f"MyForm_{int(time.time())}"
def __init__(self):
super().__init__()
self.init_htmlDf = """\
<html>
<head>
<meta http-equiv="X-UA-Compatible" content="IE=9">
<style>
table, th, td {font-size:10pt; border:1px solid black; border-collapse:collapse; text-align:left;}
th, td {padding: 5px;}
tr:nth-child(even) {background: #E0E0E0;}
tr:hover {background: silver; cursor: pointer;}
</style>
</head>
<body>
</body>
</html>
"""
self.htmlDf = self.init_htmlDf
self.InitializeComponent()
def InitializeComponent(self):
self._webBrowser1 = System.Windows.Forms.WebBrowser()
self.SuspendLayout()
#
# webBrowser1
#
self._webBrowser1.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right
self._webBrowser1.Dock = System.Windows.Forms.DockStyle.Fill
self._webBrowser1.Location = System.Drawing.Point(0, 0)
self._webBrowser1.ScriptErrorsSuppressed = True
self._webBrowser1.Name = "webBrowser1"
self._webBrowser1.Size = System.Drawing.Size(594, 528)
self._webBrowser1.TabIndex = 0
self._webBrowser1.DocumentText = self.htmlDf
self._webBrowser1.DocumentCompleted += self.WebBrowser1DocumentCompleted
#
# Form27
#
self.ClientSize = System.Drawing.Size(600, 540)
self.Controls.Add(self._webBrowser1)
self.SizeGripStyle = System.Windows.Forms.SizeGripStyle.Show
self.SizeChanged += self.FormSizeChanged
self.Name = "Form27"
self.Text = "PandasViewer"
def WebBrowser1DocumentCompleted(self, sender, e):
#self._webBrowser1.Size = self._webBrowser1.Document.Body.ScrollRectangle.Size
self._webBrowser1.Document.Body.ScrollIntoView(False)
def FormSizeChanged(self, sender, e):
self._webBrowser1.Size = System.Drawing.Size.Subtract(self.ClientSize, System.Drawing.Size(10, 10))
self._webBrowser1.Document.Body.Width = self.ClientSize.Width - 10
#@property
def get_HtmlText(self):
#print(self.htmlDf)
return self.htmlDf
#@HtmlText.setter
def set_HtmlText(self, value):
self.htmlDf = value
self._webBrowser1.Document.OpenNew(True)
self._webBrowser1.Document.Write(self.htmlDf)
self._webBrowser1.Refresh()
#self.Activate()
HtmlText = clr.clrproperty(str, get_HtmlText, set_HtmlText)
def ResetHtml(self):
self.htmlDf = self.init_htmlDf
@pd.api.extensions.register_dataframe_accessor("viewer")
class HtmlAccessor:
def __init__(self, df_obj):
self._obj = df_obj
#
def Show(self, n=0):
"""
convert DataFrame to Html and show it
"""
# get current html
htmlDf = viewerForm.htmlDf
# convert current dataFrame to html
if n == 0:
html_toAdd = self._obj.to_html()
else:
html_toAdd = self._obj.head(n).to_html()
# add to the current html
htmlDf = re.sub(r"<\/body>", html_toAdd + r"<br><hr><br></body>", htmlDf)
# update the html in form
viewerForm.HtmlText = htmlDf
def Plot(self, method="plot()"):
"""
plot a DataFrame
type = 'bar', 'area', 'box',
"""
plt.figure()
s = io.BytesIO()
if method.startswith("."):
method = method[1:]
if not method.startswith("plot"):
method = "plot." + method
#
ax = eval("self._obj."+ method)
#
plt.savefig(s, format='png', bbox_inches="tight")
plt.close()
s = base64.b64encode(s.getvalue()).decode("utf-8").replace("\n", "")
#
htmlDf = viewerForm.htmlDf
html_toAdd = """<img align="left" src="data:image/png;base64,{0}">""".format(s)
#
# add to the current
htmlDf = re.sub(r"<\/body>", html_toAdd + r"<br clear=left><hr><br></body>", htmlDf)
#
# update the html in form
viewerForm.HtmlText = htmlDf
import gc
objects =gc.get_objects()
viewerForm = next((obj for obj in objects if isinstance(obj, MyForm) and obj.Controls.Count > 0 and all(not x.IsDisposed for x in obj.Controls)), None)
if viewerForm is None:
viewerForm = MyForm()
else:
viewerForm.ResetHtml()
viewerForm.Show()
all_cable_tray = FilteredElementCollector(doc).OfCategory(BuiltInCategory.OST_CableTray).WhereElementIsNotElementType().ToElements()
df = pd.DataFrame({'DB_Element': all_cable_tray})
#
df['Name'] = df.apply(lambda x: x['DB_Element'].get_Name(), axis=1)
#
df['Service'] = df.apply(lambda x: x['DB_Element'].get_Parameter(BuiltInParameter.RBS_CTC_SERVICE_TYPE).AsValueString(), axis=1).astype(str)
#
df['Hauteur'] = df.apply(lambda x: x['DB_Element'].get_Parameter(BuiltInParameter.RBS_CABLETRAY_HEIGHT_PARAM).AsValueString(), axis=1).astype(str)
#
df['Largeur'] = df.apply(lambda x: x['DB_Element'].get_Parameter(BuiltInParameter.RBS_CABLETRAY_WIDTH_PARAM).AsValueString(), axis=1).astype(str)
#
df['Longueur'] = df.apply(lambda x: x['DB_Element'].get_Parameter(BuiltInParameter.CURVE_ELEM_LENGTH).AsValueString(), axis=1).apply(pd.to_numeric)
#
df.viewer.Show()
# show only 5 first rows
df.viewer.Show(5)
df.dtypes.to_frame().viewer.Show()
# describe
dfdes = df.describe()
# show describe
dfdes.viewer.Show()
# show transpose dscribe
dfdes.T.viewer.Show()
##
# grouby and sum by 'Service'
dfg = df.drop(['DB_Element'], axis=1)
dfg = dfg.groupby('Service')['Longueur'].sum()
dfg.to_frame().viewer.Show()
# grouby and sum by 'Service' and 'Largeur'
dfg2 = df.drop(['DB_Element'], axis=1)
dfg2 = dfg2.groupby(['Service', 'Largeur']).sum()
dfg2.viewer.Show()
###
dfg.to_frame().viewer.Plot()
dfg.to_frame().viewer.Plot("plot.bar(rot=0)")
#dfg.to_frame().viewer.Plot("plot.pie()")
dfg.to_frame().viewer.Plot("plot.pie(subplots=True, figsize=(11, 6))")
df.viewer.Plot("box()")
OUT = df.__repr__ , df.dtypes

Exemple en vidéo 



Ressources
  • documentation Pandas - lien 
  • script Dynamo (dyn) - lien


"Tout ce qui est impossible reste à accomplir". (Jules Verne)



Related Posts:

0 commentaires:

Enregistrer un commentaire