I am trying to build a random sample and random assignment generator for a website I am working on that is supposed to assign randomly selected items in an equal number to a variable set of people.
For example:
In this round of assessment, we have 9 assessors, and 477 items that need to be graded by two assessors each (with the same assessor not grading the same item twice). This would come out to 954 total "assessments" that need to be made, and 106 per assessor.
Therefore I'd need an eventual list like...
(Item, Assessor1, Assessor2)
(1, A, B)
(2, C, D)
(3, E, F)
(4, G, H)
(5, H, B)
(6, B, F)
.
.
.
And so on
I've found websites that can do random number generation and even some that can do random assignment over groups, but even then I am still finding situations where the same assessor will be grading the same item throughout the results.
I am less concerned about the inherent randomness and more concerned about making sure each result list has every assessor grading the same number of items without grading the same one.
I can usually get it to the point where I have it -close- and maybe one grader will be 1 or 2 more and another will be 1 or 2 less, but unfortunately it's a hard requirement that they be equal groupings.
EDIT
Implemented #Tinstaafl's code and ran the following:
Dim col As New MyItemCollection
col.AddAssessor("A")
col.AddAssessor("B")
col.AddAssessor("C")
col.AddAssessor("D")
col.AddAssessor("E")
col.AddAssessor("F")
col.AddAssessor("G")
col.AddAssessor("H")
col.AddAssessor("I")
For I As Integer = 1 To 477
col.AddItem(I.ToString)
Next
Dim newList As List(Of MyItemCollection.MyItem) = col.AssignAssessors
For Each item As MyItemCollection.MyItem In col.itemlist
Response.Write(item.ToString & "<br/>")
Next
Unfortunately my output looks like
1 - F - F
2 - I - I
3 - E - E
4 - F - F
5 - C - C
6 - D - D
7 - G - G
8 - A - A
9 - C - C
10 - B - B
11 - D - D
12 - D - D
13 - D - D
14 - D - D
15 - H - H
And so on...
One idea that I can think of is as follows:
Shuffle the assessors {1-9}. Then assign the first 4.5 items to them sequentially as per the result of this shuffle.
Then shuffle them again. Again assign them sequentially to the next items. If the first assessor happens to be the same one that is already assigned to the 5th item, then swap the 1st and second assessor in the list.
Keep doing this for all items.
One way is with a collection class and embedded classes for the items and assessors. Here's a partial implementation to show how the list could be generated:
Public Class MyItemCollection
Class Assessor
Public Name As String = ""
Public Shared Operator <>(LH As Assessor, RH As Assessor) As Boolean
Return LH.Name <> RH.Name
End Operator
Public Shared Operator =(LH As Assessor, RH As Assessor) As Boolean
Return RH.Name = LH.Name
End Operator
Public Overrides Function ToString() As String
Return Name
End Function
End Class
Class MyItem
Public Name As String = ""
Public Assessor1 As New Assessor
Public Assessor2 As New Assessor
Public Overrides Function ToString() As String
Return Name & " - " & Assessor1.ToString & " - " & Assessor2.ToString
End Function
End Class
Private AssessorList As New List(Of Assessor)
Private ItemList As New List(Of MyItem)
Public Sub AddAssessor(Name As String)
AssessorList.Add(New Assessor With {.Name = Name})
End Sub
Public Sub AddItem(Name As String)
ItemList.Add(New MyItem With {.Name = Name})
End Sub
Private Rnd As New Random(Now.Millisecond * Now.Day * Now.Minute)
Public Function AssignAssessors() As List(Of MyItem)
Dim OutVal As New List(Of MyItem)
Dim average As Integer = ((ItemList.Count \ AssessorList.Count) * 2) + 5
Dim AssessorCount(AssessorList.Count - 1) As Integer
For Each item As MyItem In ItemList
Dim firstassessorindex As Integer
Do
firstassessorindex = Rnd.Next(0, AssessorList.Count)
Loop Until AssessorCount(firstassessorindex) <= average
item.Assessor1 = AssessorList(firstassessorindex)
AssessorCount(firstassessorindex) += 1
Dim secondassessorindex As Integer
Do
secondassessorindex = Rnd.Next(0, AssessorList.Count)
Loop Until AssessorList(secondassessorindex) <> item.Assessor1 AndAlso AssessorCount(secondassessorindex) < average
item.Assessor2 = AssessorList(secondassessorindex)
AssessorCount(secondassessorindex) += 1
Next
OutVal = ItemList
Return OutVal
End Function
End Class
One thing that wasn't clear in your explanation was if the distribution between Assessor1 and Assessor2 is important(does it matter if an assessor is the second assessor more times than it is the first?). If you need that, then a List(Of Tuple(Integer, Integer)) might be needed for the AssessorCount instead of a List(Of Integer)
The ToString method for each item in the return list will be in the format -
ItemName - Assessor1Name - Assessor2Name
I'm not sure if I understand your assignment rules, but this might be close.
var assessors = new []
{
"A", "B", "C",
"D", "E", "F",
"G", "H", "I",
};
var rnd = new Random();
var query =
from a1 in assessors
from a2 in assessors
where a1 != a2
orderby rnd.Next()
select new { a1, a2};
var results =
Enumerable
.Range(1, 477)
.Zip(query
.Repeat(),
(i, aa) => new
{
Item = i,
Assessor1 = aa.a1,
Assessor2 = aa.a2,
});
I get this kind of result:
1 F B
2 E C
3 D H
...
476 F C
477 B E
Related
I have a table that consists of 8 columns (A, B, C, etc). Column D and E are grouped by each month. Is it possible to calculate D and E value and return the value in column G?
Here is needed more information about your scenario, as yes it’s possible, even before you make this DataTable. However in an unknown scenario, you can do that even after, as code below shows.
(And if I well understand what you want to do):
Dim dataTable As New DataTable
dataTable.Columns.Add("A")
dataTable.Columns.Add("B")
dataTable.Columns.Add("C")
dataTable.Columns.Add("D")
dataTable.Columns.Add("Result_OF_B_C")
For i As Integer = 0 To 10
dataTable.Rows.Add(dataTable.NewRow)
dataTable.Rows(dataTable.Rows.Count - 1).ItemArray =
{"ColumnA" + i.ToString, i, i + i / 2, "ColumnD" + i.ToString}
Next
Dim newDataTable As DataTable = (From R In dataTable.Rows).Select(Function(R)
Dim Cr As DataRow = CType(R, DataRow)
Cr.Item("Result_OF_B_C") = CDbl(Cr.Item("B")) + CDbl(Cr.Item("C"))
Return Cr
End Function).CopyToDataTable
DataGridView1.DataSource = newDataTable
I have this collection:
Public col As New Collection
And this class named "Class":
Dim Name As String
Dim Tests As New Collection
Public Sub SetName(x As String)
Name = x
End Sub
Public Sub AddTest(x As String)
Tests.Add x
End Sub
Public Function GetName() As String
GetName = Name
End Function
I tried to add values in in collection:
For k = 1 To 10
Dim temp As New cls
temp.SetName CStr(k)
col.Add temp
Next k
And when I print names of all items from collection:
For Each item In col
Debug.Print (item.GetName)
Next
I have next output:
10
10
10
10
10
10
10
10
10
10
Why my output is not
1
2
3
4
5
6
7
8
9
10
??
Why it puts last value on each item?
Don't use Dim temp As New cls - There are many pitfalls to the shortcut notation...
The above notation effectively only ever creates a single instance of the cls object, and you're adding 10 references to that single instance to your collection. Every iteration, you update the name, and so, at the end, all instances have the name "10". If you paused it during the loop, you'd notice that when there is 1 item in the collection, the item name would be "1", and when there are 2 items, all the names would be "2", and so on...
Declare the variable, and then set the variable to a new instance each time:
For k = 1 To 10
Dim temp As cls
Set temp = New cls
temp.SetName CStr(k)
col.Add temp
Next k
I have a list of objects and I'm trying to sort by two properties with one property being sorted by a custom order. The list has properties of ReqType and PartNumber. ReqType will be "M", "B", "S", or null and I would like to sort the list in that order. Then sort by PartNumber.
Input list:
PartNumber ReqType
124 B
125 M
123 B
121 S
120 M
115
Expected Sort:
PartNumber ReqType
120 M
125 M
123 B
124 B
121 S
115
I started with the code below but that only sorts ReqType alphabetically.
Return EBom.OrderBy(Function(f) f.ReqType).ThenBy(Function(f) f.PartNumber).ToList
I then found a method to create a custom sort order using the code below. Though using Ebom.Sort() doesn't seem to allow me to tack on the second sort order for PartNumber. I realize I could probably add the PartNumber sorting to the custom function but that seems like a lot of work.
EBom.Sort()
Return EBom.ToList
Implements IComparable(Of EBomList)
Public Function SortReq(other As EBomList) As Integer Implements IComparable(Of EBomList).CompareTo
If (Me.ReqType = other.ReqType) Then
Return 0
ElseIf (Me.ReqType = "M") Then
Return -1
ElseIf (Me.ReqType = "B") Then
If (other.ReqType = "M") Then
Return 1
Else
Return -1
End If
ElseIf (Me.ReqType = "S") Then
If (other.ReqType = "M" Or other.ReqType = "B") Then
Return 1
Else
Return -1
End If
Else
Return 1
End If
End Function
Is there an easier way to sort by a custom order or at least combine the custom sort function with a .thenby(.....) to get the order I'd like?
Cleaner code version to do this is to use a Function in the sort method like the following.
d.Sort(Function(X As EBomList, Y As EBomList)
Dim Tx As Integer = InStr("MBS ", X.ReqType.PadLeft(1, " "c))
Dim Ty As Integer = InStr("MBS ", Y.ReqType.PadLeft(1, " "c))
Select Case Tx
Case Is < Ty : Return -1
Case Is > Ty : Return 1
Case Else : Return X.PartNumber.CompareTo(Y.PartNumber)
End Select
End Function)
Note it only needs to check the partnumber when the type code is the same.
I am assuming your Partnumber is in fact a number. If it is a string you will need to pad it as appropriate. For example.
Return X.PartNumber.PadLeft(6," "c).CompareTo(Y.PartNumber.PadLeft(6," "c))
ALTERNATIVE AND FASTER APPROACH
If you have A LOT of data you may want to consider augmenting the EBomLit to create a sort key rather than doing that string search though...
As in...
Private _ReqType As String
Private _TypeKey As Integer
Public Property ReqType As String
Get
Return _ReqType
End Get
Set(value As String)
_ReqType = value
_TypeKey = InStr("MBS ", value.PadLeft(1, " "c))
End Set
End Property
Public ReadOnly Property TypeKey As Integer
Get
Return _TypeKey
End Get
End Property
Then the sort becomes...
d.Sort(Function(X As EBomList, Y As EBomList)
Select Case X.TypeKey
Case Is < Y.TypeKey : Return -1
Case Is > Y.TypeKey : Return 1
Case Else : Return X.PartNumber.CompareTo(Y.PartNumber)
End Select
End Function)
FASTER STILL
You could even extend that further by creating a full sort key out of the "TypeKey" with the padded "PartNumber" and use those as a key to save the whole shebang in a SortedDictionary instead of a List.
Just create a list of ReqTypes and lookup the index:
Dim sortOrder = "MBS "
Dim sortedList = List.OrderBy(Function(x) sortOrder.IndexOf(If(x.ReqType Is Nothing, " ", x.ReqType))).ThenBy(Function(x) x.PartNumber).ToList()
Note: If sorting by something other than chars then you will need to create a propper array/list of the object to compare by.
A bit easier with Enum and Tuple if those are an option:
Enum PartNumber As Byte : M : B : S : __ : End Enum
Dim list = New List(Of Tuple(Of PartNumber, Integer)) From {
Tuple.Create(PartNumber.B, 124),
Tuple.Create(PartNumber.M, 125),
Tuple.Create(PartNumber.B, 123),
Tuple.Create(PartNumber.S, 121),
Tuple.Create(PartNumber.M, 120),
Tuple.Create(PartNumber.__, 115)}
list.Sort()
For Each item In list
Debug.Print(item.Item2 & vbTab & item.Item1.ToString.Replace("_", ""))
Next
Output:
120 M
125 M
123 B
124 B
121 S
115
Friends I have a serious issue at present and Honestly I have no idea as to why this isn't working as expected. I am more a custom to C# than VB can anyone help with the following Code Example
Please note that the String is passed from other classes and cannot be altered
The string arrL = "Someone#Something,200,First,50.00,60,Second,60.00,20,Third,70.00,120"
E.G where "Someone" is User, "Something" is an ID, "200" is Totaltime then frequency name "First" then frequency of "50.00" then its Time to run e.g 60 seconds. then next Item and so on.
Dim Sequence As New ArrayList
Sequence.AddRange(arrL.Split(","))
If Sequence.Count > 0 Then
RunFreq.ClientName = Sequence(0).ToString.Split("#")(0)
RunFreq.ClientProgramName = Sequence(0).ToString.Split("#")(1)
RunFreq.ClientProtocolTotalTime = Sequence(1).ToString
For i As Integer = 2 To Sequence.Count - 1
Dim g() = Sequence(i).ToString().Split(",")
Dim b As New ClassWave.ClassFrequency
b.Name = g(0) 'Here i get a Value
b.Frequency = CDbl(g(1)) '< HERE I get Index was outside the bounds of the array.
b.Time = CInt(g(2)) ' Same here Index was outside the bounds of the array.
Next
End If
I get Index Outside Bounds on g(1) and g(2) instead of 50.00 and 60. Any Ideas?
Preliminaries: You should turn on Option Strict, and consider using a List(of String) in place of the ArrayList.
Your second line, splits the string by "," into Sequence, so there is no need to split it again - you get the error because they cant be split further (and you didnt check the count). This works:
Dim arrl As String = "Someone#Something,200,First,50.00,60,Second,60.00,20,Third,70.00,120"
Dim Sequence As New ArrayList
Sequence.AddRange(arrl.Split(","c))
Dim a, b, c As String
If Sequence.Count > 0 Then
a = Sequence(0).ToString.Split("#"c)(0)
b = Sequence(0).ToString.Split("#"c)(1)
c = Sequence(1).ToString
Console.WriteLine("{0} - {1} - {2}", a, b, c)
For i As Integer = 2 To Sequence.Count - 1 Step 3
a = Sequence(i + 0).ToString
b = Sequence(i + 1).ToString
c = Sequence(i + 2).ToString
Console.WriteLine("{0} - {1} - {2}", a, b, c)
Next
End If
You could also use this for the split:
Sequence.AddRange(arrl.Split("#"c, ","c))
This would create 12 elements in the ArrayList, but since the first 3 parts go elsewhere, that doesnt have as much value.
Output:
Someone - Something - 200
First - 50.00 - 60
Second - 60.00 - 20
Third - 70.00 - 120
Note that .ToString() is required when fetching from the ArrayList because it is not typed - it only ever contains Object. A List(of String) would store the parts as string.
I recently found out about n-grams and the cool possibility to compare frequency of phrases in a text body with it. Now I'm trying to make an vb.net app that simply gets an text body and returns a list of the most frequently used phrases (where n >= 2).
I found an C# example of how to generate a n-gram from a text body so I started out with converting the code to VB. The problem is that this code does create one gram per character instead of one per word. The delimiters I want to use for the words is: VbCrLf (new line), vbTab (tabs) and the following characters: !##$%^&*()_+-={}|\:\"'?¿/.,<>’¡º×÷‘;«»[]
Does anyone have an idea how I can rewrite the following function for this purpose:
Friend Shared Function GenerateNGrams(ByVal text As String, ByVal gramLength As Integer) As String()
If text Is Nothing OrElse text.Length = 0 Then
Return Nothing
End If
Dim grams As New ArrayList()
Dim length As Integer = text.Length
If length < gramLength Then
Dim gram As String
For i As Integer = 1 To length
gram = text.Substring(0, (i) - (0))
If grams.IndexOf(gram) = -1 Then
grams.Add(gram)
End If
Next
gram = text.Substring(length - 1, (length) - (length - 1))
If grams.IndexOf(gram) = -1 Then
grams.Add(gram)
End If
Else
For i As Integer = 1 To gramLength - 1
Dim gram As String = text.Substring(0, (i) - (0))
If grams.IndexOf(gram) = -1 Then
grams.Add(gram)
End If
Next
For i As Integer = 0 To (length - gramLength)
Dim gram As String = text.Substring(i, (i + gramLength) - (i))
If grams.IndexOf(gram) = -1 Then
grams.Add(gram)
End If
Next
For i As Integer = (length - gramLength) + 1 To length - 1
Dim gram As String = text.Substring(i, (length) - (i))
If grams.IndexOf(gram) = -1 Then
grams.Add(gram)
End If
Next
End If
Return Tokeniser.ArrayListToArray(grams)
End Function
An n-gram for words is just a list of length n that stores these words. A list of n-grams is then simply a list of list of words. If you want to store frequency then you need a dictionary that is indexed by these n-grams. For your special case of 2-grams, you can imagine something like this:
Dim frequencies As New Dictionary(Of String(), Integer)(New ArrayComparer(Of String)())
Const separators as String = "!##$%^&*()_+-={}|\:""'?¿/.,<>’¡º×÷‘;«»[] " & _
ControlChars.CrLf & ControlChars.Tab
Dim words = text.Split(separators.ToCharArray(), StringSplitOptions.RemoveEmptyEntries)
For i As Integer = 0 To words.Length - 2
Dim ngram = New String() { words(i), words(i + 1) }
Dim oldValue As Integer = 0
frequencies.TryGetValue(ngram, oldValue)
frequencies(ngram) = oldValue + 1
Next
frequencies should now contain a dictionary with all two consecutive word pairs contained in the text, and the frequency with which they appear (as a consecutive pair).
This code requires the ArrayComparer class:
Public Class ArrayComparer(Of T)
Implements IEqualityComparer(Of T())
Private ReadOnly comparer As IEqualityComparer(Of T)
Public Sub New()
Me.New(EqualityComparer(Of T).Default)
End Sub
Public Sub New(ByVal comparer As IEqualityComparer(Of T))
Me.comparer = comparer
End Sub
Public Overloads Function Equals(ByVal a As T(), ByVal b As T()) As Boolean _
Implements IEqualityComparer(Of T()).Equals
System.Diagnostics.Debug.Assert(a.Length = b.Length)
For i As Integer = 0 to a.Length - 1
If Not comparer.Equals(a(i), b(i)) Then Return False
Next
Return True
End Function
Public Overloads Function GetHashCode(ByVal arr As T()) As Integer _
Implements IEqualityComparer(Of T()).GetHashCode
Dim hashCode As Integer = 17
For Each obj As T In arr
hashCode = ((hashCode << 5) - 1) Xor comparer.GetHashCode(obj)
Next
Return hashCode
End Function
End Class
Unfortunately, this code doesn’t compile on Mono because the VB compiler has problems finding the generic EqualityComparer class. I’m therefore unable to test whether the GetHashCode implementationw works as expected but it should be fine.
Thank you Konrad very much for this beginning of an solution!
I tried your code and got the following result:
Text = "Hello I am a test Also I am a test"
(I also included whitespace as a separator)
frequencies now has 9 items:
---------------------
Keys: "Hello", "I"
Value: 1
---------------------
Keys: "I", "am"
Value: 1
---------------------
Keys: "am", "a"
Value: 1
---------------------
Keys: "a", "test"
Value: 1
---------------------
Keys: "test", "Also"
Value: 1
---------------------
Keys: "Also", "I"
Value: 1
---------------------
Keys: "I", "am"
Value: 1
---------------------
Keys: "am", "a"
Value: 1
---------------------
Keys: "a", "test"
Value: 1
---------------------
My first question: shouldn't the 3 last key pairs get a value of 2 as they're found two times in the text?
Second: The reason I got into the n-gram approach is that I don't want to limit the word count (n) to a specific length. Is there a way to make a dynamic approach that tries to find the longest phrase match first and then step down to the last wordcount of 2?
My goal result for the sample query above is:
---------------------
Match: "I am a test"
Frequency: 2
---------------------
Match: "I am a"
Frequency: 2
---------------------
Match: "am a test"
Frequency: 2
---------------------
Match: "I am"
Frequency: 2
---------------------
Match: "am a"
Frequency: 2
---------------------
Match: "a test"
Frequency: 2
---------------------
There is an similar C++ approach for this written by Hatem Mostafa over at codeproject.com: N-gram and Fast Pattern Extraction Algorithm
Sadly I'm no C++ expert and have no idea how to convert this bit of code as it includes a lot of memory handling that .Net doesn't. The only problem with this example is that you have to specify the minimum word pattern length and I want it to be dynamic from 2 to max found.