Last active
March 26, 2024 22:11
-
-
Save Mcrich23/fbfe15d8df80a9444b815c361d9689f2 to your computer and use it in GitHub Desktop.
Stem and Leaf Plot Generator
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
import Cocoa | |
var outlierRecalculationOccured = false | |
var calculatedOutliers: [Float] = [] | |
// Enter data to be used | |
let ogData: [Float] = [22, 19, 25, 27, 30, 22, 19, 16, 23, 25, 21, 27, 28, 24, 25, 30, 35, 23, 22, 27].sorted() | |
extension Float { | |
/// Rounds the Float to decimal places value | |
func rounded(toPlaces places:Int) -> Float { | |
let divisor = pow(10.0, Float(places)) | |
return (self * divisor).rounded() / divisor | |
} | |
func lastDigitRemoved() -> Float? { | |
// Convert the float to a string | |
let numberString = String(self).replacingOccurrences(of: ".0", with: "") | |
// Check if the string is empty or contains only a single character | |
if numberString.isEmpty || numberString.count == 1 { | |
return 0 | |
} | |
// Remove the last character (digit) | |
let stringWithoutLastDigit = String(numberString.dropLast()) | |
// Convert the modified string back to a float | |
if let modifiedFloat = Float(stringWithoutLastDigit) { | |
return modifiedFloat | |
} else { | |
return nil // Return nil if the conversion fails | |
} | |
} | |
} | |
func findMode<T: Hashable>(array: [T]) -> T? { | |
var counts = [T: Int]() | |
var maxCount = 0 | |
var mode: T? | |
for element in array { | |
counts[element, default: 0] += 1 | |
if let currentCount = counts[element], currentCount > maxCount { | |
maxCount = currentCount | |
mode = element | |
} | |
} | |
return mode | |
} | |
func compressedFloat(_ Float: Float) -> Float { | |
if floor(Float) == Float { | |
return Float.rounded(toPlaces: 0) | |
} else { | |
return Float.rounded(toPlaces: 3) | |
} | |
} | |
func compressedFloatString(_ Float: Float) -> String { | |
if floor(Float) == Float { | |
return "\(Int(Float))" | |
} else { | |
return "\(Float.rounded(toPlaces: 3))" | |
} | |
} | |
func compressedFloatArrayString(_ floats: [Float]) -> String { | |
let floatStrings = floats.map({ compressedFloatString($0) }) | |
return "[\(floatStrings.joined(separator: ", "))]" | |
} | |
func calculateData(_ data: [Float]) { | |
func getStemAndLeafDict() -> [Int : [Float]] { | |
var stemAndLeafDict: [Int: [Float]] = [:] | |
let splitArray: [Float] = data.compactMap({ $0.lastDigitRemoved() }) | |
let minStem = Int(splitArray.min() ?? 0) | |
let maxStem = Int(splitArray.max() ?? 0) | |
for int in minStem...maxStem { | |
stemAndLeafDict[int] = [] | |
} | |
return stemAndLeafDict | |
} | |
func getStemAndLeafDict() -> [Int : [Int]] { | |
var stemAndLeafDict: [Int: [Int]] = [:] | |
let splitArray: [Float] = data.compactMap({ $0.lastDigitRemoved() }) | |
let minStem = Int(splitArray.min() ?? 0) | |
let maxStem = Int(splitArray.max() ?? 0) | |
for int in minStem...maxStem { | |
stemAndLeafDict[int] = [] | |
} | |
return stemAndLeafDict | |
} | |
func createReadableDecimalStemAndLeafTable(data: [Float]) -> String { | |
// Create a dictionary to store stems and corresponding leaves | |
var stemAndLeafDict: [Int: [Float]] = getStemAndLeafDict() | |
// Iterate through the data and split it into stems and leaves | |
for value in data { | |
let stem = Int(value) | |
let leaf = (value - Float(stem)) * 10.0 | |
// Append the leaf to the corresponding stem | |
if var leaves = stemAndLeafDict[stem] { | |
leaves.append(leaf) | |
stemAndLeafDict[stem] = leaves | |
} else { | |
stemAndLeafDict[stem] = [leaf] | |
} | |
} | |
// Sort the stems | |
let sortedStems = stemAndLeafDict.keys.sorted() | |
// Create a string to hold the formatted stem-and-leaf plot | |
var result = "Stem | Leaves\n" | |
// Generate the stem-and-leaf plot and append it to the result string | |
for stem in sortedStems { | |
let leaves = stemAndLeafDict[stem]!.sorted() | |
let leafString = leaves.map { String(format: "%.1f", $0) }.joined(separator: " ") | |
result += String(format: "%4d | %@\n", stem, leafString) | |
} | |
return result | |
} | |
// Function to create a stem-and-leaf plot as an HTML table | |
func createCopyableDecimalStemAndLeafTable(data: [Float]) -> String { | |
// Create a dictionary to store stems and corresponding leaves | |
var stemAndLeafDict: [Int: [Float]] = getStemAndLeafDict() | |
// Iterate through the data and split it into stems and leaves | |
for value in data { | |
let stem = Int(value) | |
let leaf = (value - Float(stem)) * 10.0 | |
// Append the leaf to the corresponding stem | |
if var leaves = stemAndLeafDict[stem] { | |
leaves.append(leaf) | |
stemAndLeafDict[stem] = leaves | |
} else { | |
stemAndLeafDict[stem] = [leaf] | |
} | |
} | |
// Sort the stems | |
let sortedStems = stemAndLeafDict.keys.sorted() | |
// Create an HTML string to hold the formatted stem-and-leaf plot | |
var html = "<html><head><title>Decimal Stem-and-Leaf Plot</title></head><body><h1>Decimal Stem-and-Leaf Plot</h1><table><tr><th>Stem</th><th>Leaves</th></tr>" | |
// Generate the stem-and-leaf plot and append it to the HTML string | |
for stem in sortedStems { | |
let leaves = stemAndLeafDict[stem]!.sorted() | |
let leafString = leaves.map { String(format: "%.1f", $0) }.joined(separator: " ") | |
html += "<tr><td>\(stem)</td><td>\(leafString)</td></tr>" | |
} | |
html += "</table></body></html>" | |
return html | |
} | |
func createReadableWholeStemAndLeafTable(data: [Int]) -> String { | |
// Create a dictionary to store stems and corresponding leaves | |
var stemAndLeafDict: [Int: [Int]] = getStemAndLeafDict() | |
// Iterate through the data and split it into stems and leaves | |
for value in data { | |
let stem = value / 10 | |
let leaf = value % 10 | |
// Append the leaf to the corresponding stem | |
if var leaves = stemAndLeafDict[stem] { | |
leaves.append(leaf) | |
stemAndLeafDict[stem] = leaves | |
} else { | |
stemAndLeafDict[stem] = [leaf] | |
} | |
} | |
// Sort the stems | |
let sortedStems = stemAndLeafDict.keys.sorted() | |
// Create a string to hold the formatted stem-and-leaf plot | |
var result = "Stem | Leaves\n" | |
// Generate the stem-and-leaf plot and append it to the result string | |
for stem in sortedStems { | |
let leaves = stemAndLeafDict[stem]!.sorted() | |
let leafString = leaves.map { String($0) }.joined(separator: " ") | |
result += String(format: "%4d | %@\n", stem, leafString) | |
} | |
return result | |
} | |
func createCopyableWholeStemAndLeafTable(data: [Int]) -> String { | |
// Create a dictionary to store stems and corresponding leaves | |
var stemAndLeafDict: [Int: [Int]] = getStemAndLeafDict() | |
// Iterate through the data and split it into stems and leaves | |
for value in data { | |
let stem = value / 10 | |
let leaf = value % 10 | |
// Append the leaf to the corresponding stem | |
if var leaves = stemAndLeafDict[stem] { | |
leaves.append(leaf) | |
stemAndLeafDict[stem] = leaves | |
} else { | |
stemAndLeafDict[stem] = [leaf] | |
} | |
} | |
// Sort the stems | |
let sortedStems = stemAndLeafDict.keys.sorted() | |
// Create an HTML string to hold the formatted stem-and-leaf plot | |
var html = "<html><head><title>Stem-and-Leaf Plot</title></head><body><h1>Stem-and-Leaf Plot</h1><table><tr><th>Stem</th><th>Leaves</th></tr>" | |
// Generate the stem-and-leaf plot and append it to the HTML string | |
for stem in sortedStems { | |
let leaves = stemAndLeafDict[stem]!.sorted() | |
let leafString = leaves.map { String($0) }.joined(separator: " ") | |
html += "<tr><td>\(stem)</td><td>\(leafString)</td></tr>" | |
} | |
html += "</table></body></html>" | |
return html | |
} | |
let intDataCount = data.filter({ floor($0) == $0 }).count | |
// Determine if ints or Floats | |
let stemAndLeafTable: String | |
if data.count == intDataCount { | |
// Create the stem-and-leaf plot as a TSV | |
let intData = data.map({ Int($0) }) | |
let readableStemAndLeafTable = createReadableWholeStemAndLeafTable(data: intData) | |
// Print the TSV table (you can copy this output to OneNote) | |
stemAndLeafTable = readableStemAndLeafTable | |
let copyableStemAndLeafTable = createCopyableWholeStemAndLeafTable(data: intData) | |
let pasteboard = NSPasteboard.general | |
pasteboard.declareTypes([.string], owner: nil) | |
pasteboard.setString(copyableStemAndLeafTable, forType: .html) | |
} else { | |
// Create the stem-and-leaf plot as a TSV | |
let readableStemAndLeafTable = createReadableDecimalStemAndLeafTable(data: data) | |
// Print the TSV table (you can copy this output to OneNote) | |
stemAndLeafTable = readableStemAndLeafTable | |
let copyableStemAndLeafTable = createCopyableDecimalStemAndLeafTable(data: data) | |
let pasteboard = NSPasteboard.general | |
pasteboard.declareTypes([.string], owner: nil) | |
pasteboard.setString(copyableStemAndLeafTable, forType: .html) | |
} | |
let medianLocation: Float | |
switch (data.count+1).isMultiple(of: 2) { | |
case true: | |
medianLocation = (Float(data.count+1)/2) | |
case false: | |
medianLocation = (Float(data.count+1)/2)-1 | |
} | |
let median: String | |
if floor(medianLocation) == medianLocation { | |
let index = Int(medianLocation-1) | |
median = compressedFloatString(data[index]) | |
} else { | |
let lowerIndex = Int(medianLocation.rounded()-1) | |
let upperIndex = Int(medianLocation.rounded()) | |
let lowerNumber = data[lowerIndex] | |
let upperNumber = data[upperIndex] | |
let intMedian = (lowerNumber + upperNumber)/2 | |
median = compressedFloatString(intMedian) | |
} | |
// Get mean | |
var numerator: Float = 0 | |
for datum in data { | |
numerator += datum | |
} | |
let mean = numerator/Float(data.count) | |
// Get Mode | |
let mode = findMode(array: data) | |
// Get min and max | |
let min = data.sorted().first | |
let max = data.sorted().last | |
let roundedDownHalfDataCount: Double | |
switch (data.count+1).isMultiple(of: 2) { | |
case true: | |
roundedDownHalfDataCount = floor(Double(data.count/2))+1 | |
case false: | |
roundedDownHalfDataCount = floor(Double(data.count/2)) | |
} | |
let qDrop = Int(roundedDownHalfDataCount) | |
// Get 1st Quartile | |
let q1Data = data.dropLast(qDrop) | |
let q1Location: Float = (Float(q1Data.count+1)/2)-1 // -1 to account for array starting at 0 | |
let q1: Float | |
if floor(q1Location) == q1Location { | |
let index = Int(q1Location) | |
q1 = q1Data[index] | |
} else { | |
let lowerIndex = Int(q1Location.rounded()-1) | |
let upperIndex = Int(q1Location.rounded()) | |
let lowerNumber = data[lowerIndex] | |
let upperNumber = data[upperIndex] | |
q1 = (lowerNumber + upperNumber)/2 | |
} | |
// Get 3rd Quartile | |
let q3Data = Array(data.dropFirst(qDrop)).compactMap({ $0 }) | |
let q3Location: Float = (Float(q3Data.count+1)/2)-1 // -1 to account for array starting at 0 | |
let q3: Float | |
if floor(q3Location) == q3Location { | |
let index = Int(q3Location) | |
q3 = q3Data[index] | |
} else { | |
let lowerIndex = Int(q3Location.rounded()-1) | |
let upperIndex = Int(q3Location.rounded()) | |
let lowerNumber = q3Data[lowerIndex] | |
let upperNumber = q3Data[upperIndex] | |
q3 = (lowerNumber + upperNumber)/2 | |
} | |
// Calculate outliers | |
let iqr = q3-q1 | |
let lowerBound = q1-(iqr*1.5) | |
let upperBound = q3+(iqr*1.5) | |
let outliers: [Float] | |
if calculatedOutliers.isEmpty { | |
outliers = ogData.filter({ $0 < lowerBound || $0 > upperBound }) | |
} else { | |
outliers = calculatedOutliers | |
} | |
func printData() { | |
print("Sorted Data: \(compressedFloatArrayString(ogData))") | |
print(stemAndLeafTable) | |
print("Copied to Clipboard") | |
print("-------------- Measures of Central Tendency: --------------") | |
print("Median: \(median)") | |
print("Mean: \(compressedFloatString(mean))") | |
if let mode { | |
print("Mode: \(compressedFloatString(mode))") | |
} | |
if let min, let max { | |
let range = max-min | |
print("Range: \(compressedFloatString(range))") | |
print("-------------- The 5 Number Summary: --------------") | |
print("Minimum: \(compressedFloatString(min))") | |
} else { | |
print("-------------- The 5 Number Summary: --------------") | |
} | |
print("1st Quartile: \(compressedFloatString(q1))") | |
print("3rd Quartile: \(compressedFloatString(q3))") | |
if let max { | |
print("Maximum: \(compressedFloatString(max))") | |
} | |
if outliers.isEmpty { | |
print("No Outliers") | |
} else { | |
print("Outliers: \(outliers)") | |
} | |
} | |
if outliers.isEmpty { | |
printData() | |
} else { | |
if !outlierRecalculationOccured { | |
outlierRecalculationOccured = true | |
calculatedOutliers = outliers | |
let newData = data.filter({ !outliers.contains($0) }) | |
calculateData(newData) | |
} else { | |
printData() | |
} | |
} | |
} | |
calculateData(ogData) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Change the data variable to generate a stem and leaf plot for the data you desire. Note: it will auto copy the plot to your clipboard