seekia/internal/profiles/userStatistics/userStatistics.go

770 lines
28 KiB
Go

// userStatistics provides functions to generate statistics about stored user profiles.
// The user chooses the attribute(s), and the gui displays a chart with a button to view the statistics data.
// See statisticsGui.go to see the gui code.
package userStatistics
//TODO: Add the ability to control for confounding variables
// Example: Wealth, controlled for age and sex
import "seekia/internal/badgerDatabase"
import "seekia/internal/helpers"
import "seekia/internal/profiles/calculatedAttributes"
import "seekia/internal/profiles/profileStorage"
import "seekia/internal/profiles/attributeDisplay"
import "seekia/internal/translation"
import "seekia/internal/statisticsDatum"
import "slices"
import "strings"
import "errors"
import "math"
//Outputs:
// -int: Number of users analyzed in statistics
// -[]statisticsDatum.StatisticsDatum: Statistics datums list (sorted, not grouped)
// -bool: Grouping performed
// -[]statisticsDatum.StatisticsDatum: Grouped datums list
// -func(float64)(string, error): Function to format y axis values
// -This is used because the values must be passed to the chart code as pure floats, but they must be formatted after to be human readable
// -Example: "1000000" -> "1 million"
// -error
func GetUserStatisticsDatumsLists_BarChart(identityType string,
networkType byte,
xAxisAttribute string,
xAxisIsNumerical bool,
formatXAxisValuesFunction func(string)(string, error),
xAxisUnknownLabel string,
yAxisAttribute string)(int, []statisticsDatum.StatisticsDatum, bool, []statisticsDatum.StatisticsDatum, func(float64)(string, error), error){
isValid := helpers.VerifyNetworkType(networkType)
if (isValid == false){
networkTypeString := helpers.ConvertByteToString(networkType)
return 0, nil, false, nil, nil, errors.New("GetUserStatisticsDatumsLists_BarChart called with invalid networkType: " + networkTypeString)
}
getYAxisRoundingPrecision := func()int{
if (yAxisAttribute == "Number Of Users"){
return 0
}
if (yAxisAttribute == "Average Height"){
return 0
}
if (yAxisAttribute == "Average Age"){
return 0
}
return 1
}
yAxisRoundingPrecision := getYAxisRoundingPrecision()
//Outputs:
// -int: Total analyzed users
// -[]statisticsDatum.StatisticsDatum: Statistics datums list
// -map[string]int: Response Counts map (X axis attribute response -> Number of y axis responses)
// -bool: yAxisIsAverage
// -map[string]float64: Response Sums map (X axis attribute response -> all y axis responses summed) (If yAxisIsAverage == true)
// -func(float64)(string, error): Function to format statistics values
// -bool: Include a No Response/Unknown value datum
// -This is only needed if at least 1 user did not respond to the X axis attribute on their profile.
// -statisticsDatum.StatisticsDatum: The unknown value datum.
// -error
getStatisticsDatumsList := func()(int, []statisticsDatum.StatisticsDatum, map[string]int, bool, map[string]float64, func(float64)(string, error), bool, statisticsDatum.StatisticsDatum, error){
//TODO: Add "Probability Of ..."
// This will allow viewing of choice attribute probabilities
// For example: Probability of being Male, probability of being Female, etc.
// We need to add this for each canonical option
if (yAxisAttribute == "Number Of Users"){
totalAnalyzedUsers, statisticsDatumsList, responseCountsMap, numberOfUnknownValueUsers, err := getProfileAttributeCountStatisticsDatumsList(identityType, networkType, xAxisAttribute, formatXAxisValuesFunction)
if (err != nil) { return 0, nil, nil, false, nil, nil, false, statisticsDatum.StatisticsDatum{}, err }
formatValuesFunction := func(input float64)(string, error){
// input will be an whole number representing the number of users
result := helpers.ConvertFloat64ToStringRounded(input, 0)
return result, nil
}
if (numberOfUnknownValueUsers == 0){
return totalAnalyzedUsers, statisticsDatumsList, responseCountsMap, false, nil, formatValuesFunction, false, statisticsDatum.StatisticsDatum{}, nil
}
unknownValueFormatted := helpers.ConvertIntToString(numberOfUnknownValueUsers)
unknownDatum := statisticsDatum.StatisticsDatum{
Label: xAxisUnknownLabel,
LabelFormatted: xAxisUnknownLabel,
Value: float64(numberOfUnknownValueUsers),
ValueFormatted: unknownValueFormatted,
}
return totalAnalyzedUsers, statisticsDatumsList, responseCountsMap, false, nil, formatValuesFunction, true, unknownDatum, nil
}
userIdentityHashesToAnalyzeList, err := getUserIdentityHashesToAnalyzeList(identityType)
if (err != nil) { return 0, nil, nil, false, nil, nil, false, statisticsDatum.StatisticsDatum{}, err }
yAxisIsAverage := strings.HasPrefix(yAxisAttribute, "Average ")
if (yAxisIsAverage == true){
// This function will return the attribute name
// We must convert the attribute title to the attribute name
getAttributeToGetAverageFor := func()string{
attributeTitle := strings.TrimPrefix(yAxisAttribute, "Average ")
switch attributeTitle{
case "Wealth":{
return "WealthInGold"
}
case "23andMe Neanderthal Variants":{
return "23andMe_NeanderthalVariants"
}
case "Body Fat":{
return "BodyFat"
}
case "Body Muscle":{
return "BodyMuscle"
}
case "Fruit Rating":{
return "FruitRating"
}
case "Vegetables Rating":{
return "VegetablesRating"
}
case "Nuts Rating":{
return "NutsRating"
}
case "Grains Rating":{
return "GrainsRating"
}
case "Dairy Rating":{
return "DairyRating"
}
case "Seafood Rating":{
return "SeafoodRating"
}
case "Beef Rating":{
return "BeefRating"
}
case "Pork Rating":{
return "PorkRating"
}
case "Poultry Rating":{
return "PoultryRating"
}
case "Eggs Rating":{
return "EggsRating"
}
case "Beans Rating":{
return "BeansRating"
}
case "Alcohol Frequency":{
return "AlcoholFrequency"
}
case "Tobacco Frequency":{
return "TobaccoFrequency"
}
case "Cannabis Frequency":{
return "CannabisFrequency"
}
case "Pets Rating":{
return "PetsRating"
}
case "Dogs Rating":{
return "DogsRating"
}
case "Cats Rating":{
return "CatsRating"
}
}
return attributeTitle
}
attributeToGetAverageFor := getAttributeToGetAverageFor()
totalAnalyzedUsers := 0
//Map structure: X-Axis attribute -> Number of users with this x-axis attribute who have a y-axis attribute response
responseCountsMap := make(map[string]int)
//Map structure: X-axis attribute response -> All Y-axis attribute responses summed for users with this x-axis attribute
responseSumsMap := make(map[string]float64)
// This stores a count of the number of users whose X axis value is unknown
// For example, if X axis is Age, this is the number of users who did not respond
numberOfUsersWithUnknownXAxisValue := 0
// This stores the total sum of all y axis values for users who have an unknown X axis value
// For example, if the X axis attribute is age, and the Y axis attribute is average height,
// this stores the sum of heights of all users who have no age on their profile.
usersWithUnknownXAxisValueYAxisValuesSum := float64(0)
for _, userIdentityHash := range userIdentityHashesToAnalyzeList{
profileFound, getAnyUserAttributeValueFunction, err := getRetrieveAnyAttributeFromUserNewestProfileFunction(userIdentityHash, networkType)
if (err != nil) { return 0, nil, nil, false, nil, nil, false, statisticsDatum.StatisticsDatum{}, err }
if (profileFound == false){
continue
}
userIsDisabled, _, _, err := getAnyUserAttributeValueFunction("Disabled")
if (err != nil) { return 0, nil, nil, false, nil, nil, false, statisticsDatum.StatisticsDatum{}, err }
if (userIsDisabled == true){
continue
}
attributeExists, _, userAttributeToGetAverageForValue, err := getAnyUserAttributeValueFunction(attributeToGetAverageFor)
if (err != nil) { return 0, nil, nil, false, nil, nil, false, statisticsDatum.StatisticsDatum{}, err }
if (attributeExists == false){
// This user did not respond to the attribute we are getting the average for
// We will not add them to the statistics maps
continue
}
totalAnalyzedUsers += 1
userAttributeToGetAverageForValueFloat64, err := helpers.ConvertStringToFloat64(userAttributeToGetAverageForValue)
if (err != nil) {
return 0, nil, nil, false, nil, nil, false, statisticsDatum.StatisticsDatum{}, errors.New("Database corrupt: Contains invalid " + userAttributeToGetAverageForValue + " value: " + userAttributeToGetAverageForValue)
}
attributeFound, _, userXAxisAttributeValue, err := getAnyUserAttributeValueFunction(xAxisAttribute)
if (err != nil) { return 0, nil, nil, false, nil, nil, false, statisticsDatum.StatisticsDatum{}, err }
if (attributeFound == false){
// This user did not respond to the X axis attribute
// We calculate the average for users who do not respond and put it in its own category
numberOfUsersWithUnknownXAxisValue += 1
usersWithUnknownXAxisValueYAxisValuesSum += userAttributeToGetAverageForValueFloat64
continue
}
responseCountsMap[userXAxisAttributeValue] += 1
responseSumsMap[userXAxisAttributeValue] += userAttributeToGetAverageForValueFloat64
}
_, _, formatYAxisValuesFunction, _, _, err := attributeDisplay.GetProfileAttributeDisplayInfo(attributeToGetAverageFor)
if (err != nil) { return 0, nil, nil, false, nil, nil, false, statisticsDatum.StatisticsDatum{}, err }
statisticsDatumsList := make([]statisticsDatum.StatisticsDatum, 0, len(responseCountsMap))
for attributeResponse, responsesCount := range responseCountsMap{
attributeResponseFormatted, err := formatXAxisValuesFunction(attributeResponse)
if (err != nil) { return 0, nil, nil, false, nil, nil, false, statisticsDatum.StatisticsDatum{}, err }
allResponsesSum, exists := responseSumsMap[attributeResponse]
if (exists == false){
return 0, nil, nil, false, nil, nil, false, statisticsDatum.StatisticsDatum{}, errors.New("Response sums map missing attribute value")
}
averageValue := allResponsesSum/float64(responsesCount)
averageValueString := helpers.ConvertFloat64ToStringRounded(averageValue, yAxisRoundingPrecision)
averageValueFormatted, err := formatYAxisValuesFunction(averageValueString)
if (err != nil) { return 0, nil, nil, false, nil, nil, false, statisticsDatum.StatisticsDatum{}, err }
newStatisticsDatum := statisticsDatum.StatisticsDatum{
Label: attributeResponse,
LabelFormatted: attributeResponseFormatted,
Value: averageValue,
ValueFormatted: averageValueFormatted,
}
statisticsDatumsList = append(statisticsDatumsList, newStatisticsDatum)
}
// We use this function to format values after grouping, if grouping is needed
formatValuesFunction := func(input float64)(string, error){
inputString := helpers.ConvertFloat64ToStringRounded(input, yAxisRoundingPrecision)
valueFormatted, err := formatYAxisValuesFunction(inputString)
if (err != nil) { return "", err }
return valueFormatted, nil
}
if (numberOfUsersWithUnknownXAxisValue == 0){
return totalAnalyzedUsers, statisticsDatumsList, responseCountsMap, true, responseSumsMap, formatValuesFunction, false, statisticsDatum.StatisticsDatum{}, nil
}
unknownResponsesAverage := usersWithUnknownXAxisValueYAxisValuesSum/float64(numberOfUsersWithUnknownXAxisValue)
unknownResponsesValueFormatted, err := formatValuesFunction(unknownResponsesAverage)
if (err != nil) { return 0, nil, nil, false, nil, nil, false, statisticsDatum.StatisticsDatum{}, err }
// This datum represents the average value for the yAxisAttribute for users who did not respond.
// For example, if the xAxisAttribute is Height, and the yAxisAttribute is AverageWealth, this value
// will represent the average wealth for users who did not provide Height on their profile.
unknownStatisticsDatum := statisticsDatum.StatisticsDatum{
Label: xAxisUnknownLabel,
LabelFormatted: xAxisUnknownLabel,
Value: unknownResponsesAverage,
ValueFormatted: unknownResponsesValueFormatted,
}
return totalAnalyzedUsers, statisticsDatumsList, responseCountsMap, true, responseSumsMap, formatValuesFunction, true, unknownStatisticsDatum, nil
}
return 0, nil, nil, false, nil, nil, false, statisticsDatum.StatisticsDatum{}, errors.New("Invalid y-axis attribute: " + yAxisAttribute)
}
totalAnalyzedUsers, statisticsDatumsList, responseCountsMap, yAxisIsAverage, responseSumsMap, formatValuesFunction, includeUnknownDatum, unknownValueDatum, err := getStatisticsDatumsList()
if (err != nil) { return 0, nil, false, nil, nil, err }
sortStatisticsDatumsList(statisticsDatumsList, xAxisIsNumerical)
// We now see if we need to group the datums in the list together
// We do this if there are more than 10 categories
if (len(statisticsDatumsList) <= 10){
// No grouping needed. We are done.
if (includeUnknownDatum == true){
statisticsDatumsList = append(statisticsDatumsList, unknownValueDatum)
}
return totalAnalyzedUsers, statisticsDatumsList, false, nil, formatValuesFunction, nil
}
groupedStatisticsDatumsList, err := getStatisticsDatumsListGrouped(10, statisticsDatumsList, xAxisIsNumerical, responseCountsMap, yAxisIsAverage, responseSumsMap, formatValuesFunction)
if (err != nil) { return 0, nil, false, nil, nil, err }
if (includeUnknownDatum == true){
statisticsDatumsList = append(statisticsDatumsList, unknownValueDatum)
groupedStatisticsDatumsList = append(groupedStatisticsDatumsList, unknownValueDatum)
}
return totalAnalyzedUsers, statisticsDatumsList, true, groupedStatisticsDatumsList, formatValuesFunction, nil
}
//Outputs:
// -int: Number of users analyzed in statistics
// -[]statisticsDatum.StatisticsDatum: Statistics datums list (sorted, not grouped)
// -bool: Grouping performed
// -[]statisticsDatum.StatisticsDatum: Grouped datums list
// -error
func GetUserStatisticsDatumsLists_DonutChart(identityType string,
networkType byte,
attributeToAnalyze string,
attributeIsNumerical bool,
formatAttributeLabelsFunction func(string)(string, error),
unknownLabelTranslated string)(int, []statisticsDatum.StatisticsDatum, bool, []statisticsDatum.StatisticsDatum, error){
isValid := helpers.VerifyNetworkType(networkType)
if (isValid == false){
networkTypeString := helpers.ConvertByteToString(networkType)
return 0, nil, false, nil, errors.New("GetUserStatisticsDatumsLists_DonutChart called with invalid networkType: " + networkTypeString)
}
totalAnalyzedUsers, statisticsDatumsList, attributeCountsMap, numberOfUnknownResponders, err := getProfileAttributeCountStatisticsDatumsList(identityType, networkType, attributeToAnalyze, formatAttributeLabelsFunction)
if (err != nil) { return 0, nil, false, nil, err }
sortStatisticsDatumsList(statisticsDatumsList, attributeIsNumerical)
getUnknownValueDatum := func()statisticsDatum.StatisticsDatum{
numberOfUnknownRespondersString := helpers.ConvertIntToString(numberOfUnknownResponders)
unknownValueDatum := statisticsDatum.StatisticsDatum{
Label: unknownLabelTranslated,
LabelFormatted: unknownLabelTranslated,
Value: float64(numberOfUnknownResponders),
ValueFormatted: numberOfUnknownRespondersString,
}
return unknownValueDatum
}
if (len(statisticsDatumsList) <= 8){
// No grouping needed.
if (numberOfUnknownResponders != 0){
unknownValueDatum := getUnknownValueDatum()
statisticsDatumsList = append(statisticsDatumsList, unknownValueDatum)
}
return totalAnalyzedUsers, statisticsDatumsList, false, nil, nil
}
formatValuesFunction := func(input float64)(string, error){
// input will always be the number of users who responded with the value
// Thus, input will always be an integer
result := helpers.ConvertFloat64ToStringRounded(input, 0)
return result, nil
}
groupedStatisticsDatumsList, err := getStatisticsDatumsListGrouped(8, statisticsDatumsList, attributeIsNumerical, attributeCountsMap, false, nil, formatValuesFunction)
if (err != nil) { return 0, nil, false, nil, err }
if (numberOfUnknownResponders != 0){
unknownValueDatum := getUnknownValueDatum()
statisticsDatumsList = append(statisticsDatumsList, unknownValueDatum)
groupedStatisticsDatumsList = append(groupedStatisticsDatumsList, unknownValueDatum)
}
return totalAnalyzedUsers, statisticsDatumsList, true, groupedStatisticsDatumsList, nil
}
// This function will return a statistics datums list of the following format:
// "Label": Attribute name (Example: "Male")
// "Value": The number of users who responded with the attribute (in this example: "Male")
//
// All users of provided identityType who are not disabled will be analyzed
// -int: Number of analyzed users
// -[]statisticsDatum.StatisticsDatum: Statistics datums list (not sorted or grouped)
// -map[string]int: Response counts map (Response -> Number of responders)
// -int: Number of No Response/Unknown value responders
// -error
func getProfileAttributeCountStatisticsDatumsList(identityType string,
networkType byte,
attributeName string,
formatLabelsFunction func(string)(string, error))(int, []statisticsDatum.StatisticsDatum, map[string]int, int, error){
userIdentityHashesToAnalyzeList, err := getUserIdentityHashesToAnalyzeList(identityType)
if (err != nil) { return 0, nil, nil, 0, err }
totalAnalyzedUsers := 0
// Map structure: User Attribute response -> Number of users who responded with the response
responseCountsMap := make(map[string]int)
// This stores the number of users for whom we do not know their value
numberOfUnknownValueUsers := 0
for _, userIdentityHash := range userIdentityHashesToAnalyzeList{
profileFound, getAnyUserAttributeValueFunction, err := getRetrieveAnyAttributeFromUserNewestProfileFunction(userIdentityHash, networkType)
if (err != nil) { return 0, nil, nil, 0, err }
if (profileFound == false){
continue
}
userIsDisabled, _, _, err := getAnyUserAttributeValueFunction("Disabled")
if (err != nil) { return 0, nil, nil, 0, err }
if (userIsDisabled == true){
continue
}
totalAnalyzedUsers += 1
attributeFound, _, attributeValue, err := getAnyUserAttributeValueFunction(attributeName)
if (err != nil) { return 0, nil, nil, 0, err }
if (attributeFound == false){
numberOfUnknownValueUsers += 1
continue
}
responseCountsMap[attributeValue] += 1
}
statisticsDatumsList := make([]statisticsDatum.StatisticsDatum, 0, len(responseCountsMap))
for attributeResponse, numberOfUsers := range responseCountsMap{
attributeResponseFormatted, err := formatLabelsFunction(attributeResponse)
if (err != nil) { return 0, nil, nil, 0, err }
attributeNumberOfUsersString := helpers.ConvertIntToString(numberOfUsers)
newStatisticsDatum := statisticsDatum.StatisticsDatum{
Label: attributeResponse,
LabelFormatted: attributeResponseFormatted,
Value: float64(numberOfUsers),
ValueFormatted: attributeNumberOfUsersString,
}
statisticsDatumsList = append(statisticsDatumsList, newStatisticsDatum)
}
return totalAnalyzedUsers, statisticsDatumsList, responseCountsMap, numberOfUnknownValueUsers, nil
}
func sortStatisticsDatumsList(inputStatisticsDatumsList []statisticsDatum.StatisticsDatum, labelIsNumerical bool){
if (len(inputStatisticsDatumsList) <= 1){
return
}
if (labelIsNumerical == true){
// We sort the datums by label values in ascending order
// Example: Bar chart columns are ages, in order of youngest to oldest
compareDatumsFunction := func(datumA statisticsDatum.StatisticsDatum, datumB statisticsDatum.StatisticsDatum)int{
datumALabel := datumA.Label
datumBLabel := datumB.Label
datumAFloat64, err := helpers.ConvertStringToFloat64(datumALabel)
if (err != nil) {
panic("Invalid statistics datum: Datum Label is not float: " + datumALabel)
}
datumBFloat64, err := helpers.ConvertStringToFloat64(datumBLabel)
if (err != nil) {
panic("Invalid statistics datum: Datum Label is not float: " + datumBLabel)
}
if (datumAFloat64 == datumBFloat64){
return 0
}
if (datumAFloat64 < datumBFloat64){
return -1
}
return 1
}
slices.SortFunc(inputStatisticsDatumsList, compareDatumsFunction)
return
}
// We sort the datums by their values in descending order
compareDatumsFunction := func(datum1 statisticsDatum.StatisticsDatum, datum2 statisticsDatum.StatisticsDatum)int{
datum1Value := datum1.Value
datum2Value := datum2.Value
if (datum1Value == datum2Value){
return 0
}
if (datum1Value < datum2Value){
return 1
}
return -1
}
slices.SortFunc(inputStatisticsDatumsList, compareDatumsFunction)
}
// This function will group a statistics datums list.
// It will group Labels and their values to fit into a specified number of groups
// Example: "1","2","3","4" -> "1-2", "3-4"
//Inputs:
// -int: Maximum groups to create
// -[]statisticsDatum.StatisticsDatum: Statistics datums list to group
// -bool: Label is numerical
// -If it is, we will group labels into groups of numbers.
// -Otherwise, we will group all categories after the first maximumGroupsToCreate into a group called Other
// -map[string]int
// -Response Counts map (Attribute response -> Number of responders with that response)
// -bool: Value is average
// -If value is average, we will combine the values of each group and find their average
// -Otherwise, we will find their sum
// -map[string]float64
// -Response Sums map (X-axis attribute response -> all Y-axis responses summed) (If yAxisIsAverage == true)
// -func(float64)(string, error): This is the function we use to format the values
//Outputs:
// -[]statisticsDatum.StatisticsDatum: Grouped statistics datums list
// -error
func getStatisticsDatumsListGrouped(maximumGroupsToCreate int,
inputStatisticsDatumsList []statisticsDatum.StatisticsDatum,
labelIsNumerical bool,
responseCountsMap map[string]int,
valueIsAverage bool,
responseSumsMap map[string]float64,
formatValuesFunction func(float64)(string, error))([]statisticsDatum.StatisticsDatum, error){
if (len(inputStatisticsDatumsList) <= maximumGroupsToCreate){
return nil, errors.New("maximumGroupsToCreate is <= length of input statistics datums list")
}
// We deep copy the statistics datums list to retain the sorted version
// We need to retain both versions because the user can view the raw or grouped data in the GUI
statisticsDatumsList := slices.Clone(inputStatisticsDatumsList)
// We use this function to get the new value for a group of labels
getGroupValue := func(datumsToCombineList []statisticsDatum.StatisticsDatum)(float64, error){
// This will count the total number of users who responded with the responses within this group
// Example: Labels are "Blue", "Green", this variable will store the number of users who responded with either Blue or Green
totalRespondersCount := float64(0)
// This will store the sum for all response values within the group
// We only need to add to this sum if valueIsAverage == true
allReponsesSummed := float64(0)
for _, statisticsDatum := range datumsToCombineList{
datumLabel := statisticsDatum.Label
responderCount, exists := responseCountsMap[datumLabel]
if (exists == false){
return 0, errors.New("responseCountsMap missing label: " + datumLabel)
}
totalRespondersCount += float64(responderCount)
if (valueIsAverage == true){
yAxisAttributeResponsesSum, exists := responseSumsMap[datumLabel]
if (exists == false){
return 0, errors.New("responseSumsMap missing label: " + datumLabel)
}
allReponsesSummed += yAxisAttributeResponsesSum
}
}
if (valueIsAverage == false){
return totalRespondersCount, nil
}
// The value is an average
// We need to find the average for all of the user responses for the labels in the input list
// The Values in the inputStatisticsDatumsList are averages
// We can't average out the averages, because that will not give us the true average
// We have to use the original sums for all group datums and average them
if (totalRespondersCount == 0){
return 0, errors.New("totalRespondersCount is 0.")
}
value := allReponsesSummed/float64(totalRespondersCount)
return value, nil
}
if (labelIsNumerical == true){
maximumDatumsPerCategory := int(math.Ceil(float64(len(statisticsDatumsList))/float64(maximumGroupsToCreate)))
statisticsDatumsListSublists, err := helpers.SplitListIntoSublists(statisticsDatumsList, maximumDatumsPerCategory)
if (err != nil) { return nil, err }
groupedDatumsList := make([]statisticsDatum.StatisticsDatum, 0, len(statisticsDatumsListSublists))
for _, groupDatumsListSublist := range statisticsDatumsListSublists{
if (len(groupDatumsListSublist) == 1){
// Sometimes, a group with 1 datum will be created
// This happens if the groups cannot be evenly divided, so there is a remainder of 1.
// Example: 10->4 groups = 3, 3, 3, 1.
//TODO: Prevent this from happening so groups always have more than 1 subdatum
groupDatum := groupDatumsListSublist[0]
groupedDatumsList = append(groupedDatumsList, groupDatum)
continue
}
finalIndex := len(groupDatumsListSublist)-1
initialDatum := groupDatumsListSublist[0]
finalDatum := groupDatumsListSublist[finalIndex]
initialLabel := initialDatum.Label
initialLabelFormatted := initialDatum.LabelFormatted
finalLabel := finalDatum.Label
finalLabelFormatted := finalDatum.LabelFormatted
groupValue, err := getGroupValue(groupDatumsListSublist)
if (err != nil) { return nil, err }
groupValueFormatted, err := formatValuesFunction(groupValue)
if (err != nil) { return nil, err }
newGroupStatisticsDatum := statisticsDatum.StatisticsDatum{
Label: initialLabel + "-" + finalLabel,
LabelFormatted: initialLabelFormatted + "-" + finalLabelFormatted,
Value: groupValue,
ValueFormatted: groupValueFormatted,
}
groupedDatumsList = append(groupedDatumsList, newGroupStatisticsDatum)
}
return groupedDatumsList, nil
}
// Label is not numerical
// We combine all categories after the first maximumGroupsToCreate into a category called Other
datumsToKeep := statisticsDatumsList[:maximumGroupsToCreate]
datumsToCombine := statisticsDatumsList[maximumGroupsToCreate:]
otherTranslated := translation.TranslateTextFromEnglishToMyLanguage("Other")
otherGroupValue, err := getGroupValue(datumsToCombine)
if (err != nil) { return nil, err }
otherGroupValueFormatted, err := formatValuesFunction(otherGroupValue)
if (err != nil) { return nil, err }
otherGroupDatum := statisticsDatum.StatisticsDatum{
Label: "Other",
LabelFormatted: otherTranslated,
Value: otherGroupValue,
ValueFormatted: otherGroupValueFormatted,
}
groupedStatisticsDatumsList := append(datumsToKeep, otherGroupDatum)
return groupedStatisticsDatumsList, nil
}
func getUserIdentityHashesToAnalyzeList(identityType string)([][16]byte, error){
allUserIdentityHashesList, err := badgerDatabase.GetAllProfileIdentityHashes(identityType)
if (err != nil) { return nil, err }
if (len(allUserIdentityHashesList) < 10000){
return allUserIdentityHashesList, nil
}
helpers.RandomizeListOrder(allUserIdentityHashesList)
identityHashesToAnalyzeList := allUserIdentityHashesList[:10000]
return identityHashesToAnalyzeList, nil
}
//Outputs:
// -bool: Profile Exists
// -func(string)(bool, int, string, error)
// -error
func getRetrieveAnyAttributeFromUserNewestProfileFunction(identityHash [16]byte, networkType byte)(bool, func(string)(bool, int, string, error), error){
newestProfileExists, profileVersion, _, _, _, newestProfileMap, err := profileStorage.GetNewestUserProfile(identityHash, networkType)
if (err != nil) { return false, nil, err }
if (newestProfileExists == false){
return false, nil, nil
}
getAnyAttributeFromUserNewestProfileFunction, err := calculatedAttributes.GetRetrieveAnyProfileAttributeIncludingCalculatedFunction(profileVersion, newestProfileMap)
if (err != nil) { return false, nil, err }
return true, getAnyAttributeFromUserNewestProfileFunction, nil
}