Skip to content

Instantly share code, notes, and snippets.

@Mcrich23
Last active March 26, 2024 22:11
Show Gist options
  • Save Mcrich23/fbfe15d8df80a9444b815c361d9689f2 to your computer and use it in GitHub Desktop.
Save Mcrich23/fbfe15d8df80a9444b815c361d9689f2 to your computer and use it in GitHub Desktop.
Stem and Leaf Plot Generator
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)
@Mcrich23
Copy link
Author

Mcrich23 commented Mar 9, 2024

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment