I'm trying to predict what department should recieve a new ticket whenever a customer writes a support ticket. A support ticket will always consist of:
Message: the actual message of the ticket
Subject: the subject of the ticket
Header: and optional header to further describe the issue
Until now, the assignment of a ticket is manual, but I'm trying to create a model that can predict the department this ticket should be assigned to. My training data contains these three variables as well as the department name
Support
Development
...
My training data consists of 169k rows, and there is no issue training the model at all. I'm getting some pretty neat metrics. The issue comes when I try to predict from some kind of ticket. Instead of getting a department name, I'm getting a decimal (2.5xx). I'm not sure where this is going wrong. I've taken inspiration from the following guide from Microsoft: https://learn.microsoft.com/en-us/dotnet/machine-learning/tutorials/github-issue-classification
Loading training data
var dataView = mlContext.Data.LoadFromTextFile<Message>(dataPath, hasHeader: true, allowQuoting: true);
Building and training the model
var pipeline = mlContext.Transforms.Conversion.MapValueToKey(inputColumnName: "Name", outputColumnName: "Label") // Department name
.Append(mlContext.Transforms.Text.FeaturizeText(inputColumnName: "Message", outputColumnName: "MessageFeaturized"))
.Append(mlContext.Transforms.Text.FeaturizeText(inputColumnName: "Subject", outputColumnName: "SubjectFeaturized"))
.Append(mlContext.Transforms.Text.FeaturizeText(inputColumnName: "Header", outputColumnName: "HeaderFeaturized"))
.Append(mlContext.Transforms.Concatenate(
"Features",
"MessageFeaturized",
"SubjectFeaturized",
"HeaderFeaturized"))
.AppendCacheCheckpoint(mlContext)
.Append(mlContext.MulticlassClassification.Trainers.SdcaMaximumEntropy("Label", "Features"))
.Append(mlContext.Transforms.Conversion.MapKeyToValue("PredictedLabel"));
var model = pipeline.Fit(dataView);
Predicting department name
var predictionEngine = mlContext.Model.CreatePredictionEngine<Message, PredictedDepartment>(model);
var message = new Message()
{
Message = #"...", // removed for brevity
Subject = "Other questions",
Header = "Internet connection"
};
var prediction = predictionEngine.Predict(message);
Console.WriteLine($"Prediction result: {prediction.Name}"); // Department name
Result
Prediction result: 2.581
Models used
class Message
{
[LoadColumn(2)]
public string Message { get; set; }
[LoadColumn(0)]
public string Subject { get; set; }
[LoadColumn(1)]
public string Header { get; set; }
[LoadColumn(3)]
public string Name { get; set; } // Department name
}
class PredictedDepartment
{
[ColumnName("PredictedLabel")]
public string Name { get; set; } // Department name
}
Could someone explain me what's happening, and why I'm not getting some string value back, and instead getting some decimal?
I found my issue.
It has nothing to do with the my pipeline or the way I train my model. Nor is it an issue with the MapKeyToValue method in my pipeline. I simply had an issue with my dataset.
My CSV was comma seperated and used quoting for strings, as my Message property was multiline. Instead of trying to support this, I changed my export from my data to remove line breaks, and not quote strings. I also changed to a tab seperator instead of a comma seperator.
The model now seems to work, and I'm getting quite a good prediction.
Related
I'm fairly new to ASP.NET MVC and need some help.
I have a model called "Project", simplified below:
public class Project
{
public int ProjId { get; set; }
public string StatusID { get; set; }
}
The StatusID is a 1 character string, and can be I (In Progress), C (Completed), or K (Killed). So when I get a list of projects from the database, it returns that 1 character status code.
However, when I pass that information to my view, I'd like to show the full description rather than the 1 character.
What's the best way to approach this situation?
Thanks!
The best way would be to create a one-to-many relationship with a Statuses table. Then create a separate view model with this information.
var vm = new ProjectVM
{
ProjId = project.ProjId,
StatusDescription = statuses.Find(project.StatusId).Description
};
Notices statuses is a repository API that finds the right status description.
I'm having an issue where I'm trying to construct a Linq query in my repository that contains multiple joins from different tables in a database and then return this as an object for my controller to display. The error that I'm getting is: "Specified cast is not valid."
The problem part seems to be when I try to pass the object from my query and I'm not really sure how to solve this. (I'm still relatively new to web development and trying to learn the basics).
Below is the code in my repository:
public ListingModel GetListing(int listingId)
{
var query = from listing in listingsTable
where listing.ListingID == listingId
join feature
in featuresTable on listing.ListingID equals feature.ListingID into features
from f in features.DefaultIfEmpty()
join avail
in availabilityTable on listing.ListingID equals avail.ListingID into availability
from a in availability.DefaultIfEmpty()
join image
in imageTable on listing.ListingID equals image.ListingID into images
from i in images.DefaultIfEmpty()
select new ListingModel
{
Listing = listing,
Features = features,
Availability = availability,
Images = images
};
return query.FirstOrDefault();
}
If it's needed, here's the ListingModel class:
public class ListingModel
{
public Listing Listing { get; set; }
public IEnumerable<Feature> Features { get; set; }
public IEnumerable<Availability> Availability { get; set; }
public IEnumerable<Image> Images { get; set; }
}
When I try the query in LinqPad it works fine and returns the data exactly how I'd want it, so it seems to be erroring when I try to pass the object back. I've tried a few things with this and every time I seem to be getting the same error; I'm having real difficulty pinpointing what's causing it. Thanks in advance for any help given.
Try selecting an anonymous object in your query and then using it to instantiate a ListingModel in memory. The end of your method will look like this:
select new
{
Listing = listing,
Features = features,
Availability = availability,
Images = images
};
return query.AsEnumerable()
.Select(x => new ListingModel
{
Listing = x.Listing,
Features = x.Features,
Availability = x.Availability,
Images = x.Images
})
.FirstOrDefault();
But can't you just take advantage of Foreign Key relationships and have LINQ to SQL join the associated tables for you? Also, to avoid creating duplicate ListingModels you should remove the lines containing DefaultIfEmpty().
I have built a simple MVC3-based ticket entry site for a less-than-usable call center application and am attempting to refactor my prototype to better adhere to design patterns partly to make it more maintainable going forward but mostly as a learning exercise.
The user-facing view is a form consisting of basic user information in addition to a few panels allowing selection of various resource types. Each resource type (hardware, software, etc) is displayed in the same way: using dual, filterable listboxes with add/remove buttons, an optional “justification” textarea that conditionally displays if a requested resource requires justification, and general comments.
I have built the following ViewModel for the individual panels:
public class RequestableList
{
// list of requestable items ids requiring justification
private List<string> _restrictedItems = new List<string>();
public List<string> RestrictedItems
{
get { return _restrictedItems; }
set { _restrictedItems = value; }
}
// the key-value pairs from which to populate available items list
private Dictionary<string, string> _availableItems = new Dictionary<string, string>();
public Dictionary<string, string> AvailableItems
{
get { return _availableItems; }
set { _availableItems = value; }
}
// item ids requested by user
private List<string> _requestedItems = new List<string>();
public List<string> RequestedItems
{
get { return _requestedItems; }
set { _requestedItems = value; }
}
}
The main ViewModel is then comprised of multiple RequestableLists as necessary:
public class SimpleRequestViewModel
{
public UserInfo userInfo { get; set; }
public RequestableList Software {get;set;}
public RequestableList Hardware {get;set;}
public RequestableList Access {get;set;}
public string SoftwareAdditionalInfo { get; set; }
public string HardwareAdditionalInfo { get; set; }
public string AccessFileMailShare { get; set; }
public string AccessAdditionalInfo { get; set; }
public string SoftwareJustification { get; set; }
public string HardwareJustification { get; set; }
public string AccessJustification { get; set; }
public string Comment { get; set; }
}
I have created a strongly typed view for SimpleRequestViewModel (and its variant) and a strongly typed EditorTemplate for RequestableList that wires up the dual listboxes, filtering, and jquery. All renders well and is working but the code currently smells.
When posting to the controller, if the model is valid I must translate it into a readable text description in order to create a new ticket in in the call center app. It doesn’t feel right to have the controller performing that translation into readable text but I run into hurdles when trying to design another class to translate the viewmodels.
Only the selected item values are posted so before translating the request into text I must first lookup the appropriate text for the provided values (they are required in description). The controller is currently the only object that has access to the call center data model for this lookup query.
There are 2 similar ViewModels containing varying combinations of RequestableLists so any translator must be able to translate the various combinations. One has only Hardware and Software, another may have Hardware Software, and a few more RequestableLists.
I considered overriding ToString() directly in the ViewModel but didn’t like that business logic (conditional rendering) there, and again, once posted, the ViewModel doesn’t contain the text for the selected items in the listbox so it would need access to the data model.
The translation of posted values to text as it is currently handled in the controller smells as it’s handled in a switch statement. The controller takes each posted RequestableList and populates the original “Available” fields before it builds the new ticket description.
switch (requestCategory)
{
case RequestableCategory.Software:
itemList = sde.GetSoftware();
break;
case RequestableCategory.Hardware:
itemList = sde.GetHardware();
break;
case RequestableCategory.Access:
itemList = sde.GetAccess();
break;
case RequestableCategory.Telecom:
itemList = sde.GetTelecom();
break;
default:
throw new ArgumentException();
}
So, my question(s):
What patterns are techniques would you recommend for performing the posted viewmodel to ticket description translation?
How do you typically handle the “only posts value” issue with select boxes when you need the text as well as the value?
Is there a better way for me to be approaching this problem?
Again, I am hoping this is a learning experience for me and am more than willing to provide additional information or description if needed.
A few suggestions:
Abstract the logic that does the call center submission into its own class. Provide (from the controller) whatever dependencies it needs to access the call center DB. Have different methods to handle the various types of view models using overloading. Presumably the descriptions come from the DB so you can extract the description from the DB based on the value in this class. This class could also take responsibility for building your view models for the display actions as well. Note that with this pattern the class can interact with the DB directly, through a repository, or even via web services/an API.
Use a repository pattern that implements some caching if performance is an issue in looking up the description from the DB the second time. I suspect it won't be unless your call center is very large, but that would be the place to optimize the query logic. The repository can be the thing that the controller passes to the submission class.
If you don't need to access the DB directly in the controller, consider passing the broker class as a dependency directly.
It might look like:
private ICallCenterBroker CallCenterBroker { get; set; }
public RequestController( ICallCenterBroker broker )
{
this.CallCenterBroker = broker;
// if not using DI, instantiate a new one
// this.CallCenterBroker = broker ?? new CallCenterBroker( new CallCenterRepository() );
}
[HttpGet]
public ActionResult CreateSimple()
{
var model = this.CallCenterBroker.CreateSimpleModel( this.User.Identity.Name );
return View( model );
}
[HttpPost]
public ActionResult CreateSimple( SimpleRequestViewModel request )
{
if (Model.IsValid)
{
var ticket = this.CallCenterBroker.CreateTicket( request );
// do something with ticket, perhaps create a different model for display?
this.CallCenterBroker.SubmitTicket( ticket );
return RedirectToAction( "index" ); // list all requests?
}
return View();
}
I'm still learning, but with the stackoverflow commnuties help, I've been able to get closer and closer.
What I have right now is a View "Index.aspx":
System.Web.Mvc.ViewPage<Data.Models.GetDealsModel>
The Model:
public class GetDealsModel
{
// set up the model
public string DealId { get; set; }
public string StreetAddress { get; set; }
public string City { get; set; }
public string State { get; set; }
public string ZipCode { get; set; }
public string Logo { get; set; }
public string Website { get; set; }
public string TotalRows { get; set; }
}
And the controller:
public ActionResult Index()
{
LinqToDealsDataContext db = new LinqToDealsDataContext();
XElement xmlTree = XElement.Parse("<Request><ZipCode>92612</ZipCode></Request>");
var deals = db.spSearchDeals(xmlTree);
return View(deals);
}
And with this configuration I'm now getting this error:
The model item passed into the dictionary is of type 'System.Data.Linq.SqlClient.SqlProvider+SingleResult`1[Data.Models.spSearchDealsResult]', but this dictionary requires a model item of type 'Data.Models.GetDealsModel'.
I'm guessing that there's an issue connecting my Controller to my Model... I'm not sure why. PLEASE help me connect this final peice.
NOTE: I do understand that eventually I should separate my logic in the controller into a Repository Pattern, but for now, this will do.
You need to translate the data coming back from this call:
var deals = db.spSearchDeals(xmlTree);
into a GetDealsModel type. So something like:
GetDealsModel dealsModel = new GetDealsModel()
{
DealId = deals.DealId,
StreetAddress = deals.StreetAddress,
....
};
return View(dealsModel);
The reason being that your View is strongly typed to take a GetDealsModel, but your deals variable is not of that type and it gives you that exception when you pass it to the View.
You should create object of type GetDealsModel, but your DB Query returns object of type Data.Models.spSearchDealsResult. Try something like:
return new GetDealsModel
{
DealId = deals.Id,
// other fields here
}
Add to your learning curve list the following items:
Repository Pattern
Ask yourself the following question: Why do I need a service layer?
Read Steven Sanderson's book. It teaches you to think in MVC.
The above applies to your problems because your issues are clearly related to having code in your Controllers that should be in your Model (ie, data access code should be in a repository class). Ie, you are not thinking in MVC.
Your model should include the necessary repository classes, eg, DealRepository.
You need a Service class to map the objects your repository digs out of your database to your model class: that way conversion problems are encapsulated into the Service Layer code.
If you do this, you can then write in your controller:
public ActionResult Index()
{
return(DealService.GetByZipcode(92612));
}
Where DealService.GetByZipcode basically just maps DealRepository.GetByZipcode(92612) to your model class and returns the mapping result.
The DealRepository.GetByZipcode method would be roughly:
public static DealEntity GetByZipcode(string zip)
{
LinqToDealsDataContext db = new LinqToDealsDataContext();
XElement xmlTree = XElement.Parse("<Request><ZipCode>" + zip + "</ZipCode></Request>");
var deals = db.spSearchDeals(xmlTree);
return deals;
}
The DealEntity class is just whatever Linq gives you for your table.
The reason WHY for all this:
The reason for this structure is as follows:
a. All you data access code is in one place: DealRepository. You can test and debug that independently of everything else.
b. The mapping code is all in one place: DealService. You can test and debug that independently of everything else.
c. In other words, you need to properly separate your concerns.
The problem with your existing code is precisely that you have NOT separated concerns. Ie, you have taken a dash of MVC and put it in a food processor and ended up with mush full of problems that are way more difficult to deal with than they need be.
Your model is mixed into your controller, there is no repository, no service layer.
So hold your horses just a while and take the time to read Steve Sanderson's book.
I would also try modelling a simpler problem. That xml parsing makes my head hurt even on a good day.
NOTE:
You could seriously improve your naming conventions. LinqToDealsDataContext? You're kidding, right?
I have an enum for one of the properties of my view-model. I want to display a drop-down list that contains all the values of the enum. I can get this to work with the following code.
What I'm wondering is whether there is a simple way to convert from an enum to an IEnumerable? I can do it manually as in the following example, but when I add a new enum value the code breaks. I imagine that I can do it via reflection as per this example, but but are there other ways to do this?
public enum Currencies
{
CAD, USD, EUR
}
public ViewModel
{
[Required]
public Currencies SelectedCurrency {get; set;}
public SelectList Currencies
{
List<Currencies> c = new List<Currencies>();
c.Add(Currencies.CAD);
c.Add(Currencies.USD);
c.Add(Currencies.EUR);
return new SelectList(c);
}
}
I'm using a helper that i found here to populate my SelectLists with a generic enum type, i did a little modification to add the selected value though, here's how it looks like :
public static SelectList ToSelectList<T>(this T enumeration, string selected)
{
var source = Enum.GetValues(typeof(T));
var items = new Dictionary<object, string>();
var displayAttributeType = typeof(DisplayAttribute);
foreach (var value in source)
{
FieldInfo field = value.GetType().GetField(value.ToString());
DisplayAttribute attrs = (DisplayAttribute)field.
GetCustomAttributes(displayAttributeType, false).FirstOrDefault()
items.Add(value, attrs != null ? attrs.GetName() : value.ToString());
}
return new SelectList(items, "Key", "Value", selected);
}
The nice thing about it is that it reads the DisplayAttribute as the title rather than the enum name. (if your enums contain spaces or you need localization then it makes your life much easier)
So you will need to add the Display attirubete to your enums like this :
public enum User_Status
{
[Display(Name = "Waiting Activation")]
Pending, // User Account Is Pending. Can Login / Can't participate
[Display(Name = "Activated" )]
Active, // User Account Is Active. Can Logon
[Display(Name = "Disabled" )]
Disabled, // User Account Is Diabled. Can't Login
}
and this is how you use them in your views.
<%: Html.DropDownList("ChangeStatus" , ListExtensions.ToSelectList(Model.statusType, user.Status))%>
Model.statusType is just an enum object of type User_Status.
That's it , no more SelectLists in your ViewModels. In my example I'm refrencing an enum in my ViewModel but you can Refrence the enum type directly in your view though. I'm just doing it to make everything clean and nice.
Hope that was helpful.
Look at Enum.GetNames(typeof(Currencies))
I am very late on this one but I just found a really cool way to do this with one line of code, if you are happy to add the Unconstrained Melody NuGet package (a nice, small library from Jon Skeet).
This solution is better because:
It ensures (with generic type constraints) that the value really is an enum value (due to Unconstrained Melody)
It avoids unnecessary boxing (due to Unconstrained Melody)
It caches all the descriptions to avoid using reflection on every call (due to Unconstrained Melody)
It is less code than the other solutions!
So, here are the steps to get this working:
In Package Manager Console, "Install-Package UnconstrainedMelody"
Add a property on your model like so:
//Replace "YourEnum" with the type of your enum
public IEnumerable<SelectListItem> AllItems
{
get
{
return Enums.GetValues<YourEnum>().Select(enumValue => new SelectListItem { Value = enumValue.ToString(), Text = enumValue.GetDescription() });
}
}
Now that you have the List of SelectListItem exposed on your model, you can use the #Html.DropDownList or #Html.DropDownListFor using this property as the source.
So many good answers - I thought I'sd add my solution - I am building the SelectList in the view (and not in the Controller):
In my c#:
namespace ControlChart.Models
//My Enum
public enum FilterType {
[Display(Name = "Reportable")]
Reportable = 0,
[Display(Name = "Non-Reportable")]
NonReportable,
[Display(Name = "All")]
All };
//My model:
public class ChartModel {
[DisplayName("Filter")]
public FilterType Filter { get; set; }
}
In my cshtml:
#using System.ComponentModel.DataAnnotations
#using ControlChart.Models
#model ChartMode
#*..........*#
#Html.DropDownListFor(x => x.Filter,
from v in (ControlChart.Models.FilterType[])(Enum.GetValues(typeof(ControlChart.Models.FilterType)))
select new SelectListItem() {
Text = ((DisplayAttribute)(typeof(FilterType).GetField(v.ToString()).GetCustomAttributes(typeof(DisplayAttribute), false).First())).Name,
Value = v.ToString(),
Selected = v == Model.Filter
})
HTH
Maybe is too late, but i think it could be useful for people with the same problem.
I've found here that now with MVC 5 it's included an EnumDropDownListFor html helper that makes for no longer necesary the use of custom helpers or other workarounds.
In this particular case, just add this:
#Html.EnumDropDownListFor(x => x.SelectedCurrency)
and that's all!
You can also translate or change the displayed text via data annotations and resources files:
Add the following data annotations to your enum:
public enum Currencies
{
[Display(Name="Currencies_CAD", ResourceType=typeof(Resources.Enums)]
CAD,
[Display(Name="Currencies_USD", ResourceType=typeof(Resources.Enums)]
USD,
[Display(Name="Currencies_EUR", ResourceType=typeof(Resources.Enums)]
EUR
}
Create the corresponding resources file.