Dans le paradigme du calcul scientifique moderne et de l’ingénierie des données sur Python, l’écosystème articulé autour de la bibliothèque NumPy constitue le socle fondamental sur lequel reposent l’apprentissage automatique, la modélisation statistique et le traitement du signal. En déléguant les opérations computationnelles intensives à des routines sous-jacentes compilées en C et en Fortran, cette bibliothèque confère au développeur une vitesse d’exécution proche du métal tout en préservant l’expressivité et la concision syntaxique d’un langage interprété de haut niveau. Néanmoins, cette symbiose entre la flexibilité dynamique de Python et la rigueur statique du monde compilé engendre inévitablement des frictions conceptuelles lorsque les structures de données manipulées enfreignent les postulats d’homogénéité et de dimensionnalité imposés par l’architecture mémoire du moteur numérique.
Parmi les anomalies d’exécution récurrentes qui entravent le bon déroulement des flux de traitement tensoriel, l’exception ValueError: setting an array element with a sequence figure sans conteste parmi les plus emblématiques et les plus déroutantes pour les praticiens de la science des données. Surgissant fréquemment lors d’opérations d’affectation indicée, d’assemblage matriciel hétérogène ou d’inférence de formes au sein de vecteurs multidimensionnels, cette erreur matérialise une inadéquation structurelle fondamentale. Elle signale une rupture de contrat sémantique : le moteur d’exécution NumPy tente d’insérer une collection ordonnée d’éléments au sein d’une case mémoire élémentaire conçue exclusivement pour héberger une valeur scalaire atomique indivisible.
L’ambition de cette étude exhaustive est de disséquer méticuleusement les origines, la mécanique interne et les protocoles de résolution de cette exception critique. À travers une analyse croisant théorie des systèmes, agencement bas niveau des zones de mémoire tampon et ingénierie logicielle avancée, cet article propose une grille de lecture complète permettant non seulement de corriger l’anomalie dans des scénarios de production réels, mais également d’adopter des patrons d’architecture logicielle défensive afin de prémunir durablement les pipelines analytiques contre toute instabilité dimensionnelle.
1. Introduction et fondements théoriques de l’exception dans NumPy
1.1 Définition formelle de l’exception ValueError en Python
Dans l’architecture du langage Python, les anomalies d’exécution sont structurées selon une hiérarchie stricte de classes dérivant de l’ancêtre commun BaseException. Au sein de cette arborescence formelle, la classe ValueError hérite directement de Exception et possède un champ sémantique très précis : elle est levée lorsqu’une opération ou une fonction intégrée reçoit un argument dont le type de données est théoriquement valide, mais dont la valeur concrète ou la forme intrinsèque s’avère inappropriée pour l’exécution du calcul. Cette définition la distingue fondamentalement de TypeError, qui intervient dès lors qu’un objet d’un type non supporté est transmis à une fonction ou à un opérateur non compatible avec sa classe d’appartenance.
Pour appréhender cette frontière conceptuelle, considérons l’opération d’affectation au sein d’une structure de conteneur. Si un développeur tente de passer un objet dépourvu d’interfaces d’indexation à une fonction attendant un entier de position, une exception de type TypeError est immédiatement émise par l’interpréteur CPython en raison de l’inadéquation des protocoles d’accès. En revanche, lorsque le protocole d’assignation __setitem__ est sollicité sur un tableau NumPy, le type de l’opération est syntaxiquement irréprochable et le conteneur cible accepte formellement des modifications dynamiques de son contenu. L’anomalie surgit au niveau de la charge utile : la valeur fournie se révèle incapable de se conformer aux spécifications spatiales de l’emplacement récepteur.
Cette distinction s’avère particulièrement subtile dans le cadre des tableaux multidimensionnels. L’opérateur d’assignation reconnaît que l’argument transmis est une structure itérable valide en Python, mais l’analyse de son contenu révèle une incompatibilité structurelle avec les axiomes de la cellule réceptrice. Le système se trouve ainsi confronté à une impasse géométrique et logique : il ne peut ni dégrader silencieusement la dimensionnalité de la séquence entrante, ni redimensionner dynamiquement la mémoire tampon sous-jacente sans compromettre l’intégrité globale de la matrice. L’émission d’une ValueError devient par conséquent l’unique issue déterministe pour interrompre le flux d’instructions et signaler la violation des invariants de calcul.
1.2 Architecture mémoire et homogénéité des structures NumPy
Pour comprendre la genèse intime de ce conflit, il est indispensable de se pencher sur la topologie bas niveau de l’objet central de NumPy : le ndarray. Contrairement aux listes natives de Python, qui sont en réalité des tableaux de pointeurs vers des structures PyObject dispersées dans le tas de mémoire (heap), un ndarray est constitué d’un segment contigu d’octets bruts alloués d’un seul tenant en mémoire vive. Cette mémoire tampon continue est régie par deux grandes conventions d’organisation spatiale : l’ordre C (C-contiguous ou row-major), où les éléments d’une même ligne sont placés consécutivement en mémoire, et l’ordre Fortran (Fortran-contiguous ou column-major), où les colonnes occupent des adresses séquentielles.
L’efficacité spectaculaire des opérations vectorisées repose intégralement sur ce principe d’invariance spatiale et sur la contrainte absolue d’homogénéité. Chaque cellule d’un tableau est soumise à une règle d’atomicité stricte : elle doit posséder une taille en octets rigoureusement identique et invariable, définie par le descripteur de type de données, ou dtype. Par exemple, pour un tableau configuré en float64, chaque élément occupe exactement 8 octets (64 bits) consécutifs. Le calcul de l’adresse mémoire physique d’un élément d’indices donnés s’effectue alors via une simple fonction arithmétique affine exploitant les pas d’avancement (strides), sans nécessiter aucune déréférence de pointeur.
Cette rigueur architecturale implique qu’un élément de tableau, au sens topologique d’un indice scalaire, est un réceptacle fixe doté d’une empreinte spatiale immuable. Il ne possède aucun mécanisme élastique lui permettant de s’étendre pour loger un conteneur composé de multiples entités numériques. Si l’on demande au moteur C de NumPy d’écrire une séquence de plusieurs nombres à l’adresse mémoire correspondant à une seule cellule atomique, ce dernier refuse d’effectuer une opération qui entraînerait inévitablement un écrasement de mémoire adjacent (buffer overflow) ou une corruption systémique des données indexées par les cellules contiguës.
1.3 Impact de l’erreur dans les pipelines de calcul scientifique
Dans un contexte industriel ou au sein d’un laboratoire de recherche, la survenue imprévue de l’exception ValueError: setting an array element with a sequence constitue un facteur d’interruption particulièrement coûteux. Lorsqu’un algorithme de traitement par lots ingère des téraoctets de données issues de capteurs, d’enregistrements financiers ou d’imagerie médicale, l’interruption brutale d’une chaîne de calcul vectorisé après plusieurs heures d’exécution entraîne un gaspillage considérable de ressources computationnelles et de temps machine.
Au-delà de l’arrêt net des calculs, cette erreur compromet directement la reproductibilité des simulations quantitatives. Elle se manifeste fréquemment de manière intermittente, n’apparaissant que lorsqu’un échantillon atypique au sein d’un jeu de données volumineux présente une dimensionalité irrégulière — par exemple, une mesure vectorielle isolée là où un scalaire était historiquement attendu. Dans les systèmes distribués ou les architectures de microservices de machine learning, cette variabilité structurelle peut paralyser des processus entiers d’entraînement ou d’inférence en temps réel.
Le diagnostic de cette anomalie est en outre complexifié lorsqu’elle se produit en cascade à l’intérieur de bibliothèques tierces hautement spécialisées telles que SciPy, Scikit-Learn ou PyTorch. Au sein de ces cadriciels, les fonctions d’optimisation non linéaire, d’estimation bayésienne ou d’ajustement de modèles délèguent continuellement des calculs matriciels sous-jacents à NumPy. Si une fonction de coût personnalisée renvoie inopinément un vecteur au lieu d’une valeur scalaire lors d’une itération d’optimisation, l’exception est émise au cœur des routines internes du solveur. Le message d’erreur et la trace d’appels qui en résultent masquent alors fréquemment l’origine véritable du dysfonctionnement, renvoyant l’ingénieur à des modules d’algèbre linéaire complexes plutôt qu’à sa propre fonction d’évaluation.
2. Anatomie technique de l’erreur : décryptage du message d’erreur
2.1 Analyse lexicale de la formulation de l’erreur
L’énoncé textuel émis par l’interpréteur — ValueError: setting an array element with a sequence — se caractérise par une précision chirurgicale qui mérite une déconstruction linguistique et sémantique rigoureuse. Chaque terme employé renvoie à une entité formellement spécifiée au sein du modèle d’exécution de Python et de l’architecture objet de NumPy.
L’expression setting an array element désigne expressément l’action d’assigner une nouvelle valeur à un emplacement indicé unique au sein d’une structure de tableau préexistante. Dans la syntaxe Python, cette opération correspond formellement à une affectation indicée unitaire de la forme tableau[i] = valeur ou tableau[i, j] = valeur, dans laquelle le jeu d’indices spécifié cible de manière non ambiguë une cellule terminale scalaire du tenseur. Il ne s’agit donc pas d’une tentative de concaténation globale, ni d’un redimensionnement matriciel, mais bien d’une écriture ciblée au niveau d’un slot individuel.
À l’opposé, le terme sequence fait référence, selon la terminologie officielle du protocole de collection de Python, à tout objet conteneur linéaire implémentant les méthodes spéciales __len__() et __getitem__(). Sont ainsi désignées les listes standards, les tuples, les chaînes de caractères, mais également les tableaux NumPy unidimensionnels ou les séries Pandas. Le message met donc en lumière une contradiction fondamentale : l’opération a identifié la cible comme étant une cellule élémentaire invariable (un élément d’agrégat), tandis que la source fournie pour l’écriture est un ensemble composé de multiples unités (une séquence). L’opérateur réclame un point géométrique ; l’utilisateur lui fournit un segment.
2.2 Mécanismes internes de l’assignation dans les wrappers C de NumPy
Pour saisir l’instant précis où l’exception est émise, il est nécessaire d’examiner le chemin d’exécution emprunté par l’interpréteur au sein de la base de code C de NumPy. Lorsqu’une instruction d’assignation telle que tableau[i] = v est évaluée, l’interpréteur CPython invoque la méthode slot mp_ass_subscript associée au type PyArray_Type, qui redirige l’appel vers la routine C array_assign_subscript.
Cette fonction commence par analyser la nature de la clé d’indexation. Si celle-ci est identifiée comme un index scalaire valide sélectionnant une dimension terminale, le pointeur d’écriture se positionne directement sur l’adresse mémoire calculée pour la cellule visée. À cette étape, NumPy fait appel à la routine interne PyArray_SETITEM ou à la fonction de conversion de type dédiée au dtype du tableau cible. Cette fonction a pour responsabilité de transtyper la valeur Python entrante v en un flux d’octets natif correspondant au type de données attendu (par exemple, convertir un entier Python arbitraire en un entier signé 32 bits de format C).
C’est précisément au sein de cette fonction de conversion scalaire que le conflit éclate. Le convertisseur inspecte l’objet v et constate qu’il s’agit d’un objet composite (une instance de PyList_Type, PyTuple_Type ou PyArray_Type). Ne disposant d’aucune règle sémantique pour écraser une case de taille fixe par une séquence arbitraire sans instruction de découpage explicite, le système tente éventuellement d’appliquer un transtypage implicite vers le type cible. Face à l’impossibilité de convertir une séquence en une valeur scalaire unique (comme tenter de convertir une liste de deux nombres en un flottant atomique sans fonction d’agrégation explicite), la routine C lève le drapeau d’erreur de Python via l’appel PyErr_SetString(PyExc_ValueError, "setting an array element with a sequence") et interrompt immédiatement l’exécution sans modifier la zone de mémoire tampon.
3. Reproduction expérimentale : protocole et cas minimal
3.1 Construction de l’environnement de test minimal
Afin d’étudier l’anomalie de manière empirique et contrôlée, il convient d’élaborer un banc d’essai minimal exempt de tout effet de bord provoqué par des bibliothèques tierces. Le cas d’école consiste à instancier un vecteur unidimensionnel de petite taille, dont l’ensemble des métadonnées structurelles est parfaitement maîtrisé et inspectable.
Considérons l’initialisation d’un vecteur numérique simple au moyen de l’instruction suivante : data = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]). Dès son allocation, cet objet se voit attribuer des propriétés intrinsèques non équivoques. Son attribut shape vaut (10,), signifiant qu’il s’agit d’un espace vectoriel unidimensionnel contenant exactement dix éléments. Sa taille globale size est de 10, sa dimensionnalité ndim est égale à 1, et son type de données dtype est automatiquement inféré comme int64 ou int32 selon l’architecture du processeur hôte.
L’intégrité de ce tableau peut être vérifiée par une opération d’indexation scalaire conforme. L’instruction data[0] renvoie la valeur scalaire 1. De même, une assignation élémentaire respectueuse des types telle que data[0] = 42 remplace sans encombre le premier entier par la nouvelle valeur, confirmant que le mécanisme de modification en place fonctionne conformément aux attentes tant que la valeur transmise demeure strictement atomique.
3.2 Déclenchement délibéré de l’exception
Le déclenchement reproductible de l’exception est obtenu en violant délibérément la condition d’atomicité établie précédemment. Supposons que l’opérateur tente d’assigner non pas une valeur scalaire, mais un sous-tableau composé de deux entiers à l’emplacement indicé unique data[0] via l’instruction : data[0] = np.array([4, 5]).
À cet instant précis, l’interpréteur interrompt son exécution séquentielle et génère la trace d’appels suivante, caractéristique de cette rupture de contrat :
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: setting an array element with a sequence.
L’examen minutieux de ce traceback démontre que l’erreur se produit directement au niveau de la ligne d’affectation. L’interpréteur ne laisse passer aucune opération partielle : la cellule data[0] conserve sa valeur antérieure (ici 42, ou 1 selon l’état précédent) sans qu’aucun octet n’ait été altéré. Cette observation illustre la garantie d’atomicité transactionnelle offerte par les primitives d’assignation de NumPy : lorsqu’une opération échoue en raison d’une incohérence de forme, l’état global du tableau est entièrement préservé afin d’éviter toute corruption silencieuse des données en mémoire.
3.3 Variantes immédiates avec d’autres types de séquences
Il est fondamental de noter que cette exception ne dépend aucunement de la nature spécifique de l’objet NumPy source, mais s’étend universellement à toute structure itérable reconnue par l’écosystème Python standard. Si l’on remplace le sous-tableau NumPy de l’exemple précédent par une liste native standard via l’affectation data[0] = [4, 5], l’interpréteur lève rigoureusement la même ValueError.
De manière identique, la tentative d’assigner un tuple Python, comme dans data[0] = (4, 5), engendre exactement la même interruption d’exécution. Même si le tuple est structurellement immuable en Python, cette immuabilité n’a aucune incidence sur le refus opposé par NumPy : le moteur d’évaluation identifie le tuple comme une séquence au sens du protocole itérable et constate l’incapacité de le réduire à une valeur scalaire unique pouvant s’insérer dans l’emplacement data[0] de type int64.
Cette universalité démontre avec force que le verrouillage s’opère au niveau de l’interface de réception du conteneur récepteur et non au niveau des spécificités du conteneur émetteur. Dès lors que l’objet assigné répond positivement aux vérifications d’itérabilité interne et refuse une conversion scalaire directe, le mécanisme d’affectation de NumPy déclenche systématiquement le signal d’interruption.
4. Cause fondamentale 1 : Confusion entre indexation scalaire et découpage
4.1 Divergence théorique entre indice ponctuel et intervalle (slicing)
L’une des causes les plus courantes menant à l’apparition de cette exception réside dans une confusion conceptuelle entre l’indexation ponctuelle par entier et la notation par intervalle, communément désignée sous le terme de découpage (ou slicing). Ces deux paradigmes syntaxiques, bien que partageant l’usage des crochets, répondent à des lois mathématiques et à des comportements structurels totalement divergents.
L’adressage direct par un entier unique (par exemple tableau[k]) sélectionne un élément individuel et diminue automatiquement le nombre de dimensions du tableau résultant d’une unité. Si le tableau d’origine est de dimension 1, le résultat de l’extraction est un scalaire de dimension 0. En conséquence, l’opération inverse d’affectation exige formellement un objet de dimension 0, c’est-à-dire une entité scalaire indivisible.
À l’inverse, l’indexation par intervalle (par exemple tableau[k:k+1] ou tableau[start:stop]) préserve invariablement la dimensionnalité globale de la structure d’origine. Même si la plage sélectionnée ne comprend qu’un seul élément (comme dans le cas où stop = start + 1), l’objet extrait demeure un sous-tableau de dimension 1. Sur le plan de la modification en place, cette préservation dimensionnelle implique que l’opérateur d’assignation s’attend impérativement à recevoir une séquence ou un sous-tableau compatible en taille pour remplacer le segment ainsi délimité.
4.2 Erreurs d’implémentation fréquentes lors de la mise à jour de sous-tableaux
Dans la pratique algorithmique, de nombreux développeurs commettent l’erreur d’omettre la syntaxe d’intervalle lorsqu’ils souhaitent actualiser une section d’un vecteur. L’oubli des deux-points séparateurs transforme une opération de substitution de segment en une tentative d’écrasement scalaire. En écrivant vecteur[0] = sous_vecteur au lieu de vecteur[0:longueur] = sous_vecteur, le programmeur postule involontairement que la cellule initiale est capable d’absorber l’ensemble des valeurs du sous-vecteur.
Ce dysfonctionnement s’accompagne souvent d’une méconnaissance de la gestion de la mémoire dans les structures compilées. Les développeurs habitués à la souplesse des listes Python natives présument fréquemment que les tableaux NumPy possèdent une capacité d’extension dynamique automatique. Dans une liste native, insérer plusieurs éléments à un emplacement donné nécessite des méthodes dédiées comme extend() ou des assignations de tranches spécifiques, mais la liste gère elle-même la réallocation de ses pointeurs. Avec un ndarray pré-alloué, la mémoire physique est figée : la taille totale est invariable.
Par conséquent, penser qu’une assignation scalaire va implicitement « repousser » les éléments adjacents pour faire de la place aux composantes excédentaires de la séquence relève d’une méprise théorique. Le tableau NumPy n’a aucune possibilité de décaler automatiquement ses blocs de mémoire contigus lors d’une assignation indicée. Dès lors que l’indice ne spécifie pas explicitement une plage d’écriture capable de recevoir l’exacte longueur de la séquence fournie, l’interruption sous la forme d’une ValueError demeure inévitable.
5. Cause fondamentale 2 : Tableaux irréguliers et listes imbriquées inégales
5.1 Le problème des structures en lambeaux (Ragged Arrays)
La deuxième source prépondérante d’apparition de l’erreur concerne la construction de matrices à partir de conteneurs de conteneurs dont les sous-listes possèdent des longueurs disparates, un phénomène désigné dans la littérature informatique sous l’appellation de tableaux irréguliers ou structures en lambeaux (ragged arrays ou jagged arrays).
Imaginons un scénario où l’utilisateur souhaite créer une matrice bidimensionnelle à partir d’une collection d’enregistrements : matrice = np.array([[1, 2, 3], [4, 5]]). Dans cet exemple, la première ligne comporte trois éléments, tandis que la seconde n’en contient que deux. D’un point de vue géométrique, une telle disposition ne forme pas un rectangle régulier et viole par conséquent la définition mathématique d’une matrice dans un espace cartésien euclidien.
L’évolution historique de la bibliothèque NumPy vis-à-vis de cette situation mérite d’être soulignée. Dans les versions antérieures à la version 1.20, NumPy adoptait un comportement particulièrement permissif mais dangereux : constatant l’impossibilité de créer un tableau numérique régulier, il dégradait silencieusement le tableau en un conteneur générique à une dimension doté du type dtype=object, au sein duquel chaque cellule stockait un pointeur vers les listes Python respectives. Toutefois, face aux innombrables bogues silencieux et aux dégradations majeures de performance induits par ce mécanisme implicite, le consortium de développement de NumPy a instauré, à partir de la version 1.20 et de façon renforcée dans les versions ultérieures, une politique de dépréciation puis de blocage strict. La tentative de conversion directe d’une telle liste de listes asymétriques génère désormais fréquemment l’exception ValueError: setting an array element with a sequence dès l’appel au constructeur np.array().
5.2 Défaillance de l’inférence de forme multidimensionnelle
L’origine de l’échec réside dans l’algorithme d’inférence de forme implémenté au sein de la fonction np.array(). Lorsqu’une structure itérable imbriquée lui est fournie en argument, le constructeur explore récursivement les branches de la structure afin de déterminer deux grandeurs fondamentales : la dimensionnalité globale (le tuple shape) et le type commun le plus restreint (le dtype).
Durant cette phase exploratoire, l’algorithme lit la première sous-liste [1, 2, 3] et émet l’hypothèse initiale que le tableau sera de dimension 2, avec une forme cible préliminaire de type (N, 3) et un type numérique entier. Toutefois, en examinant la seconde sous-liste [4, 5], l’algorithme constate une discordance immédiate : la dimension de la seconde rangée (longueur 2) contredit l’hypothèse de largeur 3 établie par la rangée précédente.
Ne pouvant concevoir un tenseur dont les tranches orthogonales auraient des longueurs divergentes au sein d’un même bloc de mémoire contigu, le système tente alors une stratégie d’ajustement. Dans certaines branches du code d’initialisation, il alloue un tableau récepteur basé sur la dimension supérieure, puis tente de peupler les éléments ligne par ligne au moyen d’itérations internes bas niveau. Lors de cette phase de peuplement, la tentative d’assigner la sous-liste [4, 5] dans ce qu’il a tenté de redéfinir comme une cellule scalaire échoue inévitablement, provoquant la levée immédiate de l’exception ValueError.
6. Cause fondamentale 3 : Mauvaise gestion des types et opérations vectorisées
6.1 Castings implicites défaillants et fonctions mathématiques
Une troisième classe d’erreurs découle de situations où un flux d’exécution attendait une valeur scalaire issue d’une évaluation mathématique, mais a reçu à la place une structure itérable en raison d’un comportement imprévu d’une fonction vectorisée ou d’une routine d’optimisation numérique.
Ce phénomène se manifeste de façon exemplaire lors de l’utilisation du module d’optimisation mathématique scipy.optimize. Considérons l’emploi d’un minimiseur non linéaire tel que scipy.optimize.minimize. Le contrat d’interface formel imposé par ce solveur exige que la fonction objectif passée en argument retourne un scalaire pur (c’est-à-dire un coût ou une perte exprimée sous la forme d’un unique nombre flottant). Si le développeur, lors de l’implémentation de sa fonction de coût, applique des opérations matricielles vectorisées sans réaliser une opération finale de réduction scalaire (telle que la somme des carrés des résidus), la fonction renvoie un vecteur NumPy d’écarts individuels au lieu d’un coût global.
Lorsque le solveur interne tente de consigner ce résultat dans son tableau d’historique de convergence ou dans les variables d’état du gradient pré-allouées sous forme de scalaires, le système essaie d’écrire le vecteur renvoyé dans une case scalaire unique. L’utilisateur se retrouve alors confronté à une ValueError: setting an array element with a sequence au sein d’un fichier source interne de SciPy, sans comprendre immédiatement que le dysfonctionnement trouve son origine dans le non-respect du type de retour scalaire de sa propre fonction objectif.
6.2 Interactions conflictuelles avec les colonnes de DataFrames Pandas
L’interaction entre la bibliothèque de manipulation tabulaire Pandas et son moteur de calcul sous-jacent NumPy constitue un autre terrain fertile pour l’émergence de cette anomalie. Bien que Pandas permette conceptuellement à une cellule de DataFrame d’héberger un objet Python quelconque (y compris des listes ou des dictionnaires), cette flexibilité se heurte brutalement aux contraintes de NumPy dès lors que des opérations vectorielles ou des conversions globales sont engagées.
Considérons l’application d’une méthode de transformation vectorisée via la fonction apply() sur une colonne de DataFrame. Si la fonction personnalisée passée à apply() renvoie un tuple ou une liste pour certains enregistrements et un scalaire pour d’autres, la série résultante se retrouve dans un état structurel hybride. Lorsque le moteur interne tente d’aligner ou de convertir cette série sous la forme d’une matrice NumPy régulière (par exemple via l’appel de l’attribut .to_numpy() ou .values avec un dtype numérique spécifié), le transtypage sous-jacent échoue de manière critique.
De même, l’assignation directe d’une séquence dans une cellule spécifique d’un DataFrame au moyen d’indexeurs de position tels que df.iloc[i, j] = [1, 2] peut déclencher l’exception si la colonne j sous-jacente est adossée à un tableau NumPy de type numérique primitif (comme int64 ou float64). Pandas tente alors d’injecter la liste dans le bloc de mémoire typé existant, ce qui déclenche immédiatement le rejet par la couche C de NumPy via la ValueError analysée.
7. Solution 1 : Correction par l’alignement scalaire rigoureux
7.1 Assignation d’une valeur élémentaire unique
La première voie de résolution, applicable lorsque l’anomalie résulte d’une méprise sur la nature de la valeur à insérer, consiste à restaurer l’alignement scalaire strict. Cette démarche implique de s’assurer que la valeur transmise à l’opérateur d’assignation indicée est dépourvue de toute dimensionnalité supérieure à zéro.
Dans le cas d’école minimal présenté précédemment, où l’instruction fautive était data[0] = np.array([4, 5]), la correction immédiate consiste à isoler la valeur élémentaire désirée et à procéder à une affectation scalaire conforme, telle que : data[0] = 4. D’un point de vue informatique, cette opération s’exécute avec une efficacité optimale : la valeur entière 4 est directement convertie au niveau C selon le format binaire de int64 et copiée dans l’emplacement mémoire contigu délimité par l’index 0, sans nécessiter d’allocation intermédiaire ni de manipulation de descripteurs de conteneurs.
Une inspection post-modification de l’état mémoire du tableau démontre la conformité de l’opération : la forme globale data.shape demeure rigoureusement égale à (10,), son type data.dtype reste invariant, et la première case mémoire héberge désormais fidèlement l’entier spécifié. L’alignement scalaire garantit ainsi la préservation intégrale des propriétés algébriques de la structure sans altérer les blocs de mémoire voisins.
7.2 Extraction programmatique préalable d’un élément de séquence
Dans de nombreuses architectures logicielles complexes, la valeur assignée n’est pas codée en dur sous forme de constante, mais provient du résultat d’un traitement amont qui renvoie dynamiquement une collection d’éléments. Dans ce contexte, la résolution du conflit impose une extraction explicite de la composante pertinente avant de procéder à l’écriture.
Si la logique métier prescrit que seul le premier élément de la séquence entrante doit être consigné dans le tableau récepteur, l’instruction doit formellement déréférencer cet élément au moyen d’un indexage scalaire préalable : data[0] = sequence[0]. En décomposant l’opération, le programmeur élimine l’ambiguïté dimensionnelle : l’expression sequence[0] produit un scalaire pur qui satisfait pleinement les conditions d’écriture imposées par data[0].
Dans l’éventualité où la séquence contient une multiplicité de valeurs numériques représentatives d’une mesure globale (par exemple un vecteur d’échantillons répétés), la solution réside dans l’application explicite d’un opérateur de réduction mathématique. Selon les exigences de l’algorithme, on utilisera une fonction d’agrégation produisant une synthèse scalaire non équivoque, par exemple la moyenne arithmétique via data[0] = np.mean(sequence) ou la somme intégrale au moyen de data[0] = np.sum(sequence). Cette étape de scalarisation explicite transforme une incompatibilité dimensionnelle en une opération mathématiquement rigoureuse et totalement sécurisée pour le moteur mémoire.
8. Solution 2 : Recours au découpage dimensionnel (Slicing)
8.1 Remplacement d’une plage d’éléments par une tranche concordante
Lorsque l’intention véritable du programmeur est bel et bien d’insérer une séquence de plusieurs éléments au sein du tableau récepteur, la solution ne consiste pas à réduire la source à un scalaire, mais à transformer la cible d’assignation en une tranche dimensionnelle (slice) géométriquement concordante.
Pour insérer le sous-tableau [4, 5] au début du vecteur data, il est impératif d’abandonner l’indice ponctuel au profit d’un intervalle délimitant exactement deux cellules réceptrices : data[0:2] = [4, 5]. Dans cette syntaxe, l’expression data[0:2] ne cible pas une cellule scalaire atomique de dimension 0, mais extrait une vue (view) vectorielle de dimension 1 possédant une forme (2,). Le moteur d’assignation C de NumPy compare alors la forme de la tranche cible avec la forme de la séquence source. Constatant une égalité dimensionnelle parfaite (deux éléments d’un côté, deux éléments de l’autre), il procède à une copie mémoire par blocs (memcpy optimisé) qui met à jour simultanément les deux cases mémoires adjacentes.
Cette approche par découpage offre également la possibilité d’utiliser des pas d’avancement non unitaires (striding). Par exemple, pour insérer des valeurs dans un élément sur deux le long d’une section donnée, on pourra employer une notation de la forme data[0:4:2] = [4, 5]. La condition sine qua non pour la réussite de cette opération demeure la stricte égalité entre le nombre de positions ouvertes par la tranche et le nombre d’éléments fournis par la séquence assignée, sous peine de déclencher une autre exception majeure de dimensionnement abordée ultérieurement.
8.2 Redimensionnement explicite préalable du tableau d’accueil
Une limitation intrinsèque fondamentale de la solution par découpage réside dans l’impossibilité d’insérer une séquence si la capacité restante du tableau cible est inférieure au nombre d’éléments à introduire. Si le tableau data ne comporte que 10 éléments, l’assignation data[9:12] = [4, 5, 6] ne provoquera pas une extension magique de la taille du tableau : elle sera soit tronquée, soit rejetée en fonction des bornes allouées.
Si la séquence doit être insérée dans un vecteur sans écraser les données préexistantes et en augmentant la taille totale de l’ensemble, le recours à des fonctions d’expansion structurelle telles que np.concatenate, np.append ou np.insert devient obligatoire. Par exemple, l’insertion du sous-vecteur à l’indice 0 s’effectuera via l’instruction : data = np.insert(data, 0, [4, 5]).
Il importe toutefois de souligner le coût algorithmique élevé de cette opération sur le plan de la théorie de la complexité. Contrairement aux listes chaînées ou aux structures de données arborescentes, l’agrandissement d’un ndarray impose l’allocation d’une toute nouvelle zone contiguë de mémoire dans le tas, suivie de la copie intégrale de l’ensemble des éléments anciens et nouveaux vers cette nouvelle adresse, avant la libération de l’ancien segment. En conséquence, l’exécution répétée de telles insertions dynamiques au sein d’une boucle itérative entraîne une complexité temporelle quadratique catastrophique en $O(N^2)$. La règle d’or pour la conception d’algorithmes performants consiste donc à pré-allouer l’intégralité de l’espace mémoire nécessaire dès le départ à l’aide de primitives comme np.zeros ou np.empty, puis à peupler cet espace au moyen d’assignations par découpage rigoureusement bornées.
9. Solution 3 : Emploi délibéré des tableaux d’objets génériques
9.1 Déclaration explicite du type générique (dtype=object)
Dans certains contextes académiques ou dans des étapes exploratoires de prétraitement de données hautement hétérogènes, il peut être nécessaire de concevoir un tableau multidimensionnel dont chaque cellule héberge véritablement une séquence arbitraire de taille variable. Pour contraindre NumPy à accepter ce comportement sans lever de ValueError, il faut expressément désactiver le typage numérique bas niveau en forçant le descripteur de type générique : dtype=object.
En formulant explicitement l’instanciation de la manière suivante : data = np.empty(10, dtype=object), le modèle d’agencement mémoire est radicalement transformé. Au lieu d’allouer un segment d’octets bruts destinés à recevoir des nombres flottants ou entiers contigus, le système alloue un tableau de pointeurs CPython (chacun occupant typiquement 8 octets sur une architecture 64 bits). Dès lors, l’assignation data[0] = [4, 5] devient parfaitement licite. L’emplacement data[0] ne stocke plus directement les valeurs numériques 4 et 5, mais consigne l’adresse mémoire de l’objet liste standard instancié dans le tas Python.
De même, pour contourner le blocage lors de la création d’un tableau irrégulier à partir de listes asymétriques, l’adjonction du paramètre explicite résout le blocage syntaxique : matrice = np.array([[1, 2, 3], [4, 5]], dtype=object). Cette déclaration formelle lève toute ambiguïté pour l’algorithme d’inférence de forme, lui intimant l’ordre de ne pas chercher à construire une matrice rectangulaire numérique, mais un vecteur de pointeurs de haut niveau.
9.2 Évaluation comparative des compromis techniques
Si le recours au type générique dtype=object apporte une réponse immédiate et débloque l’exécution d’un script récalcitrant, cette solution s’accompagne de pénalités computationnelles majeures qu’un ingénieur logiciel doit impérativement évaluer avant tout déploiement.
Le premier tribut payé est l’anéantissement pur et simple de l’accélération vectorielle. Sur un tableau de type float64 ou int64, les opérations arithmétiques sont exécutées directement par des instructions vectorielles du processeur (instructions SIMD telles que AVX-512) capables de traiter plusieurs données numériques par cycle d’horloge. Sur un tableau de type object, toute opération arithmétique nécessite une boucle d’interprétation Python standard, impliquant un déréférencement de pointeur pour chaque élément, une inspection dynamique de son type (type checking), l’appel de méthodes magiques associées et la création d’un nouvel objet d’encapsulation pour le résultat. Les gains de performance offerts par NumPy sont alors totalement annihilés, l’exécution devenant souvent plus lente qu’avec une boucle native sur des listes ordinaires.
Le second impact concerne l’explosion de l’empreinte mémoire et la destruction de la localité spatiale du cache processeur. Alors qu’un tableau numérique regroupe ses valeurs dans des blocs contigus facilement pré-chargés par le cache L1/L2 du processeur, un tableau d’objets disperse ses structures dans des zones arbitraires du tas de mémoire. Chaque accès séquentiel génère des défauts de cache (cache misses) à répétition, réduisant drastiquement le débit de traitement des données.
9.3 Alternatives natives : listes standards vs tableaux d’objets
Face aux contre-performances inhérentes aux tableaux d’objets, il convient de s’interroger sur la pertinence architecturale de maintenir une coquille NumPy pour manipuler des données intrinsèquement irrégulières. Dans la majorité des cas de figure, l’usage d’une simple liste native Python standard (list de list) se révèle conceptuellement plus propre, plus lisible et tout aussi performant si aucune opération matricielle n’est réalisable sur l’ensemble.
Pour les scénarios où l’hétérogénéité des données est structurelle mais obéit néanmoins à un schéma fixe pour chaque enregistrement, une alternative hautement élégante consiste à recourir aux tableaux structurés (structured arrays). Ces structures permettent de définir des types de données composés personnalisés analogues aux structures du langage C. Par exemple, on peut déclarer un champ spécifique contenant un sous-vecteur de longueur fixe pré-allouée tout en conservant l’agencement mémoire contigu et la performance de calcul de NumPy.
Enfin, pour les traitements massifs de données intrinsèquement non rectangulaires (comme les événements observés en physique des particules ou les graphes imbriqués complexes), il est vivement recommandé de délaisser NumPy au profit de bibliothèques tierces de pointe spécifiquement conçues pour résoudre cette équation, telles que Awkward Array. Cette bibliothèque permet de manipuler des structures de données en lambeaux avec des performances d’exécution vectorisées de niveau C, en évitant les compromis destructeurs du dtype=object.
10. Diagnostics avancés : Techniques d’inspection et de débogage
10.1 Inspection dynamique des dimensions et de la structure des données
L’éradication pérenne de l’erreur ValueError: setting an array element with a sequence exige la mise en œuvre de méthodologies rigoureuses d’introspection dynamique des structures de données au moment de l’exécution. Lorsque l’exception survient au sein d’une routine complexe traitant des flux dynamiques, le premier réflexe d’ingénierie consiste à sonder les métadonnées de dimensionnalité des objets en conflit.
L’interrogation systématique des attributs fondamentaux fournit un diagnostic immédiat : l’attribut data.shape permet de quantifier exactement le nombre d’éléments disponibles le long de chaque axe ; l’attribut data.ndim révèle le degré de liberté dimensionnel du tenseur ; enfin, data.dtype renseigne sur la nature précise du format mémoire sous-jacent. L’identification d’une inadéquation entre la forme de l’opérande gauche (la cellule ou la vue cible) et celle de l’opérande droit (la charge utile assignée) constitue l’étape fondamentale de tout protocole de débogage.
Dans le cas spécifique des tableaux irréguliers issus de listes imbriquées, l’inspection visuelle s’avère souvent impraticable en présence de milliers de lignes. Il est alors recommandé d’exécuter des compréhensions de listes analytiques destinées à détecter instantanément l’hétérogénéité de longueur, par exemple en calculant la variance des tailles des sous-composants : longueurs = [len(element) for element in liste_source if hasattr(element, '__len__')]. La mise en évidence d’un ensemble de longueurs disjointes (par exemple via set(longueurs)) apporte la preuve irréfutable du caractère irrégulier de la source et explique l’échec de la création matricielle.
Pour les analyses interactives complexes, l’insertion de points d’arrêt au moyen du débogueur interactif de Python (pdb ou son extension enrichie ipdb) immédiatement en amont de la ligne incriminée permet de geler l’état d’exécution. Le praticien peut alors tester interactivement le type exact des objets, vérifier s’ils répondent aux interfaces de séquences via isinstance(valeur, (list, tuple, np.ndarray)), et valider la syntaxe d’assignation la plus appropriée avant de modifier le code source.
10.2 Validation formelle des flux de données en amont
Au sein d’environnements industriels où le code est destiné à fonctionner de manière autonome sans intervention humaine, la détection a posteriori des erreurs par rupture d’exécution n’est pas tolérable. Il convient de substituer à l’analyse réactive une politique de validation formelle préventive à l’entrée des modules de calcul vectorisé.
La mise en place d’instructions d’assertion strictes permet de verrouiller l’atomicité des entrées. L’insertion d’une directive telle que assert np.isscalar(valeur), f"Erreur critique : une valeur scalaire était attendue, mais une structure de type {type(valeur)} a été fournie." assure que toute violation de contrat est stoppée net avec un message explicite rédigé par l’architecte du pipeline, avant même que la couche bas niveau de NumPy ne lève son exception générique.
Dans le cadre des procédures d’intégration continue et de tests automatisés, l’exploitation du sous-module dédié numpy.testing offre un arsenal d’outils sophistiqués pour certifier la robustesse structurelle des fonctions. Les fonctions d’assertion telles que assert_array_equal ou assert_array_almost_equal permettent de valider non seulement la concordance des valeurs numériques calculées, mais également la stricte équivalence des dimensions des tenseurs résultants. Couplés à des fonctions de validation personnalisées capables d’inspecter récursivement la régularité des conteneurs avant instanciation, ces dispositifs garantissent que les pipelines de données n’acheminent vers le moteur matriciel que des structures géométriquement irréprochables.
11. Différenciation avec les erreurs connexes de l’écosystème NumPy
11.1 Distinction avec ‘ValueError: could not broadcast input array from shape’
Dans le paysage des exceptions dimensionnelles levées par NumPy, il est essentiel d’opérer une distinction nette entre l’erreur objet de cet article et une autre exception matricielle omniprésente : ValueError: could not broadcast input array from shape (A,) into shape (B,).
Cette dernière intervient dans le cadre spécifique où l’opération d’assignation utilise bel et bien une notation de découpage valide (une tranche), mais où les dimensions des deux opérandes ne respectent pas les règles universelles de diffusion (ou broadcasting) édictées par NumPy. Considérons par exemple une assignation de la forme data[0:3] = [1, 2]. Dans cette situation, la cible est une tranche de longueur 3 (forme (3,)) et la source est une séquence de longueur 2 (forme (2,)). Le moteur constate que les deux entités sont des séquences compatibles avec le protocole de tranche, mais les règles arithmétiques de diffusion stipulent que deux dimensions ne peuvent s’aligner que si elles sont égales ou si l’une d’entre elles est égale à 1.
Par conséquent, tandis que setting an array element with a sequence sanctionne une rupture ontologique qualitative (l’opposition irréconciliable entre un point scalaire et une séquence itérable), l’erreur de diffusion sanctionne une rupture quantitative d’échelle (l’impossibilité géométrique d’ajuster deux segments de longueurs incompatibles non étirables). La première survient lors d’une indexation scalaire ponctuelle, la seconde lors d’une indexation par plage dimensionnelle.
11.2 Distinction avec ‘TypeError: only integer scalar arrays can be converted to a scalar index’
Une seconde confusion d’indexation très fréquente réside dans la confusion entre la valeur que l’on tente d’assigner et l’index utilisé pour désigner la cellule cible. Cette méprise se traduit par l’émission de l’exception TypeError: only integer scalar arrays can be converted to a scalar index.
Cette exception survient typiquement lorsque l’utilisateur fournit par erreur une séquence ou une structure multidimensionnelle au sein des crochets d’indexation, à un endroit où seul un entier ponctuel est toléré par la grammaire interne de l’interpréteur. Supposons que par une inversion accidentelle de variables, un programmeur écrive data[[1, 2]] = 3 en pensant modifier une coordonnée bidimensionnelle sur un tableau qui n’est en fait qu’un vecteur de dimension 1, ou qu’il utilise un tableau de flottants comme indice dans data[indice_flottant]. L’interpréteur CPython rejette la tentative dès la phase de résolution d’adresse, signalant que la clé d’adressage elle-même est structurellement invalide.
Le tableau comparatif suivant synthétise les propriétés distinctives de ces trois anomalies fondamentales régulièrement confondues par les praticiens de l’écosystème numérique :
- ValueError: setting an array element with a sequence
- Emplacement de l’anomalie : Opérande droit de l’assignation (la valeur assignée).
- Nature de la sélection : Indexation scalaire unitaire ciblant un emplacement atomique de dimension 0.
- Cause fondamentale : Tentative d’écrasement d’une cellule scalaire par un conteneur composite itérable.
- ValueError: could not broadcast input array from shape
- Emplacement de l’anomalie : Alignement entre les deux membres de l’assignation.
- Nature de la sélection : Indexation par tranche ou sélection multidimensionnelle préservant les dimensions.
- Cause fondamentale : Incompatibilité arithmétique entre la longueur de la tranche cible et la forme du tenseur source selon les règles de diffusion.
- TypeError: only integer scalar arrays can be converted to a scalar index
- Emplacement de l’anomalie : Opérande gauche, à l’intérieur des crochets (la clé d’indexation).
- Nature de la sélection : Résolution d’adresse d’indexation.
- Cause fondamentale : Fourniture d’une séquence mal formée ou d’un type non entier comme sélecteur de position.
12. Guide méthodologique et bonnes pratiques de programmation défensive
12.1 Règles d’or pour la conception d’algorithmes matriciels robustes
Pour prévenir l’apparition de telles défaillances logicielles au sein de systèmes critiques, il est impératif d’adopter des règles d’ingénierie logicielle fondées sur la programmation défensive et la modélisation formelle des données matricielles dès les phases de conception architecturale.
La première règle d’or réside dans la pré-allocation systématique et le dimensionnement déterministe. Les algorithmes qui construisent progressivement des matrices par concaténations successives ou insertions dynamiques non contrôlées doivent être proscrits. Les tableaux d’accueil doivent être rigoureusement dimensionnés en amont à l’aide de constructeurs comme np.zeros, np.ones ou np.empty en spécifiant explicitement la forme géométrique complète et le type de données dtype précis. Ce formalisme initial oblige le concepteur à définir contractuellement l’espace de mémoire alloué et prévient toute dérive dimensionnelle ultérieure.
La deuxième règle impose le recours aux annotations de type statiques modernes introduites à partir de Python 3.9 et renforcées par le module spécialisé numpy.typing. L’utilisation d’annotations telles que NDArray[np.float64] pour typifier les signatures de fonctions fournit aux outils d’analyse statique de code (comme mypy) la capacité de détecter les incohérences de type et de forme bien avant l’exécution du code en environnement réel. De même, expliciter les retours de fonctions d’évaluation scalaire via le type standard float interdit contractuellement le renvoi accidentel de vecteurs multidimensionnels.
La troisième règle préconise l’isolation stricte des phases d’ingénierie et de nettoyage des données vis-à-vis des phases de calcul vectorisé pur. Les données brutes issues de sources externes (fichiers JSON, bases NoSQL, requêtes API) se caractérisent souvent par une forte hétérogénéité et une variabilité dimensionnelle intrinsèque. Tenter d’injecter directement ces flux non assainis dans des tableaux NumPy expose immédiatement le système à l’apparition de structures en lambeaux et à des erreurs d’assignation. Les étapes de nettoyage doivent être opérées au moyen de structures élastiques adaptées (listes Python pures, dictionnaires, ou schémas stricts Pandas/Polars), l’instanciation de tenseurs NumPy compacts n’intervenant qu’une fois la parfaite régularité rectangulaire certifiée.
12.2 Arbre de décision pour la résolution de l’erreur
Lorsqu’un ingénieur ou un chercheur est confronté à l’interruption inopinée d’un pipeline par l’exception ValueError: setting an array element with a sequence, l’application d’un protocole décisionnel méthodique permet de diagnostiquer la cause racine et de déployer le correctif optimal en quelques étapes séquentielles :
Étape 1 : Diagnostic du point d’impact. La ligne de code fautive correspond-elle à une instanciation globale de tableau (appel à np.array()) ou à une opération d’assignation en place (tableau[index] = valeur) ?
Si l’erreur survient lors de l’instanciation np.array(), le diagnostic pointe immédiatement vers une structure en lambeaux (Cause 2). L’utilisateur doit vérifier l’égalité de longueur de ses sous-listes. Si la structure doit impérativement rester irrégulière, il convient soit d’utiliser une liste Python native, soit d’opter pour une bibliothèque spécialisée comme Awkward Array, soit enfin de déclarer explicitement dtype=object si les pénalités de performance sont jugées acceptables. Si l’irrégularité est accidentelle, un alignement préalable par remplissage (padding avec des zéros ou des NaN) doit être appliqué pour restaurer la rectangularité.
Étape 2 : Analyse de l’opérande droit lors d’une assignation. Si l’erreur se produit lors d’une assignation indicée, il faut inspecter immédiatement la variable source. La présence d’une séquence (liste, tuple, sous-vecteur) est-elle intentionnelle ou résulte-t-elle d’une anomalie en amont ?
Si la séquence est accidentelle (par exemple une fonction mathématique qui a vectorisé son calcul de manière imprévue), la solution réside dans l’application d’un alignement scalaire rigoureux (Solution 1) : corriger la fonction amont pour qu’elle produise un scalaire, ou extraire/réduire la valeur via une agrégation explicite (np.sum, np.mean, indexation [0]).
Étape 3 : Réévaluation de la sémantique de l’indice lors d’une assignation. Si la séquence fournie est légitime et doit être insérée dans le tableau, le diagnostic confirme une confusion entre index scalaire et intervalle (Cause 1). L’utilisateur doit convertir son indexation ponctuelle en une notation de découpage concordante (Solution 2) sous la forme tableau[start:stop] = sequence, en s’assurant rigoureusement que la longueur du segment délimité correspond scrupuleusement au nombre d’éléments injectés.
Pour assurer la pérennité industrielle du correctif, l’ingénieur doit soumettre son implémentation à une liste de contrôle (checklist) finale avant toute mise en production :
- Vérifier que les dimensions cibles et sources sont explicitement validées par des tests unitaires intégrant le module
numpy.testing. - Proscrire tout recours implicite à des conversions d’objets et éliminer les avertissements (warnings) d’obsolescence émis par l’interpréteur.
- S’assurer que la taille des tableaux alloués est fixe et que le code ne dissimule aucune tentative de redimensionnement dynamique au sein de boucles critiques.
- Documenter formellement les préconditions dimensionnelles dans les docstrings des fonctions à l’aide de conventions reconnues (style NumPy ou Google Docstrings).
En observant scrupuleusement cette démarche méthodologique et en respectant les impératifs d’homogénéité spatiale inhérents aux structures compilées de bas niveau, les développeurs et data scientists s’assurent de concevoir des architectures de calcul matriciel à la fois performantes, maintenables et à l’épreuve des anomalies d’exécution.
Références
Harris, C. R., Millman, K. J., van der Walt, S. J., Gommers, R., Virtanen, P., Cournapeau, D., Wieser, E., Taylor, J., Berg, S., Smith, N. J., Kern, R., Picus, M., Hoyer, S., van Kerkwijk, M. H., Brett, M., Haldane, A., del Río, J. F., Wiebe, M., Peterson, P., … Oliphant, T. E. (2020). Array programming with NumPy. Nature, 585(7825), 357–362. https://doi.org/10.1038/s41586-020-2649-2
NumPy Developers. (2023). NumPy reference documentation (Release 1.26.0). NumPy.org. https://numpy.org/doc/stable/reference/
Oliphant, T. E. (2006). A guide to NumPy. Trelgol Publishing.
Python Software Foundation. (2023). Built-in exceptions: ValueError. Python 3 Documentation. https://docs.python.org/3/library/exceptions.html#ValueError
Van der Walt, S., Colbert, S. C., & Varoquaux, G. (2011). The NumPy array: A structure for efficient numerical computation. Computing in Science & Engineering, 13(2), 22–30. https://doi.org/10.1109/MCSE.2011.37
Virtanen, P., Gommers, R., Oliphant, T. E., Haberland, M., Reddy, T., Cournapeau, D., Burovski, E., Peterson, P., Weckesser, W., Bright, J., van der Walt, S. J., Brett, M., Wilson, J., Millman, K. J., Mayorov, N., Nelson, A. R. J., Jones, E., Kern, R., Larson, E., … SciPy 1.0 Contributors. (2020). SciPy 1.0: Fundamental algorithms for scientific computing in Python. Nature Methods, 17(3), 261–272. https://doi.org/10.1038/s41592-019-0686-2