# Introduction to DataFrames in Julia¶

By Tyler Ransom, Duke University Social Science Research Institute

[email protected]

https://github.com/tyleransom

Julia's DataFrames package is largely mirrored after the data.frame() package in R. The key underlying principle is that data frames allow for storage of mixed data types (e.g. strings and numbers) in the same object. Data frames also allow for a missing data type, which is NA in Julia. Other statistical software packages such as SAS, Stata, SPSS, and Matlab offer similar features in their data storage.

This tutorial serves to familiarize Julia users with the primary syntax and capaibilities of Julia's DataFrames package. There will be emphasis on making connections with Stata's syntax and features, but anyone with experience in statistical programming will be able to make connections to their preferred language.

First, let's call the packages we'll need for this demonstration. We'll be using Julia version 0.4.1 with DataFrames version 0.6.10 and FreqTables version 0.0.1.

In [ ]:
using DataFrames
using FreqTables


## 1. Reading in data, summarizing data structure, and browsing¶

### Reading in a delimited text file¶

Now let's read in some sample data --- the auto dataset from Stata (in CSV form: https://github.com/jmxpearson/duke-julia-ssri-2016/auto.csv). In Julia, the readtable() function converts delimited text files into data frames.

There are a number of options for configuring the read-in operation, but for now we'll use a simple comma-separated file with standard configurations.

Notice that whatever variable name you choose on the left-hand side of the equals sign will be the name of your data frame moving forward.

In [2]:
auto = readtable("auto.csv");


### Summary of data structure¶

Next, let's look at the variables that are in our data frame. The showcols function accomplishes this task. This is very similar to Stata's describe command.

In [3]:
showcols(auto)

74x12 DataFrames.DataFrame
| Col # | Name         | Eltype     | Missing |
|-------|--------------|------------|---------|
| 1     | make         | UTF8String | 0       |
| 2     | price        | Int64      | 0       |
| 3     | mpg          | Int64      | 0       |
| 4     | rep78        | Int64      | 5       |
| 5     | headroom     | Float64    | 0       |
| 6     | trunk        | Int64      | 0       |
| 7     | weight       | Int64      | 0       |
| 8     | length       | Int64      | 0       |
| 9     | turn         | Int64      | 0       |
| 10    | displacement | Int64      | 0       |
| 11    | gear_ratio   | Float64    | 0       |
| 12    | foreign      | Int64      | 0       |

The output of showcols() shows us that we have 74 observations, 12 variables, the name and format of each of our variables, and the number of missing observations for each.

We can also get the length and width of our data frame using the size() function:

In [112]:
num_obs  = size(auto,1)

Out[112]:
74
In [113]:
num_vars = size(auto,2)

Out[113]:
12

Next, let's look at some of our variables. We do this by either referencing the name with a ":" in front, or with the column number:

In [114]:
auto[:price]

Out[114]:
74-element DataArrays.DataArray{Int64,1}:
4099
4749
3799
4816
7827
5788
4453
5189
10372
4082
11385
14500
15906
⋮
3995
12990
3895
3798
5899
3748
5719
7140
5397
4697
6850
11995
In [115]:
auto[2]

Out[115]:
74-element DataArrays.DataArray{Int64,1}:
4099
4749
3799
4816
7827
5788
4453
5189
10372
4082
11385
14500
15906
⋮
3995
12990
3895
3798
5899
3748
5719
7140
5397
4697
6850
11995
In [116]:
auto[:,[:price,:mpg]]

Out[116]:
pricempg
1409922
2474917
3379922
4481620
5782715
6578818
7445326
8518920
91037216
10408219
111138514
121450014
131590621
14329929
15570516
16450422
17510422
18366724
19395519
20398430
21401018
22588616
23634217
24438928
25418721
261149712
271359412
281346614
29382922
30537914

We can also use the head() and tail() functions to view the first k and last k observations for all variables in our data frame:

In [117]:
head(auto,4)

Out[117]:
1AMC Concord40992232.5112930186401213.580
2AMC Pacer47491733.0113350173402582.530
3AMC Spirit379922NA3.0122640168351213.080
4Buick Century48162034.5163250196401962.930
In [118]:
tail(auto,4)

Out[118]:
1VW Diesel53974153.015204015535903.781
2VW Rabbit46972543.015193015535893.781
3VW Scirocco68502542.016199015636973.781
4Volvo 260119951752.5143170193371632.981

We can also list observations of certain variables indexed by their row number:

In [119]:
auto[[1;2;4;15],[:headroom,:trunk]]

Out[119]:
12.511
23.011
34.516
44.020

We can also list observations that meet some condition. For example, suppose we want to look at the headroom and trunk space for all cars that achieve less than 20 miles per gallon:

In [120]:
auto[(auto[:,:mpg].<20),[:headroom,:trunk]]

Out[120]:
13.011
24.020
34.021
43.517
53.513
64.020
73.516
84.020
93.513
104.017
114.017
124.521
133.522
142.518
153.515
163.516
173.523
183.015
193.016
202.016
214.516
224.020
234.514
243.517
255.016
264.020
271.57
282.016
293.517
303.513

## 2. Summary statistics¶

We can look at summary statistics in a few different ways. First, notice that the showcols() function reported the number of NA or missing values for each variable.

The describe() function also displays missing value frequencies and percentages, in addition to reporting the min/max, mean, median, number of unique observations, and quartiles for each variable in the data frame:

In [4]:
describe(auto)

make
Length  74
Type    UTF8String
NAs     0
NA%     0.0%
Unique  74

price
Min      3291.0
1st Qu.  4220.25
Median   5006.5
Mean     6165.256756756757
3rd Qu.  6332.25
Max      15906.0
NAs      0
NA%      0.0%

mpg
Min      12.0
1st Qu.  18.0
Median   20.0
Mean     21.2972972972973
3rd Qu.  24.75
Max      41.0
NAs      0
NA%      0.0%

rep78
Min      1.0
1st Qu.  3.0
Median   3.0
Mean     3.4057971014492754
3rd Qu.  4.0
Max      5.0
NAs      5
NA%      6.76%

Min      1.5
1st Qu.  2.5
Median   3.0
Mean     2.9932432432432434
3rd Qu.  3.5
Max      5.0
NAs      0
NA%      0.0%

trunk
Min      5.0
1st Qu.  10.25
Median   14.0
Mean     13.756756756756756
3rd Qu.  16.75
Max      23.0
NAs      0
NA%      0.0%

weight
Min      1760.0
1st Qu.  2250.0
Median   3190.0
Mean     3019.4594594594596
3rd Qu.  3600.0
Max      4840.0
NAs      0
NA%      0.0%

length
Min      142.0
1st Qu.  170.0
Median   192.5
Mean     187.93243243243242
3rd Qu.  203.75
Max      233.0
NAs      0
NA%      0.0%

turn
Min      31.0
1st Qu.  36.0
Median   40.0
Mean     39.648648648648646
3rd Qu.  43.0
Max      51.0
NAs      0
NA%      0.0%

displacement
Min      79.0
1st Qu.  119.0
Median   196.0
Mean     197.2972972972973
3rd Qu.  245.25
Max      425.0
NAs      0
NA%      0.0%

gear_ratio
Min      2.19
1st Qu.  2.73
Median   2.955
Mean     3.0148648648648644
3rd Qu.  3.3525
Max      3.89
NAs      0
NA%      0.0%

foreign
Min      0.0
1st Qu.  0.0
Median   0.0
Mean     0.2972972972972973
3rd Qu.  1.0
Max      1.0
NAs      0
NA%      0.0%



We can also display the mean for each variable using the colwise() function. However, this function will return an error if we include string variables.

In [122]:
colwise(mean,auto)

LoadError: MethodError: + has no method matching +(::UTF8String, ::UTF8String)
Closest candidates are:
+(::Any, ::Any, !Matched::Any, !Matched::Any...)

in mapreduce_seq_impl at reduce.jl:228
in mapreduce_pairwise_impl at reduce.jl:108
in _mapreduce at reduce.jl:153
in mapreduce at C:\Users\tmr17\.julia\v0.4\DataArrays\src\reduce.jl:110
in mean at C:\Users\tmr17\.julia\v0.4\DataArrays\src\reduce.jl:137
in colwise at C:\Users\tmr17\.julia\v0.4\DataFrames\src\groupeddataframe\grouping.jl:248
In [171]:
round(colwise(mean,auto[:,2:end]),3)

Out[171]:
11-element Array{Any,1}:
[6165.257]
[21.297]
[NA]
[2.993]
[13.757]
[3019.459]
[187.932]
[39.649]
[197.297]
[3.015]
[0.297]   

Notice that this returns NA for variables with at least one missing observation.

### Tabulations and cross-tabulations¶

We can also compute frequencies of categorical variables, using a couple of different functions:

countmap() returns cell counts as a dictionary:

In [124]:
countmap(auto[:foreign])

Out[124]:
Dict{Union{DataArrays.NAtype,Int64},Int64} with 2 entries:
0 => 52
1 => 22

We can also use the by structure, coupled with the nrow function:

In [125]:
by(auto,:foreign,nrow)

Out[125]:
foreignx1
1052
2122

For cross-tabulations, we require the FreqTables package, which was loaded earlier.

In [126]:
freqtable(auto, :rep78, :foreign, subset=!isna(auto[:rep78]))

Out[126]:
5x2 NamedArrays.NamedArray{Int64,2,Array{Int64,2},Tuple{Dict{Int64,Int64},Dict{Int64,Int64}}}
rep78 \ foreign 0  1
1               2  0
2               8  0
3               27 3
4               9  9
5               2  9


Notice that, at the moment, the Julia's DataFrames is substantially lagging other languages in terms of computing cross-tabulations and contingency tables.

## 3. Dropping, keeping, renaming, and generating¶

### Dropping observations and variables¶

Suppose we want to delete observations in our dataset according to some rule. This amounts to keeping the complement of the rule. For example, if we want to drop all observations of the data frame where a variable is missing, we index the rows we want to keep with !isna() and select all columns:

In [5]:
auto1 = auto[!isna(auto[:,:rep78]), :];

In [6]:
showcols(auto1)

69x12 DataFrames.DataFrame
| Col # | Name         | Eltype     | Missing |
|-------|--------------|------------|---------|
| 1     | make         | UTF8String | 0       |
| 2     | price        | Int64      | 0       |
| 3     | mpg          | Int64      | 0       |
| 4     | rep78        | Int64      | 0       |
| 5     | headroom     | Float64    | 0       |
| 6     | trunk        | Int64      | 0       |
| 7     | weight       | Int64      | 0       |
| 8     | length       | Int64      | 0       |
| 9     | turn         | Int64      | 0       |
| 10    | displacement | Int64      | 0       |
| 11    | gear_ratio   | Float64    | 0       |
| 12    | foreign      | Int64      | 0       |

Notice that we now have 5 fewer observations in the new data frame, and that there are no missing values.

We can drop variables in two different ways:

First, by using the complement of a keep statement:

In [7]:
auto1 = auto1[setdiff(names(auto1), [:price,:mpg])];

In [8]:
showcols(autod1)

69x10 DataFrames.DataFrame
| Col # | Name         | Eltype     | Missing |
|-------|--------------|------------|---------|
| 1     | make         | UTF8String | 0       |
| 2     | rep78        | Int64      | 0       |
| 3     | headroom     | Float64    | 0       |
| 4     | trunk        | Int64      | 0       |
| 5     | weight       | Int64      | 0       |
| 6     | length       | Int64      | 0       |
| 7     | turn         | Int64      | 0       |
| 8     | displacement | Int64      | 0       |
| 9     | gear_ratio   | Float64    | 0       |
| 10    | foreign      | Int64      | 0       |

Second, we can drop in-place using the delete!() function, which overwrites the data frame.

In [139]:
delete!(auto1,[:weight,:length]);

In [140]:
showcols(auto1)

69x8 DataFrames.DataFrame
| Col # | Name         | Eltype     | Missing |
|-------|--------------|------------|---------|
| 1     | make         | UTF8String | 0       |
| 2     | rep78        | Int64      | 0       |
| 3     | headroom     | Float64    | 0       |
| 4     | trunk        | Int64      | 0       |
| 5     | turn         | Int64      | 0       |
| 6     | displacement | Int64      | 0       |
| 7     | gear_ratio   | Float64    | 0       |
| 8     | foreign      | Int64      | 0       |

### Keeping variables¶

We can keep variables simply by indexing the variable names or column numbers of interest:

In [141]:
auto2 = auto[:,[:make,:mpg,:displacement,:gear_ratio]];

In [142]:
showcols(auto2)

74x4 DataFrames.DataFrame
| Col # | Name         | Eltype     | Missing |
|-------|--------------|------------|---------|
| 1     | make         | UTF8String | 0       |
| 2     | mpg          | Int64      | 0       |
| 3     | displacement | Int64      | 0       |
| 4     | gear_ratio   | Float64    | 0       |

### Renaming variables¶

Suppose we want to rename the variables in the "kept" data frame from directly above. This is easily accomplished with the rename!() function:

In [143]:
rename!(auto2,[:make,:displacement],[:make_name,:CCs]);

In [144]:
showcols(auto2)

74x4 DataFrames.DataFrame
| Col # | Name       | Eltype     | Missing |
|-------|------------|------------|---------|
| 1     | make_name  | UTF8String | 0       |
| 2     | mpg        | Int64      | 0       |
| 3     | CCs        | Int64      | 0       |
| 4     | gear_ratio | Float64    | 0       |

### Generating new variables¶

#### Cloning variables¶

Cloning a variable is easily done as follows:

In [146]:
auto2[:mpg_same] = auto2[:mpg];

In [147]:
showcols(auto2)

74x5 DataFrames.DataFrame
| Col # | Name       | Eltype     | Missing |
|-------|------------|------------|---------|
| 1     | make_name  | UTF8String | 0       |
| 2     | mpg        | Int64      | 0       |
| 3     | CCs        | Int64      | 0       |
| 4     | gear_ratio | Float64    | 0       |
| 5     | mpg_same   | Int64      | 0       |

#### Generating new variables using functions of existing variables¶

To generate a new variable using a function of one or more existing variables, the syntax is a bit more involved. For instance, suppose we want to create a new variable called mpgSquared, which is equal to mpg squared:

In [148]:
auto2[:mpgSquared] = map(temp -> temp.^2, auto[:mpg]);


We use map() to accomplish the task, which takes as arguments a function (using arrow notation) and an input (auto[:mpg]). Note that the argument of the function (temp, here) can be any name. I used temp to emphasize that it is a variable of local scope, and thus purley temporary.

Finally, note that there is a "." before the caret symbol, indicating that this is a vectorized operation. Failure to include the "." will result in an error.

We can verify that the function worked as expected:

In [149]:
head(auto2[:,[:mpg,:mpgSquared]])

Out[149]:
mpgmpgSquared
122484
217289
322484
420400
515225
618324

We can use this framework to generate new variables using any mathematical function. For example, a dummy variable that is equal to 1 if :mpg is less than 20 and :gear_ratio is less than 3, and 0 otherwise:

In [157]:
auto2[:dummy_var] = map((tempx,tempy) -> (tempx.<20) & (tempy.<3), auto2[:mpg], auto2[:gear_ratio]);

In [158]:
showcols(auto2)

74x7 DataFrames.DataFrame
| Col # | Name       | Eltype     | Missing |
|-------|------------|------------|---------|
| 1     | make_name  | UTF8String | 0       |
| 2     | mpg        | Int64      | 0       |
| 3     | CCs        | Int64      | 0       |
| 4     | gear_ratio | Float64    | 0       |
| 5     | mpg_same   | Int64      | 0       |
| 6     | mpgSquared | Any        | 0       |
| 7     | dummy_var  | Bool       | 0       |

Note that the type of the new dummy variable is Bool instead of Int64.

## 4. Ordering and Sorting¶

### Ordering columns¶

Suppose we want to change the ordering of the variables of our data frame. This is most easily done as follows:

In [159]:
auto2 = auto2[:,[2;3;4;1;5:end]];

In [160]:
showcols(auto2)

74x7 DataFrames.DataFrame
| Col # | Name       | Eltype     | Missing |
|-------|------------|------------|---------|
| 1     | mpg        | Int64      | 0       |
| 2     | CCs        | Int64      | 0       |
| 3     | gear_ratio | Float64    | 0       |
| 4     | make_name  | UTF8String | 0       |
| 5     | mpg_same   | Int64      | 0       |
| 6     | mpgSquared | Any        | 0       |
| 7     | dummy_var  | Bool       | 0       |

### Sorting observations by various variables¶

We can also sort the observations in our data frame by any number of columns and any number of methods (ascending or descending). Performance of the sort!() function closely mirrors Stata's gsort capabilities.

Below, we will sort ascending by :mpg and descending by :make_name:

In [169]:
sort!(auto2,cols=[:mpg,:make_name],rev=[false,true]);

In [170]:
head(auto2,4)

Out[170]:
mpgCCsgear_ratiomake_namempg_samempgSquareddummy_var
1121632.98Volvo 260121681false
212973.78VW Scirocco121225false
314893.78VW Rabbit141225false
414903.78VW Diesel141156false

## 5. Reshaping and Merging¶

### Reshaping¶

Julia's DataFrames allows for reshaping of longitudinal datasets in a similar fashion as other statistical software programs.

Let's start by hand-creating a "wide" panel dataset with 3 individuals and 3 time periods:

In [173]:
reshape1 = DataFrame(id = 1:3, sex = [0;1;0],
inc1980 = [5000;2000;3000],
inc1981 = [5500;2200;2000],
inc1982 = [6000;4400;1000])

Out[173]:
idsexinc1980inc1981inc1982
110500055006000
221200022004400
330300020001000

We can reshape this data frame to "long" format by using the stack() command provided by the DataFrames package:

In [174]:
longform1A = stack(reshape1, [:inc1980, :inc1981, :inc1982], [:id, :sex])

Out[174]:
variablevalueidsex
1inc1980500010
2inc1980200021
3inc1980300030
4inc1981550010
5inc1981220021
6inc1981200030
7inc1982600010
8inc1982440021
9inc1982100030

Now we have three replications of each :id and :sex (the time-invariant columns), as well as two new columns, labeled :variable and :value.

We can sort the new dataframe so that it is in a more readable format:

In [175]:
sort!(longform1A, cols = [:id, :variable])

Out[175]:
variablevalueidsex
1inc1980500010
2inc1981550010
3inc1982600010
4inc1980200021
5inc1981220021
6inc1982440021
7inc1980300030
8inc1981200030
9inc1982100030

And we can also reshape back to "wide" format using the unstack() function:

In [176]:
wideform1A = unstack(longform1A, :variable, :value)

Out[176]:
idsexinc1980inc1981inc1982
110500055006000
221200022004400
330300020001000

It's worth noting that this method does not work very well when there are multiple time-varying variables per :id. We'll discuss this in detail a bit later.

### Merging¶

DataFrames also has functions that allow the user to merge two data frames together. There are many different types of possible merges, all accessible via the join() function.

The different types of merges depend on if the user wants to keep unmatched observations from either data frame, not on whether or not the identification is duplicated in the merging data frames (i.e. each type of merge can be used for both one-to-one merges and many-to-one merges).

The basic syntax is c = join(a, b, on = [:id1, :id2], kind = symbol), where a and b are data frames each with the identifiers :id1 and id2, and kind is a symbol that can take any of the following 7 values:

Let's show how to do each of these merges using a simple set of data frames.

1. :inner: The output contains rows for values of the key that exist in both the first (left) and second (right) arguments to join (this is the keep(match) option in Stata)
2. :left: The output contains rows for values of the key that exist in the first (left) argument to join, whether or not that value exists in the second (right) argument (this is the keep(master) option in Stata)
3. :right: The output contains rows for values of the key that exist in the second (right) argument to join, whether or not that value exists in the first (left) argument (this is the keep(using) option in Stata)
4. :outer: The output contains rows for values of the key that exist in the first (left) or second (right) argument to join (this is the Stata default)
1. :semi: Like an inner join, but output is restricted to columns from the first (left) argument to join (this is the keep(match master) option in Stata)
2. :anti: The output contains rows for values of the key that exist in the first (left) but not the second (right) argument to join. As with semi joins, output is restricted to columns from the first (left) argument (there is no natural stata equivalent for this)
3. :cross: The output is the cartesian product of rows from the first (left) and second (right) arguments to join (this is equivalent to Stata's append command). Note also that :cross is the only merge type that does not require an identifier in each data frame

Let's show how to do each of these merges using a simple set of data frames.

In [178]:
name = DataFrame(ID = [1, 2, 3, 4, 5, 6], Name = ["John", "Jane", "Mark", "Ann", "Vlad", "Maria"])

Out[178]:
IDName
11John
22Jane
33Mark
44Ann
66Maria
In [179]:
jobs = DataFrame(ID = [1, 2, 3, 4, 5, 6], Job = ["Lawyer", "Doctor", "Mechanic", "Doctor", "Judge", "Pilot"])

Out[179]:
IDJob
11Lawyer
22Doctor
33Mechanic
44Doctor
55Judge
66Pilot
In [186]:
siblings = DataFrame(ID = [1, 1, 2, 3, 5, 5, 5, 6],
Sibling = ["Eric", "Ryan", "Jennifer", "Heather", "Carl", "Dmitri", "Andrei", "Pedro"])

Out[186]:
IDSibling
11Eric
21Ryan
32Jennifer
43Heather
55Carl
65Dmitri
75Andrei
86Pedro

Let's do a simple :inner merge on the first name and jobs data frames:

In [183]:
mergedNameJobs = join(name,jobs, on = :ID, kind = :inner)

Out[183]:
IDNameJob
11JohnLawyer
22JaneDoctor
33MarkMechanic
44AnnDoctor
66MariaPilot

Now let's see what happens when we merge name with siblings, under a variety of join types:

In [184]:
mergedNameSibsInner = join(name,siblings, on = :ID, kind = :inner)

Out[184]:
IDNameSibling
11JohnEric
21JohnRyan
32JaneJennifer
43MarkHeather
86MariaPedro

With the :inner join, those who don't have siblings are removed from the merged data frame.

In [185]:
mergedNameSibsOuter = join(name,siblings, on = :ID, kind = :outer)

Out[185]:
IDNameSibling
11JohnEric
21JohnRyan
32JaneJennifer
43MarkHeather
86MariaPedro
94AnnNA

When we instead do an :outer join, we see that Ann, who doesn't have any siblings, shows as NA under :Sibling.

#### Other less-common merge types:¶

In [187]:
mergedNameSibsLeft = join(name,siblings, on = :ID, kind = :left)

Out[187]:
IDNameSibling
11JohnEric
21JohnRyan
32JaneJennifer
43MarkHeather
86MariaPedro
94AnnNA
In [188]:
mergedNameSibsOuter = join(name,siblings, on = :ID, kind = :right)

Out[188]:
NameIDSibling
1John1Eric
2John1Ryan
3Jane2Jennifer
4Mark3Heather
8Maria6Pedro
In [191]:
mergedNameSibsSemi = join(name,siblings, on = :ID, kind = :semi)

Out[191]:
IDName
11John
22Jane
33Mark
56Maria
In [192]:
mergedNameSibsAnti = join(name,siblings, on = :ID, kind = :anti)

Out[192]:
IDName
14Ann
In [193]:
mergedNameSibsCross = join(name,siblings, kind = :cross)

Out[193]:
IDNameID_1Sibling
11John1Eric
21John1Ryan
31John2Jennifer
41John3Heather
51John5Carl
61John5Dmitri
71John5Andrei
81John6Pedro
92Jane1Eric
102Jane1Ryan
112Jane2Jennifer
122Jane3Heather
132Jane5Carl
142Jane5Dmitri
152Jane5Andrei
162Jane6Pedro
173Mark1Eric
183Mark1Ryan
193Mark2Jennifer
203Mark3Heather
213Mark5Carl
223Mark5Dmitri
233Mark5Andrei
243Mark6Pedro
254Ann1Eric
264Ann1Ryan
274Ann2Jennifer
284Ann3Heather
294Ann5Carl
304Ann5Dmitri

### Reshaping with multiple time-varying variables¶

I mentioned previously that the reshaping method outlined previously does not work very well when there are multiple time-varying variables per :id. With the join() functions in hand, this is possible, though not ideal compared to other software packages.

Let's revisit our previous example, except now with two time-varying variables (inc* and ue*):

In [194]:
reshape2 = DataFrame(id = 1:3, sex = [0;1;0], inc1980 = [5000;2000;3000],
inc1981 = [5500;2200;2000],inc1982 = [6000;4400;1000],
ue1980 = [0;1;0], ue1981 = [1;0;0], ue1982 = [0;0;1])

Out[194]:
idsexinc1980inc1981inc1982ue1980ue1981ue1982
110500055006000010
221200022004400100
330300020001000001

If we try to reshape this using a similar stack() call as before, we get:

In [195]:
longform2 = stack(reshape2, [:inc1980, :inc1981, :inc1982, :ue1980, :ue1981, :ue1982],
[:id, :sex])

Out[195]:
variablevalueidsex
1inc1980500010
2inc1980200021
3inc1980300030
4inc1981550010
5inc1981220021
6inc1981200030
7inc1982600010
8inc1982440021
9inc1982100030
10ue1980010
11ue1980121
12ue1980030
13ue1981110
14ue1981021
15ue1981030
16ue1982010
17ue1982021
18ue1982130

The inc* and ue* values are stacked, so that we have double the number of observations we would like to have.

The remedy for this is to do the reshaping separately for each type of variable, and then merge together.

## Converting data frames to regular Julia arrays¶

Conversion from data frames to regular Julia arrays may be required for use of libraries outside of the DataFrames and GLM world.

To convert, simply type

arrayName = convert(Array,dataFrameName)

But be aware that any NA elements of the data frame will cause an error to be thrown (because Julia's regular arrays do not know the NA type).