Automatiser et structurer le traitement de données

a marqué ce sujet comme résolu.

Bonjour,

Premièrement, veuillez m'excuser pour ce titre incompréhensible. Plus explicitement, le problème est le suivant :

MPF est un projet où j'ai besoin d'analyser des données. L'objectif est de prévoir la production laitière future à partir de celles quotidiennes passées. J'ai donc, dans le dossier data, un ensemble de données qu'il me faut étudier. Elles s'organisent ainsi :

  • 2251 (vache)
    • 1 (lactation)
      • data.txt
    • 2
      • data.txt
  • 3138
    • 1
      • data.txt

Dans le dossier scripts, j'ai des programmes Python dont le but est d'analyser ces données : dessiner les graphes, effectuer une moyenne mobile dessus, calculer la transformée de Fourier… Seulement, comme vous pouvez le constater, mon code est immonde. Du coup, j'aimerais l'organiser, mais j'ignore un peu comment faire. Le but est de pouvoir aisément faire des trucs du genre :

  • Tracer le graphe des productions de chaque lactation de la vache 2251 et l'enregistrer sous 2251/lactation/graphe.png.
  • Pour tous les animaux et toutes les lactations, tracer la droite des moindres carrés sur le graphe et l'enregistrer dans vache/lactation/least-squares.png.

Comme certaines opérations prennent du temps, j'aimerais avoir le choix entre deux options :

  • Si le graphe existe déjà, ne pas le redessiner
  • Le redessiner quoi qu'il arrive

Pour l'instant, j'ai regroupé mes fonctions de traitement (least_squares, moving_average…) dans une classe Processor et mes fonctions de dessin dans Drawer. Là où je bloque, c'est pour le lancement des analyses. Actuellement, ça se fait dans scripts/shell_commands.py, mais c'est très fouillis.

Merci !

+0 -0

Salut,

Bon, le design ne me semble pas forcément très feng-shui d'après ce que tu racontes, en effet.

D'abord, pour qu'on soit bien d'accord, un traitement :

  • produit toujours un graphe dans une image ?
  • repose sur une fonction calculatoire ?

Pourquoi ne pas créer une classe Processor qui modélise tout sauf le calcul spécifique des données (quitte à encapsuler la production des images dans ta classe Drawer), et ensuite l'instancier en lui passant juste la fonction de calcul qui va bien ?

De cette façon, tu ne codes qu'une et une seule fois la sélection, le chargement des données, la production des graphes, la vérification s'il existe un cache ou non ou bien le forçage du traitement, et tu simplifies grandement ton interface.

Pour le lancement des analyses, ça a quelle tête ? Tu parses les arguments en ligne de commande ? Si oui, je ne peux que te recommander l'excellent module docopt. Je l'ai découvert assez récemment, et depuis, je l'utilise dans absolument tous les tools que je développe au boulot (et y'en a un paquet !).

+0 -0

Pour simplifier les choses, je ne vais plus générer les graphes indépendamment comme je le fais. Mes courbes seront uniquement présentées sous forme de galeries.

En outre, je vais créer un dossier pour les analyses ("processing" ?) et stocker ces dernières non pas selon l'animal mais selon le type (graphes des valeurs brutes, après lissage, avec droite des moindres carrés…).

A priori, une image est toujours générée en effet, afin de conserver des traces. Pour l'instant, je n'ai pas d'autres utilisations en tête.

Pour la fonction calculatoire… Tout dépend ce que tu entends par là. L'objectif est d'analyser des données (pour une vache et lactation données, un ensemble de couples (jour, production)). Donc je ne vois pas trop ce que pourrait être un traitement non calculatoire.

A propos de ta suggestion sur la classe Processor, c'est effectivement ce que je souhaite faire. Mais j'ignore comment.

En ce qui concerne le lancement des analyses, je vois deux possibilités :

  • Passage par des arguments via la ligne de commande
  • Ecriture en dur dans un fichier

Le problème des arguments en ligne de commande, c'est qu'il faut préciser la vache, la lactation, la fonction de traitement, les paramètres inhérents à cette dernière… Ce sont justement des questions que je me pose ici : comment rendre ça simple à utiliser ?

Je réfléchis plus en détails à tout ça.

Merci !

+0 -0

Pour la classe Processor il y a un écueil dans lequel il ne faut surtout pas tomber, c'est lui donner plus de responsabilités que nécessaire.

En gros, il faut identifier les actions ou objets indépendants sur lesquels tu peux réfléchir sans te casser la tête avec le big picture.

Par exemple pour la sélection des données : typiquement, il faut sélectionner une ou plusieurs lactations de une ou plusieurs vaches ? Si c'est le cas, je te suggère de te donner une syntaxe que tu vas ensuite traduire en objets. D'abord la sélection simple ("eglantine:lactation03"), puis des sélections par liste ("eglantine,marguerite:03,06,12") pour sélectionner les lactations 3, 6 et 12 pour deux vaches (soit 6 données), pourquoi pas te donner également des wildcards ("eglantine,marguerite,noiraude:*" pour toutes les lactations de ces 3 vaches ou encore "*:lactation01" pour la première lactation de toutes les vaches).

Ensuite, les objets. Pour démarrer je verrais bien un truc du genre ceci :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class LactationSelector:
    '''
    Sélection d'une lactation particulière pour une vache donnée
    '''
    def __init__(self, lactfile):
        self.lactfile = lactfile  # nom du fichier

    def get(self, cow):
        filename = os.path.join(cow, self.lactfile)
        # récupère les données dans le fichier dont le chemin est contenu dans 'filename'
        # ...
        return data


class DataSelector:
    '''
    Sélecteur pour une seule vache
    '''
    def __init__(self, cow_folder, lactation_selector):
        self.cow = cow_folder
        self.selector = lactation_selector

    def get(self):
        return self.selector.get(self.cow)


class MultiDataSelector:
    '''
    Sélecteur pour une liste de vaches
    '''
    def __init__(self, cow_list, lactation_selector):
        self.selectors = [
            DataSelector(cow, lactation_selector) for cow in cow_list]

    def get(self):
        for sel in selectors:
            local_data = sel.get()
            # récupère les données de chaque sélecteur et agrège-les
            # ...
        return data

Et ainsi de suite (manque le MultiLactationSelector, par exemple).

De cette façon, tu n'as plus qu'à te faire une fonction select_data(dataspec) qui parse la chaîne de caractères, crée les bons sélecteurs et les combine entre eux pour sélectionner les données que tu veux et les retourner, de façon totalement indépendante du reste du soft.

Ça demande donc de définir la forme que tu vas donner à tes données : choisis une et une seule façon de les représenter, pour que les différents sous-modules (sélecteur, processing, production des graphes) partagent le même format.

Là, déjà, tu viens de te donner une jolie syntaxe facile à utiliser en ligne de commande pour sélectionner les vaches et les lactations. Reste une inconnue : quelle tête ont les paramètres inhérents à chaque vache ?

Si y'a vraiment beaucoup de paramètres, alors ce n'est peut-être pas déconnant de te donner le moyen de les parser dans un fichier de configuration (par exemple en Yaml, ou bien si tu préfères utiliser un module standard, avec un configparser).

+1 -0

Par ailleurs, l'avantage de ce genre d'abstraction, c'est que si demain tu comptes utiliser une BDD, genre sqlite, pour stocker tes données, tu n'auras qu'à modifier ta fonction select_data et le reste de ton soft continuera à fonctionner exactement de la même façon. ;)

+1 -0

Merci beaucoup ! Je vais commencer par mettre ça en place. =)

Du coup, j'aurai un script pour parser les arguments puis utilisant ces classes pour récupérer les données ?

Peut-être que LactationSelector n'est pas nécessaire si je travaille sur des galeries. Comme il n'y aura pas plus de 10-12 lactations, je pourrai mettre tous les graphes sur la même image et donc j'appliquerai nécessairement mon traitement à toutes les lactations. Edit : en fait si, pour simplifier la classe DataSelector.

Après, le souci est que ces classes me retournes les données brutes, sans informations sur la vache, la lactation… ce dont j'aurai besoin au moment de la sauvegarde et pour identifier les analyses (ce tableau contient les productions lissées de telle lactation de telle vache). Va-t-il falloir que je crée une autre classe contenant :

  • La liste des vaches considérées (puisqu'on ne considère plus les lactations individuellement)
  • Les données associées
  • Les résultats des analyses de ces données

et à partir de laquelle je pourrai facilement enregistrer mes images sous la forme, processing/processing-type/cow.png. Par exemple : processing/moving-average/2251.png. Au passage, processing est-il le mot le plus adapté pour traduire, dans ce contexte, "analyses" ?

Je ne comprends pas trop la question sur les paramètres. A chaque vache est associé un ensemble de lactations contenant :

  • un ensemble de couples (jour, production)
  • prochainement : un ensemble de couples (jour, quantité de granulés)

Les paramètres sont donc surtout nécessaires pour les scripts de traitement : pour la moyenne mobile, combien de valeurs je considère ? Combien de fois j'itère le lissage ? …

Encore merci !

+0 -0

Du coup, j'aurai un script pour parser les arguments puis utilisant ces classes pour récupérer les données ?

Eh bien disons que ça te donne un module et surtout une fonction pour la sélection des données. Ça ne fait pas tout le script mais ça permet au moins d'avoir cette partie de résolue (et un moyen de passer cet argument de sélection en ligne de commandes).

Pour le data, je t'invite à remarquer que j'ai laissé tout ce qui en dépendait volontairement blanc : rien ne t'empêche d'inclure n'importe quelle information qui te semble pertinente dans ta classe Data, pas seulement les données calculatoires. ;)

Pour les autres paramètres tu peux facilement t'en sortir avec docopt et un choix judicieux des paramètres par défaut.

+0 -0

Suite à quelques réflexions, l'organisation suivante me semble judicieuse :

  • data
    • productions
      • 2251
        • 1.csv : les (x, y) d'origine
        • 2.csv
        • graph.png
    • feeds : les quantités de granulés. Prochainement.
    • least-squares : traitement principal
      • 2251
        • 1.productions.csv : la pente et l'ordonnée à l'origine de la droite
        • graph.productions.png
      • moving-average : traitement secondaire
        • 2251`
          • graph.productions.png
    • moving-average
      • 2251
        • 1.productions.csv : les (x, y) lissés
        • 2.productions.csv
        • graph.productions.png

Un fichier serait donc identifié de la sorte :

  • traitements : droite des moindres carrés après moyenne mobile
  • vache : 2251
  • lactation : 1
  • type de données : productions

Un traitement consisterait alors à prendre un/des fichiers en entrée et à en générer un autre en sortie. Le chemin de la sortie permettrait de connaître les traitements effectués et les paramètres utilisés. Par exemple :

  • Programme : moving-average.py --step 3 --rep 1
  • Entrée : productions quotidiennes de la vache 2251 (les data/productions/2251/x.csv)
  • Sortie : les data/moving-average/2251/x.productions.step-3_rep-1.csv et data/moving-average/2251/graph.productions.step-3_rep-1.png

Ou encore :

  • Programme : least-squares.py --thresh 50
  • Entrée : les data/moving-average/2251/x.productions.step-3_rep-1.csv
  • Sortie : data/least-squares/moving-average/2251/graph.productions.thresh-50_step-3_rep-1.png

Il faut juste que je trouve comment simplement relier le programme, les entrées et les sorties pour ne pas à avoir à créer les dossiers manuellement, en fonction du type de traitement.

Merci. =)

+0 -0

Le robot de traite permet d'exporter les données comme ça :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
Numéro de l'animal  Production quotidienne  Cons. en concentrés Date    Jours en lactation  Lactation
8941    29,10   6,0 01/01/2013  161 2
8933    27,75   5,0 01/01/2013  216 2
3500            01/01/2013      
8959    11,94   2,7 01/01/2013  113 2
3138    22,97   4,6 01/01/2013  58  2
8936    27,22   6,1 01/01/2013  100 3
...
3230            02/01/2013      
8959    23,70   7,2 02/01/2013  114 2
8936    27,57   6,1 02/01/2013  101 3
3524            02/01/2013      
3138    22,97   4,6 02/01/2013  59  2
...

Du coup, je pense que je vais les organiser ainsi, pour une vache donnée :

1
2
3
4
Date    Production  Consommation    Jours en lactation  Lactation
01/01/2014  10.5    5.5 3   2
02/01/2014  10.5    5.5 3   2
... 

Et j'aurais une structure de dossiers comme la suivante, puisqu'il me semble inutile de séparer les lactations : on travaillera sur une période (par exemple, de 2010 à aujourd'hui). Quoique… à approfondir.

  • data
    • crude
      • 2251
        • data.csv
        • graph.productions.png
    • least-squares : traitement principal
      • 2251
        • productions.csv : la pente et l'ordonnée à l'origine de la droite
        • graph.productions.png
      • moving-average : traitement secondaire
        • 2251`
          • graph.productions.png
    • moving-average
      • 2251
        • productions.csv : les (x, y) lissés
        • graph.productions.png

Seulement, je me pose la question du format des données. Pour certains traitements, j'ai beaucoup de lignes : données brutes, données lissées… Et le CSV me semble approprié. Mais pour d'autre, comme les moindres carrés, je n'aurai qu'une seule ligne (la pente de la droite et son ordonnée à l'origine) et là le JSON paraît plus adéquate. Je ne vois pas trop comment uniformiser tout ça de sorte que mes programmes de traitement n'aient pas à parser les entrées de multiples manières selon le cas.

En ce qui concerne les instructions pour les traitements, je pourrais avoir quelque chose de la sorte :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
{
    "data": "productions",
    "operations": [
        { 
            "name": "moving-average",
            "step": 3,
            "rep": 1
        }, 
        { 
            "name": "least-squares",
            "threshold": 50
        }
    ],
    "cows": ["2251", "3138"],
    "dest": "least-squares/moving-average/"
}

La lecture de ce fichier s'effectuerait ainsi :

  • Vache : 2251
  • Pour least-squares, il me faut moving-average
  • Je cherche data/moving-average/2251/productions.csv
    • Existe : rien à faire
    • N'existe pas : on exécute moving-average sur les productions de 2251 (comment les récupérer depuis data/crude/2251/data.csv ?) avec les paramètres spécifiés
  • Je récupère data/moving-average/2251/productions.csv
  • J'exécute least-squares sur ces données, avec les paramètres spécifiés
  • J'enregistre le résultat dans least-squares/moving-average/2251/graph.productions.png

Néanmoins, plusieurs problèmes se posent :

Si pour le seuil des moindres carrés je veux donner le jour où la production est maximale, comment fais-je ? Va-t-il falloir que j'invente une syntaxe pour dire ça dans mon fichier JSON ?

Comment spécifier le type d'entrée ? Par exemple, je pourrais vouloir :

  • Les productions en fonction de la date
  • Les productions en fonction du jour de lactation
  • La consommation en fonction de la production
  • Les productions lissées en fonction de la date

Faudrait-il que je génère les trois premières automatiquement et que je ne conserve pas le fichier data.csv ?

Enfin, est-il préférable de considérer les lactations une à une ou de travailler sur une période ?

Merci !

+0 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte