HW5 — Exploring PHOIBLE with pandas
LIN 301: Computation for Linguists
Goal: Practice loading a real linguistic dataset (PHOIBLE) into pandas, inspecting structure, filtering, grouping, adding derived columns, and exporting a
.csvfor later use.
Instructions: Fill out this
.qmdfile and submit ashw5_lastname_firstname.qmdto Canvas.
Tip: I’ve compiled a fairly large appendix of pandas methods at the end of this document. Look through it if you’re stuck about how to proceed in the homework.
Step 0 — Setup, Load, and Clean
Load up the .csv using the below code. Note that you can download the .csv file, save it locally, and load it that way, or load it directly from the website.
import pandas as pd
url = "https://github.com/phoible/dev/blob/master/data/phoible.csv?raw=true"
df = pd.read_csv(url, low_memory=False)
df.head()You’re also going to run the below code block that will replace all “+” values with True, and “-” with False. After cleaning, feature columns like tone, nasal, and continuant should be booleans.
import pandas as pd
import numpy as np
import re
# 1) Identify "feature-like" columns (only punctuation +, -, 0, commas, spaces after stripping)
def is_feature_like(series, sample_n=300):
s = series.dropna().astype(str).str.strip()
if len(s) == 0:
return False
if len(s) > sample_n:
s = s.sample(sample_n, random_state=0)
# allow only + - 0 , and whitespace
return s.str.match(r'^[\s,\+\-0]+$').all()
feature_cols = [c for c in df.columns if df[c].dtype == "object" and is_feature_like(df[c])]
# 2) Normalize strings -> booleans: True if ANY '+' present; else False
def plus_any_to_bool(x):
if pd.isna(x):
return False
if isinstance(x, (bool, np.bool_)):
return bool(x)
s = str(x).strip()
if s == "" or s == "0":
return False
# split on commas/spaces; True if any token has '+'
return any('+' in tok for tok in re.split(r'[,\s]+', s) if tok != "")
df[feature_cols] = df[feature_cols].applymap(plus_any_to_bool).astype("boolean")
# Confirm that it works: feature columns should now be True/False (nullable boolean)
df[["tone","nasal","continuant","delayedRelease"]].dtypes
df[["tone","nasal","continuant","delayedRelease"]].head(3)Step 1 — Inspect the DataFrame
In an executable cell, write code that identifies:
- How many rows and columns are in the dataset;
- Column names;
- The type of index the
dfhas created, if any
Step 2 — Basic Exploration
In an executable cell, write code that completes each of the below tasks and shows the relevant output only (i.e, avoid printing the whole df).
- How many distinct languages are represented?
- Which language has the largest inventory? (hint: group by
LanguageNameand count uniquePhoneme, then sort descending.)” - Display only rows where SegmentClass is
"vowel". - How many unique languages with tones are listed in PHOIBLE? (hint: group by
LanguageNameand use.any()on tone to flag languages that have at least one tone segment) - What is the most common manner of articulation (stops, fricatives, etc.)? PHOIBLE doesn’t encode manner using these terms; rather, it encodes manner via boolean features. We’ll use these within a
dictto create more well known natural classes.
# Traditional natural classes using PHOIBLE boolean features
nat_classes = {
"stop": (df["continuant"] == False) & (df["delayedRelease"] == False),
"affricate": (df["continuant"] == False) & (df["delayedRelease"] == True),
"fricative": (df["continuant"] == True) & (df["delayedRelease"] == True),
"nasal": (df["consonantal"] == True) & (df["continuant"] == False) & (df["nasal"] == True),
"liquid": (df["approximant"] == True) & (df["consonantal"] == True),
"glide": (df["approximant"] == True) & (df["syllabic"] == False),
"tap": df["tap"] == True,
"vowel": (df["approximant"] == True) & (df["syllabic"] == True),
}Step 3 — Derived Columns
Build a new df, that contains at least two new columns for the Phoible df. You may select from the below ideas, or come up with your own.
IsVowel— True ifSegmentClassis"vowel".
HasDiacritic— True ifPhonemecontains common diacritics (e.g.,ː ʰ ʷ ʼ ̃).
PhonemeLength— character length of thePhonemestring.
Step 4 — Focus on One Language
Pick one language (e.g., Ancient Greek, English, Yoruba, Quechua). Within an executable cell, write code that:
- Filters the DataFrame to only that language.
- Identifies how many total segments are in the language.
- Identifies how many vowels vs. consonants are in the language.
- Identifies the most frequent places and manners of articulation in your language. You should use the above
nat_classesdictionary, as well as the followingdict:
places = {
"bilabial": (df["labial"] == True) & (df["labiodental"] == False),
"labiodental": (df["labiodental"] == True),
"dental": (df["coronal"] == True) & (df["anterior"] == True) & (df["distributed"] == False),
"alveolar": (df["coronal"] == True) & (df["anterior"] == True) & (df["distributed"] == True),
"postalveolar": (df["coronal"] == True) & (df["anterior"] == False) & (df["distributed"] == True),
"retroflex": (df["coronal"] == True) & (df["anterior"] == False) & (df["distributed"] == False),
"palatal": (df["dorsal"] == True) & (df["high"] == True) & (df["front"] == True),
"velar": (df["dorsal"] == True) & (df["back"] == True) & (df["high"] == False),
"uvular": (df["dorsal"] == True) & (df["back"] == True) & (df["low"] == True),
"pharyngeal": (df["retractedTongueRoot"] == True), # heuristic proxy
"glottal": ((df["spreadGlottis"] == True) | (df["constrictedGlottis"] == True)) &
(df["SegmentClass"].astype(str).str.lower() != "vowel")
}- Exports your filtered DataFrame as
phoible_output.csv.
Step 5 — Short Reflection (3–5 sentences)
- What patterns did you notice in your language’s inventory?
- Were there any surprises in your investigation?
- How might PHOIBLE connect to your project’s goals, if at all?
(Write here in markdown.)
Grading (100 points total)
| Component | Points |
|---|---|
| Step 0 — Setup & Cleaning | 10 |
| Step 1 — Inspection | 10 |
| Step 2 — Exploration | 25 |
| Step 3 — Derived Columns | 10 |
| Step 4 — Language Analysis | 25 |
| Step 5 — Reflection | 10 |
| Code Quality & Presentation | 10 |
| Total | 100 |
Appendix — Quick pd Reference
Use this appendix as a quick guide to common pandas operations.
It’s not exhaustive, but includes everything you’ll need for HW6 and future assignments.
Inspecting Your DataFrame
| Command | Description |
|---|---|
df.head() / df.tail() |
Show the first / last 5 rows. |
df.shape |
Returns (rows, columns). |
df.info() |
Displays column names, types, and non-null counts. |
df.columns |
Lists all column names. |
df.describe() / df.describe(include='all') |
Summary stats for numeric / all columns. |
df.isna().sum() / df.notna().sum() |
Count missing or non-missing values. |
df.dtypes |
Displays the data type of each column. |
Selecting & Filtering Rows
| Command | Description |
|---|---|
df["col"] |
Select one column (returns a Series). |
df[["col1","col2"]] |
Select multiple columns. |
df[df["col"] == "value"] |
Filter rows where the condition is true. |
df[(df["col1"] == "x") & (df["col2"] == "y")] |
Combine filters with AND (&). |
df[(df["col1"] == "x") | (df["col2"] == "y")] |
Combine filters with OR (|). |
df.query("SegmentClass == 'vowel' and LanguageName == 'English'") |
Alternate syntax for filtering with queries. |
df.drop_duplicates(subset=["col"]) |
Remove duplicate rows based on one column. |
Summaries & Aggregations
| Command | Description |
|---|---|
df["col"].value_counts() |
Frequency of unique values. |
df["col"].unique() |
List of all unique values. |
df["col"].nunique() |
Number of unique values. |
df["num_col"].sum() |
Sum of numeric values in a column. |
df["num_col"].mean() / .median() / .min() / .max() / .std() |
Basic statistics for numeric columns. |
df["bool_col"].sum() |
Count of True values (since True=1). |
df.groupby("col").size() |
Count rows per group. |
df.groupby("col")["num_col"].agg(["count","mean","min","max"]) |
Multiple summary stats by group. |
df.idxmax() / df["col"].idxmax() |
Index position of the maximum value. |
String Operations & Derived Columns
| Command | Description |
|---|---|
df["col"].astype(str) |
Convert column to strings. |
df["col"].str.contains("pattern", case=False, na=False) |
True/False if pattern appears in each string. |
df["col"].str.len() |
Length of each string. |
df["col"].str.lower() / .str.upper() / .str.strip() |
Common string transformations. |
df["col"].str.replace("a","b",regex=True) |
Replace text using regex. |
df["new"] = df["old"].apply(func) |
Apply a Python function to each value. |
pd.to_numeric(df["col"], errors="coerce") |
Convert strings to numbers (invalids → NaN). |
Sorting, Indexing, and Exporting
| Command | Description |
|---|---|
df.sort_values(["col1","col2"], ascending=[True, False]) |
Sort by one or more columns. |
df.sort_index() |
Sort by index labels. |
df.reset_index(drop=True) |
Reset index after filtering/sorting. |
df.to_csv("file.csv", index=False) |
Save DataFrame as a CSV file. |
pd.read_csv("file.csv") |
Load a CSV file into a DataFrame. |
Handy Python + pandas Patterns
| Command | Description |
|---|---|
len(df) |
Number of rows. |
len(set(df["col"])) |
Number of unique items (pure Python). |
(df["col"] == "x").sum() |
Count rows where condition is True. |
df["num_col"].between(10, 20) |
Filter rows with values in a range. |