From 81fa9cc575a33d86316e621e213cb2810af63679 Mon Sep 17 00:00:00 2001 From: andis Date: Fri, 11 Aug 2017 20:34:17 +0200 Subject: [PATCH] div tzg --- classifier/classifier.py | 29 +++++--- classifier/training.py | 2 +- create_migration | 27 ++++++++ data.yml | 70 ++++++++++---------- db_repository/versions/005_answered_lang.py | 39 +++++++++++ flaskapp/__init__.py | 58 ++++++++++++---- flaskapp/templates/index.html | 19 +++--- run.py | 28 +++++--- storage/thread_model.py | 9 ++- test.sqlite | Bin 11720704 -> 11720704 bytes 10 files changed, 202 insertions(+), 79 deletions(-) create mode 100755 create_migration create mode 100644 db_repository/versions/005_answered_lang.py diff --git a/classifier/classifier.py b/classifier/classifier.py index 51c87fc..b332771 100644 --- a/classifier/classifier.py +++ b/classifier/classifier.py @@ -41,7 +41,7 @@ def store_training_data(i, d,key=b"answered"): # Lade Trainingsdaten fuer einen angegebenen key (Label/Eigenschaft) -def get_training_threads(key="answered", filter=[]): +def get_training_threads(key="answered", filters=[]): if not data_types.has_key(key): raise ValueError("Key "+str(key)+" unknown") #------------------------------------ @@ -49,12 +49,23 @@ def get_training_threads(key="answered", filter=[]): d_a=[] d_a2=[] #------------------------------------ - for i in train: - if train[i].has_key(key): # In den Trainingsdaten muss der relevante Key sein - t=db_session.query(MailThread).filter(MailThread.firstmail==i).first() - if not t is None: # Thread muss in der Datenbank sein - t_a.append(t) - d_a.append(train[i][key]) + if "db" in filters: + tt=db_session.query(MailThread).filter(MailThread.istrained==True).all() + for t in tt: + t_a.append(t) + if key =="answered": + d_a.append(t.answered) + elif key=="maintopic": + d_a.append(t.maintopic) + + + else: + for i in train: + if train[i].has_key(key): # In den Trainingsdaten muss der relevante Key sein + t=db_session.query(MailThread).filter(MailThread.firstmail==i).first() + if not t is None: # Thread muss in der Datenbank sein + t_a.append(t) + d_a.append(train[i][key]) le=LabelEncoder() d_a2=le.fit_transform(d_a) return (t_a,d_a2,le) @@ -91,9 +102,9 @@ class ThreadTextExtractor(BaseEstimator, TransformerMixin): def transform(self, X,y=None): return [t.text() for t in X] -def get_pipe(p=b"pipe1",k=b"answered"): +def get_pipe(p=b"pipe1",k=b"answered",filters=[]): p=build_pipe(p) - tt= get_training_threads(k) + tt= get_training_threads(k,filters) if len(tt[0]) > 0: p.fit(tt[0],tt[1]) return p,tt[2] diff --git a/classifier/training.py b/classifier/training.py index cf68507..f340d3d 100644 --- a/classifier/training.py +++ b/classifier/training.py @@ -60,7 +60,7 @@ def train_single_thread(tid,p,le,key="answered"): l=le.inverse_transform([ca])[0] if type(l) is numpy.bool_: l=bool(l) - if type(l) is numpy.string_: + if type(l) is numpy.string_ or type(l) is numpy.unicode_: l=str(l) store_training_data(tid,l, key) elif not ca.strip() == "": diff --git a/create_migration b/create_migration new file mode 100755 index 0000000..5d39afb --- /dev/null +++ b/create_migration @@ -0,0 +1,27 @@ +#!/bin/bash +if [ $# -eq 0 ] +then +echo "No Arguments supplied" +exit +fi + +echo "creating a new migration" +./migration.py compare_model_to_db storage.metadata + +echo "Dump current database state to file" +./migration.py create_model > oldmodel.py + +ls db_repository/versions +echo "Choose a filename for the new migration" +read filename + +./migration.py make_update_script_for_model --oldmodel=oldmodel:meta --model=storage:metadata > db_repository/versions/$filename.py + +cp test.sqlite test.sqlite.bak +./migration.py test +rm test.sqlite +mv test.sqlite.bak test.sqlite + + + +rm oldmodel.py \ No newline at end of file diff --git a/data.yml b/data.yml index 0a3f95b..9334d62 100644 --- a/data.yml +++ b/data.yml @@ -1,38 +1,38 @@ -{26808: {maintopic: jobausschreibung}, 27008: {lang: de}, 27017: {lang: de, maintopic: jobausschreibung}, - 27061: {lang: de}, 27070: {maintopic: ausleihen}, 27083: {maintopic: ausleihen}, - 27086: {maintopic: information}, 27094: {maintopic: information}, 27096: {maintopic: jobausschreibung}, - 27102: {lang: en, maintopic: studium}, 27118: {maintopic: information}, 27127: { - maintopic: studium}, 27130: {maintopic: information}, 27133: {maintopic: information}, - 27141: {maintopic: information}, 27146: {maintopic: information}, 27166: {maintopic: umfragen}, - 27171: {maintopic: ausleihen}, 27178: {maintopic: studium}, 27182: {maintopic: studium}, - 27197: {maintopic: information}, 27201: {maintopic: information}, 27218: {maintopic: information}, - 27219: {maintopic: studium}, 27222: {maintopic: information}, 27226: {maintopic: ausleihen}, - 27263: {maintopic: ausleihen}, 27267: {maintopic: ausleihen}, 27420: {answered: true, - maintopic: studium}, 27422: {answered: true, maintopic: studium}, 27425: {answered: false, - maintopic: studium}, 27431: {answered: false, maintopic: information}, 27434: { - answered: false, lang: de, maintopic: information}, 27435: {answered: false}, - 27438: {answered: false, maintopic: information}, 27439: {answered: true, maintopic: studium}, - 27441: {answered: false, maintopic: studium}, 27444: {answered: true, maintopic: ausleihen}, - 27454: {answered: false, maintopic: information}, 27455: {answered: false, maintopic: information}, - 27456: {answered: false, lang: de, maintopic: studium}, 27457: {answered: false, - maintopic: jobausschreibung}, 27468: {answered: true, maintopic: studium}, 27489: { - answered: false, lang: en, maintopic: information}, 27490: {answered: false, maintopic: fachschaftenzeugs}, - 27491: {answered: false, maintopic: jobausschreibung}, 27492: {answered: false, - maintopic: information}, 27495: {answered: false, maintopic: information}, 27496: { - answered: true, maintopic: ausleihen}, 27497: {answered: false, maintopic: information}, - 27500: {answered: true, lang: en, maintopic: studium}, 27501: {answered: false, - lang: en, maintopic: information}, 27514: {answered: true, maintopic: studium}, - 27515: {answered: true, lang: en, maintopic: studium}, 27518: {answered: true, maintopic: studium}, - 27523: {answered: false, maintopic: jobausschreibung}, 27526: {answered: false, - maintopic: studium}, 27536: {answered: true, lang: de, maintopic: studium}, 27541: { - answered: true, maintopic: studium}, 27542: {answered: false, maintopic: studium}, - 27543: {answered: false, maintopic: information}, 27544: {answered: true, maintopic: studium}, - 27545: {answered: false, maintopic: umfragen}, 27546: {answered: false, maintopic: information}, - 27547: {answered: false, maintopic: studium}, 27549: {answered: false}, 27550: { - answered: false, maintopic: information}, 27553: {answered: false, maintopic: information}, - 27558: {answered: false}, 27560: {answered: false, maintopic: ausleihen}, 27562: { - answered: false}, 27564: {answered: false, maintopic: jobausschreibung}, 27565: { - answered: true, maintopic: ausleihen}, 27566: {answered: false, maintopic: information}, +{26808: {maintopic: jobausschreibung}, 26992: {maintopic: jobausschreibung}, 27008: { + lang: de}, 27017: {lang: de, maintopic: jobausschreibung}, 27061: {lang: de}, + 27070: {maintopic: ausleihen}, 27083: {maintopic: ausleihen}, 27086: {maintopic: information}, + 27094: {maintopic: information}, 27096: {maintopic: jobausschreibung}, 27102: { + lang: en, maintopic: studium}, 27118: {maintopic: information}, 27127: {maintopic: studium}, + 27130: {maintopic: information}, 27133: {maintopic: information}, 27141: {maintopic: information}, + 27146: {maintopic: information}, 27166: {maintopic: umfragen}, 27171: {maintopic: ausleihen}, + 27178: {maintopic: studium}, 27182: {maintopic: studium}, 27197: {maintopic: information}, + 27201: {maintopic: information}, 27218: {maintopic: information}, 27219: {maintopic: studium}, + 27222: {maintopic: information}, 27226: {maintopic: ausleihen}, 27263: {maintopic: ausleihen}, + 27267: {maintopic: ausleihen}, 27420: {answered: true, maintopic: studium}, 27422: { + answered: true, maintopic: studium}, 27425: {answered: false, maintopic: studium}, + 27431: {answered: false, maintopic: information}, 27434: {answered: false, lang: de, + maintopic: information}, 27435: {answered: false}, 27438: {answered: false, maintopic: information}, + 27439: {answered: true, maintopic: studium}, 27441: {answered: false, maintopic: studium}, + 27444: {answered: true, maintopic: ausleihen}, 27454: {answered: false, maintopic: information}, + 27455: {answered: false, maintopic: information}, 27456: {answered: false, lang: de, + maintopic: studium}, 27457: {answered: false, maintopic: jobausschreibung}, 27468: { + answered: true, maintopic: studium}, 27489: {answered: false, lang: en, maintopic: information}, + 27490: {answered: false, maintopic: fachschaftenzeugs}, 27491: {answered: false, + maintopic: jobausschreibung}, 27492: {answered: false, maintopic: information}, + 27495: {answered: false, maintopic: information}, 27496: {answered: true, maintopic: ausleihen}, + 27497: {answered: false, maintopic: information}, 27500: {answered: true, lang: en, + maintopic: studium}, 27501: {answered: false, lang: en, maintopic: information}, + 27514: {answered: true, maintopic: studium}, 27515: {answered: true, lang: en, maintopic: studium}, + 27518: {answered: true, maintopic: studium}, 27523: {answered: false, maintopic: jobausschreibung}, + 27526: {answered: false, maintopic: studium}, 27536: {answered: true, lang: de, + maintopic: studium}, 27541: {answered: true, maintopic: studium}, 27542: {answered: false, + maintopic: studium}, 27543: {answered: false, maintopic: information}, 27544: { + answered: true, maintopic: studium}, 27545: {answered: false, maintopic: umfragen}, + 27546: {answered: false, maintopic: information}, 27547: {answered: false, maintopic: studium}, + 27549: {answered: false}, 27550: {answered: false, maintopic: information}, 27553: { + answered: false, maintopic: information}, 27558: {answered: false}, 27560: {answered: false, + maintopic: ausleihen}, 27562: {answered: false}, 27564: {answered: false, maintopic: jobausschreibung}, + 27565: {answered: true, maintopic: ausleihen}, 27566: {answered: false, maintopic: information}, 27567: {answered: false, maintopic: information}, 27568: {answered: false}, 27575: { answered: false, maintopic: information}, 27577: {answered: false, maintopic: information}, 27579: {answered: true, maintopic: diplomarbeit}, 27582: {answered: false, maintopic: studium}, diff --git a/db_repository/versions/005_answered_lang.py b/db_repository/versions/005_answered_lang.py new file mode 100644 index 0000000..84e096b --- /dev/null +++ b/db_repository/versions/005_answered_lang.py @@ -0,0 +1,39 @@ +from sqlalchemy import * +from migrate import * + + +from migrate.changeset import schema +pre_meta = MetaData() +post_meta = MetaData() +threads = Table('threads', post_meta, + Column('created_at', TIMESTAMP, nullable=False), + Column('updated_at', TIMESTAMP, nullable=False), + Column('id', Integer, primary_key=True, nullable=False), + Column('firstmail', Integer), + Column('date', DateTime), + Column('islabeled', Boolean), + Column('istrained', Boolean), + Column('opened', Boolean), + Column('body', Text), + Column('maintopic', String), + Column('lang', String), + Column('answered', String), +) + + +def upgrade(migrate_engine): + # Upgrade operations go here. Don't create your own engine; bind + # migrate_engine to your metadata + pre_meta.bind = migrate_engine + post_meta.bind = migrate_engine + post_meta.tables['threads'].columns['answered'].create() + post_meta.tables['threads'].columns['lang'].create() + + +def downgrade(migrate_engine): + # Operations to reverse the above upgrade go here. + pre_meta.bind = migrate_engine + post_meta.bind = migrate_engine + post_meta.tables['threads'].columns['answered'].drop() + post_meta.tables['threads'].columns['lang'].drop() + diff --git a/flaskapp/__init__.py b/flaskapp/__init__.py index 5899ea6..2a43be7 100644 --- a/flaskapp/__init__.py +++ b/flaskapp/__init__.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- import flask -from flask import Flask,jsonify,send_from_directory, render_template +from flask import Flask,jsonify,send_from_directory, render_template, request,redirect,url_for from config import Config import yaml import os @@ -14,12 +14,12 @@ package_directory = os.path.dirname(os.path.abspath(__file__)) cfg = Config(file(os.path.join(package_directory, 'config.cfg'))) -def render_index(mths,code=200): +def render_index(mths,opened=None,code=200): return render_template("index.html",mths=mths, - title=cfg.title.decode("utf8"), + title=cfg.title.decode("utf8"),opened=opened ), code from classifier import get_pipe -mail_threads=db_session.query(MailThread).all() +#mail_threads=db_session.query(MailThread).all() #pipe1,le=get_pipe("pipe1",b"answered") #pipe2,le2=get_pipe("pipe2b", b"maintopic") #pipe3,le3=get_pipe("pipe2b", b"lang") @@ -33,28 +33,62 @@ mail_threads=db_session.query(MailThread).all() # t.maintopic=maintopic[i] # t.lang=lang[i] +maintopic_values=["studium", "information","ausleihen"] + @app.route("/") def hello(): mth=db_session.query(MailThread).order_by(desc(MailThread.date)).all() return render_index(mth) -@app.route("/answered//") -def store_answered(id, value): +def store_value(id,key,value): mth=db_session.query(MailThread).filter(MailThread.firstmail==int(id)).first() - value= value in ["true", "True", "1", "t"] - mth.answered=bool(value) - mth.opened=bool(value) - return render_index([mth]) + + if key =="answered": + value = value in ["true", "True", "1", "t"] + mth.answered=bool(value) + mth.opened=bool(value) + if key=="maintopic" and value in maintopic_values: + mth.maintopic=str(value) + if key =="trained": + value = value in ["true", "True", "1", "t"] + mth.istrained=bool(value) + +@app.route("/") +def store_answered(id): + + key = request.args.get('key') + value = request.args.get('value') + if not key is None and not value is None: + store_value(id,key,value) + + return render_index([mth], opened=id) -@app.route("/studium") +@app.route("/studium/") def studium(): mth=db_session.query(MailThread).filter(MailThread.maintopic=="studium").order_by(desc(MailThread.date)).all() return render_index(mth) -@app.route("/") +@app.route("//") def maintopic(maintopic): mth=db_session.query(MailThread).filter(MailThread.maintopic=="%s" % maintopic).order_by(desc(MailThread.date)).all() return render_index(mth) + +@app.route("//") +def maintopic_store(maintopic,id): + if maintopic == "trained": + mth=db_session.query(MailThread).filter(MailThread.istrained==True).order_by(desc(MailThread.date)).all() + else: + mth=db_session.query(MailThread).filter(MailThread.maintopic=="%s" % maintopic).order_by(desc(MailThread.date)).all() + + key = request.args.get('key') + value = request.args.get('value') + + + if not key is None and not value is None: + store_value(id,key,value) + return redirect(url_for('maintopic_store', id=id, maintopic=maintopic), 302) + else: + return render_index(mth,opened=id) diff --git a/flaskapp/templates/index.html b/flaskapp/templates/index.html index dc96d23..172aa41 100644 --- a/flaskapp/templates/index.html +++ b/flaskapp/templates/index.html @@ -1,13 +1,13 @@ {{title}} - - - + + + @@ -19,19 +19,20 @@
{% for m in mths %} -
+
-
+
-
- {{m.maintopic}} +
+ answered:{{(not m.is_answered())}} + {{m.maintopic}}, {{ m.istrained }} trained:{{(not m.istrained)}}
{{ m.print_text() }}
diff --git a/run.py b/run.py index 9544bf4..4406645 100644 --- a/run.py +++ b/run.py @@ -34,18 +34,18 @@ if len(sys.argv)>1: pipe1,le=get_pipe("pipe1",b"answered") pipe2,le2=get_pipe("pipe2b", b"maintopic") pipe3,le3=get_pipe("pipe2b", b"lang") - mail_threads=db_session.query(MailThread).all() + mail_threads=db_session.query(MailThread).filter(MailThread.istrained==False).all() answered=le.inverse_transform(pipe1.predict(mail_threads)) maintopic=le2.inverse_transform(pipe2.predict(mail_threads)) lang=le3.inverse_transform(pipe3.predict(mail_threads)) for i, t in enumerate(mail_threads): - t.answered=answered[i] - t.opened=answered[i] + t.answered=bool(answered[i]) + t.opened=bool(answered[i]) - t.maintopic=maintopic[i] - t.lang=lang[i] + t.maintopic=str(maintopic[i]) + t.lang=str(lang[i]) db_session.add(t) db_session.commit() @@ -74,8 +74,15 @@ if len(sys.argv)>1: mth=db_session.query(MailThread).all() for t in mth: t.compile() - - + + if sys.argv[1] == "trained_threads_from_yml": + from classifier.classifier import train + for k in train: + print k + t=db_session.query(MailThread).filter(MailThread.firstmail==k).first() + t.istrained=True + db_session.add(t) + db_session.commit() if sys.argv[1] == "print_threads2": mth=db_session.query(MailThread).all() for t in mth: @@ -83,8 +90,8 @@ if len(sys.argv)>1: print "---------------\n" if sys.argv[1] == "train_thrd2": - p, le=get_pipe("pipe2", "maintopic") - pb, lb =get_pipe("pipe2b", "maintopic") + p, le=get_pipe("pipe2", "maintopic",["db"]) + pb, lb =get_pipe("pipe2b", "maintopic",["db"]) train_single_thread(int(sys.argv[2]),p,le,b"maintopic") @@ -120,7 +127,8 @@ if len(sys.argv)>1: t=db_session.query(MailThread).filter(MailThread.firstmail==sys.argv[2]).first() print t.to_text() print le.inverse_transform(pipe2.predict([t])) - + + if sys.argv[1] == "train_thrd": pipe1, labelencoder=train_fit_pipe() diff --git a/storage/thread_model.py b/storage/thread_model.py index 79c3e63..69b96fa 100644 --- a/storage/thread_model.py +++ b/storage/thread_model.py @@ -29,16 +29,19 @@ class MailThread(Base): opened = Column(Boolean) body = Column(Text) maintopic=Column(String) + lang=Column(String) + answered=Column(String) __schema__=FullThreadSchema __jsonid__='thread' __whiteattrs__= ["body"] __jsonattrs__=None - answered=False +# answered=False # maintopic="information" - lang="" +# lang="" def bdy(self): return yaml.load(self.body) - + def is_answered(self): + return self.answered in ["1", "true", "True", "t","T"] def to_text(self): mmm=self.mails() txt="" diff --git a/test.sqlite b/test.sqlite index 21a1c0a49dde7e2793db38ddc32f1cd8da4b33f4..5ef28748012ed0d1abccf7c07d307d93e0650562 100644 GIT binary patch delta 12684 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 delta 11311 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 bAK^TS^AyfOoTqV~!Fl%3sF(ib%c=hd1oeLU