Oefenen met survey data 🙈 - jonahgoldwastaken/functional-programming GitHub Wiki
Oefenen met survey data
Voor het introductie-vak Datavisualisatie heeft de gehele cluster een enquête ingevuld met veel verschillende vragen. Deze vragen bieden allemaal verschillende antwoorden, en daarbij dus veel mogelijkheden om mee te oefenen voor data-verwerking en Functional Programming.
De dataset bevat vragen als lievelingskleur, hoeveelheid ooms en tantes, huisdieren, stress-niveau, favoriete film, lievelingsdrug en gesproken talen
Voordat ik begin met de oefening
Voordat ik startte met de oefening dinsdag, heb ik eerst alle CSV data, zoals dat heet, omgevormd naar een JSON bestand. Ik was al bekend met het verwerken van CSV data, en heb hiervoor mijn eigen parser geschreven om dit snel om te kunnen zetten. CSV staat trouwens voor Comma-Separated Values. Het is een bestandstype waarin de data in een tabel staat, waarvan de kolommen worden gescheiden met een komma, of punt-komma wat in dit geval zo is.
Nu ik dit voor elkaar heb gekregen, kunnen we aan de slag.
Doel van de oefening
Het doel van deze oefening is voor mij om een set aan functies te hebben geschreven die toe te passen zijn op de data van het RDW. Ik ben al zeer bekend met het Functional Programming paradigma, en zal voornamelijk leren hoe ik de patronen toe kan passen i.p.v. inhoudelijk ga leren hoe je FP-code schrijft in JavaScript.
Oefenen met talen
Uit deze enorme grabbelton aan vragen trek ik dus de vraag gesproken talen. Dit vind ik een goed beginpunt omdat het mijn Functional-Programming skills weer wat opfrist. Die kliekjes staan al een tijdje in de vriezer, en moeten even opgewarmd worden.
De code schrijven gaat mij eigenlijk wel makkelijker af dan ik had gedacht. Ik heb de code in een middag geschreven, en hiermee al veel stappen bereikt. De stappen zijn namelijk:
- Kies alleen de gesproken talen uit alle data-objecten, zodat ik een array krijg met alle antwoorden op alleen die vraag.
- Aangezien de antwoorden alleen strings zijn, en niet strings met individuele talen, splits ik deze strings op bepaalde waardes waarop mijn mede-studenten en ik onze gesproken talen in ons antwoord hebben opgedeeld door middel van een RegEx die kijkt naar puntkomma, komma, punt en spaties (\s). Ik ben achter deze waardes gekomen door stapsgewijs de code uit te voeren totdat ik alle talen netjes per persoon in een array heb.
- Tijdens de test voor de functie hiervoor kwam ik erachter dat sommige mensen talen afgekort hebben ingevuld, die verander ik naar de volle variant met deze functie, die de ingevulde talen pakt, kijkt of de property met dezelfde naam een waarde heeft, en die dan terugstuurt. De test wordt gedaan op een variabele genaamd shortenedValues, die ik helaas heb moeten hardcoden aangezien 1 iemand "Engels" als "ENG" heeft geschreven, wat niet de standaard ("EN") is. Daarnaast zijn alle packages die ik hiervoor heb onderzocht in het Engels, en dit is allemaal in het Nederlands.
- Ik kijk met deze functie of de taal wel echt een valide taal is. Ik kijk of de string geen lege string of een nummer is (want mensen dachten soms dat "2" ook een taal is) en daarna of de waarde van de string terugkomt in mijn hardcoded "validValues" array. Ik heb de filtering in een losse functie gezet, maar heb deze later weer terug in de hoofdfunctie zelf gezet. Ik heb bij die commit ook tevens documentatie voor alle functies toegevoegd met TSDoc, de TypeScript variant van JSDoc. Dit zijn allebei documentatie-standaarden die werken op basis van comments. Nog later heb ik nog wat extra talen uit validValues weggehaald.
- Hierna geef ik de talen een hoofdletter, is wel zo netjes natuurlijk.
- In deze commit voeg ik een functie toe die de talen op alfabetische volgorde sorteert, zodat de antwoorden nog netter worden weergegeven.
Er waren gelukkig geen edge-cases zoals lege antwoorden en dergelijken waar ik rekening mee hoefde te houden. Ongeldige talen zijn alleen wel weggefilterd dus.
Momentje van rust en reflectie
Nadat ik deze code had geschreven, heb ik mij een dag gefocust op de documentatie en de opzet van mijn repository zodat ik een paar handelingen niet meer handmatig hoef uit te voeren (zoals het compileren van TypeScript en het opnieuw opstarten van Node).
Door de dag heen heb ik na kunnen denken over de organisatie van mijn code, en wilde ik eigenlijk al van start gaan met de data van het RDW. Ik zat me daar heel erg op te focussen, maar kwam tijdens de standup erachter dat we voor nu nog niet met de API hoeven te gaan sleutelen (thanks Gijs 😊).
Ik ga me dus focussen op het "algemeniseren", zoals ik dat noem, van mijn code. Dit doe ik om het uiteindelijk verder toe te kunnen passen op de oefen-data, en uiteindelijk dan op de RDW data.
Refactor
De codestructuur die ik wil aanpakken heb ik van het voorbeeld dat Danny hier heeft uitgelicht. Dat is dus een hoofdmap met daarin een "utilities", "helpers" en "modules" map.
Utilities
Hierin heb ik de functies neergezet die los van hun functie om de talen te verwerken getrokken konden worden. Dit zijn dus functies die in principe overal inzetbaar, en uitbreidbaar zijn.
Helpers
Hier heb ik de spullen die nodig zijn voor specifieke use cases neergezet, denk aan de "validValues" array of het object "shortenedLanguages", die heb ik dan ook verwerkt in twee van de utility functions, en alvast deze twee waardes in verwerkt zodat ik dat niet handmatig hoef te doen. Een helping hand if you will.
Modules
Hier komt de uiteindelijke verwerking van de data terecht. De modules bevatten functional chains die de data omzetten naar wat het moet wezen. Met nog een kleine tip van Laurens heb ik nog wat aanpassingen gemaakt om een dataset-functie nog abstracter te maken. Misschien moet de naam van de functie nog wel even veranderd worden. 🤔
Al met al is deze refactor dus erg goed geslaagd. Ik heb functional chaining, projectorganisatie en pure functions toegepast om de code-base consistent te krijgen. Nu nog de TSDoc weer toevoegen, maar dat is bijzaak. Op naar de volgende kolom!
Huisdieren
De huisdieren-kolom is ontzettend uitdagend, want de kolom bevat erg veel edge cases. De vraag was om de huisdieren te onderscheiden met een puntkomma, en het type huisdier en de naam te onderscheiden met een dubbele punt. Niemand heeft zich echter aan deze structuur gehouden.
De antwoorden zijn zo willekeurig, dat het bijna onmogelijk is om een consistente stijl hieruit te krijgen. De mogelijkheid is er echter wel, dus het is een leuke uitdaging voor mij. De edge cases zijn onder andere:
- Alleen soort huisdier opgegeven
- Hoeveelheid van soort huisdier bij verschillende ouders, met het aantal erbij.
- Geen scheidingstekens tussen huisdieren.
- Huisdier-namen voor het soort.
- huisdier-namen achter het soort.
- verschillende scheidingstekens tussen namen, soorten en huisdieren.
Het is dus duidelijk dat ik eerst de type huisdieren uit de strings moet halen, en de aantallen moet kunnen aantonen.
Ik schrijf dit proces pas achteraf, dus heb veel tijd lopen verdoen met het uitvinden van de manier om de data te verwerken, voordat ik de data überhaupt heb doorgespit.
Bouwstenen verwerken
Allereerst probeerde ik de functions die ik al had geschreven toe te passen op de data. Ik wilde kijken hoe ver ik zou komen. Deze tactiek was van korte duur, want het enige wat ik voor elkaar kreeg was dat ik de antwoorden kan opsplitsen in dirty strings die nog verder opgeschoond moeten worden. Ik kon nog net tuples maken met de soorten en aantallen per antwoord, maar dat was dan ook met soorten die niet opgeschoond waren, en grotendeels dus niet klopten.
Ik moest dus verder gaan met het schrijven van nieuwe functies, maar merkte dat de chain die ik tot nu had geschreven heel erg leek op de chain voor de talenkolom. Het lijkt me dus handiger om individuele chains te schrijven die algemeen in te zetten zijn, zodat ik met deze chains andere kolommen uit de survey data op kan frissen ook.
Tijdens het implementeren van de al bestaande functies besefte ik me ook dat de term "modules", die worden gebruikt om functions te "composeren" (het samenvoegen van functies om functionaliteiten uit te breiden), helemaal verkeerd had. Ik kende de structuur ook nog niet voor het vak, zoals ik hierboven heb uitgelicht, dus ga ik de code omzetten in compositions of function pipes die data door meerdere functies heen sturen.
Modules refactoren
Ik ben dus aan de slag gegaan met het refactoren van mijn modules. Het is een hele uitdaging om uit te vogelen hoe ik een structuur opzet die de kolommen zo prepareert dat ik daarna functies erop los kan laten. Het is wel erg interessant om te zien hoe ik niet rekening hield met de mogelijkheid om ook andere kolommen te verwerken met mijn eigen huidige code-structuur.
Wat ik voor nu dus wil maken voor alle kolommen is een pipe die:
- De juiste kolom kiesbaar stelt voor de dataset
- De rijen uit de kolom split op bepaalde karakters
- De strings waar ik niets aan heb weggefilterd uit de gesplitte string.
Dit is gelukt. Ik heb nu netjes twee modules, "objectArray" en "stringArray" met daarin respectievelijk function compositions voor arrays met objecten en arrays met strings. Ik heb de tuple-code weggehaald, omdat het niet meer paste in de huidige code-structuur. Ik ga nu een structuur uitwerken waarin de data uiteindelijk verwerkt moet worden, zodat ik weet waar ik naartoe werk.
Diagrammen
Ik heb in een diagram-tool genaamd Diagrams (erg origineel) deze diagrammen gemaakt. Dit zal me flink op weg helpen met het schrijven van de code. De diagrammen tonen de opbouw van de ruwe data, de edge cases, wat het eindresultaat moet worden en de flow die ik ga toepassen. De moet omgevormd worden naar een array waarin voor elke inzending een array met tuples komt. De eerste tuple is wat ik noem een OccurenceTuple: een array met daarin een waarde die voorkomt in een andere array op index 0, en de hoeveelheid voorvallen op index 1. De tweede tuple is een PetTuple: een array met daarin een huisdiersoort en de naam van het huisdier op index 0 en 1 respectievelijk. Ik kies voor tuples, omdat alle waardes die ik hier verwerkt bruikbaar moeten zijn voor data-visualisaties. Object keys gebruiken kan, maar ziet er over het algemeen minder netjes uit. De arrays kan je veel netter destructuren als je ze ophaalt:
const pet = ["hond", "boris"];
parsePet(pet);
function parsePet([species, name]) {
document.querySelector("[data-pet]").innerText = `${species}: ${name}`;
}
Ik weet trouwens dat tuple tegenwoordig de betekenis "readonly array with fixed length" en de arrays die ik gebruik niet readonly zijn. Het komt echter wel in de buurt, en aangezien de Tuple primitive type nog in proposal is, moeten we nog even wachten op "echte" tuples.
Van diagram naar realiteit
Het was een onwijze uitdaging om dit te implementeren. De grootste roadblock was het schrijven van de reducer. Ik kwam er namelijk al snel achter dat de manier waarop ik de logica wilde schrijven niet werkt met de manier waarop Ramda, de library die ik gebruik, zijn reducer toepast. Ik krijg van Ramda alleen de accumulator en current value mee; de index en de array worden achterwege gelaten. Het heeft wel geresulteerd in hele schone code, want ik moest erg creatief zijn met mijn implementatie. Ik heb de flow diagram hierop geüpdate met een reducer implementatie die alles op zou moeten vangen. De reducer haalt de huidige waarde door vier stappen, die pas runnen wanneer de waarde verwerkt kan worden in 1 van die stappen:
- Verwerk getal als huidige waarde een getal is
- Verwerk huisdiersoort in meervoud als waarde voorkomt in de hard-coded lijst met huisdiersoorten in meervoud.
- Verwerk huisdiersoort in enkelvoud als waarde voorkomt in de hard-coded lijst met huisdiersoorten in enkelvoud.
- Verwerk huisdier-naam als alledrie de tests hierboven onwaar zijn.
Hierop kon ik een flink stuk voortborduren. De logica had ik vrij snel geschreven, alleen zat er een hele nare bug in waardoor ik per reduce de resultaten van het huidige antwoord + alle navolgende antwoorden op 1 plek kreeg. Ik dacht eerst dat dit te maken had met dat ik alles in arrays stop, dus heb ik besloten om de namen en hoeveelheden in een object te stoppen met keys 'amount' en 'names'. De waardes van deze keys blijven wel arrays van tuples. Ik heb deze aanpassing in de uiteindelijke versie van de diagrammen verwerkt, die je aan het einde van dit document vindt.
Nadat ik hier heel lang mee bezig te was geweest, kwam ik er uiteindelijk achter dat het probleem lag bij het direct overnemen van de accumulator. Op een of andere manier maakte ik geen kopie hiervan, en manipuleerde ik de accumulator direct (en veroorzaakte zo side-effects 😱). Hierdoor ging het allemaal op de schop, en heb dat snel aangepast in deze commit door de Ramda functie clone te gebruiken, die dat heel netjes voor mij regelt. Ik dacht dat het met de Ramda reduce function te maken had, dus had ik deze omgezet naar een Array.reduce function met dezelfde logica. Dit bleek uiteindelijk niet nodig, dus heb ik dat na wat ik hieronder vertel weer teruggezet
De data is verwerkt door de reducer! Ik zag allen nog wat kleine problemen met de uiteindelijke data. Sommige items hadden alleen een aantal, sommige items alleen een soort en sommige items alleen een naam. Deze fouten hebben ik met een filter opgelost, die deze data verder nog opschoont. De manier hoe ik met mensen die huisdiersoorten in meervoud hebben ingevuld vond ik niet netjes, en heb het zo gemaakt dat er geen onderscheid meer is. Er mistten ook nog wat items in validPets en in de petLookUpTable en heb deze toegevoegd (commit 1 en 2).
Een paar edge cases tijdens het verwerken van de dataset die op een aparte manier zijn verwerkt:
- Irrelevante waardes worden weggefilterd uit de data.
- De data-entry van personen die geen huisdieren hebben is een string met de tekst "Heeft geen huisdieren".
- Sommige mensen hebben het ras van hun huisdier opgegeven, dit wordt omgezet naar het soort huisdier.
- Er wordt niks gedaan met huisdiersoorten die in meervoud zijn opgegeven. Die worden letterlijk overgenomen. Getallen hier voor of na worden toegevoegd aan de hoeveelheid.
- Sommige mensen hadden bijvoorbeeld aangegeven welke huisdieren ze bij hun vader en moeder hebben. Deze data wordt samengevoegd naar één lijst met huisdieren.
Overigens heb ik tijdens dit proces talloze functies geschreven en herschreven. Het is echter te veel om op te noemen. Maar wat ik hier vooral heb geleerd is dat je met een goede basislaag aan utilities alles kan omzetten naar je eigen hand. Dit was oprecht een moeilijke uitdaging. Ik heb heel lang lopen peinzen over accumuluator-issue, en weet dus nu: altijd je variabelen klonen, ongeacht of het overbodig is of niet (maar dan met meer mate 🤓).