mirror of
https://github.com/koodiklinikka/palkkakysely.git
synced 2026-02-15 23:53:18 +00:00
Tweak everything for 2023
This commit is contained in:
@@ -20,10 +20,11 @@ def set_yaxis_cash(plot):
|
||||
|
||||
def get_categorical_stats_plot(df, *, category, value, na_as_category=None, line=True):
|
||||
df = get_categorical_stats(df, category, value, na_as_category=na_as_category)
|
||||
df.reset_index(inplace=True)
|
||||
df = df.reset_index()
|
||||
df[category] = df[category].astype("category")
|
||||
plot = bp.figure(
|
||||
title=f"{category}/{value}", x_range=list(df[category].cat.categories)
|
||||
title=f"{category}/{value}",
|
||||
x_range=list(df[category].cat.categories),
|
||||
)
|
||||
set_yaxis_cash(plot)
|
||||
plot.vbar(
|
||||
@@ -64,7 +65,11 @@ def get_categorical_stats_plot(df, *, category, value, na_as_category=None, line
|
||||
line_width=4,
|
||||
)
|
||||
plot.line(
|
||||
df[category], df["mean"], legend_label="mean", color="#B53471", line_width=4
|
||||
df[category],
|
||||
df["mean"],
|
||||
legend_label="mean",
|
||||
color="#B53471",
|
||||
line_width=4,
|
||||
)
|
||||
else:
|
||||
plot.circle(
|
||||
|
||||
180
pulkka/column_maps.py
Normal file
180
pulkka/column_maps.py
Normal file
@@ -0,0 +1,180 @@
|
||||
from __future__ import annotations
|
||||
|
||||
MISTA_ASIAKKAAT_COL = "Mistä asiakkaat ovat?"
|
||||
IKA_COL = "Ikä"
|
||||
KAUPUNKI_COL = "Kaupunki"
|
||||
KIKY_COL = "Onko palkkasi nykyroolissasi mielestäsi kilpailukykyinen?"
|
||||
KIKY_OTHER_COL = (
|
||||
"Onko palkkasi nykyroolissasi mielestäsi kilpailukykyinen? (muut vastaukset)"
|
||||
)
|
||||
KKPALKKA_COL = "Kuukausipalkka"
|
||||
PALKANSAAJA_VAI_LASKUTTAJA_COL = "Palkansaaja vai laskuttaja"
|
||||
PALVELUT_COL = "Palvelut"
|
||||
ROOLI_COL = "Rooli"
|
||||
SIIRTYNYT_COL = (
|
||||
"Oletko siirtynyt palkansaajasta laskuttajaksi tai päinvastoin 1.10.2022 jälkeen?"
|
||||
)
|
||||
SUKUPUOLI_COL = "Sukupuoli"
|
||||
TYOAIKA_COL = "Työaika"
|
||||
TYOKOKEMUS_COL = "Työkokemus alalta (vuosina)"
|
||||
TYOPAIKKA_COL = "Työpaikka"
|
||||
VUOSITULOT_COL = "Vuositulot"
|
||||
MILLAISESSA_COL = "Millaisessa yrityksessä työskentelet?"
|
||||
LAHITYO_COL = "Kuinka suuren osan ajasta teet lähityönä toimistolla?"
|
||||
LANG_COL = "Vastauskieli"
|
||||
KK_TULOT_COL = "Kk-tulot (laskennallinen)"
|
||||
KK_TULOT_NORM_COL = "Kk-tulot (laskennallinen, normalisoitu)"
|
||||
ROOLI_NORM_COL = "Rooli (normalisoitu)"
|
||||
|
||||
COLUMN_MAP_2023 = {
|
||||
"Timestamp": "Timestamp",
|
||||
"Oletko palkansaaja vai laskuttaja?": PALKANSAAJA_VAI_LASKUTTAJA_COL,
|
||||
"Oletko siirtynyt palkansaajasta laskuttajaksi tai päinvastoin 1.10.2022 jälkeen?": SIIRTYNYT_COL,
|
||||
"Ikä": "Ikä",
|
||||
"Sukupuoli": "Sukupuoli",
|
||||
"Työkokemus alalta (vuosina)": TYOKOKEMUS_COL,
|
||||
"Koulutustaustasi": "Koulutustaustasi",
|
||||
"Tulojen muutos viime vuodesta (%)": "Tulojen muutos viime vuodesta (%)",
|
||||
"Montako vuotta olet tehnyt laskuttavaa työtä alalla?": "Montako vuotta olet tehnyt laskuttavaa työtä alalla?",
|
||||
"Mitä palveluja tarjoat?": PALVELUT_COL,
|
||||
"Tuntilaskutus (ALV 0%, euroina)": "Tuntilaskutus (ALV 0%, euroina)",
|
||||
"Vuosilaskutus (ALV 0%, euroina)": "Vuosilaskutus (ALV 0%, euroina)",
|
||||
"Hankitko asiakkaasi itse suoraan vai käytätkö välitysfirmojen palveluita?": "Hankitko asiakkaasi itse suoraan vai käytätkö välitysfirmojen palveluita?",
|
||||
"Mistä asiakkaat ovat?": MISTA_ASIAKKAAT_COL,
|
||||
"Työpaikka": "Työpaikka",
|
||||
"Missä kaupungissa työpaikkasi pääasiallinen toimisto sijaitsee?": KAUPUNKI_COL,
|
||||
"Millaisessa yrityksessä työskentelet?": MILLAISESSA_COL,
|
||||
"Työaika": TYOAIKA_COL,
|
||||
"Kuinka suuren osan ajasta teet lähityönä toimistolla?": LAHITYO_COL,
|
||||
"Rooli / titteli": ROOLI_COL,
|
||||
"Kuukausipalkka (brutto, euroina)": KKPALKKA_COL,
|
||||
"Vuositulot (sis. bonukset, osingot yms, euroina)": VUOSITULOT_COL,
|
||||
"Vapaa kuvaus kokonaiskompensaatiomallista": "Vapaa kuvaus kokonaiskompensaatiomallista",
|
||||
"Onko palkkasi nykyroolissasi mielestäsi kilpailukykyinen?": KIKY_COL,
|
||||
"Vapaa sana": "Vapaa sana",
|
||||
"Palautetta kyselystä ja ideoita ensi vuoden kyselyyn": "Palautetta kyselystä ja ideoita ensi vuoden kyselyyn",
|
||||
}
|
||||
|
||||
COLUMN_MAP_2023_EN_TO_FI = {
|
||||
"Timestamp": "Timestamp",
|
||||
"Employee or entrepreneur": "Oletko palkansaaja vai laskuttaja?",
|
||||
"Have you switched from employment to entrepreneurship or vice versa after 1.10.2022?": "Oletko siirtynyt palkansaajasta laskuttajaksi tai päinvastoin 1.10.2022 jälkeen?",
|
||||
"Age": "Ikä",
|
||||
"Gender": "Sukupuoli",
|
||||
"Relevant work experience from the industry (in years)": "Työkokemus alalta (vuosina)",
|
||||
"Education": "Koulutustaustasi",
|
||||
"Change in income from last year (in %)": "Tulojen muutos viime vuodesta (%)",
|
||||
"How many years have you worked as an entrepreneur in this industry?": "Montako vuotta olet tehnyt laskuttavaa työtä alalla?",
|
||||
"What services do you offer?": "Mitä palveluja tarjoat?",
|
||||
"Hourly rate (VAT 0%, in euros)": "Tuntilaskutus (ALV 0%, euroina)",
|
||||
"Yearly billing (VAT 0%, in euros)": "Vuosilaskutus (ALV 0%, euroina)",
|
||||
"Do you use agencies or find your clients yourself?": "Hankitko asiakkaasi itse suoraan vai käytätkö välitysfirmojen palveluita?",
|
||||
"Where are your clients from?": "Mistä asiakkaat ovat?",
|
||||
"Company": "Työpaikka",
|
||||
"In which city is your office?": "Missä kaupungissa työpaikkasi pääasiallinen toimisto sijaitsee?",
|
||||
"What kind of a company you work in?": "Millaisessa yrityksessä työskentelet?",
|
||||
"Full time / part time": "Työaika",
|
||||
"How much of your work time you spend in company office? (in %)": "Kuinka suuren osan ajasta teet lähityönä toimistolla?",
|
||||
"Role / title": "Rooli / titteli",
|
||||
"Monthly salary (gross, in EUR)": "Kuukausipalkka (brutto, euroina)",
|
||||
"Yearly income (incl. bonuses, etc; in EUR)": "Vuositulot (sis. bonukset, osingot yms, euroina)",
|
||||
"Free description of your compensation model": "Vapaa kuvaus kokonaiskompensaatiomallista",
|
||||
"Is your salary competitive?": "Onko palkkasi nykyroolissasi mielestäsi kilpailukykyinen?",
|
||||
"What was left unasked that you want to answer to?": "Vapaa sana",
|
||||
"Feedback of the survey": "Palautetta kyselystä ja ideoita ensi vuoden kyselyyn",
|
||||
}
|
||||
|
||||
# ensure all columns have translations
|
||||
assert set(COLUMN_MAP_2023.keys()) == set(COLUMN_MAP_2023_EN_TO_FI.values())
|
||||
|
||||
VALUE_MAP_2023_EN_TO_FI = {
|
||||
PALKANSAAJA_VAI_LASKUTTAJA_COL: {
|
||||
"Employee": "Palkansaaja",
|
||||
"Entrepreneur": "Laskuttaja",
|
||||
},
|
||||
SIIRTYNYT_COL: {
|
||||
"No": "Ei",
|
||||
"En": "Ei",
|
||||
"Kyllä, palkansaajasta laskuttajaksi": "palkansaaja → laskuttaja",
|
||||
"Kyllä, laskuttajasta palkansaajaksi": "laskuttaja → palkansaaja",
|
||||
"Yes, from employee to entrepreneur": "palkansaaja → laskuttaja",
|
||||
"Yes, from entrepreneur to employee": "laskuttaja → palkansaaja",
|
||||
},
|
||||
IKA_COL: {
|
||||
"< 15 yrs": "< 15v",
|
||||
"> 55 yrs": "> 55v",
|
||||
},
|
||||
MISTA_ASIAKKAAT_COL: {
|
||||
"Finland": "Suomesta",
|
||||
},
|
||||
KAUPUNKI_COL: {
|
||||
"PK-Seutu (Helsinki, Espoo, Vantaa)": "PK-seutu",
|
||||
"Capital region (Helsinki, Espoo, Vantaa)": "PK-seutu",
|
||||
},
|
||||
MILLAISESSA_COL: {
|
||||
"Product company with softaware as their core business": "Tuotetalossa, jonka core-bisnes on softa",
|
||||
},
|
||||
}
|
||||
|
||||
BOOLEAN_TEXT_TO_BOOLEAN_MAP = {
|
||||
"Kyllä": True,
|
||||
"Ei": False,
|
||||
"Yes": True,
|
||||
"No": False,
|
||||
}
|
||||
|
||||
COMPANY_MAP = {
|
||||
"Mavericks Software": "Mavericks",
|
||||
"Mavericks: a Witted company": "Mavericks",
|
||||
"Netum Groupj": "Netum",
|
||||
"Siili Solutions": "Siili",
|
||||
"Vincitj": "Vincit",
|
||||
}
|
||||
|
||||
FULL_STACK_ROLE = "*Full-stack Developer"
|
||||
SENIOR_DEVELOPER_ROLE = "*Senior Developer"
|
||||
DEVOPS_CONSULTANT_ROLE = "*Devops Consultant"
|
||||
|
||||
ROLE_MAP = {
|
||||
"DevOps Consult": DEVOPS_CONSULTANT_ROLE,
|
||||
"DevOps Consultant": DEVOPS_CONSULTANT_ROLE,
|
||||
"Devops consultant": DEVOPS_CONSULTANT_ROLE,
|
||||
"Devops konsultti": DEVOPS_CONSULTANT_ROLE,
|
||||
"Full Stack": FULL_STACK_ROLE,
|
||||
"Full stack developer": FULL_STACK_ROLE,
|
||||
"Full stack engineer": FULL_STACK_ROLE,
|
||||
"Full stack web developer": FULL_STACK_ROLE,
|
||||
"Full-stack Developer": FULL_STACK_ROLE,
|
||||
"Full-stack developer": FULL_STACK_ROLE,
|
||||
"Full-stack kehittäjä": FULL_STACK_ROLE,
|
||||
"Full-stack ohjelmistokehittäjä": FULL_STACK_ROLE,
|
||||
"Full-stack software developer": FULL_STACK_ROLE,
|
||||
"Full-stack web developer": FULL_STACK_ROLE,
|
||||
"Full-stack-kehittäjä": FULL_STACK_ROLE,
|
||||
"Fullstack developer": FULL_STACK_ROLE,
|
||||
"Fullstack web developer": FULL_STACK_ROLE,
|
||||
"Fullstack": FULL_STACK_ROLE,
|
||||
"Ohjelmistokehittäjä (full-stack)": FULL_STACK_ROLE,
|
||||
"Ohjelmistokehittäjä, full-stack": FULL_STACK_ROLE,
|
||||
"Senior developer": SENIOR_DEVELOPER_ROLE,
|
||||
"Senior software developer": SENIOR_DEVELOPER_ROLE,
|
||||
"Software engineer, fullstack": FULL_STACK_ROLE,
|
||||
"Full-stack cloud developer": FULL_STACK_ROLE,
|
||||
"Fullstack developer, web apps": FULL_STACK_ROLE,
|
||||
}
|
||||
NO_GENDER_VALUES = {
|
||||
"-",
|
||||
"ei liity asiaan",
|
||||
"epärelevantti",
|
||||
"jänis",
|
||||
"kyllä, kiitos",
|
||||
"leppäkerttu",
|
||||
"taisteluhelikopteri",
|
||||
"tihkutympönen",
|
||||
"yes",
|
||||
}
|
||||
OTHER_GENDER_VALUES = {
|
||||
"muu",
|
||||
"muu/ei",
|
||||
"non-binary, afab",
|
||||
}
|
||||
@@ -1,75 +1,78 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import warnings
|
||||
|
||||
import numpy as np
|
||||
import pandas
|
||||
import pandas as pd
|
||||
|
||||
from pulkka.config import DATA_DIR
|
||||
|
||||
ETA_VAI_LAHI_COL = "Etä- vai lähityö"
|
||||
|
||||
COLUMN_MAP = {
|
||||
# 2021
|
||||
"Missä kaupungissa työpaikkasi pääasiallinen toimisto sijaitsee?": "Kaupunki",
|
||||
"Työaika (jos työsuhteessa)": "Työaika",
|
||||
"Etänä vai paikallisesti?": ETA_VAI_LAHI_COL,
|
||||
"Vuositulot (sis. bonukset, osingot yms) / Vuosilaskutus (jos laskutat)": "Vuositulot",
|
||||
"Kuukausipalkka (jos työntekijä) (brutto)": "Kuukausipalkka",
|
||||
"Onko palkkasi nykyroolissasi mielestäsi kilpailukykyinen?": "Kilpailukykyinen",
|
||||
# 2022
|
||||
"Etänä vai lähityössä?": ETA_VAI_LAHI_COL,
|
||||
"Kuukausipalkka (brutto, euroina)": "Kuukausipalkka",
|
||||
"Vuositulot (sis. bonukset, osingot yms, euroina)": "Vuositulot",
|
||||
"Mitä palveluja tarjoat?": "Palvelut",
|
||||
}
|
||||
|
||||
ETATYO_MAP = {
|
||||
"Pääosin tai kokonaan etätyö": "Etä",
|
||||
"Pääosin tai kokonaan toimistolla": "Toimisto",
|
||||
"Noin 50/50 hybridimalli": "50/50",
|
||||
"Jotain siltä väliltä": "50/50",
|
||||
}
|
||||
|
||||
COMPANY_MAP = {
|
||||
"Siili Solutions": "Siili",
|
||||
"Mavericks Software": "Mavericks",
|
||||
}
|
||||
|
||||
FULL_STACK_ROLE = "Full-stack"
|
||||
|
||||
ROLE_MAP = {
|
||||
"Full-stack developer": FULL_STACK_ROLE,
|
||||
"Full-stack kehittäjä": FULL_STACK_ROLE,
|
||||
"Full-stack ohjelmistokehittäjä": FULL_STACK_ROLE,
|
||||
"Full-stack-kehittäjä": FULL_STACK_ROLE,
|
||||
"Fullstack": FULL_STACK_ROLE,
|
||||
"Ohjelmistokehittäjä (full-stack)": FULL_STACK_ROLE,
|
||||
"Ohjelmistokehittäjä, full-stack": FULL_STACK_ROLE,
|
||||
}
|
||||
from pulkka.config import DATA_DIR, YEAR
|
||||
from pulkka.column_maps import (
|
||||
COLUMN_MAP_2023_EN_TO_FI,
|
||||
KIKY_COL,
|
||||
KKPALKKA_COL,
|
||||
PALVELUT_COL,
|
||||
TYOAIKA_COL,
|
||||
VUOSITULOT_COL,
|
||||
TYOPAIKKA_COL,
|
||||
ROOLI_COL,
|
||||
KIKY_OTHER_COL,
|
||||
BOOLEAN_TEXT_TO_BOOLEAN_MAP,
|
||||
COMPANY_MAP,
|
||||
SUKUPUOLI_COL,
|
||||
ROLE_MAP,
|
||||
COLUMN_MAP_2023,
|
||||
VALUE_MAP_2023_EN_TO_FI,
|
||||
LAHITYO_COL,
|
||||
IKA_COL,
|
||||
LANG_COL,
|
||||
KK_TULOT_COL,
|
||||
KK_TULOT_NORM_COL,
|
||||
NO_GENDER_VALUES,
|
||||
OTHER_GENDER_VALUES,
|
||||
TYOKOKEMUS_COL,
|
||||
ROOLI_NORM_COL,
|
||||
)
|
||||
|
||||
|
||||
def map_sukupuoli(value: str):
|
||||
if isinstance(value, str):
|
||||
value = value.lower()
|
||||
if "nainen" in value or "female" in value:
|
||||
return "nainen"
|
||||
def map_sukupuoli(value: str) -> str | None:
|
||||
if not isinstance(value, str):
|
||||
return value
|
||||
|
||||
if (
|
||||
"mies" in value
|
||||
or "uros" in value
|
||||
or "miäs" in value
|
||||
or "äiä" in value
|
||||
or "male" in value
|
||||
or value == "m"
|
||||
):
|
||||
return "mies"
|
||||
return "muu" # Map the handful of outliers into "muu" (so a given value but not specified)
|
||||
return value
|
||||
value = value.lower()
|
||||
if (
|
||||
"nainen" in value
|
||||
or "female" in value
|
||||
or "woman" in value
|
||||
or value == "f"
|
||||
or value == "women"
|
||||
):
|
||||
return "nainen"
|
||||
|
||||
if (
|
||||
"mies" in value
|
||||
or "uros" in value
|
||||
or "miäs" in value
|
||||
or "äiä" in value
|
||||
or "male" in value
|
||||
or value in ("m", "man", "m i ä s", "ukko")
|
||||
):
|
||||
return "mies"
|
||||
|
||||
if value in NO_GENDER_VALUES:
|
||||
return None
|
||||
|
||||
if value in OTHER_GENDER_VALUES:
|
||||
return "muu"
|
||||
|
||||
raise NotImplementedError(f"Unknown sukupuoli: {value}")
|
||||
|
||||
|
||||
def map_vuositulot(r):
|
||||
if r["Vuositulot"] is np.nan:
|
||||
return r["Kuukausipalkka"] * 12.5
|
||||
return r["Vuositulot"]
|
||||
if r[VUOSITULOT_COL] is np.nan:
|
||||
return r[KKPALKKA_COL] * 12.5
|
||||
return r[VUOSITULOT_COL]
|
||||
|
||||
|
||||
def map_numberlike(d):
|
||||
@@ -81,78 +84,150 @@ def map_numberlike(d):
|
||||
return d
|
||||
|
||||
|
||||
def map_ika(d):
|
||||
if d == "30-35 v": # Early answers had a wrong bracket here
|
||||
d = "31-35 v"
|
||||
return d
|
||||
|
||||
|
||||
def ucfirst(val):
|
||||
if isinstance(val, str):
|
||||
return val[0].upper() + val[1:]
|
||||
return val
|
||||
|
||||
|
||||
def read_data() -> pd.DataFrame:
|
||||
df: pd.DataFrame = pd.read_excel(
|
||||
DATA_DIR / "results.xlsx",
|
||||
def read_initial_dfs() -> pd.DataFrame:
|
||||
df_fi: pd.DataFrame = pd.read_excel(
|
||||
DATA_DIR / "results-fi.xlsx",
|
||||
skiprows=[1], # Google Sheets exports one empty row
|
||||
)
|
||||
df.rename(columns=COLUMN_MAP, inplace=True)
|
||||
|
||||
df["Kaupunki"].replace(
|
||||
"PK-Seutu (Helsinki, Espoo, Vantaa)", "PK-Seutu", inplace=True
|
||||
df_fi[LANG_COL] = "fi"
|
||||
df_en: pd.DataFrame = pd.read_excel(
|
||||
DATA_DIR / "results-en.xlsx",
|
||||
skiprows=[1], # Google Sheets exports one empty row
|
||||
)
|
||||
df["Kaupunki"] = df["Kaupunki"].astype("category")
|
||||
df["Sukupuoli"] = df["Sukupuoli"].apply(map_sukupuoli).astype("category")
|
||||
df["Ikä"] = df["Ikä"].apply(map_ika).astype("category")
|
||||
# Turn työaika into 0% - 100%
|
||||
df["Työaika"] = pd.to_numeric(df["Työaika"], errors="coerce").clip(0, 1)
|
||||
|
||||
df["Etä"] = df[ETA_VAI_LAHI_COL].map(ETATYO_MAP).astype("category")
|
||||
df["Kilpailukykyinen"].replace({"Kyllä": True, "Ei": False}, inplace=True)
|
||||
|
||||
# Try to clean up numbers with spaces, etc. to real numbers
|
||||
df["Kuukausipalkka"] = df["Kuukausipalkka"].apply(map_numberlike)
|
||||
df["Vuositulot"] = df["Vuositulot"].apply(map_numberlike)
|
||||
|
||||
# Fix up Työpaikka
|
||||
df["Työpaikka"].replace("-", np.nan, inplace=True)
|
||||
df["Työpaikka"].replace(re.compile(r"\s+oy|oyj$", flags=re.I), "", inplace=True)
|
||||
df["Työpaikka"] = df["Työpaikka"].map(COMPANY_MAP).fillna(df["Työpaikka"])
|
||||
|
||||
# Normalize initial capitalization in Rooli and Palvelut
|
||||
df["Rooli"] = df["Rooli"].apply(ucfirst)
|
||||
df["Palvelut"] = df["Palvelut"].apply(ucfirst)
|
||||
|
||||
# Map Rooli via known roles
|
||||
df["Rooli"] = df["Rooli"].map(ROLE_MAP).fillna(df["Rooli"])
|
||||
|
||||
# Fill in Vuositulot as 12.5 * Kk-tulot if empty
|
||||
df["Vuositulot"] = df.apply(map_vuositulot, axis=1)
|
||||
|
||||
# Fudge some known outliers
|
||||
df.loc[df.Vuositulot == 912500, "Vuositulot"] = 91250
|
||||
df.loc[df.Kuukausipalkka == 87000, "Kuukausipalkka"] = 7250
|
||||
|
||||
# Synthesize kk-tulot from Vuositulot
|
||||
df["Kk-tulot"] = pd.to_numeric(df["Vuositulot"], errors="coerce") / 12
|
||||
df_en[LANG_COL] = "en"
|
||||
df_en = df_en.rename(columns=COLUMN_MAP_2023_EN_TO_FI)
|
||||
df = pd.concat([df_fi, df_en], ignore_index=True)
|
||||
df = df[df["Timestamp"].notna()] # Remove rows with no timestamp
|
||||
df[LANG_COL] = df[LANG_COL].astype("category")
|
||||
return df
|
||||
|
||||
|
||||
def force_tulot_numeric(df):
|
||||
df["Kuukausipalkka"] = pd.to_numeric(df["Kuukausipalkka"], errors="coerce")
|
||||
df["Vuositulot"] = pd.to_numeric(df["Vuositulot"], errors="coerce")
|
||||
def map_case_insensitive(series: pd.Series, mapping: dict[str, str]) -> pd.Series:
|
||||
"""
|
||||
Map a series of strings to another series of strings, case-insensitively.
|
||||
"""
|
||||
lower_mapping = {k.lower(): v for k, v in mapping.items()}
|
||||
|
||||
def map_value(v):
|
||||
if v is np.nan:
|
||||
return ""
|
||||
assert isinstance(v, str)
|
||||
return lower_mapping.get(v.lower().strip(), v)
|
||||
|
||||
return series.apply(map_value).fillna(series)
|
||||
|
||||
|
||||
def read_data() -> pd.DataFrame:
|
||||
if YEAR != "2023":
|
||||
raise ValueError(
|
||||
"This code only works for 2023. "
|
||||
"Please use an older revision for older data.",
|
||||
)
|
||||
df = read_initial_dfs()
|
||||
|
||||
df = df.rename(columns=COLUMN_MAP_2023)
|
||||
|
||||
for col, val_map in VALUE_MAP_2023_EN_TO_FI.items():
|
||||
df[col] = df[col].map(val_map).fillna(df[col]).astype("category")
|
||||
|
||||
# Drop bogus data
|
||||
df = df.drop(df[df[SUKUPUOLI_COL] == "taisteluhelikopteri"].index)
|
||||
|
||||
df[SUKUPUOLI_COL] = df[SUKUPUOLI_COL].apply(map_sukupuoli).astype("category")
|
||||
df[IKA_COL] = df[IKA_COL].astype("category")
|
||||
|
||||
df[TYOAIKA_COL] = to_percentage(df[TYOAIKA_COL], 100)
|
||||
df[LAHITYO_COL] = to_percentage(df[LAHITYO_COL], 100)
|
||||
|
||||
# Split out non-boolean answers from KIKY_COL to KIKY_OTHER_COL
|
||||
df = split_boolean_column_to_other(df, KIKY_COL, KIKY_OTHER_COL)
|
||||
|
||||
# Try to clean up numbers with spaces, etc. to real numbers
|
||||
df[KKPALKKA_COL] = df[KKPALKKA_COL].apply(map_numberlike)
|
||||
df[VUOSITULOT_COL] = df[VUOSITULOT_COL].apply(map_numberlike)
|
||||
|
||||
# Fix up Työpaikka
|
||||
df[TYOPAIKKA_COL] = df[TYOPAIKKA_COL].replace("-", np.nan)
|
||||
df[TYOPAIKKA_COL] = df[TYOPAIKKA_COL].replace(
|
||||
re.compile(r"\s+oy|oyj$", flags=re.I),
|
||||
"",
|
||||
)
|
||||
df[TYOPAIKKA_COL] = df[TYOPAIKKA_COL].map(COMPANY_MAP).fillna(df[TYOPAIKKA_COL])
|
||||
|
||||
# Normalize initial capitalization in Rooli and Palvelut
|
||||
df[ROOLI_COL] = df[ROOLI_COL].apply(ucfirst)
|
||||
df[PALVELUT_COL] = df[PALVELUT_COL].apply(ucfirst)
|
||||
|
||||
# Map Rooli via known roles
|
||||
df[ROOLI_NORM_COL] = map_case_insensitive(df[ROOLI_COL], ROLE_MAP)
|
||||
|
||||
# Round työvuodet
|
||||
df[TYOKOKEMUS_COL] = df[TYOKOKEMUS_COL].round()
|
||||
|
||||
# Fix known bogus data
|
||||
df.loc[
|
||||
(df[KKPALKKA_COL] == 4900) & (df[VUOSITULOT_COL] == 620000),
|
||||
VUOSITULOT_COL,
|
||||
] = 62000
|
||||
|
||||
# Fill in Vuositulot as 12.5 * Kk-tulot if empty
|
||||
df[VUOSITULOT_COL] = df.apply(map_vuositulot, axis=1)
|
||||
|
||||
# Synthesize kk-tulot from Vuositulot
|
||||
df[KK_TULOT_COL] = pd.to_numeric(df[VUOSITULOT_COL], errors="coerce") / 12
|
||||
df[KK_TULOT_NORM_COL] = df[KK_TULOT_COL] / df[TYOAIKA_COL]
|
||||
|
||||
return df
|
||||
|
||||
|
||||
def to_percentage(ser: pandas.Series, norm_max: float) -> pandas.Series:
|
||||
"""
|
||||
Convert a series of numbers to a percentage
|
||||
"""
|
||||
ser = pd.to_numeric(ser, errors="coerce")
|
||||
if (
|
||||
norm_max * 0.7 > ser.max() > norm_max * 1.5
|
||||
): # check that we have a reasonable max value
|
||||
warnings.warn(f"Unexpected max value {ser.max()} in {ser.name}, {norm_max=}")
|
||||
ser = ser / norm_max
|
||||
return ser.clip(lower=0)
|
||||
|
||||
|
||||
def split_boolean_column_to_other(df, col, other_col):
|
||||
df[col] = df[col].replace(BOOLEAN_TEXT_TO_BOOLEAN_MAP)
|
||||
df[other_col] = df[col].apply(
|
||||
lambda r: r if (r and not isinstance(r, bool)) else None,
|
||||
)
|
||||
df[col] = (
|
||||
df[col]
|
||||
.apply(
|
||||
lambda value: ["Ei", "Kyllä"][value]
|
||||
if isinstance(value, bool)
|
||||
else (np.nan if not value else "Muu"),
|
||||
)
|
||||
.astype("category")
|
||||
)
|
||||
# reorder columns so that other_col is right after col
|
||||
cols = list(df.columns)
|
||||
cols.remove(other_col)
|
||||
cols.insert(cols.index(col) + 1, other_col)
|
||||
df = df[cols]
|
||||
return df
|
||||
|
||||
|
||||
def force_age_numeric(df):
|
||||
age_map = {}
|
||||
for cat in df["Ikä"].cat.categories:
|
||||
for cat in df[IKA_COL].cat.categories:
|
||||
m = re.match("^(\d+)-(\d+) v", cat)
|
||||
if m:
|
||||
age_map[cat] = int(round(float(m.group(1)) + float(m.group(2))) / 2)
|
||||
df["Ikä"] = df["Ikä"].apply(lambda r: age_map.get(r, r))
|
||||
df[IKA_COL] = df[IKA_COL].apply(lambda r: age_map.get(r, r))
|
||||
return df
|
||||
|
||||
|
||||
|
||||
@@ -31,11 +31,15 @@ def get_categorical_stats(
|
||||
df[value_col] = pd.to_numeric(df[value_col], errors="coerce")
|
||||
df = df[df[value_col].notna() & df[value_col] > 0]
|
||||
if na_as_category:
|
||||
df[category_col] = df[category_col].astype("string")
|
||||
df.loc[df[category_col].isna(), category_col] = na_as_category
|
||||
df[category_col] = df[category_col].astype("category")
|
||||
rename_na(df, category_col, na_as_category)
|
||||
# ... then carry on.
|
||||
group = df[[category_col, value_col]].groupby(category_col)
|
||||
return group[value_col].agg(
|
||||
["mean", "min", "max", "median", "count", q25, q50, q75, q90]
|
||||
["mean", "min", "max", "median", "count", q25, q50, q75, q90],
|
||||
)
|
||||
|
||||
|
||||
def rename_na(df: pd.DataFrame, col: str, na_name: str) -> None:
|
||||
df[col] = df[col].astype("string")
|
||||
df.loc[df[col].isna(), col] = na_name
|
||||
df[col] = df[col].astype("category")
|
||||
|
||||
@@ -9,6 +9,13 @@ from pulkka.chart_utils import (
|
||||
set_yaxis_cash,
|
||||
get_categorical_stats_plot,
|
||||
)
|
||||
from pulkka.column_maps import (
|
||||
TYOKOKEMUS_COL,
|
||||
VUOSITULOT_COL,
|
||||
KAUPUNKI_COL,
|
||||
IKA_COL,
|
||||
SUKUPUOLI_COL,
|
||||
)
|
||||
from pulkka.config import OUT_DIR
|
||||
from pulkka.data_ingest import read_data
|
||||
|
||||
@@ -30,27 +37,37 @@ def plot_kokemus_tulot(df: DataFrame):
|
||||
plot.xaxis.axis_label = "Työkokemus (v)"
|
||||
set_yaxis_cash(plot)
|
||||
plot.circle(
|
||||
x="Työkokemus", y="Vuositulot", source=source, color=gender_colormap, size=10
|
||||
x=TYOKOKEMUS_COL,
|
||||
y=VUOSITULOT_COL,
|
||||
source=source,
|
||||
color=gender_colormap,
|
||||
size=10,
|
||||
)
|
||||
return plot
|
||||
|
||||
|
||||
@plot_this
|
||||
def plot_ika_vuositulot(df: DataFrame):
|
||||
return get_categorical_stats_plot(df, category="Ikä", value="Vuositulot")
|
||||
return get_categorical_stats_plot(df, category=IKA_COL, value=VUOSITULOT_COL)
|
||||
|
||||
|
||||
@plot_this
|
||||
def plot_sukupuoli_vuositulot(df: DataFrame):
|
||||
return get_categorical_stats_plot(
|
||||
df, category="Sukupuoli", value="Vuositulot", na_as_category="EOS"
|
||||
df,
|
||||
category=SUKUPUOLI_COL,
|
||||
value=VUOSITULOT_COL,
|
||||
na_as_category="EOS",
|
||||
)
|
||||
|
||||
|
||||
@plot_this
|
||||
def plot_kaupunki_vuositulot(df: DataFrame):
|
||||
plot = get_categorical_stats_plot(
|
||||
df, category="Kaupunki", value="Vuositulot", line=False
|
||||
df,
|
||||
category=KAUPUNKI_COL,
|
||||
value=VUOSITULOT_COL,
|
||||
line=False,
|
||||
)
|
||||
plot.xaxis.major_label_orientation = "vertical"
|
||||
return plot
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import pandas as pd
|
||||
|
||||
from pulkka.column_maps import KKPALKKA_COL, VUOSITULOT_COL
|
||||
from pulkka.config import OUT_DIR
|
||||
from pulkka.data_ingest import read_data, force_tulot_numeric, force_age_numeric
|
||||
from pulkka.data_ingest import read_data, force_age_numeric
|
||||
from ydata_profiling import ProfileReport
|
||||
|
||||
|
||||
def main():
|
||||
df = read_data()
|
||||
df = force_tulot_numeric(df)
|
||||
df[KKPALKKA_COL] = pd.to_numeric(df[KKPALKKA_COL], errors="coerce")
|
||||
df[VUOSITULOT_COL] = pd.to_numeric(df[VUOSITULOT_COL], errors="coerce")
|
||||
df = force_age_numeric(df)
|
||||
profile = ProfileReport(df)
|
||||
profile.config.vars.cat.n_obs = 20
|
||||
|
||||
@@ -21,8 +21,9 @@ def write_massaged_files(env, df):
|
||||
table_html = s.getvalue()
|
||||
f.write(
|
||||
env.get_template("_table.html").render(
|
||||
table_html=table_html, body_class="table-body"
|
||||
)
|
||||
table_html=table_html,
|
||||
body_class="table-body",
|
||||
),
|
||||
)
|
||||
df.to_csv(OUT_DIR / "data.csv", index=False)
|
||||
df.to_excel(OUT_DIR / "data.xlsx", index=False)
|
||||
@@ -70,10 +71,11 @@ def main():
|
||||
"df": df,
|
||||
"year": YEAR,
|
||||
"logo_svg": read_asset_to_data_uri(
|
||||
os.path.join(TEMPLATE_DIR, "logo.svg"), "image/svg+xml"
|
||||
os.path.join(TEMPLATE_DIR, "logo.svg"),
|
||||
"image/svg+xml",
|
||||
),
|
||||
"site_url": f"https://koodiklinikka.github.io/palkkakysely/{YEAR}/",
|
||||
}
|
||||
},
|
||||
)
|
||||
render_statics(env)
|
||||
write_massaged_files(env, df)
|
||||
|
||||
Reference in New Issue
Block a user