India’s Time Use Survey 2024: from diary to estimate

Time use
India
Data wrangling
Turning a 10-million-row time diary into person-level estimates with standard errors, and reproducing every table in India’s Time Use Survey 2024 fact sheet.
Author
Published

September 11, 2026

Modified

September 13, 2026

Keywords

time use survey, time diary analysis Python, India TUS 2024, ICATUS 2016, unpaid domestic work, stratified two-stage sample, polars reshaping, complex survey analysis Python

Summary

TipTL;DR

A time use survey hands you a diary: one row per activity episode, 10.2 million of them. Everything published about how people spend their day has to be built first. This case study builds it, then estimates it.

  • 279 of 279 cells at the published precision. Every table and the figure in the TUS 2024 fact sheet: participation rate, minutes per participant and minutes per person, each by sex, by age group and by sector, plus the paid and unpaid tables.
  • Every one of the 450,457 diaries closes at exactly 1,440 minutes. Not approximately. That identity is what proves the reshaping before a single estimate is computed.
  • The fact sheet publishes no standard errors. Those are what svy adds, and they are the reason to do this in a survey library.

Background

The Time Use Survey 2024 is the second all-India time use survey run by the National Statistics Office, after 2019. Every household member aged 6 and above gave a 24-hour diary, and the resulting fact sheet is the source of the numbers many people quote about unpaid work in India: women spend 289 minutes a day on unpaid domestic services, men 88.

The design is a stratified two-stage sample, set out in the survey’s instructions to field staff. First-stage units are villages in rural areas and urban frame survey blocks in urban areas; large villages and blocks are divided into sub-units, which then serve as the first-stage units. From each first-stage unit, 14 households were drawn by simple random sampling without replacement and canvassed two per day across the seven days of the week. The realised sample, from the fact sheet, is 9,969 first-stage units, 139,487 households and 454,192 persons aged 6 and above.

The file you actually get

The published unit of analysis is a person-day. The microdata is nothing of the sort. It is an episode file: one row per activity episode, with a start time, an end time and a three-digit activity code.

Stage Rows
Episode rows in the file 10,211,478
Episode rows kept, aged 6+ with a diary 10,128,216
Slot-activity rows after expansion 24,494,603
Persons 450,457

Reshaping that into a person is not a detail to get past on the way to the estimation. It is most of the work, and it is where the survey’s own conventions live.

From diary to person

Three rules govern the collapse, and all three come from the survey’s documents.

The day starts at four in the morning. The reference period runs from 4:00 AM on the day before the interview to 4:00 AM on the day of it. An episode whose end time is at or before its start time has crossed midnight and needs a day added. Skip this and evening episodes come out with negative duration.

Activities live in 30-minute slots. Respondents were asked about their activities in designated slots of 30 minutes. So an episode is expanded into the slots it covers: a three-hour episode becomes six rows. This is why the table grows before it shrinks, from 10.1 million episodes to 24.5 million slot-activity rows.

A shared slot is split. Where more than one activity was performed in a slot, up to three were recorded, and each one gets an equal share of the 30 minutes. Most slots hold one activity, but not all of them.

Activities recorded in a slot Share of slots
One 88.2%
Two 10.3%
Three 1.5%

That last rule cannot be skipped, and it is the reason the expansion has to go through slots. Simultaneity is a property of the slot, not of the episode. Sum raw episode durations instead and more than a tenth of the day gets counted twice or three times, so the day comes out well over 24 hours.

In code, the whole collapse is one lazy pipeline. Only the five columns the split needs are carried through the expansion, since every extra column is paid for 24 million times over; the person-level identifiers are joined back afterwards, once per person.

(
    episodes
    .select("pid", "start", "end", div=..., pay=...)
    # a wrapped end time has crossed 04:00
    .with_columns(
        end=pl.when(pl.col("end") <= pl.col("start"))
              .then(pl.col("end") + 1440).otherwise(pl.col("end"))
    )
    # one row per 30-minute slot the episode covers
    .with_columns(slot=pl.int_ranges(0, (pl.col("end") - pl.col("start")) // 30))
    .explode("slot")
    .with_columns(slot_start=pl.col("start") + pl.col("slot") * 30)
    # a slot holding k activities gives 30/k minutes to each
    .with_columns(mins=30.0 / pl.len().over(["pid", "slot_start"]))
)

The check that matters

Each of the 450,457 people has exactly 48 slots, and every one of those days sums to exactly 1,440 minutes. Not to within rounding. That single identity tests the wrap handling, the slot expansion and the simultaneity split all at once, and it holds before any weight or design is involved. If a time diary pipeline does not close the day, nothing downstream of it is worth estimating.

The design in three arguments

The 2024 fact sheet carries no estimation appendix and prints no standard errors. The 2019 report, from the first round of the same two-stage design, is explicit about the variance. It notes that the first-stage units were drawn without replacement, then states that the sampling fraction is low enough that treating them as drawn with replacement loses little accuracy, and gives the with-replacement formula summed over stratum and sub-stratum. That is svy’s default. This case study assumes the 2024 round is estimated the same way: every published point estimate reproduces, but there are no 2024 standard errors to confirm the variance against.

The weight is the multiplier over 100. Stratum numbering restarts in each region and each sector, so the stratum key is the four of them together.

sample = svy.Sample(
    data=persons,
    design=svy.Design(stratum="stratum", psu="FSU_Serial_No", wgt="wgt"),
)

Three estimators carry the whole fact sheet

Every published table is one of three things.

Participation rate is the proportion of everyone who did the activity, estimated with prop. Minutes per participant is a mean restricted to the people who did the activity, which is a domain mean and not a ratio of two published numbers. Minutes per person is the mean over everyone, which is why the nine divisions add to 1,440.

A proportion is numerically the mean of a 0/1 indicator, and the two give the same estimate and standard error. prop is still the right call. It says what the quantity is, and its confidence interval respects the bounds of a share: the default symmetric interval around a mean can dip below zero for a rare activity in a thin domain, while the Korn-Graubard interval used here cannot.

Sex and sector are ordinary domains. The age groups are not a partition, since 15–29 sits inside 15–59, so each is estimated as its own domain.

Minutes per person, by sex

Men Women All
svy (SE) published svy (SE) published svy (SE) published
Employment 287.5 (1.00) 287.0 70.6 (0.63) 71.0 179.8 (0.68) 180.0
Own-use production 17.8 (0.34) 18.0 21.5 (0.31) 21.0 19.6 (0.28) 20.0
Unpaid domestic work 23.8 (0.22) 24.0 235.6 (0.65) 236.0 129.0 (0.34) 129.0
Unpaid caregiving 13.5 (0.14) 13.0 46.6 (0.31) 47.0 29.9 (0.20) 30.0
Volunteer and other unpaid 1.2 (0.05) 1.0 1.2 (0.05) 1.0 1.2 (0.04) 1.0
Learning 93.7 (0.67) 94.0 83.7 (0.63) 84.0 88.7 (0.56) 89.0
Socializing and religion 123.9 (0.63) 124.0 126.0 (0.61) 126.0 125.0 (0.58) 125.0
Culture, leisure and sport 169.1 (0.60) 169.0 148.4 (0.59) 148.0 158.8 (0.54) 159.0
Self-care 709.5 (0.58) 710.0 706.4 (0.63) 706.0 708.0 (0.56) 708.0
Average minutes in a day per person, against the fact sheet's Table 7. The nine divisions sum to 1,440 in every column.

Everything else

Fact sheet section Cells at published precision
Tables 1-3: participation rate 81 / 81
Tables 4-6: minutes per participant 81 / 81
Tables 7-9: minutes per person 81 / 81
Figure 1, Tables 10-11: paid and unpaid 33 / 33
The 1440-minute budget 3 / 3

Two things the documents do not tell you

The population is not who you would first guess

454,192 people aged 6 and above were surveyed, but 3,735 of them have no diary at all. Of the 450,457 who do, 5,960 carry a response code of 2 rather than 1. Keep only code 1 and several published cells drift by a tenth of a point: own-use production for women reads 20.85 against the published 20.7. Keep every person with a diary, whatever the response code, and every cell lands.

One sub-stratum has a single surveyed first-stage unit

The allocation was four first-stage units to every sub-stratum, and 2,399 of the 2,460 strata have exactly four. One has a single unit, covering 48 people, or 0.01% of the sample. The official variance formula divides by the number of units times that number minus one, so for this stratum it is undefined. The survey’s appendix covers casualties but says nothing about the case where they leave one unit behind, and the published tables carry no standard errors, so there is no official answer to match.

svy does not pick for you. It stops, names the stratum, and lists the ways out. The choice is the analyst’s, so it is worth seeing what it is worth. Below is the same national estimate under every option.

Unpaid domestic work Employment
estimate standard error estimate standard error
collapse — merge into a neighbouring stratum 129.03565 0.33999 179.75063 0.68039
center — centre on the grand mean 129.03565 0.33999 179.75063 0.68039
certainty — treat as self-representing 129.03565 0.33998 179.75063 0.68038
pool — pool the singletons into one stratum 129.03565 0.33998 179.75063 0.68038
scale — inflate the variance 129.03565 0.34005 179.75063 0.68051
skip — drop from the variance 129.03565 0.33998 179.75063 0.68038
Average minutes in a day per person, national. Only skip moves the estimate, because it drops the stratum's respondents rather than only their variance contribution.

The standard errors differ by less than a thousandth of a minute on a quantity of 129 minutes. With one thin stratum in 2,460, nothing else was possible.

One distinction matters anyway, and it is not numerical. Dropping the stratum from the variance also drops its respondents from the estimate, which is why that row alone moves in the third decimal. Every other option leaves the estimate untouched and changes only how the stratum contributes to the variance.

svy’s own recommendation here is to drop it, on the grounds that well under one percent of rows are affected. This case study collapses instead, on principle rather than on the numbers: losing respondents to fix a variance problem is harmless at this scale and quietly wrong at another.

sample = svy.Sample(persons, design).singleton.collapse()

The value is not which option wins. It is that a library which refuses to guess makes you find a stratum you would never have gone looking for, in a survey whose own documentation does not mention it.

What it costs

The diary is large, and this is the step people give up on. It does not need a large machine.

Step Time Peak memory
First run only: convert the 886 MB Stata file to parquet 82 s 8.1 GB
Reshape the diary to one row per person 2.7 s 4.8 GB
Build the design and handle the thin stratum 0.3 s
One nine-variable table with standard errors 1.3 s
Every estimate on this page 8.5 s

Measured on an Apple M1 Max, 10 cores and 32 GB, running Python 3.14, polars 1.44 and svy 0.28. Another machine will differ, and the first row most of all, since it is dominated by reading one large file.

The Stata file can only be read whole, which is what makes the first run expensive. It is converted once to parquet, 22 MB for the columns used, and every later step scans that from disk rather than holding the file in memory. If memory is tight, the reshaping is the step to watch and not the estimation: the expansion is where the row count triples, while estimation runs on a frame of 450,457 rows and costs tens of milliseconds per cell.

The fact sheet figures are the only transcribed numbers on this page. Everything else is computed at render time by the same modules that run the validation harness, so the write-up cannot drift from the check.

Back to top