Chapter 03 · Section II · 28 min read
Fixing types, dates, and Nepali text encoding
Wrong types, dual calendars, and broken encoding catch every builder working with Nepali data. Three short tools fix all three — parse dates correctly, convert types deliberately, and read files in the right encoding.
Three categories of problem catch almost every Nepali builder in their first year: a column whose values look numeric but are actually strings, a date column that mixes Bikram Sambat and AD, and a CSV that shows ?????? where Devanagari should be. None of them is hard. All of them are confusing the first time. This section teaches all three at once because they tend to arrive on the same file.
Diagnosing types
The first move on any messy dataset is asking pandas what type it thinks each column is:
import pandas as pd
df = pd.read_csv("data/messy_ministry_export.csv")
print(df.dtypes)
Output might look like:
district object
school_id object
students object
date object
dtype: object
Everything is object — pandas’s name for “string or mixed”. That is your first signal something is wrong: a column called students that should be integers is sitting as strings.
A second look:
df["students"].head()
0 245
1 1,250
2 178
3 NA
4 820
Name: students, dtype: object
The comma in 1,250 is what blocked pandas from auto-detecting the column as numeric. And NA is a string, not a missing value.
Converting types deliberately
The pattern to convert a string column to numeric:
df["students"] = (
df["students"]
.str.replace(",", "", regex=False)
.replace("NA", pd.NA)
.astype("Int64")
)
Three steps in one chain:
.str.replace(",", "", regex=False)removes commas. Theregex=Falseis faster and avoids treating the comma as a regex pattern..replace("NA", pd.NA)turns the string"NA"into pandas’s real missing-value marker..astype("Int64")(capital-I) converts to a nullable integer type — unlike plainint, this can hold missing values without converting to float.
After this:
print(df["students"].head())
print(df["students"].sum())
You get real integers, with pd.NA for the missing ones, and arithmetic works.
A more permissive conversion that does not fail on bad values:
df["students"] = pd.to_numeric(df["students"], errors="coerce")
errors="coerce" turns anything pandas cannot parse into NaN. Useful for first-pass conversions when you do not yet know what bad values lurk in the column.
Parsing dates — including BS
Dates are the second universal headache. A typical Nepali ministry CSV might have:
date
2026-06-23
2026/06/24
23-06-2026
2080-03-09
The fourth row is in Bikram Sambat. The others are AD, with three different formats.
The first step is always: ask the data owner which calendar each column uses. If you cannot ask, look for context — surrounding fiscal-year mentions, BS-specific month names (Shrawan, Bhadra), or a sample where the year is clearly above 2070 (almost certainly BS) or in the 1990s-2030s (almost certainly AD, though 2080 is plausible in both).
Once you know the calendar, parsing:
df["date_ad"] = pd.to_datetime(
df["date"],
errors="coerce", # rows that fail to parse become NaT (Not a Time)
format="mixed", # let pandas try multiple formats per row
dayfirst=False, # treat ambiguous DD-MM-YYYY vs MM-DD-YYYY as MM-DD by default
)
pd.to_datetime handles the common AD formats. For mixed dayfirst (Nepali context is often DD-MM-YYYY), set dayfirst=True if you are confident.
For BS dates, you need a converter library. The cleanest in 2026 is nepali_datetime:
pip install nepali-datetime
from nepali_datetime import date as nepali_date
bs = nepali_date(2080, 3, 9)
ad = bs.to_datetime_date()
print(ad) # datetime.date(2023, 6, 23)
To convert a whole column, write a small function and apply it:
from nepali_datetime import date as nepali_date
def bs_string_to_ad(s):
try:
y, m, d = map(int, s.split("-"))
return nepali_date(y, m, d).to_datetime_date()
except Exception:
return None
df["date_ad"] = df["date_bs"].apply(bs_string_to_ad)
The encoding problem, finally
The third universal headache: your CSV shows ?????? or पà¥à¤à¤¤à¥à¤ª where Devanagari should be.
This is an encoding mismatch. The file was written in one character encoding and is being read in another. The fix is to read with the right encoding.
# Common offender 1: Excel for Windows saves CSV as 'cp1252' or 'utf-8-sig'
df = pd.read_csv("data/file.csv", encoding="utf-8-sig")
# Common offender 2: legacy systems use Latin-1
df = pd.read_csv("data/file.csv", encoding="latin-1")
# Modern default and the right answer for 95% of cases
df = pd.read_csv("data/file.csv", encoding="utf-8")
How to figure out which to use:
- Try
encoding="utf-8"first. If it raises aUnicodeDecodeError, the file is not UTF-8. - Try
encoding="utf-8-sig". This is “UTF-8 with a BOM” — what Excel for Windows often produces. Most files that fail UTF-8 succeed here. - Try
encoding="latin-1". This never errors but may produce wrong characters; only use if 1 and 2 fail. - As a last resort, install
chardetand let it guess:
pip install chardet
import chardet
with open("data/file.csv", "rb") as f:
raw = f.read(10000)
guess = chardet.detect(raw)
print(guess) # {'encoding': 'utf-8-sig', 'confidence': 0.99, 'language': ''}
Even when the file is read in the right encoding, you might find that earlier processing already corrupted the text. Once Devanagari has been written as ?????? to a file, the original characters are gone. There is no fix from that point. The only path is to re-export from the source.
Writing files in the right encoding
The reverse problem: when you write a CSV that downstream tools (Excel for Windows, say) will open, write it with the encoding those tools expect.
df.to_csv("data/out.csv", index=False, encoding="utf-8-sig")
utf-8-sig writes a UTF-8 file with a leading byte order mark, which is what makes Devanagari display correctly in Excel for Windows. Plain utf-8 works for most other tools.
A complete cleaning pass — types, dates, encoding
A single script that does all three:
import pandas as pd
from nepali_datetime import date as nepali_date
def bs_string_to_ad(s):
try:
y, m, d = map(int, str(s).split("-"))
return nepali_date(y, m, d).to_datetime_date()
except Exception:
return None
# 1. Read in the right encoding
df = pd.read_csv("data/messy_ministry_export.csv", encoding="utf-8-sig")
# 2. Fix numeric column
df["students"] = (
df["students"]
.astype(str)
.str.replace(",", "", regex=False)
.replace({"NA": pd.NA, "": pd.NA})
.astype("Int64")
)
# 3. Parse BS dates to AD
df["date_ad"] = df["date_bs"].apply(bs_string_to_ad)
df["date_ad"] = pd.to_datetime(df["date_ad"])
# 4. Save in a downstream-friendly encoding
df.to_csv("data/clean/ministry.csv", index=False, encoding="utf-8-sig")
Sixteen lines of cleaning code. They will, with small adaptations, work on most Nepali government and NGO datasets you ever meet.
Check your understanding
Quick check
—You open a CSV in Python with `pd.read_csv('data/file.csv')` and the Devanagari column shows as `पà¥à¤à¤¤à¥à¤ª`. You try `encoding='utf-8'` and get a `UnicodeDecodeError`. What is the most likely fix?
Quick check
—A CSV column called `students` contains values like `'1,250'`, `'NA'`, `'845'`. You want a numeric column you can sum. Which approach is the most robust?
What comes next
You can now read any file, parse any column, and translate any date. The last universal cleaning task is standardising values — turning Khalti, khalti, KHALTI, Khalti , and खल्ती all into the same "Khalti". The shape is similar but the judgment is subtle, especially for Nepali names and places. That is the next section.