DSL в Python для табличного анализа и обработки? - PullRequest
0 голосов
/ 12 января 2012

У меня есть идея создать ограниченную форму языка для табличного анализа. Вопрос в том, есть ли уже что-то вроде следующего или есть лучшие идеи для этого. Ключевое требование заключается в том, что пользователь должен иметь возможность использовать голые файлы сценариев без необходимости создавать полноценную программу на Python. OTOH-элементы языка сценариев компилируются во время выполнения и должны предлагать возможность формулировать сложные условия и вычисления (в основном, арифметические и строковые операции). Декларативный способ формулирования программ на языке (см. Ниже) запрещает прямое использование синтаксиса Python в качестве средства для языка (см. Значение функции @PART) - по крайней мере, я так думаю. Есть ли лучший / более умный / более элегантный способ достижения моих целей, чем программировать в полуразборной смеси Python и неуклюжего самоопределенного синтаксиса, как я делал ниже?

Ниже я попытаюсь прояснить свои идеи на примере. Таблица ввода создается другой программной частью и выглядит так при запуске интерпретации скрипта:

# First,Last,Department,Hourly Wage
[ ('Greg','Foo','HR',100),
  ('Judy','Bar','EE',51),
  ('Jake','Baz','HR',75),
  ('Lila','Bax','HR',49),
  ('Norm','Fob','EE',49) ]

Ниже приведен сам файл 'script'. Это будет файл для себя в производственной системе. Код программы в настоящее время представлен в виде массива строк Python - возможно, даже не в окончательной версии.

# A program to produce per department the average hourly rate, separated for the higher and lower 50% of earners: 
[ "@SORT(2,-3)", 
     "@SET({max},@MAX({3}))",
     "@PART({2}!={^2} or {3}<{max}/2)",
        "@SET({dep},@FIRST({2}))",
        "@PRINT({dep},float(@SUM({3}))/@CNT({3}))"
]

Я постараюсь шаг за шагом объяснить, что должен делать скрипт:

"@SORT(2,-3)" 

сортирует таблицу после столбца 2 (по возрастанию), затем по столбцу 3 (по убыванию). Мы получаем

[ ('Judy','Bar','EE',51),
  ('Norm','Fob','EE',49),
  ('Greg','Foo','HR',100),
  ('Jake','Baz','HR',75),
  ('Lila','Bax','HR',49),
]

"@SET({max},@MAX({3}))" 

берет максимум столбца 3 и помещает его в динамическую локальную переменную max

"@PART({2}!={^2} or {3}<{max}/2)" 

немного сложнее. @PART разделяет текущую таблицу в несколько вложенных таблиц, оценивая данное выражение для каждой строки и обрезая перед строкой, если это правда. Здесь мы хотим сократить границы департамента (столбец 2). {^ 2} является восходящей ссылкой, то есть элементом в столбце 2 из предыдущего ряда. Этот синтаксис необходим, поскольку я считаю, что возможность разбивать таблицы на более сложные условия чем «строка отличается от предыдущей строки в X» очень важно (представьте, что вы хотите разбить таблицу на классы с доходом 10 тыс.) поэтому мне нужна выразительная сила (ограниченного) выражения Python в аргументе PART. Также это имеет значение что выражение не может быть оценено для первой строки, так как нет предшественника, поэтому функция PART просто пойдет через это. После этой функции у нас есть следующие таблицы:

[ ('Judy','Bar','EE',51) ] # Department EE

[ ('Norm','Fob','EE',49) ] # Norm Fob is in the same department but earns less than half of the maximum

[ ('Greg','Foo','HR',100), # New department HR
  ('Jake','Baz','HR',75) ]

[ ('Lila','Bax','HR',49) ] # HR dept. but less than half of the best earner

С этого момента функции в скрипте будут работать с каждой дополнительной таблицей отдельно. Функция PART более или менее запускает цикл над всеми полученными вложенными таблицами, и каждая последующая функция (включая больше PART) выполняется субтаблица в изоляции.

"@SET({dep},@FIRST({2}))",
"@PRINT({dep},float(@SUM({3}))/@CNT({3}))"

@ FIRST ({2}) просто принимает значение столбца 2 первой строки. @SUM ({3}) принимает сумму всего столбца 3 и @CNT ({3}) считает количество строк, столбец 3 которых не равен None. Я представляю результат функции примерно здесь:

[ ('Judy','Bar','EE',51) ]
        "@SET({dep},@FIRST({2}))"  --> {dep} = "EE"
        "@PRINT({dep},float(@SUM({3}))/@CNT({3}))"  --> output  "EE 51"

[ ('Norm','Fob','EE',49) ]
        "@SET({dep},@FIRST({2}))", --> {dep} = "EE"
        "@PRINT({dep},float(@SUM({3}))/@CNT({3}))"  --> output  "EE 49"

[ ('Greg','Foo','HR',100), 
  ('Jake','Baz','HR',75) ]
        "@SET({dep},@FIRST({2}))", --> {dep} = "HR"
        "@PRINT({dep},float(@SUM({3}))/@CNT({3}))"  --> output  "HR 87.5"

[ ('Lila','Bax','HR',49) ] 
        "@SET({dep},@FIRST({2}))", --> {dep} = "HR"
        "@PRINT({dep},float(@SUM({3}))/@CNT({3}))"  --> output  "HR 49"

Я должен добавить, что предпочел бы, чтобы решение было небольшим, то есть не использовать нестандартные пакеты Python, такие как pyparsing и т. Д.

1 Ответ

0 голосов
/ 14 января 2012

littletable - это модуль, который я написал, чтобы сделать некоторые из этого табличного анализа списка похожих элементов. littletable не использует SQL для выбора и запросов, но поля могут быть проиндексированы, а таблицы могут выполнять соединения, сводки и запросы. Таблицы могут рассматриваться как списки Python. Вероятно, самая большая философская мысль о littletable заключается в том, что каждое соединение, запрос и т. Д. Возвращает новую таблицу, так что сложное выражение может быть построено из промежуточных объединений и запросов. Вот несколько примеров манипулирования вашими данными с помощью littletable:

attrs = "Id,First,Last,Department,Hourly_Wage".split(',') 
data = [ (1, 'Greg','Foo','HR',100), 
          (2, 'Judy','Bar','EE',51), 
          (3, 'Jake','Bar','HR',75), 
          (4, 'Lila','Bax','HR',49), 
          (5, 'Norm','Fob','EE',49) ] 

from littletable import Table, DataObject

instructors = Table()
instructors.insert_many(
    DataObject(**dict(zip(attrs,d))) 
        for d in data)

# can add index before or after items are added to table
instructors.create_index("Id", unique=True)
instructors.create_index("Department")

# unique keys are enforced
try:
    instructors.insert(DataObject(Id=4, First="Bob", Last="Fob"))
except KeyError as e:
    print e

# keys checked for uniqueness when creating unique index
try:
    instructors.create_index("Last", unique=True)
except KeyError as e:
    print e

# Uniquely indexed access gives a single record
print "%(First)s %(Last)s" % instructors.by.Id[3]

# Non-uniquely indexed access gives a new Table
print '\n'.join("%(Department)s %(First)s %(Last)s" % inst 
                    for inst in instructors.by.Department["HR"])

# Table can still be accessed like a Python list
print "%(First)s %(Last)s" % instructors[-1]
print '\n'.join("%(Department)s %(First)s %(Last)s" % inst 
                    for inst in instructors)

# use pivot for multi-dimensional grouping
instructors.addfield("wage_bracket", lambda d:d.Hourly_Wage/10*10)
instructors.create_index("wage_bracket")
instructors.pivot("Department wage_bracket").dump()
instructors.pivot("Department wage_bracket").dump_counts()

import sys
instructors.csv_export(sys.stdout)

печать:

("duplicate unique key value '4' for index Id", {'Last': 'Fob', 'Id': 4, 'First': 'Bob'})
'duplicate key value Bar'
Jake Bar
HR Greg Foo
HR Jake Bar
HR Lila Bax
Norm Fob
HR Greg Foo
EE Judy Bar
HR Jake Bar
HR Lila Bax
EE Norm Fob
Pivot: Department,wage_bracket
  Department:EE
    Department:EE/wage_bracket:40
      {'Last': 'Fob', 'Hourly_Wage': 49, 'Department': 'EE', 'wage_bracket': 40, 'Id': 5, 'First': 'Norm'}
    Department:EE/wage_bracket:50
      {'Last': 'Bar', 'Hourly_Wage': 51, 'Department': 'EE', 'wage_bracket': 50, 'Id': 2, 'First': 'Judy'}
  Department:HR
    Department:HR/wage_bracket:40
      {'Last': 'Bax', 'Hourly_Wage': 49, 'Department': 'HR', 'wage_bracket': 40, 'Id': 4, 'First': 'Lila'}
    Department:HR/wage_bracket:70
      {'Last': 'Bar', 'Hourly_Wage': 75, 'Department': 'HR', 'wage_bracket': 70, 'Id': 3, 'First': 'Jake'}
    Department:HR/wage_bracket:100
      {'Last': 'Foo', 'Hourly_Wage': 100, 'Department': 'HR', 'wage_bracket': 100, 'Id': 1, 'First': 'Greg'}
Pivot: Department,wage_bracket
              40         50         70        100      Total
EE             1          1          0          0          2
HR             1          0          1          1          3
Total          2          1          1          1          5
Last,Hourly_Wage,Department,wage_bracket,Id,First
Foo,100,HR,100,1,Greg
Bar,51,EE,50,2,Judy
Bar,75,HR,70,3,Jake
Bax,49,HR,40,4,Lila
Fob,49,EE,40,5,Norm
...