Je cherche un moyen simple d’analyser des fichiers texte complexes dans un pandas DataFrame. Vous trouverez ci-dessous un exemple de fichier, ce que je veux que le résultat ressemble à après l’analyse et ma méthode actuelle.
Y a-t-il un moyen de le rendre plus concis/plus rapide/plus Pythonic/plus lisible?
J'ai également posé cette question sur Code Review .
J'ai finalement écrit un article de blog pour expliquer cela aux débutants .
Voici un exemple de fichier:
Sample text
A selection of students from Riverdale High and Hogwarts took part in a quiz. This is a record of their scores.
School = Riverdale High
Grade = 1
Student number, Name
0, Phoebe
1, Rachel
Student number, Score
0, 3
1, 7
Grade = 2
Student number, Name
0, Angela
1, Tristan
2, Aurora
Student number, Score
0, 6
1, 3
2, 9
School = Hogwarts
Grade = 1
Student number, Name
0, Ginny
1, Luna
Student number, Score
0, 8
1, 7
Grade = 2
Student number, Name
0, Harry
1, Hermione
Student number, Score
0, 5
1, 10
Grade = 3
Student number, Name
0, Fred
1, George
Student number, Score
0, 0
1, 0
Voici ce que je veux que le résultat ressemble après l'analyse:
Name Score
School Grade Student number
Hogwarts 1 0 Ginny 8
1 Luna 7
2 0 Harry 5
1 Hermione 10
3 0 Fred 0
1 George 0
Riverdale High 1 0 Phoebe 3
1 Rachel 7
2 0 Angela 6
1 Tristan 3
2 Aurora 9
Voici comment je l'analyse actuellement:
import re
import pandas as pd
def parse(filepath):
"""
Parse text at given filepath
Parameters
----------
filepath : str
Filepath for file to be parsed
Returns
-------
data : pd.DataFrame
Parsed data
"""
data = []
with open(filepath, 'r') as file:
line = file.readline()
while line:
reg_match = _RegExLib(line)
if reg_match.school:
school = reg_match.school.group(1)
if reg_match.grade:
grade = reg_match.grade.group(1)
grade = int(grade)
if reg_match.name_score:
value_type = reg_match.name_score.group(1)
line = file.readline()
while line.strip():
number, value = line.strip().split(',')
value = value.strip()
dict_of_data = {
'School': school,
'Grade': grade,
'Student number': number,
value_type: value
}
data.append(dict_of_data)
line = file.readline()
line = file.readline()
data = pd.DataFrame(data)
data.set_index(['School', 'Grade', 'Student number'], inplace=True)
# consolidate df to remove nans
data = data.groupby(level=data.index.names).first()
# upgrade Score from float to integer
data = data.apply(pd.to_numeric, errors='ignore')
return data
class _RegExLib:
"""Set up regular expressions"""
# use https://regexper.com to visualise these if required
_reg_school = re.compile('School = (.*)\n')
_reg_grade = re.compile('Grade = (.*)\n')
_reg_name_score = re.compile('(Name|Score)')
def __init__(self, line):
# check whether line has a positive match with all of the regular expressions
self.school = self._reg_school.match(line)
self.grade = self._reg_grade.match(line)
self.name_score = self._reg_name_score.search(line)
if __name__ == '__main__':
filepath = 'sample.txt'
data = parse(filepath)
print(data)
voici ma suggestion en utilisant split et pd.concat ("txt" représente une copie du texte original de la question), l'idée de base est de scinder le groupe de mots, puis de le concaténer dans des trames de données, l'analyse la plus interne en tire parti du fait que les noms et les grades sont dans un format de type csv. Voici:
import pandas as pd
from io import StringIO
schools = txt.lower().split('school = ')
schools_dfs = []
for school in schools[1:]:
grades = school.split('grade = ')
grades_dfs = []
for grade in grades[1:]:
features = grade.split('student number,')
feature_dfs = []
for feature in features[1:]:
feature_dfs.append(pd.read_csv(StringIO(feature)))
feature_df = pd.concat(feature_dfs, axis=1)
feature_df['grade'] = features[0].replace('\n','')
grades_dfs.append(feature_df)
grades_df = pd.concat(grades_dfs)
grades_df['school'] = grades[0].replace('\n','')
schools_dfs.append(grades_df)
schools_df = pd.concat(schools_dfs)
schools_df.set_index(['school', 'grade'])
Je suggérerais d'utiliser une bibliothèque de combinateur d'analyseurs telle que analyse . Comparé à l'utilisation de regex, le résultat ne sera pas aussi concis, mais il sera beaucoup plus lisible et robuste, tout en restant relativement léger.
L’analyse syntaxique est en général une tâche ardue et il peut être difficile de trouver une approche intéressante pour les débutants en programmation générale.
EDIT: Un exemple de code qui effectue une analyse minimale de votre exemple fourni. Il ne passe pas aux pandas, ni même ne fait correspondre les noms aux partitions, les étudiants aux notes, etc. - il renvoie simplement une hiérarchie d'objets commençant par School
au sommet, avec les attributs pertinents que vous êtes en droit d'attendre:
from parsy import string, regex, seq
import attr
@attr.s
class Student():
name = attr.ib()
number = attr.ib()
@attr.s
class Score():
score = attr.ib()
number = attr.ib()
@attr.s
class Grade():
grade = attr.ib()
students = attr.ib()
scores = attr.ib()
@attr.s
class School():
name = attr.ib()
grades = attr.ib()
integer = regex(r"\d+").map(int)
student_number = integer
score = integer
student_name = regex(r"[^\n]+")
student_def = seq(student_number.tag('number') << string(", "),
student_name.tag('name') << string("\n")).combine_dict(Student)
student_def_list = string("Student number, Name\n") >> student_def.many()
score_def = seq(student_number.tag('number') << string(", "),
score.tag('score') << string("\n")).combine_dict(Score)
score_def_list = string("Student number, Score\n") >> score_def.many()
grade_value = integer
grade_def = string("Grade = ") >> grade_value << string("\n")
school_grade = seq(grade_def.tag('grade'),
student_def_list.tag('students') << regex(r"\n*"),
score_def_list.tag('scores') << regex(r"\n*")
).combine_dict(Grade)
school_name = regex(r"[^\n]+")
school_def = string("School = ") >> school_name << string("\n")
school = seq(school_def.tag('name'),
school_grade.many().tag('grades')
).combine_dict(School)
def parse(text):
return school.many().parse(text)
C'est beaucoup plus détaillé qu'une solution regex, mais beaucoup plus proche d'une définition déclarative de votre format de fichier.
De manière similaire à votre code d'origine, je définis l'analyse de regex
import re
import pandas as pd
parse_re = {
'school': re.compile(r'School = (?P<school>.*)$'),
'grade': re.compile(r'Grade = (?P<grade>\d+)'),
'student': re.compile(r'Student number, (?P<info>\w+)'),
'data': re.compile(r'(?P<number>\d+), (?P<value>.*)$'),
}
def parse(line):
'''parse the line by regex search against possible line formats
returning the id and match result of first matching regex,
or None if no match is found'''
return reduce(lambda (i,m),(id,rx): (i,m) if m else (id, rx.search(line)),
parse_re.items(), (None,None))
parcourez ensuite les lignes rassemblant les informations sur chaque élève. Une fois que l'enregistrement est terminé (lorsque nous avons Score
l'enregistrement est terminé), nous ajoutons l'enregistrement à une liste.
Une petite machine à états pilotée par les correspondances ligne par ligne regex assemble chaque enregistrement. Nous devons en particulier enregistrer les élèves dans une note par numéro car leur score et leur nom sont fournis séparément dans le fichier d'entrée.
results = []
with open('sample.txt') as f:
record = {}
for line in f:
id, match = parse(line)
if match is None:
continue
if id == 'school':
record['School'] = match.group('school')
Elif id == 'grade':
record['Grade'] = int(match.group('grade'))
names = {} # names is a number indexed dictionary of student names
Elif id == 'student':
info = match.group('info')
Elif id == 'data':
number = int(match.group('number'))
value = match.group('value')
if info == 'Name':
names[number] = value
Elif info == 'Score':
record['Student number'] = number
record['Name'] = names[number]
record['Score'] = int(value)
results.append(record.copy())
Enfin, la liste des enregistrements est convertie en DataFrame
.
df = pd.DataFrame(results, columns=['School', 'Grade', 'Student number', 'Name', 'Score'])
print df
Les sorties:
School Grade Student number Name Score
0 Riverdale High 1 0 Phoebe 3
1 Riverdale High 1 1 Rachel 7
2 Riverdale High 2 0 Angela 6
3 Riverdale High 2 1 Tristan 3
4 Riverdale High 2 2 Aurora 9
5 Hogwarts 1 0 Ginny 8
6 Hogwarts 1 1 Luna 7
7 Hogwarts 2 0 Harry 5
8 Hogwarts 2 1 Hermione 10
9 Hogwarts 3 0 Fred 0
10 Hogwarts 3 1 George 0
Certaines optimisations consisteraient à comparer les premières expressions rationnelles les plus courantes et à ignorer explicitement les lignes vides. Construire le cadre de données au fur et à mesure évite des copies supplémentaires des données, mais je suppose que l’ajout d’un fichier à une base de données est une opération coûteuse.